opencode-magi 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +19 -0
  2. package/dist/commands.js +4 -0
  3. package/dist/config/output.js +11 -2
  4. package/dist/config/resolve.js +81 -1
  5. package/dist/config/validate.js +341 -3
  6. package/dist/config/worktree.js +8 -2
  7. package/dist/github/commands.js +381 -19
  8. package/dist/index.js +252 -26
  9. package/dist/orchestrator/ci.js +1 -1
  10. package/dist/orchestrator/findings.js +4 -3
  11. package/dist/orchestrator/inline-comments.js +79 -0
  12. package/dist/orchestrator/majority.js +14 -0
  13. package/dist/orchestrator/merge.js +108 -34
  14. package/dist/orchestrator/report.js +25 -7
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +122 -14
  17. package/dist/orchestrator/run-manager.js +408 -17
  18. package/dist/orchestrator/triage.js +1119 -0
  19. package/dist/permissions/editor.json +8 -1
  20. package/dist/prompts/compose.js +163 -1
  21. package/dist/prompts/contracts.js +131 -18
  22. package/dist/prompts/output.js +173 -22
  23. package/dist/prompts/templates/merge/edit.md +12 -5
  24. package/dist/prompts/templates/review/review.md +6 -0
  25. package/dist/prompts/templates/triage/acceptance.md +7 -0
  26. package/dist/prompts/templates/triage/action.md +5 -0
  27. package/dist/prompts/templates/triage/category.md +10 -0
  28. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  29. package/dist/prompts/templates/triage/comment.md +5 -0
  30. package/dist/prompts/templates/triage/create.md +7 -0
  31. package/dist/prompts/templates/triage/duplicate.md +7 -0
  32. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  33. package/dist/prompts/templates/triage/question.md +5 -0
  34. package/dist/prompts/templates/triage/reconsider.md +5 -0
  35. package/package.json +5 -2
  36. package/schema.json +162 -5
@@ -71,6 +71,110 @@ function requireNumber(value, path) {
71
71
  throw new Error(`${path} must be an integer`);
72
72
  return value;
73
73
  }
