opencode-magi 0.10.0 → 0.12.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.ja.md +254 -0
- package/README.md +31 -26
- package/dist/commands.js +11 -9
- package/dist/config/index.js +2 -0
- package/dist/config/resolve.js +120 -246
- package/dist/config/validate.js +98 -1110
- package/dist/constant.js +131 -0
- package/dist/graphql/index.generated.js +125 -0
- package/dist/graphql/index.js +49 -0
- package/dist/index.js +12 -800
- package/dist/magi.js +357 -0
- package/dist/permissions/common.json +22 -22
- package/dist/permissions/editor.json +12 -12
- package/dist/prompts/index.js +98 -0
- package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
- package/dist/prompts/merge/ci-classification/task.md +10 -0
- package/dist/prompts/merge/ci-classification/validate.json +20 -0
- package/dist/prompts/merge/conflict/output-contract.md +12 -0
- package/dist/prompts/merge/conflict/task.md +9 -0
- package/dist/prompts/merge/conflict/validate.json +4 -0
- package/dist/prompts/merge/edit/output-contract.md +26 -0
- package/dist/prompts/merge/edit/task.md +10 -0
- package/dist/prompts/merge/edit/validate.json +60 -0
- package/dist/prompts/review/ci-classification/output-contract.md +21 -0
- package/dist/prompts/review/ci-classification/task.md +7 -0
- package/dist/prompts/review/ci-classification/validate.json +20 -0
- package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
- package/dist/prompts/review/close-reconsideration/task.md +6 -0
- package/dist/prompts/review/close-reconsideration/validate.json +44 -0
- package/dist/prompts/review/comment/output-contract.md +11 -0
- package/dist/prompts/review/comment/task.md +3 -0
- package/dist/prompts/review/comment/validate.json +8 -0
- package/dist/prompts/review/finding-validation/output-contract.md +25 -0
- package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
- package/dist/prompts/review/finding-validation/validate.json +21 -0
- package/dist/prompts/review/rereview/output-contract.md +30 -0
- package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
- package/dist/prompts/review/rereview/validate.json +87 -0
- package/dist/prompts/review/review/output-contract.md +25 -0
- package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
- package/dist/prompts/review/review/validate.json +54 -0
- package/dist/prompts/triage/acceptance/output-contract.md +16 -0
- package/dist/prompts/triage/acceptance/validate.json +23 -0
- package/dist/prompts/triage/category/output-contract.md +16 -0
- package/dist/prompts/triage/category/validate.json +23 -0
- package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
- package/dist/prompts/triage/comment-classification/validate.json +28 -0
- package/dist/prompts/triage/create/output-contract.md +28 -0
- package/dist/prompts/triage/create/validate.json +73 -0
- package/dist/prompts/triage/deplicate/output-contract.md +16 -0
- package/dist/prompts/triage/deplicate/validate.json +20 -0
- package/dist/prompts/triage/existing/output-contract.md +16 -0
- package/dist/prompts/triage/existing/validate.json +13 -0
- package/dist/prompts/triage/reconsider/output-contract.md +16 -0
- package/dist/prompts/triage/reconsider/validate.json +23 -0
- package/dist/prompts/triage/signal/output-contract.md +19 -0
- package/dist/prompts/triage/signal/validate.json +18 -0
- package/dist/tools/clear/index.js +21 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/merge/action.js +47 -0
- package/dist/tools/merge/context.js +116 -0
- package/dist/tools/merge/editor.js +256 -0
- package/dist/tools/merge/index.js +151 -0
- package/dist/tools/merge/index.type.js +1 -0
- package/dist/tools/merge/merge.js +42 -0
- package/dist/tools/merge/report.js +73 -0
- package/dist/tools/review/action.js +459 -0
- package/dist/tools/review/check.js +335 -0
- package/dist/tools/review/context.js +254 -0
- package/dist/tools/review/index.js +104 -0
- package/dist/tools/review/index.type.js +1 -0
- package/dist/tools/review/report.js +143 -0
- package/dist/tools/review/review.js +146 -0
- package/dist/tools/review/reviewer.js +409 -0
- package/dist/tools/triage/index.js +16 -0
- package/dist/tools/validate/index.js +28 -0
- package/dist/utils/array.js +6 -0
- package/dist/utils/assertion.js +38 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/fs.js +24 -0
- package/dist/utils/function.js +46 -0
- package/dist/utils/github.js +63 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.type.js +1 -0
- package/dist/utils/object.js +21 -0
- package/dist/utils/opencode.js +25 -0
- package/dist/utils/string.js +38 -0
- package/dist/utils/worker.js +30 -0
- package/package.json +36 -23
- package/schema.json +116 -20
- package/dist/config/load.js +0 -62
- package/dist/config/output.js +0 -25
- package/dist/config/worktree.js +0 -25
- package/dist/github/commands.js +0 -905
- package/dist/github/retry.js +0 -44
- package/dist/orchestrator/abort.js +0 -9
- package/dist/orchestrator/ci.js +0 -579
- package/dist/orchestrator/findings.js +0 -88
- package/dist/orchestrator/inline-comments.js +0 -73
- package/dist/orchestrator/majority.js +0 -62
- package/dist/orchestrator/merge.js +0 -1260
- package/dist/orchestrator/model.js +0 -216
- package/dist/orchestrator/pool.js +0 -15
- package/dist/orchestrator/report.js +0 -175
- package/dist/orchestrator/review-context.js +0 -342
- package/dist/orchestrator/review.js +0 -1480
- package/dist/orchestrator/run-manager.js +0 -2132
- package/dist/orchestrator/safety.js +0 -44
- package/dist/orchestrator/triage.js +0 -1370
- package/dist/prompts/compose.js +0 -473
- package/dist/prompts/contracts.js +0 -300
- package/dist/prompts/output.js +0 -401
- package/dist/prompts/templates/merge/ci-classification.md +0 -16
- package/dist/prompts/templates/merge/conflict.md +0 -10
- package/dist/prompts/templates/merge/edit.md +0 -15
- package/dist/prompts/templates/review/ci-classification.md +0 -9
- package/dist/prompts/templates/review/close-reconsideration.md +0 -6
- /package/dist/{types.js → config/index.type.js} +0 -0
- /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
- /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
- /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
- /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
- /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
- /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
- /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
- /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
|
@@ -1,2132 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, readdir, rm, rmdir, writeFile, } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { issueRunOutputDir, outputBaseDirs, prRunOutputDir, } from "../config/output";
|
|
5
|
-
import { worktreeBaseDirs } from "../config/worktree";
|
|
6
|
-
import { removeBranch, removeWorktree, } from "../github/commands";
|
|
7
|
-
import { withGitHubApiRetry } from "../github/retry";
|
|
8
|
-
import { runMerge, } from "./merge";
|
|
9
|
-
import { runReview } from "./review";
|
|
10
|
-
import { runTriage } from "./triage";
|
|
11
|
-
const EVENT_LAST_UPDATE_THROTTLE_MS = 5_000;
|
|
12
|
-
const DEFAULT_CLEAR_OPTIONS = {
|
|
13
|
-
branch: true,
|
|
14
|
-
output: true,
|
|
15
|
-
session: true,
|
|
16
|
-
worktree: true,
|
|
17
|
-
};
|
|
18
|
-
function createRunId() {
|
|
19
|
-
return `run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
20
|
-
}
|
|
21
|
-
function now() {
|
|
22
|
-
return new Date().toISOString();
|
|
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
|
-
}
|
|
32
|
-
function isActiveStatus(status) {
|
|
33
|
-
return (status === "blocked" ||
|
|
34
|
-
status === "preparing" ||
|
|
35
|
-
status === "running" ||
|
|
36
|
-
status === "posting");
|
|
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
|
-
}
|
|
51
|
-
function isWithinDirectory(directory, path) {
|
|
52
|
-
const relation = relative(directory, path);
|
|
53
|
-
return (relation === "" || (!relation.startsWith("..") && !isAbsolute(relation)));
|
|
54
|
-
}
|
|
55
|
-
async function pruneEmptyDirectories(input) {
|
|
56
|
-
const boundary = resolve(input.boundary);
|
|
57
|
-
const start = resolve(input.start);
|
|
58
|
-
if (!isWithinDirectory(boundary, start) || start === boundary)
|
|
59
|
-
return;
|
|
60
|
-
async function pruneChildren(dir) {
|
|
61
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
if (!entry.isDirectory())
|
|
64
|
-
continue;
|
|
65
|
-
const path = join(dir, entry.name);
|
|
66
|
-
await pruneChildren(path);
|
|
67
|
-
await rmdir(path).catch(() => undefined);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (input.recursive)
|
|
71
|
-
await pruneChildren(start);
|
|
72
|
-
let current = start;
|
|
73
|
-
while (current !== boundary && isWithinDirectory(boundary, current)) {
|
|
74
|
-
try {
|
|
75
|
-
await rmdir(current);
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
const code = error.code;
|
|
79
|
-
if (code !== "ENOENT")
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
current = dirname(current);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function reviewerArtifactBase(progressType, reviewer) {
|
|
86
|
-
return `${reviewer}.${progressType}`;
|
|
87
|
-
}
|
|
88
|
-
function prUrl(repository, pr) {
|
|
89
|
-
const host = repository.github.host || "github.com";
|
|
90
|
-
return `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
|
|
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
|
-
}
|
|
103
|
-
function prMarkdownLink(state) {
|
|
104
|
-
if (state.pr == null)
|
|
105
|
-
return state.runId;
|
|
106
|
-
return state.prUrl ? `[#${state.pr}](${state.prUrl})` : `#${state.pr}`;
|
|
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
|
-
}
|
|
115
|
-
function runLabel(state) {
|
|
116
|
-
if (state.pr != null)
|
|
117
|
-
return prMarkdownLink(state);
|
|
118
|
-
if (state.issue != null)
|
|
119
|
-
return issueMarkdownLink(state);
|
|
120
|
-
return state.runId;
|
|
121
|
-
}
|
|
122
|
-
function reviewerCompletionText(input) {
|
|
123
|
-
const reviewer = `**Reviewer ${input.reviewer}**`;
|
|
124
|
-
if (input.verdict === "MERGE") {
|
|
125
|
-
return `${reviewer} approved ${input.pr}.`;
|
|
126
|
-
}
|
|
127
|
-
if (input.verdict === "CHANGES_REQUESTED") {
|
|
128
|
-
return `${reviewer} requested changes on ${input.pr}.`;
|
|
129
|
-
}
|
|
130
|
-
if (input.verdict === "CLOSE") {
|
|
131
|
-
return `${reviewer} requested closing ${input.pr}.`;
|
|
132
|
-
}
|
|
133
|
-
return `${reviewer} finished reviewing ${input.pr}.`;
|
|
134
|
-
}
|
|
135
|
-
function reviewDecisionText(input) {
|
|
136
|
-
if (input.verdict === "MERGE")
|
|
137
|
-
return `Reviewers approved ${input.pr}.`;
|
|
138
|
-
if (input.verdict === "CHANGES_REQUESTED") {
|
|
139
|
-
return `Reviewers requested changes on ${input.pr}.`;
|
|
140
|
-
}
|
|
141
|
-
if (input.verdict === "CLOSE") {
|
|
142
|
-
return `Reviewers requested closing ${input.pr}.`;
|
|
143
|
-
}
|
|
144
|
-
return `Reviewers finished reviewing ${input.pr}.`;
|
|
145
|
-
}
|
|
146
|
-
function ciReportText(input) {
|
|
147
|
-
const failed = input.report.failed.length;
|
|
148
|
-
const rerun = input.report.rerun.length;
|
|
149
|
-
const recovered = input.report.scopeOutsideRecovered.length;
|
|
150
|
-
const unresolved = input.report.scopeOutsideUnresolved.length;
|
|
151
|
-
const scopeInside = input.report.scopeInside.length;
|
|
152
|
-
return `CI report for ${input.pr}: ${failed} failed, ${scopeInside} scope-in, ${rerun} rerun, ${recovered} recovered, ${unresolved} unresolved.`;
|
|
153
|
-
}
|
|
154
|
-
function ciClassifierCompletedText(input) {
|
|
155
|
-
const summary = input.checks
|
|
156
|
-
.map((check) => `${check.name}: ${check.classification} - ${check.reason}`)
|
|
157
|
-
.join("; ");
|
|
158
|
-
return `**CI classifier ${input.reviewer}** completed for ${input.pr}: ${summary}`;
|
|
159
|
-
}
|
|
160
|
-
function closeReconsiderationText(input) {
|
|
161
|
-
if (input.to === "MERGE") {
|
|
162
|
-
return `**Reviewer ${input.reviewer}** changed their close request to approval for ${input.pr}.`;
|
|
163
|
-
}
|
|
164
|
-
if (input.to === "CHANGES_REQUESTED") {
|
|
165
|
-
return `**Reviewer ${input.reviewer}** changed their close request to changes requested for ${input.pr}.`;
|
|
166
|
-
}
|
|
167
|
-
return `**Reviewer ${input.reviewer}** reconsidered closing ${input.pr}.`;
|
|
168
|
-
}
|
|
169
|
-
function findingsValidationText(input) {
|
|
170
|
-
return `Validated review findings by majority for ${input.pr}: ${input.kept} kept, ${input.discarded} discarded.`;
|
|
171
|
-
}
|
|
172
|
-
function reviewerFailureText(input) {
|
|
173
|
-
const repairs = repairAttemptsText(input.repairAttempts);
|
|
174
|
-
return `**Reviewer ${input.reviewer}** failed reviewing ${input.pr}${repairs}: ${input.error}`;
|
|
175
|
-
}
|
|
176
|
-
function editorFailureText(input) {
|
|
177
|
-
const repairs = repairAttemptsText(input.repairAttempts);
|
|
178
|
-
return `**Editor** failed editing ${input.pr}${repairs}: ${input.error}`;
|
|
179
|
-
}
|
|
180
|
-
function triageCreatorFailureText(input) {
|
|
181
|
-
const repairs = repairAttemptsText(input.repairAttempts);
|
|
182
|
-
return `**Triage creator** failed creating an implementation PR for ${input.issue}${repairs}: ${input.error}`;
|
|
183
|
-
}
|
|
184
|
-
function triageDecisionNotification(input) {
|
|
185
|
-
return `Triage decided ${input.issue}: ${input.result}. Planned action: ${input.action}.`;
|
|
186
|
-
}
|
|
187
|
-
function repairAttemptsText(attempts) {
|
|
188
|
-
if (!attempts)
|
|
189
|
-
return "";
|
|
190
|
-
return ` after ${attempts} JSON regeneration attempt${attempts === 1 ? "" : "s"}`;
|
|
191
|
-
}
|
|
192
|
-
function mergePhaseText(input) {
|
|
193
|
-
if (input.phase === "fetching PR metadata") {
|
|
194
|
-
return `Fetching PR metadata for ${input.pr}.`;
|
|
195
|
-
}
|
|
196
|
-
if (input.phase === "fetching existing reviews") {
|
|
197
|
-
return `Fetching existing reviews for ${input.pr}.`;
|
|
198
|
-
}
|
|
199
|
-
if (input.phase === "waiting for checks") {
|
|
200
|
-
return `Waiting for checks for ${input.pr}.`;
|
|
201
|
-
}
|
|
202
|
-
if (input.phase === "posting reviews") {
|
|
203
|
-
return `Posting review results to GitHub for ${input.pr}.`;
|
|
204
|
-
}
|
|
205
|
-
if (input.phase === "waiting for CI checks") {
|
|
206
|
-
return `Waiting for CI checks for ${input.pr}.`;
|
|
207
|
-
}
|
|
208
|
-
if (input.phase === "CI checks passed") {
|
|
209
|
-
return `CI checks passed for ${input.pr}.`;
|
|
210
|
-
}
|
|
211
|
-
if (input.phase === "investigating failed CI checks") {
|
|
212
|
-
return `Investigating failed CI checks for ${input.pr}.`;
|
|
213
|
-
}
|
|
214
|
-
if (input.phase === "fetching failed CI logs") {
|
|
215
|
-
return `Fetching failed CI logs for ${input.pr}.`;
|
|
216
|
-
}
|
|
217
|
-
if (input.phase === "classifying CI failures") {
|
|
218
|
-
return `Classifying CI failures for ${input.pr}.`;
|
|
219
|
-
}
|
|
220
|
-
if (input.phase === "CI failures classified as scope-in") {
|
|
221
|
-
return `CI failures were classified as scope-in for ${input.pr}.`;
|
|
222
|
-
}
|
|
223
|
-
if (input.phase === "scope-out CI failures remain unresolved") {
|
|
224
|
-
return `Scope-out CI failures remain unresolved for ${input.pr}.`;
|
|
225
|
-
}
|
|
226
|
-
if (input.phase === "rerunning scope-out CI jobs") {
|
|
227
|
-
return `Rerunning scope-out CI jobs for ${input.pr}.`;
|
|
228
|
-
}
|
|
229
|
-
if (input.phase === "waiting for rerun CI checks") {
|
|
230
|
-
return `Waiting for rerun CI checks for ${input.pr}.`;
|
|
231
|
-
}
|
|
232
|
-
if (input.phase === "rerun CI checks passed") {
|
|
233
|
-
return `Rerun CI checks passed for ${input.pr}.`;
|
|
234
|
-
}
|
|
235
|
-
if (input.phase === "merging PR")
|
|
236
|
-
return `Merging ${input.pr}.`;
|
|
237
|
-
if (input.phase === "closing PR")
|
|
238
|
-
return `Closing ${input.pr}.`;
|
|
239
|
-
if (input.phase === "creating worktree") {
|
|
240
|
-
return `Creating worktree for ${input.pr}.`;
|
|
241
|
-
}
|
|
242
|
-
if (input.phase === "validating review findings") {
|
|
243
|
-
return `Validating review findings for ${input.pr}.`;
|
|
244
|
-
}
|
|
245
|
-
if (input.phase === "reconsidering close verdicts") {
|
|
246
|
-
return `Reconsidering close verdicts for ${input.pr}.`;
|
|
247
|
-
}
|
|
248
|
-
if (input.phase.startsWith("editing cycle")) {
|
|
249
|
-
return `**Editor** started editing ${input.pr}.`;
|
|
250
|
-
}
|
|
251
|
-
if (input.phase.startsWith("waiting for checks after edit")) {
|
|
252
|
-
return `Waiting for checks after editing ${input.pr}.`;
|
|
253
|
-
}
|
|
254
|
-
if (input.phase.startsWith("rereview cycle")) {
|
|
255
|
-
return `Started re-reviewing ${input.pr}.`;
|
|
256
|
-
}
|
|
257
|
-
return `Magi phase for ${input.pr}: ${input.phase}.`;
|
|
258
|
-
}
|
|
259
|
-
function threadLimitText(input) {
|
|
260
|
-
const links = input.threads
|
|
261
|
-
.map((thread) => `[${thread.label}](${thread.url})`)
|
|
262
|
-
.join(", ");
|
|
263
|
-
if (input.threads.length === 1) {
|
|
264
|
-
return `Review thread ${links} reached the resolution attempt limit for ${input.pr}.`;
|
|
265
|
-
}
|
|
266
|
-
return `Review threads ${links} reached the resolution attempt limit for ${input.pr}.`;
|
|
267
|
-
}
|
|
268
|
-
function extractSessionId(properties) {
|
|
269
|
-
if (!properties)
|
|
270
|
-
return undefined;
|
|
271
|
-
const direct = properties.sessionID ?? properties.sessionId;
|
|
272
|
-
if (typeof direct === "string")
|
|
273
|
-
return direct;
|
|
274
|
-
const info = properties.info;
|
|
275
|
-
if (info && typeof info === "object") {
|
|
276
|
-
const value = info
|
|
277
|
-
.sessionID ??
|
|
278
|
-
info
|
|
279
|
-
.sessionId ??
|
|
280
|
-
info.id;
|
|
281
|
-
if (typeof value === "string")
|
|
282
|
-
return value;
|
|
283
|
-
}
|
|
284
|
-
const part = properties.part;
|
|
285
|
-
if (part && typeof part === "object") {
|
|
286
|
-
const value = part.sessionID ??
|
|
287
|
-
part.sessionId;
|
|
288
|
-
if (typeof value === "string")
|
|
289
|
-
return value;
|
|
290
|
-
}
|
|
291
|
-
return undefined;
|
|
292
|
-
}
|
|
293
|
-
function extractToolPart(properties) {
|
|
294
|
-
if (!properties)
|
|
295
|
-
return undefined;
|
|
296
|
-
const part = properties.part && typeof properties.part === "object"
|
|
297
|
-
? properties.part
|
|
298
|
-
: properties;
|
|
299
|
-
const type = typeof part.type === "string" ? part.type : undefined;
|
|
300
|
-
const tool = typeof part.tool === "string" ? part.tool : undefined;
|
|
301
|
-
const state = part.state && typeof part.state === "object"
|
|
302
|
-
? part.state
|
|
303
|
-
: undefined;
|
|
304
|
-
if (!tool && type !== "tool")
|
|
305
|
-
return undefined;
|
|
306
|
-
return {
|
|
307
|
-
callId: typeof part.callID === "string" ? part.callID : undefined,
|
|
308
|
-
id: typeof part.id === "string" ? part.id : undefined,
|
|
309
|
-
input: state?.input && typeof state.input === "object"
|
|
310
|
-
? state.input
|
|
311
|
-
: undefined,
|
|
312
|
-
status: typeof state?.status === "string" ? state.status : undefined,
|
|
313
|
-
tool,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
function extractQuestions(input) {
|
|
317
|
-
return Array.isArray(input?.questions) ? input.questions : undefined;
|
|
318
|
-
}
|
|
319
|
-
function formatQuestionRequest(question) {
|
|
320
|
-
if (!question?.questions?.length)
|
|
321
|
-
return undefined;
|
|
322
|
-
return question.questions
|
|
323
|
-
.map((item, index) => {
|
|
324
|
-
if (!item || typeof item !== "object")
|
|
325
|
-
return `${index + 1}. ${String(item)}`;
|
|
326
|
-
const record = item;
|
|
327
|
-
const header = typeof record.header === "string" ? record.header : undefined;
|
|
328
|
-
const text = typeof record.question === "string" ? record.question : undefined;
|
|
329
|
-
const options = Array.isArray(record.options)
|
|
330
|
-
? record.options
|
|
331
|
-
.map((option) => {
|
|
332
|
-
if (!option || typeof option !== "object")
|
|
333
|
-
return undefined;
|
|
334
|
-
const label = option.label;
|
|
335
|
-
return typeof label === "string" ? label : undefined;
|
|
336
|
-
})
|
|
337
|
-
.filter((value) => Boolean(value))
|
|
338
|
-
: [];
|
|
339
|
-
const suffix = options.length ? ` Options: ${options.join(", ")}.` : "";
|
|
340
|
-
return `${index + 1}. ${header ? `${header}: ` : ""}${text ?? JSON.stringify(item)}${suffix}`;
|
|
341
|
-
})
|
|
342
|
-
.join("\n");
|
|
343
|
-
}
|
|
344
|
-
function questionWaitText(input) {
|
|
345
|
-
const details = formatQuestionRequest(input.question);
|
|
346
|
-
const request = input.question?.id ? ` Request: ${input.question.id}.` : "";
|
|
347
|
-
return [
|
|
348
|
-
`Magi ${input.agent} is waiting for a question answer on ${input.pr}.${request}`,
|
|
349
|
-
details
|
|
350
|
-
? `Question:\n${details}`
|
|
351
|
-
: "Question details were not included in the event.",
|
|
352
|
-
].join("\n");
|
|
353
|
-
}
|
|
354
|
-
function extractPermissionRequest(properties) {
|
|
355
|
-
if (!properties)
|
|
356
|
-
return undefined;
|
|
357
|
-
const sessionId = extractSessionId(properties);
|
|
358
|
-
const id = typeof properties.id === "string"
|
|
359
|
-
? properties.id
|
|
360
|
-
: typeof properties.requestID === "string"
|
|
361
|
-
? properties.requestID
|
|
362
|
-
: typeof properties.permissionID === "string"
|
|
363
|
-
? properties.permissionID
|
|
364
|
-
: undefined;
|
|
365
|
-
const permission = typeof properties.permission === "string"
|
|
366
|
-
? properties.permission
|
|
367
|
-
: typeof properties.type === "string"
|
|
368
|
-
? properties.type
|
|
369
|
-
: undefined;
|
|
370
|
-
const patterns = Array.isArray(properties.patterns)
|
|
371
|
-
? properties.patterns.filter((item) => typeof item === "string")
|
|
372
|
-
: undefined;
|
|
373
|
-
const tool = typeof properties.tool === "string"
|
|
374
|
-
? properties.tool
|
|
375
|
-
: properties.tool && typeof properties.tool === "object"
|
|
376
|
-
? typeof properties.tool.name === "string"
|
|
377
|
-
? properties.tool.name
|
|
378
|
-
: undefined
|
|
379
|
-
: undefined;
|
|
380
|
-
if (!sessionId && !id && !permission && !tool)
|
|
381
|
-
return undefined;
|
|
382
|
-
return { id, patterns, permission, sessionId, tool };
|
|
383
|
-
}
|
|
384
|
-
function extractQuestionRequest(properties) {
|
|
385
|
-
if (!properties)
|
|
386
|
-
return undefined;
|
|
387
|
-
const sessionId = extractSessionId(properties);
|
|
388
|
-
const id = typeof properties.id === "string"
|
|
389
|
-
? properties.id
|
|
390
|
-
: typeof properties.requestID === "string"
|
|
391
|
-
? properties.requestID
|
|
392
|
-
: typeof properties.questionID === "string"
|
|
393
|
-
? properties.questionID
|
|
394
|
-
: undefined;
|
|
395
|
-
const questions = Array.isArray(properties.questions)
|
|
396
|
-
? properties.questions
|
|
397
|
-
: undefined;
|
|
398
|
-
const tool = typeof properties.tool === "string"
|
|
399
|
-
? properties.tool
|
|
400
|
-
: properties.tool && typeof properties.tool === "object"
|
|
401
|
-
? typeof properties.tool.name === "string"
|
|
402
|
-
? properties.tool.name
|
|
403
|
-
: undefined
|
|
404
|
-
: undefined;
|
|
405
|
-
if (!sessionId && !id && !questions && !tool)
|
|
406
|
-
return undefined;
|
|
407
|
-
return { id, questions, sessionId, tool };
|
|
408
|
-
}
|
|
409
|
-
export class MagiRunManager {
|
|
410
|
-
input;
|
|
411
|
-
active = new Map();
|
|
412
|
-
activePrRuns = 0;
|
|
413
|
-
activeTriageRuns = 0;
|
|
414
|
-
countedToolParts = new Map();
|
|
415
|
-
controllers = new Map();
|
|
416
|
-
eventLastUpdates = new Map();
|
|
417
|
-
notifiedPermissions = new Map();
|
|
418
|
-
runPaths = new Map();
|
|
419
|
-
outputDirs = new Set();
|
|
420
|
-
sessionToRun = new Map();
|
|
421
|
-
prQueue = [];
|
|
422
|
-
triageQueue = [];
|
|
423
|
-
constructor(input) {
|
|
424
|
-
this.input = input;
|
|
425
|
-
}
|
|
426
|
-
async startReview(input) {
|
|
427
|
-
const runId = createRunId();
|
|
428
|
-
const outputDir = prRunOutputDir({
|
|
429
|
-
config: input.config,
|
|
430
|
-
directory: this.input.directory,
|
|
431
|
-
pr: input.pr,
|
|
432
|
-
runId,
|
|
433
|
-
});
|
|
434
|
-
const createdAt = now();
|
|
435
|
-
const state = {
|
|
436
|
-
command: "review",
|
|
437
|
-
createdAt,
|
|
438
|
-
dryRun: input.dryRun,
|
|
439
|
-
outputDir,
|
|
440
|
-
parentSessionId: input.parentSessionId,
|
|
441
|
-
phase: "queued",
|
|
442
|
-
pr: input.pr,
|
|
443
|
-
prUrl: prUrl(input.repository, input.pr),
|
|
444
|
-
repository: input.repository.alias,
|
|
445
|
-
reviewers: Object.fromEntries(input.repository.agents.reviewers.map((reviewer) => [
|
|
446
|
-
reviewer.key,
|
|
447
|
-
{
|
|
448
|
-
account: reviewer.account,
|
|
449
|
-
repairAttempts: 0,
|
|
450
|
-
status: "pending",
|
|
451
|
-
toolCalls: 0,
|
|
452
|
-
},
|
|
453
|
-
])),
|
|
454
|
-
runId,
|
|
455
|
-
status: "preparing",
|
|
456
|
-
updatedAt: createdAt,
|
|
457
|
-
};
|
|
458
|
-
this.active.set(runId, state);
|
|
459
|
-
this.runPaths.set(runId, join(outputDir, "state.json"));
|
|
460
|
-
for (const dir of outputBaseDirs(this.input.directory, input.config))
|
|
461
|
-
this.outputDirs.add(dir);
|
|
462
|
-
await this.persist(state);
|
|
463
|
-
await this.notify(state, `Started Magi review for ${prMarkdownLink(state)}.`);
|
|
464
|
-
const controller = new AbortController();
|
|
465
|
-
this.controllers.set(runId, controller);
|
|
466
|
-
const execute = () => this.executeReview({
|
|
467
|
-
...input,
|
|
468
|
-
runId,
|
|
469
|
-
signal: controller.signal,
|
|
470
|
-
});
|
|
471
|
-
if (input.sync)
|
|
472
|
-
return this.executeSync(state, controller, execute, input.timeoutMs);
|
|
473
|
-
this.prQueue.push({
|
|
474
|
-
execute,
|
|
475
|
-
repository: input.repository,
|
|
476
|
-
runId,
|
|
477
|
-
});
|
|
478
|
-
this.drainPrQueue();
|
|
479
|
-
return state;
|
|
480
|
-
}
|
|
481
|
-
async startMerge(input) {
|
|
482
|
-
const runId = createRunId();
|
|
483
|
-
const outputDir = prRunOutputDir({
|
|
484
|
-
config: input.config,
|
|
485
|
-
directory: this.input.directory,
|
|
486
|
-
pr: input.pr,
|
|
487
|
-
runId,
|
|
488
|
-
});
|
|
489
|
-
const createdAt = now();
|
|
490
|
-
const editor = input.repository.agents.editor;
|
|
491
|
-
const state = {
|
|
492
|
-
command: "merge",
|
|
493
|
-
createdAt,
|
|
494
|
-
dryRun: input.dryRun,
|
|
495
|
-
editor: editor
|
|
496
|
-
? {
|
|
497
|
-
account: editor.account,
|
|
498
|
-
repairAttempts: 0,
|
|
499
|
-
status: "pending",
|
|
500
|
-
toolCalls: 0,
|
|
501
|
-
}
|
|
502
|
-
: undefined,
|
|
503
|
-
outputDir,
|
|
504
|
-
parentSessionId: input.parentSessionId,
|
|
505
|
-
phase: "queued",
|
|
506
|
-
pr: input.pr,
|
|
507
|
-
prUrl: prUrl(input.repository, input.pr),
|
|
508
|
-
repository: input.repository.alias,
|
|
509
|
-
reviewers: Object.fromEntries(input.repository.agents.reviewers.map((reviewer) => [
|
|
510
|
-
reviewer.key,
|
|
511
|
-
{
|
|
512
|
-
account: reviewer.account,
|
|
513
|
-
repairAttempts: 0,
|
|
514
|
-
status: "pending",
|
|
515
|
-
toolCalls: 0,
|
|
516
|
-
},
|
|
517
|
-
])),
|
|
518
|
-
runId,
|
|
519
|
-
status: "preparing",
|
|
520
|
-
updatedAt: createdAt,
|
|
521
|
-
};
|
|
522
|
-
this.active.set(runId, state);
|
|
523
|
-
this.runPaths.set(runId, join(outputDir, "state.json"));
|
|
524
|
-
for (const dir of outputBaseDirs(this.input.directory, input.config))
|
|
525
|
-
this.outputDirs.add(dir);
|
|
526
|
-
await this.persist(state);
|
|
527
|
-
await this.notify(state, `Started Magi merge for ${prMarkdownLink(state)}.`);
|
|
528
|
-
const controller = new AbortController();
|
|
529
|
-
this.controllers.set(runId, controller);
|
|
530
|
-
const execute = () => this.executeMerge({
|
|
531
|
-
...input,
|
|
532
|
-
runId,
|
|
533
|
-
signal: controller.signal,
|
|
534
|
-
});
|
|
535
|
-
if (input.sync)
|
|
536
|
-
return this.executeSync(state, controller, execute, input.timeoutMs);
|
|
537
|
-
this.prQueue.push({
|
|
538
|
-
execute,
|
|
539
|
-
repository: input.repository,
|
|
540
|
-
runId,
|
|
541
|
-
});
|
|
542
|
-
this.drainPrQueue();
|
|
543
|
-
return state;
|
|
544
|
-
}
|
|
545
|
-
async startTriage(input) {
|
|
546
|
-
const runId = createRunId();
|
|
547
|
-
const outputDir = issueRunOutputDir({
|
|
548
|
-
config: input.config,
|
|
549
|
-
directory: this.input.directory,
|
|
550
|
-
issue: input.issue,
|
|
551
|
-
runId,
|
|
552
|
-
});
|
|
553
|
-
const createdAt = now();
|
|
554
|
-
const state = {
|
|
555
|
-
command: "triage",
|
|
556
|
-
createdAt,
|
|
557
|
-
dryRun: input.dryRun,
|
|
558
|
-
issue: input.issue,
|
|
559
|
-
issueUrl: issueUrl(input.repository, input.issue),
|
|
560
|
-
outputDir,
|
|
561
|
-
parentSessionId: input.parentSessionId,
|
|
562
|
-
phase: "queued",
|
|
563
|
-
repository: input.repository.alias,
|
|
564
|
-
reviewers: Object.fromEntries((input.repository.agents.triage ?? []).map((agent) => [
|
|
565
|
-
agent.key,
|
|
566
|
-
{
|
|
567
|
-
account: "",
|
|
568
|
-
repairAttempts: 0,
|
|
569
|
-
status: "pending",
|
|
570
|
-
toolCalls: 0,
|
|
571
|
-
},
|
|
572
|
-
])),
|
|
573
|
-
runId,
|
|
574
|
-
status: "preparing",
|
|
575
|
-
triageCreator: input.repository.agents.triageCreator
|
|
576
|
-
? {
|
|
577
|
-
account: input.repository.agents.triageCreator.account,
|
|
578
|
-
repairAttempts: 0,
|
|
579
|
-
status: "pending",
|
|
580
|
-
toolCalls: 0,
|
|
581
|
-
}
|
|
582
|
-
: undefined,
|
|
583
|
-
updatedAt: createdAt,
|
|
584
|
-
};
|
|
585
|
-
this.active.set(runId, state);
|
|
586
|
-
this.runPaths.set(runId, join(outputDir, "state.json"));
|
|
587
|
-
for (const dir of outputBaseDirs(this.input.directory, input.config))
|
|
588
|
-
this.outputDirs.add(dir);
|
|
589
|
-
await this.persist(state);
|
|
590
|
-
await this.notify(state, `Started Magi triage for ${issueMarkdownLink(state)}.`);
|
|
591
|
-
const controller = new AbortController();
|
|
592
|
-
this.controllers.set(runId, controller);
|
|
593
|
-
const execute = () => this.executeTriage({
|
|
594
|
-
...input,
|
|
595
|
-
runId,
|
|
596
|
-
signal: controller.signal,
|
|
597
|
-
});
|
|
598
|
-
if (input.sync)
|
|
599
|
-
return this.executeSync(state, controller, execute, input.timeoutMs);
|
|
600
|
-
this.triageQueue.push({
|
|
601
|
-
execute,
|
|
602
|
-
repository: input.repository,
|
|
603
|
-
runId,
|
|
604
|
-
});
|
|
605
|
-
this.drainTriageQueue();
|
|
606
|
-
return state;
|
|
607
|
-
}
|
|
608
|
-
drainPrQueue() {
|
|
609
|
-
while (this.prQueue.length) {
|
|
610
|
-
const next = this.prQueue[0];
|
|
611
|
-
if (!next)
|
|
612
|
-
return;
|
|
613
|
-
if (this.activePrRuns >= next.repository.concurrency.runs)
|
|
614
|
-
return;
|
|
615
|
-
this.prQueue.shift();
|
|
616
|
-
const state = this.active.get(next.runId);
|
|
617
|
-
if (!state || state.status === "cancelled")
|
|
618
|
-
continue;
|
|
619
|
-
this.activePrRuns += 1;
|
|
620
|
-
void next
|
|
621
|
-
.execute()
|
|
622
|
-
.catch(async (error) => {
|
|
623
|
-
await this.failRun(next.runId, error);
|
|
624
|
-
})
|
|
625
|
-
.finally(() => {
|
|
626
|
-
this.activePrRuns -= 1;
|
|
627
|
-
this.drainPrQueue();
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
drainTriageQueue() {
|
|
632
|
-
while (this.triageQueue.length) {
|
|
633
|
-
const next = this.triageQueue[0];
|
|
634
|
-
if (!next)
|
|
635
|
-
return;
|
|
636
|
-
const limit = next.repository.triage?.concurrency.runs ?? 1;
|
|
637
|
-
if (this.activeTriageRuns >= limit)
|
|
638
|
-
return;
|
|
639
|
-
this.triageQueue.shift();
|
|
640
|
-
const state = this.active.get(next.runId);
|
|
641
|
-
if (!state || state.status === "cancelled")
|
|
642
|
-
continue;
|
|
643
|
-
this.activeTriageRuns += 1;
|
|
644
|
-
void next
|
|
645
|
-
.execute()
|
|
646
|
-
.catch(async (error) => {
|
|
647
|
-
await this.failRun(next.runId, error);
|
|
648
|
-
})
|
|
649
|
-
.finally(() => {
|
|
650
|
-
this.activeTriageRuns -= 1;
|
|
651
|
-
this.drainTriageQueue();
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
async status(input = {}) {
|
|
656
|
-
const timeoutMs = input.timeoutMs;
|
|
657
|
-
const startedAt = Date.now();
|
|
658
|
-
while (input.block) {
|
|
659
|
-
const states = await this.filteredStates(input);
|
|
660
|
-
if (states.length &&
|
|
661
|
-
hasAllRequestedPrStates(states, input.pr) &&
|
|
662
|
-
states.every((state) => !isActiveStatus(state.status)))
|
|
663
|
-
return states;
|
|
664
|
-
if (timeoutMs != null && Date.now() - startedAt >= timeoutMs)
|
|
665
|
-
return states;
|
|
666
|
-
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
667
|
-
}
|
|
668
|
-
return this.filteredStates(input);
|
|
669
|
-
}
|
|
670
|
-
hasSession(sessionId) {
|
|
671
|
-
return this.sessionToRun.has(sessionId);
|
|
672
|
-
}
|
|
673
|
-
async output(input) {
|
|
674
|
-
const state = await this.selectState(input);
|
|
675
|
-
if (!state)
|
|
676
|
-
return `Magi run not found: ${this.selectorText(input)}`;
|
|
677
|
-
if (input.reviewer) {
|
|
678
|
-
const reviewer = this.agentState(state, input.reviewer);
|
|
679
|
-
if (!reviewer)
|
|
680
|
-
return `Agent not found in ${this.selectorText(input)}: ${input.reviewer}`;
|
|
681
|
-
const sections = [
|
|
682
|
-
`# ${input.reviewer}`,
|
|
683
|
-
`status: ${reviewer.status}`,
|
|
684
|
-
reviewer.sessionId ? `session: ${reviewer.sessionId}` : undefined,
|
|
685
|
-
reviewer.verdict ? `verdict: ${reviewer.verdict}` : undefined,
|
|
686
|
-
reviewer.error ? `error: ${reviewer.error}` : undefined,
|
|
687
|
-
].filter(Boolean);
|
|
688
|
-
if (reviewer.parsedPath) {
|
|
689
|
-
sections.push("\n## Parsed");
|
|
690
|
-
sections.push(await readFile(reviewer.parsedPath, "utf8").catch(() => "(missing parsed artifact)"));
|
|
691
|
-
}
|
|
692
|
-
if (reviewer.rawPath) {
|
|
693
|
-
sections.push("\n## Raw");
|
|
694
|
-
sections.push(await readFile(reviewer.rawPath, "utf8").catch(() => "(missing raw artifact)"));
|
|
695
|
-
}
|
|
696
|
-
return sections.join("\n");
|
|
697
|
-
}
|
|
698
|
-
const sections = [this.formatStates([state], { verbose: true })];
|
|
699
|
-
if (state.reportPath) {
|
|
700
|
-
sections.push("\n## Report");
|
|
701
|
-
sections.push(await readFile(state.reportPath, "utf8").catch(() => "(missing report artifact)"));
|
|
702
|
-
}
|
|
703
|
-
const output = sections.join("\n");
|
|
704
|
-
return output;
|
|
705
|
-
}
|
|
706
|
-
async cancel(input) {
|
|
707
|
-
const selector = typeof input === "string" ? { runId: input } : input;
|
|
708
|
-
const state = await this.selectState(selector);
|
|
709
|
-
if (!state)
|
|
710
|
-
return undefined;
|
|
711
|
-
const runId = state.runId;
|
|
712
|
-
this.controllers.get(runId)?.abort();
|
|
713
|
-
state.status = "cancelled";
|
|
714
|
-
state.phase = "cancelled";
|
|
715
|
-
state.completedAt = now();
|
|
716
|
-
await this.finishActiveAgents(state, "cancelled");
|
|
717
|
-
if (state.worktreePath) {
|
|
718
|
-
await removeWorktree(this.input.exec, state.worktreePath).catch(() => undefined);
|
|
719
|
-
}
|
|
720
|
-
await this.persist(state);
|
|
721
|
-
await this.notify(state, `Cancelled ${state.command} for ${runLabel(state)}.`, { reply: true });
|
|
722
|
-
this.active.delete(runId);
|
|
723
|
-
this.controllers.delete(runId);
|
|
724
|
-
return state;
|
|
725
|
-
}
|
|
726
|
-
async clear(input) {
|
|
727
|
-
const configured = input.options ?? {};
|
|
728
|
-
const options = {
|
|
729
|
-
branch: configured.branch ?? DEFAULT_CLEAR_OPTIONS.branch,
|
|
730
|
-
output: configured.output ?? DEFAULT_CLEAR_OPTIONS.output,
|
|
731
|
-
session: configured.session ?? DEFAULT_CLEAR_OPTIONS.session,
|
|
732
|
-
worktree: configured.worktree ?? DEFAULT_CLEAR_OPTIONS.worktree,
|
|
733
|
-
};
|
|
734
|
-
const states = await this.filteredStates(input);
|
|
735
|
-
const cleanupDirs = new Set(this.absoluteWorktreeDirs(input));
|
|
736
|
-
const cleanupTrees = new Set(this.emptyOutputCleanupRoots(input));
|
|
737
|
-
const summary = {
|
|
738
|
-
branchDeleted: 0,
|
|
739
|
-
branchFailed: 0,
|
|
740
|
-
branchSkipped: 0,
|
|
741
|
-
outputDeleted: 0,
|
|
742
|
-
outputFailed: 0,
|
|
743
|
-
runsCleared: 0,
|
|
744
|
-
runsSkippedActive: 0,
|
|
745
|
-
sessionDeleted: 0,
|
|
746
|
-
sessionFailed: 0,
|
|
747
|
-
worktreeDeleted: 0,
|
|
748
|
-
worktreeFailed: 0,
|
|
749
|
-
};
|
|
750
|
-
const lines = [];
|
|
751
|
-
if (!states.length) {
|
|
752
|
-
await this.pruneEmptyMagiDirectories({
|
|
753
|
-
dirs: cleanupDirs,
|
|
754
|
-
trees: cleanupTrees,
|
|
755
|
-
});
|
|
756
|
-
return `No Magi runs found: ${this.selectorText(input)}`;
|
|
757
|
-
}
|
|
758
|
-
for (const state of states) {
|
|
759
|
-
if (isActiveStatus(state.status)) {
|
|
760
|
-
summary.runsSkippedActive += 1;
|
|
761
|
-
lines.push(`Skipped active run ${state.runId} for ${runLabel(state)}: ${state.status}`);
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
if (options.session) {
|
|
765
|
-
if (!this.input.client.session.delete) {
|
|
766
|
-
summary.sessionFailed += this.collectSessionIds(state).length;
|
|
767
|
-
lines.push("OpenCode client does not support session deletion.");
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
for (const sessionId of this.collectSessionIds(state)) {
|
|
771
|
-
try {
|
|
772
|
-
await this.input.client.session.delete({
|
|
773
|
-
path: { id: sessionId },
|
|
774
|
-
});
|
|
775
|
-
summary.sessionDeleted += 1;
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
summary.sessionFailed += 1;
|
|
779
|
-
lines.push(`Failed to delete session ${sessionId}: ${error.message}`);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
if (options.worktree && state.worktreePath) {
|
|
785
|
-
let removed = false;
|
|
786
|
-
let unregisterError;
|
|
787
|
-
try {
|
|
788
|
-
await removeWorktree(this.input.exec, state.worktreePath);
|
|
789
|
-
removed = true;
|
|
790
|
-
}
|
|
791
|
-
catch (error) {
|
|
792
|
-
unregisterError = error;
|
|
793
|
-
}
|
|
794
|
-
try {
|
|
795
|
-
await rm(state.worktreePath, { force: true, recursive: true });
|
|
796
|
-
removed = true;
|
|
797
|
-
}
|
|
798
|
-
catch (error) {
|
|
799
|
-
summary.worktreeFailed += 1;
|
|
800
|
-
if (unregisterError) {
|
|
801
|
-
lines.push(`Failed to unregister worktree ${state.worktreePath}: ${unregisterError.message}`);
|
|
802
|
-
}
|
|
803
|
-
lines.push(`Failed to delete worktree directory ${state.worktreePath}: ${error.message}`);
|
|
804
|
-
}
|
|
805
|
-
if (removed)
|
|
806
|
-
summary.worktreeDeleted += 1;
|
|
807
|
-
cleanupDirs.add(state.worktreePath);
|
|
808
|
-
}
|
|
809
|
-
if (options.branch) {
|
|
810
|
-
if (state.worktreeBranch) {
|
|
811
|
-
try {
|
|
812
|
-
await removeBranch(this.input.exec, state.worktreeBranch);
|
|
813
|
-
summary.branchDeleted += 1;
|
|
814
|
-
}
|
|
815
|
-
catch (error) {
|
|
816
|
-
summary.branchFailed += 1;
|
|
817
|
-
lines.push(`Failed to delete branch ${state.worktreeBranch}: ${error.message}`);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
else {
|
|
821
|
-
summary.branchSkipped += 1;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
if (options.output) {
|
|
825
|
-
try {
|
|
826
|
-
await rm(state.outputDir, { force: true, recursive: true });
|
|
827
|
-
summary.outputDeleted += 1;
|
|
828
|
-
cleanupDirs.add(state.outputDir);
|
|
829
|
-
}
|
|
830
|
-
catch (error) {
|
|
831
|
-
summary.outputFailed += 1;
|
|
832
|
-
lines.push(`Failed to delete output ${state.outputDir}: ${error.message}`);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
this.active.delete(state.runId);
|
|
836
|
-
this.controllers.delete(state.runId);
|
|
837
|
-
this.runPaths.delete(state.runId);
|
|
838
|
-
summary.runsCleared += 1;
|
|
839
|
-
lines.push(`Cleared run ${state.runId} for ${runLabel(state)}.`);
|
|
840
|
-
}
|
|
841
|
-
await this.pruneEmptyMagiDirectories({
|
|
842
|
-
dirs: cleanupDirs,
|
|
843
|
-
trees: cleanupTrees,
|
|
844
|
-
});
|
|
845
|
-
return this.formatClearSummary(summary, lines);
|
|
846
|
-
}
|
|
847
|
-
async replyPermission(input) {
|
|
848
|
-
const state = await this.selectState(input);
|
|
849
|
-
if (!state)
|
|
850
|
-
return `Magi run not found: ${this.selectorText(input)}`;
|
|
851
|
-
const selected = this.selectPendingAgent(state, "permission", input.agent, input.requestId);
|
|
852
|
-
if (typeof selected === "string")
|
|
853
|
-
return selected;
|
|
854
|
-
const requestId = input.requestId ?? selected.state.pendingPermission?.id;
|
|
855
|
-
if (!requestId)
|
|
856
|
-
return `Permission request id not found for ${selected.key}.`;
|
|
857
|
-
if (!this.input.client.permission?.reply) {
|
|
858
|
-
return "OpenCode client does not support permission replies.";
|
|
859
|
-
}
|
|
860
|
-
await this.input.client.permission.reply({
|
|
861
|
-
requestID: requestId,
|
|
862
|
-
reply: input.reply,
|
|
863
|
-
});
|
|
864
|
-
selected.state.pendingPermission = undefined;
|
|
865
|
-
if (!selected.state.pendingQuestion &&
|
|
866
|
-
selected.state.status === "blocked") {
|
|
867
|
-
selected.state.status = "running";
|
|
868
|
-
}
|
|
869
|
-
if (state.status === "blocked" && !this.hasBlockedAgents(state)) {
|
|
870
|
-
state.status = "running";
|
|
871
|
-
}
|
|
872
|
-
await this.persist(state);
|
|
873
|
-
return `Replied to permission request ${requestId} for ${selected.key}: ${input.reply}.`;
|
|
874
|
-
}
|
|
875
|
-
async replyQuestion(input) {
|
|
876
|
-
const state = await this.selectState(input);
|
|
877
|
-
if (!state)
|
|
878
|
-
return `Magi run not found: ${this.selectorText(input)}`;
|
|
879
|
-
const selected = this.selectPendingAgent(state, "question", input.agent, input.requestId);
|
|
880
|
-
if (typeof selected === "string")
|
|
881
|
-
return selected;
|
|
882
|
-
const requestId = input.requestId ?? selected.state.pendingQuestion?.id;
|
|
883
|
-
if (!requestId)
|
|
884
|
-
return `Question request id not found for ${selected.key}.`;
|
|
885
|
-
if (!this.input.client.question?.reply) {
|
|
886
|
-
return "OpenCode client does not support question replies.";
|
|
887
|
-
}
|
|
888
|
-
await this.input.client.question.reply({
|
|
889
|
-
answers: input.answers,
|
|
890
|
-
requestID: requestId,
|
|
891
|
-
});
|
|
892
|
-
selected.state.pendingQuestion = undefined;
|
|
893
|
-
if (!selected.state.pendingPermission &&
|
|
894
|
-
selected.state.status === "blocked") {
|
|
895
|
-
selected.state.status = "running";
|
|
896
|
-
}
|
|
897
|
-
if (state.status === "blocked" && !this.hasBlockedAgents(state)) {
|
|
898
|
-
state.status = "running";
|
|
899
|
-
}
|
|
900
|
-
await this.persist(state);
|
|
901
|
-
return `Replied to question request ${requestId} for ${selected.key}.`;
|
|
902
|
-
}
|
|
903
|
-
async rejectQuestion(input) {
|
|
904
|
-
const state = await this.selectState(input);
|
|
905
|
-
if (!state)
|
|
906
|
-
return `Magi run not found: ${this.selectorText(input)}`;
|
|
907
|
-
const selected = this.selectPendingAgent(state, "question", input.agent, input.requestId);
|
|
908
|
-
if (typeof selected === "string")
|
|
909
|
-
return selected;
|
|
910
|
-
const requestId = input.requestId ?? selected.state.pendingQuestion?.id;
|
|
911
|
-
if (!requestId)
|
|
912
|
-
return `Question request id not found for ${selected.key}.`;
|
|
913
|
-
if (!this.input.client.question?.reject) {
|
|
914
|
-
return "OpenCode client does not support question rejection.";
|
|
915
|
-
}
|
|
916
|
-
await this.input.client.question.reject({ requestID: requestId });
|
|
917
|
-
selected.state.pendingQuestion = undefined;
|
|
918
|
-
if (!selected.state.pendingPermission &&
|
|
919
|
-
selected.state.status === "blocked") {
|
|
920
|
-
selected.state.status = "running";
|
|
921
|
-
}
|
|
922
|
-
if (state.status === "blocked" && !this.hasBlockedAgents(state)) {
|
|
923
|
-
state.status = "running";
|
|
924
|
-
}
|
|
925
|
-
await this.persist(state);
|
|
926
|
-
return `Rejected question request ${requestId} for ${selected.key}.`;
|
|
927
|
-
}
|
|
928
|
-
async handleEvent(input) {
|
|
929
|
-
const sessionId = extractSessionId(input.event.properties);
|
|
930
|
-
if (!sessionId)
|
|
931
|
-
return;
|
|
932
|
-
const mapping = this.sessionToRun.get(sessionId);
|
|
933
|
-
if (!mapping)
|
|
934
|
-
return;
|
|
935
|
-
const state = this.active.get(mapping.runId) ??
|
|
936
|
-
(await this.readStateByRunId(mapping.runId));
|
|
937
|
-
if (!state)
|
|
938
|
-
return;
|
|
939
|
-
const agent = this.agentState(state, mapping.agent);
|
|
940
|
-
if (!agent)
|
|
941
|
-
return;
|
|
942
|
-
let dirty = false;
|
|
943
|
-
const receivedAt = now();
|
|
944
|
-
const receivedAtMs = Date.now();
|
|
945
|
-
const markUpdated = (force = false) => {
|
|
946
|
-
const last = this.eventLastUpdates.get(sessionId) ?? 0;
|
|
947
|
-
if (!force && receivedAtMs - last < EVENT_LAST_UPDATE_THROTTLE_MS)
|
|
948
|
-
return;
|
|
949
|
-
agent.lastUpdate = receivedAt;
|
|
950
|
-
this.eventLastUpdates.set(sessionId, receivedAtMs);
|
|
951
|
-
dirty = true;
|
|
952
|
-
};
|
|
953
|
-
const toolPart = extractToolPart(input.event.properties);
|
|
954
|
-
if (toolPart) {
|
|
955
|
-
const counted = this.countedToolParts.get(sessionId) ?? new Set();
|
|
956
|
-
if (!toolPart.id || !counted.has(toolPart.id)) {
|
|
957
|
-
agent.toolCalls += 1;
|
|
958
|
-
if (toolPart.id)
|
|
959
|
-
counted.add(toolPart.id);
|
|
960
|
-
this.countedToolParts.set(sessionId, counted);
|
|
961
|
-
markUpdated(true);
|
|
962
|
-
dirty = true;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
if (input.event.type === "message.part.updated" &&
|
|
966
|
-
toolPart?.tool === "question" &&
|
|
967
|
-
(toolPart.status === "pending" || toolPart.status === "running")) {
|
|
968
|
-
const existing = agent.pendingQuestion;
|
|
969
|
-
const question = {
|
|
970
|
-
id: toolPart.id ?? toolPart.callId,
|
|
971
|
-
questions: extractQuestions(toolPart.input),
|
|
972
|
-
sessionId,
|
|
973
|
-
tool: toolPart.tool,
|
|
974
|
-
};
|
|
975
|
-
agent.pendingQuestion = question;
|
|
976
|
-
agent.status = "blocked";
|
|
977
|
-
state.status = "blocked";
|
|
978
|
-
agent.error = "Question is waiting for an answer.";
|
|
979
|
-
markUpdated(true);
|
|
980
|
-
dirty = true;
|
|
981
|
-
if (!existing) {
|
|
982
|
-
await this.notify(state, questionWaitText({
|
|
983
|
-
agent: mapping.agent,
|
|
984
|
-
pr: runLabel(state),
|
|
985
|
-
question,
|
|
986
|
-
}), { reply: true });
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
if (input.event.type === "permission.asked" ||
|
|
990
|
-
input.event.type === "permission.updated") {
|
|
991
|
-
const permission = extractPermissionRequest(input.event.properties);
|
|
992
|
-
const notified = this.notifiedPermissions.get(sessionId) ?? new Set();
|
|
993
|
-
const permissionId = permission?.id ?? `${mapping.agent}:${Date.now()}`;
|
|
994
|
-
if (!notified.has(permissionId)) {
|
|
995
|
-
notified.add(permissionId);
|
|
996
|
-
this.notifiedPermissions.set(sessionId, notified);
|
|
997
|
-
agent.pendingPermission = permission;
|
|
998
|
-
agent.status = "blocked";
|
|
999
|
-
state.status = "blocked";
|
|
1000
|
-
agent.error = `Permission ${permission?.permission ?? "request"} is waiting for approval.`;
|
|
1001
|
-
markUpdated(true);
|
|
1002
|
-
dirty = true;
|
|
1003
|
-
await this.notify(state, `Magi ${mapping.agent} is waiting for permission on ${runLabel(state)}: ${agent.error}`, { reply: true });
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
if (input.event.type === "question.asked") {
|
|
1007
|
-
const question = extractQuestionRequest(input.event.properties);
|
|
1008
|
-
const notified = this.notifiedPermissions.get(sessionId) ?? new Set();
|
|
1009
|
-
const questionId = question?.id ?? `${mapping.agent}:${Date.now()}`;
|
|
1010
|
-
if (agent.pendingQuestion) {
|
|
1011
|
-
agent.pendingQuestion = question;
|
|
1012
|
-
markUpdated(true);
|
|
1013
|
-
dirty = true;
|
|
1014
|
-
}
|
|
1015
|
-
else if (!notified.has(questionId)) {
|
|
1016
|
-
notified.add(questionId);
|
|
1017
|
-
this.notifiedPermissions.set(sessionId, notified);
|
|
1018
|
-
agent.pendingQuestion = question;
|
|
1019
|
-
agent.status = "blocked";
|
|
1020
|
-
state.status = "blocked";
|
|
1021
|
-
agent.error = "Question is waiting for an answer.";
|
|
1022
|
-
markUpdated(true);
|
|
1023
|
-
dirty = true;
|
|
1024
|
-
await this.notify(state, questionWaitText({
|
|
1025
|
-
agent: mapping.agent,
|
|
1026
|
-
pr: runLabel(state),
|
|
1027
|
-
question,
|
|
1028
|
-
}), { reply: true });
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
if ((input.event.type === "permission.replied" ||
|
|
1032
|
-
input.event.type === "permission.rejected") &&
|
|
1033
|
-
agent.pendingPermission) {
|
|
1034
|
-
agent.pendingPermission = undefined;
|
|
1035
|
-
if (!agent.pendingQuestion && agent.status === "blocked") {
|
|
1036
|
-
agent.status = "running";
|
|
1037
|
-
}
|
|
1038
|
-
if (state.status === "blocked" && !this.hasBlockedAgents(state)) {
|
|
1039
|
-
state.status = "running";
|
|
1040
|
-
}
|
|
1041
|
-
markUpdated(true);
|
|
1042
|
-
dirty = true;
|
|
1043
|
-
}
|
|
1044
|
-
if ((input.event.type === "question.replied" ||
|
|
1045
|
-
input.event.type === "question.rejected") &&
|
|
1046
|
-
agent.pendingQuestion) {
|
|
1047
|
-
agent.pendingQuestion = undefined;
|
|
1048
|
-
if (!agent.pendingPermission && agent.status === "blocked") {
|
|
1049
|
-
agent.status = "running";
|
|
1050
|
-
}
|
|
1051
|
-
if (state.status === "blocked" && !this.hasBlockedAgents(state)) {
|
|
1052
|
-
state.status = "running";
|
|
1053
|
-
}
|
|
1054
|
-
markUpdated(true);
|
|
1055
|
-
dirty = true;
|
|
1056
|
-
}
|
|
1057
|
-
if (input.event.type === "message.part.updated" &&
|
|
1058
|
-
toolPart?.tool === "question" &&
|
|
1059
|
-
(toolPart.status === "completed" || toolPart.status === "error") &&
|
|
1060
|
-
agent.pendingQuestion?.tool === "question") {
|
|
1061
|
-
agent.pendingQuestion = undefined;
|
|
1062
|
-
if (!agent.pendingPermission && agent.status === "blocked") {
|
|
1063
|
-
agent.status = "running";
|
|
1064
|
-
}
|
|
1065
|
-
if (state.status === "blocked" && !this.hasBlockedAgents(state)) {
|
|
1066
|
-
state.status = "running";
|
|
1067
|
-
}
|
|
1068
|
-
markUpdated(true);
|
|
1069
|
-
dirty = true;
|
|
1070
|
-
}
|
|
1071
|
-
if (input.event.type === "permission.replied" &&
|
|
1072
|
-
agent.status === "blocked") {
|
|
1073
|
-
agent.status = "running";
|
|
1074
|
-
markUpdated(true);
|
|
1075
|
-
dirty = true;
|
|
1076
|
-
}
|
|
1077
|
-
if (input.event.type === "session.error") {
|
|
1078
|
-
agent.status = "failed";
|
|
1079
|
-
agent.error = redactSecrets(JSON.stringify(input.event.properties?.error ?? "session error"));
|
|
1080
|
-
markUpdated(true);
|
|
1081
|
-
dirty = true;
|
|
1082
|
-
}
|
|
1083
|
-
if (!dirty)
|
|
1084
|
-
markUpdated();
|
|
1085
|
-
if (dirty)
|
|
1086
|
-
await this.persist(state);
|
|
1087
|
-
}
|
|
1088
|
-
formatStates(states, options = {}) {
|
|
1089
|
-
if (!states.length)
|
|
1090
|
-
return "No Magi runs found.";
|
|
1091
|
-
return states
|
|
1092
|
-
.map((state) => {
|
|
1093
|
-
const editorLine = state.editor
|
|
1094
|
-
? this.formatAgentLine("editor", state.editor, options)
|
|
1095
|
-
: undefined;
|
|
1096
|
-
const triageCreatorLine = state.triageCreator
|
|
1097
|
-
? this.formatAgentLine("triageCreator", state.triageCreator, options)
|
|
1098
|
-
: undefined;
|
|
1099
|
-
const reviewerLines = Object.entries(state.reviewers).map(([key, reviewer]) => {
|
|
1100
|
-
return this.formatAgentLine(key, reviewer, options);
|
|
1101
|
-
});
|
|
1102
|
-
const classifierLines = Object.entries(state.ciClassifiers ?? {}).map(([key, classifier]) => this.formatAgentLine(`ci:${key}`, classifier, options));
|
|
1103
|
-
const lines = [
|
|
1104
|
-
options.verbose ? `Run: ${state.runId}` : undefined,
|
|
1105
|
-
state.pr == null ? undefined : `PR: #${state.pr}`,
|
|
1106
|
-
state.issue == null ? undefined : `Issue: #${state.issue}`,
|
|
1107
|
-
`Command: ${state.command}`,
|
|
1108
|
-
state.dryRun ? "Dry run: true" : undefined,
|
|
1109
|
-
`Status: ${state.status}`,
|
|
1110
|
-
`Phase: ${state.phase}`,
|
|
1111
|
-
state.verdict ? `Verdict: ${state.verdict}` : undefined,
|
|
1112
|
-
state.error ? `Error: ${state.error}` : undefined,
|
|
1113
|
-
...(state.ciReports ?? []).flatMap((report) => [
|
|
1114
|
-
...report.scopeOutsideRecovered.map((item) => `CI scope outside recovered: ${item.check.name}${report.attempts ? ` (recovered after ${report.attempts} retry attempt${report.attempts === 1 ? "" : "s"})` : ""} - ${item.reason}`),
|
|
1115
|
-
...report.scopeOutsideUnresolved.map((item) => `CI scope outside unresolved: ${item.check.name}${report.attempts ? ` (${report.attempts} retry attempt${report.attempts === 1 ? "" : "s"})` : ""} - ${item.reason}`),
|
|
1116
|
-
...report.scopeInside.map((item) => `CI scope inside: ${item.check.name} - ${item.reason}`),
|
|
1117
|
-
]),
|
|
1118
|
-
...(state.warnings ?? []).map((warning) => `Warning: ${warning}`),
|
|
1119
|
-
options.verbose && state.threadAttempts
|
|
1120
|
-
? `Thread attempts: ${Object.keys(state.threadAttempts).length} tracked, ${Object.values(state.threadAttempts).filter((attempt) => attempt.exhaustedAtCycle != null).length} exhausted`
|
|
1121
|
-
: undefined,
|
|
1122
|
-
options.verbose ? `Artifacts: ${state.outputDir}` : undefined,
|
|
1123
|
-
options.verbose && state.reportPath
|
|
1124
|
-
? `Report: ${state.reportPath}`
|
|
1125
|
-
: undefined,
|
|
1126
|
-
editorLine,
|
|
1127
|
-
triageCreatorLine,
|
|
1128
|
-
...classifierLines,
|
|
1129
|
-
...reviewerLines,
|
|
1130
|
-
];
|
|
1131
|
-
return lines.filter(Boolean).join("\n");
|
|
1132
|
-
})
|
|
1133
|
-
.join("\n\n");
|
|
1134
|
-
}
|
|
1135
|
-
async formatStatesWithReports(states, options = {}) {
|
|
1136
|
-
const sections = [this.formatStates(states, options)];
|
|
1137
|
-
if (!options.verbose)
|
|
1138
|
-
return sections[0];
|
|
1139
|
-
for (const state of states) {
|
|
1140
|
-
if (state.status !== "completed" || !state.reportPath)
|
|
1141
|
-
continue;
|
|
1142
|
-
const report = await readFile(state.reportPath, "utf8").catch(() => "(missing report artifact)");
|
|
1143
|
-
sections.push([
|
|
1144
|
-
`Report for ${runLabel(state)} (${state.runId}):`,
|
|
1145
|
-
"",
|
|
1146
|
-
report.trimEnd(),
|
|
1147
|
-
].join("\n"));
|
|
1148
|
-
}
|
|
1149
|
-
return sections.join("\n\n");
|
|
1150
|
-
}
|
|
1151
|
-
collectSessionIds(state) {
|
|
1152
|
-
const ids = [
|
|
1153
|
-
state.editor?.sessionId,
|
|
1154
|
-
state.triageCreator?.sessionId,
|
|
1155
|
-
...Object.values(state.reviewers).map((reviewer) => reviewer.sessionId),
|
|
1156
|
-
...Object.values(state.ciClassifiers ?? {}).map((classifier) => classifier.sessionId),
|
|
1157
|
-
...Object.values(state.sessionIds ?? {}),
|
|
1158
|
-
...(state.ciReports ?? []).flatMap((report) => (report.classifierRuns ?? []).map((run) => run.sessionId)),
|
|
1159
|
-
].filter((id) => Boolean(id));
|
|
1160
|
-
return [...new Set(ids)];
|
|
1161
|
-
}
|
|
1162
|
-
formatClearSummary(summary, lines) {
|
|
1163
|
-
return [
|
|
1164
|
-
`Cleared Magi runs: ${summary.runsCleared}`,
|
|
1165
|
-
`Skipped active runs: ${summary.runsSkippedActive}`,
|
|
1166
|
-
`Sessions deleted: ${summary.sessionDeleted}${summary.sessionFailed ? ` (${summary.sessionFailed} failed)` : ""}`,
|
|
1167
|
-
`Worktrees deleted: ${summary.worktreeDeleted}${summary.worktreeFailed ? ` (${summary.worktreeFailed} failed)` : ""}`,
|
|
1168
|
-
`Branches deleted: ${summary.branchDeleted}${summary.branchFailed ? ` (${summary.branchFailed} failed)` : ""}${summary.branchSkipped ? `, ${summary.branchSkipped} skipped` : ""}`,
|
|
1169
|
-
`Outputs deleted: ${summary.outputDeleted}${summary.outputFailed ? ` (${summary.outputFailed} failed)` : ""}`,
|
|
1170
|
-
...lines.map((line) => `- ${line}`),
|
|
1171
|
-
].join("\n");
|
|
1172
|
-
}
|
|
1173
|
-
formatAgentLine(key, agent, options) {
|
|
1174
|
-
const details = [
|
|
1175
|
-
agent.verdict,
|
|
1176
|
-
options.verbose && agent.sessionId
|
|
1177
|
-
? `session=${agent.sessionId}`
|
|
1178
|
-
: undefined,
|
|
1179
|
-
options.verbose && agent.toolCalls
|
|
1180
|
-
? `tools=${agent.toolCalls}`
|
|
1181
|
-
: undefined,
|
|
1182
|
-
options.verbose && agent.repairAttempts
|
|
1183
|
-
? `repairs=${agent.repairAttempts}`
|
|
1184
|
-
: undefined,
|
|
1185
|
-
options.verbose && agent.pendingPermission
|
|
1186
|
-
? `pendingPermission=${agent.pendingPermission.id ?? agent.pendingPermission.permission ?? "unknown"}`
|
|
1187
|
-
: undefined,
|
|
1188
|
-
options.verbose && agent.pendingQuestion
|
|
1189
|
-
? `pendingQuestion=${agent.pendingQuestion.id ?? "unknown"}`
|
|
1190
|
-
: undefined,
|
|
1191
|
-
].filter(Boolean);
|
|
1192
|
-
return `- ${key}: ${agent.status}${details.length ? ` (${details.join(", ")})` : ""}`;
|
|
1193
|
-
}
|
|
1194
|
-
agentState(state, key) {
|
|
1195
|
-
if (key.startsWith("ci:"))
|
|
1196
|
-
return state.ciClassifiers?.[key.slice(3)];
|
|
1197
|
-
if (key === "editor")
|
|
1198
|
-
return state.editor;
|
|
1199
|
-
if (key === "triageCreator")
|
|
1200
|
-
return state.triageCreator;
|
|
1201
|
-
return state.reviewers[key];
|
|
1202
|
-
}
|
|
1203
|
-
agentEntries(state) {
|
|
1204
|
-
return [
|
|
1205
|
-
...(state.editor
|
|
1206
|
-
? [["editor", state.editor]]
|
|
1207
|
-
: []),
|
|
1208
|
-
...(state.triageCreator
|
|
1209
|
-
? [
|
|
1210
|
-
["triageCreator", state.triageCreator],
|
|
1211
|
-
]
|
|
1212
|
-
: []),
|
|
1213
|
-
...Object.entries(state.ciClassifiers ?? {}).map(([key, value]) => [`ci:${key}`, value]),
|
|
1214
|
-
...Object.entries(state.reviewers),
|
|
1215
|
-
];
|
|
1216
|
-
}
|
|
1217
|
-
isActiveAgent(agent) {
|
|
1218
|
-
return (agent.status === "pending" ||
|
|
1219
|
-
agent.status === "running" ||
|
|
1220
|
-
agent.status === "repairing" ||
|
|
1221
|
-
agent.status === "blocked");
|
|
1222
|
-
}
|
|
1223
|
-
async finishActiveAgents(state, status, error) {
|
|
1224
|
-
for (const [, agent] of this.agentEntries(state)) {
|
|
1225
|
-
if (this.isActiveAgent(agent)) {
|
|
1226
|
-
agent.status = status;
|
|
1227
|
-
if (error != null)
|
|
1228
|
-
agent.error = error;
|
|
1229
|
-
}
|
|
1230
|
-
if (agent.sessionId) {
|
|
1231
|
-
await this.input.client.session
|
|
1232
|
-
.abort?.({ path: { id: agent.sessionId } })
|
|
1233
|
-
.catch(() => undefined);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
selectPendingAgent(state, kind, key, requestId) {
|
|
1238
|
-
const entries = key
|
|
1239
|
-
? this.agentState(state, key)
|
|
1240
|
-
? [[key, this.agentState(state, key)]]
|
|
1241
|
-
: []
|
|
1242
|
-
: this.agentEntries(state);
|
|
1243
|
-
const matches = entries.filter(([, agent]) => {
|
|
1244
|
-
const pending = kind === "permission" ? agent.pendingPermission : agent.pendingQuestion;
|
|
1245
|
-
if (!pending)
|
|
1246
|
-
return false;
|
|
1247
|
-
return !requestId || pending.id === requestId;
|
|
1248
|
-
});
|
|
1249
|
-
if (!matches.length) {
|
|
1250
|
-
return key
|
|
1251
|
-
? `No pending ${kind} request found for ${key}.`
|
|
1252
|
-
: `No pending ${kind} request found for ${runLabel(state)}.`;
|
|
1253
|
-
}
|
|
1254
|
-
if (matches.length > 1) {
|
|
1255
|
-
return `Multiple pending ${kind} requests found for ${runLabel(state)}. Specify agent or requestId.`;
|
|
1256
|
-
}
|
|
1257
|
-
return { key: matches[0][0], state: matches[0][1] };
|
|
1258
|
-
}
|
|
1259
|
-
hasBlockedAgents(state) {
|
|
1260
|
-
return this.agentEntries(state).some(([, agent]) => agent.status === "blocked");
|
|
1261
|
-
}
|
|
1262
|
-
async executeSync(state, controller, execute, timeoutMs) {
|
|
1263
|
-
let timeout;
|
|
1264
|
-
const timeoutPromise = timeoutMs == null
|
|
1265
|
-
? undefined
|
|
1266
|
-
: new Promise((resolve) => {
|
|
1267
|
-
timeout = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
1268
|
-
});
|
|
1269
|
-
try {
|
|
1270
|
-
const result = await (timeoutPromise
|
|
1271
|
-
? Promise.race([
|
|
1272
|
-
execute().then(() => "completed"),
|
|
1273
|
-
timeoutPromise,
|
|
1274
|
-
])
|
|
1275
|
-
: execute().then(() => "completed"));
|
|
1276
|
-
if (result === "timeout") {
|
|
1277
|
-
const timeoutSeconds = (timeoutMs ?? 0) / 1_000;
|
|
1278
|
-
controller.abort();
|
|
1279
|
-
await this.failRun(state.runId, new Error(`Magi sync run timed out after ${timeoutSeconds} seconds.`));
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
catch (error) {
|
|
1283
|
-
controller.abort();
|
|
1284
|
-
await this.failRun(state.runId, error);
|
|
1285
|
-
}
|
|
1286
|
-
finally {
|
|
1287
|
-
if (timeout)
|
|
1288
|
-
clearTimeout(timeout);
|
|
1289
|
-
}
|
|
1290
|
-
return (await this.readStateByRunId(state.runId)) ?? state;
|
|
1291
|
-
}
|
|
1292
|
-
assertSuccessfulSyncFollowUp(state) {
|
|
1293
|
-
if (state.status === "completed")
|
|
1294
|
-
return;
|
|
1295
|
-
throw new Error(`Synchronous follow-up ${state.command} run ${state.runId} finished with status ${state.status}.`);
|
|
1296
|
-
}
|
|
1297
|
-
async executeReview(input) {
|
|
1298
|
-
const result = await runReview({
|
|
1299
|
-
approvalPolicy: input.repository.merge.approvalPolicy,
|
|
1300
|
-
client: this.input.client,
|
|
1301
|
-
config: input.config,
|
|
1302
|
-
directory: this.input.directory,
|
|
1303
|
-
dryRun: input.dryRun,
|
|
1304
|
-
exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
|
|
1305
|
-
onProgress: (progress) => this.applyReviewProgress(input.runId, progress),
|
|
1306
|
-
parentSessionId: input.parentSessionId,
|
|
1307
|
-
pr: input.pr,
|
|
1308
|
-
repository: input.repository,
|
|
1309
|
-
runId: input.runId,
|
|
1310
|
-
signal: input.signal,
|
|
1311
|
-
});
|
|
1312
|
-
const state = this.active.get(input.runId);
|
|
1313
|
-
if (!state)
|
|
1314
|
-
return;
|
|
1315
|
-
if (state.status === "cancelled")
|
|
1316
|
-
return;
|
|
1317
|
-
state.status = "completed";
|
|
1318
|
-
state.phase = "completed";
|
|
1319
|
-
state.completedAt = now();
|
|
1320
|
-
state.verdict = result.verdict;
|
|
1321
|
-
state.majority = result.verdict;
|
|
1322
|
-
state.posted = result.posted;
|
|
1323
|
-
state.reportPath = join(state.outputDir, "report.md");
|
|
1324
|
-
state.sessionIds = result.sessionIds;
|
|
1325
|
-
for (const [key, output] of Object.entries(result.outputs)) {
|
|
1326
|
-
const reviewer = state.reviewers[key];
|
|
1327
|
-
if (!reviewer)
|
|
1328
|
-
continue;
|
|
1329
|
-
reviewer.status = "completed";
|
|
1330
|
-
reviewer.verdict = output.verdict;
|
|
1331
|
-
const artifact = "resolve" in output
|
|
1332
|
-
? reviewerArtifactBase("rereview", key)
|
|
1333
|
-
: reviewerArtifactBase("review", key);
|
|
1334
|
-
reviewer.rawPath = join(state.outputDir, `${artifact}.raw.txt`);
|
|
1335
|
-
reviewer.parsedPath = join(state.outputDir, `${artifact}.json`);
|
|
1336
|
-
}
|
|
1337
|
-
for (const [key, posted] of Object.entries(result.posted)) {
|
|
1338
|
-
const reviewer = state.reviewers[key];
|
|
1339
|
-
if (!reviewer || !posted.startsWith("skipped:"))
|
|
1340
|
-
continue;
|
|
1341
|
-
reviewer.status = "skipped";
|
|
1342
|
-
}
|
|
1343
|
-
await this.persist(state);
|
|
1344
|
-
if (result.worktreePath) {
|
|
1345
|
-
await removeWorktree(this.input.exec, result.worktreePath).catch(() => undefined);
|
|
1346
|
-
}
|
|
1347
|
-
if (state.worktreeBranch) {
|
|
1348
|
-
await removeBranch(this.input.exec, state.worktreeBranch).catch(() => undefined);
|
|
1349
|
-
}
|
|
1350
|
-
await this.notify(state, [`Finished reviewing ${prMarkdownLink(state)}.`, "", result.report].join("\n"), { reply: true });
|
|
1351
|
-
this.active.delete(input.runId);
|
|
1352
|
-
this.controllers.delete(input.runId);
|
|
1353
|
-
}
|
|
1354
|
-
async executeMerge(input) {
|
|
1355
|
-
const result = await runMerge({
|
|
1356
|
-
client: this.input.client,
|
|
1357
|
-
config: input.config,
|
|
1358
|
-
directory: this.input.directory,
|
|
1359
|
-
dryRun: input.dryRun,
|
|
1360
|
-
exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
|
|
1361
|
-
onProgress: (progress) => this.applyMergeProgress(input.runId, progress),
|
|
1362
|
-
parentSessionId: input.parentSessionId,
|
|
1363
|
-
pr: input.pr,
|
|
1364
|
-
repository: input.repository,
|
|
1365
|
-
runId: input.runId,
|
|
1366
|
-
signal: input.signal,
|
|
1367
|
-
});
|
|
1368
|
-
const state = this.active.get(input.runId);
|
|
1369
|
-
if (!state)
|
|
1370
|
-
return;
|
|
1371
|
-
if (state.status === "cancelled")
|
|
1372
|
-
return;
|
|
1373
|
-
state.status = "completed";
|
|
1374
|
-
state.phase = result.status;
|
|
1375
|
-
state.completedAt = now();
|
|
1376
|
-
state.verdict = result.status;
|
|
1377
|
-
state.reportPath = join(state.outputDir, "report.md");
|
|
1378
|
-
if (state.editor?.status === "pending")
|
|
1379
|
-
state.editor.status = "skipped";
|
|
1380
|
-
await this.persist(state);
|
|
1381
|
-
await this.notify(state, [
|
|
1382
|
-
`Finished merge workflow for ${prMarkdownLink(state)}.`,
|
|
1383
|
-
"",
|
|
1384
|
-
result.report,
|
|
1385
|
-
].join("\n"), { reply: true });
|
|
1386
|
-
this.active.delete(input.runId);
|
|
1387
|
-
this.controllers.delete(input.runId);
|
|
1388
|
-
}
|
|
1389
|
-
async executeTriage(input) {
|
|
1390
|
-
const state = this.active.get(input.runId);
|
|
1391
|
-
if (state) {
|
|
1392
|
-
state.status = "running";
|
|
1393
|
-
state.phase = "triaging";
|
|
1394
|
-
await this.persist(state);
|
|
1395
|
-
}
|
|
1396
|
-
const result = await runTriage({
|
|
1397
|
-
client: this.input.client,
|
|
1398
|
-
config: input.config,
|
|
1399
|
-
directory: this.input.directory,
|
|
1400
|
-
dryRun: input.dryRun,
|
|
1401
|
-
exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
|
|
1402
|
-
issue: input.issue,
|
|
1403
|
-
onProgress: (progress) => this.applyTriageProgress(input.runId, progress),
|
|
1404
|
-
parentSessionId: input.parentSessionId,
|
|
1405
|
-
repository: input.repository,
|
|
1406
|
-
runId: input.runId,
|
|
1407
|
-
signal: input.signal,
|
|
1408
|
-
});
|
|
1409
|
-
const completed = this.active.get(input.runId);
|
|
1410
|
-
if (!completed || completed.status === "cancelled")
|
|
1411
|
-
return;
|
|
1412
|
-
const triageResult = JSON.stringify(result.result);
|
|
1413
|
-
completed.status =
|
|
1414
|
-
result.result.disposition === "failed" ? "failed" : "completed";
|
|
1415
|
-
completed.phase = triageResult;
|
|
1416
|
-
completed.completedAt = now();
|
|
1417
|
-
completed.verdict = triageResult;
|
|
1418
|
-
completed.reportPath = join(completed.outputDir, "report.md");
|
|
1419
|
-
for (const agent of Object.values(completed.reviewers)) {
|
|
1420
|
-
if (agent.status === "pending")
|
|
1421
|
-
agent.status = "completed";
|
|
1422
|
-
}
|
|
1423
|
-
if (completed.triageCreator?.status === "pending") {
|
|
1424
|
-
completed.triageCreator.status = "skipped";
|
|
1425
|
-
}
|
|
1426
|
-
await this.persist(completed);
|
|
1427
|
-
await this.notify(completed, [
|
|
1428
|
-
`Finished triage for ${issueMarkdownLink(completed)}.`,
|
|
1429
|
-
"",
|
|
1430
|
-
result.report,
|
|
1431
|
-
].join("\n"), { reply: true });
|
|
1432
|
-
const followUpPr = result.prUrl
|
|
1433
|
-
? pullRequestNumberFromUrl(result.prUrl)
|
|
1434
|
-
: undefined;
|
|
1435
|
-
const triageAutomation = input.repository.triage?.automation;
|
|
1436
|
-
if (followUpPr != null && triageAutomation?.merge) {
|
|
1437
|
-
const followUp = await this.startMerge({
|
|
1438
|
-
config: input.config,
|
|
1439
|
-
dryRun: input.dryRun,
|
|
1440
|
-
parentSessionId: input.parentSessionId,
|
|
1441
|
-
pr: followUpPr,
|
|
1442
|
-
repository: input.repository,
|
|
1443
|
-
signal: input.signal,
|
|
1444
|
-
sync: input.sync,
|
|
1445
|
-
timeoutMs: input.timeoutMs,
|
|
1446
|
-
});
|
|
1447
|
-
if (input.sync)
|
|
1448
|
-
this.assertSuccessfulSyncFollowUp(followUp);
|
|
1449
|
-
}
|
|
1450
|
-
else if (followUpPr != null && triageAutomation?.review) {
|
|
1451
|
-
const followUp = await this.startReview({
|
|
1452
|
-
config: input.config,
|
|
1453
|
-
dryRun: input.dryRun,
|
|
1454
|
-
parentSessionId: input.parentSessionId,
|
|
1455
|
-
pr: followUpPr,
|
|
1456
|
-
repository: input.repository,
|
|
1457
|
-
signal: input.signal,
|
|
1458
|
-
sync: input.sync,
|
|
1459
|
-
timeoutMs: input.timeoutMs,
|
|
1460
|
-
});
|
|
1461
|
-
if (input.sync)
|
|
1462
|
-
this.assertSuccessfulSyncFollowUp(followUp);
|
|
1463
|
-
}
|
|
1464
|
-
this.active.delete(input.runId);
|
|
1465
|
-
this.controllers.delete(input.runId);
|
|
1466
|
-
}
|
|
1467
|
-
async applyTriageProgress(runId, progress) {
|
|
1468
|
-
const state = this.active.get(runId);
|
|
1469
|
-
if (!state)
|
|
1470
|
-
return;
|
|
1471
|
-
const issue = issueMarkdownLink(state);
|
|
1472
|
-
const creatorState = () => state.triageCreator ??
|
|
1473
|
-
(state.triageCreator = {
|
|
1474
|
-
account: "triageCreator",
|
|
1475
|
-
repairAttempts: 0,
|
|
1476
|
-
status: "pending",
|
|
1477
|
-
toolCalls: 0,
|
|
1478
|
-
});
|
|
1479
|
-
state.updatedAt = now();
|
|
1480
|
-
if (progress.type === "phase") {
|
|
1481
|
-
state.phase = progress.phase;
|
|
1482
|
-
state.status = "running";
|
|
1483
|
-
}
|
|
1484
|
-
if (progress.type === "decision") {
|
|
1485
|
-
state.phase = `decision: ${progress.result.disposition}`;
|
|
1486
|
-
state.verdict = JSON.stringify(progress.result);
|
|
1487
|
-
}
|
|
1488
|
-
if (progress.type === "comment_posting") {
|
|
1489
|
-
state.phase = "posting triage comment";
|
|
1490
|
-
state.status = "posting";
|
|
1491
|
-
}
|
|
1492
|
-
if (progress.type === "comment_posted") {
|
|
1493
|
-
state.status = "running";
|
|
1494
|
-
}
|
|
1495
|
-
if (progress.type === "pr_creation_started") {
|
|
1496
|
-
state.phase = "creating implementation PR";
|
|
1497
|
-
state.status = "running";
|
|
1498
|
-
}
|
|
1499
|
-
if (progress.type === "worktree_created") {
|
|
1500
|
-
state.worktreePath = progress.worktreePath;
|
|
1501
|
-
state.worktreeBranch = progress.branch;
|
|
1502
|
-
}
|
|
1503
|
-
if (progress.type === "triage_session") {
|
|
1504
|
-
if (progress.options)
|
|
1505
|
-
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1506
|
-
state.sessionIds = {
|
|
1507
|
-
...state.sessionIds,
|
|
1508
|
-
[progress.key]: progress.sessionId,
|
|
1509
|
-
};
|
|
1510
|
-
this.sessionToRun.set(progress.sessionId, {
|
|
1511
|
-
agent: progress.agent,
|
|
1512
|
-
runId,
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
if (progress.type === "triage_agent_started") {
|
|
1516
|
-
const voter = state.reviewers[progress.voter];
|
|
1517
|
-
if (voter)
|
|
1518
|
-
voter.status = "running";
|
|
1519
|
-
}
|
|
1520
|
-
if (progress.type === "triage_agent_session") {
|
|
1521
|
-
const voter = state.reviewers[progress.voter];
|
|
1522
|
-
if (voter) {
|
|
1523
|
-
if (progress.options)
|
|
1524
|
-
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1525
|
-
voter.sessionId = progress.sessionId;
|
|
1526
|
-
voter.status = "running";
|
|
1527
|
-
voter.lastUpdate = now();
|
|
1528
|
-
this.sessionToRun.set(progress.sessionId, {
|
|
1529
|
-
agent: progress.voter,
|
|
1530
|
-
runId,
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
if (progress.type === "triage_agent_repair") {
|
|
1535
|
-
const voter = state.reviewers[progress.voter];
|
|
1536
|
-
if (voter) {
|
|
1537
|
-
voter.status = "repairing";
|
|
1538
|
-
voter.repairAttempts += 1;
|
|
1539
|
-
voter.lastUpdate = now();
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
if (progress.type === "triage_agent_response") {
|
|
1543
|
-
const voter = state.reviewers[progress.voter];
|
|
1544
|
-
if (voter) {
|
|
1545
|
-
voter.sessionId = progress.sessionId;
|
|
1546
|
-
voter.lastUpdate = now();
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
if (progress.type === "triage_agent_completed") {
|
|
1550
|
-
const voter = state.reviewers[progress.voter];
|
|
1551
|
-
if (voter) {
|
|
1552
|
-
voter.sessionId = progress.sessionId;
|
|
1553
|
-
voter.status = "completed";
|
|
1554
|
-
voter.verdict = progress.vote;
|
|
1555
|
-
voter.rawPath = join(state.outputDir, `${progress.voter}.${progress.phase}.raw.txt`);
|
|
1556
|
-
voter.parsedPath = join(state.outputDir, `${progress.voter}.${progress.phase}.json`);
|
|
1557
|
-
voter.lastUpdate = now();
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
if (progress.type === "triage_agent_failed") {
|
|
1561
|
-
const voter = state.reviewers[progress.voter];
|
|
1562
|
-
if (voter) {
|
|
1563
|
-
voter.status = "failed";
|
|
1564
|
-
voter.error = redactSecrets(progress.error);
|
|
1565
|
-
voter.lastUpdate = now();
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
if (progress.type === "triage_creator_started") {
|
|
1569
|
-
creatorState().status = "running";
|
|
1570
|
-
}
|
|
1571
|
-
if (progress.type === "triage_creator_session") {
|
|
1572
|
-
const creator = creatorState();
|
|
1573
|
-
if (progress.options)
|
|
1574
|
-
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1575
|
-
creator.sessionId = progress.sessionId;
|
|
1576
|
-
creator.status = "running";
|
|
1577
|
-
creator.lastUpdate = now();
|
|
1578
|
-
this.sessionToRun.set(progress.sessionId, {
|
|
1579
|
-
agent: "triageCreator",
|
|
1580
|
-
runId,
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
if (progress.type === "triage_creator_repair") {
|
|
1584
|
-
const creator = creatorState();
|
|
1585
|
-
creator.status = "repairing";
|
|
1586
|
-
creator.repairAttempts += 1;
|
|
1587
|
-
creator.lastUpdate = now();
|
|
1588
|
-
}
|
|
1589
|
-
if (progress.type === "triage_creator_response") {
|
|
1590
|
-
const creator = creatorState();
|
|
1591
|
-
creator.sessionId = progress.sessionId;
|
|
1592
|
-
creator.lastUpdate = now();
|
|
1593
|
-
}
|
|
1594
|
-
if (progress.type === "triage_creator_completed") {
|
|
1595
|
-
const creator = creatorState();
|
|
1596
|
-
creator.sessionId = progress.sessionId;
|
|
1597
|
-
creator.status = "completed";
|
|
1598
|
-
creator.parsedPath = join(state.outputDir, "create-pr.json");
|
|
1599
|
-
creator.lastUpdate = now();
|
|
1600
|
-
}
|
|
1601
|
-
if (progress.type === "triage_creator_failed") {
|
|
1602
|
-
const creator = creatorState();
|
|
1603
|
-
creator.status = "failed";
|
|
1604
|
-
creator.error = redactSecrets(progress.error);
|
|
1605
|
-
creator.lastUpdate = now();
|
|
1606
|
-
}
|
|
1607
|
-
await this.persist(state);
|
|
1608
|
-
if (progress.type === "phase") {
|
|
1609
|
-
await this.notify(state, `Triage phase for ${issue}: ${progress.phase}.`);
|
|
1610
|
-
}
|
|
1611
|
-
if (progress.type === "decision") {
|
|
1612
|
-
await this.notify(state, triageDecisionNotification({
|
|
1613
|
-
action: progress.action,
|
|
1614
|
-
issue,
|
|
1615
|
-
result: JSON.stringify(progress.result),
|
|
1616
|
-
}));
|
|
1617
|
-
}
|
|
1618
|
-
if (progress.type === "triage_agent_started") {
|
|
1619
|
-
await this.notify(state, `**Triage voter ${progress.voter}** started ${progress.phase} for ${issue}.`);
|
|
1620
|
-
}
|
|
1621
|
-
if (progress.type === "triage_agent_repair") {
|
|
1622
|
-
await this.notify(state, `**Triage voter ${progress.voter}** started JSON regeneration for ${issue}.`);
|
|
1623
|
-
}
|
|
1624
|
-
if (progress.type === "triage_agent_completed") {
|
|
1625
|
-
await this.notify(state, `**Triage voter ${progress.voter}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
|
|
1626
|
-
}
|
|
1627
|
-
if (progress.type === "triage_agent_failed") {
|
|
1628
|
-
await this.notify(state, `**Triage voter ${progress.voter}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
|
|
1629
|
-
}
|
|
1630
|
-
if (progress.type === "comment_posting") {
|
|
1631
|
-
await this.notify(state, `Posting triage comment for ${issue}.`);
|
|
1632
|
-
}
|
|
1633
|
-
if (progress.type === "comment_posted") {
|
|
1634
|
-
await this.notify(state, `Posted triage comment for ${issue}: ${progress.url}`);
|
|
1635
|
-
}
|
|
1636
|
-
if (progress.type === "pr_creation_started") {
|
|
1637
|
-
await this.notify(state, `Started implementation PR creation for ${issue}.`);
|
|
1638
|
-
}
|
|
1639
|
-
if (progress.type === "worktree_created") {
|
|
1640
|
-
await this.notify(state, `Worktree is ready for ${issue}.`);
|
|
1641
|
-
}
|
|
1642
|
-
if (progress.type === "triage_creator_started") {
|
|
1643
|
-
await this.notify(state, `**Triage creator** started creating an implementation PR for ${issue}.`);
|
|
1644
|
-
}
|
|
1645
|
-
if (progress.type === "triage_creator_repair") {
|
|
1646
|
-
await this.notify(state, `**Triage creator** started JSON regeneration for ${issue}.`);
|
|
1647
|
-
}
|
|
1648
|
-
if (progress.type === "triage_creator_completed") {
|
|
1649
|
-
await this.notify(state, `**Triage creator** completed implementation changes for ${issue}.`);
|
|
1650
|
-
}
|
|
1651
|
-
if (progress.type === "triage_creator_failed") {
|
|
1652
|
-
await this.notify(state, triageCreatorFailureText({
|
|
1653
|
-
error: redactSecrets(progress.error),
|
|
1654
|
-
issue,
|
|
1655
|
-
repairAttempts: state.triageCreator?.repairAttempts ?? 0,
|
|
1656
|
-
}));
|
|
1657
|
-
}
|
|
1658
|
-
if (progress.type === "pr_created") {
|
|
1659
|
-
await this.notify(state, `Created implementation PR for ${issue}: ${progress.url}`);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
async applyReviewProgress(runId, progress) {
|
|
1663
|
-
const state = this.active.get(runId);
|
|
1664
|
-
if (!state)
|
|
1665
|
-
return;
|
|
1666
|
-
state.updatedAt = now();
|
|
1667
|
-
if (progress.type === "phase") {
|
|
1668
|
-
state.phase = progress.phase;
|
|
1669
|
-
state.status =
|
|
1670
|
-
progress.phase === "posting reviews" ? "posting" : "running";
|
|
1671
|
-
}
|
|
1672
|
-
if (progress.type === "ci_report") {
|
|
1673
|
-
state.ciReports = [...(state.ciReports ?? []), progress.report];
|
|
1674
|
-
}
|
|
1675
|
-
if (progress.type === "ci_classifier_started") {
|
|
1676
|
-
state.ciClassifiers ??= {};
|
|
1677
|
-
state.ciClassifiers[progress.reviewer] = {
|
|
1678
|
-
account: progress.reviewer,
|
|
1679
|
-
promptPath: progress.promptPath,
|
|
1680
|
-
repairAttempts: 0,
|
|
1681
|
-
status: "running",
|
|
1682
|
-
toolCalls: 0,
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
if (progress.type === "ci_classifier_session") {
|
|
1686
|
-
state.ciClassifiers ??= {};
|
|
1687
|
-
const classifier = state.ciClassifiers[progress.reviewer] ??
|
|
1688
|
-
(state.ciClassifiers[progress.reviewer] = {
|
|
1689
|
-
account: progress.reviewer,
|
|
1690
|
-
repairAttempts: 0,
|
|
1691
|
-
status: "running",
|
|
1692
|
-
toolCalls: 0,
|
|
1693
|
-
});
|
|
1694
|
-
classifier.sessionId = progress.sessionId;
|
|
1695
|
-
classifier.status = "running";
|
|
1696
|
-
classifier.lastUpdate = now();
|
|
1697
|
-
this.sessionToRun.set(progress.sessionId, {
|
|
1698
|
-
agent: `ci:${progress.reviewer}`,
|
|
1699
|
-
runId,
|
|
1700
|
-
});
|
|
1701
|
-
}
|
|
1702
|
-
if (progress.type === "ci_classifier_repair") {
|
|
1703
|
-
const classifier = state.ciClassifiers?.[progress.reviewer];
|
|
1704
|
-
if (classifier) {
|
|
1705
|
-
classifier.repairAttempts += 1;
|
|
1706
|
-
classifier.status = "repairing";
|
|
1707
|
-
classifier.lastUpdate = now();
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
if (progress.type === "ci_classifier_completed") {
|
|
1711
|
-
const classifier = state.ciClassifiers?.[progress.reviewer];
|
|
1712
|
-
if (classifier) {
|
|
1713
|
-
classifier.checks = progress.checks;
|
|
1714
|
-
classifier.rawPath = progress.rawPath;
|
|
1715
|
-
classifier.sessionId = progress.sessionId;
|
|
1716
|
-
classifier.status = "completed";
|
|
1717
|
-
classifier.lastUpdate = now();
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
if (progress.type === "ci_classifier_failed") {
|
|
1721
|
-
const classifier = state.ciClassifiers?.[progress.reviewer];
|
|
1722
|
-
if (classifier) {
|
|
1723
|
-
classifier.error = redactSecrets(progress.error);
|
|
1724
|
-
classifier.status = "failed";
|
|
1725
|
-
classifier.lastUpdate = now();
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
if (progress.type === "worktree_created") {
|
|
1729
|
-
state.worktreePath = progress.worktreePath;
|
|
1730
|
-
state.worktreeBranch = progress.branch;
|
|
1731
|
-
}
|
|
1732
|
-
if (progress.type === "reviewer_started") {
|
|
1733
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1734
|
-
if (!reviewer)
|
|
1735
|
-
return;
|
|
1736
|
-
reviewer.status = "running";
|
|
1737
|
-
}
|
|
1738
|
-
if (progress.type === "reviewer_skipped") {
|
|
1739
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1740
|
-
if (!reviewer)
|
|
1741
|
-
return;
|
|
1742
|
-
reviewer.status = "skipped";
|
|
1743
|
-
reviewer.verdict = progress.verdict;
|
|
1744
|
-
}
|
|
1745
|
-
if (progress.type === "reviewer_session") {
|
|
1746
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1747
|
-
if (!reviewer)
|
|
1748
|
-
return;
|
|
1749
|
-
if (progress.options)
|
|
1750
|
-
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1751
|
-
reviewer.sessionId = progress.sessionId;
|
|
1752
|
-
reviewer.status = "running";
|
|
1753
|
-
reviewer.lastUpdate = now();
|
|
1754
|
-
this.sessionToRun.set(progress.sessionId, {
|
|
1755
|
-
agent: progress.reviewer,
|
|
1756
|
-
runId,
|
|
1757
|
-
});
|
|
1758
|
-
}
|
|
1759
|
-
if (progress.type === "reviewer_repair") {
|
|
1760
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1761
|
-
if (!reviewer)
|
|
1762
|
-
return;
|
|
1763
|
-
reviewer.status = "repairing";
|
|
1764
|
-
reviewer.repairAttempts += 1;
|
|
1765
|
-
reviewer.lastUpdate = now();
|
|
1766
|
-
}
|
|
1767
|
-
if (progress.type === "reviewer_response") {
|
|
1768
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1769
|
-
if (!reviewer)
|
|
1770
|
-
return;
|
|
1771
|
-
reviewer.sessionId = progress.sessionId;
|
|
1772
|
-
reviewer.lastUpdate = now();
|
|
1773
|
-
}
|
|
1774
|
-
if (progress.type === "reviewer_failed") {
|
|
1775
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1776
|
-
if (!reviewer)
|
|
1777
|
-
return;
|
|
1778
|
-
reviewer.status = "failed";
|
|
1779
|
-
reviewer.error = redactSecrets(progress.error);
|
|
1780
|
-
reviewer.lastUpdate = now();
|
|
1781
|
-
}
|
|
1782
|
-
if (progress.type === "reviewer_completed") {
|
|
1783
|
-
const reviewer = state.reviewers[progress.reviewer];
|
|
1784
|
-
if (!reviewer)
|
|
1785
|
-
return;
|
|
1786
|
-
reviewer.sessionId = progress.sessionId;
|
|
1787
|
-
reviewer.status = "completed";
|
|
1788
|
-
reviewer.verdict = progress.verdict;
|
|
1789
|
-
reviewer.lastUpdate = now();
|
|
1790
|
-
}
|
|
1791
|
-
if (progress.type === "completed") {
|
|
1792
|
-
state.verdict = progress.verdict;
|
|
1793
|
-
}
|
|
1794
|
-
await this.persist(state);
|
|
1795
|
-
if (progress.type === "reviewer_reconsidered") {
|
|
1796
|
-
await this.notify(state, closeReconsiderationText({
|
|
1797
|
-
pr: prMarkdownLink(state),
|
|
1798
|
-
reviewer: progress.reviewer,
|
|
1799
|
-
to: progress.to,
|
|
1800
|
-
}));
|
|
1801
|
-
}
|
|
1802
|
-
if (progress.type === "findings_validated") {
|
|
1803
|
-
await this.notify(state, findingsValidationText({
|
|
1804
|
-
discarded: progress.discarded,
|
|
1805
|
-
kept: progress.kept,
|
|
1806
|
-
pr: prMarkdownLink(state),
|
|
1807
|
-
}));
|
|
1808
|
-
for (const reviewer of progress.reviewersChangedToMerge) {
|
|
1809
|
-
await this.notify(state, `**Reviewer ${reviewer}** had no remaining findings after validation and approved ${prMarkdownLink(state)}.`);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (progress.type === "ci_report") {
|
|
1813
|
-
await this.notify(state, ciReportText({ pr: prMarkdownLink(state), report: progress.report }));
|
|
1814
|
-
}
|
|
1815
|
-
if (progress.type === "ci_classifier_started") {
|
|
1816
|
-
await this.notify(state, `**CI classifier ${progress.reviewer}** started for ${prMarkdownLink(state)}.`);
|
|
1817
|
-
}
|
|
1818
|
-
if (progress.type === "ci_classifier_repair") {
|
|
1819
|
-
await this.notify(state, `**CI classifier ${progress.reviewer}** started JSON regeneration for ${prMarkdownLink(state)}.`);
|
|
1820
|
-
}
|
|
1821
|
-
if (progress.type === "ci_classifier_completed") {
|
|
1822
|
-
await this.notify(state, ciClassifierCompletedText({
|
|
1823
|
-
checks: progress.checks,
|
|
1824
|
-
pr: prMarkdownLink(state),
|
|
1825
|
-
reviewer: progress.reviewer,
|
|
1826
|
-
}));
|
|
1827
|
-
}
|
|
1828
|
-
if (progress.type === "ci_classifier_failed") {
|
|
1829
|
-
await this.notify(state, `**CI classifier ${progress.reviewer}** failed for ${prMarkdownLink(state)}: ${redactSecrets(progress.error)}`);
|
|
1830
|
-
}
|
|
1831
|
-
if (progress.type === "worktree_created") {
|
|
1832
|
-
await this.notify(state, `Worktree is ready for ${prMarkdownLink(state)}.`);
|
|
1833
|
-
}
|
|
1834
|
-
if (progress.type === "reviewer_started") {
|
|
1835
|
-
await this.notify(state, `**Reviewer ${progress.reviewer}** started reviewing ${prMarkdownLink(state)}.`);
|
|
1836
|
-
}
|
|
1837
|
-
if (progress.type === "reviewer_skipped") {
|
|
1838
|
-
await this.notify(state, `**Reviewer ${progress.reviewer}** skipped ${prMarkdownLink(state)} with existing verdict ${progress.verdict}.`);
|
|
1839
|
-
}
|
|
1840
|
-
if (progress.type === "reviewer_repair") {
|
|
1841
|
-
await this.notify(state, `**Reviewer ${progress.reviewer}** started JSON regeneration for ${prMarkdownLink(state)}.`);
|
|
1842
|
-
}
|
|
1843
|
-
if (progress.type === "reviewer_failed") {
|
|
1844
|
-
await this.notify(state, reviewerFailureText({
|
|
1845
|
-
error: redactSecrets(progress.error),
|
|
1846
|
-
pr: prMarkdownLink(state),
|
|
1847
|
-
repairAttempts: state.reviewers[progress.reviewer]?.repairAttempts ?? 0,
|
|
1848
|
-
reviewer: progress.reviewer,
|
|
1849
|
-
}));
|
|
1850
|
-
}
|
|
1851
|
-
if (progress.type === "phase" && state.command === "review") {
|
|
1852
|
-
const text = mergePhaseText({
|
|
1853
|
-
phase: progress.phase,
|
|
1854
|
-
pr: prMarkdownLink(state),
|
|
1855
|
-
});
|
|
1856
|
-
if (text)
|
|
1857
|
-
await this.notify(state, text);
|
|
1858
|
-
}
|
|
1859
|
-
if (progress.type === "completed" && state.command === "merge") {
|
|
1860
|
-
await this.notify(state, reviewDecisionText({
|
|
1861
|
-
pr: prMarkdownLink(state),
|
|
1862
|
-
verdict: progress.verdict,
|
|
1863
|
-
}));
|
|
1864
|
-
}
|
|
1865
|
-
if (progress.type === "completed" && state.command === "review") {
|
|
1866
|
-
await this.notify(state, reviewDecisionText({
|
|
1867
|
-
pr: prMarkdownLink(state),
|
|
1868
|
-
verdict: progress.verdict,
|
|
1869
|
-
}));
|
|
1870
|
-
}
|
|
1871
|
-
if (progress.type === "reviewer_completed") {
|
|
1872
|
-
await this.notify(state, reviewerCompletionText({
|
|
1873
|
-
pr: prMarkdownLink(state),
|
|
1874
|
-
reviewer: progress.reviewer,
|
|
1875
|
-
verdict: progress.verdict,
|
|
1876
|
-
}));
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
async applyMergeProgress(runId, progress) {
|
|
1880
|
-
if (progress.type === "phase" ||
|
|
1881
|
-
progress.type === "worktree_created" ||
|
|
1882
|
-
progress.type === "reviewer_started" ||
|
|
1883
|
-
progress.type === "reviewer_skipped" ||
|
|
1884
|
-
progress.type === "reviewer_session" ||
|
|
1885
|
-
progress.type === "reviewer_repair" ||
|
|
1886
|
-
progress.type === "reviewer_response" ||
|
|
1887
|
-
progress.type === "reviewer_failed" ||
|
|
1888
|
-
progress.type === "reviewer_completed" ||
|
|
1889
|
-
progress.type === "reviewer_reconsidered" ||
|
|
1890
|
-
progress.type === "findings_validated" ||
|
|
1891
|
-
progress.type === "ci_report" ||
|
|
1892
|
-
progress.type === "completed") {
|
|
1893
|
-
if (progress.type === "phase") {
|
|
1894
|
-
const state = this.active.get(runId);
|
|
1895
|
-
const text = state
|
|
1896
|
-
? mergePhaseText({ phase: progress.phase, pr: prMarkdownLink(state) })
|
|
1897
|
-
: undefined;
|
|
1898
|
-
if (state && text)
|
|
1899
|
-
await this.notify(state, text);
|
|
1900
|
-
}
|
|
1901
|
-
await this.applyReviewProgress(runId, progress);
|
|
1902
|
-
return;
|
|
1903
|
-
}
|
|
1904
|
-
const state = this.active.get(runId);
|
|
1905
|
-
if (!state)
|
|
1906
|
-
return;
|
|
1907
|
-
if (progress.type === "warning") {
|
|
1908
|
-
state.warnings = [...(state.warnings ?? []), progress.message];
|
|
1909
|
-
await this.persist(state);
|
|
1910
|
-
await this.notify(state, `Warning for ${prMarkdownLink(state)}: ${progress.message}`);
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
if (progress.type === "thread_limit_reached") {
|
|
1914
|
-
await this.notify(state, threadLimitText({
|
|
1915
|
-
pr: prMarkdownLink(state),
|
|
1916
|
-
threads: progress.threads,
|
|
1917
|
-
}));
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
if (progress.type === "thread_attempts") {
|
|
1921
|
-
state.threadAttempts = progress.attempts;
|
|
1922
|
-
await this.persist(state);
|
|
1923
|
-
await this.notify(state, `Tracked ${Object.keys(progress.attempts).length} review thread resolution attempts for ${prMarkdownLink(state)}.`);
|
|
1924
|
-
return;
|
|
1925
|
-
}
|
|
1926
|
-
const editor = state.editor;
|
|
1927
|
-
if (!editor)
|
|
1928
|
-
return;
|
|
1929
|
-
state.updatedAt = now();
|
|
1930
|
-
if (progress.type === "editor_started") {
|
|
1931
|
-
editor.status = "running";
|
|
1932
|
-
}
|
|
1933
|
-
if (progress.type === "editor_session") {
|
|
1934
|
-
if (progress.options)
|
|
1935
|
-
this.input.setSessionOptions?.(progress.sessionId, progress.options);
|
|
1936
|
-
editor.sessionId = progress.sessionId;
|
|
1937
|
-
editor.status = "running";
|
|
1938
|
-
editor.lastUpdate = now();
|
|
1939
|
-
this.sessionToRun.set(progress.sessionId, { agent: "editor", runId });
|
|
1940
|
-
}
|
|
1941
|
-
if (progress.type === "editor_repair") {
|
|
1942
|
-
editor.status = "repairing";
|
|
1943
|
-
editor.repairAttempts += 1;
|
|
1944
|
-
editor.lastUpdate = now();
|
|
1945
|
-
}
|
|
1946
|
-
if (progress.type === "editor_response") {
|
|
1947
|
-
editor.sessionId = progress.sessionId;
|
|
1948
|
-
editor.lastUpdate = now();
|
|
1949
|
-
}
|
|
1950
|
-
if (progress.type === "editor_failed") {
|
|
1951
|
-
editor.status = "failed";
|
|
1952
|
-
editor.error = redactSecrets(progress.error);
|
|
1953
|
-
editor.lastUpdate = now();
|
|
1954
|
-
}
|
|
1955
|
-
if (progress.type === "editor_completed") {
|
|
1956
|
-
editor.status = "completed";
|
|
1957
|
-
editor.rawPath = join(state.outputDir, `editor.cycle-${progress.cycle}.raw.txt`);
|
|
1958
|
-
editor.parsedPath = join(state.outputDir, `editor.cycle-${progress.cycle}.json`);
|
|
1959
|
-
editor.lastUpdate = now();
|
|
1960
|
-
}
|
|
1961
|
-
if (progress.type === "merge_completed") {
|
|
1962
|
-
state.verdict = progress.status;
|
|
1963
|
-
}
|
|
1964
|
-
await this.persist(state);
|
|
1965
|
-
if (progress.type === "editor_started") {
|
|
1966
|
-
await this.notify(state, `**Editor** started edit cycle ${progress.cycle} for ${prMarkdownLink(state)}.`);
|
|
1967
|
-
}
|
|
1968
|
-
if (progress.type === "editor_repair") {
|
|
1969
|
-
await this.notify(state, `**Editor** started JSON regeneration for ${prMarkdownLink(state)} cycle ${progress.cycle}.`);
|
|
1970
|
-
}
|
|
1971
|
-
if (progress.type === "editor_completed") {
|
|
1972
|
-
await this.notify(state, `**Editor** finished editing ${prMarkdownLink(state)}.`);
|
|
1973
|
-
}
|
|
1974
|
-
if (progress.type === "editor_failed") {
|
|
1975
|
-
await this.notify(state, editorFailureText({
|
|
1976
|
-
error: redactSecrets(progress.error),
|
|
1977
|
-
pr: prMarkdownLink(state),
|
|
1978
|
-
repairAttempts: state.editor?.repairAttempts ?? 0,
|
|
1979
|
-
}));
|
|
1980
|
-
}
|
|
1981
|
-
if (progress.type === "merge_completed") {
|
|
1982
|
-
await this.notify(state, `Merge workflow reached ${progress.status} for ${prMarkdownLink(state)}.`);
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
async failRun(runId, error) {
|
|
1986
|
-
const state = this.active.get(runId);
|
|
1987
|
-
if (!state)
|
|
1988
|
-
return;
|
|
1989
|
-
if (state.status === "cancelled")
|
|
1990
|
-
return;
|
|
1991
|
-
state.status = "failed";
|
|
1992
|
-
state.phase = "failed";
|
|
1993
|
-
state.completedAt = now();
|
|
1994
|
-
state.error = errorMessage(error);
|
|
1995
|
-
await this.finishActiveAgents(state, "failed", state.error);
|
|
1996
|
-
await this.persist(state);
|
|
1997
|
-
await this.notify(state, `Magi ${state.command} failed for ${runLabel(state)}: ${state.error}`, { reply: true });
|
|
1998
|
-
this.active.delete(runId);
|
|
1999
|
-
this.controllers.delete(runId);
|
|
2000
|
-
}
|
|
2001
|
-
async filteredStates(input) {
|
|
2002
|
-
const states = input.runId
|
|
2003
|
-
? (await this.readStateByRunId(input.runId))
|
|
2004
|
-
? [(await this.readStateByRunId(input.runId))]
|
|
2005
|
-
: []
|
|
2006
|
-
: await this.listStates(input.outputDir);
|
|
2007
|
-
return states
|
|
2008
|
-
.filter((state) => input.command == null || state.command === input.command)
|
|
2009
|
-
.filter((state) => input.issue == null || state.issue === input.issue)
|
|
2010
|
-
.filter((state) => matchesNumberFilter(state.pr, input.pr))
|
|
2011
|
-
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2012
|
-
}
|
|
2013
|
-
async selectState(input) {
|
|
2014
|
-
if (input.runId)
|
|
2015
|
-
return this.readStateByRunId(input.runId);
|
|
2016
|
-
return (await this.filteredStates(input))[0];
|
|
2017
|
-
}
|
|
2018
|
-
selectorText(input) {
|
|
2019
|
-
if (input.runId)
|
|
2020
|
-
return input.runId;
|
|
2021
|
-
if (input.pr != null)
|
|
2022
|
-
return `PR #${input.pr}`;
|
|
2023
|
-
if (input.issue != null)
|
|
2024
|
-
return `issue #${input.issue}`;
|
|
2025
|
-
return "all runs";
|
|
2026
|
-
}
|
|
2027
|
-
absoluteOutputDir(dir) {
|
|
2028
|
-
return isAbsolute(dir) ? dir : join(this.input.directory, dir);
|
|
2029
|
-
}
|
|
2030
|
-
absoluteWorktreeDirs(input) {
|
|
2031
|
-
const worktreeDirs = Array.isArray(input.worktreeDir)
|
|
2032
|
-
? input.worktreeDir
|
|
2033
|
-
: input.worktreeDir
|
|
2034
|
-
? [input.worktreeDir]
|
|
2035
|
-
: worktreeBaseDirs(this.input.directory);
|
|
2036
|
-
return worktreeDirs.map((dir) => isAbsolute(dir) ? dir : join(this.input.directory, dir));
|
|
2037
|
-
}
|
|
2038
|
-
emptyOutputCleanupRoots(input) {
|
|
2039
|
-
const outputDirs = Array.isArray(input.outputDir)
|
|
2040
|
-
? input.outputDir
|
|
2041
|
-
: input.outputDir
|
|
2042
|
-
? [input.outputDir]
|
|
2043
|
-
: [join(this.input.directory, ".magi", "runs")];
|
|
2044
|
-
return outputDirs.map((dir) => this.absoluteOutputDir(dir));
|
|
2045
|
-
}
|
|
2046
|
-
async pruneEmptyMagiDirectories(input) {
|
|
2047
|
-
for (const dir of input.trees) {
|
|
2048
|
-
await pruneEmptyDirectories({
|
|
2049
|
-
boundary: this.input.directory,
|
|
2050
|
-
recursive: true,
|
|
2051
|
-
start: dir,
|
|
2052
|
-
});
|
|
2053
|
-
}
|
|
2054
|
-
for (const dir of input.dirs) {
|
|
2055
|
-
await pruneEmptyDirectories({
|
|
2056
|
-
boundary: this.input.directory,
|
|
2057
|
-
start: dir,
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
async listStates(outputDir) {
|
|
2062
|
-
for (const dir of Array.isArray(outputDir)
|
|
2063
|
-
? outputDir
|
|
2064
|
-
: outputDir
|
|
2065
|
-
? [outputDir]
|
|
2066
|
-
: []) {
|
|
2067
|
-
this.outputDirs.add(this.absoluteOutputDir(dir));
|
|
2068
|
-
}
|
|
2069
|
-
const baseDirs = [...this.outputDirs].map((dir) => this.absoluteOutputDir(dir));
|
|
2070
|
-
if (!baseDirs.length)
|
|
2071
|
-
baseDirs.push(join(this.input.directory, ".magi", "runs"));
|
|
2072
|
-
const states = [];
|
|
2073
|
-
async function walk(dir) {
|
|
2074
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
2075
|
-
for (const entry of entries) {
|
|
2076
|
-
const path = join(dir, entry.name);
|
|
2077
|
-
if (entry.isDirectory()) {
|
|
2078
|
-
await walk(path);
|
|
2079
|
-
continue;
|
|
2080
|
-
}
|
|
2081
|
-
if (entry.name !== "state.json")
|
|
2082
|
-
continue;
|
|
2083
|
-
const state = await readFile(path, "utf8")
|
|
2084
|
-
.then((text) => JSON.parse(text))
|
|
2085
|
-
.catch(() => undefined);
|
|
2086
|
-
if (state)
|
|
2087
|
-
states.push(state);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
for (const baseDir of baseDirs)
|
|
2091
|
-
await walk(baseDir);
|
|
2092
|
-
for (const state of this.active.values()) {
|
|
2093
|
-
if (!states.some((item) => item.runId === state.runId))
|
|
2094
|
-
states.push(state);
|
|
2095
|
-
}
|
|
2096
|
-
return states;
|
|
2097
|
-
}
|
|
2098
|
-
async readStateByRunId(runId) {
|
|
2099
|
-
const active = this.active.get(runId);
|
|
2100
|
-
if (active)
|
|
2101
|
-
return active;
|
|
2102
|
-
const knownPath = this.runPaths.get(runId);
|
|
2103
|
-
if (knownPath) {
|
|
2104
|
-
const state = await readFile(knownPath, "utf8")
|
|
2105
|
-
.then((text) => JSON.parse(text))
|
|
2106
|
-
.catch(() => undefined);
|
|
2107
|
-
if (state)
|
|
2108
|
-
return state;
|
|
2109
|
-
}
|
|
2110
|
-
return (await this.listStates()).find((state) => state.runId === runId);
|
|
2111
|
-
}
|
|
2112
|
-
async persist(state) {
|
|
2113
|
-
state.updatedAt = now();
|
|
2114
|
-
await mkdir(state.outputDir, { recursive: true });
|
|
2115
|
-
const path = join(state.outputDir, "state.json");
|
|
2116
|
-
this.runPaths.set(state.runId, path);
|
|
2117
|
-
await writeFile(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
2118
|
-
}
|
|
2119
|
-
async notify(state, text, options = {}) {
|
|
2120
|
-
if (!state.parentSessionId || !this.input.client.session.promptAsync)
|
|
2121
|
-
return;
|
|
2122
|
-
void options;
|
|
2123
|
-
await this.input.client.session
|
|
2124
|
-
.promptAsync({
|
|
2125
|
-
body: {
|
|
2126
|
-
parts: [{ type: "text", text, synthetic: true }],
|
|
2127
|
-
},
|
|
2128
|
-
path: { id: state.parentSessionId },
|
|
2129
|
-
})
|
|
2130
|
-
.catch(() => undefined);
|
|
2131
|
-
}
|
|
2132
|
-
}
|