opencode-magi 0.2.0 → 0.3.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 (35) 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 +290 -3
  6. package/dist/config/worktree.js +8 -2
  7. package/dist/github/commands.js +343 -15
  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 +73 -0
  12. package/dist/orchestrator/majority.js +14 -0
  13. package/dist/orchestrator/merge.js +16 -3
  14. package/dist/orchestrator/report.js +15 -1
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +49 -9
  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 +162 -1
  21. package/dist/prompts/contracts.js +119 -12
  22. package/dist/prompts/output.js +149 -14
  23. package/dist/prompts/templates/review/review.md +6 -0
  24. package/dist/prompts/templates/triage/acceptance.md +7 -0
  25. package/dist/prompts/templates/triage/action.md +5 -0
  26. package/dist/prompts/templates/triage/category.md +10 -0
  27. package/dist/prompts/templates/triage/comment-classification.md +7 -0
  28. package/dist/prompts/templates/triage/comment.md +5 -0
  29. package/dist/prompts/templates/triage/create.md +7 -0
  30. package/dist/prompts/templates/triage/duplicate.md +7 -0
  31. package/dist/prompts/templates/triage/existing-pr.md +7 -0
  32. package/dist/prompts/templates/triage/question.md +5 -0
  33. package/dist/prompts/templates/triage/reconsider.md +5 -0
  34. package/package.json +5 -2
  35. package/schema.json +127 -2
@@ -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
  }
@@ -65,6 +66,22 @@ function editValues(input) {
65
66
  worktreePath: input.worktreePath,
66
67
  };
67
68
  }
69
+ function triageValues(input) {
70
+ const categories = input.repository.triage?.categories ?? [];
71
+ const categoryOptions = categories
72
+ .map((category) => category.description
73
+ ? `- ${category.id}: ${category.description}`
74
+ : `- ${category.id}`)
75
+ .join("\n");
76
+ return {
77
+ ...repositoryValues(input.repository),
78
+ author: input.author ?? "",
79
+ categoryOptions,
80
+ context: input.context,
81
+ issue: String(input.issue),
82
+ worktreePath: input.worktreePath ?? "",
83
+ };
84
+ }
68
85
  function personaBlock(persona) {
69
86
  return persona ? `<persona>\n${persona}\n</persona>` : "";
70
87
  }
@@ -76,6 +93,9 @@ function previousReviewBlock(previousReview) {
76
93
  ? `<previous_review>\n${previousReview.trim()}\n</previous_review>`
77
94
  : "";
78
95
  }
