opencode-magi 0.0.0-dev-20260521221222 → 0.0.0-dev-20260521235114

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.
@@ -515,9 +515,6 @@ export async function postCloseComment(exec, repository, pr, account, body) {
515
515
  await rm(payloadPath, { force: true });
516
516
  }
517
517
  }
518
- function isInlineFinding(finding) {
519
- return finding.line != null;
520
- }
521
518
  function findingComment(finding) {
522
519
  const comment = {
523
520
  body: `**Issue:** ${finding.issue}\n\n**Fix:** ${finding.fix}`,
@@ -531,54 +528,18 @@ function findingComment(finding) {
531
528
  }
532
529
  return comment;
533
530
  }
534
- function requirementFindingSummary(finding) {
535
- return [
536
- `- Missing issue #${finding.issueNumber} requirement: ${finding.requirement}`,
537
- ` Evidence: ${finding.evidence}`,
538
- ` Fix: ${finding.fix}`,
539
- ].join("\n");
540
- }
541
- function findingLocation(finding) {
542
- if (finding.line == null)
543
- return finding.path;
544
- if (finding.startLine == null)
545
- return `${finding.path}:${finding.line}`;
546
- return `${finding.path}:${finding.startLine}-${finding.line}`;
547
- }
548
- function findingSummary(finding) {
549
- return [
550
- `- ${findingLocation(finding)}: ${finding.issue}`,
551
- ` Fix: ${finding.fix}`,
552
- ]
553
- .filter(Boolean)
554
- .join("\n");
555
- }
556
- function changesRequestedBody(findings, requirementFindings) {
557
- const inlineFindings = findings.filter(isInlineFinding);
558
- const fileLevelFindings = findings.filter((finding) => !isInlineFinding(finding));
559
- const sections = [];
560
- if (inlineFindings.length) {
561
- sections.push(["Inline findings:", ...inlineFindings.map(findingSummary)].join("\n"));
562
- }
563
- if (fileLevelFindings.length) {
564
- sections.push(["File-level findings:", ...fileLevelFindings.map(findingSummary)].join("\n"));
565
- }
566
- if (requirementFindings.length) {
567
- sections.push([
568
- "Requirement findings:",
569
- ...requirementFindings.map(requirementFindingSummary),
570
- ].join("\n"));
571
- }
572
- return sections.join("\n\n");
531
+ function changesRequestedBody(findings) {
532
+ return findings.length === 1
533
+ ? "Changes requested: 1 inline comment."
534
+ : `Changes requested: ${findings.length} inline comments.`;
573
535
  }
574
- export async function postChangesRequested(exec, repository, pr, account, findings, requirementFindings = []) {
536
+ export async function postChangesRequested(exec, repository, pr, account, findings) {
575
537
  const token = await ghToken(exec, repository, account);
576
538
  const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
577
- const inlineFindings = findings.filter(isInlineFinding);
578
- const body = changesRequestedBody(findings, requirementFindings);
539
+ const body = changesRequestedBody(findings);
579
540
  await writeFile(payloadPath, JSON.stringify({
580
541
  body,
581
- comments: inlineFindings.map(findingComment),
542
+ comments: findings.map(findingComment),
582
543
  event: "REQUEST_CHANGES",
583
544
  }));
584
545
  try {
package/dist/index.js CHANGED
@@ -457,6 +457,7 @@ export const MagiPlugin = async ({ client, directory }) => {
457
457
  prs: tool.schema.string(),
458
458
  dryRun: tool.schema.boolean().optional(),
459
459
  sync: tool.schema.boolean().optional(),
460
+ timeoutSeconds: tool.schema.number().optional(),
460
461
  },
461
462
  async execute(args, context) {
462
463
  const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
@@ -482,6 +483,9 @@ export const MagiPlugin = async ({ client, directory }) => {
482
483
  parentSessionId: context.sessionID,
483
484
  signal: context.abort,
484
485
  sync,
486
+ timeoutMs: args.timeoutSeconds == null
487
+ ? undefined
488
+ : args.timeoutSeconds * 1_000,
485
489
  }), { signal: context.abort });
486
490
  if (sync)
487
491
  return syncResult(runManager, states);
@@ -499,6 +503,7 @@ export const MagiPlugin = async ({ client, directory }) => {
499
503
  prs: tool.schema.string(),
500
504
  dryRun: tool.schema.boolean().optional(),
501
505
  sync: tool.schema.boolean().optional(),
506
+ timeoutSeconds: tool.schema.number().optional(),
502
507
  },
503
508
  async execute(args, context) {
504
509
  const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
@@ -523,6 +528,9 @@ export const MagiPlugin = async ({ client, directory }) => {
523
528
  parentSessionId: context.sessionID,
524
529
  signal: context.abort,
525
530
  sync,
531
+ timeoutMs: args.timeoutSeconds == null
532
+ ? undefined
533
+ : args.timeoutSeconds * 1_000,
526
534
  }), { signal: context.abort });
527
535
  if (sync)
528
536
  return syncResult(runManager, states);
@@ -537,6 +545,7 @@ export const MagiPlugin = async ({ client, directory }) => {
537
545
  issues: tool.schema.string(),
538
546
  dryRun: tool.schema.boolean().optional(),
539
547
  sync: tool.schema.boolean().optional(),
548
+ timeoutSeconds: tool.schema.number().optional(),
540
549
  },
541
550
  async execute(args, context) {
542
551
  const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
@@ -567,6 +576,9 @@ export const MagiPlugin = async ({ client, directory }) => {
567
576
  repository,
568
577
  signal: context.abort,
569
578
  sync,
579
+ timeoutMs: args.timeoutSeconds == null
580
+ ? undefined
581
+ : args.timeoutSeconds * 1_000,
570
582
  }), { signal: context.abort });
571
583
  if (sync)
572
584
  return syncResult(runManager, states);
@@ -58,10 +58,9 @@ export function applyFindingValidation(input) {
58
58
  discarded.push(target);
59
59
  return false;
60
60
  });
61
- next[reviewer] =
62
- findings.length || output.requirementFindings.length
63
- ? { ...output, findings }
64
- : { findings: [], requirementFindings: [], verdict: "MERGE" };
61
+ next[reviewer] = findings.length
62
+ ? { ...output, findings }
63
+ : { findings: [], verdict: "MERGE" };
65
64
  }