74
+ function optionalLine(value, path) {
75
+ return value == null ? undefined : requireNumber(value, path);
76
+ }
77
+ function optionalStartLine(input) {
78
+ if (input.value == null)
79
+ return undefined;
80
+ if (input.line == null)
81
+ throw new Error(`${input.path} requires line`);
82
+ return requireNumber(input.value, input.path);
83
+ }
84
+ function parseRequirementFindings(value) {
85
+ return (value == null ? [] : requireArray(value, "requirementFindings")).map((finding, index) => {
86
+ const item = finding;
87
+ return {
88
+ evidence: requireString(item.evidence, `requirementFindings[${index}].evidence`),
89
+ fix: requireString(item.fix, `requirementFindings[${index}].fix`),
90
+ issueNumber: requireNumber(item.issueNumber, `requirementFindings[${index}].issueNumber`),
91
+ requirement: requireString(item.requirement, `requirementFindings[${index}].requirement`),
92
+ };
93
+ });
94
+ }
95
+ function requireOneOf(value, path, values) {
96
+ const text = requireString(value, path);
97
+ if (!values.includes(text)) {
98
+ throw new Error(`${path} must be ${values.join(", ")}`);
99
+ }
100
+ return text;
101
+ }
102
+ function parseTriageVote(text, votes) {
103
+ const data = extractJson(text);
104
+ if (!data || typeof data !== "object")
105
+ throw new Error("triage vote output must be an object");
106
+ return {
107
+ reason: requireString(data.reason, "reason"),
108
+ vote: requireOneOf(data.vote, "vote", votes),
109
+ };
110
+ }
111
+ export function parseTriageExistingPrOutput(text) {
112
+ return parseTriageVote(text, [
113
+ "RELATED_PR_DOES_NOT_HANDLE_ISSUE",
114
+ "RELATED_PR_HANDLES_ISSUE",
115
+ ]);
116
+ }
117
+ export function parseTriageCategoryOutput(text, categories) {
118
+ return parseTriageVote(text, ["ASK", ...categories]);
119
+ }
120
+ export function parseTriageBinaryOutput(text) {
121
+ return parseTriageVote(text, ["ASK", "NO", "YES"]);
122
+ }
123
+ export function parseTriageDuplicateOutput(text) {
124
+ const data = extractJson(text);
125
+ if (!data || typeof data !== "object")
126
+ throw new Error("triage duplicate output must be an object");
127
+ const vote = requireOneOf(data.vote, "vote", [
128
+ "DUPLICATE",
129
+ "NOT_DUPLICATE",
130
+ ]);
131
+ const duplicateOf = data.duplicateOf == null
132
+ ? undefined
133
+ : requireNumber(data.duplicateOf, "duplicateOf");
134
+ if (vote === "DUPLICATE" && duplicateOf == null)
135
+ throw new Error("DUPLICATE requires duplicateOf");
136
+ return {
137
+ duplicateOf,
138
+ reason: requireString(data.reason, "reason"),
139
+ vote,
140
+ };
141
+ }
142
+ export function parseTriageCommentClassificationOutput(text) {
143
+ const data = extractJson(text);
144
+ if (!data || typeof data !== "object")
145
+ throw new Error("triage comment classification output must be an object");
146
+ return {
147
+ comments: requireArray(data.comments, "comments").map((item, index) => {
148
+ const value = item;
149
+ return {
150
+ classification: requireOneOf(value.classification, `comments[${index}].classification`, [
151
+ "ACKNOWLEDGEMENT",
152
+ "CLARIFICATION",
153
+ "NEW_EVIDENCE",
154
+ "OBJECTION",
155
+ "UNRELATED",
156
+ ]),
157
+ commentId: requireNumber(value.commentId, `comments[${index}].commentId`),
158
+ reason: requireString(value.reason, `comments[${index}].reason`),
159
+ };
160
+ }),
161
+ };
162
+ }
163
+ export function parseTriageActionOutput(text) {
164
+ const data = extractJson(text);
165
+ if (!data || typeof data !== "object")
166
+ throw new Error("triage action output must be an object");
167
+ return {
168
+ action: requireOneOf(data.action, "action", [
169
+ "ASK",
170
+ "CLEAR_ONLY",
171
+ "CLOSE",
172
+ "COMMENT",
173
+ "PR",
174
+ ]),
175
+ reason: requireString(data.reason, "reason"),
176
+ };
177
+ }
74
178
  export function parseReviewOutput(text) {
75
179
  const data = extractJson(text);
76
180
  if (!data || typeof data !== "object")
@@ -79,25 +183,33 @@ export function parseReviewOutput(text) {
79
183
  throw new Error("verdict must be MERGE, CHANGES_REQUESTED, or CLOSE");
80
184
  const findings = requireArray(data.findings, "findings").map((finding, index) => {
81
185
  const item = finding;
186
+ const line = optionalLine(item.line, `findings[${index}].line`);
82
187
  return {
83
188
  fix: requireString(item.fix, `findings[${index}].fix`),
84
189
  issue: requireString(item.issue, `findings[${index}].issue`),
85
- line: requireNumber(item.line, `findings[${index}].line`),
190
+ line,
86
191
  path: requireString(item.path, `findings[${index}].path`),
87
192
  perspective: item.perspective == null
88
193
  ? undefined
89
194
  : requireString(item.perspective, `findings[${index}].perspective`),
90
- startLine: item.startLine == null
91
- ? undefined
92
- : requireNumber(item.startLine, `findings[${index}].startLine`),
195
+ startLine: optionalStartLine({
196
+ line,
197
+ path: `findings[${index}].startLine`,
198
+ value: item.startLine,
199
+ }),
93
200
  };
94
201
  });
95
- if (data.verdict === "MERGE" && findings.length)
96
- throw new Error("MERGE requires no findings");
97
- if (data.verdict === "CHANGES_REQUESTED" && !findings.length)
98
- throw new Error("CHANGES_REQUESTED requires findings");
99
- if (data.verdict === "CLOSE" && findings.length)
100
- throw new Error("CLOSE requires no findings");
202
+ const requirementFindings = parseRequirementFindings(data.requirementFindings);
203
+ if (data.verdict === "MERGE" &&
204
+ (findings.length || requirementFindings.length))
205
+ throw new Error("MERGE requires no findings or requirementFindings");
206
+ if (data.verdict === "CHANGES_REQUESTED" &&
207
+ !findings.length &&
208
+ !requirementFindings.length)
209
+ throw new Error("CHANGES_REQUESTED requires findings or requirementFindings");
210
+ if (data.verdict === "CLOSE" &&
211
+ (findings.length || requirementFindings.length))
212
+ throw new Error("CLOSE requires no findings or requirementFindings");
101
213
  const reason = typeof data.reason === "string" && data.reason.trim()
102
214
  ? data.reason
103
215
  : undefined;
@@ -106,6 +218,7 @@ export function parseReviewOutput(text) {
106
218
  return {
107
219
  findings,
108
220
  reason,
221
+ requirementFindings,
109
222
  verdict: data.verdict,
110
223
  };
111
224
  }
@@ -129,33 +242,41 @@ export function parseRereviewOutput(text) {
129
242
  });
130
243
  const newFindings = requireArray(data.newFindings, "newFindings").map((item, index) => {
131
244
  const value = item;
245
+ const line = optionalLine(value.line, `newFindings[${index}].line`);
132
246
  return {
133
247
  body: requireString(value.body, `newFindings[${index}].body`),
134
- line: requireNumber(value.line, `newFindings[${index}].line`),
248
+ line,
135
249
  path: requireString(value.path, `newFindings[${index}].path`),
136
- startLine: value.startLine == null
137
- ? undefined
138
- : requireNumber(value.startLine, `newFindings[${index}].startLine`),
250
+ startLine: optionalStartLine({
251
+ line,
252
+ path: `newFindings[${index}].startLine`,
253
+ value: value.startLine,
254
+ }),
139
255
  };
140
256
  });
141
- if (data.verdict === "MERGE" && (followUps.length || newFindings.length)) {
142
- throw new Error("MERGE requires no followUps or newFindings");
257
+ const requirementFindings = parseRequirementFindings(data.requirementFindings);
258
+ if (data.verdict === "MERGE" &&
259
+ (followUps.length || newFindings.length || requirementFindings.length)) {
260
+ throw new Error("MERGE requires no followUps, newFindings, or requirementFindings");
143
261
  }
144
- if (data.verdict === "CLOSE" && (followUps.length || newFindings.length)) {
145
- throw new Error("CLOSE requires no followUps or newFindings");
262
+ if (data.verdict === "CLOSE" &&
263
+ (followUps.length || newFindings.length || requirementFindings.length)) {
264
+ throw new Error("CLOSE requires no followUps, newFindings, or requirementFindings");
146
265
  }
147
266
  if (data.verdict === "CLOSE" && !data.reason) {
148
267
  throw new Error("CLOSE requires reason");
149
268
  }
150
269
  if (data.verdict === "CHANGES_REQUESTED" &&
151
270
  !followUps.length &&
152
- !newFindings.length) {
153
- throw new Error("CHANGES_REQUESTED requires followUps or newFindings");
271
+ !newFindings.length &&
272
+ !requirementFindings.length) {
273
+ throw new Error("CHANGES_REQUESTED requires followUps, newFindings, or requirementFindings");
154
274
  }
155
275
  return {
156
276
  followUps,
157
277
  newFindings,
158
278
  reason: data.reason == null ? undefined : requireString(data.reason, "reason"),
279
+ requirementFindings,
159
280
  resolve,
160
281
  verdict: data.verdict,
161
282
  };
@@ -212,7 +333,21 @@ export function parseCiClassificationOutput(text) {
212
333
  }),
213
334
  };
214
335
  }
