opencode-magi 0.9.0 → 0.11.0

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.
Files changed (126) hide show
  1. package/README.ja.md +254 -0
  2. package/README.md +37 -34
  3. package/dist/commands.js +11 -9
  4. package/dist/config/index.js +2 -0
  5. package/dist/config/resolve.js +120 -248
  6. package/dist/config/validate.js +103 -1100
  7. package/dist/constant.js +135 -0
  8. package/dist/graphql/index.generated.js +125 -0
  9. package/dist/graphql/index.js +49 -0
  10. package/dist/index.js +12 -800
  11. package/dist/magi.js +357 -0
  12. package/dist/permissions/common.json +22 -22
  13. package/dist/permissions/editor.json +12 -12
  14. package/dist/prompts/index.js +98 -0
  15. package/dist/prompts/merge/ci-classification/output-contract.md +22 -0
  16. package/dist/prompts/merge/ci-classification/task.md +10 -0
  17. package/dist/prompts/merge/ci-classification/validate.json +20 -0
  18. package/dist/prompts/merge/conflict/output-contract.md +12 -0
  19. package/dist/prompts/merge/conflict/task.md +9 -0
  20. package/dist/prompts/merge/conflict/validate.json +4 -0
  21. package/dist/prompts/merge/edit/output-contract.md +26 -0
  22. package/dist/prompts/merge/edit/task.md +10 -0
  23. package/dist/prompts/merge/edit/validate.json +60 -0
  24. package/dist/prompts/review/ci-classification/output-contract.md +21 -0
  25. package/dist/prompts/review/ci-classification/task.md +7 -0
  26. package/dist/prompts/review/ci-classification/validate.json +20 -0
  27. package/dist/prompts/review/close-reconsideration/output-contract.md +21 -0
  28. package/dist/prompts/review/close-reconsideration/task.md +6 -0
  29. package/dist/prompts/review/close-reconsideration/validate.json +44 -0
  30. package/dist/prompts/review/comment/output-contract.md +11 -0
  31. package/dist/prompts/review/comment/task.md +3 -0
  32. package/dist/prompts/review/comment/validate.json +8 -0
  33. package/dist/prompts/review/finding-validation/output-contract.md +25 -0
  34. package/dist/prompts/{templates/review/finding-validation.md → review/finding-validation/task.md} +2 -3
  35. package/dist/prompts/review/finding-validation/validate.json +21 -0
  36. package/dist/prompts/review/rereview/output-contract.md +30 -0
  37. package/dist/prompts/{templates/review/rereview.md → review/rereview/task.md} +8 -11
  38. package/dist/prompts/review/rereview/validate.json +87 -0
  39. package/dist/prompts/review/review/output-contract.md +25 -0
  40. package/dist/prompts/{templates/review/review.md → review/review/task.md} +2 -5
  41. package/dist/prompts/review/review/validate.json +54 -0
  42. package/dist/prompts/triage/acceptance/output-contract.md +16 -0
  43. package/dist/prompts/triage/acceptance/validate.json +23 -0
  44. package/dist/prompts/triage/category/output-contract.md +16 -0
  45. package/dist/prompts/triage/category/validate.json +23 -0
  46. package/dist/prompts/triage/comment-classification/output-contract.md +15 -0
  47. package/dist/prompts/triage/comment-classification/validate.json +28 -0
  48. package/dist/prompts/triage/create/output-contract.md +28 -0
  49. package/dist/prompts/triage/create/validate.json +73 -0
  50. package/dist/prompts/triage/deplicate/output-contract.md +16 -0
  51. package/dist/prompts/triage/deplicate/validate.json +20 -0
  52. package/dist/prompts/triage/existing/output-contract.md +16 -0
  53. package/dist/prompts/triage/existing/validate.json +13 -0
  54. package/dist/prompts/triage/reconsider/output-contract.md +16 -0
  55. package/dist/prompts/triage/reconsider/validate.json +23 -0
  56. package/dist/prompts/triage/signal/output-contract.md +19 -0
  57. package/dist/prompts/triage/signal/validate.json +18 -0
  58. package/dist/tools/clear/index.js +21 -0
  59. package/dist/tools/index.js +12 -0
  60. package/dist/tools/merge/action.js +47 -0
  61. package/dist/tools/merge/context.js +118 -0
  62. package/dist/tools/merge/editor.js +264 -0
  63. package/dist/tools/merge/index.js +151 -0
  64. package/dist/tools/merge/index.type.js +1 -0
  65. package/dist/tools/merge/merge.js +42 -0
  66. package/dist/tools/merge/report.js +73 -0
  67. package/dist/tools/review/action.js +429 -0
  68. package/dist/tools/review/check.js +295 -0
  69. package/dist/tools/review/context.js +258 -0
  70. package/dist/tools/review/index.js +104 -0
  71. package/dist/tools/review/index.type.js +1 -0
  72. package/dist/tools/review/report.js +143 -0
  73. package/dist/tools/review/review.js +145 -0
  74. package/dist/tools/review/reviewer.js +409 -0
  75. package/dist/tools/triage/index.js +16 -0
  76. package/dist/tools/validate/index.js +28 -0
  77. package/dist/utils/array.js +6 -0
  78. package/dist/utils/assertion.js +38 -0
  79. package/dist/utils/exec.js +14 -0
  80. package/dist/utils/fs.js +24 -0
  81. package/dist/utils/function.js +37 -0
  82. package/dist/utils/github.js +63 -0
  83. package/dist/utils/index.js +10 -0
  84. package/dist/utils/index.type.js +1 -0
  85. package/dist/utils/object.js +21 -0
  86. package/dist/utils/opencode.js +25 -0
  87. package/dist/utils/string.js +38 -0
  88. package/dist/utils/worker.js +30 -0
  89. package/package.json +20 -8
  90. package/schema.json +9 -7
  91. package/dist/config/load.js +0 -62
  92. package/dist/config/output.js +0 -25
  93. package/dist/config/worktree.js +0 -25
  94. package/dist/github/commands.js +0 -905
  95. package/dist/github/retry.js +0 -44
  96. package/dist/orchestrator/abort.js +0 -9
  97. package/dist/orchestrator/ci.js +0 -579
  98. package/dist/orchestrator/findings.js +0 -88
  99. package/dist/orchestrator/inline-comments.js +0 -73
  100. package/dist/orchestrator/majority.js +0 -62
  101. package/dist/orchestrator/merge.js +0 -1260
  102. package/dist/orchestrator/model.js +0 -216
  103. package/dist/orchestrator/pool.js +0 -15
  104. package/dist/orchestrator/report.js +0 -175
  105. package/dist/orchestrator/review-context.js +0 -342
  106. package/dist/orchestrator/review.js +0 -1478
  107. package/dist/orchestrator/run-manager.js +0 -2132
  108. package/dist/orchestrator/safety.js +0 -44
  109. package/dist/orchestrator/triage.js +0 -1370
  110. package/dist/prompts/compose.js +0 -473
  111. package/dist/prompts/contracts.js +0 -300
  112. package/dist/prompts/output.js +0 -401
  113. package/dist/prompts/templates/merge/ci-classification.md +0 -16
  114. package/dist/prompts/templates/merge/conflict.md +0 -10
  115. package/dist/prompts/templates/merge/edit.md +0 -15
  116. package/dist/prompts/templates/review/ci-classification.md +0 -9
  117. package/dist/prompts/templates/review/close-reconsideration.md +0 -6
  118. /package/dist/{types.js → config/index.type.js} +0 -0
  119. /package/dist/prompts/{templates/triage/acceptance.md → triage/acceptance/task.md} +0 -0
  120. /package/dist/prompts/{templates/triage/category.md → triage/category/task.md} +0 -0
  121. /package/dist/prompts/{templates/triage/comment-classification.md → triage/comment-classification/task.md} +0 -0
  122. /package/dist/prompts/{templates/triage/create.md → triage/create/task.md} +0 -0
  123. /package/dist/prompts/{templates/triage/duplicate.md → triage/deplicate/task.md} +0 -0
  124. /package/dist/prompts/{templates/triage/existing-pr.md → triage/existing/task.md} +0 -0
  125. /package/dist/prompts/{templates/triage/reconsider.md → triage/reconsider/task.md} +0 -0
  126. /package/dist/prompts/{templates/triage/signal.md → triage/signal/task.md} +0 -0
