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.
@@ -16,24 +16,17 @@ function pullRequestLine(input) {
16
16
  return `- **Pull Request**: [#${input.pr}](${url})`;
17
17
  }
18
18
  function formatFinding(finding) {
19
- const line = finding.line == null
20
- ? finding.path
21
- : finding.startLine == null
22
- ? `${finding.path}:${finding.line}`
23
- : `${finding.path}:${finding.startLine}-${finding.line}`;
19
+ const line = finding.startLine == null
20
+ ? `${finding.path}:${finding.line}`
21
+ : `${finding.path}:${finding.startLine}-${finding.line}`;
24
22
  return `\`${line}\`: ${finding.issue}`;
25
23
  }
26
24
  function formatRereviewFinding(finding) {
27
- const line = finding.line == null
28
- ? finding.path
29
- : finding.startLine == null
30
- ? `${finding.path}:${finding.line}`
31
- : `${finding.path}:${finding.startLine}-${finding.line}`;
25
+ const line = finding.startLine == null
26
+ ? `${finding.path}:${finding.line}`
27
+ : `${finding.path}:${finding.startLine}-${finding.line}`;
32
28
  return `\`${line}\`: ${finding.body}`;
33
29
  }
34
- function formatRequirementFinding(finding) {
35
- return `Issue #${finding.issueNumber}: ${finding.requirement}`;
36
- }
37
30
  function isReviewOutput(output) {
38
31
  return "findings" in output;
39
32
  }
@@ -85,10 +78,7 @@ function reviewerDetailLines(output) {
85
78
  return output.reason ? [output.reason] : [];
86
79
  if (output.verdict !== "CHANGES_REQUESTED")
87
80
  return [];
88
- return [
89
- ...output.findings.map(formatFinding),
90
- ...output.requirementFindings.map(formatRequirementFinding),
91
- ];
81
+ return output.findings.map(formatFinding);
92
82
  }
93
83
  if (output.verdict === "CLOSE")
94
84
  return output.reason ? [output.reason] : [];
@@ -96,7 +86,6 @@ function reviewerDetailLines(output) {
96
86
  return [];
97
87
  return [
98
88
  ...output.newFindings.map(formatRereviewFinding),
99
- ...output.requirementFindings.map(formatRequirementFinding),
100
89
  ...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
101
90
  ];
102
91
  }
@@ -37,6 +37,23 @@ function quoteEvidence(value) {
37
37
  const compact = value.replaceAll(/\s+/g, " ").trim();
38
38
  return compact.length > 120 ? `${compact.slice(0, 117)}...` : compact;
39
39
  }