215
- export function parseEditOutput(text) {
336
+ function parsePullRequest(value, options) {
337
+ if (value == null) {
338
+ if (options.required)
339
+ throw new Error("pullRequest is required");
340
+ return undefined;
341
+ }
342
+ if (typeof value !== "object")
343
+ throw new Error("pullRequest must be an object");
344
+ const pullRequest = value;
345
+ return {
346
+ body: requireString(pullRequest.body, "pullRequest.body"),
347
+ title: requireString(pullRequest.title, "pullRequest.title"),
348
+ };
349
+ }
350
+ function parseEditOutputWithOptions(text, options) {
216
351
  const data = extractJson(text);
217
352
  if (!data || typeof data !== "object")
218
353
  throw new Error("edit output must be an object");
@@ -230,16 +365,20 @@ export function parseEditOutput(text) {
230
365
  commentId: requireNumber(value.commentId, `responses[${index}].commentId`),
231
366
  };
232
367
  });
233
- if (!responses.length)
368
+ if (options.requireResponses && data.mode === "REPLIED" && !responses.length)
234
369
  throw new Error("responses must not be empty");
235
370
  if (data.mode === "EDITED") {
236
371
  if (!filesTouched.length)
237
372
  throw new Error("EDITED requires filesTouched");
373
+ const pullRequest = parsePullRequest(data.pullRequest, {
374
+ required: options.requirePullRequest,
375
+ });
238
376
  return {
239
377
  commitMessage: requireString(data.commitMessage, "commitMessage"),
240
378
  commitSha: requireString(data.commitSha, "commitSha"),
241
379
  filesTouched,
242
380
  mode: data.mode,
381
+ ...(pullRequest ? { pullRequest } : {}),
243
382
  responses,
244
383
  };
245
384
  }
