opencode-magi 0.1.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 (43) hide show
  1. package/README.md +33 -10
  2. package/dist/commands.js +4 -0
  3. package/dist/config/output.js +11 -2
  4. package/dist/config/resolve.js +124 -26
  5. package/dist/config/validate.js +486 -191
  6. package/dist/config/worktree.js +19 -0
  7. package/dist/github/commands.js +349 -17
  8. package/dist/index.js +257 -27
  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 +24 -4
  14. package/dist/orchestrator/report.js +15 -1
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +78 -10
  17. package/dist/orchestrator/run-manager.js +418 -20
  18. package/dist/orchestrator/triage.js +1119 -0
  19. package/dist/permissions/editor.json +8 -1
  20. package/dist/prompts/compose.js +172 -15
  21. package/dist/prompts/contracts.js +119 -12
  22. package/dist/prompts/output.js +149 -14
  23. package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
  24. package/dist/prompts/templates/review/review.md +13 -0
  25. package/dist/prompts/templates/triage/acceptance.md +7 -0
  26. package/dist/prompts/templates/triage/action.md +5 -0
  27. package/dist/prompts/templates/triage/category.md +10 -0
  28. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  29. package/dist/prompts/templates/triage/comment.md +5 -0
  30. package/dist/prompts/templates/triage/create.md +7 -0
  31. package/dist/prompts/templates/triage/duplicate.md +7 -0
  32. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  33. package/dist/prompts/templates/triage/question.md +5 -0
  34. package/dist/prompts/templates/triage/reconsider.md +5 -0
  35. package/package.json +28 -27
  36. package/schema.json +234 -90
  37. package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
  38. package/dist/prompts/templates/review.md +0 -7
  39. /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
  40. /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
  41. /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
  42. /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
  43. /package/dist/prompts/templates/{rereview.md → review/rereview.md} +0 -0
@@ -1,11 +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
+ import { worktreeBaseDirs } from "../config/worktree";
5
6
  import { removeBranch, removeWorktree, } from "../github/commands";
6
7
  import { withGitHubApiRetry } from "../github/retry";
7
8
  import { runMerge, } from "./merge";
8
9
  import { runReview } from "./review";
10
+ import { runTriage } from "./triage";
9
11
  const EVENT_LAST_UPDATE_THROTTLE_MS = 5_000;
