opencode-magi 0.0.0-dev-20260721051539 → 0.0.0-dev-20260722060754
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/magi.js +61 -19
- package/dist/prompts/index.js +9 -4
- package/dist/tools/merge/action.js +2 -2
- package/dist/tools/merge/editor.js +14 -8
- package/dist/tools/review/action.js +7 -4
- package/dist/tools/review/check.js +7 -4
- package/dist/tools/review/reviewer.js +30 -18
- package/dist/utils/function.js +7 -5
- package/package.json +1 -1
package/dist/magi.js
CHANGED
|
@@ -217,27 +217,69 @@ export class Magi {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
async promptSession(sessionID, text, signal) {
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
const events = await this.input.client.event.subscribe(undefined, {
|
|
222
|
+
signal: controller.signal,
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
const result = await Promise.race([
|
|
226
|
+
this.input.client.session.prompt({
|
|
227
|
+
parts: [{ text, type: "text" }],
|
|
228
|
+
sessionID,
|
|
229
|
+
}, { signal }),
|
|
230
|
+
this.monitorSession(sessionID, events.stream),
|
|
231
|
+
]);
|
|
232
|
+
if (result.error) {
|
|
233
|
+
if (!isUndefined(result.response) && result.response.statusText)
|
|
234
|
+
throw new Error(result.response.statusText);
|
|
235
|
+
else if (result.error instanceof Error)
|
|
236
|
+
throw new Error(result.error.message);
|
|
237
|
+
else
|
|
238
|
+
throw new Error(JSON.stringify(result.error));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const output = result.data.parts
|
|
242
|
+
.filter((part) => part.type === "text")
|
|
243
|
+
.map((part) => part.text)
|
|
244
|
+
.join("\n");
|
|
245
|
+
if (!output)
|
|
246
|
+
throw new Error("OpenCode session.prompt did not return text output.");
|
|
247
|
+
return output;
|
|
248
|
+
}
|
|
231
249
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
catch (e) {
|
|
251
|
+
if (e instanceof MagiError)
|
|
252
|
+
await this.input.client.session.abort({ sessionID });
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
controller.abort();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async monitorSession(sessionID, events) {
|
|
260
|
+
for await (const ev of events) {
|
|
261
|
+
if (ev.type === "permission.asked" &&
|
|
262
|
+
ev.properties.sessionID === sessionID) {
|
|
263
|
+
const { id, patterns, permission } = ev.properties;
|
|
264
|
+
const result = await this.input.client.permission.reply({
|
|
265
|
+
reply: "reject",
|
|
266
|
+
requestID: id,
|
|
267
|
+
});
|
|
268
|
+
if (result.error)
|
|
269
|
+
throw new MagiError("blocked", "Could not reject permission request.");
|
|
270
|
+
throw new MagiError("blocked", `OpenCode session requested ${permission} permission for ${patterns.join(", ")}.`);
|
|
271
|
+
}
|
|
272
|
+
if (ev.type === "question.asked" &&
|
|
273
|
+
ev.properties.sessionID === sessionID) {
|
|
274
|
+
const result = await this.input.client.question.reject({
|
|
275
|
+
requestID: ev.properties.id,
|
|
276
|
+
});
|
|
277
|
+
if (result.error)
|
|
278
|
+
throw new MagiError("blocked", "Could not reject user question.");
|
|
279
|
+
throw new MagiError("blocked", "OpenCode session requested user input.");
|
|
280
|
+
}
|
|
240
281
|
}
|
|
282
|
+
return new Promise(() => { });
|
|
241
283
|
}
|
|
242
284
|
async deleteSessions(state) {
|
|
243
285
|
const sessionIds = this.getSessionIds(state);
|
package/dist/prompts/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { isString } from "@/utils";
|
|
4
|
+
import { isString, isUndefined } from "@/utils";
|
|
5
5
|
export class Prompt {
|
|
6
6
|
magi;
|
|
7
7
|
config;
|
|
@@ -43,11 +43,16 @@ export class Prompt {
|
|
|
43
43
|
].join("\n\n");
|
|
44
44
|
return Object.entries(values).reduce((prev, [key, value]) => prev.replaceAll(`{${key}}`, value), content);
|
|
45
45
|
}
|
|
46
|
-
async repair() {
|
|
46
|
+
async repair(e) {
|
|
47
47
|
const instructions = [
|
|
48
|
-
"Your previous output
|
|
48
|
+
"Your previous output failed validation. Regenerate the result.",
|
|
49
|
+
!isUndefined(e)
|
|
50
|
+
? `Validation error: ${e instanceof Error ? e.message : "Invalid output."}`
|
|
51
|
+
: undefined,
|
|
49
52
|
"Return only a JSON object matching the output contract below. Do not include analysis, explanation, apologies, markdown, or any text before or after the JSON object.",
|
|
50
|
-
]
|
|
53
|
+
]
|
|
54
|
+
.filter((instruction) => !isUndefined(instruction))
|
|
55
|
+
.join("\n\n");
|
|
51
56
|
try {
|
|
52
57
|
const outputContract = await readFile(join(this.pathname, "output-contract.md"), "utf-8");
|
|
53
58
|
return [
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { MagiError } from "@/magi";
|
|
2
2
|
import { retry } from "@/utils";
|
|
3
|
-
export async function editCycles(
|
|
3
|
+
export async function editCycles(cb) {
|
|
4
4
|
this.context.abort.throwIfAborted();
|
|
5
5
|
const error = new Error("Continue edit cycle.");
|
|
6
6
|
const verdict = await retry(async (cycle) => {
|
|
7
|
-
const verdict = await
|
|
7
|
+
const verdict = await cb(cycle);
|
|
8
8
|
if (verdict === "CHANGES_REQUESTED")
|
|
9
9
|
throw error;
|
|
10
10
|
return verdict;
|
|
@@ -23,9 +23,8 @@ export async function edit() {
|
|
|
23
23
|
repo: this.config.github.repo,
|
|
24
24
|
worktreePath: this.state.worktree.path,
|
|
25
25
|
});
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
26
|
+
const output = await retry(async (count, e) => {
|
|
27
|
+
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
29
28
|
await this.createAgentFile("edit", "editor", raw, count, cycle);
|
|
30
29
|
const parsed = prompt.parse(raw);
|
|
31
30
|
if (!prompt.validate(parsed))
|
|
@@ -44,7 +43,11 @@ export async function edit() {
|
|
|
44
43
|
}
|
|
45
44
|
return output;
|
|
46
45
|
}, {
|
|
47
|
-
error: (
|
|
46
|
+
error: async (e, count) => {
|
|
47
|
+
if (e instanceof MagiError)
|
|
48
|
+
throw e;
|
|
49
|
+
await this.updateEvent(`Attempt ${count} failed to edit. Retrying...`);
|
|
50
|
+
},
|
|
48
51
|
retries: this.config.output.repairAttempts,
|
|
49
52
|
signal: this.context.abort,
|
|
50
53
|
});
|
|
@@ -111,9 +114,8 @@ export async function resolveConflict() {
|
|
|
111
114
|
repo: this.config.github.repo,
|
|
112
115
|
worktreePath: this.state.worktree.path,
|
|
113
116
|
});
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
117
|
+
const output = await retry(async (count, e) => {
|
|
118
|
+
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
117
119
|
await this.createAgentFile("conflict", "editor", raw, count, cycle);
|
|
118
120
|
const parsed = prompt.parse(raw);
|
|
119
121
|
if (!prompt.validate(parsed))
|
|
@@ -136,7 +138,11 @@ export async function resolveConflict() {
|
|
|
136
138
|
responses: [],
|
|
137
139
|
};
|
|
138
140
|
}, {
|
|
139
|
-
error: (
|
|
141
|
+
error: async (e, count) => {
|
|
142
|
+
if (e instanceof MagiError)
|
|
143
|
+
throw e;
|
|
144
|
+
await this.updateEvent(`Attempt ${count} failed to resolve conflicts. Retrying...`);
|
|
145
|
+
},
|
|
140
146
|
retries: this.config.output.repairAttempts,
|
|
141
147
|
signal: this.context.abort,
|
|
142
148
|
});
|
|
@@ -78,16 +78,19 @@ export async function postReviews() {
|
|
|
78
78
|
repo: this.config.github.repo,
|
|
79
79
|
verdict: this.state.pr.verdict,
|
|
80
80
|
});
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
81
|
+
const output = await retry(async (count, e) => {
|
|
82
|
+
const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
84
83
|
await this.createAgentFile("comment", "operator", raw, count);
|
|
85
84
|
const parsed = prompt.parse(raw);
|
|
86
85
|
if (!prompt.validate(parsed))
|
|
87
86
|
throw new Error("Invalid output for operator.");
|
|
88
87
|
return parsed.comment;
|
|
89
88
|
}, {
|
|
90
|
-
error: (
|
|
89
|
+
error: async (e, count) => {
|
|
90
|
+
if (e instanceof MagiError)
|
|
91
|
+
throw e;
|
|
92
|
+
await this.updateEvent(`Attempt ${count} failed to post comment by operator. Retrying...`);
|
|
93
|
+
},
|
|
91
94
|
retries: this.config.output.repairAttempts,
|
|
92
95
|
signal: this.context.abort,
|
|
93
96
|
});
|
|
@@ -101,20 +101,23 @@ export async function classifyChecks() {
|
|
|
101
101
|
repo: this.config.github.repo,
|
|
102
102
|
worktreePath: this.state.worktree.path,
|
|
103
103
|
});
|
|
104
|
-
const repairMessage = await prompt.repair();
|
|
105
104
|
await Promise.all(Object.entries(this.state.reviewers ?? {}).map(([id, { sessionId }]) => worker.run(async () => {
|
|
106
105
|
if (!sessionId)
|
|
107
106
|
throw new Error(`No session ID found for reviewer ${id}.`);
|
|
108
107
|
await this.updateEvent(`Classifying CI checks with reviewer ${id}.`);
|
|
109
|
-
const output = await retry(async (count) => {
|
|
110
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage :
|
|
108
|
+
const output = await retry(async (count, e) => {
|
|
109
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
111
110
|
await this.createAgentFile("ci-classification", id, raw, count);
|
|
112
111
|
const parsed = prompt.parse(raw);
|
|
113
112
|
if (!prompt.validate(parsed))
|
|
114
113
|
throw new Error(`Invalid output for reviewer ${id}.`);
|
|
115
114
|
return parsed;
|
|
116
115
|
}, {
|
|
117
|
-
error: (
|
|
116
|
+
error: async (e, count) => {
|
|
117
|
+
if (e instanceof MagiError)
|
|
118
|
+
throw e;
|
|
119
|
+
await this.updateEvent(`Attempt ${count} failed to classify CI checks with reviewer ${id}. Retrying...`);
|
|
120
|
+
},
|
|
118
121
|
retries: this.config.output.repairAttempts,
|
|
119
122
|
signal: this.context.abort,
|
|
120
123
|
});
|
|
@@ -70,10 +70,10 @@ export async function review() {
|
|
|
70
70
|
}), null, 2);
|
|
71
71
|
tags.push(["previous_review", previousReviewContext]);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
tags.push([
|
|
74
|
+
"unresolved_threads",
|
|
75
|
+
JSON.stringify(unresolvedThreads, null, 2),
|
|
76
|
+
]);
|
|
77
77
|
if (failedChecks.length) {
|
|
78
78
|
const ciFailureContext = JSON.stringify(failedChecks, null, 2);
|
|
79
79
|
tags.push(["ci_failure", ciFailureContext]);
|
|
@@ -94,9 +94,8 @@ export async function review() {
|
|
|
94
94
|
repo: this.config.github.repo,
|
|
95
95
|
worktreePath: this.state.worktree.path,
|
|
96
96
|
}));
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
97
|
+
const output = await retry(async (count, e) => {
|
|
98
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
100
99
|
await this.createAgentFile(rereview ? "rereview" : "review", id, raw, count, cycle);
|
|
101
100
|
const parsed = prompt.parse(raw);
|
|
102
101
|
if (!prompt.validate(parsed))
|
|
@@ -126,7 +125,11 @@ export async function review() {
|
|
|
126
125
|
]).join("\n\n"));
|
|
127
126
|
return parsed;
|
|
128
127
|
}, {
|
|
129
|
-
error: (
|
|
128
|
+
error: async (e, count) => {
|
|
129
|
+
if (e instanceof MagiError)
|
|
130
|
+
throw e;
|
|
131
|
+
await this.updateEvent(`Attempt ${count} failed to ${label} with reviewer ${id}. Retrying...`);
|
|
132
|
+
},
|
|
130
133
|
retries: this.config.output.repairAttempts,
|
|
131
134
|
signal: this.context.abort,
|
|
132
135
|
});
|
|
@@ -199,10 +202,9 @@ export async function reconsiderClose() {
|
|
|
199
202
|
pr: this.number.toString(),
|
|
200
203
|
repo: this.config.github.repo,
|
|
201
204
|
});
|
|
202
|
-
const repairMessage = await prompt.repair();
|
|
203
205
|
await this.updateEvent(`Reconsidering close verdict with reviewer ${id}.`);
|
|
204
|
-
const output = await retry(async (count) => {
|
|
205
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage :
|
|
206
|
+
const output = await retry(async (count, e) => {
|
|
207
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
206
208
|
await this.createAgentFile("close-reconsideration", id, raw, count, cycle);
|
|
207
209
|
const parsed = prompt.parse(raw);
|
|
208
210
|
if (!prompt.validate(parsed))
|
|
@@ -210,7 +212,11 @@ export async function reconsiderClose() {
|
|
|
210
212
|
validateInlineCommentTargets("initial", parsed, inlineCommentTargets);
|
|
211
213
|
return parsed;
|
|
212
214
|
}, {
|
|
213
|
-
error: (
|
|
215
|
+
error: async (e, count) => {
|
|
216
|
+
if (e instanceof MagiError)
|
|
217
|
+
throw e;
|
|
218
|
+
await this.updateEvent(`Attempt ${count} failed to reconsider close verdict with reviewer ${id}. Retrying...`);
|
|
219
|
+
},
|
|
214
220
|
retries: this.config.output.repairAttempts,
|
|
215
221
|
signal: this.context.abort,
|
|
216
222
|
});
|
|
@@ -267,9 +273,8 @@ async function collectAcceptedFindings(findings) {
|
|
|
267
273
|
pr: this.number.toString(),
|
|
268
274
|
repo: this.config.github.repo,
|
|
269
275
|
});
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
276
|
+
const output = await retry(async (count, e) => {
|
|
277
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
|
|
273
278
|
await this.createAgentFile("finding-validation", id, raw, count);
|
|
274
279
|
const parsed = prompt.parse(raw);
|
|
275
280
|
if (!prompt.validate(parsed))
|
|
@@ -290,7 +295,11 @@ async function collectAcceptedFindings(findings) {
|
|
|
290
295
|
throw new Error(`Missing finding vote: ${key}.`);
|
|
291
296
|
return parsed;
|
|
292
297
|
}, {
|
|
293
|
-
error: (
|
|
298
|
+
error: async (e, count) => {
|
|
299
|
+
if (e instanceof MagiError)
|
|
300
|
+
throw e;
|
|
301
|
+
await this.updateEvent(`Attempt ${count} failed to validate review findings with reviewer ${id}. Retrying...`);
|
|
302
|
+
},
|
|
294
303
|
retries: this.config.output.repairAttempts,
|
|
295
304
|
signal: this.context.abort,
|
|
296
305
|
});
|
|
@@ -386,12 +395,15 @@ function validateInlineCommentTargets(status, output, inlineCommentTargets) {
|
|
|
386
395
|
}
|
|
387
396
|
function validateThreadTargets({ followUps, resolves }, threads) {
|
|
388
397
|
const targets = new Map(threads.flatMap(({ comments, id }) => comments.flatMap(({ databaseId }) => databaseId == null ? [] : [[databaseId, id]])));
|
|
398
|
+
const allowedTargets = [...targets.entries()]
|
|
399
|
+
.map(([commentId, threadId]) => `- ${commentId}:${threadId}`)
|
|
400
|
+
.join("\n") || "none";
|
|
389
401
|
if (followUps)
|
|
390
402
|
for (const [index, { commentId }] of followUps.entries())
|
|
391
403
|
if (!targets.has(commentId))
|
|
392
|
-
throw new Error(`followUps[${index}].commentId must target an unresolved thread owned by the reviewer
|
|
404
|
+
throw new Error(`followUps[${index}].commentId must target an unresolved thread owned by the reviewer.\n\nAllowed targets:\n${allowedTargets}`);
|
|
393
405
|
if (resolves)
|
|
394
406
|
for (const [index, { commentId, threadId }] of resolves.entries())
|
|
395
407
|
if (targets.get(commentId) !== threadId)
|
|
396
|
-
throw new Error(`resolves[${index}] must target an unresolved thread owned by the reviewer
|
|
408
|
+
throw new Error(`resolves[${index}] must target an unresolved thread owned by the reviewer.\n\nAllowed targets:\n${allowedTargets}`);
|
|
397
409
|
}
|
package/dist/utils/function.js
CHANGED
|
@@ -12,24 +12,26 @@ export function wait(ms = 0, signal) {
|
|
|
12
12
|
signal?.addEventListener("abort", abort, { once: true });
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
export async function loop(
|
|
15
|
+
export async function loop(cb, ms = 0) {
|
|
16
16
|
for (;;) {
|
|
17
|
-
const value = await
|
|
17
|
+
const value = await cb();
|
|
18
18
|
if (value != null)
|
|
19
19
|
return value;
|
|
20
20
|
await wait(ms);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
export async function retry(
|
|
23
|
+
export async function retry(cb, { error: errorCb, retries = 1, signal }) {
|
|
24
24
|
let count = 1;
|
|
25
|
+
let error;
|
|
25
26
|
while (count <= retries)
|
|
26
27
|
try {
|
|
27
28
|
signal?.throwIfAborted();
|
|
28
|
-
return await
|
|
29
|
+
return await cb(count, error);
|
|
29
30
|
}
|
|
30
31
|
catch (e) {
|
|
31
32
|
signal?.throwIfAborted();
|
|
32
|
-
await
|
|
33
|
+
await errorCb?.(e, count);
|
|
34
|
+
error = e;
|
|
33
35
|
count += 1;
|
|
34
36
|
}
|
|
35
37
|
}
|
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-20260722060754",
|
|
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>",
|