opencode-magi 0.1.0 → 0.3.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 (43) hide show
  1. package/README.md +33 -10
  2. package/dist/commands.js +4 -0
  3. package/dist/config/output.js +11 -2
  4. package/dist/config/resolve.js +124 -26
  5. package/dist/config/validate.js +486 -191
  6. package/dist/config/worktree.js +19 -0
  7. package/dist/github/commands.js +349 -17
  8. package/dist/index.js +257 -27
  9. package/dist/orchestrator/ci.js +1 -1
  10. package/dist/orchestrator/findings.js +4 -3
  11. package/dist/orchestrator/inline-comments.js +73 -0
  12. package/dist/orchestrator/majority.js +14 -0
  13. package/dist/orchestrator/merge.js +24 -4
  14. package/dist/orchestrator/report.js +15 -1
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +78 -10
  17. package/dist/orchestrator/run-manager.js +418 -20
  18. package/dist/orchestrator/triage.js +1119 -0
  19. package/dist/permissions/editor.json +8 -1
  20. package/dist/prompts/compose.js +172 -15
  21. package/dist/prompts/contracts.js +119 -12
  22. package/dist/prompts/output.js +149 -14
  23. package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
  24. package/dist/prompts/templates/review/review.md +13 -0
  25. package/dist/prompts/templates/triage/acceptance.md +7 -0
  26. package/dist/prompts/templates/triage/action.md +5 -0
  27. package/dist/prompts/templates/triage/category.md +10 -0
  28. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  29. package/dist/prompts/templates/triage/comment.md +5 -0
  30. package/dist/prompts/templates/triage/create.md +7 -0
  31. package/dist/prompts/templates/triage/duplicate.md +7 -0
  32. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  33. package/dist/prompts/templates/triage/question.md +5 -0
  34. package/dist/prompts/templates/triage/reconsider.md +5 -0
  35. package/package.json +28 -27
  36. package/schema.json +234 -90
  37. package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
  38. package/dist/prompts/templates/review.md +0 -7
  39. /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
  40. /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
  41. /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
  42. /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
  43. /package/dist/prompts/templates/{rereview.md → review/rereview.md} +0 -0
@@ -71,6 +71,100 @@ function requireNumber(value, path) {
71
71
  throw new Error(`${path} must be an integer`);
72
72
  return value;
73
73
  }
