opencode-magi 0.7.0 → 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/dist/config/resolve.js +28 -1
- package/dist/config/validate.js +97 -3
- package/dist/github/commands.js +32 -0
- package/dist/orchestrator/merge.js +310 -10
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +97 -1
- package/dist/orchestrator/triage.js +249 -64
- 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 +57 -4
|
@@ -163,6 +163,90 @@ export async function inlineCommentTargetsForDiff(input) {
|
|
|
163
163
|
cwd: input.worktreePath,
|
|
164
164
|
}));
|
|
165
165
|
}
|
|
166
|
+
function firstTargetLine(targets, path) {
|
|
167
|
+
const lines = targets.get(path);
|
|
168
|
+
if (!lines?.size)
|
|
169
|
+
return undefined;
|
|
170
|
+
return [...lines].sort((a, b) => a - b)[0];
|
|
171
|
+
}
|
|
172
|
+
function mergeInlineCommentTargets(left, right) {
|
|
173
|
+
const merged = new Map();
|
|
174
|
+
for (const [path, lines] of [...left, ...right]) {
|
|
175
|
+
const targetLines = merged.get(path) ?? new Set();
|
|
176
|
+
for (const line of lines)
|
|
177
|
+
targetLines.add(line);
|
|
178
|
+
merged.set(path, targetLines);
|
|
179
|
+
}
|
|
180
|
+
return merged;
|
|
181
|
+
}
|
|
182
|
+
function targetLineSummary(targets, path) {
|
|
183
|
+
const lines = targets.get(path);
|
|
184
|
+
if (!lines?.size)
|
|
185
|
+
return "(none)";
|
|
186
|
+
const sorted = [...lines].sort((a, b) => a - b);
|
|
187
|
+
const shown = sorted.slice(0, 12).join(", ");
|
|
188
|
+
return sorted.length > 12 ? `${shown}, ...` : shown;
|
|
189
|
+
}
|
|
190
|
+
function indentedExcerpt(lines) {
|
|
191
|
+
return lines
|
|
192
|
+
.slice(0, 24)
|
|
193
|
+
.map((line) => ` ${line}`)
|
|
194
|
+
.join("\n");
|
|
195
|
+
}
|
|
196
|
+
function parseMergeConflictSections(output) {
|
|
197
|
+
const conflictHeaders = new Set([
|
|
198
|
+
"added in both",
|
|
199
|
+
"changed in both",
|
|
200
|
+
"removed in local",
|
|
201
|
+
"removed in remote",
|
|
202
|
+
]);
|
|
203
|
+
const sections = [];
|
|
204
|
+
let current;
|
|
205
|
+
for (const line of output.split("\n")) {
|
|
206
|
+
if (!line.trim())
|
|
207
|
+
continue;
|
|
208
|
+
if (!line.startsWith(" ") &&
|
|
209
|
+
!line.startsWith("+") &&
|
|
210
|
+
!line.startsWith("-") &&
|
|
211
|
+
!line.startsWith("@")) {
|
|
212
|
+
current = conflictHeaders.has(line)
|
|
213
|
+
? { lines: [line], paths: new Set() }
|
|
214
|
+
: undefined;
|
|
215
|
+
if (current)
|
|
216
|
+
sections.push(current);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!current)
|
|
220
|
+
continue;
|
|
221
|
+
current.lines.push(line);
|
|
222
|
+
const path = /^ (?:base|our|their)\s+\d+\s+[0-9a-f]+\s+(.+)$/.exec(line)?.[1];
|
|
223
|
+
if (path)
|
|
224
|
+
current.paths.add(path);
|
|
225
|
+
}
|
|
226
|
+
return sections.flatMap((section) => [...section.paths].map((path) => ({
|
|
227
|
+
excerpt: indentedExcerpt(section.lines),
|
|
228
|
+
path,
|
|
229
|
+
})));
|
|
230
|
+
}
|
|
231
|
+
export async function mergeConflictContextForDiff(input) {
|
|
232
|
+
const mergeBase = (await input.exec(`git merge-base ${shellQuote(input.baseSha)} ${shellQuote(input.headSha)}`, { cwd: input.worktreePath })).trim();
|
|
233
|
+
const output = await input.exec(`git merge-tree ${shellQuote(mergeBase)} ${shellQuote(input.headSha)} ${shellQuote(input.baseSha)}`, { cwd: input.worktreePath });
|
|
234
|
+
const conflicts = parseMergeConflictSections(output);
|
|
235
|
+
if (!conflicts.length)
|
|
236
|
+
return "";
|
|
237
|
+
return [
|
|
238
|
+
"The PR currently has unresolved merge conflicts with the base branch.",
|
|
239
|
+
"Treat unresolved conflicts as review findings and request changes when they make the PR unsafe or impossible to merge.",
|
|
240
|
+
"Use suggestedLine when it is present; it is a valid right-side PR diff line for an inline finding.",
|
|
241
|
+
...conflicts.map((conflict) => {
|
|
242
|
+
const suggestedLine = firstTargetLine(input.inlineCommentTargets, conflict.path);
|
|
243
|
+
const suggestedLineText = suggestedLine
|
|
244
|
+
? `suggestedLine: ${suggestedLine}`
|
|
245
|
+
: "suggestedLine: (no right-side PR diff line found)";
|
|
246
|
+
return `<conflict_file>\npath: ${conflict.path}\n${suggestedLineText}\nrightSideDiffLines: ${targetLineSummary(input.inlineCommentTargets, conflict.path)}\nmergeTreeExcerpt:\n${conflict.excerpt}\n</conflict_file>`;
|
|
247
|
+
}),
|
|
248
|
+
].join("\n");
|
|
249
|
+
}
|
|
166
250
|
function parsePostedFindingLocation(location) {
|
|
167
251
|
const range = /^(.*):(\d+)-(\d+)$/.exec(location);
|
|
168
252
|
if (range) {
|
|
@@ -709,6 +793,13 @@ export async function runReview(input) {
|
|
|
709
793
|
toSha: meta.headRefOid,
|
|
710
794
|
worktreePath,
|
|
711
795
|
});
|
|
796
|
+
const mergeConflictContext = await mergeConflictContextForDiff({
|
|
797
|
+
baseSha: meta.baseRefOid,
|
|
798
|
+
exec,
|
|
799
|
+
headSha: meta.headRefOid,
|
|
800
|
+
inlineCommentTargets: initialInlineCommentTargets,
|
|
801
|
+
worktreePath,
|
|
802
|
+
});
|
|
712
803
|
for (const reviewer of input.repository.agents.reviewers) {
|
|
713
804
|
const assignment = mode.assignments.get(reviewer.account);
|
|
714
805
|
if (assignment?.type !== "skip")
|
|
@@ -740,6 +831,9 @@ export async function runReview(input) {
|
|
|
740
831
|
toSha: meta.headRefOid,
|
|
741
832
|
worktreePath,
|
|
742
833
|
});
|
|
834
|
+
const rereviewInlineCommentTargets = mergeConflictContext
|
|
835
|
+
? mergeInlineCommentTargets(inlineCommentTargets, initialInlineCommentTargets)
|
|
836
|
+
: inlineCommentTargets;
|
|
743
837
|
const unresolved = unresolvedThreadsByAccount.get(reviewer.account) ??
|
|
744
838
|
(await fetchUnresolvedThreads(exec, input.repository, input.pr, reviewer.account));
|
|
745
839
|
const prompt = await composeRereviewPrompt({
|
|
@@ -747,6 +841,7 @@ export async function runReview(input) {
|
|
|
747
841
|
ciFailureContext,
|
|
748
842
|
directory: input.directory,
|
|
749
843
|
headSha: meta.headRefOid,
|
|
844
|
+
mergeConflictContext,
|
|
750
845
|
pr: input.pr,
|
|
751
846
|
previousReview: previousReviewText(previous),
|
|
752
847
|
previousHeadSha: previous.commit.oid,
|
|
@@ -787,7 +882,7 @@ export async function runReview(input) {
|
|
|
787
882
|
},
|
|
788
883
|
options: reviewer.options,
|
|
789
884
|
parentSessionId: input.parentSessionId,
|
|
790
|
-
parse: (text) => parseRereviewOutputWithInlineTargets(text,
|
|
885
|
+
parse: (text) => parseRereviewOutputWithInlineTargets(text, rereviewInlineCommentTargets),
|
|
791
886
|
permission: reviewer.permission,
|
|
792
887
|
prompt,
|
|
793
888
|
repairAttempts: input.config.output?.repairAttempts ?? 3,
|
|
@@ -819,6 +914,7 @@ export async function runReview(input) {
|
|
|
819
914
|
ciFailureContext,
|
|
820
915
|
directory: input.directory,
|
|
821
916
|
headSha: meta.headRefOid,
|
|
917
|
+
mergeConflictContext,
|
|
822
918
|
pr: input.pr,
|
|
823
919
|
repository: input.repository,
|
|
824
920
|
reviewContext,
|
|
@@ -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;
|
|
@@ -305,6 +360,86 @@ async function runPhaseVote(input) {
|
|
|
305
360
|
vote: majority.vote,
|
|
306
361
|
};
|
|
307
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;
|
|
442
|
+
}
|
|
308
443
|
async function relationshipScan(input, issue) {
|
|
309
444
|
const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
|
|
310
445
|
fetchIssueComments(input.exec, input.repository, input.issue),
|
|
@@ -384,40 +519,40 @@ export function eligibleMentionReplies(input) {
|
|
|
384
519
|
function finalResultFromMarker(marker) {
|
|
385
520
|
if (marker.disposition) {
|
|
386
521
|
return {
|
|
387
|
-
askReason: marker.askReason,
|
|
388
522
|
category: marker.category ?? null,
|
|
389
523
|
disposition: marker.disposition,
|
|
524
|
+
signals: marker.signals,
|
|
390
525
|
};
|
|
391
526
|
}
|
|
392
527
|
switch (marker.result) {
|
|
393
528
|
case "BUG_ACCEPTED":
|
|
394
529
|
case "RESOLVED_BY_MERGED_PR":
|
|
395
|
-
return { category: "bug", disposition: "accepted" };
|
|
530
|
+
return { category: "bug", disposition: "accepted", signals: [] };
|
|
396
531
|
case "BUG_REJECTED":
|
|
397
|
-
return { category: "bug", disposition: "rejected" };
|
|
532
|
+
return { category: "bug", disposition: "rejected", signals: [] };
|
|
398
533
|
case "FEATURE_ACCEPTED":
|
|
399
|
-
return { category: "feature", disposition: "accepted" };
|
|
534
|
+
return { category: "feature", disposition: "accepted", signals: [] };
|
|
400
535
|
case "FEATURE_REJECTED":
|
|
401
|
-
return { category: "feature", disposition: "rejected" };
|
|
536
|
+
return { category: "feature", disposition: "rejected", signals: [] };
|
|
402
537
|
case "ASK":
|
|
403
538
|
return {
|
|
404
|
-
askReason: "acceptance_unclear",
|
|
405
539
|
category: null,
|
|
406
|
-
disposition: "
|
|
540
|
+
disposition: "needs_acceptance",
|
|
541
|
+
signals: [],
|
|
407
542
|
};
|
|
408
543
|
case "CLEAR_ONLY":
|
|
409
|
-
return { category: null, disposition: "
|
|
544
|
+
return { category: null, disposition: "already_handled", signals: [] };
|
|
410
545
|
case "DUPLICATE":
|
|
411
|
-
return { category: null, disposition: "duplicate" };
|
|
546
|
+
return { category: null, disposition: "duplicate", signals: [] };
|
|
412
547
|
default:
|
|
413
|
-
return { category: null, disposition: "failed" };
|
|
548
|
+
return { category: null, disposition: "failed", signals: [] };
|
|
414
549
|
}
|
|
415
550
|
}
|
|
416
551
|
function decisionText(decision) {
|
|
417
552
|
return JSON.stringify(decision);
|
|
418
553
|
}
|
|
419
554
|
function actionPlan(input) {
|
|
420
|
-
if (input.result.disposition === "
|
|
555
|
+
if (input.result.disposition === "already_handled") {
|
|
421
556
|
return {
|
|
422
557
|
action: "CLEAR_ONLY",
|
|
423
558
|
allowedActions: ["CLEAR_ONLY"],
|
|
@@ -427,11 +562,12 @@ function actionPlan(input) {
|
|
|
427
562
|
postComment: false,
|
|
428
563
|
};
|
|
429
564
|
}
|
|
430
|
-
if (input.result.disposition === "
|
|
565
|
+
if (input.result.disposition === "needs_category" ||
|
|
566
|
+
input.result.disposition === "needs_acceptance") {
|
|
431
567
|
return {
|
|
432
568
|
action: "ASK",
|
|
433
569
|
allowedActions: ["ASK"],
|
|
434
|
-
clearLabels:
|
|
570
|
+
clearLabels: true,
|
|
435
571
|
closeIssue: false,
|
|
436
572
|
createPr: false,
|
|
437
573
|
postComment: true,
|
|
@@ -439,6 +575,7 @@ function actionPlan(input) {
|
|
|
439
575
|
}
|
|
440
576
|
const closeIssue = input.triage.automation.close &&
|
|
441
577
|
(input.result.disposition === "rejected" ||
|
|
578
|
+
input.result.disposition === "invalid" ||
|
|
442
579
|
input.result.disposition === "duplicate");
|
|
443
580
|
const createPr = input.triage.automation.create && input.result.disposition === "accepted";
|
|
444
581
|
return {
|
|
@@ -452,8 +589,13 @@ function actionPlan(input) {
|
|
|
452
589
|
}
|
|
453
590
|
function previousAutomationPlan(input) {
|
|
454
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
|
+
});
|
|
455
597
|
const clearLabels = base.clearLabels &&
|
|
456
|
-
|
|
598
|
+
(labelChanges.add.length > 0 || labelChanges.remove.length > 0);
|
|
457
599
|
const closeIssue = input.marker.action === "CLOSE" &&
|
|
458
600
|
base.closeIssue &&
|
|
459
601
|
input.issue.state === "OPEN";
|
|
@@ -558,6 +700,9 @@ function decisionCommentFallback(input) {
|
|
|
558
700
|
if (input.result.disposition === "duplicate") {
|
|
559
701
|
return "Magi marked this issue as a duplicate.";
|
|
560
702
|
}
|
|
703
|
+
if (input.result.disposition === "invalid") {
|
|
704
|
+
return "Magi marked this issue as invalid or not actionable.";
|
|
705
|
+
}
|
|
561
706
|
return "Magi completed triage for this issue.";
|
|
562
707
|
}
|
|
563
708
|
function agentForKey(repository, key) {
|
|
@@ -675,7 +820,9 @@ async function finishWithResult(input) {
|
|
|
675
820
|
});
|
|
676
821
|
let prUrl;
|
|
677
822
|
const reporter = triageReporter(input.input.repository, input.issue.number);
|
|
678
|
-
const comment = plan.postComment &&
|
|
823
|
+
const comment = plan.postComment &&
|
|
824
|
+
input.result.disposition !== "needs_category" &&
|
|
825
|
+
input.result.disposition !== "needs_acceptance"
|
|
679
826
|
? `${decisionCommentBody({
|
|
680
827
|
action: plan.action,
|
|
681
828
|
reason: input.commentReason,
|
|
@@ -691,7 +838,19 @@ async function finishWithResult(input) {
|
|
|
691
838
|
if (comment) {
|
|
692
839
|
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
693
840
|
}
|
|
694
|
-
|
|
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) {
|
|
695
854
|
await postAskComments({
|
|
696
855
|
action: plan.action,
|
|
697
856
|
dryRun: input.input.dryRun,
|
|
@@ -723,9 +882,11 @@ async function finishWithResult(input) {
|
|
|
723
882
|
});
|
|
724
883
|
}
|
|
725
884
|
if (plan.clearLabels) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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);
|
|
729
890
|
}
|
|
730
891
|
}
|
|
731
892
|
if (plan.closeIssue) {
|
|
@@ -934,7 +1095,7 @@ export async function runTriage(input) {
|
|
|
934
1095
|
issue: input.issue,
|
|
935
1096
|
outputDir,
|
|
936
1097
|
report,
|
|
937
|
-
result: { category: null, disposition: "
|
|
1098
|
+
result: { category: null, disposition: "blocked", signals: [] },
|
|
938
1099
|
};
|
|
939
1100
|
}
|
|
940
1101
|
let context = issueContext({ issue, relationship });
|
|
@@ -1016,8 +1177,7 @@ export async function runTriage(input) {
|
|
|
1016
1177
|
});
|
|
1017
1178
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
1018
1179
|
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
1019
|
-
if (previous.disposition !== "
|
|
1020
|
-
previous.askReason !== "acceptance_unclear") {
|
|
1180
|
+
if (previous.disposition !== "needs_acceptance") {
|
|
1021
1181
|
const reconsideration = await runReconsiderationVote({
|
|
1022
1182
|
context,
|
|
1023
1183
|
input,
|
|
@@ -1026,15 +1186,23 @@ export async function runTriage(input) {
|
|
|
1026
1186
|
commentReason = reconsideration.reason;
|
|
1027
1187
|
result =
|
|
1028
1188
|
reconsideration.vote === "YES"
|
|
1029
|
-
? {
|
|
1189
|
+
? {
|
|
1190
|
+
category: previous.category,
|
|
1191
|
+
disposition: "accepted",
|
|
1192
|
+
signals: [],
|
|
1193
|
+
}
|
|
1030
1194
|
: reconsideration.vote === "NO"
|
|
1031
|
-
? {
|
|
1195
|
+
? {
|
|
1196
|
+
category: previous.category,
|
|
1197
|
+
disposition: "rejected",
|
|
1198
|
+
signals: [],
|
|
1199
|
+
}
|
|
1032
1200
|
: {
|
|
1033
|
-
askReason: "acceptance_unclear",
|
|
1034
1201
|
category: previous.category,
|
|
1035
|
-
disposition: "
|
|
1202
|
+
disposition: "needs_acceptance",
|
|
1203
|
+
signals: [],
|
|
1036
1204
|
};
|
|
1037
|
-
if (result.disposition === "
|
|
1205
|
+
if (result.disposition === "needs_acceptance") {
|
|
1038
1206
|
askCommentOutputs = askOutputs(reconsideration.outputs);
|
|
1039
1207
|
markAskComments = true;
|
|
1040
1208
|
}
|
|
@@ -1057,6 +1225,7 @@ export async function runTriage(input) {
|
|
|
1057
1225
|
const relatedPrDecision = {
|
|
1058
1226
|
category: resolveIssueCategory(issue, input.repository) ?? null,
|
|
1059
1227
|
disposition: "accepted",
|
|
1228
|
+
signals: [],
|
|
1060
1229
|
};
|
|
1061
1230
|
const plan = {
|
|
1062
1231
|
action: "CLOSE",
|
|
@@ -1086,7 +1255,7 @@ export async function runTriage(input) {
|
|
|
1086
1255
|
outputDir,
|
|
1087
1256
|
processed,
|
|
1088
1257
|
relationship,
|
|
1089
|
-
result: { category: null, disposition: "
|
|
1258
|
+
result: { category: null, disposition: "already_handled", signals: [] },
|
|
1090
1259
|
runId,
|
|
1091
1260
|
});
|
|
1092
1261
|
}
|
|
@@ -1101,7 +1270,7 @@ export async function runTriage(input) {
|
|
|
1101
1270
|
if (duplicate) {
|
|
1102
1271
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
1103
1272
|
commentReason = duplicate.reason;
|
|
1104
|
-
result = { category: null, disposition: "duplicate" };
|
|
1273
|
+
result = { category: null, disposition: "duplicate", signals: [] };
|
|
1105
1274
|
}
|
|
1106
1275
|
}
|
|
1107
1276
|
if (!result) {
|
|
@@ -1125,9 +1294,9 @@ export async function runTriage(input) {
|
|
|
1125
1294
|
const category = resolvedCategory ?? categoryVote?.vote ?? "ASK";
|
|
1126
1295
|
if (category === "ASK") {
|
|
1127
1296
|
result = {
|
|
1128
|
-
askReason: "category_unclear",
|
|
1129
1297
|
category: null,
|
|
1130
|
-
disposition: "
|
|
1298
|
+
disposition: "needs_category",
|
|
1299
|
+
signals: [],
|
|
1131
1300
|
};
|
|
1132
1301
|
askCommentOutputs = askOutputs(categoryVote?.outputs);
|
|
1133
1302
|
markAskComments = false;
|
|
@@ -1146,25 +1315,41 @@ export async function runTriage(input) {
|
|
|
1146
1315
|
phase: "acceptance",
|
|
1147
1316
|
prompt: composeTriageAcceptancePrompt,
|
|
1148
1317
|
schemaName: "triage acceptance",
|
|
1149
|
-
votes:
|
|
1318
|
+
votes: ACCEPTANCE_VOTES,
|
|
1150
1319
|
});
|
|
1151
1320
|
commentReason = acceptance.reason;
|
|
1152
1321
|
result =
|
|
1153
1322
|
acceptance.vote === "YES"
|
|
1154
|
-
? { category, disposition: "accepted" }
|
|
1323
|
+
? { category, disposition: "accepted", signals: [] }
|
|
1155
1324
|
: acceptance.vote === "NO"
|
|
1156
|
-
? { category, disposition: "rejected" }
|
|
1157
|
-
:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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") {
|
|
1163
1334
|
askCommentOutputs = askOutputs(acceptance.outputs);
|
|
1164
1335
|
markAskComments = true;
|
|
1165
1336
|
}
|
|
1166
1337
|
}
|
|
1167
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
|
+
}
|
|
1168
1353
|
return finishWithResult({
|
|
1169
1354
|
askOutputs: askCommentOutputs,
|
|
1170
1355
|
commentReason,
|
|
@@ -1176,9 +1361,9 @@ export async function runTriage(input) {
|
|
|
1176
1361
|
processed,
|
|
1177
1362
|
relationship,
|
|
1178
1363
|
result: result ?? {
|
|
1179
|
-
askReason: "acceptance_unclear",
|
|
1180
1364
|
category: null,
|
|
1181
|
-
disposition: "
|
|
1365
|
+
disposition: "needs_acceptance",
|
|
1366
|
+
signals: [],
|
|
1182
1367
|
},
|
|
1183
1368
|
runId,
|
|
1184
1369
|
});
|