opencode-magi 0.2.0 → 0.4.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.
Files changed (36) hide show
  1. package/README.md +19 -0
  2. package/dist/commands.js +4 -0
  3. package/dist/config/output.js +11 -2
  4. package/dist/config/resolve.js +81 -1
  5. package/dist/config/validate.js +341 -3
  6. package/dist/config/worktree.js +8 -2
  7. package/dist/github/commands.js +381 -19
  8. package/dist/index.js +252 -26
  9. package/dist/orchestrator/ci.js +1 -1
  10. package/dist/orchestrator/findings.js +4 -3
  11. package/dist/orchestrator/inline-comments.js +79 -0
  12. package/dist/orchestrator/majority.js +14 -0
  13. package/dist/orchestrator/merge.js +108 -34
  14. package/dist/orchestrator/report.js +25 -7
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +122 -14
  17. package/dist/orchestrator/run-manager.js +408 -17
  18. package/dist/orchestrator/triage.js +1119 -0
  19. package/dist/permissions/editor.json +8 -1
  20. package/dist/prompts/compose.js +163 -1
  21. package/dist/prompts/contracts.js +131 -18
  22. package/dist/prompts/output.js +173 -22
  23. package/dist/prompts/templates/merge/edit.md +12 -5
  24. package/dist/prompts/templates/review/review.md +6 -0
  25. package/dist/prompts/templates/triage/acceptance.md +7 -0
  26. package/dist/prompts/templates/triage/action.md +5 -0
  27. package/dist/prompts/templates/triage/category.md +10 -0
  28. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  29. package/dist/prompts/templates/triage/comment.md +5 -0
  30. package/dist/prompts/templates/triage/create.md +7 -0
  31. package/dist/prompts/templates/triage/duplicate.md +7 -0
  32. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  33. package/dist/prompts/templates/triage/question.md +5 -0
  34. package/dist/prompts/templates/triage/reconsider.md +5 -0
  35. package/package.json +5 -2
  36. package/schema.json +162 -5
@@ -1,17 +1,19 @@
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";
13
14
  import { mapPool } from "./pool";
14
15
  import { formatReviewReport } from "./report";
16
+ import { buildReviewContextSnapshot, renderReviewContext, } from "./review-context";
15
17
  import { checkSafetyGate, hasSafetyGate } from "./safety";
16
18
  function errorMessage(error) {
17
19
  return error instanceof Error ? error.message : String(error);
@@ -37,7 +39,7 @@ async function postReviewOutput(input, reviewerKey, output) {
37
39
  return postApproval(input.exec, input.repository, input.pr, reviewer.account);
38
40
  if (output.verdict === "CLOSE")
39
41
  return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
40
- return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings);
42
+ return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.findings, output.requirementFindings);
41
43
  }
42
44
  function dryRunReviewPost(key, output) {
43
45
  if (output.verdict === "MERGE")
@@ -123,11 +125,87 @@ function previousReviewText(review) {
123
125
  submittedAt: review.submittedAt,
124
126
  }, null, 2);
125
127
  }
