opencode-magi 0.4.0 → 0.6.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.
@@ -1,12 +1,12 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { issueRunOutputDir } from "../config/output";
4
- import { worktreeBaseDir } from "../config/worktree";
4
+ import { issueRunWorktreeDir } 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
- import { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
7
- import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
6
+ import { composeTriageAcceptancePrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
7
+ import { parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
8
8
  import { aggregateStringMajority, majorityThreshold } from "./majority";
9
- import { runModelText, runModelWithRepair, } from "./model";
9
+ import { runModelWithRepair, } from "./model";
10
10
  const MARKER_PREFIX = "opencode-magi:triage";
11
11
  const BINARY_VOTES = ["ASK", "NO", "YES"];
12
12
  const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
@@ -106,24 +106,24 @@ async function emitTriageModelProgress(input) {
106
106
  await emitProgress(input.run, {
107
107
  options: input.progress.options,
108
108
  phase: input.phase,
109
- reviewer: input.reviewer,
110
109
  sessionId: input.progress.sessionId,
111
110
  type: "triage_agent_session",
111
+ voter: input.voter,
112
112
  });
113
113
  }
114
114
  if (input.progress.type === "repair") {
115
115
  await emitProgress(input.run, {
116
116
  phase: input.phase,
117
- reviewer: input.reviewer,
118
117
  type: "triage_agent_repair",
118
+ voter: input.voter,
119
119
  });
120
120
  }
121
121
  if (input.progress.type === "response") {
122
122
  await emitProgress(input.run, {
123
123
  phase: input.phase,
124
- reviewer: input.reviewer,
125
124
  sessionId: input.progress.sessionId,
126
125
  type: "triage_agent_response",
126
+ voter: input.voter,
127
127
  });
128
128
  }
129
129
  }
@@ -133,12 +133,12 @@ async function runVote(input) {
133
133
  directory: input.directory,
134
134
  issue: input.issue,
135
135
  repository: input.repository,
136
- reviewer: input.agent,
136
+ voter: input.agent,
137
137
  });
138
138
  await emitProgress(input.run, {
139
139
  phase: input.phase,
140
- reviewer: input.agent.key,
141
140
  type: "triage_agent_started",
141
+ voter: input.agent.key,
142
142
  });
143
143
  let result;
144
144
  try {
@@ -148,10 +148,11 @@ async function runVote(input) {
148
148
  onProgress: (progress) => emitTriageModelProgress({
149
149
  phase: input.phase,
150
150
  progress,
151
- reviewer: input.agent.key,
152
151
  run: input.run,
152
+ voter: input.agent.key,
153
153
  }),
154
154
  options: input.agent.options,
155
+ parentSessionId: input.run.parentSessionId,
155
156
  parse: input.parse,
156
157
  permission: input.agent.permission,
157
158
  prompt,
@@ -165,16 +166,16 @@ async function runVote(input) {
165
166
  await emitProgress(input.run, {
166
167
  error: error instanceof Error ? error.message : String(error),
167
168
  phase: input.phase,
168
- reviewer: input.agent.key,
169
169
  type: "triage_agent_failed",
170
+ voter: input.agent.key,
170
171
  });
171
172
  throw error;
172
173
  }
173
174
  await emitProgress(input.run, {
174
175
  phase: input.phase,
175
- reviewer: input.agent.key,
176
176
  sessionId: result.sessionId,
177
177
  type: "triage_agent_completed",
178
+ voter: input.agent.key,
178
179
  vote: result.value.vote,
179
180
  });
180
181
  return {
@@ -182,13 +183,15 @@ async function runVote(input) {
182
183
  promptText: prompt,
183
184
  raw: result.raw,
184
185
  sessionId: result.sessionId,
186
+ voter: input.agent.key,
185
187
  };
186
188
  }
187
189
  async function writeVoteArtifacts(input) {
188
- const base = join(input.outputDir, `${input.reviewer}.${input.phase}`);
190
+ const base = join(input.outputDir, `${input.voter}.${input.phase}`);
189
191
  await writeFile(`${base}.prompt.txt`, `${input.output.promptText}\n`);
190
192
  await writeFile(`${base}.raw.txt`, `${input.output.raw}\n`);
191
193
  await writeJson(`${base}.json`, {
194
+ body: input.output.body,
192
195
  reason: input.output.reason,
193
196
  vote: input.output.vote,
194
197
  });
@@ -230,14 +233,14 @@ async function runDuplicateVote(input) {
230
233
  signal: input.input.signal,
231
234
  })));
232
235
  const majority = aggregateStringMajority(outputs.map((output, index) => ({
233
- reviewer: agents[index].key,
236
+ voter: agents[index].key,
234
237
  vote: output.vote,
235
238
  })), DUPLICATE_VOTES);
236
239
  await Promise.all(outputs.map((output, index) => writeVoteArtifacts({
237
240
  output,
238
241
  outputDir: input.outputDir,
239
242
  phase: "duplicate",
240
- reviewer: agents[index].key,
243
+ voter: agents[index].key,
241
244
  })));
242
245
  await Promise.all(outputs.map((output, index) => writeJson(join(input.outputDir, `${agents[index].key}.duplicate.json`), {
243
246
  duplicateOf: output.duplicateOf,
@@ -272,17 +275,17 @@ async function runPhaseVote(input) {
272
275
  signal: input.input.signal,
273
276
  })));
274
277
  const majority = aggregateStringMajority(outputs.map((output, index) => ({
275
- reviewer: agents[index].key,
278
+ voter: agents[index].key,
276
279
  vote: output.vote,
277
280
  })), input.votes);
278
281
  await Promise.all(outputs.map((output, index) => writeVoteArtifacts({
279
282
  output,
280
283
  outputDir: input.outputDir,
281
284
  phase: input.phase,
282
- reviewer: agents[index].key,
285
+ voter: agents[index].key,
283
286
  })));
284
287
  await writeJson(join(input.outputDir, `${input.phase}-majority.json`), majority);
285
- return majority.vote;
288
+ return { outputs, vote: majority.vote };
286
289
  }
287
290
  async function relationshipScan(input, issue) {
288
291
  const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
@@ -290,17 +293,20 @@ async function relationshipScan(input, issue) {
290
293
  fetchRelatedPullRequests(input.exec, input.repository, input.issue),
291
294
  searchDuplicateIssues(input.exec, input.repository, issue),
292
295
  ]);
296
+ const triageAccounts = new Set((input.repository.agents.triage ?? []).map((agent) => agent.account));
293
297
  const markers = comments
294
- .filter((comment) => comment.author === input.repository.triage?.account)
298
+ .filter((comment) => triageAccounts.has(comment.author))
295
299
  .map((comment) => {
296
300
  const parsed = parseTriageMarker(comment.body);
297
- return parsed ? { ...parsed, commentId: comment.id } : undefined;
301
+ return parsed
302
+ ? { ...parsed, account: comment.author, commentId: comment.id }
303
+ : undefined;
298
304
  })
299
305
  .filter(Boolean);
300
306
  const previousMarker = markers.at(-1);
301
307
  const mentionReplies = previousMarker
302
308
  ? eligibleMentionReplies({
303
- account: input.repository.triage?.account ?? "",
309
+ account: previousMarker.account ?? "",
304
310
  comments,
305
311
  marker: previousMarker,
306
312
  processed: previousMarker.processed,
@@ -450,63 +456,38 @@ function previousAutomationPlan(input) {
450
456
  postComment: false,
451
457
  };
452
458
  }
453
- async function runActionPrompt(input) {
454
- const agent = input.input.repository.agents.triage?.[0];
455
- if (!agent)
456
- throw new Error("triage.agents is required");
457
- const context = JSON.stringify({
458
- allowedActions: input.plan.allowedActions,
459
- deterministicPlan: input.plan,
460
- result: input.result,
461
- triageContext: input.context,
462
- }, null, 2);
463
- const prompt = await composeTriageActionPrompt({
464
- context,
465
- directory: input.input.directory,
466
- issue: input.input.issue,
467
- repository: input.input.repository,
468
- reviewer: agent,
469
- });
470
- const result = await runModelWithRepair({
471
- client: input.input.client,
472
- model: agent.model,
473
- options: agent.options,
474
- parse: parseTriageActionOutput,
475
- permission: agent.permission,
476
- prompt,
477
- repairAttempts: 3,
478
- schemaName: "triage action",
479
- signal: input.input.signal,
480
- title: `Magi triage action #${input.input.issue}`,
481
- });
482
- await writeJson(join(input.outputDir, "action.json"), {
483
- model: result.value,
484
- plan: input.plan,
485
- });
486
- return result.value;
487
- }
488
459
  async function classifyMentionReplies(input) {
489
- const agent = input.input.repository.agents.triage?.[0];
490
- if (!agent)
491
- throw new Error("triage.agents is required");
460
+ const agent = triageReporter(input.input.repository, input.input.issue);
492
461
  const prompt = await composeTriageCommentClassificationPrompt({
493
462
  context: JSON.stringify({ context: input.context, mentionReplies: input.replies }, null, 2),
494
463
  directory: input.input.directory,
495
464
  issue: input.input.issue,
496
465
  repository: input.input.repository,
497
- reviewer: agent,
466
+ voter: agent,
498
467
  });
499
468
  const result = await runModelWithRepair({
500
469
  client: input.input.client,
501
470
  model: agent.model,
471
+ onProgress: async (progress) => {
472
+ if (progress.type !== "session_created")
473
+ return;
474
+ await emitProgress(input.input, {
475
+ agent: agent.key,
476
+ key: `triage:comment-classification:${agent.key}:${progress.sessionId}`,
477
+ options: progress.options,
478
+ sessionId: progress.sessionId,
479
+ type: "triage_session",
480
+ });
481
+ },
502
482
  options: agent.options,
483
+ parentSessionId: input.input.parentSessionId,
503
484
  parse: parseTriageCommentClassificationOutput,
504
485
  permission: agent.permission,
505
486
  prompt,
506
487
  repairAttempts: 3,
507
488
  schemaName: "triage comment classification",
508
489
  signal: input.input.signal,
509
- title: `Magi triage comment classification #${input.input.issue}`,
490
+ title: `Magi triage comment classification #${input.input.issue} (${agent.key})`,
510
491
  });
511
492
  await writeJson(join(input.outputDir, "comment-classification.json"), result.value);
512
493
  return result.value;
@@ -523,54 +504,40 @@ async function runReconsiderationVote(input) {
523
504
  votes: BINARY_VOTES,
524
505
  });
525
506
  }
526
- async function composeResultComment(input) {
527
- const agents = input.input.repository.agents.triage;
528
- if (!agents?.length)
507
+ function triageReporter(repository, issue) {
508
+ const agents = repository.agents.triage ?? [];
509
+ if (!agents.length)
529
510
  throw new Error("triage.agents is required");
530
- if (input.result.disposition === "ask" &&
531
- input.result.askReason === "category_unclear") {
532
- const language = input.input.repository.language?.toLowerCase() ?? "";
533
- const body = language.includes("ja") || language.includes("japanese")
534
- ? `@${input.issue.author} 現在の説明だけでは、何をすべきか判断できません。\n\n期待する動作、実際の動作、必要な理由、関連する例・ログ・スクリーンショットなどを追記してください。`
535
- : `@${input.issue.author} I can't determine what should be done from the current description.\n\nPlease add more detail, such as the expected behavior, the actual behavior, the reason this is needed, or any relevant examples, logs, or screenshots.`;
536
- const comment = `${body}\n\n${marker({
537
- action: input.action,
538
- checkpoint: "pending",
539
- decision: input.result,
540
- issue: input.issue.number,
541
- processed: input.processed,
542
- })}`;
543
- await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
544
- return comment;
545
- }
546
- const prompt = await (input.result.disposition === "ask"
547
- ? composeTriageQuestionPrompt
548
- : composeTriageCommentPrompt)({
549
- author: input.issue.author,
550
- context: input.context,
551
- directory: input.input.directory,
552
- issue: input.issue.number,
553
- repository: input.input.repository,
554
- });
555
- const comment = (await runModelText({
556
- allowEmpty: false,
557
- client: input.input.client,
558
- model: agents[0].model,
559
- options: agents[0].options,
560
- permission: agents[0].permission,
561
- prompt,
562
- signal: input.input.signal,
563
- title: `Magi triage comment #${input.issue.number}`,
564
- })).raw +
565
- `\n\n${marker({
566
- action: input.action,
567
- checkpoint: "pending",
568
- decision: input.result,
569
- issue: input.issue.number,
570
- processed: input.processed,
571
- })}`;
572
- await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
573
- return comment;
511
+ const configured = repository.triage?.reporter;
512
+ const reporter = configured
513
+ ? agents.find((agent) => agent.key === configured)
514
+ : agents[Math.abs(issue) % agents.length];
515
+ if (!reporter)
516
+ throw new Error(`Unknown triage reporter: ${configured}`);
517
+ return reporter;
518
+ }
519
+ function decisionCommentBody(input) {
520
+ const reason = input.reason?.trim();
521
+ const result = JSON.stringify(input.result);
522
+ return reason
523
+ ? `Magi triage decision: ${result}\n\nReason: ${reason}`
524
+ : `Magi triage decision: ${result}\n\nAction: ${input.action}`;
525
+ }
526
+ function agentForKey(repository, key) {
527
+ const agent = repository.agents.triage?.find((item) => item.key === key);
528
+ if (!agent)
529
+ throw new Error(`Unknown triage agent: ${key}`);
530
+ return agent;
531
+ }
532
+ function askOutputs(outputs) {
533
+ return (outputs ?? []).filter((output) => output.vote === "ASK");
534
+ }
535
+ function chooseDecisionReason(input) {
536
+ return (input.outputs?.find((output) => output.voter === input.reporter.key &&
537
+ output.vote === input.vote &&
538
+ output.reason)?.reason ??
539
+ input.outputs?.find((output) => output.vote === input.vote)?.reason ??
540
+ input.outputs?.find((output) => output.voter === input.reporter.key)?.reason);
574
541
  }
575
542
  async function postMarkedIssueComment(input) {
576
543
  const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
@@ -578,9 +545,20 @@ async function postMarkedIssueComment(input) {
578
545
  const updated = body === input.body
579
546
  ? posted
580
547
  : await updateIssueComment(input.exec, input.repository, posted.id, input.account, body);
581
- await writeJson(join(input.outputDir, "posted.json"), updated);
548
+ await writeJson(join(input.outputDir, `posted-${updated.id}.json`), {
549
+ account: input.account,
550
+ ...updated,
551
+ });
582
552
  return updated;
583
553
  }
554
+ async function postPlainIssueComment(input) {
555
+ const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
556
+ await writeJson(join(input.outputDir, `posted-${posted.id}.json`), {
557
+ account: input.account,
558
+ ...posted,
559
+ });
560
+ return posted;
561
+ }
584
562
  async function persistProcessedMarker(input) {
585
563
  if (!input.marker.commentId)
586
564
  return;
@@ -604,6 +582,49 @@ async function persistProcessedMarker(input) {
604
582
  updated,
605
583
  });
606
584
  }
585
+ async function postAskComments(input) {
586
+ const urls = [];
587
+ for (const output of askOutputs(input.outputs)) {
588
+ const agent = agentForKey(input.repository, output.voter);
589
+ const body = input.mark
590
+ ? `${output.body}\n\n${marker({
591
+ action: input.action,
592
+ checkpoint: "pending",
593
+ decision: input.result,
594
+ issue: input.issue.number,
595
+ processed: input.processed,
596
+ })}`
597
+ : output.body;
598
+ if (!body?.trim())
599
+ continue;
600
+ await writeFile(join(input.outputDir, `${agent.key}.ask-comment.md`), `${body}\n`);
601
+ if (input.dryRun) {
602
+ urls.push(`dry-run:would-comment:${agent.key}`);
603
+ continue;
604
+ }
605
+ await emitProgress(input.run, { type: "comment_posting" });
606
+ const posted = input.mark
607
+ ? await postMarkedIssueComment({
608
+ account: agent.account,
609
+ body,
610
+ exec: input.exec,
611
+ issue: input.issue.number,
612
+ outputDir: input.outputDir,
613
+ repository: input.repository,
614
+ })
615
+ : await postPlainIssueComment({
616
+ account: agent.account,
617
+ body,
618
+ exec: input.exec,
619
+ issue: input.issue.number,
620
+ outputDir: input.outputDir,
621
+ repository: input.repository,
622
+ });
623
+ urls.push(posted.url);
624
+ await emitProgress(input.run, { type: "comment_posted", url: posted.url });
625
+ }
626
+ return urls;
627
+ }
607
628
  async function finishWithResult(input) {
608
629
  const triage = input.input.repository.triage;
609
630
  if (!triage)
@@ -614,30 +635,44 @@ async function finishWithResult(input) {
614
635
  result: input.result,
615
636
  type: "decision",
616
637
  });
617
- await runActionPrompt({
618
- context: input.context,
619
- input: input.input,
620
- outputDir: input.outputDir,
621
- plan,
622
- result: input.result,
623
- });
624
638
  let prUrl;
625
- const comment = plan.postComment
626
- ? await composeResultComment({
639
+ const reporter = triageReporter(input.input.repository, input.issue.number);
640
+ const comment = plan.postComment && input.result.disposition !== "ask"
641
+ ? `${decisionCommentBody({
642
+ action: plan.action,
643
+ reason: input.commentReason,
644
+ result: input.result,
645
+ })}\n\n${marker({
627
646
  action: plan.action,
628
- context: `Result: ${decisionText(input.result)}\nAction: ${plan.action}\n\n${input.context}`,
629
- input: input.input,
647
+ checkpoint: "pending",
648
+ decision: input.result,
649
+ issue: input.issue.number,
650
+ processed: input.processed,
651
+ })}`
652
+ : undefined;
653
+ if (comment) {
654
+ await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
655
+ }
656
+ if (input.result.disposition === "ask" && input.askOutputs) {
657
+ await postAskComments({
658
+ action: plan.action,
659
+ dryRun: input.input.dryRun,
660
+ exec: input.input.exec,
630
661
  issue: input.issue,
662
+ mark: input.markAskComments ?? false,
663
+ outputs: input.askOutputs,
631
664
  outputDir: input.outputDir,
632
665
  processed: input.processed,
666
+ repository: input.input.repository,
633
667
  result: input.result,
634
- })
635
- : undefined;
668
+ run: input.input,
669
+ });
670
+ }
636
671
  if (!input.input.dryRun) {
637
672
  if (comment) {
638
673
  await emitProgress(input.input, { type: "comment_posting" });
639
674
  const posted = await postMarkedIssueComment({
640
- account: triage.account ?? "",
675
+ account: reporter.account,
641
676
  body: comment,
642
677
  exec: input.input.exec,
643
678
  issue: input.issue.number,
@@ -652,18 +687,18 @@ async function finishWithResult(input) {
652
687
  if (plan.clearLabels) {
653
688
  const clearLabels = existingClearLabels(input.issue, triage.automation.clear);
654
689
  if (clearLabels.length) {
655
- await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels, triage.account ?? "");
690
+ await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels, reporter.account);
656
691
  }
657
692
  }
658
693
  if (plan.closeIssue) {
659
694
  const closedPrs = [];
660
695
  for (const pr of input.relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
661
- await closePullRequest(input.input.exec, input.input.repository, pr.number, triage.account ?? "");
696
+ await closePullRequest(input.input.exec, input.input.repository, pr.number, reporter.account);
662
697
  closedPrs.push(pr.number);
663
698
  }
664
699
  if (closedPrs.length)
665
700
  await writeJson(join(input.outputDir, "closed-prs.json"), closedPrs);
666
- await closeIssue(input.input.exec, input.input.repository, input.issue.number, triage.account ?? "");
701
+ await closeIssue(input.input.exec, input.input.repository, input.issue.number, reporter.account);
667
702
  }
668
703
  if (plan.createPr) {
669
704
  prUrl = await createImplementationPr({
@@ -671,6 +706,7 @@ async function finishWithResult(input) {
671
706
  input: input.input,
672
707
  issue: input.issue,
673
708
  outputDir: input.outputDir,
709
+ runId: input.runId,
674
710
  });
675
711
  if (prUrl) {
676
712
  await writeJson(join(input.outputDir, "pr.json"), { url: prUrl });
@@ -679,7 +715,7 @@ async function finishWithResult(input) {
679
715
  }
680
716
  if (input.previousMarker && prUrl) {
681
717
  await persistProcessedMarker({
682
- account: triage.account ?? "",
718
+ account: input.previousMarker.account ?? reporter.account,
683
719
  comments: input.relationship.comments,
684
720
  exec: input.input.exec,
685
721
  issue: input.issue,
@@ -734,15 +770,17 @@ async function createImplementationPr(input) {
734
770
  const creator = input.input.repository.agents.triageCreator;
735
771
  if (!creator)
736
772
  return undefined;
737
- const triage = input.input.repository.triage;
738
- if (!triage?.account)
739
- throw new Error("triage.account is required");
740
773
  await emitProgress(input.input, { type: "pr_creation_started" });
741
774
  await emitProgress(input.input, { type: "triage_creator_started" });
742
775
  try {
743
- await assignIssue(input.input.exec, input.input.repository, input.issue.number, triage.account);
776
+ await assignIssue(input.input.exec, input.input.repository, input.issue.number, creator.account);
744
777
  const branch = `magi/issue-${input.issue.number}-${Date.now().toString(36)}`;
745
- const worktreePath = join(worktreeBaseDir(input.input.directory, input.input.config, "issue"), `issue-${input.issue.number}`);
778
+ const worktreePath = issueRunWorktreeDir({
779
+ config: input.input.config,
780
+ directory: input.input.directory,
781
+ issue: input.issue.number,
782
+ runId: input.runId,
783
+ });
746
784
  await mkdir(dirname(worktreePath), { recursive: true });
747
785
  await input.input.exec(`git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`);
748
786
  await emitProgress(input.input, {
@@ -781,6 +819,7 @@ async function createImplementationPr(input) {
781
819
  }
782
820
  },
783
821
  options: creator.options,
822
+ parentSessionId: input.input.parentSessionId,
784
823
  parse: parseTriageCreatePrOutput,
785
824
  permission: creator.permission,
786
825
  prompt,
@@ -824,8 +863,8 @@ async function createImplementationPr(input) {
824
863
  }
825
864
  export async function runTriage(input) {
826
865
  const triage = input.repository.triage;
827
- if (!triage?.account)
828
- throw new Error("triage.account is required");
866
+ if (!triage)
867
+ throw new Error("triage configuration is required");
829
868
  const agents = input.repository.agents.triage;
830
869
  if (!agents?.length)
831
870
  throw new Error("triage.agents is required");
@@ -865,6 +904,9 @@ export async function runTriage(input) {
865
904
  await emitProgress(input, { phase: "triaging", type: "phase" });
866
905
  let processed = relationship.previousMarker?.processed ?? [];
867
906
  let result;
907
+ let askCommentOutputs;
908
+ let commentReason;
909
+ let markAskComments = false;
868
910
  if (relationship.previousMarker) {
869
911
  if (!relationship.mentionReplies.length) {
870
912
  const result = finalResultFromMarker(relationship.previousMarker);
@@ -886,6 +928,7 @@ export async function runTriage(input) {
886
928
  processed,
887
929
  relationship,
888
930
  result,
931
+ runId,
889
932
  });
890
933
  }
891
934
  const report = `Magi triage skipped #${issue.number} because no eligible mention replies were found for reconsideration.`;
@@ -909,7 +952,7 @@ export async function runTriage(input) {
909
952
  if (!triggeringComments.length) {
910
953
  if (!input.dryRun) {
911
954
  await persistProcessedMarker({
912
- account: triage.account,
955
+ account: relationship.previousMarker.account ?? "",
913
956
  comments: relationship.comments,
914
957
  exec: input.exec,
915
958
  issue,
@@ -934,21 +977,38 @@ export async function runTriage(input) {
934
977
  },
935
978
  });
936
979
  await writeFile(join(outputDir, "context.md"), `${context}\n`);
937
- const vote = await runReconsiderationVote({ context, input, outputDir });
938
980
  const previous = finalResultFromMarker(relationship.previousMarker);
939
- result =
940
- vote === "YES"
941
- ? { category: previous.category, disposition: "accepted" }
942
- : vote === "NO"
943
- ? { category: previous.category, disposition: "rejected" }
944
- : {
945
- askReason: "acceptance_unclear",
946
- category: previous.category,
947
- disposition: "ask",
948
- };
981
+ if (previous.disposition !== "ask" ||
982
+ previous.askReason !== "acceptance_unclear") {
983
+ const reconsideration = await runReconsiderationVote({
984
+ context,
985
+ input,
986
+ outputDir,
987
+ });
988
+ const reporter = triageReporter(input.repository, issue.number);
989
+ commentReason = chooseDecisionReason({
990
+ outputs: reconsideration.outputs,
991
+ reporter,
992
+ vote: reconsideration.vote ?? "ASK",
993
+ });
994
+ result =
995
+ reconsideration.vote === "YES"
996
+ ? { category: previous.category, disposition: "accepted" }
997
+ : reconsideration.vote === "NO"
998
+ ? { category: previous.category, disposition: "rejected" }
999
+ : {
1000
+ askReason: "acceptance_unclear",
1001
+ category: previous.category,
1002
+ disposition: "ask",
1003
+ };
1004
+ if (result.disposition === "ask") {
1005
+ askCommentOutputs = askOutputs(reconsideration.outputs);
1006
+ markAskComments = true;
1007
+ }
1008
+ }
949
1009
  }
950
1010
  if (!result && relationship.relatedPullRequests.length) {
951
- const vote = await runPhaseVote({
1011
+ const existingPr = await runPhaseVote({
952
1012
  context,
953
1013
  input,
954
1014
  outputDir,
@@ -958,7 +1018,7 @@ export async function runTriage(input) {
958
1018
  schemaName: "triage existing PR",
959
1019
  votes: EXISTING_PR_VOTES,
960
1020
  });
961
- if (vote === "RELATED_PR_HANDLES_ISSUE") {
1021
+ if (existingPr.vote === "RELATED_PR_HANDLES_ISSUE") {
962
1022
  const merged = relationship.relatedPullRequests.some((pr) => pr.state === "MERGED");
963
1023
  if (merged && triage.automation.close) {
964
1024
  const relatedPrDecision = {
@@ -973,59 +1033,22 @@ export async function runTriage(input) {
973
1033
  createPr: false,
974
1034
  postComment: true,
975
1035
  };
976
- await emitProgress(input, {
977
- action: plan.action,
978
- result: relatedPrDecision,
979
- type: "decision",
980
- });
981
- await runActionPrompt({
1036
+ return finishWithResult({
1037
+ commentReason: chooseDecisionReason({
1038
+ outputs: existingPr.outputs,
1039
+ reporter: triageReporter(input.repository, issue.number),
1040
+ vote: "RELATED_PR_HANDLES_ISSUE",
1041
+ }),
982
1042
  context,
983
1043
  input,
984
- outputDir,
985
- plan,
986
- result: relatedPrDecision,
987
- });
988
- const body = await composeResultComment({
989
- action: "CLOSE",
990
- context: `Result: ${decisionText(relatedPrDecision)}\nAction: CLOSE\n\n${context}`,
991
- input,
992
1044
  issue,
993
1045
  outputDir,
1046
+ plan,
994
1047
  processed,
1048
+ relationship,
995
1049
  result: relatedPrDecision,
1050
+ runId,
996
1051
  });
997
- if (!input.dryRun) {
998
- await emitProgress(input, { type: "comment_posting" });
999
- const posted = await postMarkedIssueComment({
1000
- account: triage.account,
1001
- body,
1002
- exec: input.exec,
1003
- issue: issue.number,
1004
- outputDir,
1005
- repository: input.repository,
1006
- });
1007
- await emitProgress(input, { type: "comment_posted", url: posted.url });
1008
- const clearLabels = existingClearLabels(issue, triage.automation.clear);
1009
- if (clearLabels.length) {
1010
- await removeIssueLabels(input.exec, input.repository, issue.number, clearLabels, triage.account);
1011
- }
1012
- const closedPrs = [];
1013
- for (const pr of relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
1014
- await closePullRequest(input.exec, input.repository, pr.number, triage.account);
1015
- closedPrs.push(pr.number);
1016
- }
1017
- if (closedPrs.length)
1018
- await writeJson(join(outputDir, "closed-prs.json"), closedPrs);
1019
- await closeIssue(input.exec, input.repository, issue.number, triage.account);
1020
- }
1021
- const report = `Magi triage closed #${issue.number} because a related PR was merged.`;
1022
- await writeFile(join(outputDir, "report.md"), `${report}\n`);
1023
- return {
1024
- issue: issue.number,
1025
- outputDir,
1026
- report,
1027
- result: relatedPrDecision,
1028
- };
1029
1052
  }
1030
1053
  return finishWithResult({
1031
1054
  context,
@@ -1035,6 +1058,7 @@ export async function runTriage(input) {
1035
1058
  processed,
1036
1059
  relationship,
1037
1060
  result: { category: null, disposition: "clear_only" },
1061
+ runId,
1038
1062
  });
1039
1063
  }
1040
1064
  }
@@ -1047,6 +1071,7 @@ export async function runTriage(input) {
1047
1071
  });
1048
1072
  if (duplicate) {
1049
1073
  context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
1074
+ commentReason = duplicate.reason;
1050
1075
  result = { category: null, disposition: "duplicate" };
1051
1076
  }
1052
1077
  }
@@ -1056,8 +1081,9 @@ export async function runTriage(input) {
1056
1081
  category: resolvedCategory,
1057
1082
  source: resolvedCategory ? "config" : "vote",
1058
1083
  });
1059
- const category = resolvedCategory ??
1060
- (await runPhaseVote({
1084
+ const categoryVote = resolvedCategory
1085
+ ? undefined
1086
+ : await runPhaseVote({
1061
1087
  context,
1062
1088
  input,
1063
1089
  outputDir,
@@ -1066,14 +1092,16 @@ export async function runTriage(input) {
1066
1092
  prompt: composeTriageCategoryPrompt,
1067
1093
  schemaName: "triage category",
1068
1094
  votes: ["ASK", ...triage.categories.map((item) => item.id)],
1069
- })) ??
1070
- "ASK";
1095
+ });
1096
+ const category = resolvedCategory ?? categoryVote?.vote ?? "ASK";
1071
1097
  if (category === "ASK") {
1072
1098
  result = {
1073
1099
  askReason: "category_unclear",
1074
1100
  category: null,
1075
1101
  disposition: "ask",
1076
1102
  };
1103
+ askCommentOutputs = askOutputs(categoryVote?.outputs);
1104
+ markAskComments = false;
1077
1105
  }
1078
1106
  else {
1079
1107
  const categoryConfig = triage.categories.find((item) => item.id === category);
@@ -1081,7 +1109,7 @@ export async function runTriage(input) {
1081
1109
  category: categoryConfig,
1082
1110
  triageContext: context,
1083
1111
  }, null, 2);
1084
- const vote = await runPhaseVote({
1112
+ const acceptance = await runPhaseVote({
1085
1113
  context: voteContext,
1086
1114
  input,
1087
1115
  outputDir,
@@ -1091,22 +1119,35 @@ export async function runTriage(input) {
1091
1119
  schemaName: "triage acceptance",
1092
1120
  votes: BINARY_VOTES,
1093
1121
  });
1122
+ const reporter = triageReporter(input.repository, issue.number);
1123
+ commentReason = chooseDecisionReason({
1124
+ outputs: acceptance.outputs,
1125
+ reporter,
1126
+ vote: acceptance.vote ?? "ASK",
1127
+ });
1094
1128
  result =
1095
- vote === "YES"
1129
+ acceptance.vote === "YES"
1096
1130
  ? { category, disposition: "accepted" }
1097
- : vote === "NO"
1131
+ : acceptance.vote === "NO"
1098
1132
  ? { category, disposition: "rejected" }
1099
1133
  : {
1100
1134
  askReason: "acceptance_unclear",
1101
1135
  category,
1102
1136
  disposition: "ask",
1103
1137
  };
1138
+ if (result.disposition === "ask") {
1139
+ askCommentOutputs = askOutputs(acceptance.outputs);
1140
+ markAskComments = true;
1141
+ }
1104
1142
  }
1105
1143
  }
1106
1144
  return finishWithResult({
1145
+ askOutputs: askCommentOutputs,
1146
+ commentReason,
1107
1147
  context,
1108
1148
  input,
1109
1149
  issue,
1150
+ markAskComments,
1110
1151
  outputDir,
1111
1152
  processed,
1112
1153
  relationship,
@@ -1115,5 +1156,6 @@ export async function runTriage(input) {
1115
1156
  category: null,
1116
1157
  disposition: "ask",
1117
1158
  },
1159
+ runId,
1118
1160
  });
1119
1161
  }