opencode-magi 0.0.0-dev-20260520173258 → 0.0.0-dev-20260520180659

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.
@@ -164,6 +164,13 @@ function editorFailureText(input) {
164
164
  const repairs = repairAttemptsText(input.repairAttempts);
165
165
  return `**Editor** failed editing ${input.pr}${repairs}: ${input.error}`;
166
166
  }
167
+ function triageCreatorFailureText(input) {
168
+ const repairs = repairAttemptsText(input.repairAttempts);
169
+ return `**Triage creator** failed creating an implementation PR for ${input.issue}${repairs}: ${input.error}`;
170
+ }
171
+ function triageDecisionNotification(input) {
172
+ return `Triage decided ${input.issue}: ${input.result}. Planned action: ${input.action}.`;
173
+ }
167
174
  function repairAttemptsText(attempts) {
168
175
  if (!attempts)
169
176
  return "";
@@ -536,6 +543,14 @@ export class MagiRunManager {
536
543
  ])),
537
544
  runId,
538
545
  status: "preparing",
546
+ triageCreator: input.repository.agents.triageCreator
547
+ ? {
548
+ account: input.repository.agents.triageCreator.account,
549
+ repairAttempts: 0,
550
+ status: "pending",
551
+ toolCalls: 0,
552
+ }
553
+ : undefined,
539
554
  updatedAt: createdAt,
540
555
  };
541
556
  this.active.set(runId, state);
@@ -626,6 +641,17 @@ export class MagiRunManager {
626
641
  .abort?.({ path: { id: state.editor.sessionId } })
627
642
  .catch(() => undefined);
628
643
  }