74
+ function parseRequirementFindings(value) {
75
+ return (value == null ? [] : requireArray(value, "requirementFindings")).map((finding, index) => {
76
+ const item = finding;
77
+ return {
78
+ evidence: requireString(item.evidence, `requirementFindings[${index}].evidence`),
79
+ fix: requireString(item.fix, `requirementFindings[${index}].fix`),
80
+ issueNumber: requireNumber(item.issueNumber, `requirementFindings[${index}].issueNumber`),
81
+ requirement: requireString(item.requirement, `requirementFindings[${index}].requirement`),
82
+ };
83
+ });
84
+ }
85
+ function requireOneOf(value, path, values) {
86
+ const text = requireString(value, path);
87
+ if (!values.includes(text)) {
88
+ throw new Error(`${path} must be ${values.join(", ")}`);
89
+ }
90
+ return text;
91
+ }
92
+ function parseTriageVote(text, votes) {
93
+ const data = extractJson(text);
94
+ if (!data || typeof data !== "object")
95
+ throw new Error("triage vote output must be an object");
96
+ return {
97
+ reason: requireString(data.reason, "reason"),
98
+ vote: requireOneOf(data.vote, "vote", votes),
99
+ };
100
+ }
101
+ export function parseTriageExistingPrOutput(text) {
102
+ return parseTriageVote(text, [
103
+ "RELATED_PR_DOES_NOT_HANDLE_ISSUE",
104
+ "RELATED_PR_HANDLES_ISSUE",
105
+ ]);
106
+ }
107
+ export function parseTriageCategoryOutput(text, categories) {
108
+ return parseTriageVote(text, ["ASK", ...categories]);
109
+ }
110
+ export function parseTriageBinaryOutput(text) {
111
+ return parseTriageVote(text, ["ASK", "NO", "YES"]);
112
+ }
113
+ export function parseTriageDuplicateOutput(text) {
114
+ const data = extractJson(text);
115
+ if (!data || typeof data !== "object")
116
+ throw new Error("triage duplicate output must be an object");
117
+ const vote = requireOneOf(data.vote, "vote", [
118
+ "DUPLICATE",
119
+ "NOT_DUPLICATE",
120
+ ]);
121
+ const duplicateOf = data.duplicateOf == null
122
+ ? undefined
123
+ : requireNumber(data.duplicateOf, "duplicateOf");
124
+ if (vote === "DUPLICATE" && duplicateOf == null)
125
+ throw new Error("DUPLICATE requires duplicateOf");
126
+ return {
127
+ duplicateOf,
128
+ reason: requireString(data.reason, "reason"),
129
+ vote,
130
+ };
131
+ }
132
+ export function parseTriageCommentClassificationOutput(text) {
133
+ const data = extractJson(text);
134
+ if (!data || typeof data !== "object")
135
+ throw new Error("triage comment classification output must be an object");
136
+ return {
137
+ comments: requireArray(data.comments, "comments").map((item, index) => {
138
+ const value = item;
139
+ return {
140
+ classification: requireOneOf(value.classification, `comments[${index}].classification`, [
141
+ "ACKNOWLEDGEMENT",
142
+ "CLARIFICATION",
143
+ "NEW_EVIDENCE",
144
+ "OBJECTION",
145
+ "UNRELATED",
146
+ ]),
147
+ commentId: requireNumber(value.commentId, `comments[${index}].commentId`),
148
+ reason: requireString(value.reason, `comments[${index}].reason`),
149
+ };
150
+ }),
151
+ };
152
+ }
153
+ export function parseTriageActionOutput(text) {
154
+ const data = extractJson(text);
155
+ if (!data || typeof data !== "object")
156
+ throw new Error("triage action output must be an object");
157
+ return {
158
+ action: requireOneOf(data.action, "action", [
159
+ "ASK",
160
+ "CLEAR_ONLY",
161
+ "CLOSE",
162
+ "COMMENT",
163
+ "PR",
164
+ ]),
165
+ reason: requireString(data.reason, "reason"),
166
+ };
167
+ }
74
168
  export function parseReviewOutput(text) {
75
169
  const data = extractJson(text);
76
170
  if (!data || typeof data !== "object")
@@ -92,12 +186,17 @@ export function parseReviewOutput(text) {
92
186
  : requireNumber(item.startLine, `findings[${index}].startLine`),
93
187
  };
94
188
  });
95
- if (data.verdict === "MERGE" && findings.length)
96
- throw new Error("MERGE requires no findings");
97
- if (data.verdict === "CHANGES_REQUESTED" && !findings.length)
98
- throw new Error("CHANGES_REQUESTED requires findings");
99
- if (data.verdict === "CLOSE" && findings.length)
100
- throw new Error("CLOSE requires no findings");
189
+ const requirementFindings = parseRequirementFindings(data.requirementFindings);
190
+ if (data.verdict === "MERGE" &&
191
+ (findings.length || requirementFindings.length))
192
+ throw new Error("MERGE requires no findings or requirementFindings");
193
+ if (data.verdict === "CHANGES_REQUESTED" &&
194
+ !findings.length &&
195
+ !requirementFindings.length)
196
+ throw new Error("CHANGES_REQUESTED requires findings or requirementFindings");
197
+ if (data.verdict === "CLOSE" &&
198
+ (findings.length || requirementFindings.length))
199
+ throw new Error("CLOSE requires no findings or requirementFindings");
101
200
  const reason = typeof data.reason === "string" && data.reason.trim()
102
201
  ? data.reason
103
202
  : undefined;
@@ -106,6 +205,7 @@ export function parseReviewOutput(text) {
106
205
  return {
107
206
  findings,
108
207
  reason,
208
+ requirementFindings,
109
209
  verdict: data.verdict,
110
210
  };
111
211
  }
