opencode-magi 0.7.0 → 0.9.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.
- package/README.md +26 -9
- package/dist/config/resolve.js +36 -1
- package/dist/config/validate.js +137 -15
- package/dist/github/commands.js +48 -6
- package/dist/orchestrator/merge.js +367 -23
- package/dist/orchestrator/report.js +1 -1
- package/dist/orchestrator/review.js +485 -46
- package/dist/orchestrator/triage.js +249 -64
- package/dist/prompts/compose.js +59 -2
- package/dist/prompts/contracts.js +20 -1
- package/dist/prompts/output.js +19 -1
- package/dist/prompts/templates/merge/conflict.md +10 -0
- package/dist/prompts/templates/review/rereview.md +2 -0
- package/dist/prompts/templates/review/review.md +2 -0
- package/dist/prompts/templates/triage/acceptance.md +1 -1
- package/dist/prompts/templates/triage/signal.md +10 -0
- package/package.json +8 -8
- package/schema.json +60 -5
|
@@ -2,13 +2,14 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { issueRunOutputDir } from "../config/output";
|
|
4
4
|
import { issueRunWorktreeDir } from "../config/worktree";
|
|
5
|
-
import { assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
|
|
6
|
-
import { composeTriageAcceptancePrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
|
-
import { parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
|
|
5
|
+
import { addIssueLabels, assignIssue, closeIssue, closePullRequest, configureGitIdentity, createPullRequest, fetchIssue, fetchIssueComments, fetchRelatedPullRequests, postIssueComment, pushHead, removeIssueLabels, removeWorktree, searchDuplicateIssues, shellQuote, updateIssueComment, } from "../github/commands";
|
|
6
|
+
import { composeTriageAcceptancePrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, composeTriageSignalPrompt, } from "../prompts/compose";
|
|
7
|
+
import { parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, parseTriageSignalOutput, } from "../prompts/output";
|
|
8
8
|
import { aggregateStringMajority, majorityThreshold } from "./majority";
|
|
9
9
|
import { runModelWithRepair, } from "./model";
|
|
10
10
|
const MARKER_PREFIX = "opencode-magi:triage";
|
|
11
11
|
const BINARY_VOTES = ["ASK", "NO", "YES"];
|
|
12
|
+
const ACCEPTANCE_VOTES = ["ASK", "INVALID", "NO", "YES"];
|
|
12
13
|
const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
|
|
13
14
|
const EXISTING_PR_VOTES = [
|
|
14
15
|
"RELATED_PR_DOES_NOT_HANDLE_ISSUE",
|
|
@@ -20,10 +21,29 @@ const RECONSIDERATION_CLASSES = new Set([
|
|
|
20
21
|
"OBJECTION",
|
|
21
22
|
]);
|
|
22
23
|
function marker(input) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return `<!-- ${MARKER_PREFIX} v=2 issue=${input.issue} category=${input.decision.category ?? "none"} disposition=${input.decision.disposition} signals=${input.decision.signals.join(",")} action=${input.action} checkpoint=${input.checkpoint ?? "pending"} pr=${input.pr ?? "none"} processed=${(input.processed ?? []).join(",")} -->`;
|
|
25
|
+
}
|
|
26
|
+
function markerDisposition(input) {
|
|
27
|
+
switch (input.disposition) {
|
|
28
|
+
case "accepted":
|
|
29
|
+
case "rejected":
|
|
30
|
+
case "invalid":
|
|
31
|
+
case "duplicate":
|
|
32
|
+
case "already_handled":
|
|
33
|
+
case "needs_category":
|
|
34
|
+
case "needs_acceptance":
|
|
35
|
+
case "blocked":
|
|
36
|
+
case "failed":
|
|
37
|
+
return input.disposition;
|
|
38
|
+
case "ask":
|
|
39
|
+
return input.askReason === "category_unclear"
|
|
40
|
+
? "needs_category"
|
|
41
|
+
: "needs_acceptance";
|
|
42
|
+
case "clear_only":
|
|
43
|
+
return "already_handled";
|
|
44
|
+
default:
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
27
47
|
}
|
|
28
48
|
export function parseTriageMarker(body) {
|
|
29
49
|
const match = body.match(/<!--\s*opencode-magi:triage\s+([^>]+?)\s*-->/);
|
|
@@ -41,30 +61,28 @@ export function parseTriageMarker(body) {
|
|
|
41
61
|
const version = Number(entries.v);
|
|
42
62
|
if (version !== 1 && version !== 2)
|
|
43
63
|
return undefined;
|
|
64
|
+
const askReason = entries.askReason === "acceptance_unclear" ||
|
|
65
|
+
entries.askReason === "category_unclear"
|
|
66
|
+
? entries.askReason
|
|
67
|
+
: undefined;
|
|
44
68
|
return {
|
|
45
69
|
action: entries.action,
|
|
46
|
-
askReason
|
|
47
|
-
entries.askReason === "category_unclear"
|
|
48
|
-
? entries.askReason
|
|
49
|
-
: undefined,
|
|
70
|
+
askReason,
|
|
50
71
|
category: entries.category === "none" ? null : entries.category || undefined,
|
|
51
72
|
checkpoint: entries.checkpoint && Number.isFinite(Number(entries.checkpoint))
|
|
52
73
|
? Number(entries.checkpoint)
|
|
53
74
|
: undefined,
|
|
54
|
-
disposition:
|
|
55
|
-
|
|
56
|
-
entries.disposition
|
|
57
|
-
|
|
58
|
-
entries.disposition === "clear_only" ||
|
|
59
|
-
entries.disposition === "failed"
|
|
60
|
-
? entries.disposition
|
|
61
|
-
: undefined,
|
|
75
|
+
disposition: markerDisposition({
|
|
76
|
+
askReason,
|
|
77
|
+
disposition: entries.disposition,
|
|
78
|
+
}),
|
|
62
79
|
issue: entries.issue ? Number(entries.issue) : undefined,
|
|
63
80
|
pr: entries.pr,
|
|
64
81
|
processed: entries.processed
|
|
65
82
|
? entries.processed.split(",").filter(Boolean).map(Number)
|
|
66
83
|
: [],
|
|
67
84
|
result: entries.result,
|
|
85
|
+
signals: entries.signals ? entries.signals.split(",").filter(Boolean) : [],
|
|
68
86
|
v: version,
|
|
69
87
|
};
|
|
70
88
|
}
|
|
@@ -72,9 +90,46 @@ function labelsContain(labels, targets) {
|
|
|
72
90
|
const set = new Set(labels.map((label) => label.toLowerCase()));
|
|
73
91
|
return targets.some((target) => set.has(target.toLowerCase()));
|
|
74
92
|
}
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
93
|
+
function addLabel(labels, label) {
|
|
94
|
+
const key = label.toLowerCase();
|
|
95
|
+
if (!labels.has(key))
|
|
96
|
+
labels.set(key, label);
|
|
97
|
+
}
|
|
98
|
+
function labelRuleMatches(rule, result) {
|
|
99
|
+
const when = rule.when;
|
|
100
|
+
if (when.disposition && when.disposition !== result.disposition)
|
|
101
|
+
return false;
|
|
102
|
+
if (when.category && when.category !== result.category)
|
|
103
|
+
return false;
|
|
104
|
+
if (when.signals?.length) {
|
|
105
|
+
const signals = new Set(result.signals);
|
|
106
|
+
if (when.signals.some((signal) => !signals.has(signal)))
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
export function triageLabelChanges(input) {
|
|
112
|
+
const add = new Map();
|
|
113
|
+
const remove = new Map();
|
|
114
|
+
for (const rule of input.rules) {
|
|
115
|
+
if (!labelRuleMatches(rule, input.result))
|
|
116
|
+
continue;
|
|
117
|
+
for (const label of rule.add ?? [])
|
|
118
|
+
addLabel(add, label);
|
|
119
|
+
for (const label of rule.remove ?? [])
|
|
120
|
+
addLabel(remove, label);
|
|
121
|
+
}
|
|
122
|
+
for (const key of add.keys())
|
|
123
|
+
remove.delete(key);
|
|
124
|
+
const existing = new Set(input.issueLabels.map((label) => label.toLowerCase()));
|
|
125
|
+
return {
|
|
126
|
+
add: [...add]
|
|
127
|
+
.filter(([key]) => !existing.has(key))
|
|
128
|
+
.map(([, label]) => label),
|
|
129
|
+
remove: [...remove]
|
|
130
|
+
.filter(([key]) => existing.has(key))
|
|
131
|
+
.map(([, label]) => label),
|
|
132
|
+
};
|
|
78
133
|
}
|
|
79
134
|
export function resolveIssueCategory(issue, repository) {
|
|
80
135
|
const triage = repository.triage;
|
|
@@ -305,6 +360,86 @@ async function runPhaseVote(input) {
|
|
|
305
360
|
vote: majority.vote,
|
|
306
361
|
};
|
|
307
362
|
}
|
|
363
|
+
async function runSignalVote(input) {
|
|
364
|
+
const agents = input.input.repository.agents.triage;
|
|
365
|
+
const signals = input.input.repository.triage?.signals ?? [];
|
|
366
|
+
if (!agents?.length)
|
|
367
|
+
throw new Error("triage.voters is required");
|
|
368
|
+
if (!signals.length)
|
|
369
|
+
return [];
|
|
370
|
+
await emitProgress(input.input, { phase: "signal", type: "phase" });
|
|
371
|
+
const signalIds = signals.map((signal) => signal.id);
|
|
372
|
+
const promptTexts = await Promise.all(agents.map((agent) => composeTriageSignalPrompt({
|
|
373
|
+
context: input.context,
|
|
374
|
+
directory: input.input.directory,
|
|
375
|
+
issue: input.input.issue,
|
|
376
|
+
repository: input.input.repository,
|
|
377
|
+
voter: agent,
|
|
378
|
+
})));
|
|
379
|
+
const outputs = await Promise.all(agents.map(async (agent, index) => {
|
|
380
|
+
const prompt = promptTexts[index];
|
|
381
|
+
await emitProgress(input.input, {
|
|
382
|
+
phase: "signal",
|
|
383
|
+
type: "triage_agent_started",
|
|
384
|
+
voter: agent.key,
|
|
385
|
+
});
|
|
386
|
+
const result = await runModelWithRepair({
|
|
387
|
+
client: input.input.client,
|
|
388
|
+
model: agent.model,
|
|
389
|
+
onProgress: (progress) => emitTriageModelProgress({
|
|
390
|
+
phase: "signal",
|
|
391
|
+
progress,
|
|
392
|
+
run: input.input,
|
|
393
|
+
voter: agent.key,
|
|
394
|
+
}),
|
|
395
|
+
options: agent.options,
|
|
396
|
+
parentSessionId: input.input.parentSessionId,
|
|
397
|
+
parse: (text) => parseTriageSignalOutput(text, signalIds),
|
|
398
|
+
permission: agent.permission,
|
|
399
|
+
prompt,
|
|
400
|
+
repairAttempts: input.input.config.output?.repairAttempts ?? 3,
|
|
401
|
+
schemaName: "triage signal",
|
|
402
|
+
signal: input.input.signal,
|
|
403
|
+
title: `Magi triage signal #${input.input.issue} (${agent.key})`,
|
|
404
|
+
});
|
|
405
|
+
await emitProgress(input.input, {
|
|
406
|
+
phase: "signal",
|
|
407
|
+
sessionId: result.sessionId,
|
|
408
|
+
type: "triage_agent_completed",
|
|
409
|
+
voter: agent.key,
|
|
410
|
+
vote: result.value.signals.map((signal) => signal.id).join(",") || "none",
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
...result.value,
|
|
414
|
+
promptText: prompt,
|
|
415
|
+
raw: result.raw,
|
|
416
|
+
sessionId: result.sessionId,
|
|
417
|
+
voter: agent.key,
|
|
418
|
+
};
|
|
419
|
+
}));
|
|
420
|
+
const threshold = majorityThreshold(outputs.length);
|
|
421
|
+
const counts = new Map();
|
|
422
|
+
for (const output of outputs) {
|
|
423
|
+
for (const id of new Set(output.signals.map((signal) => signal.id))) {
|
|
424
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const selected = signalIds.filter((id) => (counts.get(id) ?? 0) >= threshold);
|
|
428
|
+
await Promise.all(outputs.map((output) => {
|
|
429
|
+
const base = join(input.outputDir, `${output.voter}.signal`);
|
|
430
|
+
return Promise.all([
|
|
431
|
+
writeFile(`${base}.prompt.txt`, `${output.promptText}\n`),
|
|
432
|
+
writeFile(`${base}.raw.txt`, `${output.raw}\n`),
|
|
433
|
+
writeJson(`${base}.json`, { signals: output.signals }),
|
|
434
|
+
]);
|
|
435
|
+
}));
|
|
436
|
+
await writeJson(join(input.outputDir, "signal-majority.json"), {
|
|
437
|
+
counts: Object.fromEntries(counts),
|
|
438
|
+
selected,
|
|
439
|
+
threshold,
|
|
440
|
+
});
|
|
441
|
+
return selected;
|
|
442
|
+
}
|
|
308
443
|
async function relationshipScan(input, issue) {
|
|
309
444
|
const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
|
|
310
445
|
fetchIssueComments(input.exec, input.repository, input.issue),
|
|
@@ -384,40 +519,40 @@ export function eligibleMentionReplies(input) {
|
|
|
384
519
|
function finalResultFromMarker(marker) {
|
|
385
520
|
if (marker.disposition) {
|
|
386
521
|
return {
|
|
387
|
-
askReason: marker.askReason,
|
|
388
522
|
category: marker.category ?? null,
|
|
389
523
|
disposition: marker.disposition,
|
|
524
|
+
signals: marker.signals,
|
|
390
525
|
};
|
|
391
526
|
}
|
|
392
527
|
switch (marker.result) {
|
|
393
528
|
case "BUG_ACCEPTED":
|
|
394
529
|
case "RESOLVED_BY_MERGED_PR":
|
|
395
|
-
return { category: "bug", disposition: "accepted" };
|
|
530
|
+
return { category: "bug", disposition: "accepted", signals: [] };
|
|
396
531
|
case "BUG_REJECTED":
|
|
397
|
-
return { category: "bug", disposition: "rejected" };
|
|
532
|
+
return { category: "bug", disposition: "rejected", signals: [] };
|
|
398
533
|
case "FEATURE_ACCEPTED":
|
|
399
|
-
return { category: "feature", disposition: "accepted" };
|
|
534
|
+
return { category: "feature", disposition: "accepted", signals: [] };
|
|
400
535
|
case "FEATURE_REJECTED":
|
|
401
|
-
return { category: "feature", disposition: "rejected" };
|
|
536
|
+
return { category: "feature", disposition: "rejected", signals: [] };
|
|
402
537
|
case "ASK":
|
|
403
538
|
return {
|
|
404
|
-
askReason: "acceptance_unclear",
|
|
405
539
|
category: null,
|
|
406
|
-
disposition: "
|
|
540
|
+
disposition: "needs_acceptance",
|
|
541
|
+
signals: [],
|
|
407
542
|
};
|
|
408
543
|
case "CLEAR_ONLY":
|
|
409
|
-
return { category: null, disposition: "
|
|
544
|
+
return { category: null, disposition: "already_handled", signals: [] };
|
|
410
545
|
case "DUPLICATE":
|
|
411
|
-
return { category: null, disposition: "duplicate" };
|
|
546
|
+
return { category: null, disposition: "duplicate", signals: [] };
|
|
412
547
|
default:
|
|
413
|
-
return { category: null, disposition: "failed" };
|
|
548
|
+
return { category: null, disposition: "failed", signals: [] };
|
|
414
549
|
}
|
|
415
550
|
}
|
|
416
551
|
function decisionText(decision) {
|
|
417
552
|
return JSON.stringify(decision);
|
|
418
553
|
}
|
|
419
554
|
function actionPlan(input) {
|
|
420
|
-
if (input.result.disposition === "
|
|
555
|
+
if (input.result.disposition === "already_handled") {
|
|
421
556
|
return {
|
|
422
557
|
action: "CLEAR_ONLY",
|
|
423
558
|
allowedActions: ["CLEAR_ONLY"],
|
|
@@ -427,11 +562,12 @@ function actionPlan(input) {
|
|
|
427
562
|
postComment: false,
|
|
428
563
|
};
|
|
429
564
|
}
|
|
430
|
-
if (input.result.disposition === "
|
|
565
|
+
if (input.result.disposition === "needs_category" ||
|
|
566
|
+
input.result.disposition === "needs_acceptance") {
|
|
431
567
|
return {
|
|
432
568
|
action: "ASK",
|
|
433
569
|
allowedActions: ["ASK"],
|
|
434
|
-
clearLabels:
|
|
570
|
+
clearLabels: true,
|
|
435
571
|
closeIssue: false,
|
|
436
572
|
createPr: false,
|
|
437
573
|
postComment: true,
|
|
@@ -439,6 +575,7 @@ function actionPlan(input) {
|
|
|
439
575
|
}
|
|
440
576
|
const closeIssue = input.triage.automation.close &&
|
|
441
577
|
(input.result.disposition === "rejected" ||
|
|
578
|
+
input.result.disposition === "invalid" ||
|
|
442
579
|
input.result.disposition === "duplicate");
|
|
443
580
|
const createPr = input.triage.automation.create && input.result.disposition === "accepted";
|
|
444
581
|
return {
|
|
@@ -452,8 +589,13 @@ function actionPlan(input) {
|
|
|
452
589
|
}
|
|
453
590
|
function previousAutomationPlan(input) {
|
|
454
591
|
const base = actionPlan({ result: input.result, triage: input.triage });
|
|
592
|
+
const labelChanges = triageLabelChanges({
|
|
593
|
+
issueLabels: input.issue.labels,
|
|
594
|
+
result: input.result,
|
|
595
|
+
rules: input.triage.automation.label,
|
|
596
|
+
});
|
|
455
597
|
const clearLabels = base.clearLabels &&
|
|
456
|
-
|
|
598
|
+
(labelChanges.add.length > 0 || labelChanges.remove.length > 0);
|
|
457
599
|
const closeIssue = input.marker.action === "CLOSE" &&
|
|
458
600
|
base.closeIssue &&
|
|
459
601
|
input.issue.state === "OPEN";
|
|
@@ -558,6 +700,9 @@ function decisionCommentFallback(input) {
|
|
|
558
700
|
if (input.result.disposition === "duplicate") {
|
|
559
701
|
return "Magi marked this issue as a duplicate.";
|
|
560
702
|
}
|
|
703
|
+
if (input.result.disposition === "invalid") {
|
|
704
|
+
return "Magi marked this issue as invalid or not actionable.";
|
|
705
|
+
}
|
|
561
706
|
return "Magi completed triage for this issue.";
|
|
562
707
|
}
|
|
563
708
|
function agentForKey(repository, key) {
|
|
@@ -675,7 +820,9 @@ async function finishWithResult(input) {
|
|
|
675
820
|
});
|
|
676
821
|
let prUrl;
|
|
677
822
|
const reporter = triageReporter(input.input.repository, input.issue.number);
|
|
678
|
-
const comment = plan.postComment &&
|
|
823
|
+
const comment = plan.postComment &&
|
|
824
|
+
input.result.disposition !== "needs_category" &&
|
|
825
|
+
input.result.disposition !== "needs_acceptance"
|
|
679
826
|
? `${decisionCommentBody({
|
|
680
827
|
action: plan.action,
|
|
681
828
|
reason: input.commentReason,
|
|
@@ -691,7 +838,19 @@ async function finishWithResult(input) {
|
|
|
691
838
|
if (comment) {
|
|
692
839
|
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
693
840
|
}
|
|
694
|
-
|
|
841
|
+
const labelChanges = plan.clearLabels
|
|
842
|
+
? triageLabelChanges({
|
|
843
|
+
issueLabels: input.issue.labels,
|
|
844
|
+
result: input.result,
|
|
845
|
+
rules: triage.automation.label,
|
|
846
|
+
})
|
|
847
|
+
: { add: [], remove: [] };
|
|
848
|
+
if (plan.clearLabels) {
|
|
849
|
+
await writeJson(join(input.outputDir, "label-changes.json"), labelChanges);
|
|
850
|
+
}
|
|
851
|
+
if ((input.result.disposition === "needs_category" ||
|
|
852
|
+
input.result.disposition === "needs_acceptance") &&
|
|
853
|
+
input.askOutputs) {
|
|
695
854
|
await postAskComments({
|
|
696
855
|
action: plan.action,
|
|
697
856
|
dryRun: input.input.dryRun,
|
|
@@ -723,9 +882,11 @@ async function finishWithResult(input) {
|
|
|
723
882
|
});
|
|
724
883
|
}
|
|
725
884
|
if (plan.clearLabels) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
885
|
+
if (labelChanges.remove.length) {
|
|
886
|
+
await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, labelChanges.remove, reporter.account);
|
|
887
|
+
}
|
|
888
|
+
if (labelChanges.add.length) {
|
|
889
|
+
await addIssueLabels(input.input.exec, input.input.repository, input.issue.number, labelChanges.add, reporter.account);
|
|
729
890
|
}
|
|
730
891
|
}
|
|
731
892
|
if (plan.closeIssue) {
|
|
@@ -934,7 +1095,7 @@ export async function runTriage(input) {
|
|
|
934
1095
|
issue: input.issue,
|
|
935
1096
|
outputDir,
|
|
936
1097
|
report,
|
|
937
|
-
result: { category: null, disposition: "
|
|
1098
|
+
result: { category: null, disposition: "blocked", signals: [] },
|
|
938
1099
|
};
|
|
939
1100
|
}
|
|
940
1101
|
let context = issueContext({ issue, relationship });
|
|
@@ -1016,8 +1177,7 @@ export async function runTriage(input) {
|
|
|
1016
1177
|
});
|
|
1017
1178
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
1018
1179
|
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
1019
|
-
if (previous.disposition !== "
|
|
1020
|
-
previous.askReason !== "acceptance_unclear") {
|
|
1180
|
+
if (previous.disposition !== "needs_acceptance") {
|
|
1021
1181
|
const reconsideration = await runReconsiderationVote({
|
|
1022
1182
|
context,
|
|
1023
1183
|
input,
|
|
@@ -1026,15 +1186,23 @@ export async function runTriage(input) {
|
|
|
1026
1186
|
commentReason = reconsideration.reason;
|
|
1027
1187
|
result =
|
|
1028
1188
|
reconsideration.vote === "YES"
|
|
1029
|
-
? {
|
|
1189
|
+
? {
|
|
1190
|
+
category: previous.category,
|
|
1191
|
+
disposition: "accepted",
|
|
1192
|
+
signals: [],
|
|
1193
|
+
}
|
|
1030
1194
|
: reconsideration.vote === "NO"
|
|
1031
|
-
? {
|
|
1195
|
+
? {
|
|
1196
|
+
category: previous.category,
|
|
1197
|
+
disposition: "rejected",
|
|
1198
|
+
signals: [],
|
|
1199
|
+
}
|
|
1032
1200
|
: {
|
|
1033
|
-
askReason: "acceptance_unclear",
|
|
1034
1201
|
category: previous.category,
|
|
1035
|
-
disposition: "
|
|
1202
|
+
disposition: "needs_acceptance",
|
|
1203
|
+
signals: [],
|
|
1036
1204
|
};
|
|
1037
|
-
if (result.disposition === "
|
|
1205
|
+
if (result.disposition === "needs_acceptance") {
|
|
1038
1206
|
askCommentOutputs = askOutputs(reconsideration.outputs);
|
|
1039
1207
|
markAskComments = true;
|
|
1040
1208
|
}
|
|
@@ -1057,6 +1225,7 @@ export async function runTriage(input) {
|
|
|
1057
1225
|
const relatedPrDecision = {
|
|
1058
1226
|
category: resolveIssueCategory(issue, input.repository) ?? null,
|
|
1059
1227
|
disposition: "accepted",
|
|
1228
|
+
signals: [],
|
|
1060
1229
|
};
|
|
1061
1230
|
const plan = {
|
|
1062
1231
|
action: "CLOSE",
|
|
@@ -1086,7 +1255,7 @@ export async function runTriage(input) {
|
|
|
1086
1255
|
outputDir,
|
|
1087
1256
|
processed,
|
|
1088
1257
|
relationship,
|
|
1089
|
-
result: { category: null, disposition: "
|
|
1258
|
+
result: { category: null, disposition: "already_handled", signals: [] },
|
|
1090
1259
|
runId,
|
|
1091
1260
|
});
|
|
1092
1261
|
}
|
|
@@ -1101,7 +1270,7 @@ export async function runTriage(input) {
|
|
|
1101
1270
|
if (duplicate) {
|
|
1102
1271
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
1103
1272
|
commentReason = duplicate.reason;
|
|
1104
|
-
result = { category: null, disposition: "duplicate" };
|
|
1273
|
+
result = { category: null, disposition: "duplicate", signals: [] };
|
|
1105
1274
|
}
|
|
1106
1275
|
}
|
|
1107
1276
|
if (!result) {
|
|
@@ -1125,9 +1294,9 @@ export async function runTriage(input) {
|
|
|
1125
1294
|
const category = resolvedCategory ?? categoryVote?.vote ?? "ASK";
|
|
1126
1295
|
if (category === "ASK") {
|
|
1127
1296
|
result = {
|
|
1128
|
-
askReason: "category_unclear",
|
|
1129
1297
|
category: null,
|
|
1130
|
-
disposition: "
|
|
1298
|
+
disposition: "needs_category",
|
|
1299
|
+
signals: [],
|
|
1131
1300
|
};
|
|
1132
1301
|
askCommentOutputs = askOutputs(categoryVote?.outputs);
|
|
1133
1302
|
markAskComments = false;
|
|
@@ -1146,25 +1315,41 @@ export async function runTriage(input) {
|
|
|
1146
1315
|
phase: "acceptance",
|
|
1147
1316
|
prompt: composeTriageAcceptancePrompt,
|
|
1148
1317
|
schemaName: "triage acceptance",
|
|
1149
|
-
votes:
|
|
1318
|
+
votes: ACCEPTANCE_VOTES,
|
|
1150
1319
|
});
|
|
1151
1320
|
commentReason = acceptance.reason;
|
|
1152
1321
|
result =
|
|
1153
1322
|
acceptance.vote === "YES"
|
|
1154
|
-
? { category, disposition: "accepted" }
|
|
1323
|
+
? { category, disposition: "accepted", signals: [] }
|
|
1155
1324
|
: acceptance.vote === "NO"
|
|
1156
|
-
? { category, disposition: "rejected" }
|
|
1157
|
-
:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1325
|
+
? { category, disposition: "rejected", signals: [] }
|
|
1326
|
+
: acceptance.vote === "INVALID"
|
|
1327
|
+
? { category, disposition: "invalid", signals: [] }
|
|
1328
|
+
: {
|
|
1329
|
+
category,
|
|
1330
|
+
disposition: "needs_acceptance",
|
|
1331
|
+
signals: [],
|
|
1332
|
+
};
|
|
1333
|
+
if (result.disposition === "needs_acceptance") {
|
|
1163
1334
|
askCommentOutputs = askOutputs(acceptance.outputs);
|
|
1164
1335
|
markAskComments = true;
|
|
1165
1336
|
}
|
|
1166
1337
|
}
|
|
1167
1338
|
}
|
|
1339
|
+
if (result &&
|
|
1340
|
+
result.disposition !== "blocked" &&
|
|
1341
|
+
result.disposition !== "failed" &&
|
|
1342
|
+
triage.signals.length) {
|
|
1343
|
+
const signalContext = `${context}\n\nFinal triage result: ${JSON.stringify(result)}`;
|
|
1344
|
+
result = {
|
|
1345
|
+
...result,
|
|
1346
|
+
signals: await runSignalVote({
|
|
1347
|
+
context: signalContext,
|
|
1348
|
+
input,
|
|
1349
|
+
outputDir,
|
|
1350
|
+
}),
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1168
1353
|
return finishWithResult({
|
|
1169
1354
|
askOutputs: askCommentOutputs,
|
|
1170
1355
|
commentReason,
|
|
@@ -1176,9 +1361,9 @@ export async function runTriage(input) {
|
|
|
1176
1361
|
processed,
|
|
1177
1362
|
relationship,
|
|
1178
1363
|
result: result ?? {
|
|
1179
|
-
askReason: "acceptance_unclear",
|
|
1180
1364
|
category: null,
|
|
1181
|
-
disposition: "
|
|
1365
|
+
disposition: "needs_acceptance",
|
|
1366
|
+
signals: [],
|
|
1182
1367
|
},
|
|
1183
1368
|
runId,
|
|
1184
1369
|
});
|
package/dist/prompts/compose.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
-
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageVoteOutputContract, } from "./contracts";
|
|
4
|
+
import { ciClassificationAfterEditOutputContract, ciClassificationOutputContract, closeReconsiderationOutputContract, editOutputContract, findingValidationOutputContract, rereviewCloseReconsiderationOutputContract, rereviewOutputContract, reviewOutputContract, triageCommentClassificationOutputContract, triageCreatePrOutputContract, triageDuplicateOutputContract, triageSignalOutputContract, triageVoteOutputContract, } from "./contracts";
|
|
5
5
|
async function readOptionalPrompt(directory, path, values = {}) {
|
|
6
6
|
if (!path)
|
|
7
7
|
return "";
|
|
@@ -35,6 +35,7 @@ function repositoryValues(repository) {
|
|
|
35
35
|
}
|
|
36
36
|
function reviewValues(input) {
|
|
37
37
|
const ciFailureContext = input.ciFailureContext?.trim() ?? "";
|
|
38
|
+
const mergeConflictContext = input.mergeConflictContext?.trim() ?? "";
|
|
38
39
|
return {
|
|
39
40
|
...repositoryValues(input.repository),
|
|
40
41
|
baseSha: input.baseSha,
|
|
@@ -44,6 +45,10 @@ function reviewValues(input) {
|
|
|
44
45
|
: "",
|
|
45
46
|
headSha: input.headSha,
|
|
46
47
|
jsonEncodedWorktreePath: JSON.stringify(input.worktreePath),
|
|
48
|
+
mergeConflictContext,
|
|
49
|
+
mergeConflictContextBlock: mergeConflictContext
|
|
50
|
+
? `<merge_conflict_context>\n${mergeConflictContext}\n</merge_conflict_context>`
|
|
51
|
+
: "",
|
|
47
52
|
pr: String(input.pr),
|
|
48
53
|
reviewContext: input.reviewContext ?? "",
|
|
49
54
|
worktreePath: input.worktreePath,
|
|
@@ -67,6 +72,17 @@ function editValues(input) {
|
|
|
67
72
|
worktreePath: input.worktreePath,
|
|
68
73
|
};
|
|
69
74
|
}
|
|
75
|
+
function mergeConflictValues(input) {
|
|
76
|
+
return {
|
|
77
|
+
...repositoryValues(input.repository),
|
|
78
|
+
baseBranch: input.baseBranch,
|
|
79
|
+
baseSha: input.baseSha,
|
|
80
|
+
conflictedFiles: input.conflictedFiles,
|
|
81
|
+
headSha: input.headSha,
|
|
82
|
+
pr: String(input.pr),
|
|
83
|
+
worktreePath: input.worktreePath,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
70
86
|
function triageValues(input) {
|
|
71
87
|
const categories = input.repository.triage?.categories ?? [];
|
|
72
88
|
const categoryOptions = categories
|
|
@@ -74,12 +90,16 @@ function triageValues(input) {
|
|
|
74
90
|
? `- ${category.id}: ${category.description}`
|
|
75
91
|
: `- ${category.id}`)
|
|
76
92
|
.join("\n");
|
|
93
|
+
const signalOptions = (input.repository.triage?.signals ?? [])
|
|
94
|
+
.map((signal) => `- ${signal.id}: ${signal.description}`)
|
|
95
|
+
.join("\n");
|
|
77
96
|
return {
|
|
78
97
|
...repositoryValues(input.repository),
|
|
79
98
|
author: input.author ?? "",
|
|
80
99
|
categoryOptions,
|
|
81
100
|
context: input.context,
|
|
82
101
|
issue: String(input.issue),
|
|
102
|
+
signalOptions,
|
|
83
103
|
worktreePath: input.worktreePath ?? "",
|
|
84
104
|
};
|
|
85
105
|
}
|
|
@@ -97,6 +117,12 @@ function previousReviewBlock(previousReview) {
|
|
|
97
117
|
function reviewContextBlock(reviewContext) {
|
|
98
118
|
return reviewContext?.trim() ? reviewContext.trim() : "";
|
|
99
119
|
}
|
|
120
|
+
function mergeConflictContextBlock(mergeConflictContext) {
|
|
121
|
+
const body = mergeConflictContext?.trim();
|
|
122
|
+
return body
|
|
123
|
+
? `<merge_conflict_context>\n${body}\n</merge_conflict_context>`
|
|
124
|
+
: "";
|
|
125
|
+
}
|
|
100
126
|
async function reviewGuidelinesBlock(input) {
|
|
101
127
|
const body = (await readOptionalPrompt(input.directory, input.path, input.values)).trim();
|
|
102
128
|
return body ? `<review_guidelines>\n${body}\n</review_guidelines>` : "";
|
|
@@ -136,6 +162,7 @@ export async function composeReviewPrompt(input) {
|
|
|
136
162
|
return [
|
|
137
163
|
task,
|
|
138
164
|
reviewContextBlock(input.reviewContext),
|
|
165
|
+
mergeConflictContextBlock(input.mergeConflictContext),
|
|
139
166
|
languageBlock(input.repository.language),
|
|
140
167
|
personaBlock(input.reviewer.persona),
|
|
141
168
|
await reviewGuidelinesBlock({
|
|
@@ -159,6 +186,7 @@ export async function composeRereviewPrompt(input) {
|
|
|
159
186
|
return [
|
|
160
187
|
task,
|
|
161
188
|
reviewContextBlock(input.reviewContext),
|
|
189
|
+
mergeConflictContextBlock(input.mergeConflictContext),
|
|
162
190
|
input.includeSessionContext === false
|
|
163
191
|
? ""
|
|
164
192
|
: languageBlock(input.repository.language),
|
|
@@ -200,6 +228,28 @@ export async function composeEditPrompt(input) {
|
|
|
200
228
|
.filter(Boolean)
|
|
201
229
|
.join("\n\n");
|
|
202
230
|
}
|
|
231
|
+
export async function composeMergeConflictPrompt(input) {
|
|
232
|
+
const values = mergeConflictValues(input);
|
|
233
|
+
const task = await taskBlock({
|
|
234
|
+
builtin: "merge/conflict",
|
|
235
|
+
directory: input.directory,
|
|
236
|
+
values,
|
|
237
|
+
});
|
|
238
|
+
const persona = input.repository.agents.editor?.persona;
|
|
239
|
+
return [
|
|
240
|
+
task,
|
|
241
|
+
languageBlock(input.repository.language),
|
|
242
|
+
personaBlock(persona),
|
|
243
|
+
await editGuidelinesBlock({
|
|
244
|
+
directory: input.directory,
|
|
245
|
+
path: input.repository.prompts.editGuidelines,
|
|
246
|
+
values,
|
|
247
|
+
}),
|
|
248
|
+
editOutputContract,
|
|
249
|
+
]
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join("\n\n");
|
|
252
|
+
}
|
|
203
253
|
export async function composeFindingValidationPrompt(input) {
|
|
204
254
|
const values = { ...reviewValues(input), findings: input.findings };
|
|
205
255
|
const task = await taskBlock({
|
|
@@ -395,7 +445,14 @@ export async function composeTriageAcceptancePrompt(input) {
|
|
|
395
445
|
...input,
|
|
396
446
|
builtin: "acceptance",
|
|
397
447
|
customPath: input.repository.triage?.prompts.acceptance,
|
|
398
|
-
outputContract: triageVoteOutputContract('"YES" | "NO" | "ASK"'),
|
|
448
|
+
outputContract: triageVoteOutputContract('"YES" | "NO" | "INVALID" | "ASK"'),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
export async function composeTriageSignalPrompt(input) {
|
|
452
|
+
return composeTriageVotePrompt({
|
|
453
|
+
...input,
|
|
454
|
+
builtin: "signal",
|
|
455
|
+
outputContract: triageSignalOutputContract,
|
|
399
456
|
});
|
|
400
457
|
}
|
|
401
458
|
export async function composeTriageCommentClassificationPrompt(input) {
|