opencode-magi 0.0.0-dev-20260525164734 → 0.0.0-dev-20260525205150

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 CHANGED
@@ -64,20 +64,18 @@ Add the following content to the configuration file.
64
64
  "agents": {
65
65
  "refs": {
66
66
  "account-1": {
67
- "model": "openai/gpt-5.5",
68
- "account": "account-1"
67
+ "model": "openai/gpt-5.5"
69
68
  },
70
69
  "account-2": {
71
- "model": "anthropic/claude-opus-4-7",
72
- "account": "account-2"
70
+ "model": "anthropic/claude-opus-4-7"
73
71
  },
74
72
  "account-3": {
75
- "model": "opencode/kimi-k2-6",
76
- "account": "account-3"
73
+ "model": "opencode/kimi-k2-6"
77
74
  }
78
75
  }
79
76
  },
80
77
  "review": {
78
+ "account": "your-account",
81
79
  "reviewers": [
82
80
  { "ref": "account-1" },
83
81
  { "ref": "account-2" },
@@ -87,19 +85,22 @@ Add the following content to the configuration file.
87
85
  }
88
86
  ```
89
87
 
90
- After `refs` are expanded, `review.reviewers[].account` is the GitHub account used to post reviews and approvals in the default `review.mode: "multi"`. Must be authenticated with `gh auth token --user <account>` and must be unique.
88
+ By default, `review.mode` is `"single"`. Magi uses one `review.account` to post reviewer-originated GitHub mutations while still running multiple logical reviewer agents and preserving majority voting, finding validation, and close reconsideration. The account must be authenticated with `gh auth token --user <account>`.
91
89
 
92
- For individual setups, use `review.mode: "single"` with one `review.account`. Magi still runs an odd number of at least 3 logical reviewer agents and keeps majority voting, finding validation, and close reconsideration, but GitHub branch protection sees only the one configured review account.
90
+ For team setups that need separate GitHub review identities, set `review.mode: "multi"` and configure a unique account for each reviewer.
93
91
 
94
92
  ```json
95
93
  {
96
94
  "review": {
97
- "mode": "single",
98
- "account": "your-account",
95
+ "mode": "multi",
99
96
  "reviewers": [
100
- { "id": "general", "model": "openai/gpt-5.5" },
101
- { "id": "security", "model": "anthropic/claude-opus-4-7" },
102
- { "id": "compat", "model": "opencode/kimi-k2-6" }
97
+ { "id": "general", "model": "openai/gpt-5.5", "account": "account-1" },
98
+ {
99
+ "id": "security",
100
+ "model": "anthropic/claude-opus-4-7",
101
+ "account": "account-2"
102
+ },
103
+ { "id": "compat", "model": "opencode/kimi-k2-6", "account": "account-3" }
103
104
  ]
104
105
  }
105
106
  }
@@ -113,7 +113,7 @@ export function resolveAgents(config) {
113
113
  const agents = config.agents ?? {};
114
114
  const editor = config.merge?.editor;
115
115
  const creator = config.triage?.creator;
116
- const singleReviewAccount = config.review?.mode === "single" ? config.review.account : undefined;
116
+ const singleReviewAccount = config.review && config.mode !== "multi" ? config.account : undefined;
117
117
  return {
118
118
  editor: editor
119
119
  ? {
@@ -186,6 +186,8 @@ export function resolveRepository(config) {
186
186
  repo: config.github.repo,
187
187
  },
188
188
  language: config.language,
189
+ account: config.account,
190
+ mode: config.mode ?? "single",
189
191
  merge: {
190
192
  approvalPolicy: config.review?.merge?.approvalPolicy ?? "majority",
191
193
  method: config.review?.merge?.method ?? "squash",
@@ -206,10 +208,6 @@ export function resolveRepository(config) {
206
208
  review: config.review?.prompts?.review,
207
209
  reviewGuidelines: config.review?.prompts?.reviewGuidelines,
208
210
  },
209
- review: {
210
- account: config.review?.account,
211
- mode: config.review?.mode ?? "multi",
212
- },
213
211
  reviewAutomation: {
214
212
  close: config.review?.automation?.close ?? false,
215
213
  merge: config.review?.automation?.merge ?? true,
@@ -13,11 +13,13 @@ const TRIAGE_CATEGORY_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
13
13
  const RESERVED_TRIAGE_CATEGORY_IDS = new Set(["ASK", "none"]);
14
14
  const CONFIG_KEYS = new Set([
15
15
  "$schema",
16
+ "account",
16
17
  "agents",
17
18
  "clear",
18
19
  "github",
19
20
  "language",
20
21
  "merge",
22
+ "mode",
21
23
  "output",
22
24
  "review",
23
25
  "triage",
@@ -54,12 +56,10 @@ const TRIAGE_CREATOR_KEYS = new Set([
54
56
  const AUTHOR_KEYS = new Set(["email", "name"]);
55
57
  const GITHUB_KEYS = new Set(["apiRetryAttempts", "host", "owner", "repo"]);
56
58
  const REVIEW_KEYS = new Set([
57
- "account",
58
59
  "automation",
59
60
  "checks",
60
61
  "concurrency",
61
62
  "merge",
62
- "mode",
63
63
  "output",
64
64
  "prompts",
65
65
  "reviewers",
@@ -386,7 +386,7 @@ function validateAndNormalizeModel(target, path, errors, catalog) {
386
386
  }
387
387
  errors.push(`${path} must contain at least one usable OpenCode model candidate${candidateErrors.length ? ` (${candidateErrors.join("; ")})` : ""}`);
388
388
  }
389
- function validateReviewerList(reviewers, path, errors, catalog, mode = "multi") {
389
+ function validateReviewerList(reviewers, path, errors, catalog, mode = "single") {
390
390
  if (reviewers == null)
391
391
  return;
392
392
  if (!Array.isArray(reviewers)) {
@@ -456,7 +456,7 @@ function validateTriageAgentList(voters, path, errors, catalog) {
456
456
  }
457
457
  });
458
458
  }
459
- function validateResolvedReviewers(reviewers, path, errors, mode = "multi") {
459
+ function validateResolvedReviewers(reviewers, path, errors, mode = "single") {
460
460
  const keys = new Set();
461
461
  const accounts = new Set();
462
462
  for (const reviewer of reviewers) {
@@ -469,16 +469,15 @@ function validateResolvedReviewers(reviewers, path, errors, mode = "multi") {
469
469
  }
470
470
  }
471
471
  function reviewMode(config) {
472
- return config.review?.mode === "single" ? "single" : "multi";
472
+ return config.mode === "multi" ? "multi" : "single";
473
473
  }
474
474
  function validateReviewIdentity(config, errors) {
475
- const mode = config.review?.mode;
475
+ const mode = config.mode;
476
476
  if (mode != null && mode !== "multi" && mode !== "single") {
477
- errors.push("review.mode must be multi or single");
477
+ errors.push("mode must be multi or single");
478
478
  }
479
- validateString(config.review?.account, "review.account", errors);
480
- if (mode === "single" && !config.review?.account) {
481
- errors.push("review.account is required when review.mode is single");
479
+ if ((mode == null || mode === "single") && !config.account) {
480
+ errors.push("account is required when mode is single");
482
481
  }
483
482
  }
484
483
  function validateResolvedTriageAgents(agents, path, errors) {
@@ -924,8 +923,8 @@ async function validateAuth(config, exec, errors) {
924
923
  const accounts = new Set();
925
924
  const agents = resolveAgents(config);
926
925
  if (reviewMode(config) === "single") {
927
- if (config.review?.account)
928
- accounts.add(config.review.account);
926
+ if (config.account)
927
+ accounts.add(config.account);
929
928
  }
930
929
  else {
931
930
  for (const reviewer of agents.reviewers)
@@ -978,8 +977,8 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
978
977
  return;
979
978
  const agents = resolveAgents(config);
980
979
  const reviewAccounts = reviewMode(config) === "single"
981
- ? config.review?.account
982
- ? [config.review.account]
980
+ ? config.account
981
+ ? [config.account]
983
982
  : []
984
983
  : agents.reviewers.map((reviewer) => reviewer.account);
985
984
  await Promise.all(reviewAccounts.map(async (account) => {
@@ -1040,6 +1039,7 @@ export async function validateConfig(config, options = {}) {
1040
1039
  validateJsonSchema(config, errors);
1041
1040
  validateKnownKeys(config, "config", CONFIG_KEYS, errors);
1042
1041
  validateString(config.$schema, "$schema", errors);
1042
+ validateString(config.account, "account", errors);
1043
1043
  validateString(config.language, "language", errors);
1044
1044
  if (config.agents != null && !isPlainObject(config.agents)) {
1045
1045
  errors.push("agents must be an object");
@@ -213,7 +213,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
213
213
  worktreePath,
214
214
  });
215
215
  const artifactDir = outputDir(input);
216
- const singleReviewMode = input.repository.review?.mode === "single";
216
+ const singleReviewMode = input.repository.mode !== "multi";
217
217
  const reviewerKeys = input.repository.agents.reviewers.map((reviewer) => reviewer.key);
218
218
  const singleModeThreads = singleReviewMode
219
219
  ? assignThreadsByReviewFindingMarker({
@@ -221,7 +221,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
221
221
  pr: input.pr,
222
222
  reviewerKeys,
223
223
  threads: options.dryRunThreads == null
224
- ? await fetchUnresolvedThreads(input.exec, input.repository, input.pr, input.repository.review?.account ?? "")
224
+ ? await fetchUnresolvedThreads(input.exec, input.repository, input.pr, input.repository.account ?? "")
225
225
  : Object.values(options.dryRunThreads).flat(),
226
226
  })
227
227
  : undefined;
@@ -412,7 +412,7 @@ async function runRereview(input, worktreePath, previousHeadSha, cycle, sessionI
412
412
  ? { consensus: `dry-run:would-post-single-review:${verdict}` }
413
413
  : {
414
414
  consensus: await (async () => {
415
- const account = input.repository.review?.account ?? "";
415
+ const account = input.repository.account ?? "";
416
416
  await Promise.all(entries.flatMap((entry) => entry.output.resolve.map((item) => resolveThread(input.exec, input.repository, account, item.threadId))));
417
417
  await Promise.all(entries.flatMap((entry) => entry.output.followUps.map((item) => postReply(input.exec, input.repository, input.pr, account, item.commentId, [
418
418
  `**Reviewer:** ${entry.reviewer}`,
@@ -16,12 +16,14 @@ import { formatReviewReport } from "./report";
16
16
  import { buildReviewContextSnapshot, renderReviewContext, } from "./review-context";
17
17
  import { checkSafetyGate, hasSafetyGate } from "./safety";
18
18
  function resolvedReviewMode(repository) {
19
- return repository.review?.mode === "single" ? "single" : "multi";
19
+ return repository.mode === "multi" ? "multi" : "single";
20
20
  }
21
21
  export function reviewPostingAccount(repository, reviewer) {
22
- return resolvedReviewMode(repository) === "single"
23
- ? (repository.review?.account ?? reviewer.account)
24
- : reviewer.account;
22
+ if (resolvedReviewMode(repository) !== "single")
23
+ return reviewer.account;
24
+ if (repository.account)
25
+ return repository.account;
26
+ throw new Error("account is required for single review mode");
25
27
  }
26
28
  function reviewAssignmentKey(repository, reviewer) {
27
29
  return resolvedReviewMode(repository) === "single"
@@ -579,9 +581,9 @@ function singleFindingBody(input) {
579
581
  ].join("\n");
580
582
  }
581
583
  export async function postSingleConsensusReview(input) {
582
- const account = input.repository.review?.account;
584
+ const account = input.repository.account;
583
585
  if (!account)
584
- throw new Error("review.account is required for single review mode");
586
+ throw new Error("account is required for single review mode");
585
587
  const body = singleReviewBody(input);
586
588
  if (input.verdict === "MERGE") {
587
589
  return postApproval(input.exec, input.repository, input.pr, account, body);
@@ -927,7 +929,7 @@ export async function runReview(input) {
927
929
  const reviewerAccounts = input.repository.agents.reviewers.map((reviewer) => reviewer.account);
928
930
  const preliminaryMode = singleReviewMode
929
931
  ? resolveSingleAccountReviewMode({
930
- account: input.repository.review?.account ?? "",
932
+ account: input.repository.account ?? "",
931
933
  current: freshnessTarget,
932
934
  pr: input.pr,
933
935
  reviewerKeys,
@@ -942,7 +944,7 @@ export async function runReview(input) {
942
944
  return (preliminaryMode.assignments.get(reviewAssignmentKey(input.repository, reviewer))?.type === "skip");
943
945
  });
944
946
  if (singleReviewMode) {
945
- const account = input.repository.review?.account ?? "";
947
+ const account = input.repository.account ?? "";
946
948
  const threads = await fetchUnresolvedThreads(exec, input.repository, input.pr, account);
947
949
  const assigned = assignThreadsByReviewFindingMarker({
948
950
  fallbackReviewerKeys: reviewerKeys,
@@ -973,7 +975,7 @@ export async function runReview(input) {
973
975
  const mode = singleReviewMode
974
976
  ? pendingThreadReplyReviewers.size
975
977
  ? resolveSingleAccountReviewMode({
976
- account: input.repository.review?.account ?? "",
978
+ account: input.repository.account ?? "",
977
979
  current: freshnessTarget,
978
980
  pendingReviewers: pendingThreadReplyReviewers,
979
981
  pr: input.pr,
@@ -1368,7 +1370,7 @@ export async function runReview(input) {
1368
1370
  consensus: input.dryRun
1369
1371
  ? `dry-run:would-post-single-review:${verdict}`
1370
1372
  : await (async () => {
1371
- const account = input.repository.review?.account ?? "";
1373
+ const account = input.repository.account ?? "";
1372
1374
  await Promise.all(Object.values(activeOutputs).flatMap((output) => {
1373
1375
  if (!("resolve" in output))
1374
1376
  return [];
@@ -1414,7 +1416,7 @@ export async function runReview(input) {
1414
1416
  ]))),
1415
1417
  };
1416
1418
  const automationAccount = singleReviewMode
1417
- ? input.repository.review?.account
1419
+ ? input.repository.account
1418
1420
  : input.repository.agents.reviewers[0]?.account;
1419
1421
  const enableReviewAutomation = input.enableReviewAutomation ?? true;
1420
1422
  if (enableReviewAutomation &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260525164734",
3
+ "version": "0.0.0-dev-20260525205150",
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>",
package/schema.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "additionalProperties": false,
6
6
  "properties": {
7
7
  "$schema": { "type": "string" },
8
+ "account": { "type": "string", "minLength": 1 },
8
9
  "agents": {
9
10
  "type": "object",
10
11
  "additionalProperties": false,
@@ -44,6 +45,7 @@
44
45
  },
45
46
  "language": { "type": "string" },
46
47
  "merge": { "$ref": "#/$defs/merge" },
48
+ "mode": { "enum": ["multi", "single"], "default": "single" },
47
49
  "output": {
48
50
  "type": "object",
49
51
  "additionalProperties": false,
@@ -366,8 +368,6 @@
366
368
  "type": "object",
367
369
  "additionalProperties": false,
368
370
  "properties": {
369
- "mode": { "enum": ["multi", "single"], "default": "multi" },
370
- "account": { "type": "string", "minLength": 1 },
371
371
  "reviewers": {
372
372
  "type": "array",
373
373
  "minItems": 3,