opencode-magi 0.0.0-dev-20260721055255 → 0.0.0-dev-20260722111427
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.
|
@@ -52,6 +52,14 @@ export const MergeQueueStatusDocument = gql `
|
|
|
52
52
|
mergeQueueEntry {
|
|
53
53
|
id
|
|
54
54
|
}
|
|
55
|
+
timelineItems(last: 1, itemTypes: REMOVED_FROM_MERGE_QUEUE_EVENT) {
|
|
56
|
+
nodes {
|
|
57
|
+
... on RemovedFromMergeQueueEvent {
|
|
58
|
+
createdAt
|
|
59
|
+
reason
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
}
|
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);
|
|
@@ -43,7 +43,11 @@ export async function edit() {
|
|
|
43
43
|
}
|
|
44
44
|
return output;
|
|
45
45
|
}, {
|
|
46
|
-
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
|
+
},
|
|
47
51
|
retries: this.config.output.repairAttempts,
|
|
48
52
|
signal: this.context.abort,
|
|
49
53
|
});
|
|
@@ -134,7 +138,11 @@ export async function resolveConflict() {
|
|
|
134
138
|
responses: [],
|
|
135
139
|
};
|
|
136
140
|
}, {
|
|
137
|
-
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
|
+
},
|
|
138
146
|
retries: this.config.output.repairAttempts,
|
|
139
147
|
signal: this.context.abort,
|
|
140
148
|
});
|
|
@@ -86,7 +86,11 @@ export async function postReviews() {
|
|
|
86
86
|
throw new Error("Invalid output for operator.");
|
|
87
87
|
return parsed.comment;
|
|
88
88
|
}, {
|
|
89
|
-
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
|
+
},
|
|
90
94
|
retries: this.config.output.repairAttempts,
|
|
91
95
|
signal: this.context.abort,
|
|
92
96
|
});
|
|
@@ -228,9 +232,10 @@ export async function automate() {
|
|
|
228
232
|
if (action === "merge" && this.config.review.merge.queue) {
|
|
229
233
|
const octokit = await this.magi.createOctokit(this.config, this.context.abort, account);
|
|
230
234
|
const graphql = this.magi.createGraphql(octokit);
|
|
235
|
+
const enqueuedAt = new Date().toISOString();
|
|
231
236
|
await graphql.enqueuePullRequest({ id: this.state.pr.metadata.node_id });
|
|
232
237
|
await this.updateEvent(`Waiting for merge queue.`);
|
|
233
|
-
const result = await waitMergeQueue.call(this);
|
|
238
|
+
const result = await waitMergeQueue.call(this, enqueuedAt);
|
|
234
239
|
if (result === "CONFLICT")
|
|
235
240
|
return result;
|
|
236
241
|
}
|
|
@@ -354,8 +359,10 @@ function getCheckTime(check) {
|
|
|
354
359
|
return Number.isNaN(time) ? prev : Math.max(prev, time);
|
|
355
360
|
}, 0);
|
|
356
361
|
}
|
|
357
|
-
async function waitMergeQueue(leftMergeQueue = false) {
|
|
362
|
+
async function waitMergeQueue(enqueuedAt, leftMergeQueue = false, retries = 0) {
|
|
358
363
|
this.context.abort.throwIfAborted();
|
|
364
|
+
if (!this.state.pr?.metadata)
|
|
365
|
+
throw new MagiError("blocked", "PR metadata not found.");
|
|
359
366
|
const { repository } = await this.graphql.mergeQueueStatus({
|
|
360
367
|
owner: this.config.github.owner,
|
|
361
368
|
pr: this.number,
|
|
@@ -363,20 +370,33 @@ async function waitMergeQueue(leftMergeQueue = false) {
|
|
|
363
370
|
});
|
|
364
371
|
if (!repository?.pullRequest)
|
|
365
372
|
throw new MagiError("blocked", "Could not fetch merge queue status.");
|
|
366
|
-
const { isInMergeQueue, mergeQueueEntry, state } = repository.pullRequest;
|
|
373
|
+
const { isInMergeQueue, mergeQueueEntry, state, timelineItems } = repository.pullRequest;
|
|
367
374
|
if (state === "MERGED")
|
|
368
375
|
return "MERGED";
|
|
369
376
|
const nextLeftMergeQueue = state === "OPEN" && !isInMergeQueue && !mergeQueueEntry;
|
|
370
377
|
if (leftMergeQueue && nextLeftMergeQueue) {
|
|
371
378
|
if (await isConflict.call(this)) {
|
|
372
379
|
await this.updateState({ pr: { automation: "CONFLICT" } });
|
|
373
|
-
await this.updateEvent(
|
|
380
|
+
await this.updateEvent("Merge automation found conflicts.");
|
|
374
381
|
return "CONFLICT";
|
|
375
382
|
}
|
|
383
|
+
const removedFromQueue = timelineItems.nodes?.at(-1);
|
|
384
|
+
if (removedFromQueue?.reason === "failed_checks" &&
|
|
385
|
+
Date.parse(removedFromQueue.createdAt) >= Date.parse(enqueuedAt)) {
|
|
386
|
+
if (retries >= this.config.review.checks.retryFailedJobs)
|
|
387
|
+
throw new MagiError("blocked", `PR left the merge queue after failed checks exceeded ${retries} retries.`);
|
|
388
|
+
const attempt = retries + 1;
|
|
389
|
+
const nextEnqueuedAt = new Date().toISOString();
|
|
390
|
+
await this.updateEvent(`Attempt ${attempt} failed to merge from the merge queue. Retrying...`);
|
|
391
|
+
await this.graphql.enqueuePullRequest({
|
|
392
|
+
id: this.state.pr.metadata.node_id,
|
|
393
|
+
});
|
|
394
|
+
return await waitMergeQueue.call(this, nextEnqueuedAt, false, attempt);
|
|
395
|
+
}
|
|
376
396
|
throw new MagiError("blocked", `PR left the merge queue before merging.`);
|
|
377
397
|
}
|
|
378
398
|
await wait(30_000, this.context.abort);
|
|
379
|
-
return await waitMergeQueue.call(this, nextLeftMergeQueue);
|
|
399
|
+
return await waitMergeQueue.call(this, enqueuedAt, nextLeftMergeQueue, retries);
|
|
380
400
|
}
|
|
381
401
|
async function isConflict() {
|
|
382
402
|
if (!this.state.worktree)
|
|
@@ -113,7 +113,11 @@ export async function classifyChecks() {
|
|
|
113
113
|
throw new Error(`Invalid output for reviewer ${id}.`);
|
|
114
114
|
return parsed;
|
|
115
115
|
}, {
|
|
116
|
-
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
|
+
},
|
|
117
121
|
retries: this.config.output.repairAttempts,
|
|
118
122
|
signal: this.context.abort,
|
|
119
123
|
});
|
|
@@ -125,7 +125,11 @@ export async function review() {
|
|
|
125
125
|
]).join("\n\n"));
|
|
126
126
|
return parsed;
|
|
127
127
|
}, {
|
|
128
|
-
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
|
+
},
|
|
129
133
|
retries: this.config.output.repairAttempts,
|
|
130
134
|
signal: this.context.abort,
|
|
131
135
|
});
|
|
@@ -208,7 +212,11 @@ export async function reconsiderClose() {
|
|
|
208
212
|
validateInlineCommentTargets("initial", parsed, inlineCommentTargets);
|
|
209
213
|
return parsed;
|
|
210
214
|
}, {
|
|
211
|
-
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
|
+
},
|
|
212
220
|
retries: this.config.output.repairAttempts,
|
|
213
221
|
signal: this.context.abort,
|
|
214
222
|
});
|
|
@@ -287,7 +295,11 @@ async function collectAcceptedFindings(findings) {
|
|
|
287
295
|
throw new Error(`Missing finding vote: ${key}.`);
|
|
288
296
|
return parsed;
|
|
289
297
|
}, {
|
|
290
|
-
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
|
+
},
|
|
291
303
|
retries: this.config.output.repairAttempts,
|
|
292
304
|
signal: this.context.abort,
|
|
293
305
|
});
|
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-20260722111427",
|
|
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>",
|