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,264 @@
1
+ import { MagiError } from "@/magi";
2
+ import { Prompt } from "@/prompts";
3
+ import { getMetadata } from "@/tools/review/check";
4
+ import { command, filterDuplicates, filterEmpty, quote, retry } from "@/utils";
5
+ export async function edit() {
6
+ this.context.abort.throwIfAborted();
7
+ await this.updateEvent(`Editing.`);
8
+ await setAccount.call(this);
9
+ const unresolvedThreads = await getUnresolvedThreads.call(this);
10
+ const threads = this.state.dryRun
11
+ ? [...createSyntheticThreads.call(this), ...unresolvedThreads]
12
+ : unresolvedThreads;
13
+ if (!threads.length)
14
+ throw new MagiError("blocked", "No editable review threads found.");
15
+ const cycle = (this.state.editor.outputs?.length ?? 0) + 1;
16
+ const prompt = await Prompt.init(this.magi, this.config, "merge/edit");
17
+ const taskMessage = await prompt.create(this.config.merge.prompts?.edit, [
18
+ "output_contract",
19
+ ["unresolved_threads", JSON.stringify(threads, null, 2)],
20
+ ], {
21
+ owner: this.config.github.owner,
22
+ pr: this.number.toString(),
23
+ repo: this.config.github.repo,
24
+ worktreePath: this.state.worktree.path,
25
+ });
26
+ const output = await retry(async (count, e) => {
27
+ const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
28
+ await this.createAgentFile("edit", "editor", raw, count, cycle);
29
+ const parsed = prompt.parse(raw);
30
+ if (!prompt.validate(parsed))
31
+ throw new Error("Invalid output for editor.");
32
+ const output = {
33
+ ...parsed,
34
+ filesTouched: parsed.filesTouched ?? [],
35
+ };
36
+ if (output.mode === "EDITED") {
37
+ const head = await this.exec(command("git", "rev-parse", "HEAD"), {
38
+ cwd: this.state.worktree.path,
39
+ signal: this.context.abort,
40
+ });
41
+ if (head !== output.commitSha)
42
+ throw new Error(`Editor reported commit ${output.commitSha}, but worktree HEAD is ${head}.`);
43
+ }
44
+ return output;
45
+ }, {
46
+ error: async (e, count) => {
47
+ if (e instanceof MagiError)
48
+ throw e;
49
+ await this.updateEvent(`Attempt ${count} failed to edit. Retrying...`);
50
+ },
51
+ retries: this.config.output.repairAttempts,
52
+ signal: this.context.abort,
53
+ });
54
+ if (!output)
55
+ throw new MagiError("blocked", "Invalid output for editor.");
56
+ await this.updateState({
57
+ editor: { outputs: [...(this.state.editor.outputs ?? []), output] },
58
+ });
59
+ await this.updateEvent(filterEmpty([
60
+ `Finished editing.`,
61
+ `Result: ${output.mode.toLocaleLowerCase()}.`,
62
+ output.commitSha
63
+ ? `Commit: ${output.commitSha}${output.commitMessage ? ` ${output.commitMessage}` : ""}`
64
+ : undefined,
65
+ output.filesTouched.length
66
+ ? `Files touched:\n${output.filesTouched.map((file) => `- ${file}`).join("\n")}`
67
+ : undefined,
68
+ output.responses.length
69
+ ? `Responses:\n${output.responses
70
+ .map(({ action, body, commentId }) => `- ${action} comment ${commentId}: ${body}`)
71
+ .join("\n")}`
72
+ : undefined,
73
+ ]).join("\n\n"));
74
+ if (output.mode === "EDITED")
75
+ if (this.state.dryRun) {
76
+ await this.updateState({
77
+ pr: {
78
+ files: filterDuplicates([
79
+ ...(this.state.pr?.files ?? []),
80
+ ...output.filesTouched,
81
+ ]),
82
+ metadata: { head: { sha: output.commitSha } },
83
+ },
84
+ });
85
+ await this.updateEvent(`Skipped pushing editor changes during dry run.`);
86
+ }
87
+ else {
88
+ await this.updateEvent(`Pushing editor changes.`);
89
+ const pr = await push.call(this);
90
+ await this.updateState({ pr });
91
+ await this.updateEvent(`Finished pushing editor changes.`);
92
+ }
93
+ return output.mode === "EDITED" && !this.state.dryRun;
94
+ }
95
+ export async function resolveConflict() {
96
+ this.context.abort.throwIfAborted();
97
+ await this.updateEvent(`Resolving merge conflicts.`);
98
+ if (!this.state.pr?.metadata)
99
+ throw new MagiError("blocked", "PR metadata not found.");
100
+ const options = { cwd: this.state.worktree.path, signal: this.context.abort };
101
+ await setAccount.call(this);
102
+ 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);
103
+ const conflictedFiles = await getConflictedFiles.call(this);
104
+ if (!conflictedFiles.length)
105
+ throw new MagiError("blocked", "No merge conflicts found in worktree.");
106
+ const cycle = (this.state.editor.outputs?.length ?? 0) + 1;
107
+ const prompt = await Prompt.init(this.magi, this.config, "merge/conflict");
108
+ const taskMessage = await prompt.create(undefined, [
109
+ "output_contract",
110
+ ["conflicted_files", JSON.stringify(conflictedFiles, null, 2)],
111
+ ], {
112
+ owner: this.config.github.owner,
113
+ pr: this.number.toString(),
114
+ repo: this.config.github.repo,
115
+ worktreePath: this.state.worktree.path,
116
+ });
117
+ const output = await retry(async (count, e) => {
118
+ const raw = await this.magi.promptSession(this.state.editor.sessionId, count === 1 ? taskMessage : await prompt.repair(e), this.context.abort);
119
+ await this.createAgentFile("conflict", "editor", raw, count, cycle);
120
+ const parsed = prompt.parse(raw);
121
+ if (!prompt.validate(parsed))
122
+ throw new Error("Invalid output for conflict editor.");
123
+ const diff = await this.exec(command("git", "diff", "--name-only", "--diff-filter=U"), options);
124
+ if (diff)
125
+ throw new Error("Merge conflicts remain.");
126
+ const commitSha = await this.exec(command("git", "rev-parse", "HEAD"), options);
127
+ if (commitSha === this.state.pr.metadata.head.sha)
128
+ throw new Error("Conflict editor did not create a commit.");
129
+ const parents = await this.exec(command("git", "rev-list", "--parents", "-n", "1", "HEAD"), options);
130
+ if (parents.split(" ").length < 3)
131
+ throw new Error("Conflict editor did not create a merge commit.");
132
+ const commitMessage = await this.exec(command("git", "log", "-1", "--pretty=%s"), options);
133
+ return {
134
+ commitMessage,
135
+ commitSha,
136
+ filesTouched: conflictedFiles,
137
+ mode: "RESOLVED",
138
+ responses: [],
139
+ };
140
+ }, {
141
+ error: async (e, count) => {
142
+ if (e instanceof MagiError)
143
+ throw e;
144
+ await this.updateEvent(`Attempt ${count} failed to resolve conflicts. Retrying...`);
145
+ },
146
+ retries: this.config.output.repairAttempts,
147
+ signal: this.context.abort,
148
+ });
149
+ if (!output)
150
+ throw new MagiError("blocked", "Invalid output for conflict editor.");
151
+ await this.updateState({
152
+ editor: { outputs: [...(this.state.editor.outputs ?? []), output] },
153
+ });
154
+ await this.updateEvent(`Finished resolving merge conflicts.`);
155
+ if (this.state.dryRun) {
156
+ await this.updateState({
157
+ pr: {
158
+ files: filterDuplicates([
159
+ ...(this.state.pr.files ?? []),
160
+ ...output.filesTouched,
161
+ ]),
162
+ metadata: { head: { sha: output.commitSha } },
163
+ },
164
+ });
165
+ await this.updateEvent(`Skipped pushing conflict resolution during dry run.`);
166
+ }
167
+ else {
168
+ await this.updateEvent(`Pushing conflict resolution.`);
169
+ const pr = await push.call(this);
170
+ await this.updateState({ pr });
171
+ await this.updateEvent(`Finished pushing conflict resolution.`);
172
+ }
173
+ }
174
+ async function setAccount() {
175
+ if (!this.state.editor?.sessionId)
176
+ throw new MagiError("blocked", "Editor session ID not found.");
177
+ if (!this.state.editor.author)
178
+ throw new MagiError("blocked", "Editor author not found.");
179
+ if (!this.state.worktree)
180
+ throw new MagiError("blocked", "PR worktree not found.");
181
+ const options = { cwd: this.state.worktree.path, signal: this.context.abort };
182
+ await this.exec(command("git", "config", "user.name", quote(this.state.editor.author.name)), options);
183
+ await this.exec(command("git", "config", "user.email", quote(this.state.editor.author.email)), options);
184
+ }
185
+ async function push() {
186
+ if (!this.state.editor?.account)
187
+ throw new MagiError("blocked", "Editor account not found.");
188
+ if (!this.state.pr?.metadata)
189
+ throw new MagiError("blocked", "PR metadata not found.");
190
+ if (!this.state.worktree)
191
+ throw new MagiError("blocked", "PR worktree not found.");
192
+ const token = await this.magi.getGhToken(this.state.editor.account, this.context.abort);
193
+ const url = `https://${this.config.github.host}/${this.state.pr.metadata.head.repo.owner.login}/${this.state.pr.metadata.head.repo.name}.git`;
194
+ const ref = `HEAD:refs/heads/${this.state.pr.metadata.head.ref}`;
195
+ await this.exec(command("git", "push", quote(url), quote(ref)), {
196
+ cwd: this.state.worktree.path,
197
+ env: {
198
+ GIT_CONFIG_COUNT: "2",
199
+ GIT_CONFIG_KEY_0: "credential.helper",
200
+ GIT_CONFIG_KEY_1: "credential.helper",
201
+ GIT_CONFIG_VALUE_0: "",
202
+ GIT_CONFIG_VALUE_1: "!f() { echo username=git; echo password=$GIT_PASSWORD; }; f",
203
+ GIT_PASSWORD: token,
204
+ GIT_TERMINAL_PROMPT: "0",
205
+ },
206
+ signal: this.context.abort,
207
+ });
208
+ return getMetadata.call(this);
209
+ }
210
+ async function getConflictedFiles() {
211
+ if (!this.state.worktree)
212
+ throw new MagiError("blocked", "PR worktree not found.");
213
+ const options = { cwd: this.state.worktree.path, signal: this.context.abort };
214
+ try {
215
+ await this.exec(command("git", "merge", "--no-commit", "--no-ff", "FETCH_HEAD"), options);
216
+ await this.exec(command("git", "merge", "--abort"), options);
217
+ return [];
218
+ }
219
+ catch {
220
+ const result = await this.exec(command("git", "diff", "--name-only", "--diff-filter=U"), options);
221
+ if (!result) {
222
+ await this.exec(command("git", "merge", "--abort"), options);
223
+ return [];
224
+ }
225
+ return result.split("\n");
226
+ }
227
+ }
228
+ async function getUnresolvedThreads() {
229
+ const data = await this.graphql.paginate(this.graphql.reviewThreads, {
230
+ owner: this.config.github.owner,
231
+ pr: this.number,
232
+ repo: this.config.github.repo,
233
+ });
234
+ return (data.repository?.pullRequest?.reviewThreads.nodes ?? []).flatMap((node) => {
235
+ if (!node || node.isResolved)
236
+ return [];
237
+ return [{ ...node, comments: filterEmpty(node.comments.nodes ?? []) }];
238
+ });
239
+ }
240
+ function createSyntheticThreads() {
241
+ const findings = Object.entries(this.state.reviewers ?? {}).flatMap(([reviewer, { outputs }]) => {
242
+ const output = outputs?.at(-1);
243
+ if (output?.verdict !== "CHANGES_REQUESTED")
244
+ return [];
245
+ const findings = output.findings ?? output.newFindings ?? [];
246
+ return findings.map((finding) => ({ ...finding, reviewer }));
247
+ });
248
+ return findings.map(({ body, line, path, reviewer, state }, index) => ({
249
+ comments: [
250
+ {
251
+ author: { login: reviewer },
252
+ body,
253
+ createdAt: new Date(0).toISOString(),
254
+ databaseId: -(index + 1),
255
+ url: "",
256
+ },
257
+ ],
258
+ id: `dry-run:${reviewer}:${index + 1}`,
259
+ isResolved: false,
260
+ line,
261
+ path,
262
+ state,
263
+ }));
264
+ }
@@ -0,0 +1,151 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { parsePrs, split, Worker } from "@/utils";
3
+ import { Merge } from "./merge";
4
+ async function editCycle(run, { conflict, cycle } = {}) {
5
+ if (!run.state.editor?.sessionId)
6
+ await run.createSession();
7
+ if (!run.state.worktree)
8
+ await run.createWorktree();
9
+ const reviewers = Object.entries(run.state.reviewers ?? {});
10
+ const hasSessions = reviewers.length && reviewers.every(([_, { sessionId }]) => sessionId);
11
+ const edited = cycle === 1 && conflict ? true : await run.edit();
12
+ if (cycle === 1 && conflict)
13
+ await run.resolveConflict();
14
+ else
15
+ await run.postReplies();
16
+ if (edited)
17
+ await run.checkCi(run.config.merge.checks.wait);
18
+ if (!hasSessions)
19
+ await run.createSessions();
20
+ if (edited) {
21
+ await run.classifyChecks();
22
+ await run.rerunChecks();
23
+ await run.checkExistingReviews();
24
+ }
25
+ await run.fetchMergeContext();
26
+ if (!edited)
27
+ await run.markRepliedReviewers();
28
+ await run.review();
29
+ await run.validateFindings();
30
+ await run.reconsiderClose();
31
+ const verdict = await run.resolveVerdict();
32
+ await run.postReviews();
33
+ return verdict;
34
+ }
35
+ function overrideConfig(config, args, dryRun = false) {
36
+ for (let index = 0; index < args.length; index++) {
37
+ const arg = args[index];
38
+ const value = args[index + 1];
39
+ if (!arg.startsWith("--"))
40
+ continue;
41
+ switch (arg) {
42
+ case "--dry-run":
43
+ dryRun = true;
44
+ break;
45
+ case "--retry-api-attempts":
46
+ config.github.retryApiAttempts = parseInt(value);
47
+ break;
48
+ case "--language":
49
+ config.language = value;
50
+ break;
51
+ case "--merge":
52
+ config.merge.automation.merge = true;
53
+ break;
54
+ case "--no-merge":
55
+ config.merge.automation.merge = false;
56
+ break;
57
+ case "--close":
58
+ config.merge.automation.close = true;
59
+ break;
60
+ case "--no-close":
61
+ config.merge.automation.close = false;
62
+ break;
63
+ case "--max-cycles":
64
+ config.merge.maxThreadResolutionCycles = parseInt(value);
65
+ break;
66
+ case "--retry-failed-jobs":
67
+ config.review.checks.retryFailedJobs = parseInt(value);
68
+ break;
69
+ case "--concurrency-reviewers":
70
+ config.review.concurrency.reviewers = parseInt(value);
71
+ break;
72
+ case "--concurrency-runs":
73
+ config.review.concurrency.runs = parseInt(value);
74
+ break;
75
+ case "--wait-checks":
76
+ config.review.checks.wait = true;
77
+ break;
78
+ case "--no-wait-checks":
79
+ config.review.checks.wait = false;
80
+ break;
81
+ case "--wait-checks-after-edit":
82
+ config.merge.checks.wait = true;
83
+ break;
84
+ case "--no-wait-checks-after-edit":
85
+ config.merge.checks.wait = false;
86
+ break;
87
+ }
88
+ }
89
+ return { config, dryRun };
90
+ }
91
+ export const merge = function (magi) {
92
+ return {
93
+ magi_merge: tool({
94
+ args: {
95
+ dryRun: tool.schema.boolean().optional(),
96
+ prs: tool.schema.string(),
97
+ },
98
+ description: "Review, fix, and merge one or more pull requests.",
99
+ async execute(args, context) {
100
+ const prs = parsePrs(args.prs);
101
+ const { config, dryRun } = overrideConfig(await magi.getConfig({ editor: true, reviewers: true }), split(args.prs), args.dryRun);
102
+ const worker = new Worker(config.review.concurrency.runs);
103
+ const reports = await Promise.all(prs.map(async (pr) => {
104
+ context.abort.throwIfAborted();
105
+ return worker.run(async () => {
106
+ const run = await Merge.init(pr, magi, config, context, {
107
+ dryRun,
108
+ });
109
+ try {
110
+ await run.checkPr();
111
+ const skip = await run.checkExistingReviews();
112
+ await run.checkCi();
113
+ if (!skip) {
114
+ await run.createSessions();
115
+ await run.createWorktree();
116
+ await run.classifyChecks();
117
+ await run.rerunChecks();
118
+ await run.fetchReviewContext();
119
+ await run.review();
120
+ await run.validateFindings();
121
+ await run.reconsiderClose();
122
+ }
123
+ const verdict = await run.resolveVerdict();
124
+ if (!skip)
125
+ await run.postReviews();
126
+ if (verdict === "CHANGES_REQUESTED")
127
+ await run.editCycles(async () => editCycle(run));
128
+ const automation = await run.automate();
129
+ if (automation === "CONFLICT" &&
130
+ run.config.merge.automation.conflict) {
131
+ await run.editCycles(async (cycle) => editCycle(run, { conflict: true, cycle }));
132
+ await run.automate();
133
+ }
134
+ return { error: false, report: await run.createReport() };
135
+ }
136
+ catch (e) {
137
+ return { error: true, report: await run.createReport(e) };
138
+ }
139
+ finally {
140
+ await run.cleanup();
141
+ }
142
+ });
143
+ }));
144
+ const errors = reports.filter(({ error }) => error);
145
+ if (errors.length)
146
+ throw new Error(errors.map(({ report }) => report).join("\n\n"));
147
+ return reports.map(({ report }) => report).join("\n\n");
148
+ },
149
+ }),
150
+ };
151
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { join } from "node:path";
2
+ import { MagiError } from "@/magi";
3
+ import { Review } from "@/tools/review/review";
4
+ import { editCycles, postReplies } from "./action";
5
+ import { fetchMergeContext, markRepliedReviewers } from "./context";
6
+ import { edit, resolveConflict } from "./editor";
7
+ import { createReport } from "./report";
8
+ export class Merge extends Review {
9
+ static async init(number, magi, config, context, options) {
10
+ const { exec, graphql, octokit, ...rest } = await this.setup(number, magi, config, context);
11
+ const editor = {
12
+ account: config.merge.editor.account,
13
+ author: config.merge.editor.author,
14
+ model: config.merge.editor.model,
15
+ permissions: config.merge.editor.permissions,
16
+ };
17
+ const state = await magi.createState(join(config.review.output, number.toString()), { ...options, ...rest, command: "merge", editor });
18
+ await magi.updateEvent(state.output, `Started merging.`);
19
+ return new Merge(number, magi, config, context, octokit, graphql, exec, state);
20
+ }
21
+ createReport = createReport;
22
+ editCycles = editCycles;
23
+ edit = edit;
24
+ fetchMergeContext = fetchMergeContext;
25
+ markRepliedReviewers = markRepliedReviewers;
26
+ postReplies = postReplies;
27
+ resolveConflict = resolveConflict;
28
+ async createSession() {
29
+ this.context.abort.throwIfAborted();
30
+ if (!this.state.editor)
31
+ throw new MagiError("blocked", "Editor not found.");
32
+ await this.updateEvent(`Creating editor session.`);
33
+ const editor = {
34
+ sessionId: await this.magi.createSession(this.state.sessionId, `magi merge #${this.number} editor`, {
35
+ model: this.state.editor.model,
36
+ permissions: this.state.editor.permissions,
37
+ }, this.context.abort),
38
+ };
39
+ await this.updateState({ editor });
40
+ await this.updateEvent(`Finished creating editor session.`);
41
+ }
42
+ }
@@ -0,0 +1,73 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { MagiError } from "@/magi";
4
+ import { createCheckContent, createMetaContent, createReviewerContent, } from "@/tools/review/report";
5
+ import { filterEmpty, toTitleCase } from "@/utils";
6
+ export async function createReport(e) {
7
+ this.context.abort.throwIfAborted();
8
+ if (!e) {
9
+ const status = "completed";
10
+ const text = await createContent.call(this, { status });
11
+ await writeFile(join(this.state.output, "report.md"), `${text}\n`);
12
+ await this.updateState({
13
+ completedAt: new Date().toISOString(),
14
+ status,
15
+ });
16
+ return text;
17
+ }
18
+ else {
19
+ const error = e instanceof Error ? e.message : "Unknown error";
20
+ const status = e instanceof MagiError
21
+ ? e.status
22
+ : this.context.abort.aborted
23
+ ? "cancelled"
24
+ : "failed";
25
+ const text = await createContent.call(this, { error, status });
26
+ await writeFile(join(this.state.output, "report.md"), `${text}\n`);
27
+ await this.updateState({
28
+ completedAt: new Date().toISOString(),
29
+ status,
30
+ });
31
+ return text;
32
+ }
33
+ }
34
+ async function createContent(input) {
35
+ return filterEmpty([
36
+ ...(await createMetaContent.call(this, input)),
37
+ ...createCheckContent.call(this),
38
+ ...createReviewerContent.call(this),
39
+ ...createEditorContent.call(this),
40
+ ]).join("\n");
41
+ }
42
+ function createEditorContent() {
43
+ if (!this.state.editor?.outputs?.length)
44
+ return [];
45
+ return [
46
+ [
47
+ "- **Editor**:",
48
+ ...this.state.editor.outputs.flatMap((output, index) => {
49
+ const lines = [
50
+ ` - **Cycle ${index + 1}**: ${toTitleCase(output.mode.toLocaleLowerCase())}`,
51
+ ];
52
+ if (output.commitSha)
53
+ lines.push(` - **Commit**: \`${output.commitSha}\` ${output.commitMessage}`);
54
+ if (output.filesTouched.length) {
55
+ lines.push(" - **Files touched**:");
56
+ for (const file of output.filesTouched)
57
+ lines.push(` - \`${file}\``);
58
+ }
59
+ if (output.responses.length) {
60
+ lines.push(" - **Responses**:");
61
+ for (const { action, body, commentId } of output.responses) {
62
+ const thread = this.state.pr?.threads?.find(({ comments }) => comments.some(({ databaseId }) => databaseId === commentId));
63
+ const prefix = thread
64
+ ? `${thread.path}:${thread.line ?? "N/A"}`
65
+ : commentId;
66
+ lines.push(` - **${toTitleCase(action.toLocaleLowerCase())}** \`${prefix}\`: ${body}`);
67
+ }
68
+ }
69
+ return lines;
70
+ }),
71
+ ].join("\n"),
72
+ ];
73
+ }