@@ -138,24 +238,29 @@ export function parseRereviewOutput(text) {
138
238
  : requireNumber(value.startLine, `newFindings[${index}].startLine`),
139
239
  };
140
240
  });
141
- if (data.verdict === "MERGE" && (followUps.length || newFindings.length)) {
142
- throw new Error("MERGE requires no followUps or newFindings");
241
+ const requirementFindings = parseRequirementFindings(data.requirementFindings);
242
+ if (data.verdict === "MERGE" &&
243
+ (followUps.length || newFindings.length || requirementFindings.length)) {
244
+ throw new Error("MERGE requires no followUps, newFindings, or requirementFindings");
143
245
  }
144
- if (data.verdict === "CLOSE" && (followUps.length || newFindings.length)) {
145
- throw new Error("CLOSE requires no followUps or newFindings");
246
+ if (data.verdict === "CLOSE" &&
247
+ (followUps.length || newFindings.length || requirementFindings.length)) {
248
+ throw new Error("CLOSE requires no followUps, newFindings, or requirementFindings");
146
249
  }
147
250
  if (data.verdict === "CLOSE" && !data.reason) {
148
251
  throw new Error("CLOSE requires reason");
149
252
  }
150
253
  if (data.verdict === "CHANGES_REQUESTED" &&
151
254
  !followUps.length &&
152
- !newFindings.length) {
153
- throw new Error("CHANGES_REQUESTED requires followUps or newFindings");
255
+ !newFindings.length &&
256
+ !requirementFindings.length) {
257
+ throw new Error("CHANGES_REQUESTED requires followUps, newFindings, or requirementFindings");
154
258
  }
155
259
  return {
156
260
  followUps,
157
261
  newFindings,
158
262
  reason: data.reason == null ? undefined : requireString(data.reason, "reason"),
263
+ requirementFindings,
159
264
  resolve,
160
265
  verdict: data.verdict,
161
266
  };
@@ -212,7 +317,21 @@ export function parseCiClassificationOutput(text) {
212
317
  }),
213
318
  };
214
319
  }
215
- export function parseEditOutput(text) {
320
+ function parsePullRequest(value, options) {
321
+ if (value == null) {
322
+ if (options.required)
323
+ throw new Error("pullRequest is required");
324
+ return undefined;
325
+ }
326
+ if (typeof value !== "object")
327
+ throw new Error("pullRequest must be an object");
328
+ const pullRequest = value;
329
+ return {
330
+ body: requireString(pullRequest.body, "pullRequest.body"),
331
+ title: requireString(pullRequest.title, "pullRequest.title"),
332
+ };
333
+ }
334
+ function parseEditOutputWithOptions(text, options) {
216
335
  const data = extractJson(text);
217
336
  if (!data || typeof data !== "object")
218
337
  throw new Error("edit output must be an object");
@@ -230,16 +349,20 @@ export function parseEditOutput(text) {
230
349
  commentId: requireNumber(value.commentId, `responses[${index}].commentId`),
231
350
  };
232
351
  });
233
- if (!responses.length)
352
+ if (options.requireResponses && !responses.length)
234
353
  throw new Error("responses must not be empty");
235
354
  if (data.mode === "EDITED") {
236
355
  if (!filesTouched.length)
237
356
  throw new Error("EDITED requires filesTouched");
357
+ const pullRequest = parsePullRequest(data.pullRequest, {
358
+ required: options.requirePullRequest,
359
+ });
238
360
  return {
239
361
  commitMessage: requireString(data.commitMessage, "commitMessage"),
240
362
  commitSha: requireString(data.commitSha, "commitSha"),
241
363
  filesTouched,
242
364
  mode: data.mode,
365
+ ...(pullRequest ? { pullRequest } : {}),
243
366
  responses,
244
367
  };
245
368
  }
@@ -258,3 +381,15 @@ export function parseEditOutput(text) {
258
381
  responses,
259
382
  };
260
383
  }
