opencode-magi 0.2.0 → 0.4.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.
Files changed (36) hide show
  1. package/README.md +19 -0
  2. package/dist/commands.js +4 -0
  3. package/dist/config/output.js +11 -2
  4. package/dist/config/resolve.js +81 -1
  5. package/dist/config/validate.js +341 -3
  6. package/dist/config/worktree.js +8 -2
  7. package/dist/github/commands.js +381 -19
  8. package/dist/index.js +252 -26
  9. package/dist/orchestrator/ci.js +1 -1
  10. package/dist/orchestrator/findings.js +4 -3
  11. package/dist/orchestrator/inline-comments.js +79 -0
  12. package/dist/orchestrator/majority.js +14 -0
  13. package/dist/orchestrator/merge.js +108 -34
  14. package/dist/orchestrator/report.js +25 -7
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +122 -14
  17. package/dist/orchestrator/run-manager.js +408 -17
  18. package/dist/orchestrator/triage.js +1119 -0
  19. package/dist/permissions/editor.json +8 -1
  20. package/dist/prompts/compose.js +163 -1
  21. package/dist/prompts/contracts.js +131 -18
  22. package/dist/prompts/output.js +173 -22
  23. package/dist/prompts/templates/merge/edit.md +12 -5
  24. package/dist/prompts/templates/review/review.md +6 -0
  25. package/dist/prompts/templates/triage/acceptance.md +7 -0
  26. package/dist/prompts/templates/triage/action.md +5 -0
  27. package/dist/prompts/templates/triage/category.md +10 -0
  28. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  29. package/dist/prompts/templates/triage/comment.md +5 -0
  30. package/dist/prompts/templates/triage/create.md +7 -0
  31. package/dist/prompts/templates/triage/duplicate.md +7 -0
  32. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  33. package/dist/prompts/templates/triage/question.md +5 -0
  34. package/dist/prompts/templates/triage/reconsider.md +5 -0
  35. package/package.json +5 -2
  36. package/schema.json +162 -5
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "bash": {
3
+ "bun *": "allow",
4
+ "bunx *": "allow",
5
+ "corepack *": "allow",
3
6
  "git add*": "allow",
4
- "git commit*": "allow"
7
+ "git commit*": "allow",
8
+ "npm *": "allow",
9
+ "npx *": "allow",
10
+ "pnpm *": "allow",
11
+ "yarn *": "allow"
5
12
  },
6
13
  "edit": "allow"
7
14
  }
@@ -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, } from "./contracts";
4
+ import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageActionOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageVoteOutputContract, } from "./contracts";
5
5
  async function readOptionalPrompt(directory, path, values = {}) {
6
6
  if (!path)
7
7
  return "";
@@ -45,6 +45,7 @@ function reviewValues(input) {
45
45
  headSha: input.headSha,
46
46
  jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
47
47
  pr: String(input.pr),
48
+ reviewContext: input.reviewContext ?? "",
48
49
  worktreePath: input.worktreePath,
49
50
  };
50
51
  }
@@ -61,10 +62,27 @@ function editValues(input) {
61
62
  return {
62
63
  ...repositoryValues(input.repository),
63
64
  pr: String(input.pr),
65
+ reviewFindings: input.reviewFindings,
64
66
  unresolvedThreads: input.unresolvedThreads,
65
67
  worktreePath: input.worktreePath,
66
68
  };
67
69
  }
70
+ function triageValues(input) {
71
+ const categories = input.repository.triage?.categories ?? [];
72
+ const categoryOptions = categories
73
+ .map((category) => category.description
74
+ ? `- ${category.id}: ${category.description}`
75
+ : `- ${category.id}`)
76
+ .join("\n");
77
+ return {
78
+ ...repositoryValues(input.repository),
79
+ author: input.author ?? "",
80
+ categoryOptions,
81
+ context: input.context,
82
+ issue: String(input.issue),
83
+ worktreePath: input.worktreePath ?? "",
84
+ };
85
+ }
68
86
  function personaBlock(persona) {
69
87
  return persona ? `<persona>\n${persona}\n</persona>` : "";
70
88
  }
@@ -76,6 +94,9 @@ function previousReviewBlock(previousReview) {
76
94
  ? `<previous_review>\n${previousReview.trim()}\n</previous_review>`
77
95
  : "";
78
96
  }