96
+ function reviewContextBlock(reviewContext) {
97
+ return reviewContext?.trim() ? reviewContext.trim() : "";
98
+ }
79
99
  async function reviewGuidelinesBlock(input) {
80
100
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
81
101
  return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
@@ -84,8 +104,15 @@ async function editGuidelinesBlock(input) {
84
104
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
85
105
  return body ? `<edit_guidelines>\n${body}\n</edit_guidelines>` : "";
86
106
  }
107
+ async function createGuidelinesBlock(input) {
108
+ const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
109
+ return body ? `<create_guidelines>\n${body}\n</create_guidelines>` : "";
110
+ }
87
111
  async function sessionContextBlocks(input) {
88
112
  return [
113
+ input.includeSessionContext
114
+ ? reviewContextBlock(input.values.reviewContext)
115
+ : "",
89
116
  input.includeSessionContext ? languageBlock(input.repository.language) : "",
90
117
  input.includeSessionContext ? personaBlock(input.reviewer.persona) : "",
91
118
  input.includeReviewGuidelines
@@ -107,6 +134,7 @@ export async function composeReviewPrompt(input) {
107
134
  });
108
135
  return [
109
136
  task,
137
+ reviewContextBlock(input.reviewContext),
110
138
  languageBlock(input.repository.language),
111
139
  personaBlock(input.reviewer.persona),
112
140
  await reviewGuidelinesBlock({
@@ -129,6 +157,7 @@ export async function composeRereviewPrompt(input) {
129
157
  });
130
158
  return [
131
159
  task,
160
+ reviewContextBlock(input.reviewContext),
132
161
  input.includeSessionContext === false
133
162
  ? ""
134
163
  : languageBlock(input.repository.language),
@@ -292,3 +321,135 @@ export async function composeCiClassificationAfterEditPrompt(input) {
292
321
  .filter(Boolean)
293
322
  .join("\n\n");
294
323
  }
324
+ async function composeTriageVotePrompt(input) {
325
+ const values = triageValues(input);
326
+ const task = await taskBlock({
327
+ builtin: `triage/${input.builtin}`,
328
+ customPath: input.customPath,
329
+ directory: input.directory,
330
+ values,
331
+ });
332
+ return [
333
+ task,
334
+ languageBlock(input.repository.language),
335
+ personaBlock(input.reviewer.persona),
336
+ input.outputContract,
337
+ ]
338
+ .filter(Boolean)
339
+ .join("\n\n");
340
+ }
341
+ export async function composeTriageActionPrompt(input) {
342
+ return composeTriageVotePrompt({
343
+ ...input,
344
+ builtin: "action",
345
+ customPath: input.repository.triage?.prompts.action,
346
+ outputContract: triageActionOutputContract,
347
+ });
348
+ }
349
+ export async function composeTriageCommentPrompt(input) {
350
+ const values = triageValues(input);
351
+ const task = await taskBlock({
352
+ builtin: "triage/comment",
353
+ customPath: input.repository.triage?.prompts.comment,
354
+ directory: input.directory,
355
+ values,
356
+ });
357
+ return [
358
+ task,
359
+ languageBlock(input.repository.language),
360
+ `<context>\n${input.context}\n</context>`,
361
+ ]
362
+ .filter(Boolean)
363
+ .join("\n\n");
364
+ }
365
+ export async function composeTriageQuestionPrompt(input) {
366
+ const values = triageValues(input);
367
+ const task = await taskBlock({
368
+ builtin: "triage/question",
369
+ customPath: input.repository.triage?.prompts.question,
370
+ directory: input.directory,
371
+ values,
372
+ });
373
+ return [
374
+ task,
375
+ languageBlock(input.repository.language),
376
+ `<context>\n${input.context}\n</context>`,
377
+ ]
378
+ .filter(Boolean)
379
+ .join("\n\n");
380
+ }
381
+ export async function composeTriageCreatePrPrompt(input) {
382
+ const values = triageValues(input);
383
+ const task = await taskBlock({
384
+ builtin: "triage/create",
385
+ customPath: input.repository.triage?.prompts.create,
386
+ directory: input.directory,
387
+ values,
388
+ });
389
+ const persona = input.repository.agents.triageCreator?.persona;
390
+ return [
391
+ task,
392
+ languageBlock(input.repository.language),
393
+ personaBlock(persona),
394
+ await createGuidelinesBlock({
395
+ directory: input.directory,
396
+ path: input.repository.triage?.prompts.createGuidelines,
397
+ values,
398
+ }),
399
+ triageCreatePrOutputContract,
400
+ ]
401
+ .filter(Boolean)
402
+ .join("\n\n");
403
+ }
404
+ export async function composeTriageExistingPrPrompt(input) {
405
+ return composeTriageVotePrompt({
406
+ ...input,
407
+ builtin: "existing-pr",
408
+ customPath: input.repository.triage?.prompts.existingPr,
409
+ outputContract: triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
410
+ });
411
+ }
412
+ export async function composeTriageDuplicatePrompt(input) {
413
+ return composeTriageVotePrompt({
414
+ ...input,
415
+ builtin: "duplicate",
416
+ customPath: input.repository.triage?.prompts.duplicate,
417
+ outputContract: triageDuplicateOutputContract,
418
+ });
419
+ }
420
+ export async function composeTriageCategoryPrompt(input) {
421
+ const categories = input.repository.triage?.categories ?? [];
422
+ const votes = ["ASK", ...categories.map((category) => category.id)]
423
+ .map((vote) => JSON.stringify(vote))
424
+ .join(" | ");
425
+ return composeTriageVotePrompt({
426
+ ...input,
427
+ builtin: "category",
428
+ customPath: input.repository.triage?.prompts.category,
429
+ outputContract: triageVoteOutputContract(votes),
430
+ });
431
+ }
432
+ export async function composeTriageAcceptancePrompt(input) {
433
+ return composeTriageVotePrompt({
434
+ ...input,
435
+ builtin: "acceptance",
436
+ customPath: input.repository.triage?.prompts.acceptance,
437
+ outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
438
+ });
439
+ }
440
+ export async function composeTriageCommentClassificationPrompt(input) {
441
+ return composeTriageVotePrompt({
442
+ ...input,
443
+ builtin: "comment-classification",
444
+ customPath: input.repository.triage?.prompts.commentClassification,
445
+ outputContract: triageCommentClassificationOutputContract,
446
+ });
447
+ }
448
+ export async function composeTriageReconsiderPrompt(input) {
449
+ return composeTriageVotePrompt({
450
+ ...input,
451
+ builtin: "reconsider",
452
+ customPath: input.repository.triage?.prompts.reconsider,
453
+ outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
454
+ });
455
+ }
@@ -15,16 +15,25 @@ 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
34
  - line and startLine must refer to lines inside the PR diff hunk.
27
35
  - Omit startLine for single-line findings.
36
+ - Use requirementFindings for missing closing-issue requirements that do not map cleanly to a diff line.
28
37
  </output_contract>`.trim();
29
38
  export const rereviewOutputContract = `
30
39
  <output_contract>
@@ -36,15 +45,17 @@ The object must match this shape:
36
45
  "resolve": [{ "commentId": 123, "threadId": "..." }],
37
46
  "followUps": [{ "commentId": 123, "body": "..." }],
38
47
  "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
48
+ "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }],
39
49
  "reason": "Required only for CLOSE."
40
50
  }
41
51
 
42
52
  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.
53
+ - MERGE requires empty followUps, newFindings, and requirementFindings arrays.
54
+ - CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
55
+ - CLOSE requires a reason and empty followUps, newFindings, and requirementFindings arrays.
46
56
  - line and startLine must refer to lines inside the latest PR diff hunk.
47
57
  - Omit startLine for single-line findings.
58
+ - Use requirementFindings for missing closing-issue requirements that do not map cleanly to a diff line.
48
59
  </output_contract>`.trim();
49
60
  export const findingValidationOutputContract = `
50
61
  <output_contract>
@@ -83,12 +94,13 @@ The object must match this shape:
83
94
  "issue": "What is wrong.",
84
95
  "fix": "How to fix it."
85
96
  }
86
- ]
97
+ ],
98
+ "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
87
99
  }
