opencode-magi 0.0.0-dev-20260520045400 → 0.0.0-dev-20260520064744
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/dist/commands.js +12 -0
- package/dist/github/commands.js +73 -6
- package/dist/index.js +34 -14
- package/dist/orchestrator/run-manager.js +109 -2
- package/dist/orchestrator/triage.js +554 -126
- package/dist/prompts/compose.js +62 -3
- package/dist/prompts/contracts.js +20 -0
- package/dist/prompts/output.js +25 -0
- package/dist/prompts/templates/triage/action.md +1 -1
- package/dist/prompts/templates/triage/comment.md +1 -1
- package/dist/prompts/templates/triage/create-pr.md +1 -1
- package/dist/prompts/templates/triage/question.md +1 -1
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -3,6 +3,10 @@ export const MAGI_COMMANDS = {
|
|
|
3
3
|
description: "Clear inactive Magi runs, sessions, worktrees, and outputs",
|
|
4
4
|
template: "Call the `magi_clear` tool.",
|
|
5
5
|
},
|
|
6
|
+
"magi:cancel": {
|
|
7
|
+
description: "Cancel a Magi background run",
|
|
8
|
+
template: [`Call the \`magi_cancel\` tool.`, "Selector: $ARGUMENTS"].join("\n"),
|
|
9
|
+
},
|
|
6
10
|
"magi:merge": {
|
|
7
11
|
description: "Review and merge pull requests with Magi",
|
|
8
12
|
template: [`Call the \`magi_merge\` tool.`, "PR: $ARGUMENTS"].join("\n"),
|
|
@@ -15,6 +19,14 @@ export const MAGI_COMMANDS = {
|
|
|
15
19
|
description: "Review pull requests with Magi",
|
|
16
20
|
template: [`Call the \`magi_review\` tool.`, "PR: $ARGUMENTS"].join("\n"),
|
|
17
21
|
},
|
|
22
|
+
"magi:output": {
|
|
23
|
+
description: "Show Magi run output artifacts",
|
|
24
|
+
template: [`Call the \`magi_output\` tool.`, "Selector: $ARGUMENTS"].join("\n"),
|
|
25
|
+
},
|
|
26
|
+
"magi:status": {
|
|
27
|
+
description: "Show Magi background run status",
|
|
28
|
+
template: [`Call the \`magi_status\` tool.`, "Selector: $ARGUMENTS"].join("\n"),
|
|
29
|
+
},
|
|
18
30
|
"magi:validate": {
|
|
19
31
|
description: "Validate Magi config",
|
|
20
32
|
template: "Call the `magi_validate` tool.",
|
package/dist/github/commands.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
function normalizeRelatedPullRequestState(state) {
|
|
5
|
+
const normalized = state?.toUpperCase();
|
|
6
|
+
if (normalized === "MERGED")
|
|
7
|
+
return "MERGED";
|
|
8
|
+
if (normalized === "CLOSED")
|
|
9
|
+
return "CLOSED";
|
|
10
|
+
return "OPEN";
|
|
11
|
+
}
|
|
4
12
|
const WORKTREE_CHECKOUT_RETRY_ATTEMPTS = 5;
|
|
5
13
|
const WORKTREE_CHECKOUT_RETRY_DELAY_MS = 100;
|
|
6
14
|
const worktreeCreateLocks = new Map();
|
|
@@ -157,9 +165,7 @@ export async function fetchRelatedPullRequests(exec, repository, issue) {
|
|
|
157
165
|
continue;
|
|
158
166
|
const state = source.mergedAt
|
|
159
167
|
? "MERGED"
|
|
160
|
-
: source.state
|
|
161
|
-
? "CLOSED"
|
|
162
|
-
: "OPEN";
|
|
168
|
+
: normalizeRelatedPullRequestState(source.state);
|
|
163
169
|
prs.set(source.number, {
|
|
164
170
|
author: source.author?.login ?? "",
|
|
165
171
|
body: source.body,
|
|
@@ -170,25 +176,86 @@ export async function fetchRelatedPullRequests(exec, repository, issue) {
|
|
|
170
176
|
url: source.url,
|
|
171
177
|
});
|
|
172
178
|
}
|
|
179
|
+
const searchQuery = `repo:${repoSlug(repository)} is:pr ${issue}`;
|
|
180
|
+
const searchRaw = await exec(`gh search prs ${shellQuote(searchQuery)} --json number,title,url,state,body,author --limit 10`).catch(() => "[]");
|
|
181
|
+
const searchData = JSON.parse(searchRaw);
|
|
182
|
+
const closingReference = new RegExp(`\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issue}\\b`, "i");
|
|
183
|
+
for (const item of searchData) {
|
|
184
|
+
if (!closingReference.test(item.body ?? ""))
|
|
185
|
+
continue;
|
|
186
|
+
prs.set(item.number, {
|
|
187
|
+
author: item.author?.login ?? "",
|
|
188
|
+
body: item.body,
|
|
189
|
+
number: item.number,
|
|
190
|
+
state: normalizeRelatedPullRequestState(item.state),
|
|
191
|
+
title: item.title,
|
|
192
|
+
url: item.url,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
173
195
|
return [...prs.values()];
|
|
174
196
|
}
|
|
197
|
+
function duplicateReferences(text) {
|
|
198
|
+
const refs = new Set();
|
|
199
|
+
const pattern = /duplicate(?:s)?\s+(?:of\s+)?#(\d+)/gi;
|
|
200
|
+
for (const match of text.matchAll(pattern))
|
|
201
|
+
refs.add(Number(match[1]));
|
|
202
|
+
return [...refs];
|
|
203
|
+
}
|
|
204
|
+
async function fetchIssueCandidate(exec, repository, number, whyCandidate) {
|
|
205
|
+
const raw = await exec(`gh issue view ${number} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,state,body,createdAt`).catch(() => undefined);
|
|
206
|
+
if (!raw)
|
|
207
|
+
return undefined;
|
|
208
|
+
const data = JSON.parse(raw);
|
|
209
|
+
return { ...data, whyCandidate };
|
|
210
|
+
}
|
|
175
211
|
export async function searchDuplicateIssues(exec, repository, issue, limit = 5) {
|
|
176
212
|
const query = `${issue.title} repo:${repoSlug(repository)} is:issue -${issue.number}`;
|
|
213
|
+
const explicitCandidates = await Promise.all(duplicateReferences(issue.body)
|
|
214
|
+
.filter((number) => number !== issue.number)
|
|
215
|
+
.map((number) => fetchIssueCandidate(exec, repository, number, "Issue body explicitly references a duplicate target.")));
|
|
177
216
|
const raw = await exec(`gh search issues ${shellQuote(query)} --json number,title,url,state,body --limit ${limit}`);
|
|
178
217
|
const data = JSON.parse(raw);
|
|
179
|
-
|
|
218
|
+
const candidates = new Map();
|
|
219
|
+
for (const candidate of explicitCandidates) {
|
|
220
|
+
if (candidate)
|
|
221
|
+
candidates.set(candidate.number, candidate);
|
|
222
|
+
}
|
|
223
|
+
for (const item of data
|
|
180
224
|
.filter((item) => item.number !== issue.number)
|
|
181
225
|
.map((item) => ({
|
|
182
226
|
...item,
|
|
183
227
|
whyCandidate: "GitHub issue search matched the title.",
|
|
184
|
-
}))
|
|
228
|
+
}))) {
|
|
229
|
+
if (!candidates.has(item.number))
|
|
230
|
+
candidates.set(item.number, item);
|
|
231
|
+
}
|
|
232
|
+
return [...candidates.values()].slice(0, limit);
|
|
185
233
|
}
|
|
186
234
|
export async function postIssueComment(exec, repository, issue, account, body) {
|
|
187
235
|
const token = await ghToken(exec, repository, account);
|
|
188
236
|
const payloadPath = join(tmpdir(), `magi-issue-${process.pid}-${Date.now()}.json`);
|
|
189
237
|
await writeFile(payloadPath, JSON.stringify({ body }));
|
|
190
238
|
try {
|
|
191
|
-
|
|
239
|
+
const raw = await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/issues/${issue}/comments --method POST --input ${shellQuote(payloadPath)} --jq '{id: .id, url: .html_url}'`, ghTokenEnv(token));
|
|
240
|
+
const data = JSON.parse(raw);
|
|
241
|
+
if (!data.id || !data.url)
|
|
242
|
+
throw new Error("GitHub issue comment response did not include id and url");
|
|
243
|
+
return { id: data.id, url: data.url };
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
await rm(payloadPath, { force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
export async function updateIssueComment(exec, repository, commentId, account, body) {
|
|
250
|
+
const token = await ghToken(exec, repository, account);
|
|
251
|
+
const payloadPath = join(tmpdir(), `magi-issue-comment-${process.pid}-${Date.now()}.json`);
|
|
252
|
+
await writeFile(payloadPath, JSON.stringify({ body }));
|
|
253
|
+
try {
|
|
254
|
+
const raw = await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/issues/comments/${commentId} --method PATCH --input ${shellQuote(payloadPath)} --jq '{id: .id, url: .html_url}'`, ghTokenEnv(token));
|
|
255
|
+
const data = JSON.parse(raw);
|
|
256
|
+
if (!data.id || !data.url)
|
|
257
|
+
throw new Error("GitHub issue comment response did not include id and url");
|
|
258
|
+
return { id: data.id, url: data.url };
|
|
192
259
|
}
|
|
193
260
|
finally {
|
|
194
261
|
await rm(payloadPath, { force: true });
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,6 @@ import { validateConfig } from "./config/validate";
|
|
|
13
13
|
import { withGitHubApiRetry } from "./github/retry";
|
|
14
14
|
import { mapPool } from "./orchestrator/pool";
|
|
15
15
|
import { MagiRunManager } from "./orchestrator/run-manager";
|
|
16
|
-
import { runTriage } from "./orchestrator/triage";
|
|
17
16
|
const execAsync = promisify(nodeExec);
|
|
18
17
|
const GLOBAL_CONFIG_PATH = join(homedir(), ".config", "opencode", "magi.json");
|
|
19
18
|
const PROJECT_CONFIG_PATH = join(".opencode", "magi.json");
|
|
@@ -119,6 +118,11 @@ function parseOptionalPr(value) {
|
|
|
119
118
|
return undefined;
|
|
120
119
|
return parsePrToken(value);
|
|
121
120
|
}
|
|
121
|
+
function parseOptionalIssue(value) {
|
|
122
|
+
if (!value?.trim())
|
|
123
|
+
return undefined;
|
|
124
|
+
return parseIssueToken(value);
|
|
125
|
+
}
|
|
122
126
|
function clearFlag(value) {
|
|
123
127
|
return typeof value === "boolean" ? value : undefined;
|
|
124
128
|
}
|
|
@@ -161,6 +165,11 @@ function prMarkdownLink(repository, pr) {
|
|
|
161
165
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
|
|
162
166
|
return `[#${pr}](${url})`;
|
|
163
167
|
}
|
|
168
|
+
function issueMarkdownLink(repository, issue) {
|
|
169
|
+
const host = repository.github.host || "github.com";
|
|
170
|
+
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
|
|
171
|
+
return `[#${issue}](${url})`;
|
|
172
|
+
}
|
|
164
173
|
function isPlainObject(value) {
|
|
165
174
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
166
175
|
}
|
|
@@ -391,27 +400,28 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
391
400
|
const repository = resolveRepository(loaded.config);
|
|
392
401
|
if (!repository.triage)
|
|
393
402
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
394
|
-
const
|
|
395
|
-
client: modelClient,
|
|
403
|
+
const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
|
|
396
404
|
config: loaded.config,
|
|
397
|
-
directory,
|
|
398
405
|
dryRun: parsed.dryRun,
|
|
399
|
-
exec: retryingExec,
|
|
400
406
|
issue,
|
|
407
|
+
parentSessionId: context.sessionID,
|
|
401
408
|
repository,
|
|
402
409
|
signal: context.abort,
|
|
403
410
|
}), { signal: context.abort });
|
|
404
|
-
return
|
|
411
|
+
return states
|
|
412
|
+
.map((state) => `Started triaging ${issueMarkdownLink(repository, state.issue)}.`)
|
|
413
|
+
.join("\n");
|
|
405
414
|
},
|
|
406
415
|
}),
|
|
407
416
|
magi_status: tool({
|
|
408
417
|
description: [
|
|
409
|
-
"Show Magi background run status. Optionally filter by runId or
|
|
418
|
+
"Show Magi background run status. Optionally filter by runId, PR, or issue and wait for completion.",
|
|
410
419
|
INTERNAL_FOLLOW_UP_TOOL_NOTE,
|
|
411
420
|
].join(" "),
|
|
412
421
|
args: {
|
|
413
422
|
runId: tool.schema.string().optional(),
|
|
414
423
|
pr: tool.schema.string().optional(),
|
|
424
|
+
issue: tool.schema.string().optional(),
|
|
415
425
|
block: tool.schema.boolean().optional(),
|
|
416
426
|
timeoutSeconds: tool.schema.number().optional(),
|
|
417
427
|
verbose: tool.schema.boolean().optional(),
|
|
@@ -419,6 +429,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
419
429
|
async execute(args) {
|
|
420
430
|
const states = await runManager.status({
|
|
421
431
|
block: args.block,
|
|
432
|
+
issue: parseOptionalIssue(args.issue),
|
|
422
433
|
outputDir: await configuredOutputDir(),
|
|
423
434
|
pr: parseOptionalPr(args.pr),
|
|
424
435
|
runId: args.runId,
|
|
@@ -433,22 +444,24 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
433
444
|
}),
|
|
434
445
|
magi_output: tool({
|
|
435
446
|
description: [
|
|
436
|
-
"Show artifacts and details for a Magi background run by runId or
|
|
447
|
+
"Show artifacts and details for a Magi background run by runId, PR, or issue, optionally for a single reviewer.",
|
|
437
448
|
INTERNAL_FOLLOW_UP_TOOL_NOTE,
|
|
438
449
|
].join(" "),
|
|
439
450
|
args: {
|
|
440
451
|
runId: tool.schema.string().optional(),
|
|
441
452
|
pr: tool.schema.string().optional(),
|
|
453
|
+
issue: tool.schema.string().optional(),
|
|
442
454
|
reviewer: tool.schema.string().optional(),
|
|
443
455
|
},
|
|
444
456
|
async execute(args) {
|
|
445
|
-
if (!args.runId && !args.pr)
|
|
446
|
-
return "Specify runId or
|
|
457
|
+
if (!args.runId && !args.pr && !args.issue)
|
|
458
|
+
return "Specify runId, pr, or issue.";
|
|
447
459
|
const outputDir = await configuredOutputDir();
|
|
448
460
|
if (outputDir)
|
|
449
461
|
await runManager.status({ outputDir });
|
|
450
462
|
return runManager.output({
|
|
451
463
|
outputDir,
|
|
464
|
+
issue: parseOptionalIssue(args.issue),
|
|
452
465
|
pr: parseOptionalPr(args.pr),
|
|
453
466
|
reviewer: args.reviewer,
|
|
454
467
|
runId: args.runId,
|
|
@@ -456,19 +469,22 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
456
469
|
},
|
|
457
470
|
}),
|
|
458
471
|
magi_cancel: tool({
|
|
459
|
-
description: "Cancel a Magi background run by runId or
|
|
472
|
+
description: "Cancel a Magi background run by runId, PR, or issue.",
|
|
460
473
|
args: {
|
|
461
474
|
runId: tool.schema.string().optional(),
|
|
462
475
|
pr: tool.schema.string().optional(),
|
|
476
|
+
issue: tool.schema.string().optional(),
|
|
463
477
|
},
|
|
464
478
|
async execute(args) {
|
|
465
|
-
if (!args.runId && !args.pr)
|
|
466
|
-
return "Specify runId or
|
|
479
|
+
if (!args.runId && !args.pr && !args.issue)
|
|
480
|
+
return "Specify runId, pr, or issue.";
|
|
467
481
|
const outputDir = await configuredOutputDir();
|
|
468
482
|
if (outputDir)
|
|
469
483
|
await runManager.status({ outputDir });
|
|
470
484
|
const pr = parseOptionalPr(args.pr);
|
|
485
|
+
const issue = parseOptionalIssue(args.issue);
|
|
471
486
|
const state = await runManager.cancel({
|
|
487
|
+
issue,
|
|
472
488
|
outputDir,
|
|
473
489
|
pr,
|
|
474
490
|
runId: args.runId,
|
|
@@ -476,7 +492,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
476
492
|
if (!state) {
|
|
477
493
|
return args.runId
|
|
478
494
|
? `Magi run not found: ${args.runId}`
|
|
479
|
-
:
|
|
495
|
+
: issue
|
|
496
|
+
? `Magi run not found for issue #${issue}`
|
|
497
|
+
: `Magi run not found for PR #${pr}`;
|
|
480
498
|
}
|
|
481
499
|
return runManager.formatStates([state]);
|
|
482
500
|
},
|
|
@@ -486,6 +504,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
486
504
|
args: {
|
|
487
505
|
runId: tool.schema.string().optional(),
|
|
488
506
|
pr: tool.schema.string().optional(),
|
|
507
|
+
issue: tool.schema.string().optional(),
|
|
489
508
|
branch: tool.schema.enum(["true", "false"]).optional(),
|
|
490
509
|
output: tool.schema.enum(["true", "false"]).optional(),
|
|
491
510
|
session: tool.schema.enum(["true", "false"]).optional(),
|
|
@@ -511,6 +530,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
511
530
|
};
|
|
512
531
|
return runManager.clear({
|
|
513
532
|
options,
|
|
533
|
+
issue: parseOptionalIssue(args.issue),
|
|
514
534
|
outputDir: loaded
|
|
515
535
|
? outputBaseDirs(directory, loaded.config)
|
|
516
536
|
: undefined,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, readdir, rm, rmdir, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { outputBaseDirs, prRunOutputDir } from "../config/output";
|
|
4
|
+
import { issueRunOutputDir, outputBaseDirs, prRunOutputDir, } from "../config/output";
|
|
5
5
|
import { worktreeBaseDirs } from "../config/worktree";
|
|
6
6
|
import { removeBranch, removeWorktree, } from "../github/commands";
|
|
7
7
|
import { withGitHubApiRetry } from "../github/retry";
|
|
8
8
|
import { runMerge, } from "./merge";
|
|
9
9
|
import { runReview } from "./review";
|
|
10
|
+
import { runTriage } from "./triage";
|
|
10
11
|
const EVENT_LAST_UPDATE_THROTTLE_MS = 5_000;
|
|
11
12
|
const DEFAULT_CLEAR_OPTIONS = {
|
|
12
13
|
branch: true,
|
|
@@ -75,13 +76,28 @@ function prUrl(repository, pr) {
|
|
|
75
76
|
const host = repository.github.host || "github.com";
|
|
76
77
|
return `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
|
|
77
78
|
}
|
|
79
|
+
function issueUrl(repository, issue) {
|
|
80
|
+
const host = repository.github.host || "github.com";
|
|
81
|
+
return `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
|
|
82
|
+
}
|
|
78
83
|
function prMarkdownLink(state) {
|
|
79
84
|
if (state.pr == null)
|
|
80
85
|
return state.runId;
|
|
81
86
|
return state.prUrl ? `[#${state.pr}](${state.prUrl})` : `#${state.pr}`;
|
|
82
87
|
}
|
|
88
|
+
function issueMarkdownLink(state) {
|
|
89
|
+
if (state.issue == null)
|
|
90
|
+
return state.runId;
|
|
91
|
+
return state.issueUrl
|
|
92
|
+
? `[#${state.issue}](${state.issueUrl})`
|
|
93
|
+
: `#${state.issue}`;
|
|
94
|
+
}
|
|
83
95
|
function runLabel(state) {
|
|
84
|
-
|
|
96
|
+
if (state.pr != null)
|
|
97
|
+
return prMarkdownLink(state);
|
|
98
|
+
if (state.issue != null)
|
|
99
|
+
return issueMarkdownLink(state);
|
|
100
|
+
return state.runId;
|
|
85
101
|
}
|
|
86
102
|
function reviewerCompletionText(input) {
|
|
87
103
|
const reviewer = `**Reviewer ${input.reviewer}**`;
|
|
@@ -477,6 +493,55 @@ export class MagiRunManager {
|
|
|
477
493
|
});
|
|
478
494
|
return state;
|
|
479
495
|
}
|
|
496
|
+
async startTriage(input) {
|
|
497
|
+
const runId = createRunId();
|
|
498
|
+
const outputDir = issueRunOutputDir({
|
|
499
|
+
config: input.config,
|
|
500
|
+
directory: this.input.directory,
|
|
501
|
+
issue: input.issue,
|
|
502
|
+
runId,
|
|
503
|
+
});
|
|
504
|
+
const createdAt = now();
|
|
505
|
+
const state = {
|
|
506
|
+
command: "triage",
|
|
507
|
+
createdAt,
|
|
508
|
+
dryRun: input.dryRun,
|
|
509
|
+
issue: input.issue,
|
|
510
|
+
issueUrl: issueUrl(input.repository, input.issue),
|
|
511
|
+
outputDir,
|
|
512
|
+
parentSessionId: input.parentSessionId,
|
|
513
|
+
phase: "queued",
|
|
514
|
+
repository: input.repository.alias,
|
|
515
|
+
reviewers: Object.fromEntries((input.repository.agents.triage ?? []).map((agent) => [
|
|
516
|
+
agent.key,
|
|
517
|
+
{
|
|
518
|
+
account: "",
|
|
519
|
+
repairAttempts: 0,
|
|
520
|
+
status: "pending",
|
|
521
|
+
toolCalls: 0,
|
|
522
|
+
},
|
|
523
|
+
])),
|
|
524
|
+
runId,
|
|
525
|
+
status: "preparing",
|
|
526
|
+
updatedAt: createdAt,
|
|
527
|
+
};
|
|
528
|
+
this.active.set(runId, state);
|
|
529
|
+
this.runPaths.set(runId, join(outputDir, "state.json"));
|
|
530
|
+
for (const dir of outputBaseDirs(this.input.directory, input.config))
|
|
531
|
+
this.outputDirs.add(dir);
|
|
532
|
+
await this.persist(state);
|
|
533
|
+
await this.notify(state, `Started Magi triage for ${issueMarkdownLink(state)}.`);
|
|
534
|
+
const controller = new AbortController();
|
|
535
|
+
this.controllers.set(runId, controller);
|
|
536
|
+
void this.executeTriage({
|
|
537
|
+
...input,
|
|
538
|
+
runId,
|
|
539
|
+
signal: controller.signal,
|
|
540
|
+
}).catch(async (error) => {
|
|
541
|
+
await this.failRun(runId, error);
|
|
542
|
+
});
|
|
543
|
+
return state;
|
|
544
|
+
}
|
|
480
545
|
async status(input = {}) {
|
|
481
546
|
const timeoutMs = Math.min(input.timeoutMs ?? 60_000, 600_000);
|
|
482
547
|
const startedAt = Date.now();
|
|
@@ -1173,6 +1238,45 @@ export class MagiRunManager {
|
|
|
1173
1238
|
this.active.delete(input.runId);
|
|
1174
1239
|
this.controllers.delete(input.runId);
|
|
1175
1240
|
}
|
|
1241
|
+
async executeTriage(input) {
|
|
1242
|
+
const state = this.active.get(input.runId);
|
|
1243
|
+
if (state) {
|
|
1244
|
+
state.status = "running";
|
|
1245
|
+
state.phase = "triaging";
|
|
1246
|
+
await this.persist(state);
|
|
1247
|
+
}
|
|
1248
|
+
const result = await runTriage({
|
|
1249
|
+
client: this.input.client,
|
|
1250
|
+
config: input.config,
|
|
1251
|
+
directory: this.input.directory,
|
|
1252
|
+
dryRun: input.dryRun,
|
|
1253
|
+
exec: withGitHubApiRetry(this.input.exec, input.config.github?.apiRetryAttempts ?? 3),
|
|
1254
|
+
issue: input.issue,
|
|
1255
|
+
repository: input.repository,
|
|
1256
|
+
runId: input.runId,
|
|
1257
|
+
signal: input.signal,
|
|
1258
|
+
});
|
|
1259
|
+
const completed = this.active.get(input.runId);
|
|
1260
|
+
if (!completed || completed.status === "cancelled")
|
|
1261
|
+
return;
|
|
1262
|
+
completed.status = result.result === "FAILED" ? "failed" : "completed";
|
|
1263
|
+
completed.phase = result.result;
|
|
1264
|
+
completed.completedAt = now();
|
|
1265
|
+
completed.verdict = result.result;
|
|
1266
|
+
completed.reportPath = join(completed.outputDir, "report.md");
|
|
1267
|
+
for (const agent of Object.values(completed.reviewers)) {
|
|
1268
|
+
if (agent.status === "pending")
|
|
1269
|
+
agent.status = "completed";
|
|
1270
|
+
}
|
|
1271
|
+
await this.persist(completed);
|
|
1272
|
+
await this.notify(completed, [
|
|
1273
|
+
`Finished triage for ${issueMarkdownLink(completed)}.`,
|
|
1274
|
+
"",
|
|
1275
|
+
result.report,
|
|
1276
|
+
].join("\n"), { reply: true });
|
|
1277
|
+
this.active.delete(input.runId);
|
|
1278
|
+
this.controllers.delete(input.runId);
|
|
1279
|
+
}
|
|
1176
1280
|
async applyReviewProgress(runId, progress) {
|
|
1177
1281
|
const state = this.active.get(runId);
|
|
1178
1282
|
if (!state)
|
|
@@ -1556,6 +1660,7 @@ export class MagiRunManager {
|
|
|
1556
1660
|
: await this.listStates(input.outputDir);
|
|
1557
1661
|
return states
|
|
1558
1662
|
.filter((state) => input.command == null || state.command === input.command)
|
|
1663
|
+
.filter((state) => input.issue == null || state.issue === input.issue)
|
|
1559
1664
|
.filter((state) => input.pr == null || state.pr === input.pr)
|
|
1560
1665
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1561
1666
|
}
|
|
@@ -1569,6 +1674,8 @@ export class MagiRunManager {
|
|
|
1569
1674
|
return input.runId;
|
|
1570
1675
|
if (input.pr != null)
|
|
1571
1676
|
return `PR #${input.pr}`;
|
|
1677
|
+
if (input.issue != null)
|
|
1678
|
+
return `issue #${input.issue}`;
|
|
1572
1679
|
return "all runs";
|
|
1573
1680
|
}
|
|
1574
1681
|
absoluteOutputDir(dir) {
|