opencode-magi 0.2.0 → 0.3.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 (35) 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 +290 -3
  6. package/dist/config/worktree.js +8 -2
  7. package/dist/github/commands.js +343 -15
  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 +73 -0
  12. package/dist/orchestrator/majority.js +14 -0
  13. package/dist/orchestrator/merge.js +16 -3
  14. package/dist/orchestrator/report.js +15 -1
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +49 -9
  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 +162 -1
  21. package/dist/prompts/contracts.js +119 -12
  22. package/dist/prompts/output.js +149 -14
  23. package/dist/prompts/templates/review/review.md +6 -0
  24. package/dist/prompts/templates/triage/acceptance.md +7 -0
  25. package/dist/prompts/templates/triage/action.md +5 -0
  26. package/dist/prompts/templates/triage/category.md +10 -0
  27. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  28. package/dist/prompts/templates/triage/comment.md +5 -0
  29. package/dist/prompts/templates/triage/create.md +7 -0
  30. package/dist/prompts/templates/triage/duplicate.md +7 -0
  31. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  32. package/dist/prompts/templates/triage/question.md +5 -0
  33. package/dist/prompts/templates/triage/reconsider.md +5 -0
  34. package/package.json +5 -2
  35. package/schema.json +127 -2
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, readdir, rm, rmdir, writeFile, } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { outputBaseDirs, prRunOutputDir } from "../config/output";
4
+ import { issueRunOutputDir, outputBaseDirs, prRunOutputDir, } from "../config/output";
5
5
  import { worktreeBaseDirs } from "../config/worktree";
6
6
  import { removeBranch, removeWorktree, } from "../github/commands";
7
7
  import { withGitHubApiRetry } from "../github/retry";
8
8
  import { runMerge, } from "./merge";
9
9
  import { runReview } from "./review";
10
+ import { runTriage } from "./triage";
10
11
  const EVENT_LAST_UPDATE_THROTTLE_MS = 5_000;