66
65
  return { outputs: next, summary: { discarded, kept } };
67
66
  }
@@ -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`);
@@ -146,14 +146,14 @@ async function postRereviewOutput(input, reviewerKey, output) {
146
146
  if (output.verdict === "CLOSE") {
147
147
  return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
148
148
  }
149
- if (output.newFindings.length || output.requirementFindings.length) {
149
+ if (output.newFindings.length) {
150
150
  return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
151
151
  fix: "Please address this before merging.",
152
152
  issue: finding.body,
153
153
  path: finding.path,
154
- ...(finding.line == null ? {} : { line: finding.line }),
154
+ line: finding.line,
155
155
  startLine: finding.startLine,
156
- })), output.requirementFindings);
156
+ })));
157
157
  }
158
158
  return replies[0] ?? "";
159
159
  }
@@ -166,45 +166,29 @@ function newFindingToEditorFinding(reviewer, finding) {
166
166
  return {
167
167
  body: finding.body,
168
168
  fix: "Please address this before merging.",
169
+ line: finding.line,
169
170
  path: finding.path,
170
171
  reviewer,
171
- ...(finding.line == null ? {} : { line: finding.line }),
172
172
  ...(finding.startLine == null ? {} : { startLine: finding.startLine }),
173
- type: finding.line == null ? "file" : "inline",
173
+ type: "inline",
174
174
  };
175
175
  }
176
176
  export function blockingReviewFindings(outputs) {
177
177
  return Object.entries(outputs).flatMap(([reviewer, output]) => {
178
178
  if (output.verdict !== "CHANGES_REQUESTED")
179
179
  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
180
  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
- ];
181
+ return output.findings.map((finding) => ({
182
+ fix: finding.fix,
183
+ issue: finding.issue,
184
+ line: finding.line,
185
+ path: finding.path,
186
+ reviewer,
187
+ ...(finding.startLine == null ? {} : { startLine: finding.startLine }),
188
+ type: "inline",
189
+ }));
203
190
  }
204
- return [
205
- ...output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding)),
206
- ...requirementFindings,
207
- ];
191
+ return output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding));
208
192
  });
209
193
  }
210
194
  async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
@@ -507,8 +491,6 @@ function syntheticReviewThreads(outputs) {
507
491
  for (const [reviewer, output] of Object.entries(outputs)) {
508
492
  if ("findings" in output) {
509
493
  threads[reviewer] = output.findings.flatMap((finding) => {
510
- if (finding.line == null)
511
- return [];
512
494
  const commentId = nextCommentId--;
513
495
  return [
514
496
  {
@@ -531,8 +513,6 @@ function syntheticReviewThreads(outputs) {
531
513
  continue;
532
514
  }
533
515
  threads[reviewer] = output.newFindings.flatMap((finding) => {
534
- if (finding.line == null)
535
- return [];
536
516
  const commentId = nextCommentId--;
537
517
  return [
538
518
  {
@@ -721,9 +701,7 @@ export async function runMerge(input) {
721
701
  threads: unresolvedThreads,
722
702
  });
723
703
  const editorFindings = blockingReviewFindings(reportOutputs);
724
- const editableFindings = editableThreads.length
725
- ? editorFindings
726
- : editorFindings.filter((finding) => finding.type !== "inline");
704
+ const editableFindings = editableThreads.length ? editorFindings : [];
727
705
  const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
728
706
  cycle > input.repository.merge.maxThreadResolutionCycles;
729
707
  if (!editableThreads.length &&
@@ -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
  }
@@ -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")
@@ -147,11 +147,10 @@ function parsePostedFindingLocation(location) {
147
147
  const line = /^(.*):(\d+)$/.exec(location);
148
148
  if (line)
149
149
  return { line: Number(line[2]), path: line[1] ?? location };
150
- return { path: location };
150
+ return undefined;
151
151
  }
152
152
  function reviewFindingsFromBody(body) {
153
153
  const findings = [];
154
- const requirementFindings = [];
155
154
  const lines = (body ?? "").split(/\r?\n/);
156
155
  let section;
157
156
  for (let index = 0; index < lines.length; index += 1) {
@@ -164,26 +163,16 @@ function reviewFindingsFromBody(body) {
164
163
  section = undefined;
165
164
  continue;
166
165
  }
167
- const requirementMatch = /^- Missing issue #(\d+) requirement: (.+)$/.exec(line ?? "");
168
- const evidence = /^\s+Evidence: (.+)$/.exec(lines[index + 1] ?? "");
169
- const requirementFix = /^\s+Fix: (.+)$/.exec(lines[index + 2] ?? "");
170
- if (requirementMatch && evidence && requirementFix) {
171
- requirementFindings.push({
172
- evidence: evidence[1] ?? "See review body.",
173
- fix: requirementFix[1] ?? "Please address this before merging.",
174
- issueNumber: Number(requirementMatch[1]),
175
- requirement: requirementMatch[2] ?? "Review requirement.",
176
- });
177
- index += 2;
178
- continue;
179
- }
180
166
  if (section === "finding") {
181
167
  const match = /^- (.*): (.+)$/.exec(line ?? "");
182
168
  const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
183
169
  if (!match || !fix)
184
170
  continue;
171
+ const location = parsePostedFindingLocation(match[1] ?? "");
172
+ if (!location)
173
+ continue;
185
174
  findings.push({
186
- ...parsePostedFindingLocation(match[1] ?? ""),
175
+ ...location,
187
176
  fix: fix[1] ?? "Please address this before merging.",
188
177
  issue: match[2] ?? "Review finding.",
189
178
  });
@@ -191,7 +180,7 @@ function reviewFindingsFromBody(body) {
191
180
  continue;
192
181
  }
193
182
  }
194
- return { findings, requirementFindings };
183
+ return { findings };
195
184
  }
196
185
  export function reviewOutputFromState(review) {
197
186
  const verdict = reviewStateToVerdict(review.state);
@@ -201,10 +190,9 @@ export function reviewOutputFromState(review) {
201
190
  ? {
202
191
  findings: [],
203
192
  reason: review.body || "Close requested.",
204
- requirementFindings: [],
205
193
  verdict,
206
194
  }
207
- : { findings: [], requirementFindings: [], verdict };
195
+ : { findings: [], verdict };
208
196
  }
209
197
  export function hasPendingThreadReply(threads, reviewerAccount) {
210
198
  return threads.some((thread) => {
@@ -228,15 +216,15 @@ async function postRereviewOutput(input, reviewerKey, output) {
228
216
  return postApproval(input.exec, input.repository, input.pr, reviewer.account);
229
217
  if (output.verdict === "CLOSE")
230
218
  return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
231
- if (!output.newFindings.length && !output.requirementFindings.length)
219
+ if (!output.newFindings.length)
232
220
  return "";
233
221
  return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
234
222
  fix: "Please address this before merging.",
235
223
  issue: finding.body,
236
224
  path: finding.path,
237
- ...(finding.line == null ? {} : { line: finding.line }),
225
+ line: finding.line,
238
226
  startLine: finding.startLine,
239
- })), output.requirementFindings);
227
+ })));
240
228
  }
241
229
  function isReviewOutput(output) {
242
230
  return "findings" in output;
@@ -15,7 +15,6 @@ const DEFAULT_CLEAR_OPTIONS = {
15
15
  session: true,
16
16
  worktree: true,
17
17
  };
18
- const SYNC_RUN_TIMEOUT_MS = 600_000;
19
18
  function createRunId() {
20
19
  return `run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