97
+ function reviewContextBlock(reviewContext) {
98
+ return reviewContext?.trim() ? reviewContext.trim() : "";
99
+ }
79
100
  async function reviewGuidelinesBlock(input) {
80
101
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
81
102
  return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
@@ -84,8 +105,15 @@ async function editGuidelinesBlock(input) {
84
105
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
85
106
  return body ? `<edit_guidelines>\n${body}\n</edit_guidelines>` : "";
86
107
  }
108
+ async function createGuidelinesBlock(input) {
109
+ const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
110
+ return body ? `<create_guidelines>\n${body}\n</create_guidelines>` : "";
111
+ }
87
112
  async function sessionContextBlocks(input) {
88
113
  return [
114
+ input.includeSessionContext
115
+ ? reviewContextBlock(input.values.reviewContext)
116
+ : "",
89
117
  input.includeSessionContext ? languageBlock(input.repository.language) : "",
90
118
  input.includeSessionContext ? personaBlock(input.reviewer.persona) : "",
91
119
  input.includeReviewGuidelines
@@ -107,6 +135,7 @@ export async function composeReviewPrompt(input) {
107
135
  });
108
136
  return [
109
137
  task,
138
+ reviewContextBlock(input.reviewContext),
110
139
  languageBlock(input.repository.language),
111
140
  personaBlock(input.reviewer.persona),
112
141
  await reviewGuidelinesBlock({
@@ -129,6 +158,7 @@ export async function composeRereviewPrompt(input) {
129
158
  });
130
159
  return [
131
160
  task,
161
+ reviewContextBlock(input.reviewContext),
132
162
  input.includeSessionContext === false
133
163
  ? ""
134
164
  : languageBlock(input.repository.language),
@@ -292,3 +322,135 @@ export async function composeCiClassificationAfterEditPrompt(input) {
292
322
  .filter(Boolean)
293
323
  .join("\n\n");
294
324
  }
325
+ async function composeTriageVotePrompt(input) {
326
+ const values = triageValues(input);
327
+ const task = await taskBlock({
328
+ builtin: `triage/${input.builtin}`,
329
+ customPath: input.customPath,
330
+ directory: input.directory,
331
+ values,
332
+ });
333
+ return [
334
+ task,
335
+ languageBlock(input.repository.language),
336
+ personaBlock(input.reviewer.persona),
337
+ input.outputContract,
338
+ ]
339
+ .filter(Boolean)
340
+ .join("\n\n");
341
+ }
342
+ export async function composeTriageActionPrompt(input) {
343
+ return composeTriageVotePrompt({
344
+ ...input,
345
+ builtin: "action",
346
+ customPath: input.repository.triage?.prompts.action,
347
+ outputContract: triageActionOutputContract,
348
+ });
349
+ }
350
+ export async function composeTriageCommentPrompt(input) {
351
+ const values = triageValues(input);
352
+ const task = await taskBlock({
353
+ builtin: "triage/comment",
354
+ customPath: input.repository.triage?.prompts.comment,
355
+ directory: input.directory,
356
+ values,
357
+ });
358
+ return [
359
+ task,
360
+ languageBlock(input.repository.language),
361
+ `<context>\n${input.context}\n</context>`,
362
+ ]
363
+ .filter(Boolean)
364
+ .join("\n\n");
365
+ }
366
+ export async function composeTriageQuestionPrompt(input) {
367
+ const values = triageValues(input);
368
+ const task = await taskBlock({
369
+ builtin: "triage/question",
370
+ customPath: input.repository.triage?.prompts.question,
371
+ directory: input.directory,
372
+ values,
373
+ });
374
+ return [
375
+ task,
376
+ languageBlock(input.repository.language),
377
+ `<context>\n${input.context}\n</context>`,
378
+ ]
379
+ .filter(Boolean)
380
+ .join("\n\n");
381
+ }
382
+ export async function composeTriageCreatePrPrompt(input) {
383
+ const values = triageValues(input);
384
+ const task = await taskBlock({
385
+ builtin: "triage/create",
386
+ customPath: input.repository.triage?.prompts.create,
387
+ directory: input.directory,
388
+ values,
389
+ });
390
+ const persona = input.repository.agents.triageCreator?.persona;
391
+ return [
392
+ task,
393
+ languageBlock(input.repository.language),
394
+ personaBlock(persona),
395
+ await createGuidelinesBlock({
396
+ directory: input.directory,
397
+ path: input.repository.triage?.prompts.createGuidelines,
398
+ values,
399
+ }),
400
+ triageCreatePrOutputContract,
401
+ ]
402
+ .filter(Boolean)
403
+ .join("\n\n");
404
+ }
405
+ export async function composeTriageExistingPrPrompt(input) {
406
+ return composeTriageVotePrompt({
407
+ ...input,
408
+ builtin: "existing-pr",
409
+ customPath: input.repository.triage?.prompts.existingPr,
410
+ outputContract: triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
411
+ });
412
+ }
413
+ export async function composeTriageDuplicatePrompt(input) {
414
+ return composeTriageVotePrompt({
415
+ ...input,
416
+ builtin: "duplicate",
417
+ customPath: input.repository.triage?.prompts.duplicate,
418
+ outputContract: triageDuplicateOutputContract,
419
+ });
420
+ }
421
+ export async function composeTriageCategoryPrompt(input) {
422
+ const categories = input.repository.triage?.categories ?? [];
423
+ const votes = ["ASK", ...categories.map((category) => category.id)]
424
+ .map((vote) => JSON.stringify(vote))
425
+ .join(" | ");
426
+ return composeTriageVotePrompt({
427
+ ...input,
428
+ builtin: "category",
429
+ customPath: input.repository.triage?.prompts.category,
430
+ outputContract: triageVoteOutputContract(votes),
431
+ });
432
+ }
433
+ export async function composeTriageAcceptancePrompt(input) {
434
+ return composeTriageVotePrompt({
435
+ ...input,
436
+ builtin: "acceptance",
437
+ customPath: input.repository.triage?.prompts.acceptance,
438
+ outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
439
+ });
440
+ }
441
+ export async function composeTriageCommentClassificationPrompt(input) {
442
+ return composeTriageVotePrompt({
443
+ ...input,
444
+ builtin: "comment-classification",
445
+ customPath: input.repository.triage?.prompts.commentClassification,
446
+ outputContract: triageCommentClassificationOutputContract,
447
+ });
448
+ }
449
+ export async function composeTriageReconsiderPrompt(input) {
450
+ return composeTriageVotePrompt({
451
+ ...input,
452
+ builtin: "reconsider",
453
+ customPath: input.repository.triage?.prompts.reconsider,
454
+ outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
455
+ });
456
+ }
@@ -15,16 +15,26 @@ The object must match this shape:
15
15
  "perspective": "Optional review perspective."
16
16
  }
17
17
  ],
18
+ "requirementFindings": [
19
+ {
20
+ "issueNumber": 47,
21
+ "requirement": "Required closing-issue behavior that is missing.",
22
+ "evidence": "Why the PR does not satisfy the requirement.",
23
+ "fix": "How to satisfy the requirement."
24
+ }
25
+ ],
18
26
  "reason": "Required only for CLOSE."
19
27
  }
20
28
 
21
29
  Rules:
22
- - MERGE requires an empty findings array.
23
- - CHANGES_REQUESTED requires at least one finding.
24
- - CLOSE requires a reason and an empty findings array.
30
+ - MERGE requires empty findings and requirementFindings arrays.
31
+ - CHANGES_REQUESTED requires at least one finding or requirementFinding.
32
+ - CLOSE requires a reason and empty findings and requirementFindings arrays.
25
33
  - path must be repository-relative.
26
- - line and startLine must refer to lines inside the PR diff hunk.
27
- - Omit startLine for single-line findings.
34
+ - line is optional. Include line only when the finding targets a valid line inside the PR diff hunk.
35
+ - startLine is allowed only when line is present and must also refer to a line inside the PR diff hunk.
36
+ - Omit startLine for single-line findings and omit line for file-level or body-only findings.
37
+ - Use requirementFindings only for missing closing-issue requirements; use findings for ordinary file-level issues that do not map cleanly to a diff line.
28
38
  </output_contract>`.trim();
29
39
  export const rereviewOutputContract = `
30
40
  <output_contract>
@@ -36,15 +46,18 @@ The object must match this shape:
36
46
  "resolve": [{ "commentId": 123, "threadId": "..." }],
37
47
  "followUps": [{ "commentId": 123, "body": "..." }],
38
48
  "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
49
+ "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }],
39
50
  "reason": "Required only for CLOSE."
40
51
  }
41
52
 
42
53
  Rules:
43
- - MERGE requires empty followUps and newFindings arrays.
44
- - CHANGES_REQUESTED requires at least one followUp or newFinding.
45
- - CLOSE requires a reason and empty followUps and newFindings arrays.
46
- - line and startLine must refer to lines inside the latest PR diff hunk.
47
- - Omit startLine for single-line findings.
54
+ - MERGE requires empty followUps, newFindings, and requirementFindings arrays.
55
+ - CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
56
+ - CLOSE requires a reason and empty followUps, newFindings, and requirementFindings arrays.
57
+ - line is optional. Include line only when the newFinding targets a valid line inside the latest PR diff hunk.
58
+ - startLine is allowed only when line is present and must also refer to a line inside the latest PR diff hunk.
59
+ - Omit startLine for single-line findings and omit line for file-level or body-only findings.
60
+ - Use requirementFindings only for missing closing-issue requirements; use newFindings for ordinary file-level issues that do not map cleanly to a diff line.
48
61
  </output_contract>`.trim();
49
62
  export const findingValidationOutputContract = `
50
63
  <output_contract>
@@ -83,14 +96,17 @@ The object must match this shape:
83
96
  "issue": "What is wrong.",
84
97
  "fix": "How to fix it."
85
98
  }
