opencode-magi 0.0.0-dev-20260525091701 → 0.0.0-dev-20260525101932
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 +27 -1
- package/dist/config/validate.js +95 -2
- package/dist/github/commands.js +9 -0
- package/dist/orchestrator/triage.js +249 -64
- package/dist/prompts/compose.js +13 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- 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 +47 -3
package/dist/config/resolve.js
CHANGED
|
@@ -24,6 +24,31 @@ const DEFAULT_TRIAGE_CATEGORIES = [
|
|
|
24
24
|
types: ["Feature"],
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
|
+
export const DEFAULT_TRIAGE_LABEL_RULES = [
|
|
28
|
+
{ remove: ["triage"], when: { disposition: "accepted" } },
|
|
29
|
+
{
|
|
30
|
+
add: ["duplicate"],
|
|
31
|
+
remove: ["triage"],
|
|
32
|
+
when: { disposition: "duplicate" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
add: ["duplicate"],
|
|
36
|
+
remove: ["triage"],
|
|
37
|
+
when: { disposition: "already_handled" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
add: ["wontfix"],
|
|
41
|
+
remove: ["triage"],
|
|
42
|
+
when: { disposition: "rejected" },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
add: ["invalid"],
|
|
46
|
+
remove: ["triage"],
|
|
47
|
+
when: { disposition: "invalid" },
|
|
48
|
+
},
|
|
49
|
+
{ add: ["question"], when: { disposition: "needs_category" } },
|
|
50
|
+
{ add: ["question"], when: { disposition: "needs_acceptance" } },
|
|
51
|
+
];
|
|
27
52
|
export function reviewerKey(reviewer, index) {
|
|
28
53
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
29
54
|
}
|
|
@@ -191,9 +216,9 @@ export function resolveRepository(config) {
|
|
|
191
216
|
},
|
|
192
217
|
triage: {
|
|
193
218
|
automation: {
|
|
194
|
-
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
195
219
|
close: config.triage?.automation?.close ?? false,
|
|
196
220
|
create: config.triage?.automation?.create ?? false,
|
|
221
|
+
label: config.triage?.automation?.label ?? DEFAULT_TRIAGE_LABEL_RULES,
|
|
197
222
|
merge: config.triage?.automation?.merge ?? false,
|
|
198
223
|
review: config.triage?.automation?.review ?? false,
|
|
199
224
|
},
|
|
@@ -216,6 +241,7 @@ export function resolveRepository(config) {
|
|
|
216
241
|
blockedLabels: config.triage?.safety?.blockedLabels ?? [],
|
|
217
242
|
requiredLabels: config.triage?.safety?.requiredLabels ?? ["triage"],
|
|
218
243
|
},
|
|
244
|
+
signals: config.triage?.signals ?? [],
|
|
219
245
|
worktree: config.triage?.worktree,
|
|
220
246
|
},
|
|
221
247
|
};
|
package/dist/config/validate.js
CHANGED
|
@@ -80,6 +80,7 @@ const TRIAGE_KEYS = new Set([
|
|
|
80
80
|
"prompts",
|
|
81
81
|
"reporter",
|
|
82
82
|
"safety",
|
|
83
|
+
"signals",
|
|
83
84
|
"voters",
|
|
84
85
|
"worktree",
|
|
85
86
|
]);
|
|
@@ -98,13 +99,19 @@ const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
|
98
99
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
99
100
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
100
101
|
const TRIAGE_AUTOMATION_KEYS = new Set([
|
|
101
|
-
"clear",
|
|
102
102
|
"close",
|
|
103
103
|
"create",
|
|
104
|
+
"label",
|
|
104
105
|
"merge",
|
|
105
106
|
"review",
|
|
106
107
|
]);
|
|
107
108
|
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
109
|
+
const TRIAGE_LABEL_RULE_KEYS = new Set(["add", "remove", "when"]);
|
|
110
|
+
const TRIAGE_LABEL_RULE_WHEN_KEYS = new Set([
|
|
111
|
+
"category",
|
|
112
|
+
"disposition",
|
|
113
|
+
"signals",
|
|
114
|
+
]);
|
|
108
115
|
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
109
116
|
const TRIAGE_SAFETY_KEYS = new Set([
|
|
110
117
|
"allowAuthors",
|
|
@@ -113,6 +120,18 @@ const TRIAGE_SAFETY_KEYS = new Set([
|
|
|
113
120
|
"blockedLabels",
|
|
114
121
|
"requiredLabels",
|
|
115
122
|
]);
|
|
123
|
+
const TRIAGE_SIGNAL_KEYS = new Set(["description", "id"]);
|
|
124
|
+
const TRIAGE_DISPOSITIONS = new Set([
|
|
125
|
+
"accepted",
|
|
126
|
+
"rejected",
|
|
127
|
+
"invalid",
|
|
128
|
+
"duplicate",
|
|
129
|
+
"already_handled",
|
|
130
|
+
"needs_category",
|
|
131
|
+
"needs_acceptance",
|
|
132
|
+
"blocked",
|
|
133
|
+
"failed",
|
|
134
|
+
]);
|
|
116
135
|
const SAFETY_KEYS = new Set([
|
|
117
136
|
"allowAuthors",
|
|
118
137
|
"blockedPaths",
|
|
@@ -692,6 +711,79 @@ function validateTriageCategories(categories, path, errors) {
|
|
|
692
711
|
validateString(category.description, `${itemPath}.description`, errors);
|
|
693
712
|
});
|
|
694
713
|
}
|
|
714
|
+
function validateTriageSignals(signals, path, errors) {
|
|
715
|
+
if (signals == null)
|
|
716
|
+
return;
|
|
717
|
+
if (!Array.isArray(signals)) {
|
|
718
|
+
errors.push(`${path} must be an array`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const ids = new Set();
|
|
722
|
+
signals.forEach((item, index) => {
|
|
723
|
+
const itemPath = `${path}[${index}]`;
|
|
724
|
+
if (!isPlainObject(item)) {
|
|
725
|
+
errors.push(`${itemPath} must be an object`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const signal = item;
|
|
729
|
+
validateKnownKeys(signal, itemPath, TRIAGE_SIGNAL_KEYS, errors);
|
|
730
|
+
if (!signal.id) {
|
|
731
|
+
errors.push(`${itemPath}.id is required`);
|
|
732
|
+
}
|
|
733
|
+
else if (typeof signal.id !== "string") {
|
|
734
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
735
|
+
}
|
|
736
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(signal.id)) {
|
|
737
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
738
|
+
}
|
|
739
|
+
else if (ids.has(signal.id)) {
|
|
740
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
ids.add(signal.id);
|
|
744
|
+
}
|
|
745
|
+
if (!signal.description) {
|
|
746
|
+
errors.push(`${itemPath}.description is required`);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
validateString(signal.description, `${itemPath}.description`, errors);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function validateTriageLabelRules(rules, path, errors) {
|
|
754
|
+
if (rules == null)
|
|
755
|
+
return;
|
|
756
|
+
if (!Array.isArray(rules)) {
|
|
757
|
+
errors.push(`${path} must be an array`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
rules.forEach((item, index) => {
|
|
761
|
+
const itemPath = `${path}[${index}]`;
|
|
762
|
+
if (!isPlainObject(item)) {
|
|
763
|
+
errors.push(`${itemPath} must be an object`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const rule = item;
|
|
767
|
+
validateKnownKeys(rule, itemPath, TRIAGE_LABEL_RULE_KEYS, errors);
|
|
768
|
+
validateStringArray(rule.add, `${itemPath}.add`, errors);
|
|
769
|
+
validateStringArray(rule.remove, `${itemPath}.remove`, errors);
|
|
770
|
+
if (!isPlainObject(rule.when)) {
|
|
771
|
+
errors.push(`${itemPath}.when must be an object`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
validateKnownKeys(rule.when, `${itemPath}.when`, TRIAGE_LABEL_RULE_WHEN_KEYS, errors);
|
|
775
|
+
if (!Object.keys(rule.when).length) {
|
|
776
|
+
errors.push(`${itemPath}.when must not be empty`);
|
|
777
|
+
}
|
|
778
|
+
if (rule.when.disposition != null &&
|
|
779
|
+
(typeof rule.when.disposition !== "string" ||
|
|
780
|
+
!TRIAGE_DISPOSITIONS.has(rule.when.disposition))) {
|
|
781
|
+
errors.push(`${itemPath}.when.disposition must be a triage disposition`);
|
|
782
|
+
}
|
|
783
|
+
validateString(rule.when.category, `${itemPath}.when.category`, errors);
|
|
784
|
+
validateStringArray(rule.when.signals, `${itemPath}.when.signals`, errors);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
695
787
|
function validateSafety(config, errors) {
|
|
696
788
|
const safety = config.review?.safety;
|
|
697
789
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -765,7 +857,7 @@ function validateTriage(config, errors, options) {
|
|
|
765
857
|
validateBoolean(automation?.create, "triage.automation.create", errors);
|
|
766
858
|
validateBoolean(automation?.merge, "triage.automation.merge", errors);
|
|
767
859
|
validateBoolean(automation?.review, "triage.automation.review", errors);
|
|
768
|
-
|
|
860
|
+
validateTriageLabelRules(automation?.label, "triage.automation.label", errors);
|
|
769
861
|
if (automation?.review && !automation.create) {
|
|
770
862
|
errors.push("triage.automation.review requires triage.automation.create to be true");
|
|
771
863
|
}
|
|
@@ -780,6 +872,7 @@ function validateTriage(config, errors, options) {
|
|
|
780
872
|
errors.push("triage.concurrency.runs must be a positive integer");
|
|
781
873
|
}
|
|
782
874
|
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
875
|
+
validateTriageSignals(triage.signals, "triage.signals", errors);
|
|
783
876
|
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
784
877
|
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
785
878
|
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
package/dist/github/commands.js
CHANGED
|
@@ -404,6 +404,15 @@ export async function assignIssue(exec, repository, issue, account) {
|
|
|
404
404
|
const token = await ghToken(exec, repository, account);
|
|
405
405
|
return exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-assignee ${shellQuote(account)}`, ghTokenEnv(token));
|
|
406
406
|
}
|
|
407
|
+
export async function addIssueLabels(exec, repository, issue, labels, account) {
|
|
408
|
+
const token = await ghToken(exec, repository, account);
|
|
409
|
+
const added = [];
|
|
410
|
+
for (const label of labels) {
|
|
411
|
+
await exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-label ${shellQuote(label)}`, ghTokenEnv(token));
|
|
412
|
+
added.push(label);
|
|
413
|
+
}
|
|
414
|
+
return added;
|
|
415
|
+
}
|
|
407
416
|
export async function removeIssueLabels(exec, repository, issue, labels, account) {
|
|
408
417
|
const token = await ghToken(exec, repository, account);
|
|
409
418
|
const removed = [];
|
|
@@ -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
|
});
|
package/dist/prompts/compose.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
-
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageVoteOutputContract, } from "./contracts";
|
|
4
|
+
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageSignalOutputContract, triageVoteOutputContract, } from "./contracts";
|
|
5
5
|
async function readOptionalPrompt(directory, path, values = {}) {
|
|
6
6
|
if (!path)
|
|
7
7
|
return "";
|
|
@@ -90,12 +90,16 @@ function triageValues(input) {
|
|
|
90
90
|
? `- ${category.id}: ${category.description}`
|
|
91
91
|
: `- ${category.id}`)
|
|
92
92
|
.join("\n");
|
|
93
|
+
const signalOptions = (input.repository.triage?.signals ?? [])
|
|
94
|
+
.map((signal) => `- ${signal.id}: ${signal.description}`)
|
|
95
|
+
.join("\n");
|
|
93
96
|
return {
|
|
94
97
|
...repositoryValues(input.repository),
|
|
95
98
|
author: input.author ?? "",
|
|
96
99
|
categoryOptions,
|
|
97
100
|
context: input.context,
|
|
98
101
|
issue: String(input.issue),
|
|
102
|
+
signalOptions,
|
|
99
103
|
worktreePath: input.worktreePath ?? "",
|
|
100
104
|
};
|
|
101
105
|
}
|
|
@@ -441,7 +445,14 @@ export async function composeTriageAcceptancePrompt(input) {
|
|
|
441
445
|
...input,
|
|
442
446
|
builtin: "acceptance",
|
|
443
447
|
customPath: input.repository.triage?.prompts.acceptance,
|
|
444
|
-
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
448
|
+
outputContract: triageVoteOutputContract('"YES" | "NO" | "INVALID" | "ASK"'),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
export async function composeTriageSignalPrompt(input) {
|
|
452
|
+
return composeTriageVotePrompt({
|
|
453
|
+
...input,
|
|
454
|
+
builtin: "signal",
|
|
455
|
+
outputContract: triageSignalOutputContract,
|
|
445
456
|
});
|
|
446
457
|
}
|
|
447
458
|
export async function composeTriageCommentClassificationPrompt(input) {
|
|
@@ -256,6 +256,24 @@ The object must match this shape:
|
|
|
256
256
|
]
|
|
257
257
|
}
|
|
258
258
|
</output_contract>`.trim();
|
|
259
|
+
export const triageSignalOutputContract = `
|
|
260
|
+
<output_contract>
|
|
261
|
+
Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
262
|
+
|
|
263
|
+
The object must match this shape:
|
|
264
|
+
{
|
|
265
|
+
"signals": [
|
|
266
|
+
{
|
|
267
|
+
"id": "configured_signal_id",
|
|
268
|
+
"reason": "Short rationale."
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Rules:
|
|
274
|
+
- Return only configured signal IDs that apply.
|
|
275
|
+
- Return an empty signals array when none apply.
|
|
276
|
+
</output_contract>`.trim();
|
|
259
277
|
const outputContractsBySchemaName = {
|
|
260
278
|
"CI classification": ciClassificationOutputContract,
|
|
261
279
|
"close reconsideration": closeReconsiderationOutputContract,
|
|
@@ -264,13 +282,14 @@ const outputContractsBySchemaName = {
|
|
|
264
282
|
rereview: rereviewOutputContract,
|
|
265
283
|
"rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
|
|
266
284
|
review: reviewOutputContract,
|
|
267
|
-
"triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
285
|
+
"triage acceptance": triageVoteOutputContract('"YES" | "NO" | "INVALID" | "ASK"'),
|
|
268
286
|
"triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
|
|
269
287
|
"triage create PR": triageCreatePrOutputContract,
|
|
270
288
|
"triage comment classification": triageCommentClassificationOutputContract,
|
|
271
289
|
"triage duplicate": triageDuplicateOutputContract,
|
|
272
290
|
"triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
|
|
273
291
|
"triage reconsider": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
292
|
+
"triage signal": triageSignalOutputContract,
|
|
274
293
|
};
|
|
275
294
|
export function repairPrompt(schemaName) {
|
|
276
295
|
const outputContract = outputContractsBySchemaName[schemaName];
|
package/dist/prompts/output.js
CHANGED
|
@@ -112,7 +112,25 @@ export function parseTriageCategoryOutput(text, categories) {
|
|
|
112
112
|
return parseTriageVote(text, ["ASK", ...categories]);
|
|
113
113
|
}
|
|
114
114
|
export function parseTriageBinaryOutput(text) {
|
|
115
|
-
return parseTriageVote(text, ["ASK", "NO", "YES"]);
|
|
115
|
+
return parseTriageVote(text, ["ASK", "INVALID", "NO", "YES"]);
|
|
116
|
+
}
|
|
117
|
+
export function parseTriageSignalOutput(text, signalIds) {
|
|
118
|
+
const data = extractJson(text);
|
|
119
|
+
if (!data || typeof data !== "object")
|
|
120
|
+
throw new Error("triage signal output must be an object");
|
|
121
|
+
const ids = new Set(signalIds);
|
|
122
|
+
return {
|
|
123
|
+
signals: requireArray(data.signals, "signals").map((item, index) => {
|
|
124
|
+
const value = item;
|
|
125
|
+
const id = requireString(value.id, `signals[${index}].id`);
|
|
126
|
+
if (!ids.has(id))
|
|
127
|
+
throw new Error(`signals[${index}].id is not configured`);
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
reason: requireString(value.reason, `signals[${index}].reason`),
|
|
131
|
+
};
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
116
134
|
}
|
|
117
135
|
export function parseTriageDuplicateOutput(text) {
|
|
118
136
|
const data = extractJson(text);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Evaluate issue #{issue} in {owner}/{repo} for the selected category.
|
|
2
2
|
|
|
3
|
-
Choose YES when the issue should be accepted for the project. Choose NO when it should be
|
|
3
|
+
Choose YES when the issue should be accepted for the project. Choose NO when it should not be worked on for normal wontfix reasons. Choose INVALID when the report is unreproducible, contradictory, based on an impossible premise, or otherwise not actionable as an issue. Choose ASK when specific missing information is required before deciding.
|
|
4
4
|
|
|
5
5
|
<context>
|
|
6
6
|
{context}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Evaluate optional triage signals for issue #{issue} in {owner}/{repo}.
|
|
2
|
+
|
|
3
|
+
Configured signals:
|
|
4
|
+
{signalOptions}
|
|
5
|
+
|
|
6
|
+
Return every configured signal that applies to the final triage result and issue context. Do not invent signal IDs.
|
|
7
|
+
|
|
8
|
+
<context>
|
|
9
|
+
{context}
|
|
10
|
+
</context>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-magi",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20260525101932",
|
|
4
4
|
"description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",
|
package/schema.json
CHANGED
|
@@ -279,6 +279,47 @@
|
|
|
279
279
|
"description": { "type": "string" }
|
|
280
280
|
}
|
|
281
281
|
},
|
|
282
|
+
"triageSignal": {
|
|
283
|
+
"type": "object",
|
|
284
|
+
"additionalProperties": false,
|
|
285
|
+
"required": ["id", "description"],
|
|
286
|
+
"properties": {
|
|
287
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
288
|
+
"description": { "type": "string", "minLength": 1 }
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
"triageLabelRuleCondition": {
|
|
292
|
+
"type": "object",
|
|
293
|
+
"additionalProperties": false,
|
|
294
|
+
"minProperties": 1,
|
|
295
|
+
"properties": {
|
|
296
|
+
"disposition": {
|
|
297
|
+
"enum": [
|
|
298
|
+
"accepted",
|
|
299
|
+
"rejected",
|
|
300
|
+
"invalid",
|
|
301
|
+
"duplicate",
|
|
302
|
+
"already_handled",
|
|
303
|
+
"needs_category",
|
|
304
|
+
"needs_acceptance",
|
|
305
|
+
"blocked",
|
|
306
|
+
"failed"
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
"category": { "type": "string" },
|
|
310
|
+
"signals": { "type": "array", "items": { "type": "string" } }
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"triageLabelRule": {
|
|
314
|
+
"type": "object",
|
|
315
|
+
"additionalProperties": false,
|
|
316
|
+
"required": ["when"],
|
|
317
|
+
"properties": {
|
|
318
|
+
"when": { "$ref": "#/$defs/triageLabelRuleCondition" },
|
|
319
|
+
"add": { "type": "array", "items": { "type": "string" } },
|
|
320
|
+
"remove": { "type": "array", "items": { "type": "string" } }
|
|
321
|
+
}
|
|
322
|
+
},
|
|
282
323
|
"triageAutomation": {
|
|
283
324
|
"type": "object",
|
|
284
325
|
"additionalProperties": false,
|
|
@@ -287,10 +328,9 @@
|
|
|
287
328
|
"create": { "type": "boolean", "default": false },
|
|
288
329
|
"review": { "type": "boolean", "default": false },
|
|
289
330
|
"merge": { "type": "boolean", "default": false },
|
|
290
|
-
"
|
|
331
|
+
"label": {
|
|
291
332
|
"type": "array",
|
|
292
|
-
"items": { "
|
|
293
|
-
"default": ["triage"]
|
|
333
|
+
"items": { "$ref": "#/$defs/triageLabelRule" }
|
|
294
334
|
}
|
|
295
335
|
}
|
|
296
336
|
},
|
|
@@ -371,6 +411,10 @@
|
|
|
371
411
|
"type": "array",
|
|
372
412
|
"items": { "$ref": "#/$defs/triageCategory" }
|
|
373
413
|
},
|
|
414
|
+
"signals": {
|
|
415
|
+
"type": "array",
|
|
416
|
+
"items": { "$ref": "#/$defs/triageSignal" }
|
|
417
|
+
},
|
|
374
418
|
"automation": { "$ref": "#/$defs/triageAutomation" },
|
|
375
419
|
"safety": { "$ref": "#/$defs/triageSafety" },
|
|
376
420
|
"concurrency": { "$ref": "#/$defs/triageConcurrency" },
|