384
+ export function parseEditOutput(text) {
385
+ return parseEditOutputWithOptions(text, {
386
+ requirePullRequest: false,
387
+ requireResponses: true,
388
+ });
389
+ }
390
+ export function parseTriageCreatePrOutput(text) {
391
+ return parseEditOutputWithOptions(text, {
392
+ requirePullRequest: true,
393
+ requireResponses: false,
394
+ });
395
+ }
@@ -1,6 +1,5 @@
1
1
  You requested CLOSE for pull request #{pr} in {owner}/{repo}, but the other reviewers did not.
2
- Reconsider your decision and choose MERGE or CHANGES_REQUESTED instead.
3
- Use the existing review session context.
2
+ Reconsider your decision using the existing session context and choose MERGE or CHANGES_REQUESTED instead.
4
3
  Original close reason:
5
4
  {closeReason}
6
5
  Do not edit files or perform write operations.
@@ -0,0 +1,13 @@
1
+ Review pull request #{pr} for {owner}/{repo}.
2
+ The PR worktree is {worktreePath}.
3
+ Review only the diff from {baseSha} to {headSha}.
4
+ Use: git -C {jsonEncodedWorktreePath} diff {baseSha}...{headSha}
5
+ Do not edit files or perform write operations.
6
+
7
+ This PR may include closing issue references.
8
+ For each closing issue, review whether the PR fully satisfies the issue body, acceptance criteria, required behavior, required tests, required documentation, and bounded issue comments.
9
+ Request changes if a closing issue requirement is missing, only documented, only schema-exposed, or not wired into runtime behavior.
10
+ Do not approve solely because the PR improves the codebase if it claims to close an issue that remains incomplete.
11
+ For referenced non-closing issues, use them as context only unless the PR body explicitly claims to complete them.
12
+
13
+ {ciFailureContextBlock}
@@ -0,0 +1,7 @@
1
+ Evaluate issue #{issue} in {owner}/{repo} for the selected category.
2
+
3
+ Choose YES when the issue should be accepted for the project. Choose NO when it should be rejected, is not actionable, or is not appropriate for this project. Choose ASK when specific missing information is required before deciding.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,5 @@
1
+ Decide the next action for issue #{issue} in {owner}/{repo} from the provided triage result and allowed actions.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
@@ -0,0 +1,10 @@
1
+ Classify issue #{issue} in {owner}/{repo}.
2
+
3
+ Choose ASK when more information is required to classify what should be done. Otherwise choose exactly one configured category ID.
4
+
5
+ Configured categories:
6
+ {categoryOptions}
7
+
8
+ <context>
9
+ {context}
10
+ </context>
@@ -0,0 +1,7 @@
1
+ Classify allowed mention replies for issue #{issue} in {owner}/{repo}.
2
+
3
+ Use OBJECTION for disagreement or reconsideration requests. Use NEW_EVIDENCE for new logs, reproduction steps, screenshots, links, or use cases. Use CLARIFICATION for answers to questions or ambiguity reduction. Use ACKNOWLEDGEMENT for acceptance or thanks. Use UNRELATED for unrelated content.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,5 @@
1
+ Compose one concise final triage comment for issue #{issue} in {owner}/{repo}. Mention @{author}. Do not include markdown fences.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
@@ -0,0 +1,7 @@
1
+ Create an implementation pull request for issue #{issue} in {owner}/{repo}.
2
+
3
+ Use the checked-out worktree at {worktreePath} and commit your changes. Return the required structured output.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,7 @@
1
+ Decide whether issue #{issue} in {owner}/{repo} duplicates one of the provided duplicate candidates.
2
+
3
+ Use only the provided context. Return DUPLICATE only when the target issue is clearly the same underlying report or request.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,7 @@
1
+ Decide whether an existing related pull request already handles issue #{issue} in {owner}/{repo}.
2
+
3
+ Use only the provided context. Return HANDLE only when the PR clearly addresses the issue.
4
+
5
+ <context>
6
+ {context}
7
+ </context>
@@ -0,0 +1,5 @@
1
+ Compose concrete, actionable questions for issue #{issue} in {owner}/{repo}. Mention @{author}. Do not include markdown fences.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
@@ -0,0 +1,5 @@
1
+ Reconsider issue #{issue} in {owner}/{repo} using the provided previous triage result and triggering comments.
2
+
3
+ <context>
4
+ {context}
5
+ </context>
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
5
5
  "license": "MIT",
