opencode-magi 0.7.0 → 0.8.0

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.
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
- import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageVoteOutputContract, } from "./contracts";
4
+ import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageSignalOutputContract, triageVoteOutputContract, } from "./contracts";
5
5
  async function readOptionalPrompt(directory, path, values = {}) {
6
6
  if (!path)
7
7
  return "";
@@ -35,6 +35,7 @@ function repositoryValues(repository) {
35
35
  }
36
36
  function reviewValues(input) {
37
37
  const ciFailureContext = input.ciFailureContext?.trim() ?? "";
38
+ const mergeConflictContext = input.mergeConflictContext?.trim() ?? "";
38
39
  return {
39
40
  ...repositoryValues(input.repository),
40
41
  baseSha: input.baseSha,
@@ -44,6 +45,10 @@ function reviewValues(input) {
44
45
  : "",
45
46
  headSha: input.headSha,
46
47
  jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
48
+ mergeConflictContext,
49
+ mergeConflictContextBlock: mergeConflictContext
50
+ ? `<merge_conflict_context>\n${mergeConflictContext}\n</merge_conflict_context>`
51
+ : "",
47
52
  pr: String(input.pr),
48
53
  reviewContext: input.reviewContext ?? "",
49
54
  worktreePath: input.worktreePath,
@@ -67,6 +72,17 @@ function editValues(input) {
67
72
  worktreePath: input.worktreePath,
68
73
  };
69
74
  }
75
+ function mergeConflictValues(input) {
76
+ return {
77
+ ...repositoryValues(input.repository),
78
+ baseBranch: input.baseBranch,
79
+ baseSha: input.baseSha,
80
+ conflictedFiles: input.conflictedFiles,
81
+ headSha: input.headSha,
82
+ pr: String(input.pr),
83
+ worktreePath: input.worktreePath,
84
+ };
85
+ }
70
86
  function triageValues(input) {
71
87
  const categories = input.repository.triage?.categories ?? [];
72
88
  const categoryOptions = categories
@@ -74,12 +90,16 @@ function triageValues(input) {
74
90
  ? `- ${category.id}: ${category.description}`
75
91
  : `- ${category.id}`)
76
92
  .join("\n");
93
+ const signalOptions = (input.repository.triage?.signals ?? [])
94
+ .map((signal) => `- ${signal.id}: ${signal.description}`)
95
+ .join("\n");
77
96
  return {
78
97
  ...repositoryValues(input.repository),
79
98
  author: input.author ?? "",
80
99
  categoryOptions,
81
100
  context: input.context,
82
101
  issue: String(input.issue),
102
+ signalOptions,
83
103
  worktreePath: input.worktreePath ?? "",
84
104
  };
85
105
  }
@@ -97,6 +117,12 @@ function previousReviewBlock(previousReview) {
97
117
  function reviewContextBlock(reviewContext) {
98
118
  return reviewContext?.trim() ? reviewContext.trim() : "";
99
119
  }
120
+ function mergeConflictContextBlock(mergeConflictContext) {
121
+ const body = mergeConflictContext?.trim();
122
+ return body
123
+ ? `<merge_conflict_context>\n${body}\n</merge_conflict_context>`
124
+ : "";
125
+ }
100
126
  async function reviewGuidelinesBlock(input) {
101
127
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
102
128
  return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
@@ -136,6 +162,7 @@ export async function composeReviewPrompt(input) {
136
162
  return [
137
163
  task,
138
164
  reviewContextBlock(input.reviewContext),
165
+ mergeConflictContextBlock(input.mergeConflictContext),
139
166
  languageBlock(input.repository.language),
140
167
  personaBlock(input.reviewer.persona),
141
168
  await reviewGuidelinesBlock({
@@ -159,6 +186,7 @@ export async function composeRereviewPrompt(input) {
159
186
  return [
160
187
  task,
161
188
  reviewContextBlock(input.reviewContext),
189
+ mergeConflictContextBlock(input.mergeConflictContext),
162
190
  input.includeSessionContext === false
163
191
  ? ""
164
192
  : languageBlock(input.repository.language),
@@ -200,6 +228,28 @@ export async function composeEditPrompt(input) {
200
228
  .filter(Boolean)
201
229
  .join("\n\n");
202
230
  }
231
+ export async function composeMergeConflictPrompt(input) {
232
+ const values = mergeConflictValues(input);
233
+ const task = await taskBlock({
234
+ builtin: "merge/conflict",
235
+ directory: input.directory,
236
+ values,
237
+ });
238
+ const persona = input.repository.agents.editor?.persona;
239
+ return [
240
+ task,
241
+ languageBlock(input.repository.language),
242
+ personaBlock(persona),
243
+ await editGuidelinesBlock({
244
+ directory: input.directory,
245
+ path: input.repository.prompts.editGuidelines,
246
+ values,
247
+ }),
248
+ editOutputContract,
249
+ ]
250
+ .filter(Boolean)
251
+ .join("\n\n");
252
+ }
203
253
  export async function composeFindingValidationPrompt(input) {
204
254
  const values = { ...reviewValues(input), findings: input.findings };
205
255
  const task = await taskBlock({
@@ -395,7 +445,14 @@ export async function composeTriageAcceptancePrompt(input) {
395
445
  ...input,
396
446
  builtin: "acceptance",
397
447
  customPath: input.repository.triage?.prompts.acceptance,
398
- outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
448
+ outputContract: triageVoteOutputContract('"YES" | "NO" | "INVALID" | "ASK"'),
449
+ });
450
+ }
451
+ export async function composeTriageSignalPrompt(input) {
452
+ return composeTriageVotePrompt({
453
+ ...input,
454
+ builtin: "signal",
455
+ outputContract: triageSignalOutputContract,
399
456
  });
400
457
  }
401
458
  export async function composeTriageCommentClassificationPrompt(input) {
@@ -256,6 +256,24 @@ The object must match this shape:
256
256
  ]
257
257
  }
258
258
  </output_contract>`.trim();
259
+ export const triageSignalOutputContract = `
260
+ <output_contract>
261
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
262
+
263
+ The object must match this shape:
264
+ {
265
+ "signals": [
266
+ {
267
+ "id": "configured_signal_id",
268
+ "reason": "Short rationale."
269
+ }
270
+ ]
271
+ }
272
+
273
+ Rules:
274
+ - Return only configured signal IDs that apply.
275
+ - Return an empty signals array when none apply.
276
+ </output_contract>`.trim();
259
277
  const outputContractsBySchemaName = {
260
278
  "CI classification": ciClassificationOutputContract,
261
279
  "close reconsideration": closeReconsiderationOutputContract,
@@ -264,13 +282,14 @@ const outputContractsBySchemaName = {
264
282
  rereview: rereviewOutputContract,
265
283
  "rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
266
284
  review: reviewOutputContract,
267
- "triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
285
+ "triage acceptance": triageVoteOutputContract('"YES" | "NO" | "INVALID" | "ASK"'),
268
286
  "triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
269
287
  "triage create PR": triageCreatePrOutputContract,
270
288
  "triage comment classification": triageCommentClassificationOutputContract,
271
289
  "triage duplicate": triageDuplicateOutputContract,
272
290
  "triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
273
291
  "triage reconsider": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
292
+ "triage signal": triageSignalOutputContract,
274
293
  };
275
294
  export function repairPrompt(schemaName) {
276
295
  const outputContract = outputContractsBySchemaName[schemaName];
@@ -112,7 +112,25 @@ export function parseTriageCategoryOutput(text, categories) {
112
112
  return parseTriageVote(text, ["ASK", ...categories]);
113
113
  }
114
114
  export function parseTriageBinaryOutput(text) {
115
- return parseTriageVote(text, ["ASK", "NO", "YES"]);
115
+ return parseTriageVote(text, ["ASK", "INVALID", "NO", "YES"]);
116
+ }
117
+ export function parseTriageSignalOutput(text, signalIds) {
118
+ const data = extractJson(text);
119
+ if (!data || typeof data !== "object")
120
+ throw new Error("triage signal output must be an object");
121
+ const ids = new Set(signalIds);
122
+ return {
123
+ signals: requireArray(data.signals, "signals").map((item, index) => {
124
+ const value = item;
125
+ const id = requireString(value.id, `signals[${index}].id`);
126
+ if (!ids.has(id))
127
+ throw new Error(`signals[${index}].id is not configured`);
128
+ return {
129
+ id,
130
+ reason: requireString(value.reason, `signals[${index}].reason`),
131
+ };
132
+ }),
133
+ };
116
134
  }
117
135
  export function parseTriageDuplicateOutput(text) {
118
136
  const data = extractJson(text);
@@ -0,0 +1,10 @@
1
+ Resolve merge conflicts for pull request #{pr} in {owner}/{repo}.
2
+ The PR worktree is {worktreePath}.
3
+
4
+ The latest base branch is {baseBranch} at {baseSha}.
5
+ The PR head before conflict recovery was {headSha}.
6
+
7
+ Conflicted files:
8
+ {conflictedFiles}
9
+
10
+ Resolve every merge conflict in the worktree. Preserve the intended PR behavior while incorporating the latest base branch changes. Stage all resolved files and create a commit. Do not push.
@@ -13,6 +13,8 @@ Every newFinding must target a valid right-side line in the PR diff.
13
13
  If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
14
14
  Do not omit line. Do not create file-level or body-only newFindings.
15
15
 
16
+ If `<merge_conflict_context>` is present, treat unresolved merge conflicts as review findings. Request changes when a conflict makes the PR unsafe or impossible to merge, and prefer the provided `suggestedLine` when it is present.
17
+
16
18
  {ciFailureContextBlock}
17
19
  Do not edit files or perform write operations.
18
20
 
@@ -14,4 +14,6 @@ Every finding must target a valid right-side line in the PR diff.
14
14
  If the problem itself does not have an exact changed line, choose the nearest changed line that represents the cause, responsibility, missing implementation, or affected behavior. This includes but is not limited to missing validation, missing wiring, missing requirements, missing tests, missing documentation, affected configuration, or relevant call sites.
15
15
  Do not omit line. Do not create file-level or body-only findings.
16
16
 
17
+ If `<merge_conflict_context>` is present, treat unresolved merge conflicts as review findings. Request changes when a conflict makes the PR unsafe or impossible to merge, and prefer the provided `suggestedLine` when it is present.
18
+
17
19
  {ciFailureContextBlock}
@@ -1,6 +1,6 @@
1
1
  Evaluate issue #{issue} in {owner}/{repo} for the selected category.
2
2
 
3
- Choose YES when the issue should be accepted for the project. Choose NO when it should be rejected, is not actionable, or is not appropriate for this project. Choose ASK when specific missing information is required before deciding.
3
+ Choose YES when the issue should be accepted for the project. Choose NO when it should not be worked on for normal wontfix reasons. Choose INVALID when the report is unreproducible, contradictory, based on an impossible premise, or otherwise not actionable as an issue. Choose ASK when specific missing information is required before deciding.
4
4
 
5
5
  <context>
6
6
  {context}
@@ -0,0 +1,10 @@
1
+ Evaluate optional triage signals for issue #{issue} in {owner}/{repo}.
2
+
3
+ Configured signals:
4
+ {signalOptions}
5
+
6
+ Return every configured signal that applies to the final triage result and issue context. Do not invent signal IDs.
7
+
8
+ <context>
9
+ {context}
10
+ </context>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-magi",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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
@@ -179,6 +179,15 @@
179
179
  "close": { "type": "boolean" }
180
180
  }
181
181
  },
182
+ "mergeAutomation": {
183
+ "type": "object",
184
+ "additionalProperties": false,
185
+ "properties": {
186
+ "merge": { "type": "boolean", "default": true },
187
+ "close": { "type": "boolean", "default": false },
188
+ "conflict": { "type": "boolean", "default": false }
189
+ }
190
+ },
182
191
  "reviewChecks": {
183
192
  "type": "object",
184
193
  "additionalProperties": false,
@@ -270,6 +279,47 @@
270
279
  "description": { "type": "string" }
271
280
  }
272
281
  },
282
+ "triageSignal": {
283
+ "type": "object",
284
+ "additionalProperties": false,
285
+ "required": ["id", "description"],
286
+ "properties": {
287
+ "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
288
+ "description": { "type": "string", "minLength": 1 }
289
+ }
290
+ },
291
+ "triageLabelRuleCondition": {
292
+ "type": "object",
293
+ "additionalProperties": false,
294
+ "minProperties": 1,
295
+ "properties": {
296
+ "disposition": {
297
+ "enum": [
298
+ "accepted",
299
+ "rejected",
300
+ "invalid",
301
+ "duplicate",
302
+ "already_handled",
303
+ "needs_category",
304
+ "needs_acceptance",
305
+ "blocked",
306
+ "failed"
307
+ ]
308
+ },
309
+ "category": { "type": "string" },
310
+ "signals": { "type": "array", "items": { "type": "string" } }
311
+ }
312
+ },
313
+ "triageLabelRule": {
314
+ "type": "object",
315
+ "additionalProperties": false,
316
+ "required": ["when"],
317
+ "properties": {
318
+ "when": { "$ref": "#/$defs/triageLabelRuleCondition" },
319
+ "add": { "type": "array", "items": { "type": "string" } },
320
+ "remove": { "type": "array", "items": { "type": "string" } }
321
+ }
322
+ },
273
323
  "triageAutomation": {
274
324
  "type": "object",
275
325
  "additionalProperties": false,
@@ -278,10 +328,9 @@
278
328
  "create": { "type": "boolean", "default": false },
279
329
  "review": { "type": "boolean", "default": false },
280
330
  "merge": { "type": "boolean", "default": false },
281
- "clear": {
331
+ "label": {
282
332
  "type": "array",
283
- "items": { "type": "string" },
284
- "default": ["triage"]
333
+ "items": { "$ref": "#/$defs/triageLabelRule" }
285
334
  }
286
335
  }
287
336
  },
@@ -339,7 +388,7 @@
339
388
  "editor": { "$ref": "#/$defs/editor" },
340
389
  "checks": { "$ref": "#/$defs/mergeChecks" },
341
390
  "prompts": { "$ref": "#/$defs/mergePrompts" },
342
- "automation": { "$ref": "#/$defs/automation" },
391
+ "automation": { "$ref": "#/$defs/mergeAutomation" },
343
392
  "maxThreadResolutionCycles": {
344
393
  "type": "integer",
345
394
  "minimum": 0,
@@ -362,6 +411,10 @@
362
411
  "type": "array",
363
412
  "items": { "$ref": "#/$defs/triageCategory" }
364
413
  },
414
+ "signals": {
415
+ "type": "array",
416
+ "items": { "$ref": "#/$defs/triageSignal" }
417
+ },
365
418
  "automation": { "$ref": "#/$defs/triageAutomation" },
366
419
  "safety": { "$ref": "#/$defs/triageSafety" },
367
420
  "concurrency": { "$ref": "#/$defs/triageConcurrency" },