opencode-magi 0.6.1 → 0.8.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 +16 -6
- package/dist/config/resolve.js +40 -3
- package/dist/config/validate.js +231 -66
- package/dist/github/commands.js +32 -0
- package/dist/index.js +21 -50
- package/dist/orchestrator/merge.js +310 -10
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +97 -1
- package/dist/orchestrator/run-manager.js +4 -4
- package/dist/orchestrator/triage.js +312 -103
- package/dist/prompts/compose.js +59 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- package/dist/prompts/templates/merge/conflict.md +10 -0
- package/dist/prompts/templates/review/rereview.md +2 -0
- package/dist/prompts/templates/review/review.md +2 -0
- package/dist/prompts/templates/triage/acceptance.md +1 -1
- package/dist/prompts/templates/triage/signal.md +10 -0
- package/package.json +1 -1
- package/schema.json +89 -16
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Add the following content to the configuration file.
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"review": {
|
|
81
|
-
"
|
|
81
|
+
"reviewers": [
|
|
82
82
|
{ "ref": "account-1" },
|
|
83
83
|
{ "ref": "account-2" },
|
|
84
84
|
{ "ref": "account-3" }
|
|
@@ -87,7 +87,7 @@ Add the following content to the configuration file.
|
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
After refs are expanded, `review.
|
|
90
|
+
After `refs` are expanded, `review.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.
|
|
91
91
|
|
|
92
92
|
#### Set project config
|
|
93
93
|
|
|
@@ -133,7 +133,7 @@ Add the following content to the configuration file.
|
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
135
|
"review": {
|
|
136
|
-
"
|
|
136
|
+
"reviewers": [
|
|
137
137
|
{ "ref": "account-1" },
|
|
138
138
|
{ "ref": "account-2" },
|
|
139
139
|
{ "ref": "account-3" }
|
|
@@ -143,8 +143,7 @@ Add the following content to the configuration file.
|
|
|
143
143
|
"editor": { "ref": "account-4" }
|
|
144
144
|
},
|
|
145
145
|
"triage": {
|
|
146
|
-
"
|
|
147
|
-
"agents": [
|
|
146
|
+
"voters": [
|
|
148
147
|
{ "ref": "account-1" },
|
|
149
148
|
{ "ref": "account-2" },
|
|
150
149
|
{ "ref": "account-3" }
|
|
@@ -155,7 +154,18 @@ Add the following content to the configuration file.
|
|
|
155
154
|
|
|
156
155
|
Entries with `ref` are expanded from `agents.refs`. Fields set alongside `ref` override fields from the preset.
|
|
157
156
|
|
|
158
|
-
|
|
157
|
+
`model` can be a single `provider/model` string, a single object with `id` and `options`, or an ordered candidate array. Candidate arrays are resolved during validation against OpenCode's model catalog; the first available model is selected. Put provider-specific options on model objects, not on the agent role.
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"model": {
|
|
162
|
+
"id": "openai/gpt-5.1",
|
|
163
|
+
"options": { "reasoningEffort": "high" }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
After `refs` are expanded, `review.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. `merge.editor.account` is used by `/magi:merge` to push fixes, close PRs, and merge PRs.
|
|
159
169
|
|
|
160
170
|
#### Validate config
|
|
161
171
|
|
package/dist/config/resolve.js
CHANGED
|
@@ -24,6 +24,31 @@ const DEFAULT_TRIAGE_CATEGORIES = [
|
|
|
24
24
|
types: ["Feature"],
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
|
+
export const DEFAULT_TRIAGE_LABEL_RULES = [
|
|
28
|
+
{ remove: ["triage"], when: { disposition: "accepted" } },
|
|
29
|
+
{
|
|
30
|
+
add: ["duplicate"],
|
|
31
|
+
remove: ["triage"],
|
|
32
|
+
when: { disposition: "duplicate" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
add: ["duplicate"],
|
|
36
|
+
remove: ["triage"],
|
|
37
|
+
when: { disposition: "already_handled" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
add: ["wontfix"],
|
|
41
|
+
remove: ["triage"],
|
|
42
|
+
when: { disposition: "rejected" },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
add: ["invalid"],
|
|
46
|
+
remove: ["triage"],
|
|
47
|
+
when: { disposition: "invalid" },
|
|
48
|
+
},
|
|
49
|
+
{ add: ["question"], when: { disposition: "needs_category" } },
|
|
50
|
+
{ add: ["question"], when: { disposition: "needs_acceptance" } },
|
|
51
|
+
];
|
|
27
52
|
export function reviewerKey(reviewer, index) {
|
|
28
53
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
29
54
|
}
|
|
@@ -33,6 +58,12 @@ export function triageAgentKey(agent, index) {
|
|
|
33
58
|
export function validateReviewerId(id) {
|
|
34
59
|
return ID_PATTERN.test(id);
|
|
35
60
|
}
|
|
61
|
+
function normalizedModel(model) {
|
|
62
|
+
if (typeof model !== "string") {
|
|
63
|
+
throw new Error("model must be normalized before resolving agents");
|
|
64
|
+
}
|
|
65
|
+
return model;
|
|
66
|
+
}
|
|
36
67
|
function clonePermissionValue(value) {
|
|
37
68
|
return typeof value === "string" ? value : { ...value };
|
|
38
69
|
}
|
|
@@ -86,25 +117,29 @@ export function resolveAgents(config) {
|
|
|
86
117
|
editor: editor
|
|
87
118
|
? {
|
|
88
119
|
...editor,
|
|
120
|
+
model: normalizedModel(editor.model),
|
|
89
121
|
permission: resolveEditorPermission(agents, editor),
|
|
90
122
|
}
|
|
91
123
|
: undefined,
|
|
92
|
-
reviewers: (config.review?.
|
|
124
|
+
reviewers: (config.review?.reviewers ?? []).map((reviewer, index) => ({
|
|
93
125
|
...reviewer,
|
|
94
126
|
key: reviewerKey(reviewer, index),
|
|
95
127
|
index,
|
|
128
|
+
model: normalizedModel(reviewer.model),
|
|
96
129
|
permission: resolveReviewerPermission(agents, reviewer),
|
|
97
130
|
})),
|
|
98
|
-
triage: (config.triage?.
|
|
131
|
+
triage: (config.triage?.voters ?? []).map((agent, index) => ({
|
|
99
132
|
...agent,
|
|
100
133
|
key: triageAgentKey(agent, index),
|
|
101
134
|
index,
|
|
135
|
+
model: normalizedModel(agent.model),
|
|
102
136
|
permission: resolveTriageAgentPermission(agents, agent),
|
|
103
137
|
})),
|
|
104
138
|
triageCreator: creator
|
|
105
139
|
? {
|
|
106
140
|
...creator,
|
|
107
141
|
account: creator.account ?? "",
|
|
142
|
+
model: normalizedModel(creator.model),
|
|
108
143
|
permission: resolveTriageCreatorPermission(agents, creator),
|
|
109
144
|
}
|
|
110
145
|
: undefined,
|
|
@@ -128,6 +163,7 @@ export function resolveRepository(config) {
|
|
|
128
163
|
agents: resolveAgents(config),
|
|
129
164
|
automation: {
|
|
130
165
|
close: config.merge?.automation?.close ?? false,
|
|
166
|
+
conflict: config.merge?.automation?.conflict ?? false,
|
|
131
167
|
merge: config.merge?.automation?.merge ?? true,
|
|
132
168
|
},
|
|
133
169
|
checks: {
|
|
@@ -180,9 +216,9 @@ export function resolveRepository(config) {
|
|
|
180
216
|
},
|
|
181
217
|
triage: {
|
|
182
218
|
automation: {
|
|
183
|
-
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
184
219
|
close: config.triage?.automation?.close ?? false,
|
|
185
220
|
create: config.triage?.automation?.create ?? false,
|
|
221
|
+
label: config.triage?.automation?.label ?? DEFAULT_TRIAGE_LABEL_RULES,
|
|
186
222
|
merge: config.triage?.automation?.merge ?? false,
|
|
187
223
|
review: config.triage?.automation?.review ?? false,
|
|
188
224
|
},
|
|
@@ -205,6 +241,7 @@ export function resolveRepository(config) {
|
|
|
205
241
|
blockedLabels: config.triage?.safety?.blockedLabels ?? [],
|
|
206
242
|
requiredLabels: config.triage?.safety?.requiredLabels ?? ["triage"],
|
|
207
243
|
},
|
|
244
|
+
signals: config.triage?.signals ?? [],
|
|
208
245
|
worktree: config.triage?.worktree,
|
|
209
246
|
},
|
|
210
247
|
};
|
package/dist/config/validate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { access } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { isAbsolute, join } from "node:path";
|
|
6
6
|
import schema from "../../schema.json" with { type: "json" };
|
|
7
|
-
import { resolveAgents, validateReviewerId } from "./resolve";
|
|
7
|
+
import { resolveAgents, reviewerKey, triageAgentKey, validateReviewerId, } from "./resolve";
|
|
8
8
|
const RESERVED_REVIEWER_KEYS = new Set(["editor", "orchestrator", "system"]);
|
|
9
9
|
const PERMISSION_ACTIONS = new Set(["allow", "ask", "deny"]);
|
|
10
10
|
const AJV = new Ajv2020({ allErrors: true, strict: false });
|
|
@@ -27,7 +27,6 @@ const REVIEWER_KEYS = new Set([
|
|
|
27
27
|
"account",
|
|
28
28
|
"id",
|
|
29
29
|
"model",
|
|
30
|
-
"options",
|
|
31
30
|
"permissions",
|
|
32
31
|
"persona",
|
|
33
32
|
]);
|
|
@@ -35,7 +34,6 @@ const EDITOR_KEYS = new Set([
|
|
|
35
34
|
"account",
|
|
36
35
|
"author",
|
|
37
36
|
"model",
|
|
38
|
-
"options",
|
|
39
37
|
"permissions",
|
|
40
38
|
"persona",
|
|
41
39
|
]);
|
|
@@ -43,7 +41,6 @@ const TRIAGE_AGENT_KEYS = new Set([
|
|
|
43
41
|
"account",
|
|
44
42
|
"id",
|
|
45
43
|
"model",
|
|
46
|
-
"options",
|
|
47
44
|
"permissions",
|
|
48
45
|
"persona",
|
|
49
46
|
]);
|
|
@@ -51,20 +48,19 @@ const TRIAGE_CREATOR_KEYS = new Set([
|
|
|
51
48
|
"account",
|
|
52
49
|
"author",
|
|
53
50
|
"model",
|
|
54
|
-
"options",
|
|
55
51
|
"permissions",
|
|
56
52
|
"persona",
|
|
57
53
|
]);
|
|
58
54
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
59
55
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
60
56
|
const REVIEW_KEYS = new Set([
|
|
61
|
-
"agents",
|
|
62
57
|
"automation",
|
|
63
58
|
"checks",
|
|
64
59
|
"concurrency",
|
|
65
60
|
"merge",
|
|
66
61
|
"output",
|
|
67
62
|
"prompts",
|
|
63
|
+
"reviewers",
|
|
68
64
|
"safety",
|
|
69
65
|
"worktree",
|
|
70
66
|
]);
|
|
@@ -76,7 +72,6 @@ const MERGE_KEYS = new Set([
|
|
|
76
72
|
"prompts",
|
|
77
73
|
]);
|
|
78
74
|
const TRIAGE_KEYS = new Set([
|
|
79
|
-
"agents",
|
|
80
75
|
"automation",
|
|
81
76
|
"categories",
|
|
82
77
|
"concurrency",
|
|
@@ -85,6 +80,8 @@ const TRIAGE_KEYS = new Set([
|
|
|
85
80
|
"prompts",
|
|
86
81
|
"reporter",
|
|
87
82
|
"safety",
|
|
83
|
+
"signals",
|
|
84
|
+
"voters",
|
|
88
85
|
"worktree",
|
|
89
86
|
]);
|
|
90
87
|
const REVIEW_MERGE_KEYS = new Set([
|
|
@@ -97,17 +94,24 @@ const REVIEW_MERGE_KEYS = new Set([
|
|
|
97
94
|
const REVIEW_CHECKS_KEYS = new Set(["exclude", "retryFailedJobs", "wait"]);
|
|
98
95
|
const MERGE_CHECKS_KEYS = new Set(["wait"]);
|
|
99
96
|
const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
97
|
+
const MERGE_AUTOMATION_KEYS = new Set(["close", "conflict", "merge"]);
|
|
100
98
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
101
99
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
102
100
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
103
101
|
const TRIAGE_AUTOMATION_KEYS = new Set([
|
|
104
|
-
"clear",
|
|
105
102
|
"close",
|
|
106
103
|
"create",
|
|
104
|
+
"label",
|
|
107
105
|
"merge",
|
|
108
106
|
"review",
|
|
109
107
|
]);
|
|
110
108
|
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
109
|
+
const TRIAGE_LABEL_RULE_KEYS = new Set(["add", "remove", "when"]);
|
|
110
|
+
const TRIAGE_LABEL_RULE_WHEN_KEYS = new Set([
|
|
111
|
+
"category",
|
|
112
|
+
"disposition",
|
|
113
|
+
"signals",
|
|
114
|
+
]);
|
|
111
115
|
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
112
116
|
const TRIAGE_SAFETY_KEYS = new Set([
|
|
113
117
|
"allowAuthors",
|
|
@@ -116,6 +120,18 @@ const TRIAGE_SAFETY_KEYS = new Set([
|
|
|
116
120
|
"blockedLabels",
|
|
117
121
|
"requiredLabels",
|
|
118
122
|
]);
|
|
123
|
+
const TRIAGE_SIGNAL_KEYS = new Set(["description", "id"]);
|
|
124
|
+
const TRIAGE_DISPOSITIONS = new Set([
|
|
125
|
+
"accepted",
|
|
126
|
+
"rejected",
|
|
127
|
+
"invalid",
|
|
128
|
+
"duplicate",
|
|
129
|
+
"already_handled",
|
|
130
|
+
"needs_category",
|
|
131
|
+
"needs_acceptance",
|
|
132
|
+
"blocked",
|
|
133
|
+
"failed",
|
|
134
|
+
]);
|
|
119
135
|
const SAFETY_KEYS = new Set([
|
|
120
136
|
"allowAuthors",
|
|
121
137
|
"blockedPaths",
|
|
@@ -145,6 +161,7 @@ const TRIAGE_PROMPT_KEYS = new Set([
|
|
|
145
161
|
"existingPr",
|
|
146
162
|
"reconsider",
|
|
147
163
|
]);
|
|
164
|
+
const MODEL_CANDIDATE_KEYS = new Set(["id", "options"]);
|
|
148
165
|
function githubHost(config) {
|
|
149
166
|
return config.github?.host ?? "github.com";
|
|
150
167
|
}
|
|
@@ -190,14 +207,14 @@ function expandAgentRefs(config, errors) {
|
|
|
190
207
|
const refsValue = isPlainObject(agents) ? agents.refs : undefined;
|
|
191
208
|
const refsInvalid = refsValue != null && !isPlainObject(refsValue);
|
|
192
209
|
const refs = isPlainObject(refsValue) ? refsValue : undefined;
|
|
193
|
-
if (Array.isArray(magiConfig.review?.
|
|
194
|
-
magiConfig.review.
|
|
210
|
+
if (Array.isArray(magiConfig.review?.reviewers)) {
|
|
211
|
+
magiConfig.review.reviewers = magiConfig.review.reviewers.map((agent, index) => expandAgentRefUse(agent, `review.reviewers[${index}]`, refs, refsInvalid, errors));
|
|
195
212
|
}
|
|
196
213
|
if (isPlainObject(magiConfig.merge?.editor)) {
|
|
197
214
|
magiConfig.merge.editor = expandAgentRefUse(magiConfig.merge.editor, "merge.editor", refs, refsInvalid, errors);
|
|
198
215
|
}
|
|
199
|
-
if (Array.isArray(magiConfig.triage?.
|
|
200
|
-
magiConfig.triage.
|
|
216
|
+
if (Array.isArray(magiConfig.triage?.voters)) {
|
|
217
|
+
magiConfig.triage.voters = magiConfig.triage.voters.map((agent, index) => expandAgentRefUse(agent, `triage.voters[${index}]`, refs, refsInvalid, errors));
|
|
201
218
|
}
|
|
202
219
|
if (isPlainObject(magiConfig.triage?.creator)) {
|
|
203
220
|
magiConfig.triage.creator = expandAgentRefUse(magiConfig.triage.creator, "triage.creator", refs, refsInvalid, errors);
|
|
@@ -275,26 +292,97 @@ function validatePermissionConfig(permission, path, errors) {
|
|
|
275
292
|
}
|
|
276
293
|
}
|
|
277
294
|
}
|
|
278
|
-
function
|
|
279
|
-
if (!model)
|
|
280
|
-
return;
|
|
295
|
+
function modelValidationError(model, path, catalog) {
|
|
281
296
|
const slash = model.indexOf("/");
|
|
282
|
-
if (slash <= 0 || slash === model.length - 1)
|
|
283
|
-
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
297
|
+
if (slash <= 0 || slash === model.length - 1)
|
|
298
|
+
return `${path} must be a full OpenCode model ID in provider/model form`;
|
|
286
299
|
if (!catalog)
|
|
287
|
-
return;
|
|
300
|
+
return undefined;
|
|
288
301
|
const providerId = model.slice(0, slash);
|
|
289
302
|
const modelId = model.slice(slash + 1);
|
|
290
303
|
const models = catalog[providerId];
|
|
291
|
-
if (!models)
|
|
292
|
-
|
|
304
|
+
if (!models)
|
|
305
|
+
return `${path} uses unknown OpenCode provider: ${providerId}`;
|
|
306
|
+
if (!models.includes(modelId))
|
|
307
|
+
return `${path} uses unknown OpenCode model: ${model}`;
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
function validateModelId(model, path, errors, catalog) {
|
|
311
|
+
const error = modelValidationError(model, path, catalog);
|
|
312
|
+
if (error) {
|
|
313
|
+
errors.push(error);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
function readModelCandidate(value, path, errors) {
|
|
319
|
+
if (typeof value === "string")
|
|
320
|
+
return { id: value };
|
|
321
|
+
if (!isPlainObject(value)) {
|
|
322
|
+
errors.push(`${path} must be a string or an object`);
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
validateKnownKeys(value, path, MODEL_CANDIDATE_KEYS, errors);
|
|
326
|
+
if (typeof value.id !== "string") {
|
|
327
|
+
errors.push(`${path}.id must be a string`);
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
if (value.options != null && !isPlainObject(value.options)) {
|
|
331
|
+
errors.push(`${path}.options must be an object`);
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
return { id: value.id, options: value.options };
|
|
335
|
+
}
|
|
336
|
+
function validateAndNormalizeModel(target, path, errors, catalog) {
|
|
337
|
+
const model = target.model;
|
|
338
|
+
if (typeof model === "string") {
|
|
339
|
+
validateModelId(model, path, errors, catalog);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (isPlainObject(model)) {
|
|
343
|
+
const candidate = readModelCandidate(model, path, errors);
|
|
344
|
+
if (candidate &&
|
|
345
|
+
validateModelId(candidate.id, `${path}.id`, errors, catalog)) {
|
|
346
|
+
target.model = candidate.id;
|
|
347
|
+
if (candidate.options)
|
|
348
|
+
target.options = candidate.options;
|
|
349
|
+
else
|
|
350
|
+
delete target.options;
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!Array.isArray(model)) {
|
|
355
|
+
if (model != null)
|
|
356
|
+
errors.push(`${path} must be a string, an object, or an array`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!model.length) {
|
|
360
|
+
errors.push(`${path} must contain at least one model candidate`);
|
|
293
361
|
return;
|
|
294
362
|
}
|
|
295
|
-
if (!
|
|
296
|
-
errors.push(`${path}
|
|
363
|
+
if (!catalog) {
|
|
364
|
+
errors.push(`${path} requires an OpenCode model catalog`);
|
|
365
|
+
return;
|
|
297
366
|
}
|
|
367
|
+
const candidateErrors = [];
|
|
368
|
+
for (const [index, value] of model.entries()) {
|
|
369
|
+
const candidatePath = `${path}[${index}]`;
|
|
370
|
+
const candidate = readModelCandidate(value, candidatePath, errors);
|
|
371
|
+
if (!candidate)
|
|
372
|
+
continue;
|
|
373
|
+
const idPath = isPlainObject(value) ? `${candidatePath}.id` : candidatePath;
|
|
374
|
+
const error = modelValidationError(candidate.id, idPath, catalog);
|
|
375
|
+
if (!error) {
|
|
376
|
+
target.model = candidate.id;
|
|
377
|
+
if (candidate.options)
|
|
378
|
+
target.options = candidate.options;
|
|
379
|
+
else
|
|
380
|
+
delete target.options;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
candidateErrors.push(error);
|
|
384
|
+
}
|
|
385
|
+
errors.push(`${path} must contain at least one usable OpenCode model candidate${candidateErrors.length ? ` (${candidateErrors.join("; ")})` : ""}`);
|
|
298
386
|
}
|
|
299
387
|
function validateReviewerList(reviewers, path, errors, catalog) {
|
|
300
388
|
if (reviewers == null)
|
|
@@ -315,14 +403,11 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
315
403
|
validateKnownKeys(reviewer, `${path}[${index}]`, REVIEWER_KEYS, errors);
|
|
316
404
|
if (!reviewer.model)
|
|
317
405
|
errors.push(`${path}[${index}].model is required`);
|
|
318
|
-
|
|
319
|
-
validateModel(reviewer.model, `${path}[${index}].model`, errors, catalog);
|
|
406
|
+
validateAndNormalizeModel(reviewer, `${path}[${index}].model`, errors, catalog);
|
|
320
407
|
if (!reviewer.account)
|
|
321
408
|
errors.push(`${path}[${index}].account is required`);
|
|
322
409
|
validateString(reviewer.account, `${path}[${index}].account`, errors);
|
|
323
410
|
validateString(reviewer.persona, `${path}[${index}].persona`, errors);
|
|
324
|
-
if (reviewer.options != null && !isPlainObject(reviewer.options))
|
|
325
|
-
errors.push(`${path}[${index}].options must be an object`);
|
|
326
411
|
validatePermissionConfig(reviewer.permissions, `${path}[${index}].permissions`, errors);
|
|
327
412
|
if (reviewer.id) {
|
|
328
413
|
if (!validateReviewerId(reviewer.id)) {
|
|
@@ -334,18 +419,18 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
334
419
|
}
|
|
335
420
|
});
|
|
336
421
|
}
|
|
337
|
-
function validateTriageAgentList(
|
|
338
|
-
if (
|
|
422
|
+
function validateTriageAgentList(voters, path, errors, catalog) {
|
|
423
|
+
if (voters == null)
|
|
339
424
|
return;
|
|
340
|
-
if (!Array.isArray(
|
|
425
|
+
if (!Array.isArray(voters)) {
|
|
341
426
|
errors.push(`${path} must be an array`);
|
|
342
427
|
return;
|
|
343
428
|
}
|
|
344
|
-
if (
|
|
345
|
-
errors.push(`${path} must contain at least 3
|
|
346
|
-
if (
|
|
347
|
-
errors.push(`${path} must contain an odd number of
|
|
348
|
-
|
|
429
|
+
if (voters.length < 3)
|
|
430
|
+
errors.push(`${path} must contain at least 3 voters`);
|
|
431
|
+
if (voters.length % 2 === 0)
|
|
432
|
+
errors.push(`${path} must contain an odd number of voters`);
|
|
433
|
+
voters.forEach((agent, index) => {
|
|
349
434
|
if (!agent || typeof agent !== "object") {
|
|
350
435
|
errors.push(`${path}[${index}] must be an object`);
|
|
351
436
|
return;
|
|
@@ -353,14 +438,11 @@ function validateTriageAgentList(agents, path, errors, catalog) {
|
|
|
353
438
|
validateKnownKeys(agent, `${path}[${index}]`, TRIAGE_AGENT_KEYS, errors);
|
|
354
439
|
if (!agent.model)
|
|
355
440
|
errors.push(`${path}[${index}].model is required`);
|
|
356
|
-
|
|
357
|
-
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
441
|
+
validateAndNormalizeModel(agent, `${path}[${index}].model`, errors, catalog);
|
|
358
442
|
if (!agent.account)
|
|
359
443
|
errors.push(`${path}[${index}].account is required`);
|
|
360
444
|
validateString(agent.account, `${path}[${index}].account`, errors);
|
|
361
445
|
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
362
|
-
if (agent.options != null && !isPlainObject(agent.options))
|
|
363
|
-
errors.push(`${path}[${index}].options must be an object`);
|
|
364
446
|
validatePermissionConfig(agent.permissions, `${path}[${index}].permissions`, errors);
|
|
365
447
|
if (agent.id) {
|
|
366
448
|
if (!validateReviewerId(agent.id)) {
|
|
@@ -406,15 +488,11 @@ function validateEditor(editor, path, errors, catalog) {
|
|
|
406
488
|
if (!editor.model)
|
|
407
489
|
errors.push(`${path}.model is required`);
|
|
408
490
|
validateKnownKeys(editor, path, EDITOR_KEYS, errors);
|
|
409
|
-
|
|
491
|
+
validateAndNormalizeModel(editor, `${path}.model`, errors, catalog);
|
|
410
492
|
validateString(editor.account, `${path}.account`, errors);
|
|
411
493
|
validateString(editor.persona, `${path}.persona`, errors);
|
|
412
|
-
validateModel(editor.model, `${path}.model`, errors, catalog);
|
|
413
494
|
if (!editor.account)
|
|
414
495
|
errors.push(`${path}.account is required`);
|
|
415
|
-
if (editor.options != null && !isPlainObject(editor.options)) {
|
|
416
|
-
errors.push(`${path}.options must be an object`);
|
|
417
|
-
}
|
|
418
496
|
validatePermissionConfig(editor.permissions, `${path}.permissions`, errors);
|
|
419
497
|
const author = editor.author;
|
|
420
498
|
if (!author || !isPlainObject(author)) {
|
|
@@ -450,12 +528,8 @@ function validateTriageCreator(creator, path, errors, catalog) {
|
|
|
450
528
|
if (!creator.model)
|
|
451
529
|
errors.push(`${path}.model is required`);
|
|
452
530
|
validateString(creator.account, `${path}.account`, errors);
|
|
453
|
-
|
|
531
|
+
validateAndNormalizeModel(creator, `${path}.model`, errors, catalog);
|
|
454
532
|
validateString(creator.persona, `${path}.persona`, errors);
|
|
455
|
-
validateModel(creator.model, `${path}.model`, errors, catalog);
|
|
456
|
-
if (creator.options != null && !isPlainObject(creator.options)) {
|
|
457
|
-
errors.push(`${path}.options must be an object`);
|
|
458
|
-
}
|
|
459
533
|
validatePermissionConfig(creator.permissions, `${path}.permissions`, errors);
|
|
460
534
|
const author = creator.author;
|
|
461
535
|
if (!author || !isPlainObject(author)) {
|
|
@@ -498,7 +572,7 @@ function validateMerge(config, errors, options) {
|
|
|
498
572
|
errors.push("merge must be an object");
|
|
499
573
|
}
|
|
500
574
|
validateKnownKeys(merge, "merge", MERGE_KEYS, errors);
|
|
501
|
-
validateBooleanObject(merge?.automation, "merge.automation",
|
|
575
|
+
validateBooleanObject(merge?.automation, "merge.automation", MERGE_AUTOMATION_KEYS, errors);
|
|
502
576
|
const checks = merge?.checks;
|
|
503
577
|
validateKnownKeys(checks, "merge.checks", MERGE_CHECKS_KEYS, errors);
|
|
504
578
|
validateBoolean(checks?.wait, "merge.checks.wait", errors);
|
|
@@ -637,6 +711,79 @@ function validateTriageCategories(categories, path, errors) {
|
|
|
637
711
|
validateString(category.description, `${itemPath}.description`, errors);
|
|
638
712
|
});
|
|
639
713
|
}
|
|
714
|
+
function validateTriageSignals(signals, path, errors) {
|
|
715
|
+
if (signals == null)
|
|
716
|
+
return;
|
|
717
|
+
if (!Array.isArray(signals)) {
|
|
718
|
+
errors.push(`${path} must be an array`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const ids = new Set();
|
|
722
|
+
signals.forEach((item, index) => {
|
|
723
|
+
const itemPath = `${path}[${index}]`;
|
|
724
|
+
if (!isPlainObject(item)) {
|
|
725
|
+
errors.push(`${itemPath} must be an object`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const signal = item;
|
|
729
|
+
validateKnownKeys(signal, itemPath, TRIAGE_SIGNAL_KEYS, errors);
|
|
730
|
+
if (!signal.id) {
|
|
731
|
+
errors.push(`${itemPath}.id is required`);
|
|
732
|
+
}
|
|
733
|
+
else if (typeof signal.id !== "string") {
|
|
734
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
735
|
+
}
|
|
736
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(signal.id)) {
|
|
737
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
738
|
+
}
|
|
739
|
+
else if (ids.has(signal.id)) {
|
|
740
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
ids.add(signal.id);
|
|
744
|
+
}
|
|
745
|
+
if (!signal.description) {
|
|
746
|
+
errors.push(`${itemPath}.description is required`);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
validateString(signal.description, `${itemPath}.description`, errors);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function validateTriageLabelRules(rules, path, errors) {
|
|
754
|
+
if (rules == null)
|
|
755
|
+
return;
|
|
756
|
+
if (!Array.isArray(rules)) {
|
|
757
|
+
errors.push(`${path} must be an array`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
rules.forEach((item, index) => {
|
|
761
|
+
const itemPath = `${path}[${index}]`;
|
|
762
|
+
if (!isPlainObject(item)) {
|
|
763
|
+
errors.push(`${itemPath} must be an object`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const rule = item;
|
|
767
|
+
validateKnownKeys(rule, itemPath, TRIAGE_LABEL_RULE_KEYS, errors);
|
|
768
|
+
validateStringArray(rule.add, `${itemPath}.add`, errors);
|
|
769
|
+
validateStringArray(rule.remove, `${itemPath}.remove`, errors);
|
|
770
|
+
if (!isPlainObject(rule.when)) {
|
|
771
|
+
errors.push(`${itemPath}.when must be an object`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
validateKnownKeys(rule.when, `${itemPath}.when`, TRIAGE_LABEL_RULE_WHEN_KEYS, errors);
|
|
775
|
+
if (!Object.keys(rule.when).length) {
|
|
776
|
+
errors.push(`${itemPath}.when must not be empty`);
|
|
777
|
+
}
|
|
778
|
+
if (rule.when.disposition != null &&
|
|
779
|
+
(typeof rule.when.disposition !== "string" ||
|
|
780
|
+
!TRIAGE_DISPOSITIONS.has(rule.when.disposition))) {
|
|
781
|
+
errors.push(`${itemPath}.when.disposition must be a triage disposition`);
|
|
782
|
+
}
|
|
783
|
+
validateString(rule.when.category, `${itemPath}.when.category`, errors);
|
|
784
|
+
validateStringArray(rule.when.signals, `${itemPath}.when.signals`, errors);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
640
787
|
function validateSafety(config, errors) {
|
|
641
788
|
const safety = config.review?.safety;
|
|
642
789
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -680,15 +827,20 @@ function validateTriage(config, errors, options) {
|
|
|
680
827
|
const creator = triage.creator;
|
|
681
828
|
const reporter = typeof triage.reporter === "string" ? triage.reporter : undefined;
|
|
682
829
|
const safety = triage.safety;
|
|
683
|
-
if (!triage.
|
|
684
|
-
errors.push("triage.
|
|
685
|
-
validateTriageAgentList(triage.
|
|
686
|
-
if (Array.isArray(triage.
|
|
687
|
-
const resolvedTriageAgents =
|
|
830
|
+
if (!triage.voters)
|
|
831
|
+
errors.push("triage.voters is required");
|
|
832
|
+
validateTriageAgentList(triage.voters, "triage.voters", errors, options.modelCatalog);
|
|
833
|
+
if (Array.isArray(triage.voters)) {
|
|
834
|
+
const resolvedTriageAgents = triage.voters.map((agent, index) => ({
|
|
835
|
+
account: agent && typeof agent === "object" && typeof agent.account === "string"
|
|
836
|
+
? agent.account
|
|
837
|
+
: "",
|
|
838
|
+
key: agent && typeof agent === "object" ? triageAgentKey(agent, index) : "",
|
|
839
|
+
}));
|
|
688
840
|
validateResolvedTriageAgents(resolvedTriageAgents, "triage.resolvedAgents", errors);
|
|
689
841
|
if (reporter != null &&
|
|
690
842
|
!resolvedTriageAgents.some((agent) => agent.key === reporter)) {
|
|
691
|
-
errors.push(`triage.reporter must match a triage
|
|
843
|
+
errors.push(`triage.reporter must match a triage voter key: ${reporter}`);
|
|
692
844
|
}
|
|
693
845
|
}
|
|
694
846
|
validateString(triage.reporter, "triage.reporter", errors);
|
|
@@ -705,7 +857,7 @@ function validateTriage(config, errors, options) {
|
|
|
705
857
|
validateBoolean(automation?.create, "triage.automation.create", errors);
|
|
706
858
|
validateBoolean(automation?.merge, "triage.automation.merge", errors);
|
|
707
859
|
validateBoolean(automation?.review, "triage.automation.review", errors);
|
|
708
|
-
|
|
860
|
+
validateTriageLabelRules(automation?.label, "triage.automation.label", errors);
|
|
709
861
|
if (automation?.review && !automation.create) {
|
|
710
862
|
errors.push("triage.automation.review requires triage.automation.create to be true");
|
|
711
863
|
}
|
|
@@ -720,6 +872,7 @@ function validateTriage(config, errors, options) {
|
|
|
720
872
|
errors.push("triage.concurrency.runs must be a positive integer");
|
|
721
873
|
}
|
|
722
874
|
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
875
|
+
validateTriageSignals(triage.signals, "triage.signals", errors);
|
|
723
876
|
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
724
877
|
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
725
878
|
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
|
@@ -778,10 +931,10 @@ async function fetchPermissions(config, exec, account) {
|
|
|
778
931
|
return JSON.parse(raw);
|
|
779
932
|
}
|
|
780
933
|
async function validateWorktreeConfig(config, exec, options, errors) {
|
|
781
|
-
const
|
|
782
|
-
|
|
934
|
+
const checkEditor = Boolean(config.merge?.editor &&
|
|
935
|
+
(options.requireEditor || options.requireWorktreeConfig));
|
|
783
936
|
const checkTriageCreator = Boolean(config.triage?.automation?.create &&
|
|
784
|
-
|
|
937
|
+
config.triage?.creator &&
|
|
785
938
|
(options.requireTriage || options.requireWorktreeConfig));
|
|
786
939
|
if (!checkEditor && !checkTriageCreator)
|
|
787
940
|
return;
|
|
@@ -853,6 +1006,9 @@ export async function validateConfig(config, options = {}) {
|
|
|
853
1006
|
const warnings = [];
|
|
854
1007
|
if (!config || typeof config !== "object")
|
|
855
1008
|
errors.push("config must be an object");
|
|
1009
|
+
if (options.requireModelCatalog && !options.modelCatalog) {
|
|
1010
|
+
errors.push("OpenCode model catalog could not be loaded");
|
|
1011
|
+
}
|
|
856
1012
|
expandAgentRefs(config, errors);
|
|
857
1013
|
if (config && typeof config === "object")
|
|
858
1014
|
validateJsonSchema(config, errors);
|
|
@@ -876,11 +1032,20 @@ export async function validateConfig(config, options = {}) {
|
|
|
876
1032
|
else {
|
|
877
1033
|
validateKnownKeys(config.review, "review", REVIEW_KEYS, errors);
|
|
878
1034
|
}
|
|
879
|
-
if (!config.review.
|
|
880
|
-
errors.push("review.
|
|
881
|
-
validateReviewerList(config.review.
|
|
882
|
-
if (Array.isArray(config.review.
|
|
883
|
-
validateResolvedReviewers(
|
|
1035
|
+
if (!config.review.reviewers)
|
|
1036
|
+
errors.push("review.reviewers is required");
|
|
1037
|
+
validateReviewerList(config.review.reviewers, "review.reviewers", errors, options.modelCatalog);
|
|
1038
|
+
if (Array.isArray(config.review.reviewers)) {
|
|
1039
|
+
validateResolvedReviewers(config.review.reviewers.map((reviewer, index) => ({
|
|
1040
|
+
account: reviewer &&
|
|
1041
|
+
typeof reviewer === "object" &&
|
|
1042
|
+
typeof reviewer.account === "string"
|
|
1043
|
+
? reviewer.account
|
|
1044
|
+
: "",
|
|
1045
|
+
key: reviewer && typeof reviewer === "object"
|
|
1046
|
+
? reviewerKey(reviewer, index)
|
|
1047
|
+
: "",
|
|
1048
|
+
})), "review.resolvedReviewers", errors);
|
|
884
1049
|
}
|
|
885
1050
|
}
|
|
886
1051
|
if (options.requireTriage && !config.triage) {
|