opencode-magi 0.0.0-dev-20260519144738 → 0.0.0-dev-20260520030110
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 +2 -0
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +59 -0
- package/dist/config/validate.js +226 -2
- package/dist/config/worktree.js +8 -2
- package/dist/github/commands.js +93 -0
- package/dist/index.js +67 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/triage.js +420 -0
- package/dist/prompts/compose.js +82 -1
- package/dist/prompts/contracts.js +48 -0
- package/dist/prompts/output.js +68 -0
- package/dist/prompts/templates/triage/action.md +5 -0
- package/dist/prompts/templates/triage/bug.md +7 -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-pr.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/feature.md +7 -0
- package/dist/prompts/templates/triage/kind.md +7 -0
- package/dist/prompts/templates/triage/question.md +5 -0
- package/dist/prompts/templates/triage/reconsider.md +5 -0
- package/package.json +1 -1
- package/schema.json +122 -1
package/README.md
CHANGED
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
|
@@ -7,6 +7,9 @@ const DEFAULT_EDITOR_PERMISSION = mergePermissions(DEFAULT_COMMON_PERMISSION, ed
|
|
|
7
7
|
export function reviewerKey(reviewer, index) {
|
|
8
8
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
9
9
|
}
|
|
10
|
+
export function triageAgentKey(agent, index) {
|
|
11
|
+
return agent.id ?? `triage-${index + 1}`;
|
|
12
|
+
}
|
|
10
13
|
export function validateReviewerId(id) {
|
|
11
14
|
return ID_PATTERN.test(id);
|
|
12
15
|
}
|
|
@@ -49,9 +52,16 @@ export function resolveReviewerPermission(agents, reviewer) {
|
|
|
49
52
|
export function resolveEditorPermission(agents, editor) {
|
|
50
53
|
return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), editor.permissions);
|
|
51
54
|
}
|
|
55
|
+
export function resolveTriageAgentPermission(agents, agent) {
|
|
56
|
+
return mergePermissions(mergePermissions(DEFAULT_REVIEWER_PERMISSION, agents.permissions), agent.permissions);
|
|
57
|
+
}
|
|
58
|
+
export function resolveTriageCreatorPermission(agents, creator) {
|
|
59
|
+
return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), creator.permissions);
|
|
60
|
+
}
|
|
52
61
|
export function resolveAgents(config) {
|
|
53
62
|
const agents = config.agents ?? {};
|
|
54
63
|
const editor = config.merge?.editor;
|
|
64
|
+
const creator = config.triage?.creator;
|
|
55
65
|
return {
|
|
56
66
|
editor: editor
|
|
57
67
|
? {
|
|
@@ -65,6 +75,19 @@ export function resolveAgents(config) {
|
|
|
65
75
|
index,
|
|
66
76
|
permission: resolveReviewerPermission(agents, reviewer),
|
|
67
77
|
})),
|
|
78
|
+
triage: (config.triage?.agents ?? []).map((agent, index) => ({
|
|
79
|
+
...agent,
|
|
80
|
+
key: triageAgentKey(agent, index),
|
|
81
|
+
index,
|
|
82
|
+
permission: resolveTriageAgentPermission(agents, agent),
|
|
83
|
+
})),
|
|
84
|
+
triageCreator: creator
|
|
85
|
+
? {
|
|
86
|
+
...creator,
|
|
87
|
+
account: creator.account ?? config.triage?.account ?? "",
|
|
88
|
+
permission: resolveTriageCreatorPermission(agents, creator),
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
68
91
|
};
|
|
69
92
|
}
|
|
70
93
|
export function resolveRepository(config) {
|
|
@@ -127,5 +150,41 @@ export function resolveRepository(config) {
|
|
|
127
150
|
maxChangedFiles: config.review?.safety?.maxChangedFiles,
|
|
128
151
|
requiredLabels: config.review?.safety?.requiredLabels ?? [],
|
|
129
152
|
},
|
|
153
|
+
triage: {
|
|
154
|
+
account: config.triage?.account,
|
|
155
|
+
automation: {
|
|
156
|
+
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
157
|
+
close: config.triage?.automation?.close ?? false,
|
|
158
|
+
pr: config.triage?.automation?.pr ?? false,
|
|
159
|
+
},
|
|
160
|
+
concurrency: {
|
|
161
|
+
runs: config.triage?.concurrency?.runs ?? 3,
|
|
162
|
+
},
|
|
163
|
+
kind: {
|
|
164
|
+
bug: {
|
|
165
|
+
label: config.triage?.kind?.bug?.label ?? ["bug"],
|
|
166
|
+
type: config.triage?.kind?.bug?.type ?? ["Bug"],
|
|
167
|
+
},
|
|
168
|
+
feature: {
|
|
169
|
+
label: config.triage?.kind?.feature?.label ?? ["enhancement"],
|
|
170
|
+
type: config.triage?.kind?.feature?.type ?? ["Feature"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
output: config.triage?.output,
|
|
174
|
+
prompts: config.triage?.prompts ?? {},
|
|
175
|
+
safety: {
|
|
176
|
+
allowAuthors: config.triage?.safety?.allowAuthors ?? [],
|
|
177
|
+
allowMentionActors: config.triage?.safety?.allowMentionActors ?? [],
|
|
178
|
+
allowMentionRoles: config.triage?.safety?.allowMentionRoles ?? [
|
|
179
|
+
"AUTHOR",
|
|
180
|
+
"OWNER",
|
|
181
|
+
"MEMBER",
|
|
182
|
+
"COLLABORATOR",
|
|
183
|
+
],
|
|
184
|
+
blockedLabels: config.triage?.safety?.blockedLabels ?? [],
|
|
185
|
+
requiredLabels: config.triage?.safety?.requiredLabels ?? ["triage"],
|
|
186
|
+
},
|
|
187
|
+
worktree: config.triage?.worktree,
|
|
188
|
+
},
|
|
130
189
|
};
|
|
131
190
|
}
|
package/dist/config/validate.js
CHANGED
|
@@ -18,6 +18,7 @@ const CONFIG_KEYS = new Set([
|
|
|
18
18
|
"merge",
|
|
19
19
|
"output",
|
|
20
20
|
"review",
|
|
21
|
+
"triage",
|
|
21
22
|
]);
|
|
22
23
|
const AGENTS_KEYS = new Set(["permissions"]);
|
|
23
24
|
const REVIEWER_KEYS = new Set([
|
|
@@ -36,6 +37,21 @@ const EDITOR_KEYS = new Set([
|
|
|
36
37
|
"permissions",
|
|
37
38
|
"persona",
|
|
38
39
|
]);
|
|
40
|
+
const TRIAGE_AGENT_KEYS = new Set([
|
|
41
|
+
"id",
|
|
42
|
+
"model",
|
|
43
|
+
"options",
|
|
44
|
+
"permissions",
|
|
45
|
+
"persona",
|
|
46
|
+
]);
|
|
47
|
+
const TRIAGE_CREATOR_KEYS = new Set([
|
|
48
|
+
"account",
|
|
49
|
+
"author",
|
|
50
|
+
"model",
|
|
51
|
+
"options",
|
|
52
|
+
"permissions",
|
|
53
|
+
"persona",
|
|
54
|
+
]);
|
|
39
55
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
40
56
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
41
57
|
const REVIEW_KEYS = new Set([
|
|
@@ -56,6 +72,18 @@ const MERGE_KEYS = new Set([
|
|
|
56
72
|
"maxThreadResolutionCycles",
|
|
57
73
|
"prompts",
|
|
58
74
|
]);
|
|
75
|
+
const TRIAGE_KEYS = new Set([
|
|
76
|
+
"account",
|
|
77
|
+
"agents",
|
|
78
|
+
"automation",
|
|
79
|
+
"concurrency",
|
|
80
|
+
"creator",
|
|
81
|
+
"kind",
|
|
82
|
+
"output",
|
|
83
|
+
"prompts",
|
|
84
|
+
"safety",
|
|
85
|
+
"worktree",
|
|
86
|
+
]);
|
|
59
87
|
const REVIEW_MERGE_KEYS = new Set([
|
|
60
88
|
"approvalPolicy",
|
|
61
89
|
"auto",
|
|
@@ -69,6 +97,17 @@ const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
|
69
97
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
70
98
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
71
99
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
100
|
+
const TRIAGE_AUTOMATION_KEYS = new Set(["clear", "close", "pr"]);
|
|
101
|
+
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
102
|
+
const TRIAGE_KIND_KEYS = new Set(["bug", "feature"]);
|
|
103
|
+
const TRIAGE_KIND_RULE_KEYS = new Set(["label", "type"]);
|
|
104
|
+
const TRIAGE_SAFETY_KEYS = new Set([
|
|
105
|
+
"allowAuthors",
|
|
106
|
+
"allowMentionActors",
|
|
107
|
+
"allowMentionRoles",
|
|
108
|
+
"blockedLabels",
|
|
109
|
+
"requiredLabels",
|
|
110
|
+
]);
|
|
72
111
|
const SAFETY_KEYS = new Set([
|
|
73
112
|
"allowAuthors",
|
|
74
113
|
"blockedPaths",
|
|
@@ -88,6 +127,19 @@ const MERGE_PROMPT_KEYS = new Set([
|
|
|
88
127
|
"edit",
|
|
89
128
|
"editGuidelines",
|
|
90
129
|
]);
|
|
130
|
+
const TRIAGE_PROMPT_KEYS = new Set([
|
|
131
|
+
"action",
|
|
132
|
+
"bug",
|
|
133
|
+
"comment",
|
|
134
|
+
"commentClassification",
|
|
135
|
+
"createPr",
|
|
136
|
+
"duplicate",
|
|
137
|
+
"existingPr",
|
|
138
|
+
"feature",
|
|
139
|
+
"kind",
|
|
140
|
+
"question",
|
|
141
|
+
"reconsider",
|
|
142
|
+
]);
|
|
91
143
|
function githubHost(config) {
|
|
92
144
|
return config.github?.host ?? "github.com";
|
|
93
145
|
}
|
|
@@ -227,6 +279,41 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
227
279
|
}
|
|
228
280
|
});
|
|
229
281
|
}
|
|
282
|
+
function validateTriageAgentList(agents, path, errors, catalog) {
|
|
283
|
+
if (agents == null)
|
|
284
|
+
return;
|
|
285
|
+
if (!Array.isArray(agents)) {
|
|
286
|
+
errors.push(`${path} must be an array`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (agents.length < 3)
|
|
290
|
+
errors.push(`${path} must contain at least 3 agents`);
|
|
291
|
+
if (agents.length % 2 === 0)
|
|
292
|
+
errors.push(`${path} must contain an odd number of agents`);
|
|
293
|
+
agents.forEach((agent, index) => {
|
|
294
|
+
if (!agent || typeof agent !== "object") {
|
|
295
|
+
errors.push(`${path}[${index}] must be an object`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
validateKnownKeys(agent, `${path}[${index}]`, TRIAGE_AGENT_KEYS, errors);
|
|
299
|
+
if (!agent.model)
|
|
300
|
+
errors.push(`${path}[${index}].model is required`);
|
|
301
|
+
validateString(agent.model, `${path}[${index}].model`, errors);
|
|
302
|
+
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
303
|
+
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
304
|
+
if (agent.options != null && !isPlainObject(agent.options))
|
|
305
|
+
errors.push(`${path}[${index}].options must be an object`);
|
|
306
|
+
validatePermissionConfig(agent.permissions, `${path}[${index}].permissions`, errors);
|
|
307
|
+
if (agent.id) {
|
|
308
|
+
if (!validateReviewerId(agent.id)) {
|
|
309
|
+
errors.push(`${path}[${index}].id may contain only letters, numbers, underscores, and hyphens`);
|
|
310
|
+
}
|
|
311
|
+
if (RESERVED_REVIEWER_KEYS.has(agent.id)) {
|
|
312
|
+
errors.push(`${path}[${index}].id is reserved: ${agent.id}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
230
317
|
function validateResolvedReviewers(reviewers, path, errors) {
|
|
231
318
|
const keys = new Set();
|
|
232
319
|
const accounts = new Set();
|
|
@@ -239,6 +326,14 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
239
326
|
accounts.add(reviewer.account);
|
|
240
327
|
}
|
|
241
328
|
}
|
|
329
|
+
function validateResolvedAgentKeys(agents, path, errors) {
|
|
330
|
+
const keys = new Set();
|
|
331
|
+
for (const agent of agents) {
|
|
332
|
+
if (keys.has(agent.key))
|
|
333
|
+
errors.push(`${path} has duplicate agent key: ${agent.key}`);
|
|
334
|
+
keys.add(agent.key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
242
337
|
function validateEditor(editor, path, errors, catalog) {
|
|
243
338
|
if (!editor)
|
|
244
339
|
return;
|
|
@@ -282,6 +377,40 @@ function validateEditor(editor, path, errors, catalog) {
|
|
|
282
377
|
}
|
|
283
378
|
}
|
|
284
379
|
}
|
|
380
|
+
function validateTriageCreator(creator, path, errors, catalog) {
|
|
381
|
+
if (!creator)
|
|
382
|
+
return;
|
|
383
|
+
if (!isPlainObject(creator)) {
|
|
384
|
+
errors.push(`${path} must be an object`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
validateKnownKeys(creator, path, TRIAGE_CREATOR_KEYS, errors);
|
|
388
|
+
if (!creator.model)
|
|
389
|
+
errors.push(`${path}.model is required`);
|
|
390
|
+
validateString(creator.account, `${path}.account`, errors);
|
|
391
|
+
validateString(creator.model, `${path}.model`, errors);
|
|
392
|
+
validateString(creator.persona, `${path}.persona`, errors);
|
|
393
|
+
validateModel(creator.model, `${path}.model`, errors, catalog);
|
|
394
|
+
if (creator.options != null && !isPlainObject(creator.options)) {
|
|
395
|
+
errors.push(`${path}.options must be an object`);
|
|
396
|
+
}
|
|
397
|
+
validatePermissionConfig(creator.permissions, `${path}.permissions`, errors);
|
|
398
|
+
const author = creator.author;
|
|
399
|
+
if (!author || !isPlainObject(author)) {
|
|
400
|
+
if (author != null)
|
|
401
|
+
errors.push(`${path}.author must be an object`);
|
|
402
|
+
errors.push(`${path}.author.name is required`);
|
|
403
|
+
errors.push(`${path}.author.email is required`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
validateKnownKeys(author, `${path}.author`, AUTHOR_KEYS, errors);
|
|
407
|
+
if (!author.name)
|
|
408
|
+
errors.push(`${path}.author.name is required`);
|
|
409
|
+
validateString(author.name, `${path}.author.name`, errors);
|
|
410
|
+
if (!author.email)
|
|
411
|
+
errors.push(`${path}.author.email is required`);
|
|
412
|
+
validateString(author.email, `${path}.author.email`, errors);
|
|
413
|
+
}
|
|
285
414
|
function validateMerge(config, errors, options) {
|
|
286
415
|
const merge = config.merge;
|
|
287
416
|
if (options.requireGithub ?? true) {
|
|
@@ -407,6 +536,17 @@ function validateStringArray(value, path, errors) {
|
|
|
407
536
|
errors.push(`${path}[${index}] must be a string`);
|
|
408
537
|
});
|
|
409
538
|
}
|
|
539
|
+
function validateStringArrayObject(value, path, keys, errors) {
|
|
540
|
+
if (value == null)
|
|
541
|
+
return;
|
|
542
|
+
if (!isPlainObject(value)) {
|
|
543
|
+
errors.push(`${path} must be an object`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
validateKnownKeys(value, path, keys, errors);
|
|
547
|
+
for (const key of keys)
|
|
548
|
+
validateStringArray(value[key], `${path}.${key}`, errors);
|
|
549
|
+
}
|
|
410
550
|
function validateSafety(config, errors) {
|
|
411
551
|
const safety = config.review?.safety;
|
|
412
552
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -436,12 +576,65 @@ function validatePromptObject(prompts, path, keys, errors) {
|
|
|
436
576
|
errors.push(`${path}.${key} must be a string`);
|
|
437
577
|
}
|
|
438
578
|
}
|
|
579
|
+
function validateTriage(config, errors, options) {
|
|
580
|
+
const triage = config.triage;
|
|
581
|
+
if (!triage)
|
|
582
|
+
return;
|
|
583
|
+
if (!isPlainObject(triage)) {
|
|
584
|
+
errors.push("triage must be an object");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
588
|
+
const automation = triage.automation;
|
|
589
|
+
const concurrency = triage.concurrency;
|
|
590
|
+
const kind = triage.kind;
|
|
591
|
+
const safety = triage.safety;
|
|
592
|
+
if (!triage.account)
|
|
593
|
+
errors.push("triage.account is required");
|
|
594
|
+
validateString(triage.account, "triage.account", errors);
|
|
595
|
+
if (!triage.agents)
|
|
596
|
+
errors.push("triage.agents is required");
|
|
597
|
+
validateTriageAgentList(triage.agents, "triage.agents", errors, options.modelCatalog);
|
|
598
|
+
if (Array.isArray(triage.agents)) {
|
|
599
|
+
validateResolvedAgentKeys(resolveAgents(config).triage ?? [], "triage.resolvedAgents", errors);
|
|
600
|
+
}
|
|
601
|
+
validateTriageCreator(triage.creator, "triage.creator", errors, options.modelCatalog);
|
|
602
|
+
if (automation?.pr && !triage.creator)
|
|
603
|
+
errors.push("triage.creator is required when triage.automation.pr is true");
|
|
604
|
+
if (automation != null && !isPlainObject(automation)) {
|
|
605
|
+
errors.push("triage.automation must be an object");
|
|
606
|
+
}
|
|
607
|
+
validateKnownKeys(automation, "triage.automation", TRIAGE_AUTOMATION_KEYS, errors);
|
|
608
|
+
validateBoolean(automation?.close, "triage.automation.close", errors);
|
|
609
|
+
validateBoolean(automation?.pr, "triage.automation.pr", errors);
|
|
610
|
+
validateStringArray(automation?.clear, "triage.automation.clear", errors);
|
|
611
|
+
validateKnownKeys(concurrency, "triage.concurrency", TRIAGE_CONCURRENCY_KEYS, errors);
|
|
612
|
+
if (concurrency?.runs != null &&
|
|
613
|
+
(typeof concurrency.runs !== "number" ||
|
|
614
|
+
!Number.isInteger(concurrency.runs) ||
|
|
615
|
+
concurrency.runs < 1)) {
|
|
616
|
+
errors.push("triage.concurrency.runs must be a positive integer");
|
|
617
|
+
}
|
|
618
|
+
validateKnownKeys(kind, "triage.kind", TRIAGE_KIND_KEYS, errors);
|
|
619
|
+
validateStringArrayObject(kind?.bug, "triage.kind.bug", TRIAGE_KIND_RULE_KEYS, errors);
|
|
620
|
+
validateStringArrayObject(kind?.feature, "triage.kind.feature", TRIAGE_KIND_RULE_KEYS, errors);
|
|
621
|
+
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
622
|
+
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
623
|
+
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
|
624
|
+
validateStringArray(safety?.allowMentionRoles, "triage.safety.allowMentionRoles", errors);
|
|
625
|
+
validateStringArray(safety?.blockedLabels, "triage.safety.blockedLabels", errors);
|
|
626
|
+
validateStringArray(safety?.requiredLabels, "triage.safety.requiredLabels", errors);
|
|
627
|
+
validateString(triage.output, "triage.output", errors);
|
|
628
|
+
validateString(triage.worktree, "triage.worktree", errors);
|
|
629
|
+
}
|
|
439
630
|
async function validatePrompts(config, errors, directory) {
|
|
440
631
|
validatePromptObject(config.review?.prompts, "review.prompts", REVIEW_PROMPT_KEYS, errors);
|
|
441
632
|
validatePromptObject(config.merge?.prompts, "merge.prompts", MERGE_PROMPT_KEYS, errors);
|
|
633
|
+
validatePromptObject(config.triage?.prompts, "triage.prompts", TRIAGE_PROMPT_KEYS, errors);
|
|
442
634
|
const promptEntries = [
|
|
443
635
|
...Object.entries(config.review?.prompts ?? {}).map(([key, value]) => [`review.prompts.${key}`, value]),
|
|
444
636
|
...Object.entries(config.merge?.prompts ?? {}).map(([key, value]) => [`merge.prompts.${key}`, value]),
|
|
637
|
+
...Object.entries(config.triage?.prompts ?? {}).map(([key, value]) => [`triage.prompts.${key}`, value]),
|
|
445
638
|
];
|
|
446
639
|
await Promise.all(promptEntries.map(async ([path, value]) => {
|
|
447
640
|
if (typeof value !== "string")
|
|
@@ -464,6 +657,10 @@ async function validateAuth(config, exec, errors) {
|
|
|
464
657
|
accounts.add(reviewer.account);
|
|
465
658
|
if (agents.editor)
|
|
466
659
|
accounts.add(agents.editor.account);
|
|
660
|
+
if (config.triage?.account)
|
|
661
|
+
accounts.add(config.triage.account);
|
|
662
|
+
if (agents.triageCreator?.account)
|
|
663
|
+
accounts.add(agents.triageCreator.account);
|
|
467
664
|
await Promise.all([...accounts].filter(Boolean).map(async (account) => {
|
|
468
665
|
try {
|
|
469
666
|
await exec(`gh auth token${ghHostOption(config)} --user ${JSON.stringify(account)}`);
|
|
@@ -493,6 +690,29 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
493
690
|
warnings.push(`Could not validate repository permissions for GitHub account: ${reviewer.account} (${error.message})`);
|
|
494
691
|
}
|
|
495
692
|
}));
|
|
693
|
+
if (config.triage?.account) {
|
|
694
|
+
try {
|
|
695
|
+
const permissions = await fetchPermissions(config, exec, config.triage.account);
|
|
696
|
+
if (!permissions.pull) {
|
|
697
|
+
errors.push(`GitHub account cannot read repository for issue triage: ${config.triage.account}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${config.triage.account} (${error.message})`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (agents.triageCreator?.account &&
|
|
705
|
+
agents.triageCreator.account !== config.triage?.account) {
|
|
706
|
+
try {
|
|
707
|
+
const permissions = await fetchPermissions(config, exec, agents.triageCreator.account);
|
|
708
|
+
if (!permissions.push) {
|
|
709
|
+
errors.push(`GitHub account cannot push to repository for triage PR creation: ${agents.triageCreator.account}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${agents.triageCreator.account} (${error.message})`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
496
716
|
if (!agents.editor)
|
|
497
717
|
return;
|
|
498
718
|
try {
|
|
@@ -522,10 +742,10 @@ export async function validateConfig(config, options = {}) {
|
|
|
522
742
|
validateKnownKeys(config.agents, "agents", AGENTS_KEYS, errors);
|
|
523
743
|
validatePermissionConfig(config.agents?.permissions, "agents.permissions", errors);
|
|
524
744
|
}
|
|
525
|
-
if (!config.review) {
|
|
745
|
+
if ((options.requireReview ?? true) && !config.review) {
|
|
526
746
|
errors.push("review is required");
|
|
527
747
|
}
|
|
528
|
-
else {
|
|
748
|
+
else if (config.review) {
|
|
529
749
|
if (!isPlainObject(config.review)) {
|
|
530
750
|
errors.push("review must be an object");
|
|
531
751
|
}
|
|
@@ -539,6 +759,9 @@ export async function validateConfig(config, options = {}) {
|
|
|
539
759
|
validateResolvedReviewers(resolveAgents(config).reviewers, "review.resolvedAgents", errors);
|
|
540
760
|
}
|
|
541
761
|
}
|
|
762
|
+
if (options.requireTriage && !config.triage) {
|
|
763
|
+
errors.push("triage is required");
|
|
764
|
+
}
|
|
542
765
|
validateMerge(config, errors, options);
|
|
543
766
|
validateReviewMerge(config, errors);
|
|
544
767
|
validateAutomation(config, errors);
|
|
@@ -546,6 +769,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
546
769
|
validateChecks(config, errors);
|
|
547
770
|
validateConcurrency(config, errors);
|
|
548
771
|
validateSafety(config, errors);
|
|
772
|
+
validateTriage(config, errors, options);
|
|
549
773
|
await validatePrompts(config, errors, options.directory);
|
|
550
774
|
if (config.output != null && !isPlainObject(config.output)) {
|
|
551
775
|
errors.push("output must be an object");
|
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
|
}
|
package/dist/github/commands.js
CHANGED
|
@@ -97,6 +97,94 @@ export async function fetchPullRequest(exec, repository, pr) {
|
|
|
97
97
|
const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner`);
|
|
98
98
|
return JSON.parse(json);
|
|
99
99
|
}
|
|
100
|
+
export async function fetchIssue(exec, repository, issue) {
|
|
101
|
+
const raw = await exec(`gh issue view ${issue} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,body,url,state,author,labels,type`);
|
|
102
|
+
const data = JSON.parse(raw);
|
|
103
|
+
return {
|
|
104
|
+
author: data.author?.login ?? "",
|
|
105
|
+
body: data.body ?? "",
|
|
106
|
+
labels: data.labels?.map((label) => label.name) ?? [],
|
|
107
|
+
number: data.number,
|
|
108
|
+
state: data.state,
|
|
109
|
+
title: data.title,
|
|
110
|
+
type: data.type?.name,
|
|
111
|
+
url: data.url,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export async function fetchIssueComments(exec, repository, issue, limit = 50) {
|
|
115
|
+
const query = `query($owner: String!, $repo: String!, $issue: Int!, $limit: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { comments(last: $limit) { nodes { databaseId author { login } authorAssociation body createdAt url } } } } }`;
|
|
116
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F issue=${issue} -F limit=${limit}`);
|
|
117
|
+
const data = JSON.parse(raw);
|
|
118
|
+
return (data.data?.repository?.issue?.comments?.nodes?.map((comment) => ({
|
|
119
|
+
author: comment.author?.login ?? "",
|
|
120
|
+
authorAssociation: comment.authorAssociation,
|
|
121
|
+
body: comment.body ?? "",
|
|
122
|
+
createdAt: comment.createdAt,
|
|
123
|
+
id: comment.databaseId,
|
|
124
|
+
url: comment.url,
|
|
125
|
+
})) ?? []);
|
|
126
|
+
}
|
|
127
|
+
export async function fetchRelatedPullRequests(exec, repository, issue) {
|
|
128
|
+
const query = `query($owner: String!, $repo: String!, $issue: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { timelineItems(first: 50, itemTypes: [CONNECTED_EVENT, CROSS_REFERENCED_EVENT]) { nodes { __typename ... on ConnectedEvent { subject { __typename ... on PullRequest { number title url state mergedAt body author { login } } } } ... on CrossReferencedEvent { source { __typename ... on PullRequest { number title url state mergedAt body author { login } } } } } } } } } }`;
|
|
129
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F issue=${issue}`);
|
|
130
|
+
const data = JSON.parse(raw);
|
|
131
|
+
const prs = new Map();
|
|
132
|
+
for (const node of data.data?.repository?.issue?.timelineItems?.nodes ?? []) {
|
|
133
|
+
const source = (node.subject ?? node.source);
|
|
134
|
+
if (!source?.number || !source.url)
|
|
135
|
+
continue;
|
|
136
|
+
const state = source.mergedAt
|
|
137
|
+
? "MERGED"
|
|
138
|
+
: source.state === "CLOSED"
|
|
139
|
+
? "CLOSED"
|
|
140
|
+
: "OPEN";
|
|
141
|
+
prs.set(source.number, {
|
|
142
|
+
author: source.author?.login ?? "",
|
|
143
|
+
body: source.body,
|
|
144
|
+
mergedAt: source.mergedAt,
|
|
145
|
+
number: source.number,
|
|
146
|
+
state,
|
|
147
|
+
title: source.title ?? `PR #${source.number}`,
|
|
148
|
+
url: source.url,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return [...prs.values()];
|
|
152
|
+
}
|
|
153
|
+
export async function searchDuplicateIssues(exec, repository, issue, limit = 5) {
|
|
154
|
+
const query = `${issue.title} repo:${repoSlug(repository)} is:issue -${issue.number}`;
|
|
155
|
+
const raw = await exec(`gh search issues ${shellQuote(query)} --json number,title,url,state,body --limit ${limit}`);
|
|
156
|
+
const data = JSON.parse(raw);
|
|
157
|
+
return data
|
|
158
|
+
.filter((item) => item.number !== issue.number)
|
|
159
|
+
.map((item) => ({
|
|
160
|
+
...item,
|
|
161
|
+
whyCandidate: "GitHub issue search matched the title.",
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
export async function postIssueComment(exec, repository, issue, account, body) {
|
|
165
|
+
const token = await ghToken(exec, repository, account);
|
|
166
|
+
const payloadPath = join(tmpdir(), `magi-issue-${process.pid}-${Date.now()}.json`);
|
|
167
|
+
await writeFile(payloadPath, JSON.stringify({ body }));
|
|
168
|
+
try {
|
|
169
|
+
return await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/issues/${issue}/comments --method POST --input ${shellQuote(payloadPath)} --jq .html_url`, ghTokenEnv(token));
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await rm(payloadPath, { force: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function closeIssue(exec, repository, issue, account) {
|
|
176
|
+
const token = await ghToken(exec, repository, account);
|
|
177
|
+
return exec(`gh issue close ${issue} --repo ${shellQuote(repoSpecifier(repository))}`, ghTokenEnv(token));
|
|
178
|
+
}
|
|
179
|
+
export async function removeIssueLabels(exec, repository, issue, labels, account) {
|
|
180
|
+
const token = await ghToken(exec, repository, account);
|
|
181
|
+
const removed = [];
|
|
182
|
+
for (const label of labels) {
|
|
183
|
+
await exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --remove-label ${shellQuote(label)}`, ghTokenEnv(token));
|
|
184
|
+
removed.push(label);
|
|
185
|
+
}
|
|
186
|
+
return removed;
|
|
187
|
+
}
|
|
100
188
|
export async function fetchPullRequestReviews(exec, repository, pr) {
|
|
101
189
|
const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviews(first: 100) { nodes { author { login } submittedAt state body commit { oid } } } } } }`;
|
|
102
190
|
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}`);
|
|
@@ -366,6 +454,11 @@ export async function pushHead(exec, repository, worktreePath, account, head) {
|
|
|
366
454
|
},
|
|
367
455
|
});
|
|
368
456
|
}
|
|
457
|
+
export async function createPullRequest(exec, repository, account, input) {
|
|
458
|
+
const token = await ghToken(exec, repository, account);
|
|
459
|
+
const baseFlag = input.base ? ` --base ${shellQuote(input.base)}` : "";
|
|
460
|
+
return exec(`gh pr create --repo ${shellQuote(repoSpecifier(repository))} --head ${shellQuote(input.head)}${baseFlag} --title ${shellQuote(input.title)} --body ${shellQuote(input.body)}`, ghTokenEnv(token));
|
|
461
|
+
}
|
|
369
462
|
export async function configureGitIdentity(exec, worktreePath, identity) {
|
|
370
463
|
if (identity.name) {
|
|
371
464
|
await exec(`git config --worktree user.name ${shellQuote(identity.name)}`, {
|