opencode-magi 0.5.0 → 0.6.1
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 +3 -3
- package/dist/config/validate.js +31 -20
- package/dist/config/worktree.js +6 -0
- package/dist/github/commands.js +197 -65
- package/dist/index.js +29 -3
- package/dist/orchestrator/ci.js +32 -21
- package/dist/orchestrator/findings.js +29 -7
- package/dist/orchestrator/majority.js +1 -1
- package/dist/orchestrator/merge.js +31 -10
- package/dist/orchestrator/model.js +23 -9
- package/dist/orchestrator/review.js +162 -36
- package/dist/orchestrator/run-manager.js +171 -146
- package/dist/orchestrator/triage.js +246 -204
- package/dist/prompts/compose.js +2 -42
- package/dist/prompts/contracts.js +6 -20
- package/dist/prompts/output.js +6 -16
- package/dist/prompts/templates/triage/existing-pr.md +1 -1
- package/package.json +1 -1
- package/schema.json +3 -5
- package/dist/prompts/templates/triage/action.md +0 -5
- package/dist/prompts/templates/triage/comment.md +0 -5
- package/dist/prompts/templates/triage/question.md +0 -5
package/dist/config/resolve.js
CHANGED
|
@@ -28,7 +28,7 @@ export function reviewerKey(reviewer, index) {
|
|
|
28
28
|
return reviewer.id ?? `reviewer-${index + 1}`;
|
|
29
29
|
}
|
|
30
30
|
export function triageAgentKey(agent, index) {
|
|
31
|
-
return agent.id ?? `
|
|
31
|
+
return agent.id ?? `voter-${index + 1}`;
|
|
32
32
|
}
|
|
33
33
|
export function validateReviewerId(id) {
|
|
34
34
|
return ID_PATTERN.test(id);
|
|
@@ -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
|
]);
|
|
@@ -135,16 +136,13 @@ const MERGE_PROMPT_KEYS = new Set([
|
|
|
135
136
|
"editGuidelines",
|
|
136
137
|
]);
|
|
137
138
|
const TRIAGE_PROMPT_KEYS = new Set([
|
|
138
|
-
"action",
|
|
139
139
|
"acceptance",
|
|
140
140
|
"category",
|
|
141
|
-
"comment",
|
|
142
141
|
"commentClassification",
|
|
143
142
|
"create",
|
|
144
143
|
"createGuidelines",
|
|
145
144
|
"duplicate",
|
|
146
145
|
"existingPr",
|
|
147
|
-
"question",
|
|
148
146
|
"reconsider",
|
|
149
147
|
]);
|
|
150
148
|
function githubHost(config) {
|
|
@@ -357,6 +355,9 @@ function validateTriageAgentList(agents, path, errors, catalog) {
|
|
|
357
355
|
errors.push(`${path}[${index}].model is required`);
|
|
358
356
|
validateString(agent.model, `${path}[${index}].model`, errors);
|
|
359
357
|
validateModel(agent.model, `${path}[${index}].model`, errors, catalog);
|
|
358
|
+
if (!agent.account)
|
|
359
|
+
errors.push(`${path}[${index}].account is required`);
|
|
360
|
+
validateString(agent.account, `${path}[${index}].account`, errors);
|
|
360
361
|
validateString(agent.persona, `${path}[${index}].persona`, errors);
|
|
361
362
|
if (agent.options != null && !isPlainObject(agent.options))
|
|
362
363
|
errors.push(`${path}[${index}].options must be an object`);
|
|
@@ -383,12 +384,16 @@ function validateResolvedReviewers(reviewers, path, errors) {
|
|
|
383
384
|
accounts.add(reviewer.account);
|
|
384
385
|
}
|
|
385
386
|
}
|
|
386
|
-
function
|
|
387
|
+
function validateResolvedTriageAgents(agents, path, errors) {
|
|
387
388
|
const keys = new Set();
|
|
389
|
+
const accounts = new Set();
|
|
388
390
|
for (const agent of agents) {
|
|
389
391
|
if (keys.has(agent.key))
|
|
390
392
|
errors.push(`${path} has duplicate agent key: ${agent.key}`);
|
|
391
393
|
keys.add(agent.key);
|
|
394
|
+
if (accounts.has(agent.account))
|
|
395
|
+
errors.push(`${path} has duplicate agent account: ${agent.account}`);
|
|
396
|
+
accounts.add(agent.account);
|
|
392
397
|
}
|
|
393
398
|
}
|
|
394
399
|
function validateEditor(editor, path, errors, catalog) {
|
|
@@ -672,19 +677,26 @@ function validateTriage(config, errors, options) {
|
|
|
672
677
|
validateKnownKeys(triage, "triage", TRIAGE_KEYS, errors);
|
|
673
678
|
const automation = triage.automation;
|
|
674
679
|
const concurrency = triage.concurrency;
|
|
680
|
+
const creator = triage.creator;
|
|
681
|
+
const reporter = typeof triage.reporter === "string" ? triage.reporter : undefined;
|
|
675
682
|
const safety = triage.safety;
|
|
676
|
-
if (!triage.account)
|
|
677
|
-
errors.push("triage.account is required");
|
|
678
|
-
validateString(triage.account, "triage.account", errors);
|
|
679
683
|
if (!triage.agents)
|
|
680
684
|
errors.push("triage.agents is required");
|
|
681
685
|
validateTriageAgentList(triage.agents, "triage.agents", errors, options.modelCatalog);
|
|
682
686
|
if (Array.isArray(triage.agents)) {
|
|
683
|
-
|
|
687
|
+
const resolvedTriageAgents = resolveAgents(config).triage ?? [];
|
|
688
|
+
validateResolvedTriageAgents(resolvedTriageAgents, "triage.resolvedAgents", errors);
|
|
689
|
+
if (reporter != null &&
|
|
690
|
+
!resolvedTriageAgents.some((agent) => agent.key === reporter)) {
|
|
691
|
+
errors.push(`triage.reporter must match a triage agent key: ${reporter}`);
|
|
692
|
+
}
|
|
684
693
|
}
|
|
685
|
-
|
|
686
|
-
|
|
694
|
+
validateString(triage.reporter, "triage.reporter", errors);
|
|
695
|
+
validateTriageCreator(creator, "triage.creator", errors, options.modelCatalog);
|
|
696
|
+
if (automation?.create && !creator)
|
|
687
697
|
errors.push("triage.creator is required when triage.automation.create is true");
|
|
698
|
+
if (automation?.create && creator && !creator.account)
|
|
699
|
+
errors.push("triage.creator.account is required when triage.automation.create is true");
|
|
688
700
|
if (automation != null && !isPlainObject(automation)) {
|
|
689
701
|
errors.push("triage.automation must be an object");
|
|
690
702
|
}
|
|
@@ -745,10 +757,10 @@ async function validateAuth(config, exec, errors) {
|
|
|
745
757
|
const agents = resolveAgents(config);
|
|
746
758
|
for (const reviewer of agents.reviewers)
|
|
747
759
|
accounts.add(reviewer.account);
|
|
760
|
+
for (const agent of agents.triage ?? [])
|
|
761
|
+
accounts.add(agent.account);
|
|
748
762
|
if (agents.editor)
|
|
749
763
|
accounts.add(agents.editor.account);
|
|
750
|
-
if (config.triage?.account)
|
|
751
|
-
accounts.add(config.triage.account);
|
|
752
764
|
if (agents.triageCreator?.account)
|
|
753
765
|
accounts.add(agents.triageCreator.account);
|
|
754
766
|
await Promise.all([...accounts].filter(Boolean).map(async (account) => {
|
|
@@ -802,19 +814,18 @@ async function validateRepositoryPermissions(config, exec, errors, warnings) {
|
|
|
802
814
|
warnings.push(`Could not validate repository permissions for GitHub account: ${reviewer.account} (${error.message})`);
|
|
803
815
|
}
|
|
804
816
|
}));
|
|
805
|
-
|
|
817
|
+
await Promise.all((agents.triage ?? []).map(async (agent) => {
|
|
806
818
|
try {
|
|
807
|
-
const permissions = await fetchPermissions(config, exec,
|
|
819
|
+
const permissions = await fetchPermissions(config, exec, agent.account);
|
|
808
820
|
if (!permissions.pull) {
|
|
809
|
-
errors.push(`GitHub account cannot read repository for issue triage: ${
|
|
821
|
+
errors.push(`GitHub account cannot read repository for issue triage: ${agent.account}`);
|
|
810
822
|
}
|
|
811
823
|
}
|
|
812
824
|
catch (error) {
|
|
813
|
-
warnings.push(`Could not validate repository permissions for GitHub account: ${
|
|
825
|
+
warnings.push(`Could not validate repository permissions for GitHub account: ${agent.account} (${error.message})`);
|
|
814
826
|
}
|
|
815
|
-
}
|
|
816
|
-
if (agents.triageCreator?.account
|
|
817
|
-
agents.triageCreator.account !== config.triage?.account) {
|
|
827
|
+
}));
|
|
828
|
+
if (agents.triageCreator?.account) {
|
|
818
829
|
try {
|
|
819
830
|
const permissions = await fetchPermissions(config, exec, agents.triageCreator.account);
|
|
820
831
|
if (!permissions.push) {
|
package/dist/config/worktree.js
CHANGED
|
@@ -17,3 +17,9 @@ export function worktreeBaseDirs(directory, config = {}) {
|
|
|
17
17
|
worktreeBaseDir(directory, config, "issue"),
|
|
18
18
|
];
|
|
19
19
|
}
|
|
20
|
+
export function prRunWorktreeDir(input) {
|
|
21
|
+
return join(worktreeBaseDir(input.directory, input.config, "pr"), String(input.pr), input.runId);
|
|
22
|
+
}
|
|
23
|
+
export function issueRunWorktreeDir(input) {
|
|
24
|
+
return join(worktreeBaseDir(input.directory, input.config, "issue"), String(input.issue), input.runId);
|
|
25
|
+
}
|
package/dist/github/commands.js
CHANGED
|
@@ -20,6 +20,82 @@ function errorText(error) {
|
|
|
20
20
|
.filter((item) => typeof item === "string")
|
|
21
21
|
.join("\n");
|
|
22
22
|
}
|
|
23
|
+
function isIssueTypeUnavailableText(text) {
|
|
24
|
+
return (/cannot query field ["']?issueType["']? on type ["']?Issue["']?/i.test(text) ||
|
|
25
|
+
/field ["']?issueType["']?.*(does not exist|doesn't exist|is not defined|not found).*type ["']?Issue["']?/i.test(text) ||
|
|
26
|
+
/undefinedField.*issueType/i.test(text) ||
|
|
27
|
+
/issueType.*unsupported field|unsupported field.*issueType/i.test(text));
|
|
28
|
+
}
|
|
29
|
+
function isIssueTypeUnavailableError(error) {
|
|
30
|
+
return isIssueTypeUnavailableText(errorText(error));
|
|
31
|
+
}
|
|
32
|
+
function isIssueTypeUnavailableGraphqlResponse(data) {
|
|
33
|
+
return (data.errors?.some((error) => isIssueTypeUnavailableText([error.message, error.type]
|
|
34
|
+
.filter((item) => typeof item === "string")
|
|
35
|
+
.join("\n"))) ?? false);
|
|
36
|
+
}
|
|
37
|
+
async function localCommitExists(exec, worktreePath, sha) {
|
|
38
|
+
try {
|
|
39
|
+
await exec(`git cat-file -e ${shellQuote(`${sha}^{commit}`)}`, {
|
|
40
|
+
cwd: worktreePath,
|
|
41
|
+
});
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function pullRequestCommitSource(input) {
|
|
49
|
+
if (input.source === "base") {
|
|
50
|
+
return {
|
|
51
|
+
owner: input.repository.github.owner,
|
|
52
|
+
refName: input.meta.baseRefName,
|
|
53
|
+
repo: input.repository.github.repo,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
owner: input.meta.headRepositoryOwner?.login ?? input.repository.github.owner,
|
|
58
|
+
refName: input.meta.headRefName,
|
|
59
|
+
repo: input.meta.headRepository?.name ?? input.repository.github.repo,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function fetchPullRequestCommitSource(input) {
|
|
63
|
+
const commitSource = pullRequestCommitSource(input);
|
|
64
|
+
try {
|
|
65
|
+
await input.exec(`git fetch --no-tags ${shellQuote(repositoryGitUrl(input.repository, commitSource.owner, commitSource.repo))} ${shellQuote(`refs/heads/${commitSource.refName}`)}`, { cwd: input.worktreePath });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`Could not fetch ${input.source} ref ${commitSource.refName} for #${input.meta.number}: ${errorText(error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function ensurePullRequestCommits(input) {
|
|
72
|
+
const missing = [];
|
|
73
|
+
for (const commit of input.commits) {
|
|
74
|
+
if (!(await localCommitExists(input.exec, input.worktreePath, commit.sha))) {
|
|
75
|
+
missing.push(commit);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const source of new Set(missing.map((commit) => commit.source))) {
|
|
79
|
+
await fetchPullRequestCommitSource({
|
|
80
|
+
exec: input.exec,
|
|
81
|
+
meta: input.meta,
|
|
82
|
+
repository: input.repository,
|
|
83
|
+
source,
|
|
84
|
+
worktreePath: input.worktreePath,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const commit of missing) {
|
|
88
|
+
if (await localCommitExists(input.exec, input.worktreePath, commit.sha)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const source = pullRequestCommitSource({
|
|
92
|
+
meta: input.meta,
|
|
93
|
+
repository: input.repository,
|
|
94
|
+
source: commit.source,
|
|
95
|
+
});
|
|
96
|
+
throw new Error(`${commit.label} commit ${commit.sha} is unavailable after fetching ${commit.source} ref ${source.refName}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
23
99
|
function isCheckoutConfigLockError(error) {
|
|
24
100
|
const text = errorText(error);
|
|
25
101
|
return (/could not lock config file/i.test(text) ||
|
|
@@ -122,26 +198,34 @@ export async function fetchPullRequestClosingIssues(exec, repository, pr) {
|
|
|
122
198
|
}
|
|
123
199
|
export async function fetchIssue(exec, repository, issue) {
|
|
124
200
|
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 } } } }`;
|
|
201
|
+
let raw;
|
|
125
202
|
try {
|
|
126
|
-
|
|
127
|
-
const data = JSON.parse(raw);
|
|
128
|
-
const graphqlIssue = data.data?.repository?.issue;
|
|
129
|
-
if (!graphqlIssue)
|
|
130
|
-
throw new Error(`Could not fetch issue #${issue}`);
|
|
131
|
-
return {
|
|
132
|
-
author: graphqlIssue.author?.login ?? "",
|
|
133
|
-
body: graphqlIssue.body ?? "",
|
|
134
|
-
labels: graphqlIssue.labels?.nodes?.map((label) => label.name) ?? [],
|
|
135
|
-
number: graphqlIssue.number,
|
|
136
|
-
state: graphqlIssue.state,
|
|
137
|
-
title: graphqlIssue.title,
|
|
138
|
-
type: graphqlIssue.issueType?.name,
|
|
139
|
-
url: graphqlIssue.url,
|
|
140
|
-
};
|
|
203
|
+
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}`);
|
|
141
204
|
}
|
|
142
|
-
catch {
|
|
143
|
-
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (isIssueTypeUnavailableError(error)) {
|
|
207
|
+
return fetchIssueWithCli(exec, repository, issue);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
144
210
|
}
|
|
211
|
+
const data = JSON.parse(raw);
|
|
212
|
+
const graphqlIssue = data.data?.repository?.issue;
|
|
213
|
+
if (!graphqlIssue) {
|
|
214
|
+
if (isIssueTypeUnavailableGraphqlResponse(data)) {
|
|
215
|
+
return fetchIssueWithCli(exec, repository, issue);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Could not fetch issue #${issue}`);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
author: graphqlIssue.author?.login ?? "",
|
|
221
|
+
body: graphqlIssue.body ?? "",
|
|
222
|
+
labels: graphqlIssue.labels?.nodes?.map((label) => label.name) ?? [],
|
|
223
|
+
number: graphqlIssue.number,
|
|
224
|
+
state: graphqlIssue.state,
|
|
225
|
+
title: graphqlIssue.title,
|
|
226
|
+
type: graphqlIssue.issueType?.name,
|
|
227
|
+
url: graphqlIssue.url,
|
|
228
|
+
};
|
|
145
229
|
}
|
|
146
230
|
async function fetchIssueWithCli(exec, repository, issue) {
|
|
147
231
|
const raw = await exec(`gh issue view ${issue} --repo ${shellQuote(repoSpecifier(repository))} --json number,title,body,url,state,author,labels`);
|
|
@@ -330,16 +414,43 @@ export async function removeIssueLabels(exec, repository, issue, labels, account
|
|
|
330
414
|
return removed;
|
|
331
415
|
}
|
|
332
416
|
export async function fetchPullRequestReviews(exec, repository, pr) {
|
|
333
|
-
const query = `query($owner: String!, $repo: String!, $pr: Int
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
417
|
+
const query = `query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviews(first: 100, after: $cursor) { nodes { author { login } submittedAt state body commit { oid } comments(first: 100) { nodes { body path line startLine } } } pageInfo { hasNextPage endCursor } } } } }`;
|
|
418
|
+
const reviews = [];
|
|
419
|
+
let cursor;
|
|
420
|
+
for (;;) {
|
|
421
|
+
const cursorFlag = cursor ? ` -F cursor=${shellQuote(cursor)}` : "";
|
|
422
|
+
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}${cursorFlag}`);
|
|
423
|
+
const data = JSON.parse(raw);
|
|
424
|
+
const connection = data.data.repository.pullRequest.reviews;
|
|
425
|
+
reviews.push(...connection.nodes);
|
|
426
|
+
if (!connection.pageInfo?.hasNextPage)
|
|
427
|
+
break;
|
|
428
|
+
cursor = connection.pageInfo.endCursor;
|
|
429
|
+
if (!cursor)
|
|
430
|
+
throw new Error("GitHub reviews page was truncated");
|
|
431
|
+
}
|
|
432
|
+
return reviews.map((review) => ({
|
|
433
|
+
...review,
|
|
434
|
+
comments: review.comments?.nodes ?? [],
|
|
435
|
+
}));
|
|
337
436
|
}
|
|
338
437
|
export async function fetchPullRequestCommits(exec, repository, pr) {
|
|
339
|
-
const query = `query($owner: String!, $repo: String!, $pr: Int
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
438
|
+
const query = `query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { commits(first: 100, after: $cursor) { nodes { commit { oid committedDate parents { totalCount } } } pageInfo { hasNextPage endCursor } } } } }`;
|
|
439
|
+
const commits = [];
|
|
440
|
+
let cursor;
|
|
441
|
+
for (;;) {
|
|
442
|
+
const cursorFlag = cursor ? ` -F cursor=${shellQuote(cursor)}` : "";
|
|
443
|
+
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}${cursorFlag}`);
|
|
444
|
+
const data = JSON.parse(raw);
|
|
445
|
+
const connection = data.data.repository.pullRequest.commits;
|
|
446
|
+
commits.push(...connection.nodes);
|
|
447
|
+
if (!connection.pageInfo?.hasNextPage)
|
|
448
|
+
break;
|
|
449
|
+
cursor = connection.pageInfo.endCursor;
|
|
450
|
+
if (!cursor)
|
|
451
|
+
throw new Error("GitHub commits page was truncated");
|
|
452
|
+
}
|
|
453
|
+
return commits.map(({ commit }) => ({
|
|
343
454
|
committedDate: commit.committedDate,
|
|
344
455
|
oid: commit.oid,
|
|
345
456
|
parentCount: commit.parents.totalCount,
|
|
@@ -369,37 +480,9 @@ export async function fetchPullRequestSafetyMeta(exec, repository, pr) {
|
|
|
369
480
|
}
|
|
370
481
|
return { author, changedFiles, files, labels };
|
|
371
482
|
}
|
|
372
|
-
export async function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const report = {
|
|
376
|
-
attempts: 0,
|
|
377
|
-
excluded: [],
|
|
378
|
-
failed: [],
|
|
379
|
-
rerun: [],
|
|
380
|
-
scopeInside: [],
|
|
381
|
-
scopeOutsideRecovered: [],
|
|
382
|
-
scopeOutsideUnresolved: [],
|
|
383
|
-
};
|
|
384
|
-
try {
|
|
385
|
-
await watchChecks(exec, repository, pr);
|
|
386
|
-
return report;
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
report.failed = applyCheckExclusions({
|
|
390
|
-
checks: await fetchFailedChecks(exec, repository, pr),
|
|
391
|
-
excluded: report.excluded,
|
|
392
|
-
patterns: repository.checks.exclude,
|
|
393
|
-
});
|
|
394
|
-
return report;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
export async function watchChecks(exec, repository, pr) {
|
|
398
|
-
await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --watch`);
|
|
399
|
-
}
|
|
400
|
-
export async function fetchFailedChecks(exec, repository, pr) {
|
|
401
|
-
const checks = await fetchPullRequestChecks(exec, repository, pr);
|
|
402
|
-
return checks.filter((check) => isFailedCheck(check) || isCancelledCheck(check));
|
|
483
|
+
export async function watchChecks(exec, repository, pr, options = {}) {
|
|
484
|
+
const requiredFlag = options.requiredOnly ? " --required" : "";
|
|
485
|
+
await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --watch${requiredFlag}`);
|
|
403
486
|
}
|
|
404
487
|
export function isCancelledCheck(check) {
|
|
405
488
|
return check.bucket === "cancel" || check.state === "CANCELLED";
|
|
@@ -409,8 +492,9 @@ export function isFailedCheck(check) {
|
|
|
409
492
|
}
|
|
410
493
|
export async function fetchPullRequestChecks(exec, repository, pr, options = {}) {
|
|
411
494
|
let raw;
|
|
495
|
+
const requiredFlag = options.requiredOnly ? " --required" : "";
|
|
412
496
|
try {
|
|
413
|
-
raw = await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json name,state,bucket,link,workflow`);
|
|
497
|
+
raw = await exec(`gh pr checks ${pr} --repo ${shellQuote(repoSpecifier(repository))} --json name,state,bucket,link,workflow${requiredFlag}`);
|
|
414
498
|
}
|
|
415
499
|
catch (error) {
|
|
416
500
|
if (options.tolerateMissingChecks &&
|
|
@@ -472,9 +556,8 @@ export async function fetchMergeQueueRequirement(exec, repository, branch) {
|
|
|
472
556
|
const rules = JSON.parse(raw);
|
|
473
557
|
return rules.some((rule) => rule.type === "merge_queue");
|
|
474
558
|
}
|
|
475
|
-
export async function createWorktree(exec, repository, pr,
|
|
476
|
-
const
|
|
477
|
-
const lockKey = `${repoSpecifier(repository)}:${root}`;
|
|
559
|
+
export async function createWorktree(exec, repository, pr, worktreePath) {
|
|
560
|
+
const lockKey = `${repoSpecifier(repository)}:${dirname(dirname(worktreePath))}`;
|
|
478
561
|
return withWorktreeCreateLock(lockKey, async () => {
|
|
479
562
|
let worktreeAdded = false;
|
|
480
563
|
try {
|
|
@@ -591,6 +674,17 @@ export async function waitForMergeQueue(exec, repository, pr, intervalMs = 30_00
|
|
|
591
674
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
592
675
|
}
|
|
593
676
|
}
|
|
677
|
+
export async function waitForAutoMerge(exec, repository, pr, intervalMs = 30_000) {
|
|
678
|
+
for (;;) {
|
|
679
|
+
const status = await fetchPullRequestMergeStatus(exec, repository, pr);
|
|
680
|
+
if (status.state === "MERGED")
|
|
681
|
+
return "merged";
|
|
682
|
+
if (status.state !== "OPEN" || status.autoMergeRequest == null) {
|
|
683
|
+
return "dequeued";
|
|
684
|
+
}
|
|
685
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
594
688
|
export async function closePullRequest(exec, repository, pr, account) {
|
|
595
689
|
const token = await ghToken(exec, repository, account);
|
|
596
690
|
return exec(`gh pr close ${pr} --repo ${shellQuote(repoSpecifier(repository))}`, ghTokenEnv(token));
|
|
@@ -629,11 +723,49 @@ export async function configureGitIdentity(exec, worktreePath, identity) {
|
|
|
629
723
|
}
|
|
630
724
|
}
|
|
631
725
|
export async function fetchUnresolvedThreads(exec, repository, pr, author) {
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
726
|
+
const threadQuery = `query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(first: 100, after: $cursor) { nodes { id isResolved comments(first: 100) { nodes { databaseId author { login } path line body createdAt } pageInfo { hasNextPage endCursor } } } pageInfo { hasNextPage endCursor } } } } }`;
|
|
727
|
+
const commentQuery = `query($threadId: ID!, $cursor: String) { node(id: $threadId) { ... on PullRequestReviewThread { comments(first: 100, after: $cursor) { nodes { databaseId author { login } path line body createdAt } pageInfo { hasNextPage endCursor } } } } }`;
|
|
728
|
+
const threads = [];
|
|
729
|
+
let cursor;
|
|
730
|
+
async function fetchRemainingComments(threadId, initialCursor) {
|
|
731
|
+
const comments = [];
|
|
732
|
+
let commentsCursor = initialCursor;
|
|
733
|
+
for (;;) {
|
|
734
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(commentQuery)} -F threadId=${shellQuote(threadId)} -F cursor=${shellQuote(commentsCursor)}`);
|
|
735
|
+
const data = JSON.parse(raw);
|
|
736
|
+
const connection = data.data.node?.comments;
|
|
737
|
+
if (!connection)
|
|
738
|
+
throw new Error("GitHub review thread comments were missing");
|
|
739
|
+
comments.push(...connection.nodes);
|
|
740
|
+
if (!connection.pageInfo?.hasNextPage)
|
|
741
|
+
break;
|
|
742
|
+
const nextCursor = connection.pageInfo.endCursor;
|
|
743
|
+
if (!nextCursor)
|
|
744
|
+
throw new Error("GitHub review thread comments page was truncated");
|
|
745
|
+
commentsCursor = nextCursor;
|
|
746
|
+
}
|
|
747
|
+
return comments;
|
|
748
|
+
}
|
|
749
|
+
for (;;) {
|
|
750
|
+
const cursorFlag = cursor ? ` -F cursor=${shellQuote(cursor)}` : "";
|
|
751
|
+
const raw = await exec(`gh api${ghHostOption(repository)} graphql -f query=${shellQuote(threadQuery)} -F owner=${shellQuote(repository.github.owner)} -F repo=${shellQuote(repository.github.repo)} -F pr=${pr}${cursorFlag}`);
|
|
752
|
+
const data = JSON.parse(raw);
|
|
753
|
+
const connection = data.data.repository.pullRequest.reviewThreads;
|
|
754
|
+
for (const thread of connection.nodes) {
|
|
755
|
+
const commentsCursor = thread.comments.pageInfo?.endCursor;
|
|
756
|
+
if (thread.comments.pageInfo?.hasNextPage) {
|
|
757
|
+
if (!commentsCursor)
|
|
758
|
+
throw new Error("GitHub review thread comments page was truncated");
|
|
759
|
+
thread.comments.nodes.push(...(await fetchRemainingComments(thread.id, commentsCursor)));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
threads.push(...connection.nodes);
|
|
763
|
+
if (!connection.pageInfo?.hasNextPage)
|
|
764
|
+
break;
|
|
765
|
+
cursor = connection.pageInfo.endCursor;
|
|
766
|
+
if (!cursor)
|
|
767
|
+
throw new Error("GitHub review threads page was truncated");
|
|
768
|
+
}
|
|
637
769
|
return threads.flatMap((thread) => {
|
|
638
770
|
if (thread.isResolved || !thread.comments.nodes.length)
|
|
639
771
|
return [];
|
package/dist/index.js
CHANGED
|
@@ -96,6 +96,7 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
96
96
|
const configOverrides = {};
|
|
97
97
|
const prTokens = [];
|
|
98
98
|
let sync = false;
|
|
99
|
+
let timeoutMs;
|
|
99
100
|
for (let index = 0; index < tokens.length; index++) {
|
|
100
101
|
const token = tokens[index];
|
|
101
102
|
if (token === "--dry-run") {
|
|
@@ -110,6 +111,11 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
110
111
|
case "--language":
|
|
111
112
|
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
112
113
|
break;
|
|
114
|
+
case "--timeout":
|
|
115
|
+
timeoutMs =
|
|
116
|
+
parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0) *
|
|
117
|
+
1_000;
|
|
118
|
+
break;
|
|
113
119
|
case "--merge":
|
|
114
120
|
case "--no-merge":
|
|
115
121
|
setConfigOverride(configOverrides, [command, "automation", "merge"], token === "--merge");
|
|
@@ -151,13 +157,20 @@ export function parseRunArguments(value, dryRun = false, command = "review") {
|
|
|
151
157
|
prTokens.push(token);
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
configOverrides,
|
|
162
|
+
dryRun,
|
|
163
|
+
prs: parsePrs(prTokens.join(" ")),
|
|
164
|
+
sync,
|
|
165
|
+
timeoutMs,
|
|
166
|
+
};
|
|
155
167
|
}
|
|
156
168
|
export function parseIssueRunArguments(value, dryRun = false) {
|
|
157
169
|
const tokens = value.split(/[\s,]+/).filter(Boolean);
|
|
158
170
|
const configOverrides = {};
|
|
159
171
|
const issueTokens = [];
|
|
160
172
|
let sync = false;
|
|
173
|
+
let timeoutMs;
|
|
161
174
|
for (let index = 0; index < tokens.length; index++) {
|
|
162
175
|
const token = tokens[index];
|
|
163
176
|
if (token === "--dry-run") {
|
|
@@ -172,6 +185,11 @@ export function parseIssueRunArguments(value, dryRun = false) {
|
|
|
172
185
|
case "--language":
|
|
173
186
|
setConfigOverride(configOverrides, ["language"], nextFlagValue(tokens, ++index, token));
|
|
174
187
|
break;
|
|
188
|
+
case "--timeout":
|
|
189
|
+
timeoutMs =
|
|
190
|
+
parseIntegerFlag(nextFlagValue(tokens, ++index, token), token, 0) *
|
|
191
|
+
1_000;
|
|
192
|
+
break;
|
|
175
193
|
case "--close":
|
|
176
194
|
case "--no-close":
|
|
177
195
|
setConfigOverride(configOverrides, ["triage", "automation", "close"], token === "--close");
|
|
@@ -210,6 +228,7 @@ export function parseIssueRunArguments(value, dryRun = false) {
|
|
|
210
228
|
dryRun,
|
|
211
229
|
issues: parseIssues(issueTokens.join(" ")),
|
|
212
230
|
sync,
|
|
231
|
+
timeoutMs,
|
|
213
232
|
};
|
|
214
233
|
}
|
|
215
234
|
function nextFlagValue(tokens, index, flag) {
|
|
@@ -306,6 +325,10 @@ function prMarkdownLink(repository, pr) {
|
|
|
306
325
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/pull/${pr}`;
|
|
307
326
|
return `[#${pr}](${url})`;
|
|
308
327
|
}
|
|
328
|
+
export function formatRunStartMessage(command, repository, pr) {
|
|
329
|
+
const action = command === "merge" ? "merge flow" : "reviewing";
|
|
330
|
+
return `Started ${action} ${prMarkdownLink(repository, pr)}.`;
|
|
331
|
+
}
|
|
309
332
|
function issueMarkdownLink(repository, issue) {
|
|
310
333
|
const host = repository.github.host || "github.com";
|
|
311
334
|
const url = `https://${host}/${repository.github.owner}/${repository.github.repo}/issues/${issue}`;
|
|
@@ -482,11 +505,12 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
482
505
|
parentSessionId: context.sessionID,
|
|
483
506
|
signal: context.abort,
|
|
484
507
|
sync,
|
|
508
|
+
timeoutMs: parsed.timeoutMs,
|
|
485
509
|
}), { signal: context.abort });
|
|
486
510
|
if (sync)
|
|
487
511
|
return syncResult(runManager, states);
|
|
488
512
|
return states
|
|
489
|
-
.map((state) =>
|
|
513
|
+
.map((state) => formatRunStartMessage("merge", repository, state.pr))
|
|
490
514
|
.join("\n");
|
|
491
515
|
},
|
|
492
516
|
}),
|
|
@@ -523,11 +547,12 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
523
547
|
parentSessionId: context.sessionID,
|
|
524
548
|
signal: context.abort,
|
|
525
549
|
sync,
|
|
550
|
+
timeoutMs: parsed.timeoutMs,
|
|
526
551
|
}), { signal: context.abort });
|
|
527
552
|
if (sync)
|
|
528
553
|
return syncResult(runManager, states);
|
|
529
554
|
return states
|
|
530
|
-
.map((state) =>
|
|
555
|
+
.map((state) => formatRunStartMessage("review", repository, state.pr))
|
|
531
556
|
.join("\n");
|
|
532
557
|
},
|
|
533
558
|
}),
|
|
@@ -567,6 +592,7 @@ export const MagiPlugin = async ({ client, directory }) => {
|
|
|
567
592
|
repository,
|
|
568
593
|
signal: context.abort,
|
|
569
594
|
sync,
|
|
595
|
+
timeoutMs: parsed.timeoutMs,
|
|
570
596
|
}), { signal: context.abort });
|
|
571
597
|
if (sync)
|
|
572
598
|
return syncResult(runManager, states);
|