opencode-magi 0.0.0-dev-20260520165753 → 0.0.0-dev-20260520173258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/resolve.js +29 -10
- package/dist/config/validate.js +42 -17
- package/dist/github/commands.js +98 -6
- package/dist/index.js +133 -24
- package/dist/orchestrator/findings.js +4 -3
- package/dist/orchestrator/report.js +8 -1
- package/dist/orchestrator/review-context.js +309 -0
- package/dist/orchestrator/review.js +26 -5
- package/dist/orchestrator/run-manager.js +5 -3
- package/dist/orchestrator/triage.js +151 -88
- package/dist/prompts/compose.js +28 -16
- package/dist/prompts/contracts.js +28 -16
- package/dist/prompts/output.js +36 -24
- package/dist/prompts/templates/review/review.md +6 -0
- package/dist/prompts/templates/triage/acceptance.md +7 -0
- package/dist/prompts/templates/triage/category.md +10 -0
- package/package.json +1 -1
- package/schema.json +16 -15
- package/dist/prompts/templates/triage/bug.md +0 -7
- package/dist/prompts/templates/triage/feature.md +0 -7
- package/dist/prompts/templates/triage/kind.md +0 -7
package/dist/config/resolve.js
CHANGED
|
@@ -4,6 +4,26 @@ const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
4
4
|
const DEFAULT_COMMON_PERMISSION = commonPermission;
|
|
5
5
|
const DEFAULT_REVIEWER_PERMISSION = DEFAULT_COMMON_PERMISSION;
|
|
6
6
|
const DEFAULT_EDITOR_PERMISSION = mergePermissions(DEFAULT_COMMON_PERMISSION, editorPermission);
|
|
7
|
+
const DEFAULT_TRIAGE_CATEGORIES = [
|
|
8
|
+
{
|
|
9
|
+
description: "Something is broken or behaves incorrectly.",
|
|
10
|
+
id: "bug",
|
|
11
|
+
labels: ["bug"],
|
|
12
|
+
types: ["Bug"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
description: "Maintenance, refactoring, chores, or planned work.",
|
|
16
|
+
id: "task",
|
|
17
|
+
labels: ["task"],
|
|
18
|
+
types: ["Task"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
description: "New or improved user-facing capability.",
|
|
22
|
+
id: "feature",
|
|
23
|
+
labels: ["enhancement"],
|
|
24
|
+
types: ["Feature"],
|
|
25
|
+
},
|
|
26
|
+
];
|
|
7
27
|
export function reviewerKey(reviewer, index) {
|
|
8
28
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
9
29
|
}
|
|
@@ -90,6 +110,14 @@ export function resolveAgents(config) {
|
|
|
90
110
|
: undefined,
|
|
91
111
|
};
|
|
92
112
|
}
|
|
113
|
+
function resolveTriageCategories(config) {
|
|
114
|
+
return (config.triage?.categories ?? DEFAULT_TRIAGE_CATEGORIES).map((category) => ({
|
|
115
|
+
description: category.description,
|
|
116
|
+
id: category.id ?? "",
|
|
117
|
+
labels: category.labels ?? [],
|
|
118
|
+
types: category.types ?? [],
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
93
121
|
export function resolveRepository(config) {
|
|
94
122
|
if (!config.github?.owner)
|
|
95
123
|
throw new Error("github.owner is required");
|
|
@@ -157,19 +185,10 @@ export function resolveRepository(config) {
|
|
|
157
185
|
close: config.triage?.automation?.close ?? false,
|
|
158
186
|
pr: config.triage?.automation?.pr ?? false,
|
|
159
187
|
},
|
|
188
|
+
categories: resolveTriageCategories(config),
|
|
160
189
|
concurrency: {
|
|
161
190
|
runs: config.triage?.concurrency?.runs ?? 3,
|
|
162
191
|
},
|
|
163
|
-
kind: {
|
|
164
|
-
bug: {
|
|
165
|
-
label: config.triage?.kind?.bug?.label ?? ["bug"],
|
|
166
|
-
type: config.triage?.kind?.bug?.type ?? ["Bug"],
|
|
167
|
-
},
|
|
168
|
-
feature: {
|
|
169
|
-
label: config.triage?.kind?.feature?.label ?? ["enhancement"],
|
|
170
|
-
type: config.triage?.kind?.feature?.type ?? ["Feature"],
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
192
|
output: config.triage?.output,
|
|
174
193
|
prompts: config.triage?.prompts ?? {},
|
|
175
194
|
safety: {
|
package/dist/config/validate.js
CHANGED
|
@@ -9,6 +9,8 @@ const RESERVED_REVIEWER_KEYS = new Set(["editor", "orchestrator", "system"]);
|
|
|
9
9
|
const PERMISSION_ACTIONS = new Set(["allow", "ask", "deny"]);
|
|
10
10
|
const AJV = new Ajv2020({ allErrors: true, strict: false });
|
|
11
11
|
const validateSchema = AJV.compile(schema);
|
|
12
|
+
const TRIAGE_CATEGORY_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
const RESERVED_TRIAGE_CATEGORY_IDS = new Set(["ASK", "none"]);
|
|
12
14
|
const CONFIG_KEYS = new Set([
|
|
13
15
|
"$schema",
|
|
14
16
|
"agents",
|
|
@@ -76,9 +78,9 @@ const TRIAGE_KEYS = new Set([
|
|
|
76
78
|
"account",
|
|
77
79
|
"agents",
|
|
78
80
|
"automation",
|
|
81
|
+
"categories",
|
|
79
82
|
"concurrency",
|
|
80
83
|
"creator",
|
|
81
|
-
"kind",
|
|
82
84
|
"output",
|
|
83
85
|
"prompts",
|
|
84
86
|
"safety",
|
|
@@ -98,9 +100,8 @@ const CLEAR_KEYS = new Set(["branch", "output", "session", "worktree"]);
|
|
|
98
100
|
const CONCURRENCY_KEYS = new Set(["reviewers", "runs"]);
|
|
99
101
|
const OUTPUT_KEYS = new Set(["repairAttempts"]);
|
|
100
102
|
const TRIAGE_AUTOMATION_KEYS = new Set(["clear", "close", "pr"]);
|
|
103
|
+
const TRIAGE_CATEGORY_KEYS = new Set(["description", "id", "labels", "types"]);
|
|
101
104
|
const TRIAGE_CONCURRENCY_KEYS = new Set(["runs"]);
|
|
102
|
-
const TRIAGE_KIND_KEYS = new Set(["bug", "feature"]);
|
|
103
|
-
const TRIAGE_KIND_RULE_KEYS = new Set(["label", "type"]);
|
|
104
105
|
const TRIAGE_SAFETY_KEYS = new Set([
|
|
105
106
|
"allowAuthors",
|
|
106
107
|
"allowMentionActors",
|
|
@@ -129,14 +130,13 @@ const MERGE_PROMPT_KEYS = new Set([
|
|
|
129
130
|
]);
|
|
130
131
|
const TRIAGE_PROMPT_KEYS = new Set([
|
|
131
132
|
"action",
|
|
132
|
-
"
|
|
133
|
+
"acceptance",
|
|
134
|
+
"category",
|
|
133
135
|
"comment",
|
|
134
136
|
"commentClassification",
|
|
135
137
|
"createPr",
|
|
136
138
|
"duplicate",
|
|
137
139
|
"existingPr",
|
|
138
|
-
"feature",
|
|
139
|
-
"kind",
|
|
140
140
|
"question",
|
|
141
141
|
"reconsider",
|
|
142
142
|
]);
|
|
@@ -536,16 +536,44 @@ function validateStringArray(value, path, errors) {
|
|
|
536
536
|
errors.push(`${path}[${index}] must be a string`);
|
|
537
537
|
});
|
|
538
538
|
}
|
|
539
|
-
function
|
|
540
|
-
if (
|
|
539
|
+
function validateTriageCategories(categories, path, errors) {
|
|
540
|
+
if (categories == null)
|
|
541
541
|
return;
|
|
542
|
-
if (!
|
|
543
|
-
errors.push(`${path} must be an
|
|
542
|
+
if (!Array.isArray(categories)) {
|
|
543
|
+
errors.push(`${path} must be an array`);
|
|
544
544
|
return;
|
|
545
545
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
546
|
+
const ids = new Set();
|
|
547
|
+
categories.forEach((item, index) => {
|
|
548
|
+
const itemPath = `${path}[${index}]`;
|
|
549
|
+
if (!isPlainObject(item)) {
|
|
550
|
+
errors.push(`${itemPath} must be an object`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const category = item;
|
|
554
|
+
validateKnownKeys(category, itemPath, TRIAGE_CATEGORY_KEYS, errors);
|
|
555
|
+
if (!category.id) {
|
|
556
|
+
errors.push(`${itemPath}.id is required`);
|
|
557
|
+
}
|
|
558
|
+
else if (typeof category.id !== "string") {
|
|
559
|
+
errors.push(`${itemPath}.id must be a string`);
|
|
560
|
+
}
|
|
561
|
+
else if (!TRIAGE_CATEGORY_ID_PATTERN.test(category.id)) {
|
|
562
|
+
errors.push(`${itemPath}.id must match /^[A-Za-z0-9_-]+$/`);
|
|
563
|
+
}
|
|
564
|
+
else if (RESERVED_TRIAGE_CATEGORY_IDS.has(category.id)) {
|
|
565
|
+
errors.push(`${itemPath}.id is reserved: ${category.id}`);
|
|
566
|
+
}
|
|
567
|
+
else if (ids.has(category.id)) {
|
|
568
|
+
errors.push(`${itemPath}.id must be unique`);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
ids.add(category.id);
|
|
572
|
+
}
|
|
573
|
+
validateStringArray(category.labels, `${itemPath}.labels`, errors);
|
|
574
|
+
validateStringArray(category.types, `${itemPath}.types`, errors);
|
|
575
|
+
validateString(category.description, `${itemPath}.description`, errors);
|
|
576
|
+
});
|
|
549
577
|
}
|
|
550
578
|
function validateSafety(config, errors) {
|
|
551
579
|
const safety = config.review?.safety;
|
|
@@ -587,7 +615,6 @@ function validateTriage(config, errors, options) {
|
|
|
587
615
|
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
588
616
|
const automation = triage.automation;
|
|
589
617
|
const concurrency = triage.concurrency;
|
|
590
|
-
const kind = triage.kind;
|
|
591
618
|
const safety = triage.safety;
|
|
592
619
|
if (!triage.account)
|
|
593
620
|
errors.push("triage.account is required");
|
|
@@ -615,9 +642,7 @@ function validateTriage(config, errors, options) {
|
|
|
615
642
|
concurrency.runs < 1)) {
|
|
616
643
|
errors.push("triage.concurrency.runs must be a positive integer");
|
|
617
644
|
}
|
|
618
|
-
|
|
619
|
-
validateStringArrayObject(kind?.bug, "triage.kind.bug", TRIAGE_KIND_RULE_KEYS, errors);
|
|
620
|
-
validateStringArrayObject(kind?.feature, "triage.kind.feature", TRIAGE_KIND_RULE_KEYS, errors);
|
|
645
|
+
validateTriageCategories(triage.categories, "triage.categories", errors);
|
|
621
646
|
validateKnownKeys(safety, "triage.safety", TRIAGE_SAFETY_KEYS, errors);
|
|
622
647
|
validateStringArray(safety?.allowAuthors, "triage.safety.allowAuthors", errors);
|
|
623
648
|
validateStringArray(safety?.allowMentionActors, "triage.safety.allowMentionActors", errors);
|
package/dist/github/commands.js
CHANGED
|
@@ -102,9 +102,24 @@ async function fetchPullRequestQueueInput(exec, repository, pr, token) {
|
|
|
102
102
|
return { headRefOid: pullRequest.headRefOid, id: pullRequest.id };
|
|
103
103
|
}
|
|
104
104
|
export async function fetchPullRequest(exec, repository, pr) {
|
|
105
|
-
const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,url,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner`);
|
|
105
|
+
const json = await exec(`gh pr view ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,body,url,state,author,isDraft,baseRefOid,headRefOid,baseRefName,headRefName,headRepository,headRepositoryOwner,changedFiles`);
|
|
106
106
|
return JSON.parse(json);
|
|
107
107
|
}
|
|
108
|
+
export async function fetchPullRequestClosingIssues(exec, repository, pr) {
|
|
109
|
+
const query = `query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { closingIssuesReferences(first: 20) { nodes { number title body url state author { login } labels(first: 100) { nodes { name } } issueType { name } } } } } }`;
|
|
110
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}`);
|
|
111
|
+
const data = JSON.parse(raw);
|
|
112
|
+
return (data.data?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map((issue) => ({
|
|
113
|
+
author: issue.author?.login ?? "",
|
|
114
|
+
body: issue.body ?? "",
|
|
115
|
+
labels: issue.labels?.nodes?.map((label) => label.name) ?? [],
|
|
116
|
+
number: issue.number,
|
|
117
|
+
state: issue.state,
|
|
118
|
+
title: issue.title,
|
|
119
|
+
type: issue.issueType?.name,
|
|
120
|
+
url: issue.url,
|
|
121
|
+
})) ?? []);
|
|
122
|
+
}
|
|
108
123
|
export async function fetchIssue(exec, repository, issue) {
|
|
109
124
|
const query = `query($owner: String!, $repo: String!, $issue: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { number title body url state author { login } labels(first: 100) { nodes { name } } issueType { name } } } }`;
|
|
110
125
|
try {
|
|
@@ -142,17 +157,47 @@ async function fetchIssueWithCli(exec, repository, issue) {
|
|
|
142
157
|
};
|
|
143
158
|
}
|
|
144
159
|
export async function fetchIssueComments(exec, repository, issue, limit = 50) {
|
|
145
|
-
|
|
160
|
+
return (await fetchIssueCommentPage(exec, repository, issue, limit)).comments;
|
|
161
|
+
}
|
|
162
|
+
export async function fetchIssueCommentPage(exec, repository, issue, limit = 50) {
|
|
163
|
+
const query = `query($owner: String!, $repo: String!, $issue: Int!, $limit: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { comments(last: $limit) { totalCount nodes { databaseId author { login } authorAssociation body createdAt url } } } } }`;
|
|
146
164
|
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F issue=${issue} -F limit=${limit}`);
|
|
147
165
|
const data = JSON.parse(raw);
|
|
148
|
-
|
|
166
|
+
const connection = data.data?.repository?.issue?.comments;
|
|
167
|
+
const comments = connection?.nodes?.map((comment) => ({
|
|
149
168
|
author: comment.author?.login ?? "",
|
|
150
169
|
authorAssociation: comment.authorAssociation,
|
|
151
170
|
body: comment.body ?? "",
|
|
152
171
|
createdAt: comment.createdAt,
|
|
153
172
|
id: comment.databaseId,
|
|
154
173
|
url: comment.url,
|
|
155
|
-
})) ?? []
|
|
174
|
+
})) ?? [];
|
|
175
|
+
return {
|
|
176
|
+
comments,
|
|
177
|
+
omitted: Math.max(0, (connection?.totalCount ?? comments.length) - comments.length),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function fetchPullRequestComments(exec, repository, pr, limit = 50) {
|
|
181
|
+
return (await fetchPullRequestCommentPage(exec, repository, pr, limit))
|
|
182
|
+
.comments;
|
|
183
|
+
}
|
|
184
|
+
export async function fetchPullRequestCommentPage(exec, repository, pr, limit = 50) {
|
|
185
|
+
const query = `query($owner: String!, $repo: String!, $pr: Int!, $limit: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { comments(last: $limit) { totalCount nodes { databaseId author { login } authorAssociation body createdAt url } } } } }`;
|
|
186
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr} -F limit=${limit}`);
|
|
187
|
+
const data = JSON.parse(raw);
|
|
188
|
+
const connection = data.data?.repository?.pullRequest?.comments;
|
|
189
|
+
const comments = connection?.nodes?.map((comment) => ({
|
|
190
|
+
author: comment.author?.login ?? "",
|
|
191
|
+
authorAssociation: comment.authorAssociation,
|
|
192
|
+
body: comment.body ?? "",
|
|
193
|
+
createdAt: comment.createdAt,
|
|
194
|
+
id: comment.databaseId,
|
|
195
|
+
url: comment.url,
|
|
196
|
+
})) ?? [];
|
|
197
|
+
return {
|
|
198
|
+
comments,
|
|
199
|
+
omitted: Math.max(0, (connection?.totalCount ?? comments.length) - comments.length),
|
|
200
|
+
};
|
|
156
201
|
}
|
|
157
202
|
export async function fetchRelatedPullRequests(exec, repository, issue) {
|
|
158
203
|
const query = `query($owner: String!, $repo: String!, $issue: Int!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { timelineItems(first: 50, itemTypes: [CONNECTED_EVENT, CROSS_REFERENCED_EVENT]) { nodes { __typename ... on ConnectedEvent { subject { __typename ... on PullRequest { number title url state mergedAt body author { login } } } } ... on CrossReferencedEvent { source { __typename ... on PullRequest { number title url state mergedAt body author { login } } } } } } } } }`;
|
|
@@ -295,7 +340,7 @@ export async function fetchPullRequestCommits(exec, repository, pr) {
|
|
|
295
340
|
}));
|
|
296
341
|
}
|
|
297
342
|
export async function fetchPullRequestSafetyMeta(exec, repository, pr) {
|
|
298
|
-
const query = `query($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { author { login } changedFiles labels(first: 100) { nodes { name } } files(first: 100, after: $filesCursor) { nodes { path } pageInfo { hasNextPage endCursor } } } } }
|
|
343
|
+
const query = `query($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { author { login } changedFiles labels(first: 100) { nodes { name } } files(first: 100, after: $filesCursor) { nodes { path } pageInfo { hasNextPage endCursor } } } } }`;
|
|
299
344
|
const files = [];
|
|
300
345
|
let author = "";
|
|
301
346
|
let changedFiles = 0;
|
|
@@ -467,11 +512,19 @@ function findingComment(finding) {
|
|
|
467
512
|
}
|
|
468
513
|
return comment;
|
|
469
514
|
}
|
|
470
|
-
|
|
515
|
+
function requirementFindingSummary(finding) {
|
|
516
|
+
return [
|
|
517
|
+
`- Missing issue #${finding.issueNumber} requirement: ${finding.requirement}`,
|
|
518
|
+
` Evidence: ${finding.evidence}`,
|
|
519
|
+
` Fix: ${finding.fix}`,
|
|
520
|
+
].join("\n");
|
|
521
|
+
}
|
|
522
|
+
export async function postChangesRequested(exec, repository, pr, account, findings, requirementFindings = []) {
|
|
471
523
|
const token = await ghToken(exec, repository, account);
|
|
472
524
|
const payloadPath = join(tmpdir(), `magi-review-${process.pid}-${Date.now()}.json`);
|
|
473
525
|
const body = findings
|
|
474
526
|
.map((finding) => `- ${finding.issue.split("\n")[0]}`)
|
|
527
|
+
.concat(requirementFindings.map(requirementFindingSummary))
|
|
475
528
|
.join("\n");
|
|
476
529
|
await writeFile(payloadPath, JSON.stringify({
|
|
477
530
|
body,
|
|
@@ -610,6 +663,45 @@ export async function fetchUnresolvedThreads(exec, repository, pr, author) {
|
|
|
610
663
|
];
|
|
611
664
|
});
|
|
612
665
|
}
|
|
666
|
+
export async function fetchPullRequestReviewThreads(exec, repository, pr, threadLimit = 50, commentsPerThread = 20) {
|
|
667
|
+
return (await fetchPullRequestReviewThreadPage(exec, repository, pr, threadLimit, commentsPerThread)).threads;
|
|
668
|
+
}
|
|
669
|
+
export async function fetchPullRequestReviewThreadPage(exec, repository, pr, threadLimit = 50, commentsPerThread = 20) {
|
|
670
|
+
const query = `query($owner: String!, $repo: String!, $pr: Int!, $threadLimit: Int!, $commentsPerThread: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(last: $threadLimit) { totalCount nodes { id isResolved comments(last: $commentsPerThread) { totalCount nodes { databaseId author { login } path line body createdAt } } } } } } }`;
|
|
671
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(query)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr} -F threadLimit=${threadLimit} -F commentsPerThread=${commentsPerThread}`);
|
|
672
|
+
const data = JSON.parse(raw);
|
|
673
|
+
const connection = data.data?.repository?.pullRequest?.reviewThreads;
|
|
674
|
+
const nodes = connection?.nodes ?? [];
|
|
675
|
+
const threads = nodes.flatMap((thread) => {
|
|
676
|
+
const comments = [...thread.comments.nodes]
|
|
677
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
|
678
|
+
.map((comment) => ({
|
|
679
|
+
author: comment.author?.login ?? "",
|
|
680
|
+
body: comment.body ?? "",
|
|
681
|
+
commentId: comment.databaseId,
|
|
682
|
+
createdAt: comment.createdAt,
|
|
683
|
+
}));
|
|
684
|
+
const first = thread.comments.nodes[0];
|
|
685
|
+
if (!first)
|
|
686
|
+
return [];
|
|
687
|
+
return [
|
|
688
|
+
{
|
|
689
|
+
body: first.body ?? "",
|
|
690
|
+
commentId: first.databaseId,
|
|
691
|
+
comments,
|
|
692
|
+
isResolved: thread.isResolved,
|
|
693
|
+
line: first.line,
|
|
694
|
+
omittedComments: Math.max(0, (thread.comments.totalCount ?? comments.length) - comments.length),
|
|
695
|
+
path: first.path,
|
|
696
|
+
threadId: thread.id,
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
omitted: Math.max(0, (connection?.totalCount ?? threads.length) - threads.length),
|
|
702
|
+
threads,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
613
705
|
export async function postReply(exec, repository, pr, account, commentId, body) {
|
|
614
706
|
const token = await ghToken(exec, repository, account);
|
|
615
707
|
const payloadPath = join(tmpdir(), `magi-reply-${process.pid}-${Date.now()}-${commentId}.json`);
|
package/dist/index.js
CHANGED
|
@@ -91,27 +91,133 @@ export function parseIssues(value) {
|
|
|
91
91
|
throw new Error("Specify one or more issue numbers or issue URLs.");
|
|
92
92
|
return issues;
|
|
93
93
|
}
|
|
94
|
-
export function parseRunArguments(value, dryRun = false) {
|
|
94
|
+
export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
95
95
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
96
|
-
const
|
|
96
|
+
const configOverrides = {};
|
|
97
|
+
const prTokens = [];
|
|
98
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
99
|
+
const token = tokens[index];
|
|
97
100
|
if (token === "--dry-run") {
|
|
98
101
|
dryRun = true;
|
|
99
|
-
|
|
102
|
+
continue;
|
|
100
103
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
switch (token) {
|
|
105
|
+
case "--language":
|
|
106
|
+
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
107
|
+
break;
|
|
108
|
+
case "--merge":
|
|
109
|
+
case "--no-merge":
|
|
110
|
+
setConfigOverride(configOverrides, [command, "automation", "merge"], token === "--merge");
|
|
111
|
+
break;
|
|
112
|
+
case "--close":
|
|
113
|
+
case "--no-close":
|
|
114
|
+
setConfigOverride(configOverrides, [command, "automation", "close"], token === "--close");
|
|
115
|
+
break;
|
|
116
|
+
case "--max-cycles":
|
|
117
|
+
if (command !== "merge")
|
|
118
|
+
throw unsupportedFlag(token, command);
|
|
119
|
+
setConfigOverride(configOverrides, ["merge", "maxThreadResolutionCycles"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
|
|
120
|
+
break;
|
|
121
|
+
case "--retry-failed-jobs":
|
|
122
|
+
setConfigOverride(configOverrides, ["review", "checks", "retryFailedJobs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0));
|
|
123
|
+
break;
|
|
124
|
+
case "--reviewer-concurrency":
|
|
125
|
+
setConfigOverride(configOverrides, ["review", "concurrency", "reviewers"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
|
|
126
|
+
break;
|
|
127
|
+
case "--run-concurrency":
|
|
128
|
+
setConfigOverride(configOverrides, ["review", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
|
|
129
|
+
break;
|
|
130
|
+
case "--wait-checks":
|
|
131
|
+
case "--no-wait-checks":
|
|
132
|
+
setConfigOverride(configOverrides, ["review", "checks", "wait"], token === "--wait-checks");
|
|
133
|
+
break;
|
|
134
|
+
case "--wait-checks-after-edit":
|
|
135
|
+
case "--no-wait-checks-after-edit":
|
|
136
|
+
if (command !== "merge")
|
|
137
|
+
throw unsupportedFlag(token, command);
|
|
138
|
+
setConfigOverride(configOverrides, ["merge", "checks", "wait"], token === "--wait-checks-after-edit");
|
|
139
|
+
break;
|
|
140
|
+
case "--pr":
|
|
141
|
+
case "--no-pr":
|
|
142
|
+
throw unsupportedFlag(token, command);
|
|
143
|
+
default:
|
|
144
|
+
if (token.startsWith("--"))
|
|
145
|
+
throw unsupportedFlag(token, command);
|
|
146
|
+
prTokens.push(token);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { configOverrides, dryRun, prs: parsePrs(prTokens.join(" ")) };
|
|
104
150
|
}
|
|
105
151
|
export function parseIssueRunArguments(value, dryRun = false) {
|
|
106
152
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
107
|
-
const
|
|
153
|
+
const configOverrides = {};
|
|
154
|
+
const issueTokens = [];
|
|
155
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
156
|
+
const token = tokens[index];
|
|
108
157
|
if (token === "--dry-run") {
|
|
109
158
|
dryRun = true;
|
|
110
|
-
|
|
159
|
+
continue;
|
|
111
160
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
161
|
+
switch (token) {
|
|
162
|
+
case "--language":
|
|
163
|
+
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
164
|
+
break;
|
|
165
|
+
case "--close":
|
|
166
|
+
case "--no-close":
|
|
167
|
+
setConfigOverride(configOverrides, ["triage", "automation", "close"], token === "--close");
|
|
168
|
+
break;
|
|
169
|
+
case "--pr":
|
|
170
|
+
case "--no-pr":
|
|
171
|
+
setConfigOverride(configOverrides, ["triage", "automation", "pr"], token === "--pr");
|
|
172
|
+
break;
|
|
173
|
+
case "--run-concurrency":
|
|
174
|
+
setConfigOverride(configOverrides, ["triage", "concurrency", "runs"], parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 1));
|
|
175
|
+
break;
|
|
176
|
+
case "--merge":
|
|
177
|
+
case "--no-merge":
|
|
178
|
+
case "--max-cycles":
|
|
179
|
+
case "--retry-failed-jobs":
|
|
180
|
+
case "--reviewer-concurrency":
|
|
181
|
+
case "--wait-checks":
|
|
182
|
+
case "--no-wait-checks":
|
|
183
|
+
case "--wait-checks-after-edit":
|
|
184
|
+
case "--no-wait-checks-after-edit":
|
|
185
|
+
throw unsupportedFlag(token, "triage");
|
|
186
|
+
default:
|
|
187
|
+
if (token.startsWith("--"))
|
|
188
|
+
throw unsupportedFlag(token, "triage");
|
|
189
|
+
issueTokens.push(token);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { configOverrides, dryRun, issues: parseIssues(issueTokens.join(" ")) };
|
|
193
|
+
}
|
|
194
|
+
function nextFlagValue(tokens, index, flag) {
|
|
195
|
+
const value = tokens[index];
|
|
196
|
+
if (!value || value.startsWith("--"))
|
|
197
|
+
throw new Error(`${flag} requires a value.`);
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
function parseIntegerFlag(value, flag, minimum) {
|
|
201
|
+
const parsed = Number.parseInt(value, 10);
|
|
202
|
+
if (!Number.isInteger(parsed) ||
|
|
203
|
+
String(parsed) !== value ||
|
|
204
|
+
parsed < minimum) {
|
|
205
|
+
throw new Error(`${flag} must be an integer greater than or equal to ${minimum}.`);
|
|
206
|
+
}
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
209
|
+
function setConfigOverride(target, path, value) {
|
|
210
|
+
let current = target;
|
|
211
|
+
for (const key of path.slice(0, -1)) {
|
|
212
|
+
const existing = current[key];
|
|
213
|
+
const next = isPlainObject(existing) ? existing : {};
|
|
214
|
+
current[key] = next;
|
|
215
|
+
current = next;
|
|
216
|
+
}
|
|
217
|
+
current[path[path.length - 1]] = value;
|
|
218
|
+
}
|
|
219
|
+
function unsupportedFlag(flag, command) {
|
|
220
|
+
return new Error(`${flag} is not supported for /magi:${command}.`);
|
|
115
221
|
}
|
|
116
222
|
function parseOptionalPr(value) {
|
|
117
223
|
if (!value?.trim())
|
|
@@ -322,10 +428,11 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
322
428
|
dryRun: tool.schema.boolean().optional(),
|
|
323
429
|
},
|
|
324
430
|
async execute(args, context) {
|
|
325
|
-
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
431
|
+
const parsed = parseRunArguments(args.prs, args.dryRun ?? false, "merge");
|
|
326
432
|
const loaded = await loadConfig(directory);
|
|
327
|
-
const
|
|
328
|
-
const
|
|
433
|
+
const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
|
|
434
|
+
const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
|
|
435
|
+
const validation = await validateConfig(config, {
|
|
329
436
|
checkAuth: true,
|
|
330
437
|
directory,
|
|
331
438
|
exec: retryingExec,
|
|
@@ -334,9 +441,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
334
441
|
});
|
|
335
442
|
if (!validation.ok)
|
|
336
443
|
return JSON.stringify(validation, null, 2);
|
|
337
|
-
const repository = resolveRepository(
|
|
444
|
+
const repository = resolveRepository(config);
|
|
338
445
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startMerge({
|
|
339
|
-
config
|
|
446
|
+
config,
|
|
340
447
|
dryRun: parsed.dryRun,
|
|
341
448
|
repository,
|
|
342
449
|
pr,
|
|
@@ -360,8 +467,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
360
467
|
async execute(args, context) {
|
|
361
468
|
const parsed = parseRunArguments(args.prs, args.dryRun ?? false);
|
|
362
469
|
const loaded = await loadConfig(directory);
|
|
363
|
-
const
|
|
364
|
-
const
|
|
470
|
+
const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
|
|
471
|
+
const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
|
|
472
|
+
const validation = await validateConfig(config, {
|
|
365
473
|
checkAuth: true,
|
|
366
474
|
directory,
|
|
367
475
|
exec: retryingExec,
|
|
@@ -369,9 +477,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
369
477
|
});
|
|
370
478
|
if (!validation.ok)
|
|
371
479
|
return JSON.stringify(validation, null, 2);
|
|
372
|
-
const repository = resolveRepository(
|
|
480
|
+
const repository = resolveRepository(config);
|
|
373
481
|
const states = await mapPool(parsed.prs, repository.concurrency.runs, (pr) => runManager.startReview({
|
|
374
|
-
config
|
|
482
|
+
config,
|
|
375
483
|
dryRun: parsed.dryRun,
|
|
376
484
|
repository,
|
|
377
485
|
pr,
|
|
@@ -392,8 +500,9 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
392
500
|
async execute(args, context) {
|
|
393
501
|
const parsed = parseIssueRunArguments(args.issues, args.dryRun ?? false);
|
|
394
502
|
const loaded = await loadConfig(directory);
|
|
395
|
-
const
|
|
396
|
-
const
|
|
503
|
+
const config = mergeMagiConfig(loaded.config, parsed.configOverrides);
|
|
504
|
+
const retryingExec = withGitHubApiRetry(exec, config.github?.apiRetryAttempts ?? 3);
|
|
505
|
+
const validation = await validateConfig(config, {
|
|
397
506
|
checkAuth: true,
|
|
398
507
|
directory,
|
|
399
508
|
exec: retryingExec,
|
|
@@ -403,11 +512,11 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
403
512
|
});
|
|
404
513
|
if (!validation.ok)
|
|
405
514
|
return JSON.stringify(validation, null, 2);
|
|
406
|
-
const repository = resolveRepository(
|
|
515
|
+
const repository = resolveRepository(config);
|
|
407
516
|
if (!repository.triage)
|
|
408
517
|
return JSON.stringify({ errors: ["triage configuration is required"], ok: false }, null, 2);
|
|
409
518
|
const states = await mapPool(parsed.issues, repository.triage.concurrency.runs, (issue) => runManager.startTriage({
|
|
410
|
-
config
|
|
519
|
+
config,
|
|
411
520
|
dryRun: parsed.dryRun,
|
|
412
521
|
issue,
|
|
413
522
|
parentSessionId: context.sessionID,
|
|
@@ -58,9 +58,10 @@ export function applyFindingValidation(input) {
|
|
|
58
58
|
discarded.push(target);
|
|
59
59
|
return false;
|
|
60
60
|
});
|
|
61
|
-
next[reviewer] =
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
next[reviewer] =
|
|
62
|
+
findings.length || output.requirementFindings.length
|
|
63
|
+
? { ...output, findings }
|
|
64
|
+
: { findings: [], requirementFindings: [], verdict: "MERGE" };
|
|
64
65
|
}
|
|
65
66
|
return { outputs: next, summary: { discarded, kept } };
|
|
66
67
|
}
|
|
@@ -27,6 +27,9 @@ function formatRereviewFinding(finding) {
|
|
|
27
27
|
: `${finding.path}:${finding.startLine}-${finding.line}`;
|
|
28
28
|
return `\`${line}\`: ${finding.body}`;
|
|
29
29
|
}
|
|
30
|
+
function formatRequirementFinding(finding) {
|
|
31
|
+
return `Issue #${finding.issueNumber}: ${finding.requirement}`;
|
|
32
|
+
}
|
|
30
33
|
function isReviewOutput(output) {
|
|
31
34
|
return "findings" in output;
|
|
32
35
|
}
|
|
@@ -78,7 +81,10 @@ function reviewerDetailLines(output) {
|
|
|
78
81
|
return output.reason ? [output.reason] : [];
|
|
79
82
|
if (output.verdict !== "CHANGES_REQUESTED")
|
|
80
83
|
return [];
|
|
81
|
-
return
|
|
84
|
+
return [
|
|
85
|
+
...output.findings.map(formatFinding),
|
|
86
|
+
...output.requirementFindings.map(formatRequirementFinding),
|
|
87
|
+
];
|
|
82
88
|
}
|
|
83
89
|
if (output.verdict === "CLOSE")
|
|
84
90
|
return output.reason ? [output.reason] : [];
|
|
@@ -86,6 +92,7 @@ function reviewerDetailLines(output) {
|
|
|
86
92
|
return [];
|
|
87
93
|
return [
|
|
88
94
|
...output.newFindings.map(formatRereviewFinding),
|
|
95
|
+
...output.requirementFindings.map(formatRequirementFinding),
|
|
89
96
|
...output.followUps.map((item) => `Comment #${item.commentId}: ${item.body}`),
|
|
90
97
|
];
|
|
91
98
|
}
|