opencode-magi 0.0.0-dev-20260630051947 → 0.0.0-dev-20260630052239

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 CHANGED
@@ -200,7 +200,7 @@ export class Magi {
200
200
  const prev = await this.getState(dir);
201
201
  const state = merge(prev, next);
202
202
  const values = [this.createStateFile(state)];
203
- if (next.text)
203
+ if (!state.sync && next.text)
204
204
  values.push(this.notify(prev.sessionId, next.text));
205
205
  await Promise.all(values);
206
206
  return state;
@@ -267,10 +267,8 @@ export class Magi {
267
267
  const path = this.getPath(join(dir, number.toString(), id));
268
268
  try {
269
269
  await mkdir(dirname(path), { recursive: true });
270
- await this.exec(command("git", "worktree", "add", quote(path)), {
271
- signal,
272
- });
273
- await this.exec(command("gh", "pr", "checkout", number), {
270
+ await this.exec(command("git", "worktree", "add", "--detach", quote(path)), { signal });
271
+ await this.exec(command("gh", "pr", "checkout", number, "--detach"), {
274
272
  cwd: path,
275
273
  signal,
276
274
  });
@@ -278,9 +276,7 @@ export class Magi {
278
276
  cwd: path,
279
277
  signal,
280
278
  });
281
- if (!branch)
282
- throw new Error("Failed to determine worktree branch");
283
- return { branch, path };
279
+ return { branch: branch || undefined, path };
284
280
  }
285
281
  catch (e) {
286
282
  try {
@@ -9,10 +9,10 @@ export async function editCycles(callback) {
9
9
  throw error;
10
10
  return verdict;
11
11
  }, {
12
- error: (e, count) => {
12
+ error: async (e, count) => {
13
13
  if (e !== error)
14
14
  throw e;
15
- this.magi.notify(this.state.sessionId, `Attempt ${count} failed to edit cycles for ${this.getLink()}. Retrying...`);
15
+ await this.notify(`Attempt ${count} failed to edit cycles for ${this.getLink()}. Retrying...`);
16
16
  },
17
17
  retries: this.config.merge.maxThreadResolutionCycles,
18
18
  });
@@ -40,7 +40,7 @@ export async function edit() {
40
40
  }
41
41
  return parsed;
42
42
  }, {
43
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to edit ${this.getLink()}. Retrying...`),
43
+ error: (_, count) => this.notify(`Attempt ${count} failed to edit ${this.getLink()}. Retrying...`),
44
44
  retries: this.config.output.repairAttempts,
45
45
  });
46
46
  if (!output)
@@ -49,7 +49,7 @@ export async function edit() {
49
49
  editor: { outputs: [...(this.state.editor.outputs ?? []), output] },
50
50
  text: `Finished editing ${this.getLink()}.`,
51
51
  });
52
- await this.magi.notify(this.state.sessionId, filterEmpty([
52
+ await this.notify(filterEmpty([
53
53
  `Editor ${output.mode === "EDITED" ? "edited" : "replied to"} ${this.getLink()}.`,
54
54
  output.commitSha
55
55
  ? `Commit: ${output.commitSha}${output.commitMessage ? ` ${output.commitMessage}` : ""}`
@@ -135,7 +135,7 @@ export async function resolveConflict() {
135
135
  responses: [],
136
136
  };
137
137
  }, {
138
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to resolve conflicts for ${this.getLink()}. Retrying...`),
138
+ error: (_, count) => this.notify(`Attempt ${count} failed to resolve conflicts for ${this.getLink()}. Retrying...`),
139
139
  retries: this.config.output.repairAttempts,
140
140
  });
141
141
  if (!output)
@@ -27,8 +27,8 @@ export class Merge extends Review {
27
27
  permissions: config.merge.editor.permissions,
28
28
  };
29
29
  const state = await magi.createState(join(config.review.output, number.toString()), {
30
+ ...options,
30
31
  command: "merge",
31
- dryRun: options.dryRun,
32
32
  editor,
33
33
  operator,
34
34
  pr: { number, url },
@@ -45,7 +45,7 @@ export async function postReviews() {
45
45
  .map((finding) => ({ ...finding, id }));
46
46
  });
47
47
  if (!findings.length) {
48
- this.magi.notify(this.state.sessionId, `Finished posting reviews for ${this.getLink()}.`);
48
+ await this.notify(`Finished posting reviews for ${this.getLink()}.`);
49
49
  return;
50
50
  }
51
51
  params.comments = findings.map(({ body, id, line, path, startLine }) => ({
@@ -63,7 +63,7 @@ export async function postReviews() {
63
63
  if (this.state.pr.verdict !== "APPROVED") {
64
64
  if (!this.state.operator?.sessionId)
65
65
  throw new MagiError("blocked", "Reporter session ID not found.");
66
- await this.magi.notify(this.state.sessionId, `Generating comment for ${this.getLink()} by operator.`);
66
+ await this.notify(`Generating comment for ${this.getLink()} by operator.`);
67
67
  const contents = JSON.stringify(reviewers.flatMap(([, { outputs }]) => {
68
68
  const output = outputs?.at(-1);
69
69
  if (!output || output.verdict === "APPROVED")
@@ -88,12 +88,12 @@ export async function postReviews() {
88
88
  throw new Error("Invalid output for operator.");
89
89
  return parsed.comment;
90
90
  }, {
91
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to post comment for ${this.getLink()} by operator. Retrying...`),
91
+ error: (_, count) => this.notify(`Attempt ${count} failed to post comment for ${this.getLink()} by operator. Retrying...`),
92
92
  retries: this.config.output.repairAttempts,
93
93
  });
94
94
  if (!output)
95
95
  throw new MagiError("blocked", "Invalid output for operator.");
96
- await this.magi.notify(this.state.sessionId, `Generated comment for ${this.getLink()} by operator.`);
96
+ await this.notify(`Generated comment for ${this.getLink()} by operator.`);
97
97
  params.body = output;
98
98
  }
99
99
  params.body = [
@@ -115,7 +115,7 @@ export async function classifyChecks() {
115
115
  await Promise.all(Object.entries(this.state.reviewers ?? {}).map(([id, { sessionId }]) => worker.run(async () => {
116
116
  if (!sessionId)
117
117
  throw new Error(`No session ID found for reviewer ${id}.`);
118
- await this.magi.notify(this.state.sessionId, `Classifying CI checks for ${this.getLink()} with reviewer ${id}.`);
118
+ await this.notify(`Classifying CI checks for ${this.getLink()} with reviewer ${id}.`);
119
119
  const output = await retry(async (count) => {
120
120
  const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
121
121
  const parsed = prompt.parse(raw);
@@ -123,7 +123,7 @@ export async function classifyChecks() {
123
123
  throw new Error(`Invalid output for reviewer ${id}.`);
124
124
  return parsed;
125
125
  }, {
126
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to classify CI checks for ${this.getLink()} with reviewer ${id}. Retrying...`),
126
+ error: (_, count) => this.notify(`Attempt ${count} failed to classify CI checks for ${this.getLink()} with reviewer ${id}. Retrying...`),
127
127
  retries: this.config.output.repairAttempts,
128
128
  });
129
129
  if (!output)
@@ -151,7 +151,7 @@ export async function classifyChecks() {
151
151
  .filter(([, { scope }]) => !scope)
152
152
  .map(([id, { comment }]) => `- ${id}: ${comment}`),
153
153
  };
154
- await this.magi.notify(this.state.sessionId, filterEmpty([
154
+ await this.notify(filterEmpty([
155
155
  scope
156
156
  ? `Check ${name} for ${this.getLink()} was classified as in scope by majority vote.`
157
157
  : `Check ${name} for ${this.getLink()} was classified as out of scope by majority vote. Rerunning it.`,
@@ -192,7 +192,7 @@ export async function rerunChecks() {
192
192
  }
193
193
  else {
194
194
  await retry(async () => {
195
- await this.magi.notify(this.state.sessionId, `Rerunning checks ${label} for ${this.getLink()}.`);
195
+ await this.notify(`Rerunning checks ${label} for ${this.getLink()}.`);
196
196
  await Promise.all(checks.failed
197
197
  .filter(({ scope }) => !scope)
198
198
  .map(async ({ id }) => rerunCheck.call(this, id)));
@@ -212,7 +212,7 @@ export async function rerunChecks() {
212
212
  label = failedChecks.map(({ name }) => name).join(", ");
213
213
  throw new Error(`Checks ${label} for ${this.getLink()} still failed.`);
214
214
  }, {
215
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to rerun checks ${label} for ${this.getLink()}. Retrying...`),
215
+ error: (_, count) => this.notify(`Attempt ${count} failed to rerun checks ${label} for ${this.getLink()}. Retrying...`),
216
216
  retries: this.config.review.checks.retryFailedJobs,
217
217
  });
218
218
  }
@@ -227,7 +227,7 @@ export async function rerunChecks() {
227
227
  : undefined,
228
228
  ]).join("\n");
229
229
  if (message)
230
- await this.magi.notify(this.state.sessionId, message);
230
+ await this.notify(message);
231
231
  this.state = await this.magi.updateState(this.state.output, {
232
232
  pr: { checks },
233
233
  text: `Finished rerunning checks for ${this.getLink()}.`,
@@ -40,8 +40,8 @@ export class Review {
40
40
  ? config.review.reviewers.find(({ id }) => id === config.review.operator)
41
41
  : config.review.reviewers[Math.abs(number) % config.review.reviewers.length];
42
42
  const state = await magi.createState(join(config.review.output, number.toString()), {
43
+ ...options,
43
44
  command: "review",
44
- dryRun: options.dryRun,
45
45
  operator,
46
46
  pr: { number, url },
47
47
  repo: quote(`${config.github.owner}/${config.github.repo}`),
@@ -64,6 +64,10 @@ export class Review {
64
64
  postReviews = postReviews;
65
65
  automate = automate;
66
66
  createReport = createReport;
67
+ async notify(text) {
68
+ if (!this.state.sync)
69
+ await this.magi.notify(this.state.sessionId, text);
70
+ }
67
71
  async cleanup() {
68
72
  if (this.state.worktree?.path)
69
73
  await this.magi.deleteWorktree(this.state.worktree.path);
@@ -22,7 +22,7 @@ export async function review() {
22
22
  throw new MagiError("blocked", "Reviewers not found.");
23
23
  const { outputs, review, sessionId, status } = this.state.reviewers[id];
24
24
  if (status === "skip") {
25
- this.magi.notify(this.state.sessionId, `Skipping review for ${this.getLink()} with reviewer ${id}.`);
25
+ await this.notify(`Skipping review for ${this.getLink()} with reviewer ${id}.`);
26
26
  return [id, {}];
27
27
  }
28
28
  else {
@@ -32,7 +32,7 @@ export async function review() {
32
32
  throw new MagiError("blocked", `Missing previous review commit for reviewer ${id}.`);
33
33
  const rereview = status !== "initial";
34
34
  const label = rereview ? "rereview" : "review";
35
- await this.magi.notify(this.state.sessionId, `Running ${label} for ${this.getLink()} with reviewer ${id}.`);
35
+ await this.notify(`Running ${label} for ${this.getLink()} with reviewer ${id}.`);
36
36
  const sha = rereview
37
37
  ? (review?.commit_id ?? this.state.pr.metadata.base.sha)
38
38
  : this.state.pr.metadata.base.sha;
@@ -104,7 +104,7 @@ export async function review() {
104
104
  validateInlineCommentTargets(status, parsed, inlineCommentTargets);
105
105
  return parsed;
106
106
  }, {
107
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to ${label} for ${this.getLink()} with reviewer ${id}. Retrying...`),
107
+ error: (_, count) => this.notify(`Attempt ${count} failed to ${label} for ${this.getLink()} with reviewer ${id}. Retrying...`),
108
108
  retries: this.config.output.repairAttempts,
109
109
  });
110
110
  if (!output)
@@ -184,7 +184,7 @@ export async function reconsiderClose() {
184
184
  repo: this.config.github.repo,
185
185
  });
186
186
  const repairMessage = await prompt.repair();
187
- await this.magi.notify(this.state.sessionId, `Reconsidering close verdict for ${this.getLink()} with reviewer ${id}.`);
187
+ await this.notify(`Reconsidering close verdict for ${this.getLink()} with reviewer ${id}.`);
188
188
  const output = await retry(async (count) => {
189
189
  const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
190
190
  const parsed = prompt.parse(raw);
@@ -193,7 +193,7 @@ export async function reconsiderClose() {
193
193
  validateInlineCommentTargets("initial", parsed, inlineCommentTargets);
194
194
  return parsed;
195
195
  }, {
196
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to reconsider close verdict for ${this.getLink()} with reviewer ${id}. Retrying...`),
196
+ error: (_, count) => this.notify(`Attempt ${count} failed to reconsider close verdict for ${this.getLink()} with reviewer ${id}. Retrying...`),
197
197
  retries: this.config.output.repairAttempts,
198
198
  });
199
199
  if (!output)
@@ -240,7 +240,7 @@ async function collectAcceptedFindings(findings) {
240
240
  const { sessionId } = this.state.reviewers[id];
241
241
  if (!sessionId)
242
242
  throw new MagiError("blocked", `No session ID found for reviewer ${id}.`);
243
- await this.magi.notify(this.state.sessionId, `Validating review findings for ${this.getLink()} with reviewer ${id}.`);
243
+ await this.notify(`Validating review findings for ${this.getLink()} with reviewer ${id}.`);
244
244
  const targetFindings = findings.filter((target) => target.reviewer !== id);
245
245
  const expectedKeys = new Set(targetFindings.map(({ index, reviewer }) => `${reviewer}:${index}`));
246
246
  const taskMessage = await prompt.create(this.config.review.prompts?.findingValidation, [
@@ -273,7 +273,7 @@ async function collectAcceptedFindings(findings) {
273
273
  throw new Error(`Missing finding vote: ${key}.`);
274
274
  return parsed;
275
275
  }, {
276
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to validate review findings for ${this.getLink()} with reviewer ${id}. Retrying...`),
276
+ error: (_, count) => this.notify(`Attempt ${count} failed to validate review findings for ${this.getLink()} with reviewer ${id}. Retrying...`),
277
277
  retries: this.config.output.repairAttempts,
278
278
  });
279
279
  if (!output)
@@ -298,7 +298,7 @@ async function collectAcceptedFindings(findings) {
298
298
  .map(({ validator, vote }) => `- ${validator}: ${vote.comment}`);
299
299
  if (agrees >= threshold)
300
300
  accepted.add(key);
301
- await this.magi.notify(this.state.sessionId, filterEmpty([
301
+ await this.notify(filterEmpty([
302
302
  `Finding ${reviewer} #${index + 1} for ${this.getLink()} was ${agrees >= threshold ? "accepted" : "rejected"} by majority vote.`,
303
303
  `Finding: ${finding.path}:${finding.line}\n${finding.body}`,
304
304
  acceptedComments.length
@@ -338,7 +338,7 @@ async function notifyVerdictChanges(prev, next, reason) {
338
338
  const nextVerdict = reviewer.outputs?.at(-1)?.verdict;
339
339
  if (!prevVerdict || !nextVerdict || prevVerdict === nextVerdict)
340
340
  return;
341
- await this.magi.notify(this.state.sessionId, `Reviewer ${id} verdict changed from ${prevVerdict} to ${nextVerdict} for ${this.getLink()} ${reason}.`);
341
+ await this.notify(`Reviewer ${id} verdict changed from ${prevVerdict} to ${nextVerdict} for ${this.getLink()} ${reason}.`);
342
342
  }));
343
343
  }
344
344
  function validateInlineCommentTargets(status, output, inlineCommentTargets) {
@@ -16,7 +16,7 @@ export async function retry(callback, { error, retries = 1 }) {
16
16
  return await callback(count);
17
17
  }
18
18
  catch (e) {
19
- error?.(e, count);
19
+ await error?.(e, count);
20
20
  count += 1;
21
21
  }
22
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260630051947",
3
+ "version": "0.0.0-dev-20260630052239",
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>",