@@ -0,0 +1,429 @@
1
+ import { MagiError } from "@/magi";
2
+ import { Prompt } from "@/prompts";
3
+ import { command, filterEmpty, isArray, isObject, isString, loop, marker, omitNullish, quote, retry, wait, Worker, } from "@/utils";
4
+ const events = {
5
+ APPROVED: "APPROVE",
6
+ CHANGES_REQUESTED: "REQUEST_CHANGES",
7
+ CLOSED: "COMMENT",
8
+ };
9
+ const mergeWorker = new Worker(1);
10
+ export async function postReviews() {
11
+ this.context.abort.throwIfAborted();
12
+ if (this.state.dryRun)
13
+ return;
14
+ await this.updateEvent(`Posting reviews.`);
15
+ if (!this.config.review.reviewers?.length)
16
+ throw new MagiError("blocked", "No reviewers configured.");
17
+ if (!this.state.reviewers)
18
+ throw new MagiError("blocked", "Reviewers not found.");
19
+ const args = {
20
+ owner: this.config.github.owner,
21
+ pull_number: this.number,
22
+ repo: this.config.github.repo,
23
+ };
24
+ if (this.config.mode === "single") {
25
+ if (!this.state.pr?.verdict)
26
+ throw new MagiError("blocked", "PR verdict not found.");
27
+ const octokit = await this.magi.createOctokit(this.config, this.context.abort, this.config.account);
28
+ const graphql = this.magi.createGraphql(octokit);
29
+ const reviewers = Object.entries(this.state.reviewers).filter(([, { status }]) => status !== "skip");
30
+ await Promise.all(reviewers.flatMap(([, { outputs }]) => (outputs?.at(-1)?.resolves ?? []).map(({ threadId }) => graphql.resolveReviewThread({ threadId }))));
31
+ await Promise.all(reviewers.flatMap(([, { outputs }]) => (outputs?.at(-1)?.followUps ?? []).map(({ body, commentId }) => octokit.rest.pulls.createReplyForReviewComment({
32
+ ...args,
33
+ body,
34
+ comment_id: commentId,
35
+ }))));
36
+ const event = events[this.state.pr.verdict];
37
+ const params = { ...args, event };
38
+ if (this.state.pr.verdict === "CHANGES_REQUESTED") {
39
+ const findings = reviewers.flatMap(([id, { outputs }]) => {
40
+ const output = outputs?.at(-1);
41
+ return (output?.findings ?? output?.newFindings ?? [])
42
+ .filter(({ state }) => state === "accepted")
43
+ .map((finding) => ({ ...finding, id }));
44
+ });
45
+ if (!findings.length) {
46
+ await this.updateEvent(`Finished posting reviews.`);
47
+ return;
48
+ }
49
+ params.comments = findings.map(({ body, id, line, path, startLine }) => ({
50
+ body: [
51
+ body,
52
+ marker.stringify({ command: "review", reviewer: id }),
53
+ ].join("\n\n"),
54
+ line,
55
+ path,
56
+ ...(startLine == null
57
+ ? {}
58
+ : { start_line: startLine, start_side: "RIGHT" }),
59
+ }));
60
+ }
61
+ if (this.state.pr.verdict !== "APPROVED") {
62
+ if (!this.state.operator?.sessionId)
63
+ throw new MagiError("blocked", "Reporter session ID not found.");
64
+ await this.updateEvent(`Generating comment by operator.`);
65
+ const contents = JSON.stringify(reviewers.flatMap(([, { outputs }]) => {
66
+ const output = outputs?.at(-1);
67
+ if (!output || output.verdict === "APPROVED")
68
+ return [];
69
+ if (output.verdict === "CLOSED")
70
+ return [output.comment];
71
+ else
72
+ return (output.findings ?? output.newFindings ?? []).filter(({ state }) => state === "accepted");
73
+ }), null, 2);
74
+ const prompt = await Prompt.init(this.magi, this.config, "review/comment");
75
+ const taskMessage = await prompt.create(undefined, ["output_contract", ["contents", contents]], {
76
+ owner: this.config.github.owner,
77
+ pr: this.number.toString(),
78
+ repo: this.config.github.repo,
79
+ verdict: this.state.pr.verdict,
80
+ });
81
+ const output = await retry(async (count, e) => {
82
+ const raw = await this.magi.promptSession(this.state.operator.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
83
+ await this.createAgentFile("comment", "operator", raw, count);
84
+ const parsed = prompt.parse(raw);
85
+ if (!prompt.validate(parsed))
86
+ throw new Error("Invalid output for operator.");
87
+ return parsed.comment;
88
+ }, {
89
+ error: async (e, count) => {
90
+ if (e instanceof MagiError)
91
+ throw e;
92
+ await this.updateEvent(`Attempt ${count} failed to post comment by operator. Retrying...`);
93
+ },
94
+ retries: this.config.output.repairAttempts,
95
+ signal: this.context.abort,
96
+ });
97
+ if (!output)
98
+ throw new MagiError("blocked", "Invalid output for operator.");
99
+ await this.updateEvent(`Generated comment by operator.`);
100
+ params.body = output;
101
+ }
102
+ params.body = [
103
+ params.body ?? "",
104
+ marker.stringify(...filterEmpty(reviewers.map(([id, { outputs }]) => {
105
+ const output = outputs?.at(-1);
106
+ if (!output)
107
+ return;
108
+ const body = output.comment
109
+ ? encodeURIComponent(output.comment)
110
+ : undefined;
111
+ return omitNullish({
112
+ body,
113
+ command: "review",
114
+ reviewer: id,
115
+ verdict: output.verdict,
116
+ });
117
+ }))),
118
+ ].join("\n\n");
119
+ const { data } = await octokit.rest.pulls.createReview(params);
120
+ const posted = data.html_url;
121
+ await this.updateState({
122
+ reviewers: Object.fromEntries(Object.keys(this.state.reviewers).map((id) => [id, { posted }])),
123
+ });
124
+ await this.updateEvent(`Finished posting reviews.`);
125
+ }
126
+ else {
127
+ const worker = new Worker(this.config.review.concurrency.reviewers);
128
+ const reviewers = Object.fromEntries(await Promise.all(Object.entries(this.state.reviewers).map(([id, { account, outputs, review, status }]) => worker.run(async () => {
129
+ if (status === "skip")
130
+ return [id, { posted: review?.html_url }];
131
+ const output = outputs?.at(-1);
132
+ if (!output)
133
+ throw new MagiError("blocked", "Reviewer output not found.");
134
+ const octokit = await this.magi.createOctokit(this.config, this.context.abort, account);
135
+ const graphql = this.magi.createGraphql(octokit);
136
+ await Promise.all((output.resolves ?? []).map(({ threadId }) => graphql.resolveReviewThread({ threadId })));
137
+ await Promise.all((output.followUps ?? []).map(({ body, commentId }) => octokit.rest.pulls.createReplyForReviewComment({
138
+ ...args,
139
+ body,
140
+ comment_id: commentId,
141
+ })));
142
+ const event = events[output.verdict];
143
+ const params = { ...args, event };
144
+ if (output.verdict !== "APPROVED")
145
+ params.body = output.comment;
146
+ if (output.verdict === "CHANGES_REQUESTED") {
147
+ const findings = (output.findings ??
148
+ output.newFindings ??
149
+ []).filter(({ state }) => state === "accepted");
150
+ if (!findings.length)
151
+ return [id, {}];
152
+ params.comments = findings.map(({ body, line, path, startLine }) => ({
153
+ body,
154
+ line,
155
+ path,
156
+ ...(startLine == null
157
+ ? {}
158
+ : { start_line: startLine, start_side: "RIGHT" }),
159
+ }));
160
+ }
161
+ params.body = [
162
+ params.body ?? "",
163
+ marker.stringify({
164
+ command: "review",
165
+ reviewer: id,
166
+ verdict: output.verdict,
167
+ }),
168
+ ].join("\n\n");
169
+ const { data } = await octokit.rest.pulls.createReview(params);
170
+ return [id, { posted: data.html_url }];
171
+ }))));
172
+ await this.updateState({ reviewers });
173
+ await this.updateEvent(`Finished posting reviews.`);
174
+ }
175
+ }
176
+ export async function automate() {
177
+ this.context.abort.throwIfAborted();
178
+ if (!this.state.pr?.metadata)
179
+ throw new MagiError("blocked", "PR metadata not found.");
180
+ if (!this.state.pr.verdict)
181
+ throw new MagiError("blocked", "PR verdict not found.");
182
+ if (!["APPROVED", "CLOSED"].includes(this.state.pr.verdict))
183
+ return "SKIPPED";
184
+ const automation = this.config[this.state.command].automation;
185
+ const action = this.state.pr.verdict === "APPROVED" ? "merge" : "close";
186
+ if (!automation[action]) {
187
+ await this.updateState({ pr: { automation: "SKIPPED" } });
188
+ await this.updateEvent(`Skipped ${action} automation.`);
189
+ return "SKIPPED";
190
+ }
191
+ const account = this.state.operator.account;
192
+ if (action === "merge") {
193
+ if (!this.state.pr.checks)
194
+ throw new MagiError("blocked", "PR checks not found.");
195
+ const failed = this.state.pr.checks.failed;
196
+ const pending = this.state.pr.checks.pending;
197
+ if (failed.length || pending.length) {
198
+ await this.updateState({ pr: { automation: "SKIPPED" } });
199
+ await this.updateEvent(`Skipped merge automation because unresolved CI checks remain.`);
200
+ return "SKIPPED";
201
+ }
202
+ if (await isConflict.call(this)) {
203
+ await this.updateState({ pr: { automation: "CONFLICT" } });
204
+ await this.updateEvent(`Merge automation found conflicts.`);
205
+ return "CONFLICT";
206
+ }
207
+ }
208
+ if (this.state.dryRun) {
209
+ await this.updateState({ pr: { automation: "SKIPPED" } });
210
+ await this.updateEvent(`Skipped ${action} automation during dry run.`);
211
+ return "SKIPPED";
212
+ }
213
+ const token = await this.magi.getGhToken(account, this.context.abort);
214
+ if (action === "merge" && !this.config.review.merge.queue) {
215
+ 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}`)), {
216
+ env: { GH_TOKEN: token },
217
+ signal: this.context.abort,
218
+ }));
219
+ if (isArray(rules)) {
220
+ const enabledMergeQueue = rules.some((rule) => !!rule &&
221
+ isObject(rule) &&
222
+ "type" in rule &&
223
+ rule.type === "merge_queue");
224
+ if (enabledMergeQueue)
225
+ throw new MagiError("blocked", `Base branch \`${this.state.pr.metadata.base.ref}\` requires merge queue, but \`review.merge.queue\` is \`false\`. Enable \`review.merge.queue\` or target a branch without merge queue.`);
226
+ }
227
+ }
228
+ if (action !== "merge" || this.config.review.merge.queue)
229
+ await this.updateEvent(`${action === "merge" ? "Merging" : "Closing"}.`);
230
+ else
231
+ await this.updateEvent(`Waiting to merge.`);
232
+ if (action === "merge" && this.config.review.merge.queue) {
233
+ const octokit = await this.magi.createOctokit(this.config, this.context.abort, account);
234
+ const graphql = this.magi.createGraphql(octokit);
235
+ const enqueuedAt = new Date().toISOString();
236
+ await graphql.enqueuePullRequest({ id: this.state.pr.metadata.node_id });
237
+ await this.updateEvent(`Waiting for merge queue.`);
238
+ const result = await waitMergeQueue.call(this, enqueuedAt);
239
+ if (result === "CONFLICT")
240
+ return result;
241
+ }
242
+ else {
243
+ const prefix = ["gh", "pr"];
244
+ const suffix = [this.number.toString(), "--repo", this.state.repo];
245
+ const args = [...prefix, action, ...suffix];
246
+ if (action === "merge") {
247
+ if (this.config.review.merge.method === "merge")
248
+ args.push("--merge");
249
+ if (this.config.review.merge.method === "rebase")
250
+ args.push("--rebase");
251
+ if (this.config.review.merge.method === "squash")
252
+ args.push("--squash");
253
+ if (this.config.review.merge.auto)
254
+ args.push("--auto");
255
+ if (this.config.review.merge.deleteBranch)
256
+ args.push("--delete-branch");
257
+ }
258
+ const options = { env: { GH_TOKEN: token }, signal: this.context.abort };
259
+ const waitMerge = async () => {
260
+ return await loop(async () => {
261
+ this.context.abort.throwIfAborted();
262
+ const data = await this.exec(command(...prefix, "view", ...suffix, "--json", "autoMergeRequest,mergeStateStatus,state,statusCheckRollup"), options);
263
+ const { autoMergeRequest: auto, mergeStateStatus: status, state, statusCheckRollup: checks, } = JSON.parse(data);
264
+ if (state === "MERGED")
265
+ return "MERGED";
266
+ if (status === "DIRTY")
267
+ return "CONFLICT";
268
+ if (status === "BEHIND") {
269
+ await this.updateEvent(`Updating with the base branch before merging.`);
270
+ await this.exec(command(...prefix, "update-branch", ...suffix), options);
271
+ return;
272
+ }
273
+ if (status === "BLOCKED" && hasFailedChecks(checks))
274
+ throw new MagiError("blocked", `Required checks failed before merging.`);
275
+ if (!auto && state === "OPEN")
276
+ throw new MagiError("blocked", `Auto-merge is no longer enabled.`);
277
+ }, 30_000);
278
+ };
279
+ const runAutomation = async () => {
280
+ try {
281
+ await this.updateEvent(`Merging.`);
282
+ await this.exec(command(...args), options);
283
+ if (action === "merge" && this.config.review.merge.auto) {
284
+ const result = await waitMerge();
285
+ if (result === "CONFLICT") {
286
+ await this.updateState({ pr: { automation: "CONFLICT" } });
287
+ await this.updateEvent(`Merge automation found conflicts.`);
288
+ return "CONFLICT";
289
+ }
290
+ }
291
+ }
292
+ catch (e) {
293
+ const message = e instanceof Error ? e.message : String(e);
294
+ if (action !== "merge")
295
+ throw e;
296
+ switch (true) {
297
+ case /\bconflicts?\b|merge commit cannot be cleanly created/i.test(message):
298
+ await this.updateState({ pr: { automation: "CONFLICT" } });
299
+ await this.updateEvent(`Merge automation found conflicts.`);
300
+ return "CONFLICT";
301
+ case /head (branch|ref) is (not up to date|out of date)/i.test(message):
302
+ await this.updateEvent(`Updating with the base branch before merging.`);
303
+ await this.exec(command(...prefix, "update-branch", ...suffix), options);
304
+ await this.exec(command(...args), options);
305
+ if (this.config.review.merge.auto) {
306
+ const result = await waitMerge();
307
+ if (result === "CONFLICT") {
308
+ await this.updateState({ pr: { automation: "CONFLICT" } });
309
+ await this.updateEvent(`Merge automation found conflicts.`);
310
+ return "CONFLICT";
311
+ }
312
+ }
313
+ break;
314
+ default:
315
+ throw e;
316
+ }
317
+ }
318
+ };
319
+ const result = action === "merge"
320
+ ? await mergeWorker.run(runAutomation)
321
+ : await runAutomation();
322
+ if (result)
323
+ return result;
324
+ }
325
+ await this.updateState({
326
+ pr: { automation: action === "merge" ? "MERGED" : "CLOSED" },
327
+ });
328
+ await this.updateEvent(`Finished ${action} automation.`);
329
+ return action === "merge" ? "MERGED" : "CLOSED";
330
+ }
331
+ function hasFailedChecks(checks) {
332
+ if (!isArray(checks))
333
+ return false;
334
+ const latest = checks.reduce((prev, check) => {
335
+ if (!isObject(check))
336
+ return prev;
337
+ const key = [check.workflowName, check.name, check.context]
338
+ .filter(isString)
339
+ .join("/");
340
+ if (!key)
341
+ return prev;
342
+ const current = prev.get(key);
343
+ if (!current || getCheckTime(check) >= getCheckTime(current))
344
+ prev.set(key, check);
345
+ return prev;
346
+ }, new Map());
347
+ return [...latest.values()].some(isFailedCheck);
348
+ }
349
+ function isFailedCheck(check) {
350
+ const regexp = /failure|failed|error|cancelled|timed_out|action_required/i;
351
+ return ["conclusion", "state", "status"].some((key) => isString(check[key]) && regexp.test(check[key]));
352
+ }
353
+ function getCheckTime(check) {
354
+ return ["startedAt", "completedAt", "updatedAt"].reduce((prev, key) => {
355
+ const value = check[key];
356
+ if (!isString(value))
357
+ return prev;
358
+ const time = Date.parse(value);
359
+ return Number.isNaN(time) ? prev : Math.max(prev, time);
360
+ }, 0);
361
+ }
362
+ async function waitMergeQueue(enqueuedAt, leftMergeQueue = false, retries = 0) {
363
+ this.context.abort.throwIfAborted();
364
+ if (!this.state.pr?.metadata)
365
+ throw new MagiError("blocked", "PR metadata not found.");
366
+ const { repository } = await this.graphql.mergeQueueStatus({
367
+ owner: this.config.github.owner,
368
+ pr: this.number,
369
+ repo: this.config.github.repo,
370
+ });
371
+ if (!repository?.pullRequest)
372
+ throw new MagiError("blocked", "Could not fetch merge queue status.");
373
+ const { isInMergeQueue, mergeQueueEntry, state, timelineItems } = repository.pullRequest;
374
+ if (state === "MERGED")
375
+ return "MERGED";
376
+ const nextLeftMergeQueue = state === "OPEN" && !isInMergeQueue && !mergeQueueEntry;
377
+ if (leftMergeQueue && nextLeftMergeQueue) {
378
+ if (await isConflict.call(this)) {
379
+ await this.updateState({ pr: { automation: "CONFLICT" } });
380
+ await this.updateEvent("Merge automation found conflicts.");
381
+ return "CONFLICT";
382
+ }
383
+ const removedFromQueue = timelineItems.nodes?.at(-1);
384
+ if (removedFromQueue?.reason === "failed_checks" &&
385
+ Date.parse(removedFromQueue.createdAt) >= Date.parse(enqueuedAt)) {
386
+ if (retries >= this.config.review.checks.retryFailedJobs)
387
+ throw new MagiError("blocked", `PR left the merge queue after failed checks exceeded ${retries} retries.`);
388
+ const attempt = retries + 1;
389
+ const nextEnqueuedAt = new Date().toISOString();
390
+ await this.updateEvent(`Attempt ${attempt} failed to merge from the merge queue. Retrying...`);
391
+ await this.graphql.enqueuePullRequest({
392
+ id: this.state.pr.metadata.node_id,
393
+ });
394
+ return await waitMergeQueue.call(this, nextEnqueuedAt, false, attempt);
395
+ }
396
+ throw new MagiError("blocked", `PR left the merge queue before merging.`);
397
+ }
398
+ await wait(30_000, this.context.abort);
399
+ return await waitMergeQueue.call(this, enqueuedAt, nextLeftMergeQueue, retries);
400
+ }
401
+ async function isConflict() {
402
+ if (!this.state.worktree)
403
+ return false;
404
+ if (!this.state.pr?.metadata)
405
+ throw new MagiError("blocked", "PR metadata not found.");
406
+ const options = { cwd: this.state.worktree.path, signal: this.context.abort };
407
+ const status = await this.exec(command("git", "status", "--porcelain"), options);
408
+ if (status)
409
+ throw new MagiError("blocked", "PR worktree has uncommitted changes.");
410
+ await this.exec(command("git", "fetch", "--no-tags", quote(this.state.pr.metadata.base.repo.clone_url), quote(`refs/heads/${this.state.pr.metadata.base.ref}`)), options);
411
+ try {
412
+ try {
413
+ await this.exec(command("git", "merge", "--no-commit", "--no-ff", "FETCH_HEAD"), options);
414
+ }
415
+ catch (e) {
416
+ const conflicts = await this.exec(command("git", "diff", "--name-only", "--diff-filter=U"), options);
417
+ if (!conflicts)
418
+ throw e;
419
+ return true;
420
+ }
421
+ }
422
+ finally {
423
+ try {
424
+ await this.exec(command("git", "merge", "--abort"), options);
425
+ }
426
+ catch { }
427
+ }
428
+ return false;
429
+ }