@@ -258,3 +397,15 @@ export function parseEditOutput(text) {
258
397
  responses,
259
398
  };
260
399
  }
400
+ export function parseEditOutput(text) {
401
+ return parseEditOutputWithOptions(text, {
402
+ requirePullRequest: false,
403
+ requireResponses: true,
404
+ });
405
+ }
406
+ export function parseTriageCreatePrOutput(text) {
407
+ return parseEditOutputWithOptions(text, {
408
+ requirePullRequest: true,
409
+ requireResponses: false,
410
+ });
411
+ }
@@ -1,9 +1,16 @@
1
1
  Fix pull request #{pr} for {owner}/{repo}.
2
2
  The PR worktree is {worktreePath}.
3
- Act as the PR author and resolve every unresolved review thread listed below.
3
+
4
+ Act as the PR author and address every blocking review finding listed below.
5
+ Review findings are the complete set of requested changes. Inline findings target a PR diff line; file-level findings may not have a GitHub thread; requirement findings describe missing closing-issue requirements.
6
+ {reviewFindings}
7
+
8
+ Unresolved GitHub review threads are conversations that may need replies or resolution.
4
9
  {unresolvedThreads}
5
- For each thread, decide whether you agree with the reviewer.
6
- If you understand and agree with the requested change, edit the code, stage changes, commit, and reply with action FIXED.
7
- If the requested change is incorrect or unnecessary and you have a clear reason, do not edit for that thread; reply with action DISAGREE and explain why.
8
- If you cannot determine whether the request is correct or what change is expected, do not blindly edit; reply with action ASK and ask a concrete question.
10
+
11
+ For each review finding and thread, decide whether you agree with the reviewer.
12
+ If you understand and agree with the requested change, edit the code, stage changes, commit, and reply with action FIXED for each related thread.
13
+ If a requested change in a thread is incorrect or unnecessary and you have a clear reason, do not edit for that thread; reply with action DISAGREE and explain why.
14
+ If you cannot determine whether a threaded request is correct or what change is expected, do not blindly edit; reply with action ASK and ask a concrete question.
15
+ File-level and requirement findings may not have a thread to reply to, but they are still blocking and must be addressed.
9
16
  Do not make changes just because a reviewer requested them. Do not push.
@@ -4,4 +4,10 @@ Review only the diff from {baseSha} to {headSha}.
4
4
  Use: git -C {jsonEncodedWorktreePath} diff {baseSha}...{headSha}
5
5
  Do not edit files or perform write operations.
6
6
 
