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.
@@ -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
- getLink() {
77
- return `[#${this.state.pr.number}](${this.state.pr.url})`;
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
- this.state = await this.magi.updateState(this.state.output, {
101
- operator,
102
- reviewers,
103
- });
96
+ await this.updateState({ operator, reviewers });
104
97
  }
105
98
  async createWorktree() {
106
99
  this.context.abort.throwIfAborted();
107
- this.state = await this.magi.updateState(this.state.output, {
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
- this.state = await this.magi.updateState(this.state.output, {
112
- text: `Finished creating worktree for ${this.getLink()}.`,
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
- this.state = await this.magi.updateState(this.state.output, {
142
- pr: { verdict },
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
- this.state = await this.magi.updateState(this.state.output, {
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.notify(`Skipping review for ${this.getLink()} with reviewer ${id}.`);
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.notify(`Running ${label} for ${this.getLink()} with reviewer ${id}.`);
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.notify(`Attempt ${count} failed to ${label} for ${this.getLink()} with reviewer ${id}. Retrying...`),
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
- this.state = await this.magi.updateState(this.state.output, {
122
- reviewers,
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
- this.state = await this.magi.updateState(this.state.output, {
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
- this.state = await this.magi.updateState(this.state.output, {
150
- reviewers,
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
- this.state = await this.magi.updateState(this.state.output, {
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.notify(`Reconsidering close verdict for ${this.getLink()} with reviewer ${id}.`);
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.notify(`Attempt ${count} failed to reconsider close verdict for ${this.getLink()} with reviewer ${id}. Retrying...`),
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
- this.state = await this.magi.updateState(this.state.output, {
229
- reviewers: validatedReviewers,
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.notify(`Validating review findings for ${this.getLink()} with reviewer ${id}.`);
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.notify(`Attempt ${count} failed to validate review findings for ${this.getLink()} with reviewer ${id}. Retrying...`),
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.notify(filterEmpty([
307
- `Finding ${reviewer} #${index + 1} for ${this.getLink()} was ${agrees >= threshold ? "accepted" : "rejected"} by majority vote.`,
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.notify(`Reviewer ${id} verdict changed from ${prevVerdict} to ${nextVerdict} for ${this.getLink()} ${reason}.`);
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, sync: _sync = false, }) {
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-20260702001022",
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
- "clean": "rimraf node_modules dist coverage",
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
- "quality": "pnpm format:check && pnpm lint:check && pnpm typecheck && pnpm test",
75
+ "type:check": "tsgo --noEmit",
76
76
  "test": "vitest run --passWithNoTests",
77
77
  "test:dev": "vitest --watch --ui --passWithNoTests",
78
- "typecheck": "tsgo --noEmit",
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
- };