opencode-magi 0.0.0-dev-20260520165753 → 0.0.0-dev-20260520173258
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.
- package/dist/config/resolve.js +29 -10
- package/dist/config/validate.js +42 -17
- package/dist/github/commands.js +98 -6
- package/dist/index.js +133 -24
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/report.js +8 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +26 -5
- package/dist/orchestrator/run-manager.js +5 -3
- package/dist/orchestrator/triage.js +151 -88
- package/dist/prompts/compose.js +28 -16
- package/dist/prompts/contracts.js +28 -16
- package/dist/prompts/output.js +36 -24
- package/dist/prompts/templates/review/review.md +6 -0
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/package.json +1 -1
- package/schema.json +16 -15
- package/dist/prompts/templates/triage/bug.md +0 -7
- package/dist/prompts/templates/triage/feature.md +0 -7
- package/dist/prompts/templates/triage/kind.md +0 -7
|
@@ -3,33 +3,27 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { issueRunOutputDir } from "../config/output";
|
|
4
4
|
import { worktreeBaseDir } from "../config/worktree";
|
|
5
5
|
import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
|
|
6
|
-
import {
|
|
7
|
-
import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput,
|
|
6
|
+
import { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCommentPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageQuestionPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
|
+
import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
|
|
8
8
|
import { aggregateStringMajority, majorityThreshold } from "./majority";
|
|
9
9
|
import { runModelText, runModelWithRepair } from "./model";
|
|
10
10
|
const MARKER_PREFIX = "opencode-magi:triage";
|
|
11
|
-
const KIND_VOTES = ["ASK", "BUG", "FEATURE"];
|
|
12
11
|
const BINARY_VOTES = ["ASK", "NO", "YES"];
|
|
13
12
|
const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
|
|
14
13
|
const EXISTING_PR_VOTES = [
|
|
15
14
|
"RELATED_PR_DOES_NOT_HANDLE_ISSUE",
|
|
16
15
|
"RELATED_PR_HANDLES_ISSUE",
|
|
17
16
|
];
|
|
18
|
-
const FINAL_VOTES = [
|
|
19
|
-
"ASK",
|
|
20
|
-
"BUG_ACCEPTED",
|
|
21
|
-
"BUG_REJECTED",
|
|
22
|
-
"DUPLICATE",
|
|
23
|
-
"FEATURE_ACCEPTED",
|
|
24
|
-
"FEATURE_REJECTED",
|
|
25
|
-
];
|
|
26
17
|
const RECONSIDERATION_CLASSES = new Set([
|
|
27
18
|
"CLARIFICATION",
|
|
28
19
|
"NEW_EVIDENCE",
|
|
29
20
|
"OBJECTION",
|
|
30
21
|
]);
|
|
31
22
|
function marker(input) {
|
|
32
|
-
|
|
23
|
+
const askReason = input.decision.askReason
|
|
24
|
+
? ` askReason=${input.decision.askReason}`
|
|
25
|
+
: "";
|
|
26
|
+
return `<!-- ${MARKER_PREFIX} v=2 issue=${input.issue} category=${input.decision.category ?? "none"} disposition=${input.decision.disposition}${askReason} action=${input.action} checkpoint=${input.checkpoint ?? "pending"} pr=${input.pr ?? "none"} processed=${(input.processed ?? []).join(",")} -->`;
|
|
33
27
|
}
|
|
34
28
|
export function parseTriageMarker(body) {
|
|
35
29
|
const match = body.match(/<!--\s*opencode-magi:triage\s+([^>]+?)\s*-->/);
|
|
@@ -45,13 +39,26 @@ export function parseTriageMarker(body) {
|
|
|
45
39
|
: [part.slice(0, index), part.slice(index + 1)];
|
|
46
40
|
}));
|
|
47
41
|
const version = Number(entries.v);
|
|
48
|
-
if (version !== 1)
|
|
42
|
+
if (version !== 1 && version !== 2)
|
|
49
43
|
return undefined;
|
|
50
44
|
return {
|
|
51
45
|
action: entries.action,
|
|
46
|
+
askReason: entries.askReason === "acceptance_unclear" ||
|
|
47
|
+
entries.askReason === "category_unclear"
|
|
48
|
+
? entries.askReason
|
|
49
|
+
: undefined,
|
|
50
|
+
category: entries.category === "none" ? null : entries.category || undefined,
|
|
52
51
|
checkpoint: entries.checkpoint && Number.isFinite(Number(entries.checkpoint))
|
|
53
52
|
? Number(entries.checkpoint)
|
|
54
53
|
: undefined,
|
|
54
|
+
disposition: entries.disposition === "accepted" ||
|
|
55
|
+
entries.disposition === "rejected" ||
|
|
56
|
+
entries.disposition === "ask" ||
|
|
57
|
+
entries.disposition === "duplicate" ||
|
|
58
|
+
entries.disposition === "clear_only" ||
|
|
59
|
+
entries.disposition === "failed"
|
|
60
|
+
? entries.disposition
|
|
61
|
+
: undefined,
|
|
55
62
|
issue: entries.issue ? Number(entries.issue) : undefined,
|
|
56
63
|
pr: entries.pr,
|
|
57
64
|
processed: entries.processed
|
|
@@ -69,17 +76,15 @@ function existingClearLabels(issue, labels) {
|
|
|
69
76
|
const existing = new Set(issue.labels.map((label) => label.toLowerCase()));
|
|
70
77
|
return labels.filter((label) => existing.has(label.toLowerCase()));
|
|
71
78
|
}
|
|
72
|
-
export function
|
|
79
|
+
export function resolveIssueCategory(issue, repository) {
|
|
73
80
|
const triage = repository.triage;
|
|
74
81
|
if (!triage)
|
|
75
82
|
throw new Error("triage configuration is required");
|
|
76
|
-
const
|
|
77
|
-
(issue.type != null &&
|
|
78
|
-
|
|
79
|
-
(issue.type != null && triage.kind.feature.type.includes(issue.type));
|
|
80
|
-
if (bug === feature)
|
|
83
|
+
const matches = triage.categories.filter((category) => labelsContain(issue.labels, category.labels) ||
|
|
84
|
+
(issue.type != null && category.types.includes(issue.type)));
|
|
85
|
+
if (matches.length !== 1)
|
|
81
86
|
return undefined;
|
|
82
|
-
return
|
|
87
|
+
return matches[0].id;
|
|
83
88
|
}
|
|
84
89
|
function issueContext(input) {
|
|
85
90
|
return JSON.stringify({
|
|
@@ -288,22 +293,42 @@ export function eligibleMentionReplies(input) {
|
|
|
288
293
|
});
|
|
289
294
|
}
|
|
290
295
|
function finalResultFromMarker(marker) {
|
|
291
|
-
if (marker.
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
296
|
+
if (marker.disposition) {
|
|
297
|
+
return {
|
|
298
|
+
askReason: marker.askReason,
|
|
299
|
+
category: marker.category ?? null,
|
|
300
|
+
disposition: marker.disposition,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
switch (marker.result) {
|
|
304
|
+
case "BUG_ACCEPTED":
|
|
305
|
+
case "RESOLVED_BY_MERGED_PR":
|
|
306
|
+
return { category: "bug", disposition: "accepted" };
|
|
307
|
+
case "BUG_REJECTED":
|
|
308
|
+
return { category: "bug", disposition: "rejected" };
|
|
309
|
+
case "FEATURE_ACCEPTED":
|
|
310
|
+
return { category: "feature", disposition: "accepted" };
|
|
311
|
+
case "FEATURE_REJECTED":
|
|
312
|
+
return { category: "feature", disposition: "rejected" };
|
|
313
|
+
case "ASK":
|
|
314
|
+
return {
|
|
315
|
+
askReason: "acceptance_unclear",
|
|
316
|
+
category: null,
|
|
317
|
+
disposition: "ask",
|
|
318
|
+
};
|
|
319
|
+
case "CLEAR_ONLY":
|
|
320
|
+
return { category: null, disposition: "clear_only" };
|
|
321
|
+
case "DUPLICATE":
|
|
322
|
+
return { category: null, disposition: "duplicate" };
|
|
323
|
+
default:
|
|
324
|
+
return { category: null, disposition: "failed" };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function decisionText(decision) {
|
|
328
|
+
return JSON.stringify(decision);
|
|
304
329
|
}
|
|
305
330
|
function actionPlan(input) {
|
|
306
|
-
if (input.result === "
|
|
331
|
+
if (input.result.disposition === "clear_only") {
|
|
307
332
|
return {
|
|
308
333
|
action: "CLEAR_ONLY",
|
|
309
334
|
allowedActions: ["CLEAR_ONLY"],
|
|
@@ -313,7 +338,7 @@ function actionPlan(input) {
|
|
|
313
338
|
postComment: false,
|
|
314
339
|
};
|
|
315
340
|
}
|
|
316
|
-
if (input.result === "
|
|
341
|
+
if (input.result.disposition === "ask") {
|
|
317
342
|
return {
|
|
318
343
|
action: "ASK",
|
|
319
344
|
allowedActions: ["ASK"],
|
|
@@ -324,11 +349,9 @@ function actionPlan(input) {
|
|
|
324
349
|
};
|
|
325
350
|
}
|
|
326
351
|
const closeIssue = input.triage.automation.close &&
|
|
327
|
-
(input.result === "
|
|
328
|
-
input.result === "
|
|
329
|
-
|
|
330
|
-
const createPr = input.triage.automation.pr &&
|
|
331
|
-
(input.result === "BUG_ACCEPTED" || input.result === "FEATURE_ACCEPTED");
|
|
352
|
+
(input.result.disposition === "rejected" ||
|
|
353
|
+
input.result.disposition === "duplicate");
|
|
354
|
+
const createPr = input.triage.automation.pr && input.result.disposition === "accepted";
|
|
332
355
|
return {
|
|
333
356
|
action: closeIssue ? "CLOSE" : createPr ? "PR" : "COMMENT",
|
|
334
357
|
allowedActions: [closeIssue ? "CLOSE" : createPr ? "PR" : "COMMENT"],
|
|
@@ -428,18 +451,34 @@ async function runReconsiderationVote(input) {
|
|
|
428
451
|
context: input.context,
|
|
429
452
|
input: input.input,
|
|
430
453
|
outputDir: input.outputDir,
|
|
431
|
-
parse:
|
|
454
|
+
parse: parseTriageBinaryOutput,
|
|
432
455
|
phase: "reconsider",
|
|
433
456
|
prompt: composeTriageReconsiderPrompt,
|
|
434
457
|
schemaName: "triage reconsider",
|
|
435
|
-
votes:
|
|
458
|
+
votes: BINARY_VOTES,
|
|
436
459
|
});
|
|
437
460
|
}
|
|
438
461
|
async function composeResultComment(input) {
|
|
439
462
|
const agents = input.input.repository.agents.triage;
|
|
440
463
|
if (!agents?.length)
|
|
441
464
|
throw new Error("triage.agents is required");
|
|
442
|
-
|
|
465
|
+
if (input.result.disposition === "ask" &&
|
|
466
|
+
input.result.askReason === "category_unclear") {
|
|
467
|
+
const language = input.input.repository.language?.toLowerCase() ?? "";
|
|
468
|
+
const body = language.includes("ja") || language.includes("japanese")
|
|
469
|
+
? `@${input.issue.author} 現在の説明だけでは、何をすべきか判断できません。\n\n期待する動作、実際の動作、必要な理由、関連する例・ログ・スクリーンショットなどを追記してください。`
|
|
470
|
+
: `@${input.issue.author} I can't determine what should be done from the current description.\n\nPlease add more detail, such as the expected behavior, the actual behavior, the reason this is needed, or any relevant examples, logs, or screenshots.`;
|
|
471
|
+
const comment = `${body}\n\n${marker({
|
|
472
|
+
action: input.action,
|
|
473
|
+
checkpoint: "pending",
|
|
474
|
+
decision: input.result,
|
|
475
|
+
issue: input.issue.number,
|
|
476
|
+
processed: input.processed,
|
|
477
|
+
})}`;
|
|
478
|
+
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
479
|
+
return comment;
|
|
480
|
+
}
|
|
481
|
+
const prompt = await (input.result.disposition === "ask"
|
|
443
482
|
? composeTriageQuestionPrompt
|
|
444
483
|
: composeTriageCommentPrompt)({
|
|
445
484
|
author: input.issue.author,
|
|
@@ -461,9 +500,9 @@ async function composeResultComment(input) {
|
|
|
461
500
|
`\n\n${marker({
|
|
462
501
|
action: input.action,
|
|
463
502
|
checkpoint: "pending",
|
|
503
|
+
decision: input.result,
|
|
464
504
|
issue: input.issue.number,
|
|
465
505
|
processed: input.processed,
|
|
466
|
-
result: input.result,
|
|
467
506
|
})}`;
|
|
468
507
|
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
469
508
|
return comment;
|
|
@@ -486,10 +525,10 @@ async function persistProcessedMarker(input) {
|
|
|
486
525
|
const updatedMarker = marker({
|
|
487
526
|
action: input.marker.action ?? input.marker.result ?? "ASK",
|
|
488
527
|
checkpoint: markerCheckpoint(input.marker),
|
|
528
|
+
decision: finalResultFromMarker(input.marker),
|
|
489
529
|
issue: input.issue.number,
|
|
490
530
|
pr: input.pr ?? markerPr(input.marker),
|
|
491
531
|
processed: input.processed,
|
|
492
|
-
result: input.marker.result ?? "ASK",
|
|
493
532
|
});
|
|
494
533
|
const body = previousComment.body.replace(/<!--\s*opencode-magi:triage\s+[^>]+?\s*-->/, updatedMarker);
|
|
495
534
|
if (body === previousComment.body)
|
|
@@ -516,7 +555,7 @@ async function finishWithResult(input) {
|
|
|
516
555
|
const comment = plan.postComment
|
|
517
556
|
? await composeResultComment({
|
|
518
557
|
action: plan.action,
|
|
519
|
-
context: `Result: ${input.result}\nAction: ${plan.action}\n\n${input.context}`,
|
|
558
|
+
context: `Result: ${decisionText(input.result)}\nAction: ${plan.action}\n\n${input.context}`,
|
|
520
559
|
input: input.input,
|
|
521
560
|
issue: input.issue,
|
|
522
561
|
outputDir: input.outputDir,
|
|
@@ -576,7 +615,7 @@ async function finishWithResult(input) {
|
|
|
576
615
|
}
|
|
577
616
|
}
|
|
578
617
|
const report = [
|
|
579
|
-
`Magi triage result for #${input.issue.number}: ${input.result}`,
|
|
618
|
+
`Magi triage result for #${input.issue.number}: ${decisionText(input.result)}`,
|
|
580
619
|
prUrl ? `Created PR: ${prUrl}` : undefined,
|
|
581
620
|
input.input.dryRun
|
|
582
621
|
? "Dry run: no GitHub mutations were performed."
|
|
@@ -690,7 +729,12 @@ export async function runTriage(input) {
|
|
|
690
729
|
if (block) {
|
|
691
730
|
const report = `Magi triage blocked for #${input.issue}: ${block}`;
|
|
692
731
|
await writeFile(join(outputDir, "report.md"), `${report}\n`);
|
|
693
|
-
return {
|
|
732
|
+
return {
|
|
733
|
+
issue: input.issue,
|
|
734
|
+
outputDir,
|
|
735
|
+
report,
|
|
736
|
+
result: { category: null, disposition: "failed" },
|
|
737
|
+
};
|
|
694
738
|
}
|
|
695
739
|
let context = issueContext({ issue, relationship });
|
|
696
740
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
@@ -765,8 +809,18 @@ export async function runTriage(input) {
|
|
|
765
809
|
},
|
|
766
810
|
});
|
|
767
811
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
812
|
+
const vote = await runReconsiderationVote({ context, input, outputDir });
|
|
813
|
+
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
768
814
|
result =
|
|
769
|
-
|
|
815
|
+
vote === "YES"
|
|
816
|
+
? { category: previous.category, disposition: "accepted" }
|
|
817
|
+
: vote === "NO"
|
|
818
|
+
? { category: previous.category, disposition: "rejected" }
|
|
819
|
+
: {
|
|
820
|
+
askReason: "acceptance_unclear",
|
|
821
|
+
category: previous.category,
|
|
822
|
+
disposition: "ask",
|
|
823
|
+
};
|
|
770
824
|
}
|
|
771
825
|
if (!result && relationship.relatedPullRequests.length) {
|
|
772
826
|
const vote = await runPhaseVote({
|
|
@@ -782,6 +836,10 @@ export async function runTriage(input) {
|
|
|
782
836
|
if (vote === "RELATED_PR_HANDLES_ISSUE") {
|
|
783
837
|
const merged = relationship.relatedPullRequests.some((pr) => pr.state === "MERGED");
|
|
784
838
|
if (merged && triage.automation.close) {
|
|
839
|
+
const relatedPrDecision = {
|
|
840
|
+
category: resolveIssueCategory(issue, input.repository) ?? null,
|
|
841
|
+
disposition: "accepted",
|
|
842
|
+
};
|
|
785
843
|
const plan = {
|
|
786
844
|
action: "CLOSE",
|
|
787
845
|
allowedActions: ["CLOSE"],
|
|
@@ -795,16 +853,16 @@ export async function runTriage(input) {
|
|
|
795
853
|
input,
|
|
796
854
|
outputDir,
|
|
797
855
|
plan,
|
|
798
|
-
result:
|
|
856
|
+
result: relatedPrDecision,
|
|
799
857
|
});
|
|
800
858
|
const body = await composeResultComment({
|
|
801
859
|
action: "CLOSE",
|
|
802
|
-
context: `Result:
|
|
860
|
+
context: `Result: ${decisionText(relatedPrDecision)}\nAction: CLOSE\n\n${context}`,
|
|
803
861
|
input,
|
|
804
862
|
issue,
|
|
805
863
|
outputDir,
|
|
806
864
|
processed,
|
|
807
|
-
result:
|
|
865
|
+
result: relatedPrDecision,
|
|
808
866
|
});
|
|
809
867
|
if (!input.dryRun) {
|
|
810
868
|
await postMarkedIssueComment({
|
|
@@ -834,7 +892,7 @@ export async function runTriage(input) {
|
|
|
834
892
|
issue: issue.number,
|
|
835
893
|
outputDir,
|
|
836
894
|
report,
|
|
837
|
-
result:
|
|
895
|
+
result: relatedPrDecision,
|
|
838
896
|
};
|
|
839
897
|
}
|
|
840
898
|
return finishWithResult({
|
|
@@ -844,7 +902,7 @@ export async function runTriage(input) {
|
|
|
844
902
|
outputDir,
|
|
845
903
|
processed,
|
|
846
904
|
relationship,
|
|
847
|
-
result: "
|
|
905
|
+
result: { category: null, disposition: "clear_only" },
|
|
848
906
|
});
|
|
849
907
|
}
|
|
850
908
|
}
|
|
@@ -857,59 +915,60 @@ export async function runTriage(input) {
|
|
|
857
915
|
});
|
|
858
916
|
if (duplicate) {
|
|
859
917
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
860
|
-
result = "
|
|
918
|
+
result = { category: null, disposition: "duplicate" };
|
|
861
919
|
}
|
|
862
920
|
}
|
|
863
921
|
if (!result) {
|
|
864
|
-
const
|
|
865
|
-
await writeJson(join(outputDir, "
|
|
866
|
-
|
|
867
|
-
source:
|
|
922
|
+
const resolvedCategory = resolveIssueCategory(issue, input.repository);
|
|
923
|
+
await writeJson(join(outputDir, "category-resolution.json"), {
|
|
924
|
+
category: resolvedCategory,
|
|
925
|
+
source: resolvedCategory ? "config" : "vote",
|
|
868
926
|
});
|
|
869
|
-
const
|
|
927
|
+
const category = resolvedCategory ??
|
|
870
928
|
(await runPhaseVote({
|
|
871
929
|
context,
|
|
872
930
|
input,
|
|
873
931
|
outputDir,
|
|
874
|
-
parse:
|
|
875
|
-
phase: "
|
|
876
|
-
prompt:
|
|
877
|
-
schemaName: "triage
|
|
878
|
-
votes:
|
|
932
|
+
parse: (text) => parseTriageCategoryOutput(text, triage.categories.map((item) => item.id)),
|
|
933
|
+
phase: "category",
|
|
934
|
+
prompt: composeTriageCategoryPrompt,
|
|
935
|
+
schemaName: "triage category",
|
|
936
|
+
votes: ["ASK", ...triage.categories.map((item) => item.id)],
|
|
879
937
|
})) ??
|
|
880
938
|
"ASK";
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
parse: parseTriageBinaryOutput,
|
|
888
|
-
phase: "bug",
|
|
889
|
-
prompt: composeTriageBugPrompt,
|
|
890
|
-
schemaName: "triage bug",
|
|
891
|
-
votes: BINARY_VOTES,
|
|
892
|
-
});
|
|
893
|
-
result =
|
|
894
|
-
vote === "YES" ? "BUG_ACCEPTED" : vote === "NO" ? "BUG_REJECTED" : "ASK";
|
|
939
|
+
if (category === "ASK") {
|
|
940
|
+
result = {
|
|
941
|
+
askReason: "category_unclear",
|
|
942
|
+
category: null,
|
|
943
|
+
disposition: "ask",
|
|
944
|
+
};
|
|
895
945
|
}
|
|
896
|
-
|
|
946
|
+
else {
|
|
947
|
+
const categoryConfig = triage.categories.find((item) => item.id === category);
|
|
948
|
+
const voteContext = JSON.stringify({
|
|
949
|
+
category: categoryConfig,
|
|
950
|
+
triageContext: context,
|
|
951
|
+
}, null, 2);
|
|
897
952
|
const vote = await runPhaseVote({
|
|
898
|
-
context,
|
|
953
|
+
context: voteContext,
|
|
899
954
|
input,
|
|
900
955
|
outputDir,
|
|
901
956
|
parse: parseTriageBinaryOutput,
|
|
902
|
-
phase: "
|
|
903
|
-
prompt:
|
|
904
|
-
schemaName: "triage
|
|
957
|
+
phase: "acceptance",
|
|
958
|
+
prompt: composeTriageAcceptancePrompt,
|
|
959
|
+
schemaName: "triage acceptance",
|
|
905
960
|
votes: BINARY_VOTES,
|
|
906
961
|
});
|
|
907
962
|
result =
|
|
908
963
|
vote === "YES"
|
|
909
|
-
? "
|
|
964
|
+
? { category, disposition: "accepted" }
|
|
910
965
|
: vote === "NO"
|
|
911
|
-
? "
|
|
912
|
-
:
|
|
966
|
+
? { category, disposition: "rejected" }
|
|
967
|
+
: {
|
|
968
|
+
askReason: "acceptance_unclear",
|
|
969
|
+
category,
|
|
970
|
+
disposition: "ask",
|
|
971
|
+
};
|
|
913
972
|
}
|
|
914
973
|
}
|
|
915
974
|
return finishWithResult({
|
|
@@ -919,6 +978,10 @@ export async function runTriage(input) {
|
|
|
919
978
|
outputDir,
|
|
920
979
|
processed,
|
|
921
980
|
relationship,
|
|
922
|
-
result: result ??
|
|
981
|
+
result: result ?? {
|
|
982
|
+
askReason: "acceptance_unclear",
|
|
983
|
+
category: null,
|
|
984
|
+
disposition: "ask",
|
|
985
|
+
},
|
|
923
986
|
});
|
|
924
987
|
}
|
package/dist/prompts/compose.js
CHANGED
|
@@ -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
|
}
|
|
@@ -66,9 +67,16 @@ function editValues(input) {
|
|
|
66
67
|
};
|
|
67
68
|
}
|
|
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");
|
|
69
76
|
return {
|
|
70
77
|
...repositoryValues(input.repository),
|
|
71
78
|
author: input.author ?? "",
|
|
79
|
+
categoryOptions,
|
|
72
80
|
context: input.context,
|
|
73
81
|
issue: String(input.issue),
|
|
74
82
|
worktreePath: input.worktreePath ?? "",
|
|
@@ -85,6 +93,9 @@ function previousReviewBlock(previousReview) {
|
|
|
85
93
|
? `<previous_review>\n${previousReview.trim()}\n</previous_review>`
|
|
86
94
|
: "";
|
|
87
95
|
}
|
|
96
|
+
function reviewContextBlock(reviewContext) {
|
|
97
|
+
return reviewContext?.trim() ? reviewContext.trim() : "";
|
|
98
|
+
}
|
|
88
99
|
async function reviewGuidelinesBlock(input) {
|
|
89
100
|
const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
|
|
90
101
|
return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
|
|
@@ -95,6 +106,9 @@ async function editGuidelinesBlock(input) {
|
|
|
95
106
|
}
|
|
96
107
|
async function sessionContextBlocks(input) {
|
|
97
108
|
return [
|
|
109
|
+
input.includeSessionContext
|
|
110
|
+
? reviewContextBlock(input.values.reviewContext)
|
|
111
|
+
: "",
|
|
98
112
|
input.includeSessionContext ? languageBlock(input.repository.language) : "",
|
|
99
113
|
input.includeSessionContext ? personaBlock(input.reviewer.persona) : "",
|
|
100
114
|
input.includeReviewGuidelines
|
|
@@ -116,6 +130,7 @@ export async function composeReviewPrompt(input) {
|
|
|
116
130
|
});
|
|
117
131
|
return [
|
|
118
132
|
task,
|
|
133
|
+
reviewContextBlock(input.reviewContext),
|
|
119
134
|
languageBlock(input.repository.language),
|
|
120
135
|
personaBlock(input.reviewer.persona),
|
|
121
136
|
await reviewGuidelinesBlock({
|
|
@@ -138,6 +153,7 @@ export async function composeRereviewPrompt(input) {
|
|
|
138
153
|
});
|
|
139
154
|
return [
|
|
140
155
|
task,
|
|
156
|
+
reviewContextBlock(input.reviewContext),
|
|
141
157
|
input.includeSessionContext === false
|
|
142
158
|
? ""
|
|
143
159
|
: languageBlock(input.repository.language),
|
|
@@ -392,27 +408,23 @@ export async function composeTriageDuplicatePrompt(input) {
|
|
|
392
408
|
outputContract: triageDuplicateOutputContract,
|
|
393
409
|
});
|
|
394
410
|
}
|
|
395
|
-
export async function
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
outputContract: triageVoteOutputContract('"BUG" | "FEATURE" | "ASK"'),
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
export async function composeTriageBugPrompt(input) {
|
|
411
|
+
export async function composeTriageCategoryPrompt(input) {
|
|
412
|
+
const categories = input.repository.triage?.categories ?? [];
|
|
413
|
+
const votes = ["ASK", ...categories.map((category) => category.id)]
|
|
414
|
+
.map((vote) => JSON.stringify(vote))
|
|
415
|
+
.join(" | ");
|
|
404
416
|
return composeTriageVotePrompt({
|
|
405
417
|
...input,
|
|
406
|
-
builtin: "
|
|
407
|
-
customPath: input.repository.triage?.prompts.
|
|
408
|
-
outputContract: triageVoteOutputContract(
|
|
418
|
+
builtin: "category",
|
|
419
|
+
customPath: input.repository.triage?.prompts.category,
|
|
420
|
+
outputContract: triageVoteOutputContract(votes),
|
|
409
421
|
});
|
|
410
422
|
}
|
|
411
|
-
export async function
|
|
423
|
+
export async function composeTriageAcceptancePrompt(input) {
|
|
412
424
|
return composeTriageVotePrompt({
|
|
413
425
|
...input,
|
|
414
|
-
builtin: "
|
|
415
|
-
customPath: input.repository.triage?.prompts.
|
|
426
|
+
builtin: "acceptance",
|
|
427
|
+
customPath: input.repository.triage?.prompts.acceptance,
|
|
416
428
|
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
417
429
|
});
|
|
418
430
|
}
|
|
@@ -429,6 +441,6 @@ export async function composeTriageReconsiderPrompt(input) {
|
|
|
429
441
|
...input,
|
|
430
442
|
builtin: "reconsider",
|
|
431
443
|
customPath: input.repository.triage?.prompts.reconsider,
|
|
432
|
-
outputContract: triageVoteOutputContract('"
|
|
444
|
+
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
433
445
|
});
|
|
434
446
|
}
|
|
@@ -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
|
|
23
|
-
- CHANGES_REQUESTED requires at least one finding.
|
|
24
|
-
- CLOSE requires a reason and
|
|
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
|
|
44
|
-
- CHANGES_REQUESTED requires at least one followUp or
|
|
45
|
-
- CLOSE requires a reason and empty followUps and
|
|
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
|
|
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
|
|
109
|
-
- CHANGES_REQUESTED requires at least one followUp or
|
|
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();
|
|
@@ -240,13 +253,12 @@ const outputContractsBySchemaName = {
|
|
|
240
253
|
"rereview close reconsideration": rereviewCloseReconsiderationOutputContract,
|
|
241
254
|
review: reviewOutputContract,
|
|
242
255
|
"triage action": triageActionOutputContract,
|
|
243
|
-
"triage
|
|
256
|
+
"triage acceptance": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
257
|
+
"triage category": triageVoteOutputContract('"ASK" or one of the configured category IDs'),
|
|
244
258
|
"triage comment classification": triageCommentClassificationOutputContract,
|
|
245
259
|
"triage duplicate": triageDuplicateOutputContract,
|
|
246
260
|
"triage existing PR": triageVoteOutputContract('"RELATED_PR_HANDLES_ISSUE" | "RELATED_PR_DOES_NOT_HANDLE_ISSUE"'),
|
|
247
|
-
"triage
|
|
248
|
-
"triage kind": triageVoteOutputContract('"BUG" | "FEATURE" | "ASK"'),
|
|
249
|
-
"triage reconsider": triageVoteOutputContract('"ASK" | "BUG_ACCEPTED" | "BUG_REJECTED" | "DUPLICATE" | "FEATURE_ACCEPTED" | "FEATURE_REJECTED"'),
|
|
261
|
+
"triage reconsider": triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
250
262
|
};
|
|
251
263
|
export function repairPrompt(schemaName) {
|
|
252
264
|
const outputContract = outputContractsBySchemaName[schemaName];
|