10
12
  const DEFAULT_CLEAR_OPTIONS = {
11
13
  branch: true,
@@ -19,12 +21,33 @@ function createRunId() {
19
21
  function now() {
20
22
  return new Date().toISOString();
21
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
+ }
22
32
  function isActiveStatus(status) {
23
33
  return (status === "blocked" ||
24
34
  status === "preparing" ||
25
35
  status === "running" ||
26
36
  status === "posting");
27
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
+ }
28
51
  function isWithinDirectory(directory, path) {
29
52
  const relation = relative(directory, path);
30
53
  return (relation === "" || (!relation.startsWith("..") && !isAbsolute(relation)));
@@ -66,13 +89,35 @@ function prUrl(repository, pr) {
66
89
  const host = repository.github.host || "github.com";
67
90
  return `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
68
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
+ }
69
103
  function prMarkdownLink(state) {
70
104
  if (state.pr == null)
71
105
  return state.runId;
72
106
  return state.prUrl ? `[#${state.pr}](${state.prUrl})` : `#${state.pr}`;
73
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
+ }
74
115
  function runLabel(state) {
75
- 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;
76
121
  }
77
122
  function reviewerCompletionText(input) {
78
123
  const reviewer = `**Reviewer ${input.reviewer}**`;
@@ -126,6 +171,13 @@ function editorFailureText(input) {
126
171
  const repairs = repairAttemptsText(input.repairAttempts);
127
172
  return `**Editor** failed editing ${input.pr}${repairs}: ${input.error}`;
128
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
+ }
129
181
  function repairAttemptsText(attempts) {
130
182
  if (!attempts)
131
183
  return "";
@@ -468,12 +520,70 @@ export class MagiRunManager {
468
520
  });
469
521
  return state;
470
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
+ }
471
580
  async status(input = {}) {
472
581
  const timeoutMs = Math.min(input.timeoutMs ?? 60_000, 600_000);
473
582
  const startedAt = Date.now();
474
583
  while (input.block) {
475
584
  const states = await this.filteredStates(input);
476
585
  if (states.length &&
586
+ hasAllRequestedPrStates(states, input.pr) &&
477
587
  states.every((state) => !isActiveStatus(state.status)))
478
588
  return states;
479
589
  if (Date.now() - startedAt >= timeoutMs)
@@ -538,6 +648,17 @@ export class MagiRunManager {
538
648
  .abort?.({ path: { id: state.editor.sessionId } })
539
649
  .catch(() => undefined);
540
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
+ }
541
662
  for (const reviewer of Object.values(state.reviewers)) {
542
663
  if (reviewer.status === "pending" ||
543
664
  reviewer.status === "running" ||
@@ -582,9 +703,7 @@ export class MagiRunManager {
582
703
  worktree: configured.worktree ?? DEFAULT_CLEAR_OPTIONS.worktree,
583
704
  };
584
705
  const states = await this.filteredStates(input);
585
- const cleanupDirs = new Set([
586
- join(this.input.directory, ".magi", "worktrees"),
587
- ]);
706
+ const cleanupDirs = new Set(this.absoluteWorktreeDirs(input));
588
707
  const cleanupTrees = new Set(this.emptyOutputCleanupRoots(input));
589
708
  const summary = {
590
709
  branchDeleted: 0,
@@ -833,7 +952,7 @@ export class MagiRunManager {
833
952
  if (!existing) {
834
953
  await this.notify(state, questionWaitText({
835
954
  agent: mapping.agent,
836
- pr: prMarkdownLink(state),
955
+ pr: runLabel(state),
837
956
  question,
838
957
  }), { reply: true });
839
958
  }
@@ -852,7 +971,7 @@ export class MagiRunManager {
852
971
  agent.error = `Permission ${permission?.permission ?? "request"} is waiting for approval.`;
853
972
  markUpdated(true);
854
973
  dirty = true;
855
- 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 });
856
975
  }
857
976
  }
858
977
  if (input.event.type === "question.asked") {
@@ -875,7 +994,7 @@ export class MagiRunManager {
875
994
  dirty = true;
876
995
  await this.notify(state, questionWaitText({
877
996
  agent: mapping.agent,
878
- pr: prMarkdownLink(state),
997
+ pr: runLabel(state),
879
998
  question,
880
999
  }), { reply: true });
881
1000
  }
@@ -928,7 +1047,7 @@ export class MagiRunManager {
928
1047
  }
929
1048
  if (input.event.type === "session.error") {
930
1049
  agent.status = "failed";
931
- agent.error = JSON.stringify(input.event.properties?.error ?? "session error");
1050
+ agent.error = redactSecrets(JSON.stringify(input.event.properties?.error ?? "session error"));
932
1051
  markUpdated(true);
933
1052
  dirty = true;
934
1053
  }
@@ -945,6 +1064,9 @@ export class MagiRunManager {
945
1064
  const editorLine = state.editor
946
1065
  ? this.formatAgentLine("editor", state.editor, options)
947
1066
  : undefined;
1067
+ const triageCreatorLine = state.triageCreator
1068
+ ? this.formatAgentLine("triageCreator", state.triageCreator, options)
1069
+ : undefined;
948
1070
  const reviewerLines = Object.entries(state.reviewers).map(([key, reviewer]) => {
949
1071
  return this.formatAgentLine(key, reviewer, options);
950
1072
  });
@@ -952,6 +1074,7 @@ export class MagiRunManager {
952
1074
  const lines = [
953
1075
  options.verbose ? `Run: ${state.runId}` : undefined,
954
1076
  state.pr == null ? undefined : `PR: #${state.pr}`,
1077
+ state.issue == null ? undefined : `Issue: #${state.issue}`,
955
1078
  `Command: ${state.command}`,
956
1079
  state.dryRun ? "Dry run: true" : undefined,
957
1080
  `Status: ${state.status}`,
@@ -972,6 +1095,7 @@ export class MagiRunManager {
972
1095
  ? `Report: ${state.reportPath}`
973
1096
  : undefined,
974
1097
  editorLine,
1098
+ triageCreatorLine,
975
1099
  ...classifierLines,
976
1100
  ...reviewerLines,
977
1101
  ];
@@ -998,6 +1122,7 @@ export class MagiRunManager {
998
1122
  collectSessionIds(state) {
999
1123
  const ids = [
1000
1124
  state.editor?.sessionId,
1125
+ state.triageCreator?.sessionId,
1001
1126
  ...Object.values(state.reviewers).map((reviewer) => reviewer.sessionId),
1002
1127
  ...Object.values(state.ciClassifiers ?? {}).map((classifier) => classifier.sessionId),
1003
1128
  ...Object.values(state.sessionIds ?? {}),
@@ -1040,13 +1165,22 @@ export class MagiRunManager {
1040
1165
  agentState(state, key) {
1041
1166
  if (key.startsWith("ci:"))
1042
1167
  return state.ciClassifiers?.[key.slice(3)];
1043
- 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];
1044
1173
  }
1045
1174
  agentEntries(state) {
1046
1175
  return [
1047
1176
  ...(state.editor
1048
1177
  ? [["editor", state.editor]]
1049
1178
  : []),
1179
+ ...(state.triageCreator
1180
+ ? [
1181
+ ["triageCreator", state.triageCreator],
1182
+ ]
1183
+ : []),
1050
1184
  ...Object.entries(state.ciClassifiers ?? {}).map(([key, value]) => [`ci:${key}`, value]),
1051
1185
  ...Object.entries(state.reviewers),
1052
1186
  ];
@@ -1066,10 +1200,10 @@ export class MagiRunManager {
1066
1200
  if (!matches.length) {
1067
1201
  return key
1068
1202
  ? `No pending ${kind} request found for ${key}.`
1069
- : `No pending ${kind} request found for ${prMarkdownLink(state)}.`;
1203
+ : `No pending ${kind} request found for ${runLabel(state)}.`;
1070
1204
  }
1071
1205
  if (matches.length > 1) {
1072
- 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.`;
1073
1207
  }
1074
1208
  return { key: matches[0][0], state: matches[0][1] };
1075
1209
  }
@@ -1078,6 +1212,7 @@ export class MagiRunManager {
1078
1212
  }
1079
1213
  async executeReview(input) {
1080
1214
  const result = await runReview({
1215
+ approvalPolicy: input.repository.merge.approvalPolicy,
1081
1216
  client: this.input.client,
1082
1217
  config: input.config,
1083
1218
  directory: this.input.directory,
@@ -1165,6 +1300,258 @@ export class MagiRunManager {
1165
1300
  this.active.delete(input.runId);
1166
1301
  this.controllers.delete(input.runId);
1167
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
+ }
1168
1555
  async applyReviewProgress(runId, progress) {
1169
1556
  const state = this.active.get(runId);
1170
1557
  if (!state)
@@ -1227,7 +1614,7 @@ export class MagiRunManager {
1227
1614
  if (progress.type === "ci_classifier_failed") {
1228
1615
  const classifier = state.ciClassifiers?.[progress.reviewer];
1229
1616
  if (classifier) {
1230
- classifier.error = progress.error;
1617
+ classifier.error = redactSecrets(progress.error);
1231
1618
  classifier.status = "failed";
1232
1619
  classifier.lastUpdate = now();
1233
1620
  }
@@ -1283,7 +1670,7 @@ export class MagiRunManager {
1283
1670
  if (!reviewer)
1284
1671
  return;
1285
1672
  reviewer.status = "failed";
1286
- reviewer.error = progress.error;
1673
+ reviewer.error = redactSecrets(progress.error);
1287
1674
  reviewer.lastUpdate = now();
1288
1675
  }
1289
1676
  if (progress.type === "reviewer_completed") {
@@ -1329,7 +1716,7 @@ export class MagiRunManager {
1329
1716
  await this.notify(state, `**CI classifier ${progress.reviewer}** completed for ${prMarkdownLink(state)}: ${progress.classification} - ${progress.reason}`);
1330
1717
  }
1331
1718
  if (progress.type === "ci_classifier_failed") {
1332
- 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)}`);
1333
1720
  }
1334
1721
  if (progress.type === "worktree_created") {
1335
1722
  await this.notify(state, `Worktree is ready for ${prMarkdownLink(state)}.`);
@@ -1345,7 +1732,7 @@ export class MagiRunManager {
1345
1732
  }
1346
1733
  if (progress.type === "reviewer_failed") {
1347
1734
  await this.notify(state, reviewerFailureText({
1348
- error: progress.error,
1735
+ error: redactSecrets(progress.error),
1349
1736
  pr: prMarkdownLink(state),
1350
1737
  repairAttempts: state.reviewers[progress.reviewer]?.repairAttempts ?? 0,
1351
1738
  reviewer: progress.reviewer,
@@ -1452,7 +1839,7 @@ export class MagiRunManager {
1452
1839
  }
1453
1840
  if (progress.type === "editor_failed") {
1454
1841
  editor.status = "failed";
1455
- editor.error = progress.error;
1842
+ editor.error = redactSecrets(progress.error);
1456
1843
  editor.lastUpdate = now();
1457
1844
  }
1458
1845
  if (progress.type === "editor_completed") {
@@ -1476,7 +1863,7 @@ export class MagiRunManager {
1476
1863
  }
1477
1864
  if (progress.type === "editor_failed") {
1478
1865
  await this.notify(state, editorFailureText({
1479
- error: progress.error,
1866
+ error: redactSecrets(progress.error),
1480
1867
  pr: prMarkdownLink(state),
1481
1868
  repairAttempts: state.editor?.repairAttempts ?? 0,
1482
1869
  }));
@@ -1494,7 +1881,7 @@ export class MagiRunManager {
1494
1881
  state.status = "failed";
1495
1882
  state.phase = "failed";
1496
1883
  state.completedAt = now();
1497
- state.error = error instanceof Error ? error.message : String(error);
1884
+ state.error = errorMessage(error);
1498
1885
  if (state.editor?.status === "pending" ||
1499
1886
  state.editor?.status === "running" ||
1500
1887
  state.editor?.status === "repairing" ||
@@ -1548,7 +1935,8 @@ export class MagiRunManager {
1548
1935
  : await this.listStates(input.outputDir);
1549
1936
  return states
1550
1937
  .filter((state) => input.command == null || state.command === input.command)
1551
- .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))
1552
1940
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1553
1941
  }
1554
1942
  async selectState(input) {
@@ -1561,11 +1949,21 @@ export class MagiRunManager {
1561
1949
  return input.runId;
1562
1950
  if (input.pr != null)
1563
1951
  return `PR #${input.pr}`;
1952
+ if (input.issue != null)
1953
+ return `issue #${input.issue}`;
1564
1954
  return "all runs";
1565
1955
  }
1566
1956
  absoluteOutputDir(dir) {
1567
1957
  return isAbsolute(dir) ? dir : join(this.input.directory, dir);
1568
1958
  }
1959
+ absoluteWorktreeDirs(input) {
1960
+ const worktreeDirs = Array.isArray(input.worktreeDir)
1961
+ ? input.worktreeDir
1962
+ : input.worktreeDir
1963
+ ? [input.worktreeDir]
1964
+ : worktreeBaseDirs(this.input.directory);
1965
+ return worktreeDirs.map((dir) => isAbsolute(dir) ? dir : join(this.input.directory, dir));
1966
+ }
1569
1967
  emptyOutputCleanupRoots(input) {
1570
1968
  const outputDirs = Array.isArray(input.outputDir)
1571
1969
  ? input.outputDir