644
+ if (state.triageCreator?.status === "pending" ||
645
+ state.triageCreator?.status === "running" ||
646
+ state.triageCreator?.status === "repairing" ||
647
+ state.triageCreator?.status === "blocked") {
648
+ state.triageCreator.status = "cancelled";
649
+ }
650
+ if (state.triageCreator?.sessionId) {
651
+ await this.input.client.session
652
+ .abort?.({ path: { id: state.triageCreator.sessionId } })
653
+ .catch(() => undefined);
654
+ }
629
655
  for (const reviewer of Object.values(state.reviewers)) {
630
656
  if (reviewer.status === "pending" ||
631
657
  reviewer.status === "running" ||
@@ -919,7 +945,7 @@ export class MagiRunManager {
919
945
  if (!existing) {
920
946
  await this.notify(state, questionWaitText({
921
947
  agent: mapping.agent,
922
- pr: prMarkdownLink(state),
948
+ pr: runLabel(state),
923
949
  question,
924
950
  }), { reply: true });
925
951
  }
@@ -938,7 +964,7 @@ export class MagiRunManager {
938
964
  agent.error = `Permission ${permission?.permission ?? "request"} is waiting for approval.`;
939
965
  markUpdated(true);
940
966
  dirty = true;
941
- await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${prMarkdownLink(state)}: ${agent.error}`, { reply: true });
967
+ await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${runLabel(state)}: ${agent.error}`, { reply: true });
942
968
  }
943
969
  }
944
970
  if (input.event.type === "question.asked") {
@@ -961,7 +987,7 @@ export class MagiRunManager {
961
987
  dirty = true;
962
988
  await this.notify(state, questionWaitText({
963
989
  agent: mapping.agent,
964
- pr: prMarkdownLink(state),
990
+ pr: runLabel(state),
965
991
  question,
966
992
  }), { reply: true });
967
993
  }
@@ -1031,6 +1057,9 @@ export class MagiRunManager {
1031
1057
  const editorLine = state.editor
1032
1058
  ? this.formatAgentLine("editor", state.editor, options)
1033
1059
  : undefined;
1060
+ const triageCreatorLine = state.triageCreator
1061
+ ? this.formatAgentLine("triageCreator", state.triageCreator, options)
1062
+ : undefined;
1034
1063
  const reviewerLines = Object.entries(state.reviewers).map(([key, reviewer]) => {
1035
1064
  return this.formatAgentLine(key, reviewer, options);
1036
1065
  });
@@ -1038,6 +1067,7 @@ export class MagiRunManager {
1038
1067
  const lines = [
1039
1068
  options.verbose ? `Run: ${state.runId}` : undefined,
1040
1069
  state.pr == null ? undefined : `PR: #${state.pr}`,
1070
+ state.issue == null ? undefined : `Issue: #${state.issue}`,
1041
1071
  `Command: ${state.command}`,
1042
1072
  state.dryRun ? "Dry run: true" : undefined,
1043
1073
  `Status: ${state.status}`,
@@ -1058,6 +1088,7 @@ export class MagiRunManager {
1058
1088
  ? `Report: ${state.reportPath}`
1059
1089
  : undefined,
1060
1090
  editorLine,
1091
+ triageCreatorLine,
1061
1092
  ...classifierLines,
1062
1093
  ...reviewerLines,
1063
1094
  ];
@@ -1084,6 +1115,7 @@ export class MagiRunManager {
1084
1115
  collectSessionIds(state) {
1085
1116
  const ids = [
1086
1117
  state.editor?.sessionId,
1118
+ state.triageCreator?.sessionId,
1087
1119
  ...Object.values(state.reviewers).map((reviewer) => reviewer.sessionId),
1088
1120
  ...Object.values(state.ciClassifiers ?? {}).map((classifier) => classifier.sessionId),
1089
1121
  ...Object.values(state.sessionIds ?? {}),
@@ -1126,13 +1158,22 @@ export class MagiRunManager {
1126
1158
  agentState(state, key) {
1127
1159
  if (key.startsWith("ci:"))
1128
1160
  return state.ciClassifiers?.[key.slice(3)];
1129
- return key === "editor" ? state.editor : state.reviewers[key];
1161
+ if (key === "editor")
1162
+ return state.editor;
1163
+ if (key === "triageCreator")
1164
+ return state.triageCreator;
1165
+ return state.reviewers[key];
1130
1166
  }
1131
1167
  agentEntries(state) {
1132
1168
  return [
1133
1169
  ...(state.editor
1134
1170
  ? [["editor", state.editor]]
1135
1171
  : []),
1172
+ ...(state.triageCreator
1173
+ ? [
1174
+ ["triageCreator", state.triageCreator],
1175
+ ]
1176
+ : []),
1136
1177
  ...Object.entries(state.ciClassifiers ?? {}).map(([key, value]) => [`ci:${key}`, value]),
1137
1178
  ...Object.entries(state.reviewers),
1138
1179
  ];
@@ -1152,10 +1193,10 @@ export class MagiRunManager {
1152
1193
  if (!matches.length) {
1153
1194
  return key
1154
1195
  ? `No pending ${kind} request found for ${key}.`
1155
- : `No pending ${kind} request found for ${prMarkdownLink(state)}.`;
1196
+ : `No pending ${kind} request found for ${runLabel(state)}.`;
1156
1197
  }
1157
1198
  if (matches.length > 1) {
1158
- return `Multiple pending ${kind} requests found for ${prMarkdownLink(state)}. Specify agent or requestId.`;
1199
+ return `Multiple pending ${kind} requests found for ${runLabel(state)}. Specify agent or requestId.`;
1159
1200
  }
1160
1201
  return { key: matches[0][0], state: matches[0][1] };
1161
1202
  }
@@ -1266,6 +1307,7 @@ export class MagiRunManager {
1266
1307
  dryRun: input.dryRun,
1267
1308
  exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
1268
1309
  issue: input.issue,
1310
+ onProgress: (progress) => this.applyTriageProgress(input.runId, progress),
1269
1311
  repository: input.repository,
1270
1312
  runId: input.runId,
1271
1313
  signal: input.signal,
@@ -1284,6 +1326,9 @@ export class MagiRunManager {
1284
1326
  if (agent.status === "pending")
1285
1327
  agent.status = "completed";
1286
1328
  }
1329
+ if (completed.triageCreator?.status === "pending") {
1330
+ completed.triageCreator.status = "skipped";
1331
+ }
1287
1332
  await this.persist(completed);
1288
1333
  await this.notify(completed, [
1289
1334
  `Finished triage for ${issueMarkdownLink(completed)}.`,
@@ -1293,6 +1338,189 @@ export class MagiRunManager {
1293
1338
  this.active.delete(input.runId);
1294
1339
  this.controllers.delete(input.runId);
1295
1340
  }
1341
+ async applyTriageProgress(runId, progress) {
1342
+ const state = this.active.get(runId);
1343
+ if (!state)
1344
+ return;
1345
+ const issue = issueMarkdownLink(state);
1346
+ const creatorState = () => state.triageCreator ??
1347
+ (state.triageCreator = {
1348
+ account: "triageCreator",
1349
+ repairAttempts: 0,
1350
+ status: "pending",
1351
+ toolCalls: 0,
1352
+ });
1353
+ state.updatedAt = now();
1354
+ if (progress.type === "phase") {
1355
+ state.phase = progress.phase;
1356
+ state.status = "running";
1357
+ }
1358
+ if (progress.type === "decision") {
1359
+ state.phase = `decision: ${progress.result.disposition}`;
1360
+ state.verdict = JSON.stringify(progress.result);
1361
+ }
1362
+ if (progress.type === "comment_posting") {
1363
+ state.phase = "posting triage comment";
1364
+ state.status = "posting";
1365
+ }
1366
+ if (progress.type === "comment_posted") {
1367
+ state.status = "running";
1368
+ }
1369
+ if (progress.type === "pr_creation_started") {
1370
+ state.phase = "creating implementation PR";
1371
+ state.status = "running";
1372
+ }
1373
+ if (progress.type === "worktree_created") {
1374
+ state.worktreePath = progress.worktreePath;
1375
+ state.worktreeBranch = progress.branch;
1376
+ }
1377
+ if (progress.type === "triage_agent_started") {
1378
+ const reviewer = state.reviewers[progress.reviewer];
1379
+ if (reviewer)
1380
+ reviewer.status = "running";
1381
+ }
1382
+ if (progress.type === "triage_agent_session") {
1383
+ const reviewer = state.reviewers[progress.reviewer];
1384
+ if (reviewer) {
1385
+ if (progress.options)
1386
+ this.input.setSessionOptions?.(progress.sessionId, progress.options);
1387
+ reviewer.sessionId = progress.sessionId;
1388
+ reviewer.status = "running";
1389
+ reviewer.lastUpdate = now();
1390
+ this.sessionToRun.set(progress.sessionId, {
1391
+ agent: progress.reviewer,
1392
+ runId,
1393
+ });
1394
+ }
1395
+ }
1396
+ if (progress.type === "triage_agent_repair") {
1397
+ const reviewer = state.reviewers[progress.reviewer];
1398
+ if (reviewer) {
1399
+ reviewer.status = "repairing";
1400
+ reviewer.repairAttempts += 1;
1401
+ reviewer.lastUpdate = now();
1402
+ }
1403
+ }
1404
+ if (progress.type === "triage_agent_response") {
1405
+ const reviewer = state.reviewers[progress.reviewer];
1406
+ if (reviewer) {
1407
+ reviewer.sessionId = progress.sessionId;
1408
+ reviewer.lastUpdate = now();
1409
+ }
1410
+ }
1411
+ if (progress.type === "triage_agent_completed") {
1412
+ const reviewer = state.reviewers[progress.reviewer];
1413
+ if (reviewer) {
1414
+ reviewer.sessionId = progress.sessionId;
1415
+ reviewer.status = "completed";
1416
+ reviewer.verdict = progress.vote;
1417
+ reviewer.rawPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.raw.txt`);
1418
+ reviewer.parsedPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.json`);
1419
+ reviewer.lastUpdate = now();
1420
+ }
1421
+ }
1422
+ if (progress.type === "triage_agent_failed") {
1423
+ const reviewer = state.reviewers[progress.reviewer];
1424
+ if (reviewer) {
1425
+ reviewer.status = "failed";
1426
+ reviewer.error = redactSecrets(progress.error);
1427
+ reviewer.lastUpdate = now();
1428
+ }
1429
+ }
1430
+ if (progress.type === "triage_creator_started") {
1431
+ creatorState().status = "running";
1432
+ }
1433
+ if (progress.type === "triage_creator_session") {
1434
+ const creator = creatorState();
1435
+ if (progress.options)
1436
+ this.input.setSessionOptions?.(progress.sessionId, progress.options);
1437
+ creator.sessionId = progress.sessionId;
1438
+ creator.status = "running";
1439
+ creator.lastUpdate = now();
1440
+ this.sessionToRun.set(progress.sessionId, {
1441
+ agent: "triageCreator",
1442
+ runId,
1443
+ });
1444
+ }
1445
+ if (progress.type === "triage_creator_repair") {
1446
+ const creator = creatorState();
1447
+ creator.status = "repairing";
1448
+ creator.repairAttempts += 1;
1449
+ creator.lastUpdate = now();
1450
+ }
1451
+ if (progress.type === "triage_creator_response") {
1452
+ const creator = creatorState();
1453
+ creator.sessionId = progress.sessionId;
1454
+ creator.lastUpdate = now();
1455
+ }
1456
+ if (progress.type === "triage_creator_completed") {
1457
+ const creator = creatorState();
1458
+ creator.sessionId = progress.sessionId;
1459
+ creator.status = "completed";
1460
+ creator.parsedPath = join(state.outputDir, "create-pr.json");
1461
+ creator.lastUpdate = now();
1462
+ }
1463
+ if (progress.type === "triage_creator_failed") {
1464
+ const creator = creatorState();
1465
+ creator.status = "failed";
1466
+ creator.error = redactSecrets(progress.error);
1467
+ creator.lastUpdate = now();
1468
+ }
1469
+ await this.persist(state);
1470
+ if (progress.type === "phase") {
1471
+ await this.notify(state, `Triage phase for ${issue}: ${progress.phase}.`);
1472
+ }
1473
+ if (progress.type === "decision") {
1474
+ await this.notify(state, triageDecisionNotification({
1475
+ action: progress.action,
1476
+ issue,
1477
+ result: JSON.stringify(progress.result),
1478
+ }));
1479
+ }
1480
+ if (progress.type === "triage_agent_started") {
1481
+ await this.notify(state, `**Triage agent ${progress.reviewer}** started ${progress.phase} for ${issue}.`);
1482
+ }
1483
+ if (progress.type === "triage_agent_repair") {
1484
+ await this.notify(state, `**Triage agent ${progress.reviewer}** started JSON regeneration for ${issue}.`);
1485
+ }
1486
+ if (progress.type === "triage_agent_completed") {
1487
+ await this.notify(state, `**Triage agent ${progress.reviewer}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
1488
+ }
1489
+ if (progress.type === "triage_agent_failed") {
1490
+ await this.notify(state, `**Triage agent ${progress.reviewer}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
1491
+ }
1492
+ if (progress.type === "comment_posting") {
1493
+ await this.notify(state, `Posting triage comment for ${issue}.`);
1494
+ }
1495
+ if (progress.type === "comment_posted") {
1496
+ await this.notify(state, `Posted triage comment for ${issue}: ${progress.url}`);
1497
+ }
1498
+ if (progress.type === "pr_creation_started") {
1499
+ await this.notify(state, `Started implementation PR creation for ${issue}.`);
1500
+ }
1501
+ if (progress.type === "worktree_created") {
1502
+ await this.notify(state, `Worktree is ready for ${issue}.`);
1503
+ }
1504
+ if (progress.type === "triage_creator_started") {
1505
+ await this.notify(state, `**Triage creator** started creating an implementation PR for ${issue}.`);
1506
+ }
1507
+ if (progress.type === "triage_creator_repair") {
1508
+ await this.notify(state, `**Triage creator** started JSON regeneration for ${issue}.`);
1509
+ }
1510
+ if (progress.type === "triage_creator_completed") {
1511
+ await this.notify(state, `**Triage creator** completed implementation changes for ${issue}.`);
1512
+ }
1513
+ if (progress.type === "triage_creator_failed") {
1514
+ await this.notify(state, triageCreatorFailureText({
1515
+ error: redactSecrets(progress.error),
1516
+ issue,
1517
+ repairAttempts: state.triageCreator?.repairAttempts ?? 0,
1518
+ }));
1519
+ }
1520
+ if (progress.type === "pr_created") {
1521
+ await this.notify(state, `Created implementation PR for ${issue}: ${progress.url}`);
1522
+ }
1523
+ }
1296
1524
  async applyReviewProgress(runId, progress) {
1297
1525
  const state = this.active.get(runId);
1298
1526
  if (!state)
@@ -6,7 +6,7 @@ import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, create
6
6
  import { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
7
7
  import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
8
8
  import { aggregateStringMajority, majorityThreshold } from "./majority";
9
- import { runModelText, runModelWithRepair } from "./model";
9
+ import { runModelText, 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"];
@@ -98,6 +98,35 @@ function issueContext(input) {
98
98
  async function writeJson(path, value) {
99
99
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
100
100
  }
101
+ async function emitProgress(input, progress) {
102
+ await input.onProgress?.(progress);
103
+ }
104
+ async function emitTriageModelProgress(input) {
105
+ if (input.progress.type === "session_created") {
106
+ await emitProgress(input.run, {
107
+ options: input.progress.options,
108
+ phase: input.phase,
109
+ reviewer: input.reviewer,
110
+ sessionId: input.progress.sessionId,
111
+ type: "triage_agent_session",
112
+ });
113
+ }
114
+ if (input.progress.type === "repair") {
115
+ await emitProgress(input.run, {
116
+ phase: input.phase,
117
+ reviewer: input.reviewer,
118
+ type: "triage_agent_repair",
119
+ });
120
+ }
121
+ if (input.progress.type === "response") {
122
+ await emitProgress(input.run, {
123
+ phase: input.phase,
124
+ reviewer: input.reviewer,
125
+ sessionId: input.progress.sessionId,
126
+ type: "triage_agent_response",
127
+ });
128
+ }
129
+ }
101
130
  async function runVote(input) {
102
131
  const prompt = await input.prompt({
103
132
  context: input.context,
@@ -106,17 +135,47 @@ async function runVote(input) {
106
135
  repository: input.repository,
107
136
  reviewer: input.agent,
108
137
  });
109
- const result = await runModelWithRepair({
110
- client: input.client,
111
- model: input.agent.model,
112
- options: input.agent.options,
113
- parse: input.parse,
114
- permission: input.agent.permission,
115
- prompt,
116
- repairAttempts: 3,
117
- schemaName: input.schemaName,
118
- signal: input.signal,
119
- title: `Magi triage ${input.schemaName} #${input.issue} (${input.agent.key})`,
138
+ await emitProgress(input.run, {
139
+ phase: input.phase,
140
+ reviewer: input.agent.key,
141
+ type: "triage_agent_started",
142
+ });
143
+ let result;
144
+ try {
145
+ result = await runModelWithRepair({
146
+ client: input.client,
147
+ model: input.agent.model,
148
+ onProgress: (progress) => emitTriageModelProgress({
149
+ phase: input.phase,
150
+ progress,
151
+ reviewer: input.agent.key,
152
+ run: input.run,
153
+ }),
154
+ options: input.agent.options,
155
+ parse: input.parse,
156
+ permission: input.agent.permission,
157
+ prompt,
158
+ repairAttempts: 3,
159
+ schemaName: input.schemaName,
160
+ signal: input.signal,
161
+ title: `Magi triage ${input.schemaName} #${input.issue} (${input.agent.key})`,
162
+ });
163
+ }
164
+ catch (error) {
165
+ await emitProgress(input.run, {
166
+ error: error instanceof Error ? error.message : String(error),
167
+ phase: input.phase,
168
+ reviewer: input.agent.key,
169
+ type: "triage_agent_failed",
170
+ });
171
+ throw error;
172
+ }
173
+ await emitProgress(input.run, {
174
+ phase: input.phase,
175
+ reviewer: input.agent.key,
176
+ sessionId: result.sessionId,
177
+ type: "triage_agent_completed",
178
+ vote: result.value.vote,
120
179
  });
121
180
  return {
122
181
  ...result.value,
@@ -155,6 +214,7 @@ async function runDuplicateVote(input) {
155
214
  const agents = input.input.repository.agents.triage;
156
215
  if (!agents?.length)
157
216
  throw new Error("triage.agents is required");
217
+ await emitProgress(input.input, { phase: "duplicate", type: "phase" });
158
218
  const outputs = await Promise.all(agents.map((agent) => runVote({
159
219
  agent,
160
220
  client: input.input.client,
@@ -162,8 +222,10 @@ async function runDuplicateVote(input) {
162
222
  directory: input.input.directory,
163
223
  issue: input.input.issue,
164
224
  parse: parseTriageDuplicateOutput,
225
+ phase: "duplicate",
165
226
  prompt: composeTriageDuplicatePrompt,
166
227
  repository: input.input.repository,
228
+ run: input.input,
167
229
  schemaName: "triage duplicate",
168
230
  signal: input.input.signal,
169
231
  })));
@@ -194,6 +256,7 @@ async function runPhaseVote(input) {
194
256
  const agents = input.input.repository.agents.triage;
195
257
  if (!agents?.length)
196
258
  throw new Error("triage.agents is required");
259
+ await emitProgress(input.input, { phase: input.phase, type: "phase" });
197
260
  const outputs = await Promise.all(agents.map((agent) => runVote({
198
261
  agent,
199
262
  client: input.input.client,
@@ -201,8 +264,10 @@ async function runPhaseVote(input) {
201
264
  directory: input.input.directory,
202
265
  issue: input.input.issue,
203
266
  parse: input.parse,
267
+ phase: input.phase,
204
268
  prompt: input.prompt,
205
269
  repository: input.input.repository,
270
+ run: input.input,
206
271
  schemaName: input.schemaName,
207
272
  signal: input.input.signal,
208
273
  })));
@@ -544,6 +609,11 @@ async function finishWithResult(input) {
544
609
  if (!triage)
545
610
  throw new Error("triage configuration is required");
546
611
  const plan = input.plan ?? actionPlan({ result: input.result, triage });
612
+ await emitProgress(input.input, {
613
+ action: plan.action,
614
+ result: input.result,
615
+ type: "decision",
616
+ });
547
617
  await runActionPrompt({
548
618
  context: input.context,
549
619
  input: input.input,
@@ -565,7 +635,8 @@ async function finishWithResult(input) {
565
635
  : undefined;
566
636
  if (!input.input.dryRun) {
567
637
  if (comment) {
568
- await postMarkedIssueComment({
638
+ await emitProgress(input.input, { type: "comment_posting" });
639
+ const posted = await postMarkedIssueComment({
569
640
  account: triage.account ?? "",
570
641
  body: comment,
571
642
  exec: input.input.exec,
@@ -573,6 +644,10 @@ async function finishWithResult(input) {
573
644
  outputDir: input.outputDir,
574
645
  repository: input.input.repository,
575
646
  });
647
+ await emitProgress(input.input, {
648
+ type: "comment_posted",
649
+ url: posted.url,
650
+ });
576
651
  }
577
652
  if (plan.clearLabels) {
578
653
  const clearLabels = existingClearLabels(input.issue, triage.automation.clear);
@@ -597,8 +672,10 @@ async function finishWithResult(input) {
597
672
  issue: input.issue,
598
673
  outputDir: input.outputDir,
599
674
  });
600
- if (prUrl)
675
+ if (prUrl) {
601
676
  await writeJson(join(input.outputDir, "pr.json"), { url: prUrl });
677
+ await emitProgress(input.input, { type: "pr_created", url: prUrl });
678
+ }
602
679
  }
603
680
  if (input.previousMarker && prUrl) {
604
681
  await persistProcessedMarker({
@@ -659,48 +736,86 @@ async function createImplementationPr(input) {
659
736
  const triage = input.input.repository.triage;
660
737
  if (!triage?.account)
661
738
  throw new Error("triage.account is required");
662
- await assignIssue(input.input.exec, input.input.repository, input.issue.number, triage.account);
663
- const branch = `magi/issue-${input.issue.number}-${Date.now().toString(36)}`;
664
- const worktreePath = join(worktreeBaseDir(input.input.directory, input.input.config, "issue"), `issue-${input.issue.number}`);
665
- await mkdir(dirname(worktreePath), { recursive: true });
666
- await input.input.exec(`git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`);
739
+ await emitProgress(input.input, { type: "pr_creation_started" });
740
+ await emitProgress(input.input, { type: "triage_creator_started" });
667
741
  try {
668
- await configureGitIdentity(input.input.exec, worktreePath, creator.author);
669
- const prompt = await composeTriageCreatePrPrompt({
670
- context: input.context,
671
- directory: input.input.directory,
672
- issue: input.issue.number,
673
- repository: input.input.repository,
742
+ await assignIssue(input.input.exec, input.input.repository, input.issue.number, triage.account);
743
+ const branch = `magi/issue-${input.issue.number}-${Date.now().toString(36)}`;
744
+ const worktreePath = join(worktreeBaseDir(input.input.directory, input.input.config, "issue"), `issue-${input.issue.number}`);
745
+ await mkdir(dirname(worktreePath), { recursive: true });
746
+ await input.input.exec(`git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`);
747
+ await emitProgress(input.input, {
748
+ branch,
749
+ type: "worktree_created",
674
750
  worktreePath,
675
751
  });
676
- const result = await runModelWithRepair({
677
- client: input.input.client,
678
- model: creator.model,
679
- options: creator.options,
680
- parse: parseTriageCreatePrOutput,
681
- permission: creator.permission,
682
- prompt,
683
- repairAttempts: 3,
684
- schemaName: "edit",
685
- signal: input.input.signal,
686
- title: `Magi triage create PR #${input.issue.number}`,
687
- });
688
- await writeJson(join(input.outputDir, "create-pr.json"), result.value);
689
- if (result.value.mode !== "EDITED")
690
- return undefined;
691
- await pushHead(input.input.exec, input.input.repository, worktreePath, creator.account, {
692
- owner: input.input.repository.github.owner,
693
- ref: branch,
694
- repo: input.input.repository.github.repo,
695
- });
696
- return createPullRequest(input.input.exec, input.input.repository, creator.account, {
697
- body: `Closes #${input.issue.number}`,
698
- head: branch,
699
- title: `fix: address issue #${input.issue.number}`,
700
- });
752
+ try {
753
+ await configureGitIdentity(input.input.exec, worktreePath, creator.author);
754
+ const prompt = await composeTriageCreatePrPrompt({
755
+ context: input.context,
756
+ directory: input.input.directory,
757
+ issue: input.issue.number,
758
+ repository: input.input.repository,
759
+ worktreePath,
760
+ });
761
+ const result = await runModelWithRepair({
762
+ client: input.input.client,
763
+ model: creator.model,
764
+ onProgress: async (progress) => {
765
+ if (progress.type === "session_created") {
766
+ await emitProgress(input.input, {
767
+ options: progress.options,
768
+ sessionId: progress.sessionId,
769
+ type: "triage_creator_session",
770
+ });
771
+ }
772
+ if (progress.type === "repair") {
773
+ await emitProgress(input.input, { type: "triage_creator_repair" });
774
+ }
775
+ if (progress.type === "response") {
776
+ await emitProgress(input.input, {
777
+ sessionId: progress.sessionId,
778
+ type: "triage_creator_response",
779
+ });
780
+ }
781
+ },
782
+ options: creator.options,
783
+ parse: parseTriageCreatePrOutput,
784
+ permission: creator.permission,
785
+ prompt,
786
+ repairAttempts: 3,
787
+ schemaName: "edit",
788
+ signal: input.input.signal,
789
+ title: `Magi triage create PR #${input.issue.number}`,
790
+ });
791
+ await emitProgress(input.input, {
792
+ sessionId: result.sessionId,
793
+ type: "triage_creator_completed",
794
+ });
795
+ await writeJson(join(input.outputDir, "create-pr.json"), result.value);
796
+ if (result.value.mode !== "EDITED")
797
+ return undefined;
798
+ await pushHead(input.input.exec, input.input.repository, worktreePath, creator.account, {
799
+ owner: input.input.repository.github.owner,
800
+ ref: branch,
801
+ repo: input.input.repository.github.repo,
802
+ });
803
+ return createPullRequest(input.input.exec, input.input.repository, creator.account, {
804
+ body: `Closes #${input.issue.number}`,
805
+ head: branch,
806
+ title: `fix: address issue #${input.issue.number}`,
807
+ });
808
+ }
809
+ finally {
810
+ await removeWorktree(input.input.exec, worktreePath).catch(() => undefined);
811
+ }
701
812
  }
702
- finally {
703
- await removeWorktree(input.input.exec, worktreePath).catch(() => undefined);
813
+ catch (error) {
814
+ await emitProgress(input.input, {
815
+ error: error instanceof Error ? error.message : String(error),
816
+ type: "triage_creator_failed",
817
+ });
818
+ throw error;
704
819
  }
705
820
  }
706
821
  export async function runTriage(input) {
@@ -718,7 +833,12 @@ export async function runTriage(input) {
718
833
  runId,
719
834
  });
720
835
  await mkdir(outputDir, { recursive: true });
836
+ await emitProgress(input, { phase: "fetching issue", type: "phase" });
721
837
  const issue = await fetchIssue(input.exec, input.repository, input.issue);
838
+ await emitProgress(input, {
839
+ phase: "scanning issue relationships",
840
+ type: "phase",
841
+ });
722
842
  const relationship = await relationshipScan(input, issue);
723
843
  const block = safetyBlocked(input, issue, Boolean(relationship.previousMarker));
724
844
  await writeJson(join(outputDir, "issue.json"), issue);
@@ -738,6 +858,7 @@ export async function runTriage(input) {
738
858
  }
739
859
  let context = issueContext({ issue, relationship });
740
860
  await writeFile(join(outputDir, "context.md"), `${context}\n`);
861
+ await emitProgress(input, { phase: "triaging", type: "phase" });
741
862
  let processed = relationship.previousMarker?.processed ?? [];
742
863
  let result;
743
864
  if (relationship.previousMarker) {
@@ -848,6 +969,11 @@ export async function runTriage(input) {
848
969
  createPr: false,
849
970
  postComment: true,
850
971
  };
972
+ await emitProgress(input, {
973
+ action: plan.action,
974
+ result: relatedPrDecision,
975
+ type: "decision",
976
+ });
851
977
  await runActionPrompt({
852
978
  context,
853
979
  input,
@@ -865,7 +991,8 @@ export async function runTriage(input) {
865
991
  result: relatedPrDecision,
866
992
  });
867
993
  if (!input.dryRun) {
868
- await postMarkedIssueComment({
994
+ await emitProgress(input, { type: "comment_posting" });
995
+ const posted = await postMarkedIssueComment({
869
996
  account: triage.account,
870
997
  body,
871
998
  exec: input.exec,
@@ -873,6 +1000,7 @@ export async function runTriage(input) {
873
1000
  outputDir,
874
1001
  repository: input.repository,
875
1002
  });
1003
+ await emitProgress(input, { type: "comment_posted", url: posted.url });
876
1004
  const clearLabels = existingClearLabels(issue, triage.automation.clear);
877
1005
  if (clearLabels.length) {
878
1006
  await removeIssueLabels(input.exec, input.repository, issue.number, clearLabels, triage.account);
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "bash": {
3
+ "bun *": "allow",
4
+ "bunx *": "allow",
5
+ "corepack *": "allow",
3
6
  "git add*": "allow",
4
- "git commit*": "allow"
7
+ "git commit*": "allow",
8
+ "npm *": "allow",
9
+ "npx *": "allow",
10
+ "pnpm *": "allow",
11
+ "yarn *": "allow"
5
12
  },
6
13
  "edit": "allow"
7
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260520173258",
3
+ "version": "0.0.0-dev-20260520180659",
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>",