opencode-magi 0.6.0 → 0.6.1

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.
@@ -138,13 +138,11 @@ const MERGE_PROMPT_KEYS = new Set([
138
138
  const TRIAGE_PROMPT_KEYS = new Set([
139
139
  "acceptance",
140
140
  "category",
141
- "comment",
142
141
  "commentClassification",
143
142
  "create",
144
143
  "createGuidelines",
145
144
  "duplicate",
146
145
  "existingPr",
147
- "question",
148
146
  "reconsider",
149
147
  ]);
150
148
  function githubHost(config) {
@@ -20,6 +20,20 @@ function errorText(error) {
20
20
  .filter((item) => typeof item === "string")
21
21
  .join("\n");
22
22
  }
23
+ function isIssueTypeUnavailableText(text) {
24
+ return (/cannot query field ["']?issueType["']? on type ["']?Issue["']?/i.test(text) ||
25
+ /field ["']?issueType["']?.*(does not exist|doesn't exist|is not defined|not found).*type ["']?Issue["']?/i.test(text) ||
26
+ /undefinedField.*issueType/i.test(text) ||
27
+ /issueType.*unsupported field|unsupported field.*issueType/i.test(text));
28
+ }
29
+ function isIssueTypeUnavailableError(error) {
30
+ return isIssueTypeUnavailableText(errorText(error));
31
+ }
32
+ function isIssueTypeUnavailableGraphqlResponse(data) {
33
+ return (data.errors?.some((error) => isIssueTypeUnavailableText([error.message, error.type]
34
+ .filter((item) => typeof item === "string")
35
+ .join("\n"))) ?? false);
36
+ }
23
37
  async function localCommitExists(exec, worktreePath, sha) {
24
38
  try {
25
39
  await exec(`git cat-file -e ${shellQuote(`${sha}^{commit}`)}`, {
@@ -184,26 +198,34 @@ export async function fetchPullRequestClosingIssues(exec, repository, pr) {
184
198
  }
185
199
  export async function fetchIssue(exec, repository, issue) {
186
200
  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 } } } }`;
201
+ let raw;
187
202
  try {
188
- 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}`);
189
- const data = JSON.parse(raw);
190
- const graphqlIssue = data.data?.repository?.issue;
191
- if (!graphqlIssue)
192
- throw new Error(`Could not fetch issue #${issue}`);
193
- return {
194
- author: graphqlIssue.author?.login ?? "",
195
- body: graphqlIssue.body ?? "",
196
- labels: graphqlIssue.labels?.nodes?.map((label) => label.name) ?? [],
197
- number: graphqlIssue.number,
198
- state: graphqlIssue.state,
199
- title: graphqlIssue.title,
200
- type: graphqlIssue.issueType?.name,
201
- url: graphqlIssue.url,
202
- };
203
+ 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}`);
203
204
  }
204
- catch {
205
- return fetchIssueWithCli(exec, repository, issue);
205
+ catch (error) {
206
+ if (isIssueTypeUnavailableError(error)) {
207
+ return fetchIssueWithCli(exec, repository, issue);
208
+ }
209
+ throw error;
210
+ }
211
+ const data = JSON.parse(raw);
212
+ const graphqlIssue = data.data?.repository?.issue;
213
+ if (!graphqlIssue) {
214
+ if (isIssueTypeUnavailableGraphqlResponse(data)) {
215
+ return fetchIssueWithCli(exec, repository, issue);
216
+ }
217
+ throw new Error(`Could not fetch issue #${issue}`);
206
218
  }
219
+ return {
220
+ author: graphqlIssue.author?.login ?? "",
221
+ body: graphqlIssue.body ?? "",
222
+ labels: graphqlIssue.labels?.nodes?.map((label) => label.name) ?? [],
223
+ number: graphqlIssue.number,
224
+ state: graphqlIssue.state,
225
+ title: graphqlIssue.title,
226
+ type: graphqlIssue.issueType?.name,
227
+ url: graphqlIssue.url,
228
+ };
207
229
  }
208
230
  async function fetchIssueWithCli(exec, repository, issue) {
209
231
  const raw = await exec(`gh issue view ${issue} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,body,url,state,author,labels`);
@@ -458,8 +480,9 @@ export async function fetchPullRequestSafetyMeta(exec, repository, pr) {
458
480
  }
459
481
  return { author, changedFiles, files, labels };
460
482
  }
461
- export async function watchChecks(exec, repository, pr) {
462
- await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --watch`);
483
+ export async function watchChecks(exec, repository, pr, options = {}) {
484
+ const requiredFlag = options.requiredOnly ? " --required" : "";
485
+ await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --watch${requiredFlag}`);
463
486
  }
464
487
  export function isCancelledCheck(check) {
465
488
  return check.bucket === "cancel" || check.state === "CANCELLED";
@@ -469,8 +492,9 @@ export function isFailedCheck(check) {
469
492
  }
470
493
  export async function fetchPullRequestChecks(exec, repository, pr, options = {}) {
471
494
  let raw;
495
+ const requiredFlag = options.requiredOnly ? " --required" : "";
472
496
  try {
473
- raw = await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json name,state,bucket,link,workflow`);
497
+ raw = await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json name,state,bucket,link,workflow${requiredFlag}`);
474
498
  }
475
499
  catch (error) {
476
500
  if (options.tolerateMissingChecks &&
@@ -229,7 +229,7 @@ async function watchRerunRuns(exec, repository, checks) {
229
229
  await Promise.all(runIds.map((runId) => watchRun(exec, repository, runId)));
230
230
  }
231
231
  async function checksForHead(input) {
232
- const checks = await fetchPullRequestChecks(input.exec, input.repository, input.pr, { tolerateMissingChecks: Boolean(input.headSha) });
232
+ const checks = await fetchPullRequestChecks(input.exec, input.repository, input.pr, { requiredOnly: true, tolerateMissingChecks: Boolean(input.headSha) });
233
233
  const targetChecks = [];
234
234
  let hasAnyActionCheck = false;
235
235
  let hasTargetActionCheck = false;
@@ -254,7 +254,6 @@ async function checksForHead(input) {
254
254
  return {
255
255
  blocking: targetChecks.filter((check) => isFailedCheck(check) || isCancelledCheck(check)),
256
256
  hasAnyActionCheck,
257
- hasAnyCheck: checks.length > 0,
258
257
  hasPending: targetChecks.some(isPendingCheck),
259
258
  hasTargetActionCheck,
260
259
  };
@@ -463,15 +462,17 @@ export async function waitForChecksWithClassification(input) {
463
462
  await input.onProgress?.("waiting for CI checks");
464
463
  for (let attempt = 0;; attempt += 1) {
465
464
  try {
466
- await watchChecks(input.exec, input.repository, input.pr);
465
+ await watchChecks(input.exec, input.repository, input.pr, {
466
+ requiredOnly: true,
467
+ });
467
468
  }
468
469
  catch {
469
470
  // gh exits non-zero for pending checks too; re-read check state below.
470
471
  }
471
472
  const target = await readTargetChecks();
472
473
  const waitingForTargetHead = Boolean(input.headSha) &&
473
- (!target.hasAnyCheck ||
474
- (target.hasAnyActionCheck && !target.hasTargetActionCheck));
474
+ target.hasAnyActionCheck &&
475
+ !target.hasTargetActionCheck;
475
476
  if (!waitingForTargetHead && !target.hasPending) {
476
477
  await assignBlockingChecks(target.blocking);
477
478
  break;
@@ -552,8 +553,11 @@ export async function waitForChecksWithClassification(input) {
552
553
  try {
553
554
  await input.onProgress?.("waiting for rerun CI checks");
554
555
  await watchRerunRuns(input.exec, input.repository, rerunnable);
555
- if (input.wait)
556
- await watchChecks(input.exec, input.repository, input.pr);
556
+ if (input.wait) {
557
+ await watchChecks(input.exec, input.repository, input.pr, {
558
+ requiredOnly: true,
559
+ });
560
+ }
557
561
  }
558
562
  catch {
559
563
  // Re-read the PR checks below so stale failed checks are not trusted.
@@ -156,7 +156,7 @@ async function runVote(input) {
156
156
  parse: input.parse,
157
157
  permission: input.agent.permission,
158
158
  prompt,
159
- repairAttempts: 3,
159
+ repairAttempts: input.run.config.output?.repairAttempts ?? 3,
160
160
  schemaName: input.schemaName,
161
161
  signal: input.signal,
162
162
  title: `Magi triage ${input.schemaName} #${input.issue} (${input.agent.key})`,
@@ -484,7 +484,7 @@ async function classifyMentionReplies(input) {
484
484
  parse: parseTriageCommentClassificationOutput,
485
485
  permission: agent.permission,
486
486
  prompt,
487
- repairAttempts: 3,
487
+ repairAttempts: input.input.config.output?.repairAttempts ?? 3,
488
488
  schemaName: "triage comment classification",
489
489
  signal: input.input.signal,
490
490
  title: `Magi triage comment classification #${input.input.issue} (${agent.key})`,
@@ -823,7 +823,7 @@ async function createImplementationPr(input) {
823
823
  parse: parseTriageCreatePrOutput,
824
824
  permission: creator.permission,
825
825
  prompt,
826
- repairAttempts: 3,
826
+ repairAttempts: input.input.config.output?.repairAttempts ?? 3,
827
827
  schemaName: "triage create PR",
828
828
  signal: input.input.signal,
829
829
  title: `Magi triage create PR #${input.issue.number}`,
@@ -339,38 +339,6 @@ async function composeTriageVotePrompt(input) {
339
339
  .filter(Boolean)
340
340
  .join("\n\n");
341
341
  }
342
- export async function composeTriageCommentPrompt(input) {
343
- const values = triageValues(input);
344
- const task = await taskBlock({
345
- builtin: "triage/comment",
346
- customPath: input.repository.triage?.prompts.comment,
347
- directory: input.directory,
348
- values,
349
- });
350
- return [
351
- task,
352
- languageBlock(input.repository.language),
353
- `<context>\n${input.context}\n</context>`,
354
- ]
355
- .filter(Boolean)
356
- .join("\n\n");
357
- }
358
- export async function composeTriageQuestionPrompt(input) {
359
- const values = triageValues(input);
360
- const task = await taskBlock({
361
- builtin: "triage/question",
362
- customPath: input.repository.triage?.prompts.question,
363
- directory: input.directory,
364
- values,
365
- });
366
- return [
367
- task,
368
- languageBlock(input.repository.language),
369
- `<context>\n${input.context}\n</context>`,
370
- ]
371
- .filter(Boolean)
372
- .join("\n\n");
373
- }
374
342
  export async function composeTriageCreatePrPrompt(input) {
375
343
  const values = triageValues(input);
376
344
  const task = await taskBlock({
@@ -1,6 +1,6 @@
1
1
  Decide whether an existing related pull request already handles issue #{issue} in {owner}/{repo}.
2
2
 
3
- Use only the provided context. Return HANDLE only when the PR clearly addresses the issue.
3
+ Use only the provided context. Return RELATED_PR_HANDLES_ISSUE only when the PR clearly addresses the issue. Otherwise return RELATED_PR_DOES_NOT_HANDLE_ISSUE.
4
4
 
5
5
  <context>
6
6
  {context}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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
@@ -229,8 +229,6 @@
229
229
  "duplicate": { "type": "string" },
230
230
  "category": { "type": "string" },
231
231
  "acceptance": { "type": "string" },
232
- "question": { "type": "string" },
233
- "comment": { "type": "string" },
234
232
  "commentClassification": { "type": "string" },
235
233
  "reconsider": { "type": "string" },
236
234
  "create": { "type": "string" },
@@ -1,5 +0,0 @@
1
- Compose one concise final triage comment for issue #{issue} in {owner}/{repo}. Mention @{author}. Do not include markdown fences.
2
-
3
- <context>
4
- {context}
5
- </context>
@@ -1,5 +0,0 @@
1
- Compose concrete, actionable questions for issue #{issue} in {owner}/{repo}. Mention @{author}. Do not include markdown fences.
2
-
3
- <context>
4
- {context}
5
- </context>