21
20
  }
@@ -460,7 +459,7 @@ export class MagiRunManager {
460
459
  signal: controller.signal,
461
460
  });
462
461
  if (input.sync)
463
- return this.executeSync(state, controller, execute);
462
+ return this.executeSync(state, controller, execute, input.timeoutMs);
464
463
  void execute().catch(async (error) => {
465
464
  await this.failRun(runId, error);
466
465
  });
@@ -521,7 +520,7 @@ export class MagiRunManager {
521
520
  signal: controller.signal,
522
521
  });
523
522
  if (input.sync)
524
- return this.executeSync(state, controller, execute);
523
+ return this.executeSync(state, controller, execute, input.timeoutMs);
525
524
  void execute().catch(async (error) => {
526
525
  await this.failRun(runId, error);
527
526
  });
@@ -581,14 +580,14 @@ export class MagiRunManager {
581
580
  signal: controller.signal,
582
581
  });
583
582
  if (input.sync)
584
- return this.executeSync(state, controller, execute);
583
+ return this.executeSync(state, controller, execute, input.timeoutMs);
585
584
  void execute().catch(async (error) => {
586
585
  await this.failRun(runId, error);
587
586
  });
588
587
  return state;
589
588
  }
590
589
  async status(input = {}) {
591
- const timeoutMs = Math.min(input.timeoutMs ?? 60_000, 600_000);
590
+ const timeoutMs = input.timeoutMs;
592
591
  const startedAt = Date.now();
593
592
  while (input.block) {
594
593
  const states = await this.filteredStates(input);
@@ -596,7 +595,7 @@ export class MagiRunManager {
596
595
  hasAllRequestedPrStates(states, input.pr) &&
597
596
  states.every((state) => !isActiveStatus(state.status)))
598
597
  return states;
599
- if (Date.now() - startedAt >= timeoutMs)
598
+ if (timeoutMs != null && Date.now() - startedAt >= timeoutMs)
600
599
  return states;
601
600
  await new Promise((resolve) => setTimeout(resolve, 1_000));
602
601
  }
@@ -1220,19 +1219,24 @@ export class MagiRunManager {
1220
1219
  hasBlockedAgents(state) {
1221
1220
  return this.agentEntries(state).some(([, agent]) => agent.status === "blocked");
1222
1221
  }
1223
- async executeSync(state, controller, execute) {
1222
+ async executeSync(state, controller, execute, timeoutMs) {
1224
1223
  let timeout;
1225
- const timeoutPromise = new Promise((resolve) => {
1226
- timeout = setTimeout(() => resolve("timeout"), SYNC_RUN_TIMEOUT_MS);
1227
- });
1224
+ const timeoutPromise = timeoutMs == null
1225
+ ? undefined
1226
+ : new Promise((resolve) => {
1227
+ timeout = setTimeout(() => resolve("timeout"), timeoutMs);
1228
+ });
1228
1229
  try {
1229
- const result = await Promise.race([
1230
- execute().then(() => "completed"),
1231
- timeoutPromise,
1232
- ]);
1230
+ const result = await (timeoutPromise
1231
+ ? Promise.race([
1232
+ execute().then(() => "completed"),
1233
+ timeoutPromise,
1234
+ ])
1235
+ : execute().then(() => "completed"));
1233
1236
  if (result === "timeout") {
1237
+ const timeoutSeconds = (timeoutMs ?? 0) / 1_000;
1234
1238
  controller.abort();
1235
- await this.failRun(state.runId, new Error("Magi sync run timed out after 600 seconds."));
1239
+ await this.failRun(state.runId, new Error(`Magi sync run timed out after ${timeoutSeconds} seconds.`));
1236
1240
  }
1237
1241
  }
1238
1242
  catch (error) {
@@ -1395,6 +1399,7 @@ export class MagiRunManager {
1395
1399
  repository: input.repository,
1396
1400
  signal: input.signal,
1397
1401
  sync: input.sync,
1402
+ timeoutMs: input.timeoutMs,
1398
1403
  });
1399
1404
  if (input.sync)
1400
1405
  this.assertSuccessfulSyncFollowUp(followUp);
@@ -1408,6 +1413,7 @@ export class MagiRunManager {
1408
1413
  repository: input.repository,
1409
1414
  signal: input.signal,
1410
1415
  sync: input.sync,
1416
+ timeoutMs: input.timeoutMs,
1411
1417
  });
1412
1418
  if (input.sync)
1413
1419
  this.assertSuccessfulSyncFollowUp(followUp);
@@ -15,26 +15,20 @@ The object must match this shape:
15
15
  "perspective": "Optional review perspective."
16
16
  }
17
17
  ],
18
- "requirementFindings": [
19
- {
20
- "issueNumber": 47,
21
- "requirement": "Required closing-issue behavior that is missing.",
22
- "evidence": "Why the PR does not satisfy the requirement.",
23
- "fix": "How to satisfy the requirement."
24
- }
25
- ],
26
18
  "reason": "Required only for CLOSE."
27
19
  }
