opencode-magi 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/commands.js +18 -0
- package/dist/config/load.js +62 -0
- package/dist/config/output.js +16 -0
- package/dist/config/resolve.js +113 -0
- package/dist/config/validate.js +567 -0
- package/dist/github/commands.js +398 -0
- package/dist/github/retry.js +44 -0
- package/dist/index.js +536 -0
- package/dist/orchestrator/abort.js +9 -0
- package/dist/orchestrator/ci.js +568 -0
- package/dist/orchestrator/findings.js +66 -0
- package/dist/orchestrator/majority.js +48 -0
- package/dist/orchestrator/merge.js +836 -0
- package/dist/orchestrator/model.js +202 -0
- package/dist/orchestrator/pool.js +15 -0
- package/dist/orchestrator/report.js +168 -0
- package/dist/orchestrator/review.js +790 -0
- package/dist/orchestrator/run-manager.js +1663 -0
- package/dist/orchestrator/safety.js +44 -0
- package/dist/permissions/common.json +24 -0
- package/dist/permissions/editor.json +7 -0
- package/dist/prompts/compose.js +298 -0
- package/dist/prompts/contracts.js +189 -0
- package/dist/prompts/output.js +260 -0
- package/dist/prompts/templates/ci-classification-after-edit.md +16 -0
- package/dist/prompts/templates/ci-classification.md +9 -0
- package/dist/prompts/templates/close-reconsideration.md +6 -0
- package/dist/prompts/templates/edit.md +9 -0
- package/dist/prompts/templates/finding-validation.md +7 -0
- package/dist/prompts/templates/rereview-close-reconsideration.md +6 -0
- package/dist/prompts/templates/rereview.md +16 -0
- package/dist/prompts/templates/review.md +7 -0
- package/dist/types.js +1 -0
- package/package.json +69 -0
- package/schema.json +200 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hirotomo Yamada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# OpenCode Magi
|
|
2
|
+
|
|
3
|
+
Multi-agent GitHub pull request review and merge orchestration for OpenCode.
|
|
4
|
+
|
|
5
|
+
## Why Magi?
|
|
6
|
+
|
|
7
|
+
Magi is inspired by the three wise men: independent perspectives that reach a decision together.
|
|
8
|
+
|
|
9
|
+
One AI model is still not enough to trust blindly. OpenCode Magi improves confidence by asking multiple models to inspect the same pull request from different perspectives, then requiring an odd-number majority before approving, requesting changes, or closing.
|
|
10
|
+
|
|
11
|
+
The goal is not to treat a single AI answer as final, but to make AI review behave more like a real team: diverse viewpoints, explicit disagreement, and a final decision backed by consensus.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
OpenCode Magi recreates the review cycle humans already run on GitHub: multiple reviewers inspect a pull request, request changes, verify fixes, resolve threads, and approve when the work is ready.
|
|
16
|
+
|
|
17
|
+
- Multi-agent reviews with an odd-number majority of 3 or more reviewers.
|
|
18
|
+
- Optional unanimous approval policy for merge automation when every reviewer must approve before a PR is merged.
|
|
19
|
+
- Finding-level voting before posting change requests, so only findings accepted by reviewer majority are submitted.
|
|
20
|
+
- Each reviewer acts through its configured GitHub account, posting real reviews, approvals, change requests, and follow-up comments.
|
|
21
|
+
- Re-review support for edited PRs: fixed threads are resolved, satisfied reviewers approve, and remaining issues are posted as additional comments.
|
|
22
|
+
- Optional merge and close automation where an editor agent responds on behalf of the author, fixes changes it agrees with, pushes commits when needed, and repeats the reviewer/editor cycle until the PR can be approved, queued, merged, or closed.
|
|
23
|
+
- Per-agent OpenCode permissions for reviewer, CI classifier, and editor child sessions.
|
|
24
|
+
- Prompt customization that adds repository-specific guidance without replacing the fixed output contracts.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Install
|
|
29
|
+
|
|
30
|
+
Add the plugin to `opencode.json`.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"$schema": "https://opencode.ai/config.json",
|
|
35
|
+
"plugin": ["opencode-magi"]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Configure
|
|
40
|
+
|
|
41
|
+
Configure global defaults in `~/.config/opencode/magi.json` and project overrides in `<project>/.opencode/magi.json`.
|
|
42
|
+
|
|
43
|
+
Magi config files are merged by OpenCode Magi, not by OpenCode. Priority, lowest to highest.
|
|
44
|
+
|
|
45
|
+
1. `~/.config/opencode/magi.json`
|
|
46
|
+
2. `<project>/.opencode/magi.json`
|
|
47
|
+
|
|
48
|
+
#### Set global config
|
|
49
|
+
|
|
50
|
+
You do not need to set global config values if the settings exist in your project config. However, using the global config is useful when you want to apply shared values across multiple projects.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
mkdir -p ~/.config/opencode
|
|
54
|
+
touch ~/.config/opencode/magi.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add the following content to the configuration file.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"$schema": "https://raw.githubusercontent.com/hirotomoyamada/opencode-magi/main/schema.json",
|
|
62
|
+
"agents": {
|
|
63
|
+
"reviewers": [
|
|
64
|
+
{
|
|
65
|
+
"account": "your-account-1",
|
|
66
|
+
"model": "openai/gpt-5.5"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"account": "your-account-2",
|
|
70
|
+
"model": "anthropic/claude-opus-4-7"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"account": "your-account-3",
|
|
74
|
+
"model": "opencode/kimi-k2-6"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`agents.reviewers[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
|
|
82
|
+
|
|
83
|
+
#### Set project config
|
|
84
|
+
|
|
85
|
+
Global config is optional, but project config is required.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cd <project>
|
|
89
|
+
mkdir -p .opencode
|
|
90
|
+
touch .opencode/magi.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Add the following content to the configuration file.
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"$schema": "https://raw.githubusercontent.com/hirotomoyamada/opencode-magi/main/schema.json",
|
|
98
|
+
"github": {
|
|
99
|
+
"owner": "your-owner",
|
|
100
|
+
"repo": "your-repo"
|
|
101
|
+
},
|
|
102
|
+
"agents": {
|
|
103
|
+
"reviewers": [
|
|
104
|
+
{
|
|
105
|
+
"account": "your-account-1",
|
|
106
|
+
"model": "openai/gpt-5.5"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"account": "your-account-2",
|
|
110
|
+
"model": "anthropic/claude-opus-4-7"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"account": "your-account-3",
|
|
114
|
+
"model": "opencode/kimi-k2-6"
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"editor": {
|
|
118
|
+
"account": "your-editor-account",
|
|
119
|
+
"model": "openai/gpt-5.5",
|
|
120
|
+
"author": {
|
|
121
|
+
"name": "your-account",
|
|
122
|
+
"email": "your-email@example.com"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`agents.reviewers[].account` is the GitHub account used to post reviews and approvals. Must be authenticated with `gh auth token --user <account>` and must be unique.
|
|
130
|
+
|
|
131
|
+
#### Validate config
|
|
132
|
+
|
|
133
|
+
After creating or updating your global or project configuration, validate it.
|
|
134
|
+
|
|
135
|
+
```txt
|
|
136
|
+
/magi:validate
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Commands
|
|
140
|
+
|
|
141
|
+
Run commands from OpenCode.
|
|
142
|
+
|
|
143
|
+
```txt
|
|
144
|
+
/magi:review 123 124
|
|
145
|
+
/magi:review --dry-run 123
|
|
146
|
+
/magi:merge 123
|
|
147
|
+
/magi:merge --dry-run 123
|
|
148
|
+
/magi:clear
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Docs
|
|
152
|
+
|
|
153
|
+
- [Commands](docs/commands/index.md)
|
|
154
|
+
- [Config](docs/config.md)
|
|
155
|
+
- [Prompts](docs/prompts.md)
|
|
156
|
+
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
Wouldn't you like to contribute? That's amazing! We have prepared a [contribution guide](CONTRIBUTING.md) to assist you.
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const MAGI_COMMANDS = {
|
|
2
|
+
"magi:clear": {
|
|
3
|
+
description: "Clear inactive Magi runs, sessions, worktrees, and outputs",
|
|
4
|
+
template: "Call the `magi_clear` tool.",
|
|
5
|
+
},
|
|
6
|
+
"magi:merge": {
|
|
7
|
+
description: "Review and merge pull requests with Magi",
|
|
8
|
+
template: [`Call the \`magi_merge\` tool.`, "PR: $ARGUMENTS"].join("\n"),
|
|
9
|
+
},
|
|
10
|
+
"magi:review": {
|
|
11
|
+
description: "Review pull requests with Magi",
|
|
12
|
+
template: [`Call the \`magi_review\` tool.`, "PR: $ARGUMENTS"].join("\n"),
|
|
13
|
+
},
|
|
14
|
+
"magi:validate": {
|
|
15
|
+
description: "Validate Magi config",
|
|
16
|
+
template: "Call the `magi_validate` tool.",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
const GLOBAL_CONFIG = join(homedir(), ".config", "opencode", "magi.json");
|
|
5
|
+
const PROJECT_CONFIG = join(".opencode", "magi.json");
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
return (!!value &&
|
|
8
|
+
typeof value === "object" &&
|
|
9
|
+
!Array.isArray(value) &&
|
|
10
|
+
Object.getPrototypeOf(value) === Object.prototype);
|
|
11
|
+
}
|
|
12
|
+
export function mergeMagiConfig(base, override) {
|
|
13
|
+
const merged = { ...base };
|
|
14
|
+
for (const [key, value] of Object.entries(override)) {
|
|
15
|
+
const existing = merged[key];
|
|
16
|
+
merged[key] =
|
|
17
|
+
isPlainObject(existing) && isPlainObject(value)
|
|
18
|
+
? mergeMagiConfig(existing, value)
|
|
19
|
+
: value;
|
|
20
|
+
}
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
|
23
|
+
async function readConfig(path) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error.code;
|
|
29
|
+
if (code === "ENOENT")
|
|
30
|
+
return null;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function loadConfig(directory, configPath) {
|
|
35
|
+
if (configPath) {
|
|
36
|
+
const path = isAbsolute(configPath)
|
|
37
|
+
? configPath
|
|
38
|
+
: join(directory, configPath);
|
|
39
|
+
const config = await readConfig(path);
|
|
40
|
+
if (!config)
|
|
41
|
+
throw new Error(`Magi config not found: ${path}`);
|
|
42
|
+
return { config: config, path };
|
|
43
|
+
}
|
|
44
|
+
const projectPath = join(directory, PROJECT_CONFIG);
|
|
45
|
+
const configs = await Promise.all([
|
|
46
|
+
readConfig(GLOBAL_CONFIG),
|
|
47
|
+
readConfig(projectPath),
|
|
48
|
+
]);
|
|
49
|
+
const loaded = configs
|
|
50
|
+
.map((config, index) => ({
|
|
51
|
+
config,
|
|
52
|
+
path: index === 0 ? GLOBAL_CONFIG : projectPath,
|
|
53
|
+
}))
|
|
54
|
+
.filter((item) => Boolean(item.config));
|
|
55
|
+
if (!loaded.length)
|
|
56
|
+
throw new Error(`Magi config not found. Tried: ${GLOBAL_CONFIG}, ${projectPath}`);
|
|
57
|
+
const config = loaded.reduce((merged, item) => mergeMagiConfig(merged, item.config), {});
|
|
58
|
+
return {
|
|
59
|
+
config: config,
|
|
60
|
+
path: loaded.map((item) => item.path).join(", "),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path";
|
|
2
|
+
const DEFAULT_OUTPUT_DIRS = {
|
|
3
|
+
pr: ".magi/runs/pr",
|
|
4
|
+
};
|
|
5
|
+
function resolvePath(directory, path) {
|
|
6
|
+
return isAbsolute(path) ? path : join(directory, path);
|
|
7
|
+
}
|
|
8
|
+
export function outputBaseDir(directory, config, kind) {
|
|
9
|
+
return resolvePath(directory, config.output?.dirs?.[kind] ?? DEFAULT_OUTPUT_DIRS[kind]);
|
|
10
|
+
}
|
|
11
|
+
export function outputBaseDirs(directory, config) {
|
|
12
|
+
return [outputBaseDir(directory, config, "pr")];
|
|
13
|
+
}
|
|
14
|
+
export function prRunOutputDir(input) {
|
|
15
|
+
return join(outputBaseDir(input.directory, input.config, "pr"), String(input.pr), ...(input.runId ? [input.runId] : []));
|
|
16
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import editorPermission from "../permissions/editor.json" with { type: "json" };
|
|
2
|
+
import commonPermission from "../permissions/common.json" with { type: "json" };
|
|
3
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
4
|
+
const DEFAULT_COMMON_PERMISSION = commonPermission;
|
|
5
|
+
const DEFAULT_REVIEWER_PERMISSION = DEFAULT_COMMON_PERMISSION;
|
|
6
|
+
const DEFAULT_EDITOR_PERMISSION = mergePermissions(DEFAULT_COMMON_PERMISSION, editorPermission);
|
|
7
|
+
export function reviewerKey(reviewer, index) {
|
|
8
|
+
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
9
|
+
}
|
|
10
|
+
export function validateReviewerId(id) {
|
|
11
|
+
return ID_PATTERN.test(id);
|
|
12
|
+
}
|
|
13
|
+
function clonePermissionValue(value) {
|
|
14
|
+
return typeof value === "string" ? value : { ...value };
|
|
15
|
+
}
|
|
16
|
+
export function mergePermissions(base, override) {
|
|
17
|
+
if (!override) {
|
|
18
|
+
return typeof base === "string"
|
|
19
|
+
? base
|
|
20
|
+
: Object.fromEntries(Object.entries(base).map(([key, value]) => [
|
|
21
|
+
key,
|
|
22
|
+
clonePermissionValue(value),
|
|
23
|
+
]));
|
|
24
|
+
}
|
|
25
|
+
if (typeof override === "string")
|
|
26
|
+
return override;
|
|
27
|
+
if (typeof base === "string") {
|
|
28
|
+
return Object.fromEntries(Object.entries(override).map(([key, value]) => [
|
|
29
|
+
key,
|
|
30
|
+
clonePermissionValue(value),
|
|
31
|
+
]));
|
|
32
|
+
}
|
|
33
|
+
const merged = Object.fromEntries(Object.entries(base).map(([key, value]) => [
|
|
34
|
+
key,
|
|
35
|
+
clonePermissionValue(value),
|
|
36
|
+
]));
|
|
37
|
+
for (const [permission, value] of Object.entries(override)) {
|
|
38
|
+
const existing = merged[permission];
|
|
39
|
+
merged[permission] =
|
|
40
|
+
existing && typeof existing !== "string" && typeof value !== "string"
|
|
41
|
+
? { ...existing, ...value }
|
|
42
|
+
: clonePermissionValue(value);
|
|
43
|
+
}
|
|
44
|
+
return merged;
|
|
45
|
+
}
|
|
46
|
+
export function resolveReviewerPermission(agents, reviewer) {
|
|
47
|
+
return mergePermissions(mergePermissions(DEFAULT_REVIEWER_PERMISSION, agents.permissions), reviewer.permission);
|
|
48
|
+
}
|
|
49
|
+
export function resolveEditorPermission(agents, editor) {
|
|
50
|
+
return mergePermissions(mergePermissions(DEFAULT_EDITOR_PERMISSION, agents.permissions), editor.permission);
|
|
51
|
+
}
|
|
52
|
+
export function resolveAgents(agents) {
|
|
53
|
+
return {
|
|
54
|
+
editor: agents.editor
|
|
55
|
+
? {
|
|
56
|
+
...agents.editor,
|
|
57
|
+
permission: resolveEditorPermission(agents, agents.editor),
|
|
58
|
+
}
|
|
59
|
+
: undefined,
|
|
60
|
+
reviewers: (agents.reviewers ?? []).map((reviewer, index) => ({
|
|
61
|
+
...reviewer,
|
|
62
|
+
key: reviewerKey(reviewer, index),
|
|
63
|
+
index,
|
|
64
|
+
permission: resolveReviewerPermission(agents, reviewer),
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function resolveRepository(config) {
|
|
69
|
+
if (!config.github?.owner)
|
|
70
|
+
throw new Error("github.owner is required");
|
|
71
|
+
if (!config.github?.repo)
|
|
72
|
+
throw new Error("github.repo is required");
|
|
73
|
+
return {
|
|
74
|
+
alias: config.github.repo,
|
|
75
|
+
agents: resolveAgents(config.agents),
|
|
76
|
+
automation: {
|
|
77
|
+
close: config.automation?.close ?? true,
|
|
78
|
+
merge: config.automation?.merge ?? true,
|
|
79
|
+
},
|
|
80
|
+
checks: {
|
|
81
|
+
exclude: config.checks?.exclude ?? [],
|
|
82
|
+
waitAfterEdit: config.checks?.waitAfterEdit ?? true,
|
|
83
|
+
waitBeforeReview: config.checks?.waitBeforeReview ?? true,
|
|
84
|
+
retryFailedJobs: config.checks?.retryFailedJobs ?? 3,
|
|
85
|
+
},
|
|
86
|
+
concurrency: {
|
|
87
|
+
runs: config.concurrency?.runs ?? 3,
|
|
88
|
+
reviewers: config.concurrency?.reviewers ?? 3,
|
|
89
|
+
},
|
|
90
|
+
github: {
|
|
91
|
+
apiRetryAttempts: config.github.apiRetryAttempts ?? 3,
|
|
92
|
+
host: config.github.host ?? "github.com",
|
|
93
|
+
owner: config.github.owner,
|
|
94
|
+
repo: config.github.repo,
|
|
95
|
+
},
|
|
96
|
+
language: config.language,
|
|
97
|
+
merge: {
|
|
98
|
+
approvalPolicy: config.merge?.approvalPolicy ?? "majority",
|
|
99
|
+
method: config.merge?.method ?? "squash",
|
|
100
|
+
auto: config.merge?.auto ?? true,
|
|
101
|
+
deleteBranch: config.merge?.deleteBranch ?? true,
|
|
102
|
+
mergeQueue: config.merge?.mergeQueue ?? false,
|
|
103
|
+
maxThreadResolutionCycles: config.merge?.maxThreadResolutionCycles ?? 5,
|
|
104
|
+
},
|
|
105
|
+
prompts: config.prompts ?? {},
|
|
106
|
+
safety: {
|
|
107
|
+
allowAuthors: config.safety?.allowAuthors ?? [],
|
|
108
|
+
blockedPaths: config.safety?.blockedPaths ?? [],
|
|
109
|
+
maxChangedFiles: config.safety?.maxChangedFiles,
|
|
110
|
+
requiredLabels: config.safety?.requiredLabels ?? [],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|