opencode-magi 0.0.0-dev-20260630052048 → 0.0.0-dev-20260630070412

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;
@@ -19,6 +19,8 @@ Rules:
19
19
  - `"ASK"` means you need clarification and did not edit.
20
20
  - Do not make changes just because a reviewer requested them; edit only when you understand and agree.
21
21
  - Do not push. The orchestrator pushes after validating this envelope.
22
- - `filesTouched` must include every final changed file.
23
- - `responses` must include a reply for each thread you addressed.
24
- - `"REPLIED"` requires `filesTouched` to be empty and at least one `"DISAGREE"` or `"ASK"` response.
22
+ - `filesTouched` is required for `"EDITED"` and must include every final changed file.
23
+ - `responses` is required and must include a reply for each thread you addressed.
24
+ - `"EDITED"` requires at least one response.
25
+ - `"REPLIED"` may omit `filesTouched`. If present, it must be empty.
26
+ - `"REPLIED"` requires at least one `"DISAGREE"` or `"ASK"` response.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["mode", "filesTouched", "responses"],
3
+ "required": ["mode", "responses"],
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
6
  "mode": { "enum": ["EDITED", "REPLIED"] },
@@ -28,8 +28,11 @@
28
28
  {
29
29
  "if": { "properties": { "mode": { "const": "EDITED" } } },
30
30
  "then": {
31
- "required": ["commitSha", "commitMessage"],
32
- "properties": { "filesTouched": { "type": "array", "minItems": 1 } }
31
+ "required": ["commitSha", "commitMessage", "filesTouched"],
32
+ "properties": {
33
+ "filesTouched": { "type": "array", "minItems": 1 },
34
+ "responses": { "type": "array", "minItems": 1 }
35
+ }
33
36
  }
34
37
  },
35
38
  {
@@ -11,7 +11,8 @@ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
11
11
  Rules:
12
12
 
13
13
  - `"verdict"` must be `"APPROVED"` or `"CHANGES_REQUESTED"`.
14
- - `"APPROVED"` requires an empty `"findings"` array.
14
+ - `"APPROVED"` may omit `"findings"`. If present, it must be empty.
15
+ - `"APPROVED"` may omit `"comment"`. If present, it must be an empty string.
15
16
  - `"CHANGES_REQUESTED"` requires `"comment"` and at least one `"finding"`.
16
17
  - `"comment"` for `"CHANGES_REQUESTED"` must be a concise prose review summary, not a bullet list of findings.
17
18
  - `"CLOSED"` is not allowed in this reconsideration step.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["verdict", "findings"],
3
+ "required": ["verdict"],
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
6
  "verdict": { "enum": ["APPROVED", "CHANGES_REQUESTED"] },
@@ -18,20 +18,26 @@
18
18
  }
19
19
  }
20
20
  },
21
- "comment": { "type": "string", "minLength": 1 }
21
+ "comment": { "type": "string" }
22
22
  },
23
23
  "allOf": [
24
24
  {
25
25
  "if": { "properties": { "verdict": { "const": "APPROVED" } } },
26
26
  "then": {
27
- "properties": { "findings": { "type": "array", "maxItems": 0 } }
27
+ "properties": {
28
+ "findings": { "type": "array", "maxItems": 0 },
29
+ "comment": { "const": "" }
30
+ }
28
31
  }
29
32
  },
30
33
  {
31
34
  "if": { "properties": { "verdict": { "const": "CHANGES_REQUESTED" } } },
32
35
  "then": {
33
- "required": ["comment"],
34
- "properties": { "findings": { "type": "array", "minItems": 1 } }
36
+ "required": ["comment", "findings"],
37
+ "properties": {
38
+ "findings": { "type": "array", "minItems": 1 },
39
+ "comment": { "type": "string", "minLength": 1 }
40
+ }
35
41
  }
36
42
  }
37
43
  ]
@@ -15,11 +15,12 @@ Rules:
15
15
  - `"verdict"` must be `"APPROVED"`, `"CHANGES_REQUESTED"`, or `"CLOSED"`.
16
16
  - `"resolves"` contains threads that should be resolved because the issue is fixed or the user's explanation is acceptable.
17
17
  - Each `"resolves"` item must use the exact `"commentId"` and `"threadId"` from `<unresolved_threads>`.
18
- - Use an empty `"resolves"` array when no thread should be resolved.
19
- - `"APPROVED"` requires empty `"followUps"` and `"newFindings"` arrays.
18
+ - Omit `"resolves"` when no thread should be resolved.
19
+ - `"APPROVED"` may omit `"followUps"` and `"newFindings"`. If present, they must be empty.
20
+ - `"APPROVED"` may omit `"comment"`. If present, it must be an empty string.
20
21
  - `"CHANGES_REQUESTED"` requires `"comment"` and at least one `"followUp"` or `"newFinding"`.
21
22
  - `"comment"` for `"CHANGES_REQUESTED"` must be a concise prose review summary, not a bullet list of findings.
