opencode-magi 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/dist/config/resolve.js +12 -2
- package/dist/config/validate.js +134 -65
- package/dist/github/commands.js +44 -20
- package/dist/index.js +21 -50
- package/dist/orchestrator/ci.js +11 -7
- package/dist/orchestrator/run-manager.js +4 -4
- package/dist/orchestrator/triage.js +66 -42
- package/dist/prompts/compose.js +0 -32
- package/dist/prompts/templates/triage/existing-pr.md +1 -1
- package/package.json +1 -1
- package/schema.json +32 -14
- package/dist/prompts/templates/triage/comment.md +0 -5
- package/dist/prompts/templates/triage/question.md +0 -5
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Add the following content to the configuration file.
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"review": {
|
|
81
|
-
"
|
|
81
|
+
"reviewers": [
|
|
82
82
|
{ "ref": "account-1" },
|
|
83
83
|
{ "ref": "account-2" },
|
|
84
84
|
{ "ref": "account-3" }
|
|
@@ -87,7 +87,7 @@ Add the following content to the configuration file.
|
|
|
87
87
|
}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
After refs are expanded, `review.
|
|
90
|
+
After `refs` are expanded, `review.reviewers[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
|
|
91
91
|
|
|
92
92
|
#### Set project config
|
|
93
93
|
|
|
@@ -133,7 +133,7 @@ Add the following content to the configuration file.
|
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
135
|
"review": {
|
|
136
|
-
"
|
|
136
|
+
"reviewers": [
|
|
137
137
|
{ "ref": "account-1" },
|
|
138
138
|
{ "ref": "account-2" },
|
|
139
139
|
{ "ref": "account-3" }
|
|
@@ -143,8 +143,7 @@ Add the following content to the configuration file.
|
|
|
143
143
|
"editor": { "ref": "account-4" }
|
|
144
144
|
},
|
|
145
145
|
"triage": {
|
|
146
|
-
"
|
|
147
|
-
"agents": [
|
|
146
|
+
"voters": [
|
|
148
147
|
{ "ref": "account-1" },
|
|
149
148
|
{ "ref": "account-2" },
|
|
150
149
|
{ "ref": "account-3" }
|
|
@@ -155,7 +154,18 @@ Add the following content to the configuration file.
|
|
|
155
154
|
|
|
156
155
|
Entries with `ref` are expanded from `agents.refs`. Fields set alongside `ref` override fields from the preset.
|
|
157
156
|
|
|
158
|
-
|
|
157
|
+
`model` can be a single `provider/model` string, a single object with `id` and `options`, or an ordered candidate array. Candidate arrays are resolved during validation against OpenCode's model catalog; the first available model is selected. Put provider-specific options on model objects, not on the agent role.
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"model": {
|
|
162
|
+
"id": "openai/gpt-5.1",
|
|
163
|
+
"options": { "reasoningEffort": "high" }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
After `refs` are expanded, `review.reviewers[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique. `merge.editor.account` is used by `/magi:merge` to push fixes, close PRs, and merge PRs.
|
|
159
169
|
|
|
160
170
|
#### Validate config
|
|
161
171
|
|
package/dist/config/resolve.js
CHANGED
|
@@ -33,6 +33,12 @@ export function triageAgentKey(agent, index) {
|
|
|
33
33
|
export function validateReviewerId(id) {
|
|
34
34
|
return ID_PATTERN.test(id);
|
|
35
35
|
}
|
|
36
|
+
function normalizedModel(model) {
|
|
37
|
+
if (typeof model !== "string") {
|
|
38
|
+
throw new Error("model must be normalized before resolving agents");
|
|
39
|
+
}
|
|
40
|
+
return model;
|
|
41
|
+
}
|
|
36
42
|
function clonePermissionValue(value) {
|
|
37
43
|
return typeof value === "string" ? value : { ...value };
|
|
38
44
|
}
|
|
@@ -86,25 +92,29 @@ export function resolveAgents(config) {
|
|
|
86
92
|
editor: editor
|
|
87
93
|
? {
|
|
88
94
|
...editor,
|
|
95
|
+
model: normalizedModel(editor.model),
|
|
89
96
|
permission: resolveEditorPermission(agents, editor),
|
|
90
97
|
}
|
|
91
98
|
: undefined,
|
|
92
|
-
reviewers: (config.review?.
|
|
99
|
+
reviewers: (config.review?.reviewers ?? []).map((reviewer, index) => ({
|
|
93
100
|
...reviewer,
|
|
94
101
|
key: reviewerKey(reviewer, index),
|
|
95
102
|
index,
|
|
103
|
+
model: normalizedModel(reviewer.model),
|
|
96
104
|
permission: resolveReviewerPermission(agents, reviewer),
|
|
97
105
|
})),
|
|
98
|
-
triage: (config.triage?.
|
|
106
|
+
triage: (config.triage?.voters ?? []).map((agent, index) => ({
|
|
99
107
|
...agent,
|
|
100
108
|
key: triageAgentKey(agent, index),
|
|
101
109
|
index,
|
|
110
|
+
model: normalizedModel(agent.model),
|
|
102
111
|
permission: resolveTriageAgentPermission(agents, agent),
|
|
103
112
|
})),
|
|
104
113
|
triageCreator: creator
|
|
105
114
|
? {
|
|
106
115
|
...creator,
|
|
107
116
|
account: creator.account ?? "",
|
|
117
|
+
model: normalizedModel(creator.model),
|
|
108
118
|
permission: resolveTriageCreatorPermission(agents, creator),
|
|
109
119
|
}
|
|
110
120
|
: undefined,
|
package/dist/config/validate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { access } from "node:fs/promises";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { isAbsolute, join } from "node:path";
|
|
6
6
|
import schema from "../../schema.json" with { type: "json" };
|
|
7
|
-
import { resolveAgents, validateReviewerId } from "./resolve";
|
|
7
|
+
import { resolveAgents, reviewerKey, triageAgentKey, validateReviewerId, } from "./resolve";
|
|
8
8
|
const RESERVED_REVIEWER_KEYS = new Set(["editor", "orchestrator", "system"]);
|
|
9
9
|
const PERMISSION_ACTIONS = new Set(["allow", "ask", "deny"]);
|
|
10
10
|
const AJV = new Ajv2020({ allErrors: true, strict: false });
|
|
@@ -27,7 +27,6 @@ const REVIEWER_KEYS = new Set([
|
|
|
27
27
|
"account",
|
|
28
28
|
"id",
|
|
29
29
|
"model",
|
|
30
|
-
"options",
|
|
31
30
|
"permissions",
|
|
32
31
|
"persona",
|
|
33
32
|
]);
|
|
@@ -35,7 +34,6 @@ const EDITOR_KEYS = new Set([
|
|
|
35
34
|
"account",
|
|
36
35
|
"author",
|
|
37
36
|
"model",
|
|
38
|
-
"options",
|
|
39
37
|
"permissions",
|
|
40
38
|
"persona",
|
|
41
39
|
]);
|
|
@@ -43,7 +41,6 @@ const TRIAGE_AGENT_KEYS = new Set([
|
|
|
43
41
|
"account",
|
|
44
42
|
"id",
|
|
45
43
|
"model",
|
|
46
|
-
"options",
|
|
47
44
|
"permissions",
|
|
48
45
|
"persona",
|
|
49
46
|
]);
|
|
@@ -51,20 +48,19 @@ const TRIAGE_CREATOR_KEYS = new Set([
|
|
|
51
48
|
"account",
|
|
52
49
|
"author",
|
|
53
50
|
"model",
|
|
54
|
-
"options",
|
|
55
51
|
"permissions",
|
|
56
52
|
"persona",
|
|
57
53
|
]);
|
|
58
54
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
59
55
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
60
56
|
const REVIEW_KEYS = new Set([
|
|
61
|
-
"agents",
|
|
62
57
|
"automation",
|
|
63
58
|
"checks",
|
|
64
59
|
"concurrency",
|
|
65
60
|
"merge",
|
|
66
61
|
"output",
|
|
67
62
|
"prompts",
|
|
63
|
+
"reviewers",
|
|
68
64
|
"safety",
|
|
69
65
|
"worktree",
|
|
70
66
|
]);
|
|
@@ -76,7 +72,6 @@ const MERGE_KEYS = new Set([
|
|
|
76
72
|
"prompts",
|
|
77
73
|
]);
|
|
78
74
|
const TRIAGE_KEYS = new Set([
|
|
79
|
-
"agents",
|
|
80
75
|
"automation",
|
|
81
76
|
"categories",
|
|
82
77
|
"concurrency",
|
|
@@ -85,6 +80,7 @@ const TRIAGE_KEYS = new Set([
|
|
|
85
80
|
"prompts",
|
|
86
81
|
"reporter",
|
|
87
82
|
"safety",
|
|
83
|
+
"voters",
|
|
88
84
|
"worktree",
|
|
89
85
|
]);
|
|
90
86
|
const REVIEW_MERGE_KEYS = new Set([
|
|
@@ -138,15 +134,14 @@ const MERGE_PROMPT_KEYS = new Set([
|
|
|
138
134
|
const TRIAGE_PROMPT_KEYS = new Set([
|
|
139
135
|
"acceptance",
|
|
140
136
|
"category",
|
|
141
|
-
"comment",
|
|
142
137
|
"commentClassification",
|
|
143
138
|
"create",
|
|
144
139
|
"createGuidelines",
|
|
145
140
|
"duplicate",
|
|
146
141
|
"existingPr",
|
|
147
|
-
"question",
|
|
148
142
|
"reconsider",
|
|
149
143
|
]);
|
|
144
|
+
const MODEL_CANDIDATE_KEYS = new Set(["id", "options"]);
|
|
150
145
|
function githubHost(config) {
|
|
151
146
|
return config.github?.host ?? "github.com";
|
|
152
147
|
}
|
|
@@ -192,14 +187,14 @@ function expandAgentRefs(config, errors) {
|
|
|
192
187
|
const refsValue = isPlainObject(agents) ? agents.refs : undefined;
|
|
193
188
|
const refsInvalid = refsValue != null && !isPlainObject(refsValue);
|
|
194
189
|
const refs = isPlainObject(refsValue) ? refsValue : undefined;
|
|
195
|
-
if (Array.isArray(magiConfig.review?.
|
|
196
|
-
magiConfig.review.
|
|
190
|
+
if (Array.isArray(magiConfig.review?.reviewers)) {
|
|
191
|
+
magiConfig.review.reviewers = magiConfig.review.reviewers.map((agent, index) => expandAgentRefUse(agent, `review.reviewers[${index}]`, refs, refsInvalid, errors));
|
|
197
192
|
}
|
|
198
193
|
if (isPlainObject(magiConfig.merge?.editor)) {
|
|
199
194
|
magiConfig.merge.editor = expandAgentRefUse(magiConfig.merge.editor, "merge.editor", refs, refsInvalid, errors);
|
|
200
195
|
}
|
|
201
|
-
if (Array.isArray(magiConfig.triage?.
|
|
202
|
-
magiConfig.triage.
|
|
196
|
+
if (Array.isArray(magiConfig.triage?.voters)) {
|
|
197
|
+
magiConfig.triage.voters = magiConfig.triage.voters.map((agent, index) => expandAgentRefUse(agent, `triage.voters[${index}]`, refs, refsInvalid, errors));
|
|
203
198
|
}
|
|
204
199
|
if (isPlainObject(magiConfig.triage?.creator)) {
|
|
205
200
|
magiConfig.triage.creator = expandAgentRefUse(magiConfig.triage.creator, "triage.creator", refs, refsInvalid, errors);
|
|
@@ -277,26 +272,97 @@ function validatePermissionConfig(permission, path, errors) {
|
|
|
277
272
|
}
|
|
278
273
|
}
|
|
279
274
|
}
|
|
280
|
-
function
|
|
281
|
-
if (!model)
|
|
282
|
-
return;
|
|
275
|
+
function modelValidationError(model, path, catalog) {
|
|
283
276
|
const slash = model.indexOf("/");
|
|
284
|
-
if (slash <= 0 || slash === model.length - 1)
|
|
285
|
-
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
277
|
+
if (slash <= 0 || slash === model.length - 1)
|
|
278
|
+
return `${path} must be a full OpenCode model ID in provider/model form`;
|
|
288
279
|
if (!catalog)
|
|
289
|
-
return;
|
|
280
|
+
return undefined;
|
|
290
281
|
const providerId = model.slice(0, slash);
|
|
291
282
|
const modelId = model.slice(slash + 1);
|
|
292
283
|
const models = catalog[providerId];
|
|
293
|
-
if (!models)
|
|
294
|
-
|
|
284
|
+
if (!models)
|
|
285
|
+
return `${path} uses unknown OpenCode provider: ${providerId}`;
|
|
286
|
+
if (!models.includes(modelId))
|
|
287
|
+
return `${path} uses unknown OpenCode model: ${model}`;
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
function validateModelId(model, path, errors, catalog) {
|
|
291
|
+
const error = modelValidationError(model, path, catalog);
|
|
292
|
+
if (error) {
|
|
293
|
+
errors.push(error);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
function readModelCandidate(value, path, errors) {
|
|
299
|
+
if (typeof value === "string")
|
|
300
|
+
return { id: value };
|
|
301
|
+
if (!isPlainObject(value)) {
|
|
302
|
+
errors.push(`${path} must be a string or an object`);
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
validateKnownKeys(value, path, MODEL_CANDIDATE_KEYS, errors);
|
|
306
|
+
if (typeof value.id !== "string") {
|
|
307
|
+
errors.push(`${path}.id must be a string`);
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
if (value.options != null && !isPlainObject(value.options)) {
|
|
311
|
+
errors.push(`${path}.options must be an object`);
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return { id: value.id, options: value.options };
|
|
315
|
+
}
|
|
316
|
+
function validateAndNormalizeModel(target, path, errors, catalog) {
|
|
317
|
+
const model = target.model;
|
|
318
|
+
if (typeof model === "string") {
|
|
319
|
+
validateModelId(model, path, errors, catalog);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (isPlainObject(model)) {
|
|
323
|
+
const candidate = readModelCandidate(model, path, errors);
|
|
324
|
+
if (candidate &&
|
|
325
|
+
validateModelId(candidate.id, `${path}.id`, errors, catalog)) {
|
|
326
|
+
target.model = candidate.id;
|
|
327
|
+
if (candidate.options)
|
|
328
|
+
target.options = candidate.options;
|
|
329
|
+
else
|
|
330
|
+
delete target.options;
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!Array.isArray(model)) {
|
|
335
|
+
if (model != null)
|
|
336
|
+
errors.push(`${path} must be a string, an object, or an array`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!model.length) {
|
|
340
|
+
errors.push(`${path} must contain at least one model candidate`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!catalog) {
|
|
344
|
+
errors.push(`${path} requires an OpenCode model catalog`);
|
|
295
345
|
return;
|
|
296
346
|
}
|
|
297
|
-
|
|
298
|
-
|
|
347
|
+
const candidateErrors = [];
|
|
348
|
+
for (const [index, value] of model.entries()) {
|
|
349
|
+
const candidatePath = `${path}[${index}]`;
|
|
350
|
+
const candidate = readModelCandidate(value, candidatePath, errors);
|
|
351
|
+
if (!candidate)
|
|
352
|
+
continue;
|
|
353
|
+
const idPath = isPlainObject(value) ? `${candidatePath}.id` : candidatePath;
|
|
354
|
+
const error = modelValidationError(candidate.id, idPath, catalog);
|
|
355
|
+
if (!error) {
|
|
356
|
+
target.model = candidate.id;
|
|
357
|
+
if (candidate.options)
|
|
358
|
+
target.options = candidate.options;
|
|
359
|
+
else
|
|
360
|
+
delete target.options;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
candidateErrors.push(error);
|
|
299
364
|
}
|
|
365
|
+
errors.push(`${path} must contain at least one usable OpenCode model candidate${candidateErrors.length ? ` (${candidateErrors.join("; ")})` : ""}`);
|
|
300
366
|
}
|
|
301
367
|
function validateReviewerList(reviewers, path, errors, catalog) {
|
|
302
368
|
if (reviewers == null)
|
|
@@ -317,14 +383,11 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
317
383
|
validateKnownKeys(reviewer, `${path}[${index}]`, REVIEWER_KEYS, errors);
|
|
318
384
|
if (!reviewer.model)
|
|
319
385
|
errors.push(`${path}[${index}].model is required`);
|
|
320
|
-
|
|
321
|
-
validateModel(reviewer.model, `${path}[${index}].model`, errors, catalog);
|
|
386
|
+
validateAndNormalizeModel(reviewer, `${path}[${index}].model`, errors, catalog);
|
|
322
387
|
if (!reviewer.account)
|
|
323
388
|
errors.push(`${path}[${index}].account is required`);
|
|
324
389
|
validateString(reviewer.account, `${path}[${index}].account`, errors);
|
|
325
390
|
validateString(reviewer.persona, `${path}[${index}].persona`, errors);
|
|
326
|
-
if (reviewer.options != null && !isPlainObject(reviewer.options))
|
|
327
|
-
errors.push(`${path}[${index}].options must be an object`);
|
|
328
391
|
validatePermissionConfig(reviewer.permissions, `${path}[${index}].permissions`, errors);
|
|
329
392
|
if (reviewer.id) {
|
|
330
393
|
if (!validateReviewerId(reviewer.id)) {
|
|
@@ -336,18 +399,18 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
336
399
|
}
|
|
337
400
|
});
|
|
338
401
|
}
|
|
339
|
-
function validateTriageAgentList(
|
|
340
|
-
if (
|
|
402
|
+
function validateTriageAgentList(voters, path, errors, catalog) {
|
|
403
|
+
if (voters == null)
|
|
341
404
|
return;
|
|
342
|
-
if (!Array.isArray(
|
|
405
|
+
if (!Array.isArray(voters)) {
|
|
343
406
|
errors.push(`${path} must be an array`);
|
|
344
407
|
return;
|
|
345
408
|
}
|
|
346
|
-
if (
|
|
347
|
-
errors.push(`${path} must contain at least 3
|
|
348
|
-
if (
|
|
349
|
-
errors.push(`${path} must contain an odd number of
|
|
350
|
-
|
|
409
|
+
if (voters.length < 3)
|
|
410
|
+
errors.push(`${path} must contain at least 3 voters`);
|
|
411
|
+
if (voters.length % 2 === 0)
|
|
412
|
+
errors.push(`${path} must contain an odd number of voters`);
|
|
413
|
+
voters.forEach((agent, index) => {
|
|
351
414
|
if (!agent || typeof agent !== "object") {
|
|
352
415
|
errors.push(`${path}[${index}] must be an object`);
|
|
353
416
|
return;
|
|
@@ -355,14 +418,11 @@ function validateTriageAgentList(agents, path, errors, catalog) {
|
|
|
355
418
|
validateKnownKeys(agent, `${path}[${index}]`, TRIAGE_AGENT_KEYS, errors);
|
|
356
419
|
if (!agent.model)
|
|
357
420
|
errors.push(`${path}[${index}].model is required`);
|
|
358
|
-
|
|
359
|
-
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
421
|
+
validateAndNormalizeModel(agent, `${path}[${index}].model`, errors, catalog);
|
|
360
422
|
if (!agent.account)
|
|
361
423
|
errors.push(`${path}[${index}].account is required`);
|
|
362
424
|
validateString(agent.account, `${path}[${index}].account`, errors);
|
|
363
425
|
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
364
|
-
if (agent.options != null && !isPlainObject(agent.options))
|
|
365
|
-
errors.push(`${path}[${index}].options must be an object`);
|
|
366
426
|
validatePermissionConfig(agent.permissions, `${path}[${index}].permissions`, errors);
|
|
367
427
|
if (agent.id) {
|
|
368
428
|
if (!validateReviewerId(agent.id)) {
|
|
@@ -408,15 +468,11 @@ function validateEditor(editor, path, errors, catalog) {
|
|
|
408
468
|
if (!editor.model)
|
|
409
469
|
errors.push(`${path}.model is required`);
|
|
410
470
|
validateKnownKeys(editor, path, EDITOR_KEYS, errors);
|
|
411
|
-
|
|
471
|
+
validateAndNormalizeModel(editor, `${path}.model`, errors, catalog);
|
|
412
472
|
validateString(editor.account, `${path}.account`, errors);
|
|
413
473
|
validateString(editor.persona, `${path}.persona`, errors);
|
|
414
|
-
validateModel(editor.model, `${path}.model`, errors, catalog);
|
|
415
474
|
if (!editor.account)
|
|
416
475
|
errors.push(`${path}.account is required`);
|
|
417
|
-
if (editor.options != null && !isPlainObject(editor.options)) {
|
|
418
|
-
errors.push(`${path}.options must be an object`);
|
|
419
|
-
}
|
|
420
476
|
validatePermissionConfig(editor.permissions, `${path}.permissions`, errors);
|
|
421
477
|
const author = editor.author;
|
|
422
478
|
if (!author || !isPlainObject(author)) {
|
|
@@ -452,12 +508,8 @@ function validateTriageCreator(creator, path, errors, catalog) {
|
|
|
452
508
|
if (!creator.model)
|
|
453
509
|
errors.push(`${path}.model is required`);
|
|
454
510
|
validateString(creator.account, `${path}.account`, errors);
|
|
455
|
-
|
|
511
|
+
validateAndNormalizeModel(creator, `${path}.model`, errors, catalog);
|
|
456
512
|
validateString(creator.persona, `${path}.persona`, errors);
|
|
457
|
-
validateModel(creator.model, `${path}.model`, errors, catalog);
|
|
458
|
-
if (creator.options != null && !isPlainObject(creator.options)) {
|
|
459
|
-
errors.push(`${path}.options must be an object`);
|
|
460
|
-
}
|
|
461
513
|
validatePermissionConfig(creator.permissions, `${path}.permissions`, errors);
|
|
462
514
|
const author = creator.author;
|
|
463
515
|
if (!author || !isPlainObject(author)) {
|
|
@@ -682,15 +734,20 @@ function validateTriage(config, errors, options) {
|
|
|
682
734
|
const creator = triage.creator;
|
|
683
735
|
const reporter = typeof triage.reporter === "string" ? triage.reporter : undefined;
|
|
684
736
|
const safety = triage.safety;
|
|
685
|
-
if (!triage.
|
|
686
|
-
errors.push("triage.
|
|
687
|
-
validateTriageAgentList(triage.
|
|
688
|
-
if (Array.isArray(triage.
|
|
689
|
-
const resolvedTriageAgents =
|
|
737
|
+
if (!triage.voters)
|
|
738
|
+
errors.push("triage.voters is required");
|
|
739
|
+
validateTriageAgentList(triage.voters, "triage.voters", errors, options.modelCatalog);
|
|
740
|
+
if (Array.isArray(triage.voters)) {
|
|
741
|
+
const resolvedTriageAgents = triage.voters.map((agent, index) => ({
|
|
742
|
+
account: agent && typeof agent === "object" && typeof agent.account === "string"
|
|
743
|
+
? agent.account
|
|
744
|
+
: "",
|
|
745
|
+
key: agent && typeof agent === "object" ? triageAgentKey(agent, index) : "",
|
|
746
|
+
}));
|
|
690
747
|
validateResolvedTriageAgents(resolvedTriageAgents, "triage.resolvedAgents", errors);
|
|
691
748
|
if (reporter != null &&
|
|
692
749
|
!resolvedTriageAgents.some((agent) => agent.key === reporter)) {
|
|
693
|
-
errors.push(`triage.reporter must match a triage
|
|
750
|
+
errors.push(`triage.reporter must match a triage voter key: ${reporter}`);
|
|
694
751
|
}
|
|
695
752
|
}
|
|
696
753
|
validateString(triage.reporter, "triage.reporter", errors);
|
|
@@ -780,10 +837,10 @@ async function fetchPermissions(config, exec, account) {
|
|
|
780
837
|
return JSON.parse(raw);
|
|
781
838
|
}
|
|
782
839
|
async function validateWorktreeConfig(config, exec, options, errors) {
|
|
783
|
-
const
|
|
784
|
-
|
|
840
|
+
const checkEditor = Boolean(config.merge?.editor &&
|
|
841
|
+
(options.requireEditor || options.requireWorktreeConfig));
|
|
785
842
|
const checkTriageCreator = Boolean(config.triage?.automation?.create &&
|
|
786
|
-
|
|
843
|
+
config.triage?.creator &&
|
|
787
844
|
(options.requireTriage || options.requireWorktreeConfig));
|
|
788
845
|
if (!checkEditor && !checkTriageCreator)
|
|
789
846
|
return;
|
|
@@ -855,6 +912,9 @@ export async function validateConfig(config, options = {}) {
|
|
|
855
912
|
const warnings = [];
|
|
856
913
|
if (!config || typeof config !== "object")
|
|
857
914
|
errors.push("config must be an object");
|
|
915
|
+
if (options.requireModelCatalog && !options.modelCatalog) {
|
|
916
|
+
errors.push("OpenCode model catalog could not be loaded");
|
|
917
|
+
}
|
|
858
918
|
expandAgentRefs(config, errors);
|
|
859
919
|
if (config && typeof config === "object")
|
|
860
920
|
validateJsonSchema(config, errors);
|
|
@@ -878,11 +938,20 @@ export async function validateConfig(config, options = {}) {
|
|
|
878
938
|
else {
|
|
879
939
|
validateKnownKeys(config.review, "review", REVIEW_KEYS, errors);
|
|
880
940
|
}
|
|
881
|
-
if (!config.review.
|
|
882
|
-
errors.push("review.
|
|
883
|
-
validateReviewerList(config.review.
|
|
884
|
-
if (Array.isArray(config.review.
|
|
885
|
-
validateResolvedReviewers(
|
|
941
|
+
if (!config.review.reviewers)
|
|
942
|
+
errors.push("review.reviewers is required");
|
|
943
|
+
validateReviewerList(config.review.reviewers, "review.reviewers", errors, options.modelCatalog);
|
|
944
|
+
if (Array.isArray(config.review.reviewers)) {
|
|
945
|
+
validateResolvedReviewers(config.review.reviewers.map((reviewer, index) => ({
|
|
946
|
+
account: reviewer &&
|
|
947
|
+
typeof reviewer === "object" &&
|
|
948
|
+
typeof reviewer.account === "string"
|
|
949
|
+
? reviewer.account
|
|
950
|
+
: "",
|
|
951
|
+
key: reviewer && typeof reviewer === "object"
|
|
952
|
+
? reviewerKey(reviewer, index)
|
|
953
|
+
: "",
|
|
954
|
+
})), "review.resolvedReviewers", errors);
|
|
886
955
|
}
|
|
887
956
|
}
|
|
888
957
|
if (options.requireTriage && !config.triage) {
|
package/dist/github/commands.js
CHANGED
|
@@ -20,6 +20,20 @@ function errorText(error) {
|
|
|
20
20
|
.filter((item) => typeof item === "string")
|
|
21
21
|
.join("\n");
|
|
22
22
|
}
|
|
23
|
+
function isIssueTypeUnavailableText(text) {
|
|
24
|
+
return (/cannot query field ["']?issueType["']? on type ["']?Issue["']?/i.test(text) ||
|
|
25
|
+
/field ["']?issueType["']?.*(does not exist|doesn't exist|is not defined|not found).*type ["']?Issue["']?/i.test(text) ||
|
|
26
|
+
/undefinedField.*issueType/i.test(text) ||
|
|
27
|
+
/issueType.*unsupported field|unsupported field.*issueType/i.test(text));
|
|
28
|
+
}
|
|
29
|
+
function isIssueTypeUnavailableError(error) {
|
|
30
|
+
return isIssueTypeUnavailableText(errorText(error));
|
|
31
|
+
}
|
|
32
|
+
function isIssueTypeUnavailableGraphqlResponse(data) {
|
|
33
|
+
return (data.errors?.some((error) => isIssueTypeUnavailableText([error.message, error.type]
|
|
34
|
+
.filter((item) => typeof item === "string")
|
|
35
|
+
.join("\n"))) ?? false);
|
|
36
|
+
}
|
|
23
37
|
async function localCommitExists(exec, worktreePath, sha) {
|
|
24
38
|
try {
|
|
25
39
|
await exec(`git cat-file -e ${shellQuote(`${sha}^{commit}`)}`, {
|
|
@@ -184,26 +198,34 @@ export async function fetchPullRequestClosingIssues(exec, repository, pr) {
|
|
|
184
198
|
}
|
|
185
199
|
export async function fetchIssue(exec, repository, issue) {
|
|
186
200
|
const query = `query($owner: String!, $repo: String!, $issue: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { number title body url state author { login } labels(first: 100) { nodes { name } } issueType { name } } } }`;
|
|
201
|
+
let raw;
|
|
187
202
|
try {
|
|
188
|
-
|
|
189
|
-
const data = JSON.parse(raw);
|
|
190
|
-
const graphqlIssue = data.data?.repository?.issue;
|
|
191
|
-
if (!graphqlIssue)
|
|
192
|
-
throw new Error(`Could not fetch issue #${issue}`);
|
|
193
|
-
return {
|
|
194
|
-
author: graphqlIssue.author?.login ?? "",
|
|
195
|
-
body: graphqlIssue.body ?? "",
|
|
196
|
-
labels: graphqlIssue.labels?.nodes?.map((label) => label.name) ?? [],
|
|
197
|
-
number: graphqlIssue.number,
|
|
198
|
-
state: graphqlIssue.state,
|
|
199
|
-
title: graphqlIssue.title,
|
|
200
|
-
type: graphqlIssue.issueType?.name,
|
|
201
|
-
url: graphqlIssue.url,
|
|
202
|
-
};
|
|
203
|
+
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}`);
|
|
203
204
|
}
|
|
204
|
-
catch {
|
|
205
|
-
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (isIssueTypeUnavailableError(error)) {
|
|
207
|
+
return fetchIssueWithCli(exec, repository, issue);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
const data = JSON.parse(raw);
|
|
212
|
+
const graphqlIssue = data.data?.repository?.issue;
|
|
213
|
+
if (!graphqlIssue) {
|
|
214
|
+
if (isIssueTypeUnavailableGraphqlResponse(data)) {
|
|
215
|
+
return fetchIssueWithCli(exec, repository, issue);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Could not fetch issue #${issue}`);
|
|
206
218
|
}
|
|
219
|
+
return {
|
|
220
|
+
author: graphqlIssue.author?.login ?? "",
|
|
221
|
+
body: graphqlIssue.body ?? "",
|
|
222
|
+
labels: graphqlIssue.labels?.nodes?.map((label) => label.name) ?? [],
|
|
223
|
+
number: graphqlIssue.number,
|
|
224
|
+
state: graphqlIssue.state,
|
|
225
|
+
title: graphqlIssue.title,
|
|
226
|
+
type: graphqlIssue.issueType?.name,
|
|
227
|
+
url: graphqlIssue.url,
|
|
228
|
+
};
|
|
207
229
|
}
|
|
208
230
|
async function fetchIssueWithCli(exec, repository, issue) {
|
|
209
231
|
const raw = await exec(`gh issue view ${issue} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,body,url,state,author,labels`);
|
|
@@ -458,8 +480,9 @@ export async function fetchPullRequestSafetyMeta(exec, repository, pr) {
|
|
|
458
480
|
}
|
|
459
481
|
return { author, changedFiles, files, labels };
|
|
460
482
|
}
|
|
461
|
-
export async function watchChecks(exec, repository, pr) {
|
|
462
|
-
|
|
483
|
+
export async function watchChecks(exec, repository, pr, options = {}) {
|
|
484
|
+
const requiredFlag = options.requiredOnly ? " --required" : "";
|
|
485
|
+
await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --watch${requiredFlag}`);
|
|
463
486
|
}
|
|
464
487
|
export function isCancelledCheck(check) {
|
|
465
488
|
return check.bucket === "cancel" || check.state === "CANCELLED";
|
|
@@ -469,8 +492,9 @@ export function isFailedCheck(check) {
|
|
|
469
492
|
}
|
|
470
493
|
export async function fetchPullRequestChecks(exec, repository, pr, options = {}) {
|
|
471
494
|
let raw;
|
|
495
|
+
const requiredFlag = options.requiredOnly ? " --required" : "";
|
|
472
496
|
try {
|
|
473
|
-
raw = await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json name,state,bucket,link,workflow`);
|
|
497
|
+
raw = await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json name,state,bucket,link,workflow${requiredFlag}`);
|
|
474
498
|
}
|
|
475
499
|
catch (error) {
|
|
476
500
|
if (options.tolerateMissingChecks &&
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadConfig, mergeMagiConfig } from "./config/load";
|
|
|
9
9
|
import { outputBaseDirs } from "./config/output";
|
|
10
10
|
import { worktreeBaseDirs } from "./config/worktree";
|
|
11
11
|
import { resolveRepository } from "./config/resolve";
|
|
12
|
-
import { validateConfig } from "./config/validate";
|
|
12
|
+
import { validateConfig, } from "./config/validate";
|
|
13
13
|
import { withGitHubApiRetry } from "./github/retry";
|
|
14
14
|
import { mapPool } from "./orchestrator/pool";
|
|
15
15
|
import { MagiRunManager } from "./orchestrator/run-manager";
|
|
@@ -286,23 +286,6 @@ function parseOptionalIssue(value) {
|
|
|
286
286
|
function clearFlag(value) {
|
|
287
287
|
return typeof value === "boolean" ? value : undefined;
|
|
288
288
|
}
|
|
289
|
-
function clearToolFlag(value) {
|
|
290
|
-
if (value === true || value === "true")
|
|
291
|
-
return true;
|
|
292
|
-
if (value === "false")
|
|
293
|
-
return false;
|
|
294
|
-
return undefined;
|
|
295
|
-
}
|
|
296
|
-
function hasBlankSelector(args) {
|
|
297
|
-
return !args.runId?.trim() && !args.pr?.trim();
|
|
298
|
-
}
|
|
299
|
-
function hasDefaultedFalseClearFlags(args) {
|
|
300
|
-
return (hasBlankSelector(args) &&
|
|
301
|
-
args.branch === "false" &&
|
|
302
|
-
args.output === "false" &&
|
|
303
|
-
args.session === "false" &&
|
|
304
|
-
args.worktree === "false");
|
|
305
|
-
}
|
|
306
289
|
function parseQuestionAnswers(value) {
|
|
307
290
|
const trimmed = value.trim();
|
|
308
291
|
if (!trimmed)
|
|
@@ -334,6 +317,9 @@ function issueMarkdownLink(repository, issue) {
|
|
|
334
317
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
|
|
335
318
|
return `[#${issue}](${url})`;
|
|
336
319
|
}
|
|
320
|
+
function validationError(validation) {
|
|
321
|
+
return new Error(JSON.stringify(validation, null, 2));
|
|
322
|
+
}
|
|
337
323
|
function isPlainObject(value) {
|
|
338
324
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
339
325
|
}
|
|
@@ -395,7 +381,8 @@ export async function validateMagiConfigFiles(directory, options = {}) {
|
|
|
395
381
|
? withGitHubApiRetry(options.exec, mergedConfig.github?.apiRetryAttempts ?? 3)
|
|
396
382
|
: undefined,
|
|
397
383
|
modelCatalog: options.modelCatalog,
|
|
398
|
-
requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.
|
|
384
|
+
requireGithub: hasProjectConfig && Boolean(mergedConfig.review?.reviewers),
|
|
385
|
+
requireModelCatalog: true,
|
|
399
386
|
requireWorktreeConfig: true,
|
|
400
387
|
});
|
|
401
388
|
loadedFrom = existing.map((status) => status.path).join(", ");
|
|
@@ -447,7 +434,8 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
447
434
|
.then(extractModelCatalog)
|
|
448
435
|
.catch(() => catalogClient.provider
|
|
449
436
|
?.list({ query: { directory } })
|
|
450
|
-
.then(extractModelCatalog))
|
|
437
|
+
.then(extractModelCatalog))
|
|
438
|
+
.catch(() => undefined);
|
|
451
439
|
return modelCatalogPromise;
|
|
452
440
|
}
|
|
453
441
|
return {
|
|
@@ -492,9 +480,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
492
480
|
exec: retryingExec,
|
|
493
481
|
modelCatalog: await modelCatalog(),
|
|
494
482
|
requireEditor: true,
|
|
483
|
+
requireModelCatalog: true,
|
|
495
484
|
});
|
|
496
485
|
if (!validation.ok)
|
|
497
|
-
|
|
486
|
+
throw validationError(validation);
|
|
498
487
|
const repository = resolveRepository(config);
|
|
499
488
|
const sync = parsed.sync || args.sync === true;
|
|
500
489
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
@@ -534,9 +523,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
534
523
|
directory,
|
|
535
524
|
exec: retryingExec,
|
|
536
525
|
modelCatalog: await modelCatalog(),
|
|
526
|
+
requireModelCatalog: true,
|
|
537
527
|
});
|
|
538
528
|
if (!validation.ok)
|
|
539
|
-
|
|
529
|
+
throw validationError(validation);
|
|
540
530
|
const repository = resolveRepository(config);
|
|
541
531
|
const sync = parsed.sync || args.sync === true;
|
|
542
532
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
@@ -557,7 +547,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
557
547
|
},
|
|
558
548
|
}),
|
|
559
549
|
magi_triage: tool({
|
|
560
|
-
description: "Triage one or more GitHub issues with configured Magi triage
|
|
550
|
+
description: "Triage one or more GitHub issues with configured Magi triage voters.",
|
|
561
551
|
args: {
|
|
562
552
|
issues: tool.schema.string(),
|
|
563
553
|
dryRun: tool.schema.boolean().optional(),
|
|
@@ -574,12 +564,13 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
574
564
|
exec: retryingExec,
|
|
575
565
|
modelCatalog: await modelCatalog(),
|
|
576
566
|
requireEditor: config.triage?.automation?.merge === true,
|
|
567
|
+
requireModelCatalog: true,
|
|
577
568
|
requireReview: config.triage?.automation?.review === true ||
|
|
578
569
|
config.triage?.automation?.merge === true,
|
|
579
570
|
requireTriage: true,
|
|
580
571
|
});
|
|
581
572
|
if (!validation.ok)
|
|
582
|
-
|
|
573
|
+
throw validationError(validation);
|
|
583
574
|
const repository = resolveRepository(config);
|
|
584
575
|
if (!repository.triage)
|
|
585
576
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
@@ -692,41 +683,21 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
692
683
|
}),
|
|
693
684
|
magi_clear: tool({
|
|
694
685
|
description: "Clear all inactive Magi runs by deleting configured sessions, worktrees, branches, and output artifacts.",
|
|
695
|
-
args: {
|
|
696
|
-
|
|
697
|
-
pr: tool.schema.string().optional(),
|
|
698
|
-
issue: tool.schema.string().optional(),
|
|
699
|
-
branch: tool.schema.enum(["true", "false"]).optional(),
|
|
700
|
-
output: tool.schema.enum(["true", "false"]).optional(),
|
|
701
|
-
session: tool.schema.enum(["true", "false"]).optional(),
|
|
702
|
-
worktree: tool.schema.enum(["true", "false"]).optional(),
|
|
703
|
-
},
|
|
704
|
-
async execute(args) {
|
|
686
|
+
args: {},
|
|
687
|
+
async execute() {
|
|
705
688
|
const loaded = await loadConfig(directory).catch(() => undefined);
|
|
706
689
|
const clear = loaded?.config.clear;
|
|
707
|
-
const useConfiguredDefaults = hasDefaultedFalseClearFlags(args);
|
|
708
690
|
const options = {
|
|
709
|
-
branch: (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
? undefined
|
|
714
|
-
: clearToolFlag(args.output)) ?? clearFlag(clear?.output),
|
|
715
|
-
session: (useConfiguredDefaults
|
|
716
|
-
? undefined
|
|
717
|
-
: clearToolFlag(args.session)) ?? clearFlag(clear?.session),
|
|
718
|
-
worktree: (useConfiguredDefaults
|
|
719
|
-
? undefined
|
|
720
|
-
: clearToolFlag(args.worktree)) ?? clearFlag(clear?.worktree),
|
|
691
|
+
branch: clearFlag(clear?.branch),
|
|
692
|
+
output: clearFlag(clear?.output),
|
|
693
|
+
session: clearFlag(clear?.session),
|
|
694
|
+
worktree: clearFlag(clear?.worktree),
|
|
721
695
|
};
|
|
722
696
|
return runManager.clear({
|
|
723
697
|
options,
|
|
724
|
-
issue: parseOptionalIssue(args.issue),
|
|
725
698
|
outputDir: loaded
|
|
726
699
|
? outputBaseDirs(directory, loaded.config)
|
|
727
700
|
: undefined,
|
|
728
|
-
pr: parseOptionalPr(args.pr),
|
|
729
|
-
runId: args.runId,
|
|
730
701
|
worktreeDir: loaded
|
|
731
702
|
? worktreeBaseDirs(directory, loaded.config)
|
|
732
703
|
: undefined,
|
package/dist/orchestrator/ci.js
CHANGED
|
@@ -229,7 +229,7 @@ async function watchRerunRuns(exec, repository, checks) {
|
|
|
229
229
|
await Promise.all(runIds.map((runId) => watchRun(exec, repository, runId)));
|
|
230
230
|
}
|
|
231
231
|
async function checksForHead(input) {
|
|
232
|
-
const checks = await fetchPullRequestChecks(input.exec, input.repository, input.pr, { tolerateMissingChecks: Boolean(input.headSha) });
|
|
232
|
+
const checks = await fetchPullRequestChecks(input.exec, input.repository, input.pr, { requiredOnly: true, tolerateMissingChecks: Boolean(input.headSha) });
|
|
233
233
|
const targetChecks = [];
|
|
234
234
|
let hasAnyActionCheck = false;
|
|
235
235
|
let hasTargetActionCheck = false;
|
|
@@ -254,7 +254,6 @@ async function checksForHead(input) {
|
|
|
254
254
|
return {
|
|
255
255
|
blocking: targetChecks.filter((check) => isFailedCheck(check) || isCancelledCheck(check)),
|
|
256
256
|
hasAnyActionCheck,
|
|
257
|
-
hasAnyCheck: checks.length > 0,
|
|
258
257
|
hasPending: targetChecks.some(isPendingCheck),
|
|
259
258
|
hasTargetActionCheck,
|
|
260
259
|
};
|
|
@@ -463,15 +462,17 @@ export async function waitForChecksWithClassification(input) {
|
|
|
463
462
|
await input.onProgress?.("waiting for CI checks");
|
|
464
463
|
for (let attempt = 0;; attempt += 1) {
|
|
465
464
|
try {
|
|
466
|
-
await watchChecks(input.exec, input.repository, input.pr
|
|
465
|
+
await watchChecks(input.exec, input.repository, input.pr, {
|
|
466
|
+
requiredOnly: true,
|
|
467
|
+
});
|
|
467
468
|
}
|
|
468
469
|
catch {
|
|
469
470
|
// gh exits non-zero for pending checks too; re-read check state below.
|
|
470
471
|
}
|
|
471
472
|
const target = await readTargetChecks();
|
|
472
473
|
const waitingForTargetHead = Boolean(input.headSha) &&
|
|
473
|
-
|
|
474
|
-
|
|
474
|
+
target.hasAnyActionCheck &&
|
|
475
|
+
!target.hasTargetActionCheck;
|
|
475
476
|
if (!waitingForTargetHead && !target.hasPending) {
|
|
476
477
|
await assignBlockingChecks(target.blocking);
|
|
477
478
|
break;
|
|
@@ -552,8 +553,11 @@ export async function waitForChecksWithClassification(input) {
|
|
|
552
553
|
try {
|
|
553
554
|
await input.onProgress?.("waiting for rerun CI checks");
|
|
554
555
|
await watchRerunRuns(input.exec, input.repository, rerunnable);
|
|
555
|
-
if (input.wait)
|
|
556
|
-
await watchChecks(input.exec, input.repository, input.pr
|
|
556
|
+
if (input.wait) {
|
|
557
|
+
await watchChecks(input.exec, input.repository, input.pr, {
|
|
558
|
+
requiredOnly: true,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
557
561
|
}
|
|
558
562
|
catch {
|
|
559
563
|
// Re-read the PR checks below so stale failed checks are not trusted.
|
|
@@ -1616,16 +1616,16 @@ export class MagiRunManager {
|
|
|
1616
1616
|
}));
|
|
1617
1617
|
}
|
|
1618
1618
|
if (progress.type === "triage_agent_started") {
|
|
1619
|
-
await this.notify(state, `**Triage
|
|
1619
|
+
await this.notify(state, `**Triage voter ${progress.voter}** started ${progress.phase} for ${issue}.`);
|
|
1620
1620
|
}
|
|
1621
1621
|
if (progress.type === "triage_agent_repair") {
|
|
1622
|
-
await this.notify(state, `**Triage
|
|
1622
|
+
await this.notify(state, `**Triage voter ${progress.voter}** started JSON regeneration for ${issue}.`);
|
|
1623
1623
|
}
|
|
1624
1624
|
if (progress.type === "triage_agent_completed") {
|
|
1625
|
-
await this.notify(state, `**Triage
|
|
1625
|
+
await this.notify(state, `**Triage voter ${progress.voter}** completed ${progress.phase} for ${issue}: ${progress.vote}.`);
|
|
1626
1626
|
}
|
|
1627
1627
|
if (progress.type === "triage_agent_failed") {
|
|
1628
|
-
await this.notify(state, `**Triage
|
|
1628
|
+
await this.notify(state, `**Triage voter ${progress.voter}** failed ${progress.phase} for ${issue}: ${redactSecrets(progress.error)}`);
|
|
1629
1629
|
}
|
|
1630
1630
|
if (progress.type === "comment_posting") {
|
|
1631
1631
|
await this.notify(state, `Posting triage comment for ${issue}.`);
|
|
@@ -128,13 +128,14 @@ async function emitTriageModelProgress(input) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
async function runVote(input) {
|
|
131
|
-
const prompt =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
const prompt = input.promptText ??
|
|
132
|
+
(await input.prompt({
|
|
133
|
+
context: input.context,
|
|
134
|
+
directory: input.directory,
|
|
135
|
+
issue: input.issue,
|
|
136
|
+
repository: input.repository,
|
|
137
|
+
voter: input.agent,
|
|
138
|
+
}));
|
|
138
139
|
await emitProgress(input.run, {
|
|
139
140
|
phase: input.phase,
|
|
140
141
|
type: "triage_agent_started",
|
|
@@ -156,7 +157,7 @@ async function runVote(input) {
|
|
|
156
157
|
parse: input.parse,
|
|
157
158
|
permission: input.agent.permission,
|
|
158
159
|
prompt,
|
|
159
|
-
repairAttempts: 3,
|
|
160
|
+
repairAttempts: input.run.config.output?.repairAttempts ?? 3,
|
|
160
161
|
schemaName: input.schemaName,
|
|
161
162
|
signal: input.signal,
|
|
162
163
|
title: `Magi triage ${input.schemaName} #${input.issue} (${input.agent.key})`,
|
|
@@ -216,7 +217,7 @@ export function chooseDuplicateOutput(input) {
|
|
|
216
217
|
async function runDuplicateVote(input) {
|
|
217
218
|
const agents = input.input.repository.agents.triage;
|
|
218
219
|
if (!agents?.length)
|
|
219
|
-
throw new Error("triage.
|
|
220
|
+
throw new Error("triage.voters is required");
|
|
220
221
|
await emitProgress(input.input, { phase: "duplicate", type: "phase" });
|
|
221
222
|
const outputs = await Promise.all(agents.map((agent) => runVote({
|
|
222
223
|
agent,
|
|
@@ -258,9 +259,16 @@ async function runDuplicateVote(input) {
|
|
|
258
259
|
async function runPhaseVote(input) {
|
|
259
260
|
const agents = input.input.repository.agents.triage;
|
|
260
261
|
if (!agents?.length)
|
|
261
|
-
throw new Error("triage.
|
|
262
|
+
throw new Error("triage.voters is required");
|
|
262
263
|
await emitProgress(input.input, { phase: input.phase, type: "phase" });
|
|
263
|
-
const
|
|
264
|
+
const promptTexts = await Promise.all(agents.map((agent) => input.prompt({
|
|
265
|
+
context: input.context,
|
|
266
|
+
directory: input.input.directory,
|
|
267
|
+
issue: input.input.issue,
|
|
268
|
+
repository: input.input.repository,
|
|
269
|
+
voter: agent,
|
|
270
|
+
})));
|
|
271
|
+
const outputs = await Promise.all(agents.map((agent, index) => runVote({
|
|
264
272
|
agent,
|
|
265
273
|
client: input.input.client,
|
|
266
274
|
context: input.context,
|
|
@@ -269,6 +277,7 @@ async function runPhaseVote(input) {
|
|
|
269
277
|
parse: input.parse,
|
|
270
278
|
phase: input.phase,
|
|
271
279
|
prompt: input.prompt,
|
|
280
|
+
promptText: promptTexts[index],
|
|
272
281
|
repository: input.input.repository,
|
|
273
282
|
run: input.input,
|
|
274
283
|
schemaName: input.schemaName,
|
|
@@ -285,7 +294,16 @@ async function runPhaseVote(input) {
|
|
|
285
294
|
voter: agents[index].key,
|
|
286
295
|
})));
|
|
287
296
|
await writeJson(join(input.outputDir, `${input.phase}-majority.json`), majority);
|
|
288
|
-
return {
|
|
297
|
+
return {
|
|
298
|
+
outputs,
|
|
299
|
+
reason: chooseDecisionReason({
|
|
300
|
+
outputs,
|
|
301
|
+
threshold: majority.threshold,
|
|
302
|
+
vote: majority.vote,
|
|
303
|
+
voters: majority.vote ? majority.voters[majority.vote] : undefined,
|
|
304
|
+
}),
|
|
305
|
+
vote: majority.vote,
|
|
306
|
+
};
|
|
289
307
|
}
|
|
290
308
|
async function relationshipScan(input, issue) {
|
|
291
309
|
const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
|
|
@@ -484,7 +502,7 @@ async function classifyMentionReplies(input) {
|
|
|
484
502
|
parse: parseTriageCommentClassificationOutput,
|
|
485
503
|
permission: agent.permission,
|
|
486
504
|
prompt,
|
|
487
|
-
repairAttempts: 3,
|
|
505
|
+
repairAttempts: input.input.config.output?.repairAttempts ?? 3,
|
|
488
506
|
schemaName: "triage comment classification",
|
|
489
507
|
signal: input.input.signal,
|
|
490
508
|
title: `Magi triage comment classification #${input.input.issue} (${agent.key})`,
|
|
@@ -507,7 +525,7 @@ async function runReconsiderationVote(input) {
|
|
|
507
525
|
function triageReporter(repository, issue) {
|
|
508
526
|
const agents = repository.agents.triage ?? [];
|
|
509
527
|
if (!agents.length)
|
|
510
|
-
throw new Error("triage.
|
|
528
|
+
throw new Error("triage.voters is required");
|
|
511
529
|
const configured = repository.triage?.reporter;
|
|
512
530
|
const reporter = configured
|
|
513
531
|
? agents.find((agent) => agent.key === configured)
|
|
@@ -518,26 +536,46 @@ function triageReporter(repository, issue) {
|
|
|
518
536
|
}
|
|
519
537
|
function decisionCommentBody(input) {
|
|
520
538
|
const reason = input.reason?.trim();
|
|
521
|
-
const result = JSON.stringify(input.result);
|
|
522
539
|
return reason
|
|
523
|
-
?
|
|
524
|
-
:
|
|
540
|
+
? reason
|
|
541
|
+
: decisionCommentFallback({ action: input.action, result: input.result });
|
|
542
|
+
}
|
|
543
|
+
function decisionCommentFallback(input) {
|
|
544
|
+
if (input.result.disposition === "accepted") {
|
|
545
|
+
const category = input.result.category
|
|
546
|
+
? `${input.result.category} issue`
|
|
547
|
+
: "issue";
|
|
548
|
+
return input.action === "PR"
|
|
549
|
+
? `Magi accepted this ${category} and will prepare an implementation pull request.`
|
|
550
|
+
: `Magi accepted this ${category}.`;
|
|
551
|
+
}
|
|
552
|
+
if (input.result.disposition === "rejected") {
|
|
553
|
+
const category = input.result.category
|
|
554
|
+
? `${input.result.category} issue`
|
|
555
|
+
: "issue";
|
|
556
|
+
return `Magi does not plan to act on this ${category}.`;
|
|
557
|
+
}
|
|
558
|
+
if (input.result.disposition === "duplicate") {
|
|
559
|
+
return "Magi marked this issue as a duplicate.";
|
|
560
|
+
}
|
|
561
|
+
return "Magi completed triage for this issue.";
|
|
525
562
|
}
|
|
526
563
|
function agentForKey(repository, key) {
|
|
527
564
|
const agent = repository.agents.triage?.find((item) => item.key === key);
|
|
528
565
|
if (!agent)
|
|
529
|
-
throw new Error(`Unknown triage
|
|
566
|
+
throw new Error(`Unknown triage voter: ${key}`);
|
|
530
567
|
return agent;
|
|
531
568
|
}
|
|
532
569
|
function askOutputs(outputs) {
|
|
533
570
|
return (outputs ?? []).filter((output) => output.vote === "ASK");
|
|
534
571
|
}
|
|
535
572
|
function chooseDecisionReason(input) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
573
|
+
if (!input.vote)
|
|
574
|
+
return undefined;
|
|
575
|
+
const canonicalVoter = input.voters?.[input.threshold - 1];
|
|
576
|
+
const canonicalReason = input.outputs?.find((output) => output.voter === canonicalVoter && output.vote === input.vote);
|
|
577
|
+
return (canonicalReason?.reason ??
|
|
578
|
+
input.outputs?.find((output) => output.vote === input.vote)?.reason);
|
|
541
579
|
}
|
|
542
580
|
async function postMarkedIssueComment(input) {
|
|
543
581
|
const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
|
|
@@ -823,7 +861,7 @@ async function createImplementationPr(input) {
|
|
|
823
861
|
parse: parseTriageCreatePrOutput,
|
|
824
862
|
permission: creator.permission,
|
|
825
863
|
prompt,
|
|
826
|
-
repairAttempts: 3,
|
|
864
|
+
repairAttempts: input.input.config.output?.repairAttempts ?? 3,
|
|
827
865
|
schemaName: "triage create PR",
|
|
828
866
|
signal: input.input.signal,
|
|
829
867
|
title: `Magi triage create PR #${input.issue.number}`,
|
|
@@ -867,7 +905,7 @@ export async function runTriage(input) {
|
|
|
867
905
|
throw new Error("triage configuration is required");
|
|
868
906
|
const agents = input.repository.agents.triage;
|
|
869
907
|
if (!agents?.length)
|
|
870
|
-
throw new Error("triage.
|
|
908
|
+
throw new Error("triage.voters is required");
|
|
871
909
|
const runId = input.runId ?? `run-${Date.now().toString(36)}`;
|
|
872
910
|
const outputDir = issueRunOutputDir({
|
|
873
911
|
config: input.config,
|
|
@@ -985,12 +1023,7 @@ export async function runTriage(input) {
|
|
|
985
1023
|
input,
|
|
986
1024
|
outputDir,
|
|
987
1025
|
});
|
|
988
|
-
|
|
989
|
-
commentReason = chooseDecisionReason({
|
|
990
|
-
outputs: reconsideration.outputs,
|
|
991
|
-
reporter,
|
|
992
|
-
vote: reconsideration.vote ?? "ASK",
|
|
993
|
-
});
|
|
1026
|
+
commentReason = reconsideration.reason;
|
|
994
1027
|
result =
|
|
995
1028
|
reconsideration.vote === "YES"
|
|
996
1029
|
? { category: previous.category, disposition: "accepted" }
|
|
@@ -1034,11 +1067,7 @@ export async function runTriage(input) {
|
|
|
1034
1067
|
postComment: true,
|
|
1035
1068
|
};
|
|
1036
1069
|
return finishWithResult({
|
|
1037
|
-
commentReason:
|
|
1038
|
-
outputs: existingPr.outputs,
|
|
1039
|
-
reporter: triageReporter(input.repository, issue.number),
|
|
1040
|
-
vote: "RELATED_PR_HANDLES_ISSUE",
|
|
1041
|
-
}),
|
|
1070
|
+
commentReason: existingPr.reason,
|
|
1042
1071
|
context,
|
|
1043
1072
|
input,
|
|
1044
1073
|
issue,
|
|
@@ -1119,12 +1148,7 @@ export async function runTriage(input) {
|
|
|
1119
1148
|
schemaName: "triage acceptance",
|
|
1120
1149
|
votes: BINARY_VOTES,
|
|
1121
1150
|
});
|
|
1122
|
-
|
|
1123
|
-
commentReason = chooseDecisionReason({
|
|
1124
|
-
outputs: acceptance.outputs,
|
|
1125
|
-
reporter,
|
|
1126
|
-
vote: acceptance.vote ?? "ASK",
|
|
1127
|
-
});
|
|
1151
|
+
commentReason = acceptance.reason;
|
|
1128
1152
|
result =
|
|
1129
1153
|
acceptance.vote === "YES"
|
|
1130
1154
|
? { category, disposition: "accepted" }
|
package/dist/prompts/compose.js
CHANGED
|
@@ -339,38 +339,6 @@ async function composeTriageVotePrompt(input) {
|
|
|
339
339
|
.filter(Boolean)
|
|
340
340
|
.join("\n\n");
|
|
341
341
|
}
|
|
342
|
-
export async function composeTriageCommentPrompt(input) {
|
|
343
|
-
const values = triageValues(input);
|
|
344
|
-
const task = await taskBlock({
|
|
345
|
-
builtin: "triage/comment",
|
|
346
|
-
customPath: input.repository.triage?.prompts.comment,
|
|
347
|
-
directory: input.directory,
|
|
348
|
-
values,
|
|
349
|
-
});
|
|
350
|
-
return [
|
|
351
|
-
task,
|
|
352
|
-
languageBlock(input.repository.language),
|
|
353
|
-
`<context>\n${input.context}\n</context>`,
|
|
354
|
-
]
|
|
355
|
-
.filter(Boolean)
|
|
356
|
-
.join("\n\n");
|
|
357
|
-
}
|
|
358
|
-
export async function composeTriageQuestionPrompt(input) {
|
|
359
|
-
const values = triageValues(input);
|
|
360
|
-
const task = await taskBlock({
|
|
361
|
-
builtin: "triage/question",
|
|
362
|
-
customPath: input.repository.triage?.prompts.question,
|
|
363
|
-
directory: input.directory,
|
|
364
|
-
values,
|
|
365
|
-
});
|
|
366
|
-
return [
|
|
367
|
-
task,
|
|
368
|
-
languageBlock(input.repository.language),
|
|
369
|
-
`<context>\n${input.context}\n</context>`,
|
|
370
|
-
]
|
|
371
|
-
.filter(Boolean)
|
|
372
|
-
.join("\n\n");
|
|
373
|
-
}
|
|
374
342
|
export async function composeTriageCreatePrPrompt(input) {
|
|
375
343
|
const values = triageValues(input);
|
|
376
344
|
const task = await taskBlock({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Decide whether an existing related pull request already handles issue #{issue} in {owner}/{repo}.
|
|
2
2
|
|
|
3
|
-
Use only the provided context. Return
|
|
3
|
+
Use only the provided context. Return RELATED_PR_HANDLES_ISSUE only when the PR clearly addresses the issue. Otherwise return RELATED_PR_DOES_NOT_HANDLE_ISSUE.
|
|
4
4
|
|
|
5
5
|
<context>
|
|
6
6
|
{context}
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -60,8 +60,7 @@
|
|
|
60
60
|
"additionalProperties": false,
|
|
61
61
|
"properties": {
|
|
62
62
|
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
63
|
-
"model": { "
|
|
64
|
-
"options": { "type": "object", "additionalProperties": true },
|
|
63
|
+
"model": { "$ref": "#/$defs/modelConfig" },
|
|
65
64
|
"account": { "type": "string", "minLength": 1 },
|
|
66
65
|
"author": {
|
|
67
66
|
"type": "object",
|
|
@@ -83,8 +82,7 @@
|
|
|
83
82
|
"properties": {
|
|
84
83
|
"ref": { "type": "string", "minLength": 1 },
|
|
85
84
|
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
86
|
-
"model": { "
|
|
87
|
-
"options": { "type": "object", "additionalProperties": true },
|
|
85
|
+
"model": { "$ref": "#/$defs/modelConfig" },
|
|
88
86
|
"account": { "type": "string", "minLength": 1 },
|
|
89
87
|
"permissions": { "$ref": "#/$defs/permissions" },
|
|
90
88
|
"persona": { "type": "string" }
|
|
@@ -97,8 +95,7 @@
|
|
|
97
95
|
"additionalProperties": false,
|
|
98
96
|
"properties": {
|
|
99
97
|
"ref": { "type": "string", "minLength": 1 },
|
|
100
|
-
"model": { "
|
|
101
|
-
"options": { "type": "object", "additionalProperties": true },
|
|
98
|
+
"model": { "$ref": "#/$defs/modelConfig" },
|
|
102
99
|
"account": { "type": "string", "minLength": 1 },
|
|
103
100
|
"author": {
|
|
104
101
|
"type": "object",
|
|
@@ -121,9 +118,8 @@
|
|
|
121
118
|
"properties": {
|
|
122
119
|
"ref": { "type": "string", "minLength": 1 },
|
|
123
120
|
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
124
|
-
"model": { "
|
|
121
|
+
"model": { "$ref": "#/$defs/modelConfig" },
|
|
125
122
|
"account": { "type": "string", "minLength": 1 },
|
|
126
|
-
"options": { "type": "object", "additionalProperties": true },
|
|
127
123
|
"permissions": { "$ref": "#/$defs/permissions" },
|
|
128
124
|
"persona": { "type": "string" }
|
|
129
125
|
}
|
|
@@ -136,8 +132,7 @@
|
|
|
136
132
|
"properties": {
|
|
137
133
|
"ref": { "type": "string", "minLength": 1 },
|
|
138
134
|
"account": { "type": "string", "minLength": 1 },
|
|
139
|
-
"model": { "
|
|
140
|
-
"options": { "type": "object", "additionalProperties": true },
|
|
135
|
+
"model": { "$ref": "#/$defs/modelConfig" },
|
|
141
136
|
"author": {
|
|
142
137
|
"type": "object",
|
|
143
138
|
"required": ["name", "email"],
|
|
@@ -151,6 +146,31 @@
|
|
|
151
146
|
"persona": { "type": "string" }
|
|
152
147
|
}
|
|
153
148
|
},
|
|
149
|
+
"modelConfig": {
|
|
150
|
+
"oneOf": [
|
|
151
|
+
{ "type": "string", "minLength": 1 },
|
|
152
|
+
{ "$ref": "#/$defs/modelCandidate" },
|
|
153
|
+
{
|
|
154
|
+
"type": "array",
|
|
155
|
+
"minItems": 1,
|
|
156
|
+
"items": {
|
|
157
|
+
"oneOf": [
|
|
158
|
+
{ "type": "string", "minLength": 1 },
|
|
159
|
+
{ "$ref": "#/$defs/modelCandidate" }
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"modelCandidate": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"required": ["id"],
|
|
168
|
+
"additionalProperties": false,
|
|
169
|
+
"properties": {
|
|
170
|
+
"id": { "type": "string", "minLength": 1 },
|
|
171
|
+
"options": { "type": "object", "additionalProperties": true }
|
|
172
|
+
}
|
|
173
|
+
},
|
|
154
174
|
"automation": {
|
|
155
175
|
"type": "object",
|
|
156
176
|
"additionalProperties": false,
|
|
@@ -229,8 +249,6 @@
|
|
|
229
249
|
"duplicate": { "type": "string" },
|
|
230
250
|
"category": { "type": "string" },
|
|
231
251
|
"acceptance": { "type": "string" },
|
|
232
|
-
"question": { "type": "string" },
|
|
233
|
-
"comment": { "type": "string" },
|
|
234
252
|
"commentClassification": { "type": "string" },
|
|
235
253
|
"reconsider": { "type": "string" },
|
|
236
254
|
"create": { "type": "string" },
|
|
@@ -299,7 +317,7 @@
|
|
|
299
317
|
"type": "object",
|
|
300
318
|
"additionalProperties": false,
|
|
301
319
|
"properties": {
|
|
302
|
-
"
|
|
320
|
+
"reviewers": {
|
|
303
321
|
"type": "array",
|
|
304
322
|
"minItems": 3,
|
|
305
323
|
"items": { "$ref": "#/$defs/reviewer" }
|
|
@@ -334,7 +352,7 @@
|
|
|
334
352
|
"type": "object",
|
|
335
353
|
"additionalProperties": false,
|
|
336
354
|
"properties": {
|
|
337
|
-
"
|
|
355
|
+
"voters": {
|
|
338
356
|
"type": "array",
|
|
339
357
|
"minItems": 3,
|
|
340
358
|
"items": { "$ref": "#/$defs/triageAgent" }
|