opencode-magi 0.6.1 → 0.8.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.
@@ -163,6 +163,90 @@ export async function inlineCommentTargetsForDiff(input) {
163
163
  cwd: input.worktreePath,
164
164
  }));
165
165
  }
166
+ function firstTargetLine(targets, path) {
167
+ const lines = targets.get(path);
168
+ if (!lines?.size)
169
+ return undefined;
170
+ return [...lines].sort((a, b) => a - b)[0];
171
+ }
172
+ function mergeInlineCommentTargets(left, right) {
173
+ const merged = new Map();
174
+ for (const [path, lines] of [...left, ...right]) {
175
+ const targetLines = merged.get(path) ?? new Set();
176
+ for (const line of lines)
177
+ targetLines.add(line);
178
+ merged.set(path, targetLines);
179
+ }
180
+ return merged;
181
+ }
182
+ function targetLineSummary(targets, path) {
183
+ const lines = targets.get(path);
184
+ if (!lines?.size)
185
+ return "(none)";
186
+ const sorted = [...lines].sort((a, b) => a - b);
187
+ const shown = sorted.slice(0, 12).join(", ");
188
+ return sorted.length > 12 ? `${shown}, ...` : shown;
189
+ }
190
+ function indentedExcerpt(lines) {
191
+ return lines
192
+ .slice(0, 24)
193
+ .map((line) => ` ${line}`)
194
+ .join("\n");
195
+ }
196
+ function parseMergeConflictSections(output) {
197
+ const conflictHeaders = new Set([
198
+ "added in both",
199
+ "changed in both",
200
+ "removed in local",
201
+ "removed in remote",
202
+ ]);
203
+ const sections = [];
204
+ let current;
205
+ for (const line of output.split("\n")) {
206
+ if (!line.trim())
207
+ continue;
208
+ if (!line.startsWith(" ") &&
209
+ !line.startsWith("+") &&
210
+ !line.startsWith("-") &&
211
+ !line.startsWith("@")) {
212
+ current = conflictHeaders.has(line)
213
+ ? { lines: [line], paths: new Set() }
214
+ : undefined;
215
+ if (current)
216
+ sections.push(current);
217
+ continue;
218
+ }
219
+ if (!current)
220
+ continue;
221
+ current.lines.push(line);
222
+ const path = /^ (?:base|our|their)\s+\d+\s+[0-9a-f]+\s+(.+)$/.exec(line)?.[1];
223
+ if (path)
224
+ current.paths.add(path);
225
+ }
226
+ return sections.flatMap((section) => [...section.paths].map((path) => ({
227
+ excerpt: indentedExcerpt(section.lines),
228
+ path,
229
+ })));
230
+ }
231
+ export async function mergeConflictContextForDiff(input) {
232
+ const mergeBase = (await input.exec(`git merge-base ${shellQuote(input.baseSha)} ${shellQuote(input.headSha)}`, { cwd: input.worktreePath })).trim();
233
+ const output = await input.exec(`git merge-tree ${shellQuote(mergeBase)} ${shellQuote(input.headSha)} ${shellQuote(input.baseSha)}`, { cwd: input.worktreePath });
234
+ const conflicts = parseMergeConflictSections(output);
235
+ if (!conflicts.length)
236
+ return "";
237
+ return [
238
+ "The PR currently has unresolved merge conflicts with the base branch.",
239
+ "Treat unresolved conflicts as review findings and request changes when they make the PR unsafe or impossible to merge.",
240
+ "Use suggestedLine when it is present; it is a valid right-side PR diff line for an inline finding.",
241
+ ...conflicts.map((conflict) => {
242
+ const suggestedLine = firstTargetLine(input.inlineCommentTargets, conflict.path);
243
+ const suggestedLineText = suggestedLine
244
+ ? `suggestedLine: ${suggestedLine}`
245
+ : "suggestedLine: (no right-side PR diff line found)";
246
+ return `<conflict_file>\npath: ${conflict.path}\n${suggestedLineText}\nrightSideDiffLines: ${targetLineSummary(input.inlineCommentTargets, conflict.path)}\nmergeTreeExcerpt:\n${conflict.excerpt}\n</conflict_file>`;
247
+ }),
248
+ ].join("\n");
249
+ }
166
250
  function parsePostedFindingLocation(location) {
167
251
  const range = /^(.*):(\d+)-(\d+)$/.exec(location);
168
252
  if (range) {
@@ -709,6 +793,13 @@ export async function runReview(input) {
709
793
  toSha: meta.headRefOid,
710
794
  worktreePath,
711
795
  });
796
+ const mergeConflictContext = await mergeConflictContextForDiff({
797
+ baseSha: meta.baseRefOid,
798
+ exec,
799
+ headSha: meta.headRefOid,
800
+ inlineCommentTargets: initialInlineCommentTargets,
801
+ worktreePath,
802
+ });
712
803
  for (const reviewer of input.repository.agents.reviewers) {
713
804
  const assignment = mode.assignments.get(reviewer.account);
714
805
  if (assignment?.type !== "skip")
@@ -740,6 +831,9 @@ export async function runReview(input) {
740
831
  toSha: meta.headRefOid,
741
832
  worktreePath,
742
833
  });
834
+ const rereviewInlineCommentTargets = mergeConflictContext
835
+ ? mergeInlineCommentTargets(inlineCommentTargets, initialInlineCommentTargets)
836
+ : inlineCommentTargets;
743
837
  const unresolved = unresolvedThreadsByAccount.get(reviewer.account) ??
744
838
  (await fetchUnresolvedThreads(exec, input.repository, input.pr, reviewer.account));
745
839
  const prompt = await composeRereviewPrompt({
@@ -747,6 +841,7 @@ export async function runReview(input) {
747
841
  ciFailureContext,
748
842
  directory: input.directory,
749
843
  headSha: meta.headRefOid,
844
+ mergeConflictContext,
750
845
  pr: input.pr,
751
846
  previousReview: previousReviewText(previous),
752
847
  previousHeadSha: previous.commit.oid,
@@ -787,7 +882,7 @@ export async function runReview(input) {
787
882
  },
788
883
  options: reviewer.options,
789
884
  parentSessionId: input.parentSessionId,
790
- parse: (text) => parseRereviewOutputWithInlineTargets(text, inlineCommentTargets),
885
+ parse: (text) => parseRereviewOutputWithInlineTargets(text, rereviewInlineCommentTargets),
791
886
  permission: reviewer.permission,
792
887
  prompt,
793
888
  repairAttempts: input.config.output?.repairAttempts ?? 3,
@@ -819,6 +914,7 @@ export async function runReview(input) {
819
914
  ciFailureContext,
820
915
  directory: input.directory,
821
916
  headSha: meta.headRefOid,
917
+ mergeConflictContext,
822
918
  pr: input.pr,
823
919
  repository: input.repository,
824
920
  reviewContext,
@@ -1616,16 +1616,16 @@ export class MagiRunManager {
1616
1616
  }));
1617
1617
  }
1618
1618
  if (progress.type === "triage_agent_started") {
1619
- await this.notify(state, `**Triage agent ${progress.voter}** started ${progress.phase} for ${issue}.`);
1619
+ await this.notify(state, `**Triage voter ${progress.voter}** started ${progress.phase} for ${issue}.`);
1620
1620
  }
1621
1621
  if (progress.type === "triage_agent_repair") {
1622
- await this.notify(state, `**Triage agent ${progress.voter}** started JSON regeneration for ${issue}.`);
1622
+ await this.notify(state, `**Triage voter ${progress.voter}** started JSON regeneration for ${issue}.`);
1623
1623
  }
1624
1624
  if (progress.type === "triage_agent_completed") {
1625
- await this.notify(state, `**Triage agent ${progress.voter}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
1625
+ await this.notify(state, `**Triage voter ${progress.voter}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
1626
1626
  }
1627
1627
  if (progress.type === "triage_agent_failed") {
1628
- await this.notify(state, `**Triage agent ${progress.voter}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
1628
+ await this.notify(state, `**Triage voter ${progress.voter}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
1629
1629
  }
1630
1630
  if (progress.type === "comment_posting") {
1631
1631
  await this.notify(state, `Posting triage comment for ${issue}.`);