opencode-magi 0.1.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 (43) hide show
  1. package/README.md +33 -10
  2. package/dist/commands.js +4 -0
  3. package/dist/config/output.js +11 -2
  4. package/dist/config/resolve.js +124 -26
  5. package/dist/config/validate.js +486 -191
  6. package/dist/config/worktree.js +19 -0
  7. package/dist/github/commands.js +349 -17
  8. package/dist/index.js +257 -27
  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 +24 -4
  14. package/dist/orchestrator/report.js +15 -1
  15. package/dist/orchestrator/review-context.js +309 -0
  16. package/dist/orchestrator/review.js +78 -10
  17. package/dist/orchestrator/run-manager.js +418 -20
  18. package/dist/orchestrator/triage.js +1119 -0
  19. package/dist/permissions/editor.json +8 -1
  20. package/dist/prompts/compose.js +172 -15
  21. package/dist/prompts/contracts.js +119 -12
  22. package/dist/prompts/output.js +149 -14
  23. package/dist/prompts/templates/{close-reconsideration.md → review/close-reconsideration.md} +1 -2
  24. package/dist/prompts/templates/review/review.md +13 -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 +28 -27
  36. package/schema.json +234 -90
  37. package/dist/prompts/templates/rereview-close-reconsideration.md +0 -6
  38. package/dist/prompts/templates/review.md +0 -7
  39. /package/dist/prompts/templates/{ci-classification-after-edit.md → merge/ci-classification.md} +0 -0
  40. /package/dist/prompts/templates/{edit.md → merge/edit.md} +0 -0
  41. /package/dist/prompts/templates/{ci-classification.md → review/ci-classification.md} +0 -0
  42. /package/dist/prompts/templates/{finding-validation.md → review/finding-validation.md} +0 -0
  43. /package/dist/prompts/templates/{rereview.md → review/rereview.md} +0 -0
@@ -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 "";
@@ -34,7 +34,7 @@ function repositoryValues(repository) {
34
34
  };
35
35
  }
36
36
  function reviewValues(input) {
37
- const ciFailureContext = input.ciFailureContext?.trim() ?? input.ciFailureLogs?.trim() ?? "";
37
+ const ciFailureContext = input.ciFailureContext?.trim() ?? "";
38
38
  return {
39
39
  ...repositoryValues(input.repository),
40
40
  baseSha: input.baseSha,
@@ -42,13 +42,10 @@ function reviewValues(input) {
42
42
  ciFailureContextBlock: ciFailureContext
43
43
  ? `<ci_failure_context>\n${ciFailureContext}\n</ci_failure_context>`
44
44
  : "",
45
- ciFailureLogs: ciFailureContext,
46
- ciFailureLogsBlock: ciFailureContext
47
- ? `<ci_failure_context>\n${ciFailureContext}\n</ci_failure_context>`
48
- : "",
49
45
  headSha: input.headSha,
50
46
  jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
51
47
  pr: String(input.pr),
48
+ reviewContext: input.reviewContext ?? "",
52
49
  worktreePath: input.worktreePath,
53
50
  };
54
51
  }
@@ -69,6 +66,22 @@ function editValues(input) {
69
66
  worktreePath: input.worktreePath,
70
67
  };
71
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
+ }
72
85
  function personaBlock(persona) {
73
86
  return persona ? `<persona>\n${persona}\n</persona>` : "";
74
87
  }
@@ -80,6 +93,9 @@ function previousReviewBlock(previousReview) {
80
93
  ? `<previous_review>\n${previousReview.trim()}\n</previous_review>`
81
94
  : "";
82
95
  }
