opencode-magi 0.0.0-dev-20260727045328 → 0.0.0-dev-20260727095903

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/dist/constant.js CHANGED
@@ -60,11 +60,7 @@ export const DEFAULT_CONFIG = {
60
60
  queue: false,
61
61
  },
62
62
  output: ".magi/runs/pr",
63
- safety: {
64
- allowAuthors: [],
65
- blockedPaths: [],
66
- requiredLabels: [],
67
- },
63
+ safety: [],
68
64
  worktree: ".magi/worktrees/pr",
69
65
  },
70
66
  triage: {
@@ -127,7 +127,7 @@ export const merge = function (magi) {
127
127
  await run.editCycles(async () => editCycle(run));
128
128
  const automation = await run.automate();
129
129
  if (automation === "CONFLICT" &&
130
- run.config.merge.automation.conflict) {
130
+ run.isAutomationEnabled(run.config.merge.automation.conflict)) {
131
131
  await run.editCycles(async (cycle) => editCycle(run, { conflict: true, cycle }));
132
132
  await run.automate();
133
133
  }
@@ -1,6 +1,7 @@
1
1
  import { MagiError } from "@/magi";
2
2
  import { Prompt } from "@/prompts";
3
- import { command, filterEmpty, ignoreError, isArray, isObject, isString, loop, marker, omitNullish, quote, retry, wait, Worker, } from "@/utils";
3
+ import { command, filterEmpty, ignoreError, isArray, isBoolean, isObject, isString, loop, marker, omitNullish, quote, retry, wait, Worker, } from "@/utils";
4
+ import { getConditionErrors } from "./check";
4
5
  const events = {
5
6
  APPROVED: "APPROVE",
6
7
  CHANGES_REQUESTED: "REQUEST_CHANGES",
@@ -181,9 +182,8 @@ export async function automate() {
181
182
  throw new MagiError("blocked", "PR verdict not found.");
182
183
  if (!["APPROVED", "CLOSED"].includes(this.state.pr.verdict))
183
184
  return "SKIPPED";
184
- const automation = this.config[this.state.command].automation;
185
185
  const action = this.state.pr.verdict === "APPROVED" ? "merge" : "close";
186
- if (!automation[action]) {
186
+ if (!isAutomationEnabled.call(this, this.config[this.state.command].automation[action])) {
187
187
  await this.updateState({ pr: { automation: "SKIPPED" } });
188
188
  await this.updateEvent(`Skipped ${action} automation.`);
189
189
  return "SKIPPED";
@@ -345,6 +345,18 @@ export async function automate() {
345
345
  await this.updateEvent(`Finished ${action} automation.`);
346
346
  return action === "merge" ? "MERGED" : "CLOSED";
347
347
  }
348
+ export function isAutomationEnabled(conditions) {
349
+ if (isBoolean(conditions))
350
+ return conditions;
351
+ const edited = !this.state.dryRun &&
352
+ this.state.editor?.outputs?.some(({ commitSha }) => !!commitSha);
353
+ return conditions.every(([expected, condition]) => {
354
+ const errors = getConditionErrors(condition, this.state.pr.metadata, this.state.pr.files ?? []);
355
+ const matches = !errors.length &&
356
+ (!("edited" in condition) || condition.edited === !!edited);
357
+ return matches === expected;
358
+ });
359
+ }
348
360
  function hasFailedChecks(checks) {
349
361
  if (!isArray(checks))
350
362
  return false;
@@ -1,7 +1,7 @@
1
1
  import picomatch from "picomatch";
2
2
  import { MagiError } from "@/magi";
3
3
  import { Prompt } from "@/prompts";
4
- import { command, filterEmpty, isNumber, omitNullish, quote, retry, Worker, } from "@/utils";
4
+ import { command, filterEmpty, isBoolean, isNumber, omitNullish, quote, retry, Worker, } from "@/utils";
5
5
  const ci = {
6
6
  isExcluded(exclude, { name }) {
7
7
  return exclude.some((pattern) => {
@@ -22,6 +22,65 @@ const ci = {
22
22
  return !this.isFailed(check) && !this.isPassed(check);
23
23
  },
24
24
  };
25
+ function matchesValues(filter, values) {
26
+ if (!filter)
27
+ return true;
28
+ if ("include" in filter &&
29
+ !values.some((value) => filter.include.includes(value)))
30
+ return false;
31
+ return !("exclude" in filter &&
32
+ values.some((value) => filter.exclude.includes(value)));
33
+ }
34
+ function matchesPatterns(filter, values) {
35
+ if (!filter)
36
+ return true;
37
+ if ("include" in filter) {
38
+ const isIncluded = picomatch(filter.include, { dot: true });
39
+ if (!values.some((value) => isIncluded(value)))
40
+ return false;
41
+ }
42
+ if ("exclude" in filter) {
43
+ const isExcluded = picomatch(filter.exclude, { dot: true });
44
+ if (values.some((value) => isExcluded(value)))
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ export function getConditionErrors(condition, metadata, files) {
50
+ const errors = [];
51
+ const branches = condition.branches
52
+ ? {
53
+ base: "base" in condition.branches ? condition.branches.base : undefined,
54
+ head: "head" in condition.branches ? condition.branches.head : undefined,
55
+ }
56
+ : {};
57
+ if (!matchesValues(condition.authors, [metadata.user.login]))
58
+ errors.push(`Author does not match safety filter: ${metadata.user.login}.`);
59
+ if (!matchesPatterns(branches.base, [metadata.base.ref]))
60
+ errors.push(`Base branch does not match safety filter: ${metadata.base.ref}.`);
61
+ if (!matchesPatterns(branches.head, [metadata.head.ref]))
62
+ errors.push(`Head branch does not match safety filter: ${metadata.head.ref}.`);
63
+ if (!matchesValues(condition.labels, metadata.labels.map(({ name }) => name)))
64
+ errors.push(`Labels do not match safety filter.`);
65
+ if (!matchesPatterns(condition.paths, files))
66
+ errors.push(`Paths do not match safety filter.`);
67
+ if (isNumber(condition.maxChangedFiles) &&
68
+ metadata.changed_files > condition.maxChangedFiles)
69
+ errors.push(`Changed files exceed limit: ${metadata.changed_files} > ${condition.maxChangedFiles}.`);
70
+ return errors;
71
+ }
72
+ function getSafetyErrors(conditions, metadata, files) {
73
+ if (isBoolean(conditions))
74
+ return conditions ? [] : ["Safety is disabled."];
75
+ return conditions.flatMap(([expected, condition], index) => {
76
+ const errors = getConditionErrors(condition, metadata, files);
77
+ if (!errors.length === expected)
78
+ return [];
79
+ if (expected)
80
+ return errors;
81
+ return [`Safety condition ${index + 1} unexpectedly matched.`];
82
+ });
83
+ }
25
84
  export async function checkPr() {
26
85
  this.context.abort.throwIfAborted();
27
86
  await this.updateState({ status: "running" });
@@ -31,26 +90,7 @@ export async function checkPr() {
31
90
  throw new MagiError("blocked", `PR is not open.`);
32
91
  if (metadata.draft)
33
92
  throw new MagiError("blocked", `PR is a draft.`);
34
- const errors = [];
35
- if (this.config.review.safety.allowAuthors.length)
36
- if (!this.config.review.safety.allowAuthors.includes(metadata.user.login))
37
- errors.push(`Author is not allowed: ${metadata.user.login}.`);
38
- if (this.config.review.safety.requiredLabels.length) {
39
- const missingLabels = this.config.review.safety.requiredLabels.filter((label) => !metadata.labels.some(({ name }) => name === label));
40
- if (missingLabels.length)
41
- errors.push(`Required labels missing: ${missingLabels.join(", ")}.`);
42
- }
43
- if (isNumber(this.config.review.safety.maxChangedFiles) &&
44
- metadata.changed_files > this.config.review.safety.maxChangedFiles)
45
- errors.push(`Changed files exceed limit: ${metadata.changed_files} > ${this.config.review.safety.maxChangedFiles}.`);
46
- if (this.config.review.safety.blockedPaths.length) {
47
- const isBlocked = picomatch(this.config.review.safety.blockedPaths, {
48
- dot: true,
49
- });
50
- const blocked = files.filter((file) => isBlocked(file));
51
- if (blocked.length)
52
- errors.push(`Blocked paths changed: ${blocked.join(", ")}.`);
53
- }
93
+ const errors = getSafetyErrors(this.config.review.safety, metadata, files);
54
94
  if (errors.length)
55
95
  throw new MagiError("blocked", `PR is safety blocked. ${errors.join(" ")}`);
56
96
  if (this.config.mode === "single") {
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { MagiError } from "@/magi";
3
3
  import { createExecWithGitHubApiRetry, quote } from "@/utils";
4
- import { automate, postReviews } from "./action";
4
+ import { automate, isAutomationEnabled, postReviews } from "./action";
5
5
  import { checkCi, checkPr, classifyChecks, rerunChecks } from "./check";
6
6
  import { checkExistingReviews, fetchReviewContext } from "./context";
7
7
  import { createReport } from "./report";
@@ -62,6 +62,7 @@ export class Review {
62
62
  validateFindings = validateFindings;
63
63
  reconsiderClose = reconsiderClose;
64
64
  postReviews = postReviews;
65
+ isAutomationEnabled = isAutomationEnabled;
65
66
  automate = automate;
66
67
  createReport = createReport;
67
68
  async cleanup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.0.0-dev-20260727045328",
3
+ "version": "0.0.0-dev-20260727095903",
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
@@ -174,21 +174,125 @@
174
174
  "variant": { "type": "string" }
175
175
  }
176
176
  },
177
+ "safetyFilter": {
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "minProperties": 1,
181
+ "properties": {
182
+ "exclude": { "type": "array", "items": { "type": "string" } },
183
+ "include": { "type": "array", "items": { "type": "string" } }
184
+ }
185
+ },
186
+ "safetyBranchFilter": {
187
+ "type": "object",
188
+ "additionalProperties": false,
189
+ "minProperties": 1,
190
+ "properties": {
191
+ "base": { "$ref": "#/$defs/safetyFilter" },
192
+ "head": { "$ref": "#/$defs/safetyFilter" }
193
+ }
194
+ },
195
+ "safety": {
196
+ "type": "object",
197
+ "additionalProperties": false,
198
+ "properties": {
199
+ "authors": { "$ref": "#/$defs/safetyFilter" },
200
+ "branches": { "$ref": "#/$defs/safetyBranchFilter" },
201
+ "labels": { "$ref": "#/$defs/safetyFilter" },
202
+ "maxChangedFiles": { "type": "integer", "minimum": 0 },
203
+ "paths": { "$ref": "#/$defs/safetyFilter" }
204
+ }
205
+ },
206
+ "conditions": {
207
+ "oneOf": [
208
+ { "type": "boolean" },
209
+ {
210
+ "type": "array",
211
+ "items": {
212
+ "type": "array",
213
+ "minItems": 2,
214
+ "maxItems": 2,
215
+ "prefixItems": [{ "type": "boolean" }, { "$ref": "#/$defs/safety" }]
216
+ }
217
+ }
218
+ ]
219
+ },
220
+ "automationSafety": {
221
+ "type": "object",
222
+ "additionalProperties": false,
223
+ "properties": {
224
+ "authors": { "$ref": "#/$defs/safetyFilter" },
225
+ "branches": { "$ref": "#/$defs/safetyBranchFilter" },
226
+ "labels": { "$ref": "#/$defs/safetyFilter" },
227
+ "paths": { "$ref": "#/$defs/safetyFilter" }
228
+ }
229
+ },
230
+ "automationConditions": {
231
+ "oneOf": [
232
+ { "type": "boolean" },
233
+ {
234
+ "type": "array",
235
+ "items": {
236
+ "type": "array",
237
+ "minItems": 2,
238
+ "maxItems": 2,
239
+ "prefixItems": [
240
+ { "type": "boolean" },
241
+ { "$ref": "#/$defs/automationSafety" }
242
+ ]
243
+ }
244
+ }
245
+ ]
246
+ },
247
+ "mergeAutomationConditions": {
248
+ "oneOf": [
249
+ { "type": "boolean" },
250
+ {
251
+ "type": "array",
252
+ "items": {
253
+ "type": "array",
254
+ "minItems": 2,
255
+ "maxItems": 2,
256
+ "prefixItems": [
257
+ { "type": "boolean" },
258
+ { "$ref": "#/$defs/mergeAutomationSafety" }
259
+ ]
260
+ }
261
+ }
262
+ ]
263
+ },
264
+ "mergeAutomationSafety": {
265
+ "type": "object",
266
+ "additionalProperties": false,
267
+ "properties": {
268
+ "authors": { "$ref": "#/$defs/safetyFilter" },
269
+ "branches": { "$ref": "#/$defs/safetyBranchFilter" },
270
+ "edited": { "type": "boolean" },
271
+ "labels": { "$ref": "#/$defs/safetyFilter" },
272
+ "paths": { "$ref": "#/$defs/safetyFilter" }
273
+ }
274
+ },
177
275
  "automation": {
178
276
  "type": "object",
179
277
  "additionalProperties": false,
180
278
  "properties": {
181
- "merge": { "type": "boolean", "default": true },
182
- "close": { "type": "boolean" }
279
+ "merge": { "$ref": "#/$defs/automationConditions", "default": true },
280
+ "close": { "$ref": "#/$defs/automationConditions", "default": false }
183
281
  }
184
282
  },
185
283
  "mergeAutomation": {
186
284
  "type": "object",
187
285
  "additionalProperties": false,
188
286
  "properties": {
189
- "merge": { "type": "boolean", "default": true },
190
- "close": { "type": "boolean", "default": false },
191
- "conflict": { "type": "boolean", "default": false }
287
+ "merge": {
288
+ "$ref": "#/$defs/mergeAutomationConditions",
289
+ "default": true
290
+ },
291
+ "close": {
292
+ "$ref": "#/$defs/mergeAutomationConditions",
293
+ "default": false
294
+ },
295
+ "conflict": { "$ref": "#/$defs/automationConditions", "default": false }
192
296
  }
193
297
  },
194
298
  "reviewChecks": {
@@ -222,16 +326,6 @@
222
326
  "runs": { "type": "integer", "minimum": 1, "default": 3 }
223
327
  }
224
328
  },
225
- "safety": {
226
- "type": "object",
227
- "additionalProperties": false,
228
- "properties": {
229
- "allowAuthors": { "type": "array", "items": { "type": "string" } },
230
- "blockedPaths": { "type": "array", "items": { "type": "string" } },
231
- "maxChangedFiles": { "type": "integer", "minimum": 0 },
232
- "requiredLabels": { "type": "array", "items": { "type": "string" } }
233
- }
234
- },
235
329
  "reviewPrompts": {
236
330
  "type": "object",
237
331
  "additionalProperties": false,
@@ -376,7 +470,7 @@
376
470
  },
377
471
  "prompts": { "$ref": "#/$defs/reviewPrompts" },
378
472
  "checks": { "$ref": "#/$defs/reviewChecks" },
379
- "safety": { "$ref": "#/$defs/safety" },
473
+ "safety": { "$ref": "#/$defs/conditions" },
380
474
  "automation": { "$ref": "#/$defs/automation" },
381
475
  "concurrency": { "$ref": "#/$defs/concurrency" },
382
476
  "merge": { "$ref": "#/$defs/reviewMerge" },