opencode-magi 0.4.0 → 0.6.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 +53 -45
- package/dist/config/resolve.js +3 -3
- package/dist/config/validate.js +31 -18
- package/dist/config/worktree.js +6 -0
- package/dist/github/commands.js +167 -92
- package/dist/index.js +69 -4
- package/dist/orchestrator/ci.js +21 -14
- package/dist/orchestrator/findings.js +28 -7
- package/dist/orchestrator/inline-comments.js +0 -6
- package/dist/orchestrator/majority.js +1 -1
- package/dist/orchestrator/merge.js +46 -47
- package/dist/orchestrator/model.js +23 -9
- package/dist/orchestrator/report.js +7 -18
- package/dist/orchestrator/review-context.js +37 -4
- package/dist/orchestrator/review.js +174 -61
- package/dist/orchestrator/run-manager.js +209 -138
- package/dist/orchestrator/triage.js +243 -201
- package/dist/prompts/compose.js +2 -10
- package/dist/prompts/contracts.js +36 -57
- package/dist/prompts/output.js +28 -56
- package/dist/prompts/templates/merge/edit.md +1 -2
- package/dist/prompts/templates/review/close-reconsideration.md +1 -0
- package/dist/prompts/templates/review/rereview.md +3 -0
- package/dist/prompts/templates/review/review.md +4 -0
- package/package.json +1 -1
- package/schema.json +3 -3
- package/dist/prompts/templates/triage/action.md +0 -5
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { issueRunOutputDir } from "../config/output";
|
|
4
|
-
import {
|
|
4
|
+
import { issueRunWorktreeDir } 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,
|
|
7
|
-
import {
|
|
6
|
+
import { composeTriageAcceptancePrompt, composeTriageCategoryPrompt, composeTriageCommentClassificationPrompt, composeTriageCreatePrPrompt, composeTriageDuplicatePrompt, composeTriageExistingPrPrompt, composeTriageReconsiderPrompt, } from "../prompts/compose";
|
|
7
|
+
import { 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"];
|
|
@@ -106,24 +106,24 @@ async function emitTriageModelProgress(input) {
|
|
|
106
106
|
await emitProgress(input.run, {
|
|
107
107
|
options: input.progress.options,
|
|
108
108
|
phase: input.phase,
|
|
109
|
-
reviewer: input.reviewer,
|
|
110
109
|
sessionId: input.progress.sessionId,
|
|
111
110
|
type: "triage_agent_session",
|
|
111
|
+
voter: input.voter,
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
if (input.progress.type === "repair") {
|
|
115
115
|
await emitProgress(input.run, {
|
|
116
116
|
phase: input.phase,
|
|
117
|
-
reviewer: input.reviewer,
|
|
118
117
|
type: "triage_agent_repair",
|
|
118
|
+
voter: input.voter,
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
if (input.progress.type === "response") {
|
|
122
122
|
await emitProgress(input.run, {
|
|
123
123
|
phase: input.phase,
|
|
124
|
-
reviewer: input.reviewer,
|
|
125
124
|
sessionId: input.progress.sessionId,
|
|
126
125
|
type: "triage_agent_response",
|
|
126
|
+
voter: input.voter,
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
}
|
|
@@ -133,12 +133,12 @@ async function runVote(input) {
|
|
|
133
133
|
directory: input.directory,
|
|
134
134
|
issue: input.issue,
|
|
135
135
|
repository: input.repository,
|
|
136
|
-
|
|
136
|
+
voter: input.agent,
|
|
137
137
|
});
|
|
138
138
|
await emitProgress(input.run, {
|
|
139
139
|
phase: input.phase,
|
|
140
|
-
reviewer: input.agent.key,
|
|
141
140
|
type: "triage_agent_started",
|
|
141
|
+
voter: input.agent.key,
|
|
142
142
|
});
|
|
143
143
|
let result;
|
|
144
144
|
try {
|
|
@@ -148,10 +148,11 @@ async function runVote(input) {
|
|
|
148
148
|
onProgress: (progress) => emitTriageModelProgress({
|
|
149
149
|
phase: input.phase,
|
|
150
150
|
progress,
|
|
151
|
-
reviewer: input.agent.key,
|
|
152
151
|
run: input.run,
|
|
152
|
+
voter: input.agent.key,
|
|
153
153
|
}),
|
|
154
154
|
options: input.agent.options,
|
|
155
|
+
parentSessionId: input.run.parentSessionId,
|
|
155
156
|
parse: input.parse,
|
|
156
157
|
permission: input.agent.permission,
|
|
157
158
|
prompt,
|
|
@@ -165,16 +166,16 @@ async function runVote(input) {
|
|
|
165
166
|
await emitProgress(input.run, {
|
|
166
167
|
error: error instanceof Error ? error.message : String(error),
|
|
167
168
|
phase: input.phase,
|
|
168
|
-
reviewer: input.agent.key,
|
|
169
169
|
type: "triage_agent_failed",
|
|
170
|
+
voter: input.agent.key,
|
|
170
171
|
});
|
|
171
172
|
throw error;
|
|
172
173
|
}
|
|
173
174
|
await emitProgress(input.run, {
|
|
174
175
|
phase: input.phase,
|
|
175
|
-
reviewer: input.agent.key,
|
|
176
176
|
sessionId: result.sessionId,
|
|
177
177
|
type: "triage_agent_completed",
|
|
178
|
+
voter: input.agent.key,
|
|
178
179
|
vote: result.value.vote,
|
|
179
180
|
});
|
|
180
181
|
return {
|
|
@@ -182,13 +183,15 @@ async function runVote(input) {
|
|
|
182
183
|
promptText: prompt,
|
|
183
184
|
raw: result.raw,
|
|
184
185
|
sessionId: result.sessionId,
|
|
186
|
+
voter: input.agent.key,
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
189
|
async function writeVoteArtifacts(input) {
|
|
188
|
-
const base = join(input.outputDir, `${input.
|
|
190
|
+
const base = join(input.outputDir, `${input.voter}.${input.phase}`);
|
|
189
191
|
await writeFile(`${base}.prompt.txt`, `${input.output.promptText}\n`);
|
|
190
192
|
await writeFile(`${base}.raw.txt`, `${input.output.raw}\n`);
|
|
191
193
|
await writeJson(`${base}.json`, {
|
|
194
|
+
body: input.output.body,
|
|
192
195
|
reason: input.output.reason,
|
|
193
196
|
vote: input.output.vote,
|
|
194
197
|
});
|
|
@@ -230,14 +233,14 @@ async function runDuplicateVote(input) {
|
|
|
230
233
|
signal: input.input.signal,
|
|
231
234
|
})));
|
|
232
235
|
const majority = aggregateStringMajority(outputs.map((output, index) => ({
|
|
233
|
-
|
|
236
|
+
voter: agents[index].key,
|
|
234
237
|
vote: output.vote,
|
|
235
238
|
})), DUPLICATE_VOTES);
|
|
236
239
|
await Promise.all(outputs.map((output, index) => writeVoteArtifacts({
|
|
237
240
|
output,
|
|
238
241
|
outputDir: input.outputDir,
|
|
239
242
|
phase: "duplicate",
|
|
240
|
-
|
|
243
|
+
voter: agents[index].key,
|
|
241
244
|
})));
|
|
242
245
|
await Promise.all(outputs.map((output, index) => writeJson(join(input.outputDir, `${agents[index].key}.duplicate.json`), {
|
|
243
246
|
duplicateOf: output.duplicateOf,
|
|
@@ -272,17 +275,17 @@ async function runPhaseVote(input) {
|
|
|
272
275
|
signal: input.input.signal,
|
|
273
276
|
})));
|
|
274
277
|
const majority = aggregateStringMajority(outputs.map((output, index) => ({
|
|
275
|
-
|
|
278
|
+
voter: agents[index].key,
|
|
276
279
|
vote: output.vote,
|
|
277
280
|
})), input.votes);
|
|
278
281
|
await Promise.all(outputs.map((output, index) => writeVoteArtifacts({
|
|
279
282
|
output,
|
|
280
283
|
outputDir: input.outputDir,
|
|
281
284
|
phase: input.phase,
|
|
282
|
-
|
|
285
|
+
voter: agents[index].key,
|
|
283
286
|
})));
|
|
284
287
|
await writeJson(join(input.outputDir, `${input.phase}-majority.json`), majority);
|
|
285
|
-
return majority.vote;
|
|
288
|
+
return { outputs, vote: majority.vote };
|
|
286
289
|
}
|
|
287
290
|
async function relationshipScan(input, issue) {
|
|
288
291
|
const [comments, relatedPullRequests, duplicateCandidates] = await Promise.all([
|
|
@@ -290,17 +293,20 @@ async function relationshipScan(input, issue) {
|
|
|
290
293
|
fetchRelatedPullRequests(input.exec, input.repository, input.issue),
|
|
291
294
|
searchDuplicateIssues(input.exec, input.repository, issue),
|
|
292
295
|
]);
|
|
296
|
+
const triageAccounts = new Set((input.repository.agents.triage ?? []).map((agent) => agent.account));
|
|
293
297
|
const markers = comments
|
|
294
|
-
.filter((comment) => comment.author
|
|
298
|
+
.filter((comment) => triageAccounts.has(comment.author))
|
|
295
299
|
.map((comment) => {
|
|
296
300
|
const parsed = parseTriageMarker(comment.body);
|
|
297
|
-
return parsed
|
|
301
|
+
return parsed
|
|
302
|
+
? { ...parsed, account: comment.author, commentId: comment.id }
|
|
303
|
+
: undefined;
|
|
298
304
|
})
|
|
299
305
|
.filter(Boolean);
|
|
300
306
|
const previousMarker = markers.at(-1);
|
|
301
307
|
const mentionReplies = previousMarker
|
|
302
308
|
? eligibleMentionReplies({
|
|
303
|
-
account:
|
|
309
|
+
account: previousMarker.account ?? "",
|
|
304
310
|
comments,
|
|
305
311
|
marker: previousMarker,
|
|
306
312
|
processed: previousMarker.processed,
|
|
@@ -450,63 +456,38 @@ function previousAutomationPlan(input) {
|
|
|
450
456
|
postComment: false,
|
|
451
457
|
};
|
|
452
458
|
}
|
|
453
|
-
async function runActionPrompt(input) {
|
|
454
|
-
const agent = input.input.repository.agents.triage?.[0];
|
|
455
|
-
if (!agent)
|
|
456
|
-
throw new Error("triage.agents is required");
|
|
457
|
-
const context = JSON.stringify({
|
|
458
|
-
allowedActions: input.plan.allowedActions,
|
|
459
|
-
deterministicPlan: input.plan,
|
|
460
|
-
result: input.result,
|
|
461
|
-
triageContext: input.context,
|
|
462
|
-
}, null, 2);
|
|
463
|
-
const prompt = await composeTriageActionPrompt({
|
|
464
|
-
context,
|
|
465
|
-
directory: input.input.directory,
|
|
466
|
-
issue: input.input.issue,
|
|
467
|
-
repository: input.input.repository,
|
|
468
|
-
reviewer: agent,
|
|
469
|
-
});
|
|
470
|
-
const result = await runModelWithRepair({
|
|
471
|
-
client: input.input.client,
|
|
472
|
-
model: agent.model,
|
|
473
|
-
options: agent.options,
|
|
474
|
-
parse: parseTriageActionOutput,
|
|
475
|
-
permission: agent.permission,
|
|
476
|
-
prompt,
|
|
477
|
-
repairAttempts: 3,
|
|
478
|
-
schemaName: "triage action",
|
|
479
|
-
signal: input.input.signal,
|
|
480
|
-
title: `Magi triage action #${input.input.issue}`,
|
|
481
|
-
});
|
|
482
|
-
await writeJson(join(input.outputDir, "action.json"), {
|
|
483
|
-
model: result.value,
|
|
484
|
-
plan: input.plan,
|
|
485
|
-
});
|
|
486
|
-
return result.value;
|
|
487
|
-
}
|
|
488
459
|
async function classifyMentionReplies(input) {
|
|
489
|
-
const agent = input.input.repository.
|
|
490
|
-
if (!agent)
|
|
491
|
-
throw new Error("triage.agents is required");
|
|
460
|
+
const agent = triageReporter(input.input.repository, input.input.issue);
|
|
492
461
|
const prompt = await composeTriageCommentClassificationPrompt({
|
|
493
462
|
context: JSON.stringify({ context: input.context, mentionReplies: input.replies }, null, 2),
|
|
494
463
|
directory: input.input.directory,
|
|
495
464
|
issue: input.input.issue,
|
|
496
465
|
repository: input.input.repository,
|
|
497
|
-
|
|
466
|
+
voter: agent,
|
|
498
467
|
});
|
|
499
468
|
const result = await runModelWithRepair({
|
|
500
469
|
client: input.input.client,
|
|
501
470
|
model: agent.model,
|
|
471
|
+
onProgress: async (progress) => {
|
|
472
|
+
if (progress.type !== "session_created")
|
|
473
|
+
return;
|
|
474
|
+
await emitProgress(input.input, {
|
|
475
|
+
agent: agent.key,
|
|
476
|
+
key: `triage:comment-classification:${agent.key}:${progress.sessionId}`,
|
|
477
|
+
options: progress.options,
|
|
478
|
+
sessionId: progress.sessionId,
|
|
479
|
+
type: "triage_session",
|
|
480
|
+
});
|
|
481
|
+
},
|
|
502
482
|
options: agent.options,
|
|
483
|
+
parentSessionId: input.input.parentSessionId,
|
|
503
484
|
parse: parseTriageCommentClassificationOutput,
|
|
504
485
|
permission: agent.permission,
|
|
505
486
|
prompt,
|
|
506
487
|
repairAttempts: 3,
|
|
507
488
|
schemaName: "triage comment classification",
|
|
508
489
|
signal: input.input.signal,
|
|
509
|
-
title: `Magi triage comment classification #${input.input.issue}`,
|
|
490
|
+
title: `Magi triage comment classification #${input.input.issue} (${agent.key})`,
|
|
510
491
|
});
|
|
511
492
|
await writeJson(join(input.outputDir, "comment-classification.json"), result.value);
|
|
512
493
|
return result.value;
|
|
@@ -523,54 +504,40 @@ async function runReconsiderationVote(input) {
|
|
|
523
504
|
votes: BINARY_VOTES,
|
|
524
505
|
});
|
|
525
506
|
}
|
|
526
|
-
|
|
527
|
-
const agents =
|
|
528
|
-
if (!agents
|
|
507
|
+
function triageReporter(repository, issue) {
|
|
508
|
+
const agents = repository.agents.triage ?? [];
|
|
509
|
+
if (!agents.length)
|
|
529
510
|
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
|
-
permission: agents[0].permission,
|
|
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;
|
|
511
|
+
const configured = repository.triage?.reporter;
|
|
512
|
+
const reporter = configured
|
|
513
|
+
? agents.find((agent) => agent.key === configured)
|
|
514
|
+
: agents[Math.abs(issue) % agents.length];
|
|
515
|
+
if (!reporter)
|
|
516
|
+
throw new Error(`Unknown triage reporter: ${configured}`);
|
|
517
|
+
return reporter;
|
|
518
|
+
}
|
|
519
|
+
function decisionCommentBody(input) {
|
|
520
|
+
const reason = input.reason?.trim();
|
|
521
|
+
const result = JSON.stringify(input.result);
|
|
522
|
+
return reason
|
|
523
|
+
? `Magi triage decision: ${result}\n\nReason: ${reason}`
|
|
524
|
+
: `Magi triage decision: ${result}\n\nAction: ${input.action}`;
|
|
525
|
+
}
|
|
526
|
+
function agentForKey(repository, key) {
|
|
527
|
+
const agent = repository.agents.triage?.find((item) => item.key === key);
|
|
528
|
+
if (!agent)
|
|
529
|
+
throw new Error(`Unknown triage agent: ${key}`);
|
|
530
|
+
return agent;
|
|
531
|
+
}
|
|
532
|
+
function askOutputs(outputs) {
|
|
533
|
+
return (outputs ?? []).filter((output) => output.vote === "ASK");
|
|
534
|
+
}
|
|
535
|
+
function chooseDecisionReason(input) {
|
|
536
|
+
return (input.outputs?.find((output) => output.voter === input.reporter.key &&
|
|
537
|
+
output.vote === input.vote &&
|
|
538
|
+
output.reason)?.reason ??
|
|
539
|
+
input.outputs?.find((output) => output.vote === input.vote)?.reason ??
|
|
540
|
+
input.outputs?.find((output) => output.voter === input.reporter.key)?.reason);
|
|
574
541
|
}
|
|
575
542
|
async function postMarkedIssueComment(input) {
|
|
576
543
|
const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
|
|
@@ -578,9 +545,20 @@ async function postMarkedIssueComment(input) {
|
|
|
578
545
|
const updated = body === input.body
|
|
579
546
|
? posted
|
|
580
547
|
: await updateIssueComment(input.exec, input.repository, posted.id, input.account, body);
|
|
581
|
-
await writeJson(join(input.outputDir,
|
|
548
|
+
await writeJson(join(input.outputDir, `posted-${updated.id}.json`), {
|
|
549
|
+
account: input.account,
|
|
550
|
+
...updated,
|
|
551
|
+
});
|
|
582
552
|
return updated;
|
|
583
553
|
}
|
|
554
|
+
async function postPlainIssueComment(input) {
|
|
555
|
+
const posted = await postIssueComment(input.exec, input.repository, input.issue, input.account, input.body);
|
|
556
|
+
await writeJson(join(input.outputDir, `posted-${posted.id}.json`), {
|
|
557
|
+
account: input.account,
|
|
558
|
+
...posted,
|
|
559
|
+
});
|
|
560
|
+
return posted;
|
|
561
|
+
}
|
|
584
562
|
async function persistProcessedMarker(input) {
|
|
585
563
|
if (!input.marker.commentId)
|
|
586
564
|
return;
|
|
@@ -604,6 +582,49 @@ async function persistProcessedMarker(input) {
|
|
|
604
582
|
updated,
|
|
605
583
|
});
|
|
606
584
|
}
|
|
585
|
+
async function postAskComments(input) {
|
|
586
|
+
const urls = [];
|
|
587
|
+
for (const output of askOutputs(input.outputs)) {
|
|
588
|
+
const agent = agentForKey(input.repository, output.voter);
|
|
589
|
+
const body = input.mark
|
|
590
|
+
? `${output.body}\n\n${marker({
|
|
591
|
+
action: input.action,
|
|
592
|
+
checkpoint: "pending",
|
|
593
|
+
decision: input.result,
|
|
594
|
+
issue: input.issue.number,
|
|
595
|
+
processed: input.processed,
|
|
596
|
+
})}`
|
|
597
|
+
: output.body;
|
|
598
|
+
if (!body?.trim())
|
|
599
|
+
continue;
|
|
600
|
+
await writeFile(join(input.outputDir, `${agent.key}.ask-comment.md`), `${body}\n`);
|
|
601
|
+
if (input.dryRun) {
|
|
602
|
+
urls.push(`dry-run:would-comment:${agent.key}`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
await emitProgress(input.run, { type: "comment_posting" });
|
|
606
|
+
const posted = input.mark
|
|
607
|
+
? await postMarkedIssueComment({
|
|
608
|
+
account: agent.account,
|
|
609
|
+
body,
|
|
610
|
+
exec: input.exec,
|
|
611
|
+
issue: input.issue.number,
|
|
612
|
+
outputDir: input.outputDir,
|
|
613
|
+
repository: input.repository,
|
|
614
|
+
})
|
|
615
|
+
: await postPlainIssueComment({
|
|
616
|
+
account: agent.account,
|
|
617
|
+
body,
|
|
618
|
+
exec: input.exec,
|
|
619
|
+
issue: input.issue.number,
|
|
620
|
+
outputDir: input.outputDir,
|
|
621
|
+
repository: input.repository,
|
|
622
|
+
});
|
|
623
|
+
urls.push(posted.url);
|
|
624
|
+
await emitProgress(input.run, { type: "comment_posted", url: posted.url });
|
|
625
|
+
}
|
|
626
|
+
return urls;
|
|
627
|
+
}
|
|
607
628
|
async function finishWithResult(input) {
|
|
608
629
|
const triage = input.input.repository.triage;
|
|
609
630
|
if (!triage)
|
|
@@ -614,30 +635,44 @@ async function finishWithResult(input) {
|
|
|
614
635
|
result: input.result,
|
|
615
636
|
type: "decision",
|
|
616
637
|
});
|
|
617
|
-
await runActionPrompt({
|
|
618
|
-
context: input.context,
|
|
619
|
-
input: input.input,
|
|
620
|
-
outputDir: input.outputDir,
|
|
621
|
-
plan,
|
|
622
|
-
result: input.result,
|
|
623
|
-
});
|
|
624
638
|
let prUrl;
|
|
625
|
-
const
|
|
626
|
-
|
|
639
|
+
const reporter = triageReporter(input.input.repository, input.issue.number);
|
|
640
|
+
const comment = plan.postComment && input.result.disposition !== "ask"
|
|
641
|
+
? `${decisionCommentBody({
|
|
642
|
+
action: plan.action,
|
|
643
|
+
reason: input.commentReason,
|
|
644
|
+
result: input.result,
|
|
645
|
+
})}\n\n${marker({
|
|
627
646
|
action: plan.action,
|
|
628
|
-
|
|
629
|
-
|
|
647
|
+
checkpoint: "pending",
|
|
648
|
+
decision: input.result,
|
|
649
|
+
issue: input.issue.number,
|
|
650
|
+
processed: input.processed,
|
|
651
|
+
})}`
|
|
652
|
+
: undefined;
|
|
653
|
+
if (comment) {
|
|
654
|
+
await writeFile(join(input.outputDir, "comment.md"), `${comment}\n`);
|
|
655
|
+
}
|
|
656
|
+
if (input.result.disposition === "ask" && input.askOutputs) {
|
|
657
|
+
await postAskComments({
|
|
658
|
+
action: plan.action,
|
|
659
|
+
dryRun: input.input.dryRun,
|
|
660
|
+
exec: input.input.exec,
|
|
630
661
|
issue: input.issue,
|
|
662
|
+
mark: input.markAskComments ?? false,
|
|
663
|
+
outputs: input.askOutputs,
|
|
631
664
|
outputDir: input.outputDir,
|
|
632
665
|
processed: input.processed,
|
|
666
|
+
repository: input.input.repository,
|
|
633
667
|
result: input.result,
|
|
634
|
-
|
|
635
|
-
|
|
668
|
+
run: input.input,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
636
671
|
if (!input.input.dryRun) {
|
|
637
672
|
if (comment) {
|
|
638
673
|
await emitProgress(input.input, { type: "comment_posting" });
|
|
639
674
|
const posted = await postMarkedIssueComment({
|
|
640
|
-
account:
|
|
675
|
+
account: reporter.account,
|
|
641
676
|
body: comment,
|
|
642
677
|
exec: input.input.exec,
|
|
643
678
|
issue: input.issue.number,
|
|
@@ -652,18 +687,18 @@ async function finishWithResult(input) {
|
|
|
652
687
|
if (plan.clearLabels) {
|
|
653
688
|
const clearLabels = existingClearLabels(input.issue, triage.automation.clear);
|
|
654
689
|
if (clearLabels.length) {
|
|
655
|
-
await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels,
|
|
690
|
+
await removeIssueLabels(input.input.exec, input.input.repository, input.issue.number, clearLabels, reporter.account);
|
|
656
691
|
}
|
|
657
692
|
}
|
|
658
693
|
if (plan.closeIssue) {
|
|
659
694
|
const closedPrs = [];
|
|
660
695
|
for (const pr of input.relationship.relatedPullRequests.filter((pr) => pr.state === "OPEN")) {
|
|
661
|
-
await closePullRequest(input.input.exec, input.input.repository, pr.number,
|
|
696
|
+
await closePullRequest(input.input.exec, input.input.repository, pr.number, reporter.account);
|
|
662
697
|
closedPrs.push(pr.number);
|
|
663
698
|
}
|
|
664
699
|
if (closedPrs.length)
|
|
665
700
|
await writeJson(join(input.outputDir, "closed-prs.json"), closedPrs);
|
|
666
|
-
await closeIssue(input.input.exec, input.input.repository, input.issue.number,
|
|
701
|
+
await closeIssue(input.input.exec, input.input.repository, input.issue.number, reporter.account);
|
|
667
702
|
}
|
|
668
703
|
if (plan.createPr) {
|
|
669
704
|
prUrl = await createImplementationPr({
|
|
@@ -671,6 +706,7 @@ async function finishWithResult(input) {
|
|
|
671
706
|
input: input.input,
|
|
672
707
|
issue: input.issue,
|
|
673
708
|
outputDir: input.outputDir,
|
|
709
|
+
runId: input.runId,
|
|
674
710
|
});
|
|
675
711
|
if (prUrl) {
|
|
676
712
|
await writeJson(join(input.outputDir, "pr.json"), { url: prUrl });
|
|
@@ -679,7 +715,7 @@ async function finishWithResult(input) {
|
|
|
679
715
|
}
|
|
680
716
|
if (input.previousMarker && prUrl) {
|
|
681
717
|
await persistProcessedMarker({
|
|
682
|
-
account:
|
|
718
|
+
account: input.previousMarker.account ?? reporter.account,
|
|
683
719
|
comments: input.relationship.comments,
|
|
684
720
|
exec: input.input.exec,
|
|
685
721
|
issue: input.issue,
|
|
@@ -734,15 +770,17 @@ async function createImplementationPr(input) {
|
|
|
734
770
|
const creator = input.input.repository.agents.triageCreator;
|
|
735
771
|
if (!creator)
|
|
736
772
|
return undefined;
|
|
737
|
-
const triage = input.input.repository.triage;
|
|
738
|
-
if (!triage?.account)
|
|
739
|
-
throw new Error("triage.account is required");
|
|
740
773
|
await emitProgress(input.input, { type: "pr_creation_started" });
|
|
741
774
|
await emitProgress(input.input, { type: "triage_creator_started" });
|
|
742
775
|
try {
|
|
743
|
-
await assignIssue(input.input.exec, input.input.repository, input.issue.number,
|
|
776
|
+
await assignIssue(input.input.exec, input.input.repository, input.issue.number, creator.account);
|
|
744
777
|
const branch = `magi/issue-${input.issue.number}-${Date.now().toString(36)}`;
|
|
745
|
-
const worktreePath =
|
|
778
|
+
const worktreePath = issueRunWorktreeDir({
|
|
779
|
+
config: input.input.config,
|
|
780
|
+
directory: input.input.directory,
|
|
781
|
+
issue: input.issue.number,
|
|
782
|
+
runId: input.runId,
|
|
783
|
+
});
|
|
746
784
|
await mkdir(dirname(worktreePath), { recursive: true });
|
|
747
785
|
await input.input.exec(`git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`);
|
|
748
786
|
await emitProgress(input.input, {
|
|
@@ -781,6 +819,7 @@ async function createImplementationPr(input) {
|
|
|
781
819
|
}
|
|
782
820
|
},
|
|
783
821
|
options: creator.options,
|
|
822
|
+
parentSessionId: input.input.parentSessionId,
|
|
784
823
|
parse: parseTriageCreatePrOutput,
|
|
785
824
|
permission: creator.permission,
|
|
786
825
|
prompt,
|
|
@@ -824,8 +863,8 @@ async function createImplementationPr(input) {
|
|
|
824
863
|
}
|
|
825
864
|
export async function runTriage(input) {
|
|
826
865
|
const triage = input.repository.triage;
|
|
827
|
-
if (!triage
|
|
828
|
-
throw new Error("triage
|
|
866
|
+
if (!triage)
|
|
867
|
+
throw new Error("triage configuration is required");
|
|
829
868
|
const agents = input.repository.agents.triage;
|
|
830
869
|
if (!agents?.length)
|
|
831
870
|
throw new Error("triage.agents is required");
|
|
@@ -865,6 +904,9 @@ export async function runTriage(input) {
|
|
|
865
904
|
await emitProgress(input, { phase: "triaging", type: "phase" });
|
|
866
905
|
let processed = relationship.previousMarker?.processed ?? [];
|
|
867
906
|
let result;
|
|
907
|
+
let askCommentOutputs;
|
|
908
|
+
let commentReason;
|
|
909
|
+
let markAskComments = false;
|
|
868
910
|
if (relationship.previousMarker) {
|
|
869
911
|
if (!relationship.mentionReplies.length) {
|
|
870
912
|
const result = finalResultFromMarker(relationship.previousMarker);
|
|
@@ -886,6 +928,7 @@ export async function runTriage(input) {
|
|
|
886
928
|
processed,
|
|
887
929
|
relationship,
|
|
888
930
|
result,
|
|
931
|
+
runId,
|
|
889
932
|
});
|
|
890
933
|
}
|
|
891
934
|
const report = `Magi triage skipped #${issue.number} because no eligible mention replies were found for reconsideration.`;
|
|
@@ -909,7 +952,7 @@ export async function runTriage(input) {
|
|
|
909
952
|
if (!triggeringComments.length) {
|
|
910
953
|
if (!input.dryRun) {
|
|
911
954
|
await persistProcessedMarker({
|
|
912
|
-
account:
|
|
955
|
+
account: relationship.previousMarker.account ?? "",
|
|
913
956
|
comments: relationship.comments,
|
|
914
957
|
exec: input.exec,
|
|
915
958
|
issue,
|
|
@@ -934,21 +977,38 @@ export async function runTriage(input) {
|
|
|
934
977
|
},
|
|
935
978
|
});
|
|
936
979
|
await writeFile(join(outputDir, "context.md"), `${context}\n`);
|
|
937
|
-
const vote = await runReconsiderationVote({ context, input, outputDir });
|
|
938
980
|
const previous = finalResultFromMarker(relationship.previousMarker);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
981
|
+
if (previous.disposition !== "ask" ||
|
|
982
|
+
previous.askReason !== "acceptance_unclear") {
|
|
983
|
+
const reconsideration = await runReconsiderationVote({
|
|
984
|
+
context,
|
|
985
|
+
input,
|
|
986
|
+
outputDir,
|
|
987
|
+
});
|
|
988
|
+
const reporter = triageReporter(input.repository, issue.number);
|
|
989
|
+
commentReason = chooseDecisionReason({
|
|
990
|
+
outputs: reconsideration.outputs,
|
|
991
|
+
reporter,
|
|
992
|
+
vote: reconsideration.vote ?? "ASK",
|
|
993
|
+
});
|
|
994
|
+
result =
|
|
995
|
+
reconsideration.vote === "YES"
|
|
996
|
+
? { category: previous.category, disposition: "accepted" }
|
|
997
|
+
: reconsideration.vote === "NO"
|
|
998
|
+
? { category: previous.category, disposition: "rejected" }
|
|
999
|
+
: {
|
|
1000
|
+
askReason: "acceptance_unclear",
|
|
1001
|
+
category: previous.category,
|
|
1002
|
+
disposition: "ask",
|
|
1003
|
+
};
|
|
1004
|
+
if (result.disposition === "ask") {
|
|
1005
|
+
askCommentOutputs = askOutputs(reconsideration.outputs);
|
|
1006
|
+
markAskComments = true;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
949
1009
|
}
|
|
950
1010
|
if (!result && relationship.relatedPullRequests.length) {
|
|
951
|
-
const
|
|
1011
|
+
const existingPr = await runPhaseVote({
|
|
952
1012
|
context,
|
|
953
1013
|
input,
|
|
954
1014
|
outputDir,
|
|
@@ -958,7 +1018,7 @@ export async function runTriage(input) {
|
|
|
958
1018
|
schemaName: "triage existing PR",
|
|
959
1019
|
votes: EXISTING_PR_VOTES,
|
|
960
1020
|
});
|
|
961
|
-
if (vote === "RELATED_PR_HANDLES_ISSUE") {
|
|
1021
|
+
if (existingPr.vote === "RELATED_PR_HANDLES_ISSUE") {
|
|
962
1022
|
const merged = relationship.relatedPullRequests.some((pr) => pr.state === "MERGED");
|
|
963
1023
|
if (merged && triage.automation.close) {
|
|
964
1024
|
const relatedPrDecision = {
|
|
@@ -973,59 +1033,22 @@ export async function runTriage(input) {
|
|
|
973
1033
|
createPr: false,
|
|
974
1034
|
postComment: true,
|
|
975
1035
|
};
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1036
|
+
return finishWithResult({
|
|
1037
|
+
commentReason: chooseDecisionReason({
|
|
1038
|
+
outputs: existingPr.outputs,
|
|
1039
|
+
reporter: triageReporter(input.repository, issue.number),
|
|
1040
|
+
vote: "RELATED_PR_HANDLES_ISSUE",
|
|
1041
|
+
}),
|
|
982
1042
|
context,
|
|
983
1043
|
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
1044
|
issue,
|
|
993
1045
|
outputDir,
|
|
1046
|
+
plan,
|
|
994
1047
|
processed,
|
|
1048
|
+
relationship,
|
|
995
1049
|
result: relatedPrDecision,
|
|
1050
|
+
runId,
|
|
996
1051
|
});
|
|
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
1052
|
}
|
|
1030
1053
|
return finishWithResult({
|
|
1031
1054
|
context,
|
|
@@ -1035,6 +1058,7 @@ export async function runTriage(input) {
|
|
|
1035
1058
|
processed,
|
|
1036
1059
|
relationship,
|
|
1037
1060
|
result: { category: null, disposition: "clear_only" },
|
|
1061
|
+
runId,
|
|
1038
1062
|
});
|
|
1039
1063
|
}
|
|
1040
1064
|
}
|
|
@@ -1047,6 +1071,7 @@ export async function runTriage(input) {
|
|
|
1047
1071
|
});
|
|
1048
1072
|
if (duplicate) {
|
|
1049
1073
|
context = `${context}\n\nDuplicate decision: ${JSON.stringify(duplicate)}`;
|
|
1074
|
+
commentReason = duplicate.reason;
|
|
1050
1075
|
result = { category: null, disposition: "duplicate" };
|
|
1051
1076
|
}
|
|
1052
1077
|
}
|
|
@@ -1056,8 +1081,9 @@ export async function runTriage(input) {
|
|
|
1056
1081
|
category: resolvedCategory,
|
|
1057
1082
|
source: resolvedCategory ? "config" : "vote",
|
|
1058
1083
|
});
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1084
|
+
const categoryVote = resolvedCategory
|
|
1085
|
+
? undefined
|
|
1086
|
+
: await runPhaseVote({
|
|
1061
1087
|
context,
|
|
1062
1088
|
input,
|
|
1063
1089
|
outputDir,
|
|
@@ -1066,14 +1092,16 @@ export async function runTriage(input) {
|
|
|
1066
1092
|
prompt: composeTriageCategoryPrompt,
|
|
1067
1093
|
schemaName: "triage category",
|
|
1068
1094
|
votes: ["ASK", ...triage.categories.map((item) => item.id)],
|
|
1069
|
-
})
|
|
1070
|
-
|
|
1095
|
+
});
|
|
1096
|
+
const category = resolvedCategory ?? categoryVote?.vote ?? "ASK";
|
|
1071
1097
|
if (category === "ASK") {
|
|
1072
1098
|
result = {
|
|
1073
1099
|
askReason: "category_unclear",
|
|
1074
1100
|
category: null,
|
|
1075
1101
|
disposition: "ask",
|
|
1076
1102
|
};
|
|
1103
|
+
askCommentOutputs = askOutputs(categoryVote?.outputs);
|
|
1104
|
+
markAskComments = false;
|
|
1077
1105
|
}
|
|
1078
1106
|
else {
|
|
1079
1107
|
const categoryConfig = triage.categories.find((item) => item.id === category);
|
|
@@ -1081,7 +1109,7 @@ export async function runTriage(input) {
|
|
|
1081
1109
|
category: categoryConfig,
|
|
1082
1110
|
triageContext: context,
|
|
1083
1111
|
}, null, 2);
|
|
1084
|
-
const
|
|
1112
|
+
const acceptance = await runPhaseVote({
|
|
1085
1113
|
context: voteContext,
|
|
1086
1114
|
input,
|
|
1087
1115
|
outputDir,
|
|
@@ -1091,22 +1119,35 @@ export async function runTriage(input) {
|
|
|
1091
1119
|
schemaName: "triage acceptance",
|
|
1092
1120
|
votes: BINARY_VOTES,
|
|
1093
1121
|
});
|
|
1122
|
+
const reporter = triageReporter(input.repository, issue.number);
|
|
1123
|
+
commentReason = chooseDecisionReason({
|
|
1124
|
+
outputs: acceptance.outputs,
|
|
1125
|
+
reporter,
|
|
1126
|
+
vote: acceptance.vote ?? "ASK",
|
|
1127
|
+
});
|
|
1094
1128
|
result =
|
|
1095
|
-
vote === "YES"
|
|
1129
|
+
acceptance.vote === "YES"
|
|
1096
1130
|
? { category, disposition: "accepted" }
|
|
1097
|
-
: vote === "NO"
|
|
1131
|
+
: acceptance.vote === "NO"
|
|
1098
1132
|
? { category, disposition: "rejected" }
|
|
1099
1133
|
: {
|
|
1100
1134
|
askReason: "acceptance_unclear",
|
|
1101
1135
|
category,
|
|
1102
1136
|
disposition: "ask",
|
|
1103
1137
|
};
|
|
1138
|
+
if (result.disposition === "ask") {
|
|
1139
|
+
askCommentOutputs = askOutputs(acceptance.outputs);
|
|
1140
|
+
markAskComments = true;
|
|
1141
|
+
}
|
|
1104
1142
|
}
|
|
1105
1143
|
}
|
|
1106
1144
|
return finishWithResult({
|
|
1145
|
+
askOutputs: askCommentOutputs,
|
|
1146
|
+
commentReason,
|
|
1107
1147
|
context,
|
|
1108
1148
|
input,
|
|
1109
1149
|
issue,
|
|
1150
|
+
markAskComments,
|
|
1110
1151
|
outputDir,
|
|
1111
1152
|
processed,
|
|
1112
1153
|
relationship,
|
|
@@ -1115,5 +1156,6 @@ export async function runTriage(input) {
|
|
|
1115
1156
|
category: null,
|
|
1116
1157
|
disposition: "ask",
|
|
1117
1158
|
},
|
|
1159
|
+
runId,
|
|
1118
1160
|
});
|
|
1119
1161
|
}
|