22
- - `"CLOSED"` requires `"comment"` and empty `"followUps"` and `"newFindings"` arrays.
23
+ - `"CLOSED"` requires `"comment"` and may omit `"followUps"` and `"newFindings"`. If present, they must be empty.
23
24
  - `"line"` is required and must target a valid right-side line inside the latest PR diff hunk.
24
25
  - `"startLine"` is optional and must also target a valid right-side line inside the same latest PR diff hunk range.
25
26
  - Omit `"startLine"` for single-line findings.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["verdict", "resolves", "followUps", "newFindings"],
3
+ "required": ["verdict"],
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
6
  "verdict": { "enum": ["APPROVED", "CHANGES_REQUESTED", "CLOSED"] },
@@ -42,7 +42,7 @@
42
42
  }
43
43
  }
44
44
  },
45
- "comment": { "type": "string", "minLength": 1 }
45
+ "comment": { "type": "string" }
46
46
  },
47
47
  "allOf": [
48
48
  {
@@ -50,7 +50,8 @@
50
50
  "then": {
51
51
  "properties": {
52
52
  "followUps": { "type": "array", "maxItems": 0 },
53
- "newFindings": { "type": "array", "maxItems": 0 }
53
+ "newFindings": { "type": "array", "maxItems": 0 },
54
+ "comment": { "const": "" }
54
55
  }
55
56
  }
56
57
  },
@@ -59,11 +60,16 @@
59
60
  "then": {
60
61
  "required": ["comment"],
61
62
  "anyOf": [
62
- { "properties": { "followUps": { "type": "array", "minItems": 1 } } },
63
63
  {
64
+ "required": ["followUps"],
65
+ "properties": { "followUps": { "type": "array", "minItems": 1 } }
66
+ },
67
+ {
68
+ "required": ["newFindings"],
64
69
  "properties": { "newFindings": { "type": "array", "minItems": 1 } }
65
70
  }
66
- ]
71
+ ],
72
+ "properties": { "comment": { "type": "string", "minLength": 1 } }
67
73
  }
68
74
  },
69
75
  {
@@ -72,7 +78,8 @@
72
78
  "required": ["comment"],
73
79
  "properties": {
74
80
  "followUps": { "type": "array", "maxItems": 0 },
75
- "newFindings": { "type": "array", "maxItems": 0 }
81
+ "newFindings": { "type": "array", "maxItems": 0 },
82
+ "comment": { "type": "string", "minLength": 1 }
76
83
  }
77
84
  }
78
85
  }
@@ -11,10 +11,11 @@ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
11
11
  Rules:
12
12
 
13
13
  - `"verdict"` must be `"APPROVED"`, `"CHANGES_REQUESTED"`, or `"CLOSED"`.
14
- - `"APPROVED"` requires an empty `"findings"` array.
14
+ - `"APPROVED"` may omit `"findings"`. If present, it must be empty.
15
+ - `"APPROVED"` may omit `"comment"`. If present, it must be an empty string.
15
16
  - `"CHANGES_REQUESTED"` requires `"comment"` and at least one `"finding"`.
16
17
  - `"comment"` for `"CHANGES_REQUESTED"` must be a concise prose review summary, not a bullet list of findings.
17
- - `"CLOSED"` requires `"comment"` and an empty `"findings"` array.
18
+ - `"CLOSED"` requires `"comment"` and may omit `"findings"`. If present, `"findings"` must be empty.
18
19
  - `"path"` must be repository-relative.
19
20
  - `"line"` is required and must target a valid right-side line inside the PR diff hunk.
20
21
  - `"startLine"` is optional and must also target a valid right-side line inside the same PR diff hunk range.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["verdict", "findings"],
3
+ "required": ["verdict"],
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
6
  "verdict": { "enum": ["APPROVED", "CHANGES_REQUESTED", "CLOSED"] },
@@ -18,27 +18,36 @@
18
18
  }
19
19
  }
20
20
  },
21
- "comment": { "type": "string", "minLength": 1 }
21
+ "comment": { "type": "string" }
22
22
  },
23
23
  "allOf": [
24
24
  {
25
25
  "if": { "properties": { "verdict": { "const": "APPROVED" } } },
26
26
  "then": {
27
- "properties": { "findings": { "type": "array", "maxItems": 0 } }
27
+ "properties": {
28
+ "findings": { "type": "array", "maxItems": 0 },
29
+ "comment": { "const": "" }
30
+ }
28
31
  }
29
32
  },
30
33
  {
31
34
  "if": { "properties": { "verdict": { "const": "CHANGES_REQUESTED" } } },
32
35
  "then": {
33
- "required": ["comment"],
34
- "properties": { "findings": { "type": "array", "minItems": 1 } }
36
+ "required": ["comment", "findings"],
37
+ "properties": {
38
+ "findings": { "type": "array", "minItems": 1 },
39
+ "comment": { "type": "string", "minLength": 1 }
40
+ }
35
41
  }
36
42
  },
37
43
  {
38
44
  "if": { "properties": { "verdict": { "const": "CLOSED" } } },
39
45
  "then": {
40
46
  "required": ["comment"],
41
- "properties": { "findings": { "type": "array", "maxItems": 0 } }
47
+ "properties": {
48
+ "findings": { "type": "array", "maxItems": 0 },
49
+ "comment": { "type": "string", "minLength": 1 }
50
+ }
42
51
  }
43
52
  }
44
53
  ]
