opencode-magi 0.3.0 → 0.5.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 +53 -45
- package/dist/config/validate.js +51 -0
- package/dist/github/commands.js +13 -12
- package/dist/index.js +41 -2
- package/dist/orchestrator/findings.js +3 -4
- package/dist/orchestrator/merge.js +68 -29
- package/dist/orchestrator/report.js +1 -8
- package/dist/orchestrator/review-context.js +37 -4
- package/dist/orchestrator/review.js +65 -10
- package/dist/orchestrator/run-manager.js +54 -8
- package/dist/prompts/compose.js +1 -0
- package/dist/prompts/contracts.js +28 -29
- package/dist/prompts/output.js +40 -42
- package/dist/prompts/templates/merge/edit.md +11 -5
- package/dist/prompts/templates/review/close-reconsideration.md +1 -0
- package/dist/prompts/templates/review/rereview.md +3 -0
- package/dist/prompts/templates/review/review.md +4 -0
- package/package.json +1 -1
- package/schema.json +37 -5
package/README.md
CHANGED
|
@@ -61,26 +61,33 @@ Add the following content to the configuration file.
|
|
|
61
61
|
```json
|
|
62
62
|
{
|
|
63
63
|
"$schema": "https://raw.githubusercontent.com/magi-ai/opencode-magi/main/schema.json",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
{
|
|
67
|
-
"
|
|
68
|
-
"
|
|
64
|
+
"agents": {
|
|
65
|
+
"refs": {
|
|
66
|
+
"account-1": {
|
|
67
|
+
"model": "openai/gpt-5.5",
|
|
68
|
+
"account": "account-1"
|
|
69
69
|
},
|
|
70
|
-
{
|
|
71
|
-
"
|
|
72
|
-
"
|
|
70
|
+
"account-2": {
|
|
71
|
+
"model": "anthropic/claude-opus-4-7",
|
|
72
|
+
"account": "account-2"
|
|
73
73
|
},
|
|
74
|
-
{
|
|
75
|
-
"
|
|
76
|
-
"
|
|
74
|
+
"account-3": {
|
|
75
|
+
"model": "opencode/kimi-k2-6",
|
|
76
|
+
"account": "account-3"
|
|
77
77
|
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"review": {
|
|
81
|
+
"agents": [
|
|
82
|
+
{ "ref": "account-1" },
|
|
83
|
+
{ "ref": "account-2" },
|
|
84
|
+
{ "ref": "account-3" }
|
|
78
85
|
]
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
```
|
|
82
89
|
|
|
83
|
-
`review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
|
|
90
|
+
After refs are expanded, `review.agents[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
|
|
84
91
|
|
|
85
92
|
#### Set project config
|
|
86
93
|
|
|
@@ -101,53 +108,54 @@ Add the following content to the configuration file.
|
|
|
101
108
|
"owner": "your-owner",
|
|
102
109
|
"repo": "your-repo"
|
|
103
110
|
},
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
{
|
|
107
|
-
"
|
|
108
|
-
"
|
|
111
|
+
"agents": {
|
|
112
|
+
"refs": {
|
|
113
|
+
"account-1": {
|
|
114
|
+
"model": "openai/gpt-5.5",
|
|
115
|
+
"account": "account-1"
|
|
109
116
|
},
|
|
110
|
-
{
|
|
111
|
-
"
|
|
112
|
-
"
|
|
117
|
+
"account-2": {
|
|
118
|
+
"model": "anthropic/claude-opus-4-7",
|
|
119
|
+
"account": "account-2"
|
|
113
120
|
},
|
|
114
|
-
{
|
|
115
|
-
"
|
|
116
|
-
"
|
|
121
|
+
"account-3": {
|
|
122
|
+
"model": "opencode/kimi-k2-6",
|
|
123
|
+
"account": "account-3"
|
|
124
|
+
},
|
|
125
|
+
"account-4": {
|
|
126
|
+
"model": "openai/gpt-5.5",
|
|
127
|
+
"account": "account-4",
|
|
128
|
+
"author": {
|
|
129
|
+
"name": "account-4",
|
|
130
|
+
"email": "your-email@example.com"
|
|
131
|
+
}
|
|
117
132
|
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"review": {
|
|
136
|
+
"agents": [
|
|
137
|
+
{ "ref": "account-1" },
|
|
138
|
+
{ "ref": "account-2" },
|
|
139
|
+
{ "ref": "account-3" }
|
|
118
140
|
]
|
|
119
141
|
},
|
|
120
142
|
"merge": {
|
|
121
|
-
"editor": {
|
|
122
|
-
"account": "your-editor-account",
|
|
123
|
-
"model": "openai/gpt-5.5",
|
|
124
|
-
"author": {
|
|
125
|
-
"name": "your-account",
|
|
126
|
-
"email": "your-email@example.com"
|
|
127
|
-
}
|
|
128
|
-
}
|
|
143
|
+
"editor": { "ref": "account-4" }
|
|
129
144
|
},
|
|
130
145
|
"triage": {
|
|
131
|
-
"account": "
|
|
146
|
+
"account": "account-5",
|
|
132
147
|
"agents": [
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
"id": "maintenance",
|
|
139
|
-
"model": "anthropic/claude-opus-4-7"
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
"id": "product",
|
|
143
|
-
"model": "opencode/kimi-k2-6"
|
|
144
|
-
}
|
|
148
|
+
{ "ref": "account-1" },
|
|
149
|
+
{ "ref": "account-2" },
|
|
150
|
+
{ "ref": "account-3" }
|
|
145
151
|
]
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
154
|
```
|
|
149
155
|
|
|
150
|
-
|
|
156
|
+
Entries with `ref` are expanded from `agents.refs`. Fields set alongside `ref` override fields from the preset.
|
|
157
|
+
|
|
158
|
+
After refs are expanded, `review.agents[].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.
|
|
151
159
|
|
|
152
160
|
#### Validate config
|
|
153
161
|
|
package/dist/config/validate.js
CHANGED
|
@@ -157,6 +157,56 @@ function ghHostOption(config) {
|
|
|
157
157
|
function isPlainObject(value) {
|
|
158
158
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
159
159
|
}
|
|
160
|
+
function expandAgentRefUse(value, path, refs, refsInvalid, errors) {
|
|
161
|
+
if (!isPlainObject(value) || !Object.hasOwn(value, "ref"))
|
|
162
|
+
return value;
|
|
163
|
+
const use = { ...value };
|
|
164
|
+
const ref = use.ref;
|
|
165
|
+
delete use.ref;
|
|
166
|
+
if (typeof ref !== "string") {
|
|
167
|
+
errors.push(`${path}.ref must be a string`);
|
|
168
|
+
return use;
|
|
169
|
+
}
|
|
170
|
+
if (refsInvalid) {
|
|
171
|
+
errors.push(`agents.refs must be an object to resolve ${path}.ref`);
|
|
172
|
+
return use;
|
|
173
|
+
}
|
|
174
|
+
const preset = refs?.[ref];
|
|
175
|
+
if (preset == null) {
|
|
176
|
+
errors.push(`${path}.ref references unknown agents.refs preset: ${ref}`);
|
|
177
|
+
return use;
|
|
178
|
+
}
|
|
179
|
+
if (!isPlainObject(preset)) {
|
|
180
|
+
errors.push(`agents.refs.${ref} must be an object when referenced by ${path}.ref`);
|
|
181
|
+
return use;
|
|
182
|
+
}
|
|
183
|
+
const presetFields = { ...preset };
|
|
184
|
+
delete presetFields.ref;
|
|
185
|
+
return { ...presetFields, ...use };
|
|
186
|
+
}
|
|
187
|
+
function expandAgentRefs(config, errors) {
|
|
188
|
+
if (!config || typeof config !== "object")
|
|
189
|
+
return;
|
|
190
|
+
const magiConfig = config;
|
|
191
|
+
const agents = magiConfig.agents;
|
|
192
|
+
const refsValue = isPlainObject(agents) ? agents.refs : undefined;
|
|
193
|
+
const refsInvalid = refsValue != null && !isPlainObject(refsValue);
|
|
194
|
+
const refs = isPlainObject(refsValue) ? refsValue : undefined;
|
|
195
|
+
if (Array.isArray(magiConfig.review?.agents)) {
|
|
196
|
+
magiConfig.review.agents = magiConfig.review.agents.map((agent, index) => expandAgentRefUse(agent, `review.agents[${index}]`, refs, refsInvalid, errors));
|
|
197
|
+
}
|
|
198
|
+
if (isPlainObject(magiConfig.merge?.editor)) {
|
|
199
|
+
magiConfig.merge.editor = expandAgentRefUse(magiConfig.merge.editor, "merge.editor", refs, refsInvalid, errors);
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(magiConfig.triage?.agents)) {
|
|
202
|
+
magiConfig.triage.agents = magiConfig.triage.agents.map((agent, index) => expandAgentRefUse(agent, `triage.agents[${index}]`, refs, refsInvalid, errors));
|
|
203
|
+
}
|
|
204
|
+
if (isPlainObject(magiConfig.triage?.creator)) {
|
|
205
|
+
magiConfig.triage.creator = expandAgentRefUse(magiConfig.triage.creator, "triage.creator", refs, refsInvalid, errors);
|
|
206
|
+
}
|
|
207
|
+
if (isPlainObject(magiConfig.agents))
|
|
208
|
+
delete magiConfig.agents.refs;
|
|
209
|
+
}
|
|
160
210
|
function validateKnownKeys(value, path, keys, errors) {
|
|
161
211
|
if (!isPlainObject(value))
|
|
162
212
|
return;
|
|
@@ -792,6 +842,7 @@ export async function validateConfig(config, options = {}) {
|
|
|
792
842
|
const warnings = [];
|
|
793
843
|
if (!config || typeof config !== "object")
|
|
794
844
|
errors.push("config must be an object");
|
|
845
|
+
expandAgentRefs(config, errors);
|
|
795
846
|
if (config && typeof config === "object")
|
|
796
847
|
validateJsonSchema(config, errors);
|
|
797
848
|
validateKnownKeys(config, "config", CONFIG_KEYS, errors);
|
package/dist/github/commands.js
CHANGED
|
@@ -246,6 +246,12 @@ function duplicateReferences(text) {
|
|
|
246
246
|
refs.add(Number(match[1]));
|
|
247
247
|
return [...refs];
|
|
248
248
|
}
|
|
249
|
+
function issueTitleSearchQuery(title, fallback) {
|
|
250
|
+
return (title
|
|
251
|
+
.replaceAll(/[^\p{L}\p{N}_]+/gu, " ")
|
|
252
|
+
.replaceAll(/\s+/g, " ")
|
|
253
|
+
.trim() || fallback);
|
|
254
|
+
}
|
|
249
255
|
async function fetchIssueCandidate(exec, repository, number, whyCandidate) {
|
|
250
256
|
const raw = await exec(`gh issue view ${number} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,state,body,createdAt`).catch(() => undefined);
|
|
251
257
|
if (!raw)
|
|
@@ -254,7 +260,7 @@ async function fetchIssueCandidate(exec, repository, number, whyCandidate) {
|
|
|
254
260
|
return { ...data, whyCandidate };
|
|
255
261
|
}
|
|
256
262
|
export async function searchDuplicateIssues(exec, repository, issue, limit = 5) {
|
|
257
|
-
const query = issue.title;
|
|
263
|
+
const query = issueTitleSearchQuery(issue.title, String(issue.number));
|
|
258
264
|
const explicitCandidates = await Promise.all(duplicateReferences(issue.body)
|
|
259
265
|
.filter((number) => number !== issue.number)
|
|
260
266
|
.map((number) => fetchIssueCandidate(exec, repository, number, "Issue body explicitly references a duplicate target.")));
|
|
@@ -522,20 +528,15 @@ function findingComment(finding) {
|
|
|
522
528
|
}
|
|
523
529
|
return comment;
|
|
524
530
|
}
|
|
525
|
-
function
|
|
526
|
-
return
|
|
527
|
-
|
|
528
|
-
`
|
|
529
|
-
` Fix: ${finding.fix}`,
|
|
530
|
-
].join("\n");
|
|
531
|
+
function changesRequestedBody(findings) {
|
|
532
|
+
return findings.length === 1
|
|
533
|
+
? "Changes requested: 1 inline comment."
|
|
534
|
+
: `Changes requested: ${findings.length} inline comments.`;
|
|
531
535
|
}
|
|
532
|
-
export async function postChangesRequested(exec, repository, pr, account, findings
|
|
536
|
+
export async function postChangesRequested(exec, repository, pr, account, findings) {
|
|
533
537
|
const token = await ghToken(exec, repository, account);
|
|
534
538
|
const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
|
|
535
|
-
const body = findings
|
|
536
|
-
.map((finding) => `- ${finding.issue.split("\n")[0]}`)
|
|
537
|
-
.concat(requirementFindings.map(requirementFindingSummary))
|
|
538
|
-
.join("\n");
|
|
539
|
+
const body = changesRequestedBody(findings);
|
|
539
540
|
await writeFile(payloadPath, JSON.stringify({
|
|
540
541
|
body,
|
|
541
542
|
comments: findings.map(findingComment),
|
package/dist/index.js
CHANGED
|
@@ -95,12 +95,17 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
95
95
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
96
96
|
const configOverrides = {};
|
|
97
97
|
const prTokens = [];
|
|
98
|
+
let sync = false;
|
|
98
99
|
for (let index = 0; index < tokens.length; index++) {
|
|
99
100
|
const token = tokens[index];
|
|
100
101
|
if (token === "--dry-run") {
|
|
101
102
|
dryRun = true;
|
|
102
103
|
continue;
|
|
103
104
|
}
|
|
105
|
+
if (token === "--sync") {
|
|
106
|
+
sync = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
104
109
|
switch (token) {
|
|
105
110
|
case "--language":
|
|
106
111
|
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
@@ -146,18 +151,23 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
146
151
|
prTokens.push(token);
|
|
147
152
|
}
|
|
148
153
|
}
|
|
149
|
-
return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")) };
|
|
154
|
+
return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")), sync };
|
|
150
155
|
}
|
|
151
156
|
export function parseIssueRunArguments(value, dryRun = false) {
|
|
152
157
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
153
158
|
const configOverrides = {};
|
|
154
159
|
const issueTokens = [];
|
|
160
|
+
let sync = false;
|
|
155
161
|
for (let index = 0; index < tokens.length; index++) {
|
|
156
162
|
const token = tokens[index];
|
|
157
163
|
if (token === "--dry-run") {
|
|
158
164
|
dryRun = true;
|
|
159
165
|
continue;
|
|
160
166
|
}
|
|
167
|
+
if (token === "--sync") {
|
|
168
|
+
sync = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
161
171
|
switch (token) {
|
|
162
172
|
case "--language":
|
|
163
173
|
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
@@ -195,7 +205,12 @@ export function parseIssueRunArguments(value, dryRun = false) {
|
|
|
195
205
|
issueTokens.push(token);
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
configOverrides,
|
|
210
|
+
dryRun,
|
|
211
|
+
issues: parseIssues(issueTokens.join(" ")),
|
|
212
|
+
sync,
|
|
213
|
+
};
|
|
199
214
|
}
|
|
200
215
|
function nextFlagValue(tokens, index, flag) {
|
|
201
216
|
const value = tokens[index];
|
|
@@ -203,6 +218,15 @@ function nextFlagValue(tokens, index, flag) {
|
|
|
203
218
|
throw new Error(`${flag} requires a value.`);
|
|
204
219
|
return value;
|
|
205
220
|
}
|
|
221
|
+
async function syncResult(runManager, states) {
|
|
222
|
+
const output = await runManager.formatStatesWithReports(states, {
|
|
223
|
+
verbose: true,
|
|
224
|
+
});
|
|
225
|
+
const failed = states.filter((state) => state.status !== "completed");
|
|
226
|
+
if (failed.length)
|
|
227
|
+
throw new Error(output);
|
|
228
|
+
return output;
|
|
229
|
+
}
|
|
206
230
|
function parseIntegerFlag(value, flag, minimum) {
|
|
207
231
|
const parsed = Number.parseInt(value, 10);
|
|
208
232
|
if (!Number.isInteger(parsed) ||
|
|
@@ -432,6 +456,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
432
456
|
args: {
|
|
433
457
|
prs: tool.schema.string(),
|
|
434
458
|
dryRun: tool.schema.boolean().optional(),
|
|
459
|
+
sync: tool.schema.boolean().optional(),
|
|
435
460
|
},
|
|
436
461
|
async execute(args, context) {
|
|
437
462
|
const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
|
|
@@ -448,6 +473,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
448
473
|
if (!validation.ok)
|
|
449
474
|
return JSON.stringify(validation, null, 2);
|
|
450
475
|
const repository = resolveRepository(config);
|
|
476
|
+
const sync = parsed.sync || args.sync === true;
|
|
451
477
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
452
478
|
config,
|
|
453
479
|
dryRun: parsed.dryRun,
|
|
@@ -455,7 +481,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
455
481
|
pr,
|
|
456
482
|
parentSessionId: context.sessionID,
|
|
457
483
|
signal: context.abort,
|
|
484
|
+
sync,
|
|
458
485
|
}), { signal: context.abort });
|
|
486
|
+
if (sync)
|
|
487
|
+
return syncResult(runManager, states);
|
|
459
488
|
return states
|
|
460
489
|
.map((state) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
|
|
461
490
|
.join("\n");
|
|
@@ -469,6 +498,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
469
498
|
args: {
|
|
470
499
|
prs: tool.schema.string(),
|
|
471
500
|
dryRun: tool.schema.boolean().optional(),
|
|
501
|
+
sync: tool.schema.boolean().optional(),
|
|
472
502
|
},
|
|
473
503
|
async execute(args, context) {
|
|
474
504
|
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
@@ -484,6 +514,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
484
514
|
if (!validation.ok)
|
|
485
515
|
return JSON.stringify(validation, null, 2);
|
|
486
516
|
const repository = resolveRepository(config);
|
|
517
|
+
const sync = parsed.sync || args.sync === true;
|
|
487
518
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
488
519
|
config,
|
|
489
520
|
dryRun: parsed.dryRun,
|
|
@@ -491,7 +522,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
491
522
|
pr,
|
|
492
523
|
parentSessionId: context.sessionID,
|
|
493
524
|
signal: context.abort,
|
|
525
|
+
sync,
|
|
494
526
|
}), { signal: context.abort });
|
|
527
|
+
if (sync)
|
|
528
|
+
return syncResult(runManager, states);
|
|
495
529
|
return states
|
|
496
530
|
.map((state) => `Started reviewing ${prMarkdownLink(repository, state.pr)}.`)
|
|
497
531
|
.join("\n");
|
|
@@ -502,6 +536,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
502
536
|
args: {
|
|
503
537
|
issues: tool.schema.string(),
|
|
504
538
|
dryRun: tool.schema.boolean().optional(),
|
|
539
|
+
sync: tool.schema.boolean().optional(),
|
|
505
540
|
},
|
|
506
541
|
async execute(args, context) {
|
|
507
542
|
const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
|
|
@@ -523,6 +558,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
523
558
|
const repository = resolveRepository(config);
|
|
524
559
|
if (!repository.triage)
|
|
525
560
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
561
|
+
const sync = parsed.sync || args.sync === true;
|
|
526
562
|
const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
|
|
527
563
|
config,
|
|
528
564
|
dryRun: parsed.dryRun,
|
|
@@ -530,7 +566,10 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
530
566
|
parentSessionId: context.sessionID,
|
|
531
567
|
repository,
|
|
532
568
|
signal: context.abort,
|
|
569
|
+
sync,
|
|
533
570
|
}), { signal: context.abort });
|
|
571
|
+
if (sync)
|
|
572
|
+
return syncResult(runManager, states);
|
|
534
573
|
return states
|
|
535
574
|
.map((state) => `Started triaging ${issueMarkdownLink(repository, state.issue)}.`)
|
|
536
575
|
.join("\n");
|
|
@@ -58,10 +58,9 @@ export function applyFindingValidation(input) {
|
|
|
58
58
|
discarded.push(target);
|
|
59
59
|
return false;
|
|
60
60
|
});
|
|
61
|
-
next[reviewer] =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
: { findings: [], requirementFindings: [], verdict: "MERGE" };
|
|
61
|
+
next[reviewer] = findings.length
|
|
62
|
+
? { ...output, findings }
|
|
63
|
+
: { findings: [], verdict: "MERGE" };
|
|
65
64
|
}
|
|
66
65
|
return { outputs: next, summary: { discarded, kept } };
|
|
67
66
|
}
|
|
@@ -50,7 +50,7 @@ async function withReviewerFailureProgress(input) {
|
|
|
50
50
|
throw error;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
|
|
53
|
+
async function runEditor(input, worktreePath, cycle, reviewFindings, unresolvedThreads) {
|
|
54
54
|
const editor = input.repository.agents.editor;
|
|
55
55
|
if (!editor)
|
|
56
56
|
throw new Error("agents.editor is required for magi_merge");
|
|
@@ -64,6 +64,7 @@ async function runEditor(input, worktreePath, cycle, unresolvedThreads) {
|
|
|
64
64
|
directory: input.directory,
|
|
65
65
|
pr: input.pr,
|
|
66
66
|
repository: input.repository,
|
|
67
|
+
reviewFindings: JSON.stringify(reviewFindings, null, 2),
|
|
67
68
|
unresolvedThreads: JSON.stringify(unresolvedThreads, null, 2),
|
|
68
69
|
worktreePath,
|
|
69
70
|
});
|
|
@@ -149,8 +150,8 @@ async function postRereviewOutput(input, reviewerKey, output) {
|
|
|
149
150
|
return postChangesRequested(input.exec, input.repository, input.pr, reviewer.account, output.newFindings.map((finding) => ({
|
|
150
151
|
fix: "Please address this before merging.",
|
|
151
152
|
issue: finding.body,
|
|
152
|
-
line: finding.line,
|
|
153
153
|
path: finding.path,
|
|
154
|
+
line: finding.line,
|
|
154
155
|
startLine: finding.startLine,
|
|
155
156
|
})));
|
|
156
157
|
}
|
|
@@ -161,6 +162,35 @@ function parseRereviewOutputWithInlineTargets(text, targets) {
|
|
|
161
162
|
validateInlineCommentTargets(output.newFindings, targets, "newFindings");
|
|
162
163
|
return output;
|
|
163
164
|
}
|
|
165
|
+
function newFindingToEditorFinding(reviewer, finding) {
|
|
166
|
+
return {
|
|
167
|
+
body: finding.body,
|
|
168
|
+
fix: "Please address this before merging.",
|
|
169
|
+
line: finding.line,
|
|
170
|
+
path: finding.path,
|
|
171
|
+
reviewer,
|
|
172
|
+
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
173
|
+
type: "inline",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function blockingReviewFindings(outputs) {
|
|
177
|
+
return Object.entries(outputs).flatMap(([reviewer, output]) => {
|
|
178
|
+
if (output.verdict !== "CHANGES_REQUESTED")
|
|
179
|
+
return [];
|
|
180
|
+
if ("findings" in output) {
|
|
181
|
+
return output.findings.map((finding) => ({
|
|
182
|
+
fix: finding.fix,
|
|
183
|
+
issue: finding.issue,
|
|
184
|
+
line: finding.line,
|
|
185
|
+
path: finding.path,
|
|
186
|
+
reviewer,
|
|
187
|
+
...(finding.startLine == null ? {} : { startLine: finding.startLine }),
|
|
188
|
+
type: "inline",
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
return output.newFindings.map((finding) => newFindingToEditorFinding(reviewer, finding));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
164
194
|
async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionIds, ciFailureContext, options = {}) {
|
|
165
195
|
throwIfAborted(input.signal);
|
|
166
196
|
const meta = await fetchPullRequest(input.exec, input.repository, input.pr);
|
|
@@ -460,15 +490,38 @@ function syntheticReviewThreads(outputs) {
|
|
|
460
490
|
const threads = {};
|
|
461
491
|
for (const [reviewer, output] of Object.entries(outputs)) {
|
|
462
492
|
if ("findings" in output) {
|
|
463
|
-
threads[reviewer] = output.findings.
|
|
493
|
+
threads[reviewer] = output.findings.flatMap((finding) => {
|
|
464
494
|
const commentId = nextCommentId--;
|
|
465
|
-
return
|
|
466
|
-
|
|
495
|
+
return [
|
|
496
|
+
{
|
|
497
|
+
body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
|
|
498
|
+
commentId,
|
|
499
|
+
comments: [
|
|
500
|
+
{
|
|
501
|
+
author: reviewer,
|
|
502
|
+
body: `Issue: ${finding.issue}\n\nFix: ${finding.fix}`,
|
|
503
|
+
commentId,
|
|
504
|
+
createdAt: new Date(0).toISOString(),
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
line: finding.line,
|
|
508
|
+
path: finding.path,
|
|
509
|
+
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
});
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
threads[reviewer] = output.newFindings.flatMap((finding) => {
|
|
516
|
+
const commentId = nextCommentId--;
|
|
517
|
+
return [
|
|
518
|
+
{
|
|
519
|
+
body: finding.body,
|
|
467
520
|
commentId,
|
|
468
521
|
comments: [
|
|
469
522
|
{
|
|
470
523
|
author: reviewer,
|
|
471
|
-
body:
|
|
524
|
+
body: finding.body,
|
|
472
525
|
commentId,
|
|
473
526
|
createdAt: new Date(0).toISOString(),
|
|
474
527
|
},
|
|
@@ -476,27 +529,8 @@ function syntheticReviewThreads(outputs) {
|
|
|
476
529
|
line: finding.line,
|
|
477
530
|
path: finding.path,
|
|
478
531
|
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
threads[reviewer] = output.newFindings.map((finding) => {
|
|
484
|
-
const commentId = nextCommentId--;
|
|
485
|
-
return {
|
|
486
|
-
body: finding.body,
|
|
487
|
-
commentId,
|
|
488
|
-
comments: [
|
|
489
|
-
{
|
|
490
|
-
author: reviewer,
|
|
491
|
-
body: finding.body,
|
|
492
|
-
commentId,
|
|
493
|
-
createdAt: new Date(0).toISOString(),
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
line: finding.line,
|
|
497
|
-
path: finding.path,
|
|
498
|
-
threadId: `dry-run:${reviewer}:${Math.abs(commentId)}`,
|
|
499
|
-
};
|
|
532
|
+
},
|
|
533
|
+
];
|
|
500
534
|
});
|
|
501
535
|
}
|
|
502
536
|
return threads;
|
|
@@ -666,7 +700,12 @@ export async function runMerge(input) {
|
|
|
666
700
|
maxThreadResolutionCycles: input.repository.merge.maxThreadResolutionCycles,
|
|
667
701
|
threads: unresolvedThreads,
|
|
668
702
|
});
|
|
669
|
-
|
|
703
|
+
const editorFindings = blockingReviewFindings(reportOutputs);
|
|
704
|
+
const editableFindings = editableThreads.length ? editorFindings : [];
|
|
705
|
+
const findingAttemptsExhausted = input.repository.merge.maxThreadResolutionCycles !== 0 &&
|
|
706
|
+
cycle > input.repository.merge.maxThreadResolutionCycles;
|
|
707
|
+
if (!editableThreads.length &&
|
|
708
|
+
(!editableFindings.length || findingAttemptsExhausted)) {
|
|
670
709
|
await input.onProgress?.({
|
|
671
710
|
status: "changes_unresolved",
|
|
672
711
|
type: "merge_completed",
|
|
@@ -693,7 +732,7 @@ export async function runMerge(input) {
|
|
|
693
732
|
});
|
|
694
733
|
if (!review.worktreePath)
|
|
695
734
|
throw new Error("Review worktree is missing");
|
|
696
|
-
const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableThreads);
|
|
735
|
+
const editorOutput = await runEditor(abortableInput, review.worktreePath, cycle, editableFindings, editableThreads);
|
|
697
736
|
editorOutputs.push(editorOutput);
|
|
698
737
|
dryRunThreads = input.dryRun
|
|
699
738
|
? appendDryRunEditorResponses({
|
|
@@ -27,9 +27,6 @@ function formatRereviewFinding(finding) {
|
|
|
27
27
|
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
28
28
|
return `\`${line}\`: ${finding.body}`;
|
|
29
29
|
}
|
|
30
|
-
function formatRequirementFinding(finding) {
|
|
31
|
-
return `Issue #${finding.issueNumber}: ${finding.requirement}`;
|
|
32
|
-
}
|
|
33
30
|
function isReviewOutput(output) {
|
|
34
31
|
return "findings" in output;
|
|
35
32
|
}
|
|
@@ -81,10 +78,7 @@ function reviewerDetailLines(output) {
|
|
|
81
78
|
return output.reason ? [output.reason] : [];
|
|
82
79
|
if (output.verdict !== "CHANGES_REQUESTED")
|
|
83
80
|
return [];
|
|
84
|
-
return
|
|
85
|
-
...output.findings.map(formatFinding),
|
|
86
|
-
...output.requirementFindings.map(formatRequirementFinding),
|
|
87
|
-
];
|
|
81
|
+
return output.findings.map(formatFinding);
|
|
88
82
|
}
|
|
89
83
|
if (output.verdict === "CLOSE")
|
|
90
84
|
return output.reason ? [output.reason] : [];
|
|
@@ -92,7 +86,6 @@ function reviewerDetailLines(output) {
|
|
|
92
86
|
return [];
|
|
93
87
|
return [
|
|
94
88
|
...output.newFindings.map(formatRereviewFinding),
|
|
95
|
-
...output.requirementFindings.map(formatRequirementFinding),
|
|
96
89
|
...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
|
|
97
90
|
];
|
|
98
91
|
}
|