88
100
 
89
101
  Rules:
90
- - MERGE requires an empty findings array.
91
- - CHANGES_REQUESTED requires at least one finding.
102
+ - MERGE requires empty findings and requirementFindings arrays.
103
+ - CHANGES_REQUESTED requires at least one finding or requirementFinding.
92
104
  - CLOSE is not allowed in this reconsideration step.
93
105
  - Omit startLine for single-line findings.
94
106
  </output_contract>`.trim();
@@ -101,12 +113,13 @@ The object must match this shape:
101
113
  "verdict": "MERGE" | "CHANGES_REQUESTED",
102
114
  "resolve": [{ "commentId": 123, "threadId": "..." }],
103
115
  "followUps": [{ "commentId": 123, "body": "..." }],
104
- "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }]
116
+ "newFindings": [{ "path": "relative/path.ext", "line": 123, "startLine": 120, "body": "..." }],
117
+ "requirementFindings": [{ "issueNumber": 47, "requirement": "Missing requirement.", "evidence": "Why it is missing.", "fix": "How to fix it." }]
105
118
  }
106
119
 
107
120
  Rules:
108
- - MERGE requires empty followUps and newFindings arrays.
109
- - CHANGES_REQUESTED requires at least one followUp or newFinding.
121
+ - MERGE requires empty followUps, newFindings, and requirementFindings arrays.
122
+ - CHANGES_REQUESTED requires at least one followUp, newFinding, or requirementFinding.
110
123
  - CLOSE is not allowed in this reconsideration step.
111
124
  - Omit startLine for single-line findings.
112
125
  </output_contract>`.trim();