86
- ]
99
+ ],
100
+ "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
87
101
  }
88
102
 
89
103
  Rules:
90
- - MERGE requires an empty findings array.
91
- - CHANGES_REQUESTED requires at least one finding.
104
+ - MERGE requires empty findings and requirementFindings arrays.
105
+ - CHANGES_REQUESTED requires at least one finding or requirementFinding.
92
106
  - CLOSE is not allowed in this reconsideration step.
93
- - Omit startLine for single-line findings.
107
+ - line is optional. Include line only when the finding targets a valid line inside the PR diff hunk.
108
+ - startLine is allowed only when line is present.
109
+ - Omit startLine for single-line findings and omit line for file-level or body-only findings.
94
110
  </output_contract>`.trim();
95
111
  export const rereviewCloseReconsiderationOutputContract = `
96
112
  <output_contract>
@@ -101,14 +117,17 @@ The object must match this shape:
101
117
  "verdict": "MERGE" | "CHANGES_REQUESTED",
102
118
  "resolve": [{ "commentId": 123, "threadId": "..." }],
103
119
  "followUps": [{ "commentId": 123, "body": "..." }],
104
- "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }]
120
+ "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
121
+ "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
105
122
  }
106
123
 
107
124
  Rules:
108
- - MERGE requires empty followUps and newFindings arrays.
109
- - CHANGES_REQUESTED requires at least one followUp or newFinding.
125
+ - MERGE requires empty followUps, newFindings, and requirementFindings arrays.
126
+ - CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
110
127
  - CLOSE is not allowed in this reconsideration step.