28
20
 
29
21
  Rules:
30
- - MERGE requires empty findings and requirementFindings arrays.
31
- - CHANGES_REQUESTED requires at least one finding or requirementFinding.
32
- - CLOSE requires a reason and empty findings and requirementFindings arrays.
22
+ - MERGE requires an empty findings array.
23
+ - CHANGES_REQUESTED requires at least one finding.
24
+ - CLOSE requires a reason and an empty findings array.
33
25
  - path must be repository-relative.
34
- - line is optional. Include line only when the finding targets a valid line inside the PR diff hunk.
35
- - startLine is allowed only when line is present and must also refer to a line inside the PR diff hunk.
36
- - Omit startLine for single-line findings and omit line for file-level or body-only findings.
37
- - Use requirementFindings only for missing closing-issue requirements; use findings for ordinary file-level issues that do not map cleanly to a diff line.
26
+ - line is required and must target a valid right-side line inside the PR diff hunk.
27
+ - startLine is optional and must also target a valid right-side line inside the same PR diff hunk range.
28
+ - Omit startLine for single-line findings.
29
+ - Do not omit line. Do not create file-level or body-only findings.
30
+ - Missing closing-issue requirements must be normal findings anchored to the nearest responsible changed line.
31
+ - If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
38
32
  </output_contract>`.trim();
39
33
  export const rereviewOutputContract = `
