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.
- package/README.md +33 -10
- package/dist/commands.js +4 -0
- package/dist/config/output.js +11 -2
- package/dist/config/resolve.js +124 -26
- package/dist/config/validate.js +486 -191
- package/dist/config/worktree.js +19 -0
- package/dist/github/commands.js +349 -17
- package/dist/index.js +257 -27
- package/dist/orchestrator/ci.js +1 -1
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/inline-comments.js +73 -0
- package/dist/orchestrator/majority.js +14 -0
- package/dist/orchestrator/merge.js +24 -4
- package/dist/orchestrator/report.js +15 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +78 -10
- package/dist/orchestrator/run-manager.js +418 -20
- package/dist/orchestrator/triage.js +1119 -0
- package/dist/permissions/editor.json +8 -1
- package/dist/prompts/compose.js +172 -15
- package/dist/prompts/contracts.js +119 -12
- package/dist/prompts/output.js +149 -14
- package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
- package/dist/prompts/templates/review/review.md +13 -0
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/action.md +5 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/dist/prompts/templates/triage/comment-classification.md +7 -0
- package/dist/prompts/templates/triage/comment.md +5 -0
- package/dist/prompts/templates/triage/create.md +7 -0
- package/dist/prompts/templates/triage/duplicate.md +7 -0
- package/dist/prompts/templates/triage/existing-pr.md +7 -0
- package/dist/prompts/templates/triage/question.md +5 -0
- package/dist/prompts/templates/triage/reconsider.md +5 -0
- package/package.json +28 -27
- package/schema.json +234 -90
- package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
- package/dist/prompts/templates/review.md +0 -7
- /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
- /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
- /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
- /package/dist/prompts/templates/{rereview.md → review/rereview.md} +0 -0
package/dist/prompts/output.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw new Error("
|
|
99
|
-
if (data.verdict === "
|
|
100
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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" &&
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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,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>
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-magi",
|
|
3
|
-
"version": "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/
|
|
9
|
+
"url": "https://github.com/magi-ai/opencode-magi"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/
|
|
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": "
|
|
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.
|
|
41
|
+
"@types/node": "^25.9.0",
|
|
58
42
|
"@types/picomatch": "^4.0.3",
|
|
59
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
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.
|
|
64
|
-
"oxlint": "^1.
|
|
65
|
-
"oxlint-tsgolint": "^0.
|
|
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.
|
|
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
|
+
}
|