opencode-magi 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +81 -1
- package/dist/config/validate.js +341 -3
- package/dist/config/worktree.js +8 -2
- package/dist/github/commands.js +381 -19
- package/dist/index.js +252 -26
- package/dist/orchestrator/ci.js +1 -1
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/inline-comments.js +79 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +108 -34
- package/dist/orchestrator/report.js +25 -7
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +122 -14
- package/dist/orchestrator/run-manager.js +408 -17
- package/dist/orchestrator/triage.js +1119 -0
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +163 -1
- package/dist/prompts/contracts.js +131 -18
- package/dist/prompts/output.js +173 -22
- package/dist/prompts/templates/merge/edit.md +12 -5
- package/dist/prompts/templates/review/review.md +6 -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 +5 -2
- package/schema.json +162 -5
package/README.md
CHANGED
|
@@ -126,6 +126,23 @@ Add the following content to the configuration file.
|
|
|
126
126
|
"email": "your-email@example.com"
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
},
|
|
130
|
+
"triage": {
|
|
131
|
+
"account": "your-triage-account",
|
|
132
|
+
"agents": [
|
|
133
|
+
{
|
|
134
|
+
"id": "general",
|
|
135
|
+
"model": "openai/gpt-5.5"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "maintenance",
|
|
139
|
+
"model": "anthropic/claude-opus-4-7"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "product",
|
|
143
|
+
"model": "opencode/kimi-k2-6"
|
|
144
|
+
}
|
|
145
|
+
]
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
```
|
|
@@ -149,6 +166,8 @@ Run commands from OpenCode.
|
|
|
149
166
|
/magi:review --dry-run 123
|
|
150
167
|
/magi:merge 123
|
|
151
168
|
/magi:merge --dry-run 123
|
|
169
|
+
/magi:triage 47 48
|
|
170
|
+
/magi:triage --dry-run 47
|
|
152
171
|
/magi:clear
|
|
153
172
|
```
|
|
154
173
|
|
package/dist/commands.js
CHANGED
|
@@ -7,6 +7,10 @@ export const MAGI_COMMANDS = {
|
|
|
7
7
|
description: "Review and merge pull requests with Magi",
|
|
8
8
|
template: [`Call the \`magi_merge\` tool.`, "PR: $ARGUMENTS"].join("\n"),
|
|
9
9
|
},
|
|
10
|
+
"magi:triage": {
|
|
11
|
+
description: "Triage GitHub issues with Magi",
|
|
12
|
+
template: [`Call the \`magi_triage\` tool.`, "Issue: $ARGUMENTS"].join("\n"),
|
|
13
|
+
},
|
|
10
14
|
"magi:review": {
|
|
11
15
|
description: "Review pull requests with Magi",
|
|
12
16
|
template: [`Call the \`magi_review\` tool.`, "PR: $ARGUMENTS"].join("\n"),
|
package/dist/config/output.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
2
|
const DEFAULT_OUTPUT_DIRS = {
|
|
3
|
+
issue: ".magi/runs/issue",
|
|
3
4
|
pr: ".magi/runs/pr",
|
|
4
5
|
};
|
|
5
6
|
function resolvePath(directory, path) {
|
|
6
7
|
return isAbsolute(path) ? path : join(directory, path);
|
|
7
8
|
}
|
|
8
9
|
export function outputBaseDir(directory, config, kind) {
|
|
9
|
-
return resolvePath(directory,
|
|
10
|
+
return resolvePath(directory, kind === "issue"
|
|
11
|
+
? (config.triage?.output ?? DEFAULT_OUTPUT_DIRS[kind])
|
|
12
|
+
: (config.review?.output ?? DEFAULT_OUTPUT_DIRS[kind]));
|
|
10
13
|
}
|
|
11
14
|
export function outputBaseDirs(directory, config) {
|
|
12
|
-
return [
|
|
15
|
+
return [
|
|
16
|
+
outputBaseDir(directory, config, "pr"),
|
|
17
|
+
outputBaseDir(directory, config, "issue"),
|
|
18
|
+
];
|
|
13
19
|
}
|
|
14
20
|
export function prRunOutputDir(input) {
|
|
15
21
|
return join(outputBaseDir(input.directory, input.config, "pr"), String(input.pr), ...(input.runId ? [input.runId] : []));
|
|
16
22
|
}
|
|
23
|
+
export function issueRunOutputDir(input) {
|
|
24
|
+
return join(outputBaseDir(input.directory, input.config, "issue"), String(input.issue), ...(input.runId ? [input.runId] : []));
|
|
25
|
+
}
|
package/dist/config/resolve.js
CHANGED
|
@@ -4,9 +4,32 @@ const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
4
4
|
const DEFAULT_COMMON_PERMISSION = commonPermission;
|
|
5
5
|
const DEFAULT_REVIEWER_PERMISSION = DEFAULT_COMMON_PERMISSION;
|
|
6
6
|
const DEFAULT_EDITOR_PERMISSION = mergePermissions(DEFAULT_COMMON_PERMISSION, editorPermission);
|
|
7
|
+
const DEFAULT_TRIAGE_CATEGORIES = [
|
|
8
|
+
{
|
|
9
|
+
description: "Something is broken or behaves incorrectly.",
|
|
10
|
+
id: "bug",
|
|
11
|
+
labels: ["bug"],
|
|
12
|
+
types: ["Bug"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
description: "Maintenance, refactoring, chores, or planned work.",
|
|
16
|
+
id: "task",
|
|
17
|
+
labels: ["task"],
|
|
18
|
+
types: ["Task"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
description: "New or improved user-facing capability.",
|
|
22
|
+
id: "feature",
|
|
23
|
+
labels: ["enhancement"],
|
|
24
|
+
types: ["Feature"],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
7
27
|
export function reviewerKey(reviewer, index) {
|
|
8
28
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
9
29
|
}
|
|
30
|
+
export function triageAgentKey(agent, index) {
|
|
31
|
+
return agent.id ?? `triage-${index + 1}`;
|
|
32
|
+
}
|
|
10
33
|
export function validateReviewerId(id) {
|
|
11
34
|
return ID_PATTERN.test(id);
|
|
12
35
|
}
|
|
@@ -49,9 +72,16 @@ export function resolveReviewerPermission(agents, reviewer) {
|
|
|
49
72
|
export function resolveEditorPermission(agents, editor) {
|
|
50
73
|
return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), editor.permissions);
|
|
51
74
|
}
|
|
75
|
+
export function resolveTriageAgentPermission(agents, agent) {
|
|
76
|
+
return mergePermissions(mergePermissions(DEFAULT_REVIEWER_PERMISSION, agents.permissions), agent.permissions);
|
|
77
|
+
}
|
|
78
|
+
export function resolveTriageCreatorPermission(agents, creator) {
|
|
79
|
+
return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), creator.permissions);
|
|
80
|
+
}
|
|
52
81
|
export function resolveAgents(config) {
|
|
53
82
|
const agents = config.agents ?? {};
|
|
54
83
|
const editor = config.merge?.editor;
|
|
84
|
+
const creator = config.triage?.creator;
|
|
55
85
|
return {
|
|
56
86
|
editor: editor
|
|
57
87
|
? {
|
|
@@ -65,8 +95,29 @@ export function resolveAgents(config) {
|
|
|
65
95
|
index,
|
|
66
96
|
permission: resolveReviewerPermission(agents, reviewer),
|
|
67
97
|
})),
|
|
98
|
+
triage: (config.triage?.agents ?? []).map((agent, index) => ({
|
|
99
|
+
...agent,
|
|
100
|
+
key: triageAgentKey(agent, index),
|
|
101
|
+
index,
|
|
102
|
+
permission: resolveTriageAgentPermission(agents, agent),
|
|
103
|
+
})),
|
|
104
|
+
triageCreator: creator
|
|
105
|
+
? {
|
|
106
|
+
...creator,
|
|
107
|
+
account: creator.account ?? config.triage?.account ?? "",
|
|
108
|
+
permission: resolveTriageCreatorPermission(agents, creator),
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
68
111
|
};
|
|
69
112
|
}
|
|
113
|
+
function resolveTriageCategories(config) {
|
|
114
|
+
return (config.triage?.categories ?? DEFAULT_TRIAGE_CATEGORIES).map((category) => ({
|
|
115
|
+
description: category.description,
|
|
116
|
+
id: category.id ?? "",
|
|
117
|
+
labels: category.labels ?? [],
|
|
118
|
+
types: category.types ?? [],
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
70
121
|
export function resolveRepository(config) {
|
|
71
122
|
if (!config.github?.owner)
|
|
72
123
|
throw new Error("github.owner is required");
|
|
@@ -119,7 +170,7 @@ export function resolveRepository(config) {
|
|
|
119
170
|
},
|
|
120
171
|
reviewAutomation: {
|
|
121
172
|
close: config.review?.automation?.close ?? false,
|
|
122
|
-
merge: config.review?.automation?.merge ??
|
|
173
|
+
merge: config.review?.automation?.merge ?? true,
|
|
123
174
|
},
|
|
124
175
|
safety: {
|
|
125
176
|
allowAuthors: config.review?.safety?.allowAuthors ?? [],
|
|
@@ -127,5 +178,34 @@ export function resolveRepository(config) {
|
|
|
127
178
|
maxChangedFiles: config.review?.safety?.maxChangedFiles,
|
|
128
179
|
requiredLabels: config.review?.safety?.requiredLabels ?? [],
|
|
129
180
|
},
|
|
181
|
+
triage: {
|
|
182
|
+
account: config.triage?.account,
|
|
183
|
+
automation: {
|
|
184
|
+
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
185
|
+
close: config.triage?.automation?.close ?? false,
|
|
186
|
+
create: config.triage?.automation?.create ?? false,
|
|
187
|
+
merge: config.triage?.automation?.merge ?? false,
|
|
188
|
+
review: config.triage?.automation?.review ?? false,
|
|
189
|
+
},
|
|
190
|
+
categories: resolveTriageCategories(config),
|
|
191
|
+
concurrency: {
|
|
192
|
+
runs: config.triage?.concurrency?.runs ?? 3,
|
|
193
|
+
},
|
|
194
|
+
output: config.triage?.output,
|
|
195
|
+
prompts: config.triage?.prompts ?? {},
|
|
196
|
+
safety: {
|
|
197
|
+
allowAuthors: config.triage?.safety?.allowAuthors ?? [],
|
|
198
|
+
allowMentionActors: config.triage?.safety?.allowMentionActors ?? [],
|
|
199
|
+
allowMentionRoles: config.triage?.safety?.allowMentionRoles ?? [
|
|
200
|
+
"AUTHOR",
|
|
201
|
+
"OWNER",
|
|
202
|
+
"MEMBER",
|
|
203
|
+
"COLLABORATOR",
|
|
204
|
+
],
|
|
205
|
+
blockedLabels: config.triage?.safety?.blockedLabels ?? [],
|
|
206
|
+
requiredLabels: config.triage?.safety?.requiredLabels ?? ["triage"],
|
|
207
|
+
},
|
|
208
|
+
worktree: config.triage?.worktree,
|
|
209
|
+
},
|
|
130
210
|
};
|
|
131
211
|
}
|
package/dist/config/validate.js
CHANGED
|
@@ -9,6 +9,8 @@ const RESERVED_REVIEWER_KEYS = new Set(["editor", "orchestrator", "system"]);
|
|
|
9
9
|
const PERMISSION_ACTIONS = new Set(["allow", "ask", "deny"]);
|
|
10
10
|
const AJV = new Ajv2020({ allErrors: true, strict: false });
|
|
11
11
|
const validateSchema = AJV.compile(schema);
|
|
12
|
+
const TRIAGE_CATEGORY_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
const RESERVED_TRIAGE_CATEGORY_IDS = new Set(["ASK", "none"]);
|
|
12
14
|
const CONFIG_KEYS = new Set([
|
|
13
15
|
"$schema",
|
|
14
16
|
"agents",
|
|
@@ -18,6 +20,7 @@ const CONFIG_KEYS = new Set([
|
|
|
18
20
|
"merge",
|
|
19
21
|
"output",
|
|
20
22
|
"review",
|
|
23
|
+
"triage",
|
|
21
24
|
]);
|
|
22
25
|
const AGENTS_KEYS = new Set(["permissions"]);
|
|
23
26
|
const REVIEWER_KEYS = new Set([
|
|
@@ -36,6 +39,21 @@ const EDITOR_KEYS = new Set([
|
|
|
36
39
|
"permissions",
|
|
37
40
|
"persona",
|
|
38
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",
|
|
55
|
+
"persona",
|
|
56
|
+
]);
|
|
39
57
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
40
58
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
41
59
|
const REVIEW_KEYS = new Set([
|
|
@@ -56,6 +74,18 @@ const MERGE_KEYS = new Set([
|
|
|
56
74
|
"maxThreadResolutionCycles",
|
|
57
75
|
"prompts",
|
|
58
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
|
+
]);
|
|
59
89
|
const REVIEW_MERGE_KEYS = new Set([
|
|
60
90
|
"approvalPolicy",
|
|
61
91
|
"auto",
|
|
@@ -69,6 +99,22 @@ const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
|
69
99
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
70
100
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
71
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
|
+
]);
|
|
72
118
|
const SAFETY_KEYS = new Set([
|
|
73
119
|
"allowAuthors",
|
|
74
120
|
"blockedPaths",
|
|
@@ -88,6 +134,19 @@ const MERGE_PROMPT_KEYS = new Set([
|
|
|
88
134
|
"edit",
|
|
89
135
|
"editGuidelines",
|
|
90
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
|
+
]);
|
|
91
150
|
function githubHost(config) {
|
|
92
151
|
return config.github?.host ?? "github.com";
|
|
93
152
|
}
|
|
@@ -98,6 +157,56 @@ function ghHostOption(config) {
|
|
|
98
157
|
function isPlainObject(value) {
|
|
99
158
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
100
159
|
}
|
|
160
|
+
function expandAgentRefUse(value, path, refs, refsInvalid, errors) {
|
|
161
|
+
if (!isPlainObject(value) || !Object.hasOwn(value, "ref"))
|
|
162
|
+
return value;
|
|
163
|
+
const use = { ...value };
|
|
164
|
+
const ref = use.ref;
|
|
165
|
+
delete use.ref;
|
|
166
|
+
if (typeof ref !== "string") {
|
|
167
|
+
errors.push(`${path}.ref must be a string`);
|
|
168
|
+
return use;
|
|
169
|
+
}
|
|
170
|
+
if (refsInvalid) {
|
|
171
|
+
errors.push(`agents.refs must be an object to resolve ${path}.ref`);
|
|
172
|
+
return use;
|
|
173
|
+
}
|
|
174
|
+
const preset = refs?.[ref];
|
|
175
|
+
if (preset == null) {
|
|
176
|
+
errors.push(`${path}.ref references unknown agents.refs preset: ${ref}`);
|
|
177
|
+
return use;
|
|
178
|
+
}
|
|
179
|
+
if (!isPlainObject(preset)) {
|
|
180
|
+
errors.push(`agents.refs.${ref} must be an object when referenced by ${path}.ref`);
|
|
181
|
+
return use;
|
|
182
|
+
}
|
|
183
|
+
const presetFields = { ...preset };
|
|
184
|
+
delete presetFields.ref;
|
|
185
|
+
return { ...presetFields, ...use };
|
|
186
|
+
}
|
|
187
|
+
function expandAgentRefs(config, errors) {
|
|
188
|
+
if (!config || typeof config !== "object")
|
|
189
|
+
return;
|
|
190
|
+
const magiConfig = config;
|
|
191
|
+
const agents = magiConfig.agents;
|
|
192
|
+
const refsValue = isPlainObject(agents) ? agents.refs : undefined;
|
|
193
|
+
const refsInvalid = refsValue != null && !isPlainObject(refsValue);
|
|
194
|
+
const refs = isPlainObject(refsValue) ? refsValue : undefined;
|
|
195
|
+
if (Array.isArray(magiConfig.review?.agents)) {
|
|
196
|
+
magiConfig.review.agents = magiConfig.review.agents.map((agent, index) => expandAgentRefUse(agent, `review.agents[${index}]`, refs, refsInvalid, errors));
|
|
197
|
+
}
|
|
198
|
+
if (isPlainObject(magiConfig.merge?.editor)) {
|
|
199
|
+
magiConfig.merge.editor = expandAgentRefUse(magiConfig.merge.editor, "merge.editor", refs, refsInvalid, errors);
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(magiConfig.triage?.agents)) {
|
|
202
|
+
magiConfig.triage.agents = magiConfig.triage.agents.map((agent, index) => expandAgentRefUse(agent, `triage.agents[${index}]`, refs, refsInvalid, errors));
|
|
203
|
+
}
|
|
204
|
+
if (isPlainObject(magiConfig.triage?.creator)) {
|
|
205
|
+
magiConfig.triage.creator = expandAgentRefUse(magiConfig.triage.creator, "triage.creator", refs, refsInvalid, errors);
|
|
206
|
+
}
|
|
207
|
+
if (isPlainObject(magiConfig.agents))
|
|
208
|
+
delete magiConfig.agents.refs;
|
|
209
|
+
}
|
|
101
210
|
function validateKnownKeys(value, path, keys, errors) {
|
|
102
211
|
if (!isPlainObject(value))
|
|
103
212
|
return;
|
|
@@ -227,6 +336,41 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
227
336
|
}
|
|
228
337
|
});
|
|
229
338
|
}
|
|
339
|
+
function validateTriageAgentList(agents, path, errors, catalog) {
|
|
340
|
+
if (agents == null)
|
|
341
|
+
return;
|
|
342
|
+
if (!Array.isArray(agents)) {
|
|
343
|
+
errors.push(`${path} must be an array`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (agents.length < 3)
|
|
347
|
+
errors.push(`${path} must contain at least 3 agents`);
|
|
348
|
+
if (agents.length % 2 === 0)
|
|
349
|
+
errors.push(`${path} must contain an odd number of agents`);
|
|
350
|
+
agents.forEach((agent, index) => {
|
|
351
|
+
if (!agent || typeof agent !== "object") {
|
|
352
|
+
errors.push(`${path}[${index}] must be an object`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
validateKnownKeys(agent, `${path}[${index}]`, TRIAGE_AGENT_KEYS, errors);
|
|
356
|
+
if (!agent.model)
|
|
357
|
+
errors.push(`${path}[${index}].model is required`);
|
|
358
|
+
validateString(agent.model, `${path}[${index}].model`, errors);
|
|
359
|
+
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
360
|
+
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
361
|
+
if (agent.options != null && !isPlainObject(agent.options))
|
|
362
|
+
errors.push(`${path}[${index}].options must be an object`);
|
|
363
|
+
validatePermissionConfig(agent.permissions, `${path}[${index}].permissions`, errors);
|
|
364
|
+
if (agent.id) {
|
|
365
|
+
if (!validateReviewerId(agent.id)) {
|
|
366
|
+
errors.push(`${path}[${index}].id may contain only letters, numbers, underscores, and hyphens`);
|
|
367
|
+
}
|
|
368
|
+
if (RESERVED_REVIEWER_KEYS.has(agent.id)) {
|
|
369
|
+
errors.push(`${path}[${index}].id is reserved: ${agent.id}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
230
374
|
function validateResolvedReviewers(reviewers, path, errors) {
|
|
231
375
|
const keys = new Set();
|
|
232
376
|
const accounts = new Set();
|
|
@@ -239,6 +383,14 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
239
383
|
accounts.add(reviewer.account);
|
|
240
384
|
}
|
|
241
385
|
}
|
|
386
|
+
function validateResolvedAgentKeys(agents, path, errors) {
|
|
387
|
+
const keys = new Set();
|
|
388
|
+
for (const agent of agents) {
|
|
389
|
+
if (keys.has(agent.key))
|
|
390
|
+
errors.push(`${path} has duplicate agent key: ${agent.key}`);
|
|
391
|
+
keys.add(agent.key);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
242
394
|
function validateEditor(editor, path, errors, catalog) {
|
|
243
395
|
if (!editor)
|
|
244
396
|
return;
|
|
@@ -282,6 +434,40 @@ function validateEditor(editor, path, errors, catalog) {
|
|
|
282
434
|
}
|
|
283
435
|
}
|
|
284
436
|
}
|
|
437
|
+
function validateTriageCreator(creator, path, errors, catalog) {
|
|
438
|
+
if (!creator)
|
|
439
|
+
return;
|
|
440
|
+
if (!isPlainObject(creator)) {
|
|
441
|
+
errors.push(`${path} must be an object`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
validateKnownKeys(creator, path, TRIAGE_CREATOR_KEYS, errors);
|
|
445
|
+
if (!creator.model)
|
|
446
|
+
errors.push(`${path}.model is required`);
|
|
447
|
+
validateString(creator.account, `${path}.account`, errors);
|
|
448
|
+
validateString(creator.model, `${path}.model`, errors);
|
|
449
|
+
validateString(creator.persona, `${path}.persona`, errors);
|
|
450
|
+
validateModel(creator.model, `${path}.model`, errors, catalog);
|
|
451
|
+
if (creator.options != null && !isPlainObject(creator.options)) {
|
|
452
|
+
errors.push(`${path}.options must be an object`);
|
|
453
|
+
}
|
|
454
|
+
validatePermissionConfig(creator.permissions, `${path}.permissions`, errors);
|
|
455
|
+
const author = creator.author;
|
|
456
|
+
if (!author || !isPlainObject(author)) {
|
|
457
|
+
if (author != null)
|
|
458
|
+
errors.push(`${path}.author must be an object`);
|
|
459
|
+
errors.push(`${path}.author.name is required`);
|
|
460
|
+
errors.push(`${path}.author.email is required`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
validateKnownKeys(author, `${path}.author`, AUTHOR_KEYS, errors);
|
|
464
|
+
if (!author.name)
|
|
465
|
+
errors.push(`${path}.author.name is required`);
|
|
466
|
+
validateString(author.name, `${path}.author.name`, errors);
|
|
467
|
+
if (!author.email)
|
|
468
|
+
errors.push(`${path}.author.email is required`);
|
|
469
|
+
validateString(author.email, `${path}.author.email`, errors);
|
|
470
|
+
}
|
|
285
471
|
function validateMerge(config, errors, options) {
|
|
286
472
|
const merge = config.merge;
|
|
287
473
|
if (options.requireGithub ?? true) {
|
|
@@ -407,6 +593,45 @@ function validateStringArray(value, path, errors) {
|
|
|
407
593
|
errors.push(`${path}[${index}] must be a string`);
|
|
408
594
|
});
|
|
409
595
|
}
|
|
596
|
+
function validateTriageCategories(categories, path, errors) {
|
|
597
|
+
if (categories == null)
|
|
598
|
+
return;
|
|
599
|
+
if (!Array.isArray(categories)) {
|
|
600
|
+
errors.push(`${path} must be an array`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const ids = new Set();
|
|
604
|
+
categories.forEach((item, index) => {
|
|
605
|
+
const itemPath = `${path}[${index}]`;
|
|
606
|
+
if (!isPlainObject(item)) {
|
|
607
|
+
errors.push(`${itemPath} must be an object`);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const category = item;
|
|
611
|
+
validateKnownKeys(category, itemPath, TRIAGE_CATEGORY_KEYS, errors);
|
|
612
|
+
if (!category.id) {
|
|
613
|
+
errors.push(`${itemPath}.id is required`);
|
|
614
|
+
}
|
|
615
|
+
else if (typeof category.id !== "string") {
|
|
616
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
617
|
+
}
|
|
618
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(category.id)) {
|
|
619
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
620
|
+
}
|
|
621
|
+
else if (RESERVED_TRIAGE_CATEGORY_IDS.has(category.id)) {
|
|
622
|
+
errors.push(`${itemPath}.id is reserved: ${category.id}`);
|
|
623
|
+
}
|
|
624
|
+
else if (ids.has(category.id)) {
|
|
625
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
ids.add(category.id);
|
|
629
|
+
}
|
|
630
|
+
validateStringArray(category.labels, `${itemPath}.labels`, errors);
|
|
631
|
+
validateStringArray(category.types, `${itemPath}.types`, errors);
|
|
632
|
+
validateString(category.description, `${itemPath}.description`, errors);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
410
635
|
function validateSafety(config, errors) {
|
|
411
636
|
const safety = config.review?.safety;
|
|
412
637
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -436,12 +661,70 @@ function validatePromptObject(prompts, path, keys, errors) {
|
|
|
436
661
|
errors.push(`${path}.${key} must be a string`);
|
|
437
662
|
}
|
|
438
663
|
}
|
|
664
|
+
function validateTriage(config, errors, options) {
|
|
665
|
+
const triage = config.triage;
|
|
666
|
+
if (!triage)
|
|
667
|
+
return;
|
|
668
|
+
if (!isPlainObject(triage)) {
|
|
669
|
+
errors.push("triage must be an object");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
673
|
+
const automation = triage.automation;
|
|
674
|
+
const concurrency = triage.concurrency;
|
|
675
|
+
const safety = triage.safety;
|
|
676
|
+
if (!triage.account)
|
|
677
|
+
errors.push("triage.account is required");
|
|
678
|
+
validateString(triage.account, "triage.account", errors);
|
|
679
|
+
if (!triage.agents)
|
|
680
|
+
errors.push("triage.agents is required");
|
|
681
|
+
validateTriageAgentList(triage.agents, "triage.agents", errors, options.modelCatalog);
|
|
682
|
+
if (Array.isArray(triage.agents)) {
|
|
683
|
+
validateResolvedAgentKeys(resolveAgents(config).triage ?? [], "triage.resolvedAgents", errors);
|
|
684
|
+
}
|
|
685
|
+
validateTriageCreator(triage.creator, "triage.creator", errors, options.modelCatalog);
|
|
686
|
+
if (automation?.create && !triage.creator)
|
|
687
|
+
errors.push("triage.creator is required when triage.automation.create is true");
|
|
688
|
+
if (automation != null && !isPlainObject(automation)) {
|
|
689
|
+
errors.push("triage.automation must be an object");
|
|
690
|
+
}
|
|
691
|
+
validateKnownKeys(automation, "triage.automation", TRIAGE_AUTOMATION_KEYS, errors);
|
|
692
|
+
validateBoolean(automation?.close, "triage.automation.close", errors);
|
|
693
|
+
validateBoolean(automation?.create, "triage.automation.create", errors);
|
|
694
|
+
validateBoolean(automation?.merge, "triage.automation.merge", errors);
|
|
695
|
+
validateBoolean(automation?.review, "triage.automation.review", errors);
|
|
696
|
+
validateStringArray(automation?.clear, "triage.automation.clear", errors);
|
|
697
|
+
if (automation?.review && !automation.create) {
|
|
698
|
+
errors.push("triage.automation.review requires triage.automation.create to be true");
|
|
699
|
+
}
|
|
700
|
+
if (automation?.merge && !automation.create) {
|
|
701
|
+
errors.push("triage.automation.merge requires triage.automation.create to be true");
|
|
702
|
+
}
|
|
703
|
+
validateKnownKeys(concurrency, "triage.concurrency", TRIAGE_CONCURRENCY_KEYS, errors);
|
|
704
|
+
if (concurrency?.runs != null &&
|
|
705
|
+
(typeof concurrency.runs !== "number" ||
|
|
706
|
+
!Number.isInteger(concurrency.runs) ||
|
|
707
|
+
concurrency.runs < 1)) {
|
|
708
|
+
errors.push("triage.concurrency.runs must be a positive integer");
|
|
709
|
+
}
|
|
710
|
+
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
711
|
+
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
712
|
+
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
713
|
+
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
|
714
|
+
validateStringArray(safety?.allowMentionRoles, "triage.safety.allowMentionRoles", errors);
|
|
715
|
+
validateStringArray(safety?.blockedLabels, "triage.safety.blockedLabels", errors);
|
|
716
|
+
validateStringArray(safety?.requiredLabels, "triage.safety.requiredLabels", errors);
|
|
717
|
+
validateString(triage.output, "triage.output", errors);
|
|
718
|
+
validateString(triage.worktree, "triage.worktree", errors);
|
|
719
|
+
}
|
|
439
720
|
async function validatePrompts(config, errors, directory) {
|
|
440
721
|
validatePromptObject(config.review?.prompts, "review.prompts", REVIEW_PROMPT_KEYS, errors);
|
|
441
722
|
validatePromptObject(config.merge?.prompts, "merge.prompts", MERGE_PROMPT_KEYS, errors);
|
|
723
|
+
validatePromptObject(config.triage?.prompts, "triage.prompts", TRIAGE_PROMPT_KEYS, errors);
|
|
442
724
|
const promptEntries = [
|
|
443
725
|
...Object.entries(config.review?.prompts ?? {}).map(([key, value]) => [`review.prompts.${key}`, value]),
|
|
444
726
|
...Object.entries(config.merge?.prompts ?? {}).map(([key, value]) => [`merge.prompts.${key}`, value]),
|
|
727
|
+
...Object.entries(config.triage?.prompts ?? {}).map(([key, value]) => [`triage.prompts.${key}`, value]),
|
|
445
728
|
];
|
|
446
729
|
await Promise.all(promptEntries.map(async ([path, value]) => {
|
|
447
730
|
if (typeof value !== "string")
|
|
@@ -464,6 +747,10 @@ async function validateAuth(config, exec, errors) {
|
|
|
464
747
|
accounts.add(reviewer.account);
|
|
465
748
|
if (agents.editor)
|
|
466
749
|
accounts.add(agents.editor.account);
|
|
750
|
+
if (config.triage?.account)
|
|
751
|
+
accounts.add(config.triage.account);
|
|
752
|
+
if (agents.triageCreator?.account)
|
|
753
|
+
accounts.add(agents.triageCreator.account);
|
|
467
754
|
await Promise.all([...accounts].filter(Boolean).map(async (account) => {
|
|
468
755
|
try {
|
|
469
756
|
await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`);
|
|
@@ -475,9 +762,31 @@ async function validateAuth(config, exec, errors) {
|
|
|
475
762
|
}
|
|
476
763
|
async function fetchPermissions(config, exec, account) {
|
|
477
764
|
const token = (await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`)).trim();
|
|
478
|
-
const raw = await exec(`
|
|
765
|
+
const raw = await exec(`gh api${ghHostOption(config)} repos/${config.github?.owner}/${config.github?.repo} --jq .permissions`, { env: { GH_TOKEN: token } });
|
|
479
766
|
return JSON.parse(raw);
|
|
480
767
|
}
|
|
768
|
+
async function validateWorktreeConfig(config, exec, options, errors) {
|
|
769
|
+
const agents = resolveAgents(config);
|
|
770
|
+
const checkEditor = Boolean(agents.editor && (options.requireEditor || options.requireWorktreeConfig));
|
|
771
|
+
const checkTriageCreator = Boolean(config.triage?.automation?.create &&
|
|
772
|
+
agents.triageCreator &&
|
|
773
|
+
(options.requireTriage || options.requireWorktreeConfig));
|
|
774
|
+
if (!checkEditor && !checkTriageCreator)
|
|
775
|
+
return;
|
|
776
|
+
if (!exec)
|
|
777
|
+
return;
|
|
778
|
+
const error = "git config extensions.worktreeConfig must be true when editor or triage PR creator is configured";
|
|
779
|
+
try {
|
|
780
|
+
const value = (await exec("git config --bool --get extensions.worktreeConfig"))
|
|
781
|
+
.trim()
|
|
782
|
+
.toLowerCase();
|
|
783
|
+
if (value !== "true")
|
|
784
|
+
errors.push(error);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
errors.push(error);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
481
790
|
async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
482
791
|
if (!config.github?.owner || !config.github.repo)
|
|
483
792
|
return;
|
|
@@ -493,6 +802,29 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
493
802
|
warnings.push(`Could not validate repository permissions for GitHub account: ${reviewer.account} (${error.message})`);
|
|
494
803
|
}
|
|
495
804
|
}));
|
|
805
|
+
if (config.triage?.account) {
|
|
806
|
+
try {
|
|
807
|
+
const permissions = await fetchPermissions(config, exec, config.triage.account);
|
|
808
|
+
if (!permissions.pull) {
|
|
809
|
+
errors.push(`GitHub account cannot read repository for issue triage: ${config.triage.account}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${config.triage.account} (${error.message})`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (agents.triageCreator?.account &&
|
|
817
|
+
agents.triageCreator.account !== config.triage?.account) {
|
|
818
|
+
try {
|
|
819
|
+
const permissions = await fetchPermissions(config, exec, agents.triageCreator.account);
|
|
820
|
+
if (!permissions.push) {
|
|
821
|
+
errors.push(`GitHub account cannot push to repository for triage PR creation: ${agents.triageCreator.account}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${agents.triageCreator.account} (${error.message})`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
496
828
|
if (!agents.editor)
|
|
497
829
|
return;
|
|
498
830
|
try {
|
|
@@ -510,6 +842,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
510
842
|
const warnings = [];
|
|
511
843
|
if (!config || typeof config !== "object")
|
|
512
844
|
errors.push("config must be an object");
|
|
845
|
+
expandAgentRefs(config, errors);
|
|
513
846
|
if (config && typeof config === "object")
|
|
514
847
|
validateJsonSchema(config, errors);
|
|
515
848
|
validateKnownKeys(config, "config", CONFIG_KEYS, errors);
|
|
@@ -522,10 +855,10 @@ export async function validateConfig(config, options = {}) {
|
|
|
522
855
|
validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
|
|
523
856
|
validatePermissionConfig(config.agents?.permissions, "agents.permissions", errors);
|
|
524
857
|
}
|
|
525
|
-
if (!config.review) {
|
|
858
|
+
if ((options.requireReview ?? true) && !config.review) {
|
|
526
859
|
errors.push("review is required");
|
|
527
860
|
}
|
|
528
|
-
else {
|
|
861
|
+
else if (config.review) {
|
|
529
862
|
if (!isPlainObject(config.review)) {
|
|
530
863
|
errors.push("review must be an object");
|
|
531
864
|
}
|
|
@@ -539,6 +872,9 @@ export async function validateConfig(config, options = {}) {
|
|
|
539
872
|
validateResolvedReviewers(resolveAgents(config).reviewers, "review.resolvedAgents", errors);
|
|
540
873
|
}
|
|
541
874
|
}
|
|
875
|
+
if (options.requireTriage && !config.triage) {
|
|
876
|
+
errors.push("triage is required");
|
|
877
|
+
}
|
|
542
878
|
validateMerge(config, errors, options);
|
|
543
879
|
validateReviewMerge(config, errors);
|
|
544
880
|
validateAutomation(config, errors);
|
|
@@ -546,6 +882,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
546
882
|
validateChecks(config, errors);
|
|
547
883
|
validateConcurrency(config, errors);
|
|
548
884
|
validateSafety(config, errors);
|
|
885
|
+
validateTriage(config, errors, options);
|
|
549
886
|
await validatePrompts(config, errors, options.directory);
|
|
550
887
|
if (config.output != null && !isPlainObject(config.output)) {
|
|
551
888
|
errors.push("output must be an object");
|
|
@@ -560,6 +897,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
560
897
|
}
|
|
561
898
|
validateString(config.review?.output, "review.output", errors);
|
|
562
899
|
validateString(config.review?.worktree, "review.worktree", errors);
|
|
900
|
+
await validateWorktreeConfig(config, options.exec, options, errors);
|
|
563
901
|
if (options.checkAuth && !errors.length) {
|
|
564
902
|
if (!options.exec) {
|
|
565
903
|
errors.push("validateConfig requires exec when checkAuth is true");
|