6
6
  "author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/hirotomoyamada/opencode-magi"
9
+ "url": "https://github.com/magi-ai/opencode-magi"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/hirotomoyamada/opencode-magi/issues"
12
+ "url": "https://github.com/magi-ai/opencode-magi/issues"
13
13
  },
14
14
  "keywords": [
15
15
  "opencode",
@@ -27,24 +27,8 @@
27
27
  "dist",
28
28
  "schema.json"
29
29
  ],
30
- "packageManager": "pnpm@10.33.0",
31
- "scripts": {
32
- "prebuild": "node scripts/copy-data.ts",
33
- "build": "tsgo -p tsconfig.build.json",
34
- "clean": "rimraf node_modules dist coverage",
35
- "format:check": "oxfmt --check .",
36
- "format:write": "oxfmt --write .",
37
- "lint:check": "oxlint . --max-warnings=0",
38
- "lint:fix": "oxlint . --max-warnings=0 --fix",
39
- "prepare": "git rev-parse --is-inside-work-tree >/dev/null 2>&1 && lefthook install || true",
40
- "quality": "pnpm format:check && pnpm lint:check && pnpm typecheck && pnpm test",
41
- "test": "vitest run",
42
- "typecheck": "tsgo --noEmit",
43
- "release": "changeset publish",
44
- "release:version": "changeset version"
45
- },
46
30
  "dependencies": {
47
- "@opencode-ai/plugin": "latest",
31
+ "@opencode-ai/plugin": "^1.15.5",
48
32
  "ajv": "^8.20.0",
49
33
  "picomatch": "^4.0.4",
50
34
  "valibot": "^1.4.0"
@@ -54,16 +38,33 @@
54
38
  "@changesets/cli": "^2.30.0",
55
39
  "@commitlint/cli": "^21.0.1",
56
40
  "@commitlint/config-conventional": "^21.0.1",
57
- "@types/node": "^25.7.0",
41
+ "@types/node": "^25.9.0",
58
42
  "@types/picomatch": "^4.0.3",
59
- "@typescript/native-preview": "7.0.0-dev.20260513.1",
43
+ "@typescript/native-preview": "7.0.0-dev.20260518.1",
44
+ "@vitest/coverage-v8": "^4.1.7",
45
+ "@vitest/ui": "^4.1.7",
60
46
  "eslint-plugin-perfectionist": "^5.9.0",
61
47
  "eslint-plugin-unused-imports": "^4.4.1",
62
48
  "lefthook": "^2.1.6",
63
- "oxfmt": "^0.49.0",
64
- "oxlint": "^1.64.0",
65
- "oxlint-tsgolint": "^0.22.1",
49
+ "oxfmt": "^0.50.0",
50
+ "oxlint": "^1.65.0",
51
+ "oxlint-tsgolint": "^0.23.0",
66
52
  "rimraf": "^6.1.3",
67
- "vitest": "^4.1.5"
53
+ "vitest": "^4.1.7"
54
+ },
55
+ "scripts": {
56
+ "prebuild": "node scripts/copy-data.ts",
57
+ "build": "tsgo -p tsconfig.build.json",
58
+ "clean": "rimraf node_modules dist coverage",
59
+ "format:check": "oxfmt --check .",
60
+ "format:write": "oxfmt --write .",
61
+ "lint:check": "oxlint . --max-warnings=0",
62
+ "lint:fix": "oxlint . --max-warnings=0 --fix",
63
+ "quality": "pnpm format:check && pnpm lint:check && pnpm typecheck && pnpm test",
64
+ "test": "vitest run",
65
+ "test:dev": "vitest --watch --ui",
66
+ "typecheck": "tsgo --noEmit",
67
+ "release": "changeset publish",
68
+ "release:dev": "changeset version --snapshot dev && changeset publish --tag dev"
68
69
  }
69
- }
70
+ }