40
34
  <output_contract>
@@ -46,18 +40,19 @@ The object must match this shape:
46
40
  "resolve": [{ "commentId": 123, "threadId": "..." }],
47
41
  "followUps": [{ "commentId": 123, "body": "..." }],
48
42
  "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
49
- "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }],
50
43
  "reason": "Required only for CLOSE."
51
44
  }
52
45
 
53
46
  Rules:
54
- - MERGE requires empty followUps, newFindings, and requirementFindings arrays.
55
- - CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
56
- - CLOSE requires a reason and empty followUps, newFindings, and requirementFindings arrays.
57
- - line is optional. Include line only when the newFinding targets a valid line inside the latest PR diff hunk.
58
- - startLine is allowed only when line is present and must also refer to a line inside the latest PR diff hunk.
59
- - Omit startLine for single-line findings and omit line for file-level or body-only findings.
60
- - Use requirementFindings only for missing closing-issue requirements; use newFindings for ordinary file-level issues that do not map cleanly to a diff line.
47
+ - MERGE requires empty followUps and newFindings arrays.
48
+ - CHANGES_REQUESTED requires at least one followUp or newFinding.
49
+ - CLOSE requires a reason and empty followUps and newFindings arrays.
50
+ - line is required and must target a valid right-side line inside the latest PR diff hunk.
51
+ - startLine is optional and must also target a valid right-side line inside the same latest PR diff hunk range.
52
+ - Omit startLine for single-line findings.
53
+ - Do not omit line. Do not create file-level or body-only findings.
54
+ - Missing closing-issue requirements must be normal newFindings anchored to the nearest responsible changed line.
55
+ - If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
61
56
  </output_contract>`.trim();
62
57
  export const findingValidationOutputContract = `