11
12
  const DEFAULT_CLEAR_OPTIONS = {
12
13
  branch: true,
@@ -20,12 +21,33 @@ function createRunId() {
20
21
  function now() {
21
22
  return new Date().toISOString();
22
23
  }
24
+ export function redactSecrets(value) {
25
+ return value
26
+ .replace(/\b(GH_TOKEN|GITHUB_TOKEN|GH_ENTERPRISE_TOKEN)=('[^']*'|"[^"]*"|\S+)/g, "$1=<redacted>")
27
+ .replace(/(password=)([^;'\s]+)/g, "$1<redacted>");
28
+ }
29
+ function errorMessage(error) {
30
+ return redactSecrets(error instanceof Error ? error.message : String(error));
31
+ }
23
32
  function isActiveStatus(status) {
24
33
  return (status === "blocked" ||
25
34
  status === "preparing" ||
26
35
  status === "running" ||
27
36
  status === "posting");
28
37
  }
38
+ function matchesNumberFilter(value, filter) {
39
+ if (filter == null)
40
+ return true;
41
+ return Array.isArray(filter)
42
+ ? value != null && filter.includes(value)
43
+ : value === filter;
44
+ }
45
+ function hasAllRequestedPrStates(states, pr) {
46
+ if (pr == null)
47
+ return true;
48
+ const prs = Array.isArray(pr) ? pr : [pr];
49
+ return prs.every((item) => states.some((state) => state.pr === item));
50
+ }
29
51
  function isWithinDirectory(directory, path) {
30
52
  const relation = relative(directory, path);
31
53
  return (relation === "" || (!relation.startsWith("..") && !isAbsolute(relation)));
@@ -67,13 +89,35 @@ function prUrl(repository, pr) {
67
89
  const host = repository.github.host || "github.com";
68
90
  return `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
69
91
  }
92
+ function pullRequestNumberFromUrl(url) {
93
+ const match = url.match(/(?:^|\/)pull\/(\d+)(?:[/?#].*)?$/);
94
+ if (!match)
95
+ return undefined;
96
+ const pr = Number.parseInt(match[1], 10);
97
+ return Number.isInteger(pr) && pr > 0 ? pr : undefined;
98
+ }
99
+ function issueUrl(repository, issue) {
100
+ const host = repository.github.host || "github.com";
101
+ return `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
102
+ }
70
103
  function prMarkdownLink(state) {
71
104
  if (state.pr == null)
72
105
  return state.runId;
73
106
  return state.prUrl ? `[#${state.pr}](${state.prUrl})` : `#${state.pr}`;
74
107
  }
108
+ function issueMarkdownLink(state) {
109
+ if (state.issue == null)
110
+ return state.runId;
111
+ return state.issueUrl
112
+ ? `[#${state.issue}](${state.issueUrl})`
113
+ : `#${state.issue}`;
114
+ }
75
115
  function runLabel(state) {
76
- return state.pr == null ? state.runId : prMarkdownLink(state);
116
+ if (state.pr != null)
117
+ return prMarkdownLink(state);
118
+ if (state.issue != null)
119
+ return issueMarkdownLink(state);
120
+ return state.runId;
77
121
  }
78
122
  function reviewerCompletionText(input) {
79
123
  const reviewer = `**Reviewer ${input.reviewer}**`;
@@ -127,6 +171,13 @@ function editorFailureText(input) {
127
171
  const repairs = repairAttemptsText(input.repairAttempts);
128
172
  return `**Editor** failed editing ${input.pr}${repairs}: ${input.error}`;
129
173
  }
174
+ function triageCreatorFailureText(input) {
175
+ const repairs = repairAttemptsText(input.repairAttempts);
176
+ return `**Triage creator** failed creating an implementation PR for ${input.issue}${repairs}: ${input.error}`;
177
+ }
178
+ function triageDecisionNotification(input) {
179
+ return `Triage decided ${input.issue}: ${input.result}. Planned action: ${input.action}.`;
180
+ }
130
181
  function repairAttemptsText(attempts) {
131
182
  if (!attempts)
132
183
  return "";
@@ -469,12 +520,70 @@ export class MagiRunManager {
469
520
  });
470
521
  return state;
471
522
  }
523
+ async startTriage(input) {
524
+ const runId = createRunId();
525
+ const outputDir = issueRunOutputDir({
526
+ config: input.config,
527
+ directory: this.input.directory,
528
+ issue: input.issue,
529
+ runId,
530
+ });
531
+ const createdAt = now();
532
+ const state = {
533
+ command: "triage",
534
+ createdAt,
535
+ dryRun: input.dryRun,
536
+ issue: input.issue,
537
+ issueUrl: issueUrl(input.repository, input.issue),
538
+ outputDir,
539
+ parentSessionId: input.parentSessionId,
540
+ phase: "queued",
541
+ repository: input.repository.alias,
542
+ reviewers: Object.fromEntries((input.repository.agents.triage ?? []).map((agent) => [
543
+ agent.key,
544
+ {
545
+ account: "",
546
+ repairAttempts: 0,
547
+ status: "pending",
548
+ toolCalls: 0,
549
+ },
550
+ ])),
551
+ runId,
552
+ status: "preparing",
553
+ triageCreator: input.repository.agents.triageCreator
554
+ ? {
555
+ account: input.repository.agents.triageCreator.account,
556
+ repairAttempts: 0,
557
+ status: "pending",
558
+ toolCalls: 0,
559
+ }
560
+ : undefined,
561
+ updatedAt: createdAt,
562
+ };
563
+ this.active.set(runId, state);
564
+ this.runPaths.set(runId, join(outputDir, "state.json"));
565
+ for (const dir of outputBaseDirs(this.input.directory, input.config))
566
+ this.outputDirs.add(dir);
567
+ await this.persist(state);
568
+ await this.notify(state, `Started Magi triage for ${issueMarkdownLink(state)}.`);
569
+ const controller = new AbortController();
570
+ this.controllers.set(runId, controller);
571
+ void this.executeTriage({
572
+ ...input,
573
+ runId,
574
+ signal: controller.signal,
575
+ }).catch(async (error) => {
576
+ await this.failRun(runId, error);
577
+ });
578
+ return state;
579
+ }
472
580
  async status(input = {}) {
473
581
  const timeoutMs = Math.min(input.timeoutMs ?? 60_000, 600_000);
474
582
  const startedAt = Date.now();
475
583
  while (input.block) {
476
584
  const states = await this.filteredStates(input);
477
585
  if (states.length &&
586
+ hasAllRequestedPrStates(states, input.pr) &&
478
587
  states.every((state) => !isActiveStatus(state.status)))
479
588
  return states;
480
589
  if (Date.now() - startedAt >= timeoutMs)
@@ -539,6 +648,17 @@ export class MagiRunManager {
539
648
  .abort?.({ path: { id: state.editor.sessionId } })
540
649
  .catch(() => undefined);
541
650
  }
651
+ if (state.triageCreator?.status === "pending" ||
652
+ state.triageCreator?.status === "running" ||
653
+ state.triageCreator?.status === "repairing" ||
654
+ state.triageCreator?.status === "blocked") {
655
+ state.triageCreator.status = "cancelled";
656
+ }
657
+ if (state.triageCreator?.sessionId) {
658
+ await this.input.client.session
659
+ .abort?.({ path: { id: state.triageCreator.sessionId } })
660
+ .catch(() => undefined);
661
+ }
542
662
  for (const reviewer of Object.values(state.reviewers)) {
543
663
  if (reviewer.status === "pending" ||
544
664
  reviewer.status === "running" ||
@@ -832,7 +952,7 @@ export class MagiRunManager {
832
952
  if (!existing) {
833
953
  await this.notify(state, questionWaitText({
834
954
  agent: mapping.agent,
835
- pr: prMarkdownLink(state),
955
+ pr: runLabel(state),
836
956
  question,
837
957
  }), { reply: true });
838
958
  }
@@ -851,7 +971,7 @@ export class MagiRunManager {
851
971
  agent.error = `Permission ${permission?.permission ?? "request"} is waiting for approval.`;
852
972
  markUpdated(true);
853
973
  dirty = true;
854
- await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${prMarkdownLink(state)}: ${agent.error}`, { reply: true });
974
+ await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${runLabel(state)}: ${agent.error}`, { reply: true });
855
975
  }
856
976
  }
857
977
  if (input.event.type === "question.asked") {
@@ -874,7 +994,7 @@ export class MagiRunManager {
874
994
  dirty = true;
875
995
  await this.notify(state, questionWaitText({
876
996
  agent: mapping.agent,
877
- pr: prMarkdownLink(state),
997
+ pr: runLabel(state),
878
998
  question,
879
999
  }), { reply: true });
880
1000
  }
@@ -927,7 +1047,7 @@ export class MagiRunManager {
927
1047
  }
928
1048
  if (input.event.type === "session.error") {
929
1049
  agent.status = "failed";
930
- agent.error = JSON.stringify(input.event.properties?.error ?? "session error");
1050
+ agent.error = redactSecrets(JSON.stringify(input.event.properties?.error ?? "session error"));
931
1051
  markUpdated(true);
932
1052
  dirty = true;
933
1053
  }
@@ -944,6 +1064,9 @@ export class MagiRunManager {
944
1064
  const editorLine = state.editor
945
1065
  ? this.formatAgentLine("editor", state.editor, options)
946
1066
  : undefined;
1067
+ const triageCreatorLine = state.triageCreator
1068
+ ? this.formatAgentLine("triageCreator", state.triageCreator, options)
1069
+ : undefined;
947
1070
  const reviewerLines = Object.entries(state.reviewers).map(([key, reviewer]) => {
948
1071
  return this.formatAgentLine(key, reviewer, options);
949
1072
  });
@@ -951,6 +1074,7 @@ export class MagiRunManager {
951
1074
  const lines = [
952
1075
  options.verbose ? `Run: ${state.runId}` : undefined,
953
1076
  state.pr == null ? undefined : `PR: #${state.pr}`,
1077
+ state.issue == null ? undefined : `Issue: #${state.issue}`,
954
1078
  `Command: ${state.command}`,
955
1079
  state.dryRun ? "Dry run: true" : undefined,
956
1080
  `Status: ${state.status}`,
@@ -971,6 +1095,7 @@ export class MagiRunManager {
971
1095
  ? `Report: ${state.reportPath}`
972
1096
  : undefined,
973
1097
  editorLine,
1098
+ triageCreatorLine,
974
1099
  ...classifierLines,
975
1100
  ...reviewerLines,
976
1101
  ];
@@ -997,6 +1122,7 @@ export class MagiRunManager {
997
1122
  collectSessionIds(state) {
998
1123
  const ids = [
999
1124
  state.editor?.sessionId,
1125
+ state.triageCreator?.sessionId,
1000
1126
  ...Object.values(state.reviewers).map((reviewer) => reviewer.sessionId),
1001
1127
  ...Object.values(state.ciClassifiers ?? {}).map((classifier) => classifier.sessionId),
1002
1128
  ...Object.values(state.sessionIds ?? {}),
@@ -1039,13 +1165,22 @@ export class MagiRunManager {
1039
1165
  agentState(state, key) {
1040
1166
  if (key.startsWith("ci:"))
1041
1167
  return state.ciClassifiers?.[key.slice(3)];
1042
- return key === "editor" ? state.editor : state.reviewers[key];
1168
+ if (key === "editor")
1169
+ return state.editor;
1170
+ if (key === "triageCreator")
1171
+ return state.triageCreator;
1172
+ return state.reviewers[key];
1043
1173
  }
1044
1174
  agentEntries(state) {
1045
1175
  return [
1046
1176
  ...(state.editor
1047
1177
  ? [["editor", state.editor]]
1048
1178
  : []),
1179
+ ...(state.triageCreator
1180
+ ? [
1181
+ ["triageCreator", state.triageCreator],
1182
+ ]
1183
+ : []),
1049
1184
  ...Object.entries(state.ciClassifiers ?? {}).map(([key, value]) => [`ci:${key}`, value]),
1050
1185
  ...Object.entries(state.reviewers),
1051
1186
  ];
@@ -1065,10 +1200,10 @@ export class MagiRunManager {
1065
1200
  if (!matches.length) {
1066
1201
  return key
1067
1202
  ? `No pending ${kind} request found for ${key}.`
1068
- : `No pending ${kind} request found for ${prMarkdownLink(state)}.`;
1203
+ : `No pending ${kind} request found for ${runLabel(state)}.`;
1069
1204
  }
1070
1205
  if (matches.length > 1) {
1071
- return `Multiple pending ${kind} requests found for ${prMarkdownLink(state)}. Specify agent or requestId.`;
1206
+ return `Multiple pending ${kind} requests found for ${runLabel(state)}. Specify agent or requestId.`;
1072
1207
  }
1073
1208
  return { key: matches[0][0], state: matches[0][1] };
1074
1209
  }
@@ -1077,6 +1212,7 @@ export class MagiRunManager {
1077
1212
  }
1078
1213
  async executeReview(input) {
1079
1214
  const result = await runReview({
1215
+ approvalPolicy: input.repository.merge.approvalPolicy,
1080
1216
  client: this.input.client,
1081
1217
  config: input.config,
1082
1218
  directory: this.input.directory,
@@ -1164,6 +1300,258 @@ export class MagiRunManager {
1164
1300
  this.active.delete(input.runId);
1165
1301
  this.controllers.delete(input.runId);
1166
1302
  }
1303
+ async executeTriage(input) {
1304
+ const state = this.active.get(input.runId);
1305
+ if (state) {
1306
+ state.status = "running";
1307
+ state.phase = "triaging";
1308
+ await this.persist(state);
1309
+ }
1310
+ const result = await runTriage({
1311
+ client: this.input.client,
1312
+ config: input.config,
1313
+ directory: this.input.directory,
1314
+ dryRun: input.dryRun,
1315
+ exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
1316
+ issue: input.issue,
1317
+ onProgress: (progress) => this.applyTriageProgress(input.runId, progress),
1318
+ repository: input.repository,
1319
+ runId: input.runId,
1320
+ signal: input.signal,
1321
+ });
1322
+ const completed = this.active.get(input.runId);
1323
+ if (!completed || completed.status === "cancelled")
1324
+ return;
1325
+ const triageResult = JSON.stringify(result.result);
1326
+ completed.status =
1327
+ result.result.disposition === "failed" ? "failed" : "completed";
1328
+ completed.phase = triageResult;
1329
+ completed.completedAt = now();
1330
+ completed.verdict = triageResult;
1331
+ completed.reportPath = join(completed.outputDir, "report.md");
1332
+ for (const agent of Object.values(completed.reviewers)) {
1333
+ if (agent.status === "pending")
1334
+ agent.status = "completed";
1335
+ }
1336
+ if (completed.triageCreator?.status === "pending") {
1337
+ completed.triageCreator.status = "skipped";
1338
+ }
1339
+ await this.persist(completed);
1340
+ await this.notify(completed, [
1341
+ `Finished triage for ${issueMarkdownLink(completed)}.`,
1342
+ "",
1343
+ result.report,
1344
+ ].join("\n"), { reply: true });
1345
+ const followUpPr = result.prUrl
1346
+ ? pullRequestNumberFromUrl(result.prUrl)
1347
+ : undefined;
1348
+ const triageAutomation = input.repository.triage?.automation;
1349
+ if (followUpPr != null && triageAutomation?.merge) {
1350
+ await this.startMerge({
1351
+ config: input.config,
1352
+ dryRun: input.dryRun,
1353
+ parentSessionId: input.parentSessionId,
1354
+ pr: followUpPr,
1355
+ repository: input.repository,
1356
+ signal: input.signal,
1357
+ });
1358
+ }
1359
+ else if (followUpPr != null && triageAutomation?.review) {
1360
+ await this.startReview({
1361
+ config: input.config,
1362
+ dryRun: input.dryRun,
1363
+ parentSessionId: input.parentSessionId,
1364
+ pr: followUpPr,
1365
+ repository: input.repository,
1366
+ signal: input.signal,
1367
+ });
1368
+ }
1369
+ this.active.delete(input.runId);
1370
+ this.controllers.delete(input.runId);
1371
+ }
1372
+ async applyTriageProgress(runId, progress) {
1373
+ const state = this.active.get(runId);
1374
+ if (!state)
1375
+ return;
1376
+ const issue = issueMarkdownLink(state);
1377
+ const creatorState = () => state.triageCreator ??
1378
+ (state.triageCreator = {
1379
+ account: "triageCreator",
1380
+ repairAttempts: 0,
1381
+ status: "pending",
1382
+ toolCalls: 0,
1383
+ });
1384
+ state.updatedAt = now();
1385
+ if (progress.type === "phase") {
1386
+ state.phase = progress.phase;
1387
+ state.status = "running";
1388
+ }
1389
+ if (progress.type === "decision") {
1390
+ state.phase = `decision: ${progress.result.disposition}`;
1391
+ state.verdict = JSON.stringify(progress.result);
1392
+ }
1393
+ if (progress.type === "comment_posting") {
1394
+ state.phase = "posting triage comment";
1395
+ state.status = "posting";
1396
+ }
1397
+ if (progress.type === "comment_posted") {
1398
+ state.status = "running";
1399
+ }
1400
+ if (progress.type === "pr_creation_started") {
1401
+ state.phase = "creating implementation PR";
1402
+ state.status = "running";
1403
+ }
1404
+ if (progress.type === "worktree_created") {
1405
+ state.worktreePath = progress.worktreePath;
1406
+ state.worktreeBranch = progress.branch;
1407
+ }
1408
+ if (progress.type === "triage_agent_started") {
1409
+ const reviewer = state.reviewers[progress.reviewer];
1410
+ if (reviewer)
1411
+ reviewer.status = "running";
1412
+ }
1413
+ if (progress.type === "triage_agent_session") {
1414
+ const reviewer = state.reviewers[progress.reviewer];
1415
+ if (reviewer) {
1416
+ if (progress.options)
1417
+ this.input.setSessionOptions?.(progress.sessionId, progress.options);
1418
+ reviewer.sessionId = progress.sessionId;
1419
+ reviewer.status = "running";
1420
+ reviewer.lastUpdate = now();
1421
+ this.sessionToRun.set(progress.sessionId, {
1422
+ agent: progress.reviewer,
1423
+ runId,
1424
+ });
1425
+ }
1426
+ }
1427
+ if (progress.type === "triage_agent_repair") {
1428
+ const reviewer = state.reviewers[progress.reviewer];
1429
+ if (reviewer) {
1430
+ reviewer.status = "repairing";
1431
+ reviewer.repairAttempts += 1;
1432
+ reviewer.lastUpdate = now();
1433
+ }
1434
+ }
1435
+ if (progress.type === "triage_agent_response") {
1436
+ const reviewer = state.reviewers[progress.reviewer];
1437
+ if (reviewer) {
1438
+ reviewer.sessionId = progress.sessionId;
1439
+ reviewer.lastUpdate = now();
1440
+ }
1441
+ }
1442
+ if (progress.type === "triage_agent_completed") {
1443
+ const reviewer = state.reviewers[progress.reviewer];
1444
+ if (reviewer) {
1445
+ reviewer.sessionId = progress.sessionId;
1446
+ reviewer.status = "completed";
1447
+ reviewer.verdict = progress.vote;
1448
+ reviewer.rawPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.raw.txt`);
1449
+ reviewer.parsedPath = join(state.outputDir, `${progress.reviewer}.${progress.phase}.json`);
1450
+ reviewer.lastUpdate = now();
1451
+ }
1452
+ }
1453
+ if (progress.type === "triage_agent_failed") {
1454
+ const reviewer = state.reviewers[progress.reviewer];
1455
+ if (reviewer) {
1456
+ reviewer.status = "failed";
1457
+ reviewer.error = redactSecrets(progress.error);
1458
+ reviewer.lastUpdate = now();
1459
+ }
1460
+ }
1461
+ if (progress.type === "triage_creator_started") {
1462
+ creatorState().status = "running";
1463
+ }
1464
+ if (progress.type === "triage_creator_session") {
1465
+ const creator = creatorState();
1466
+ if (progress.options)
1467
+ this.input.setSessionOptions?.(progress.sessionId, progress.options);
1468
+ creator.sessionId = progress.sessionId;
1469
+ creator.status = "running";
1470
+ creator.lastUpdate = now();
1471
+ this.sessionToRun.set(progress.sessionId, {
1472
+ agent: "triageCreator",
1473
+ runId,
1474
+ });
1475
+ }
1476
+ if (progress.type === "triage_creator_repair") {
1477
+ const creator = creatorState();
1478
+ creator.status = "repairing";
1479
+ creator.repairAttempts += 1;
1480
+ creator.lastUpdate = now();
1481
+ }
1482
+ if (progress.type === "triage_creator_response") {
1483
+ const creator = creatorState();
1484
+ creator.sessionId = progress.sessionId;
1485
+ creator.lastUpdate = now();
1486
+ }
1487
+ if (progress.type === "triage_creator_completed") {
1488
+ const creator = creatorState();
1489
+ creator.sessionId = progress.sessionId;
1490
+ creator.status = "completed";
1491
+ creator.parsedPath = join(state.outputDir, "create-pr.json");
1492
+ creator.lastUpdate = now();
1493
+ }
1494
+ if (progress.type === "triage_creator_failed") {
1495
+ const creator = creatorState();
1496
+ creator.status = "failed";
1497
+ creator.error = redactSecrets(progress.error);
1498
+ creator.lastUpdate = now();
1499
+ }
1500
+ await this.persist(state);
1501
+ if (progress.type === "phase") {
1502
+ await this.notify(state, `Triage phase for ${issue}: ${progress.phase}.`);
1503
+ }
1504
+ if (progress.type === "decision") {
1505
+ await this.notify(state, triageDecisionNotification({
1506
+ action: progress.action,
1507
+ issue,
1508
+ result: JSON.stringify(progress.result),
1509
+ }));
1510
+ }
1511
+ if (progress.type === "triage_agent_started") {
1512
+ await this.notify(state, `**Triage agent ${progress.reviewer}** started ${progress.phase} for ${issue}.`);
1513
+ }
1514
+ if (progress.type === "triage_agent_repair") {
1515
+ await this.notify(state, `**Triage agent ${progress.reviewer}** started JSON regeneration for ${issue}.`);
1516
+ }
1517
+ if (progress.type === "triage_agent_completed") {
1518
+ await this.notify(state, `**Triage agent ${progress.reviewer}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
1519
+ }
1520
+ if (progress.type === "triage_agent_failed") {
1521
+ await this.notify(state, `**Triage agent ${progress.reviewer}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
1522
+ }
1523
+ if (progress.type === "comment_posting") {
1524
+ await this.notify(state, `Posting triage comment for ${issue}.`);
1525
+ }
1526
+ if (progress.type === "comment_posted") {
1527
+ await this.notify(state, `Posted triage comment for ${issue}: ${progress.url}`);
1528
+ }
1529
+ if (progress.type === "pr_creation_started") {
1530
+ await this.notify(state, `Started implementation PR creation for ${issue}.`);
1531
+ }
1532
+ if (progress.type === "worktree_created") {
1533
+ await this.notify(state, `Worktree is ready for ${issue}.`);
1534
+ }
1535
+ if (progress.type === "triage_creator_started") {
1536
+ await this.notify(state, `**Triage creator** started creating an implementation PR for ${issue}.`);
1537
+ }
1538
+ if (progress.type === "triage_creator_repair") {
1539
+ await this.notify(state, `**Triage creator** started JSON regeneration for ${issue}.`);
1540
+ }
1541
+ if (progress.type === "triage_creator_completed") {
1542
+ await this.notify(state, `**Triage creator** completed implementation changes for ${issue}.`);
1543
+ }
1544
+ if (progress.type === "triage_creator_failed") {
1545
+ await this.notify(state, triageCreatorFailureText({
1546
+ error: redactSecrets(progress.error),
1547
+ issue,
1548
+ repairAttempts: state.triageCreator?.repairAttempts ?? 0,
1549
+ }));
1550
+ }
1551
+ if (progress.type === "pr_created") {
1552
+ await this.notify(state, `Created implementation PR for ${issue}: ${progress.url}`);
1553
+ }
1554
+ }
1167
1555
  async applyReviewProgress(runId, progress) {
1168
1556
  const state = this.active.get(runId);
1169
1557
  if (!state)
@@ -1226,7 +1614,7 @@ export class MagiRunManager {
1226
1614
  if (progress.type === "ci_classifier_failed") {
1227
1615
  const classifier = state.ciClassifiers?.[progress.reviewer];
1228
1616
  if (classifier) {
1229
- classifier.error = progress.error;
1617
+ classifier.error = redactSecrets(progress.error);
1230
1618
  classifier.status = "failed";
1231
1619
  classifier.lastUpdate = now();
1232
1620
  }
@@ -1282,7 +1670,7 @@ export class MagiRunManager {
1282
1670
  if (!reviewer)
1283
1671
  return;
1284
1672
  reviewer.status = "failed";
1285
- reviewer.error = progress.error;
1673
+ reviewer.error = redactSecrets(progress.error);
1286
1674
  reviewer.lastUpdate = now();
1287
1675
  }
1288
1676
  if (progress.type === "reviewer_completed") {
@@ -1328,7 +1716,7 @@ export class MagiRunManager {
1328
1716
  await this.notify(state, `**CI classifier ${progress.reviewer}** completed for ${prMarkdownLink(state)}: ${progress.classification} - ${progress.reason}`);
1329
1717
  }
1330
1718
  if (progress.type === "ci_classifier_failed") {
1331
- await this.notify(state, `**CI classifier ${progress.reviewer}** failed for ${prMarkdownLink(state)}: ${progress.error}`);
1719
+ await this.notify(state, `**CI classifier ${progress.reviewer}** failed for ${prMarkdownLink(state)}: ${redactSecrets(progress.error)}`);
1332
1720
  }
1333
1721
  if (progress.type === "worktree_created") {
1334
1722
  await this.notify(state, `Worktree is ready for ${prMarkdownLink(state)}.`);
@@ -1344,7 +1732,7 @@ export class MagiRunManager {
1344
1732
  }
1345
1733
  if (progress.type === "reviewer_failed") {
1346
1734
  await this.notify(state, reviewerFailureText({
1347
- error: progress.error,
1735
+ error: redactSecrets(progress.error),
1348
1736
  pr: prMarkdownLink(state),
1349
1737
  repairAttempts: state.reviewers[progress.reviewer]?.repairAttempts ?? 0,
1350
1738
  reviewer: progress.reviewer,
@@ -1451,7 +1839,7 @@ export class MagiRunManager {
1451
1839
  }
1452
1840
  if (progress.type === "editor_failed") {
1453
1841
  editor.status = "failed";
1454
- editor.error = progress.error;
1842
+ editor.error = redactSecrets(progress.error);
1455
1843
  editor.lastUpdate = now();
1456
1844
  }
1457
1845
  if (progress.type === "editor_completed") {
@@ -1475,7 +1863,7 @@ export class MagiRunManager {
1475
1863
  }
1476
1864
  if (progress.type === "editor_failed") {
1477
1865
  await this.notify(state, editorFailureText({
1478
- error: progress.error,
1866
+ error: redactSecrets(progress.error),
1479
1867
  pr: prMarkdownLink(state),
1480
1868
  repairAttempts: state.editor?.repairAttempts ?? 0,
1481
1869
  }));
@@ -1493,7 +1881,7 @@ export class MagiRunManager {
1493
1881
  state.status = "failed";
1494
1882
  state.phase = "failed";
1495
1883
  state.completedAt = now();
1496
- state.error = error instanceof Error ? error.message : String(error);
1884
+ state.error = errorMessage(error);
1497
1885
  if (state.editor?.status === "pending" ||
1498
1886
  state.editor?.status === "running" ||
1499
1887
  state.editor?.status === "repairing" ||
@@ -1547,7 +1935,8 @@ export class MagiRunManager {
1547
1935
  : await this.listStates(input.outputDir);
1548
1936
  return states
1549
1937
  .filter((state) => input.command == null || state.command === input.command)
1550
- .filter((state) => input.pr == null || state.pr === input.pr)
1938
+ .filter((state) => input.issue == null || state.issue === input.issue)
1939
+ .filter((state) => matchesNumberFilter(state.pr, input.pr))
1551
1940
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1552
1941
  }
1553
1942
  async selectState(input) {
@@ -1560,6 +1949,8 @@ export class MagiRunManager {
1560
1949
  return input.runId;
1561
1950
  if (input.pr != null)
1562
1951
  return `PR #${input.pr}`;
1952
+ if (input.issue != null)
1953
+ return `issue #${input.issue}`;
1563
1954
  return "all runs";
1564
1955
  }
1565
1956
  absoluteOutputDir(dir) {