opencode-magi 0.0.0-dev-20260520171120 → 0.0.0-dev-20260520173258

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.
@@ -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: {
@@ -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
- "bug",
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 validateStringArrayObject(value, path, keys, errors) {
540
- if (value == null)
539
+ function validateTriageCategories(categories, path, errors) {
540
+ if (categories == null)
541
541
  return;
542
- if (!isPlainObject(value)) {
543
- errors.push(`${path} must be an object`);
542
+ if (!Array.isArray(categories)) {
543
+ errors.push(`${path} must be an array`);
544
544
  return;
545
545
  }
546
- validateKnownKeys(value, path, keys, errors);
547
- for (const key of keys)
548
- validateStringArray(value[key], `${path}.${key}`, errors);
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
- validateKnownKeys(kind, "triage.kind", TRIAGE_KIND_KEYS, errors);
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 prTokens = tokens.filter((token) => {
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
- return false;
102
+ continue;
100
103
  }
101
- return true;
102
- });
103
- return { dryRun, prs: parsePrs(prTokens.join(" ")) };
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 issueTokens = tokens.filter((token) => {
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
- return false;
159
+ continue;
111
160
  }
112
- return true;
113
- });
114
- return { dryRun, issues: parseIssues(issueTokens.join(" ")) };
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 retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
328
- const validation = await validateConfig(loaded.config, {
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(loaded.config);
444
+ const repository = resolveRepository(config);
338
445
  const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
339
- config: loaded.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 retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
364
- const validation = await validateConfig(loaded.config, {
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(loaded.config);
480
+ const repository = resolveRepository(config);
373
481
  const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
374
- config: loaded.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 retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
396
- const validation = await validateConfig(loaded.config, {
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(loaded.config);
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: loaded.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
- completed.status = result.result === "FAILED" ? "failed" : "completed";
1277
- completed.phase = result.result;
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 = result.result;
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 { composeTriageBugPrompt, composeTriageActionPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageFeaturePrompt, composeTriageKindPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
7
- import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, parseTriageFinalOutput, parseTriageKindOutput, } from "../prompts/output";
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
- return `<!-- ${MARKER_PREFIX} v=1 issue=${input.issue} result=${input.result} action=${input.action} checkpoint=${input.checkpoint ?? "pending"} pr=${input.pr ?? "none"} processed=${(input.processed ?? []).join(",")} -->`;
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 resolveIssueKind(issue, repository) {
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 bug = labelsContain(issue.labels, triage.kind.bug.label) ||
77
- (issue.type != null && triage.kind.bug.type.includes(issue.type));
78
- const feature = labelsContain(issue.labels, triage.kind.feature.label) ||
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 bug ? "BUG" : "FEATURE";
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.result === "RESOLVED_BY_MERGED_PR")
292
- return "BUG_ACCEPTED";
293
- return isFinalResult(marker.result) ? marker.result : "FAILED";
294
- }
295
- function isFinalResult(value) {
296
- return (value === "ASK" ||
297
- value === "BUG_ACCEPTED" ||
298
- value === "BUG_REJECTED" ||
299
- value === "CLEAR_ONLY" ||
300
- value === "DUPLICATE" ||
301
- value === "FEATURE_ACCEPTED" ||
302
- value === "FEATURE_REJECTED" ||
303
- value === "FAILED");
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 === "CLEAR_ONLY") {
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 === "ASK") {
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 === "BUG_REJECTED" ||
328
- input.result === "DUPLICATE" ||
329
- input.result === "FEATURE_REJECTED");
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: parseTriageFinalOutput,
454
+ parse: parseTriageBinaryOutput,
432
455
  phase: "reconsider",
433
456
  prompt: composeTriageReconsiderPrompt,
434
457
  schemaName: "triage reconsider",
435
- votes: FINAL_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
- const prompt = await (input.result === "ASK"
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 { issue: input.issue, outputDir, report, result: "FAILED" };
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
- (await runReconsiderationVote({ context, input, outputDir })) ?? "ASK";
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: "BUG_ACCEPTED",
856
+ result: relatedPrDecision,
799
857
  });
800
858
  const body = await composeResultComment({
801
859
  action: "CLOSE",
802
- context: `Result: BUG_ACCEPTED\nAction: CLOSE\n\n${context}`,
860
+ context: `Result: ${decisionText(relatedPrDecision)}\nAction: CLOSE\n\n${context}`,
803
861
  input,
804
862
  issue,
805
863
  outputDir,
806
864
  processed,
807
- result: "BUG_ACCEPTED",
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: "BUG_ACCEPTED",
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: "CLEAR_ONLY",
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 = "DUPLICATE";
918
+ result = { category: null, disposition: "duplicate" };
861
919
  }
862
920
  }
863
921
  if (!result) {
864
- const resolvedKind = resolveIssueKind(issue, input.repository);
865
- await writeJson(join(outputDir, "kind-resolution.json"), {
866
- kind: resolvedKind,
867
- source: resolvedKind ? "config" : "vote",
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 kind = resolvedKind ??
927
+ const category = resolvedCategory ??
870
928
  (await runPhaseVote({
871
929
  context,
872
930
  input,
873
931
  outputDir,
874
- parse: parseTriageKindOutput,
875
- phase: "kind",
876
- prompt: composeTriageKindPrompt,
877
- schemaName: "triage kind",
878
- votes: KIND_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
- result = "ASK";
882
- if (kind === "BUG") {
883
- const vote = await runPhaseVote({
884
- context,
885
- input,
886
- outputDir,
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
- if (kind === "FEATURE") {
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: "feature",
903
- prompt: composeTriageFeaturePrompt,
904
- schemaName: "triage feature",
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
- ? "FEATURE_ACCEPTED"
964
+ ? { category, disposition: "accepted" }
910
965
  : vote === "NO"
911
- ? "FEATURE_REJECTED"
912
- : "ASK";
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 ?? "ASK",
981
+ result: result ?? {
982
+ askReason: "acceptance_unclear",
983
+ category: null,
984
+ disposition: "ask",
985
+ },
923
986
  });
924
987
  }
@@ -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 composeTriageKindPrompt(input) {
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: "kind",
408
- customPath: input.repository.triage?.prompts.kind,
409
- outputContract: triageVoteOutputContract('"BUG" | "FEATURE" | "ASK"'),
418
+ builtin: "category",
419
+ customPath: input.repository.triage?.prompts.category,
420
+ outputContract: triageVoteOutputContract(votes),
410
421
  });
411
422
  }
412
- export async function composeTriageBugPrompt(input) {
423
+ export async function composeTriageAcceptancePrompt(input) {
413
424
  return composeTriageVotePrompt({
414
425
  ...input,
415
- builtin: "bug",
416
- customPath: input.repository.triage?.prompts.bug,
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('"ASK" | "BUG_ACCEPTED" | "BUG_REJECTED" | "DUPLICATE" | "FEATURE_ACCEPTED" | "FEATURE_REJECTED"'),
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 bug": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
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 feature": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
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];
@@ -104,18 +104,8 @@ export function parseTriageExistingPrOutput(text) {
104
104
  "RELATED_PR_HANDLES_ISSUE",
105
105
  ]);
106
106
  }
107
- export function parseTriageKindOutput(text) {
108
- return parseTriageVote(text, ["ASK", "BUG", "FEATURE"]);
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-20260520171120",
3
+ "version": "0.0.0-dev-20260520173258",
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
- "kind": { "type": "string" },
198
- "bug": { "type": "string" },
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
- "triageKindRule": {
207
+ "triageCategory": {
209
208
  "type": "object",
210
209
  "additionalProperties": false,
210
+ "required": ["id"],
211
211
  "properties": {
212
- "label": { "type": "array", "items": { "type": "string" } },
213
- "type": { "type": "array", "items": { "type": "string" } }
214
- }
215
- },
216
- "triageKind": {
217
- "type": "object",
218
- "additionalProperties": false,
219
- "properties": {
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
- "kind": { "$ref": "#/$defs/triageKind" },
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>
@@ -1,7 +0,0 @@
1
- Evaluate feature issue #{issue} in {owner}/{repo}.
2
-
3
- Choose YES when the feature should be implemented. Choose NO when it should not be implemented for this project. Choose ASK when specific missing information is required.
4
-
5
- <context>
6
- {context}
7
- </context>
@@ -1,7 +0,0 @@
1
- Classify issue #{issue} in {owner}/{repo}.
2
-
3
- Choose BUG when it reports broken existing behavior. Choose FEATURE when it requests new behavior or enhancement. Choose ASK when more information is required to classify it.
4
-
5
- <context>
6
- {context}
7
- </context>