96
+ function reviewContextBlock(reviewContext) {
97
+ return reviewContext?.trim() ? reviewContext.trim() : "";
98
+ }
83
99
  async function reviewGuidelinesBlock(input) {
84
100
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
85
101
  return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
@@ -88,8 +104,15 @@ async function editGuidelinesBlock(input) {
88
104
  const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
89
105
  return body ? `<edit_guidelines>\n${body}\n</edit_guidelines>` : "";
90
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
+ }
91
111
  async function sessionContextBlocks(input) {
92
112
  return [
113
+ input.includeSessionContext
114
+ ? reviewContextBlock(input.values.reviewContext)
115
+ : "",
93
116
  input.includeSessionContext ? languageBlock(input.repository.language) : "",
94
117
  input.includeSessionContext ? personaBlock(input.reviewer.persona) : "",
95
118
  input.includeReviewGuidelines
@@ -104,13 +127,14 @@ async function sessionContextBlocks(input) {
104
127
  export async function composeReviewPrompt(input) {
105
128
  const values = reviewValues(input);
106
129
  const task = await taskBlock({
107
- builtin: "review",
130
+ builtin: "review/review",
108
131
  customPath: input.repository.prompts.review,
109
132
  directory: input.directory,
110
133
  values,
111
134
  });
112
135
  return [
113
136
  task,
137
+ reviewContextBlock(input.reviewContext),
114
138
  languageBlock(input.repository.language),
115
139
  personaBlock(input.reviewer.persona),
116
140
  await reviewGuidelinesBlock({
@@ -126,13 +150,14 @@ export async function composeReviewPrompt(input) {
126
150
  export async function composeRereviewPrompt(input) {
127
151
  const values = rereviewValues(input);
128
152
  const task = await taskBlock({
129
- builtin: "rereview",
153
+ builtin: "review/rereview",
130
154
  customPath: input.repository.prompts.rereview,
131
155
  directory: input.directory,
132
156
  values,
133
157
  });
134
158
  return [
135
159
  task,
160
+ reviewContextBlock(input.reviewContext),
136
161
  input.includeSessionContext === false
137
162
  ? ""
138
163
  : languageBlock(input.repository.language),
@@ -154,7 +179,7 @@ export async function composeRereviewPrompt(input) {
154
179
  export async function composeEditPrompt(input) {
155
180
  const values = editValues(input);
156
181
  const task = await taskBlock({
157
- builtin: "edit",
182
+ builtin: "merge/edit",
158
183
  customPath: input.repository.prompts.edit,
159
184
  directory: input.directory,
160
185
  values,
@@ -177,7 +202,7 @@ export async function composeEditPrompt(input) {
177
202
  export async function composeFindingValidationPrompt(input) {
178
203
  const values = { ...reviewValues(input), findings: input.findings };
179
204
  const task = await taskBlock({
180
- builtin: "finding-validation",
205
+ builtin: "review/finding-validation",
181
206
  customPath: input.repository.prompts.findingValidation,
182
207
  directory: input.directory,
183
208
  values,
@@ -203,7 +228,7 @@ export async function composeCloseReconsiderationPrompt(input) {
203
228
  closeReason: input.closeReason ?? "",
204
229
  };
205
230
  const task = await taskBlock({
206
- builtin: "close-reconsideration",
231
+ builtin: "review/close-reconsideration",
207
232
  customPath: input.repository.prompts.closeReconsideration,
208
233
  directory: input.directory,
209
234
  values,
@@ -230,8 +255,8 @@ export async function composeRereviewCloseReconsiderationPrompt(input) {
230
255
  previousHeadSha: input.previousHeadSha,
231
256
  };
232
257
  const task = await taskBlock({
233
- builtin: "rereview-close-reconsideration",
234
- customPath: input.repository.prompts.rereviewCloseReconsideration,
258
+ builtin: "review/close-reconsideration",
259
+ customPath: input.repository.prompts.closeReconsideration,
235
260
  directory: input.directory,
236
261
  values,
237
262
  });
@@ -257,7 +282,7 @@ export async function composeCiClassificationPrompt(input) {
257
282
  pr: String(input.pr),
258
283
  };
259
284
  const task = await taskBlock({
260
- builtin: "ci-classification",
285
+ builtin: "review/ci-classification",
261
286
  customPath: input.repository.prompts.ciClassification,
262
287
  directory: input.directory,
263
288
  values,
@@ -282,7 +307,7 @@ export async function composeCiClassificationAfterEditPrompt(input) {
282
307
  worktreePath: input.worktreePath,
283
308
  };
284
309
  const task = await taskBlock({
285
- builtin: "ci-classification-after-edit",
310
+ builtin: "merge/ci-classification",
286
311
  customPath: input.repository.prompts.ciClassificationAfterEdit ??
287
312
  input.repository.prompts.ciClassification,
288
313
  directory: input.directory,
@@ -296,3 +321,135 @@ export async function composeCiClassificationAfterEditPrompt(input) {
296
321
  .filter(Boolean)
297
322
  .join("\n\n");
298
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];