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.
- package/README.md +33 -10
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +124 -26
- package/dist/config/validate.js +486 -191
- package/dist/config/worktree.js +19 -0
- package/dist/github/commands.js +349 -17
- package/dist/index.js +257 -27
- 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 +24 -4
- package/dist/orchestrator/report.js +15 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +78 -10
- package/dist/orchestrator/run-manager.js +418 -20
- package/dist/orchestrator/triage.js +1119 -0
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +172 -15
- package/dist/prompts/contracts.js +119 -12
- package/dist/prompts/output.js +149 -14
- package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
- package/dist/prompts/templates/review/review.md +13 -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 +28 -27
- package/schema.json +234 -90
- package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
- package/dist/prompts/templates/review.md +0 -7
- /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
- /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
- /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
|
-
|
|
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:
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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 ${
|
|
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 =
|
|
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.
|
|
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
|