7
+ This PR may include closing issue references.
8
+ For each closing issue, review whether the PR fully satisfies the issue body, acceptance criteria, required behavior, required tests, required documentation, and bounded issue comments.
9
+ Request changes if a closing issue requirement is missing, only documented, only schema-exposed, or not wired into runtime behavior.
10
+ Do not approve solely because the PR improves the codebase if it claims to close an issue that remains incomplete.
11
+ For referenced non-closing issues, use them as context only unless the PR body explicitly claims to complete them.
12
+
7
13
  {ciFailureContextBlock}
@@ -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,5 @@
1
+ Decide the next action for issue #{issue} in {owner}/{repo} from the provided triage result and allowed actions.
2
+
3
+ <context>
4
+ {context}
5
+ </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>
@@ -0,0 +1,7 @@
1
+ Classify allowed mention replies for issue #{issue} in {owner}/{repo}.
2
+
3
+ Use OBJECTION for disagreement or reconsideration requests. Use NEW_EVIDENCE for new logs, reproduction steps, screenshots, links, or use cases. Use CLARIFICATION for answers to questions or ambiguity reduction. Use ACKNOWLEDGEMENT for acceptance or thanks. Use UNRELATED for unrelated content.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,5 @@
1
+ Compose one concise final triage comment for issue #{issue} in {owner}/{repo}. Mention @{author}. Do not include markdown fences.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
@@ -0,0 +1,7 @@
1
+ Create an implementation pull request for issue #{issue} in {owner}/{repo}.
2
+
3
+ Use the checked-out worktree at {worktreePath} and commit your changes. Return the required structured output.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,7 @@
1
+ Decide whether issue #{issue} in {owner}/{repo} duplicates one of the provided duplicate candidates.
2
+
3
+ Use only the provided context. Return DUPLICATE only when the target issue is clearly the same underlying report or request.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,7 @@
1
+ Decide whether an existing related pull request already handles issue #{issue} in {owner}/{repo}.
2
+
3
+ Use only the provided context. Return HANDLE only when the PR clearly addresses the issue.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,5 @@
1
+ Compose concrete, actionable questions for issue #{issue} in {owner}/{repo}. Mention @{author}. Do not include markdown fences.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
@@ -0,0 +1,5 @@
1
+ Reconsider issue #{issue} in {owner}/{repo} using the provided previous triage result and triggering comments.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
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>",
@@ -41,6 +41,8 @@
41
41
  "@types/node": "^25.9.0",
42
42
  "@types/picomatch": "^4.0.3",
43
43
  "@typescript/native-preview": "7.0.0-dev.20260518.1",
44
+ "@vitest/coverage-v8": "^4.1.7",
45
+ "@vitest/ui": "^4.1.7",
44
46
  "eslint-plugin-perfectionist": "^5.9.0",
45
47
  "eslint-plugin-unused-imports": "^4.4.1",
46
48
  "lefthook": "^2.1.6",
@@ -48,7 +50,7 @@
48
50
  "oxlint": "^1.65.0",
49
51
  "oxlint-tsgolint": "^0.23.0",
50
52
  "rimraf": "^6.1.3",
51
- "vitest": "^4.1.5"
53
+ "vitest": "^4.1.7"
52
54
  },
53
55
  "scripts": {
54
56
  "prebuild": "node scripts/copy-data.ts",
@@ -60,6 +62,7 @@
60
62
  "lint:fix": "oxlint . --max-warnings=0 --fix",
61
63
  "quality": "pnpm format:check && pnpm lint:check && pnpm typecheck && pnpm test",
62
64
  "test": "vitest run",
65
+ "test:dev": "vitest --watch --ui",
63
66
  "typecheck": "tsgo --noEmit",
64
67
  "release": "changeset publish",
65
68
  "release:dev": "changeset version --snapshot dev && changeset publish --tag dev"
package/schema.json CHANGED
@@ -9,7 +9,11 @@
9
9
  "type": "object",
10
10
  "additionalProperties": false,
11
11
  "properties": {
12
- "permissions": { "$ref": "#/$defs/permissions" }
12
+ "permissions": { "$ref": "#/$defs/permissions" },
13
+ "refs": {
14
+ "type": "object",
15
+ "additionalProperties": { "$ref": "#/$defs/agentRef" }
16
+ }
13
17
  }
14
18
  },