@@ -22,6 +22,7 @@ Rules:
22
22
  - Use REPLIED when you only replied without code changes.
23
23
  - For EDITED, pullRequest.title and pullRequest.body must be non-empty and follow the repository's PR conventions.
24
24
  - Do not push or create the PR. The orchestrator pushes and creates the PR using pullRequest exactly as provided.
25
- - filesTouched must include every final changed file.
26
- - responses may be empty when no review threads were addressed.
27
- - REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
25
+ - filesTouched is required for EDITED and must include every final changed file.
26
+ - responses may be omitted when no review threads were addressed.
27
+ - REPLIED may omit filesTouched. If present, it must be empty.
28
+ - REPLIED requires at least one DISAGREE or ASK response.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["mode", "filesTouched", "responses"],
3
+ "required": ["mode"],
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
6
  "mode": { "enum": ["EDITED", "REPLIED"] },
@@ -37,13 +37,19 @@
37
37
  {
38
38
  "if": { "properties": { "mode": { "const": "EDITED" } } },
39
39
  "then": {
40
- "required": ["commitSha", "commitMessage", "pullRequest"],
40
+ "required": [
41
+ "commitSha",
42
+ "commitMessage",
43
+ "filesTouched",
44
+ "pullRequest"
45
+ ],
41
46
  "properties": { "filesTouched": { "type": "array", "minItems": 1 } }
42
47
  }
43
48
  },
44
49
  {
45
50
  "if": { "properties": { "mode": { "const": "REPLIED" } } },
46
51
  "then": {
52
+ "required": ["responses"],
47
53
  "not": {
48
54
  "anyOf": [
49
55
  { "required": ["commitSha"] },
@@ -16,4 +16,4 @@ The object must match this shape:
16
16
  Rules:
17
17
 
18
18
  - Return only configured signal IDs that apply.
19
- - Return an empty signals array when none apply.
19
+ - Omit `"signals"` when none apply.
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "type": "object",
3
- "required": ["signals"],
4
3
  "additionalProperties": false,
5
4
  "properties": {
6
5
  "signals": {
@@ -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
  });
@@ -30,17 +30,21 @@ export async function edit() {
30
30
  const parsed = prompt.parse(raw);
31
31
  if (!prompt.validate(parsed))
32
32
  throw new Error("Invalid output for editor.");
33
- if (parsed.mode === "EDITED") {
33
+ const output = {
34
+ ...parsed,
35
+ filesTouched: parsed.filesTouched ?? [],
36
+ };
37
+ if (output.mode === "EDITED") {
34
38
  const head = await this.exec(command("git", "rev-parse", "HEAD"), {
35
39
  cwd: this.state.worktree.path,
36
40
  signal: this.context.abort,
37
41
  });
38
- if (head !== parsed.commitSha)
39
- throw new Error(`Editor reported commit ${parsed.commitSha}, but worktree HEAD is ${head}.`);
42
+ if (head !== output.commitSha)
43
+ throw new Error(`Editor reported commit ${output.commitSha}, but worktree HEAD is ${head}.`);
40
44
  }
41
- return parsed;
45
+ return output;
42
46
  }, {
43
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to edit ${this.getLink()}. Retrying...`),
47
+ error: (_, count) => this.notify(`Attempt ${count} failed to edit ${this.getLink()}. Retrying...`),
44
48
  retries: this.config.output.repairAttempts,
45
49
  });
46
50
  if (!output)
@@ -49,7 +53,7 @@ export async function edit() {
49
53
  editor: { outputs: [...(this.state.editor.outputs ?? []), output] },
50
54
  text: `Finished editing ${this.getLink()}.`,
51
55
  });
52
- await this.magi.notify(this.state.sessionId, filterEmpty([
56
+ await this.notify(filterEmpty([
53
57
  `Editor ${output.mode === "EDITED" ? "edited" : "replied to"} ${this.getLink()}.`,
54
58
  output.commitSha
55
59
  ? `Commit: ${output.commitSha}${output.commitMessage ? ` ${output.commitMessage}` : ""}`
@@ -135,7 +139,7 @@ export async function resolveConflict() {
135
139
  responses: [],
136
140
  };
137
141
  }, {
138
- error: (_, count) => this.magi.notify(this.state.sessionId, `Attempt ${count} failed to resolve conflicts for ${this.getLink()}. Retrying...`),
142
+ error: (_, count) => this.notify(`Attempt ${count} failed to resolve conflicts for ${this.getLink()}. Retrying...`),
139
143
  retries: this.config.output.repairAttempts,
140
144
  });
141
145
  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-20260630052048",
3
+ "version": "0.0.0-dev-20260630070412",
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>",