111
- - Omit startLine for single-line findings.
128
+ - line is optional. Include line only when the newFinding targets a valid line inside the latest PR diff hunk.
129
+ - startLine is allowed only when line is present.
130
+ - Omit startLine for single-line findings and omit line for file-level or body-only findings.
112
131
  </output_contract>`.trim();
113
132
  export const editOutputContract = `
114
133
  <output_contract>
@@ -135,6 +154,32 @@ Rules:
135
154
  - responses must include a reply for each thread you addressed.
136
155
  - REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
137
156
  </output_contract>`.trim();
157
+ export const triageCreatePrOutputContract = `
158
+ <output_contract>
159
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
160
+
161
+ The object must match this shape:
162
+ {
163
+ "mode": "EDITED" | "REPLIED",
164
+ "commitSha": "full sha, required only when mode is EDITED; omit when mode is REPLIED",
165
+ "commitMessage": "fix(scope): short description, required only when mode is EDITED; omit when mode is REPLIED",
166
+ "filesTouched": ["relative/path.ext"],
167
+ "pullRequest": {
168
+ "title": "PR title, required only when mode is EDITED; omit when mode is REPLIED",
169
+ "body": "PR body, required only when mode is EDITED; omit when mode is REPLIED"
170
+ },
171
+ "responses": [{ "commentId": 123, "action": "FIXED" | "DISAGREE" | "ASK", "body": "Fixed." }]
172
+ }
173
+
174
+ Rules:
175
+ - Use EDITED only when you edited files, staged changes, and committed.
176
+ - Use REPLIED when you only replied without code changes.
177
+ - For EDITED, pullRequest.title and pullRequest.body must be non-empty and follow the repository's PR conventions.
178
+ - Do not push or create the PR. The orchestrator pushes and creates the PR using pullRequest exactly as provided.
179
+ - filesTouched must include every final changed file.
180
+ - responses may be empty when no review threads were addressed.
181
+ - REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
182
+ </output_contract>`.trim();
138
183
  export const ciClassificationOutputContract = `
