opencode-magi 0.1.0 → 0.3.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.
- package/README.md +33 -10
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +124 -26
- package/dist/config/validate.js +486 -191
- package/dist/config/worktree.js +19 -0
- package/dist/github/commands.js +349 -17
- package/dist/index.js +257 -27
- package/dist/orchestrator/ci.js +1 -1
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/inline-comments.js +73 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +24 -4
- package/dist/orchestrator/report.js +15 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +78 -10
- package/dist/orchestrator/run-manager.js +418 -20
- package/dist/orchestrator/triage.js +1119 -0
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +172 -15
- package/dist/prompts/contracts.js +119 -12
- package/dist/prompts/output.js +149 -14
- package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
- package/dist/prompts/templates/review/review.md +13 -0
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/action.md +5 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/dist/prompts/templates/triage/comment-classification.md +7 -0
- package/dist/prompts/templates/triage/comment.md +5 -0
- package/dist/prompts/templates/triage/create.md +7 -0
- package/dist/prompts/templates/triage/duplicate.md +7 -0
- package/dist/prompts/templates/triage/existing-pr.md +7 -0
- package/dist/prompts/templates/triage/question.md +5 -0
- package/dist/prompts/templates/triage/reconsider.md +5 -0
- package/package.json +28 -27
- package/schema.json +234 -90
- package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
- package/dist/prompts/templates/review.md +0 -7
- /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
- /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
- /package/dist/prompts/templates/{rereview.md → review/rereview.md} +0 -0
package/dist/config/validate.js
CHANGED
|
@@ -9,28 +9,26 @@ 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",
|
|
15
|
-
"automation",
|
|
16
17
|
"clear",
|
|
17
|
-
"checks",
|
|
18
|
-
"concurrency",
|
|
19
18
|
"github",
|
|
20
19
|
"language",
|
|
21
20
|
"merge",
|
|
22
21
|
"output",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"worktree",
|
|
22
|
+
"review",
|
|
23
|
+
"triage",
|
|
26
24
|
]);
|
|
27
|
-
const AGENTS_KEYS = new Set(["
|
|
25
|
+
const AGENTS_KEYS = new Set(["permissions"]);
|
|
28
26
|
const REVIEWER_KEYS = new Set([
|
|
29
27
|
"account",
|
|
30
28
|
"id",
|
|
31
29
|
"model",
|
|
32
30
|
"options",
|
|
33
|
-
"
|
|
31
|
+
"permissions",
|
|
34
32
|
"persona",
|
|
35
33
|
]);
|
|
36
34
|
const EDITOR_KEYS = new Set([
|
|
@@ -38,50 +36,117 @@ const EDITOR_KEYS = new Set([
|
|
|
38
36
|
"author",
|
|
39
37
|
"model",
|
|
40
38
|
"options",
|
|
41
|
-
"
|
|
39
|
+
"permissions",
|
|
40
|
+
"persona",
|
|
41
|
+
]);
|
|
42
|
+
const TRIAGE_AGENT_KEYS = new Set([
|
|
43
|
+
"id",
|
|
44
|
+
"model",
|
|
45
|
+
"options",
|
|
46
|
+
"permissions",
|
|
47
|
+
"persona",
|
|
48
|
+
]);
|
|
49
|
+
const TRIAGE_CREATOR_KEYS = new Set([
|
|
50
|
+
"account",
|
|
51
|
+
"author",
|
|
52
|
+
"model",
|
|
53
|
+
"options",
|
|
54
|
+
"permissions",
|
|
42
55
|
"persona",
|
|
43
56
|
]);
|
|
44
57
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
45
58
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
59
|
+
const REVIEW_KEYS = new Set([
|
|
60
|
+
"agents",
|
|
61
|
+
"automation",
|
|
62
|
+
"checks",
|
|
63
|
+
"concurrency",
|
|
64
|
+
"merge",
|
|
65
|
+
"output",
|
|
66
|
+
"prompts",
|
|
67
|
+
"safety",
|
|
68
|
+
"worktree",
|
|
69
|
+
]);
|
|
46
70
|
const MERGE_KEYS = new Set([
|
|
71
|
+
"automation",
|
|
72
|
+
"checks",
|
|
73
|
+
"editor",
|
|
74
|
+
"maxThreadResolutionCycles",
|
|
75
|
+
"prompts",
|
|
76
|
+
]);
|
|
77
|
+
const TRIAGE_KEYS = new Set([
|
|
78
|
+
"account",
|
|
79
|
+
"agents",
|
|
80
|
+
"automation",
|
|
81
|
+
"categories",
|
|
82
|
+
"concurrency",
|
|
83
|
+
"creator",
|
|
84
|
+
"output",
|
|
85
|
+
"prompts",
|
|
86
|
+
"safety",
|
|
87
|
+
"worktree",
|
|
88
|
+
]);
|
|
89
|
+
const REVIEW_MERGE_KEYS = new Set([
|
|
47
90
|
"approvalPolicy",
|
|
48
91
|
"auto",
|
|
49
92
|
"deleteBranch",
|
|
50
|
-
"maxThreadResolutionCycles",
|
|
51
|
-
"mergeQueue",
|
|
52
93
|
"method",
|
|
94
|
+
"queue",
|
|
53
95
|
]);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
"retryFailedJobs",
|
|
57
|
-
"waitAfterEdit",
|
|
58
|
-
"waitBeforeReview",
|
|
59
|
-
]);
|
|
96
|
+
const REVIEW_CHECKS_KEYS = new Set(["exclude", "retryFailedJobs", "wait"]);
|
|
97
|
+
const MERGE_CHECKS_KEYS = new Set(["wait"]);
|
|
60
98
|
const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
61
99
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
62
100
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
63
|
-
const OUTPUT_KEYS = new Set(["
|
|
64
|
-
const
|
|
65
|
-
|
|
101
|
+
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
102
|
+
const TRIAGE_AUTOMATION_KEYS = new Set([
|
|
103
|
+
"clear",
|
|
104
|
+
"close",
|
|
105
|
+
"create",
|
|
106
|
+
"merge",
|
|
107
|
+
"review",
|
|
108
|
+
]);
|
|
109
|
+
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
110
|
+
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
111
|
+
const TRIAGE_SAFETY_KEYS = new Set([
|
|
112
|
+
"allowAuthors",
|
|
113
|
+
"allowMentionActors",
|
|
114
|
+
"allowMentionRoles",
|
|
115
|
+
"blockedLabels",
|
|
116
|
+
"requiredLabels",
|
|
117
|
+
]);
|
|
66
118
|
const SAFETY_KEYS = new Set([
|
|
67
119
|
"allowAuthors",
|
|
68
120
|
"blockedPaths",
|
|
69
121
|
"maxChangedFiles",
|
|
70
122
|
"requiredLabels",
|
|
71
123
|
]);
|
|
72
|
-
const
|
|
124
|
+
const REVIEW_PROMPT_KEYS = new Set([
|
|
73
125
|
"ciClassification",
|
|
74
|
-
"ciClassificationAfterEdit",
|
|
75
126
|
"closeReconsideration",
|
|
76
|
-
"edit",
|
|
77
|
-
"editGuidelines",
|
|
78
127
|
"findingValidation",
|
|
79
|
-
"report",
|
|
80
128
|
"rereview",
|
|
81
|
-
"rereviewCloseReconsideration",
|
|
82
129
|
"review",
|
|
83
130
|
"reviewGuidelines",
|
|
84
131
|
]);
|
|
132
|
+
const MERGE_PROMPT_KEYS = new Set([
|
|
133
|
+
"ciClassification",
|
|
134
|
+
"edit",
|
|
135
|
+
"editGuidelines",
|
|
136
|
+
]);
|
|
137
|
+
const TRIAGE_PROMPT_KEYS = new Set([
|
|
138
|
+
"action",
|
|
139
|
+
"acceptance",
|
|
140
|
+
"category",
|
|
141
|
+
"comment",
|
|
142
|
+
"commentClassification",
|
|
143
|
+
"create",
|
|
144
|
+
"createGuidelines",
|
|
145
|
+
"duplicate",
|
|
146
|
+
"existingPr",
|
|
147
|
+
"question",
|
|
148
|
+
"reconsider",
|
|
149
|
+
]);
|
|
85
150
|
function githubHost(config) {
|
|
86
151
|
return config.github?.host ?? "github.com";
|
|
87
152
|
}
|
|
@@ -210,7 +275,7 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
210
275
|
validateString(reviewer.persona, `${path}[${index}].persona`, errors);
|
|
211
276
|
if (reviewer.options != null && !isPlainObject(reviewer.options))
|
|
212
277
|
errors.push(`${path}[${index}].options must be an object`);
|
|
213
|
-
validatePermissionConfig(reviewer.
|
|
278
|
+
validatePermissionConfig(reviewer.permissions, `${path}[${index}].permissions`, errors);
|
|
214
279
|
if (reviewer.id) {
|
|
215
280
|
if (!validateReviewerId(reviewer.id)) {
|
|
216
281
|
errors.push(`${path}[${index}].id may contain only letters, numbers, underscores, and hyphens`);
|
|
@@ -221,6 +286,41 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
221
286
|
}
|
|
222
287
|
});
|
|
223
288
|
}
|
|
289
|
+
function validateTriageAgentList(agents, path, errors, catalog) {
|
|
290
|
+
if (agents == null)
|
|
291
|
+
return;
|
|
292
|
+
if (!Array.isArray(agents)) {
|
|
293
|
+
errors.push(`${path} must be an array`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (agents.length < 3)
|
|
297
|
+
errors.push(`${path} must contain at least 3 agents`);
|
|
298
|
+
if (agents.length % 2 === 0)
|
|
299
|
+
errors.push(`${path} must contain an odd number of agents`);
|
|
300
|
+
agents.forEach((agent, index) => {
|
|
301
|
+
if (!agent || typeof agent !== "object") {
|
|
302
|
+
errors.push(`${path}[${index}] must be an object`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
validateKnownKeys(agent, `${path}[${index}]`, TRIAGE_AGENT_KEYS, errors);
|
|
306
|
+
if (!agent.model)
|
|
307
|
+
errors.push(`${path}[${index}].model is required`);
|
|
308
|
+
validateString(agent.model, `${path}[${index}].model`, errors);
|
|
309
|
+
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
310
|
+
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
311
|
+
if (agent.options != null && !isPlainObject(agent.options))
|
|
312
|
+
errors.push(`${path}[${index}].options must be an object`);
|
|
313
|
+
validatePermissionConfig(agent.permissions, `${path}[${index}].permissions`, errors);
|
|
314
|
+
if (agent.id) {
|
|
315
|
+
if (!validateReviewerId(agent.id)) {
|
|
316
|
+
errors.push(`${path}[${index}].id may contain only letters, numbers, underscores, and hyphens`);
|
|
317
|
+
}
|
|
318
|
+
if (RESERVED_REVIEWER_KEYS.has(agent.id)) {
|
|
319
|
+
errors.push(`${path}[${index}].id is reserved: ${agent.id}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
224
324
|
function validateResolvedReviewers(reviewers, path, errors) {
|
|
225
325
|
const keys = new Set();
|
|
226
326
|
const accounts = new Set();
|
|
@@ -233,7 +333,93 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
233
333
|
accounts.add(reviewer.account);
|
|
234
334
|
}
|
|
235
335
|
}
|
|
336
|
+
function validateResolvedAgentKeys(agents, path, errors) {
|
|
337
|
+
const keys = new Set();
|
|
338
|
+
for (const agent of agents) {
|
|
339
|
+
if (keys.has(agent.key))
|
|
340
|
+
errors.push(`${path} has duplicate agent key: ${agent.key}`);
|
|
341
|
+
keys.add(agent.key);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function validateEditor(editor, path, errors, catalog) {
|
|
345
|
+
if (!editor)
|
|
346
|
+
return;
|
|
347
|
+
if (!isPlainObject(editor)) {
|
|
348
|
+
errors.push(`${path} must be an object`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!editor.model)
|
|
352
|
+
errors.push(`${path}.model is required`);
|
|
353
|
+
validateKnownKeys(editor, path, EDITOR_KEYS, errors);
|
|
354
|
+
validateString(editor.model, `${path}.model`, errors);
|
|
355
|
+
validateString(editor.account, `${path}.account`, errors);
|
|
356
|
+
validateString(editor.persona, `${path}.persona`, errors);
|
|
357
|
+
validateModel(editor.model, `${path}.model`, errors, catalog);
|
|
358
|
+
if (!editor.account)
|
|
359
|
+
errors.push(`${path}.account is required`);
|
|
360
|
+
if (editor.options != null && !isPlainObject(editor.options)) {
|
|
361
|
+
errors.push(`${path}.options must be an object`);
|
|
362
|
+
}
|
|
363
|
+
validatePermissionConfig(editor.permissions, `${path}.permissions`, errors);
|
|
364
|
+
const author = editor.author;
|
|
365
|
+
if (!author || !isPlainObject(author)) {
|
|
366
|
+
if (author != null)
|
|
367
|
+
errors.push(`${path}.author must be an object`);
|
|
368
|
+
errors.push(`${path}.author.name is required`);
|
|
369
|
+
errors.push(`${path}.author.email is required`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
validateKnownKeys(author, `${path}.author`, AUTHOR_KEYS, errors);
|
|
373
|
+
if (!author.name) {
|
|
374
|
+
errors.push(`${path}.author.name is required`);
|
|
375
|
+
}
|
|
376
|
+
else if (typeof author.name !== "string") {
|
|
377
|
+
errors.push(`${path}.author.name must be a string`);
|
|
378
|
+
}
|
|
379
|
+
if (!author.email) {
|
|
380
|
+
errors.push(`${path}.author.email is required`);
|
|
381
|
+
}
|
|
382
|
+
else if (typeof author.email !== "string") {
|
|
383
|
+
errors.push(`${path}.author.email must be a string`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function validateTriageCreator(creator, path, errors, catalog) {
|
|
388
|
+
if (!creator)
|
|
389
|
+
return;
|
|
390
|
+
if (!isPlainObject(creator)) {
|
|
391
|
+
errors.push(`${path} must be an object`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
validateKnownKeys(creator, path, TRIAGE_CREATOR_KEYS, errors);
|
|
395
|
+
if (!creator.model)
|
|
396
|
+
errors.push(`${path}.model is required`);
|
|
397
|
+
validateString(creator.account, `${path}.account`, errors);
|
|
398
|
+
validateString(creator.model, `${path}.model`, errors);
|
|
399
|
+
validateString(creator.persona, `${path}.persona`, errors);
|
|
400
|
+
validateModel(creator.model, `${path}.model`, errors, catalog);
|
|
401
|
+
if (creator.options != null && !isPlainObject(creator.options)) {
|
|
402
|
+
errors.push(`${path}.options must be an object`);
|
|
403
|
+
}
|
|
404
|
+
validatePermissionConfig(creator.permissions, `${path}.permissions`, errors);
|
|
405
|
+
const author = creator.author;
|
|
406
|
+
if (!author || !isPlainObject(author)) {
|
|
407
|
+
if (author != null)
|
|
408
|
+
errors.push(`${path}.author must be an object`);
|
|
409
|
+
errors.push(`${path}.author.name is required`);
|
|
410
|
+
errors.push(`${path}.author.email is required`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
validateKnownKeys(author, `${path}.author`, AUTHOR_KEYS, errors);
|
|
414
|
+
if (!author.name)
|
|
415
|
+
errors.push(`${path}.author.name is required`);
|
|
416
|
+
validateString(author.name, `${path}.author.name`, errors);
|
|
417
|
+
if (!author.email)
|
|
418
|
+
errors.push(`${path}.author.email is required`);
|
|
419
|
+
validateString(author.email, `${path}.author.email`, errors);
|
|
420
|
+
}
|
|
236
421
|
function validateMerge(config, errors, options) {
|
|
422
|
+
const merge = config.merge;
|
|
237
423
|
if (options.requireGithub ?? true) {
|
|
238
424
|
if (!config.github?.owner)
|
|
239
425
|
errors.push("github.owner is required");
|
|
@@ -247,102 +433,102 @@ function validateMerge(config, errors, options) {
|
|
|
247
433
|
if (config.github != null && !isPlainObject(config.github)) {
|
|
248
434
|
errors.push("github must be an object");
|
|
249
435
|
}
|
|
250
|
-
if (config.merge != null && !isPlainObject(config.merge)) {
|
|
251
|
-
errors.push("merge must be an object");
|
|
252
|
-
}
|
|
253
|
-
validateKnownKeys(config.merge, "merge", MERGE_KEYS, errors);
|
|
254
|
-
validateBoolean(config.merge?.auto, "merge.auto", errors);
|
|
255
|
-
validateBoolean(config.merge?.deleteBranch, "merge.deleteBranch", errors);
|
|
256
|
-
validateBoolean(config.merge?.mergeQueue, "merge.mergeQueue", errors);
|
|
257
436
|
if (config.github?.apiRetryAttempts != null &&
|
|
258
437
|
(typeof config.github.apiRetryAttempts !== "number" ||
|
|
259
438
|
!Number.isInteger(config.github.apiRetryAttempts) ||
|
|
260
439
|
config.github.apiRetryAttempts < 0)) {
|
|
261
440
|
errors.push("github.apiRetryAttempts must be a non-negative integer");
|
|
262
441
|
}
|
|
263
|
-
if (
|
|
264
|
-
(
|
|
265
|
-
!["merge", "rebase", "squash"].includes(config.merge.method))) {
|
|
266
|
-
errors.push("merge.method must be merge, squash, or rebase");
|
|
267
|
-
}
|
|
268
|
-
if (config.merge?.approvalPolicy != null &&
|
|
269
|
-
(typeof config.merge.approvalPolicy !== "string" ||
|
|
270
|
-
!["majority", "unanimous"].includes(config.merge.approvalPolicy))) {
|
|
271
|
-
errors.push("merge.approvalPolicy must be majority or unanimous");
|
|
442
|
+
if (merge != null && !isPlainObject(merge)) {
|
|
443
|
+
errors.push("merge must be an object");
|
|
272
444
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
445
|
+
validateKnownKeys(merge, "merge", MERGE_KEYS, errors);
|
|
446
|
+
validateBooleanObject(merge?.automation, "merge.automation", AUTOMATION_KEYS, errors);
|
|
447
|
+
const checks = merge?.checks;
|
|
448
|
+
validateKnownKeys(checks, "merge.checks", MERGE_CHECKS_KEYS, errors);
|
|
449
|
+
validateBoolean(checks?.wait, "merge.checks.wait", errors);
|
|
450
|
+
validateEditor(merge?.editor, "merge.editor", errors, options.modelCatalog);
|
|
451
|
+
if (merge?.maxThreadResolutionCycles != null &&
|
|
452
|
+
(typeof merge.maxThreadResolutionCycles !== "number" ||
|
|
453
|
+
!Number.isInteger(merge.maxThreadResolutionCycles) ||
|
|
454
|
+
merge.maxThreadResolutionCycles < 0)) {
|
|
277
455
|
errors.push("merge.maxThreadResolutionCycles must be a non-negative integer");
|
|
278
456
|
}
|
|
457
|
+
if (options.requireEditor && !merge?.editor)
|
|
458
|
+
errors.push("merge.editor is required");
|
|
279
459
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
460
|
+
function validateReviewMerge(config, errors) {
|
|
461
|
+
const merge = config.review?.merge;
|
|
462
|
+
if (merge != null && !isPlainObject(merge)) {
|
|
463
|
+
errors.push("review.merge must be an object");
|
|
464
|
+
}
|
|
465
|
+
validateKnownKeys(merge, "review.merge", REVIEW_MERGE_KEYS, errors);
|
|
466
|
+
validateBoolean(merge?.auto, "review.merge.auto", errors);
|
|
467
|
+
validateBoolean(merge?.deleteBranch, "review.merge.deleteBranch", errors);
|
|
468
|
+
validateBoolean(merge?.queue, "review.merge.queue", errors);
|
|
469
|
+
if (merge?.method != null &&
|
|
470
|
+
(typeof merge.method !== "string" ||
|
|
471
|
+
!["merge", "rebase", "squash"].includes(merge.method))) {
|
|
472
|
+
errors.push("review.merge.method must be merge, squash, or rebase");
|
|
473
|
+
}
|
|
474
|
+
if (merge?.approvalPolicy != null &&
|
|
475
|
+
(typeof merge.approvalPolicy !== "string" ||
|
|
476
|
+
!["majority", "unanimous"].includes(merge.approvalPolicy))) {
|
|
477
|
+
errors.push("review.merge.approvalPolicy must be majority or unanimous");
|
|
283
478
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
479
|
+
}
|
|
480
|
+
function validateConcurrency(config, errors) {
|
|
481
|
+
const concurrency = config.review?.concurrency;
|
|
482
|
+
if (concurrency != null && !isPlainObject(concurrency)) {
|
|
483
|
+
errors.push("review.concurrency must be an object");
|
|
484
|
+
}
|
|
485
|
+
validateKnownKeys(concurrency, "review.concurrency", CONCURRENCY_KEYS, errors);
|
|
486
|
+
if (concurrency?.runs != null) {
|
|
487
|
+
if (typeof concurrency.runs !== "number" ||
|
|
488
|
+
!Number.isInteger(concurrency.runs) ||
|
|
489
|
+
concurrency.runs < 1) {
|
|
490
|
+
errors.push("review.concurrency.runs must be a positive integer");
|
|
290
491
|
}
|
|
291
492
|
}
|
|
292
|
-
if (
|
|
293
|
-
if (typeof
|
|
294
|
-
!Number.isInteger(
|
|
295
|
-
|
|
296
|
-
errors.push("concurrency.reviewers must be a positive integer");
|
|
493
|
+
if (concurrency?.reviewers != null) {
|
|
494
|
+
if (typeof concurrency.reviewers !== "number" ||
|
|
495
|
+
!Number.isInteger(concurrency.reviewers) ||
|
|
496
|
+
concurrency.reviewers < 1) {
|
|
497
|
+
errors.push("review.concurrency.reviewers must be a positive integer");
|
|
297
498
|
}
|
|
298
499
|
}
|
|
299
500
|
}
|
|
300
501
|
function validateAutomation(config, errors) {
|
|
301
|
-
|
|
302
|
-
errors.push("automation must be an object");
|
|
303
|
-
}
|
|
304
|
-
validateKnownKeys(config.automation, "automation", AUTOMATION_KEYS, errors);
|
|
305
|
-
if (config.automation?.merge != null &&
|
|
306
|
-
typeof config.automation.merge !== "boolean") {
|
|
307
|
-
errors.push("automation.merge must be a boolean");
|
|
308
|
-
}
|
|
309
|
-
if (config.automation?.close != null &&
|
|
310
|
-
typeof config.automation.close !== "boolean") {
|
|
311
|
-
errors.push("automation.close must be a boolean");
|
|
312
|
-
}
|
|
502
|
+
validateBooleanObject(config.review?.automation, "review.automation", AUTOMATION_KEYS, errors);
|
|
313
503
|
}
|
|
314
504
|
function validateClear(config, errors) {
|
|
315
505
|
validateBooleanObject(config.clear, "clear", CLEAR_KEYS, errors);
|
|
316
506
|
}
|
|
317
507
|
function validateChecks(config, errors) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
508
|
+
const checks = config.review?.checks;
|
|
509
|
+
if (checks != null && !isPlainObject(checks)) {
|
|
510
|
+
errors.push("review.checks must be an object");
|
|
511
|
+
}
|
|
512
|
+
validateKnownKeys(checks, "review.checks", REVIEW_CHECKS_KEYS, errors);
|
|
513
|
+
if (checks?.exclude != null) {
|
|
514
|
+
if (!Array.isArray(checks.exclude)) {
|
|
515
|
+
errors.push("review.checks.exclude must be an array");
|
|
325
516
|
}
|
|
326
517
|
else {
|
|
327
|
-
|
|
518
|
+
checks.exclude.forEach((item, index) => {
|
|
328
519
|
if (typeof item !== "string")
|
|
329
|
-
errors.push(`checks.exclude[${index}] must be a string`);
|
|
520
|
+
errors.push(`review.checks.exclude[${index}] must be a string`);
|
|
330
521
|
});
|
|
331
522
|
}
|
|
332
523
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
errors.push("checks.waitBeforeReview must be a boolean");
|
|
524
|
+
if (checks?.wait != null && typeof checks.wait !== "boolean") {
|
|
525
|
+
errors.push("review.checks.wait must be a boolean");
|
|
336
526
|
}
|
|
337
|
-
if (
|
|
338
|
-
typeof
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
(typeof config.checks.retryFailedJobs !== "number" ||
|
|
343
|
-
!Number.isInteger(config.checks.retryFailedJobs) ||
|
|
344
|
-
config.checks.retryFailedJobs < 0)) {
|
|
345
|
-
errors.push("checks.retryFailedJobs must be a non-negative integer");
|
|
527
|
+
if (checks?.retryFailedJobs != null &&
|
|
528
|
+
(typeof checks.retryFailedJobs !== "number" ||
|
|
529
|
+
!Number.isInteger(checks.retryFailedJobs) ||
|
|
530
|
+
checks.retryFailedJobs < 0)) {
|
|
531
|
+
errors.push("review.checks.retryFailedJobs must be a non-negative integer");
|
|
346
532
|
}
|
|
347
533
|
}
|
|
348
534
|
function validateStringArray(value, path, errors) {
|
|
@@ -357,34 +543,142 @@ function validateStringArray(value, path, errors) {
|
|
|
357
543
|
errors.push(`${path}[${index}] must be a string`);
|
|
358
544
|
});
|
|
359
545
|
}
|
|
546
|
+
function validateTriageCategories(categories, path, errors) {
|
|
547
|
+
if (categories == null)
|
|
548
|
+
return;
|
|
549
|
+
if (!Array.isArray(categories)) {
|
|
550
|
+
errors.push(`${path} must be an array`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const ids = new Set();
|
|
554
|
+
categories.forEach((item, index) => {
|
|
555
|
+
const itemPath = `${path}[${index}]`;
|
|
556
|
+
if (!isPlainObject(item)) {
|
|
557
|
+
errors.push(`${itemPath} must be an object`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const category = item;
|
|
561
|
+
validateKnownKeys(category, itemPath, TRIAGE_CATEGORY_KEYS, errors);
|
|
562
|
+
if (!category.id) {
|
|
563
|
+
errors.push(`${itemPath}.id is required`);
|
|
564
|
+
}
|
|
565
|
+
else if (typeof category.id !== "string") {
|
|
566
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
567
|
+
}
|
|
568
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(category.id)) {
|
|
569
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
570
|
+
}
|
|
571
|
+
else if (RESERVED_TRIAGE_CATEGORY_IDS.has(category.id)) {
|
|
572
|
+
errors.push(`${itemPath}.id is reserved: ${category.id}`);
|
|
573
|
+
}
|
|
574
|
+
else if (ids.has(category.id)) {
|
|
575
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
ids.add(category.id);
|
|
579
|
+
}
|
|
580
|
+
validateStringArray(category.labels, `${itemPath}.labels`, errors);
|
|
581
|
+
validateStringArray(category.types, `${itemPath}.types`, errors);
|
|
582
|
+
validateString(category.description, `${itemPath}.description`, errors);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
360
585
|
function validateSafety(config, errors) {
|
|
361
|
-
|
|
362
|
-
|
|
586
|
+
const safety = config.review?.safety;
|
|
587
|
+
if (safety != null && !isPlainObject(safety)) {
|
|
588
|
+
errors.push("review.safety must be an object");
|
|
589
|
+
}
|
|
590
|
+
validateKnownKeys(safety, "review.safety", SAFETY_KEYS, errors);
|
|
591
|
+
validateStringArray(safety?.allowAuthors, "review.safety.allowAuthors", errors);
|
|
592
|
+
validateStringArray(safety?.blockedPaths, "review.safety.blockedPaths", errors);
|
|
593
|
+
validateStringArray(safety?.requiredLabels, "review.safety.requiredLabels", errors);
|
|
594
|
+
if (safety?.maxChangedFiles != null &&
|
|
595
|
+
(typeof safety.maxChangedFiles !== "number" ||
|
|
596
|
+
!Number.isInteger(safety.maxChangedFiles) ||
|
|
597
|
+
safety.maxChangedFiles < 0)) {
|
|
598
|
+
errors.push("review.safety.maxChangedFiles must be a non-negative integer");
|
|
363
599
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
600
|
+
}
|
|
601
|
+
function validatePromptObject(prompts, path, keys, errors) {
|
|
602
|
+
if (prompts == null)
|
|
603
|
+
return;
|
|
604
|
+
if (!isPlainObject(prompts)) {
|
|
605
|
+
errors.push(`${path} must be an object`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
validateKnownKeys(prompts, path, keys, errors);
|
|
609
|
+
for (const [key, value] of Object.entries(prompts)) {
|
|
610
|
+
if (typeof value !== "string")
|
|
611
|
+
errors.push(`${path}.${key} must be a string`);
|
|
373
612
|
}
|
|
374
613
|
}
|
|
375
|
-
|
|
376
|
-
|
|
614
|
+
function validateTriage(config, errors, options) {
|
|
615
|
+
const triage = config.triage;
|
|
616
|
+
if (!triage)
|
|
377
617
|
return;
|
|
378
|
-
if (!isPlainObject(
|
|
379
|
-
errors.push("
|
|
618
|
+
if (!isPlainObject(triage)) {
|
|
619
|
+
errors.push("triage must be an object");
|
|
380
620
|
return;
|
|
381
621
|
}
|
|
382
|
-
validateKnownKeys(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
622
|
+
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
623
|
+
const automation = triage.automation;
|
|
624
|
+
const concurrency = triage.concurrency;
|
|
625
|
+
const safety = triage.safety;
|
|
626
|
+
if (!triage.account)
|
|
627
|
+
errors.push("triage.account is required");
|
|
628
|
+
validateString(triage.account, "triage.account", errors);
|
|
629
|
+
if (!triage.agents)
|
|
630
|
+
errors.push("triage.agents is required");
|
|
631
|
+
validateTriageAgentList(triage.agents, "triage.agents", errors, options.modelCatalog);
|
|
632
|
+
if (Array.isArray(triage.agents)) {
|
|
633
|
+
validateResolvedAgentKeys(resolveAgents(config).triage ?? [], "triage.resolvedAgents", errors);
|
|
634
|
+
}
|
|
635
|
+
validateTriageCreator(triage.creator, "triage.creator", errors, options.modelCatalog);
|
|
636
|
+
if (automation?.create && !triage.creator)
|
|
637
|
+
errors.push("triage.creator is required when triage.automation.create is true");
|
|
638
|
+
if (automation != null && !isPlainObject(automation)) {
|
|
639
|
+
errors.push("triage.automation must be an object");
|
|
640
|
+
}
|
|
641
|
+
validateKnownKeys(automation, "triage.automation", TRIAGE_AUTOMATION_KEYS, errors);
|
|
642
|
+
validateBoolean(automation?.close, "triage.automation.close", errors);
|
|
643
|
+
validateBoolean(automation?.create, "triage.automation.create", errors);
|
|
644
|
+
validateBoolean(automation?.merge, "triage.automation.merge", errors);
|
|
645
|
+
validateBoolean(automation?.review, "triage.automation.review", errors);
|
|
646
|
+
validateStringArray(automation?.clear, "triage.automation.clear", errors);
|
|
647
|
+
if (automation?.review && !automation.create) {
|
|
648
|
+
errors.push("triage.automation.review requires triage.automation.create to be true");
|
|
649
|
+
}
|
|
650
|
+
if (automation?.merge && !automation.create) {
|
|
651
|
+
errors.push("triage.automation.merge requires triage.automation.create to be true");
|
|
652
|
+
}
|
|
653
|
+
validateKnownKeys(concurrency, "triage.concurrency", TRIAGE_CONCURRENCY_KEYS, errors);
|
|
654
|
+
if (concurrency?.runs != null &&
|
|
655
|
+
(typeof concurrency.runs !== "number" ||
|
|
656
|
+
!Number.isInteger(concurrency.runs) ||
|
|
657
|
+
concurrency.runs < 1)) {
|
|
658
|
+
errors.push("triage.concurrency.runs must be a positive integer");
|
|
659
|
+
}
|
|
660
|
+
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
661
|
+
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
662
|
+
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
663
|
+
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
|
664
|
+
validateStringArray(safety?.allowMentionRoles, "triage.safety.allowMentionRoles", errors);
|
|
665
|
+
validateStringArray(safety?.blockedLabels, "triage.safety.blockedLabels", errors);
|
|
666
|
+
validateStringArray(safety?.requiredLabels, "triage.safety.requiredLabels", errors);
|
|
667
|
+
validateString(triage.output, "triage.output", errors);
|
|
668
|
+
validateString(triage.worktree, "triage.worktree", errors);
|
|
669
|
+
}
|
|
670
|
+
async function validatePrompts(config, errors, directory) {
|
|
671
|
+
validatePromptObject(config.review?.prompts, "review.prompts", REVIEW_PROMPT_KEYS, errors);
|
|
672
|
+
validatePromptObject(config.merge?.prompts, "merge.prompts", MERGE_PROMPT_KEYS, errors);
|
|
673
|
+
validatePromptObject(config.triage?.prompts, "triage.prompts", TRIAGE_PROMPT_KEYS, errors);
|
|
674
|
+
const promptEntries = [
|
|
675
|
+
...Object.entries(config.review?.prompts ?? {}).map(([key, value]) => [`review.prompts.${key}`, value]),
|
|
676
|
+
...Object.entries(config.merge?.prompts ?? {}).map(([key, value]) => [`merge.prompts.${key}`, value]),
|
|
677
|
+
...Object.entries(config.triage?.prompts ?? {}).map(([key, value]) => [`triage.prompts.${key}`, value]),
|
|
678
|
+
];
|
|
679
|
+
await Promise.all(promptEntries.map(async ([path, value]) => {
|
|
680
|
+
if (typeof value !== "string")
|
|
386
681
|
return;
|
|
387
|
-
}
|
|
388
682
|
if (!directory)
|
|
389
683
|
return;
|
|
390
684
|
const fullPath = promptPath(directory, value);
|
|
@@ -392,17 +686,21 @@ async function validatePrompts(config, errors, directory) {
|
|
|
392
686
|
await access(fullPath, constants.R_OK);
|
|
393
687
|
}
|
|
394
688
|
catch {
|
|
395
|
-
errors.push(
|
|
689
|
+
errors.push(`${path} file is not readable: ${value}`);
|
|
396
690
|
}
|
|
397
691
|
}));
|
|
398
692
|
}
|
|
399
693
|
async function validateAuth(config, exec, errors) {
|
|
400
694
|
const accounts = new Set();
|
|
401
|
-
const agents = resolveAgents(config
|
|
695
|
+
const agents = resolveAgents(config);
|
|
402
696
|
for (const reviewer of agents.reviewers)
|
|
403
697
|
accounts.add(reviewer.account);
|
|
404
698
|
if (agents.editor)
|
|
405
699
|
accounts.add(agents.editor.account);
|
|
700
|
+
if (config.triage?.account)
|
|
701
|
+
accounts.add(config.triage.account);
|
|
702
|
+
if (agents.triageCreator?.account)
|
|
703
|
+
accounts.add(agents.triageCreator.account);
|
|
406
704
|
await Promise.all([...accounts].filter(Boolean).map(async (account) => {
|
|
407
705
|
try {
|
|
408
706
|
await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`);
|
|
@@ -414,13 +712,35 @@ async function validateAuth(config, exec, errors) {
|
|
|
414
712
|
}
|
|
415
713
|
async function fetchPermissions(config, exec, account) {
|
|
416
714
|
const token = (await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`)).trim();
|
|
417
|
-
const raw = await exec(`
|
|
715
|
+
const raw = await exec(`gh api${ghHostOption(config)} repos/${config.github?.owner}/${config.github?.repo} --jq .permissions`, { env: { GH_TOKEN: token } });
|
|
418
716
|
return JSON.parse(raw);
|
|
419
717
|
}
|
|
718
|
+
async function validateWorktreeConfig(config, exec, options, errors) {
|
|
719
|
+
const agents = resolveAgents(config);
|
|
720
|
+
const checkEditor = Boolean(agents.editor && (options.requireEditor || options.requireWorktreeConfig));
|
|
721
|
+
const checkTriageCreator = Boolean(config.triage?.automation?.create &&
|
|
722
|
+
agents.triageCreator &&
|
|
723
|
+
(options.requireTriage || options.requireWorktreeConfig));
|
|
724
|
+
if (!checkEditor && !checkTriageCreator)
|
|
725
|
+
return;
|
|
726
|
+
if (!exec)
|
|
727
|
+
return;
|
|
728
|
+
const error = "git config extensions.worktreeConfig must be true when editor or triage PR creator is configured";
|
|
729
|
+
try {
|
|
730
|
+
const value = (await exec("git config --bool --get extensions.worktreeConfig"))
|
|
731
|
+
.trim()
|
|
732
|
+
.toLowerCase();
|
|
733
|
+
if (value !== "true")
|
|
734
|
+
errors.push(error);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
errors.push(error);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
420
740
|
async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
421
741
|
if (!config.github?.owner || !config.github.repo)
|
|
422
742
|
return;
|
|
423
|
-
const agents = resolveAgents(config
|
|
743
|
+
const agents = resolveAgents(config);
|
|
424
744
|
await Promise.all(agents.reviewers.map(async (reviewer) => {
|
|
425
745
|
try {
|
|
426
746
|
const permissions = await fetchPermissions(config, exec, reviewer.account);
|
|
@@ -432,6 +752,29 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
432
752
|
warnings.push(`Could not validate repository permissions for GitHub account: ${reviewer.account} (${error.message})`);
|
|
433
753
|
}
|
|
434
754
|
}));
|
|
755
|
+
if (config.triage?.account) {
|
|
756
|
+
try {
|
|
757
|
+
const permissions = await fetchPermissions(config, exec, config.triage.account);
|
|
758
|
+
if (!permissions.pull) {
|
|
759
|
+
errors.push(`GitHub account cannot read repository for issue triage: ${config.triage.account}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${config.triage.account} (${error.message})`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (agents.triageCreator?.account &&
|
|
767
|
+
agents.triageCreator.account !== config.triage?.account) {
|
|
768
|
+
try {
|
|
769
|
+
const permissions = await fetchPermissions(config, exec, agents.triageCreator.account);
|
|
770
|
+
if (!permissions.push) {
|
|
771
|
+
errors.push(`GitHub account cannot push to repository for triage PR creation: ${agents.triageCreator.account}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${agents.triageCreator.account} (${error.message})`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
435
778
|
if (!agents.editor)
|
|
436
779
|
return;
|
|
437
780
|
try {
|
|
@@ -454,70 +797,41 @@ export async function validateConfig(config, options = {}) {
|
|
|
454
797
|
validateKnownKeys(config, "config", CONFIG_KEYS, errors);
|
|
455
798
|
validateString(config.$schema, "$schema", errors);
|
|
456
799
|
validateString(config.language, "language", errors);
|
|
457
|
-
if (!config.agents) {
|
|
458
|
-
errors.push("agents
|
|
800
|
+
if (config.agents != null && !isPlainObject(config.agents)) {
|
|
801
|
+
errors.push("agents must be an object");
|
|
459
802
|
}
|
|
460
803
|
else {
|
|
461
|
-
|
|
462
|
-
|
|
804
|
+
validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
|
|
805
|
+
validatePermissionConfig(config.agents?.permissions, "agents.permissions", errors);
|
|
806
|
+
}
|
|
807
|
+
if ((options.requireReview ?? true) && !config.review) {
|
|
808
|
+
errors.push("review is required");
|
|
809
|
+
}
|
|
810
|
+
else if (config.review) {
|
|
811
|
+
if (!isPlainObject(config.review)) {
|
|
812
|
+
errors.push("review must be an object");
|
|
463
813
|
}
|
|
464
814
|
else {
|
|
465
|
-
validateKnownKeys(config.
|
|
466
|
-
}
|
|
467
|
-
validatePermissionConfig(config.agents.permissions, "agents.permissions", errors);
|
|
468
|
-
if (!config.agents.reviewers)
|
|
469
|
-
errors.push("agents.reviewers is required");
|
|
470
|
-
validateReviewerList(config.agents.reviewers, "agents.reviewers", errors, options.modelCatalog);
|
|
471
|
-
if (options.requireEditor && !config.agents.editor)
|
|
472
|
-
errors.push("agents.editor is required");
|
|
473
|
-
if (config.agents.editor) {
|
|
474
|
-
if (!config.agents.editor.model)
|
|
475
|
-
errors.push("agents.editor.model is required");
|
|
476
|
-
validateKnownKeys(config.agents.editor, "agents.editor", EDITOR_KEYS, errors);
|
|
477
|
-
validateString(config.agents.editor.model, "agents.editor.model", errors);
|
|
478
|
-
validateString(config.agents.editor.account, "agents.editor.account", errors);
|
|
479
|
-
validateString(config.agents.editor.persona, "agents.editor.persona", errors);
|
|
480
|
-
validateModel(config.agents.editor.model, "agents.editor.model", errors, options.modelCatalog);
|
|
481
|
-
if (!config.agents.editor.account)
|
|
482
|
-
errors.push("agents.editor.account is required");
|
|
483
|
-
if (config.agents.editor.options != null &&
|
|
484
|
-
!isPlainObject(config.agents.editor.options)) {
|
|
485
|
-
errors.push("agents.editor.options must be an object");
|
|
486
|
-
}
|
|
487
|
-
validatePermissionConfig(config.agents.editor.permission, "agents.editor.permission", errors);
|
|
488
|
-
const author = config.agents.editor.author;
|
|
489
|
-
if (!author || !isPlainObject(author)) {
|
|
490
|
-
if (author != null)
|
|
491
|
-
errors.push("agents.editor.author must be an object");
|
|
492
|
-
errors.push("agents.editor.author.name is required");
|
|
493
|
-
errors.push("agents.editor.author.email is required");
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
validateKnownKeys(author, "agents.editor.author", AUTHOR_KEYS, errors);
|
|
497
|
-
if (!author.name) {
|
|
498
|
-
errors.push("agents.editor.author.name is required");
|
|
499
|
-
}
|
|
500
|
-
else if (typeof author.name !== "string") {
|
|
501
|
-
errors.push("agents.editor.author.name must be a string");
|
|
502
|
-
}
|
|
503
|
-
if (!author.email) {
|
|
504
|
-
errors.push("agents.editor.author.email is required");
|
|
505
|
-
}
|
|
506
|
-
else if (typeof author.email !== "string") {
|
|
507
|
-
errors.push("agents.editor.author.email must be a string");
|
|
508
|
-
}
|
|
509
|
-
}
|
|
815
|
+
validateKnownKeys(config.review, "review", REVIEW_KEYS, errors);
|
|
510
816
|
}
|
|
511
|
-
if (
|
|
512
|
-
|
|
817
|
+
if (!config.review.agents)
|
|
818
|
+
errors.push("review.agents is required");
|
|
819
|
+
validateReviewerList(config.review.agents, "review.agents", errors, options.modelCatalog);
|
|
820
|
+
if (Array.isArray(config.review.agents)) {
|
|
821
|
+
validateResolvedReviewers(resolveAgents(config).reviewers, "review.resolvedAgents", errors);
|
|
513
822
|
}
|
|
514
823
|
}
|
|
824
|
+
if (options.requireTriage && !config.triage) {
|
|
825
|
+
errors.push("triage is required");
|
|
826
|
+
}
|
|
515
827
|
validateMerge(config, errors, options);
|
|
828
|
+
validateReviewMerge(config, errors);
|
|
516
829
|
validateAutomation(config, errors);
|
|
517
830
|
validateClear(config, errors);
|
|
518
831
|
validateChecks(config, errors);
|
|
519
832
|
validateConcurrency(config, errors);
|
|
520
833
|
validateSafety(config, errors);
|
|
834
|
+
validateTriage(config, errors, options);
|
|
521
835
|
await validatePrompts(config, errors, options.directory);
|
|
522
836
|
if (config.output != null && !isPlainObject(config.output)) {
|
|
523
837
|
errors.push("output must be an object");
|
|
@@ -530,28 +844,9 @@ export async function validateConfig(config, options = {}) {
|
|
|
530
844
|
errors.push("output.repairAttempts must be a non-negative integer");
|
|
531
845
|
}
|
|
532
846
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
validateKnownKeys(config.output.dirs, "output.dirs", OUTPUT_DIR_KEYS, errors);
|
|
539
|
-
const dirs = config.output.dirs;
|
|
540
|
-
for (const key of OUTPUT_DIR_KEYS) {
|
|
541
|
-
const value = dirs[key];
|
|
542
|
-
if (value != null && typeof value !== "string") {
|
|
543
|
-
errors.push(`output.dirs.${key} must be a string`);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
if (config.worktree != null && !isPlainObject(config.worktree)) {
|
|
549
|
-
errors.push("worktree must be an object");
|
|
550
|
-
}
|
|
551
|
-
validateKnownKeys(config.worktree, "worktree", WORKTREE_KEYS, errors);
|
|
552
|
-
if (config.worktree?.dir != null && typeof config.worktree.dir !== "string") {
|
|
553
|
-
errors.push("worktree.dir must be a string");
|
|
554
|
-
}
|
|
847
|
+
validateString(config.review?.output, "review.output", errors);
|
|
848
|
+
validateString(config.review?.worktree, "review.worktree", errors);
|
|
849
|
+
await validateWorktreeConfig(config, options.exec, options, errors);
|
|
555
850
|
if (options.checkAuth && !errors.length) {
|
|
556
851
|
if (!options.exec) {
|
|
557
852
|
errors.push("validateConfig requires exec when checkAuth is true");
|