15
19
  "clear": {
@@ -47,14 +51,37 @@
47
51
  "repairAttempts": { "type": "integer", "minimum": 0, "default": 3 }
48
52
  }
49
53
  },
50
- "review": { "$ref": "#/$defs/review" }
54
+ "review": { "$ref": "#/$defs/review" },
55
+ "triage": { "$ref": "#/$defs/triage" }
51
56
  },
52
57
  "$defs": {
58
+ "agentRef": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "properties": {
62
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
63
+ "model": { "type": "string", "minLength": 1 },
64
+ "options": { "type": "object", "additionalProperties": true },
65
+ "account": { "type": "string", "minLength": 1 },
66
+ "author": {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "properties": {
70
+ "name": { "type": "string", "minLength": 1 },
71
+ "email": { "type": "string", "minLength": 1 }
72
+ }
73
+ },
74
+ "permissions": { "$ref": "#/$defs/permissions" },
75
+ "persona": { "type": "string" }
76
+ }
77
+ },
53
78
  "reviewer": {
54
79
  "type": "object",
55
- "required": ["model", "account"],
80
+ "if": { "not": { "required": ["ref"] } },
81
+ "then": { "required": ["model", "account"] },
56
82
  "additionalProperties": false,
57
83
  "properties": {
84
+ "ref": { "type": "string", "minLength": 1 },
58
85
  "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
59
86
  "model": { "type": "string", "minLength": 1 },
60
87
  "options": { "type": "object", "additionalProperties": true },
@@ -65,12 +92,51 @@
65
92
  },
66
93
  "editor": {
67
94
  "type": "object",
68
- "required": ["model", "account", "author"],
95
+ "if": { "not": { "required": ["ref"] } },
96
+ "then": { "required": ["model", "account", "author"] },
97
+ "additionalProperties": false,
98
+ "properties": {
99
+ "ref": { "type": "string", "minLength": 1 },
100
+ "model": { "type": "string", "minLength": 1 },
101
+ "options": { "type": "object", "additionalProperties": true },
102
+ "account": { "type": "string", "minLength": 1 },
103
+ "author": {
104
+ "type": "object",
105
+ "required": ["name", "email"],
106
+ "additionalProperties": false,
107
+ "properties": {
108
+ "name": { "type": "string", "minLength": 1 },
109
+ "email": { "type": "string", "minLength": 1 }
110
+ }
111
+ },
112
+ "permissions": { "$ref": "#/$defs/permissions" },
113
+ "persona": { "type": "string" }
114
+ }
115
+ },
116
+ "triageAgent": {
117
+ "type": "object",
118
+ "if": { "not": { "required": ["ref"] } },
119
+ "then": { "required": ["model"] },
69
120
  "additionalProperties": false,
70
121
  "properties": {
122
+ "ref": { "type": "string", "minLength": 1 },
123
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
71
124
  "model": { "type": "string", "minLength": 1 },
72
125
  "options": { "type": "object", "additionalProperties": true },
126
+ "permissions": { "$ref": "#/$defs/permissions" },
127
+ "persona": { "type": "string" }
128
+ }
129
+ },
130
+ "triageCreator": {
131
+ "type": "object",
132
+ "if": { "not": { "required": ["ref"] } },
133
+ "then": { "required": ["model", "author"] },
134
+ "additionalProperties": false,
135
+ "properties": {
136
+ "ref": { "type": "string", "minLength": 1 },
73
137
  "account": { "type": "string", "minLength": 1 },
138
+ "model": { "type": "string", "minLength": 1 },
139
+ "options": { "type": "object", "additionalProperties": true },
74
140
  "author": {
75
141
  "type": "object",
76
142
  "required": ["name", "email"],
@@ -88,7 +154,7 @@
88
154
  "type": "object",
89
155
  "additionalProperties": false,
90
156
  "properties": {
91
- "merge": { "type": "boolean" },
157
+ "merge": { "type": "boolean", "default": true },
92
158
  "close": { "type": "boolean" }
93
159
  }
94
160
  },
@@ -116,6 +182,13 @@
116
182
  "reviewers": { "type": "integer", "minimum": 1, "default": 3 }
117
183
  }
118
184
  },
185
+ "triageConcurrency": {
186
+ "type": "object",
187
+ "additionalProperties": false,
188
+ "properties": {
189
+ "runs": { "type": "integer", "minimum": 1, "default": 3 }
190
+ }
191
+ },
119
192
  "safety": {
120
193
  "type": "object",
121
194
  "additionalProperties": false,
@@ -147,6 +220,67 @@
147
220
  "ciClassification": { "type": "string" }
148
221
  }
149
222
  },
