opencode-magi 0.0.0-dev-20260520165753 → 0.0.0-dev-20260520173258

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.
@@ -4,6 +4,26 @@ const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
4
4
  const DEFAULT_COMMON_PERMISSION = commonPermission;
5
5
  const DEFAULT_REVIEWER_PERMISSION = DEFAULT_COMMON_PERMISSION;
6
6
  const DEFAULT_EDITOR_PERMISSION = mergePermissions(DEFAULT_COMMON_PERMISSION, editorPermission);
7
+ const DEFAULT_TRIAGE_CATEGORIES = [
8
+ {
9
+ description: "Something is broken or behaves incorrectly.",
10
+ id: "bug",
11
+ labels: ["bug"],
12
+ types: ["Bug"],
13
+ },
14
+ {
15
+ description: "Maintenance, refactoring, chores, or planned work.",
16
+ id: "task",
17
+ labels: ["task"],
18
+ types: ["Task"],
19
+ },
20
+ {
21
+ description: "New or improved user-facing capability.",
22
+ id: "feature",
23
+ labels: ["enhancement"],
24
+ types: ["Feature"],
25
+ },
26
+ ];
7
27
  export function reviewerKey(reviewer, index) {
8
28
  return reviewer.id ?? `reviewer-${index + 1}`;
9
29
  }
@@ -90,6 +110,14 @@ export function resolveAgents(config) {
90
110
  : undefined,
91
111
  };
92
112
  }
