opencode-magi 0.0.0-dev-20260702001022 → 0.0.0-dev-20260714065541

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.
@@ -11,9 +11,7 @@ export async function postReviews() {
11
11
  this.context.abort.throwIfAborted();
12
12
  if (this.state.dryRun)
13
13
  return;
14
- this.state = await this.magi.updateState(this.state.output, {
15
- text: `Posting reviews for ${this.getLink()}.`,
16
- });
14
+ await this.updateEvent(`Posting reviews.`);
17
15
  if (!this.config.review.reviewers?.length)
18
16
  throw new MagiError("blocked", "No reviewers configured.");
19
17
  if (!this.state.reviewers)
@@ -45,7 +43,7 @@ export async function postReviews() {
45
43
  .map((finding) => ({ ...finding, id }));
46
44
  });
47
45
  if (!findings.length) {
48
- await this.notify(`Finished posting reviews for ${this.getLink()}.`);
46
+ await this.updateEvent(`Finished posting reviews.`);
49
47
  return;
50
48
  }
51
49
  params.comments = findings.map(({ body, id, line, path, startLine }) => ({
@@ -63,7 +61,7 @@ export async function postReviews() {
63
61
  if (this.state.pr.verdict !== "APPROVED") {
64
62
  if (!this.state.operator?.sessionId)
65
63
  throw new MagiError("blocked", "Reporter session ID not found.");
66
- await this.notify(`Generating comment for ${this.getLink()} by operator.`);
64
+ await this.updateEvent(`Generating comment by operator.`);
67
65
  const contents = JSON.stringify(reviewers.flatMap(([, { outputs }]) => {
68
66
  const output = outputs?.at(-1);
69
67
  if (!output || output.verdict === "APPROVED")
@@ -89,12 +87,12 @@ export async function postReviews() {
89
87
  throw new Error("Invalid output for operator.");
90
88
  return parsed.comment;
91
89
  }, {
92
- error: (_, count) => this.notify(`Attempt ${count} failed to post comment for ${this.getLink()} by operator. Retrying...`),
90
+ error: (_, count) => this.updateEvent(`Attempt ${count} failed to post comment by operator. Retrying...`),
93
91
  retries: this.config.output.repairAttempts,
94
92
  });
95
93
  if (!output)
96
94
  throw new MagiError("blocked", "Invalid output for operator.");
97
- await this.notify(`Generated comment for ${this.getLink()} by operator.`);
95
+ await this.updateEvent(`Generated comment by operator.`);
98
96
  params.body = output;
99
97
  }
100
98
  params.body = [
@@ -116,10 +114,10 @@ export async function postReviews() {
116
114
  ].join("\n\n");
117
115
  const { data } = await octokit.rest.pulls.createReview(params);
118
116
  const posted = data.html_url;
119
- this.state = await this.magi.updateState(this.state.output, {
117
+ await this.updateState({
120
118
  reviewers: Object.fromEntries(Object.keys(this.state.reviewers).map((id) => [id, { posted }])),
121
- text: `Finished posting reviews for ${this.getLink()}.`,
122
119
  });
120
+ await this.updateEvent(`Finished posting reviews.`);
123
121
  }
124
122
  else {
125
123
  const worker = new Worker(this.config.review.concurrency.reviewers);
@@ -167,10 +165,8 @@ export async function postReviews() {
167
165
  const { data } = await octokit.rest.pulls.createReview(params);
168
166
  return [id, { posted: data.html_url }];
169
167
  }))));
170
- this.state = await this.magi.updateState(this.state.output, {
171
- reviewers,
172
- text: `Finished posting reviews for ${this.getLink()}.`,
173
- });
168
+ await this.updateState({ reviewers });
169
+ await this.updateEvent(`Finished posting reviews.`);
174
170
  }
175
171
  }
176
172
  export async function automate() {
@@ -184,10 +180,8 @@ export async function automate() {
184
180
  const automation = this.config[this.state.command].automation;
185
181
  const action = this.state.pr.verdict === "APPROVED" ? "merge" : "close";
186
182
  if (!automation[action]) {
187
- this.state = await this.magi.updateState(this.state.output, {
188
- pr: { automation: "SKIPPED" },
189
- text: `Skipped ${action} automation for ${this.getLink()}.`,
190
- });
183
+ await this.updateState({ pr: { automation: "SKIPPED" } });
184
+ await this.updateEvent(`Skipped ${action} automation.`);
191
185
  return "SKIPPED";
192
186
  }
193
187
  const account = this.state.operator.account;
@@ -197,25 +191,19 @@ export async function automate() {
197
191
  const failed = this.state.pr.checks.failed;
198
192
  const pending = this.state.pr.checks.pending;
199
193
  if (failed.length || pending.length) {
200
- this.state = await this.magi.updateState(this.state.output, {
201
- pr: { automation: "SKIPPED" },
202
- text: `Skipped merge automation for ${this.getLink()} because unresolved CI checks remain.`,
203
- });
194
+ await this.updateState({ pr: { automation: "SKIPPED" } });
195
+ await this.updateEvent(`Skipped merge automation because unresolved CI checks remain.`);
204
196
  return "SKIPPED";
205
197
  }
206
198
  if (await isConflict.call(this)) {
207
- this.state = await this.magi.updateState(this.state.output, {
208
- pr: { automation: "CONFLICT" },
209
- text: `Merge automation found conflicts for ${this.getLink()}.`,
210
- });
199
+ await this.updateState({ pr: { automation: "CONFLICT" } });
200
+ await this.updateEvent(`Merge automation found conflicts.`);
211
201
  return "CONFLICT";
212
202
  }
213
203
  }
214
204
  if (this.state.dryRun) {
215
- this.state = await this.magi.updateState(this.state.output, {
216
- pr: { automation: "SKIPPED" },
217
- text: `Skipped ${action} automation for ${this.getLink()} during dry run.`,
218
- });
205
+ await this.updateState({ pr: { automation: "SKIPPED" } });
206
+ await this.updateEvent(`Skipped ${action} automation during dry run.`);
219
207
  return "SKIPPED";
220
208
  }
221
209
  const token = await this.magi.getGhToken(account);
@@ -234,20 +222,14 @@ export async function automate() {
234
222
  }
235
223
  }
236
224
  if (action !== "merge" || this.config.review.merge.queue)
237
- this.state = await this.magi.updateState(this.state.output, {
238
- text: `${action === "merge" ? "Merging" : "Closing"} ${this.getLink()}.`,
239
- });
225
+ await this.updateEvent(`${action === "merge" ? "Merging" : "Closing"}.`);
240
226
  else
241
- this.state = await this.magi.updateState(this.state.output, {
242
- text: `Waiting to merge ${this.getLink()}.`,
243
- });
227
+ await this.updateEvent(`Waiting to merge.`);
244
228
  if (action === "merge" && this.config.review.merge.queue) {
245
229
  const octokit = await this.magi.createOctokit(this.config, this.context.abort, account);
246
230
  const graphql = this.magi.createGraphql(octokit);
247
231
  await graphql.enqueuePullRequest({ id: this.state.pr.metadata.node_id });
248
- this.state = await this.magi.updateState(this.state.output, {
249
- text: `Waiting for merge queue for ${this.getLink()}.`,
250
- });
232
+ await this.updateEvent(`Waiting for merge queue.`);
251
233
  const result = await waitMergeQueue.call(this);
252
234
  if (result === "CONFLICT")
253
235
  return result;
@@ -279,31 +261,25 @@ export async function automate() {
279
261
  if (status === "DIRTY")
280
262
  return "CONFLICT";
281
263
  if (status === "BEHIND") {
282
- this.state = await this.magi.updateState(this.state.output, {
283
- text: `Updating ${this.getLink()} with the base branch before merging.`,
284
- });
264
+ await this.updateEvent(`Updating with the base branch before merging.`);
285
265
  await this.exec(command(...prefix, "update-branch", ...suffix), options);
286
266
  return;
287
267
  }
288
268
  if (status === "BLOCKED" && hasFailedChecks(checks))
289
- throw new MagiError("blocked", `Required checks failed before merging ${this.getLink()}.`);
269
+ throw new MagiError("blocked", `Required checks failed before merging.`);
290
270
  if (!auto && state === "OPEN")
291
- throw new MagiError("blocked", `Auto-merge is no longer enabled for ${this.getLink()}.`);
271
+ throw new MagiError("blocked", `Auto-merge is no longer enabled.`);
292
272
  }, 30_000);
293
273
  };
294
274
  const runAutomation = async () => {
295
275
  try {
296
- this.state = await this.magi.updateState(this.state.output, {
297
- text: `Merging ${this.getLink()}.`,
298
- });
276
+ await this.updateEvent(`Merging.`);
299
277
  await this.exec(command(...args), options);
300
278
  if (action === "merge" && this.config.review.merge.auto) {
301
279
  const result = await waitMerge();
302
280
  if (result === "CONFLICT") {
303
- this.state = await this.magi.updateState(this.state.output, {
304
- pr: { automation: "CONFLICT" },
305
- text: `Merge automation found conflicts for ${this.getLink()}.`,
306
- });
281
+ await this.updateState({ pr: { automation: "CONFLICT" } });
282
+ await this.updateEvent(`Merge automation found conflicts.`);
307
283
  return "CONFLICT";
308
284
  }
309
285
  }
@@ -314,24 +290,18 @@ export async function automate() {
314
290
  throw e;
315
291
  switch (true) {
316
292
  case /\bconflicts?\b|merge commit cannot be cleanly created/i.test(message):
317
- this.state = await this.magi.updateState(this.state.output, {
318
- pr: { automation: "CONFLICT" },
319
- text: `Merge automation found conflicts for ${this.getLink()}.`,
320
- });
293
+ await this.updateState({ pr: { automation: "CONFLICT" } });
294
+ await this.updateEvent(`Merge automation found conflicts.`);
321
295
  return "CONFLICT";
322
296
  case /head (branch|ref) is (not up to date|out of date)/i.test(message):
323
- this.state = await this.magi.updateState(this.state.output, {
324
- text: `Updating ${this.getLink()} with the base branch before merging.`,
325
- });
297
+ await this.updateEvent(`Updating with the base branch before merging.`);
326
298
  await this.exec(command(...prefix, "update-branch", ...suffix), options);
327
299
  await this.exec(command(...args), options);
328
300
  if (this.config.review.merge.auto) {
329
301
  const result = await waitMerge();
330
302
  if (result === "CONFLICT") {
331
- this.state = await this.magi.updateState(this.state.output, {
332
- pr: { automation: "CONFLICT" },
333
- text: `Merge automation found conflicts for ${this.getLink()}.`,
334
- });
303
+ await this.updateState({ pr: { automation: "CONFLICT" } });
304
+ await this.updateEvent(`Merge automation found conflicts.`);
335
305
  return "CONFLICT";
336
306
  }
337
307
  }
@@ -347,10 +317,10 @@ export async function automate() {
347
317
  if (result)
348
318
  return result;
349
319
  }
350
- this.state = await this.magi.updateState(this.state.output, {
320
+ await this.updateState({
351
321
  pr: { automation: action === "merge" ? "MERGED" : "CLOSED" },
352
- text: `Finished ${action} automation for ${this.getLink()}.`,
353
322
  });
323
+ await this.updateEvent(`Finished ${action} automation.`);
354
324
  return action === "merge" ? "MERGED" : "CLOSED";
355
325
  }
356
326
  function hasFailedChecks(checks) {
@@ -399,13 +369,11 @@ async function waitMergeQueue(leftMergeQueue = false) {
399
369
  const nextLeftMergeQueue = state === "OPEN" && !isInMergeQueue && !mergeQueueEntry;
400
370
  if (leftMergeQueue && nextLeftMergeQueue) {
401
371
  if (await isConflict.call(this)) {
402
- this.state = await this.magi.updateState(this.state.output, {
403
- pr: { automation: "CONFLICT" },
404
- text: `Merge automation found conflicts for ${this.getLink()}.`,
405
- });
372
+ await this.updateState({ pr: { automation: "CONFLICT" } });
373
+ await this.updateEvent(`Merge automation found conflicts.`);
406
374
  return "CONFLICT";
407
375
  }
408
- throw new MagiError("blocked", `PR left the merge queue before merging ${this.getLink()}.`);
376
+ throw new MagiError("blocked", `PR left the merge queue before merging.`);
409
377
  }
410
378
  await wait(30_000);
411
379
  return await waitMergeQueue.call(this, nextLeftMergeQueue);
@@ -24,10 +24,8 @@ const ci = {
24
24
  };
25
25
  export async function checkPr() {
26
26
  this.context.abort.throwIfAborted();
27
- await this.magi.updateState(this.state.output, {
28
- status: "running",
29
- text: `Checking PR ${this.getLink()}.`,
30
- });
27
+ await this.updateState({ status: "running" });
28
+ await this.updateEvent(`Checking PR.`);
31
29
  const { files, metadata } = await getMetadata.call(this);
32
30
  if (metadata.state !== "open")
33
31
  throw new MagiError("blocked", `PR is not open.`);
@@ -57,40 +55,32 @@ export async function checkPr() {
57
55
  throw new MagiError("blocked", `PR is safety blocked. ${errors.join(" ")}`);
58
56
  if (this.config.mode === "single") {
59
57
  if (this.config.account === metadata.user.login)
60
- throw new MagiError("blocked", `Single mode account ${this.config.account} cannot review ${this.getLink()} because it opened the pull request. Configure account to a different account.`);
58
+ throw new MagiError("blocked", `Single mode account ${this.config.account} cannot review because it opened the pull request. Configure account to a different account.`);
61
59
  }
62
60
  else {
63
61
  const accounts = Object.values(this.state.reviewers ?? {})
64
62
  .filter(({ account }) => account === metadata.user.login)
65
63
  .map(({ account }) => account);
66
64
  if (accounts.length)
67
- throw new MagiError("blocked", `Multi mode accounts ${accounts.join(", ")} cannot review ${this.getLink()} because they opened the pull request. Configure accounts to different accounts.`);
65
+ throw new MagiError("blocked", `Multi mode accounts ${accounts.join(", ")} cannot review because they opened the pull request. Configure accounts to different accounts.`);
68
66
  }
69
- this.state = await this.magi.updateState(this.state.output, {
70
- pr: { files, metadata },
71
- text: `Finished checking PR ${this.getLink()}.`,
72
- });
67
+ await this.updateState({ pr: { files, metadata } });
68
+ await this.updateEvent(`Finished checking PR.`);
73
69
  }
74
70
  export async function checkCi(wait = this.config.review.checks.wait) {
75
71
  this.context.abort.throwIfAborted();
76
- this.state = await this.magi.updateState(this.state.output, {
77
- text: `Checking CI for ${this.getLink()}.`,
78
- });
72
+ await this.updateEvent(`Checking CI.`);
79
73
  if (wait)
80
74
  await watchChecks.call(this);
81
75
  const checks = await getChecks.call(this);
82
- this.state = await this.magi.updateState(this.state.output, {
83
- pr: { checks },
84
- text: `Finished checking CI for ${this.getLink()}.`,
85
- });
76
+ await this.updateState({ pr: { checks } });
77
+ await this.updateEvent(`Finished checking CI.`);
86
78
  }
87
79
  export async function classifyChecks() {
88
80
  this.context.abort.throwIfAborted();
89
81
  if (!this.state.pr?.checks?.failed.length)
90
82
  return;
91
- await this.magi.updateState(this.state.output, {
92
- text: `Classifying CI checks for ${this.getLink()}.`,
93
- });
83
+ await this.updateEvent(`Classifying CI checks.`);
94
84
  if (!this.state.pr.metadata)
95
85
  throw new MagiError("blocked", "PR metadata not found.");
96
86
  if (!this.state.worktree)
@@ -115,7 +105,7 @@ export async function classifyChecks() {
115
105
  await Promise.all(Object.entries(this.state.reviewers ?? {}).map(([id, { sessionId }]) => worker.run(async () => {
116
106
  if (!sessionId)
117
107
  throw new Error(`No session ID found for reviewer ${id}.`);
118
- await this.notify(`Classifying CI checks for ${this.getLink()} with reviewer ${id}.`);
108
+ await this.updateEvent(`Classifying CI checks with reviewer ${id}.`);
119
109
  const output = await retry(async (count) => {
120
110
  const raw = await this.magi.promptSession(sessionId, count === 1 ? taskMessage : repairMessage);
121
111
  await this.createAgentFile("ci-classification", id, raw, count);
@@ -124,7 +114,7 @@ export async function classifyChecks() {
124
114
  throw new Error(`Invalid output for reviewer ${id}.`);
125
115
  return parsed;
126
116
  }, {
127
- error: (_, count) => this.notify(`Attempt ${count} failed to classify CI checks for ${this.getLink()} with reviewer ${id}. Retrying...`),
117
+ error: (_, count) => this.updateEvent(`Attempt ${count} failed to classify CI checks with reviewer ${id}. Retrying...`),
128
118
  retries: this.config.output.repairAttempts,
129
119
  });
130
120
  if (!output)
@@ -152,10 +142,10 @@ export async function classifyChecks() {
152
142
  .filter(([, { scope }]) => !scope)
153
143
  .map(([id, { comment }]) => `- ${id}: ${comment}`),
154
144
  };
155
- await this.notify(filterEmpty([
145
+ await this.updateEvent(filterEmpty([
156
146
  scope
157
- ? `Check ${name} for ${this.getLink()} was classified as in scope by majority vote.`
158
- : `Check ${name} for ${this.getLink()} was classified as out of scope by majority vote. Rerunning it.`,
147
+ ? `Check ${name} was classified as in scope by majority vote.`
148
+ : `Check ${name} was classified as out of scope by majority vote. Rerunning it.`,
159
149
  reasons.in.length
160
150
  ? `In scope reasons:\n${reasons.in.join("\n")}`
161
151
  : undefined,
@@ -164,10 +154,8 @@ export async function classifyChecks() {
164
154
  : undefined,
165
155
  ]).join("\n\n"));
166
156
  }));
167
- this.state = await this.magi.updateState(this.state.output, {
168
- pr: { checks: { failed } },
169
- text: `Finished classifying CI checks for ${this.getLink()}.`,
170
- });
157
+ await this.updateState({ pr: { checks: { failed } } });
158
+ await this.updateEvent(`Finished classifying CI checks.`);
171
159
  }
172
160
  export async function rerunChecks() {
173
161
  this.context.abort.throwIfAborted();
@@ -180,9 +168,7 @@ export async function rerunChecks() {
180
168
  const failedChecks = checks.failed.filter(({ scope }) => !scope);
181
169
  if (!failedChecks.length)
182
170
  return;
183
- await this.magi.updateState(this.state.output, {
184
- text: `Rerunning CI checks for ${this.getLink()}.`,
185
- });
171
+ await this.updateEvent(`Rerunning CI checks.`);
186
172
  let label = failedChecks.map(({ name }) => name).join(", ");
187
173
  if (this.state.dryRun) {
188
174
  checks.passed = [
@@ -193,7 +179,7 @@ export async function rerunChecks() {
193
179
  }
194
180
  else {
195
181
  await retry(async () => {
196
- await this.notify(`Rerunning checks ${label} for ${this.getLink()}.`);
182
+ await this.updateEvent(`Rerunning checks ${label}.`);
197
183
  await Promise.all(checks.failed
198
184
  .filter(({ scope }) => !scope)
199
185
  .map(async ({ id }) => rerunCheck.call(this, id)));
@@ -211,9 +197,9 @@ export async function rerunChecks() {
211
197
  if (!failedChecks.length)
212
198
  return;
213
199
  label = failedChecks.map(({ name }) => name).join(", ");
214
- throw new Error(`Checks ${label} for ${this.getLink()} still failed.`);
200
+ throw new Error(`Checks ${label} still failed.`);
215
201
  }, {
216
- error: (_, count) => this.notify(`Attempt ${count} failed to rerun checks ${label} for ${this.getLink()}. Retrying...`),
202
+ error: (_, count) => this.updateEvent(`Attempt ${count} failed to rerun checks ${label}. Retrying...`),
217
203
  retries: this.config.review.checks.retryFailedJobs,
218
204
  });
219
205
  }
@@ -221,18 +207,16 @@ export async function rerunChecks() {
221
207
  const failedChecksAfterRerun = checks.failed.filter(({ scope }) => !scope);
222
208
  const message = filterEmpty([
223
209
  passedChecksAfterRerun.length
224
- ? `Reran checks ${passedChecksAfterRerun.map(({ name }) => name).join(", ")} for ${this.getLink()} passed.`
210
+ ? `Reran checks ${passedChecksAfterRerun.map(({ name }) => name).join(", ")} passed.`
225
211
  : undefined,
226
212
  failedChecksAfterRerun.length
227
- ? `Reran checks ${failedChecksAfterRerun.map(({ name }) => name).join(", ")} for ${this.getLink()} failed.`
213
+ ? `Reran checks ${failedChecksAfterRerun.map(({ name }) => name).join(", ")} failed.`
228
214
  : undefined,
229
215
  ]).join("\n");
230
216
  if (message)
231
- await this.notify(message);
232
- this.state = await this.magi.updateState(this.state.output, {
233
- pr: { checks },
234
- text: `Finished rerunning checks for ${this.getLink()}.`,
235
- });
217
+ await this.updateEvent(message);
218
+ await this.updateState({ pr: { checks } });
219
+ await this.updateEvent(`Finished rerunning checks.`);
236
220
  }
237
221
  export async function getMetadata() {
238
222
  const [{ data }, files] = await Promise.all([
@@ -2,9 +2,7 @@ import { MagiError } from "@/magi";
2
2
  import { command, filterEmpty, marker, quote } from "@/utils";
3
3
  export async function checkExistingReviews() {
4
4
  this.context.abort.throwIfAborted();
5
- this.state = await this.magi.updateState(this.state.output, {
6
- text: `Fetching existing reviews for ${this.getLink()}.`,
7
- });
5
+ await this.updateEvent(`Fetching existing reviews.`);
8
6
  if (!this.state.pr?.metadata)
9
7
  throw new MagiError("blocked", "PR metadata not found.");
10
8
  if (!this.config.review.reviewers?.length)
@@ -14,9 +12,7 @@ export async function checkExistingReviews() {
14
12
  getCommits.call(this),
15
13
  getReviewThreads.call(this),
16
14
  ]);
17
- this.state = await this.magi.updateState(this.state.output, {
18
- pr: { commits, reviews, threads },
19
- });
15
+ await this.updateState({ pr: { commits, reviews, threads } });
20
16
  const latestNonMergeCommit = commits
21
17
  .toReversed()
22
18
  .find(({ parents }) => parents.length < 2);
@@ -102,27 +98,23 @@ export async function checkExistingReviews() {
102
98
  }
103
99
  }
104
100
  const skip = Object.values(reviewers).every(({ status }) => status === "skip");
105
- this.state = await this.magi.updateState(this.state.output, {
106
- reviewers,
107
- text: `Finished fetching existing reviews for ${this.getLink()}.`,
108
- });
101
+ await this.updateState({ reviewers });
102
+ await this.updateEvent(`Finished fetching existing reviews.`);
109
103
  return skip;
110
104
  }
111
105
  export async function fetchReviewContext() {
112
106
  this.context.abort.throwIfAborted();
113
- this.state = await this.magi.updateState(this.state.output, {
114
- text: `Fetching review context for ${this.getLink()}.`,
115
- });
107
+ await this.updateEvent(`Fetching review context.`);
116
108
  const [comments, issues, threads] = await Promise.all([
117
109
  getComments.call(this),
118
110
  getClosingIssues.call(this),
119
111
  getReviewThreads.call(this),
120
112
  ]);
121
113
  const inlineCommentTargets = await getInlineCommentTargets.call(this);
122
- this.state = await this.magi.updateState(this.state.output, {
114
+ await this.updateState({
123
115
  pr: { comments, inlineCommentTargets, issues, threads },
124
- text: `Finished fetching review context for ${this.getLink()}.`,
125
116
  });
117
+ await this.updateEvent(`Finished fetching review context.`);
126
118
  }
127
119
  export async function getComments() {
128
120
  return await this.octokit.paginate(this.octokit.rest.issues.listComments, {
@@ -1,7 +1,7 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
- import { filterEmpty, parsePrs, split, Worker } from "@/utils";
2
+ import { parsePrs, split, Worker } from "@/utils";
3
3
  import { Review } from "./review";
4
- function overrideConfig(config, args, dryRun = false, sync = false) {
4
+ function overrideConfig(config, args, dryRun = false) {
5
5
  for (let index = 0; index < args.length; index++) {
6
6
  const arg = args[index];
7
7
  const value = args[index + 1];
@@ -11,9 +11,6 @@ function overrideConfig(config, args, dryRun = false, sync = false) {
11
11
  case "--dry-run":
12
12
  dryRun = true;
13
13
  break;
14
- case "--sync":
15
- sync = true;
16
- break;
17
14
  case "--retry-api-attempts":
18
15
  config.github.retryApiAttempts = parseInt(value);
19
16
  break;
@@ -49,7 +46,7 @@ function overrideConfig(config, args, dryRun = false, sync = false) {
49
46
  break;
50
47
  }
51
48
  }
52
- return { config, dryRun, sync };
49
+ return { config, dryRun };
53
50
  }
54
51
  export const review = function (magi) {
55
52
  return {
@@ -57,23 +54,18 @@ export const review = function (magi) {
57
54
  args: {
58
55
  dryRun: tool.schema.boolean().optional(),
59
56
  prs: tool.schema.string(),
60
- sync: tool.schema.boolean().optional(),
61
57
  },
62
- description: "Start background Magi review runs for one or more pull requests and post the reviews. After starting, monitor progress yourself when useful; do not tell users to call follow-up tools by name.",
58
+ description: "Review one or more pull requests and post the reviews.",
63
59
  async execute(args, context) {
64
60
  const prs = parsePrs(args.prs);
65
- const { config, dryRun, sync } = overrideConfig(await magi.getConfig({ reviewers: true }), split(args.prs), args.dryRun, args.sync);
61
+ const { config, dryRun } = overrideConfig(await magi.getConfig({ reviewers: true }), split(args.prs), args.dryRun);
66
62
  const worker = new Worker(config.review.concurrency.runs);
67
- const states = [];
68
- const tasks = [];
69
- for (const pr of prs) {
63
+ const reports = await Promise.all(prs.map(async (pr) => {
70
64
  context.abort.throwIfAborted();
71
- const run = await Review.init(pr, magi, config, context, {
72
- dryRun,
73
- sync,
74
- });
75
- states.push(run.state);
76
- const task = worker.run(async () => {
65
+ return worker.run(async () => {
66
+ const run = await Review.init(pr, magi, config, context, {
67
+ dryRun,
68
+ });
77
69
  try {
78
70
  await run.checkPr();
79
71
  const skip = await run.checkExistingReviews();
@@ -101,20 +93,8 @@ export const review = function (magi) {
101
93
  await run.cleanup();
102
94
  }
103
95
  });
104
- tasks.push(task);
105
- if (!sync)
106
- void task;
107
- }
108
- if (sync) {
109
- const results = await Promise.all(tasks);
110
- const output = filterEmpty(results.map(({ text }) => text)).join("\n\n");
111
- if (results.some(({ status }) => status === "failed" || status === "cancelled"))
112
- throw new Error(output);
113
- return output;
114
- }
115
- else {
116
- return filterEmpty(states.map(({ text }) => text)).join("\n");
117
- }
96
+ }));
97
+ return reports.join("\n\n");
118
98
  },
119
99
  }),
120
100
  };
@@ -5,13 +5,13 @@ import { filterEmpty, toTitleCase } from "@/utils";
5
5
  export async function createReport(e) {
6
6
  if (!e) {
7
7
  const status = "completed";
8
- const text = createContent.call(this, { status });
8
+ const text = await createContent.call(this, { status });
9
9
  await writeFile(join(this.state.output, "report.md"), `${text}\n`);
10
- return this.magi.updateState(this.state.output, {
10
+ await this.updateState({
11
11
  completedAt: new Date().toISOString(),
12
12
  status,
13
- text,
14
13
  });
14
+ return text;
15
15
  }
16
16
  else {
17
17
  const error = e instanceof Error ? e.message : "Unknown error";
@@ -20,26 +20,27 @@ export async function createReport(e) {
20
20
  : this.context.abort.aborted
21
21
  ? "cancelled"
22
22
  : "failed";
23
- const text = createContent.call(this, { error, status });
23
+ const text = await createContent.call(this, { error, status });
24
24
  await writeFile(join(this.state.output, "report.md"), `${text}\n`);
25
- return this.magi.updateState(this.state.output, {
25
+ await this.updateState({
26
26
  completedAt: new Date().toISOString(),
27
- error,
28
27
  status,
29
- text: `${toTitleCase(status)} reviewing ${this.getLink()}.\n\n${text}`,
30
28
  });
29
+ return text;
31
30
  }
32
31
  }
33
- function createContent(input) {
32
+ async function createContent(input) {
34
33
  return filterEmpty([
35
- ...createMetaContent.call(this, input),
34
+ ...(await createMetaContent.call(this, input)),
36
35
  ...createCheckContent.call(this),
37
36
  ...createReviewerContent.call(this),
38
37
  ]).join("\n");
39
38
  }
40
- export function createMetaContent(input) {
39
+ export async function createMetaContent(input) {
40
+ const events = await this.getEvents();
41
+ const lastAction = events.at(-1)?.message;
41
42
  const rows = [
42
- `- **Pull Request:** ${this.getLink()}`,
43
+ `- **Pull Request:** [${this.state.pr.number}](${this.state.pr.url})`,
43
44
  `- **Mode**: ${toTitleCase(this.config.mode)}`,
44
45
  `- **Dry run**: ${this.state.dryRun ? "Yes" : "No"}`,
45
46
  `- **Status**: ${toTitleCase(input.status)}`,
@@ -47,8 +48,8 @@ export function createMetaContent(input) {
47
48
  ];
48
49
  if (this.state.pr?.verdict)
49
50
  rows.push(`- **Verdict**: ${toTitleCase(this.state.pr.verdict.toLocaleLowerCase())}`);
50
- if (this.state.text)
51
- rows.push(`- **Last action**: ${this.state.text}`);
51
+ if (lastAction)
52
+ rows.push(`- **Last action**: ${lastAction}`);
52
53
  if (input.error)
53
54
  rows.push(`- **Error**: ${input.error}`);
54
55
  return rows;