40
+ function errorText(error) {
41
+ if (!error || typeof error !== "object")
42
+ return String(error);
43
+ const value = error;
44
+ return [value.message, value.stderr, value.stdout]
45
+ .filter((item) => typeof item === "string")
46
+ .join("\n");
47
+ }
48
+ function isIssueLookupFailure(error) {
49
+ const text = errorText(error);
50
+ return (/could not resolve to an issue/i.test(text) ||
51
+ /could not fetch issue #\d+/i.test(text) ||
52
+ /not an issue/i.test(text));
53
+ }
54
+ function isIssueUrl(url) {
55
+ return /\/issues\/\d+(?:$|[/?#])/i.test(url);
56
+ }
40
57
  function issueReferencePattern(repository) {
41
58
  const host = escapeRegExp(repository.github.host || "github.com");
42
59
  const owner = escapeRegExp(repository.github.owner);
@@ -120,6 +137,9 @@ export function collectIssueRelationships(input) {
120
137
  async function contextIssue(input) {
121
138
  const issue = input.issue ??
122
139
  (await fetchIssue(input.exec, input.repository, input.relationship.number));
140
+ if (!isIssueUrl(issue.url)) {
141
+ throw new Error(`Reference #${issue.number} resolved to ${issue.url}, not an Issue`);
142
+ }
123
143
  const commentPage = await fetchIssueCommentPage(input.exec, input.repository, issue.number, input.limit);
124
144
  return {
125
145
  author: issue.author,
@@ -138,6 +158,19 @@ async function contextIssue(input) {
138
158
  url: issue.url,
139
159
  };
140
160
  }
161
+ async function contextIssueIfIssue(input) {
162
+ try {
163
+ return await contextIssue(input);
164
+ }
165
+ catch (error) {
166
+ if (isIssueLookupFailure(error))
167
+ return undefined;
168
+ throw error;
169
+ }
170
+ }
171
+ function presentIssue(issue) {
172
+ return Boolean(issue);
173
+ }
141
174
  function orderReviewThreads(threads) {
142
175
  return [...threads]
143
176
  .sort((a, b) => {
@@ -183,13 +216,13 @@ export async function buildReviewContextSnapshot(input) {
183
216
  const closingRelationships = relationships.filter((relationship) => relationship.relationship === "closing");
184
217
  const referencedRelationships = relationships.filter((relationship) => relationship.relationship === "referenced");
185
218
  return {
186
- closingIssues: await Promise.all(closingRelationships.map((relationship) => contextIssue({
219
+ closingIssues: (await Promise.all(closingRelationships.map((relationship) => contextIssueIfIssue({
187
220
  exec: input.exec,
188
221
  issue: closingIssueMap.get(relationship.number),
189
222
  limit: LIMITS.closingIssueComments,
190
223
  relationship,
191
224
  repository: input.repository,
192
- }))),
225
+ })))).filter(presentIssue),
193
226
  pullRequest: {
194
227
  author: input.pr.author?.login ?? safetyMeta.author,
195
228
  baseRef: input.pr.baseRefName,
@@ -207,12 +240,12 @@ export async function buildReviewContextSnapshot(input) {
207
240
  title: input.pr.title,
208
241
  url: input.pr.url,
209
242
  },
210
- referencedIssues: await Promise.all(referencedRelationships.map((relationship) => contextIssue({
243
+ referencedIssues: (await Promise.all(referencedRelationships.map((relationship) => contextIssueIfIssue({
211
244
  exec: input.exec,
212
245
  limit: LIMITS.referencedIssueComments,
213
246
  relationship,
214
247
  repository: input.repository,
215
- }))),
248
+ })))).filter(presentIssue),
216
249
  reviewDiscussion: {
217
250
  prComments: boundedComments(prComments, LIMITS.prComments),
218
251
  prCommentsOmitted,
@@ -1,10 +1,10 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, shellQuote, } from "../github/commands";
4
- import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
3
+ import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, ensurePullRequestCommits, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, shellQuote, } from "../github/commands";
4
+ import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
5
5
  import { prRunOutputDir } from "../config/output";
6
- import { worktreeBaseDir } from "../config/worktree";
7
- import { parseCloseReconsiderationOutput, parseFindingValidationOutput, parseRereviewOutput, parseReviewOutput, } from "../prompts/output";
6
+ import { prRunWorktreeDir } from "../config/worktree";
7
+ import { parseCloseReconsiderationOutput, parseFindingValidationOutput, parseRereviewCloseReconsiderationOutput, parseRereviewOutput, parseReviewOutput, } from "../prompts/output";
8
8
  import { throwIfAborted, withAbortSignal } from "./abort";
9
9
  import { waitForChecksWithClassification } from "./ci";
10
10
  import { parseRightSideDiffTargets, validateInlineCommentTargets, } from "./inline-comments";
@@ -39,7 +39,7 @@ async function postReviewOutput(input, reviewerKey, output) {
39
39
  return postApproval(input.exec, input.repository, input.pr, reviewer.account);
40
40
  if (output.verdict === "CLOSE")
41
41
  return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
42
- return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings, output.requirementFindings);
42
+ return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings);
43
43
  }
44
44
  function dryRunReviewPost(key, output) {
45
45
  if (output.verdict === "MERGE")
@@ -112,7 +112,7 @@ function reviewStateToVerdict(state) {
112
112
  return "MERGE";
113
113
  if (state === "CHANGES_REQUESTED")
114
114
  return "CHANGES_REQUESTED";
115
- return "CLOSE";
115
+ throw new Error(`Unsupported GitHub review state: ${state}`);
116
116
  }
117
117
  function hasBlockingCiReports(reports) {
118
118
  return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
@@ -135,6 +135,34 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
135
135
  validateInlineCommentTargets(output.newFindings, targets, "newFindings");
136
136
  return output;
137
137
  }
138
+ export async function inlineCommentTargetsForDiff(input) {
139
+ if (input.ensure) {
140
+ await ensurePullRequestCommits({
141
+ commits: [
142
+ {
143
+ label: "base",
144
+ sha: input.fromSha,
145
+ source: input.ensure.fromSource,
146
+ },
147
+ {
148
+ label: "head",
149
+ sha: input.toSha,
150
+ source: input.ensure.toSource,
151
+ },
152
+ ],
153
+ exec: input.exec,
154
+ meta: input.ensure.meta,
155
+ repository: input.ensure.repository,
156
+ worktreePath: input.worktreePath,
157
+ });
158
+ }
159
+ const diffRange = input.range === "direct"
160
+ ? `${shellQuote(input.fromSha)} ${shellQuote(input.toSha)}`
161
+ : `${shellQuote(input.fromSha)}...${shellQuote(input.toSha)}`;
162
+ return parseRightSideDiffTargets(await input.exec(`git diff --no-ext-diff --unified=3 ${diffRange}`, {
163
+ cwd: input.worktreePath,
164
+ }));
165
+ }
138
166
  function parsePostedFindingLocation(location) {
139
167
  const range = /^(.*):(\d+)-(\d+)$/.exec(location);
140
168
  if (range) {
@@ -147,11 +175,10 @@ function parsePostedFindingLocation(location) {
147
175
  const line = /^(.*):(\d+)$/.exec(location);
148
176
  if (line)
149
177
  return { line: Number(line[2]), path: line[1] ?? location };
150
- return { path: location };
178
+ return undefined;
151
179
  }
152
180
  function reviewFindingsFromBody(body) {
153
181
  const findings = [];
154
- const requirementFindings = [];
155
182
  const lines = (body ?? "").split(/\r?\n/);
156
183
  let section;
157
184
  for (let index = 0; index < lines.length; index += 1) {
@@ -161,7 +188,7 @@ function reviewFindingsFromBody(body) {
161
188
  continue;
162
189
  }
163
190
  if (line === "Requirement findings:") {
164
- section = "requirement";
191
+ section = undefined;
165
192
  continue;
166
193
  }
167
194
  if (section === "finding") {
@@ -169,43 +196,63 @@ function reviewFindingsFromBody(body) {
169
196
  const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
170
197
  if (!match || !fix)
171
198
  continue;
199
+ const location = parsePostedFindingLocation(match[1] ?? "");
200
+ if (!location)
201
+ continue;
172
202
  findings.push({
173
- ...parsePostedFindingLocation(match[1] ?? ""),
203
+ ...location,
174
204
  fix: fix[1] ?? "Please address this before merging.",
175
205
  issue: match[2] ?? "Review finding.",
176
206
  });
177
207
  index += 1;
178
208
  continue;
179
209
  }
180
- if (section !== "requirement")
181
- continue;
182
- const match = /^- Missing issue #(\d+) requirement: (.+)$/.exec(line ?? "");
183
- const evidence = /^\s+Evidence: (.+)$/.exec(lines[index + 1] ?? "");
184
- const fix = /^\s+Fix: (.+)$/.exec(lines[index + 2] ?? "");
185
- if (!match || !evidence || !fix)
186
- continue;
187
- requirementFindings.push({
188
- evidence: evidence[1] ?? "See review body.",
189
- fix: fix[1] ?? "Please address this before merging.",
190
- issueNumber: Number(match[1]),
191
- requirement: match[2] ?? "Review requirement.",
192
- });
193
- index += 2;
194
210
  }
195
- return { findings, requirementFindings };
211
+ return { findings };
212
+ }
213
+ function parsePostedFindingComment(body) {
214
+ const match = /^\*\*Issue:\*\*\s*([\s\S]*?)\s*\r?\n\r?\n\*\*Fix:\*\*\s*([\s\S]+?)\s*$/.exec(body);
215
+ if (!match)
216
+ return undefined;
217
+ return {
218
+ fix: match[2]?.trim() || "Please address this before merging.",
219
+ issue: match[1]?.trim() || "Review finding.",
220
+ };
221
+ }
222
+ function reviewFindingsFromComments(comments) {
223
+ return {
224
+ findings: (comments ?? []).flatMap((comment) => {
225
+ if (comment.line == null)
226
+ return [];
227
+ const parsed = parsePostedFindingComment(comment.body);
228
+ if (!parsed)
229
+ return [];
230
+ return [
231
+ {
232
+ ...parsed,
233
+ line: comment.line,
234
+ path: comment.path,
235
+ startLine: comment.startLine ?? undefined,
236
+ },
237
+ ];
238
+ }),
239
+ };
196
240
  }
197
241
  export function reviewOutputFromState(review) {
198
242
  const verdict = reviewStateToVerdict(review.state);
199
- if (verdict === "CHANGES_REQUESTED")
243
+ if (verdict === "CHANGES_REQUESTED") {
244
+ const fromComments = reviewFindingsFromComments(review.comments);
245
+ if (fromComments.findings.length)
246
+ return { ...fromComments, verdict };
200
247
  return { ...reviewFindingsFromBody(review.body), verdict };
248
+ }
201
249
  return verdict === "CLOSE"
202
250
  ? {
203
251
  findings: [],
204
252
  reason: review.body || "Close requested.",
205
- requirementFindings: [],
206
253
  verdict,
207
254
  }
208
- : { findings: [], requirementFindings: [], verdict };
255
+ : { findings: [], verdict };
209
256
  }
210
257
  export function hasPendingThreadReply(threads, reviewerAccount) {
211
258
  return threads.some((thread) => {
@@ -229,22 +276,22 @@ async function postRereviewOutput(input, reviewerKey, output) {
229
276
  return postApproval(input.exec, input.repository, input.pr, reviewer.account);
230
277
  if (output.verdict === "CLOSE")
231
278
  return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
232
- if (!output.newFindings.length && !output.requirementFindings.length)
279
+ if (!output.newFindings.length)
233
280
  return "";
234
281
  return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
235
282
  fix: "Please address this before merging.",
236
283
  issue: finding.body,
237
284
  path: finding.path,
238
- ...(finding.line == null ? {} : { line: finding.line }),
285
+ line: finding.line,
239
286
  startLine: finding.startLine,
240
- })), output.requirementFindings);
287
+ })));
241
288
  }
242
289
  function isReviewOutput(output) {
243
290
  return "findings" in output;
244
291
  }
245
292
  async function runFindingValidation(input) {
246
- const reviewOutputs = Object.fromEntries(input.entries.flatMap((entry) => isReviewOutput(entry.value) ? [[entry.key, entry.value]] : []));
247
- const targets = reviewFindingTargets(reviewOutputs);
293
+ const outputs = Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value]));
294
+ const targets = reviewFindingTargets(outputs);
248
295
  if (!targets.length) {
249
296
  return {
250
297
  outputs: Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value])),
@@ -303,6 +350,7 @@ async function runFindingValidation(input) {
303
350
  }
304
351
  },
305
352
  options: reviewer.options,
353
+ parentSessionId: input.reviewInput.parentSessionId,
306
354
  parse: (text) => {
307
355
  const output = parseFindingValidationOutput(text);
308
356
  validateFindingVotes({
@@ -328,7 +376,7 @@ async function runFindingValidation(input) {
328
376
  return [reviewer.key, result.value];
329
377
  }, { signal: input.reviewInput.signal }));
330
378
  const filtered = applyFindingValidation({
331
- outputs: reviewOutputs,
379
+ outputs,
332
380
  reviewerKeys: input.reviewInput.repository.agents.reviewers.map((reviewer) => reviewer.key),
333
381
  validations,
334
382
  });
@@ -336,7 +384,7 @@ async function runFindingValidation(input) {
336
384
  await input.reviewInput.onProgress?.({
337
385
  discarded: filtered.summary.discarded.length,
338
386
  kept: filtered.summary.kept.length,
339
- reviewersChangedToMerge: Object.entries(reviewOutputs)
387
+ reviewersChangedToMerge: Object.entries(outputs)
340
388
  .filter(([reviewer, output]) => {
341
389
  return (output.verdict === "CHANGES_REQUESTED" &&
342
390
  filtered.outputs[reviewer]?.verdict === "MERGE");
@@ -365,27 +413,49 @@ async function runCloseReconsideration(input) {
365
413
  type: "phase",
366
414
  });
367
415
  return Promise.all(input.entries.map(async (entry) => {
368
- if (!targets.includes(entry.key) || !isReviewOutput(entry.value)) {
416
+ if (!targets.includes(entry.key)) {
369
417
  return entry;
370
418
  }
371
419
  const reviewer = input.reviewInput.repository.agents.reviewers.find((item) => item.key === entry.key);
372
420
  if (!reviewer)
373
421
  return entry;
374
422
  const hasReviewerSession = Boolean(input.sessionIds[reviewer.key]);
375
- const prompt = await composeCloseReconsiderationPrompt({
376
- baseSha: input.meta.baseRefOid,
377
- ciFailureContext: undefined,
378
- closeReason: entry.value.reason,
379
- directory: input.reviewInput.directory,
380
- headSha: input.meta.headRefOid,
381
- includeReviewGuidelines: !hasReviewerSession,
382
- includeSessionContext: !hasReviewerSession,
383
- pr: input.reviewInput.pr,
384
- repository: input.reviewInput.repository,
385
- reviewContext: input.reviewContext,
386
- reviewer,
387
- worktreePath: input.worktreePath,
388
- });
423
+ const isReviewEntry = isReviewOutput(entry.value);
424
+ let prompt;
425
+ if (isReviewEntry) {
426
+ prompt = await composeCloseReconsiderationPrompt({
427
+ baseSha: input.meta.baseRefOid,
428
+ ciFailureContext: undefined,
429
+ closeReason: entry.value.reason,
430
+ directory: input.reviewInput.directory,
431
+ headSha: input.meta.headRefOid,
432
+ includeReviewGuidelines: !hasReviewerSession,
433
+ includeSessionContext: !hasReviewerSession,
434
+ pr: input.reviewInput.pr,
435
+ repository: input.reviewInput.repository,
436
+ reviewContext: input.reviewContext,
437
+ reviewer,
438
+ worktreePath: input.worktreePath,
439
+ });
440
+ }
441
+ else {
442
+ if (!entry.previousHeadSha) {
443
+ throw new Error(`Missing previous review commit for ${reviewer.account}`);
444
+ }
445
+ prompt = await composeRereviewCloseReconsiderationPrompt({
446
+ baseSha: input.meta.baseRefOid,
447
+ closeReason: entry.value.reason,
448
+ directory: input.reviewInput.directory,
449
+ headSha: input.meta.headRefOid,
450
+ includeReviewGuidelines: !hasReviewerSession,
451
+ includeSessionContext: !hasReviewerSession,
452
+ pr: input.reviewInput.pr,
453
+ previousHeadSha: entry.previousHeadSha,
454
+ repository: input.reviewInput.repository,
455
+ reviewer,
456
+ worktreePath: input.worktreePath,
457
+ });
458
+ }
389
459
  const result = await withReviewerFailureProgress({
390
460
  onProgress: input.reviewInput.onProgress,
391
461
  reviewer: reviewer.key,
@@ -416,15 +486,21 @@ async function runCloseReconsideration(input) {
416
486
  }
417
487
  },
418
488
  options: reviewer.options,
489
+ parentSessionId: input.reviewInput.parentSessionId,
419
490
  parse: (text) => {
420
- const output = parseCloseReconsiderationOutput(text);
421
- validateInlineCommentTargets(output.findings, input.inlineCommentTargets);
491
+ const output = isReviewEntry
492
+ ? parseCloseReconsiderationOutput(text)
493
+ : parseRereviewCloseReconsiderationOutput(text);
494
+ const findings = "newFindings" in output ? output.newFindings : output.findings;
495
+ validateInlineCommentTargets(findings, entry.inlineCommentTargets, "newFindings" in output ? "newFindings" : "findings");
422
496
  return output;
423
497
  },
424
498
  permission: reviewer.permission,
425
499
  prompt,
426
500
  repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
427
- schemaName: "close reconsideration",
501
+ schemaName: isReviewEntry
502
+ ? "close reconsideration"
503
+ : "rereview close reconsideration",
428
504
  sessionId: input.sessionIds[reviewer.key],
429
505
  signal: input.reviewInput.signal,
430
506
  title: `magi reconsider close ${input.reviewInput.repository.alias}#${input.reviewInput.pr} ${reviewer.key}`,
@@ -447,7 +523,9 @@ async function runCloseReconsideration(input) {
447
523
  });
448
524
  input.sessionIds[reviewer.key] = result.sessionId;
449
525
  return {
526
+ inlineCommentTargets: entry.inlineCommentTargets,
450
527
  key: entry.key,
528
+ previousHeadSha: entry.previousHeadSha,
451
529
  raw: result.raw,
452
530
  sessionId: result.sessionId,
453
531
  value: result.value,
@@ -527,11 +605,13 @@ export async function runReview(input) {
527
605
  : preliminaryMode;
528
606
  if (mode.type === "already_reviewed" && !input.allowAlreadyReviewed)
529
607
  throw new Error("PR has already been reviewed by all configured accounts");
530
- const outputDir = join(prRunOutputDir({
608
+ const runId = input.runId ?? `run-${Date.now().toString(36)}`;
609
+ const outputDir = prRunOutputDir({
531
610
  config: input.config,
532
611
  directory: input.directory,
533
612
  pr: input.pr,
534
- }), ...(input.runId ? [input.runId] : []));
613
+ runId,
614
+ });
535
615
  await mkdir(outputDir, { recursive: true });
536
616
  await input.onProgress?.({ phase: "fetching review context", type: "phase" });
537
617
  const reviewContextSnapshot = await buildReviewContextSnapshot({
@@ -580,6 +660,7 @@ export async function runReview(input) {
580
660
  },
581
661
  onProgress: (phase) => input.onProgress?.({ phase, type: "phase" }),
582
662
  outputDir,
663
+ parentSessionId: input.parentSessionId,
583
664
  pr: input.pr,
584
665
  repairAttempts: input.config.output?.repairAttempts ?? 3,
585
666
  repository: input.repository,
@@ -595,10 +676,14 @@ export async function runReview(input) {
595
676
  checkResult.report.scopeInside.length)) {
596
677
  await input.onProgress?.({ report: checkResult.report, type: "ci_report" });
597
678
  }
598
- const worktreeRoot = worktreeBaseDir(input.directory, input.config, "pr");
679
+ const worktreePath = prRunWorktreeDir({
680
+ config: input.config,
681
+ directory: input.directory,
682
+ pr: input.pr,
683
+ runId,
684
+ });
599
685
  await input.onProgress?.({ phase: "creating worktree", type: "phase" });
600
- const worktree = await createWorktree(exec, input.repository, input.pr, worktreeRoot);
601
- const worktreePath = worktree.path;
686
+ const worktree = await createWorktree(exec, input.repository, input.pr, worktreePath);
602
687
  await input.onProgress?.({
603
688
  branch: worktree.branch,
604
689
  type: "worktree_created",
@@ -612,7 +697,18 @@ export async function runReview(input) {
612
697
  return [];
613
698
  return [{ assignment, reviewer }];
614
699
  });
615
- const inlineCommentTargets = parseRightSideDiffTargets(await exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(meta.headRefOid)}`, { cwd: worktreePath }));
700
+ const initialInlineCommentTargets = await inlineCommentTargetsForDiff({
701
+ ensure: {
702
+ fromSource: "base",
703
+ meta,
704
+ repository: input.repository,
705
+ toSource: "head",
706
+ },
707
+ exec,
708
+ fromSha: meta.baseRefOid,
709
+ toSha: meta.headRefOid,
710
+ worktreePath,
711
+ });
616
712
  for (const reviewer of input.repository.agents.reviewers) {
617
713
  const assignment = mode.assignments.get(reviewer.account);
618
714
  if (assignment?.type !== "skip")
@@ -632,6 +728,18 @@ export async function runReview(input) {
632
728
  const previous = assignment.review;
633
729
  if (!previous.commit?.oid)
634
730
  throw new Error(`Missing previous review commit for ${reviewer.account}`);
731
+ const inlineCommentTargets = await inlineCommentTargetsForDiff({
732
+ ensure: {
733
+ fromSource: "head",
734
+ meta,
735
+ repository: input.repository,
736
+ toSource: "head",
737
+ },
738
+ exec,
739
+ fromSha: previous.commit.oid,
740
+ toSha: meta.headRefOid,
741
+ worktreePath,
742
+ });
635
743
  const unresolved = unresolvedThreadsByAccount.get(reviewer.account) ??
636
744
  (await fetchUnresolvedThreads(exec, input.repository, input.pr, reviewer.account));
637
745
  const prompt = await composeRereviewPrompt({
@@ -678,6 +786,7 @@ export async function runReview(input) {
678
786
  }
679
787
  },
680
788
  options: reviewer.options,
789
+ parentSessionId: input.parentSessionId,
681
790
  parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
682
791
  permission: reviewer.permission,
683
792
  prompt,
@@ -697,7 +806,9 @@ export async function runReview(input) {
697
806
  verdict: result.value.verdict,
698
807
  });
699
808
  return {
809
+ inlineCommentTargets,
700
810
  key: reviewer.key,
811
+ previousHeadSha: previous.commit.oid,
701
812
  raw: result.raw,
702
813
  sessionId: result.sessionId,
703
814
  value: result.value,
@@ -744,7 +855,8 @@ export async function runReview(input) {
744
855
  }
745
856
  },
746
857
  options: reviewer.options,
747
- parse: (text) => parseReviewOutputWithInlineTargets(text, inlineCommentTargets),
858
+ parentSessionId: input.parentSessionId,
859
+ parse: (text) => parseReviewOutputWithInlineTargets(text, initialInlineCommentTargets),
748
860
  permission: reviewer.permission,
749
861
  prompt,
750
862
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -763,6 +875,7 @@ export async function runReview(input) {
763
875
  verdict: result.value.verdict,
764
876
  });
765
877
  return {
878
+ inlineCommentTargets: initialInlineCommentTargets,
766
879
  key: reviewer.key,
767
880
  raw: result.raw,
768
881
  sessionId: result.sessionId,
@@ -796,6 +909,7 @@ export async function runReview(input) {
796
909
  return [
797
910
  {
798
911
  key: reviewer.key,
912
+ inlineCommentTargets: initialInlineCommentTargets,
799
913
  raw: assignment.review.body ?? "",
800
914
  sessionId: "",
801
915
  value: reviewOutputFromState(assignment.review),
@@ -804,7 +918,6 @@ export async function runReview(input) {
804
918
  });
805
919
  entries = await runCloseReconsideration({
806
920
  entries: [...entries, ...skippedCloseEntries],
807
- inlineCommentTargets,
808
921
  meta,
809
922
  outputDir,
810
923
  reviewContext,