@@ -135,6 +148,32 @@ Rules:
135
148
  - responses must include a reply for each thread you addressed.
136
149
  - REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
137
150
  </output_contract>`.trim();
151
+ export const triageCreatePrOutputContract = `
152
+ <output_contract>
153
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
154
+
155
+ The object must match this shape:
156
+ {
157
+ "mode": "EDITED" | "REPLIED",
158
+ "commitSha": "full sha, required only when mode is EDITED; omit when mode is REPLIED",
159
+ "commitMessage": "fix(scope): short description, required only when mode is EDITED; omit when mode is REPLIED",
160
+ "filesTouched": ["relative/path.ext"],
161
+ "pullRequest": {
162
+ "title": "PR title, required only when mode is EDITED; omit when mode is REPLIED",
163
+ "body": "PR body, required only when mode is EDITED; omit when mode is REPLIED"
164
+ },
165
+ "responses": [{ "commentId": 123, "action": "FIXED" | "DISAGREE" | "ASK", "body": "Fixed." }]
166
+ }
167
+
168
+ Rules:
169
+ - Use EDITED only when you edited files, staged changes, and committed.
170
+ - Use REPLIED when you only replied without code changes.
171
+ - For EDITED, pullRequest.title and pullRequest.body must be non-empty and follow the repository's PR conventions.
172
+ - Do not push or create the PR. The orchestrator pushes and creates the PR using pullRequest exactly as provided.
173
+ - filesTouched must include every final changed file.
174
+ - responses may be empty when no review threads were addressed.
175
+ - REPLIED requires filesTouched to be empty and at least one DISAGREE or ASK response.
176
+ </output_contract>`.trim();
138
177
  export const ciClassificationOutputContract = `
139
178
  <output_contract>
140
179
  Return exactly one JSON object and nothing else. Do not wrap it in markdown.
@@ -171,6 +210,66 @@ Rules:
171
210
  - SCOPE_OUT means the failure is likely flaky, external, or infrastructure-related and may be rerun.
172
211
  - If uncertain, choose SCOPE_IN.