126
- function reviewOutputFromState(review) {
128
+ function parseReviewOutputWithInlineTargets(text, targets) {
129
+ const output = parseReviewOutput(text);
130
+ validateInlineCommentTargets(output.findings, targets);
131
+ return output;
132
+ }
133
+ function parseRereviewOutputWithInlineTargets(text, targets) {
134
+ const output = parseRereviewOutput(text);
135
+ validateInlineCommentTargets(output.newFindings, targets, "newFindings");
136
+ return output;
137
+ }
138
+ function parsePostedFindingLocation(location) {
139
+ const range = /^(.*):(\d+)-(\d+)$/.exec(location);
140
+ if (range) {
141
+ return {
142
+ line: Number(range[3]),
143
+ path: range[1] ?? location,
144
+ startLine: Number(range[2]),
145
+ };
146
+ }
147
+ const line = /^(.*):(\d+)$/.exec(location);
148
+ if (line)
149
+ return { line: Number(line[2]), path: line[1] ?? location };
150
+ return { path: location };
151
+ }
152
+ function reviewFindingsFromBody(body) {
153
+ const findings = [];
154
+ const requirementFindings = [];
155
+ const lines = (body ?? "").split(/\r?\n/);
156
+ let section;
157
+ for (let index = 0; index < lines.length; index += 1) {
158
+ const line = lines[index];
159
+ if (line === "Inline findings:" || line === "File-level findings:") {
160
+ section = "finding";
161
+ continue;
162
+ }
163
+ if (line === "Requirement findings:") {
164
+ section = "requirement";
165
+ continue;
166
+ }
167
+ if (section === "finding") {
168
+ const match = /^- (.*): (.+)$/.exec(line ?? "");
169
+ const fix = /^\s+Fix: (.+)$/.exec(lines[index + 1] ?? "");
170
+ if (!match || !fix)
171
+ continue;
172
+ findings.push({
173
+ ...parsePostedFindingLocation(match[1] ?? ""),
174
+ fix: fix[1] ?? "Please address this before merging.",
175
+ issue: match[2] ?? "Review finding.",
176
+ });
177
+ index += 1;
178
+ continue;
179
+ }
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
+ }
195
+ return { findings, requirementFindings };
196
+ }
197
+ export function reviewOutputFromState(review) {
127
198
  const verdict = reviewStateToVerdict(review.state);
199
+ if (verdict === "CHANGES_REQUESTED")
200
+ return { ...reviewFindingsFromBody(review.body), verdict };
128
201
  return verdict === "CLOSE"
129
- ? { findings: [], reason: review.body || "Close requested.", verdict }
130
- : { findings: [], verdict };
202
+ ? {
203
+ findings: [],
204
+ reason: review.body || "Close requested.",
205
+ requirementFindings: [],
206
+ verdict,
207
+ }
208
+ : { findings: [], requirementFindings: [], verdict };
131
209
  }
132
210
  export function hasPendingThreadReply(threads, reviewerAccount) {
133
211
  return threads.some((thread) => {
@@ -151,15 +229,15 @@ async function postRereviewOutput(input, reviewerKey, output) {
151
229
  return postApproval(input.exec, input.repository, input.pr, reviewer.account);
152
230
  if (output.verdict === "CLOSE")
153
231
  return postCloseComment(input.exec, input.repository, input.pr, reviewer.account, output.reason ?? "Close requested.");
154
- if (!output.newFindings.length)
232
+ if (!output.newFindings.length && !output.requirementFindings.length)
155
233
  return "";
156
234
  return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
157
235
  fix: "Please address this before merging.",
158
236
  issue: finding.body,
159
- line: finding.line,
160
237
  path: finding.path,
238
+ ...(finding.line == null ? {} : { line: finding.line }),
161
239
  startLine: finding.startLine,
162
- })));
240
+ })), output.requirementFindings);
163
241
  }
164
242
  function isReviewOutput(output) {
165
243
  return "findings" in output;
@@ -191,6 +269,7 @@ async function runFindingValidation(input) {
191
269
  includeSessionContext: !hasReviewerSession,
192
270
  pr: input.reviewInput.pr,
193
271
  repository: input.reviewInput.repository,
272
+ reviewContext: input.reviewContext,
194
273
  reviewer,
195
274
  worktreePath: input.worktreePath,
196
275
  });
@@ -303,6 +382,7 @@ async function runCloseReconsideration(input) {
303
382
  includeSessionContext: !hasReviewerSession,
304
383
  pr: input.reviewInput.pr,
305
384
  repository: input.reviewInput.repository,
385
+ reviewContext: input.reviewContext,
306
386
  reviewer,
307
387
  worktreePath: input.worktreePath,
308
388
  });
@@ -336,7 +416,11 @@ async function runCloseReconsideration(input) {
336
416
  }
337
417
  },
338
418
  options: reviewer.options,
339
- parse: parseCloseReconsiderationOutput,
419
+ parse: (text) => {
420
+ const output = parseCloseReconsiderationOutput(text);
421
+ validateInlineCommentTargets(output.findings, input.inlineCommentTargets);
422
+ return output;
423
+ },
340
424
  permission: reviewer.permission,
341
425
  prompt,
342
426
  repairAttempts: input.reviewInput.config.output?.repairAttempts ?? 3,
@@ -397,6 +481,7 @@ export async function runReview(input) {
397
481
  dryRun: input.dryRun,
398
482
  outputs: {},
399
483
  posted: {},
484
+ pr: input.pr,
400
485
  repository: input.repository,
401
486
  safety,
402
487
  });
@@ -448,6 +533,15 @@ export async function runReview(input) {
448
533
  pr: input.pr,
449
534
  }), ...(input.runId ? [input.runId] : []));
450
535
  await mkdir(outputDir, { recursive: true });
536
+ await input.onProgress?.({ phase: "fetching review context", type: "phase" });
537
+ const reviewContextSnapshot = await buildReviewContextSnapshot({
538
+ exec,
539
+ pr: meta,
540
+ repository: input.repository,
541
+ });
542
+ const reviewContext = renderReviewContext(reviewContextSnapshot);
543
+ await writeFile(join(outputDir, "review-context.json"), JSON.stringify(reviewContextSnapshot, null, 2));
544
+ await writeFile(join(outputDir, "review-context.md"), `${reviewContext}\n`);
451
545
  await input.onProgress?.({ phase: "waiting for checks", type: "phase" });
