opencode-magi 0.0.0-dev-20260520163847 → 0.0.0-dev-20260520165753

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
@@ -118,6 +118,11 @@ function parseOptionalPr(value) {
118
118
  return undefined;
119
119
  return parsePrToken(value);
120
120
  }
121
+ function parseOptionalPrs(value) {
122
+ if (!value?.trim())
123
+ return undefined;
124
+ return parsePrs(value);
125
+ }
121
126
  function parseOptionalIssue(value) {
122
127
  if (!value?.trim())
123
128
  return undefined;
@@ -432,7 +437,7 @@ export const MagiPlugin = async ({ client, directory }) => {
432
437
  block: args.block,
433
438
  issue: parseOptionalIssue(args.issue),
434
439
  outputDir: await configuredOutputDir(),
435
- pr: parseOptionalPr(args.pr),
440
+ pr: parseOptionalPrs(args.pr),
436
441
  runId: args.runId,
437
442
  timeoutMs: args.timeoutSeconds == null
438
443
  ? undefined
@@ -0,0 +1,73 @@
1
+ function parseDiffPath(value) {
2
+ if (value === "/dev/null")
3
+ return undefined;
4
+ let path = value;
5
+ if (path.startsWith('"') && path.endsWith('"')) {
6
+ try {
7
+ path = JSON.parse(path);
8
+ }
9
+ catch {
10
+ path = path.slice(1, -1);
11
+ }
12
+ }
13
+ return path.startsWith("b/") ? path.slice(2) : path;
14
+ }
15
+ function addTargetLine(targets, path, line) {
16
+ const lines = targets.get(path) ?? new Set();
17
+ lines.add(line);
18
+ targets.set(path, lines);
19
+ }
20
+ export function parseRightSideDiffTargets(diff) {
21
+ const targets = new Map();
22
+ let currentPath;
23
+ let rightLine;
24
+ for (const line of diff.split("\n")) {
25
+ if (line.startsWith("+++ ")) {
26
+ currentPath = parseDiffPath(line.slice(4));
27
+ rightLine = undefined;
28
+ continue;
29
+ }
30
+ if (line.startsWith("@@ ")) {
31
+ const match = line.match(/\+(\d+)(?:,\d+)?/);
32
+ rightLine = match ? Number(match[1]) : undefined;
33
+ continue;
34
+ }
35
+ if (!currentPath || rightLine == null)
36
+ continue;
37
+ if (line.startsWith("+") || line.startsWith(" ")) {
38
+ addTargetLine(targets, currentPath, rightLine);
39
+ rightLine += 1;
40
+ continue;
41
+ }
42
+ if (line.startsWith("-"))
43
+ continue;
44
+ }
45
+ return targets;
46
+ }
47
+ function assertPositiveInteger(value, name) {
48
+ if (!Number.isInteger(value) || value < 1) {
49
+ throw new Error(`${name} must be a positive integer`);
50
+ }
51
+ }
52
+ export function validateInlineCommentTargets(findings, targets, label = "findings") {
53
+ for (const [index, finding] of findings.entries()) {
54
+ const name = `${label}[${index}]`;
55
+ assertPositiveInteger(finding.line, `${name}.line`);
56
+ if (finding.startLine != null) {
57
+ assertPositiveInteger(finding.startLine, `${name}.startLine`);
58
+ if (finding.startLine > finding.line) {
59
+ throw new Error(`${name}.startLine must be before or equal to line`);
60
+ }
61
+ }
62
+ const lines = targets.get(finding.path);
63
+ if (!lines) {
64
+ throw new Error(`${name} targets ${finding.path}:${finding.line}, but path is not in the PR diff`);
65
+ }
66
+ const startLine = finding.startLine ?? finding.line;
67
+ for (let line = startLine; line <= finding.line; line += 1) {
68
+ if (!lines.has(line)) {
69
+ throw new Error(`${name} targets ${finding.path}:${line}, but line is not in a right-side PR diff hunk`);
70
+ }
71
+ }
72
+ }
73
+ }
@@ -1,11 +1,12 @@
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, waitForMergeQueue, } from "../github/commands";
4
+ import { closePullRequest, configureGitIdentity, fetchMergeQueueRequirement, fetchPullRequest, fetchUnresolvedThreads, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, pushHead, removeWorktree, resolveThread, shellQuote, 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
10
  import { closeMinorityReviewers, mergeVerdictForPolicy } from "./majority";
10
11
  import { runModelWithRepair } from "./model";
11
12
  import { mapPool } from "./pool";
@@ -155,10 +156,16 @@ async function postRereviewOutput(input, reviewerKey, output) {
155
156
  }
156
157
  return replies[0] ?? "";
157
158
  }
159
+ function parseRereviewOutputWithInlineTargets(text, targets) {
160
+ const output = parseRereviewOutput(text);
161
+ validateInlineCommentTargets(output.newFindings, targets, "newFindings");
162
+ return output;
163
+ }
158
164
  async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
159
165
  throwIfAborted(input.signal);
160
166
  const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
161
167
  const headSha = options.dryRunHeadSha ?? meta.headRefOid;
168
+ const inlineCommentTargets = parseRightSideDiffTargets(await input.exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(headSha)}`, { cwd: worktreePath }));
162
169
  const artifactDir = outputDir(input);
163
170
  let entries = await mapPool(input.repository.agents.reviewers, input.repository.concurrency.reviewers, async (reviewer) => {
164
171
  throwIfAborted(input.signal);
@@ -213,7 +220,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
213
220
  }
214
221
  },
215
222
  options: reviewer.options,
216
- parse: parseRereviewOutput,
223
+ parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
217
224
  permission: reviewer.permission,
218
225
  prompt,
219
226
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -296,7 +303,11 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
296
303
  }
297
304
  },
298
305
  options: reviewer.options,
299
- parse: parseRereviewCloseReconsiderationOutput,
306
+ parse: (text) => {
307
+ const output = parseRereviewCloseReconsiderationOutput(text);
308
+ validateInlineCommentTargets(output.newFindings, inlineCommentTargets, "newFindings");
309
+ return output;
310
+ },
300
311
  permission: reviewer.permission,
301
312
  prompt,
302
313
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -1,12 +1,13 @@
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, } from "../github/commands";
3
+ import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, shellQuote, } from "../github/commands";
4
4
  import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
5
5
  import { prRunOutputDir } from "../config/output";
6
6
  import { worktreeBaseDir } from "../config/worktree";
7
7
  import { parseCloseReconsiderationOutput, parseFindingValidationOutput, parseRereviewOutput, parseReviewOutput, } from "../prompts/output";
8
8
  import { throwIfAborted, withAbortSignal } from "./abort";
9
9
  import { waitForChecksWithClassification } from "./ci";
10
+ import { parseRightSideDiffTargets, validateInlineCommentTargets, } from "./inline-comments";
10
11
  import { applyFindingValidation, reviewFindingTargets, validateFindingVotes, } from "./findings";
11
12
  import { closeMinorityReviewers, mergeVerdictForPolicy, } from "./majority";
12
13
  import { runModelWithRepair } from "./model";
@@ -123,6 +124,16 @@ function previousReviewText(review) {
123
124
  submittedAt: review.submittedAt,
124
125
  }, null, 2);
125
126
  }
127
+ function parseReviewOutputWithInlineTargets(text, targets) {
128
+ const output = parseReviewOutput(text);
129
+ validateInlineCommentTargets(output.findings, targets);
130
+ return output;
131
+ }
132
+ function parseRereviewOutputWithInlineTargets(text, targets) {
133
+ const output = parseRereviewOutput(text);
134
+ validateInlineCommentTargets(output.newFindings, targets, "newFindings");
135
+ return output;
136
+ }
126
137
  function reviewOutputFromState(review) {
127
138
  const verdict = reviewStateToVerdict(review.state);
128
139
  return verdict === "CLOSE"
@@ -336,7 +347,11 @@ async function runCloseReconsideration(input) {
336
347
  }
337
348
  },
338
349
  options: reviewer.options,
339
- parse: parseCloseReconsiderationOutput,
350
+ parse: (text) => {
351
+ const output = parseCloseReconsiderationOutput(text);
352
+ validateInlineCommentTargets(output.findings, input.inlineCommentTargets);
353
+ return output;
354
+ },
340
355
  permission: reviewer.permission,
341
356
  prompt,
342
357
  repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
@@ -519,6 +534,7 @@ export async function runReview(input) {
519
534
  return [];
520
535
  return [{ assignment, reviewer }];
521
536
  });
537
+ const inlineCommentTargets = parseRightSideDiffTargets(await exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(meta.headRefOid)}`, { cwd: worktreePath }));
522
538
  for (const reviewer of input.repository.agents.reviewers) {
523
539
  const assignment = mode.assignments.get(reviewer.account);
524
540
  if (assignment?.type !== "skip")
@@ -583,7 +599,7 @@ export async function runReview(input) {
583
599
  }
584
600
  },
