pr-shepherd 0.44.1 → 0.46.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/.claude-plugin/plugin.json +1 -1
- package/README.md +19 -8
- package/bin/checks/conclusions.d.mts +8 -1
- package/bin/checks/conclusions.mjs +12 -0
- package/bin/checks/triage.d.mts +3 -2
- package/bin/checks/triage.mjs +44 -15
- package/bin/cli/api-usage-formatter.d.mts +3 -0
- package/bin/cli/api-usage-formatter.mjs +36 -0
- package/bin/cli/error-format.d.mts +13 -0
- package/bin/cli/error-format.mjs +41 -0
- package/bin/cli/fix-formatter.mjs +22 -5
- package/bin/cli/help-command-pages.d.mts +13 -0
- package/bin/cli/help-command-pages.mjs +13 -0
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +14 -1
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-activity-formatter.d.mts +2 -0
- package/bin/cli/iterate-activity-formatter.mjs +26 -0
- package/bin/cli/iterate-checks-formatter.d.mts +2 -0
- package/bin/cli/iterate-checks-formatter.mjs +55 -0
- package/bin/cli/iterate-formatter.mjs +17 -29
- package/bin/cli/iterate-instructions.mjs +14 -1
- package/bin/cli/iterate-lean.mjs +5 -0
- package/bin/cli/journal-extract-handler.d.mts +2 -0
- package/bin/cli/journal-extract-handler.mjs +48 -0
- package/bin/cli/mutate-formatter.mjs +2 -0
- package/bin/cli/safe-body-file.d.mts +14 -0
- package/bin/cli/safe-body-file.mjs +42 -0
- package/bin/cli-parser.mjs +7 -0
- package/bin/commands/check-annotations.d.mts +2 -1
- package/bin/commands/check-annotations.mjs +7 -7
- package/bin/commands/check-status.mjs +4 -4
- package/bin/commands/check.mjs +31 -9
- package/bin/commands/iterate/api-usage.d.mts +2 -0
- package/bin/commands/iterate/api-usage.mjs +35 -0
- package/bin/commands/iterate/check-instructions.d.mts +1 -1
- package/bin/commands/iterate/check-instructions.mjs +13 -18
- package/bin/commands/iterate/classify.mjs +2 -1
- package/bin/commands/iterate/escalate.mjs +63 -9
- package/bin/commands/iterate/fix-code.mjs +110 -73
- package/bin/commands/iterate/index.mjs +8 -5
- package/bin/commands/iterate/merge-state.mjs +5 -2
- package/bin/commands/iterate/render.mjs +13 -6
- package/bin/commands/iterate/run.d.mts +2 -0
- package/bin/commands/iterate/run.mjs +8 -0
- package/bin/commands/mark-files-as-viewed.mjs +8 -0
- package/bin/commands/poll-run.d.mts +2 -0
- package/bin/commands/poll-run.mjs +8 -0
- package/bin/commands/poll.d.mts +1 -2
- package/bin/commands/poll.mjs +26 -10
- package/bin/comments/rate-limit.d.mts +4 -0
- package/bin/comments/rate-limit.mjs +6 -0
- package/bin/comments/review-visibility.d.mts +1 -1
- package/bin/comments/review-visibility.mjs +3 -2
- package/bin/comments/thread-visibility.d.mts +1 -1
- package/bin/comments/thread-visibility.mjs +9 -3
- package/bin/config/load.d.mts +6 -1
- package/bin/config/load.mjs +33 -1
- package/bin/config.json +6 -1
- package/bin/github/api-telemetry-aggregate.d.mts +32 -0
- package/bin/github/api-telemetry-aggregate.mjs +84 -0
- package/bin/github/api-telemetry.d.mts +13 -0
- package/bin/github/api-telemetry.mjs +128 -0
- package/bin/github/check-annotations.d.mts +14 -1
- package/bin/github/check-annotations.mjs +29 -2
- package/bin/github/client.d.mts +1 -1
- package/bin/github/client.mjs +1 -1
- package/bin/github/errors.d.mts +2 -0
- package/bin/github/errors.mjs +2 -0
- package/bin/github/gql/batch-pr-page.gql +8 -0
- package/bin/github/gql/batch-pr.gql +8 -0
- package/bin/github/gql/check-run-annotations.gql +8 -0
- package/bin/github/gql/commit-check-contexts.gql +8 -0
- package/bin/github/gql/get-pr-body.gql +8 -0
- package/bin/github/gql/get-pr-head-sha.gql +8 -0
- package/bin/github/gql/pr-number-by-branch.gql +8 -0
- package/bin/github/gql/review-thread-comments.gql +8 -0
- package/bin/github/gql/suggestion-threads.gql +8 -0
- package/bin/github/graphql-http.mjs +63 -8
- package/bin/github/http-auth.d.mts +9 -1
- package/bin/github/http-auth.mjs +35 -14
- package/bin/github/http-intermediate.d.mts +9 -0
- package/bin/github/http-intermediate.mjs +20 -0
- package/bin/github/http-request.d.mts +1 -1
- package/bin/github/http-request.mjs +1 -1
- package/bin/github/http-utils.d.mts +6 -0
- package/bin/github/http-utils.mjs +10 -1
- package/bin/github/rest-http.d.mts +16 -1
- package/bin/github/rest-http.mjs +71 -8
- package/bin/github/rest-text.mjs +41 -5
- package/bin/index.mjs +2 -1
- package/bin/log/session.d.mts +11 -0
- package/bin/log/session.mjs +22 -0
- package/bin/mcp/server.mjs +16 -2
- package/bin/pr-reference.d.mts +1 -1
- package/bin/pr-reference.mjs +1 -1
- package/bin/quota-warning.d.mts +2 -0
- package/bin/quota-warning.mjs +6 -0
- package/bin/state/graphql-quota-claims.d.mts +2 -0
- package/bin/state/graphql-quota-claims.mjs +61 -0
- package/bin/state/graphql-quota-policy.d.mts +17 -0
- package/bin/state/graphql-quota-policy.mjs +43 -0
- package/bin/state/graphql-quota-warnings.d.mts +6 -0
- package/bin/state/graphql-quota-warnings.mjs +105 -0
- package/bin/state/rest-cache.d.mts +48 -0
- package/bin/state/rest-cache.mjs +91 -0
- package/bin/types/api-usage.d.mts +31 -0
- package/bin/types/api-usage.mjs +1 -0
- package/bin/types/escalate.d.mts +5 -3
- package/bin/types/iterate.d.mts +4 -1
- package/bin/types/report.d.mts +3 -1
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +24 -13
|
@@ -1,34 +1,11 @@
|
|
|
1
1
|
import { formatFixCodeResult } from "./fix-formatter.mjs";
|
|
2
|
+
import { formatRelevantChecks } from "./iterate-checks-formatter.mjs";
|
|
2
3
|
import { joinSections } from "../util/markdown.mjs";
|
|
3
4
|
import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, } from "./iterate-instructions.mjs";
|
|
4
5
|
import { formatMergeRequirementLines } from "../merge-status/requirements-format.mjs";
|
|
5
6
|
import { appendMergeQueueHeader, formatMergeAction } from "./iterate-merge-formatter.mjs";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
commitCount: 0,
|
|
9
|
-
reviewRoundCount: 0,
|
|
10
|
-
latestCommitCommittedAtUnix: null,
|
|
11
|
-
reviewItemsSinceLatestCommit: [],
|
|
12
|
-
};
|
|
13
|
-
const hasActiveChecks = (result.inProgressChecks?.length ?? 0) > 0;
|
|
14
|
-
if (activity.commitCount === 0 &&
|
|
15
|
-
activity.reviewRoundCount === 0 &&
|
|
16
|
-
activity.reviewItemsSinceLatestCommit.length === 0 &&
|
|
17
|
-
!hasActiveChecks) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const parts = [`${activity.commitCount} commits`, `${activity.reviewRoundCount} review rounds`];
|
|
21
|
-
if (activity.reviewItemsSinceLatestCommit.length > 0) {
|
|
22
|
-
parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
|
|
23
|
-
}
|
|
24
|
-
if (hasActiveChecks) {
|
|
25
|
-
parts.push(`active: ${result
|
|
26
|
-
.inProgressChecks.slice(0, 5)
|
|
27
|
-
.map((c) => `\`${c.name}\``)
|
|
28
|
-
.join(", ")}`);
|
|
29
|
-
}
|
|
30
|
-
return `**activity** ${parts.join(" · ")}`;
|
|
31
|
-
}
|
|
7
|
+
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
8
|
+
import { formatActivityLine } from "./iterate-activity-formatter.mjs";
|
|
32
9
|
/**
|
|
33
10
|
* Format an IterateResult as human-readable Markdown.
|
|
34
11
|
*
|
|
@@ -50,7 +27,8 @@ export function formatIterateResult(result, opts) {
|
|
|
50
27
|
const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
|
|
51
28
|
? ` · **reviewDecision** \`${result.reviewDecision}\``
|
|
52
29
|
: "";
|
|
53
|
-
const
|
|
30
|
+
const baseBranchSeg = verbose && result.baseBranch ? ` · **baseBranch** \`${result.baseBranch}\`` : "";
|
|
31
|
+
const baseLine = `**status** \`${result.status}\` · **merge** \`${result.mergeStateStatus}\`${reviewDecisionSeg} · **state** \`${result.state}\` · **repo** \`${result.repo}\`${baseBranchSeg}`;
|
|
54
32
|
let summaryLine;
|
|
55
33
|
if (verbose) {
|
|
56
34
|
let verboseBranch = "";
|
|
@@ -132,21 +110,27 @@ export function formatIterateResult(result, opts) {
|
|
|
132
110
|
if (activityLine)
|
|
133
111
|
headerLines.push(activityLine);
|
|
134
112
|
const header = headerLines.join("\n");
|
|
113
|
+
const quotaWarning = formatQuotaWarning(result.quotaWarning);
|
|
114
|
+
const apiUsage = verbose ? formatApiUsage(result.apiUsage) : null;
|
|
115
|
+
const verboseChecks = verbose ? formatRelevantChecks(result.checks) : null;
|
|
116
|
+
const telemetrySections = [quotaWarning, apiUsage, verboseChecks];
|
|
135
117
|
switch (result.action) {
|
|
136
118
|
case "wait":
|
|
137
119
|
return joinSections([
|
|
138
120
|
header,
|
|
121
|
+
...telemetrySections,
|
|
139
122
|
adaptIterateLog(result.log),
|
|
140
123
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
141
124
|
]);
|
|
142
125
|
case "mark_ready":
|
|
143
126
|
return joinSections([
|
|
144
127
|
header,
|
|
128
|
+
...telemetrySections,
|
|
145
129
|
adaptIterateLog(result.log),
|
|
146
130
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
147
131
|
]);
|
|
148
132
|
case "merge":
|
|
149
|
-
return formatMergeAction(header, result);
|
|
133
|
+
return formatMergeAction(joinSections([header, ...telemetrySections]), result);
|
|
150
134
|
case "cancel": {
|
|
151
135
|
const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
|
|
152
136
|
if (result.mergeRequirements) {
|
|
@@ -167,6 +151,8 @@ export function formatIterateResult(result, opts) {
|
|
|
167
151
|
cancelHeaderLines.push(activityLine);
|
|
168
152
|
return joinSections([
|
|
169
153
|
cancelHeaderLines.join("\n"),
|
|
154
|
+
...(apiUsage ? [apiUsage] : []),
|
|
155
|
+
...(verboseChecks ? [verboseChecks] : []),
|
|
170
156
|
adaptIterateLog(result.log),
|
|
171
157
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
172
158
|
]);
|
|
@@ -174,10 +160,12 @@ export function formatIterateResult(result, opts) {
|
|
|
174
160
|
case "escalate":
|
|
175
161
|
return joinSections([
|
|
176
162
|
header,
|
|
163
|
+
...(apiUsage ? [apiUsage] : []),
|
|
164
|
+
...(verboseChecks ? [verboseChecks] : []),
|
|
177
165
|
result.escalate.humanMessage,
|
|
178
166
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
179
167
|
]);
|
|
180
168
|
case "fix_code":
|
|
181
|
-
return formatFixCodeResult(header, result);
|
|
169
|
+
return formatFixCodeResult(joinSections([header, ...telemetrySections]), result);
|
|
182
170
|
}
|
|
183
171
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { renderMergeCommand } from "../commands/iterate/merge.mjs";
|
|
2
2
|
import { inlineCode } from "../util/markdown.mjs";
|
|
3
|
+
import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
|
|
3
4
|
export function buildSimpleIterateInstructions(result) {
|
|
4
5
|
switch (result.action) {
|
|
5
6
|
case "wait":
|
|
7
|
+
if (result.quotaWarning) {
|
|
8
|
+
return [
|
|
9
|
+
buildQuotaAwareContinuation(result.quotaWarning, "Non-terminal — no action needed this tick."),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
6
12
|
return [
|
|
7
13
|
"Non-terminal — no action needed this tick. Iterate again with the same options to continue.",
|
|
8
14
|
];
|
|
9
15
|
case "mark_ready":
|
|
16
|
+
if (result.quotaWarning) {
|
|
17
|
+
return [
|
|
18
|
+
buildQuotaAwareContinuation(result.quotaWarning, "The CLI marked the PR ready for review."),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
10
21
|
return [
|
|
11
22
|
"The CLI marked the PR ready for review. Iterate again with the same options to continue.",
|
|
12
23
|
];
|
|
@@ -20,7 +31,9 @@ export function buildSimpleIterateInstructions(result) {
|
|
|
20
31
|
else if (result.merge.fallbackCommand) {
|
|
21
32
|
instructions.push(`Only if GitHub reports that auto-merge is unavailable, run the \`plain merge fallback\` command: ${inlineCode(renderMergeCommand(result.merge.fallbackCommand))}.`);
|
|
22
33
|
}
|
|
23
|
-
instructions.push(
|
|
34
|
+
instructions.push(result.quotaWarning
|
|
35
|
+
? buildQuotaAwareContinuation(result.quotaWarning, "After running the merge command.")
|
|
36
|
+
: "Then iterate again with the same options to monitor until the PR merges or needs work.");
|
|
24
37
|
return instructions;
|
|
25
38
|
}
|
|
26
39
|
case "cancel":
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -64,6 +64,7 @@ export function projectIterateLean(result, opts) {
|
|
|
64
64
|
...((result.supersededNames?.length ?? 0) > 0 && {
|
|
65
65
|
supersededNames: result.supersededNames,
|
|
66
66
|
}),
|
|
67
|
+
...(result.quotaWarning && { quotaWarning: result.quotaWarning }),
|
|
67
68
|
};
|
|
68
69
|
switch (result.action) {
|
|
69
70
|
case "wait":
|
|
@@ -157,6 +158,10 @@ export function projectIterateLean(result, opts) {
|
|
|
157
158
|
...(result.escalate.changesRequestedReviews.length > 0 && {
|
|
158
159
|
changesRequestedReviews: result.escalate.changesRequestedReviews,
|
|
159
160
|
}),
|
|
161
|
+
...(result.escalate.checks &&
|
|
162
|
+
result.escalate.checks.length > 0 && {
|
|
163
|
+
checks: result.escalate.checks,
|
|
164
|
+
}),
|
|
160
165
|
...(result.escalate.stalledChecks &&
|
|
161
166
|
result.escalate.stalledChecks.length > 0 && {
|
|
162
167
|
stalledChecks: result.escalate.stalledChecks,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { extractShepherdJournal } from "../journal/index.mjs";
|
|
2
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
3
|
+
import { USAGE, maybePrintHelp } from "./help.mjs";
|
|
4
|
+
import { readSafeBodyFile } from "./safe-body-file.mjs";
|
|
5
|
+
/** Extracts a checked Shepherd Journal from a local PR-body file without external I/O. */
|
|
6
|
+
export async function handleJournalExtract(args) {
|
|
7
|
+
if (maybePrintHelp(args, "journal extract"))
|
|
8
|
+
return;
|
|
9
|
+
const bodyFile = parseBodyFile(args);
|
|
10
|
+
if (bodyFile === null)
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
const body = await readSafeBodyFile(bodyFile);
|
|
14
|
+
process.stdout.write(`${JSON.stringify(extractShepherdJournal(body))}\n`);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
process.stderr.write(`pr-shepherd: journal extract: ${String(error)}\n`);
|
|
18
|
+
process.exitCode = error instanceof ShepherdError ? error.exitCode : EXIT.NOINPUT;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function parseBodyFile(args) {
|
|
22
|
+
let bodyFile;
|
|
23
|
+
for (let index = 0; index < args.length; index++) {
|
|
24
|
+
const arg = args[index];
|
|
25
|
+
if (arg === "--body-file") {
|
|
26
|
+
const value = args[++index];
|
|
27
|
+
if (value === undefined || bodyFile !== undefined) {
|
|
28
|
+
return printUsage();
|
|
29
|
+
}
|
|
30
|
+
bodyFile = value;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith("--body-file=")) {
|
|
34
|
+
const value = arg.slice("--body-file=".length);
|
|
35
|
+
if (value === "" || bodyFile !== undefined)
|
|
36
|
+
return printUsage();
|
|
37
|
+
bodyFile = value;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
return printUsage();
|
|
41
|
+
}
|
|
42
|
+
return bodyFile ?? printUsage();
|
|
43
|
+
}
|
|
44
|
+
function printUsage() {
|
|
45
|
+
process.stderr.write(`${USAGE["journal extract"]}\n`);
|
|
46
|
+
process.exitCode = EXIT.USAGE;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -6,12 +6,14 @@ function formatRateLimit(result) {
|
|
|
6
6
|
const rateLimit = result.rateLimit;
|
|
7
7
|
if (rateLimit) {
|
|
8
8
|
const details = [
|
|
9
|
+
typeof rateLimit.resource === "string" ? `resource ${rateLimit.resource}` : null,
|
|
9
10
|
typeof rateLimit.retryAfterSeconds === "number"
|
|
10
11
|
? `retry after ${rateLimit.retryAfterSeconds}s`
|
|
11
12
|
: null,
|
|
12
13
|
typeof rateLimit.remaining === "number" && typeof rateLimit.limit === "number"
|
|
13
14
|
? `remaining ${rateLimit.remaining}/${rateLimit.limit}`
|
|
14
15
|
: null,
|
|
16
|
+
typeof rateLimit.used === "number" ? `used ${rateLimit.used}` : null,
|
|
15
17
|
typeof rateLimit.resetAt === "number"
|
|
16
18
|
? `reset at ${new Date(rateLimit.resetAt * 1000).toISOString()}`
|
|
17
19
|
: null,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
type FileHandle = Awaited<ReturnType<typeof open>>;
|
|
3
|
+
export interface SafeBodyFileReaderDependencies {
|
|
4
|
+
platform: NodeJS.Platform;
|
|
5
|
+
noFollow: number | undefined;
|
|
6
|
+
nonBlock: number | undefined;
|
|
7
|
+
open: (filePath: string, flags: number) => Promise<FileHandle>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads a regular file after rejecting unsafe final path entries without exposing its contents.
|
|
11
|
+
* Callers must trust parent directories: O_NOFOLLOW only protects the final path entry.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readSafeBodyFile(filePath: string, dependencies?: SafeBodyFileReaderDependencies): Promise<string>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
4
|
+
const DEFAULT_DEPENDENCIES = {
|
|
5
|
+
platform: process.platform,
|
|
6
|
+
noFollow: constants.O_NOFOLLOW,
|
|
7
|
+
nonBlock: constants.O_NONBLOCK,
|
|
8
|
+
open,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Reads a regular file after rejecting unsafe final path entries without exposing its contents.
|
|
12
|
+
* Callers must trust parent directories: O_NOFOLLOW only protects the final path entry.
|
|
13
|
+
*/
|
|
14
|
+
export async function readSafeBodyFile(filePath, dependencies = DEFAULT_DEPENDENCIES) {
|
|
15
|
+
const flags = safeOpenFlags(dependencies);
|
|
16
|
+
if (flags === null)
|
|
17
|
+
throw noInput();
|
|
18
|
+
let handle;
|
|
19
|
+
try {
|
|
20
|
+
handle = await dependencies.open(filePath, flags);
|
|
21
|
+
if (!(await handle.stat()).isFile())
|
|
22
|
+
throw new Error("not a regular file");
|
|
23
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw noInput();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await handle?.close().catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function safeOpenFlags(dependencies) {
|
|
33
|
+
if (dependencies.platform === "win32" ||
|
|
34
|
+
dependencies.noFollow === undefined ||
|
|
35
|
+
dependencies.nonBlock === undefined) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return constants.O_RDONLY | dependencies.noFollow | dependencies.nonBlock;
|
|
39
|
+
}
|
|
40
|
+
function noInput() {
|
|
41
|
+
return new ShepherdError("body file could not be read safely", EXIT.NOINPUT);
|
|
42
|
+
}
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { USAGE, helpKeyForArgs, maybePrintHelp } from "./cli/help.mjs";
|
|
|
10
10
|
import { formatMutateResult } from "./cli/formatters.mjs";
|
|
11
11
|
import { handleClean, handleCommitSuggestion, handleSuggestionPatches, handleIterate, handleMarkFilesAsViewed, } from "./cli/handlers.mjs";
|
|
12
12
|
import { handleJournal } from "./cli/journal-handler.mjs";
|
|
13
|
+
import { handleJournalExtract } from "./cli/journal-extract-handler.mjs";
|
|
13
14
|
import { handlePoll } from "./cli/poll-handler.mjs";
|
|
14
15
|
import { warnPrrcThreadIds, validateRequireSha, rejectPrrcMinimizeIds, } from "./cli/resolve-validators.mjs";
|
|
15
16
|
import { setupLog } from "./log/setup.mjs";
|
|
@@ -32,6 +33,12 @@ export async function main(argv) {
|
|
|
32
33
|
await handleLogFile(args.slice(1));
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
36
|
+
// Extraction deliberately precedes legacy warnings and logging: it is a local,
|
|
37
|
+
// GitHub/config/log-free read path for automation that already has a PR body.
|
|
38
|
+
if (subcommand === "journal" && args[1] === "extract") {
|
|
39
|
+
await handleJournalExtract(args.slice(2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
35
42
|
// The public command groups keep admin diagnostics out of the normal
|
|
36
43
|
// command namespace. Keep this before setupLog because log-file reads the
|
|
37
44
|
// log path and must not initialize/tee the log it is reporting.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AnnotationCacheOptions } from "../github/check-annotations.mts";
|
|
1
2
|
import type { CheckAnnotation, ClassifiedCheck, ShepherdReport, TriagedCheck } from "../types.mts";
|
|
2
3
|
export declare function checksWithActionableAnnotations(report: ShepherdReport): TriagedCheck[];
|
|
3
4
|
export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
@@ -8,7 +9,7 @@ export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
|
8
9
|
ignored: ClassifiedCheck[];
|
|
9
10
|
}, seenMap: Map<string, {
|
|
10
11
|
seenAt: number;
|
|
11
|
-
}>, prNumber: number): Promise<{
|
|
12
|
+
}>, prNumber: number, cacheOpts?: AnnotationCacheOptions): Promise<{
|
|
12
13
|
passing: ClassifiedCheck[];
|
|
13
14
|
failing: TriagedCheck[];
|
|
14
15
|
skipped: ClassifiedCheck[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchCheckRunAnnotations } from "../github/check-annotations.mjs";
|
|
1
|
+
import { fetchCheckRunAnnotations, } from "../github/check-annotations.mjs";
|
|
2
2
|
function shouldFetchCheckAnnotations(check) {
|
|
3
3
|
return check.id != null && check.status === "COMPLETED" && check.hasAnnotations === true;
|
|
4
4
|
}
|
|
@@ -11,7 +11,7 @@ export function checksWithActionableAnnotations(report) {
|
|
|
11
11
|
...(report.checks.ignored ?? []),
|
|
12
12
|
].filter((c) => c.conclusion !== "SUCCESS" && (c.annotations?.length ?? 0) > 0);
|
|
13
13
|
}
|
|
14
|
-
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber) {
|
|
14
|
+
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber, cacheOpts) {
|
|
15
15
|
const candidates = [
|
|
16
16
|
...buckets.failing,
|
|
17
17
|
...buckets.passing,
|
|
@@ -19,7 +19,7 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber)
|
|
|
19
19
|
...buckets.filtered,
|
|
20
20
|
...buckets.ignored,
|
|
21
21
|
].filter(shouldFetchCheckAnnotations);
|
|
22
|
-
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber);
|
|
22
|
+
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber, cacheOpts);
|
|
23
23
|
const byId = new Map(annotated.flatMap((c) => (c.id != null ? [[c.id, c]] : [])));
|
|
24
24
|
const apply = (list) => list.map((c) => {
|
|
25
25
|
if (c.id == null)
|
|
@@ -35,20 +35,20 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber)
|
|
|
35
35
|
ignored: apply(buckets.ignored),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber) {
|
|
38
|
+
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber, cacheOpts) {
|
|
39
39
|
const checksWithAnnotations = [];
|
|
40
40
|
for (const check of checks) {
|
|
41
41
|
// eslint-disable-next-line no-await-in-loop
|
|
42
|
-
checksWithAnnotations.push(await attachForCheck(check, seenMap, prNumber));
|
|
42
|
+
checksWithAnnotations.push(await attachForCheck(check, seenMap, prNumber, cacheOpts));
|
|
43
43
|
}
|
|
44
44
|
return checksWithAnnotations;
|
|
45
45
|
}
|
|
46
|
-
async function attachForCheck(check, seenMap, prNumber) {
|
|
46
|
+
async function attachForCheck(check, seenMap, prNumber, cacheOpts) {
|
|
47
47
|
if (check.id == null)
|
|
48
48
|
return check;
|
|
49
49
|
let annotations;
|
|
50
50
|
try {
|
|
51
|
-
annotations = await fetchCheckRunAnnotations(check.id);
|
|
51
|
+
annotations = await fetchCheckRunAnnotations(check.id, cacheOpts);
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
54
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -9,15 +9,15 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
9
9
|
return "FAILING";
|
|
10
10
|
if (verdict.anyInProgress)
|
|
11
11
|
return "IN_PROGRESS";
|
|
12
|
-
// BLOCKED with no remaining shepherd work —
|
|
12
|
+
// BLOCKED with no remaining shepherd work — enter ready-delay regardless of why GitHub
|
|
13
13
|
// is BLOCKED (review pending, insufficient approvals, branch-protection rule, etc.).
|
|
14
14
|
// Requires hasChecks so that a PR with zero relevant checks (CI never started, or all
|
|
15
15
|
// filtered/skipped) doesn't prematurely trigger READY before any check has reported.
|
|
16
16
|
// Exception: UNSTABLE with ignored checks — UNSTABLE means only non-required checks are
|
|
17
|
-
// pending/failing, and if those are all ignored the
|
|
17
|
+
// pending/failing, and if those are all ignored the ready state is safe even with no other checks.
|
|
18
18
|
// BLOCKED is excluded from the ignoredNames extension: BLOCKED can mean required checks haven't
|
|
19
|
-
// started, and
|
|
20
|
-
// blockingBotReviewInProgress is still excluded — a bot review is
|
|
19
|
+
// started, and entering READY prematurely there risks a broken merge attempt.
|
|
20
|
+
// blockingBotReviewInProgress is still excluded — a bot review is Shepherd work, not a READY state.
|
|
21
21
|
const hasRelevantPassingChecks = verdict.hasChecks || (mergeStatus.status === "UNSTABLE" && verdict.ignoredNames.length > 0);
|
|
22
22
|
if (verdict.allPassed &&
|
|
23
23
|
hasRelevantPassingChecks &&
|
package/bin/commands/check.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
|
|
|
16
16
|
import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, } from "../comments/review-visibility.mjs";
|
|
17
17
|
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
18
18
|
import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
|
|
19
|
-
import { normalizeBotUsernames } from "../comments/authors.mjs";
|
|
19
|
+
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
20
|
+
import { buildThreadMutationRouting } from "./iterate/thread-mutation-routing.mjs";
|
|
20
21
|
import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
21
22
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
22
23
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
@@ -27,6 +28,7 @@ export async function runCheck(opts) {
|
|
|
27
28
|
if (prNumber === null) {
|
|
28
29
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
29
30
|
}
|
|
31
|
+
const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
|
|
30
32
|
const config = loadConfig();
|
|
31
33
|
const paginateApprovedReviews = config.iterate.minimizeApprovals;
|
|
32
34
|
const result = await fetchPrBatch(prNumber, repo, { paginateApprovedReviews });
|
|
@@ -40,7 +42,7 @@ export async function runCheck(opts) {
|
|
|
40
42
|
}
|
|
41
43
|
const startupFailureChecks = result.checkSuitesComplete
|
|
42
44
|
? []
|
|
43
|
-
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
|
|
45
|
+
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber, stateKey);
|
|
44
46
|
const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
|
|
45
47
|
const classifiedPrChecks = classifyChecks(allChecks);
|
|
46
48
|
const latestRemoval = batchData.latestMergeQueueRemoval;
|
|
@@ -65,20 +67,31 @@ export async function runCheck(opts) {
|
|
|
65
67
|
const skipped = classifiedChecks.filter((c) => c.category === "skipped");
|
|
66
68
|
const filtered = classifiedChecks.filter((c) => c.category === "filtered");
|
|
67
69
|
const ignored = classifiedChecks.filter((c) => c.category === "ignored");
|
|
68
|
-
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
69
|
-
|
|
70
|
+
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
71
|
+
? await triageFailingChecks(failing, repo, stateKey)
|
|
72
|
+
: failing;
|
|
70
73
|
const seenMap = await loadSeenMap(stateKey);
|
|
71
74
|
const botUsernames = normalizeBotUsernames(config.botUsernames);
|
|
72
75
|
const ruleSet = await loadRules(discoverRuleFiles(getEffectiveCwd()));
|
|
73
76
|
const classifyIndex = buildClassifyIndex(ruleSet, batchData);
|
|
74
77
|
const partition = partitionBatch(classifyIndex, batchData);
|
|
75
|
-
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered, ignored }, seenMap, prNumber);
|
|
78
|
+
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered, ignored }, seenMap, prNumber, { stateKey, headSha: batchData.headRefOid });
|
|
76
79
|
const ignoredAnnotated = merged.ignored.filter((c) => (c.annotations?.length ?? 0) > 0);
|
|
77
80
|
const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized && !partition.suppressedCommentIds.has(c.id));
|
|
78
81
|
const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize !== true));
|
|
79
82
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate.minimizeComments, botUsernames);
|
|
80
83
|
const deniedRuleAutoResolveThreadIds = new Set(partition.ruleAutoResolveThreadIds.filter((id) => batchData.reviewThreads.find((thread) => thread.id === id)?.viewerCanResolve !== true));
|
|
81
|
-
const
|
|
84
|
+
const visibleThreadCandidates = batchData.reviewThreads.filter((t) => !partition.suppressedThreadIds.has(t.id) || deniedRuleAutoResolveThreadIds.has(t.id));
|
|
85
|
+
const threadMutationRouting = buildThreadMutationRouting(visibleThreadCandidates, botUsernames, partition.ruleAutoResolveThreadIds);
|
|
86
|
+
const replyThreadIds = new Set(threadMutationRouting.replyThreadIds);
|
|
87
|
+
const resolveThreadIds = new Set(threadMutationRouting.resolveThreadIds);
|
|
88
|
+
const repeatableThreadIds = new Set(visibleThreadCandidates
|
|
89
|
+
.filter((thread) => thread.path !== null &&
|
|
90
|
+
thread.line !== null &&
|
|
91
|
+
(!replyThreadIds.has(thread.id) || thread.viewerCanReply === true) &&
|
|
92
|
+
(!resolveThreadIds.has(thread.id) || thread.viewerCanResolve === true))
|
|
93
|
+
.map((thread) => thread.id));
|
|
94
|
+
const threadVisibility = classifyThreadVisibility(visibleThreadCandidates, seenMap, botUsernames, repeatableThreadIds);
|
|
82
95
|
const firstLookComments = minimizedCommentCandidates.flatMap((c) => {
|
|
83
96
|
const cls = classifyItem(c.id, c.body, seenMap);
|
|
84
97
|
if (cls === "unchanged")
|
|
@@ -101,7 +114,7 @@ export async function runCheck(opts) {
|
|
|
101
114
|
else
|
|
102
115
|
seenSummaries.push(r);
|
|
103
116
|
}
|
|
104
|
-
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames);
|
|
117
|
+
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames, batchData.viewerAuthorization?.viewerCanAdminister === true);
|
|
105
118
|
const approvedReviewVisibility = classifyReviewsForDisplay(batchData.approvedReviews, seenMap);
|
|
106
119
|
if (opts.persistSeen !== false) {
|
|
107
120
|
const successfulAnnotations = [
|
|
@@ -143,12 +156,21 @@ export async function runCheck(opts) {
|
|
|
143
156
|
ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize === true),
|
|
144
157
|
};
|
|
145
158
|
const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(authorizedPartition, opts.autoMinimizeSuppressed);
|
|
159
|
+
const visibleMutationThreadIds = new Set([...threadVisibility.activeThreads, ...threadVisibility.resolutionOnlyThreads].map((thread) => thread.id));
|
|
146
160
|
const ruleAutoResolveThreadIds = [
|
|
147
161
|
...authorizedRuleAutoResolveThreadIds,
|
|
148
|
-
...deniedRuleAutoResolveThreadIds,
|
|
162
|
+
...[...deniedRuleAutoResolveThreadIds].filter((id) => visibleMutationThreadIds.has(id)),
|
|
149
163
|
];
|
|
150
164
|
const changesRequestedReviews = changesRequestedReviewVisibility.visible;
|
|
151
|
-
const
|
|
165
|
+
const visibleChangesRequestedIds = new Set(changesRequestedReviews.map((review) => review.id));
|
|
166
|
+
const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((review) => {
|
|
167
|
+
if (partition.suppressedChangesRequestedIds.has(review.id))
|
|
168
|
+
return false;
|
|
169
|
+
const isBot = !isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames);
|
|
170
|
+
return (!isBot ||
|
|
171
|
+
batchData.viewerAuthorization?.viewerCanAdminister === true ||
|
|
172
|
+
visibleChangesRequestedIds.has(review.id));
|
|
173
|
+
}).length;
|
|
152
174
|
const approvedReviews = approvedReviewVisibility.visible;
|
|
153
175
|
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount);
|
|
154
176
|
if (status === "READY" && !didRefreshMergeability) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { loadConfig } from "../../config/load.mjs";
|
|
2
|
+
import { summarizeApiTelemetry } from "../../github/api-telemetry.mjs";
|
|
3
|
+
import { evaluateWorktreeGraphqlQuotaWarning } from "../../state/graphql-quota-warnings.mjs";
|
|
4
|
+
import { buildQuotaAwareContinuation } from "../../quota-warning.mjs";
|
|
5
|
+
function shouldWarn(result) {
|
|
6
|
+
return ["wait", "mark_ready", "merge", "fix_code"].includes(result.action);
|
|
7
|
+
}
|
|
8
|
+
export async function attachApiUsage(result, persistWarning, preservePersistedWarning = false) {
|
|
9
|
+
const apiUsage = summarizeApiTelemetry();
|
|
10
|
+
if (apiUsage === undefined)
|
|
11
|
+
return result;
|
|
12
|
+
let quotaWarning = preservePersistedWarning ? result.quotaWarning : undefined;
|
|
13
|
+
if (quotaWarning === undefined && apiUsage.graphql !== undefined && shouldWarn(result)) {
|
|
14
|
+
const [owner, repo] = result.repo.split("/");
|
|
15
|
+
if (owner && repo) {
|
|
16
|
+
quotaWarning = await evaluateWorktreeGraphqlQuotaWarning({ owner, repo }, loadConfig().watch.graphqlQuotaWarnings, apiUsage.graphql, persistWarning);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const { quotaWarning: _deferredWarning, ...baseResult } = result;
|
|
20
|
+
const withWarning = {
|
|
21
|
+
...baseResult,
|
|
22
|
+
apiUsage,
|
|
23
|
+
...(quotaWarning !== undefined && { quotaWarning }),
|
|
24
|
+
};
|
|
25
|
+
if (quotaWarning === undefined || withWarning.action !== "fix_code")
|
|
26
|
+
return withWarning;
|
|
27
|
+
const instructions = [...withWarning.fix.instructions];
|
|
28
|
+
const completion = instructions.at(-1);
|
|
29
|
+
if (completion !== undefined) {
|
|
30
|
+
if (/\[FIX_CODE\].*non-terminal/i.test(completion)) {
|
|
31
|
+
instructions[instructions.length - 1] = buildQuotaAwareContinuation(quotaWarning, "`[FIX_CODE]` is non-terminal. After completing these steps,");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { ...withWarning, fix: { ...withWarning.fix, instructions } };
|
|
35
|
+
}
|
|
@@ -30,4 +30,4 @@ export declare function buildBehindBaseHintInstruction(baseBranch: string, hint:
|
|
|
30
30
|
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
31
31
|
/** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
|
|
32
32
|
export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
|
|
33
|
-
export declare function buildFixCompletionInstruction(checks: AgentCheck[],
|
|
33
|
+
export declare function buildFixCompletionInstruction(checks: AgentCheck[], hasConflicts?: boolean, hasShaGatedReviewMutations?: boolean): string;
|
|
@@ -44,7 +44,7 @@ export function buildResolveCommandInstruction(resolveCommand) {
|
|
|
44
44
|
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked viewer-authored human thread is emitted resolve-only, not for another reply.");
|
|
45
45
|
}
|
|
46
46
|
if (resolveCommand.requiresHeadSha) {
|
|
47
|
-
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code,
|
|
47
|
+
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.");
|
|
48
48
|
}
|
|
49
49
|
if (resolveCommand.requiresDismissMessage) {
|
|
50
50
|
instructions.push("Replace `$DISMISS_MESSAGE` with one sentence describing what changed.");
|
|
@@ -58,32 +58,27 @@ export function buildFailingCheckInstructions(checks) {
|
|
|
58
58
|
return [];
|
|
59
59
|
const hasBare = checks.some((c) => !c.runId && !c.detailsUrl);
|
|
60
60
|
const hasTriageable = checks.some((c) => c.runId || c.detailsUrl);
|
|
61
|
+
const hasRerunAuthorized = checks.some((c) => c.rerunCommand);
|
|
61
62
|
const instructions = [];
|
|
62
63
|
if (hasTriageable) {
|
|
63
64
|
instructions.push('Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.');
|
|
64
65
|
}
|
|
66
|
+
if (hasRerunAuthorized) {
|
|
67
|
+
instructions.push('A `[rerun authorized]` check includes a `rerun:` command. See "CI failure triage" in the pr-shepherd skill for which conclusions warrant a rerun versus a code fix.');
|
|
68
|
+
}
|
|
65
69
|
if (hasBare) {
|
|
66
|
-
instructions.push("For each `(no runId)` failure,
|
|
70
|
+
instructions.push("For each `(no runId)` failure, preserve the displayed metadata; Shepherd will escalate when no other autonomous work remains.");
|
|
67
71
|
}
|
|
68
72
|
return instructions;
|
|
69
73
|
}
|
|
70
|
-
export function buildFixCompletionInstruction(checks,
|
|
71
|
-
if (
|
|
72
|
-
return "`[FIX_CODE]`
|
|
73
|
-
}
|
|
74
|
-
const hasUninspectableFailure = checks.some((check) => !check.runId && !check.detailsUrl);
|
|
75
|
-
const hasCiAuthorizationHandoff = checks.some((check) => check.conclusion === "CANCELLED" ||
|
|
76
|
-
check.conclusion === "STARTUP_FAILURE" ||
|
|
77
|
-
(check.runId === null && Boolean(check.detailsUrl)) ||
|
|
78
|
-
(check.runId !== null && !check.logExcerpt?.trim()));
|
|
79
|
-
if (hasUninspectableFailure) {
|
|
80
|
-
return "`[FIX_CODE]` requires a human handoff for an uninspectable failing check. Stop polling after escalating, and resume only after human direction.";
|
|
81
|
-
}
|
|
82
|
-
if (hasCiAuthorizationHandoff) {
|
|
83
|
-
return "`[FIX_CODE]` requires a human handoff for a failing check with no authorized follow-up action. Stop polling after escalating, and resume only after human direction.";
|
|
84
|
-
}
|
|
74
|
+
export function buildFixCompletionInstruction(checks, hasConflicts = false, hasShaGatedReviewMutations = false) {
|
|
75
|
+
if (hasConflicts)
|
|
76
|
+
return "`[FIX_CODE]` is non-terminal: resolve the conflicts, commit, push to the PR head branch, then iterate again with the same options.";
|
|
85
77
|
if (hasShaGatedReviewMutations) {
|
|
86
|
-
return "`[FIX_CODE]` is
|
|
78
|
+
return "`[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run the review mutations using the pushed commit SHA and iterate again with the same options; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
|
|
79
|
+
}
|
|
80
|
+
if (checks.some((check) => check.rerunCommand)) {
|
|
81
|
+
return "`[FIX_CODE]` is non-terminal. Run any warranted reruns for `[rerun authorized]` checks (or apply code fixes for real failures), then iterate again with the same options to continue.";
|
|
87
82
|
}
|
|
88
83
|
return "`[FIX_CODE]` is non-terminal. After completing these steps, iterate again with the same options to continue.";
|
|
89
84
|
}
|
|
@@ -67,7 +67,8 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
67
67
|
const canResolve = new Set(authorizationThreads
|
|
68
68
|
.filter((thread) => thread.viewerCanResolve === true)
|
|
69
69
|
.map((thread) => thread.id));
|
|
70
|
-
const
|
|
70
|
+
const pairedResolveIds = new Set(routed.pairedResolveThreadIds);
|
|
71
|
+
const replyThreadIds = routed.replyThreadIds.filter((id) => canReply.has(id) && (!pairedResolveIds.has(id) || canResolve.has(id)));
|
|
71
72
|
// Viewer-authored human resolves stay paired with an authorized reply. Marker-ended
|
|
72
73
|
// viewer-authored retries and bot/non-human resolves need only resolve authorization.
|
|
73
74
|
const pairedResolveThreadIds = routed.pairedResolveThreadIds.filter((id) => canReply.has(id) && canResolve.has(id));
|