opencode-magi 0.0.0-dev-20260522104950 → 0.0.0-dev-20260522105602

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.
@@ -330,10 +330,13 @@ export async function removeIssueLabels(exec, repository, issue, labels, account
330
330
  return removed;
331
331
  }
332
332
  export async function fetchPullRequestReviews(exec, repository, pr) {
333
- const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviews(first: 100) { nodes { author { login } submittedAt state body commit { oid } } } } } }`;
333
+ const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviews(first: 100) { nodes { author { login } submittedAt state body commit { oid } comments(first: 100) { nodes { body path line startLine } } } } } } }`;
334
334
  const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}`);
335
335
  const data = JSON.parse(raw);
336
- return data.data.repository.pullRequest.reviews.nodes;
336
+ return data.data.repository.pullRequest.reviews.nodes.map((review) => ({
337
+ ...review,
338
+ comments: review.comments?.nodes ?? [],
339
+ }));
337
340
  }
338
341
  export async function fetchPullRequestCommits(exec, repository, pr) {
339
342
  const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { commits(first: 100) { nodes { commit { oid committedDate parents { totalCount } } } } } } }`;
@@ -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,14 +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
  });
61
- next[reviewer] = findings.length
62
- ? { ...output, findings }
63
- : { findings: [], verdict: "MERGE" };
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));
82
+ next[reviewer] =
83
+ newFindings.length || output.followUps.length
84
+ ? { ...output, newFindings }
85
+ : { ...output, newFindings, verdict: "MERGE" };
64
86
  }
65
87
  return { outputs: next, summary: { discarded, kept } };
66
88
  }
@@ -135,6 +135,9 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
135
135
  validateInlineCommentTargets(output.newFindings, targets, "newFindings");
136
136
  return output;
137
137
  }
