pr-shepherd 0.45.0 → 0.46.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/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -9
- package/bin/api.d.mts +2 -2
- package/bin/checks/conclusions.d.mts +0 -16
- package/bin/checks/conclusions.mjs +0 -27
- package/bin/cli/fix-formatter.mjs +10 -4
- package/bin/cli/help-command-pages.d.mts +16 -5
- package/bin/cli/help-command-pages.mjs +16 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -1
- package/bin/cli/help.d.mts +17 -6
- package/bin/cli/help.mjs +2 -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 +7 -2
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/journal-extract-handler.d.mts +2 -0
- package/bin/cli/journal-extract-handler.mjs +48 -0
- package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -6
- 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-status.mjs +4 -4
- package/bin/commands/check.mjs +25 -5
- package/bin/commands/iterate/api-usage.mjs +1 -8
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +8 -32
- package/bin/commands/iterate/classify.mjs +2 -1
- package/bin/commands/iterate/escalate.mjs +63 -9
- package/bin/commands/iterate/fix-code.mjs +83 -80
- package/bin/commands/iterate/index.mjs +2 -2
- package/bin/commands/iterate/merge-state.mjs +8 -37
- package/bin/commands/iterate/merge.mjs +1 -0
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +16 -13
- package/bin/commands/journal/index.d.mts +1 -0
- package/bin/commands/journal/index.mjs +1 -10
- package/bin/commands/mark-files-as-viewed.d.mts +2 -1
- package/bin/commands/mark-files-as-viewed.mjs +116 -4
- package/bin/commands/poll.mjs +1 -4
- package/bin/commands/resolve-mutate.mjs +17 -40
- package/bin/comments/resolve.d.mts +4 -0
- package/bin/comments/resolve.mjs +10 -4
- 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/github/client.d.mts +1 -2
- package/bin/github/client.mjs +1 -4
- package/bin/github/gql/get-pr-body.gql +0 -1
- package/bin/mcp/server.mjs +2 -2
- package/bin/pr-reference.d.mts +1 -1
- package/bin/pr-reference.mjs +1 -1
- package/bin/types/escalate.d.mts +5 -3
- package/bin/types/report.d.mts +1 -1
- 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/mark-files-as-viewed/SKILL.md +2 -2
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +21 -15
|
@@ -7,45 +7,16 @@ export function buildReadyMergeResult(enabled, readyElapsed, base, report) {
|
|
|
7
7
|
return null;
|
|
8
8
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
9
9
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
10
|
-
// A merge queue is enrolled the same way auto-merge is granted: enabling
|
|
11
|
-
// auto-merge on a queue-required PR is what adds it to the queue, so
|
|
12
|
-
// viewerCanEnableAutoMerge authorizes both the plain-merge and enqueue paths.
|
|
13
|
-
if (report.viewerAuthorization?.viewerCanEnableAutoMerge === true) {
|
|
14
|
-
return {
|
|
15
|
-
...base,
|
|
16
|
-
action: "merge",
|
|
17
|
-
merge: buildMergeCommandPlan({
|
|
18
|
-
pr: report.pr,
|
|
19
|
-
repo: report.repo,
|
|
20
|
-
nodeId: report.nodeId,
|
|
21
|
-
headSha: report.headSha ?? "unknown",
|
|
22
|
-
queue,
|
|
23
|
-
}),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
const authorizationEscalateBase = {
|
|
27
|
-
triggers: ["authorization-required"],
|
|
28
|
-
unresolvedThreads: [],
|
|
29
|
-
ambiguousComments: [],
|
|
30
|
-
changesRequestedReviews: [],
|
|
31
|
-
authorization: [
|
|
32
|
-
{
|
|
33
|
-
action: "merge-or-enqueue",
|
|
34
|
-
targetIds: [report.nodeId],
|
|
35
|
-
reason: "denied-or-unverifiable",
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
suggestion: buildEscalateSuggestion(["authorization-required"]),
|
|
39
|
-
};
|
|
40
10
|
return {
|
|
41
11
|
...base,
|
|
42
|
-
action: "
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
12
|
+
action: "merge",
|
|
13
|
+
merge: buildMergeCommandPlan({
|
|
14
|
+
pr: report.pr,
|
|
15
|
+
repo: report.repo,
|
|
16
|
+
nodeId: report.nodeId,
|
|
17
|
+
headSha: report.headSha ?? "unknown",
|
|
18
|
+
queue,
|
|
19
|
+
}),
|
|
49
20
|
};
|
|
50
21
|
}
|
|
51
22
|
export async function handleActiveMergeState(input) {
|
|
@@ -2,4 +2,4 @@ import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, Fir
|
|
|
2
2
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
3
3
|
export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
4
4
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
5
|
-
isBehind?: boolean, viewerCanUpdate?: boolean
|
|
5
|
+
isBehind?: boolean, viewerCanUpdate?: boolean): string[];
|
|
@@ -11,8 +11,10 @@ export function renderResolveCommand(rc) {
|
|
|
11
11
|
return renderShellCommand(parts);
|
|
12
12
|
}
|
|
13
13
|
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
14
|
-
isBehind = false, viewerCanUpdate = false
|
|
14
|
+
isBehind = false, viewerCanUpdate = false) {
|
|
15
15
|
const instructions = [];
|
|
16
|
+
const locatedThreads = threads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
17
|
+
const unlocatedThreads = threads.filter((thread) => thread.path === null || thread.line === null);
|
|
16
18
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
17
19
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
18
20
|
const hasNonConflictHints = threads.length > 0 ||
|
|
@@ -23,8 +25,10 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
23
25
|
// Start with interpretation. The agent decides what raw feedback warrants a code change.
|
|
24
26
|
if (hasNonConflictHints) {
|
|
25
27
|
const actionableSections = [];
|
|
26
|
-
if (
|
|
28
|
+
if (locatedThreads.length > 0)
|
|
27
29
|
actionableSections.push("`## Review threads`");
|
|
30
|
+
if (unlocatedThreads.length > 0)
|
|
31
|
+
actionableSections.push("`## Unlocated review threads (logged once — no mutation)`");
|
|
28
32
|
if (actionableComments.length > 0)
|
|
29
33
|
actionableSections.push("`## Actionable comments`");
|
|
30
34
|
if (failingChecks.length > 0)
|
|
@@ -53,17 +57,20 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
53
57
|
if (editedTotal > 0) {
|
|
54
58
|
instructions.push("Read every item marked `[edited since first look]`, including edited summaries and edited first-look bullets, before deciding whether to resolve a matching thread.");
|
|
55
59
|
}
|
|
60
|
+
if (unlocatedThreads.length > 0) {
|
|
61
|
+
instructions.push("Acknowledge each item under `## Unlocated review threads (logged once — no mutation)`. Shepherd cannot route a code fix or review mutation without a path and line; the unchanged item will be skipped on later ticks.");
|
|
62
|
+
}
|
|
56
63
|
// GitHub exposes no exact viewer capability for workflow-run cancellation, so the
|
|
57
64
|
// informational run lists never produce a cancellation recommendation.
|
|
58
65
|
void inProgressRunIds;
|
|
59
66
|
void cancelledCount;
|
|
60
|
-
const hasSuggestions =
|
|
67
|
+
const hasSuggestions = locatedThreads.some((t) => t.suggestion);
|
|
61
68
|
if (hasSuggestions)
|
|
62
69
|
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
63
|
-
if (
|
|
70
|
+
if (locatedThreads.length > 0 || actionableComments.length > 0) {
|
|
64
71
|
// Actionable comments carry no file/line location (unlike threads), so "referenced above"
|
|
65
72
|
// is only accurate when threads are present.
|
|
66
|
-
const filesRef =
|
|
73
|
+
const filesRef = locatedThreads.length > 0 ? "each file referenced above" : "the relevant files";
|
|
67
74
|
instructions.push(`Apply every warranted review fix in ${filesRef}.`);
|
|
68
75
|
}
|
|
69
76
|
if (resolutionOnlyThreads.length > 0) {
|
|
@@ -81,14 +88,10 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
81
88
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
82
89
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
83
90
|
if (hasConflicts) {
|
|
84
|
-
instructions.push(
|
|
85
|
-
? `Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`
|
|
86
|
-
: `Commit any remaining conflict-resolution changes${mutationSuffix}.`);
|
|
91
|
+
instructions.push(`Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`);
|
|
87
92
|
}
|
|
88
93
|
else if (hasNonConflictHints) {
|
|
89
|
-
instructions.push(
|
|
90
|
-
? "If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue with the remaining steps."
|
|
91
|
-
: "If you changed code, commit any remaining changes, then stop and hand off for a push whose authorization is established outside Shepherd; do not run the remaining review mutations or iterate until the remote PR head changes. Shepherd cannot verify the Git credential's push authorization. If you did not change code, do not commit and continue with the remaining steps.");
|
|
94
|
+
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue with the remaining steps.");
|
|
92
95
|
}
|
|
93
96
|
if (viewerCanUpdate &&
|
|
94
97
|
(hasReviewMutations ||
|
|
@@ -100,7 +103,7 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
100
103
|
}
|
|
101
104
|
if (resolveOnlyCommand?.hasMutations)
|
|
102
105
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
103
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand
|
|
104
|
-
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha
|
|
106
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand));
|
|
107
|
+
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
105
108
|
return instructions;
|
|
106
109
|
}
|
|
@@ -13,6 +13,7 @@ export interface JournalResult {
|
|
|
13
13
|
sectionExisted: boolean;
|
|
14
14
|
dryRun: boolean;
|
|
15
15
|
previewBody?: string;
|
|
16
|
+
/** @deprecated Explicit journal requests now rely on GitHub's mutation response. */
|
|
16
17
|
authorizationSkipped?: "denied-or-unverifiable";
|
|
17
18
|
}
|
|
18
19
|
/** @deprecated Hidden implementation for standalone `journal`; use `apply journal`. */
|
|
@@ -12,17 +12,8 @@ export async function runJournal(opts) {
|
|
|
12
12
|
if (!prNumber) {
|
|
13
13
|
throw new Error("PR number is required: no PR number provided and none found for current branch");
|
|
14
14
|
}
|
|
15
|
-
const { nodeId, body
|
|
15
|
+
const { nodeId, body } = await getPullRequestBody(prNumber, owner, name);
|
|
16
16
|
const { body: newBody, mutated, sectionExisted } = appendJournalItem(body, item);
|
|
17
|
-
if (mutated && !opts.dryRun && viewerCanUpdate !== true) {
|
|
18
|
-
return {
|
|
19
|
-
prNumber,
|
|
20
|
-
mutated: false,
|
|
21
|
-
sectionExisted,
|
|
22
|
-
dryRun: false,
|
|
23
|
-
authorizationSkipped: "denied-or-unverifiable",
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
17
|
if (mutated && !opts.dryRun) {
|
|
27
18
|
await updatePullRequestBody(nodeId, newBody);
|
|
28
19
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ResolveRateLimitStop } from "../comments/rate-limit.mts";
|
|
2
2
|
import type { GlobalOptions } from "../types.mts";
|
|
3
3
|
export interface MarkFilesAsViewedOptions extends GlobalOptions {
|
|
4
4
|
prNumber?: number;
|
|
@@ -21,6 +21,7 @@ export interface MarkFilesAsViewedResult {
|
|
|
21
21
|
errors: string[];
|
|
22
22
|
rateLimit?: ResolveRateLimitStop;
|
|
23
23
|
unmarkedPaths?: string[];
|
|
24
|
+
/** @deprecated Explicit file-view requests are attempted; GitHub authorizes the mutation. */
|
|
24
25
|
authorizationSkipped?: "unverifiable";
|
|
25
26
|
}
|
|
26
27
|
/** @deprecated Hidden implementation for `mark-files-as-viewed`; use `apply files`. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
|
-
import { graphql, getCurrentPrNumber, getRepoInfo } from "../github/client.mjs";
|
|
2
|
+
import { graphql, graphqlWithRateLimit, getCurrentPrNumber, getRepoInfo, } from "../github/client.mjs";
|
|
3
3
|
import { paginateForward } from "../github/pagination.mjs";
|
|
4
|
+
import { isRateLimitMessage, rateLimitFromError, rateLimitFromGraphQlResult, } from "../comments/rate-limit.mjs";
|
|
4
5
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
5
6
|
const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) {
|
|
6
7
|
_shepherdRateLimit: rateLimit {
|
|
@@ -29,6 +30,8 @@ const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr
|
|
|
29
30
|
}
|
|
30
31
|
}`;
|
|
31
32
|
const TEST_FILE_RE = /(^|\/)(tests?|__tests__|spec)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|_tests?\.rs$|(^|\/)tests?\.rs$/i;
|
|
33
|
+
// Keep mutation batches small so rate-limit stops leave an ordered pending list.
|
|
34
|
+
const MARK_FILES_CHUNK_SIZE = 10;
|
|
32
35
|
/** @deprecated Hidden implementation for `mark-files-as-viewed`; use `apply files`. */
|
|
33
36
|
export async function runMarkFilesAsViewed(opts) {
|
|
34
37
|
const repo = opts.targetRepository ?? (await getRepoInfo());
|
|
@@ -58,12 +61,121 @@ export async function runMarkFilesAsViewed(opts) {
|
|
|
58
61
|
missingPaths: selected.missingPaths,
|
|
59
62
|
unmatchedSelectors: selected.unmatchedSelectors,
|
|
60
63
|
errors: [],
|
|
61
|
-
...(selected.pathsToMark.length > 0 && { authorizationSkipped: "unverifiable" }),
|
|
62
64
|
};
|
|
63
|
-
|
|
64
|
-
// viewerCanEditFiles describe different operations, so this command fails closed.
|
|
65
|
+
await markFilesAsViewed(fetched.pullRequestId, selected.pathsToMark, result);
|
|
65
66
|
return result;
|
|
66
67
|
}
|
|
68
|
+
async function markFilesAsViewed(pullRequestId, paths, result) {
|
|
69
|
+
for (let offset = 0; offset < paths.length; offset += MARK_FILES_CHUNK_SIZE) {
|
|
70
|
+
const chunk = paths.slice(offset, offset + MARK_FILES_CHUNK_SIZE);
|
|
71
|
+
// eslint-disable-next-line no-await-in-loop
|
|
72
|
+
const pendingPaths = await markFilesChunk(pullRequestId, chunk, result, offset + MARK_FILES_CHUNK_SIZE < paths.length);
|
|
73
|
+
if (pendingPaths === null)
|
|
74
|
+
continue;
|
|
75
|
+
const unmarkedPaths = [...pendingPaths, ...paths.slice(offset + chunk.length)];
|
|
76
|
+
if (unmarkedPaths.length > 0)
|
|
77
|
+
result.unmarkedPaths = unmarkedPaths;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Returns pending paths when a rate limit stops further mutation batches. */
|
|
82
|
+
async function markFilesChunk(pullRequestId, paths, result, hasPendingAfter) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await graphqlWithRateLimit(buildMarkFilesMutation(pullRequestId, paths), {}, { allowPartialData: true });
|
|
85
|
+
const errors = (response.errors ?? []);
|
|
86
|
+
const rateLimit = rateLimitFromGraphQlResult(errors.map((error) => error.message), {
|
|
87
|
+
rateLimit: response.rateLimit,
|
|
88
|
+
retryAfterSeconds: response.retryAfterSeconds,
|
|
89
|
+
stopOnZeroRemaining: hasPendingAfter,
|
|
90
|
+
});
|
|
91
|
+
const classifiedErrors = classifyMarkFileErrors(errors, paths.length);
|
|
92
|
+
const pendingPaths = recordMarkFileResults(paths, response.data, errors, classifiedErrors, rateLimit, result);
|
|
93
|
+
return completeMarkFilesChunk(rateLimit, pendingPaths, result);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return recordMarkFilesTransportError(error, paths, result);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function classifyMarkFileErrors(errors, pathCount) {
|
|
100
|
+
const aliasesWithNonRateErrors = new Set();
|
|
101
|
+
const aliasesWithRateLimitErrors = new Set();
|
|
102
|
+
const unscopedNonRateMessages = [];
|
|
103
|
+
for (const error of errors) {
|
|
104
|
+
const aliasIndex = markFileErrorAliasIndex(error);
|
|
105
|
+
if (aliasIndex === undefined || aliasIndex >= pathCount) {
|
|
106
|
+
if (!isRateLimitMessage(error.message))
|
|
107
|
+
unscopedNonRateMessages.push(error.message);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (isRateLimitMessage(error.message))
|
|
111
|
+
aliasesWithRateLimitErrors.add(aliasIndex);
|
|
112
|
+
else
|
|
113
|
+
aliasesWithNonRateErrors.add(aliasIndex);
|
|
114
|
+
}
|
|
115
|
+
return { aliasesWithNonRateErrors, aliasesWithRateLimitErrors, unscopedNonRateMessages };
|
|
116
|
+
}
|
|
117
|
+
function recordMarkFileResults(paths, data, errors, classifiedErrors, rateLimit, result) {
|
|
118
|
+
const pendingPaths = [];
|
|
119
|
+
for (let index = 0; index < paths.length; index += 1) {
|
|
120
|
+
const path = paths[index];
|
|
121
|
+
if (data[`f${index}`] != null) {
|
|
122
|
+
result.markedPaths.push(path);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const messages = errorsForMarkFileAlias(errors, index);
|
|
126
|
+
const hasRateLimitError = classifiedErrors.aliasesWithRateLimitErrors.has(index);
|
|
127
|
+
const nonRateMessages = classifiedErrors.aliasesWithNonRateErrors.has(index)
|
|
128
|
+
? messages.filter((message) => !isRateLimitMessage(message))
|
|
129
|
+
: classifiedErrors.unscopedNonRateMessages;
|
|
130
|
+
if (nonRateMessages.length > 0) {
|
|
131
|
+
for (const message of nonRateMessages)
|
|
132
|
+
result.errors.push(`${path}: ${message}`);
|
|
133
|
+
}
|
|
134
|
+
else if (!rateLimit) {
|
|
135
|
+
result.errors.push(`${path}: ${messages[0] ?? "mark returned null"}`);
|
|
136
|
+
}
|
|
137
|
+
if (rateLimit && (hasRateLimitError || nonRateMessages.length === 0))
|
|
138
|
+
pendingPaths.push(path);
|
|
139
|
+
}
|
|
140
|
+
return pendingPaths;
|
|
141
|
+
}
|
|
142
|
+
function completeMarkFilesChunk(rateLimit, pendingPaths, result) {
|
|
143
|
+
if (!rateLimit)
|
|
144
|
+
return null;
|
|
145
|
+
result.errors.push(`rate limit: ${rateLimit.message}`);
|
|
146
|
+
result.rateLimit = rateLimit;
|
|
147
|
+
return pendingPaths;
|
|
148
|
+
}
|
|
149
|
+
function recordMarkFilesTransportError(error, paths, result) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
const stop = rateLimitFromError(error, message);
|
|
152
|
+
if (stop) {
|
|
153
|
+
result.errors.push(`rate limit: ${stop.message}`);
|
|
154
|
+
result.rateLimit = stop;
|
|
155
|
+
return paths;
|
|
156
|
+
}
|
|
157
|
+
for (const path of paths)
|
|
158
|
+
result.errors.push(`${path}: ${message}`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function buildMarkFilesMutation(pullRequestId, paths) {
|
|
162
|
+
const operations = paths.map((path, index) => ` f${index}: markFileAsViewed(input: { pullRequestId: ${JSON.stringify(pullRequestId)}, path: ${JSON.stringify(path)} }) { clientMutationId }`);
|
|
163
|
+
return `mutation MarkFilesAsViewed {\n${operations.join("\n")}\n}`;
|
|
164
|
+
}
|
|
165
|
+
function markFileErrorAliasIndex(error) {
|
|
166
|
+
if (!Array.isArray(error.path))
|
|
167
|
+
return undefined;
|
|
168
|
+
const alias = error.path.find((part) => typeof part === "string" && /^f\d+$/.test(part));
|
|
169
|
+
if (typeof alias !== "string")
|
|
170
|
+
return undefined;
|
|
171
|
+
const index = Number.parseInt(alias.slice(1), 10);
|
|
172
|
+
return Number.isNaN(index) ? undefined : index;
|
|
173
|
+
}
|
|
174
|
+
function errorsForMarkFileAlias(errors, index) {
|
|
175
|
+
return errors
|
|
176
|
+
.filter((error) => markFileErrorAliasIndex(error) === index)
|
|
177
|
+
.map((error) => error.message);
|
|
178
|
+
}
|
|
67
179
|
async function fetchPullRequestFiles(pr, repo) {
|
|
68
180
|
const first = await graphql(FILES_QUERY, {
|
|
69
181
|
owner: repo.owner,
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -99,10 +99,7 @@ async function runPollCore(opts) {
|
|
|
99
99
|
pendingQuotaWarning !== undefined &&
|
|
100
100
|
!(lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) &&
|
|
101
101
|
!(debounceUntil !== null && !pastDebounce)) {
|
|
102
|
-
|
|
103
|
-
(lastResult.action === "fix_code" &&
|
|
104
|
-
lastResult.fix.instructions.some((instruction) => /stop polling|human direction/i.test(instruction)));
|
|
105
|
-
if (terminalOrHandoff) {
|
|
102
|
+
if (["cancel", "escalate"].includes(lastResult.action)) {
|
|
106
103
|
const { quotaWarning: _quotaWarning, ...withoutQuotaWarning } = lastResult;
|
|
107
104
|
lastResult = withoutQuotaWarning;
|
|
108
105
|
}
|
|
@@ -27,45 +27,24 @@ export async function runResolveMutate(opts) {
|
|
|
27
27
|
const humanReviewIds = new Set([...data.reviewSummaries, ...data.approvedReviews, ...data.changesRequestedReviews]
|
|
28
28
|
.filter((r) => isHumanAuthor(r) && !isConfiguredBotAuthor(r, botUsernames))
|
|
29
29
|
.map((r) => r.id));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
.filter((thread) => thread.viewerCanResolve === true)
|
|
35
|
-
.map((thread) => thread.id));
|
|
36
|
-
const minimizeAuthorizedIds = new Set([
|
|
37
|
-
...data.comments
|
|
38
|
-
.filter((comment) => comment.viewerCanMinimize === true)
|
|
39
|
-
.map((comment) => comment.id),
|
|
40
|
-
...data.reviewSummaries
|
|
41
|
-
.filter((review) => review.viewerCanMinimize === true)
|
|
42
|
-
.map((review) => review.id),
|
|
43
|
-
...data.approvedReviews
|
|
44
|
-
.filter((review) => review.viewerCanMinimize === true)
|
|
45
|
-
.map((review) => review.id),
|
|
46
|
-
]);
|
|
47
|
-
const requestedReplyIds = new Set((opts.replyThreadIds ?? []).filter((id) => replyAuthorizedIds.has(id)));
|
|
30
|
+
// Iterate uses viewer capability fields while deciding which commands to
|
|
31
|
+
// print. Once a caller explicitly runs apply, GitHub's mutation response is
|
|
32
|
+
// authoritative and this path must not second-guess that intent.
|
|
33
|
+
const requestedReplyIds = new Set(opts.replyThreadIds ?? []);
|
|
48
34
|
const allowedViewerHumanResolveIds = new Set(data.reviewThreads
|
|
49
35
|
.filter((thread) => isViewerAuthoredHuman(thread, botUsernames) &&
|
|
50
36
|
(requestedReplyIds.has(thread.id) || threadEndedByShepherd(thread)))
|
|
51
37
|
.map((thread) => thread.id));
|
|
52
|
-
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) =>
|
|
53
|
-
resolveAuthorizedIds.has(id));
|
|
38
|
+
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) => !humanThreadIds.has(id) || allowedViewerHumanResolveIds.has(id));
|
|
54
39
|
const skippedHumanResolves = (opts.resolveThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !allowedViewerHumanResolveIds.has(id));
|
|
55
|
-
const
|
|
56
|
-
!resolveAuthorizedIds.has(id));
|
|
57
|
-
const replyThreadIds = opts.replyThreadIds?.filter((id) => humanThreadIds.has(id) && replyAuthorizedIds.has(id));
|
|
40
|
+
const replyThreadIds = opts.replyThreadIds?.filter((id) => humanThreadIds.has(id));
|
|
58
41
|
const skippedNonHumanReplies = (opts.replyThreadIds ?? []).filter((id) => !humanThreadIds.has(id));
|
|
59
|
-
const
|
|
60
|
-
const minimizeCommentIds = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id) && minimizeAuthorizedIds.has(id));
|
|
42
|
+
const minimizeCommentIds = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id));
|
|
61
43
|
const skippedHumanMinimizes = (opts.minimizeCommentIds ?? []).filter((id) => humanCommentIds.has(id) || humanReviewIds.has(id));
|
|
62
|
-
const
|
|
63
|
-
const dismissReviewIds = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) &&
|
|
64
|
-
data.changesRequestedReviews.some((review) => review.id === id) &&
|
|
65
|
-
data.viewerAuthorization?.viewerCanAdminister === true);
|
|
44
|
+
const dismissReviewIds = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && data.changesRequestedReviews.some((review) => review.id === id));
|
|
66
45
|
const skippedHumanDismissals = (opts.dismissReviewIds ?? []).filter((id) => humanReviewIds.has(id));
|
|
67
|
-
const
|
|
68
|
-
const
|
|
46
|
+
const skippedIneligibleDismissals = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && !dismissReviewIds.includes(id));
|
|
47
|
+
const hasMutation = resolveThreadIds.length > 0 ||
|
|
69
48
|
(replyThreadIds?.length ?? 0) > 0 ||
|
|
70
49
|
minimizeCommentIds.length > 0 ||
|
|
71
50
|
dismissReviewIds.length > 0;
|
|
@@ -75,7 +54,7 @@ export async function runResolveMutate(opts) {
|
|
|
75
54
|
minimizeCommentIds,
|
|
76
55
|
dismissReviewIds,
|
|
77
56
|
dismissMessage: opts.dismissMessage,
|
|
78
|
-
requireSha:
|
|
57
|
+
requireSha: hasMutation ? opts.requireSha : undefined,
|
|
79
58
|
});
|
|
80
59
|
if (skippedHumanResolves.length > 0)
|
|
81
60
|
result.skippedHumanResolves = skippedHumanResolves;
|
|
@@ -85,14 +64,12 @@ export async function runResolveMutate(opts) {
|
|
|
85
64
|
result.skippedHumanDismissals = skippedHumanDismissals;
|
|
86
65
|
if (skippedNonHumanReplies.length > 0)
|
|
87
66
|
result.skippedNonHumanReplies = skippedNonHumanReplies;
|
|
88
|
-
if (
|
|
89
|
-
result.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (skippedUnauthorizedDismissals.length > 0)
|
|
95
|
-
result.skippedUnauthorizedDismissals = skippedUnauthorizedDismissals;
|
|
67
|
+
if (skippedIneligibleDismissals.length > 0) {
|
|
68
|
+
result.skippedDismissals = [
|
|
69
|
+
...(result.skippedDismissals ?? []),
|
|
70
|
+
...skippedIneligibleDismissals,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
96
73
|
if (opts.dismissMessage) {
|
|
97
74
|
const markedMessage = addPrShepherdMarker(opts.dismissMessage);
|
|
98
75
|
await Promise.all(result.repliedThreads.map((id) => {
|
|
@@ -12,9 +12,13 @@ export interface ResolveResult {
|
|
|
12
12
|
skippedHumanMinimizes?: string[];
|
|
13
13
|
skippedHumanDismissals?: string[];
|
|
14
14
|
skippedNonHumanReplies?: string[];
|
|
15
|
+
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
15
16
|
skippedUnauthorizedReplies?: string[];
|
|
17
|
+
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
16
18
|
skippedUnauthorizedResolves?: string[];
|
|
19
|
+
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
17
20
|
skippedUnauthorizedMinimizes?: string[];
|
|
21
|
+
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
18
22
|
skippedUnauthorizedDismissals?: string[];
|
|
19
23
|
rateLimit?: ResolveRateLimitStop;
|
|
20
24
|
unrepliedThreads?: string[];
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -24,6 +24,12 @@ function isCommentedDismissError(message) {
|
|
|
24
24
|
function dismissReviewNonDismissibleMessage(id) {
|
|
25
25
|
return `Not dismissed: ${id} is a COMMENTED review. Use --minimize-comment-ids instead; --dismiss-review-ids is only for CHANGES_REQUESTED reviews.`;
|
|
26
26
|
}
|
|
27
|
+
function mutationErrorMessage(errors, alias) {
|
|
28
|
+
const messages = errors
|
|
29
|
+
.filter((error) => Array.isArray(error.path) && error.path.includes(alias))
|
|
30
|
+
.map((error) => error.message);
|
|
31
|
+
return messages.length > 0 ? messages.join("; ") : undefined;
|
|
32
|
+
}
|
|
27
33
|
export async function applyResolveOptions(pr, repo, opts) {
|
|
28
34
|
const resolveThreadIds = dedupeIds(opts.resolveThreadIds ?? []);
|
|
29
35
|
const replyThreadIds = dedupeIds(opts.replyThreadIds ?? []);
|
|
@@ -168,21 +174,21 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
|
|
|
168
174
|
if (p?.comment?.id)
|
|
169
175
|
result.repliedThreads.push(id);
|
|
170
176
|
else if (!suppressCurrentChunkErrors)
|
|
171
|
-
result.errors.push(`${id}: reply returned null or comment not created`);
|
|
177
|
+
result.errors.push(`${id}: ${mutationErrorMessage(graphQlErrors, `p${i}`) ?? "reply returned null or comment not created"}`);
|
|
172
178
|
}
|
|
173
179
|
for (let i = 0; i < resolveIds.length; i++) {
|
|
174
180
|
const r = data[`r${i}`];
|
|
175
181
|
if (r?.thread?.isResolved === true)
|
|
176
182
|
result.resolvedThreads.push(resolveIds[i]);
|
|
177
183
|
else if (!suppressCurrentChunkErrors)
|
|
178
|
-
result.errors.push(`${resolveIds[i]}: resolve returned null or thread not resolved`);
|
|
184
|
+
result.errors.push(`${resolveIds[i]}: ${mutationErrorMessage(graphQlErrors, `r${i}`) ?? "resolve returned null or thread not resolved"}`);
|
|
179
185
|
}
|
|
180
186
|
for (let i = 0; i < minimizeIds.length; i++) {
|
|
181
187
|
const m = data[`m${i}`];
|
|
182
188
|
if (m?.minimizedComment?.isMinimized === true)
|
|
183
189
|
result.minimizedComments.push(minimizeIds[i]);
|
|
184
190
|
else if (!suppressCurrentChunkErrors)
|
|
185
|
-
result.errors.push(`${minimizeIds[i]}: minimize returned null or comment not minimized`);
|
|
191
|
+
result.errors.push(`${minimizeIds[i]}: ${mutationErrorMessage(graphQlErrors, `m${i}`) ?? "minimize returned null or comment not minimized"}`);
|
|
186
192
|
}
|
|
187
193
|
const singleDismiss = dismissIds.length === 1;
|
|
188
194
|
const commentedDismissErrorIndexes = new Set();
|
|
@@ -204,7 +210,7 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
|
|
|
204
210
|
else if (!suppressCurrentChunkErrors)
|
|
205
211
|
result.errors.push(commentedDismissErrorIndexes.has(i) || (singleDismiss && hasUnmappedCommentedDismissError)
|
|
206
212
|
? dismissReviewNonDismissibleMessage(dismissIds[i])
|
|
207
|
-
: `${dismissIds[i]}: dismiss returned null`);
|
|
213
|
+
: `${dismissIds[i]}: ${mutationErrorMessage(graphQlErrors, `d${i}`) ?? "dismiss returned null"}`);
|
|
208
214
|
}
|
|
209
215
|
if (rateLimitStop) {
|
|
210
216
|
result.errors.push(`rate limit: ${rateLimitStop.message}`);
|
|
@@ -24,5 +24,5 @@ export declare function classifyReviewsForDisplay(reviews: Review[], seenMap: Ma
|
|
|
24
24
|
*
|
|
25
25
|
* Human-authored CR reviews continue to flow through the standard seen-gate.
|
|
26
26
|
*/
|
|
27
|
-
export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames): ReviewVisibility;
|
|
27
|
+
export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames, repeatBotReviews?: boolean): ReviewVisibility;
|
|
28
28
|
export {};
|
|
@@ -31,7 +31,7 @@ export function classifyReviewsForDisplay(reviews, seenMap) {
|
|
|
31
31
|
*
|
|
32
32
|
* Human-authored CR reviews continue to flow through the standard seen-gate.
|
|
33
33
|
*/
|
|
34
|
-
export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botUsernames) {
|
|
34
|
+
export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botUsernames, repeatBotReviews = true) {
|
|
35
35
|
const visible = [];
|
|
36
36
|
const toMarkSeen = [];
|
|
37
37
|
for (const review of reviews) {
|
|
@@ -39,7 +39,8 @@ export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botU
|
|
|
39
39
|
const cls = classifyItem(review.id, review.body, seenMap);
|
|
40
40
|
if (isBot) {
|
|
41
41
|
if (cls === "unchanged") {
|
|
42
|
-
|
|
42
|
+
if (repeatBotReviews)
|
|
43
|
+
visible.push({ ...review, staleBotCr: true });
|
|
43
44
|
}
|
|
44
45
|
else if (cls === "edited") {
|
|
45
46
|
visible.push({ ...review, edited: true });
|
|
@@ -7,5 +7,5 @@ interface ThreadVisibility {
|
|
|
7
7
|
firstLookThreads: FirstLookThread[];
|
|
8
8
|
toMarkSeen: ReviewThread[];
|
|
9
9
|
}
|
|
10
|
-
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames): ThreadVisibility;
|
|
10
|
+
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames, repeatableThreadIds?: ReadonlySet<string>): ThreadVisibility;
|
|
11
11
|
export {};
|
|
@@ -19,14 +19,15 @@ function classifyFirstLookThread(thread, seenMap, firstLookStatus) {
|
|
|
19
19
|
return null;
|
|
20
20
|
return { ...visible, firstLookStatus };
|
|
21
21
|
}
|
|
22
|
-
export function classifyThreadVisibility(threads, seenMap, botUsernames = new Set()) {
|
|
22
|
+
export function classifyThreadVisibility(threads, seenMap, botUsernames = new Set(), repeatableThreadIds) {
|
|
23
|
+
const shouldRepeat = (thread) => repeatableThreadIds?.has(thread.id) ?? true;
|
|
23
24
|
const unresolvedThreads = threads.filter((t) => !t.isResolved);
|
|
24
25
|
const activeThreads = unresolvedThreads
|
|
25
26
|
.filter((t) => !t.isOutdated && !t.isMinimized)
|
|
26
27
|
.flatMap((t) => {
|
|
27
28
|
if (threadEndedByShepherd(t))
|
|
28
29
|
return [];
|
|
29
|
-
if (isConfiguredBotAuthor(t, botUsernames))
|
|
30
|
+
if (isConfiguredBotAuthor(t, botUsernames) && shouldRepeat(t))
|
|
30
31
|
return [t];
|
|
31
32
|
const visible = classifyVisibleThread(t, seenMap);
|
|
32
33
|
return visible ? [visible] : [];
|
|
@@ -40,7 +41,12 @@ export function classifyThreadVisibility(threads, seenMap, botUsernames = new Se
|
|
|
40
41
|
}
|
|
41
42
|
return t.isOutdated || t.isMinimized;
|
|
42
43
|
})
|
|
43
|
-
.
|
|
44
|
+
.flatMap((t) => {
|
|
45
|
+
const visible = classifyVisibleThread(t, seenMap);
|
|
46
|
+
if (visible)
|
|
47
|
+
return [visible];
|
|
48
|
+
return shouldRepeat(t) ? [t] : [];
|
|
49
|
+
});
|
|
44
50
|
const firstLookThreads = [
|
|
45
51
|
...threads.flatMap((t) => {
|
|
46
52
|
if (!t.isOutdated)
|
package/bin/github/client.d.mts
CHANGED
|
@@ -30,7 +30,6 @@ export declare function getPrHeadSha(pr: number, owner: string, name: string): P
|
|
|
30
30
|
export declare function getPullRequestBody(pr: number, owner: string, name: string): Promise<{
|
|
31
31
|
nodeId: string;
|
|
32
32
|
body: string;
|
|
33
|
-
viewerCanUpdate?: boolean;
|
|
34
33
|
}>;
|
|
35
34
|
/** Overwrites the PR body. */
|
|
36
35
|
export declare function updatePullRequestBody(pullRequestId: string, body: string): Promise<void>;
|
|
@@ -38,7 +37,7 @@ export declare function updatePullRequestBody(pullRequestId: string, body: strin
|
|
|
38
37
|
* Fetches PR state, `mergeable`, and `mergeStateStatus` via the REST API.
|
|
39
38
|
*
|
|
40
39
|
* Used when the GraphQL API returns `UNKNOWN` for mergeability or before a
|
|
41
|
-
* READY
|
|
40
|
+
* READY state. The same response carries state so a concurrent merge or
|
|
42
41
|
* close can supersede the earlier GraphQL snapshot without another request.
|
|
43
42
|
*/
|
|
44
43
|
export declare function getMergeableState(pr: number, owner: string, repo: string): Promise<{
|
package/bin/github/client.mjs
CHANGED
|
@@ -80,9 +80,6 @@ export async function getPullRequestBody(pr, owner, name) {
|
|
|
80
80
|
return {
|
|
81
81
|
nodeId: pullRequest.id,
|
|
82
82
|
body: pullRequest.body ?? "",
|
|
83
|
-
...(pullRequest.viewerCanUpdate !== undefined && {
|
|
84
|
-
viewerCanUpdate: pullRequest.viewerCanUpdate,
|
|
85
|
-
}),
|
|
86
83
|
};
|
|
87
84
|
}
|
|
88
85
|
/** Overwrites the PR body. */
|
|
@@ -93,7 +90,7 @@ export async function updatePullRequestBody(pullRequestId, body) {
|
|
|
93
90
|
* Fetches PR state, `mergeable`, and `mergeStateStatus` via the REST API.
|
|
94
91
|
*
|
|
95
92
|
* Used when the GraphQL API returns `UNKNOWN` for mergeability or before a
|
|
96
|
-
* READY
|
|
93
|
+
* READY state. The same response carries state so a concurrent merge or
|
|
97
94
|
* close can supersede the earlier GraphQL snapshot without another request.
|
|
98
95
|
*/
|
|
99
96
|
export async function getMergeableState(pr, owner, repo) {
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const reviewMutationsOperationSchema = z.object({
|
|
|
41
41
|
const markFilesViewedOperationSchema = z.object({
|
|
42
42
|
type: z.literal("mark_files_viewed"),
|
|
43
43
|
files: z.array(z.string().min(1)).optional(),
|
|
44
|
-
tests: z.boolean().optional().describe("Select changed test files
|
|
44
|
+
tests: z.boolean().optional().describe("Select changed test files to mark as viewed."),
|
|
45
45
|
matchPatterns: z.array(z.string().min(1)).optional(),
|
|
46
46
|
});
|
|
47
47
|
const appendJournalOperationSchema = z.object({
|
|
@@ -90,7 +90,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
90
90
|
},
|
|
91
91
|
}, async (input) => runTool(() => shepherd.iterate(requireRepositoryQualifiedPr(input)), formatIterateResult));
|
|
92
92
|
server.registerTool("apply", {
|
|
93
|
-
description: "Apply ordered
|
|
93
|
+
description: "Apply ordered review, journal, and file-view operations after prevalidation; explicit requests rely on GitHub's mutation response.",
|
|
94
94
|
inputSchema: applyInputSchema,
|
|
95
95
|
annotations: {
|
|
96
96
|
readOnlyHint: false,
|
package/bin/pr-reference.d.mts
CHANGED
|
@@ -15,6 +15,6 @@ export declare function parsePrReference(pr: number | string | undefined): Parse
|
|
|
15
15
|
export declare function parseCliPrReference(value: string): ParsedPrReference | null;
|
|
16
16
|
/** Converts a validated parsed reference into the command-layer target shape. */
|
|
17
17
|
export declare function resolveParsedPrTarget(parsed: ParsedPrReference): ResolvedPrTarget;
|
|
18
|
-
/** Canonical collision-safe PR reference for generated commands and
|
|
18
|
+
/** Canonical collision-safe PR reference for generated commands and escalation messages. */
|
|
19
19
|
export declare function formatPrUrl(repository: string, prNumber: number): string;
|
|
20
20
|
export declare function isRepositoryQualifiedPrReference(pr: unknown): pr is string;
|