opencode-magi 0.0.0-dev-20260714091053 → 0.0.0-dev-20260715023915
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 +5 -5
- package/dist/tools/merge/action.js +1 -0
- package/dist/tools/merge/editor.js +5 -3
- package/dist/tools/merge/merge.js +3 -20
- package/dist/tools/merge/report.js +1 -0
- package/dist/tools/review/action.js +4 -3
- package/dist/tools/review/check.js +3 -1
- package/dist/tools/review/report.js +1 -0
- package/dist/tools/review/review.js +20 -9
- package/dist/tools/review/reviewer.js +6 -3
- package/dist/utils/function.js +17 -4
- package/package.json +1 -1
package/dist/magi.js
CHANGED
|
@@ -33,7 +33,7 @@ export class Magi {
|
|
|
33
33
|
return this.exec(command("gh", "auth", "token", account && "--user", account && quote(account)), { signal });
|
|
34
34
|
}
|
|
35
35
|
async createOctokit(config, signal, account) {
|
|
36
|
-
const auth = await this.getGhToken(account);
|
|
36
|
+
const auth = await this.getGhToken(account, signal);
|
|
37
37
|
const retries = config.github.retryApiAttempts;
|
|
38
38
|
return new Octokit({
|
|
39
39
|
auth,
|
|
@@ -187,7 +187,7 @@ export class Magi {
|
|
|
187
187
|
...filterEmpty(this.getAgents(state).map(({ sessionId }) => sessionId)),
|
|
188
188
|
];
|
|
189
189
|
}
|
|
190
|
-
async createSession(parentID, title, { model, permissions, }) {
|
|
190
|
+
async createSession(parentID, title, { model, permissions, }, signal) {
|
|
191
191
|
if (isArray(model) || !isObject(model))
|
|
192
192
|
throw new Error();
|
|
193
193
|
const { id, variant } = model;
|
|
@@ -201,7 +201,7 @@ export class Magi {
|
|
|
201
201
|
parentID,
|
|
202
202
|
permission: resolvePermissions(permissions),
|
|
203
203
|
title,
|
|
204
|
-
});
|
|
204
|
+
}, { signal });
|
|
205
205
|
if (result.error) {
|
|
206
206
|
throw new Error(result.response.statusText);
|
|
207
207
|
}
|
|
@@ -210,11 +210,11 @@ export class Magi {
|
|
|
210
210
|
return id;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
-
async promptSession(sessionID, text) {
|
|
213
|
+
async promptSession(sessionID, text, signal) {
|
|
214
214
|
const result = await this.input.client.session.prompt({
|
|
215
215
|
parts: [{ text, type: "text" }],
|
|
216
216
|
sessionID,
|
|
217
|
-
});
|
|
217
|
+
}, { signal });
|
|
218
218
|
if (result.error) {
|
|
219
219
|
throw new Error(result.response.statusText);
|
|
220
220
|
}
|
|
@@ -15,6 +15,7 @@ export async function editCycles(callback) {
|
|
|
15
15
|
await this.updateEvent(`Attempt ${count} failed to edit cycles. Retrying...`);
|
|
16
16
|
},
|
|
17
17
|
retries: this.config.merge.maxThreadResolutionCycles,
|
|
18
|
+
signal: this.context.abort,
|
|
18
19
|
});
|
|
19
20
|
if (!verdict || verdict === "CHANGES_REQUESTED")
|
|
20
21
|
throw new MagiError("blocked", `Reached maximum edit cycles.`);
|
|
@@ -25,7 +25,7 @@ export async function edit() {
|
|
|
25
25
|
});
|
|
26
26
|
const repairMessage = await prompt.repair();
|
|
27
27
|
const output = await retry(async (count) => {
|
|
28
|
-
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage);
|
|
28
|
+
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
29
29
|
await this.createAgentFile("edit", "editor", raw, count, cycle);
|
|
30
30
|
const parsed = prompt.parse(raw);
|
|
31
31
|
if (!prompt.validate(parsed))
|
|
@@ -46,6 +46,7 @@ export async function edit() {
|
|
|
46
46
|
}, {
|
|
47
47
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to edit. Retrying...`),
|
|
48
48
|
retries: this.config.output.repairAttempts,
|
|
49
|
+
signal: this.context.abort,
|
|
49
50
|
});
|
|
50
51
|
if (!output)
|
|
51
52
|
throw new MagiError("blocked", "Invalid output for editor.");
|
|
@@ -112,7 +113,7 @@ export async function resolveConflict() {
|
|
|
112
113
|
});
|
|
113
114
|
const repairMessage = await prompt.repair();
|
|
114
115
|
const output = await retry(async (count) => {
|
|
115
|
-
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage);
|
|
116
|
+
const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
116
117
|
await this.createAgentFile("conflict", "editor", raw, count, cycle);
|
|
117
118
|
const parsed = prompt.parse(raw);
|
|
118
119
|
if (!prompt.validate(parsed))
|
|
@@ -137,6 +138,7 @@ export async function resolveConflict() {
|
|
|
137
138
|
}, {
|
|
138
139
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to resolve conflicts. Retrying...`),
|
|
139
140
|
retries: this.config.output.repairAttempts,
|
|
141
|
+
signal: this.context.abort,
|
|
140
142
|
});
|
|
141
143
|
if (!output)
|
|
142
144
|
throw new MagiError("blocked", "Invalid output for conflict editor.");
|
|
@@ -181,7 +183,7 @@ async function push() {
|
|
|
181
183
|
throw new MagiError("blocked", "PR metadata not found.");
|
|
182
184
|
if (!this.state.worktree)
|
|
183
185
|
throw new MagiError("blocked", "PR worktree not found.");
|
|
184
|
-
const token = await this.magi.getGhToken(this.state.editor.account);
|
|
186
|
+
const token = await this.magi.getGhToken(this.state.editor.account, this.context.abort);
|
|
185
187
|
const url = `https://${this.config.github.host}/${this.state.pr.metadata.head.repo.owner.login}/${this.state.pr.metadata.head.repo.name}.git`;
|
|
186
188
|
const ref = `HEAD:refs/heads/${this.state.pr.metadata.head.ref}`;
|
|
187
189
|
await this.exec(command("git", "push", quote(url), quote(ref)), {
|
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { MagiError } from "@/magi";
|
|
3
3
|
import { Review } from "@/tools/review/review";
|
|
4
|
-
import { createExecWithGitHubApiRetry, quote } from "@/utils";
|
|
5
4
|
import { editCycles, postReplies } from "./action";
|
|
6
5
|
import { fetchMergeContext, markRepliedReviewers } from "./context";
|
|
7
6
|
import { edit, resolveConflict } from "./editor";
|
|
8
7
|
import { createReport } from "./report";
|
|
9
8
|
export class Merge extends Review {
|
|
10
9
|
static async init(number, magi, config, context, options) {
|
|
11
|
-
const
|
|
12
|
-
const octokit = await magi.createOctokit(config, context.abort);
|
|
13
|
-
const graphql = magi.createGraphql(octokit);
|
|
14
|
-
const reviewers = Object.fromEntries(config.review.reviewers.map(({ account, id, model, permissions }) => [id, { account, model, permissions }]));
|
|
15
|
-
const operator = config.review.operator
|
|
16
|
-
? config.review.reviewers.find(({ id }) => id === config.review.operator)
|
|
17
|
-
: config.review.reviewers[Math.abs(number) % config.review.reviewers.length];
|
|
10
|
+
const { exec, graphql, octokit, ...rest } = await this.setup(number, magi, config, context);
|
|
18
11
|
const editor = {
|
|
19
12
|
account: config.merge.editor.account,
|
|
20
13
|
author: config.merge.editor.author,
|
|
21
14
|
model: config.merge.editor.model,
|
|
22
15
|
permissions: config.merge.editor.permissions,
|
|
23
16
|
};
|
|
24
|
-
const state = await magi.createState(join(config.review.output, number.toString()), {
|
|
25
|
-
...options,
|
|
26
|
-
command: "merge",
|
|
27
|
-
editor,
|
|
28
|
-
operator,
|
|
29
|
-
pr: { number, url },
|
|
30
|
-
repo: quote(`${config.github.owner}/${config.github.repo}`),
|
|
31
|
-
reviewers,
|
|
32
|
-
sessionId: context.sessionID,
|
|
33
|
-
});
|
|
17
|
+
const state = await magi.createState(join(config.review.output, number.toString()), { ...options, ...rest, command: "merge", editor });
|
|
34
18
|
await magi.updateEvent(state.output, `Started merging.`);
|
|
35
|
-
const exec = createExecWithGitHubApiRetry(magi.exec, config.github.retryApiAttempts);
|
|
36
19
|
return new Merge(number, magi, config, context, octokit, graphql, exec, state);
|
|
37
20
|
}
|
|
38
21
|
createReport = createReport;
|
|
@@ -51,7 +34,7 @@ export class Merge extends Review {
|
|
|
51
34
|
sessionId: await this.magi.createSession(this.state.sessionId, `magi merge #${this.number} editor`, {
|
|
52
35
|
model: this.state.editor.model,
|
|
53
36
|
permissions: this.state.editor.permissions,
|
|
54
|
-
}),
|
|
37
|
+
}, this.context.abort),
|
|
55
38
|
};
|
|
56
39
|
await this.updateState({ editor });
|
|
57
40
|
await this.updateEvent(`Finished creating editor session.`);
|
|
@@ -4,6 +4,7 @@ import { MagiError } from "@/magi";
|
|
|
4
4
|
import { createCheckContent, createMetaContent, createReviewerContent, } from "@/tools/review/report";
|
|
5
5
|
import { filterEmpty, toTitleCase } from "@/utils";
|
|
6
6
|
export async function createReport(e) {
|
|
7
|
+
this.context.abort.throwIfAborted();
|
|
7
8
|
if (!e) {
|
|
8
9
|
const status = "completed";
|
|
9
10
|
const text = await createContent.call(this, { status });
|
|
@@ -80,7 +80,7 @@ export async function postReviews() {
|
|
|
80
80
|
});
|
|
81
81
|
const repairMessage = await prompt.repair();
|
|
82
82
|
const output = await retry(async (count) => {
|
|
83
|
-
const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : repairMessage);
|
|
83
|
+
const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
84
84
|
await this.createAgentFile("comment", "operator", raw, count);
|
|
85
85
|
const parsed = prompt.parse(raw);
|
|
86
86
|
if (!prompt.validate(parsed))
|
|
@@ -89,6 +89,7 @@ export async function postReviews() {
|
|
|
89
89
|
}, {
|
|
90
90
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to post comment by operator. Retrying...`),
|
|
91
91
|
retries: this.config.output.repairAttempts,
|
|
92
|
+
signal: this.context.abort,
|
|
92
93
|
});
|
|
93
94
|
if (!output)
|
|
94
95
|
throw new MagiError("blocked", "Invalid output for operator.");
|
|
@@ -206,7 +207,7 @@ export async function automate() {
|
|
|
206
207
|
await this.updateEvent(`Skipped ${action} automation during dry run.`);
|
|
207
208
|
return "SKIPPED";
|
|
208
209
|
}
|
|
209
|
-
const token = await this.magi.getGhToken(account);
|
|
210
|
+
const token = await this.magi.getGhToken(account, this.context.abort);
|
|
210
211
|
if (action === "merge" && !this.config.review.merge.queue) {
|
|
211
212
|
const rules = JSON.parse(await this.exec(command("gh", "api", quote(`repos/${this.config.github.owner}/${this.config.github.repo}/rules/branches/${this.state.pr.metadata.base.ref}`)), {
|
|
212
213
|
env: { GH_TOKEN: token },
|
|
@@ -375,7 +376,7 @@ async function waitMergeQueue(leftMergeQueue = false) {
|
|
|
375
376
|
}
|
|
376
377
|
throw new MagiError("blocked", `PR left the merge queue before merging.`);
|
|
377
378
|
}
|
|
378
|
-
await wait(30_000);
|
|
379
|
+
await wait(30_000, this.context.abort);
|
|
379
380
|
return await waitMergeQueue.call(this, nextLeftMergeQueue);
|
|
380
381
|
}
|
|
381
382
|
async function isConflict() {
|
|
@@ -107,7 +107,7 @@ export async function classifyChecks() {
|
|
|
107
107
|
throw new Error(`No session ID found for reviewer ${id}.`);
|
|
108
108
|
await this.updateEvent(`Classifying CI checks with reviewer ${id}.`);
|
|
109
109
|
const output = await retry(async (count) => {
|
|
110
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
|
|
110
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
111
111
|
await this.createAgentFile("ci-classification", id, raw, count);
|
|
112
112
|
const parsed = prompt.parse(raw);
|
|
113
113
|
if (!prompt.validate(parsed))
|
|
@@ -116,6 +116,7 @@ export async function classifyChecks() {
|
|
|
116
116
|
}, {
|
|
117
117
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to classify CI checks with reviewer ${id}. Retrying...`),
|
|
118
118
|
retries: this.config.output.repairAttempts,
|
|
119
|
+
signal: this.context.abort,
|
|
119
120
|
});
|
|
120
121
|
if (!output)
|
|
121
122
|
throw new MagiError("blocked", `Invalid output for reviewer ${id}.`);
|
|
@@ -201,6 +202,7 @@ export async function rerunChecks() {
|
|
|
201
202
|
}, {
|
|
202
203
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to rerun checks ${label}. Retrying...`),
|
|
203
204
|
retries: this.config.review.checks.retryFailedJobs,
|
|
205
|
+
signal: this.context.abort,
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
const passedChecksAfterRerun = checks.passed.filter((check) => "scope" in check && !check.scope);
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { MagiError } from "@/magi";
|
|
4
4
|
import { filterEmpty, toTitleCase } from "@/utils";
|
|
5
5
|
export async function createReport(e) {
|
|
6
|
+
this.context.abort.throwIfAborted();
|
|
6
7
|
if (!e) {
|
|
7
8
|
const status = "completed";
|
|
8
9
|
const text = await createContent.call(this, { status });
|
|
@@ -27,6 +27,12 @@ export class Review {
|
|
|
27
27
|
this.state = state;
|
|
28
28
|
}
|
|
29
29
|
static async init(number, magi, config, context, options) {
|
|
30
|
+
const { exec, graphql, octokit, ...rest } = await this.setup(number, magi, config, context);
|
|
31
|
+
const state = await magi.createState(join(config.review.output, number.toString()), { ...options, ...rest, command: "review" });
|
|
32
|
+
await magi.updateEvent(state.output, `Started reviewing.`);
|
|
33
|
+
return new Review(number, magi, config, context, octokit, graphql, exec, state);
|
|
34
|
+
}
|
|
35
|
+
static async setup(number, magi, config, context) {
|
|
30
36
|
const url = `${config.github.url}/pull/${number}`;
|
|
31
37
|
const octokit = await magi.createOctokit(config, context.abort);
|
|
32
38
|
const graphql = magi.createGraphql(octokit);
|
|
@@ -34,18 +40,17 @@ export class Review {
|
|
|
34
40
|
const operator = config.review.operator
|
|
35
41
|
? config.review.reviewers.find(({ id }) => id === config.review.operator)
|
|
36
42
|
: config.review.reviewers[Math.abs(number) % config.review.reviewers.length];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const exec = createExecWithGitHubApiRetry(magi.exec, config.github.retryApiAttempts);
|
|
44
|
+
return {
|
|
45
|
+
exec,
|
|
46
|
+
graphql,
|
|
47
|
+
octokit,
|
|
40
48
|
operator,
|
|
41
49
|
pr: { number, url },
|
|
42
50
|
repo: quote(`${config.github.owner}/${config.github.repo}`),
|
|
43
51
|
reviewers,
|
|
44
52
|
sessionId: context.sessionID,
|
|
45
|
-
}
|
|
46
|
-
await magi.updateEvent(state.output, `Started reviewing.`);
|
|
47
|
-
const exec = createExecWithGitHubApiRetry(magi.exec, config.github.retryApiAttempts);
|
|
48
|
-
return new Review(number, magi, config, context, octokit, graphql, exec, state);
|
|
53
|
+
};
|
|
49
54
|
}
|
|
50
55
|
checkPr = checkPr;
|
|
51
56
|
checkExistingReviews = checkExistingReviews;
|
|
@@ -60,6 +65,12 @@ export class Review {
|
|
|
60
65
|
automate = automate;
|
|
61
66
|
createReport = createReport;
|
|
62
67
|
async cleanup() {
|
|
68
|
+
if (this.context.abort.aborted &&
|
|
69
|
+
["preparing", "running"].includes(this.state.status))
|
|
70
|
+
await this.updateState({
|
|
71
|
+
completedAt: new Date().toISOString(),
|
|
72
|
+
status: "cancelled",
|
|
73
|
+
});
|
|
63
74
|
if (this.state.worktree?.path)
|
|
64
75
|
await this.magi.deleteWorktree(this.state.worktree.path);
|
|
65
76
|
}
|
|
@@ -84,14 +95,14 @@ export class Review {
|
|
|
84
95
|
const reviewers = Object.fromEntries(await Promise.all(Object.entries(this.state.reviewers).map(async ([id, { model, permissions }]) => [
|
|
85
96
|
id,
|
|
86
97
|
{
|
|
87
|
-
sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} ${id}`, { model, permissions }),
|
|
98
|
+
sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} ${id}`, { model, permissions }, this.context.abort),
|
|
88
99
|
},
|
|
89
100
|
])));
|
|
90
101
|
const operator = {
|
|
91
102
|
sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} operator`, {
|
|
92
103
|
model: this.state.operator.model,
|
|
93
104
|
permissions: this.state.operator.permissions,
|
|
94
|
-
}),
|
|
105
|
+
}, this.context.abort),
|
|
95
106
|
};
|
|
96
107
|
await this.updateState({ operator, reviewers });
|
|
97
108
|
}
|
|
@@ -96,7 +96,7 @@ export async function review() {
|
|
|
96
96
|
}));
|
|
97
97
|
const repairMessage = await prompt.repair();
|
|
98
98
|
const output = await retry(async (count) => {
|
|
99
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
|
|
99
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
100
100
|
await this.createAgentFile(rereview ? "rereview" : "review", id, raw, count, cycle);
|
|
101
101
|
const parsed = prompt.parse(raw);
|
|
102
102
|
if (!prompt.validate(parsed))
|
|
@@ -127,6 +127,7 @@ export async function review() {
|
|
|
127
127
|
}, {
|
|
128
128
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to ${label} with reviewer ${id}. Retrying...`),
|
|
129
129
|
retries: this.config.output.repairAttempts,
|
|
130
|
+
signal: this.context.abort,
|
|
130
131
|
});
|
|
131
132
|
if (!output)
|
|
132
133
|
throw new MagiError("blocked", `Invalid output for reviewer ${id}.`);
|
|
@@ -200,7 +201,7 @@ export async function reconsiderClose() {
|
|
|
200
201
|
const repairMessage = await prompt.repair();
|
|
201
202
|
await this.updateEvent(`Reconsidering close verdict with reviewer ${id}.`);
|
|
202
203
|
const output = await retry(async (count) => {
|
|
203
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
|
|
204
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
204
205
|
await this.createAgentFile("close-reconsideration", id, raw, count, cycle);
|
|
205
206
|
const parsed = prompt.parse(raw);
|
|
206
207
|
if (!prompt.validate(parsed))
|
|
@@ -210,6 +211,7 @@ export async function reconsiderClose() {
|
|
|
210
211
|
}, {
|
|
211
212
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to reconsider close verdict with reviewer ${id}. Retrying...`),
|
|
212
213
|
retries: this.config.output.repairAttempts,
|
|
214
|
+
signal: this.context.abort,
|
|
213
215
|
});
|
|
214
216
|
if (!output)
|
|
215
217
|
throw new MagiError("blocked", `Invalid close reconsideration output for reviewer ${id}.`);
|
|
@@ -266,7 +268,7 @@ async function collectAcceptedFindings(findings) {
|
|
|
266
268
|
});
|
|
267
269
|
const repairMessage = await prompt.repair();
|
|
268
270
|
const output = await retry(async (count) => {
|
|
269
|
-
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
|
|
271
|
+
const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage, this.context.abort);
|
|
270
272
|
await this.createAgentFile("finding-validation", id, raw, count);
|
|
271
273
|
const parsed = prompt.parse(raw);
|
|
272
274
|
if (!prompt.validate(parsed))
|
|
@@ -289,6 +291,7 @@ async function collectAcceptedFindings(findings) {
|
|
|
289
291
|
}, {
|
|
290
292
|
error: (_, count) => this.updateEvent(`Attempt ${count} failed to validate review findings with reviewer ${id}. Retrying...`),
|
|
291
293
|
retries: this.config.output.repairAttempts,
|
|
294
|
+
signal: this.context.abort,
|
|
292
295
|
});
|
|
293
296
|
if (!output)
|
|
294
297
|
throw new MagiError("blocked", `Invalid finding validation output for reviewer ${id}.`);
|
package/dist/utils/function.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export function wait(ms = 0, signal) {
|
|
2
|
+
signal?.throwIfAborted();
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const abort = () => {
|
|
5
|
+
clearTimeout(timeout);
|
|
6
|
+
reject(signal?.reason);
|
|
7
|
+
};
|
|
8
|
+
const timeout = setTimeout(() => {
|
|
9
|
+
signal?.removeEventListener("abort", abort);
|
|
10
|
+
resolve();
|
|
11
|
+
}, ms);
|
|
12
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
13
|
+
});
|
|
14
|
+
}
|
|
4
15
|
export async function loop(callback, ms = 0) {
|
|
5
16
|
for (;;) {
|
|
6
17
|
const value = await callback();
|
|
@@ -9,13 +20,15 @@ export async function loop(callback, ms = 0) {
|
|
|
9
20
|
await wait(ms);
|
|
10
21
|
}
|
|
11
22
|
}
|
|
12
|
-
export async function retry(callback, { error, retries = 1 }) {
|
|
23
|
+
export async function retry(callback, { error, retries = 1, signal }) {
|
|
13
24
|
let count = 1;
|
|
14
25
|
while (count <= retries)
|
|
15
26
|
try {
|
|
27
|
+
signal?.throwIfAborted();
|
|
16
28
|
return await callback(count);
|
|
17
29
|
}
|
|
18
30
|
catch (e) {
|
|
31
|
+
signal?.throwIfAborted();
|
|
19
32
|
await error?.(e, count);
|
|
20
33
|
count += 1;
|
|
21
34
|
}
|
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-20260715023915",
|
|
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>",
|