138
+ export async function inlineCommentTargetsForDiff(input) {
139
+ return parseRightSideDiffTargets(await input.exec(`git diff --no-ext-diff --unified=3 ${shellQuote(input.fromSha)}...${shellQuote(input.toSha)}`, { cwd: input.worktreePath }));
140
+ }
138
141
  function parsePostedFindingLocation(location) {
139
142
  const range = /^(.*):(\d+)-(\d+)$/.exec(location);
140
143
  if (range) {
@@ -182,10 +185,42 @@ function reviewFindingsFromBody(body) {
182
185
  }
183
186
  return { findings };
184
187
  }
188
+ function parsePostedFindingComment(body) {
189
+ const match = /^\*\*Issue:\*\*\s*([\s\S]*?)\s*\r?\n\r?\n\*\*Fix:\*\*\s*([\s\S]+?)\s*$/.exec(body);
190
+ if (!match)
191
+ return undefined;
192
+ return {
193
+ fix: match[2]?.trim() || "Please address this before merging.",
194
+ issue: match[1]?.trim() || "Review finding.",
195
+ };
196
+ }
197
+ function reviewFindingsFromComments(comments) {
198
+ return {
199
+ findings: (comments ?? []).flatMap((comment) => {
200
+ if (comment.line == null)
201
+ return [];
202
+ const parsed = parsePostedFindingComment(comment.body);
203
+ if (!parsed)
204
+ return [];
205
+ return [
206
+ {
207
+ ...parsed,
208
+ line: comment.line,
209
+ path: comment.path,
210
+ startLine: comment.startLine ?? undefined,
211
+ },
212
+ ];
213
+ }),
214
+ };
215
+ }
185
216
  export function reviewOutputFromState(review) {
186
217
  const verdict = reviewStateToVerdict(review.state);
187
- if (verdict === "CHANGES_REQUESTED")
218
+ if (verdict === "CHANGES_REQUESTED") {
219
+ const fromComments = reviewFindingsFromComments(review.comments);
220
+ if (fromComments.findings.length)
221
+ return { ...fromComments, verdict };
188
222
  return { ...reviewFindingsFromBody(review.body), verdict };
223
+ }
189
224
  return verdict === "CLOSE"
190
225
  ? {
191
226
  findings: [],
@@ -230,8 +265,8 @@ function isReviewOutput(output) {
230
265
  return "findings" in output;
231
266
  }
232
267
  async function runFindingValidation(input) {
233
- const reviewOutputs = Object.fromEntries(input.entries.flatMap((entry) => isReviewOutput(entry.value) ? [[entry.key, entry.value]] : []));
234
- const targets = reviewFindingTargets(reviewOutputs);
268
+ const outputs = Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value]));
269
+ const targets = reviewFindingTargets(outputs);
235
270
  if (!targets.length) {
236
271
  return {
237
272
  outputs: Object.fromEntries(input.entries.map((entry) => [entry.key, entry.value])),
@@ -316,7 +351,7 @@ async function runFindingValidation(input) {
316
351
  return [reviewer.key, result.value];
317
352
  }, { signal: input.reviewInput.signal }));
318
353
  const filtered = applyFindingValidation({
319
- outputs: reviewOutputs,
354
+ outputs,
320
355
  reviewerKeys: input.reviewInput.repository.agents.reviewers.map((reviewer) => reviewer.key),
321
356
  validations,
322
357
  });
@@ -324,7 +359,7 @@ async function runFindingValidation(input) {
324
359
  await input.reviewInput.onProgress?.({
325
360
  discarded: filtered.summary.discarded.length,
326
361
  kept: filtered.summary.kept.length,
327
- reviewersChangedToMerge: Object.entries(reviewOutputs)
362
+ reviewersChangedToMerge: Object.entries(outputs)
328
363
  .filter(([reviewer, output]) => {
329
364
  return (output.verdict === "CHANGES_REQUESTED" &&
330
365
  filtered.outputs[reviewer]?.verdict === "MERGE");
@@ -407,7 +442,7 @@ async function runCloseReconsideration(input) {
407
442
  parentSessionId: input.reviewInput.parentSessionId,
408
443
  parse: (text) => {
409
444
  const output = parseCloseReconsiderationOutput(text);
410
- validateInlineCommentTargets(output.findings, input.inlineCommentTargets);
445
+ validateInlineCommentTargets(output.findings, entry.inlineCommentTargets);
411
446
  return output;
412
447
  },
413
448
  permission: reviewer.permission,
@@ -436,6 +471,7 @@ async function runCloseReconsideration(input) {
436
471
  });
437
472
  input.sessionIds[reviewer.key] = result.sessionId;
438
473
  return {
474
+ inlineCommentTargets: entry.inlineCommentTargets,
439
475
  key: entry.key,
440
476
  raw: result.raw,
441
477
  sessionId: result.sessionId,
@@ -602,7 +638,12 @@ export async function runReview(input) {
602
638
  return [];
603
639
  return [{ assignment, reviewer }];
604
640
  });
605
- const inlineCommentTargets = parseRightSideDiffTargets(await exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(meta.headRefOid)}`, { cwd: worktreePath }));
641
+ const initialInlineCommentTargets = await inlineCommentTargetsForDiff({
642
+ exec,
643
+ fromSha: meta.baseRefOid,
644
+ toSha: meta.headRefOid,
645
+ worktreePath,
646
+ });
606
647
  for (const reviewer of input.repository.agents.reviewers) {
607
648
  const assignment = mode.assignments.get(reviewer.account);
608
649
  if (assignment?.type !== "skip")
@@ -622,6 +663,12 @@ export async function runReview(input) {
622
663
  const previous = assignment.review;
623
664
  if (!previous.commit?.oid)
624
665
  throw new Error(`Missing previous review commit for ${reviewer.account}`);
666
+ const inlineCommentTargets = await inlineCommentTargetsForDiff({
667
+ exec,
668
+ fromSha: previous.commit.oid,
669
+ toSha: meta.headRefOid,
670
+ worktreePath,
671
+ });
625
672
  const unresolved = unresolvedThreadsByAccount.get(reviewer.account) ??
626
673
  (await fetchUnresolvedThreads(exec, input.repository, input.pr, reviewer.account));
627
674
  const prompt = await composeRereviewPrompt({
@@ -688,6 +735,7 @@ export async function runReview(input) {
688
735
  verdict: result.value.verdict,
689
736
  });
690
737
  return {
738
+ inlineCommentTargets,
691
739
  key: reviewer.key,
692
740
  raw: result.raw,
693
741
  sessionId: result.sessionId,
@@ -736,7 +784,7 @@ export async function runReview(input) {
736
784
  },
737
785
  options: reviewer.options,
738
786
  parentSessionId: input.parentSessionId,
739
- parse: (text) => parseReviewOutputWithInlineTargets(text, inlineCommentTargets),
787
+ parse: (text) => parseReviewOutputWithInlineTargets(text, initialInlineCommentTargets),
740
788
  permission: reviewer.permission,
741
789
  prompt,
742
790
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -755,6 +803,7 @@ export async function runReview(input) {
755
803
  verdict: result.value.verdict,
756
804
  });
757
805
  return {
806
+ inlineCommentTargets: initialInlineCommentTargets,
758
807
  key: reviewer.key,
759
808
  raw: result.raw,
760
809
  sessionId: result.sessionId,
@@ -788,6 +837,7 @@ export async function runReview(input) {
788
837
  return [
789
838
  {
790
839
  key: reviewer.key,
840
+ inlineCommentTargets: initialInlineCommentTargets,
791
841
  raw: assignment.review.body ?? "",
792
842
  sessionId: "",
793
843
  value: reviewOutputFromState(assignment.review),
@@ -796,7 +846,6 @@ export async function runReview(input) {
796
846
  });
797
847
  entries = await runCloseReconsideration({
798
848
  entries: [...entries, ...skippedCloseEntries],
799
- inlineCommentTargets,
800
849
  meta,
801
850
  outputDir,
802
851
  reviewContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260522104950",
3
+ "version": "0.0.0-dev-20260522105602",
4
4
  "description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
5
5
  "license": "MIT",
6
6
  "author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",