452
546
  const checkResult = await waitForChecksWithClassification({
453
547
  client: input.client,
@@ -518,6 +612,7 @@ export async function runReview(input) {
518
612
  return [];
519
613
  return [{ assignment, reviewer }];
520
614
  });
615
+ const inlineCommentTargets = parseRightSideDiffTargets(await exec(`git diff --no-ext-diff --unified=3 ${shellQuote(meta.baseRefOid)} ${shellQuote(meta.headRefOid)}`, { cwd: worktreePath }));
521
616
  for (const reviewer of input.repository.agents.reviewers) {
522
617
  const assignment = mode.assignments.get(reviewer.account);
523
618
  if (assignment?.type !== "skip")
@@ -548,6 +643,7 @@ export async function runReview(input) {
548
643
  previousReview: previousReviewText(previous),
549
644
  previousHeadSha: previous.commit.oid,
550
645
  repository: input.repository,
646
+ reviewContext,
551
647
  reviewer,
552
648
  unresolvedThreads: JSON.stringify(unresolved, null, 2),
553
649
  worktreePath,
@@ -582,7 +678,7 @@ export async function runReview(input) {
582
678
  }
583
679
  },
584
680
  options: reviewer.options,
585
- parse: parseRereviewOutput,
681
+ parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
586
682
  permission: reviewer.permission,
587
683
  prompt,
588
684
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -614,6 +710,7 @@ export async function runReview(input) {
614
710
  headSha: meta.headRefOid,
615
711
  pr: input.pr,
616
712
  repository: input.repository,
713
+ reviewContext,
617
714
  reviewer,
618
715
  worktreePath,
619
716
  });
@@ -647,7 +744,7 @@ export async function runReview(input) {
647
744
  }
648
745
  },
649
746
  options: reviewer.options,
650
- parse: parseReviewOutput,
747
+ parse: (text) => parseReviewOutputWithInlineTargets(text, inlineCommentTargets),
651
748
  permission: reviewer.permission,
652
749
  prompt,
653
750
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -707,8 +804,10 @@ export async function runReview(input) {
707
804
  });
708
805
  entries = await runCloseReconsideration({
709
806
  entries: [...entries, ...skippedCloseEntries],
807
+ inlineCommentTargets,
710
808
  meta,
711
809
  outputDir,
810
+ reviewContext,
712
811
  reviewInput: { ...input, exec },
713
812
  sessionIds,
714
813
  targets: closeTargets,
@@ -718,11 +817,19 @@ export async function runReview(input) {
718
817
  entries,
719
818
  meta,
720
819
  outputDir,
820
+ reviewContext,
721
821
  reviewInput: { ...input, exec },
722
822
  sessionIds,
723
823
  worktreePath,
724
824
  });
725
- const outputs = validation.outputs;
825
+ const activeOutputs = validation.outputs;
826
+ const skippedOutputs = Object.fromEntries(input.repository.agents.reviewers.flatMap((reviewer) => {
827
+ const assignment = mode.assignments.get(reviewer.account);
828
+ return assignment?.type === "skip"
829
+ ? [[reviewer.key, reviewOutputFromState(assignment.review)]]
830
+ : [];
831
+ }));
832
+ const outputs = { ...skippedOutputs, ...activeOutputs };
726
833
  const remainingSkippedVerdicts = input.repository.agents.reviewers.flatMap((reviewer) => {
727
834
  const assignment = mode.assignments.get(reviewer.account);
728
835
  if (assignment?.type !== "skip" || closeTargets.includes(reviewer.key))
@@ -734,7 +841,7 @@ export async function runReview(input) {
734
841
  },
735
842
  ];
736
843
  });
737
- const activeVerdicts = Object.entries(outputs).map(([reviewer, output]) => ({
844
+ const activeVerdicts = Object.entries(activeOutputs).map(([reviewer, output]) => ({
738
845
  reviewer,
739
846
  verdict: output.verdict,
740
847
  }));
@@ -747,7 +854,7 @@ export async function runReview(input) {
747
854
  ? [[reviewer.key, "skipped: already reviewed current head"]]
748
855
  : [];
749
856
  })),
750
- ...Object.fromEntries(await Promise.all(Object.entries(outputs).map(async ([key, output]) => [
857
+ ...Object.fromEntries(await Promise.all(Object.entries(activeOutputs).map(async ([key, output]) => [
751
858
  key,
752
859
  input.dryRun
753
860
  ? dryRunReviewPost(key, output)
@@ -793,6 +900,7 @@ export async function runReview(input) {
793
900
  dryRun: input.dryRun,
794
901
  outputs,
795
902
  posted,
903
+ pr: input.pr,
796
904
  repository: input.repository,
797
905
  });
798
906
  await writeFile(join(outputDir, "report.md"), `${report}\n`);