opencode-magi 0.0.0-dev-20260519013159 → 0.0.0-dev-20260519070815

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/README.md CHANGED
@@ -61,8 +61,8 @@ Add the following content to the configuration file.
61
61
  ```json
62
62
  {
63
63
  "$schema": "https://raw.githubusercontent.com/magi-ai/opencode-magi/main/schema.json",
64
- "agents": {
65
- "reviewers": [
64
+ "review": {
65
+ "agents": [
66
66
  {
67
67
  "account": "your-account-1",
68
68
  "model": "openai/gpt-5.5"
@@ -80,7 +80,7 @@ Add the following content to the configuration file.
80
80
  }
81
81
  ```
82
82
 
83
- `agents.reviewers[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
83
+ `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
84
84
 
85
85
  #### Set project config
86
86
 
@@ -101,8 +101,8 @@ Add the following content to the configuration file.
101
101
  "owner": "your-owner",
102
102
  "repo": "your-repo"
103
103
  },
104
- "agents": {
105
- "reviewers": [
104
+ "review": {
105
+ "agents": [
106
106
  {
107
107
  "account": "your-account-1",
108
108
  "model": "openai/gpt-5.5"
@@ -115,7 +115,9 @@ Add the following content to the configuration file.
115
115
  "account": "your-account-3",
116
116
  "model": "opencode/kimi-k2-6"
117
117
  }
118
- ],
118
+ ]
119
+ },
120
+ "merge": {
119
121
  "editor": {
120
122
  "account": "your-editor-account",
121
123
  "model": "openai/gpt-5.5",
@@ -128,7 +130,7 @@ Add the following content to the configuration file.
128
130
  }
129
131
  ```
130
132
 
131
- `agents.reviewers[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
133
+ `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique. `merge.editor.account` is used by `/magi:merge` to push fixes, close PRs, and merge PRs.
132
134
 
133
135
  #### Validate config
134
136
 
@@ -6,7 +6,7 @@ function resolvePath(directory, path) {
6
6
  return isAbsolute(path) ? path : join(directory, path);
7
7
  }
8
8
  export function outputBaseDir(directory, config, kind) {
9
- return resolvePath(directory, config.output?.dirs?.[kind] ?? DEFAULT_OUTPUT_DIRS[kind]);
9
+ return resolvePath(directory, config.review?.output ?? DEFAULT_OUTPUT_DIRS[kind]);
10
10
  }
11
11
  export function outputBaseDirs(directory, config) {
12
12
  return [outputBaseDir(directory, config, "pr")];
@@ -44,20 +44,22 @@ export function mergePermissions(base, override) {
44
44
  return merged;
45
45
  }
46
46
  export function resolveReviewerPermission(agents, reviewer) {
47
- return mergePermissions(mergePermissions(DEFAULT_REVIEWER_PERMISSION, agents.permissions), reviewer.permission);
47
+ return mergePermissions(mergePermissions(DEFAULT_REVIEWER_PERMISSION, agents.permissions), reviewer.permissions);
48
48
  }
49
49
  export function resolveEditorPermission(agents, editor) {
50
- return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), editor.permission);
50
+ return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), editor.permissions);
51
51
  }
52
- export function resolveAgents(agents) {
52
+ export function resolveAgents(config) {
53
+ const agents = config.agents ?? {};
54
+ const editor = config.merge?.editor;
53
55
  return {
54
- editor: agents.editor
56
+ editor: editor
55
57
  ? {
56
- ...agents.editor,
57
- permission: resolveEditorPermission(agents, agents.editor),
58
+ ...editor,
59
+ permission: resolveEditorPermission(agents, editor),
58
60
  }
59
61
  : undefined,
60
- reviewers: (agents.reviewers ?? []).map((reviewer, index) => ({
62
+ reviewers: (config.review?.agents ?? []).map((reviewer, index) => ({
61
63
  ...reviewer,
62
64
  key: reviewerKey(reviewer, index),
63
65
  index,
@@ -72,20 +74,21 @@ export function resolveRepository(config) {
72
74
  throw new Error("github.repo is required");
73
75
  return {
74
76
  alias: config.github.repo,
75
- agents: resolveAgents(config.agents),
77
+ agents: resolveAgents(config),
76
78
  automation: {
77
- close: config.automation?.close ?? true,
78
- merge: config.automation?.merge ?? true,
79
+ close: config.merge?.automation?.close ?? false,
80
+ merge: config.merge?.automation?.merge ?? true,
79
81
  },
80
82
  checks: {
81
- exclude: config.checks?.exclude ?? [],
82
- waitAfterEdit: config.checks?.waitAfterEdit ?? true,
83
- waitBeforeReview: config.checks?.waitBeforeReview ?? true,
84
- retryFailedJobs: config.checks?.retryFailedJobs ?? 3,
83
+ exclude: config.review?.checks?.exclude ?? [],
84
+ retryFailedJobs: config.review?.checks?.retryFailedJobs ?? 3,
85
+ wait: config.review?.checks?.wait ?? true,
86
+ waitAfterEdit: config.merge?.checks?.wait ?? true,
87
+ waitBeforeReview: config.review?.checks?.wait ?? true,
85
88
  },
86
89
  concurrency: {
87
- runs: config.concurrency?.runs ?? 3,
88
- reviewers: config.concurrency?.reviewers ?? 3,
90
+ runs: config.review?.concurrency?.runs ?? 3,
91
+ reviewers: config.review?.concurrency?.reviewers ?? 3,
89
92
  },
90
93
  github: {
91
94
  apiRetryAttempts: config.github.apiRetryAttempts ?? 3,
@@ -95,19 +98,35 @@ export function resolveRepository(config) {
95
98
  },
96
99
  language: config.language,
97
100
  merge: {
98
- approvalPolicy: config.merge?.approvalPolicy ?? "majority",
99
- method: config.merge?.method ?? "squash",
100
- auto: config.merge?.auto ?? true,
101
- deleteBranch: config.merge?.deleteBranch ?? true,
102
- mergeQueue: config.merge?.mergeQueue ?? false,
101
+ approvalPolicy: config.review?.merge?.approvalPolicy ?? "majority",
102
+ method: config.review?.merge?.method ?? "squash",
103
+ auto: config.review?.merge?.auto ?? true,
104
+ deleteBranch: config.review?.merge?.deleteBranch ?? true,
105
+ queue: config.review?.merge?.queue ?? false,
106
+ mergeQueue: config.review?.merge?.queue ?? false,
103
107
  maxThreadResolutionCycles: config.merge?.maxThreadResolutionCycles ?? 5,
104
108
  },
105
- prompts: config.prompts ?? {},
109
+ prompts: {
110
+ ciClassification: config.review?.prompts?.ciClassification,
111
+ ciClassificationAfterEdit: config.merge?.prompts?.ciClassification,
112
+ closeReconsideration: config.review?.prompts?.closeReconsideration,
113
+ edit: config.merge?.prompts?.edit,
114
+ editGuidelines: config.merge?.prompts?.editGuidelines,
115
+ findingValidation: config.review?.prompts?.findingValidation,
116
+ rereview: config.review?.prompts?.rereview,
117
+ rereviewCloseReconsideration: config.review?.prompts?.closeReconsideration,
118
+ review: config.review?.prompts?.review,
119
+ reviewGuidelines: config.review?.prompts?.reviewGuidelines,
120
+ },
121
+ reviewAutomation: {
122
+ close: config.review?.automation?.close ?? false,
123
+ merge: config.review?.automation?.merge ?? false,
124
+ },
106
125
  safety: {
107
- allowAuthors: config.safety?.allowAuthors ?? [],
108
- blockedPaths: config.safety?.blockedPaths ?? [],
109
- maxChangedFiles: config.safety?.maxChangedFiles,
110
- requiredLabels: config.safety?.requiredLabels ?? [],
126
+ allowAuthors: config.review?.safety?.allowAuthors ?? [],
127
+ blockedPaths: config.review?.safety?.blockedPaths ?? [],
128
+ maxChangedFiles: config.review?.safety?.maxChangedFiles,
129
+ requiredLabels: config.review?.safety?.requiredLabels ?? [],
111
130
  },
112
131
  };
113
132
  }
@@ -12,25 +12,20 @@ const validateSchema = AJV.compile(schema);
12
12
  const CONFIG_KEYS = new Set([
13
13
  "$schema",
14
14
  "agents",
15
- "automation",
16
15
  "clear",
17
- "checks",
18
- "concurrency",
19
16
  "github",
20
17
  "language",
21
18
  "merge",
22
19
  "output",
23
- "prompts",
24
- "safety",
25
- "worktree",
20
+ "review",
26
21
  ]);
27
- const AGENTS_KEYS = new Set(["editor", "permissions", "reviewers"]);
22
+ const AGENTS_KEYS = new Set(["permissions"]);
28
23
  const REVIEWER_KEYS = new Set([
29
24
  "account",
30
25
  "id",
31
26
  "model",
32
27
  "options",
33
- "permission",
28
+ "permissions",
34
29
  "persona",
35
30
  ]);
36
31
  const EDITOR_KEYS = new Set([
@@ -38,51 +33,61 @@ const EDITOR_KEYS = new Set([
38
33
  "author",
39
34
  "model",
40
35
  "options",
41
- "permission",
36
+ "permissions",
42
37
  "persona",
43
38
  ]);
44
39
  const AUTHOR_KEYS = new Set(["email", "name"]);
45
40
  const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
41
+ const REVIEW_KEYS = new Set([
42
+ "agents",
43
+ "automation",
44
+ "checks",
45
+ "concurrency",
46
+ "merge",
47
+ "output",
48
+ "prompts",
49
+ "safety",
50
+ "worktree",
51
+ ]);
46
52
  const MERGE_KEYS = new Set([
53
+ "automation",
54
+ "checks",
55
+ "editor",
56
+ "maxThreadResolutionCycles",
57
+ "prompts",
58
+ ]);
59
+ const REVIEW_MERGE_KEYS = new Set([
47
60
  "approvalPolicy",
48
61
  "auto",
49
62
  "deleteBranch",
50
- "maxThreadResolutionCycles",
51
- "mergeQueue",
52
63
  "method",
64
+ "queue",
53
65
  ]);
54
- const CHECKS_KEYS = new Set([
55
- "exclude",
56
- "retryFailedJobs",
57
- "waitAfterEdit",
58
- "waitBeforeReview",
59
- ]);
66
+ const REVIEW_CHECKS_KEYS = new Set(["exclude", "retryFailedJobs", "wait"]);
67
+ const MERGE_CHECKS_KEYS = new Set(["wait"]);
60
68
  const AUTOMATION_KEYS = new Set(["close", "merge"]);
61
69
  const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
62
70
  const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
63
- const OUTPUT_KEYS = new Set(["dirs", "repairAttempts"]);
64
- const OUTPUT_DIR_KEYS = new Set(["pr"]);
65
- const WORKTREE_KEYS = new Set(["dirs"]);
66
- const WORKTREE_DIR_KEYS = new Set(["pr"]);
71
+ const OUTPUT_KEYS = new Set(["repairAttempts"]);
67
72
  const SAFETY_KEYS = new Set([
68
73
  "allowAuthors",
69
74
  "blockedPaths",
70
75
  "maxChangedFiles",
71
76
  "requiredLabels",
72
77
  ]);
73
- const PROMPT_KEYS = new Set([
78
+ const REVIEW_PROMPT_KEYS = new Set([
74
79
  "ciClassification",
75
- "ciClassificationAfterEdit",
76
80
  "closeReconsideration",
77
- "edit",
78
- "editGuidelines",
79
81
  "findingValidation",
80
- "report",
81
82
  "rereview",
82
- "rereviewCloseReconsideration",
83
83
  "review",
84
84
  "reviewGuidelines",
85
85
  ]);
86
+ const MERGE_PROMPT_KEYS = new Set([
87
+ "ciClassification",
88
+ "edit",
89
+ "editGuidelines",
90
+ ]);
86
91
  function githubHost(config) {
87
92
  return config.github?.host ?? "github.com";
88
93
  }
@@ -211,7 +216,7 @@ function validateReviewerList(reviewers, path, errors, catalog) {
211
216
  validateString(reviewer.persona, `${path}[${index}].persona`, errors);
212
217
  if (reviewer.options != null && !isPlainObject(reviewer.options))
213
218
  errors.push(`${path}[${index}].options must be an object`);
214
- validatePermissionConfig(reviewer.permission, `${path}[${index}].permission`, errors);
219
+ validatePermissionConfig(reviewer.permissions, `${path}[${index}].permissions`, errors);
215
220
  if (reviewer.id) {
216
221
  if (!validateReviewerId(reviewer.id)) {
217
222
  errors.push(`${path}[${index}].id may contain only letters, numbers, underscores, and hyphens`);
@@ -234,7 +239,51 @@ function validateResolvedReviewers(reviewers, path, errors) {
234
239
  accounts.add(reviewer.account);
235
240
  }
236
241
  }
242
+ function validateEditor(editor, path, errors, catalog) {
243
+ if (!editor)
244
+ return;
245
+ if (!isPlainObject(editor)) {
246
+ errors.push(`${path} must be an object`);
247
+ return;
248
+ }
249
+ if (!editor.model)
250
+ errors.push(`${path}.model is required`);
251
+ validateKnownKeys(editor, path, EDITOR_KEYS, errors);
252
+ validateString(editor.model, `${path}.model`, errors);
253
+ validateString(editor.account, `${path}.account`, errors);
254
+ validateString(editor.persona, `${path}.persona`, errors);
255
+ validateModel(editor.model, `${path}.model`, errors, catalog);
256
+ if (!editor.account)
257
+ errors.push(`${path}.account is required`);
258
+ if (editor.options != null && !isPlainObject(editor.options)) {
259
+ errors.push(`${path}.options must be an object`);
260
+ }
261
+ validatePermissionConfig(editor.permissions, `${path}.permissions`, errors);
262
+ const author = editor.author;
263
+ if (!author || !isPlainObject(author)) {
264
+ if (author != null)
265
+ errors.push(`${path}.author must be an object`);
266
+ errors.push(`${path}.author.name is required`);
267
+ errors.push(`${path}.author.email is required`);
268
+ }
269
+ else {
270
+ validateKnownKeys(author, `${path}.author`, AUTHOR_KEYS, errors);
271
+ if (!author.name) {
272
+ errors.push(`${path}.author.name is required`);
273
+ }
274
+ else if (typeof author.name !== "string") {
275
+ errors.push(`${path}.author.name must be a string`);
276
+ }
277
+ if (!author.email) {
278
+ errors.push(`${path}.author.email is required`);
279
+ }
280
+ else if (typeof author.email !== "string") {
281
+ errors.push(`${path}.author.email must be a string`);
282
+ }
283
+ }
284
+ }
237
285
  function validateMerge(config, errors, options) {
286
+ const merge = config.merge;
238
287
  if (options.requireGithub ?? true) {
239
288
  if (!config.github?.owner)
240
289
  errors.push("github.owner is required");
@@ -248,102 +297,102 @@ function validateMerge(config, errors, options) {
248
297
  if (config.github != null && !isPlainObject(config.github)) {
249
298
  errors.push("github must be an object");
250
299
  }
251
- if (config.merge != null && !isPlainObject(config.merge)) {
252
- errors.push("merge must be an object");
253
- }
254
- validateKnownKeys(config.merge, "merge", MERGE_KEYS, errors);
255
- validateBoolean(config.merge?.auto, "merge.auto", errors);
256
- validateBoolean(config.merge?.deleteBranch, "merge.deleteBranch", errors);
257
- validateBoolean(config.merge?.mergeQueue, "merge.mergeQueue", errors);
258
300
  if (config.github?.apiRetryAttempts != null &&
259
301
  (typeof config.github.apiRetryAttempts !== "number" ||
260
302
  !Number.isInteger(config.github.apiRetryAttempts) ||
261
303
  config.github.apiRetryAttempts < 0)) {
262
304
  errors.push("github.apiRetryAttempts must be a non-negative integer");
263
305
  }
264
- if (config.merge?.method != null &&
265
- (typeof config.merge.method !== "string" ||
266
- !["merge", "rebase", "squash"].includes(config.merge.method))) {
267
- errors.push("merge.method must be merge, squash, or rebase");
268
- }
269
- if (config.merge?.approvalPolicy != null &&
270
- (typeof config.merge.approvalPolicy !== "string" ||
271
- !["majority", "unanimous"].includes(config.merge.approvalPolicy))) {
272
- errors.push("merge.approvalPolicy must be majority or unanimous");
306
+ if (merge != null && !isPlainObject(merge)) {
307
+ errors.push("merge must be an object");
273
308
  }
274
- if (config.merge?.maxThreadResolutionCycles != null &&
275
- (typeof config.merge.maxThreadResolutionCycles !== "number" ||
276
- !Number.isInteger(config.merge.maxThreadResolutionCycles) ||
277
- config.merge.maxThreadResolutionCycles < 0)) {
309
+ validateKnownKeys(merge, "merge", MERGE_KEYS, errors);
310
+ validateBooleanObject(merge?.automation, "merge.automation", AUTOMATION_KEYS, errors);
311
+ const checks = merge?.checks;
312
+ validateKnownKeys(checks, "merge.checks", MERGE_CHECKS_KEYS, errors);
313
+ validateBoolean(checks?.wait, "merge.checks.wait", errors);
314
+ validateEditor(merge?.editor, "merge.editor", errors, options.modelCatalog);
315
+ if (merge?.maxThreadResolutionCycles != null &&
316
+ (typeof merge.maxThreadResolutionCycles !== "number" ||
317
+ !Number.isInteger(merge.maxThreadResolutionCycles) ||
318
+ merge.maxThreadResolutionCycles < 0)) {
278
319
  errors.push("merge.maxThreadResolutionCycles must be a non-negative integer");
279
320
  }
321
+ if (options.requireEditor && !merge?.editor)
322
+ errors.push("merge.editor is required");
323
+ }
324
+ function validateReviewMerge(config, errors) {
325
+ const merge = config.review?.merge;
326
+ if (merge != null && !isPlainObject(merge)) {
327
+ errors.push("review.merge must be an object");
328
+ }
329
+ validateKnownKeys(merge, "review.merge", REVIEW_MERGE_KEYS, errors);
330
+ validateBoolean(merge?.auto, "review.merge.auto", errors);
331
+ validateBoolean(merge?.deleteBranch, "review.merge.deleteBranch", errors);
332
+ validateBoolean(merge?.queue, "review.merge.queue", errors);
333
+ if (merge?.method != null &&
334
+ (typeof merge.method !== "string" ||
335
+ !["merge", "rebase", "squash"].includes(merge.method))) {
336
+ errors.push("review.merge.method must be merge, squash, or rebase");
337
+ }
338
+ if (merge?.approvalPolicy != null &&
339
+ (typeof merge.approvalPolicy !== "string" ||
340
+ !["majority", "unanimous"].includes(merge.approvalPolicy))) {
341
+ errors.push("review.merge.approvalPolicy must be majority or unanimous");
342
+ }
280
343
  }
281
344
  function validateConcurrency(config, errors) {
282
- if (config.concurrency != null && !isPlainObject(config.concurrency)) {
283
- errors.push("concurrency must be an object");
284
- }
285
- validateKnownKeys(config.concurrency, "concurrency", CONCURRENCY_KEYS, errors);
286
- if (config.concurrency?.runs != null) {
287
- if (typeof config.concurrency.runs !== "number" ||
288
- !Number.isInteger(config.concurrency.runs) ||
289
- config.concurrency.runs < 1) {
290
- errors.push("concurrency.runs must be a positive integer");
345
+ const concurrency = config.review?.concurrency;
346
+ if (concurrency != null && !isPlainObject(concurrency)) {
347
+ errors.push("review.concurrency must be an object");
348
+ }
349
+ validateKnownKeys(concurrency, "review.concurrency", CONCURRENCY_KEYS, errors);
350
+ if (concurrency?.runs != null) {
351
+ if (typeof concurrency.runs !== "number" ||
352
+ !Number.isInteger(concurrency.runs) ||
353
+ concurrency.runs < 1) {
354
+ errors.push("review.concurrency.runs must be a positive integer");
291
355
  }
292
356
  }
293
- if (config.concurrency?.reviewers != null) {
294
- if (typeof config.concurrency.reviewers !== "number" ||
295
- !Number.isInteger(config.concurrency.reviewers) ||
296
- config.concurrency.reviewers < 1) {
297
- errors.push("concurrency.reviewers must be a positive integer");
357
+ if (concurrency?.reviewers != null) {
358
+ if (typeof concurrency.reviewers !== "number" ||
359
+ !Number.isInteger(concurrency.reviewers) ||
360
+ concurrency.reviewers < 1) {
361
+ errors.push("review.concurrency.reviewers must be a positive integer");
298
362
  }
299
363
  }
300
364
  }
301
365
  function validateAutomation(config, errors) {
302
- if (config.automation != null && !isPlainObject(config.automation)) {
303
- errors.push("automation must be an object");
304
- }
305
- validateKnownKeys(config.automation, "automation", AUTOMATION_KEYS, errors);
306
- if (config.automation?.merge != null &&
307
- typeof config.automation.merge !== "boolean") {
308
- errors.push("automation.merge must be a boolean");
309
- }
310
- if (config.automation?.close != null &&
311
- typeof config.automation.close !== "boolean") {
312
- errors.push("automation.close must be a boolean");
313
- }
366
+ validateBooleanObject(config.review?.automation, "review.automation", AUTOMATION_KEYS, errors);
314
367
  }
315
368
  function validateClear(config, errors) {
316
369
  validateBooleanObject(config.clear, "clear", CLEAR_KEYS, errors);
317
370
  }
318
371
  function validateChecks(config, errors) {
319
- if (config.checks != null && !isPlainObject(config.checks)) {
320
- errors.push("checks must be an object");
321
- }
322
- validateKnownKeys(config.checks, "checks", CHECKS_KEYS, errors);
323
- if (config.checks?.exclude != null) {
324
- if (!Array.isArray(config.checks.exclude)) {
325
- errors.push("checks.exclude must be an array");
372
+ const checks = config.review?.checks;
373
+ if (checks != null && !isPlainObject(checks)) {
374
+ errors.push("review.checks must be an object");
375
+ }
376
+ validateKnownKeys(checks, "review.checks", REVIEW_CHECKS_KEYS, errors);
377
+ if (checks?.exclude != null) {
378
+ if (!Array.isArray(checks.exclude)) {
379
+ errors.push("review.checks.exclude must be an array");
326
380
  }
327
381
  else {
328
- config.checks.exclude.forEach((item, index) => {
382
+ checks.exclude.forEach((item, index) => {
329
383
  if (typeof item !== "string")
330
- errors.push(`checks.exclude[${index}] must be a string`);
384
+ errors.push(`review.checks.exclude[${index}] must be a string`);
331
385
  });
332
386
  }
333
387
  }
334
- if (config.checks?.waitBeforeReview != null &&
335
- typeof config.checks.waitBeforeReview !== "boolean") {
336
- errors.push("checks.waitBeforeReview must be a boolean");
388
+ if (checks?.wait != null && typeof checks.wait !== "boolean") {
389
+ errors.push("review.checks.wait must be a boolean");
337
390
  }
338
- if (config.checks?.waitAfterEdit != null &&
339
- typeof config.checks.waitAfterEdit !== "boolean") {
340
- errors.push("checks.waitAfterEdit must be a boolean");
341
- }
342
- if (config.checks?.retryFailedJobs != null &&
343
- (typeof config.checks.retryFailedJobs !== "number" ||
344
- !Number.isInteger(config.checks.retryFailedJobs) ||
345
- config.checks.retryFailedJobs < 0)) {
346
- errors.push("checks.retryFailedJobs must be a non-negative integer");
391
+ if (checks?.retryFailedJobs != null &&
392
+ (typeof checks.retryFailedJobs !== "number" ||
393
+ !Number.isInteger(checks.retryFailedJobs) ||
394
+ checks.retryFailedJobs < 0)) {
395
+ errors.push("review.checks.retryFailedJobs must be a non-negative integer");
347
396
  }
348
397
  }
349
398
  function validateStringArray(value, path, errors) {
@@ -359,33 +408,44 @@ function validateStringArray(value, path, errors) {
359
408
  });
360
409
  }
361
410
  function validateSafety(config, errors) {
362
- if (config.safety != null && !isPlainObject(config.safety)) {
363
- errors.push("safety must be an object");
364
- }
365
- validateKnownKeys(config.safety, "safety", SAFETY_KEYS, errors);
366
- validateStringArray(config.safety?.allowAuthors, "safety.allowAuthors", errors);
367
- validateStringArray(config.safety?.blockedPaths, "safety.blockedPaths", errors);
368
- validateStringArray(config.safety?.requiredLabels, "safety.requiredLabels", errors);
369
- if (config.safety?.maxChangedFiles != null &&
370
- (typeof config.safety.maxChangedFiles !== "number" ||
371
- !Number.isInteger(config.safety.maxChangedFiles) ||
372
- config.safety.maxChangedFiles < 0)) {
373
- errors.push("safety.maxChangedFiles must be a non-negative integer");
411
+ const safety = config.review?.safety;
412
+ if (safety != null && !isPlainObject(safety)) {
413
+ errors.push("review.safety must be an object");
414
+ }
415
+ validateKnownKeys(safety, "review.safety", SAFETY_KEYS, errors);
416
+ validateStringArray(safety?.allowAuthors, "review.safety.allowAuthors", errors);
417
+ validateStringArray(safety?.blockedPaths, "review.safety.blockedPaths", errors);
418
+ validateStringArray(safety?.requiredLabels, "review.safety.requiredLabels", errors);
419
+ if (safety?.maxChangedFiles != null &&
420
+ (typeof safety.maxChangedFiles !== "number" ||
421
+ !Number.isInteger(safety.maxChangedFiles) ||
422
+ safety.maxChangedFiles < 0)) {
423
+ errors.push("review.safety.maxChangedFiles must be a non-negative integer");
374
424
  }
375
425
  }
376
- async function validatePrompts(config, errors, directory) {
377
- if (config.prompts == null)
426
+ function validatePromptObject(prompts, path, keys, errors) {
427
+ if (prompts == null)
378
428
  return;
379
- if (!isPlainObject(config.prompts)) {
380
- errors.push("prompts must be an object");
429
+ if (!isPlainObject(prompts)) {
430
+ errors.push(`${path} must be an object`);
381
431
  return;
382
432
  }
383
- validateKnownKeys(config.prompts, "prompts", PROMPT_KEYS, errors);
384
- await Promise.all(Object.entries(config.prompts).map(async ([key, value]) => {
385
- if (typeof value !== "string") {
386
- errors.push(`prompts.${key} must be a string`);
433
+ validateKnownKeys(prompts, path, keys, errors);
434
+ for (const [key, value] of Object.entries(prompts)) {
435
+ if (typeof value !== "string")
436
+ errors.push(`${path}.${key} must be a string`);
437
+ }
438
+ }
439
+ async function validatePrompts(config, errors, directory) {
440
+ validatePromptObject(config.review?.prompts, "review.prompts", REVIEW_PROMPT_KEYS, errors);
441
+ validatePromptObject(config.merge?.prompts, "merge.prompts", MERGE_PROMPT_KEYS, errors);
442
+ const promptEntries = [
443
+ ...Object.entries(config.review?.prompts ?? {}).map(([key, value]) => [`review.prompts.${key}`, value]),
444
+ ...Object.entries(config.merge?.prompts ?? {}).map(([key, value]) => [`merge.prompts.${key}`, value]),
445
+ ];
446
+ await Promise.all(promptEntries.map(async ([path, value]) => {
447
+ if (typeof value !== "string")
387
448
  return;
388
- }
389
449
  if (!directory)
390
450
  return;
391
451
  const fullPath = promptPath(directory, value);
@@ -393,13 +453,13 @@ async function validatePrompts(config, errors, directory) {
393
453
  await access(fullPath, constants.R_OK);
394
454
  }
395
455
  catch {
396
- errors.push(`prompts.${key} file is not readable: ${value}`);
456
+ errors.push(`${path} file is not readable: ${value}`);
397
457
  }
398
458
  }));
399
459
  }
400
460
  async function validateAuth(config, exec, errors) {
401
461
  const accounts = new Set();
402
- const agents = resolveAgents(config.agents);
462
+ const agents = resolveAgents(config);
403
463
  for (const reviewer of agents.reviewers)
404
464
  accounts.add(reviewer.account);
405
465
  if (agents.editor)
@@ -421,7 +481,7 @@ async function fetchPermissions(config, exec, account) {
421
481
  async function validateRepositoryPermissions(config, exec, errors, warnings) {
422
482
  if (!config.github?.owner || !config.github.repo)
423
483
  return;
424
- const agents = resolveAgents(config.agents);
484
+ const agents = resolveAgents(config);
425
485
  await Promise.all(agents.reviewers.map(async (reviewer) => {
426
486
  try {
427
487
  const permissions = await fetchPermissions(config, exec, reviewer.account);
@@ -455,65 +515,32 @@ export async function validateConfig(config, options = {}) {
455
515
  validateKnownKeys(config, "config", CONFIG_KEYS, errors);
456
516
  validateString(config.$schema, "$schema", errors);
457
517
  validateString(config.language, "language", errors);
458
- if (!config.agents) {
459
- errors.push("agents is required");
518
+ if (config.agents != null && !isPlainObject(config.agents)) {
519
+ errors.push("agents must be an object");
460
520
  }
461
521
  else {
462
- if (!isPlainObject(config.agents)) {
463
- errors.push("agents must be an object");
522
+ validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
523
+ validatePermissionConfig(config.agents?.permissions, "agents.permissions", errors);
524
+ }
525
+ if (!config.review) {
526
+ errors.push("review is required");
527
+ }
528
+ else {
529
+ if (!isPlainObject(config.review)) {
530
+ errors.push("review must be an object");
464
531
  }
465
532
  else {
466
- validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
467
- }
468
- validatePermissionConfig(config.agents.permissions, "agents.permissions", errors);
469
- if (!config.agents.reviewers)
470
- errors.push("agents.reviewers is required");
471
- validateReviewerList(config.agents.reviewers, "agents.reviewers", errors, options.modelCatalog);
472
- if (options.requireEditor && !config.agents.editor)
473
- errors.push("agents.editor is required");
474
- if (config.agents.editor) {
475
- if (!config.agents.editor.model)
476
- errors.push("agents.editor.model is required");
477
- validateKnownKeys(config.agents.editor, "agents.editor", EDITOR_KEYS, errors);
478
- validateString(config.agents.editor.model, "agents.editor.model", errors);
479
- validateString(config.agents.editor.account, "agents.editor.account", errors);
480
- validateString(config.agents.editor.persona, "agents.editor.persona", errors);
481
- validateModel(config.agents.editor.model, "agents.editor.model", errors, options.modelCatalog);
482
- if (!config.agents.editor.account)
483
- errors.push("agents.editor.account is required");
484
- if (config.agents.editor.options != null &&
485
- !isPlainObject(config.agents.editor.options)) {
486
- errors.push("agents.editor.options must be an object");
487
- }
488
- validatePermissionConfig(config.agents.editor.permission, "agents.editor.permission", errors);
489
- const author = config.agents.editor.author;
490
- if (!author || !isPlainObject(author)) {
491
- if (author != null)
492
- errors.push("agents.editor.author must be an object");
493
- errors.push("agents.editor.author.name is required");
494
- errors.push("agents.editor.author.email is required");
495
- }
496
- else {
497
- validateKnownKeys(author, "agents.editor.author", AUTHOR_KEYS, errors);
498
- if (!author.name) {
499
- errors.push("agents.editor.author.name is required");
500
- }
501
- else if (typeof author.name !== "string") {
502
- errors.push("agents.editor.author.name must be a string");
503
- }
504
- if (!author.email) {
505
- errors.push("agents.editor.author.email is required");
506
- }
507
- else if (typeof author.email !== "string") {
508
- errors.push("agents.editor.author.email must be a string");
509
- }
510
- }
533
+ validateKnownKeys(config.review, "review", REVIEW_KEYS, errors);
511
534
  }
512
- if (Array.isArray(config.agents.reviewers)) {
513
- validateResolvedReviewers(resolveAgents(config.agents).reviewers, "agents.resolvedReviewers", errors);
535
+ if (!config.review.agents)
536
+ errors.push("review.agents is required");
537
+ validateReviewerList(config.review.agents, "review.agents", errors, options.modelCatalog);
538
+ if (Array.isArray(config.review.agents)) {
539
+ validateResolvedReviewers(resolveAgents(config).reviewers, "review.resolvedAgents", errors);
514
540
  }
515
541
  }
516
542
  validateMerge(config, errors, options);
543
+ validateReviewMerge(config, errors);
517
544
  validateAutomation(config, errors);
518
545
  validateClear(config, errors);
519
546
  validateChecks(config, errors);
@@ -531,40 +558,8 @@ export async function validateConfig(config, options = {}) {
531
558
  errors.push("output.repairAttempts must be a non-negative integer");
532
559
  }
533
560
  }
534
- if (config.output?.dirs != null) {
535
- if (!isPlainObject(config.output.dirs)) {
536
- errors.push("output.dirs must be an object");
537
- }
538
- else {
539
- validateKnownKeys(config.output.dirs, "output.dirs", OUTPUT_DIR_KEYS, errors);
540
- const dirs = config.output.dirs;
541
- for (const key of OUTPUT_DIR_KEYS) {
542
- const value = dirs[key];
543
- if (value != null && typeof value !== "string") {
544
- errors.push(`output.dirs.${key} must be a string`);
545
- }
546
- }
547
- }
548
- }
549
- if (config.worktree != null && !isPlainObject(config.worktree)) {
550
- errors.push("worktree must be an object");
551
- }
552
- validateKnownKeys(config.worktree, "worktree", WORKTREE_KEYS, errors);
553
- if (config.worktree?.dirs != null) {
554
- if (!isPlainObject(config.worktree.dirs)) {
555
- errors.push("worktree.dirs must be an object");
556
- }
557
- else {
558
- validateKnownKeys(config.worktree.dirs, "worktree.dirs", WORKTREE_DIR_KEYS, errors);
559
- const dirs = config.worktree.dirs;
560
- for (const key of WORKTREE_DIR_KEYS) {
561
- const value = dirs[key];
562
- if (value != null && typeof value !== "string") {
563
- errors.push(`worktree.dirs.${key} must be a string`);
564
- }
565
- }
566
- }
567
- }
561
+ validateString(config.review?.output, "review.output", errors);
562
+ validateString(config.review?.worktree, "review.worktree", errors);
568
563
  if (options.checkAuth && !errors.length) {
569
564
  if (!options.exec) {
570
565
  errors.push("validateConfig requires exec when checkAuth is true");
@@ -6,7 +6,7 @@ function resolvePath(directory, path) {
6
6
  return isAbsolute(path) ? path : join(directory, path);
7
7
  }
8
8
  export function worktreeBaseDir(directory, config, kind) {
9
- return resolvePath(directory, config.worktree?.dirs?.[kind] ?? DEFAULT_WORKTREE_DIRS[kind]);
9
+ return resolvePath(directory, config.review?.worktree ?? DEFAULT_WORKTREE_DIRS[kind]);
10
10
  }
11
11
  export function worktreeBaseDirs(directory, config = {}) {
12
12
  return [worktreeBaseDir(directory, config, "pr")];
package/dist/index.js CHANGED
@@ -190,7 +190,7 @@ export async function validateMagiConfigFiles(directory, options = {}) {
190
190
  ? withGitHubApiRetry(options.exec, mergedConfig.github?.apiRetryAttempts ?? 3)
191
191
  : undefined,
192
192
  modelCatalog: options.modelCatalog,
193
- requireGithub: hasProjectConfig && Boolean(mergedConfig.agents?.reviewers),
193
+ requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.agents),
194
194
  });
195
195
  loadedFrom = existing.map((status) => status.path).join(", ");
196
196
  errors.push(...validation.errors);
@@ -571,6 +571,7 @@ export async function runMerge(input) {
571
571
  ...abortableInput,
572
572
  allowAlreadyReviewed: true,
573
573
  approvalPolicy: input.repository.merge.approvalPolicy,
574
+ enableReviewAutomation: false,
574
575
  onProgress: (progress) => input.onProgress?.(progress),
575
576
  runId: input.runId,
576
577
  dryRun: input.dryRun,
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, } from "../github/commands";
3
+ import { createWorktree, fetchPullRequest, fetchPullRequestCommits, fetchPullRequestReviews, fetchUnresolvedThreads, closePullRequest, mergePullRequest, postApproval, postChangesRequested, postCloseComment, postReply, removeWorktree, resolveThread, } from "../github/commands";
4
4
  import { composeFindingValidationPrompt, composeCloseReconsiderationPrompt, composeRereviewPrompt, composeReviewPrompt, } from "../prompts/compose";
5
5
  import { prRunOutputDir } from "../config/output";
6
6
  import { worktreeBaseDir } from "../config/worktree";
@@ -112,6 +112,9 @@ function reviewStateToVerdict(state) {
112
112
  return "CHANGES_REQUESTED";
113
113
  return "CLOSE";
114
114
  }
115
+ function hasBlockingCiReports(reports) {
116
+ return reports.some((report) => report.scopeInside.length || report.scopeOutsideUnresolved.length);
117
+ }
115
118
  function previousReviewText(review) {
116
119
  return JSON.stringify({
117
120
  body: review.body ?? "",
@@ -753,6 +756,30 @@ export async function runReview(input) {
753
756
  : await postReviewOutput({ ...input, exec }, key, output),
754
757
  ]))),
755
758
  };
759
+ const automationAccount = input.repository.agents.reviewers[0]?.account;
760
+ const enableReviewAutomation = input.enableReviewAutomation ?? true;
761
+ if (enableReviewAutomation &&
762
+ verdict === "MERGE" &&
763
+ input.repository.reviewAutomation?.merge) {
764
+ await input.onProgress?.({ phase: "merging PR", type: "phase" });
765
+ posted.automation = hasBlockingCiReports(ciReports)
766
+ ? "skipped: unresolved CI"
767
+ : input.dryRun
768
+ ? "dry-run:would-merge"
769
+ : automationAccount
770
+ ? await mergePullRequest(input.exec, input.repository, input.pr, automationAccount)
771
+ : "skipped: no review automation account";
772
+ }
773
+ if (enableReviewAutomation &&
774
+ verdict === "CLOSE" &&
775
+ input.repository.reviewAutomation?.close) {
776
+ await input.onProgress?.({ phase: "closing PR", type: "phase" });
777
+ posted.automation = input.dryRun
778
+ ? "dry-run:would-close"
779
+ : automationAccount
780
+ ? await closePullRequest(input.exec, input.repository, input.pr, automationAccount)
781
+ : "skipped: no review automation account";
782
+ }
756
783
  await writeFile(join(outputDir, "majority.json"), JSON.stringify({
757
784
  approvalPolicy: input.approvalPolicy ?? "majority",
758
785
  verdict,
@@ -34,7 +34,7 @@ function repositoryValues(repository) {
34
34
  };
35
35
  }
36
36
  function reviewValues(input) {
37
- const ciFailureContext = input.ciFailureContext?.trim() ?? input.ciFailureLogs?.trim() ?? "";
37
+ const ciFailureContext = input.ciFailureContext?.trim() ?? "";
38
38
  return {
39
39
  ...repositoryValues(input.repository),
40
40
  baseSha: input.baseSha,
@@ -42,10 +42,6 @@ function reviewValues(input) {
42
42
  ciFailureContextBlock: ciFailureContext
43
43
  ? `<ci_failure_context>\n${ciFailureContext}\n</ci_failure_context>`
44
44
  : "",
45
- ciFailureLogs: ciFailureContext,
46
- ciFailureLogsBlock: ciFailureContext
47
- ? `<ci_failure_context>\n${ciFailureContext}\n</ci_failure_context>`
48
- : "",
49
45
  headSha: input.headSha,
50
46
  jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
51
47
  pr: String(input.pr),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260519013159",
3
+ "version": "0.0.0-dev-20260519070815",
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
@@ -5,13 +5,11 @@
5
5
  "additionalProperties": false,
6
6
  "properties": {
7
7
  "$schema": { "type": "string" },
8
- "agents": { "$ref": "#/$defs/agents" },
9
- "automation": {
8
+ "agents": {
10
9
  "type": "object",
11
10
  "additionalProperties": false,
12
11
  "properties": {
13
- "merge": { "type": "boolean", "default": true },
14
- "close": { "type": "boolean", "default": true }
12
+ "permissions": { "$ref": "#/$defs/permissions" }
15
13
  }
16
14
  },
17
15
  "clear": {
@@ -24,24 +22,6 @@
24
22
  "branch": { "type": "boolean", "default": true }
25
23
  }
26
24
  },
27
- "checks": {
28
- "type": "object",
29
- "additionalProperties": false,
30
- "properties": {
31
- "exclude": { "type": "array", "items": { "type": "string" } },
32
- "waitAfterEdit": { "type": "boolean" },
33
- "waitBeforeReview": { "type": "boolean" },
34
- "retryFailedJobs": { "type": "integer", "minimum": 0, "default": 3 }
35
- }
36
- },
37
- "concurrency": {
38
- "type": "object",
39
- "additionalProperties": false,
40
- "properties": {
41
- "runs": { "type": "integer", "minimum": 1, "default": 3 },
42
- "reviewers": { "type": "integer", "minimum": 1, "default": 3 }
43
- }
44
- },
45
25
  "github": {
46
26
  "type": "object",
47
27
  "required": ["owner", "repo"],
@@ -59,64 +39,15 @@
59
39
  }
60
40
  },
61
41
  "language": { "type": "string" },
62
- "merge": {
63
- "type": "object",
64
- "additionalProperties": false,
65
- "properties": {
66
- "approvalPolicy": {
67
- "enum": ["majority", "unanimous"],
68
- "default": "majority"
69
- },
70
- "method": { "enum": ["merge", "squash", "rebase"] },
71
- "auto": { "type": "boolean" },
72
- "deleteBranch": { "type": "boolean" },
73
- "mergeQueue": { "type": "boolean", "default": false },
74
- "maxThreadResolutionCycles": {
75
- "type": "integer",
76
- "minimum": 0,
77
- "default": 5,
78
- "description": "Maximum resolution attempts per unresolved review thread. Set 0 for unlimited attempts."
79
- }
80
- }
81
- },
42
+ "merge": { "$ref": "#/$defs/merge" },
82
43
  "output": {
83
44
  "type": "object",
84
45
  "additionalProperties": false,
85
46
  "properties": {
86
- "dirs": {
87
- "type": "object",
88
- "additionalProperties": false,
89
- "properties": {
90
- "pr": { "type": "string" }
91
- }
92
- },
93
47
  "repairAttempts": { "type": "integer", "minimum": 0, "default": 3 }
94
48
  }
95
49
  },
96
- "prompts": { "$ref": "#/$defs/prompts" },
97
- "safety": {
98
- "type": "object",
99
- "additionalProperties": false,
100
- "properties": {
101
- "allowAuthors": { "type": "array", "items": { "type": "string" } },
102
- "blockedPaths": { "type": "array", "items": { "type": "string" } },
103
- "maxChangedFiles": { "type": "integer", "minimum": 0 },
104
- "requiredLabels": { "type": "array", "items": { "type": "string" } }
105
- }
106
- },
107
- "worktree": {
108
- "type": "object",
109
- "additionalProperties": false,
110
- "properties": {
111
- "dirs": {
112
- "type": "object",
113
- "additionalProperties": false,
114
- "properties": {
115
- "pr": { "type": "string" }
116
- }
117
- }
118
- }
119
- }
50
+ "review": { "$ref": "#/$defs/review" }
120
51
  },
121
52
  "$defs": {
122
53
  "reviewer": {
@@ -128,7 +59,7 @@
128
59
  "model": { "type": "string", "minLength": 1 },
129
60
  "options": { "type": "object", "additionalProperties": true },
130
61
  "account": { "type": "string", "minLength": 1 },
131
- "permission": { "$ref": "#/$defs/permission" },
62
+ "permissions": { "$ref": "#/$defs/permissions" },
132
63
  "persona": { "type": "string" }
133
64
  }
134
65
  },
@@ -149,21 +80,120 @@
149
80
  "email": { "type": "string", "minLength": 1 }
150
81
  }
151
82
  },
152
- "permission": { "$ref": "#/$defs/permission" },
83
+ "permissions": { "$ref": "#/$defs/permissions" },
153
84
  "persona": { "type": "string" }
154
85
  }
155
86
  },
156
- "agents": {
87
+ "automation": {
88
+ "type": "object",
89
+ "additionalProperties": false,
90
+ "properties": {
91
+ "merge": { "type": "boolean" },
92
+ "close": { "type": "boolean" }
93
+ }
94
+ },
95
+ "reviewChecks": {
96
+ "type": "object",
97
+ "additionalProperties": false,
98
+ "properties": {
99
+ "exclude": { "type": "array", "items": { "type": "string" } },
100
+ "wait": { "type": "boolean", "default": true },
101
+ "retryFailedJobs": { "type": "integer", "minimum": 0, "default": 3 }
102
+ }
103
+ },
104
+ "mergeChecks": {
157
105
  "type": "object",
158
106
  "additionalProperties": false,
159
107
  "properties": {
160
- "permissions": { "$ref": "#/$defs/permission" },
161
- "reviewers": {
108
+ "wait": { "type": "boolean", "default": true }
109
+ }
110
+ },
111
+ "concurrency": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "runs": { "type": "integer", "minimum": 1, "default": 3 },
116
+ "reviewers": { "type": "integer", "minimum": 1, "default": 3 }
117
+ }
118
+ },
119
+ "safety": {
120
+ "type": "object",
121
+ "additionalProperties": false,
122
+ "properties": {
123
+ "allowAuthors": { "type": "array", "items": { "type": "string" } },
124
+ "blockedPaths": { "type": "array", "items": { "type": "string" } },
125
+ "maxChangedFiles": { "type": "integer", "minimum": 0 },
126
+ "requiredLabels": { "type": "array", "items": { "type": "string" } }
127
+ }
128
+ },
129
+ "reviewPrompts": {
130
+ "type": "object",
131
+ "additionalProperties": false,
132
+ "properties": {
133
+ "review": { "type": "string" },
134
+ "rereview": { "type": "string" },
135
+ "reviewGuidelines": { "type": "string" },
136
+ "ciClassification": { "type": "string" },
137
+ "findingValidation": { "type": "string" },
138
+ "closeReconsideration": { "type": "string" }
139
+ }
140
+ },
141
+ "mergePrompts": {
142
+ "type": "object",
143
+ "additionalProperties": false,
144
+ "properties": {
145
+ "edit": { "type": "string" },
146
+ "editGuidelines": { "type": "string" },
147
+ "ciClassification": { "type": "string" }
148
+ }
149
+ },
150
+ "reviewMerge": {
151
+ "type": "object",
152
+ "additionalProperties": false,
153
+ "properties": {
154
+ "approvalPolicy": {
155
+ "enum": ["majority", "unanimous"],
156
+ "default": "majority"
157
+ },
158
+ "method": { "enum": ["merge", "squash", "rebase"] },
159
+ "auto": { "type": "boolean" },
160
+ "deleteBranch": { "type": "boolean" },
161
+ "queue": { "type": "boolean", "default": false }
162
+ }
163
+ },
164
+ "review": {
165
+ "type": "object",
166
+ "additionalProperties": false,
167
+ "properties": {
168
+ "agents": {
162
169
  "type": "array",
163
170
  "minItems": 3,
164
171
  "items": { "$ref": "#/$defs/reviewer" }
165
172
  },
166
- "editor": { "$ref": "#/$defs/editor" }
173
+ "prompts": { "$ref": "#/$defs/reviewPrompts" },
174
+ "checks": { "$ref": "#/$defs/reviewChecks" },
175
+ "safety": { "$ref": "#/$defs/safety" },
176
+ "automation": { "$ref": "#/$defs/automation" },
177
+ "concurrency": { "$ref": "#/$defs/concurrency" },
178
+ "merge": { "$ref": "#/$defs/reviewMerge" },
179
+ "output": { "type": "string" },
180
+ "worktree": { "type": "string" }
181
+ }
182
+ },
183
+ "merge": {
184
+ "type": "object",
185
+ "additionalProperties": false,
186
+ "properties": {
187
+ "editor": { "$ref": "#/$defs/editor" },
188
+ "checks": { "$ref": "#/$defs/mergeChecks" },
189
+ "prompts": { "$ref": "#/$defs/mergePrompts" },
190
+ "automation": { "$ref": "#/$defs/automation" },
191
+ "maxThreadResolutionCycles": {
192
+ "type": "integer",
193
+ "minimum": 0,
194
+ "default": 5,
195
+ "description": "Maximum resolution attempts per unresolved review thread. Set 0 for unlimited attempts."
196
+ }
167
197
  }
168
198
  },
169
199
  "permissionAction": { "enum": ["allow", "ask", "deny"] },
@@ -176,7 +206,7 @@
176
206
  }
177
207
  ]
178
208
  },
179
- "permission": {
209
+ "permissions": {
180
210
  "oneOf": [
181
211
  { "$ref": "#/$defs/permissionAction" },
182
212
  {
@@ -184,23 +214,6 @@
184
214
  "additionalProperties": { "$ref": "#/$defs/permissionRule" }
185
215
  }
186
216
  ]
187
- },
188
- "prompts": {
189
- "type": "object",
190
- "additionalProperties": false,
191
- "properties": {
192
- "review": { "type": "string" },
193
- "rereview": { "type": "string" },
194
- "edit": { "type": "string" },
195
- "editGuidelines": { "type": "string" },
196
- "findingValidation": { "type": "string" },
197
- "closeReconsideration": { "type": "string" },
198
- "rereviewCloseReconsideration": { "type": "string" },
199
- "ciClassification": { "type": "string" },
200
- "ciClassificationAfterEdit": { "type": "string" },
201
- "report": { "type": "string" },
202
- "reviewGuidelines": { "type": "string" }
203
- }
204
217
  }
205
218
  }
206
219
  }