opencode-magi 0.7.0 → 0.9.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 +26 -9
- package/dist/config/resolve.js +36 -1
- package/dist/config/validate.js +137 -15
- package/dist/github/commands.js +48 -6
- package/dist/orchestrator/merge.js +367 -23
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +485 -46
- package/dist/orchestrator/triage.js +249 -64
- package/dist/prompts/compose.js +59 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- package/dist/prompts/templates/merge/conflict.md +10 -0
- package/dist/prompts/templates/review/rereview.md +2 -0
- package/dist/prompts/templates/review/review.md +2 -0
- package/dist/prompts/templates/triage/acceptance.md +1 -1
- package/dist/prompts/templates/triage/signal.md +10 -0
- package/package.json +8 -8
- package/schema.json +60 -5
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ OpenCode Magi recreates the review cycle humans already run on GitHub: multiple
|
|
|
17
17
|
- Multi-agent reviews with an odd-number majority of 3 or more reviewers.
|
|
18
18
|
- Optional unanimous approval policy for merge automation when every reviewer must approve before a PR is merged.
|
|
19
19
|
- Finding-level voting before posting change requests, so only findings accepted by reviewer majority are submitted.
|
|
20
|
-
-
|
|
20
|
+
- Multi-account review mode where each reviewer posts through its configured GitHub account, plus single-account review mode where one GitHub account posts the consensus result for multiple logical reviewers.
|
|
21
21
|
- Re-review support for edited PRs: fixed threads are resolved, satisfied reviewers approve, and remaining issues are posted as additional comments.
|
|
22
22
|
- Optional merge and close automation where an editor agent responds on behalf of the author, fixes changes it agrees with, pushes commits when needed, and repeats the reviewer/editor cycle until the PR can be approved, queued, merged, or closed.
|
|
23
23
|
- Per-agent OpenCode permissions for reviewer, CI classifier, and editor child sessions.
|
|
@@ -64,20 +64,18 @@ Add the following content to the configuration file.
|
|
|
64
64
|
"agents": {
|
|
65
65
|
"refs": {
|
|
66
66
|
"account-1": {
|
|
67
|
-
"model": "openai/gpt-5.5"
|
|
68
|
-
"account": "account-1"
|
|
67
|
+
"model": "openai/gpt-5.5"
|
|
69
68
|
},
|
|
70
69
|
"account-2": {
|
|
71
|
-
"model": "anthropic/claude-opus-4-7"
|
|
72
|
-
"account": "account-2"
|
|
70
|
+
"model": "anthropic/claude-opus-4-7"
|
|
73
71
|
},
|
|
74
72
|
"account-3": {
|
|
75
|
-
"model": "opencode/kimi-k2-6"
|
|
76
|
-
"account": "account-3"
|
|
73
|
+
"model": "opencode/kimi-k2-6"
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
76
|
},
|
|
80
77
|
"review": {
|
|
78
|
+
"account": "your-account",
|
|
81
79
|
"reviewers": [
|
|
82
80
|
{ "ref": "account-1" },
|
|
83
81
|
{ "ref": "account-2" },
|
|
@@ -87,7 +85,26 @@ Add the following content to the configuration file.
|
|
|
87
85
|
}
|
|
88
86
|
```
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
By default, `review.mode` is `"single"`. Magi uses one `review.account` to post reviewer-originated GitHub mutations while still running multiple logical reviewer agents and preserving majority voting, finding validation, and close reconsideration. The account must be authenticated with `gh auth token --user <account>`.
|
|
89
|
+
|
|
90
|
+
For team setups that need separate GitHub review identities, set `review.mode: "multi"` and configure a unique account for each reviewer.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"review": {
|
|
95
|
+
"mode": "multi",
|
|
96
|
+
"reviewers": [
|
|
97
|
+
{ "id": "general", "model": "openai/gpt-5.5", "account": "account-1" },
|
|
98
|
+
{
|
|
99
|
+
"id": "security",
|
|
100
|
+
"model": "anthropic/claude-opus-4-7",
|
|
101
|
+
"account": "account-2"
|
|
102
|
+
},
|
|
103
|
+
{ "id": "compat", "model": "opencode/kimi-k2-6", "account": "account-3" }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
91
108
|
|
|
92
109
|
#### Set project config
|
|
93
110
|
|
|
@@ -165,7 +182,7 @@ Entries with `ref` are expanded from `agents.refs`. Fields set alongside `ref` o
|
|
|
165
182
|
}
|
|
166
183
|
```
|
|
167
184
|
|
|
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.
|
|
185
|
+
After `refs` are expanded, `review.reviewers[].account` is the GitHub account used to post reviews and approvals in `multi` mode. Must be authenticated with `gh auth token --user <account>` and must be unique. In `single` mode, `review.account` is used for reviewer-originated review posts, approvals, change requests, close comments, reviewer replies, and reviewer thread resolutions. `merge.editor.account` is still used by `/magi:merge` to push fixes, close PRs, and merge PRs.
|
|
169
186
|
|
|
170
187
|
#### Validate config
|
|
171
188
|
|
package/dist/config/resolve.js
CHANGED
|
@@ -24,6 +24,31 @@ const DEFAULT_TRIAGE_CATEGORIES = [
|
|
|
24
24
|
types: ["Feature"],
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
|
+
export const DEFAULT_TRIAGE_LABEL_RULES = [
|
|
28
|
+
{ remove: ["triage"], when: { disposition: "accepted" } },
|
|
29
|
+
{
|
|
30
|
+
add: ["duplicate"],
|
|
31
|
+
remove: ["triage"],
|
|
32
|
+
when: { disposition: "duplicate" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
add: ["duplicate"],
|
|
36
|
+
remove: ["triage"],
|
|
37
|
+
when: { disposition: "already_handled" },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
add: ["wontfix"],
|
|
41
|
+
remove: ["triage"],
|
|
42
|
+
when: { disposition: "rejected" },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
add: ["invalid"],
|
|
46
|
+
remove: ["triage"],
|
|
47
|
+
when: { disposition: "invalid" },
|
|
48
|
+
},
|
|
49
|
+
{ add: ["question"], when: { disposition: "needs_category" } },
|
|
50
|
+
{ add: ["question"], when: { disposition: "needs_acceptance" } },
|
|
51
|
+
];
|
|
27
52
|
export function reviewerKey(reviewer, index) {
|
|
28
53
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
29
54
|
}
|
|
@@ -88,6 +113,9 @@ export function resolveAgents(config) {
|
|
|
88
113
|
const agents = config.agents ?? {};
|
|
89
114
|
const editor = config.merge?.editor;
|
|
90
115
|
const creator = config.triage?.creator;
|
|
116
|
+
const singleReviewAccount = config.review && config.review.mode !== "multi"
|
|
117
|
+
? config.review.account
|
|
118
|
+
: undefined;
|
|
91
119
|
return {
|
|
92
120
|
editor: editor
|
|
93
121
|
? {
|
|
@@ -98,6 +126,7 @@ export function resolveAgents(config) {
|
|
|
98
126
|
: undefined,
|
|
99
127
|
reviewers: (config.review?.reviewers ?? []).map((reviewer, index) => ({
|
|
100
128
|
...reviewer,
|
|
129
|
+
account: singleReviewAccount ?? reviewer.account ?? "",
|
|
101
130
|
key: reviewerKey(reviewer, index),
|
|
102
131
|
index,
|
|
103
132
|
model: normalizedModel(reviewer.model),
|
|
@@ -138,6 +167,7 @@ export function resolveRepository(config) {
|
|
|
138
167
|
agents: resolveAgents(config),
|
|
139
168
|
automation: {
|
|
140
169
|
close: config.merge?.automation?.close ?? false,
|
|
170
|
+
conflict: config.merge?.automation?.conflict ?? false,
|
|
141
171
|
merge: config.merge?.automation?.merge ?? true,
|
|
142
172
|
},
|
|
143
173
|
checks: {
|
|
@@ -178,6 +208,10 @@ export function resolveRepository(config) {
|
|
|
178
208
|
review: config.review?.prompts?.review,
|
|
179
209
|
reviewGuidelines: config.review?.prompts?.reviewGuidelines,
|
|
180
210
|
},
|
|
211
|
+
review: {
|
|
212
|
+
account: config.review?.account,
|
|
213
|
+
mode: config.review?.mode ?? "single",
|
|
214
|
+
},
|
|
181
215
|
reviewAutomation: {
|
|
182
216
|
close: config.review?.automation?.close ?? false,
|
|
183
217
|
merge: config.review?.automation?.merge ?? true,
|
|
@@ -190,9 +224,9 @@ export function resolveRepository(config) {
|
|
|
190
224
|
},
|
|
191
225
|
triage: {
|
|
192
226
|
automation: {
|
|
193
|
-
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
194
227
|
close: config.triage?.automation?.close ?? false,
|
|
195
228
|
create: config.triage?.automation?.create ?? false,
|
|
229
|
+
label: config.triage?.automation?.label ?? DEFAULT_TRIAGE_LABEL_RULES,
|
|
196
230
|
merge: config.triage?.automation?.merge ?? false,
|
|
197
231
|
review: config.triage?.automation?.review ?? false,
|
|
198
232
|
},
|
|
@@ -215,6 +249,7 @@ export function resolveRepository(config) {
|
|
|
215
249
|
blockedLabels: config.triage?.safety?.blockedLabels ?? [],
|
|
216
250
|
requiredLabels: config.triage?.safety?.requiredLabels ?? ["triage"],
|
|
217
251
|
},
|
|
252
|
+
signals: config.triage?.signals ?? [],
|
|
218
253
|
worktree: config.triage?.worktree,
|
|
219
254
|
},
|
|
220
255
|
};
|
package/dist/config/validate.js
CHANGED
|
@@ -54,10 +54,12 @@ const TRIAGE_CREATOR_KEYS = new Set([
|
|
|
54
54
|
const AUTHOR_KEYS = new Set(["email", "name"]);
|
|
55
55
|
const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
|
|
56
56
|
const REVIEW_KEYS = new Set([
|
|
57
|
+
"account",
|
|
57
58
|
"automation",
|
|
58
59
|
"checks",
|
|
59
60
|
"concurrency",
|
|
60
61
|
"merge",
|
|
62
|
+
"mode",
|
|
61
63
|
"output",
|
|
62
64
|
"prompts",
|
|
63
65
|
"reviewers",
|
|
@@ -80,6 +82,7 @@ const TRIAGE_KEYS = new Set([
|
|
|
80
82
|
"prompts",
|
|
81
83
|
"reporter",
|
|
82
84
|
"safety",
|
|
85
|
+
"signals",
|
|
83
86
|
"voters",
|
|
84
87
|
"worktree",
|
|
85
88
|
]);
|
|
@@ -93,17 +96,24 @@ const REVIEW_MERGE_KEYS = new Set([
|
|
|
93
96
|
const REVIEW_CHECKS_KEYS = new Set(["exclude", "retryFailedJobs", "wait"]);
|
|
94
97
|
const MERGE_CHECKS_KEYS = new Set(["wait"]);
|
|
95
98
|
const AUTOMATION_KEYS = new Set(["close", "merge"]);
|
|
99
|
+
const MERGE_AUTOMATION_KEYS = new Set(["close", "conflict", "merge"]);
|
|
96
100
|
const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
97
101
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
98
102
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
99
103
|
const TRIAGE_AUTOMATION_KEYS = new Set([
|
|
100
|
-
"clear",
|
|
101
104
|
"close",
|
|
102
105
|
"create",
|
|
106
|
+
"label",
|
|
103
107
|
"merge",
|
|
104
108
|
"review",
|
|
105
109
|
]);
|
|
106
110
|
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
111
|
+
const TRIAGE_LABEL_RULE_KEYS = new Set(["add", "remove", "when"]);
|
|
112
|
+
const TRIAGE_LABEL_RULE_WHEN_KEYS = new Set([
|
|
113
|
+
"category",
|
|
114
|
+
"disposition",
|
|
115
|
+
"signals",
|
|
116
|
+
]);
|
|
107
117
|
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
108
118
|
const TRIAGE_SAFETY_KEYS = new Set([
|
|
109
119
|
"allowAuthors",
|
|
@@ -112,6 +122,18 @@ const TRIAGE_SAFETY_KEYS = new Set([
|
|
|
112
122
|
"blockedLabels",
|
|
113
123
|
"requiredLabels",
|
|
114
124
|
]);
|
|
125
|
+
const TRIAGE_SIGNAL_KEYS = new Set(["description", "id"]);
|
|
126
|
+
const TRIAGE_DISPOSITIONS = new Set([
|
|
127
|
+
"accepted",
|
|
128
|
+
"rejected",
|
|
129
|
+
"invalid",
|
|
130
|
+
"duplicate",
|
|
131
|
+
"already_handled",
|
|
132
|
+
"needs_category",
|
|
133
|
+
"needs_acceptance",
|
|
134
|
+
"blocked",
|
|
135
|
+
"failed",
|
|
136
|
+
]);
|
|
115
137
|
const SAFETY_KEYS = new Set([
|
|
116
138
|
"allowAuthors",
|
|
117
139
|
"blockedPaths",
|
|
@@ -364,7 +386,7 @@ function validateAndNormalizeModel(target, path, errors, catalog) {
|
|
|
364
386
|
}
|
|
365
387
|
errors.push(`${path} must contain at least one usable OpenCode model candidate${candidateErrors.length ? ` (${candidateErrors.join("; ")})` : ""}`);
|
|
366
388
|
}
|
|
367
|
-
function validateReviewerList(reviewers, path, errors, catalog) {
|
|
389
|
+
function validateReviewerList(reviewers, path, errors, catalog, mode = "single") {
|
|
368
390
|
if (reviewers == null)
|
|
369
391
|
return;
|
|
370
392
|
if (!Array.isArray(reviewers)) {
|
|
@@ -384,7 +406,7 @@ function validateReviewerList(reviewers, path, errors, catalog) {
|
|
|
384
406
|
if (!reviewer.model)
|
|
385
407
|
errors.push(`${path}[${index}].model is required`);
|
|
386
408
|
validateAndNormalizeModel(reviewer, `${path}[${index}].model`, errors, catalog);
|
|
387
|
-
if (!reviewer.account)
|
|
409
|
+
if (mode === "multi" && !reviewer.account)
|
|
388
410
|
errors.push(`${path}[${index}].account is required`);
|
|
389
411
|
validateString(reviewer.account, `${path}[${index}].account`, errors);
|
|
390
412
|
validateString(reviewer.persona, `${path}[${index}].persona`, errors);
|
|
@@ -434,18 +456,31 @@ function validateTriageAgentList(voters, path, errors, catalog) {
|
|
|
434
456
|
}
|
|
435
457
|
});
|
|
436
458
|
}
|
|
437
|
-
function validateResolvedReviewers(reviewers, path, errors) {
|
|
459
|
+
function validateResolvedReviewers(reviewers, path, errors, mode = "single") {
|
|
438
460
|
const keys = new Set();
|
|
439
461
|
const accounts = new Set();
|
|
440
462
|
for (const reviewer of reviewers) {
|
|
441
463
|
if (keys.has(reviewer.key))
|
|
442
464
|
errors.push(`${path} has duplicate reviewer key: ${reviewer.key}`);
|
|
443
465
|
keys.add(reviewer.key);
|
|
444
|
-
if (accounts.has(reviewer.account))
|
|
466
|
+
if (mode === "multi" && accounts.has(reviewer.account))
|
|
445
467
|
errors.push(`${path} has duplicate reviewer account: ${reviewer.account}`);
|
|
446
468
|
accounts.add(reviewer.account);
|
|
447
469
|
}
|
|
448
470
|
}
|
|
471
|
+
function reviewMode(config) {
|
|
472
|
+
return config.review?.mode === "multi" ? "multi" : "single";
|
|
473
|
+
}
|
|
474
|
+
function validateReviewIdentity(config, errors) {
|
|
475
|
+
const mode = config.review?.mode;
|
|
476
|
+
if (mode != null && mode !== "multi" && mode !== "single") {
|
|
477
|
+
errors.push("review.mode must be multi or single");
|
|
478
|
+
}
|
|
479
|
+
validateString(config.review?.account, "review.account", errors);
|
|
480
|
+
if ((mode == null || mode === "single") && !config.review?.account) {
|
|
481
|
+
errors.push("review.account is required when review.mode is single");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
449
484
|
function validateResolvedTriageAgents(agents, path, errors) {
|
|
450
485
|
const keys = new Set();
|
|
451
486
|
const accounts = new Set();
|
|
@@ -552,7 +587,7 @@ function validateMerge(config, errors, options) {
|
|
|
552
587
|
errors.push("merge must be an object");
|
|
553
588
|
}
|
|
554
589
|
validateKnownKeys(merge, "merge", MERGE_KEYS, errors);
|
|
555
|
-
validateBooleanObject(merge?.automation, "merge.automation",
|
|
590
|
+
validateBooleanObject(merge?.automation, "merge.automation", MERGE_AUTOMATION_KEYS, errors);
|
|
556
591
|
const checks = merge?.checks;
|
|
557
592
|
validateKnownKeys(checks, "merge.checks", MERGE_CHECKS_KEYS, errors);
|
|
558
593
|
validateBoolean(checks?.wait, "merge.checks.wait", errors);
|
|
@@ -691,6 +726,79 @@ function validateTriageCategories(categories, path, errors) {
|
|
|
691
726
|
validateString(category.description, `${itemPath}.description`, errors);
|
|
692
727
|
});
|
|
693
728
|
}
|
|
729
|
+
function validateTriageSignals(signals, path, errors) {
|
|
730
|
+
if (signals == null)
|
|
731
|
+
return;
|
|
732
|
+
if (!Array.isArray(signals)) {
|
|
733
|
+
errors.push(`${path} must be an array`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const ids = new Set();
|
|
737
|
+
signals.forEach((item, index) => {
|
|
738
|
+
const itemPath = `${path}[${index}]`;
|
|
739
|
+
if (!isPlainObject(item)) {
|
|
740
|
+
errors.push(`${itemPath} must be an object`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const signal = item;
|
|
744
|
+
validateKnownKeys(signal, itemPath, TRIAGE_SIGNAL_KEYS, errors);
|
|
745
|
+
if (!signal.id) {
|
|
746
|
+
errors.push(`${itemPath}.id is required`);
|
|
747
|
+
}
|
|
748
|
+
else if (typeof signal.id !== "string") {
|
|
749
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
750
|
+
}
|
|
751
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(signal.id)) {
|
|
752
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
753
|
+
}
|
|
754
|
+
else if (ids.has(signal.id)) {
|
|
755
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
ids.add(signal.id);
|
|
759
|
+
}
|
|
760
|
+
if (!signal.description) {
|
|
761
|
+
errors.push(`${itemPath}.description is required`);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
validateString(signal.description, `${itemPath}.description`, errors);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function validateTriageLabelRules(rules, path, errors) {
|
|
769
|
+
if (rules == null)
|
|
770
|
+
return;
|
|
771
|
+
if (!Array.isArray(rules)) {
|
|
772
|
+
errors.push(`${path} must be an array`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
rules.forEach((item, index) => {
|
|
776
|
+
const itemPath = `${path}[${index}]`;
|
|
777
|
+
if (!isPlainObject(item)) {
|
|
778
|
+
errors.push(`${itemPath} must be an object`);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const rule = item;
|
|
782
|
+
validateKnownKeys(rule, itemPath, TRIAGE_LABEL_RULE_KEYS, errors);
|
|
783
|
+
validateStringArray(rule.add, `${itemPath}.add`, errors);
|
|
784
|
+
validateStringArray(rule.remove, `${itemPath}.remove`, errors);
|
|
785
|
+
if (!isPlainObject(rule.when)) {
|
|
786
|
+
errors.push(`${itemPath}.when must be an object`);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
validateKnownKeys(rule.when, `${itemPath}.when`, TRIAGE_LABEL_RULE_WHEN_KEYS, errors);
|
|
790
|
+
if (!Object.keys(rule.when).length) {
|
|
791
|
+
errors.push(`${itemPath}.when must not be empty`);
|
|
792
|
+
}
|
|
793
|
+
if (rule.when.disposition != null &&
|
|
794
|
+
(typeof rule.when.disposition !== "string" ||
|
|
795
|
+
!TRIAGE_DISPOSITIONS.has(rule.when.disposition))) {
|
|
796
|
+
errors.push(`${itemPath}.when.disposition must be a triage disposition`);
|
|
797
|
+
}
|
|
798
|
+
validateString(rule.when.category, `${itemPath}.when.category`, errors);
|
|
799
|
+
validateStringArray(rule.when.signals, `${itemPath}.when.signals`, errors);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
694
802
|
function validateSafety(config, errors) {
|
|
695
803
|
const safety = config.review?.safety;
|
|
696
804
|
if (safety != null && !isPlainObject(safety)) {
|
|
@@ -764,7 +872,7 @@ function validateTriage(config, errors, options) {
|
|
|
764
872
|
validateBoolean(automation?.create, "triage.automation.create", errors);
|
|
765
873
|
validateBoolean(automation?.merge, "triage.automation.merge", errors);
|
|
766
874
|
validateBoolean(automation?.review, "triage.automation.review", errors);
|
|
767
|
-
|
|
875
|
+
validateTriageLabelRules(automation?.label, "triage.automation.label", errors);
|
|
768
876
|
if (automation?.review && !automation.create) {
|
|
769
877
|
errors.push("triage.automation.review requires triage.automation.create to be true");
|
|
770
878
|
}
|
|
@@ -779,6 +887,7 @@ function validateTriage(config, errors, options) {
|
|
|
779
887
|
errors.push("triage.concurrency.runs must be a positive integer");
|
|
780
888
|
}
|
|
781
889
|
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
890
|
+
validateTriageSignals(triage.signals, "triage.signals", errors);
|
|
782
891
|
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
783
892
|
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
784
893
|
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
|
@@ -814,8 +923,14 @@ async function validatePrompts(config, errors, directory) {
|
|
|
814
923
|
async function validateAuth(config, exec, errors) {
|
|
815
924
|
const accounts = new Set();
|
|
816
925
|
const agents = resolveAgents(config);
|
|
817
|
-
|
|
818
|
-
|
|
926
|
+
if (reviewMode(config) === "single") {
|
|
927
|
+
if (config.review?.account)
|
|
928
|
+
accounts.add(config.review.account);
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
for (const reviewer of agents.reviewers)
|
|
932
|
+
accounts.add(reviewer.account);
|
|
933
|
+
}
|
|
819
934
|
for (const agent of agents.triage ?? [])
|
|
820
935
|
accounts.add(agent.account);
|
|
821
936
|
if (agents.editor)
|
|
@@ -862,15 +977,20 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
862
977
|
if (!config.github?.owner || !config.github.repo)
|
|
863
978
|
return;
|
|
864
979
|
const agents = resolveAgents(config);
|
|
865
|
-
|
|
980
|
+
const reviewAccounts = reviewMode(config) === "single"
|
|
981
|
+
? config.review?.account
|
|
982
|
+
? [config.review.account]
|
|
983
|
+
: []
|
|
984
|
+
: agents.reviewers.map((reviewer) => reviewer.account);
|
|
985
|
+
await Promise.all(reviewAccounts.map(async (account) => {
|
|
866
986
|
try {
|
|
867
|
-
const permissions = await fetchPermissions(config, exec,
|
|
987
|
+
const permissions = await fetchPermissions(config, exec, account);
|
|
868
988
|
if (!permissions.pull) {
|
|
869
|
-
errors.push(`GitHub account cannot read repository for PR review: ${
|
|
989
|
+
errors.push(`GitHub account cannot read repository for PR review: ${account}`);
|
|
870
990
|
}
|
|
871
991
|
}
|
|
872
992
|
catch (error) {
|
|
873
|
-
warnings.push(`Could not validate repository permissions for GitHub account: ${
|
|
993
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${account} (${error.message})`);
|
|
874
994
|
}
|
|
875
995
|
}));
|
|
876
996
|
await Promise.all((agents.triage ?? []).map(async (agent) => {
|
|
@@ -932,15 +1052,17 @@ export async function validateConfig(config, options = {}) {
|
|
|
932
1052
|
errors.push("review is required");
|
|
933
1053
|
}
|
|
934
1054
|
else if (config.review) {
|
|
1055
|
+
const mode = reviewMode(config);
|
|
935
1056
|
if (!isPlainObject(config.review)) {
|
|
936
1057
|
errors.push("review must be an object");
|
|
937
1058
|
}
|
|
938
1059
|
else {
|
|
939
1060
|
validateKnownKeys(config.review, "review", REVIEW_KEYS, errors);
|
|
940
1061
|
}
|
|
1062
|
+
validateReviewIdentity(config, errors);
|
|
941
1063
|
if (!config.review.reviewers)
|
|
942
1064
|
errors.push("review.reviewers is required");
|
|
943
|
-
validateReviewerList(config.review.reviewers, "review.reviewers", errors, options.modelCatalog);
|
|
1065
|
+
validateReviewerList(config.review.reviewers, "review.reviewers", errors, options.modelCatalog, mode);
|
|
944
1066
|
if (Array.isArray(config.review.reviewers)) {
|
|
945
1067
|
validateResolvedReviewers(config.review.reviewers.map((reviewer, index) => ({
|
|
946
1068
|
account: reviewer &&
|
|
@@ -951,7 +1073,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
951
1073
|
key: reviewer && typeof reviewer === "object"
|
|
952
1074
|
? reviewerKey(reviewer, index)
|
|
953
1075
|
: "",
|
|
954
|
-
})), "review.resolvedReviewers", errors);
|
|
1076
|
+
})), "review.resolvedReviewers", errors, mode);
|
|
955
1077
|
}
|
|
956
1078
|
}
|
|
957
1079
|
if (options.requireTriage && !config.triage) {
|
package/dist/github/commands.js
CHANGED
|
@@ -404,6 +404,15 @@ export async function assignIssue(exec, repository, issue, account) {
|
|
|
404
404
|
const token = await ghToken(exec, repository, account);
|
|
405
405
|
return exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-assignee ${shellQuote(account)}`, ghTokenEnv(token));
|
|
406
406
|
}
|
|
407
|
+
export async function addIssueLabels(exec, repository, issue, labels, account) {
|
|
408
|
+
const token = await ghToken(exec, repository, account);
|
|
409
|
+
const added = [];
|
|
410
|
+
for (const label of labels) {
|
|
411
|
+
await exec(`gh issue edit ${issue} --repo ${shellQuote(repoSpecifier(repository))} --add-label ${shellQuote(label)}`, ghTokenEnv(token));
|
|
412
|
+
added.push(label);
|
|
413
|
+
}
|
|
414
|
+
return added;
|
|
415
|
+
}
|
|
407
416
|
export async function removeIssueLabels(exec, repository, issue, labels, account) {
|
|
408
417
|
const token = await ghToken(exec, repository, account);
|
|
409
418
|
const removed = [];
|
|
@@ -583,8 +592,18 @@ export async function removeWorktree(exec, worktreePath) {
|
|
|
583
592
|
export async function removeBranch(exec, branch) {
|
|
584
593
|
await exec(`git branch -D ${shellQuote(branch)}`);
|
|
585
594
|
}
|
|
586
|
-
export async function postApproval(exec, repository, pr, account) {
|
|
595
|
+
export async function postApproval(exec, repository, pr, account, body) {
|
|
587
596
|
const token = await ghToken(exec, repository, account);
|
|
597
|
+
if (body != null) {
|
|
598
|
+
const payloadPath = join(tmpdir(), `magi-approve-${process.pid}-${Date.now()}.json`);
|
|
599
|
+
await writeFile(payloadPath, JSON.stringify({ body, event: "APPROVE" }));
|
|
600
|
+
try {
|
|
601
|
+
return await exec(`gh api${ghHostOption(repository)} repos/${repository.github.owner}/${repository.github.repo}/pulls/${pr}/reviews --method POST --input ${shellQuote(payloadPath)} --jq .html_url`, ghTokenEnv(token));
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
await rm(payloadPath, { force: true });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
588
607
|
return exec(`gh pr review ${pr} --repo ${shellQuote(repoSpecifier(repository))} --approve`, ghTokenEnv(token));
|
|
589
608
|
}
|
|
590
609
|
export async function postCloseComment(exec, repository, pr, account, body) {
|
|
@@ -598,9 +617,9 @@ export async function postCloseComment(exec, repository, pr, account, body) {
|
|
|
598
617
|
await rm(payloadPath, { force: true });
|
|
599
618
|
}
|
|
600
619
|
}
|
|
601
|
-
function findingComment(finding) {
|
|
620
|
+
function findingComment(finding, body) {
|
|
602
621
|
const comment = {
|
|
603
|
-
body: `**Issue:** ${finding.issue}\n\n**Fix:** ${finding.fix}`,
|
|
622
|
+
body: body ?? `**Issue:** ${finding.issue}\n\n**Fix:** ${finding.fix}`,
|
|
604
623
|
line: finding.line,
|
|
605
624
|
path: finding.path,
|
|
606
625
|
side: "RIGHT",
|
|
@@ -616,13 +635,13 @@ function changesRequestedBody(findings) {
|
|
|
616
635
|
? "Changes requested: 1 inline comment."
|
|
617
636
|
: `Changes requested: ${findings.length} inline comments.`;
|
|
618
637
|
}
|
|
619
|
-
export async function postChangesRequested(exec, repository, pr, account, findings) {
|
|
638
|
+
export async function postChangesRequested(exec, repository, pr, account, findings, options = {}) {
|
|
620
639
|
const token = await ghToken(exec, repository, account);
|
|
621
640
|
const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
|
|
622
|
-
const body = changesRequestedBody(findings);
|
|
641
|
+
const body = options.body ?? changesRequestedBody(findings);
|
|
623
642
|
await writeFile(payloadPath, JSON.stringify({
|
|
624
643
|
body,
|
|
625
|
-
comments: findings.map(findingComment),
|
|
644
|
+
comments: findings.map((finding, index) => findingComment(finding, options.commentBodies?.[index])),
|
|
626
645
|
event: "REQUEST_CHANGES",
|
|
627
646
|
}));
|
|
628
647
|
try {
|
|
@@ -685,6 +704,29 @@ export async function waitForAutoMerge(exec, repository, pr, intervalMs = 30_000
|
|
|
685
704
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
686
705
|
}
|
|
687
706
|
}
|
|
707
|
+
export async function fetchBaseBranch(exec, repository, meta, worktreePath) {
|
|
708
|
+
await exec(`git fetch --no-tags ${shellQuote(repositoryGitUrl(repository, repository.github.owner, repository.github.repo))} ${shellQuote(`refs/heads/${meta.baseRefName}`)}`, { cwd: worktreePath });
|
|
709
|
+
}
|
|
710
|
+
export async function mergeBaseNoCommit(exec, baseSha, worktreePath) {
|
|
711
|
+
await exec(`git merge --no-commit --no-ff ${shellQuote(baseSha)}`, {
|
|
712
|
+
cwd: worktreePath,
|
|
713
|
+
}).catch(() => undefined);
|
|
714
|
+
}
|
|
715
|
+
export async function listUnmergedFiles(exec, worktreePath) {
|
|
716
|
+
const output = await exec("git diff --name-only --diff-filter=U", {
|
|
717
|
+
cwd: worktreePath,
|
|
718
|
+
});
|
|
719
|
+
return output
|
|
720
|
+
.split("\n")
|
|
721
|
+
.map((line) => line.trim())
|
|
722
|
+
.filter(Boolean);
|
|
723
|
+
}
|
|
724
|
+
export async function abortMerge(exec, worktreePath) {
|
|
725
|
+
await exec("git merge --abort", { cwd: worktreePath }).catch(() => undefined);
|
|
726
|
+
}
|
|
727
|
+
export async function currentHeadSha(exec, worktreePath) {
|
|
728
|
+
return (await exec("git rev-parse HEAD", { cwd: worktreePath })).trim();
|
|
729
|
+
}
|
|
688
730
|
export async function closePullRequest(exec, repository, pr, account) {
|
|
689
731
|
const token = await ghToken(exec, repository, account);
|
|
690
732
|
return exec(`gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`, ghTokenEnv(token));
|