opencode-magi 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/config/resolve.js +40 -3
- package/dist/config/validate.js +231 -66
- package/dist/github/commands.js +32 -0
- package/dist/index.js +21 -50
- package/dist/orchestrator/merge.js +310 -10
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +97 -1
- package/dist/orchestrator/run-manager.js +4 -4
- package/dist/orchestrator/triage.js +312 -103
- package/dist/prompts/compose.js +59 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- package/dist/prompts/templates/merge/conflict.md +10 -0
- package/dist/prompts/templates/review/rereview.md +2 -0
- package/dist/prompts/templates/review/review.md +2 -0
- package/dist/prompts/templates/triage/acceptance.md +1 -1
- package/dist/prompts/templates/triage/signal.md +10 -0
- package/package.json +1 -1
- package/schema.json +89 -16
|
@@ -2,13 +2,14 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { issueRunOutputDir } from "../config/output";
|
|
4
4
|
import { issueRunWorktreeDir } from "../config/worktree";
|
|
5
|
-
import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
|
|
6
|
-
import { composeTriageAcceptancePrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
|
-
import { parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
|
|
5
|
+
import { addIssueLabels, assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
|
|
6
|
+
import { composeTriageAcceptancePrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, composeTriageSignalPrompt, } from "../prompts/compose";
|
|
7
|
+
import { parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, parseTriageSignalOutput, } from "../prompts/output";
|
|
8
8
|
import { aggregateStringMajority, majorityThreshold } from "./majority";
|
|
9
9
|
import { runModelWithRepair, } from "./model";
|
|
10
10
|
const MARKER_PREFIX = "opencode-magi:triage";
|
|
11
11
|
const BINARY_VOTES = ["ASK", "NO", "YES"];
|
|
12
|
+
const ACCEPTANCE_VOTES = ["ASK", "INVALID", "NO", "YES"];
|
|
12
13
|
const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
|
|
13
14
|
const EXISTING_PR_VOTES = [
|
|
14
15
|
"RELATED_PR_DOES_NOT_HANDLE_ISSUE",
|
|
@@ -20,10 +21,29 @@ const RECONSIDERATION_CLASSES = new Set([
|
|
|
20
21
|
"OBJECTION",
|
|
21
22
|
]);
|
|
22
23
|
function marker(input) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return `<!-- ${MARKER_PREFIX} v=2 issue=${input.issue} category=${input.decision.category ?? "none"} disposition=${input.decision.disposition} signals=${input.decision.signals.join(",")} action=${input.action} checkpoint=${input.checkpoint ?? "pending"} pr=${input.pr ?? "none"} processed=${(input.processed ?? []).join(",")} -->`;
|
|
25
|
+
}
|
|
26
|
+
function markerDisposition(input) {
|
|
27
|
+
switch (input.disposition) {
|
|
28
|
+
case "accepted":
|
|
29
|
+
case "rejected":
|
|
30
|
+
case "invalid":
|
|
31
|
+
case "duplicate":
|
|
32
|
+
case "already_handled":
|
|
33
|
+
case "needs_category":
|
|
34
|
+
case "needs_acceptance":
|
|
35
|
+
case "blocked":
|
|
36
|
+
case "failed":
|
|
37
|
+
return input.disposition;
|
|
38
|
+
case "ask":
|
|
39
|
+
return input.askReason === "category_unclear"
|
|
40
|
+
? "needs_category"
|
|
41
|
+
: "needs_acceptance";
|
|
42
|
+
case "clear_only":
|
|
43
|
+
return "already_handled";
|
|
44
|
+
default:
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
27
47
|
}
|
|
28
48
|
export function parseTriageMarker(body) {
|
|
29
49
|
const match = body.match(/<!--\s*opencode-magi:triage\s+([^>]+?)\s*-->/);
|
|
@@ -41,30 +61,28 @@ export function parseTriageMarker(body) {
|
|
|
41
61
|
const version = Number(entries.v);
|
|
42
62
|
if (version !== 1 && version !== 2)
|
|
43
63
|
return undefined;
|
|
64
|
+
const askReason = entries.askReason === "acceptance_unclear" ||
|
|
65
|
+
entries.askReason === "category_unclear"
|
|
66
|
+
? entries.askReason
|
|
67
|
+
: undefined;
|
|
44
68
|
return {
|
|
45
69
|
action: entries.action,
|
|
46
|
-
askReason
|
|
47
|
-
entries.askReason === "category_unclear"
|
|
48
|
-
? entries.askReason
|
|
49
|
-
: undefined,
|
|
70
|
+
askReason,
|
|
50
71
|
category: entries.category === "none" ? null : entries.category || undefined,
|
|
51
72
|
checkpoint: entries.checkpoint && Number.isFinite(Number(entries.checkpoint))
|
|
52
73
|
? Number(entries.checkpoint)
|
|
53
74
|
: undefined,
|
|
54
|
-
disposition:
|
|
55
|
-
|
|
56
|
-
entries.disposition
|
|
57
|
-
|
|
58
|
-
entries.disposition === "clear_only" ||
|
|
59
|
-
entries.disposition === "failed"
|
|
60
|
-
? entries.disposition
|
|
61
|
-
: undefined,
|
|
75
|
+
disposition: markerDisposition({
|
|
76
|
+
askReason,
|
|
77
|
+
disposition: entries.disposition,
|
|
78
|
+
}),
|
|
62
79
|
issue: entries.issue ? Number(entries.issue) : undefined,
|
|
63
80
|
pr: entries.pr,
|
|
64
81
|
processed: entries.processed
|
|
65
82
|
? entries.processed.split(",").filter(Boolean).map(Number)
|
|
66
83
|
: [],
|
|
67
84
|
result: entries.result,
|
|
85
|
+
signals: entries.signals ? entries.signals.split(",").filter(Boolean) : [],
|
|
68
86
|
v: version,
|
|
69
87
|
};
|
|
70
88
|
}
|
|
@@ -72,9 +90,46 @@ function labelsContain(labels, targets) {
|
|
|
72
90
|
const set = new Set(labels.map((label) => label.toLowerCase()));
|
|
73
91
|
return targets.some((target) => set.has(target.toLowerCase()));
|
|
74
92
|
}
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
93
|
+
function addLabel(labels, label) {
|
|
94
|
+
const key = label.toLowerCase();
|
|
95
|
+
if (!labels.has(key))
|
|
96
|
+
labels.set(key, label);
|
|
97
|
+
}
|
|
98
|
+
function labelRuleMatches(rule, result) {
|
|
99
|
+
const when = rule.when;
|
|
100
|
+
if (when.disposition && when.disposition !== result.disposition)
|
|
101
|
+
return false;
|
|
102
|
+
if (when.category && when.category !== result.category)
|
|
103
|
+
return false;
|
|
104
|
+
if (when.signals?.length) {
|
|
105
|
+
const signals = new Set(result.signals);
|
|
106
|
+
if (when.signals.some((signal) => !signals.has(signal)))
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
export function triageLabelChanges(input) {
|
|
112
|
+
const add = new Map();
|
|
113
|
+
const remove = new Map();
|
|
114
|
+
for (const rule of input.rules) {
|
|
115
|
+
if (!labelRuleMatches(rule, input.result))
|
|
116
|
+
continue;
|
|
117
|
+
for (const label of rule.add ?? [])
|
|
118
|
+
addLabel(add, label);
|
|
119
|
+
for (const label of rule.remove ?? [])
|
|
120
|
+
addLabel(remove, label);
|
|
121
|
+
}
|
|
122
|
+
for (const key of add.keys())
|
|
123
|
+
remove.delete(key);
|
|
124
|
+
const existing = new Set(input.issueLabels.map((label) => label.toLowerCase()));
|
|
125
|
+
return {
|
|
126
|
+
add: [...add]
|
|
127
|
+
.filter(([key]) => !existing.has(key))
|
|
128
|
+
.map(([, label]) => label),
|
|
129
|
+
remove: [...remove]
|
|
130
|
+
.filter(([key]) => existing.has(key))
|
|
131
|
+
.map(([, label]) => label),
|
|
132
|
+
};
|
|
78
133
|
}
|
|
79
134
|
export function resolveIssueCategory(issue, repository) {
|
|
80
135
|
const triage = repository.triage;
|
|
@@ -128,13 +183,14 @@ async function emitTriageModelProgress(input) {
|
|
|
128
183
|
}
|
|
129
184
|
}
|
|
130
185
|
async function runVote(input) {
|
|
131
|
-
const prompt =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
186
|
+
const prompt = input.promptText ??
|
|
187
|
+
(await input.prompt({
|
|
188
|
+
context: input.context,
|
|
189
|
+
directory: input.directory,
|
|
190
|
+
issue: input.issue,
|
|
191
|
+
repository: input.repository,
|
|
192
|
+
voter: input.agent,
|
|
193
|
+
}));
|
|
138
194
|
await emitProgress(input.run, {
|
|
139
195
|
phase: input.phase,
|
|
140
196
|
type: "triage_agent_started",
|
|
@@ -216,7 +272,7 @@ export function chooseDuplicateOutput(input) {
|
|
|
216
272
|
async function runDuplicateVote(input) {
|
|
217
273
|
const agents = input.input.repository.agents.triage;
|
|
218
274
|
if (!agents?.length)
|
|
219
|
-
throw new Error("triage.
|
|
275
|
+
throw new Error("triage.voters is required");
|
|
220
276
|
await emitProgress(input.input, { phase: "duplicate", type: "phase" });
|
|
221
277
|
const outputs = await Promise.all(agents.map((agent) => runVote({
|
|
222
278
|
agent,
|
|
@@ -258,9 +314,16 @@ async function runDuplicateVote(input) {
|
|
|
258
314
|
async function runPhaseVote(input) {
|
|
259
315
|
const agents = input.input.repository.agents.triage;
|
|
260
316
|
if (!agents?.length)
|
|
261
|
-
throw new Error("triage.
|
|
317
|
+
throw new Error("triage.voters is required");
|
|
262
318
|
await emitProgress(input.input, { phase: input.phase, type: "phase" });
|
|
263
|
-
const
|
|
319
|
+
const promptTexts = await Promise.all(agents.map((agent) => input.prompt({
|
|
320
|
+
context: input.context,
|
|
321
|
+
directory: input.input.directory,
|
|
322
|
+
issue: input.input.issue,
|
|
323
|
+
repository: input.input.repository,
|
|
324
|
+
voter: agent,
|
|
325
|
+
})));
|
|
326
|
+
const outputs = await Promise.all(agents.map((agent, index) => runVote({
|
|
264
327
|
agent,
|
|
265
328
|
client: input.input.client,
|
|
266
329
|
context: input.context,
|
|
@@ -269,6 +332,7 @@ async function runPhaseVote(input) {
|
|
|
269
332
|
parse: input.parse,
|
|
270
333
|
phase: input.phase,
|
|
271
334
|
prompt: input.prompt,
|
|
335
|
+
promptText: promptTexts[index],
|
|
272
336
|
repository: input.input.repository,
|
|
273
337
|
run: input.input,
|
|
274
338
|
schemaName: input.schemaName,
|
|
@@ -285,7 +349,96 @@ async function runPhaseVote(input) {
|
|
|
285
349
|
voter: agents[index].key,
|
|
286
350
|
})));
|
|
287
351
|
await writeJson(join(input.outputDir, `${input.phase}-majority.json`), majority);
|
|
288
|
-
return {
|
|
352
|
+
return {
|
|
353
|
+
outputs,
|
|
354
|
+
reason: chooseDecisionReason({
|
|
355
|
+
outputs,
|
|
356
|
+
threshold: majority.threshold,
|
|
357
|
+
vote: majority.vote,
|
|
358
|
+
voters: majority.vote ? majority.voters[majority.vote] : undefined,
|
|
359
|
+
}),
|
|
360
|
+
vote: majority.vote,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function runSignalVote(input) {
|
|
364
|
+
const agents = input.input.repository.agents.triage;
|
|
365
|
+
const signals = input.input.repository.triage?.signals ?? [];
|
|
366
|
+
if (!agents?.length)
|
|
367
|
+
throw new Error("triage.voters is required");
|
|
368
|
+
if (!signals.length)
|
|
369
|
+
return [];
|
|
370
|
+
await emitProgress(input.input, { phase: "signal", type: "phase" });
|
|
371
|
+
const signalIds = signals.map((signal) => signal.id);
|
|
372
|
+
const promptTexts = await Promise.all(agents.map((agent) => composeTriageSignalPrompt({
|
|
373
|
+
context: input.context,
|
|
374
|
+
directory: input.input.directory,
|
|
375
|
+
issue: input.input.issue,
|
|
376
|
+
repository: input.input.repository,
|
|
377
|
+
voter: agent,
|
|
378
|
+
})));
|
|
379
|
+
const outputs = await Promise.all(agents.map(async (agent, index) => {
|
|
380
|
+
const prompt = promptTexts[index];
|
|
381
|
+
await emitProgress(input.input, {
|
|
382
|
+
phase: "signal",
|
|
383
|
+
type: "triage_agent_started",
|
|
384
|
+
voter: agent.key,
|
|
385
|
+
});
|
|
386
|
+
const result = await runModelWithRepair({
|
|
387
|
+
client: input.input.client,
|
|
388
|
+
model: agent.model,
|
|
389
|
+
onProgress: (progress) => emitTriageModelProgress({
|
|
390
|
+
phase: "signal",
|
|
391
|
+
progress,
|
|
392
|
+
run: input.input,
|
|
393
|
+
voter: agent.key,
|
|
394
|
+
}),
|
|
395
|
+
options: agent.options,
|
|
396
|
+
parentSessionId: input.input.parentSessionId,
|
|
397
|
+
parse: (text) => parseTriageSignalOutput(text, signalIds),
|
|
398
|
+
permission: agent.permission,
|
|
399
|
+
prompt,
|
|
400
|
+
repairAttempts: input.input.config.output?.repairAttempts ?? 3,
|
|
401
|
+
schemaName: "triage signal",
|
|
402
|
+
signal: input.input.signal,
|
|
403
|
+
title: `Magi triage signal #${input.input.issue} (${agent.key})`,
|
|
404
|
+
});
|
|
405
|
+
await emitProgress(input.input, {
|
|
406
|
+
phase: "signal",
|
|
407
|
+
sessionId: result.sessionId,
|
|
408
|
+
type: "triage_agent_completed",
|
|
409
|
+
voter: agent.key,
|
|
410
|
+
vote: result.value.signals.map((signal) => signal.id).join(",") || "none",
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
...result.value,
|
|
414
|
+
promptText: prompt,
|
|
415
|
+
raw: result.raw,
|
|
416
|
+
sessionId: result.sessionId,
|
|
417
|
+
voter: agent.key,
|
|
418
|
+
};
|
|
419
|
+
}));
|
|
420
|
+
const threshold = majorityThreshold(outputs.length);
|
|
421
|
+
const counts = new Map();
|
|
422
|
+
for (const output of outputs) {
|
|
423
|
+
for (const id of new Set(output.signals.map((signal) => signal.id))) {
|
|
424
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const selected = signalIds.filter((id) => (counts.get(id) ?? 0) >= threshold);
|
|
428
|
+
await Promise.all(outputs.map((output) => {
|
|
429
|
+
const base = join(input.outputDir, `${output.voter}.signal`);
|
|
430
|
+
return Promise.all([
|
|
431
|
+
writeFile(`${base}.prompt.txt`, `${output.promptText}\n`),
|
|
432
|
+
writeFile(`${base}.raw.txt`, `${output.raw}\n`),
|
|
433
|
+
writeJson(`${base}.json`, { signals: output.signals }),
|
|
434
|
+
]);
|
|
435
|
+
}));
|
|
436
|
+
await writeJson(join(input.outputDir, "signal-majority.json"), {
|
|
437
|
+
counts: Object.fromEntries(counts),
|
|
438
|
+
selected,
|
|
439
|
+
threshold,
|
|
440
|
+
});
|
|
441
|
+
return selected;
|
|
289
442
|
}
|
|
290
443
|
async function relationshipScan(input, issue) {
|
|
291
444
|
const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
|
|
@@ -366,40 +519,40 @@ export function eligibleMentionReplies(input) {
|
|
|
366
519
|
function finalResultFromMarker(marker) {
|
|
367
520
|
if (marker.disposition) {
|
|
368
521
|
return {
|
|
369
|
-
askReason: marker.askReason,
|
|
370
522
|
category: marker.category ?? null,
|
|
371
523
|
disposition: marker.disposition,
|
|
524
|
+
signals: marker.signals,
|
|
372
525
|
};
|
|
373
526
|
}
|
|
374
527
|
switch (marker.result) {
|
|
375
528
|
case "BUG_ACCEPTED":
|
|
376
529
|
case "RESOLVED_BY_MERGED_PR":
|
|
377
|
-
return { category: "bug", disposition: "accepted" };
|
|
530
|
+
return { category: "bug", disposition: "accepted", signals: [] };
|
|
378
531
|
case "BUG_REJECTED":
|
|
379
|
-
return { category: "bug", disposition: "rejected" };
|
|
532
|
+
return { category: "bug", disposition: "rejected", signals: [] };
|
|
380
533
|
case "FEATURE_ACCEPTED":
|
|
381
|
-
return { category: "feature", disposition: "accepted" };
|
|
534
|
+
return { category: "feature", disposition: "accepted", signals: [] };
|
|
382
535
|
case "FEATURE_REJECTED":
|
|
383
|
-
return { category: "feature", disposition: "rejected" };
|
|
536
|
+
return { category: "feature", disposition: "rejected", signals: [] };
|
|
384
537
|
case "ASK":
|
|
385
538
|
return {
|
|
386
|
-
askReason: "acceptance_unclear",
|
|
387
539
|
category: null,
|
|
388
|
-
disposition: "
|
|
540
|
+
disposition: "needs_acceptance",
|
|
541
|
+
signals: [],
|
|
389
542
|
};
|
|
390
543
|
case "CLEAR_ONLY":
|
|
391
|
-
return { category: null, disposition: "
|
|
544
|
+
return { category: null, disposition: "already_handled", signals: [] };
|
|
392
545
|
case "DUPLICATE":
|
|
393
|
-
return { category: null, disposition: "duplicate" };
|
|
546
|
+
return { category: null, disposition: "duplicate", signals: [] };
|
|
394
547
|
default:
|
|
395
|
-
return { category: null, disposition: "failed" };
|
|
548
|
+
return { category: null, disposition: "failed", signals: [] };
|
|
396
549
|
}
|
|
397
550
|
}
|
|
398
551
|
function decisionText(decision) {
|
|
399
552
|
return JSON.stringify(decision);
|
|
400
553
|
}
|
|
401
554
|
function actionPlan(input) {
|
|
402
|
-
if (input.result.disposition === "
|
|
555
|
+
if (input.result.disposition === "already_handled") {
|
|
403
556
|
return {
|
|
404
557
|
action: "CLEAR_ONLY",
|
|
405
558
|
allowedActions: ["CLEAR_ONLY"],
|
|
@@ -409,11 +562,12 @@ function actionPlan(input) {
|
|
|
409
562
|
postComment: false,
|
|
410
563
|
};
|
|
411
564
|
}
|
|
412
|
-
if (input.result.disposition === "
|
|
565
|
+
if (input.result.disposition === "needs_category" ||
|
|
566
|
+
input.result.disposition === "needs_acceptance") {
|
|
413
567
|
return {
|
|
414
568
|
action: "ASK",
|
|
415
569
|
allowedActions: ["ASK"],
|
|
416
|
-
clearLabels:
|
|
570
|
+
clearLabels: true,
|
|
417
571
|
closeIssue: false,
|
|
418
572
|
createPr: false,
|
|
419
573
|
postComment: true,
|
|
@@ -421,6 +575,7 @@ function actionPlan(input) {
|
|
|
421
575
|
}
|
|
422
576
|
const closeIssue = input.triage.automation.close &&
|
|
423
577
|
(input.result.disposition === "rejected" ||
|
|
578
|
+
input.result.disposition === "invalid" ||
|
|
424
579
|
input.result.disposition === "duplicate");
|
|
425
580
|
const createPr = input.triage.automation.create && input.result.disposition === "accepted";
|
|
426
581
|
return {
|
|
@@ -434,8 +589,13 @@ function actionPlan(input) {
|
|
|
434
589
|
}
|
|
435
590
|
function previousAutomationPlan(input) {
|
|
436
591
|
const base = actionPlan({ result: input.result, triage: input.triage });
|
|
592
|
+
const labelChanges = triageLabelChanges({
|
|
593
|
+
issueLabels: input.issue.labels,
|
|
594
|
+
result: input.result,
|
|
595
|
+
rules: input.triage.automation.label,
|
|
596
|
+
});
|
|
437
597
|
const clearLabels = base.clearLabels &&
|
|
438
|
-
|
|
598
|
+
(labelChanges.add.length > 0 || labelChanges.remove.length > 0);
|
|
439
599
|
const closeIssue = input.marker.action === "CLOSE" &&
|
|
440
600
|
base.closeIssue &&
|
|
441
601
|
input.issue.state === "OPEN";
|
|
@@ -507,7 +667,7 @@ async function runReconsiderationVote(input) {
|
|
|
507
667
|
function triageReporter(repository, issue) {
|
|
508
668
|
const agents = repository.agents.triage ?? [];
|
|
509
669
|
if (!agents.length)
|
|
510
|
-
throw new Error("triage.
|
|
670
|
+
throw new Error("triage.voters is required");
|
|
511
671
|
const configured = repository.triage?.reporter;
|
|
512
672
|
const reporter = configured
|
|
513
673
|
? agents.find((agent) => agent.key === configured)
|
|
@@ -518,26 +678,49 @@ function triageReporter(repository, issue) {
|
|
|
518
678
|
}
|
|
519
679
|
function decisionCommentBody(input) {
|
|
520
680
|
const reason = input.reason?.trim();
|
|
521
|
-
const result = JSON.stringify(input.result);
|
|
522
681
|
return reason
|
|
523
|
-
?
|
|
524
|
-
:
|
|
682
|
+
? reason
|
|
683
|
+
: decisionCommentFallback({ action: input.action, result: input.result });
|
|
684
|
+
}
|
|
685
|
+
function decisionCommentFallback(input) {
|
|
686
|
+
if (input.result.disposition === "accepted") {
|
|
687
|
+
const category = input.result.category
|
|
688
|
+
? `${input.result.category} issue`
|
|
689
|
+
: "issue";
|
|
690
|
+
return input.action === "PR"
|
|
691
|
+
? `Magi accepted this ${category} and will prepare an implementation pull request.`
|
|
692
|
+
: `Magi accepted this ${category}.`;
|
|
693
|
+
}
|
|
694
|
+
if (input.result.disposition === "rejected") {
|
|
695
|
+
const category = input.result.category
|
|
696
|
+
? `${input.result.category} issue`
|
|
697
|
+
: "issue";
|
|
698
|
+
return `Magi does not plan to act on this ${category}.`;
|
|
699
|
+
}
|
|
700
|
+
if (input.result.disposition === "duplicate") {
|
|
701
|
+
return "Magi marked this issue as a duplicate.";
|
|
702
|
+
}
|
|
703
|
+
if (input.result.disposition === "invalid") {
|
|
704
|
+
return "Magi marked this issue as invalid or not actionable.";
|
|
705
|
+
}
|
|
706
|
+
return "Magi completed triage for this issue.";
|
|
525
707
|
}
|
|
526
708
|
function agentForKey(repository, key) {
|
|
527
709
|
const agent = repository.agents.triage?.find((item) => item.key === key);
|
|
528
710
|
if (!agent)
|
|
529
|
-
throw new Error(`Unknown triage
|
|
711
|
+
throw new Error(`Unknown triage voter: ${key}`);
|
|
530
712
|
return agent;
|
|
531
713
|
}
|
|
532
714
|
function askOutputs(outputs) {
|
|
533
715
|
return (outputs ?? []).filter((output) => output.vote === "ASK");
|
|
534
716
|
}
|
|
535
717
|
function chooseDecisionReason(input) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
718
|
+
if (!input.vote)
|
|
719
|
+
return undefined;
|
|
720
|
+
const canonicalVoter = input.voters?.[input.threshold - 1];
|
|
721
|
+
const canonicalReason = input.outputs?.find((output) => output.voter === canonicalVoter && output.vote === input.vote);
|
|
722
|
+
return (canonicalReason?.reason ??
|
|
723
|
+
input.outputs?.find((output) => output.vote === input.vote)?.reason);
|
|
541
724
|
}
|
|
542
725
|
async function postMarkedIssueComment(input) {
|
|
543
726
|
const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
|
|
@@ -637,7 +820,9 @@ async function finishWithResult(input) {
|
|
|
637
820
|
});
|
|
638
821
|
let prUrl;
|
|
639
822
|
const reporter = triageReporter(input.input.repository, input.issue.number);
|
|
640
|
-
const comment = plan.postComment &&
|
|
823
|
+
const comment = plan.postComment &&
|
|
824
|
+
input.result.disposition !== "needs_category" &&
|
|
825
|
+
input.result.disposition !== "needs_acceptance"
|
|
641
826
|
? `${decisionCommentBody({
|
|
642
827
|
action: plan.action,
|
|
643
828
|
reason: input.commentReason,
|
|
@@ -653,7 +838,19 @@ async function finishWithResult(input) {
|
|
|
653
838
|
if (comment) {
|
|
654
839
|
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
655
840
|
}
|
|
656
|
-
|
|
841
|
+
const labelChanges = plan.clearLabels
|
|
842
|
+
? triageLabelChanges({
|
|
843
|
+
issueLabels: input.issue.labels,
|
|
844
|
+
result: input.result,
|
|
845
|
+
rules: triage.automation.label,
|
|
846
|
+
})
|
|
847
|
+
: { add: [], remove: [] };
|
|
848
|
+
if (plan.clearLabels) {
|
|
849
|
+
await writeJson(join(input.outputDir, "label-changes.json"), labelChanges);
|
|
850
|
+
}
|
|
851
|
+
if ((input.result.disposition === "needs_category" ||
|
|
852
|
+
input.result.disposition === "needs_acceptance") &&
|
|
853
|
+
input.askOutputs) {
|
|
657
854
|
await postAskComments({
|
|
658
855
|
action: plan.action,
|
|
659
856
|
dryRun: input.input.dryRun,
|
|
@@ -685,9 +882,11 @@ async function finishWithResult(input) {
|
|
|
685
882
|
});
|
|
686
883
|
}
|
|
687
884
|
if (plan.clearLabels) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
885
|
+
if (labelChanges.remove.length) {
|
|
886
|
+
await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, labelChanges.remove, reporter.account);
|
|
887
|
+
}
|
|
888
|
+
if (labelChanges.add.length) {
|
|
889
|
+
await addIssueLabels(input.input.exec, input.input.repository, input.issue.number, labelChanges.add, reporter.account);
|
|
691
890
|
}
|
|
692
891
|
}
|
|
693
892
|
if (plan.closeIssue) {
|
|
@@ -867,7 +1066,7 @@ export async function runTriage(input) {
|
|
|
867
1066
|
throw new Error("triage configuration is required");
|
|
868
1067
|
const agents = input.repository.agents.triage;
|
|
869
1068
|
if (!agents?.length)
|
|
870
|
-
throw new Error("triage.
|
|
1069
|
+
throw new Error("triage.voters is required");
|
|
871
1070
|
const runId = input.runId ?? `run-${Date.now().toString(36)}`;
|
|
872
1071
|
const outputDir = issueRunOutputDir({
|
|
873
1072
|
config: input.config,
|
|
@@ -896,7 +1095,7 @@ export async function runTriage(input) {
|
|
|
896
1095
|
issue: input.issue,
|
|
897
1096
|
outputDir,
|
|
898
1097
|
report,
|
|
899
|
-
result: { category: null, disposition: "
|
|
1098
|
+
result: { category: null, disposition: "blocked", signals: [] },
|
|
900
1099
|
};
|
|
901
1100
|
}
|
|
902
1101
|
let context = issueContext({ issue, relationship });
|
|
@@ -978,30 +1177,32 @@ export async function runTriage(input) {
|
|
|
978
1177
|
});
|
|
979
1178
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
980
1179
|
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
981
|
-
if (previous.disposition !== "
|
|
982
|
-
previous.askReason !== "acceptance_unclear") {
|
|
1180
|
+
if (previous.disposition !== "needs_acceptance") {
|
|
983
1181
|
const reconsideration = await runReconsiderationVote({
|
|
984
1182
|
context,
|
|
985
1183
|
input,
|
|
986
1184
|
outputDir,
|
|
987
1185
|
});
|
|
988
|
-
|
|
989
|
-
commentReason = chooseDecisionReason({
|
|
990
|
-
outputs: reconsideration.outputs,
|
|
991
|
-
reporter,
|
|
992
|
-
vote: reconsideration.vote ?? "ASK",
|
|
993
|
-
});
|
|
1186
|
+
commentReason = reconsideration.reason;
|
|
994
1187
|
result =
|
|
995
1188
|
reconsideration.vote === "YES"
|
|
996
|
-
? {
|
|
1189
|
+
? {
|
|
1190
|
+
category: previous.category,
|
|
1191
|
+
disposition: "accepted",
|
|
1192
|
+
signals: [],
|
|
1193
|
+
}
|
|
997
1194
|
: reconsideration.vote === "NO"
|
|
998
|
-
? {
|
|
1195
|
+
? {
|
|
1196
|
+
category: previous.category,
|
|
1197
|
+
disposition: "rejected",
|
|
1198
|
+
signals: [],
|
|
1199
|
+
}
|
|
999
1200
|
: {
|
|
1000
|
-
askReason: "acceptance_unclear",
|
|
1001
1201
|
category: previous.category,
|
|
1002
|
-
disposition: "
|
|
1202
|
+
disposition: "needs_acceptance",
|
|
1203
|
+
signals: [],
|
|
1003
1204
|
};
|
|
1004
|
-
if (result.disposition === "
|
|
1205
|
+
if (result.disposition === "needs_acceptance") {
|
|
1005
1206
|
askCommentOutputs = askOutputs(reconsideration.outputs);
|
|
1006
1207
|
markAskComments = true;
|
|
1007
1208
|
}
|
|
@@ -1024,6 +1225,7 @@ export async function runTriage(input) {
|
|
|
1024
1225
|
const relatedPrDecision = {
|
|
1025
1226
|
category: resolveIssueCategory(issue, input.repository) ?? null,
|
|
1026
1227
|
disposition: "accepted",
|
|
1228
|
+
signals: [],
|
|
1027
1229
|
};
|
|
1028
1230
|
const plan = {
|
|
1029
1231
|
action: "CLOSE",
|
|
@@ -1034,11 +1236,7 @@ export async function runTriage(input) {
|
|
|
1034
1236
|
postComment: true,
|
|
1035
1237
|
};
|
|
1036
1238
|
return finishWithResult({
|
|
1037
|
-
commentReason:
|
|
1038
|
-
outputs: existingPr.outputs,
|
|
1039
|
-
reporter: triageReporter(input.repository, issue.number),
|
|
1040
|
-
vote: "RELATED_PR_HANDLES_ISSUE",
|
|
1041
|
-
}),
|
|
1239
|
+
commentReason: existingPr.reason,
|
|
1042
1240
|
context,
|
|
1043
1241
|
input,
|
|
1044
1242
|
issue,
|
|
@@ -1057,7 +1255,7 @@ export async function runTriage(input) {
|
|
|
1057
1255
|
outputDir,
|
|
1058
1256
|
processed,
|
|
1059
1257
|
relationship,
|
|
1060
|
-
result: { category: null, disposition: "
|
|
1258
|
+
result: { category: null, disposition: "already_handled", signals: [] },
|
|
1061
1259
|
runId,
|
|
1062
1260
|
});
|
|
1063
1261
|
}
|
|
@@ -1072,7 +1270,7 @@ export async function runTriage(input) {
|
|
|
1072
1270
|
if (duplicate) {
|
|
1073
1271
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
1074
1272
|
commentReason = duplicate.reason;
|
|
1075
|
-
result = { category: null, disposition: "duplicate" };
|
|
1273
|
+
result = { category: null, disposition: "duplicate", signals: [] };
|
|
1076
1274
|
}
|
|
1077
1275
|
}
|
|
1078
1276
|
if (!result) {
|
|
@@ -1096,9 +1294,9 @@ export async function runTriage(input) {
|
|
|
1096
1294
|
const category = resolvedCategory ?? categoryVote?.vote ?? "ASK";
|
|
1097
1295
|
if (category === "ASK") {
|
|
1098
1296
|
result = {
|
|
1099
|
-
askReason: "category_unclear",
|
|
1100
1297
|
category: null,
|
|
1101
|
-
disposition: "
|
|
1298
|
+
disposition: "needs_category",
|
|
1299
|
+
signals: [],
|
|
1102
1300
|
};
|
|
1103
1301
|
askCommentOutputs = askOutputs(categoryVote?.outputs);
|
|
1104
1302
|
markAskComments = false;
|
|
@@ -1117,30 +1315,41 @@ export async function runTriage(input) {
|
|
|
1117
1315
|
phase: "acceptance",
|
|
1118
1316
|
prompt: composeTriageAcceptancePrompt,
|
|
1119
1317
|
schemaName: "triage acceptance",
|
|
1120
|
-
votes:
|
|
1121
|
-
});
|
|
1122
|
-
const reporter = triageReporter(input.repository, issue.number);
|
|
1123
|
-
commentReason = chooseDecisionReason({
|
|
1124
|
-
outputs: acceptance.outputs,
|
|
1125
|
-
reporter,
|
|
1126
|
-
vote: acceptance.vote ?? "ASK",
|
|
1318
|
+
votes: ACCEPTANCE_VOTES,
|
|
1127
1319
|
});
|
|
1320
|
+
commentReason = acceptance.reason;
|
|
1128
1321
|
result =
|
|
1129
1322
|
acceptance.vote === "YES"
|
|
1130
|
-
? { category, disposition: "accepted" }
|
|
1323
|
+
? { category, disposition: "accepted", signals: [] }
|
|
1131
1324
|
: acceptance.vote === "NO"
|
|
1132
|
-
? { category, disposition: "rejected" }
|
|
1133
|
-
:
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1325
|
+
? { category, disposition: "rejected", signals: [] }
|
|
1326
|
+
: acceptance.vote === "INVALID"
|
|
1327
|
+
? { category, disposition: "invalid", signals: [] }
|
|
1328
|
+
: {
|
|
1329
|
+
category,
|
|
1330
|
+
disposition: "needs_acceptance",
|
|
1331
|
+
signals: [],
|
|
1332
|
+
};
|
|
1333
|
+
if (result.disposition === "needs_acceptance") {
|
|
1139
1334
|
askCommentOutputs = askOutputs(acceptance.outputs);
|
|
1140
1335
|
markAskComments = true;
|
|
1141
1336
|
}
|
|
1142
1337
|
}
|
|
1143
1338
|
}
|
|
1339
|
+
if (result &&
|
|
1340
|
+
result.disposition !== "blocked" &&
|
|
1341
|
+
result.disposition !== "failed" &&
|
|
1342
|
+
triage.signals.length) {
|
|
1343
|
+
const signalContext = `${context}\n\nFinal triage result: ${JSON.stringify(result)}`;
|
|
1344
|
+
result = {
|
|
1345
|
+
...result,
|
|
1346
|
+
signals: await runSignalVote({
|
|
1347
|
+
context: signalContext,
|
|
1348
|
+
input,
|
|
1349
|
+
outputDir,
|
|
1350
|
+
}),
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1144
1353
|
return finishWithResult({
|
|
1145
1354
|
askOutputs: askCommentOutputs,
|
|
1146
1355
|
commentReason,
|
|
@@ -1152,9 +1361,9 @@ export async function runTriage(input) {
|
|
|
1152
1361
|
processed,
|
|
1153
1362
|
relationship,
|
|
1154
1363
|
result: result ?? {
|
|
1155
|
-
askReason: "acceptance_unclear",
|
|
1156
1364
|
category: null,
|
|
1157
|
-
disposition: "
|
|
1365
|
+
disposition: "needs_acceptance",
|
|
1366
|
+
signals: [],
|
|
1158
1367
|
},
|
|
1159
1368
|
runId,
|
|
1160
1369
|
});
|