opencode-magi 0.0.0-dev-20260519011027

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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/commands.js +18 -0
  4. package/dist/config/load.js +62 -0
  5. package/dist/config/output.js +16 -0
  6. package/dist/config/resolve.js +113 -0
  7. package/dist/config/validate.js +580 -0
  8. package/dist/config/worktree.js +13 -0
  9. package/dist/github/commands.js +398 -0
  10. package/dist/github/retry.js +44 -0
  11. package/dist/index.js +540 -0
  12. package/dist/orchestrator/abort.js +9 -0
  13. package/dist/orchestrator/ci.js +568 -0
  14. package/dist/orchestrator/findings.js +66 -0
  15. package/dist/orchestrator/majority.js +48 -0
  16. package/dist/orchestrator/merge.js +836 -0
  17. package/dist/orchestrator/model.js +202 -0
  18. package/dist/orchestrator/pool.js +15 -0
  19. package/dist/orchestrator/report.js +168 -0
  20. package/dist/orchestrator/review.js +791 -0
  21. package/dist/orchestrator/run-manager.js +1670 -0
  22. package/dist/orchestrator/safety.js +44 -0
  23. package/dist/permissions/common.json +24 -0
  24. package/dist/permissions/editor.json +7 -0
  25. package/dist/prompts/compose.js +298 -0
  26. package/dist/prompts/contracts.js +189 -0
  27. package/dist/prompts/output.js +260 -0
  28. package/dist/prompts/templates/ci-classification-after-edit.md +16 -0
  29. package/dist/prompts/templates/ci-classification.md +9 -0
  30. package/dist/prompts/templates/close-reconsideration.md +6 -0
  31. package/dist/prompts/templates/edit.md +9 -0
  32. package/dist/prompts/templates/finding-validation.md +7 -0
  33. package/dist/prompts/templates/rereview-close-reconsideration.md +6 -0
  34. package/dist/prompts/templates/rereview.md +16 -0
  35. package/dist/prompts/templates/review.md +7 -0
  36. package/dist/types.js +1 -0
  37. package/package.json +67 -0
  38. package/schema.json +206 -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,161 @@
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
+ Restart OpenCode. Done.
40
+
41
+ ### Configure
42
+
43
+ Configure global defaults in `~/.config/opencode/magi.json` and project overrides in `<project>/.opencode/magi.json`.
44
+
45
+ Magi config files are merged by OpenCode Magi, not by OpenCode. Priority, lowest to highest.
46
+
47
+ 1. `~/.config/opencode/magi.json`
48
+ 2. `<project>/.opencode/magi.json`
49
+
50
+ #### Set global config
51
+
52
+ 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.
53
+
54
+ ```bash
55
+ mkdir -p ~/.config/opencode
56
+ touch ~/.config/opencode/magi.json
57
+ ```
58
+
59
+ Add the following content to the configuration file.
60
+
61
+ ```json
62
+ {
63
+ "$schema": "https://raw.githubusercontent.com/magi-ai/opencode-magi/main/schema.json",
64
+ "agents": {
65
+ "reviewers": [
66
+ {
67
+ "account": "your-account-1",
68
+ "model": "openai/gpt-5.5"
69
+ },
70
+ {
71
+ "account": "your-account-2",
72
+ "model": "anthropic/claude-opus-4-7"
73
+ },
74
+ {
75
+ "account": "your-account-3",
76
+ "model": "opencode/kimi-k2-6"
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ `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.
84
+
85
+ #### Set project config
86
+
87
+ Global config is optional, but project config is required.
88
+
89
+ ```bash
90
+ cd <project>
91
+ mkdir -p .opencode
92
+ touch .opencode/magi.json
93
+ ```
94
+
95
+ Add the following content to the configuration file.
96
+
97
+ ```json
98
+ {
99
+ "$schema": "https://raw.githubusercontent.com/magi-ai/opencode-magi/main/schema.json",
100
+ "github": {
101
+ "owner": "your-owner",
102
+ "repo": "your-repo"
103
+ },
104
+ "agents": {
105
+ "reviewers": [
106
+ {
107
+ "account": "your-account-1",
108
+ "model": "openai/gpt-5.5"
109
+ },
110
+ {
111
+ "account": "your-account-2",
112
+ "model": "anthropic/claude-opus-4-7"
113
+ },
114
+ {
115
+ "account": "your-account-3",
116
+ "model": "opencode/kimi-k2-6"
117
+ }
118
+ ],
119
+ "editor": {
120
+ "account": "your-editor-account",
121
+ "model": "openai/gpt-5.5",
122
+ "author": {
123
+ "name": "your-account",
124
+ "email": "your-email@example.com"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ `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.
132
+
133
+ #### Validate config
134
+
135
+ After creating or updating your global or project configuration, validate it.
136
+
137
+ ```txt
138
+ /magi:validate
139
+ ```
140
+
141
+ ### Commands
142
+
143
+ Run commands from OpenCode.
144
+
145
+ ```txt
146
+ /magi:review 123 124
147
+ /magi:review --dry-run 123
148
+ /magi:merge 123
149
+ /magi:merge --dry-run 123
150
+ /magi:clear
151
+ ```
152
+
153
+ ## Docs
154
+
155
+ - [Commands](docs/commands/index.md)
156
+ - [Config](docs/config.md)
157
+ - [Prompts](docs/prompts.md)
158
+
159
+ ## Contributing
160
+
161
+ Wouldn't you like to contribute? That's amazing! We have prepared a [contribution guide](CONTRIBUTING.md) to assist you.
@@ -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
+ }