585
601
  options: reviewer.options,
586
- parse: parseRereviewOutput,
602
+ parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
587
603
  permission: reviewer.permission,
588
604
  prompt,
589
605
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -648,7 +664,7 @@ export async function runReview(input) {
648
664
  }
649
665
  },
650
666
  options: reviewer.options,
651
- parse: parseReviewOutput,
667
+ parse: (text) => parseReviewOutputWithInlineTargets(text, inlineCommentTargets),
652
668
  permission: reviewer.permission,
653
669
  prompt,
654
670
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -708,6 +724,7 @@ export async function runReview(input) {
708
724
  });
709
725
  entries = await runCloseReconsideration({
710
726
  entries: [...entries, ...skippedCloseEntries],
727
+ inlineCommentTargets,
711
728
  meta,
712
729
  outputDir,
713
730
  reviewInput: { ...input, exec },
@@ -35,6 +35,19 @@ function isActiveStatus(status) {
35
35
  status === "running" ||
36
36
  status === "posting");
37
37
  }
38
+ function matchesNumberFilter(value, filter) {
39
+ if (filter == null)
40
+ return true;
41
+ return Array.isArray(filter)
42
+ ? value != null && filter.includes(value)
43
+ : value === filter;
44
+ }
45
+ function hasAllRequestedPrStates(states, pr) {
46
+ if (pr == null)
47
+ return true;
48
+ const prs = Array.isArray(pr) ? pr : [pr];
49
+ return prs.every((item) => states.some((state) => state.pr === item));
50
+ }
38
51
  function isWithinDirectory(directory, path) {
39
52
  const relation = relative(directory, path);
40
53
  return (relation === "" || (!relation.startsWith("..") && !isAbsolute(relation)));
@@ -548,6 +561,7 @@ export class MagiRunManager {
548
561
  while (input.block) {
549
562
  const states = await this.filteredStates(input);
550
563
  if (states.length &&
564
+ hasAllRequestedPrStates(states, input.pr) &&
551
565
  states.every((state) => !isActiveStatus(state.status)))
552
566
  return states;
553
567
  if (Date.now() - startedAt >= timeoutMs)
@@ -1661,7 +1675,7 @@ export class MagiRunManager {
1661
1675
  return states
1662
1676
  .filter((state) => input.command == null || state.command === input.command)
1663
1677
  .filter((state) => input.issue == null || state.issue === input.issue)
1664
- .filter((state) => input.pr == null || state.pr === input.pr)
1678
+ .filter((state) => matchesNumberFilter(state.pr, input.pr))
1665
1679
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1666
1680
  }
1667
1681
  async selectState(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260520163847",
3
+ "version": "0.0.0-dev-20260520165753",
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>",