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.
- package/README.md +19 -0
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +81 -1
- package/dist/config/validate.js +290 -3
- package/dist/config/worktree.js +8 -2
- package/dist/github/commands.js +343 -15
- package/dist/index.js +252 -26
- package/dist/orchestrator/ci.js +1 -1
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/inline-comments.js +73 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +16 -3
- package/dist/orchestrator/report.js +15 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +49 -9
- package/dist/orchestrator/run-manager.js +408 -17
- package/dist/orchestrator/triage.js +1119 -0
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +162 -1
- package/dist/prompts/contracts.js +119 -12
- package/dist/prompts/output.js +149 -14
- package/dist/prompts/templates/review/review.md +6 -0
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/action.md +5 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/dist/prompts/templates/triage/comment-classification.md +7 -0
- package/dist/prompts/templates/triage/comment.md +5 -0
- package/dist/prompts/templates/triage/create.md +7 -0
- package/dist/prompts/templates/triage/duplicate.md +7 -0
- package/dist/prompts/templates/triage/existing-pr.md +7 -0
- package/dist/prompts/templates/triage/question.md +5 -0
- package/dist/prompts/templates/triage/reconsider.md +5 -0
- package/package.json +5 -2
- 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
|
-
|
|
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:
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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 =
|
|
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.
|
|
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) {
|