113
+ function resolveTriageCategories(config) {
114
+ return (config.triage?.categories ?? DEFAULT_TRIAGE_CATEGORIES).map((category) => ({
115
+ description: category.description,
116
+ id: category.id ?? "",
117
+ labels: category.labels ?? [],
118
+ types: category.types ?? [],
119
+ }));
120
+ }
93
121
  export function resolveRepository(config) {
94
122
  if (!config.github?.owner)
95
123
  throw new Error("github.owner is required");
@@ -157,19 +185,10 @@ export function resolveRepository(config) {
157
185
  close: config.triage?.automation?.close ?? false,
158
186
  pr: config.triage?.automation?.pr ?? false,
159
187
  },
188
+ categories: resolveTriageCategories(config),
160
189
  concurrency: {
161
190
  runs: config.triage?.concurrency?.runs ?? 3,
162
191
  },
163
- kind: {
164
- bug: {
165
- label: config.triage?.kind?.bug?.label ?? ["bug"],
166
- type: config.triage?.kind?.bug?.type ?? ["Bug"],
167
- },
168
- feature: {
169
- label: config.triage?.kind?.feature?.label ?? ["enhancement"],
170
- type: config.triage?.kind?.feature?.type ?? ["Feature"],
171
- },
172
- },
173
192
  output: config.triage?.output,
174
193
  prompts: config.triage?.prompts ?? {},
175
194
  safety: {
@@ -9,6 +9,8 @@ const RESERVED_REVIEWER_KEYS = new Set(["editor", "orchestrator", "system"]);
9
9
  const PERMISSION_ACTIONS = new Set(["allow", "ask", "deny"]);
10
10
  const AJV = new Ajv2020({ allErrors: true, strict: false });
11
11
  const validateSchema = AJV.compile(schema);
12
+ const TRIAGE_CATEGORY_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
13
+ const RESERVED_TRIAGE_CATEGORY_IDS = new Set(["ASK", "none"]);
12
14
  const CONFIG_KEYS = new Set([
13
15
  "$schema",
14
16
  "agents",
@@ -76,9 +78,9 @@ const TRIAGE_KEYS = new Set([
76
78
  "account",
77
79
  "agents",
78
80
  "automation",
81
+ "categories",
79
82
  "concurrency",
80
83
  "creator",
81
- "kind",
82
84
  "output",
83
85
  "prompts",
84
86
  "safety",
@@ -98,9 +100,8 @@ const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
98
100
  const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
99
101
  const OUTPUT_KEYS = new Set(["repairAttempts"]);
100
102
  const TRIAGE_AUTOMATION_KEYS = new Set(["clear", "close", "pr"]);
103
+ const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
101
104
  const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
102
- const TRIAGE_KIND_KEYS = new Set(["bug", "feature"]);
103
- const TRIAGE_KIND_RULE_KEYS = new Set(["label", "type"]);
104
105
  const TRIAGE_SAFETY_KEYS = new Set([
105
106
  "allowAuthors",
106
107
  "allowMentionActors",
@@ -129,14 +130,13 @@ const MERGE_PROMPT_KEYS = new Set([
129
130
  ]);
130
131
  const TRIAGE_PROMPT_KEYS = new Set([
131
132
  "action",
132
- "bug",
133
+ "acceptance",
134
+ "category",
133
135
  "comment",
134
136
  "commentClassification",
135
137
  "createPr",
136
138
  "duplicate",
137
139
  "existingPr",
138
- "feature",
139
- "kind",
140
140
  "question",
141
141
  "reconsider",
142
142
  ]);
@@ -536,16 +536,44 @@ function validateStringArray(value, path, errors) {
536
536
  errors.push(`${path}[${index}] must be a string`);
537
537
  });
538
538
  }
539
- function validateStringArrayObject(value, path, keys, errors) {
540
- if (value == null)
539
+ function validateTriageCategories(categories, path, errors) {
540
+ if (categories == null)
541
541
  return;
542
- if (!isPlainObject(value)) {
543
- errors.push(`${path} must be an object`);
542
+ if (!Array.isArray(categories)) {
543
+ errors.push(`${path} must be an array`);
544
544
  return;
545
545
  }
546
- validateKnownKeys(value, path, keys, errors);
547
- for (const key of keys)
548
- validateStringArray(value[key], `${path}.${key}`, errors);
546
+ const ids = new Set();
547
+ categories.forEach((item, index) => {
548
+ const itemPath = `${path}[${index}]`;
549
+ if (!isPlainObject(item)) {
550
+ errors.push(`${itemPath} must be an object`);
551
+ return;
552
+ }
553
+ const category = item;
554
+ validateKnownKeys(category, itemPath, TRIAGE_CATEGORY_KEYS, errors);
555
+ if (!category.id) {
556
+ errors.push(`${itemPath}.id is required`);
557
+ }
558
+ else if (typeof category.id !== "string") {
559
+ errors.push(`${itemPath}.id must be a string`);
560
+ }
561
+ else if (!TRIAGE_CATEGORY_ID_PATTERN.test(category.id)) {
562
+ errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
563
+ }
564
+ else if (RESERVED_TRIAGE_CATEGORY_IDS.has(category.id)) {
565
+ errors.push(`${itemPath}.id is reserved: ${category.id}`);
566
+ }
567
+ else if (ids.has(category.id)) {
568
+ errors.push(`${itemPath}.id must be unique`);
569
+ }
570
+ else {
571
+ ids.add(category.id);
572
+ }
573
+ validateStringArray(category.labels, `${itemPath}.labels`, errors);
574
+ validateStringArray(category.types, `${itemPath}.types`, errors);
575
+ validateString(category.description, `${itemPath}.description`, errors);
576
+ });
549
577
  }
550
578
  function validateSafety(config, errors) {
551
579
  const safety = config.review?.safety;
@@ -587,7 +615,6 @@ function validateTriage(config, errors, options) {
587
615
  validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
588
616
  const automation = triage.automation;
589
617
  const concurrency = triage.concurrency;
590
- const kind = triage.kind;
591
618
  const safety = triage.safety;
592
619
  if (!triage.account)
593
620
  errors.push("triage.account is required");
@@ -615,9 +642,7 @@ function validateTriage(config, errors, options) {
615
642
  concurrency.runs < 1)) {
616
643
  errors.push("triage.concurrency.runs must be a positive integer");
617
644
  }
618
- validateKnownKeys(kind, "triage.kind", TRIAGE_KIND_KEYS, errors);
619
- validateStringArrayObject(kind?.bug, "triage.kind.bug", TRIAGE_KIND_RULE_KEYS, errors);
620
- validateStringArrayObject(kind?.feature, "triage.kind.feature", TRIAGE_KIND_RULE_KEYS, errors);
645
+ validateTriageCategories(triage.categories, "triage.categories", errors);
621
646
  validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
622
647
  validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
623
648
  validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
@@ -102,9 +102,24 @@ async function fetchPullRequestQueueInput(exec, repository, pr, token) {
102
102
  return { headRefOid: pullRequest.headRefOid, id: pullRequest.id };
103
103
  }
104
104
  export async function fetchPullRequest(exec, repository, pr) {
105
- const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner`);
105
+ const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,body,url,state,author,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner,changedFiles`);
106
106
  return JSON.parse(json);
107
107
  }
108
+ export async function fetchPullRequestClosingIssues(exec, repository, pr) {
109
+ const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { closingIssuesReferences(first: 20) { nodes { number title body url state author { login } labels(first: 100) { nodes { name } } issueType { name } } } } } }`;
110
+ const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}`);
111
+ const data = JSON.parse(raw);
112
+ return (data.data?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map((issue) => ({
113
+ author: issue.author?.login ?? "",
114
+ body: issue.body ?? "",
115
+ labels: issue.labels?.nodes?.map((label) => label.name) ?? [],
116
+ number: issue.number,
117
+ state: issue.state,
118
+ title: issue.title,
119
+ type: issue.issueType?.name,
120
+ url: issue.url,
121
+ })) ?? []);
122
+ }
108
123
  export async function fetchIssue(exec, repository, issue) {
109
124
  const query = `query($owner: String!, $repo: String!, $issue: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { number title body url state author { login } labels(first: 100) { nodes { name } } issueType { name } } } }`;
110
125
  try {
@@ -142,17 +157,47 @@ async function fetchIssueWithCli(exec, repository, issue) {
142
157
  };
143
158
  }
144
159
  export async function fetchIssueComments(exec, repository, issue, limit = 50) {
145
- const query = `query($owner: String!, $repo: String!, $issue: Int!, $limit: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { comments(last: $limit) { nodes { databaseId author { login } authorAssociation body createdAt url } } } } }`;
160
+ return (await fetchIssueCommentPage(exec, repository, issue, limit)).comments;
161
+ }
162
+ export async function fetchIssueCommentPage(exec, repository, issue, limit = 50) {
163
+ const query = `query($owner: String!, $repo: String!, $issue: Int!, $limit: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { comments(last: $limit) { totalCount nodes { databaseId author { login } authorAssociation body createdAt url } } } } }`;
146
164
  const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F issue=${issue} -F limit=${limit}`);
147
165
  const data = JSON.parse(raw);
148
- return (data.data?.repository?.issue?.comments?.nodes?.map((comment) => ({
166
+ const connection = data.data?.repository?.issue?.comments;
167
+ const comments = connection?.nodes?.map((comment) => ({
149
168
  author: comment.author?.login ?? "",
150
169
  authorAssociation: comment.authorAssociation,
151
170
  body: comment.body ?? "",
152
171
  createdAt: comment.createdAt,
153
172
  id: comment.databaseId,
154
173
  url: comment.url,
155
- })) ?? []);
174
+ })) ?? [];
175
+ return {
176
+ comments,
177
+ omitted: Math.max(0, (connection?.totalCount ?? comments.length) - comments.length),
178
+ };
179
+ }
180
+ export async function fetchPullRequestComments(exec, repository, pr, limit = 50) {
181
+ return (await fetchPullRequestCommentPage(exec, repository, pr, limit))
182
+ .comments;
183
+ }
184
+ export async function fetchPullRequestCommentPage(exec, repository, pr, limit = 50) {
185
+ const query = `query($owner: String!, $repo: String!, $pr: Int!, $limit: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { comments(last: $limit) { totalCount nodes { databaseId author { login } authorAssociation body createdAt url } } } } }`;
186
+ const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr} -F limit=${limit}`);
187
+ const data = JSON.parse(raw);
188
+ const connection = data.data?.repository?.pullRequest?.comments;
189
+ const comments = connection?.nodes?.map((comment) => ({
190
+ author: comment.author?.login ?? "",
191
+ authorAssociation: comment.authorAssociation,
192
+ body: comment.body ?? "",
193
+ createdAt: comment.createdAt,
194
+ id: comment.databaseId,
195
+ url: comment.url,
196
+ })) ?? [];
197
+ return {
198
+ comments,
199
+ omitted: Math.max(0, (connection?.totalCount ?? comments.length) - comments.length),
200
+ };
156
201
  }
157
202
  export async function fetchRelatedPullRequests(exec, repository, issue) {
158
203
  const query = `query($owner: String!, $repo: String!, $issue: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { timelineItems(first: 50, itemTypes: [CONNECTED_EVENT, CROSS_REFERENCED_EVENT]) { nodes { __typename ... on ConnectedEvent { subject { __typename ... on PullRequest { number title url state mergedAt body author { login } } } } ... on CrossReferencedEvent { source { __typename ... on PullRequest { number title url state mergedAt body author { login } } } } } } } } }`;
@@ -295,7 +340,7 @@ export async function fetchPullRequestCommits(exec, repository, pr) {
295
340
  }));
296
341
  }
297
342
  export async function fetchPullRequestSafetyMeta(exec, repository, pr) {
298
- const query = `query($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { author { login } changedFiles labels(first: 100) { nodes { name } } files(first: 100, after: $filesCursor) { nodes { path } pageInfo { hasNextPage endCursor } } } } } }`;
343
+ const query = `query($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { author { login } changedFiles labels(first: 100) { nodes { name } } files(first: 100, after: $filesCursor) { nodes { path } pageInfo { hasNextPage endCursor } } } } }`;
299
344
  const files = [];
300
345
  let author = "";
301
346
  let changedFiles = 0;
@@ -467,11 +512,19 @@ function findingComment(finding) {
467
512
  }
468
513
  return comment;
469
514
  }
470
- export async function postChangesRequested(exec, repository, pr, account, findings) {
515
+ function requirementFindingSummary(finding) {
516
+ return [
517
+ `- Missing issue #${finding.issueNumber} requirement: ${finding.requirement}`,
518
+ ` Evidence: ${finding.evidence}`,
519
+ ` Fix: ${finding.fix}`,
520
+ ].join("\n");
521
+ }
522
+ export async function postChangesRequested(exec, repository, pr, account, findings, requirementFindings = []) {
471
523
  const token = await ghToken(exec, repository, account);
472
524
  const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
473
525
  const body = findings
474
526
  .map((finding) => `- ${finding.issue.split("\n")[0]}`)
527
+ .concat(requirementFindings.map(requirementFindingSummary))
475
528
  .join("\n");
476
529
  await writeFile(payloadPath, JSON.stringify({
477
530
  body,
@@ -610,6 +663,45 @@ export async function fetchUnresolvedThreads(exec, repository, pr, author) {
610
663
  ];
611
664
  });
612
665
  }
666
+ export async function fetchPullRequestReviewThreads(exec, repository, pr, threadLimit = 50, commentsPerThread = 20) {
667
+ return (await fetchPullRequestReviewThreadPage(exec, repository, pr, threadLimit, commentsPerThread)).threads;
668
+ }
669
+ export async function fetchPullRequestReviewThreadPage(exec, repository, pr, threadLimit = 50, commentsPerThread = 20) {
670
+ const query = `query($owner: String!, $repo: String!, $pr: Int!, $threadLimit: Int!, $commentsPerThread: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(last: $threadLimit) { totalCount nodes { id isResolved comments(last: $commentsPerThread) { totalCount nodes { databaseId author { login } path line body createdAt } } } } } } }`;
671
+ const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr} -F threadLimit=${threadLimit} -F commentsPerThread=${commentsPerThread}`);
672
+ const data = JSON.parse(raw);
673
+ const connection = data.data?.repository?.pullRequest?.reviewThreads;
674
+ const nodes = connection?.nodes ?? [];
675
+ const threads = nodes.flatMap((thread) => {
676
+ const comments = [...thread.comments.nodes]
677
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt))
678
+ .map((comment) => ({
679
+ author: comment.author?.login ?? "",
680
+ body: comment.body ?? "",
681
+ commentId: comment.databaseId,
682
+ createdAt: comment.createdAt,
683
+ }));
684
+ const first = thread.comments.nodes[0];
685
+ if (!first)
686
+ return [];
687
+ return [
688
+ {
689
+ body: first.body ?? "",
690
+ commentId: first.databaseId,
691
+ comments,
692
+ isResolved: thread.isResolved,
693
+ line: first.line,
694
+ omittedComments: Math.max(0, (thread.comments.totalCount ?? comments.length) - comments.length),
695
+ path: first.path,
696
+ threadId: thread.id,
697
+ },
698
+ ];
699
+ });
700
+ return {
701
+ omitted: Math.max(0, (connection?.totalCount ?? threads.length) - threads.length),
702
+ threads,
703
+ };
704
+ }
613
705
  export async function postReply(exec, repository, pr, account, commentId, body) {
614
706
  const token = await ghToken(exec, repository, account);
615
707
  const payloadPath = join(tmpdir(), `magi-reply-${process.pid}-${Date.now()}-${commentId}.json`);
package/dist/index.js CHANGED
@@ -91,27 +91,133 @@ export function parseIssues(value) {
91
91
  throw new Error("Specify one or more issue numbers or issue URLs.");
92
92
  return issues;
93
93
  }
94
- export function parseRunArguments(value, dryRun = false) {
94
+ export function parseRunArguments(value, dryRun = false, command = "review") {
95
95
  const tokens = value.split(/[\s,]+/).filter(Boolean);
96
- const prTokens = tokens.filter((token) => {
96
+ const configOverrides = {};
97
+ const prTokens = [];
98
+ for (let index = 0; index < tokens.length; index++) {
99
+ const token = tokens[index];
97
100
  if (token === "--dry-run") {
98
101
  dryRun = true;
99
- return false;
102
+ continue;
100
103
  }
101
- return true;
102
- });
103
- return { dryRun, prs: parsePrs(prTokens.join(" ")) };
104
+ switch (token) {
105
+ case "--language":
106
+ setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
107
+ break;
108
+ case "--merge":
109
+ case "--no-merge":
110
+ setConfigOverride(configOverrides, [command, "automation", "merge"], token === "--merge");
111
+ break;
112
+ case "--close":
113
+ case "--no-close":
114
+ setConfigOverride(configOverrides, [command, "automation", "close"], token === "--close");
115
+ break;
116
+ case "--max-cycles":
117
+ if (command !== "merge")
118
+ throw unsupportedFlag(token, command);
119
+ setConfigOverride(configOverrides, ["merge", "maxThreadResolutionCycles"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
120
+ break;
121
+ case "--retry-failed-jobs":
122
+ setConfigOverride(configOverrides, ["review", "checks", "retryFailedJobs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
123
+ break;
124
+ case "--reviewer-concurrency":
125
+ setConfigOverride(configOverrides, ["review", "concurrency", "reviewers"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
126
+ break;
127
+ case "--run-concurrency":
128
+ setConfigOverride(configOverrides, ["review", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
129
+ break;
130
+ case "--wait-checks":
131
+ case "--no-wait-checks":
132
+ setConfigOverride(configOverrides, ["review", "checks", "wait"], token === "--wait-checks");
133
+ break;
134
+ case "--wait-checks-after-edit":
135
+ case "--no-wait-checks-after-edit":
136
+ if (command !== "merge")
137
+ throw unsupportedFlag(token, command);
138
+ setConfigOverride(configOverrides, ["merge", "checks", "wait"], token === "--wait-checks-after-edit");
139
+ break;
140
+ case "--pr":
141
+ case "--no-pr":
142
+ throw unsupportedFlag(token, command);
143
+ default:
144
+ if (token.startsWith("--"))
145
+ throw unsupportedFlag(token, command);
146
+ prTokens.push(token);
147
+ }
148
+ }
149
+ return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")) };
104
150
  }
105
151
  export function parseIssueRunArguments(value, dryRun = false) {
106
152
  const tokens = value.split(/[\s,]+/).filter(Boolean);
107
- const issueTokens = tokens.filter((token) => {
153
+ const configOverrides = {};
154
+ const issueTokens = [];
155
+ for (let index = 0; index < tokens.length; index++) {
156
+ const token = tokens[index];
108
157
  if (token === "--dry-run") {
109
158
  dryRun = true;
110
- return false;
159
+ continue;
111
160
  }
112
- return true;
113
- });
114
- return { dryRun, issues: parseIssues(issueTokens.join(" ")) };
161
+ switch (token) {
162
+ case "--language":
163
+ setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
164
+ break;
165
+ case "--close":
166
+ case "--no-close":
167
+ setConfigOverride(configOverrides, ["triage", "automation", "close"], token === "--close");
168
+ break;
169
+ case "--pr":
170
+ case "--no-pr":
171
+ setConfigOverride(configOverrides, ["triage", "automation", "pr"], token === "--pr");
172
+ break;
173
+ case "--run-concurrency":
174
+ setConfigOverride(configOverrides, ["triage", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
175
+ break;
176
+ case "--merge":
177
+ case "--no-merge":
178
+ case "--max-cycles":
179
+ case "--retry-failed-jobs":
180
+ case "--reviewer-concurrency":
181
+ case "--wait-checks":
182
+ case "--no-wait-checks":
183
+ case "--wait-checks-after-edit":
184
+ case "--no-wait-checks-after-edit":
185
+ throw unsupportedFlag(token, "triage");
186
+ default:
187
+ if (token.startsWith("--"))
188
+ throw unsupportedFlag(token, "triage");
189
+ issueTokens.push(token);
190
+ }
191
+ }
192
+ return { configOverrides, dryRun, issues: parseIssues(issueTokens.join(" ")) };
193
+ }
194
+ function nextFlagValue(tokens, index, flag) {
195
+ const value = tokens[index];
196
+ if (!value || value.startsWith("--"))
197
+ throw new Error(`${flag} requires a value.`);
198
+ return value;
199
+ }
200
+ function parseIntegerFlag(value, flag, minimum) {
201
+ const parsed = Number.parseInt(value, 10);
202
+ if (!Number.isInteger(parsed) ||
203
+ String(parsed) !== value ||
204
+ parsed < minimum) {
205
+ throw new Error(`${flag} must be an integer greater than or equal to ${minimum}.`);
206
+ }
207
+ return parsed;
208
+ }
209
+ function setConfigOverride(target, path, value) {
210
+ let current = target;
211
+ for (const key of path.slice(0, -1)) {
212
+ const existing = current[key];
213
+ const next = isPlainObject(existing) ? existing : {};
214
+ current[key] = next;
215
+ current = next;
216
+ }
217
+ current[path[path.length - 1]] = value;
218
+ }
219
+ function unsupportedFlag(flag, command) {
220
+ return new Error(`${flag} is not supported for /magi:${command}.`);
115
221
  }
116
222
  function parseOptionalPr(value) {
117
223
  if (!value?.trim())
@@ -322,10 +428,11 @@ export const MagiPlugin = async ({ client, directory }) => {
322
428
  dryRun: tool.schema.boolean().optional(),
323
429
  },
324
430
  async execute(args, context) {
325
- const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
431
+ const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
326
432
  const loaded = await loadConfig(directory);
327
- const retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
328
- const validation = await validateConfig(loaded.config, {
433
+ const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
434
+ const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
435
+ const validation = await validateConfig(config, {
329
436
  checkAuth: true,
330
437
  directory,
331
438
  exec: retryingExec,
@@ -334,9 +441,9 @@ export const MagiPlugin = async ({ client, directory }) => {
334
441
  });
335
442
  if (!validation.ok)
336
443
  return JSON.stringify(validation, null, 2);
337
- const repository = resolveRepository(loaded.config);
444
+ const repository = resolveRepository(config);
338
445
  const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
339
- config: loaded.config,
446
+ config,
340
447
  dryRun: parsed.dryRun,
341
448
  repository,
342
449
  pr,
@@ -360,8 +467,9 @@ export const MagiPlugin = async ({ client, directory }) => {
360
467
  async execute(args, context) {
361
468
  const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
362
469
  const loaded = await loadConfig(directory);
363
- const retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
364
- const validation = await validateConfig(loaded.config, {
470
+ const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
471
+ const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
472
+ const validation = await validateConfig(config, {
365
473
  checkAuth: true,
366
474
  directory,
367
475
  exec: retryingExec,
@@ -369,9 +477,9 @@ export const MagiPlugin = async ({ client, directory }) => {
369
477
  });
370
478
  if (!validation.ok)
371
479
  return JSON.stringify(validation, null, 2);
372
- const repository = resolveRepository(loaded.config);
480
+ const repository = resolveRepository(config);
373
481
  const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
374
- config: loaded.config,
482
+ config,
375
483
  dryRun: parsed.dryRun,
376
484
  repository,
377
485
  pr,
@@ -392,8 +500,9 @@ export const MagiPlugin = async ({ client, directory }) => {
392
500
  async execute(args, context) {
393
501
  const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
394
502
  const loaded = await loadConfig(directory);
395
- const retryingExec = withGitHubApiRetry(exec, loaded.config.github?.apiRetryAttempts ?? 3);
396
- const validation = await validateConfig(loaded.config, {
503
+ const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
504
+ const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
505
+ const validation = await validateConfig(config, {
397
506
  checkAuth: true,
398
507
  directory,
399
508
  exec: retryingExec,
@@ -403,11 +512,11 @@ export const MagiPlugin = async ({ client, directory }) => {
403
512
  });
404
513
  if (!validation.ok)
405
514
  return JSON.stringify(validation, null, 2);
406
- const repository = resolveRepository(loaded.config);
515
+ const repository = resolveRepository(config);
407
516
  if (!repository.triage)
408
517
  return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
409
518
  const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
410
- config: loaded.config,
519
+ config,
411
520
  dryRun: parsed.dryRun,
412
521
  issue,
413
522
  parentSessionId: context.sessionID,
@@ -58,9 +58,10 @@ export function applyFindingValidation(input) {
58
58
  discarded.push(target);
59
59
  return false;
60
60
  });
61
- next[reviewer] = findings.length
62
- ? { ...output, findings }
63
- : { findings: [], verdict: "MERGE" };
61
+ next[reviewer] =
62
+ findings.length || output.requirementFindings.length
63
+ ? { ...output, findings }
64
+ : { findings: [], requirementFindings: [], verdict: "MERGE" };
64
65
  }
65
66
  return { outputs: next, summary: { discarded, kept } };
66
67
  }
@@ -27,6 +27,9 @@ function formatRereviewFinding(finding) {
27
27
  : `${finding.path}:${finding.startLine}-${finding.line}`;
28
28
  return `\`${line}\`: ${finding.body}`;
29
29
  }
30
+ function formatRequirementFinding(finding) {
31
+ return `Issue #${finding.issueNumber}: ${finding.requirement}`;
32
+ }
30
33
  function isReviewOutput(output) {
31
34
  return "findings" in output;
32
35
  }
@@ -78,7 +81,10 @@ function reviewerDetailLines(output) {
78
81
  return output.reason ? [output.reason] : [];
79
82
  if (output.verdict !== "CHANGES_REQUESTED")
80
83
  return [];
81
- return output.findings.map(formatFinding);
84
+ return [
85
+ ...output.findings.map(formatFinding),
86
+ ...output.requirementFindings.map(formatRequirementFinding),
87
+ ];
82
88
  }
83
89
  if (output.verdict === "CLOSE")
84
90
  return output.reason ? [output.reason] : [];
@@ -86,6 +92,7 @@ function reviewerDetailLines(output) {
86
92
  return [];
87
93
  return [
88
94
  ...output.newFindings.map(formatRereviewFinding),
95
+ ...output.requirementFindings.map(formatRequirementFinding),
89
96
  ...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
90
97
  ];
91
98
  }