opencode-magi 0.0.0-dev-20260520163717 → 0.0.0-dev-20260520163847

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.
@@ -4,7 +4,7 @@ import { issueRunOutputDir } from "../config/output";
4
4
  import { worktreeBaseDir } from "../config/worktree";
5
5
  import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
6
6
  import { composeTriageBugPrompt, composeTriageActionPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageFeaturePrompt, composeTriageKindPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
7
- import { parseEditOutput, parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCommentClassificationOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, parseTriageFinalOutput, parseTriageKindOutput, } from "../prompts/output";
7
+ import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, parseTriageFinalOutput, parseTriageKindOutput, } from "../prompts/output";
8
8
  import { aggregateStringMajority, majorityThreshold } from "./majority";
9
9
  import { runModelText, runModelWithRepair } from "./model";
10
10
  const MARKER_PREFIX = "opencode-magi:triage";
@@ -65,6 +65,10 @@ function labelsContain(labels, targets) {
65
65
  const set = new Set(labels.map((label) => label.toLowerCase()));
66
66
  return targets.some((target) => set.has(target.toLowerCase()));
67
67
  }
68
+ function existingClearLabels(issue, labels) {
69
+ const existing = new Set(issue.labels.map((label) => label.toLowerCase()));
70
+ return labels.filter((label) => existing.has(label.toLowerCase()));
71
+ }
68
72
  export function resolveIssueKind(issue, repository) {
69
73
  const triage = repository.triage;
70
74
  if (!triage)
@@ -251,6 +255,11 @@ function markerPr(marker) {
251
255
  const pr = Number(marker.pr);
252
256
  return Number.isInteger(pr) && pr > 0 ? pr : undefined;
253
257
  }
258
+ function pullRequestNumberFromUrl(url) {
259
+ const match = url.match(/\/pull\/(\d+)(?:\D|$)/);
260
+ const number = match ? Number(match[1]) : undefined;
261
+ return number && Number.isInteger(number) ? number : undefined;
262
+ }
254
263
  export function mentionAllowed(comment, repository) {
255
264
  const safety = repository.triage?.safety;
256
265
  if (!safety)
@@ -329,6 +338,30 @@ function actionPlan(input) {
329
338
  postComment: true,
330
339
  };
331
340
  }
341
+ function previousAutomationPlan(input) {
342
+ const base = actionPlan({ result: input.result, triage: input.triage });
343
+ const clearLabels = base.clearLabels &&
344
+ existingClearLabels(input.issue, input.triage.automation.clear).length > 0;
345
+ const closeIssue = input.marker.action === "CLOSE" &&
346
+ base.closeIssue &&
347
+ input.issue.state === "OPEN";
348
+ const createPr = input.marker.action === "PR" &&
349
+ base.createPr &&
350
+ !markerPr(input.marker) &&
351
+ !input.relationship.relatedPullRequests.length;
352
+ if (!clearLabels && !closeIssue && !createPr)
353
+ return undefined;
354
+ const action = closeIssue ? "CLOSE" : createPr ? "PR" : "CLEAR_ONLY";
355
+ return {
356
+ ...base,
357
+ action,
358
+ allowedActions: [action],
359
+ clearLabels,
360
+ closeIssue,
361
+ createPr,
362
+ postComment: false,
363
+ };
364
+ }
332
365
  async function runActionPrompt(input) {
333
366
  const agent = input.input.repository.agents.triage?.[0];
334
367
  if (!agent)
@@ -454,7 +487,7 @@ async function persistProcessedMarker(input) {
454
487
  action: input.marker.action ?? input.marker.result ?? "ASK",
455
488
  checkpoint: markerCheckpoint(input.marker),
456
489
  issue: input.issue.number,
457
- pr: markerPr(input.marker),
490
+ pr: input.pr ?? markerPr(input.marker),
458
491
  processed: input.processed,
459
492
  result: input.marker.result ?? "ASK",
460
493
  });
@@ -471,7 +504,7 @@ async function finishWithResult(input) {
471
504
  const triage = input.input.repository.triage;
472
505
  if (!triage)
473
506
  throw new Error("triage configuration is required");
474
- const plan = actionPlan({ result: input.result, triage });
507
+ const plan = input.plan ?? actionPlan({ result: input.result, triage });
475
508
  await runActionPrompt({
476
509
  context: input.context,
477
510
  input: input.input,
@@ -502,6 +535,12 @@ async function finishWithResult(input) {
502
535
  repository: input.input.repository,
503
536
  });
504
537
  }
538
+ if (plan.clearLabels) {
539
+ const clearLabels = existingClearLabels(input.issue, triage.automation.clear);
540
+ if (clearLabels.length) {
541
+ await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels, triage.account ?? "");
542
+ }
543
+ }
505
544
  if (plan.closeIssue) {
506
545
  const closedPrs = [];
507
546
  for (const pr of input.relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
@@ -522,8 +561,18 @@ async function finishWithResult(input) {
522
561
  if (prUrl)
523
562
  await writeJson(join(input.outputDir, "pr.json"), { url: prUrl });
524
563
  }
525
- if (plan.clearLabels) {
526
- await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, triage.automation.clear, triage.account ?? "");
564
+ if (input.previousMarker && prUrl) {
565
+ await persistProcessedMarker({
566
+ account: triage.account ?? "",
567
+ comments: input.relationship.comments,
568
+ exec: input.input.exec,
569
+ issue: input.issue,
570
+ marker: input.previousMarker,
571
+ outputDir: input.outputDir,
572
+ pr: pullRequestNumberFromUrl(prUrl),
573
+ processed: input.processed ?? input.previousMarker.processed,
574
+ repository: input.input.repository,
575
+ });
527
576
  }
528
577
  }
529
578
  const report = [
@@ -589,7 +638,7 @@ async function createImplementationPr(input) {
589
638
  client: input.input.client,
590
639
  model: creator.model,
591
640
  options: creator.options,
592
- parse: parseEditOutput,
641
+ parse: parseTriageCreatePrOutput,
593
642
  permission: creator.permission,
594
643
  prompt,
595
644
  repairAttempts: 3,
@@ -650,6 +699,26 @@ export async function runTriage(input) {
650
699
  if (relationship.previousMarker) {
651
700
  if (!relationship.mentionReplies.length) {
652
701
  const result = finalResultFromMarker(relationship.previousMarker);
702
+ const plan = previousAutomationPlan({
703
+ issue,
704
+ marker: relationship.previousMarker,
705
+ relationship,
706
+ result,
707
+ triage,
708
+ });
709
+ if (plan) {
710
+ return finishWithResult({
711
+ context,
712
+ input,
713
+ issue,
714
+ outputDir,
715
+ plan,
716
+ previousMarker: relationship.previousMarker,
717
+ processed,
718
+ relationship,
719
+ result,
720
+ });
721
+ }
653
722
  const report = `Magi triage skipped #${issue.number} because no eligible mention replies were found for reconsideration.`;
654
723
  await writeFile(join(outputDir, "report.md"), `${report}\n`);
655
724
  return { issue: issue.number, outputDir, report, result };
@@ -746,6 +815,10 @@ export async function runTriage(input) {
746
815
  outputDir,
747
816
  repository: input.repository,
748
817
  });
818
+ const clearLabels = existingClearLabels(issue, triage.automation.clear);
819
+ if (clearLabels.length) {
820
+ await removeIssueLabels(input.exec, input.repository, issue.number, clearLabels, triage.account);
821
+ }
749
822
  const closedPrs = [];
750
823
  for (const pr of relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
751
824
  await closePullRequest(input.exec, input.repository, pr.number, triage.account);
@@ -754,7 +827,6 @@ export async function runTriage(input) {
754
827
  if (closedPrs.length)
755
828
  await writeJson(join(outputDir, "closed-prs.json"), closedPrs);
756
829
  await closeIssue(input.exec, input.repository, issue.number, triage.account);
757
- await removeIssueLabels(input.exec, input.repository, issue.number, triage.automation.clear, triage.account);
758
830
  }
759
831
  const report = `Magi triage closed #${issue.number} because a related PR was merged.`;
760
832
  await writeFile(join(outputDir, "report.md"), `${report}\n`);
@@ -305,7 +305,7 @@ export function parseCiClassificationOutput(text) {
305
305
  }),
306
306
  };
307
307
  }
308
- export function parseEditOutput(text) {
308
+ function parseEditOutputWithOptions(text, options) {
309
309
  const data = extractJson(text);
310
310
  if (!data || typeof data !== "object")
311
311
  throw new Error("edit output must be an object");
@@ -323,7 +323,7 @@ export function parseEditOutput(text) {
323
323
  commentId: requireNumber(value.commentId, `responses[${index}].commentId`),
324
324
  };
325
325
  });
326
- if (!responses.length)
326
+ if (options.requireResponses && !responses.length)
327
327
  throw new Error("responses must not be empty");
328
328
  if (data.mode === "EDITED") {
329
329
  if (!filesTouched.length)
@@ -351,3 +351,9 @@ export function parseEditOutput(text) {
351
351
  responses,
352
352
  };
353
353
  }
354
+ export function parseEditOutput(text) {
355
+ return parseEditOutputWithOptions(text, { requireResponses: true });
356
+ }
357
+ export function parseTriageCreatePrOutput(text) {
358
+ return parseEditOutputWithOptions(text, { requireResponses: false });
359
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260520163717",
3
+ "version": "0.0.0-dev-20260520163847",
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>",