139
184
  <output_contract>
140
185
  Return exactly one JSON object and nothing else. Do not wrap it in markdown.
@@ -171,6 +216,66 @@ Rules:
171
216
  - SCOPE_OUT means the failure is likely flaky, external, or infrastructure-related and may be rerun.
172
217
  - If uncertain, choose SCOPE_IN.
173
218
  </output_contract>`.trim();
219
+ export function triageVoteOutputContract(votes) {
220
+ return `
221
+ <output_contract>
222
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
223
+
224
+ The object must match this shape:
225
+ {
226
+ "vote": ${votes},
227
+ "reason": "Short rationale."
228
+ }
229
+ </output_contract>`.trim();
230
+ }
231
+ export const triageDuplicateOutputContract = `
232
+ <output_contract>
233
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
234
+
235
+ The object must match this shape:
236
+ {
237
+ "vote": "DUPLICATE" | "NOT_DUPLICATE",
238
+ "duplicateOf": 123,
239
+ "reason": "Short rationale."
240
+ }
241
+
242
+ Rules:
243
+ - duplicateOf is required only when vote is DUPLICATE.
244
+ - duplicateOf must be one of the provided duplicate candidate issue numbers.
245
+ </output_contract>`.trim();
246
+ export const triageCommentClassificationOutputContract = `
247
+ <output_contract>
248
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
249
+
250
+ The object must match this shape:
251
+ {
252
+ "comments": [
253
+ {
254
+ "commentId": 123,
255
+ "classification": "OBJECTION" | "NEW_EVIDENCE" | "CLARIFICATION" | "ACKNOWLEDGEMENT" | "UNRELATED",
256
+ "reason": "Short rationale."
257
+ }
258
+ ]
259
+ }
260
+ </output_contract>`.trim();
261
+ export const triageActionOutputContract = `
262
+ <output_contract>
263
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
264
+
265
+ The object must match this shape:
266
+ {
267
+ "action": "ASK" | "COMMENT" | "CLOSE" | "PR" | "CLEAR_ONLY",
268
+ "reason": "Short rationale."
269
+ }
270
+
271
+ Rules:
272
+ - Choose only an action listed as allowed in the task context.
273
+ - ASK means post an author-mentioned question and do not close, create a PR, or clear labels.
274
+ - COMMENT means post a decision comment only.
275
+ - CLOSE means post a decision comment and close the issue.
276
+ - PR means post a decision comment and create an implementation PR.
277
+ - CLEAR_ONLY means clear labels without posting a comment.
278
+ </output_contract>`.trim();
174
279
  const outputContractsBySchemaName = {
175
280
  "CI classification": ciClassificationOutputContract,
176
281
  "close reconsideration": closeReconsiderationOutputContract,
@@ -179,6 +284,14 @@ const outputContractsBySchemaName = {
179
284
  rereview: rereviewOutputContract,
180
285
  "rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
181
286
  review: reviewOutputContract,
287
+ "triage action": triageActionOutputContract,
288
+ "triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
289
+ "triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
290
+ "triage create PR": triageCreatePrOutputContract,
291
+ "triage comment classification": triageCommentClassificationOutputContract,
292
+ "triage duplicate": triageDuplicateOutputContract,
293
+ "triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
294
+ "triage reconsider": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
182
295
  };
183
296
  export function repairPrompt(schemaName) {
184
297
  const outputContract = outputContractsBySchemaName[schemaName];