opencode-magi 0.2.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 +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 +290 -3
- package/dist/config/worktree.js +8 -2
- package/dist/github/commands.js +343 -15
- 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 +73 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +16 -3
- package/dist/orchestrator/report.js +15 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +49 -9
- 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 +162 -1
- package/dist/prompts/contracts.js +119 -12
- package/dist/prompts/output.js +149 -14
- 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 +127 -2
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
|
}
|
|
@@ -227,6 +286,41 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
227
286
|
}
|
|
228
287
|
});
|
|
229
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
|
+
}
|
|
230
324
|
function validateResolvedReviewers(reviewers, path, errors) {
|
|
231
325
|
const keys = new Set();
|
|
232
326
|
const accounts = new Set();
|
|
@@ -239,6 +333,14 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
239
333
|
accounts.add(reviewer.account);
|
|
240
334
|
}
|
|
241
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
|
+
}
|
|
242
344
|
function validateEditor(editor, path, errors, catalog) {
|
|
243
345
|
if (!editor)
|
|
244
346
|
return;
|
|
@@ -282,6 +384,40 @@ function validateEditor(editor, path, errors, catalog) {
|
|
|
282
384
|
}
|
|
283
385
|
}
|
|
284
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
|
+
}
|
|
285
421
|
function validateMerge(config, errors, options) {
|
|
286
422
|
const merge = config.merge;
|
|
287
423
|
if (options.requireGithub ?? true) {
|
|
@@ -407,6 +543,45 @@ function validateStringArray(value, path, errors) {
|
|
|
407
543
|
errors.push(`${path}[${index}] must be a string`);
|
|
408
544
|
});
|
|
409
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
|
+
}
|
|
410
585
|
function validateSafety(config, errors) {
|
|
411
586
|
const safety = config.review?.safety;
|
|
412
587
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -436,12 +611,70 @@ function validatePromptObject(prompts, path, keys, errors) {
|
|
|
436
611
|
errors.push(`${path}.${key} must be a string`);
|
|
437
612
|
}
|
|
438
613
|
}
|
|
614
|
+
function validateTriage(config, errors, options) {
|
|
615
|
+
const triage = config.triage;
|
|
616
|
+
if (!triage)
|
|
617
|
+
return;
|
|
618
|
+
if (!isPlainObject(triage)) {
|
|
619
|
+
errors.push("triage must be an object");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
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
|
+
}
|
|
439
670
|
async function validatePrompts(config, errors, directory) {
|
|
440
671
|
validatePromptObject(config.review?.prompts, "review.prompts", REVIEW_PROMPT_KEYS, errors);
|
|
441
672
|
validatePromptObject(config.merge?.prompts, "merge.prompts", MERGE_PROMPT_KEYS, errors);
|
|
673
|
+
validatePromptObject(config.triage?.prompts, "triage.prompts", TRIAGE_PROMPT_KEYS, errors);
|
|
442
674
|
const promptEntries = [
|
|
443
675
|
...Object.entries(config.review?.prompts ?? {}).map(([key, value]) => [`review.prompts.${key}`, value]),
|
|
444
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]),
|
|
445
678
|
];
|
|
446
679
|
await Promise.all(promptEntries.map(async ([path, value]) => {
|
|
447
680
|
if (typeof value !== "string")
|
|
@@ -464,6 +697,10 @@ async function validateAuth(config, exec, errors) {
|
|
|
464
697
|
accounts.add(reviewer.account);
|
|
465
698
|
if (agents.editor)
|
|
466
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);
|
|
467
704
|
await Promise.all([...accounts].filter(Boolean).map(async (account) => {
|
|
468
705
|
try {
|
|
469
706
|
await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`);
|
|
@@ -475,9 +712,31 @@ async function validateAuth(config, exec, errors) {
|
|
|
475
712
|
}
|
|
476
713
|
async function fetchPermissions(config, exec, account) {
|
|
477
714
|
const token = (await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`)).trim();
|
|
478
|
-
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 } });
|
|
479
716
|
return JSON.parse(raw);
|
|
480
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
|
+
}
|
|
481
740
|
async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
482
741
|
if (!config.github?.owner || !config.github.repo)
|
|
483
742
|
return;
|
|
@@ -493,6 +752,29 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
493
752
|
warnings.push(`Could not validate repository permissions for GitHub account: ${reviewer.account} (${error.message})`);
|
|
494
753
|
}
|
|
495
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
|
+
}
|
|
496
778
|
if (!agents.editor)
|
|
497
779
|
return;
|
|
498
780
|
try {
|
|
@@ -522,10 +804,10 @@ export async function validateConfig(config, options = {}) {
|
|
|
522
804
|
validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
|
|
523
805
|
validatePermissionConfig(config.agents?.permissions, "agents.permissions", errors);
|
|
524
806
|
}
|
|
525
|
-
if (!config.review) {
|
|
807
|
+
if ((options.requireReview ?? true) && !config.review) {
|
|
526
808
|
errors.push("review is required");
|
|
527
809
|
}
|
|
528
|
-
else {
|
|
810
|
+
else if (config.review) {
|
|
529
811
|
if (!isPlainObject(config.review)) {
|
|
530
812
|
errors.push("review must be an object");
|
|
531
813
|
}
|
|
@@ -539,6 +821,9 @@ export async function validateConfig(config, options = {}) {
|
|
|
539
821
|
validateResolvedReviewers(resolveAgents(config).reviewers, "review.resolvedAgents", errors);
|
|
540
822
|
}
|
|
541
823
|
}
|
|
824
|
+
if (options.requireTriage && !config.triage) {
|
|
825
|
+
errors.push("triage is required");
|
|
826
|
+
}
|
|
542
827
|
validateMerge(config, errors, options);
|
|
543
828
|
validateReviewMerge(config, errors);
|
|
544
829
|
validateAutomation(config, errors);
|
|
@@ -546,6 +831,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
546
831
|
validateChecks(config, errors);
|
|
547
832
|
validateConcurrency(config, errors);
|
|
548
833
|
validateSafety(config, errors);
|
|
834
|
+
validateTriage(config, errors, options);
|
|
549
835
|
await validatePrompts(config, errors, options.directory);
|
|
550
836
|
if (config.output != null && !isPlainObject(config.output)) {
|
|
551
837
|
errors.push("output must be an object");
|
|
@@ -560,6 +846,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
560
846
|
}
|
|
561
847
|
validateString(config.review?.output, "review.output", errors);
|
|
562
848
|
validateString(config.review?.worktree, "review.worktree", errors);
|
|
849
|
+
await validateWorktreeConfig(config, options.exec, options, errors);
|
|
563
850
|
if (options.checkAuth && !errors.length) {
|
|
564
851
|
if (!options.exec) {
|
|
565
852
|
errors.push("validateConfig requires exec when checkAuth is true");
|
package/dist/config/worktree.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
2
|
const DEFAULT_WORKTREE_DIRS = {
|
|
3
|
+
issue: ".magi/worktrees/issue",
|
|
3
4
|
pr: ".magi/worktrees/pr",
|
|
4
5
|
};
|
|
5
6
|
function resolvePath(directory, path) {
|
|
6
7
|
return isAbsolute(path) ? path : join(directory, path);
|
|
7
8
|
}
|
|
8
9
|
export function worktreeBaseDir(directory, config, kind) {
|
|
9
|
-
return resolvePath(directory,
|
|
10
|
+
return resolvePath(directory, kind === "issue"
|
|
11
|
+
? (config.triage?.worktree ?? DEFAULT_WORKTREE_DIRS[kind])
|
|
12
|
+
: (config.review?.worktree ?? DEFAULT_WORKTREE_DIRS[kind]));
|
|
10
13
|
}
|
|
11
14
|
export function worktreeBaseDirs(directory, config = {}) {
|
|
12
|
-
return [
|
|
15
|
+
return [
|
|
16
|
+
worktreeBaseDir(directory, config, "pr"),
|
|
17
|
+
worktreeBaseDir(directory, config, "issue"),
|
|
18
|
+
];
|
|
13
19
|
}
|