223
+ "triagePrompts": {
224
+ "type": "object",
225
+ "additionalProperties": false,
226
+ "properties": {
227
+ "existingPr": { "type": "string" },
228
+ "duplicate": { "type": "string" },
229
+ "category": { "type": "string" },
230
+ "acceptance": { "type": "string" },
231
+ "action": { "type": "string" },
232
+ "question": { "type": "string" },
233
+ "comment": { "type": "string" },
234
+ "commentClassification": { "type": "string" },
235
+ "reconsider": { "type": "string" },
236
+ "create": { "type": "string" },
237
+ "createGuidelines": { "type": "string" }
238
+ }
239
+ },
240
+ "triageCategory": {
241
+ "type": "object",
242
+ "additionalProperties": false,
243
+ "required": ["id"],
244
+ "properties": {
245
+ "id": {
246
+ "type": "string",
247
+ "pattern": "^[A-Za-z0-9_-]+$",
248
+ "not": { "enum": ["ASK", "none"] }
249
+ },
250
+ "labels": { "type": "array", "items": { "type": "string" } },
251
+ "types": { "type": "array", "items": { "type": "string" } },
252
+ "description": { "type": "string" }
253
+ }
254
+ },
255
+ "triageAutomation": {
256
+ "type": "object",
257
+ "additionalProperties": false,
258
+ "properties": {
259
+ "close": { "type": "boolean", "default": false },
260
+ "create": { "type": "boolean", "default": false },
261
+ "review": { "type": "boolean", "default": false },
262
+ "merge": { "type": "boolean", "default": false },
263
+ "clear": {
264
+ "type": "array",
265
+ "items": { "type": "string" },
266
+ "default": ["triage"]
267
+ }
268
+ }
269
+ },
270
+ "triageSafety": {
271
+ "type": "object",
272
+ "additionalProperties": false,
273
+ "properties": {
274
+ "requiredLabels": { "type": "array", "items": { "type": "string" } },
275
+ "blockedLabels": { "type": "array", "items": { "type": "string" } },
276
+ "allowAuthors": { "type": "array", "items": { "type": "string" } },
277
+ "allowMentionActors": {
278
+ "type": "array",
279
+ "items": { "type": "string" }
280
+ },
281
+ "allowMentionRoles": { "type": "array", "items": { "type": "string" } }
282
+ }
283
+ },
150
284
  "reviewMerge": {
151
285
  "type": "object",
152
286
  "additionalProperties": false,
@@ -196,6 +330,29 @@
196
330
  }
197
331
  }
198
332
  },
333
+ "triage": {
334
+ "type": "object",
335
+ "additionalProperties": false,
336
+ "properties": {
337
+ "account": { "type": "string", "minLength": 1 },
338
+ "agents": {
339
+ "type": "array",
340
+ "minItems": 3,
341
+ "items": { "$ref": "#/$defs/triageAgent" }
342
+ },
343
+ "creator": { "$ref": "#/$defs/triageCreator" },
344
+ "categories": {
345
+ "type": "array",
346
+ "items": { "$ref": "#/$defs/triageCategory" }
347
+ },
348
+ "automation": { "$ref": "#/$defs/triageAutomation" },
349
+ "safety": { "$ref": "#/$defs/triageSafety" },
350
+ "concurrency": { "$ref": "#/$defs/triageConcurrency" },
351
+ "prompts": { "$ref": "#/$defs/triagePrompts" },
352
+ "output": { "type": "string" },
353
+ "worktree": { "type": "string" }
354
+ }
355
+ },
199
356
  "permissionAction": { "enum": ["allow", "ask", "deny"] },
200
357
  "permissionRule": {
201
358
  "oneOf": [