opencode-magi 0.0.0-dev-20260520171120 → 0.0.0-dev-20260520175008
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 +29 -10
- package/dist/config/validate.js +42 -17
- package/dist/index.js +133 -24
- package/dist/orchestrator/run-manager.js +5 -3
- package/dist/orchestrator/triage.js +151 -88
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +19 -16
- package/dist/prompts/contracts.js +3 -4
- package/dist/prompts/output.js +2 -12
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/package.json +1 -1
- package/schema.json +16 -15
- package/dist/prompts/templates/triage/bug.md +0 -7
- package/dist/prompts/templates/triage/feature.md +0 -7
- package/dist/prompts/templates/triage/kind.md +0 -7
package/dist/config/resolve.js
CHANGED
|
@@ -4,6 +4,26 @@ const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
4
4
|
const DEFAULT_COMMON_PERMISSION = commonPermission;
|
|
5
5
|
const DEFAULT_REVIEWER_PERMISSION = DEFAULT_COMMON_PERMISSION;
|
|
6
6
|
const DEFAULT_EDITOR_PERMISSION = mergePermissions(DEFAULT_COMMON_PERMISSION, editorPermission);
|
|
7
|
+
const DEFAULT_TRIAGE_CATEGORIES = [
|
|
8
|
+
{
|
|
9
|
+
description: "Something is broken or behaves incorrectly.",
|
|
10
|
+
id: "bug",
|
|
11
|
+
labels: ["bug"],
|
|
12
|
+
types: ["Bug"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
description: "Maintenance, refactoring, chores, or planned work.",
|
|
16
|
+
id: "task",
|
|
17
|
+
labels: ["task"],
|
|
18
|
+
types: ["Task"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
description: "New or improved user-facing capability.",
|
|
22
|
+
id: "feature",
|
|
23
|
+
labels: ["enhancement"],
|
|
24
|
+
types: ["Feature"],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
7
27
|
export function reviewerKey(reviewer, index) {
|
|
8
28
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
9
29
|
}
|
|
@@ -90,6 +110,14 @@ export function resolveAgents(config) {
|
|
|
90
110
|
: undefined,
|
|
91
111
|
};
|
|
92
112
|
}
|
|
113
|
+
function resolveTriageCategories(config) {
|
|
114
|
+
return (config.triage?.categories ?? DEFAULT_TRIAGE_CATEGORIES).map((category) => ({
|
|
115
|
+
description: category.description,
|
|
116
|
+
id: category.id ?? "",
|
|
117
|
+
labels: category.labels ?? [],
|
|
118
|
+
types: category.types ?? [],
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
93
121
|
export function resolveRepository(config) {
|
|
94
122
|
if (!config.github?.owner)
|
|
95
123
|
throw new Error("github.owner is required");
|
|
@@ -157,19 +185,10 @@ export function resolveRepository(config) {
|
|
|
157
185
|
close: config.triage?.automation?.close ?? false,
|
|
158
186
|
pr: config.triage?.automation?.pr ?? false,
|
|
159
187
|
},
|
|
188
|
+
categories: resolveTriageCategories(config),
|
|
160
189
|
concurrency: {
|
|
161
190
|
runs: config.triage?.concurrency?.runs ?? 3,
|
|
162
191
|
},
|
|
163
|
-
kind: {
|
|
164
|
-
bug: {
|
|
165
|
-
label: config.triage?.kind?.bug?.label ?? ["bug"],
|
|
166
|
-
type: config.triage?.kind?.bug?.type ?? ["Bug"],
|
|
167
|
-
},
|
|
168
|
-
feature: {
|
|
169
|
-
label: config.triage?.kind?.feature?.label ?? ["enhancement"],
|
|
170
|
-
type: config.triage?.kind?.feature?.type ?? ["Feature"],
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
192
|
output: config.triage?.output,
|
|
174
193
|
prompts: config.triage?.prompts ?? {},
|
|
175
194
|
safety: {
|
package/dist/config/validate.js
CHANGED
|
@@ -9,6 +9,8 @@ const RESERVED_REVIEWER_KEYS = new Set(["editor", "orchestrator", "system"]);
|
|
|
9
9
|
const PERMISSION_ACTIONS = new Set(["allow", "ask", "deny"]);
|
|
10
10
|
const AJV = new Ajv2020({ allErrors: true, strict: false });
|
|
11
11
|
const validateSchema = AJV.compile(schema);
|
|
12
|
+
const TRIAGE_CATEGORY_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
const RESERVED_TRIAGE_CATEGORY_IDS = new Set(["ASK", "none"]);
|
|
12
14
|
const CONFIG_KEYS = new Set([
|
|
13
15
|
"$schema",
|
|
14
16
|
"agents",
|
|
@@ -76,9 +78,9 @@ const TRIAGE_KEYS = new Set([
|
|
|
76
78
|
"account",
|
|
77
79
|
"agents",
|
|
78
80
|
"automation",
|
|
81
|
+
"categories",
|
|
79
82
|
"concurrency",
|
|
80
83
|
"creator",
|
|
81
|
-
"kind",
|
|
82
84
|
"output",
|
|
83
85
|
"prompts",
|
|
84
86
|
"safety",
|
|
@@ -98,9 +100,8 @@ const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
|
98
100
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
99
101
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
100
102
|
const TRIAGE_AUTOMATION_KEYS = new Set(["clear", "close", "pr"]);
|
|
103
|
+
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
101
104
|
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
102
|
-
const TRIAGE_KIND_KEYS = new Set(["bug", "feature"]);
|
|
103
|
-
const TRIAGE_KIND_RULE_KEYS = new Set(["label", "type"]);
|
|
104
105
|
const TRIAGE_SAFETY_KEYS = new Set([
|
|
105
106
|
"allowAuthors",
|
|
106
107
|
"allowMentionActors",
|
|
@@ -129,14 +130,13 @@ const MERGE_PROMPT_KEYS = new Set([
|
|
|
129
130
|
]);
|
|
130
131
|
const TRIAGE_PROMPT_KEYS = new Set([
|
|
131
132
|
"action",
|
|
132
|
-
"
|
|
133
|
+
"acceptance",
|
|
134
|
+
"category",
|
|
133
135
|
"comment",
|
|
134
136
|
"commentClassification",
|
|
135
137
|
"createPr",
|
|
136
138
|
"duplicate",
|
|
137
139
|
"existingPr",
|
|
138
|
-
"feature",
|
|
139
|
-
"kind",
|
|
140
140
|
"question",
|
|
141
141
|
"reconsider",
|
|
142
142
|
]);
|
|
@@ -536,16 +536,44 @@ function validateStringArray(value, path, errors) {
|
|
|
536
536
|
errors.push(`${path}[${index}] must be a string`);
|
|
537
537
|
});
|
|
538
538
|
}
|
|
539
|
-
function
|
|
540
|
-
if (
|
|
539
|
+
function validateTriageCategories(categories, path, errors) {
|
|
540
|
+
if (categories == null)
|
|
541
541
|
return;
|
|
542
|
-
if (!
|
|
543
|
-
errors.push(`${path} must be an
|
|
542
|
+
if (!Array.isArray(categories)) {
|
|
543
|
+
errors.push(`${path} must be an array`);
|
|
544
544
|
return;
|
|
545
545
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
546
|
+
const ids = new Set();
|
|
547
|
+
categories.forEach((item, index) => {
|
|
548
|
+
const itemPath = `${path}[${index}]`;
|
|
549
|
+
if (!isPlainObject(item)) {
|
|
550
|
+
errors.push(`${itemPath} must be an object`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const category = item;
|
|
554
|
+
validateKnownKeys(category, itemPath, TRIAGE_CATEGORY_KEYS, errors);
|
|
555
|
+
if (!category.id) {
|
|
556
|
+
errors.push(`${itemPath}.id is required`);
|
|
557
|
+
}
|
|
558
|
+
else if (typeof category.id !== "string") {
|
|
559
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
560
|
+
}
|
|
561
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(category.id)) {
|
|
562
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
563
|
+
}
|
|
564
|
+
else if (RESERVED_TRIAGE_CATEGORY_IDS.has(category.id)) {
|
|
565
|
+
errors.push(`${itemPath}.id is reserved: ${category.id}`);
|
|
566
|
+
}
|
|
567
|
+
else if (ids.has(category.id)) {
|
|
568
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
ids.add(category.id);
|
|
572
|
+
}
|
|
573
|
+
validateStringArray(category.labels, `${itemPath}.labels`, errors);
|
|
574
|
+
validateStringArray(category.types, `${itemPath}.types`, errors);
|
|
575
|
+
validateString(category.description, `${itemPath}.description`, errors);
|
|
576
|
+
});
|
|
549
577
|
}
|
|
550
578
|
function validateSafety(config, errors) {
|
|
551
579
|
const safety = config.review?.safety;
|
|
@@ -587,7 +615,6 @@ function validateTriage(config, errors, options) {
|
|
|
587
615
|
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
588
616
|
const automation = triage.automation;
|
|
589
617
|
const concurrency = triage.concurrency;
|
|
590
|
-
const kind = triage.kind;
|
|
591
618
|
const safety = triage.safety;
|
|
592
619
|
if (!triage.account)
|
|
593
620
|
errors.push("triage.account is required");
|
|
@@ -615,9 +642,7 @@ function validateTriage(config, errors, options) {
|
|
|
615
642
|
concurrency.runs < 1)) {
|
|
616
643
|
errors.push("triage.concurrency.runs must be a positive integer");
|
|
617
644
|
}
|
|
618
|
-
|
|
619
|
-
validateStringArrayObject(kind?.bug, "triage.kind.bug", TRIAGE_KIND_RULE_KEYS, errors);
|
|
620
|
-
validateStringArrayObject(kind?.feature, "triage.kind.feature", TRIAGE_KIND_RULE_KEYS, errors);
|
|
645
|
+
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
621
646
|
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
622
647
|
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
623
648
|
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
package/dist/index.js
CHANGED
|
@@ -91,27 +91,133 @@ export function parseIssues(value) {
|
|
|
91
91
|
throw new Error("Specify one or more issue numbers or issue URLs.");
|
|
92
92
|
return issues;
|
|
93
93
|
}
|
|
94
|
-
export function parseRunArguments(value, dryRun = false) {
|
|
94
|
+
export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
95
95
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
96
|
-
const
|
|
96
|
+
const configOverrides = {};
|
|
97
|
+
const prTokens = [];
|
|
98
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
99
|
+
const token = tokens[index];
|
|
97
100
|
if (token === "--dry-run") {
|
|
98
101
|
dryRun = true;
|
|
99
|
-
|
|
102
|
+
continue;
|
|
100
103
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
switch (token) {
|
|
105
|
+
case "--language":
|
|
106
|
+
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
107
|
+
break;
|
|
108
|
+
case "--merge":
|
|
109
|
+
case "--no-merge":
|
|
110
|
+
setConfigOverride(configOverrides, [command, "automation", "merge"], token === "--merge");
|
|
111
|
+
break;
|
|
112
|
+
case "--close":
|
|
113
|
+
case "--no-close":
|
|
114
|
+
setConfigOverride(configOverrides, [command, "automation", "close"], token === "--close");
|
|
115
|
+
break;
|
|
116
|
+
case "--max-cycles":
|
|
117
|
+
if (command !== "merge")
|
|
118
|
+
throw unsupportedFlag(token, command);
|
|
119
|
+
setConfigOverride(configOverrides, ["merge", "maxThreadResolutionCycles"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
|
|
120
|
+
break;
|
|
121
|
+
case "--retry-failed-jobs":
|
|
122
|
+
setConfigOverride(configOverrides, ["review", "checks", "retryFailedJobs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
|
|
123
|
+
break;
|
|
124
|
+
case "--reviewer-concurrency":
|
|
125
|
+
setConfigOverride(configOverrides, ["review", "concurrency", "reviewers"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
|
|
126
|
+
break;
|
|
127
|
+
case "--run-concurrency":
|
|
128
|
+
setConfigOverride(configOverrides, ["review", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
|
|
129
|
+
break;
|
|
130
|
+
case "--wait-checks":
|
|
131
|
+
case "--no-wait-checks":
|
|
132
|
+
setConfigOverride(configOverrides, ["review", "checks", "wait"], token === "--wait-checks");
|
|
133
|
+
break;
|
|
134
|
+
case "--wait-checks-after-edit":
|
|
135
|
+
case "--no-wait-checks-after-edit":
|
|
136
|
+
if (command !== "merge")
|
|
137
|
+
throw unsupportedFlag(token, command);
|
|
138
|
+
setConfigOverride(configOverrides, ["merge", "checks", "wait"], token === "--wait-checks-after-edit");
|
|
139
|
+
break;
|
|
140
|
+
case "--pr":
|
|
141
|
+
case "--no-pr":
|
|
142
|
+
throw unsupportedFlag(token, command);
|
|
143
|
+
default:
|
|
144
|
+
if (token.startsWith("--"))
|
|
145
|
+
throw unsupportedFlag(token, command);
|
|
146
|
+
prTokens.push(token);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")) };
|
|
104
150
|
}
|
|
105
151
|
export function parseIssueRunArguments(value, dryRun = false) {
|
|
106
152
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
107
|
-
const
|
|
153
|
+
const configOverrides = {};
|
|
154
|
+
const issueTokens = [];
|
|
155
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
156
|
+
const token = tokens[index];
|
|
108
157
|
if (token === "--dry-run") {
|
|
109
158
|
dryRun = true;
|
|
110
|
-
|
|
159
|
+
continue;
|
|
111
160
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
161
|
+
switch (token) {
|
|
162
|
+
case "--language":
|
|
163
|
+
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
164
|
+
break;
|
|
165
|
+
case "--close":
|
|
166
|
+
case "--no-close":
|
|
167
|
+
setConfigOverride(configOverrides, ["triage", "automation", "close"], token === "--close");
|
|
168
|
+
break;
|
|
169
|
+
case "--pr":
|
|
170
|
+
case "--no-pr":
|
|
171
|
+
setConfigOverride(configOverrides, ["triage", "automation", "pr"], token === "--pr");
|
|
172
|
+
break;
|
|
173
|
+
case "--run-concurrency":
|
|
174
|
+
setConfigOverride(configOverrides, ["triage", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
|
|
175
|
+
break;
|
|
176
|
+
case "--merge":
|
|
177
|
+
case "--no-merge":
|
|
178
|
+
case "--max-cycles":
|
|
179
|
+
case "--retry-failed-jobs":
|
|
180
|
+
case "--reviewer-concurrency":
|
|
181
|
+
case "--wait-checks":
|
|
182
|
+
case "--no-wait-checks":
|
|
183
|
+
case "--wait-checks-after-edit":
|
|
184
|
+
case "--no-wait-checks-after-edit":
|
|
185
|
+
throw unsupportedFlag(token, "triage");
|
|
186
|
+
default:
|
|
187
|
+
if (token.startsWith("--"))
|
|
188
|
+
throw unsupportedFlag(token, "triage");
|
|
189
|
+
issueTokens.push(token);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { configOverrides, dryRun, issues: parseIssues(issueTokens.join(" ")) };
|
|
193
|
+
}
|
|
194
|
+
function nextFlagValue(tokens, index, flag) {
|
|
195
|
+
const value = tokens[index];
|
|
196
|
+
if (!value || value.startsWith("--"))
|
|
197
|
+
throw new Error(`${flag} requires a value.`);
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
function parseIntegerFlag(value, flag, minimum) {
|
|
201
|
+
const parsed = Number.parseInt(value, 10);
|
|
202
|
+
if (!Number.isInteger(parsed) ||
|
|
203
|
+
String(parsed) !== value ||
|
|
204
|
+
parsed < minimum) {
|
|
205
|
+
throw new Error(`${flag} must be an integer greater than or equal to ${minimum}.`);
|
|
206
|
+
}
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
function setConfigOverride(target, path, value) {
|
|
210
|
+
let current = target;
|
|
211
|
+
for (const key of path.slice(0, -1)) {
|
|
212
|
+
const existing = current[key];
|
|
213
|
+
const next = isPlainObject(existing) ? existing : {};
|
|
214
|
+
current[key] = next;
|
|
215
|
+
current = next;
|
|
216
|
+
}
|
|
217
|
+
current[path[path.length - 1]] = value;
|
|
218
|
+
}
|
|
219
|
+
function unsupportedFlag(flag, command) {
|
|
220
|
+
return new Error(`${flag} is not supported for /magi:${command}.`);
|
|
115
221
|
}
|
|
116
222
|
function parseOptionalPr(value) {
|
|
117
223
|
if (!value?.trim())
|
|
@@ -322,10 +428,11 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
322
428
|
dryRun: tool.schema.boolean().optional(),
|
|
323
429
|
},
|
|
324
430
|
async execute(args, context) {
|
|
325
|
-
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
431
|
+
const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
|
|
326
432
|
const loaded = await loadConfig(directory);
|
|
327
|
-
const
|
|
328
|
-
const
|
|
433
|
+
const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
|
|
434
|
+
const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
|
|
435
|
+
const validation = await validateConfig(config, {
|
|
329
436
|
checkAuth: true,
|
|
330
437
|
directory,
|
|
331
438
|
exec: retryingExec,
|
|
@@ -334,9 +441,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
334
441
|
});
|
|
335
442
|
if (!validation.ok)
|
|
336
443
|
return JSON.stringify(validation, null, 2);
|
|
337
|
-
const repository = resolveRepository(
|
|
444
|
+
const repository = resolveRepository(config);
|
|
338
445
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
339
|
-
config
|
|
446
|
+
config,
|
|
340
447
|
dryRun: parsed.dryRun,
|
|
341
448
|
repository,
|
|
342
449
|
pr,
|
|
@@ -360,8 +467,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
360
467
|
async execute(args, context) {
|
|
361
468
|
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
362
469
|
const loaded = await loadConfig(directory);
|
|
363
|
-
const
|
|
364
|
-
const
|
|
470
|
+
const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
|
|
471
|
+
const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
|
|
472
|
+
const validation = await validateConfig(config, {
|
|
365
473
|
checkAuth: true,
|
|
366
474
|
directory,
|
|
367
475
|
exec: retryingExec,
|
|
@@ -369,9 +477,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
369
477
|
});
|
|
370
478
|
if (!validation.ok)
|
|
371
479
|
return JSON.stringify(validation, null, 2);
|
|
372
|
-
const repository = resolveRepository(
|
|
480
|
+
const repository = resolveRepository(config);
|
|
373
481
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
374
|
-
config
|
|
482
|
+
config,
|
|
375
483
|
dryRun: parsed.dryRun,
|
|
376
484
|
repository,
|
|
377
485
|
pr,
|
|
@@ -392,8 +500,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
392
500
|
async execute(args, context) {
|
|
393
501
|
const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
|
|
394
502
|
const loaded = await loadConfig(directory);
|
|
395
|
-
const
|
|
396
|
-
const
|
|
503
|
+
const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
|
|
504
|
+
const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
|
|
505
|
+
const validation = await validateConfig(config, {
|
|
397
506
|
checkAuth: true,
|
|
398
507
|
directory,
|
|
399
508
|
exec: retryingExec,
|
|
@@ -403,11 +512,11 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
403
512
|
});
|
|
404
513
|
if (!validation.ok)
|
|
405
514
|
return JSON.stringify(validation, null, 2);
|
|
406
|
-
const repository = resolveRepository(
|
|
515
|
+
const repository = resolveRepository(config);
|
|
407
516
|
if (!repository.triage)
|
|
408
517
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
409
518
|
const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
|
|
410
|
-
config
|
|
519
|
+
config,
|
|
411
520
|
dryRun: parsed.dryRun,
|
|
412
521
|
issue,
|
|
413
522
|
parentSessionId: context.sessionID,
|
|
@@ -1273,10 +1273,12 @@ export class MagiRunManager {
|
|
|
1273
1273
|
const completed = this.active.get(input.runId);
|
|
1274
1274
|
if (!completed || completed.status === "cancelled")
|
|
1275
1275
|
return;
|
|
1276
|
-
|
|
1277
|
-
completed.
|
|
1276
|
+
const triageResult = JSON.stringify(result.result);
|
|
1277
|
+
completed.status =
|
|
1278
|
+
result.result.disposition === "failed" ? "failed" : "completed";
|
|
1279
|
+
completed.phase = triageResult;
|
|
1278
1280
|
completed.completedAt = now();
|
|
1279
|
-
completed.verdict =
|
|
1281
|
+
completed.verdict = triageResult;
|
|
1280
1282
|
completed.reportPath = join(completed.outputDir, "report.md");
|
|
1281
1283
|
for (const agent of Object.values(completed.reviewers)) {
|
|
1282
1284
|
if (agent.status === "pending")
|
|
@@ -3,33 +3,27 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { issueRunOutputDir } from "../config/output";
|
|
4
4
|
import { worktreeBaseDir } from "../config/worktree";
|
|
5
5
|
import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
|
|
6
|
-
import {
|
|
7
|
-
import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput,
|
|
6
|
+
import { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
|
+
import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
|
|
8
8
|
import { aggregateStringMajority, majorityThreshold } from "./majority";
|
|
9
9
|
import { runModelText, runModelWithRepair } from "./model";
|
|
10
10
|
const MARKER_PREFIX = "opencode-magi:triage";
|
|
11
|
-
const KIND_VOTES = ["ASK", "BUG", "FEATURE"];
|
|
12
11
|
const BINARY_VOTES = ["ASK", "NO", "YES"];
|
|
13
12
|
const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
|
|
14
13
|
const EXISTING_PR_VOTES = [
|
|
15
14
|
"RELATED_PR_DOES_NOT_HANDLE_ISSUE",
|
|
16
15
|
"RELATED_PR_HANDLES_ISSUE",
|
|
17
16
|
];
|
|
18
|
-
const FINAL_VOTES = [
|
|
19
|
-
"ASK",
|
|
20
|
-
"BUG_ACCEPTED",
|
|
21
|
-
"BUG_REJECTED",
|
|
22
|
-
"DUPLICATE",
|
|
23
|
-
"FEATURE_ACCEPTED",
|
|
24
|
-
"FEATURE_REJECTED",
|
|
25
|
-
];
|
|
26
17
|
const RECONSIDERATION_CLASSES = new Set([
|
|
27
18
|
"CLARIFICATION",
|
|
28
19
|
"NEW_EVIDENCE",
|
|
29
20
|
"OBJECTION",
|
|
30
21
|
]);
|
|
31
22
|
function marker(input) {
|
|
32
|
-
|
|
23
|
+
const askReason = input.decision.askReason
|
|
24
|
+
? ` askReason=${input.decision.askReason}`
|
|
25
|
+
: "";
|
|
26
|
+
return `<!-- ${MARKER_PREFIX} v=2 issue=${input.issue} category=${input.decision.category ?? "none"} disposition=${input.decision.disposition}${askReason} action=${input.action} checkpoint=${input.checkpoint ?? "pending"} pr=${input.pr ?? "none"} processed=${(input.processed ?? []).join(",")} -->`;
|
|
33
27
|
}
|
|
34
28
|
export function parseTriageMarker(body) {
|
|
35
29
|
const match = body.match(/<!--\s*opencode-magi:triage\s+([^>]+?)\s*-->/);
|
|
@@ -45,13 +39,26 @@ export function parseTriageMarker(body) {
|
|
|
45
39
|
: [part.slice(0, index), part.slice(index + 1)];
|
|
46
40
|
}));
|
|
47
41
|
const version = Number(entries.v);
|
|
48
|
-
if (version !== 1)
|
|
42
|
+
if (version !== 1 && version !== 2)
|
|
49
43
|
return undefined;
|
|
50
44
|
return {
|
|
51
45
|
action: entries.action,
|
|
46
|
+
askReason: entries.askReason === "acceptance_unclear" ||
|
|
47
|
+
entries.askReason === "category_unclear"
|
|
48
|
+
? entries.askReason
|
|
49
|
+
: undefined,
|
|
50
|
+
category: entries.category === "none" ? null : entries.category || undefined,
|
|
52
51
|
checkpoint: entries.checkpoint && Number.isFinite(Number(entries.checkpoint))
|
|
53
52
|
? Number(entries.checkpoint)
|
|
54
53
|
: undefined,
|
|
54
|
+
disposition: entries.disposition === "accepted" ||
|
|
55
|
+
entries.disposition === "rejected" ||
|
|
56
|
+
entries.disposition === "ask" ||
|
|
57
|
+
entries.disposition === "duplicate" ||
|
|
58
|
+
entries.disposition === "clear_only" ||
|
|
59
|
+
entries.disposition === "failed"
|
|
60
|
+
? entries.disposition
|
|
61
|
+
: undefined,
|
|
55
62
|
issue: entries.issue ? Number(entries.issue) : undefined,
|
|
56
63
|
pr: entries.pr,
|
|
57
64
|
processed: entries.processed
|
|
@@ -69,17 +76,15 @@ function existingClearLabels(issue, labels) {
|
|
|
69
76
|
const existing = new Set(issue.labels.map((label) => label.toLowerCase()));
|
|
70
77
|
return labels.filter((label) => existing.has(label.toLowerCase()));
|
|
71
78
|
}
|
|
72
|
-
export function
|
|
79
|
+
export function resolveIssueCategory(issue, repository) {
|
|
73
80
|
const triage = repository.triage;
|
|
74
81
|
if (!triage)
|
|
75
82
|
throw new Error("triage configuration is required");
|
|
76
|
-
const
|
|
77
|
-
(issue.type != null &&
|
|
78
|
-
|
|
79
|
-
(issue.type != null && triage.kind.feature.type.includes(issue.type));
|
|
80
|
-
if (bug === feature)
|
|
83
|
+
const matches = triage.categories.filter((category) => labelsContain(issue.labels, category.labels) ||
|
|
84
|
+
(issue.type != null && category.types.includes(issue.type)));
|
|
85
|
+
if (matches.length !== 1)
|
|
81
86
|
return undefined;
|
|
82
|
-
return
|
|
87
|
+
return matches[0].id;
|
|
83
88
|
}
|
|
84
89
|
function issueContext(input) {
|
|
85
90
|
return JSON.stringify({
|
|
@@ -288,22 +293,42 @@ export function eligibleMentionReplies(input) {
|
|
|
288
293
|
});
|
|
289
294
|
}
|
|
290
295
|
function finalResultFromMarker(marker) {
|
|
291
|
-
if (marker.
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
296
|
+
if (marker.disposition) {
|
|
297
|
+
return {
|
|
298
|
+
askReason: marker.askReason,
|
|
299
|
+
category: marker.category ?? null,
|
|
300
|
+
disposition: marker.disposition,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
switch (marker.result) {
|
|
304
|
+
case "BUG_ACCEPTED":
|
|
305
|
+
case "RESOLVED_BY_MERGED_PR":
|
|
306
|
+
return { category: "bug", disposition: "accepted" };
|
|
307
|
+
case "BUG_REJECTED":
|
|
308
|
+
return { category: "bug", disposition: "rejected" };
|
|
309
|
+
case "FEATURE_ACCEPTED":
|
|
310
|
+
return { category: "feature", disposition: "accepted" };
|
|
311
|
+
case "FEATURE_REJECTED":
|
|
312
|
+
return { category: "feature", disposition: "rejected" };
|
|
313
|
+
case "ASK":
|
|
314
|
+
return {
|
|
315
|
+
askReason: "acceptance_unclear",
|
|
316
|
+
category: null,
|
|
317
|
+
disposition: "ask",
|
|
318
|
+
};
|
|
319
|
+
case "CLEAR_ONLY":
|
|
320
|
+
return { category: null, disposition: "clear_only" };
|
|
321
|
+
case "DUPLICATE":
|
|
322
|
+
return { category: null, disposition: "duplicate" };
|
|
323
|
+
default:
|
|
324
|
+
return { category: null, disposition: "failed" };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function decisionText(decision) {
|
|
328
|
+
return JSON.stringify(decision);
|
|
304
329
|
}
|
|
305
330
|
function actionPlan(input) {
|
|
306
|
-
if (input.result === "
|
|
331
|
+
if (input.result.disposition === "clear_only") {
|
|
307
332
|
return {
|
|
308
333
|
action: "CLEAR_ONLY",
|
|
309
334
|
allowedActions: ["CLEAR_ONLY"],
|
|
@@ -313,7 +338,7 @@ function actionPlan(input) {
|
|
|
313
338
|
postComment: false,
|
|
314
339
|
};
|
|
315
340
|
}
|
|
316
|
-
if (input.result === "
|
|
341
|
+
if (input.result.disposition === "ask") {
|
|
317
342
|
return {
|
|
318
343
|
action: "ASK",
|
|
319
344
|
allowedActions: ["ASK"],
|
|
@@ -324,11 +349,9 @@ function actionPlan(input) {
|
|
|
324
349
|
};
|
|
325
350
|
}
|
|
326
351
|
const closeIssue = input.triage.automation.close &&
|
|
327
|
-
(input.result === "
|
|
328
|
-
input.result === "
|
|
329
|
-
|
|
330
|
-
const createPr = input.triage.automation.pr &&
|
|
331
|
-
(input.result === "BUG_ACCEPTED" || input.result === "FEATURE_ACCEPTED");
|
|
352
|
+
(input.result.disposition === "rejected" ||
|
|
353
|
+
input.result.disposition === "duplicate");
|
|
354
|
+
const createPr = input.triage.automation.pr && input.result.disposition === "accepted";
|
|
332
355
|
return {
|
|
333
356
|
action: closeIssue ? "CLOSE" : createPr ? "PR" : "COMMENT",
|
|
334
357
|
allowedActions: [closeIssue ? "CLOSE" : createPr ? "PR" : "COMMENT"],
|
|
@@ -428,18 +451,34 @@ async function runReconsiderationVote(input) {
|
|
|
428
451
|
context: input.context,
|
|
429
452
|
input: input.input,
|
|
430
453
|
outputDir: input.outputDir,
|
|
431
|
-
parse:
|
|
454
|
+
parse: parseTriageBinaryOutput,
|
|
432
455
|
phase: "reconsider",
|
|
433
456
|
prompt: composeTriageReconsiderPrompt,
|
|
434
457
|
schemaName: "triage reconsider",
|
|
435
|
-
votes:
|
|
458
|
+
votes: BINARY_VOTES,
|
|
436
459
|
});
|
|
437
460
|
}
|
|
438
461
|
async function composeResultComment(input) {
|
|
439
462
|
const agents = input.input.repository.agents.triage;
|
|
440
463
|
if (!agents?.length)
|
|
441
464
|
throw new Error("triage.agents is required");
|
|
442
|
-
|
|
465
|
+
if (input.result.disposition === "ask" &&
|
|
466
|
+
input.result.askReason === "category_unclear") {
|
|
467
|
+
const language = input.input.repository.language?.toLowerCase() ?? "";
|
|
468
|
+
const body = language.includes("ja") || language.includes("japanese")
|
|
469
|
+
? `@${input.issue.author} 現在の説明だけでは、何をすべきか判断できません。\n\n期待する動作、実際の動作、必要な理由、関連する例・ログ・スクリーンショットなどを追記してください。`
|
|
470
|
+
: `@${input.issue.author} I can't determine what should be done from the current description.\n\nPlease add more detail, such as the expected behavior, the actual behavior, the reason this is needed, or any relevant examples, logs, or screenshots.`;
|
|
471
|
+
const comment = `${body}\n\n${marker({
|
|
472
|
+
action: input.action,
|
|
473
|
+
checkpoint: "pending",
|
|
474
|
+
decision: input.result,
|
|
475
|
+
issue: input.issue.number,
|
|
476
|
+
processed: input.processed,
|
|
477
|
+
})}`;
|
|
478
|
+
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
479
|
+
return comment;
|
|
480
|
+
}
|
|
481
|
+
const prompt = await (input.result.disposition === "ask"
|
|
443
482
|
? composeTriageQuestionPrompt
|
|
444
483
|
: composeTriageCommentPrompt)({
|
|
445
484
|
author: input.issue.author,
|
|
@@ -461,9 +500,9 @@ async function composeResultComment(input) {
|
|
|
461
500
|
`\n\n${marker({
|
|
462
501
|
action: input.action,
|
|
463
502
|
checkpoint: "pending",
|
|
503
|
+
decision: input.result,
|
|
464
504
|
issue: input.issue.number,
|
|
465
505
|
processed: input.processed,
|
|
466
|
-
result: input.result,
|
|
467
506
|
})}`;
|
|
468
507
|
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
469
508
|
return comment;
|
|
@@ -486,10 +525,10 @@ async function persistProcessedMarker(input) {
|
|
|
486
525
|
const updatedMarker = marker({
|
|
487
526
|
action: input.marker.action ?? input.marker.result ?? "ASK",
|
|
488
527
|
checkpoint: markerCheckpoint(input.marker),
|
|
528
|
+
decision: finalResultFromMarker(input.marker),
|
|
489
529
|
issue: input.issue.number,
|
|
490
530
|
pr: input.pr ?? markerPr(input.marker),
|
|
491
531
|
processed: input.processed,
|
|
492
|
-
result: input.marker.result ?? "ASK",
|
|
493
532
|
});
|
|
494
533
|
const body = previousComment.body.replace(/<!--\s*opencode-magi:triage\s+[^>]+?\s*-->/, updatedMarker);
|
|
495
534
|
if (body === previousComment.body)
|
|
@@ -516,7 +555,7 @@ async function finishWithResult(input) {
|
|
|
516
555
|
const comment = plan.postComment
|
|
517
556
|
? await composeResultComment({
|
|
518
557
|
action: plan.action,
|
|
519
|
-
context: `Result: ${input.result}\nAction: ${plan.action}\n\n${input.context}`,
|
|
558
|
+
context: `Result: ${decisionText(input.result)}\nAction: ${plan.action}\n\n${input.context}`,
|
|
520
559
|
input: input.input,
|
|
521
560
|
issue: input.issue,
|
|
522
561
|
outputDir: input.outputDir,
|
|
@@ -576,7 +615,7 @@ async function finishWithResult(input) {
|
|
|
576
615
|
}
|
|
577
616
|
}
|
|
578
617
|
const report = [
|
|
579
|
-
`Magi triage result for #${input.issue.number}: ${input.result}`,
|
|
618
|
+
`Magi triage result for #${input.issue.number}: ${decisionText(input.result)}`,
|
|
580
619
|
prUrl ? `Created PR: ${prUrl}` : undefined,
|
|
581
620
|
input.input.dryRun
|
|
582
621
|
? "Dry run: no GitHub mutations were performed."
|
|
@@ -690,7 +729,12 @@ export async function runTriage(input) {
|
|
|
690
729
|
if (block) {
|
|
691
730
|
const report = `Magi triage blocked for #${input.issue}: ${block}`;
|
|
692
731
|
await writeFile(join(outputDir, "report.md"), `${report}\n`);
|
|
693
|
-
return {
|
|
732
|
+
return {
|
|
733
|
+
issue: input.issue,
|
|
734
|
+
outputDir,
|
|
735
|
+
report,
|
|
736
|
+
result: { category: null, disposition: "failed" },
|
|
737
|
+
};
|
|
694
738
|
}
|
|
695
739
|
let context = issueContext({ issue, relationship });
|
|
696
740
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
@@ -765,8 +809,18 @@ export async function runTriage(input) {
|
|
|
765
809
|
},
|
|
766
810
|
});
|
|
767
811
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
812
|
+
const vote = await runReconsiderationVote({ context, input, outputDir });
|
|
813
|
+
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
768
814
|
result =
|
|
769
|
-
|
|
815
|
+
vote === "YES"
|
|
816
|
+
? { category: previous.category, disposition: "accepted" }
|
|
817
|
+
: vote === "NO"
|
|
818
|
+
? { category: previous.category, disposition: "rejected" }
|
|
819
|
+
: {
|
|
820
|
+
askReason: "acceptance_unclear",
|
|
821
|
+
category: previous.category,
|
|
822
|
+
disposition: "ask",
|
|
823
|
+
};
|
|
770
824
|
}
|
|
771
825
|
if (!result && relationship.relatedPullRequests.length) {
|
|
772
826
|
const vote = await runPhaseVote({
|
|
@@ -782,6 +836,10 @@ export async function runTriage(input) {
|
|
|
782
836
|
if (vote === "RELATED_PR_HANDLES_ISSUE") {
|
|
783
837
|
const merged = relationship.relatedPullRequests.some((pr) => pr.state === "MERGED");
|
|
784
838
|
if (merged && triage.automation.close) {
|
|
839
|
+
const relatedPrDecision = {
|
|
840
|
+
category: resolveIssueCategory(issue, input.repository) ?? null,
|
|
841
|
+
disposition: "accepted",
|
|
842
|
+
};
|
|
785
843
|
const plan = {
|
|
786
844
|
action: "CLOSE",
|
|
787
845
|
allowedActions: ["CLOSE"],
|
|
@@ -795,16 +853,16 @@ export async function runTriage(input) {
|
|
|
795
853
|
input,
|
|
796
854
|
outputDir,
|
|
797
855
|
plan,
|
|
798
|
-
result:
|
|
856
|
+
result: relatedPrDecision,
|
|
799
857
|
});
|
|
800
858
|
const body = await composeResultComment({
|
|
801
859
|
action: "CLOSE",
|
|
802
|
-
context: `Result:
|
|
860
|
+
context: `Result: ${decisionText(relatedPrDecision)}\nAction: CLOSE\n\n${context}`,
|
|
803
861
|
input,
|
|
804
862
|
issue,
|
|
805
863
|
outputDir,
|
|
806
864
|
processed,
|
|
807
|
-
result:
|
|
865
|
+
result: relatedPrDecision,
|
|
808
866
|
});
|
|
809
867
|
if (!input.dryRun) {
|
|
810
868
|
await postMarkedIssueComment({
|
|
@@ -834,7 +892,7 @@ export async function runTriage(input) {
|
|
|
834
892
|
issue: issue.number,
|
|
835
893
|
outputDir,
|
|
836
894
|
report,
|
|
837
|
-
result:
|
|
895
|
+
result: relatedPrDecision,
|
|
838
896
|
};
|
|
839
897
|
}
|
|
840
898
|
return finishWithResult({
|
|
@@ -844,7 +902,7 @@ export async function runTriage(input) {
|
|
|
844
902
|
outputDir,
|
|
845
903
|
processed,
|
|
846
904
|
relationship,
|
|
847
|
-
result: "
|
|
905
|
+
result: { category: null, disposition: "clear_only" },
|
|
848
906
|
});
|
|
849
907
|
}
|
|
850
908
|
}
|
|
@@ -857,59 +915,60 @@ export async function runTriage(input) {
|
|
|
857
915
|
});
|
|
858
916
|
if (duplicate) {
|
|
859
917
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
860
|
-
result = "
|
|
918
|
+
result = { category: null, disposition: "duplicate" };
|
|
861
919
|
}
|
|
862
920
|
}
|
|
863
921
|
if (!result) {
|
|
864
|
-
const
|
|
865
|
-
await writeJson(join(outputDir, "
|
|
866
|
-
|
|
867
|
-
source:
|
|
922
|
+
const resolvedCategory = resolveIssueCategory(issue, input.repository);
|
|
923
|
+
await writeJson(join(outputDir, "category-resolution.json"), {
|
|
924
|
+
category: resolvedCategory,
|
|
925
|
+
source: resolvedCategory ? "config" : "vote",
|
|
868
926
|
});
|
|
869
|
-
const
|
|
927
|
+
const category = resolvedCategory ??
|
|
870
928
|
(await runPhaseVote({
|
|
871
929
|
context,
|
|
872
930
|
input,
|
|
873
931
|
outputDir,
|
|
874
|
-
parse:
|
|
875
|
-
phase: "
|
|
876
|
-
prompt:
|
|
877
|
-
schemaName: "triage
|
|
878
|
-
votes:
|
|
932
|
+
parse: (text) => parseTriageCategoryOutput(text, triage.categories.map((item) => item.id)),
|
|
933
|
+
phase: "category",
|
|
934
|
+
prompt: composeTriageCategoryPrompt,
|
|
935
|
+
schemaName: "triage category",
|
|
936
|
+
votes: ["ASK", ...triage.categories.map((item) => item.id)],
|
|
879
937
|
})) ??
|
|
880
938
|
"ASK";
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
parse: parseTriageBinaryOutput,
|
|
888
|
-
phase: "bug",
|
|
889
|
-
prompt: composeTriageBugPrompt,
|
|
890
|
-
schemaName: "triage bug",
|
|
891
|
-
votes: BINARY_VOTES,
|
|
892
|
-
});
|
|
893
|
-
result =
|
|
894
|
-
vote === "YES" ? "BUG_ACCEPTED" : vote === "NO" ? "BUG_REJECTED" : "ASK";
|
|
939
|
+
if (category === "ASK") {
|
|
940
|
+
result = {
|
|
941
|
+
askReason: "category_unclear",
|
|
942
|
+
category: null,
|
|
943
|
+
disposition: "ask",
|
|
944
|
+
};
|
|
895
945
|
}
|
|
896
|
-
|
|
946
|
+
else {
|
|
947
|
+
const categoryConfig = triage.categories.find((item) => item.id === category);
|
|
948
|
+
const voteContext = JSON.stringify({
|
|
949
|
+
category: categoryConfig,
|
|
950
|
+
triageContext: context,
|
|
951
|
+
}, null, 2);
|
|
897
952
|
const vote = await runPhaseVote({
|
|
898
|
-
context,
|
|
953
|
+
context: voteContext,
|
|
899
954
|
input,
|
|
900
955
|
outputDir,
|
|
901
956
|
parse: parseTriageBinaryOutput,
|
|
902
|
-
phase: "
|
|
903
|
-
prompt:
|
|
904
|
-
schemaName: "triage
|
|
957
|
+
phase: "acceptance",
|
|
958
|
+
prompt: composeTriageAcceptancePrompt,
|
|
959
|
+
schemaName: "triage acceptance",
|
|
905
960
|
votes: BINARY_VOTES,
|
|
906
961
|
});
|
|
907
962
|
result =
|
|
908
963
|
vote === "YES"
|
|
909
|
-
? "
|
|
964
|
+
? { category, disposition: "accepted" }
|
|
910
965
|
: vote === "NO"
|
|
911
|
-
? "
|
|
912
|
-
:
|
|
966
|
+
? { category, disposition: "rejected" }
|
|
967
|
+
: {
|
|
968
|
+
askReason: "acceptance_unclear",
|
|
969
|
+
category,
|
|
970
|
+
disposition: "ask",
|
|
971
|
+
};
|
|
913
972
|
}
|
|
914
973
|
}
|
|
915
974
|
return finishWithResult({
|
|
@@ -919,6 +978,10 @@ export async function runTriage(input) {
|
|
|
919
978
|
outputDir,
|
|
920
979
|
processed,
|
|
921
980
|
relationship,
|
|
922
|
-
result: result ??
|
|
981
|
+
result: result ?? {
|
|
982
|
+
askReason: "acceptance_unclear",
|
|
983
|
+
category: null,
|
|
984
|
+
disposition: "ask",
|
|
985
|
+
},
|
|
923
986
|
});
|
|
924
987
|
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bash": {
|
|
3
|
+
"bun *": "allow",
|
|
4
|
+
"bunx *": "allow",
|
|
5
|
+
"corepack *": "allow",
|
|
3
6
|
"git add*": "allow",
|
|
4
|
-
"git commit*": "allow"
|
|
7
|
+
"git commit*": "allow",
|
|
8
|
+
"npm *": "allow",
|
|
9
|
+
"npx *": "allow",
|
|
10
|
+
"pnpm *": "allow",
|
|
11
|
+
"yarn *": "allow"
|
|
5
12
|
},
|
|
6
13
|
"edit": "allow"
|
|
7
14
|
}
|
package/dist/prompts/compose.js
CHANGED
|
@@ -67,9 +67,16 @@ function editValues(input) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
function triageValues(input) {
|
|
70
|
+
const categories = input.repository.triage?.categories ?? [];
|
|
71
|
+
const categoryOptions = categories
|
|
72
|
+
.map((category) => category.description
|
|
73
|
+
? `- ${category.id}: ${category.description}`
|
|
74
|
+
: `- ${category.id}`)
|
|
75
|
+
.join("\n");
|
|
70
76
|
return {
|
|
71
77
|
...repositoryValues(input.repository),
|
|
72
78
|
author: input.author ?? "",
|
|
79
|
+
categoryOptions,
|
|
73
80
|
context: input.context,
|
|
74
81
|
issue: String(input.issue),
|
|
75
82
|
worktreePath: input.worktreePath ?? "",
|
|
@@ -401,27 +408,23 @@ export async function composeTriageDuplicatePrompt(input) {
|
|
|
401
408
|
outputContract: triageDuplicateOutputContract,
|
|
402
409
|
});
|
|
403
410
|
}
|
|
404
|
-
export async function
|
|
411
|
+
export async function composeTriageCategoryPrompt(input) {
|
|
412
|
+
const categories = input.repository.triage?.categories ?? [];
|
|
413
|
+
const votes = ["ASK", ...categories.map((category) => category.id)]
|
|
414
|
+
.map((vote) => JSON.stringify(vote))
|
|
415
|
+
.join(" | ");
|
|
405
416
|
return composeTriageVotePrompt({
|
|
406
417
|
...input,
|
|
407
|
-
builtin: "
|
|
408
|
-
customPath: input.repository.triage?.prompts.
|
|
409
|
-
outputContract: triageVoteOutputContract(
|
|
418
|
+
builtin: "category",
|
|
419
|
+
customPath: input.repository.triage?.prompts.category,
|
|
420
|
+
outputContract: triageVoteOutputContract(votes),
|
|
410
421
|
});
|
|
411
422
|
}
|
|
412
|
-
export async function
|
|
423
|
+
export async function composeTriageAcceptancePrompt(input) {
|
|
413
424
|
return composeTriageVotePrompt({
|
|
414
425
|
...input,
|
|
415
|
-
builtin: "
|
|
416
|
-
customPath: input.repository.triage?.prompts.
|
|
417
|
-
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
export async function composeTriageFeaturePrompt(input) {
|
|
421
|
-
return composeTriageVotePrompt({
|
|
422
|
-
...input,
|
|
423
|
-
builtin: "feature",
|
|
424
|
-
customPath: input.repository.triage?.prompts.feature,
|
|
426
|
+
builtin: "acceptance",
|
|
427
|
+
customPath: input.repository.triage?.prompts.acceptance,
|
|
425
428
|
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
426
429
|
});
|
|
427
430
|
}
|
|
@@ -438,6 +441,6 @@ export async function composeTriageReconsiderPrompt(input) {
|
|
|
438
441
|
...input,
|
|
439
442
|
builtin: "reconsider",
|
|
440
443
|
customPath: input.repository.triage?.prompts.reconsider,
|
|
441
|
-
outputContract: triageVoteOutputContract('"
|
|
444
|
+
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
442
445
|
});
|
|
443
446
|
}
|
|
@@ -253,13 +253,12 @@ const outputContractsBySchemaName = {
|
|
|
253
253
|
"rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
|
|
254
254
|
review: reviewOutputContract,
|
|
255
255
|
"triage action": triageActionOutputContract,
|
|
256
|
-
"triage
|
|
256
|
+
"triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
257
|
+
"triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
|
|
257
258
|
"triage comment classification": triageCommentClassificationOutputContract,
|
|
258
259
|
"triage duplicate": triageDuplicateOutputContract,
|
|
259
260
|
"triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
|
|
260
|
-
"triage
|
|
261
|
-
"triage kind": triageVoteOutputContract('"BUG" | "FEATURE" | "ASK"'),
|
|
262
|
-
"triage reconsider": triageVoteOutputContract('"ASK" | "BUG_ACCEPTED" | "BUG_REJECTED" | "DUPLICATE" | "FEATURE_ACCEPTED" | "FEATURE_REJECTED"'),
|
|
261
|
+
"triage reconsider": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
263
262
|
};
|
|
264
263
|
export function repairPrompt(schemaName) {
|
|
265
264
|
const outputContract = outputContractsBySchemaName[schemaName];
|
package/dist/prompts/output.js
CHANGED
|
@@ -104,18 +104,8 @@ export function parseTriageExistingPrOutput(text) {
|
|
|
104
104
|
"RELATED_PR_HANDLES_ISSUE",
|
|
105
105
|
]);
|
|
106
106
|
}
|
|
107
|
-
export function
|
|
108
|
-
return parseTriageVote(text, ["ASK",
|
|
109
|
-
}
|
|
110
|
-
export function parseTriageFinalOutput(text) {
|
|
111
|
-
return parseTriageVote(text, [
|
|
112
|
-
"ASK",
|
|
113
|
-
"BUG_ACCEPTED",
|
|
114
|
-
"BUG_REJECTED",
|
|
115
|
-
"DUPLICATE",
|
|
116
|
-
"FEATURE_ACCEPTED",
|
|
117
|
-
"FEATURE_REJECTED",
|
|
118
|
-
]);
|
|
107
|
+
export function parseTriageCategoryOutput(text, categories) {
|
|
108
|
+
return parseTriageVote(text, ["ASK", ...categories]);
|
|
119
109
|
}
|
|
120
110
|
export function parseTriageBinaryOutput(text) {
|
|
121
111
|
return parseTriageVote(text, ["ASK", "NO", "YES"]);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Evaluate issue #{issue} in {owner}/{repo} for the selected category.
|
|
2
|
+
|
|
3
|
+
Choose YES when the issue should be accepted for the project. Choose NO when it should be rejected, is not actionable, or is not appropriate for this project. Choose ASK when specific missing information is required before deciding.
|
|
4
|
+
|
|
5
|
+
<context>
|
|
6
|
+
{context}
|
|
7
|
+
</context>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Classify issue #{issue} in {owner}/{repo}.
|
|
2
|
+
|
|
3
|
+
Choose ASK when more information is required to classify what should be done. Otherwise choose exactly one configured category ID.
|
|
4
|
+
|
|
5
|
+
Configured categories:
|
|
6
|
+
{categoryOptions}
|
|
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-20260520175008",
|
|
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
|
@@ -194,9 +194,8 @@
|
|
|
194
194
|
"properties": {
|
|
195
195
|
"existingPr": { "type": "string" },
|
|
196
196
|
"duplicate": { "type": "string" },
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"feature": { "type": "string" },
|
|
197
|
+
"category": { "type": "string" },
|
|
198
|
+
"acceptance": { "type": "string" },
|
|
200
199
|
"action": { "type": "string" },
|
|
201
200
|
"question": { "type": "string" },
|
|
202
201
|
"comment": { "type": "string" },
|
|
@@ -205,20 +204,19 @@
|
|
|
205
204
|
"createPr": { "type": "string" }
|
|
206
205
|
}
|
|
207
206
|
},
|
|
208
|
-
"
|
|
207
|
+
"triageCategory": {
|
|
209
208
|
"type": "object",
|
|
210
209
|
"additionalProperties": false,
|
|
210
|
+
"required": ["id"],
|
|
211
211
|
"properties": {
|
|
212
|
-
"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"bug": { "$ref": "#/$defs/triageKindRule" },
|
|
221
|
-
"feature": { "$ref": "#/$defs/triageKindRule" }
|
|
212
|
+
"id": {
|
|
213
|
+
"type": "string",
|
|
214
|
+
"pattern": "^[A-Za-z0-9_-]+$",
|
|
215
|
+
"not": { "enum": ["ASK", "none"] }
|
|
216
|
+
},
|
|
217
|
+
"labels": { "type": "array", "items": { "type": "string" } },
|
|
218
|
+
"types": { "type": "array", "items": { "type": "string" } },
|
|
219
|
+
"description": { "type": "string" }
|
|
222
220
|
}
|
|
223
221
|
},
|
|
224
222
|
"triageAutomation": {
|
|
@@ -308,7 +306,10 @@
|
|
|
308
306
|
"items": { "$ref": "#/$defs/triageAgent" }
|
|
309
307
|
},
|
|
310
308
|
"creator": { "$ref": "#/$defs/triageCreator" },
|
|
311
|
-
"
|
|
309
|
+
"categories": {
|
|
310
|
+
"type": "array",
|
|
311
|
+
"items": { "$ref": "#/$defs/triageCategory" }
|
|
312
|
+
},
|
|
312
313
|
"automation": { "$ref": "#/$defs/triageAutomation" },
|
|
313
314
|
"safety": { "$ref": "#/$defs/triageSafety" },
|
|
314
315
|
"concurrency": { "$ref": "#/$defs/triageConcurrency" },
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
Evaluate bug issue #{issue} in {owner}/{repo}.
|
|
2
|
-
|
|
3
|
-
Choose YES when the bug is reproduced or otherwise valid based on strong evidence. Choose NO when it is not reproduced, invalid, a misunderstanding, or not actionable as a bug. Choose ASK when specific missing information is required.
|
|
4
|
-
|
|
5
|
-
<context>
|
|
6
|
-
{context}
|
|
7
|
-
</context>
|