opencode-magi 0.0.0-dev-20260702001022 → 0.0.0-dev-20260714064227
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/dist/commands.js +0 -4
- package/dist/magi.js +24 -49
- package/dist/tools/index.js +0 -2
- package/dist/tools/merge/action.js +4 -8
- package/dist/tools/merge/context.js +6 -12
- package/dist/tools/merge/editor.js +21 -33
- package/dist/tools/merge/index.js +12 -32
- package/dist/tools/merge/merge.js +4 -13
- package/dist/tools/merge/report.js +8 -9
- package/dist/tools/review/action.js +36 -68
- package/dist/tools/review/check.js +26 -42
- package/dist/tools/review/context.js +7 -15
- package/dist/tools/review/index.js +12 -32
- package/dist/tools/review/report.js +14 -13
- package/dist/tools/review/review.js +15 -28
- package/dist/tools/review/reviewer.js +19 -31
- package/dist/tools/triage/index.js +1 -2
- package/package.json +5 -5
- package/dist/tools/cancel/index.js +0 -30
|
@@ -27,11 +27,6 @@ export class Review {
|
|
|
27
27
|
this.state = state;
|
|
28
28
|
}
|
|
29
29
|
static async init(number, magi, config, context, options) {
|
|
30
|
-
if (!options.sync) {
|
|
31
|
-
const controller = new AbortController();
|
|
32
|
-
context = { ...context, abort: controller.signal };
|
|
33
|
-
magi.registerBackground(number, controller);
|
|
34
|
-
}
|
|
35
30
|
const url = `${config.github.url}/pull/${number}`;
|
|
36
31
|
const octokit = await magi.createOctokit(config, context.abort);
|
|
37
32
|
const graphql = magi.createGraphql(octokit);
|
|
@@ -47,8 +42,8 @@ export class Review {
|
|
|
47
42
|
repo: quote(`${config.github.owner}/${config.github.repo}`),
|
|
48
43
|
reviewers,
|
|
49
44
|
sessionId: context.sessionID,
|
|
50
|
-
text: `Started reviewing [#${number}](${url}).`,
|
|
51
45
|
});
|
|
46
|
+
await magi.updateEvent(state.output, `Started reviewing.`);
|
|
52
47
|
const exec = createExecWithGitHubApiRetry(magi.exec, config.github.retryApiAttempts);
|
|
53
48
|
return new Review(number, magi, config, context, octokit, graphql, exec, state);
|
|
54
49
|
}
|
|
@@ -64,17 +59,18 @@ export class Review {
|
|
|
64
59
|
postReviews = postReviews;
|
|
65
60
|
automate = automate;
|
|
66
61
|
createReport = createReport;
|
|
67
|
-
async notify(text) {
|
|
68
|
-
if (!this.state.sync)
|
|
69
|
-
await this.magi.notify(this.state.sessionId, text);
|
|
70
|
-
}
|
|
71
62
|
async cleanup() {
|
|
72
63
|
if (this.state.worktree?.path)
|
|
73
64
|
await this.magi.deleteWorktree(this.state.worktree.path);
|
|
74
|
-
this.magi.unregisterBackground(this.number, this.context.abort);
|
|
75
65
|
}
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
async updateState(next) {
|
|
67
|
+
this.state = await this.magi.updateState(this.state.output, next);
|
|
68
|
+
}
|
|
69
|
+
async updateEvent(message) {
|
|
70
|
+
await this.magi.updateEvent(this.state.output, message);
|
|
71
|
+
}
|
|
72
|
+
async getEvents() {
|
|
73
|
+
return await this.magi.getEvents(this.state.output);
|
|
78
74
|
}
|
|
79
75
|
async createAgentFile(phase, id, content, attempt, cycle) {
|
|
80
76
|
await this.magi.createAgentFile(this.state.output, phase, id, content, attempt, cycle);
|
|
@@ -97,21 +93,14 @@ export class Review {
|
|
|
97
93
|
permissions: this.state.operator.permissions,
|
|
98
94
|
}),
|
|
99
95
|
};
|
|
100
|
-
|
|
101
|
-
operator,
|
|
102
|
-
reviewers,
|
|
103
|
-
});
|
|
96
|
+
await this.updateState({ operator, reviewers });
|
|
104
97
|
}
|
|
105
98
|
async createWorktree() {
|
|
106
99
|
this.context.abort.throwIfAborted();
|
|
107
|
-
|
|
108
|
-
text: `Creating worktree for ${this.getLink()}.`,
|
|
109
|
-
});
|
|
100
|
+
await this.updateEvent(`Creating worktree.`);
|
|
110
101
|
const worktree = await this.magi.createWorktree(this.config.review.worktree, this.number, this.state.id, this.context.abort);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
worktree,
|
|
114
|
-
});
|
|
102
|
+
await this.updateState({ worktree });
|
|
103
|
+
await this.updateEvent(`Finished creating worktree.`);
|
|
115
104
|
}
|
|
116
105
|
async resolveVerdict() {
|
|
117
106
|
if (!this.config.review.reviewers?.length)
|
|
@@ -138,10 +127,8 @@ export class Review {
|
|
|
138
127
|
(majority && counts.APPROVED >= threshold)
|
|
139
128
|
? "APPROVED"
|
|
140
129
|
: "CHANGES_REQUESTED";
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
text: `Final verdict for ${this.getLink()} is ${verdict}.`,
|
|
144
|
-
});
|
|
130
|
+
await this.updateState({ pr: { verdict } });
|
|
131
|
+
await this.updateEvent(`Final verdict is ${verdict}.`);
|
|
145
132
|
return verdict;
|
|
146
133
|
}
|
|
147
134
|
}
|
|
@@ -3,9 +3,7 @@ import { Prompt } from "@/prompts";
|
|
|
3
3
|
import { filterEmpty, marker, omitNullish, retry, Worker } from "@/utils";
|
|
4
4
|
export async function review() {
|
|
5
5
|
this.context.abort.throwIfAborted();
|
|
6
|
-
|
|
7
|
-
text: `Reviewing ${this.getLink()}.`,
|
|
8
|
-
});
|
|
6
|
+
await this.updateEvent(`Reviewing.`);
|
|
9
7
|
if (!this.config.review.reviewers?.length)
|
|
10
8
|
throw new MagiError("blocked", "No reviewers configured.");
|
|
11
9
|
const worker = new Worker(this.config.review.concurrency.reviewers);
|
|
@@ -22,7 +20,7 @@ export async function review() {
|
|
|
22
20
|
throw new MagiError("blocked", "Reviewers not found.");
|
|
23
21
|
const { outputs, review, sessionId, status } = this.state.reviewers[id];
|
|
24
22
|
if (status === "skip") {
|
|
25
|
-
await this.
|
|
23
|
+
await this.updateEvent(`Skipping review with reviewer ${id}.`);
|
|
26
24
|
return [id, {}];
|
|
27
25
|
}
|
|
28
26
|
else {
|
|
@@ -33,7 +31,7 @@ export async function review() {
|
|
|
33
31
|
const rereview = status !== "initial";
|
|
34
32
|
const label = rereview ? "rereview" : "review";
|
|
35
33
|
const cycle = (outputs?.length ?? 0) + 1;
|
|
36
|
-
await this.
|
|
34
|
+
await this.updateEvent(`Running ${label} with reviewer ${id}.`);
|
|
37
35
|
const sha = rereview
|
|
38
36
|
? (review?.commit_id ?? this.state.pr.metadata.base.sha)
|
|
39
37
|
: this.state.pr.metadata.base.sha;
|
|
@@ -106,7 +104,7 @@ export async function review() {
|
|
|
106
104
|
validateInlineCommentTargets(status, parsed, inlineCommentTargets);
|
|
107
105
|
return parsed;
|
|
108
106
|
}, {
|
|
109
|
-
error: (_, count) => this.
|
|
107
|
+
error: (_, count) => this.updateEvent(`Attempt ${count} failed to ${label} with reviewer ${id}. Retrying...`),
|
|
110
108
|
retries: this.config.output.repairAttempts,
|
|
111
109
|
});
|
|
112
110
|
if (!output)
|
|
@@ -118,10 +116,8 @@ export async function review() {
|
|
|
118
116
|
return [id, { outputs: [...(outputs ?? []), output] }];
|
|
119
117
|
}
|
|
120
118
|
}))));
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
text: `Finished reviewing ${this.getLink()}.`,
|
|
124
|
-
});
|
|
119
|
+
await this.updateState({ reviewers });
|
|
120
|
+
await this.updateEvent(`Finished reviewing.`);
|
|
125
121
|
}
|
|
126
122
|
export async function validateFindings() {
|
|
127
123
|
this.context.abort.throwIfAborted();
|
|
@@ -137,19 +133,15 @@ export async function validateFindings() {
|
|
|
137
133
|
});
|
|
138
134
|
if (!findings.length)
|
|
139
135
|
return;
|
|
140
|
-
|
|
141
|
-
text: `Validating review findings for ${this.getLink()}.`,
|
|
142
|
-
});
|
|
136
|
+
await this.updateEvent(`Validating review findings.`);
|
|
143
137
|
const accepted = await collectAcceptedFindings.call(this, findings);
|
|
144
138
|
const reviewers = Object.fromEntries(Object.entries(this.state.reviewers ?? {}).map(([id, reviewer]) => [
|
|
145
139
|
id,
|
|
146
140
|
transformState(reviewer, id, accepted),
|
|
147
141
|
]));
|
|
148
142
|
await notifyVerdictChanges.call(this, this.state.reviewers ?? {}, reviewers, "after majority finding validation");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
text: `Finished validating review findings for ${this.getLink()}.`,
|
|
152
|
-
});
|
|
143
|
+
await this.updateState({ reviewers });
|
|
144
|
+
await this.updateEvent(`Finished validating review findings.`);
|
|
153
145
|
}
|
|
154
146
|
export async function reconsiderClose() {
|
|
155
147
|
this.context.abort.throwIfAborted();
|
|
@@ -164,9 +156,7 @@ export async function reconsiderClose() {
|
|
|
164
156
|
const count = targetReviewers.length;
|
|
165
157
|
if (!count || count >= threshold)
|
|
166
158
|
return;
|
|
167
|
-
|
|
168
|
-
text: `Reconsidering close verdicts for ${this.getLink()}.`,
|
|
169
|
-
});
|
|
159
|
+
await this.updateEvent(`Reconsidering close verdicts.`);
|
|
170
160
|
const worker = new Worker(this.config.review.concurrency.reviewers);
|
|
171
161
|
const prompt = await Prompt.init(this.magi, this.config, "review/close-reconsideration");
|
|
172
162
|
const reviewers = Object.fromEntries(await Promise.all(targetReviewers.map(([id, { outputs, review, sessionId, status }]) => worker.run(async () => {
|
|
@@ -187,7 +177,7 @@ export async function reconsiderClose() {
|
|
|
187
177
|
repo: this.config.github.repo,
|
|
188
178
|
});
|
|
189
179
|
const repairMessage = await prompt.repair();
|
|
190
|
-
await this.
|
|
180
|
+
await this.updateEvent(`Reconsidering close verdict with reviewer ${id}.`);
|
|
191
181
|
const output = await retry(async (count) => {
|
|
192
182
|
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
|
|
193
183
|
await this.createAgentFile("close-reconsideration", id, raw, count, cycle);
|
|
@@ -197,7 +187,7 @@ export async function reconsiderClose() {
|
|
|
197
187
|
validateInlineCommentTargets("initial", parsed, inlineCommentTargets);
|
|
198
188
|
return parsed;
|
|
199
189
|
}, {
|
|
200
|
-
error: (_, count) => this.
|
|
190
|
+
error: (_, count) => this.updateEvent(`Attempt ${count} failed to reconsider close verdict with reviewer ${id}. Retrying...`),
|
|
201
191
|
retries: this.config.output.repairAttempts,
|
|
202
192
|
});
|
|
203
193
|
if (!output)
|
|
@@ -225,10 +215,8 @@ export async function reconsiderClose() {
|
|
|
225
215
|
transformState(reviewer, id, accepted),
|
|
226
216
|
]));
|
|
227
217
|
await notifyVerdictChanges.call(this, reviewers, validatedReviewers, "after majority finding validation");
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
text: `Finished reconsidering close verdicts for ${this.getLink()}.`,
|
|
231
|
-
});
|
|
218
|
+
await this.updateState({ reviewers: validatedReviewers });
|
|
219
|
+
await this.updateEvent(`Finished reconsidering close verdicts.`);
|
|
232
220
|
}
|
|
233
221
|
async function collectAcceptedFindings(findings) {
|
|
234
222
|
const accepted = new Set();
|
|
@@ -244,7 +232,7 @@ async function collectAcceptedFindings(findings) {
|
|
|
244
232
|
const { sessionId } = this.state.reviewers[id];
|
|
245
233
|
if (!sessionId)
|
|
246
234
|
throw new MagiError("blocked", `No session ID found for reviewer ${id}.`);
|
|
247
|
-
await this.
|
|
235
|
+
await this.updateEvent(`Validating review findings with reviewer ${id}.`);
|
|
248
236
|
const targetFindings = findings.filter((target) => target.reviewer !== id);
|
|
249
237
|
const expectedKeys = new Set(targetFindings.map(({ index, reviewer }) => `${reviewer}:${index}`));
|
|
250
238
|
const taskMessage = await prompt.create(this.config.review.prompts?.findingValidation, [
|
|
@@ -278,7 +266,7 @@ async function collectAcceptedFindings(findings) {
|
|
|
278
266
|
throw new Error(`Missing finding vote: ${key}.`);
|
|
279
267
|
return parsed;
|
|
280
268
|
}, {
|
|
281
|
-
error: (_, count) => this.
|
|
269
|
+
error: (_, count) => this.updateEvent(`Attempt ${count} failed to validate review findings with reviewer ${id}. Retrying...`),
|
|
282
270
|
retries: this.config.output.repairAttempts,
|
|
283
271
|
});
|
|
284
272
|
if (!output)
|
|
@@ -303,8 +291,8 @@ async function collectAcceptedFindings(findings) {
|
|
|
303
291
|
.map(({ validator, vote }) => `- ${validator}: ${vote.comment}`);
|
|
304
292
|
if (agrees >= threshold)
|
|
305
293
|
accepted.add(key);
|
|
306
|
-
await this.
|
|
307
|
-
`Finding ${reviewer} #${index + 1}
|
|
294
|
+
await this.updateEvent(filterEmpty([
|
|
295
|
+
`Finding ${reviewer} #${index + 1} was ${agrees >= threshold ? "accepted" : "rejected"} by majority vote.`,
|
|
308
296
|
`Finding: ${finding.path}:${finding.line}\n${finding.body}`,
|
|
309
297
|
acceptedComments.length
|
|
310
298
|
? `Accepted by:\n${acceptedComments.join("\n")}`
|
|
@@ -343,7 +331,7 @@ async function notifyVerdictChanges(prev, next, reason) {
|
|
|
343
331
|
const nextVerdict = reviewer.outputs?.at(-1)?.verdict;
|
|
344
332
|
if (!prevVerdict || !nextVerdict || prevVerdict === nextVerdict)
|
|
345
333
|
return;
|
|
346
|
-
await this.
|
|
334
|
+
await this.updateEvent(`Reviewer ${id} verdict changed from ${prevVerdict} to ${nextVerdict} ${reason}.`);
|
|
347
335
|
}));
|
|
348
336
|
}
|
|
349
337
|
function validateInlineCommentTargets(status, output, inlineCommentTargets) {
|
|
@@ -5,10 +5,9 @@ export const triage = function (magi) {
|
|
|
5
5
|
args: {
|
|
6
6
|
dryRun: tool.schema.boolean().optional(),
|
|
7
7
|
issues: tool.schema.string(),
|
|
8
|
-
sync: tool.schema.boolean().optional(),
|
|
9
8
|
},
|
|
10
9
|
description: "Triage one or more issues with configured Magi triage voters.",
|
|
11
|
-
async execute({ dryRun: _dryRun = false, issues: _issues
|
|
10
|
+
async execute({ dryRun: _dryRun = false, issues: _issues }) {
|
|
12
11
|
const _config = await magi.getConfig({ creator: true, voters: true });
|
|
13
12
|
return "";
|
|
14
13
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-magi",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20260714064227",
|
|
4
4
|
"description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",
|
|
@@ -64,18 +64,18 @@
|
|
|
64
64
|
"vitest": "^4.1.7"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
+
"clean": "rimraf node_modules dist coverage",
|
|
67
68
|
"postbuild": "node scripts/copy-data.ts",
|
|
68
69
|
"build": "tsgo -p tsconfig.build.json",
|
|
69
|
-
"
|
|
70
|
+
"graphql:codegen": "graphql-codegen --config codegen.json",
|
|
70
71
|
"format:check": "oxfmt --check .",
|
|
71
72
|
"format:write": "oxfmt --write .",
|
|
72
|
-
"graphql:codegen": "graphql-codegen --config codegen.json",
|
|
73
73
|
"lint:check": "oxlint . --max-warnings=0",
|
|
74
74
|
"lint:fix": "oxlint . --max-warnings=0 --fix",
|
|
75
|
-
"
|
|
75
|
+
"type:check": "tsgo --noEmit",
|
|
76
76
|
"test": "vitest run --passWithNoTests",
|
|
77
77
|
"test:dev": "vitest --watch --ui --passWithNoTests",
|
|
78
|
-
"
|
|
78
|
+
"quality": "pnpm format:check && pnpm lint:check && pnpm type:check && pnpm test",
|
|
79
79
|
"release": "changeset publish",
|
|
80
80
|
"release:dev": "changeset version --snapshot dev && changeset publish --tag dev"
|
|
81
81
|
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
export const cancel = function (magi) {
|
|
3
|
-
return {
|
|
4
|
-
magi_cancel: tool({
|
|
5
|
-
args: {
|
|
6
|
-
numbers: tool.schema.number().array().optional(),
|
|
7
|
-
},
|
|
8
|
-
description: "Cancel active background Magi runs by pull request or issue number, or all active background runs when no target is provided.",
|
|
9
|
-
async execute({ numbers }) {
|
|
10
|
-
const result = magi.cancelBackgrounds(numbers);
|
|
11
|
-
if (!result.cancelled.length && !result.missing.length)
|
|
12
|
-
return await Promise.resolve("No active background runs.");
|
|
13
|
-
return await Promise.resolve([
|
|
14
|
-
...(result.cancelled.length
|
|
15
|
-
? [
|
|
16
|
-
`Cancelled: ${result.cancelled.length}`,
|
|
17
|
-
...result.cancelled.map((number) => `- ${number}`),
|
|
18
|
-
]
|
|
19
|
-
: []),
|
|
20
|
-
...(result.missing.length
|
|
21
|
-
? [
|
|
22
|
-
`Missing: ${result.missing.length}`,
|
|
23
|
-
...result.missing.map((number) => `- ${number}`),
|
|
24
|
-
]
|
|
25
|
-
: []),
|
|
26
|
-
].join("\n"));
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
};
|
|
30
|
-
};
|