173
212
  </output_contract>`.trim();
213
+ export function triageVoteOutputContract(votes) {
214
+ return `
215
+ <output_contract>
216
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
217
+
218
+ The object must match this shape:
219
+ {
220
+ "vote": ${votes},
221
+ "reason": "Short rationale."
222
+ }
223
+ </output_contract>`.trim();
224
+ }
225
+ export const triageDuplicateOutputContract = `
226
+ <output_contract>
227
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
228
+
229
+ The object must match this shape:
230
+ {
231
+ "vote": "DUPLICATE" | "NOT_DUPLICATE",
232
+ "duplicateOf": 123,
233
+ "reason": "Short rationale."
234
+ }
235
+
236
+ Rules:
237
+ - duplicateOf is required only when vote is DUPLICATE.
238
+ - duplicateOf must be one of the provided duplicate candidate issue numbers.
239
+ </output_contract>`.trim();
240
+ export const triageCommentClassificationOutputContract = `
241
+ <output_contract>
242
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
243
+
244
+ The object must match this shape:
245
+ {
246
+ "comments": [
247
+ {
248
+ "commentId": 123,
249
+ "classification": "OBJECTION" | "NEW_EVIDENCE" | "CLARIFICATION" | "ACKNOWLEDGEMENT" | "UNRELATED",
250
+ "reason": "Short rationale."
251
+ }
252
+ ]
253
+ }
254
+ </output_contract>`.trim();
255
+ export const triageActionOutputContract = `
256
+ <output_contract>
257
+ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
258
+
259
+ The object must match this shape:
260
+ {
261
+ "action": "ASK" | "COMMENT" | "CLOSE" | "PR" | "CLEAR_ONLY",
262
+ "reason": "Short rationale."
263
+ }
264
+
265
+ Rules:
266
+ - Choose only an action listed as allowed in the task context.
267
+ - ASK means post an author-mentioned question and do not close, create a PR, or clear labels.
268
+ - COMMENT means post a decision comment only.
269
+ - CLOSE means post a decision comment and close the issue.
270
+ - PR means post a decision comment and create an implementation PR.
271
+ - CLEAR_ONLY means clear labels without posting a comment.
272
+ </output_contract>`.trim();
174
273
  const outputContractsBySchemaName = {
175
274
  "CI classification": ciClassificationOutputContract,
176
275
  "close reconsideration": closeReconsiderationOutputContract,
@@ -179,6 +278,14 @@ const outputContractsBySchemaName = {
179
278
  rereview: rereviewOutputContract,
180
279
  "rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
181
280
  review: reviewOutputContract,
281
+ "triage action": triageActionOutputContract,
282
+ "triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
283
+ "triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
284
+ "triage create PR": triageCreatePrOutputContract,
285
+ "triage comment classification": triageCommentClassificationOutputContract,
286
+ "triage duplicate": triageDuplicateOutputContract,
287
+ "triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
288
+ "triage reconsider": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
182
289
  };
183
290
  export function repairPrompt(schemaName) {
184
291
  const outputContract = outputContractsBySchemaName[schemaName];
@@ -71,6 +71,100 @@ function requireNumber(value, path) {
71
71
  throw new Error(`${path} must be an integer`);
72
72
  return value;
73
73
  }
74
+ function parseRequirementFindings(value) {
75
+ return (value == null ? [] : requireArray(value, "requirementFindings")).map((finding, index) => {
76
+ const item = finding;
77
+ return {
78
+ evidence: requireString(item.evidence, `requirementFindings[${index}].evidence`),
79
+ fix: requireString(item.fix, `requirementFindings[${index}].fix`),
80
+ issueNumber: requireNumber(item.issueNumber, `requirementFindings[${index}].issueNumber`),
81
+ requirement: requireString(item.requirement, `requirementFindings[${index}].requirement`),
82
+ };
83
+ });
84
+ }
85
+ function requireOneOf(value, path, values) {
86
+ const text = requireString(value, path);
87
+ if (!values.includes(text)) {
88
+ throw new Error(`${path} must be ${values.join(", ")}`);
89
+ }
90
+ return text;
91
+ }
92
+ function parseTriageVote(text, votes) {
93
+ const data = extractJson(text);
94
+ if (!data || typeof data !== "object")
95
+ throw new Error("triage vote output must be an object");
96
+ return {
97
+ reason: requireString(data.reason, "reason"),
98
+ vote: requireOneOf(data.vote, "vote", votes),
99
+ };
100
+ }
101
+ export function parseTriageExistingPrOutput(text) {
102
+ return parseTriageVote(text, [
103
+ "RELATED_PR_DOES_NOT_HANDLE_ISSUE",
104
+ "RELATED_PR_HANDLES_ISSUE",
105
+ ]);
106
+ }
107
+ export function parseTriageCategoryOutput(text, categories) {
108
+ return parseTriageVote(text, ["ASK", ...categories]);
109
+ }
110
+ export function parseTriageBinaryOutput(text) {
111
+ return parseTriageVote(text, ["ASK", "NO", "YES"]);
112
+ }
113
+ export function parseTriageDuplicateOutput(text) {
114
+ const data = extractJson(text);
115
+ if (!data || typeof data !== "object")
116
+ throw new Error("triage duplicate output must be an object");
117
+ const vote = requireOneOf(data.vote, "vote", [
118
+ "DUPLICATE",
119
+ "NOT_DUPLICATE",
120
+ ]);
121
+ const duplicateOf = data.duplicateOf == null
122
+ ? undefined
123
+ : requireNumber(data.duplicateOf, "duplicateOf");
124
+ if (vote === "DUPLICATE" && duplicateOf == null)
125
+ throw new Error("DUPLICATE requires duplicateOf");
126
+ return {
127
+ duplicateOf,
128
+ reason: requireString(data.reason, "reason"),
129
+ vote,
130
+ };
131
+ }
132
+ export function parseTriageCommentClassificationOutput(text) {
133
+ const data = extractJson(text);
134
+ if (!data || typeof data !== "object")
135
+ throw new Error("triage comment classification output must be an object");
136
+ return {
137
+ comments: requireArray(data.comments, "comments").map((item, index) => {
138
+ const value = item;
139
+ return {
140
+ classification: requireOneOf(value.classification, `comments[${index}].classification`, [
141
+ "ACKNOWLEDGEMENT",
142
+ "CLARIFICATION",
143
+ "NEW_EVIDENCE",
144
+ "OBJECTION",
145
+ "UNRELATED",
146
+ ]),
147
+ commentId: requireNumber(value.commentId, `comments[${index}].commentId`),
148
+ reason: requireString(value.reason, `comments[${index}].reason`),
149
+ };
150
+ }),
151
+ };
152
+ }
153
+ export function parseTriageActionOutput(text) {
154
+ const data = extractJson(text);
155
+ if (!data || typeof data !== "object")
156
+ throw new Error("triage action output must be an object");
157
+ return {
158
+ action: requireOneOf(data.action, "action", [
159
+ "ASK",
160
+ "CLEAR_ONLY",
161
+ "CLOSE",
162
+ "COMMENT",
163
+ "PR",
164
+ ]),
165
+ reason: requireString(data.reason, "reason"),
166
+ };
167
+ }
74
168
  export function parseReviewOutput(text) {
75
169
  const data = extractJson(text);
76
170
  if (!data || typeof data !== "object")
@@ -92,12 +186,17 @@ export function parseReviewOutput(text) {
92
186
  : requireNumber(item.startLine, `findings[${index}].startLine`),
93
187
  };
94
188
  });
95
- if (data.verdict === "MERGE" && findings.length)
96
- throw new Error("MERGE requires no findings");
97
- if (data.verdict === "CHANGES_REQUESTED" && !findings.length)
98
- throw new Error("CHANGES_REQUESTED requires findings");
99
- if (data.verdict === "CLOSE" && findings.length)
100
- throw new Error("CLOSE requires no findings");
189
+ const requirementFindings = parseRequirementFindings(data.requirementFindings);
190
+ if (data.verdict === "MERGE" &&
191
+ (findings.length || requirementFindings.length))
192
+ throw new Error("MERGE requires no findings or requirementFindings");
193
+ if (data.verdict === "CHANGES_REQUESTED" &&
194
+ !findings.length &&
195
+ !requirementFindings.length)
196
+ throw new Error("CHANGES_REQUESTED requires findings or requirementFindings");
197
+ if (data.verdict === "CLOSE" &&
198
+ (findings.length || requirementFindings.length))
199
+ throw new Error("CLOSE requires no findings or requirementFindings");
101
200
  const reason = typeof data.reason === "string" && data.reason.trim()
102
201
  ? data.reason
103
202
  : undefined;
@@ -106,6 +205,7 @@ export function parseReviewOutput(text) {
106
205
  return {
107
206
  findings,
108
207
  reason,
208
+ requirementFindings,
109
209
  verdict: data.verdict,
110
210
  };
111
211
  }
@@ -138,24 +238,29 @@ export function parseRereviewOutput(text) {
138
238
  : requireNumber(value.startLine, `newFindings[${index}].startLine`),
139
239
  };
140
240
  });
141
- if (data.verdict === "MERGE" && (followUps.length || newFindings.length)) {
142
- throw new Error("MERGE requires no followUps or newFindings");
241
+ const requirementFindings = parseRequirementFindings(data.requirementFindings);
242
+ if (data.verdict === "MERGE" &&
243
+ (followUps.length || newFindings.length || requirementFindings.length)) {
244
+ throw new Error("MERGE requires no followUps, newFindings, or requirementFindings");
143
245
  }
144
- if (data.verdict === "CLOSE" && (followUps.length || newFindings.length)) {
145
- throw new Error("CLOSE requires no followUps or newFindings");
246
+ if (data.verdict === "CLOSE" &&
247
+ (followUps.length || newFindings.length || requirementFindings.length)) {
248
+ throw new Error("CLOSE requires no followUps, newFindings, or requirementFindings");
146
249
  }
147
250
  if (data.verdict === "CLOSE" && !data.reason) {
148
251
  throw new Error("CLOSE requires reason");
149
252
  }
150
253
  if (data.verdict === "CHANGES_REQUESTED" &&
151
254
  !followUps.length &&
152
- !newFindings.length) {
153
- throw new Error("CHANGES_REQUESTED requires followUps or newFindings");
255
+ !newFindings.length &&
256
+ !requirementFindings.length) {
257
+ throw new Error("CHANGES_REQUESTED requires followUps, newFindings, or requirementFindings");
154
258
  }
155
259
  return {
156
260
  followUps,
157
261
  newFindings,
158
262
  reason: data.reason == null ? undefined : requireString(data.reason, "reason"),
263
+ requirementFindings,
159
264
  resolve,
160
265
  verdict: data.verdict,
161
266
  };
@@ -212,7 +317,21 @@ export function parseCiClassificationOutput(text) {
212
317
  }),
213
318
  };
214
319
  }
215
- export function parseEditOutput(text) {
320
+ function parsePullRequest(value, options) {
321
+ if (value == null) {
322
+ if (options.required)
323
+ throw new Error("pullRequest is required");
324
+ return undefined;
325
+ }
326
+ if (typeof value !== "object")
327
+ throw new Error("pullRequest must be an object");
328
+ const pullRequest = value;
329
+ return {
330
+ body: requireString(pullRequest.body, "pullRequest.body"),
331
+ title: requireString(pullRequest.title, "pullRequest.title"),
332
+ };
333
+ }
334
+ function parseEditOutputWithOptions(text, options) {
216
335
  const data = extractJson(text);
217
336
  if (!data || typeof data !== "object")
218
337
  throw new Error("edit output must be an object");
@@ -230,16 +349,20 @@ export function parseEditOutput(text) {
230
349
  commentId: requireNumber(value.commentId, `responses[${index}].commentId`),
231
350
  };
232
351
  });
233
- if (!responses.length)
352
+ if (options.requireResponses && !responses.length)
234
353
  throw new Error("responses must not be empty");
235
354
  if (data.mode === "EDITED") {
236
355
  if (!filesTouched.length)
237
356
  throw new Error("EDITED requires filesTouched");
357
+ const pullRequest = parsePullRequest(data.pullRequest, {
358
+ required: options.requirePullRequest,
359
+ });
238
360
  return {
239
361
  commitMessage: requireString(data.commitMessage, "commitMessage"),
240
362
  commitSha: requireString(data.commitSha, "commitSha"),
241
363
  filesTouched,
242
364
  mode: data.mode,
365
+ ...(pullRequest ? { pullRequest } : {}),
243
366
  responses,
244
367
  };
245
368
  }
@@ -258,3 +381,15 @@ export function parseEditOutput(text) {
258
381
  responses,
259
382
  };
260
383
  }
384
+ export function parseEditOutput(text) {
385
+ return parseEditOutputWithOptions(text, {
386
+ requirePullRequest: false,
387
+ requireResponses: true,
388
+ });
389
+ }
390
+ export function parseTriageCreatePrOutput(text) {
391
+ return parseEditOutputWithOptions(text, {
392
+ requirePullRequest: true,
393
+ requireResponses: false,
394
+ });
395
+ }
@@ -4,4 +4,10 @@ Review only the diff from {baseSha} to {headSha}.
4
4
  Use: git -C {jsonEncodedWorktreePath} diff {baseSha}...{headSha}
5
5
  Do not edit files or perform write operations.
6
6
 
7
+ This PR may include closing issue references.
8
+ For each closing issue, review whether the PR fully satisfies the issue body, acceptance criteria, required behavior, required tests, required documentation, and bounded issue comments.
9
+ Request changes if a closing issue requirement is missing, only documented, only schema-exposed, or not wired into runtime behavior.
10
+ Do not approve solely because the PR improves the codebase if it claims to close an issue that remains incomplete.
11
+ For referenced non-closing issues, use them as context only unless the PR body explicitly claims to complete them.
12
+
7
13
  {ciFailureContextBlock}
@@ -0,0 +1,7 @@
1
+ Evaluate issue #{issue} in {owner}/{repo} for the selected category.
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.
4
+
5
+ <context>
6
+ {context}
7
+ </context>