opencode-magi 0.10.0 → 0.12.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 +31 -26
  3. package/dist/commands.js +11 -9
  4. package/dist/config/index.js +2 -0
  5. package/dist/config/resolve.js +120 -246
  6. package/dist/config/validate.js +98 -1110
  7. package/dist/constant.js +131 -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 +116 -0
  62. package/dist/tools/merge/editor.js +256 -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 +459 -0
  68. package/dist/tools/review/check.js +335 -0
  69. package/dist/tools/review/context.js +254 -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 +146 -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 +46 -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 +36 -23
  90. package/schema.json +116 -20
  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 -1480
  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 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { MagiError } from "@/magi";
4
+ import { filterEmpty, toTitleCase } from "@/utils";
5
+ export async function createReport(e) {
6
+ this.context.abort.throwIfAborted();
7
+ if (!e) {
8
+ const status = "completed";
9
+ const text = await createContent.call(this, { status });
10
+ await writeFile(join(this.state.output, "report.md"), `${text}\n`);
11
+ await this.updateState({
12
+ completedAt: new Date().toISOString(),
13
+ status,
14
+ });
15
+ return text;
16
+ }
17
+ else {
18
+ const error = e instanceof Error ? e.message : "Unknown error";
19
+ const status = e instanceof MagiError
20
+ ? e.status
21
+ : this.context.abort.aborted
22
+ ? "cancelled"
23
+ : "failed";
24
+ const text = await createContent.call(this, { error, status });
25
+ await writeFile(join(this.state.output, "report.md"), `${text}\n`);
26
+ await this.updateState({
27
+ completedAt: new Date().toISOString(),
28
+ status,
29
+ });
30
+ return text;
31
+ }
32
+ }
33
+ async function createContent(input) {
34
+ return filterEmpty([
35
+ ...(await createMetaContent.call(this, input)),
36
+ ...createCheckContent.call(this),
37
+ ...createReviewerContent.call(this),
38
+ ]).join("\n");
39
+ }
40
+ export async function createMetaContent(input) {
41
+ const events = await this.getEvents();
42
+ const lastAction = events.at(-1)?.message;
43
+ const rows = [
44
+ `- **Pull Request:** [${this.state.pr.number}](${this.state.pr.url})`,
45
+ `- **Mode**: ${toTitleCase(this.config.mode)}`,
46
+ `- **Dry run**: ${this.state.dryRun ? "Yes" : "No"}`,
47
+ `- **Status**: ${toTitleCase(input.status)}`,
48
+ `- **Automation**: ${this.state.pr?.automation ? toTitleCase(this.state.pr.automation.toLocaleLowerCase()) : "none"}`,
49
+ ];
50
+ if (this.state.pr?.verdict)
51
+ rows.push(`- **Verdict**: ${toTitleCase(this.state.pr.verdict.toLocaleLowerCase())}`);
52
+ if (lastAction)
53
+ rows.push(`- **Last action**: ${lastAction}`);
54
+ if (input.error)
55
+ rows.push(`- **Error**: ${input.error}`);
56
+ return rows;
57
+ }
58
+ export function createCheckContent() {
59
+ const checks = this.state.pr?.checks;
60
+ if (!checks)
61
+ return [];
62
+ const failures = [
63
+ ...checks.failed.map((check) => ({
64
+ comments: Object.entries(check.classifieds ?? {}).map(([reviewer, { comment, scope }]) => ({ comment, reviewer, scope })),
65
+ detail: check.scope == null
66
+ ? "Failed"
67
+ : check.scope
68
+ ? "In-scope failure"
69
+ : "Out-of-scope failure",
70
+ name: check.name,
71
+ })),
72
+ ...checks.pending.map((check) => ({
73
+ comments: [],
74
+ detail: "Pending",
75
+ name: check.name,
76
+ })),
77
+ ];
78
+ if (failures.length)
79
+ return [
80
+ "- **Check**: Failure",
81
+ ...failures.flatMap(({ comments, detail, name }) => [
82
+ ` - **${name}**: ${detail}`,
83
+ ...comments.map(({ comment, reviewer, scope }) => ` - **${reviewer}**: ${scope ? "In scope" : "Out of scope"}. ${comment}`),
84
+ ]),
85
+ ];
86
+ else
87
+ return ["- **Check**: Pass"];
88
+ }
89
+ export function createReviewerContent() {
90
+ const reviewers = Object.entries(this.state.reviewers ?? {});
91
+ if (!reviewers.length)
92
+ return [];
93
+ return [
94
+ [
95
+ "- **Reviewer**:",
96
+ ...reviewers.flatMap(([id, { outputs, posted, review }]) => {
97
+ if (!outputs?.length)
98
+ return [];
99
+ const output = outputs.at(-1);
100
+ const url = posted ?? review?.html_url;
101
+ const status = toTitleCase(output.verdict.toLocaleLowerCase());
102
+ const prevStatuses = outputs
103
+ .slice(0, -1)
104
+ .map(({ verdict }) => toTitleCase(verdict.toLocaleLowerCase()))
105
+ .reduce((prev, current) => {
106
+ if (prev.at(-1) !== current)
107
+ prev.push(current);
108
+ return prev;
109
+ }, []);
110
+ const lines = [
111
+ ` - **${id}**: ${[...prevStatuses, url ? `[${status}](${url})` : status].join(" -> ")}`,
112
+ ...outputs.flatMap((output, index) => {
113
+ const lines = [
114
+ ` - **Verdict**: ${toTitleCase(output.verdict.toLocaleLowerCase())}`,
115
+ ];
116
+ const latest = index === outputs.length - 1;
117
+ const findings = output.findings ?? output.newFindings ?? [];
118
+ if (output.verdict === "CLOSED")
119
+ lines.push(` - **Comment**: ${latest ? "" : "~~"}${output.comment ?? review?.body}${latest ? "" : "~~"}`);
120
+ if (findings.length) {
121
+ lines.push(" - **Findings**:");
122
+ for (const { body, line, path, startLine, state } of findings) {
123
+ const discarded = state === "discarded";
124
+ const prefix = `${path}:${startLine != null ? `${startLine}-` : ""}${line}`;
125
+ lines.push(` - ${discarded ? "~~" : ""}\`${prefix}\`: ${body}${discarded ? "~~" : ""}`);
126
+ }
127
+ }
128
+ if (output.followUps?.length) {
129
+ lines.push(" - **FollowUps**:");
130
+ for (const { body, commentId } of output.followUps) {
131
+ const thread = this.state.pr?.threads?.find(({ comments }) => comments.some(({ databaseId }) => databaseId === commentId));
132
+ const prefix = `${thread?.path ?? "unknown"}:${thread?.line ?? "N/A"}`;
133
+ lines.push(` - \`${prefix}\`: ${body}`);
134
+ }
135
+ }
136
+ return lines;
137
+ }),
138
+ ];
139
+ return lines;
140
+ }),
141
+ ].join("\n"),
142
+ ];
143
+ }
@@ -0,0 +1,146 @@
1
+ import { join } from "node:path";
2
+ import { MagiError } from "@/magi";
3
+ import { createExecWithGitHubApiRetry, quote } from "@/utils";
4
+ import { automate, isAutomationEnabled, postReviews } from "./action";
5
+ import { checkCi, checkPr, classifyChecks, rerunChecks } from "./check";
6
+ import { checkExistingReviews, fetchReviewContext } from "./context";
7
+ import { createReport } from "./report";
8
+ import { reconsiderClose, review, validateFindings } from "./reviewer";
9
+ export class Review {
10
+ number;
11
+ magi;
12
+ config;
13
+ context;
14
+ octokit;
15
+ graphql;
16
+ exec;
17
+ state;
18
+ constructor(number, magi, config, context, octokit, graphql, exec, state) {
19
+ this.number = number;
20
+ this.magi = magi;
21
+ this.config = config;
22
+ this.context = context;
23
+ this.octokit = octokit;
24
+ this.graphql = graphql;
25
+ this.exec = exec;
26
+ this.exec = createExecWithGitHubApiRetry(this.magi.exec, this.config.github.retryApiAttempts);
27
+ this.state = state;
28
+ }
29
+ static async init(number, magi, config, context, options) {
30
+ const { exec, graphql, octokit, ...rest } = await this.setup(number, magi, config, context);
31
+ const state = await magi.createState(join(config.review.output, number.toString()), { ...options, ...rest, command: "review" });
32
+ await magi.updateEvent(state.output, `Started reviewing.`);
33
+ return new Review(number, magi, config, context, octokit, graphql, exec, state);
34
+ }
35
+ static async setup(number, magi, config, context) {
36
+ const url = `${config.github.url}/pull/${number}`;
37
+ const octokit = await magi.createOctokit(config, context.abort);
38
+ const graphql = magi.createGraphql(octokit);
39
+ const reviewers = Object.fromEntries(config.review.reviewers.map(({ account, id, model, permissions }) => [id, { account, model, permissions }]));
40
+ const operator = config.review.operator
41
+ ? config.review.reviewers.find(({ id }) => id === config.review.operator)
42
+ : config.review.reviewers[Math.abs(number) % config.review.reviewers.length];
43
+ const exec = createExecWithGitHubApiRetry(magi.exec, config.github.retryApiAttempts);
44
+ return {
45
+ exec,
46
+ graphql,
47
+ octokit,
48
+ operator,
49
+ pr: { number, url },
50
+ repo: quote(`${config.github.owner}/${config.github.repo}`),
51
+ reviewers,
52
+ sessionId: context.sessionID,
53
+ };
54
+ }
55
+ checkPr = checkPr;
56
+ checkExistingReviews = checkExistingReviews;
57
+ checkCi = checkCi;
58
+ classifyChecks = classifyChecks;
59
+ rerunChecks = rerunChecks;
60
+ fetchReviewContext = fetchReviewContext;
61
+ review = review;
62
+ validateFindings = validateFindings;
63
+ reconsiderClose = reconsiderClose;
64
+ postReviews = postReviews;
65
+ isAutomationEnabled = isAutomationEnabled;
66
+ automate = automate;
67
+ createReport = createReport;
68
+ async cleanup() {
69
+ if (this.context.abort.aborted &&
70
+ ["preparing", "running"].includes(this.state.status))
71
+ await this.updateState({
72
+ completedAt: new Date().toISOString(),
73
+ status: "cancelled",
74
+ });
75
+ if (this.state.worktree?.path)
76
+ await this.magi.deleteWorktree(this.state.worktree.path);
77
+ }
78
+ async updateState(next) {
79
+ this.state = await this.magi.updateState(this.state.output, next);
80
+ }
81
+ async updateEvent(message) {
82
+ await this.magi.updateEvent(this.state.output, message);
83
+ }
84
+ async getEvents() {
85
+ return await this.magi.getEvents(this.state.output);
86
+ }
87
+ async createAgentFile(phase, id, content, attempt, cycle) {
88
+ await this.magi.createAgentFile(this.state.output, phase, id, content, attempt, cycle);
89
+ }
90
+ async createSessions() {
91
+ this.context.abort.throwIfAborted();
92
+ if (!this.state.reviewers)
93
+ throw new MagiError("blocked", "Reviewers not found.");
94
+ if (!this.state.operator)
95
+ throw new MagiError("blocked", "Operator not found.");
96
+ const reviewers = Object.fromEntries(await Promise.all(Object.entries(this.state.reviewers).map(async ([id, { model, permissions }]) => [
97
+ id,
98
+ {
99
+ sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} ${id}`, { model, permissions }, this.context.abort),
100
+ },
101
+ ])));
102
+ const operator = {
103
+ sessionId: await this.magi.createSession(this.state.sessionId, `magi review #${this.number} operator`, {
104
+ model: this.state.operator.model,
105
+ permissions: this.state.operator.permissions,
106
+ }, this.context.abort),
107
+ };
108
+ await this.updateState({ operator, reviewers });
109
+ }
110
+ async createWorktree() {
111
+ this.context.abort.throwIfAborted();
112
+ await this.updateEvent(`Creating worktree.`);
113
+ const worktree = await this.magi.createWorktree(this.config.review.worktree, this.number, this.state.id, this.context.abort);
114
+ await this.updateState({ worktree });
115
+ await this.updateEvent(`Finished creating worktree.`);
116
+ }
117
+ async resolveVerdict() {
118
+ if (!this.config.review.reviewers?.length)
119
+ throw new MagiError("blocked", "No reviewers configured.");
120
+ if (!this.state.reviewers)
121
+ throw new MagiError("blocked", "Reviewers not found.");
122
+ const counts = Object.entries(this.state.reviewers).reduce((prev, [id, { outputs }]) => {
123
+ const output = outputs?.at(-1);
124
+ if (!output)
125
+ throw new MagiError("blocked", `No output found for reviewer ${id}.`);
126
+ prev[output.verdict] += 1;
127
+ return prev;
128
+ }, {
129
+ APPROVED: 0,
130
+ CHANGES_REQUESTED: 0,
131
+ CLOSED: 0,
132
+ });
133
+ const length = this.config.review.reviewers.length;
134
+ const threshold = Math.floor(length / 2) + 1;
135
+ const majority = this.config.review.merge.approvalPolicy === "majority";
136
+ const verdict = counts.CLOSED >= threshold
137
+ ? "CLOSED"
138
+ : (!majority && counts.APPROVED === length) ||
139
+ (majority && counts.APPROVED >= threshold)
140
+ ? "APPROVED"
141
+ : "CHANGES_REQUESTED";
142
+ await this.updateState({ pr: { verdict } });
143
+ await this.updateEvent(`Final verdict is ${verdict}.`);
144
+ return verdict;
145
+ }
146
+ }