63
58
  <output_contract>
@@ -96,17 +91,16 @@ The object must match this shape:
96
91
  "issue": "What is wrong.",
97
92
  "fix": "How to fix it."
98
93
  }
99
- ],
100
- "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
94
+ ]
101
95
  }
102
96
 
103
97
  Rules:
104
- - MERGE requires empty findings and requirementFindings arrays.
105
- - CHANGES_REQUESTED requires at least one finding or requirementFinding.
98
+ - MERGE requires an empty findings array.
99
+ - CHANGES_REQUESTED requires at least one finding.
106
100
  - CLOSE is not allowed in this reconsideration step.
107
- - line is optional. Include line only when the finding targets a valid line inside the PR diff hunk.
108
- - startLine is allowed only when line is present.
109
- - Omit startLine for single-line findings and omit line for file-level or body-only findings.
101
+ - line is required and must target a valid right-side line inside the PR diff hunk.
102
+ - startLine is optional and must also target a valid right-side line inside the same PR diff hunk range.
103
+ - Do not omit line. Do not create file-level or body-only findings.
110
104
  </output_contract>`.trim();
111
105
  export const rereviewCloseReconsiderationOutputContract = `
112
106
  <output_contract>
@@ -117,17 +111,16 @@ The object must match this shape:
117
111
  "verdict": "MERGE" | "CHANGES_REQUESTED",
118
112
  "resolve": [{ "commentId": 123, "threadId": "..." }],
119
113
  "followUps": [{ "commentId": 123, "body": "..." }],
120
- "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
121
- "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
114
+ "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }]
122
115
  }
123
116
 
124
117
  Rules:
125
- - MERGE requires empty followUps, newFindings, and requirementFindings arrays.
126
- - CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
118
+ - MERGE requires empty followUps and newFindings arrays.
119
+ - CHANGES_REQUESTED requires at least one followUp or newFinding.
127
120
  - CLOSE is not allowed in this reconsideration step.
128
- - line is optional. Include line only when the newFinding targets a valid line inside the latest PR diff hunk.
129
- - startLine is allowed only when line is present.
130
- - Omit startLine for single-line findings and omit line for file-level or body-only findings.
121
+ - line is required and must target a valid right-side line inside the latest PR diff hunk.
122
+ - startLine is optional and must also target a valid right-side line inside the same latest PR diff hunk range.
123
+ - Do not omit line. Do not create file-level or body-only findings.
131
124
  </output_contract>`.trim();
132
125
  export const editOutputContract = `
133
126
  <output_contract>
@@ -71,27 +71,16 @@ function requireNumber(value, path) {
71
71
  throw new Error(`${path} must be an integer`);
72
72
  return value;
73
73
  }
