opencode-magi 0.0.0-dev-20260522033138 → 0.0.0-dev-20260522071924
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 +2 -2
- package/dist/config/validate.js +31 -17
- package/dist/orchestrator/triage.js +201 -138
- package/dist/prompts/contracts.js +6 -1
- package/dist/prompts/output.js +6 -1
- package/package.json +1 -1
- package/schema.json +3 -2
package/dist/config/resolve.js
CHANGED
|
@@ -104,7 +104,7 @@ export function resolveAgents(config) {
|
|
|
104
104
|
triageCreator: creator
|
|
105
105
|
? {
|
|
106
106
|
...creator,
|
|
107
|
-
account: creator.account ??
|
|
107
|
+
account: creator.account ?? "",
|
|
108
108
|
permission: resolveTriageCreatorPermission(agents, creator),
|
|
109
109
|
}
|
|
110
110
|
: undefined,
|
|
@@ -179,7 +179,6 @@ export function resolveRepository(config) {
|
|
|
179
179
|
requiredLabels: config.review?.safety?.requiredLabels ?? [],
|
|
180
180
|
},
|
|
181
181
|
triage: {
|
|
182
|
-
account: config.triage?.account,
|
|
183
182
|
automation: {
|
|
184
183
|
clear: config.triage?.automation?.clear ?? ["triage"],
|
|
185
184
|
close: config.triage?.automation?.close ?? false,
|
|
@@ -193,6 +192,7 @@ export function resolveRepository(config) {
|
|
|
193
192
|
},
|
|
194
193
|
output: config.triage?.output,
|
|
195
194
|
prompts: config.triage?.prompts ?? {},
|
|
195
|
+
reporter: config.triage?.reporter,
|
|
196
196
|
safety: {
|
|
197
197
|
allowAuthors: config.triage?.safety?.allowAuthors ?? [],
|
|
198
198
|
allowMentionActors: config.triage?.safety?.allowMentionActors ?? [],
|
package/dist/config/validate.js
CHANGED
|
@@ -40,6 +40,7 @@ const EDITOR_KEYS = new Set([
|
|
|
40
40
|
"persona",
|
|
41
41
|
]);
|
|
42
42
|
const TRIAGE_AGENT_KEYS = new Set([
|
|
43
|
+
"account",
|
|
43
44
|
"id",
|
|
44
45
|
"model",
|
|
45
46
|
"options",
|
|
@@ -75,7 +76,6 @@ const MERGE_KEYS = new Set([
|
|
|
75
76
|
"prompts",
|
|
76
77
|
]);
|
|
77
78
|
const TRIAGE_KEYS = new Set([
|
|
78
|
-
"account",
|
|
79
79
|
"agents",
|
|
80
80
|
"automation",
|
|
81
81
|
"categories",
|
|
@@ -83,6 +83,7 @@ const TRIAGE_KEYS = new Set([
|
|
|
83
83
|
"creator",
|
|
84
84
|
"output",
|
|
85
85
|
"prompts",
|
|
86
|
+
"reporter",
|
|
86
87
|
"safety",
|
|
87
88
|
"worktree",
|
|
88
89
|
]);
|
|
@@ -357,6 +358,9 @@ function validateTriageAgentList(agents, path, errors, catalog) {
|
|
|
357
358
|
errors.push(`${path}[${index}].model is required`);
|
|
358
359
|
validateString(agent.model, `${path}[${index}].model`, errors);
|
|
359
360
|
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
361
|
+
if (!agent.account)
|
|
362
|
+
errors.push(`${path}[${index}].account is required`);
|
|
363
|
+
validateString(agent.account, `${path}[${index}].account`, errors);
|
|
360
364
|
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
361
365
|
if (agent.options != null && !isPlainObject(agent.options))
|
|
362
366
|
errors.push(`${path}[${index}].options must be an object`);
|
|
@@ -383,12 +387,16 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
383
387
|
accounts.add(reviewer.account);
|
|
384
388
|
}
|
|
385
389
|
}
|
|
386
|
-
function
|
|
390
|
+
function validateResolvedTriageAgents(agents, path, errors) {
|
|
387
391
|
const keys = new Set();
|
|
392
|
+
const accounts = new Set();
|
|
388
393
|
for (const agent of agents) {
|
|
389
394
|
if (keys.has(agent.key))
|
|
390
395
|
errors.push(`${path} has duplicate agent key: ${agent.key}`);
|
|
391
396
|
keys.add(agent.key);
|
|
397
|
+
if (accounts.has(agent.account))
|
|
398
|
+
errors.push(`${path} has duplicate agent account: ${agent.account}`);
|
|
399
|
+
accounts.add(agent.account);
|
|
392
400
|
}
|
|
393
401
|
}
|
|
394
402
|
function validateEditor(editor, path, errors, catalog) {
|
|
@@ -672,19 +680,26 @@ function validateTriage(config, errors, options) {
|
|
|
672
680
|
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
673
681
|
const automation = triage.automation;
|
|
674
682
|
const concurrency = triage.concurrency;
|
|
683
|
+
const creator = triage.creator;
|
|
684
|
+
const reporter = typeof triage.reporter === "string" ? triage.reporter : undefined;
|
|
675
685
|
const safety = triage.safety;
|
|
676
|
-
if (!triage.account)
|
|
677
|
-
errors.push("triage.account is required");
|
|
678
|
-
validateString(triage.account, "triage.account", errors);
|
|
679
686
|
if (!triage.agents)
|
|
680
687
|
errors.push("triage.agents is required");
|
|
681
688
|
validateTriageAgentList(triage.agents, "triage.agents", errors, options.modelCatalog);
|
|
682
689
|
if (Array.isArray(triage.agents)) {
|
|
683
|
-
|
|
690
|
+
const resolvedTriageAgents = resolveAgents(config).triage ?? [];
|
|
691
|
+
validateResolvedTriageAgents(resolvedTriageAgents, "triage.resolvedAgents", errors);
|
|
692
|
+
if (reporter != null &&
|
|
693
|
+
!resolvedTriageAgents.some((agent) => agent.key === reporter)) {
|
|
694
|
+
errors.push(`triage.reporter must match a triage agent key: ${reporter}`);
|
|
695
|
+
}
|
|
684
696
|
}
|
|
685
|
-
|
|
686
|
-
|
|
697
|
+
validateString(triage.reporter, "triage.reporter", errors);
|
|
698
|
+
validateTriageCreator(creator, "triage.creator", errors, options.modelCatalog);
|
|
699
|
+
if (automation?.create && !creator)
|
|
687
700
|
errors.push("triage.creator is required when triage.automation.create is true");
|
|
701
|
+
if (automation?.create && creator && !creator.account)
|
|
702
|
+
errors.push("triage.creator.account is required when triage.automation.create is true");
|
|
688
703
|
if (automation != null && !isPlainObject(automation)) {
|
|
689
704
|
errors.push("triage.automation must be an object");
|
|
690
705
|
}
|
|
@@ -745,10 +760,10 @@ async function validateAuth(config, exec, errors) {
|
|
|
745
760
|
const agents = resolveAgents(config);
|
|
746
761
|
for (const reviewer of agents.reviewers)
|
|
747
762
|
accounts.add(reviewer.account);
|
|
763
|
+
for (const agent of agents.triage ?? [])
|
|
764
|
+
accounts.add(agent.account);
|
|
748
765
|
if (agents.editor)
|
|
749
766
|
accounts.add(agents.editor.account);
|
|
750
|
-
if (config.triage?.account)
|
|
751
|
-
accounts.add(config.triage.account);
|
|
752
767
|
if (agents.triageCreator?.account)
|
|
753
768
|
accounts.add(agents.triageCreator.account);
|
|
754
769
|
await Promise.all([...accounts].filter(Boolean).map(async (account) => {
|
|
@@ -802,19 +817,18 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
802
817
|
warnings.push(`Could not validate repository permissions for GitHub account: ${reviewer.account} (${error.message})`);
|
|
803
818
|
}
|
|
804
819
|
}));
|
|
805
|
-
|
|
820
|
+
await Promise.all((agents.triage ?? []).map(async (agent) => {
|
|
806
821
|
try {
|
|
807
|
-
const permissions = await fetchPermissions(config, exec,
|
|
822
|
+
const permissions = await fetchPermissions(config, exec, agent.account);
|
|
808
823
|
if (!permissions.pull) {
|
|
809
|
-
errors.push(`GitHub account cannot read repository for issue triage: ${
|
|
824
|
+
errors.push(`GitHub account cannot read repository for issue triage: ${agent.account}`);
|
|
810
825
|
}
|
|
811
826
|
}
|
|
812
827
|
catch (error) {
|
|
813
|
-
warnings.push(`Could not validate repository permissions for GitHub account: ${
|
|
828
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${agent.account} (${error.message})`);
|
|
814
829
|
}
|
|
815
|
-
}
|
|
816
|
-
if (agents.triageCreator?.account
|
|
817
|
-
agents.triageCreator.account !== config.triage?.account) {
|
|
830
|
+
}));
|
|
831
|
+
if (agents.triageCreator?.account) {
|
|
818
832
|
try {
|
|
819
833
|
const permissions = await fetchPermissions(config, exec, agents.triageCreator.account);
|
|
820
834
|
if (!permissions.push) {
|
|
@@ -3,10 +3,10 @@ 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 { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt,
|
|
6
|
+
import { composeTriageAcceptancePrompt, composeTriageActionPrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
7
|
import { parseTriageActionOutput, parseTriageBinaryOutput, parseTriageCategoryOutput, parseTriageCommentClassificationOutput, parseTriageCreatePrOutput, parseTriageDuplicateOutput, parseTriageExistingPrOutput, } from "../prompts/output";
|
|
8
8
|
import { aggregateStringMajority, majorityThreshold } from "./majority";
|
|
9
|
-
import {
|
|
9
|
+
import { runModelWithRepair, } from "./model";
|
|
10
10
|
const MARKER_PREFIX = "opencode-magi:triage";
|
|
11
11
|
const BINARY_VOTES = ["ASK", "NO", "YES"];
|
|
12
12
|
const DUPLICATE_VOTES = ["DUPLICATE", "NOT_DUPLICATE"];
|
|
@@ -181,6 +181,7 @@ async function runVote(input) {
|
|
|
181
181
|
...result.value,
|
|
182
182
|
promptText: prompt,
|
|
183
183
|
raw: result.raw,
|
|
184
|
+
reviewer: input.agent.key,
|
|
184
185
|
sessionId: result.sessionId,
|
|
185
186
|
};
|
|
186
187
|
}
|
|
@@ -189,6 +190,7 @@ async function writeVoteArtifacts(input) {
|
|
|
189
190
|
await writeFile(`${base}.prompt.txt`, `${input.output.promptText}\n`);
|
|
190
191
|
await writeFile(`${base}.raw.txt`, `${input.output.raw}\n`);
|
|
191
192
|
await writeJson(`${base}.json`, {
|
|
193
|
+
body: input.output.body,
|
|
192
194
|
reason: input.output.reason,
|
|
193
195
|
vote: input.output.vote,
|
|
194
196
|
});
|
|
@@ -282,7 +284,7 @@ async function runPhaseVote(input) {
|
|
|
282
284
|
reviewer: agents[index].key,
|
|
283
285
|
})));
|
|
284
286
|
await writeJson(join(input.outputDir, `${input.phase}-majority.json`), majority);
|
|
285
|
-
return majority.vote;
|
|
287
|
+
return { outputs, vote: majority.vote };
|
|
286
288
|
}
|
|
287
289
|
async function relationshipScan(input, issue) {
|
|
288
290
|
const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
|
|
@@ -290,17 +292,20 @@ async function relationshipScan(input, issue) {
|
|
|
290
292
|
fetchRelatedPullRequests(input.exec, input.repository, input.issue),
|
|
291
293
|
searchDuplicateIssues(input.exec, input.repository, issue),
|
|
292
294
|
]);
|
|
295
|
+
const triageAccounts = new Set((input.repository.agents.triage ?? []).map((agent) => agent.account));
|
|
293
296
|
const markers = comments
|
|
294
|
-
.filter((comment) => comment.author
|
|
297
|
+
.filter((comment) => triageAccounts.has(comment.author))
|
|
295
298
|
.map((comment) => {
|
|
296
299
|
const parsed = parseTriageMarker(comment.body);
|
|
297
|
-
return parsed
|
|
300
|
+
return parsed
|
|
301
|
+
? { ...parsed, account: comment.author, commentId: comment.id }
|
|
302
|
+
: undefined;
|
|
298
303
|
})
|
|
299
304
|
.filter(Boolean);
|
|
300
305
|
const previousMarker = markers.at(-1);
|
|
301
306
|
const mentionReplies = previousMarker
|
|
302
307
|
? eligibleMentionReplies({
|
|
303
|
-
account:
|
|
308
|
+
account: previousMarker.account ?? "",
|
|
304
309
|
comments,
|
|
305
310
|
marker: previousMarker,
|
|
306
311
|
processed: previousMarker.processed,
|
|
@@ -523,54 +528,41 @@ async function runReconsiderationVote(input) {
|
|
|
523
528
|
votes: BINARY_VOTES,
|
|
524
529
|
});
|
|
525
530
|
}
|
|
526
|
-
|
|
527
|
-
const agents =
|
|
528
|
-
if (!agents
|
|
531
|
+
function triageReporter(repository, issue) {
|
|
532
|
+
const agents = repository.agents.triage ?? [];
|
|
533
|
+
if (!agents.length)
|
|
529
534
|
throw new Error("triage.agents is required");
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
prompt,
|
|
562
|
-
signal: input.input.signal,
|
|
563
|
-
title: `Magi triage comment #${input.issue.number}`,
|
|
564
|
-
})).raw +
|
|
565
|
-
`\n\n${marker({
|
|
566
|
-
action: input.action,
|
|
567
|
-
checkpoint: "pending",
|
|
568
|
-
decision: input.result,
|
|
569
|
-
issue: input.issue.number,
|
|
570
|
-
processed: input.processed,
|
|
571
|
-
})}`;
|
|
572
|
-
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
573
|
-
return comment;
|
|
535
|
+
const configured = repository.triage?.reporter;
|
|
536
|
+
const reporter = configured
|
|
537
|
+
? agents.find((agent) => agent.key === configured)
|
|
538
|
+
: agents[Math.abs(issue) % agents.length];
|
|
539
|
+
if (!reporter)
|
|
540
|
+
throw new Error(`Unknown triage reporter: ${configured}`);
|
|
541
|
+
return reporter;
|
|
542
|
+
}
|
|
543
|
+
function decisionCommentBody(input) {
|
|
544
|
+
const reason = input.reason?.trim();
|
|
545
|
+
const result = JSON.stringify(input.result);
|
|
546
|
+
return reason
|
|
547
|
+
? `Magi triage decision: ${result}\n\nReason: ${reason}`
|
|
548
|
+
: `Magi triage decision: ${result}\n\nAction: ${input.action}`;
|
|
549
|
+
}
|
|
550
|
+
function agentForKey(repository, key) {
|
|
551
|
+
const agent = repository.agents.triage?.find((item) => item.key === key);
|
|
552
|
+
if (!agent)
|
|
553
|
+
throw new Error(`Unknown triage agent: ${key}`);
|
|
554
|
+
return agent;
|
|
555
|
+
}
|
|
556
|
+
function askOutputs(outputs) {
|
|
557
|
+
return (outputs ?? []).filter((output) => output.vote === "ASK");
|
|
558
|
+
}
|
|
559
|
+
function chooseDecisionReason(input) {
|
|
560
|
+
return (input.outputs?.find((output) => output.reviewer === input.reporter.key &&
|
|
561
|
+
output.vote === input.vote &&
|
|
562
|
+
output.reason)?.reason ??
|
|
563
|
+
input.outputs?.find((output) => output.vote === input.vote)?.reason ??
|
|
564
|
+
input.outputs?.find((output) => output.reviewer === input.reporter.key)
|
|
565
|
+
?.reason);
|
|
574
566
|
}
|
|
575
567
|
async function postMarkedIssueComment(input) {
|
|
576
568
|
const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
|
|
@@ -578,9 +570,20 @@ async function postMarkedIssueComment(input) {
|
|
|
578
570
|
const updated = body === input.body
|
|
579
571
|
? posted
|
|
580
572
|
: await updateIssueComment(input.exec, input.repository, posted.id, input.account, body);
|
|
581
|
-
await writeJson(join(input.outputDir,
|
|
573
|
+
await writeJson(join(input.outputDir, `posted-${updated.id}.json`), {
|
|
574
|
+
account: input.account,
|
|
575
|
+
...updated,
|
|
576
|
+
});
|
|
582
577
|
return updated;
|
|
583
578
|
}
|
|
579
|
+
async function postPlainIssueComment(input) {
|
|
580
|
+
const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
|
|
581
|
+
await writeJson(join(input.outputDir, `posted-${posted.id}.json`), {
|
|
582
|
+
account: input.account,
|
|
583
|
+
...posted,
|
|
584
|
+
});
|
|
585
|
+
return posted;
|
|
586
|
+
}
|
|
584
587
|
async function persistProcessedMarker(input) {
|
|
585
588
|
if (!input.marker.commentId)
|
|
586
589
|
return;
|
|
@@ -604,6 +607,49 @@ async function persistProcessedMarker(input) {
|
|
|
604
607
|
updated,
|
|
605
608
|
});
|
|
606
609
|
}
|
|
610
|
+
async function postAskComments(input) {
|
|
611
|
+
const urls = [];
|
|
612
|
+
for (const output of askOutputs(input.outputs)) {
|
|
613
|
+
const agent = agentForKey(input.repository, output.reviewer);
|
|
614
|
+
const body = input.mark
|
|
615
|
+
? `${output.body}\n\n${marker({
|
|
616
|
+
action: input.action,
|
|
617
|
+
checkpoint: "pending",
|
|
618
|
+
decision: input.result,
|
|
619
|
+
issue: input.issue.number,
|
|
620
|
+
processed: input.processed,
|
|
621
|
+
})}`
|
|
622
|
+
: output.body;
|
|
623
|
+
if (!body?.trim())
|
|
624
|
+
continue;
|
|
625
|
+
await writeFile(join(input.outputDir, `${agent.key}.ask-comment.md`), `${body}\n`);
|
|
626
|
+
if (input.dryRun) {
|
|
627
|
+
urls.push(`dry-run:would-comment:${agent.key}`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
await emitProgress(input.run, { type: "comment_posting" });
|
|
631
|
+
const posted = input.mark
|
|
632
|
+
? await postMarkedIssueComment({
|
|
633
|
+
account: agent.account,
|
|
634
|
+
body,
|
|
635
|
+
exec: input.exec,
|
|
636
|
+
issue: input.issue.number,
|
|
637
|
+
outputDir: input.outputDir,
|
|
638
|
+
repository: input.repository,
|
|
639
|
+
})
|
|
640
|
+
: await postPlainIssueComment({
|
|
641
|
+
account: agent.account,
|
|
642
|
+
body,
|
|
643
|
+
exec: input.exec,
|
|
644
|
+
issue: input.issue.number,
|
|
645
|
+
outputDir: input.outputDir,
|
|
646
|
+
repository: input.repository,
|
|
647
|
+
});
|
|
648
|
+
urls.push(posted.url);
|
|
649
|
+
await emitProgress(input.run, { type: "comment_posted", url: posted.url });
|
|
650
|
+
}
|
|
651
|
+
return urls;
|
|
652
|
+
}
|
|
607
653
|
async function finishWithResult(input) {
|
|
608
654
|
const triage = input.input.repository.triage;
|
|
609
655
|
if (!triage)
|
|
@@ -622,22 +668,43 @@ async function finishWithResult(input) {
|
|
|
622
668
|
result: input.result,
|
|
623
669
|
});
|
|
624
670
|
let prUrl;
|
|
625
|
-
const
|
|
626
|
-
|
|
671
|
+
const reporter = triageReporter(input.input.repository, input.issue.number);
|
|
672
|
+
const comment = plan.postComment && input.result.disposition !== "ask"
|
|
673
|
+
? `${decisionCommentBody({
|
|
674
|
+
action: plan.action,
|
|
675
|
+
reason: input.commentReason,
|
|
676
|
+
result: input.result,
|
|
677
|
+
})}\n\n${marker({
|
|
627
678
|
action: plan.action,
|
|
628
|
-
|
|
629
|
-
|
|
679
|
+
checkpoint: "pending",
|
|
680
|
+
decision: input.result,
|
|
681
|
+
issue: input.issue.number,
|
|
682
|
+
processed: input.processed,
|
|
683
|
+
})}`
|
|
684
|
+
: undefined;
|
|
685
|
+
if (comment) {
|
|
686
|
+
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
687
|
+
}
|
|
688
|
+
if (input.result.disposition === "ask" && input.askOutputs) {
|
|
689
|
+
await postAskComments({
|
|
690
|
+
action: plan.action,
|
|
691
|
+
dryRun: input.input.dryRun,
|
|
692
|
+
exec: input.input.exec,
|
|
630
693
|
issue: input.issue,
|
|
694
|
+
mark: input.markAskComments ?? false,
|
|
695
|
+
outputs: input.askOutputs,
|
|
631
696
|
outputDir: input.outputDir,
|
|
632
697
|
processed: input.processed,
|
|
698
|
+
repository: input.input.repository,
|
|
633
699
|
result: input.result,
|
|
634
|
-
|
|
635
|
-
|
|
700
|
+
run: input.input,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
636
703
|
if (!input.input.dryRun) {
|
|
637
704
|
if (comment) {
|
|
638
705
|
await emitProgress(input.input, { type: "comment_posting" });
|
|
639
706
|
const posted = await postMarkedIssueComment({
|
|
640
|
-
account:
|
|
707
|
+
account: reporter.account,
|
|
641
708
|
body: comment,
|
|
642
709
|
exec: input.input.exec,
|
|
643
710
|
issue: input.issue.number,
|
|
@@ -652,18 +719,18 @@ async function finishWithResult(input) {
|
|
|
652
719
|
if (plan.clearLabels) {
|
|
653
720
|
const clearLabels = existingClearLabels(input.issue, triage.automation.clear);
|
|
654
721
|
if (clearLabels.length) {
|
|
655
|
-
await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels,
|
|
722
|
+
await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels, reporter.account);
|
|
656
723
|
}
|
|
657
724
|
}
|
|
658
725
|
if (plan.closeIssue) {
|
|
659
726
|
const closedPrs = [];
|
|
660
727
|
for (const pr of input.relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
|
|
661
|
-
await closePullRequest(input.input.exec, input.input.repository, pr.number,
|
|
728
|
+
await closePullRequest(input.input.exec, input.input.repository, pr.number, reporter.account);
|
|
662
729
|
closedPrs.push(pr.number);
|
|
663
730
|
}
|
|
664
731
|
if (closedPrs.length)
|
|
665
732
|
await writeJson(join(input.outputDir, "closed-prs.json"), closedPrs);
|
|
666
|
-
await closeIssue(input.input.exec, input.input.repository, input.issue.number,
|
|
733
|
+
await closeIssue(input.input.exec, input.input.repository, input.issue.number, reporter.account);
|
|
667
734
|
}
|
|
668
735
|
if (plan.createPr) {
|
|
669
736
|
prUrl = await createImplementationPr({
|
|
@@ -679,7 +746,7 @@ async function finishWithResult(input) {
|
|
|
679
746
|
}
|
|
680
747
|
if (input.previousMarker && prUrl) {
|
|
681
748
|
await persistProcessedMarker({
|
|
682
|
-
account:
|
|
749
|
+
account: input.previousMarker.account ?? reporter.account,
|
|
683
750
|
comments: input.relationship.comments,
|
|
684
751
|
exec: input.input.exec,
|
|
685
752
|
issue: input.issue,
|
|
@@ -734,13 +801,10 @@ async function createImplementationPr(input) {
|
|
|
734
801
|
const creator = input.input.repository.agents.triageCreator;
|
|
735
802
|
if (!creator)
|
|
736
803
|
return undefined;
|
|
737
|
-
const triage = input.input.repository.triage;
|
|
738
|
-
if (!triage?.account)
|
|
739
|
-
throw new Error("triage.account is required");
|
|
740
804
|
await emitProgress(input.input, { type: "pr_creation_started" });
|
|
741
805
|
await emitProgress(input.input, { type: "triage_creator_started" });
|
|
742
806
|
try {
|
|
743
|
-
await assignIssue(input.input.exec, input.input.repository, input.issue.number,
|
|
807
|
+
await assignIssue(input.input.exec, input.input.repository, input.issue.number, creator.account);
|
|
744
808
|
const branch = `magi/issue-${input.issue.number}-${Date.now().toString(36)}`;
|
|
745
809
|
const worktreePath = join(worktreeBaseDir(input.input.directory, input.input.config, "issue"), `issue-${input.issue.number}`);
|
|
746
810
|
await mkdir(dirname(worktreePath), { recursive: true });
|
|
@@ -824,8 +888,8 @@ async function createImplementationPr(input) {
|
|
|
824
888
|
}
|
|
825
889
|
export async function runTriage(input) {
|
|
826
890
|
const triage = input.repository.triage;
|
|
827
|
-
if (!triage
|
|
828
|
-
throw new Error("triage
|
|
891
|
+
if (!triage)
|
|
892
|
+
throw new Error("triage configuration is required");
|
|
829
893
|
const agents = input.repository.agents.triage;
|
|
830
894
|
if (!agents?.length)
|
|
831
895
|
throw new Error("triage.agents is required");
|
|
@@ -865,6 +929,9 @@ export async function runTriage(input) {
|
|
|
865
929
|
await emitProgress(input, { phase: "triaging", type: "phase" });
|
|
866
930
|
let processed = relationship.previousMarker?.processed ?? [];
|
|
867
931
|
let result;
|
|
932
|
+
let askCommentOutputs;
|
|
933
|
+
let commentReason;
|
|
934
|
+
let markAskComments = false;
|
|
868
935
|
if (relationship.previousMarker) {
|
|
869
936
|
if (!relationship.mentionReplies.length) {
|
|
870
937
|
const result = finalResultFromMarker(relationship.previousMarker);
|
|
@@ -909,7 +976,7 @@ export async function runTriage(input) {
|
|
|
909
976
|
if (!triggeringComments.length) {
|
|
910
977
|
if (!input.dryRun) {
|
|
911
978
|
await persistProcessedMarker({
|
|
912
|
-
account:
|
|
979
|
+
account: relationship.previousMarker.account ?? "",
|
|
913
980
|
comments: relationship.comments,
|
|
914
981
|
exec: input.exec,
|
|
915
982
|
issue,
|
|
@@ -934,21 +1001,38 @@ export async function runTriage(input) {
|
|
|
934
1001
|
},
|
|
935
1002
|
});
|
|
936
1003
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
937
|
-
const vote = await runReconsiderationVote({ context, input, outputDir });
|
|
938
1004
|
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1005
|
+
if (previous.disposition !== "ask" ||
|
|
1006
|
+
previous.askReason !== "acceptance_unclear") {
|
|
1007
|
+
const reconsideration = await runReconsiderationVote({
|
|
1008
|
+
context,
|
|
1009
|
+
input,
|
|
1010
|
+
outputDir,
|
|
1011
|
+
});
|
|
1012
|
+
const reporter = triageReporter(input.repository, issue.number);
|
|
1013
|
+
commentReason = chooseDecisionReason({
|
|
1014
|
+
outputs: reconsideration.outputs,
|
|
1015
|
+
reporter,
|
|
1016
|
+
vote: reconsideration.vote ?? "ASK",
|
|
1017
|
+
});
|
|
1018
|
+
result =
|
|
1019
|
+
reconsideration.vote === "YES"
|
|
1020
|
+
? { category: previous.category, disposition: "accepted" }
|
|
1021
|
+
: reconsideration.vote === "NO"
|
|
1022
|
+
? { category: previous.category, disposition: "rejected" }
|
|
1023
|
+
: {
|
|
1024
|
+
askReason: "acceptance_unclear",
|
|
1025
|
+
category: previous.category,
|
|
1026
|
+
disposition: "ask",
|
|
1027
|
+
};
|
|
1028
|
+
if (result.disposition === "ask") {
|
|
1029
|
+
askCommentOutputs = askOutputs(reconsideration.outputs);
|
|
1030
|
+
markAskComments = true;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
949
1033
|
}
|
|
950
1034
|
if (!result && relationship.relatedPullRequests.length) {
|
|
951
|
-
const
|
|
1035
|
+
const existingPr = await runPhaseVote({
|
|
952
1036
|
context,
|
|
953
1037
|
input,
|
|
954
1038
|
outputDir,
|
|
@@ -958,7 +1042,7 @@ export async function runTriage(input) {
|
|
|
958
1042
|
schemaName: "triage existing PR",
|
|
959
1043
|
votes: EXISTING_PR_VOTES,
|
|
960
1044
|
});
|
|
961
|
-
if (vote === "RELATED_PR_HANDLES_ISSUE") {
|
|
1045
|
+
if (existingPr.vote === "RELATED_PR_HANDLES_ISSUE") {
|
|
962
1046
|
const merged = relationship.relatedPullRequests.some((pr) => pr.state === "MERGED");
|
|
963
1047
|
if (merged && triage.automation.close) {
|
|
964
1048
|
const relatedPrDecision = {
|
|
@@ -973,59 +1057,21 @@ export async function runTriage(input) {
|
|
|
973
1057
|
createPr: false,
|
|
974
1058
|
postComment: true,
|
|
975
1059
|
};
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1060
|
+
return finishWithResult({
|
|
1061
|
+
commentReason: chooseDecisionReason({
|
|
1062
|
+
outputs: existingPr.outputs,
|
|
1063
|
+
reporter: triageReporter(input.repository, issue.number),
|
|
1064
|
+
vote: "RELATED_PR_HANDLES_ISSUE",
|
|
1065
|
+
}),
|
|
982
1066
|
context,
|
|
983
1067
|
input,
|
|
984
|
-
outputDir,
|
|
985
|
-
plan,
|
|
986
|
-
result: relatedPrDecision,
|
|
987
|
-
});
|
|
988
|
-
const body = await composeResultComment({
|
|
989
|
-
action: "CLOSE",
|
|
990
|
-
context: `Result: ${decisionText(relatedPrDecision)}\nAction: CLOSE\n\n${context}`,
|
|
991
|
-
input,
|
|
992
1068
|
issue,
|
|
993
1069
|
outputDir,
|
|
1070
|
+
plan,
|
|
994
1071
|
processed,
|
|
1072
|
+
relationship,
|
|
995
1073
|
result: relatedPrDecision,
|
|
996
1074
|
});
|
|
997
|
-
if (!input.dryRun) {
|
|
998
|
-
await emitProgress(input, { type: "comment_posting" });
|
|
999
|
-
const posted = await postMarkedIssueComment({
|
|
1000
|
-
account: triage.account,
|
|
1001
|
-
body,
|
|
1002
|
-
exec: input.exec,
|
|
1003
|
-
issue: issue.number,
|
|
1004
|
-
outputDir,
|
|
1005
|
-
repository: input.repository,
|
|
1006
|
-
});
|
|
1007
|
-
await emitProgress(input, { type: "comment_posted", url: posted.url });
|
|
1008
|
-
const clearLabels = existingClearLabels(issue, triage.automation.clear);
|
|
1009
|
-
if (clearLabels.length) {
|
|
1010
|
-
await removeIssueLabels(input.exec, input.repository, issue.number, clearLabels, triage.account);
|
|
1011
|
-
}
|
|
1012
|
-
const closedPrs = [];
|
|
1013
|
-
for (const pr of relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
|
|
1014
|
-
await closePullRequest(input.exec, input.repository, pr.number, triage.account);
|
|
1015
|
-
closedPrs.push(pr.number);
|
|
1016
|
-
}
|
|
1017
|
-
if (closedPrs.length)
|
|
1018
|
-
await writeJson(join(outputDir, "closed-prs.json"), closedPrs);
|
|
1019
|
-
await closeIssue(input.exec, input.repository, issue.number, triage.account);
|
|
1020
|
-
}
|
|
1021
|
-
const report = `Magi triage closed #${issue.number} because a related PR was merged.`;
|
|
1022
|
-
await writeFile(join(outputDir, "report.md"), `${report}\n`);
|
|
1023
|
-
return {
|
|
1024
|
-
issue: issue.number,
|
|
1025
|
-
outputDir,
|
|
1026
|
-
report,
|
|
1027
|
-
result: relatedPrDecision,
|
|
1028
|
-
};
|
|
1029
1075
|
}
|
|
1030
1076
|
return finishWithResult({
|
|
1031
1077
|
context,
|
|
@@ -1047,6 +1093,7 @@ export async function runTriage(input) {
|
|
|
1047
1093
|
});
|
|
1048
1094
|
if (duplicate) {
|
|
1049
1095
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
1096
|
+
commentReason = duplicate.reason;
|
|
1050
1097
|
result = { category: null, disposition: "duplicate" };
|
|
1051
1098
|
}
|
|
1052
1099
|
}
|
|
@@ -1056,8 +1103,9 @@ export async function runTriage(input) {
|
|
|
1056
1103
|
category: resolvedCategory,
|
|
1057
1104
|
source: resolvedCategory ? "config" : "vote",
|
|
1058
1105
|
});
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1106
|
+
const categoryVote = resolvedCategory
|
|
1107
|
+
? undefined
|
|
1108
|
+
: await runPhaseVote({
|
|
1061
1109
|
context,
|
|
1062
1110
|
input,
|
|
1063
1111
|
outputDir,
|
|
@@ -1066,14 +1114,16 @@ export async function runTriage(input) {
|
|
|
1066
1114
|
prompt: composeTriageCategoryPrompt,
|
|
1067
1115
|
schemaName: "triage category",
|
|
1068
1116
|
votes: ["ASK", ...triage.categories.map((item) => item.id)],
|
|
1069
|
-
})
|
|
1070
|
-
|
|
1117
|
+
});
|
|
1118
|
+
const category = resolvedCategory ?? categoryVote?.vote ?? "ASK";
|
|
1071
1119
|
if (category === "ASK") {
|
|
1072
1120
|
result = {
|
|
1073
1121
|
askReason: "category_unclear",
|
|
1074
1122
|
category: null,
|
|
1075
1123
|
disposition: "ask",
|
|
1076
1124
|
};
|
|
1125
|
+
askCommentOutputs = askOutputs(categoryVote?.outputs);
|
|
1126
|
+
markAskComments = false;
|
|
1077
1127
|
}
|
|
1078
1128
|
else {
|
|
1079
1129
|
const categoryConfig = triage.categories.find((item) => item.id === category);
|
|
@@ -1081,7 +1131,7 @@ export async function runTriage(input) {
|
|
|
1081
1131
|
category: categoryConfig,
|
|
1082
1132
|
triageContext: context,
|
|
1083
1133
|
}, null, 2);
|
|
1084
|
-
const
|
|
1134
|
+
const acceptance = await runPhaseVote({
|
|
1085
1135
|
context: voteContext,
|
|
1086
1136
|
input,
|
|
1087
1137
|
outputDir,
|
|
@@ -1091,22 +1141,35 @@ export async function runTriage(input) {
|
|
|
1091
1141
|
schemaName: "triage acceptance",
|
|
1092
1142
|
votes: BINARY_VOTES,
|
|
1093
1143
|
});
|
|
1144
|
+
const reporter = triageReporter(input.repository, issue.number);
|
|
1145
|
+
commentReason = chooseDecisionReason({
|
|
1146
|
+
outputs: acceptance.outputs,
|
|
1147
|
+
reporter,
|
|
1148
|
+
vote: acceptance.vote ?? "ASK",
|
|
1149
|
+
});
|
|
1094
1150
|
result =
|
|
1095
|
-
vote === "YES"
|
|
1151
|
+
acceptance.vote === "YES"
|
|
1096
1152
|
? { category, disposition: "accepted" }
|
|
1097
|
-
: vote === "NO"
|
|
1153
|
+
: acceptance.vote === "NO"
|
|
1098
1154
|
? { category, disposition: "rejected" }
|
|
1099
1155
|
: {
|
|
1100
1156
|
askReason: "acceptance_unclear",
|
|
1101
1157
|
category,
|
|
1102
1158
|
disposition: "ask",
|
|
1103
1159
|
};
|
|
1160
|
+
if (result.disposition === "ask") {
|
|
1161
|
+
askCommentOutputs = askOutputs(acceptance.outputs);
|
|
1162
|
+
markAskComments = true;
|
|
1163
|
+
}
|
|
1104
1164
|
}
|
|
1105
1165
|
}
|
|
1106
1166
|
return finishWithResult({
|
|
1167
|
+
askOutputs: askCommentOutputs,
|
|
1168
|
+
commentReason,
|
|
1107
1169
|
context,
|
|
1108
1170
|
input,
|
|
1109
1171
|
issue,
|
|
1172
|
+
markAskComments,
|
|
1110
1173
|
outputDir,
|
|
1111
1174
|
processed,
|
|
1112
1175
|
relationship,
|
|
@@ -217,8 +217,13 @@ Return exactly one JSON object and nothing else. Do not wrap it in markdown.
|
|
|
217
217
|
The object must match this shape:
|
|
218
218
|
{
|
|
219
219
|
"vote": ${votes},
|
|
220
|
-
"reason": "Short rationale."
|
|
220
|
+
"reason": "Short rationale.",
|
|
221
|
+
"body": "Required only when vote is ASK. Public issue comment body asking for the missing information."
|
|
221
222
|
}
|
|
223
|
+
|
|
224
|
+
Rules:
|
|
225
|
+
- body is required when vote is ASK and must be written for the issue author.
|
|
226
|
+
- Omit body when vote is not ASK.
|
|
222
227
|
</output_contract>`.trim();
|
|
223
228
|
}
|
|
224
229
|
export const triageDuplicateOutputContract = `
|
package/dist/prompts/output.js
CHANGED
|
@@ -92,9 +92,14 @@ function parseTriageVote(text, votes) {
|
|
|
92
92
|
const data = extractJson(text);
|
|
93
93
|
if (!data || typeof data !== "object")
|
|
94
94
|
throw new Error("triage vote output must be an object");
|
|
95
|
+
const vote = requireOneOf(data.vote, "vote", votes);
|
|
96
|
+
const body = data.body == null ? undefined : requireString(data.body, "body");
|
|
97
|
+
if (vote === "ASK" && !body?.trim())
|
|
98
|
+
throw new Error("ASK requires body");
|
|
95
99
|
return {
|
|
100
|
+
body,
|
|
96
101
|
reason: requireString(data.reason, "reason"),
|
|
97
|
-
vote
|
|
102
|
+
vote,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
export function parseTriageExistingPrOutput(text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-magi",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20260522071924",
|
|
4
4
|
"description": "Multi-agent PR review and merge orchestration plugin for OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hirotomo Yamada <hirotomo.yamada@avap.co.jp>",
|
package/schema.json
CHANGED
|
@@ -116,12 +116,13 @@
|
|
|
116
116
|
"triageAgent": {
|
|
117
117
|
"type": "object",
|
|
118
118
|
"if": { "not": { "required": ["ref"] } },
|
|
119
|
-
"then": { "required": ["model"] },
|
|
119
|
+
"then": { "required": ["model", "account"] },
|
|
120
120
|
"additionalProperties": false,
|
|
121
121
|
"properties": {
|
|
122
122
|
"ref": { "type": "string", "minLength": 1 },
|
|
123
123
|
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]+$" },
|
|
124
124
|
"model": { "type": "string", "minLength": 1 },
|
|
125
|
+
"account": { "type": "string", "minLength": 1 },
|
|
125
126
|
"options": { "type": "object", "additionalProperties": true },
|
|
126
127
|
"permissions": { "$ref": "#/$defs/permissions" },
|
|
127
128
|
"persona": { "type": "string" }
|
|
@@ -334,7 +335,6 @@
|
|
|
334
335
|
"type": "object",
|
|
335
336
|
"additionalProperties": false,
|
|
336
337
|
"properties": {
|
|
337
|
-
"account": { "type": "string", "minLength": 1 },
|
|
338
338
|
"agents": {
|
|
339
339
|
"type": "array",
|
|
340
340
|
"minItems": 3,
|
|
@@ -349,6 +349,7 @@
|
|
|
349
349
|
"safety": { "$ref": "#/$defs/triageSafety" },
|
|
350
350
|
"concurrency": { "$ref": "#/$defs/triageConcurrency" },
|
|
351
351
|
"prompts": { "$ref": "#/$defs/triagePrompts" },
|
|
352
|
+
"reporter": { "type": "string", "minLength": 1 },
|
|
352
353
|
"output": { "type": "string" },
|
|
353
354
|
"worktree": { "type": "string" }
|
|
354
355
|
}
|