74
- function optionalLine(value, path) {
75
- return value == null ? undefined : requireNumber(value, path);
74
+ function requireLine(value, path) {
75
+ if (value == null)
76
+ throw new Error(`${path} is required`);
77
+ return requireNumber(value, path);
76
78
  }
77
79
  function optionalStartLine(input) {
78
80
  if (input.value == null)
79
81
  return undefined;
80
- if (input.line == null)
81
- throw new Error(`${input.path} requires line`);
82
82
  return requireNumber(input.value, input.path);
83
83
  }
84
- function parseRequirementFindings(value) {
85
- return (value == null ? [] : requireArray(value, "requirementFindings")).map((finding, index) => {
86
- const item = finding;
87
- return {
88
- evidence: requireString(item.evidence, `requirementFindings[${index}].evidence`),
89
- fix: requireString(item.fix, `requirementFindings[${index}].fix`),
90
- issueNumber: requireNumber(item.issueNumber, `requirementFindings[${index}].issueNumber`),
91
- requirement: requireString(item.requirement, `requirementFindings[${index}].requirement`),
92
- };
93
- });
94
- }
95
84
  function requireOneOf(value, path, values) {
96
85
  const text = requireString(value, path);
97
86
  if (!values.includes(text)) {
@@ -179,11 +168,13 @@ export function parseReviewOutput(text) {
179
168
  const data = extractJson(text);
180
169
  if (!data || typeof data !== "object")
181
170
  throw new Error("review output must be an object");
171
+ if (data.requirementFindings != null)
172
+ throw new Error("requirementFindings is not accepted");
182
173
  if (!isVerdict(data.verdict))
183
174
  throw new Error("verdict must be MERGE, CHANGES_REQUESTED, or CLOSE");
184
175
  const findings = requireArray(data.findings, "findings").map((finding, index) => {
185
176
  const item = finding;
186
- const line = optionalLine(item.line, `findings[${index}].line`);
177
+ const line = requireLine(item.line, `findings[${index}].line`);
187
178
  return {
188
179
  fix: requireString(item.fix, `findings[${index}].fix`),
189
180
  issue: requireString(item.issue, `findings[${index}].issue`),
@@ -199,17 +190,12 @@ export function parseReviewOutput(text) {
199
190
  }),
200
191
  };
201
192
  });
202
- const requirementFindings = parseRequirementFindings(data.requirementFindings);
203
- if (data.verdict === "MERGE" &&
204
- (findings.length || requirementFindings.length))
205
- throw new Error("MERGE requires no findings or requirementFindings");
206
- if (data.verdict === "CHANGES_REQUESTED" &&
207
- !findings.length &&
208
- !requirementFindings.length)
209
- throw new Error("CHANGES_REQUESTED requires findings or requirementFindings");
210
- if (data.verdict === "CLOSE" &&
211
- (findings.length || requirementFindings.length))
212
- throw new Error("CLOSE requires no findings or requirementFindings");
193
+ if (data.verdict === "MERGE" && findings.length)
194
+ throw new Error("MERGE requires no findings");
195
+ if (data.verdict === "CHANGES_REQUESTED" && !findings.length)
196
+ throw new Error("CHANGES_REQUESTED requires findings");
197
+ if (data.verdict === "CLOSE" && findings.length)
198
+ throw new Error("CLOSE requires no findings");
213
199
  const reason = typeof data.reason === "string" && data.reason.trim()
214
200
  ? data.reason
215
201
  : undefined;
@@ -218,7 +204,6 @@ export function parseReviewOutput(text) {
218
204
  return {
219
205
  findings,
220
206
  reason,
221
- requirementFindings,
222
207
  verdict: data.verdict,
223
208
  };
224
209
  }
@@ -226,6 +211,8 @@ export function parseRereviewOutput(text) {
226
211
  const data = extractJson(text);
227
212
  if (!isVerdict(data.verdict))
228
213
  throw new Error("rereview verdict must be MERGE, CHANGES_REQUESTED, or CLOSE");
214
+ if (data.requirementFindings != null)
215
+ throw new Error("requirementFindings is not accepted");
229
216
  const resolve = requireArray(data.resolve, "resolve").map((item, index) => {
230
217
  const value = item;
231
218
  return {
@@ -242,7 +229,7 @@ export function parseRereviewOutput(text) {
242
229
  });
243
230
  const newFindings = requireArray(data.newFindings, "newFindings").map((item, index) => {
244
231
  const value = item;
245
- const line = optionalLine(value.line, `newFindings[${index}].line`);
232
+ const line = requireLine(value.line, `newFindings[${index}].line`);
246
233
  return {
247
234
  body: requireString(value.body, `newFindings[${index}].body`),
248
235
  line,
@@ -254,29 +241,24 @@ export function parseRereviewOutput(text) {
254
241
  }),
255
242
  };
256
243
  });
257
- const requirementFindings = parseRequirementFindings(data.requirementFindings);
258
- if (data.verdict === "MERGE" &&
259
- (followUps.length || newFindings.length || requirementFindings.length)) {
260
- throw new Error("MERGE requires no followUps, newFindings, or requirementFindings");
244
+ if (data.verdict === "MERGE" && (followUps.length || newFindings.length)) {
245
+ throw new Error("MERGE requires no followUps or newFindings");
261
246
  }
262
- if (data.verdict === "CLOSE" &&
263
- (followUps.length || newFindings.length || requirementFindings.length)) {
264
- throw new Error("CLOSE requires no followUps, newFindings, or requirementFindings");
247
+ if (data.verdict === "CLOSE" && (followUps.length || newFindings.length)) {
248
+ throw new Error("CLOSE requires no followUps or newFindings");
265
249
  }
266
250
  if (data.verdict === "CLOSE" && !data.reason) {
267
251
  throw new Error("CLOSE requires reason");
268
252
  }
269
253
  if (data.verdict === "CHANGES_REQUESTED" &&
270
254
  !followUps.length &&
271
- !newFindings.length &&
272
- !requirementFindings.length) {
273
- throw new Error("CHANGES_REQUESTED requires followUps, newFindings, or requirementFindings");
255
+ !newFindings.length) {
256
+ throw new Error("CHANGES_REQUESTED requires followUps or newFindings");
274
257
  }
275
258
  return {
276
259
  followUps,
277
260
  newFindings,
278
261
  reason: data.reason == null ? undefined : requireString(data.reason, "reason"),
279
- requirementFindings,
280
262
  resolve,
281
263
  verdict: data.verdict,
282
264
  };
@@ -2,7 +2,7 @@ Fix pull request #{pr} for {owner}/{repo}.
2
2
  The PR worktree is {worktreePath}.
3
3
 
4
4
  Act as the PR author and address every blocking review finding listed below.
5
- Review findings are the complete set of requested changes. Inline findings target a PR diff line; file-level findings may not have a GitHub thread; requirement findings describe missing closing-issue requirements.
5
+ Review findings are the complete set of requested changes. Each finding targets a PR diff line and should have a corresponding GitHub review thread unless it comes from legacy read-side state.
6
6
  {reviewFindings}
7
7
 
8
8
  Unresolved GitHub review threads are conversations that may need replies or resolution.
@@ -12,5 +12,4 @@ For each review finding and thread, decide whether you agree with the reviewer.
12
12
  If you understand and agree with the requested change, edit the code, stage changes, commit, and reply with action FIXED for each related thread.
13
13
  If a requested change in a thread is incorrect or unnecessary and you have a clear reason, do not edit for that thread; reply with action DISAGREE and explain why.
14
14
  If you cannot determine whether a threaded request is correct or what change is expected, do not blindly edit; reply with action ASK and ask a concrete question.
15
- File-level and requirement findings may not have a thread to reply to, but they are still blocking and must be addressed.
16
15
  Do not make changes just because a reviewer requested them. Do not push.
@@ -2,4 +2,5 @@ You requested CLOSE for pull request #{pr} in {owner}/{repo}, but the other revi
2
2
  Reconsider your decision using the existing session context and choose MERGE or CHANGES_REQUESTED instead.
3
3
  Original close reason:
4
4
  {closeReason}
5
+ Every finding must target a valid right-side line in the PR diff. If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior.
5
6
  Do not edit files or perform write operations.
@@ -9,6 +9,9 @@ If there is no new commit, still reconsider the thread when a user replied after
9
9
  If you agree with the user's explanation or the code is fixed, resolve the thread.
10
10
  If you do not agree, reply in the same thread with a followUp explaining why the issue still needs changes and keep CHANGES_REQUESTED.
11
11
  Do not duplicate an existing unresolved thread as a newFinding. Use newFindings only for separate new issues.
12
+ Every newFinding must target a valid right-side line in the PR diff.
13
+ If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
14
+ Do not omit line. Do not create file-level or body-only newFindings.
12
15
 
13
16
  {ciFailureContextBlock}
14
17
  Do not edit files or perform write operations.
@@ -10,4 +10,8 @@ Request changes if a closing issue requirement is missing, only documented, only
10
10
  Do not approve solely because the PR improves the codebase if it claims to close an issue that remains incomplete.
11
11
  For referenced non-closing issues, use them as context only unless the PR body explicitly claims to complete them.
12
12
 
13
+ Every finding must target a valid right-side line in the PR diff.
14
+ If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
15
+ Do not omit line. Do not create file-level or body-only findings.
16
+
13
17
  {ciFailureContextBlock}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260521221222",
3
+ "version": "0.0.0-dev-20260521235114",
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>",