pr-shepherd 0.43.0 → 0.44.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 +18 -17
- package/bin/api.d.mts +3 -2
- package/bin/api.mjs +13 -18
- package/bin/cli/args.mjs +13 -9
- package/bin/cli/fix-formatter.mjs +8 -6
- package/bin/cli/help-command-pages.d.mts +9 -6
- package/bin/cli/help-command-pages.mjs +7 -4
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +2 -2
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +10 -7
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +5 -5
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/journal-formatter.mjs +3 -0
- package/bin/cli/journal-handler.mjs +11 -7
- package/bin/cli/list-formatters.d.mts +3 -1
- package/bin/cli/list-formatters.mjs +9 -7
- package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -1
- package/bin/cli/mutate-formatter.mjs +4 -0
- package/bin/commands/check.mjs +22 -6
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +2 -2
- package/bin/commands/commit-suggestion.mjs +3 -1
- package/bin/commands/iterate/check-instructions.d.mts +5 -19
- package/bin/commands/iterate/check-instructions.mjs +24 -25
- package/bin/commands/iterate/classify.d.mts +2 -2
- package/bin/commands/iterate/classify.mjs +43 -25
- package/bin/commands/iterate/escalate.d.mts +1 -1
- package/bin/commands/iterate/escalate.mjs +11 -0
- package/bin/commands/iterate/fix-code.mjs +81 -42
- package/bin/commands/iterate/helpers.d.mts +0 -2
- package/bin/commands/iterate/helpers.mjs +0 -15
- package/bin/commands/iterate/index.mjs +6 -12
- package/bin/commands/iterate/mark-ready.d.mts +2 -0
- package/bin/commands/iterate/mark-ready.mjs +38 -0
- package/bin/commands/iterate/merge-state.mjs +38 -9
- package/bin/commands/iterate/merge.mjs +0 -1
- package/bin/commands/iterate/render.d.mts +2 -2
- package/bin/commands/iterate/render.mjs +19 -20
- package/bin/commands/iterate/stall.mjs +4 -2
- package/bin/commands/iterate/thread-mutation-routing.d.mts +9 -0
- package/bin/commands/iterate/thread-mutation-routing.mjs +29 -0
- package/bin/commands/journal/index.d.mts +5 -0
- package/bin/commands/journal/index.mjs +11 -2
- package/bin/commands/mark-files-as-viewed.d.mts +2 -1
- package/bin/commands/mark-files-as-viewed.mjs +5 -92
- package/bin/commands/resolve-mutate.mjs +51 -9
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/commands/suggestion-patch-item.d.mts +1 -1
- package/bin/commands/suggestion-patch-item.mjs +3 -11
- package/bin/commands/suggestion-patches.mjs +3 -3
- package/bin/comments/authors.d.mts +6 -0
- package/bin/comments/authors.mjs +5 -0
- package/bin/comments/marker.d.mts +6 -1
- package/bin/comments/marker.mjs +8 -1
- package/bin/comments/resolve.d.mts +4 -0
- package/bin/comments/thread-visibility.mjs +10 -10
- package/bin/comments/visible-comments.mjs +2 -1
- package/bin/config/load.d.mts +1 -1
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +1 -1
- package/bin/github/batch-parsers.d.mts +2 -2
- package/bin/github/batch-parsers.mjs +17 -1
- package/bin/github/batch-raw-types.d.mts +12 -0
- package/bin/github/batch.mjs +1 -1
- package/bin/github/client.d.mts +1 -0
- package/bin/github/client.mjs +8 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +27 -1
- package/bin/github/gql/batch-pr-page.gql +6 -0
- package/bin/github/gql/batch-pr.gql +13 -0
- package/bin/github/gql/get-pr-body.gql +1 -0
- package/bin/github/gql/review-thread-comments.gql +1 -0
- package/bin/github/gql/suggestion-threads.gql +1 -0
- package/bin/github/graphql-http.mjs +3 -2
- package/bin/github/graphql-internal-retry.d.mts +2 -0
- package/bin/github/graphql-internal-retry.mjs +50 -0
- package/bin/github/graphql-response.mjs +11 -1
- package/bin/github/suggestion-thread.mjs +1 -0
- package/bin/mcp/server.mjs +12 -6
- package/bin/pr-reference.d.mts +13 -0
- package/bin/pr-reference.mjs +23 -1
- package/bin/reporters/agent.mjs +5 -0
- package/bin/threads/transcript.d.mts +2 -0
- package/bin/threads/transcript.mjs +2 -0
- package/bin/types/agent-thread.d.mts +1 -0
- package/bin/types/escalate.d.mts +6 -1
- package/bin/types/github.d.mts +19 -0
- package/bin/types/iterate.d.mts +9 -8
- package/bin/types/report.d.mts +14 -1
- package/bin/types/review-thread.d.mts +1 -0
- package/bin/types/suggestion-patch.d.mts +1 -1
- package/package.json +1 -1
- 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 +3 -3
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +15 -13
- package/bin/commands/iterate/reruns.d.mts +0 -20
- package/bin/commands/iterate/reruns.mjs +0 -105
package/bin/exit-codes.d.mts
CHANGED
|
@@ -35,7 +35,7 @@ export declare const EXIT: Readonly<{
|
|
|
35
35
|
readonly UNAVAILABLE: 69;
|
|
36
36
|
/** Unexpected/unclassified internal error — the fallback. */
|
|
37
37
|
readonly SOFTWARE: 70;
|
|
38
|
-
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted,
|
|
38
|
+
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted, Retry-After, GraphQL INTERNAL. */
|
|
39
39
|
readonly TEMPFAIL: 75;
|
|
40
40
|
/** GitHub 401/403 — missing token or insufficient PAT scopes. */
|
|
41
41
|
readonly NOPERM: 77;
|
package/bin/exit-codes.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export const EXIT = Object.freeze({
|
|
|
34
34
|
UNAVAILABLE: 69,
|
|
35
35
|
/** Unexpected/unclassified internal error — the fallback. */
|
|
36
36
|
SOFTWARE: 70,
|
|
37
|
-
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted,
|
|
37
|
+
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted, Retry-After, GraphQL INTERNAL. */
|
|
38
38
|
TEMPFAIL: 75,
|
|
39
39
|
/** GitHub 401/403 — missing token or insufficient PAT scopes. */
|
|
40
40
|
NOPERM: 77,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BatchPrData } from "../types.mts";
|
|
2
|
-
import type { RawPr, RawThread, RawComment, RawReview, RawReviewSummary, RawContextNode } from "./batch-raw-types.mts";
|
|
3
|
-
export declare function parseRawPr(raw: RawPr, rawThreadPages: RawThread[], rawCommentNodes: RawComment[], rawReviewNodes: RawReview[], rawReviewSummaryNodes: RawReviewSummary[], rawApprovedReviewNodes: RawReviewSummary[], rawCheckNodes: RawContextNode[]): BatchPrData;
|
|
2
|
+
import type { RawBatchResponse, RawPr, RawThread, RawComment, RawReview, RawReviewSummary, RawContextNode } from "./batch-raw-types.mts";
|
|
3
|
+
export declare function parseRawPr(raw: RawPr, rawThreadPages: RawThread[], rawCommentNodes: RawComment[], rawReviewNodes: RawReview[], rawReviewSummaryNodes: RawReviewSummary[], rawApprovedReviewNodes: RawReviewSummary[], rawCheckNodes: RawContextNode[], repository?: Pick<NonNullable<RawBatchResponse["repository"]>, "viewerPermission" | "viewerCanAdminister">): BatchPrData;
|
|
@@ -10,6 +10,7 @@ function parseReviewNode(r) {
|
|
|
10
10
|
author: r.author?.login ?? "unknown",
|
|
11
11
|
authorType: mapAuthorType(r.author?.__typename, r.author?.login),
|
|
12
12
|
...(r.authorAssociation !== undefined && { authorAssociation: r.authorAssociation }),
|
|
13
|
+
viewerCanMinimize: "viewerCanMinimize" in r && r.viewerCanMinimize === true,
|
|
13
14
|
body: r.body,
|
|
14
15
|
createdAtUnix: r.createdAt ? parseCreatedAt(r.createdAt) : 0,
|
|
15
16
|
};
|
|
@@ -18,7 +19,7 @@ function parseReviewNode(r) {
|
|
|
18
19
|
}
|
|
19
20
|
return base;
|
|
20
21
|
}
|
|
21
|
-
export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes, rawReviewSummaryNodes, rawApprovedReviewNodes, rawCheckNodes) {
|
|
22
|
+
export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes, rawReviewSummaryNodes, rawApprovedReviewNodes, rawCheckNodes, repository) {
|
|
22
23
|
const reviewRequests = (raw.reviewRequests?.nodes ?? []).flatMap((n) => {
|
|
23
24
|
const login = n.requestedReviewer?.login ?? n.requestedReviewer?.name;
|
|
24
25
|
return login ? [{ login }] : [];
|
|
@@ -37,6 +38,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
37
38
|
author: c.author?.login ?? "unknown",
|
|
38
39
|
authorType: mapAuthorType(c.author?.__typename, c.author?.login),
|
|
39
40
|
...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
|
|
41
|
+
...(c.viewerDidAuthor === true && { viewerDidAuthor: true }),
|
|
40
42
|
body: c.body,
|
|
41
43
|
url: c.url,
|
|
42
44
|
createdAtUnix: c.createdAt ? parseCreatedAt(c.createdAt) : 0,
|
|
@@ -46,6 +48,8 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
46
48
|
isResolved: t.isResolved,
|
|
47
49
|
isOutdated: t.isOutdated,
|
|
48
50
|
isMinimized: comment?.isMinimized ?? false,
|
|
51
|
+
viewerCanReply: t.viewerCanReply === true,
|
|
52
|
+
viewerCanResolve: t.viewerCanResolve === true,
|
|
49
53
|
path: t.path ?? comment?.path ?? null,
|
|
50
54
|
line: t.line ?? comment?.line ?? null,
|
|
51
55
|
startLine: t.startLine ?? comment?.startLine ?? null,
|
|
@@ -55,6 +59,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
55
59
|
...(comment?.authorAssociation !== undefined && {
|
|
56
60
|
authorAssociation: comment.authorAssociation,
|
|
57
61
|
}),
|
|
62
|
+
...(comment?.viewerDidAuthor === true && { viewerDidAuthor: true }),
|
|
58
63
|
body: comment?.body ?? "",
|
|
59
64
|
url: comment?.url ?? "",
|
|
60
65
|
createdAtUnix: comment?.createdAt ? parseCreatedAt(comment.createdAt) : 0,
|
|
@@ -64,6 +69,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
64
69
|
const comments = rawCommentNodes.map((c) => ({
|
|
65
70
|
id: c.id,
|
|
66
71
|
isMinimized: c.isMinimized,
|
|
72
|
+
viewerCanMinimize: c.viewerCanMinimize === true,
|
|
67
73
|
author: c.author?.login ?? "unknown",
|
|
68
74
|
authorType: mapAuthorType(c.author?.__typename, c.author?.login),
|
|
69
75
|
...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
|
|
@@ -108,6 +114,16 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
108
114
|
headRefOid: raw.headRefOid,
|
|
109
115
|
headRefName: raw.headRefName,
|
|
110
116
|
headRepoWithOwner: raw.headRepository?.nameWithOwner ?? null,
|
|
117
|
+
viewerAuthorization: {
|
|
118
|
+
repositoryPermission: repository?.viewerPermission ?? null,
|
|
119
|
+
viewerCanAdminister: repository?.viewerCanAdminister === true,
|
|
120
|
+
viewerDidAuthor: raw.viewerDidAuthor === true,
|
|
121
|
+
viewerCanUpdate: raw.viewerCanUpdate === true,
|
|
122
|
+
viewerCanEnableAutoMerge: raw.viewerCanEnableAutoMerge === true,
|
|
123
|
+
viewerCanEditFiles: raw.viewerCanEditFiles === true,
|
|
124
|
+
headRepositoryPermission: raw.headRepository?.viewerPermission ??
|
|
125
|
+
null,
|
|
126
|
+
},
|
|
111
127
|
baseRefName: raw.baseRefName,
|
|
112
128
|
reviewRequests,
|
|
113
129
|
latestReviews,
|
|
@@ -2,6 +2,8 @@ import type { CommentAuthorAssociation } from "../types/github.mts";
|
|
|
2
2
|
import type { RawPrMergeFields } from "./batch-raw-rules.mts";
|
|
3
3
|
export interface RawBatchResponse {
|
|
4
4
|
repository: {
|
|
5
|
+
viewerPermission: string | null;
|
|
6
|
+
viewerCanAdminister: boolean;
|
|
5
7
|
pullRequest: RawPr | null;
|
|
6
8
|
} | null;
|
|
7
9
|
}
|
|
@@ -10,6 +12,10 @@ export interface RawPr extends RawPrMergeFields {
|
|
|
10
12
|
number: number;
|
|
11
13
|
state: string;
|
|
12
14
|
isDraft: boolean;
|
|
15
|
+
viewerDidAuthor: boolean;
|
|
16
|
+
viewerCanUpdate: boolean;
|
|
17
|
+
viewerCanEnableAutoMerge: boolean;
|
|
18
|
+
viewerCanEditFiles: boolean;
|
|
13
19
|
mergeable: string;
|
|
14
20
|
mergeStateStatus: string;
|
|
15
21
|
reviewDecision: string | null;
|
|
@@ -17,6 +23,7 @@ export interface RawPr extends RawPrMergeFields {
|
|
|
17
23
|
headRefName: string;
|
|
18
24
|
headRepository: {
|
|
19
25
|
nameWithOwner: string;
|
|
26
|
+
viewerPermission: string | null;
|
|
20
27
|
} | null;
|
|
21
28
|
baseRefName: string;
|
|
22
29
|
baseRef: import("./batch-raw-rules.mts").RawBaseRef | null;
|
|
@@ -117,6 +124,7 @@ export interface RawThreadComment {
|
|
|
117
124
|
isMinimized: boolean;
|
|
118
125
|
url: string;
|
|
119
126
|
authorAssociation?: CommentAuthorAssociation;
|
|
127
|
+
viewerDidAuthor?: boolean;
|
|
120
128
|
author: RawAuthor | null;
|
|
121
129
|
pullRequestReview?: {
|
|
122
130
|
id: string;
|
|
@@ -131,6 +139,8 @@ export interface RawThread {
|
|
|
131
139
|
id: string;
|
|
132
140
|
isResolved: boolean;
|
|
133
141
|
isOutdated: boolean;
|
|
142
|
+
viewerCanReply: boolean;
|
|
143
|
+
viewerCanResolve: boolean;
|
|
134
144
|
path?: string | null;
|
|
135
145
|
line?: number | null;
|
|
136
146
|
startLine?: number | null;
|
|
@@ -157,6 +167,7 @@ export interface RawReviewThreadCommentsResponse {
|
|
|
157
167
|
export interface RawComment {
|
|
158
168
|
id: string;
|
|
159
169
|
isMinimized: boolean;
|
|
170
|
+
viewerCanMinimize: boolean;
|
|
160
171
|
url: string;
|
|
161
172
|
authorAssociation?: CommentAuthorAssociation;
|
|
162
173
|
author: RawAuthor | null;
|
|
@@ -176,6 +187,7 @@ export interface RawReview {
|
|
|
176
187
|
export interface RawReviewSummary {
|
|
177
188
|
id: string;
|
|
178
189
|
isMinimized: boolean;
|
|
190
|
+
viewerCanMinimize: boolean;
|
|
179
191
|
authorAssociation?: CommentAuthorAssociation;
|
|
180
192
|
author: RawAuthor | null;
|
|
181
193
|
body: string;
|
package/bin/github/batch.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
|
20
20
|
await hydrateMergeQueueChecks(raw, repo);
|
|
21
21
|
const paged = await paginateBatchConnections(pr, repo, raw, opts, result.rateLimit);
|
|
22
22
|
const rawThreadPages = await hydrateThreadCommentPages(paged.threads);
|
|
23
|
-
const data = parseRawPr(raw, rawThreadPages, paged.comments, paged.changesRequested, paged.reviewSummaries, paged.approvedReviews, paged.checks);
|
|
23
|
+
const data = parseRawPr(raw, rawThreadPages, paged.comments, paged.changesRequested, paged.reviewSummaries, paged.approvedReviews, paged.checks, result.data.repository);
|
|
24
24
|
data.checks = mergeStartupFailureChecks(data.checks, parseSuiteStartupFailures(raw));
|
|
25
25
|
return {
|
|
26
26
|
data,
|
package/bin/github/client.d.mts
CHANGED
|
@@ -30,6 +30,7 @@ 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;
|
|
33
34
|
}>;
|
|
34
35
|
/** Overwrites the PR body. */
|
|
35
36
|
export declare function updatePullRequestBody(pullRequestId: string, body: string): Promise<void>;
|
package/bin/github/client.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/**
|
|
2
3
|
* High-level GitHub client — wraps http.mts for application-level concerns.
|
|
3
4
|
*
|
|
@@ -76,7 +77,13 @@ export async function getPullRequestBody(pr, owner, name) {
|
|
|
76
77
|
: "PR not found or access denied";
|
|
77
78
|
throw new Error(`Could not fetch body for ${owner}/${name} PR #${pr}: ${detail}`);
|
|
78
79
|
}
|
|
79
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
nodeId: pullRequest.id,
|
|
82
|
+
body: pullRequest.body ?? "",
|
|
83
|
+
...(pullRequest.viewerCanUpdate !== undefined && {
|
|
84
|
+
viewerCanUpdate: pullRequest.viewerCanUpdate,
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
80
87
|
}
|
|
81
88
|
/** Overwrites the PR body. */
|
|
82
89
|
export async function updatePullRequestBody(pullRequestId, body) {
|
package/bin/github/errors.d.mts
CHANGED
|
@@ -3,7 +3,13 @@ import type { RateLimitInfo } from "./http.mts";
|
|
|
3
3
|
export interface GitHubGraphQlError {
|
|
4
4
|
message: string;
|
|
5
5
|
path?: unknown;
|
|
6
|
+
/** GitHub's GraphQL `type` field — `INTERNAL` on engine crashes. */
|
|
7
|
+
type?: string;
|
|
8
|
+
/** GraphQL `extensions`; GitHub often sets `{ code: "INTERNAL" }`. */
|
|
9
|
+
extensions?: unknown;
|
|
6
10
|
}
|
|
11
|
+
/** GitHub GraphQL engine crash: HTTP 200, `data: null`, INTERNAL type/code or message. */
|
|
12
|
+
export declare function isRetryableGraphQlInternal(graphqlErrors?: GitHubGraphQlError[]): boolean;
|
|
7
13
|
export declare class GitHubRequestError extends ShepherdError {
|
|
8
14
|
readonly status: number;
|
|
9
15
|
readonly rateLimit?: RateLimitInfo;
|
package/bin/github/errors.mjs
CHANGED
|
@@ -5,16 +5,42 @@ import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
|
5
5
|
// be resolved. Status alone can't see this, so classification must also inspect the
|
|
6
6
|
// GraphQL error messages themselves.
|
|
7
7
|
const GRAPHQL_PERMISSION_ERROR = /resource not accessible/i;
|
|
8
|
+
const GRAPHQL_INTERNAL_MESSAGE = /something went wrong while executing your query/i;
|
|
9
|
+
function isInternalToken(value) {
|
|
10
|
+
return typeof value === "string" && value.toUpperCase() === "INTERNAL";
|
|
11
|
+
}
|
|
12
|
+
function extensionsCode(extensions) {
|
|
13
|
+
if (typeof extensions !== "object" || extensions === null || Array.isArray(extensions)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return extensions["code"];
|
|
17
|
+
}
|
|
8
18
|
function hasPermissionError(graphqlErrors) {
|
|
9
19
|
return graphqlErrors?.some((e) => GRAPHQL_PERMISSION_ERROR.test(e.message)) ?? false;
|
|
10
20
|
}
|
|
21
|
+
/** GitHub GraphQL engine crash: HTTP 200, `data: null`, INTERNAL type/code or message. */
|
|
22
|
+
export function isRetryableGraphQlInternal(graphqlErrors) {
|
|
23
|
+
if (!graphqlErrors?.length)
|
|
24
|
+
return false;
|
|
25
|
+
return graphqlErrors.some((error) => {
|
|
26
|
+
if (isInternalToken(error.type))
|
|
27
|
+
return true;
|
|
28
|
+
if (isInternalToken(extensionsCode(error.extensions)))
|
|
29
|
+
return true;
|
|
30
|
+
return GRAPHQL_INTERNAL_MESSAGE.test(error.message);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
11
33
|
function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
|
|
12
34
|
// Retry signals take priority over everything else: GitHub's secondary rate limit
|
|
13
35
|
// returns 403 with a Retry-After header, which is a transient throttle — not the
|
|
14
36
|
// permission-denied 403 a bad/missing token produces. Treat any retry signal as
|
|
15
37
|
// TEMPFAIL first so it isn't shadowed by the checks below.
|
|
16
38
|
const rateLimitExhausted = rateLimit !== undefined && rateLimit.remaining <= 0;
|
|
17
|
-
if (status === 429 ||
|
|
39
|
+
if (status === 429 ||
|
|
40
|
+
status >= 500 ||
|
|
41
|
+
retryAfterSeconds !== undefined ||
|
|
42
|
+
rateLimitExhausted ||
|
|
43
|
+
isRetryableGraphQlInternal(graphqlErrors)) {
|
|
18
44
|
return EXIT.TEMPFAIL;
|
|
19
45
|
}
|
|
20
46
|
if (status === 401 || status === 403 || hasPermissionError(graphqlErrors))
|
|
@@ -29,6 +29,8 @@ query BatchPrPage(
|
|
|
29
29
|
id
|
|
30
30
|
isResolved
|
|
31
31
|
isOutdated
|
|
32
|
+
viewerCanReply
|
|
33
|
+
viewerCanResolve
|
|
32
34
|
path
|
|
33
35
|
line
|
|
34
36
|
startLine
|
|
@@ -42,6 +44,7 @@ query BatchPrPage(
|
|
|
42
44
|
isMinimized
|
|
43
45
|
url
|
|
44
46
|
authorAssociation
|
|
47
|
+
viewerDidAuthor
|
|
45
48
|
author {
|
|
46
49
|
__typename
|
|
47
50
|
login
|
|
@@ -66,6 +69,7 @@ query BatchPrPage(
|
|
|
66
69
|
nodes {
|
|
67
70
|
id
|
|
68
71
|
isMinimized
|
|
72
|
+
viewerCanMinimize
|
|
69
73
|
url
|
|
70
74
|
authorAssociation
|
|
71
75
|
author {
|
|
@@ -108,6 +112,7 @@ query BatchPrPage(
|
|
|
108
112
|
nodes {
|
|
109
113
|
id
|
|
110
114
|
isMinimized
|
|
115
|
+
viewerCanMinimize
|
|
111
116
|
authorAssociation
|
|
112
117
|
author {
|
|
113
118
|
__typename
|
|
@@ -126,6 +131,7 @@ query BatchPrPage(
|
|
|
126
131
|
nodes {
|
|
127
132
|
id
|
|
128
133
|
isMinimized
|
|
134
|
+
viewerCanMinimize
|
|
129
135
|
authorAssociation
|
|
130
136
|
author {
|
|
131
137
|
__typename
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
# re-select every connection (GraphQL points scale with possible nodes).
|
|
3
3
|
query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
4
4
|
repository(owner: $owner, name: $repo) {
|
|
5
|
+
viewerPermission
|
|
6
|
+
viewerCanAdminister
|
|
5
7
|
pullRequest(number: $pr) {
|
|
6
8
|
id
|
|
7
9
|
number
|
|
8
10
|
state
|
|
9
11
|
isDraft
|
|
12
|
+
viewerDidAuthor
|
|
13
|
+
viewerCanUpdate
|
|
14
|
+
viewerCanEnableAutoMerge
|
|
15
|
+
viewerCanEditFiles
|
|
10
16
|
mergeable
|
|
11
17
|
mergeStateStatus
|
|
12
18
|
reviewDecision
|
|
@@ -14,6 +20,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
14
20
|
headRefName
|
|
15
21
|
headRepository {
|
|
16
22
|
nameWithOwner
|
|
23
|
+
viewerPermission
|
|
17
24
|
}
|
|
18
25
|
baseRefName
|
|
19
26
|
isInMergeQueue
|
|
@@ -146,6 +153,8 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
146
153
|
id
|
|
147
154
|
isResolved
|
|
148
155
|
isOutdated
|
|
156
|
+
viewerCanReply
|
|
157
|
+
viewerCanResolve
|
|
149
158
|
path
|
|
150
159
|
line
|
|
151
160
|
startLine
|
|
@@ -159,6 +168,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
159
168
|
isMinimized
|
|
160
169
|
url
|
|
161
170
|
authorAssociation
|
|
171
|
+
viewerDidAuthor
|
|
162
172
|
author {
|
|
163
173
|
__typename
|
|
164
174
|
login
|
|
@@ -183,6 +193,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
183
193
|
nodes {
|
|
184
194
|
id
|
|
185
195
|
isMinimized
|
|
196
|
+
viewerCanMinimize
|
|
186
197
|
url
|
|
187
198
|
authorAssociation
|
|
188
199
|
author {
|
|
@@ -220,6 +231,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
220
231
|
nodes {
|
|
221
232
|
id
|
|
222
233
|
isMinimized
|
|
234
|
+
viewerCanMinimize
|
|
223
235
|
authorAssociation
|
|
224
236
|
author {
|
|
225
237
|
__typename
|
|
@@ -240,6 +252,7 @@ query BatchPr($owner: String!, $repo: String!, $pr: Int!) {
|
|
|
240
252
|
nodes {
|
|
241
253
|
id
|
|
242
254
|
isMinimized
|
|
255
|
+
viewerCanMinimize
|
|
243
256
|
authorAssociation
|
|
244
257
|
author {
|
|
245
258
|
__typename
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendEntry, nextEntry } from "../log/log-file.mjs";
|
|
2
2
|
import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
3
3
|
import { GitHubRequestError } from "./errors.mjs";
|
|
4
|
+
import { withGraphQlInternalRetry } from "./graphql-internal-retry.mjs";
|
|
4
5
|
import { formatGraphQlErrors, parseGraphQlPayload } from "./graphql-response.mjs";
|
|
5
6
|
import { makeHeaders } from "./http-auth.mjs";
|
|
6
7
|
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
@@ -96,10 +97,10 @@ async function graphqlInner(query, vars, opts) {
|
|
|
96
97
|
return { data: payload.data, rateLimit, retryAfterSeconds, errors: payload.errors };
|
|
97
98
|
}
|
|
98
99
|
export async function graphql(query, vars = {}, opts = {}) {
|
|
99
|
-
const { data, errors } = await graphqlInner(query, vars, opts);
|
|
100
|
+
const { data, errors } = await withGraphQlInternalRetry(query, () => graphqlInner(query, vars, opts));
|
|
100
101
|
return { data, errors };
|
|
101
102
|
}
|
|
102
103
|
export async function graphqlWithRateLimit(query, vars = {}, opts = {}) {
|
|
103
|
-
const { data, rateLimit, retryAfterSeconds, errors } = await graphqlInner(query, vars, opts);
|
|
104
|
+
const { data, rateLimit, retryAfterSeconds, errors } = await withGraphQlInternalRetry(query, () => graphqlInner(query, vars, opts));
|
|
104
105
|
return { data, rateLimit: rateLimit ?? undefined, retryAfterSeconds, errors };
|
|
105
106
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { sleep } from "../util/sleep.mjs";
|
|
2
|
+
import { GitHubRequestError, isRetryableGraphQlInternal } from "./errors.mjs";
|
|
3
|
+
const GRAPHQL_INTERNAL_RETRY_DELAYS = [500, 1500];
|
|
4
|
+
/** True when the root operation is a mutation. Anonymous `{...}` is a query. */
|
|
5
|
+
function isGraphQlMutationDocument(document) {
|
|
6
|
+
let rest = document;
|
|
7
|
+
for (;;) {
|
|
8
|
+
rest = rest.replace(/^\s+/, "");
|
|
9
|
+
if (rest.startsWith("#")) {
|
|
10
|
+
const nl = rest.indexOf("\n");
|
|
11
|
+
rest = nl === -1 ? "" : rest.slice(nl + 1);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (rest.startsWith('"""')) {
|
|
15
|
+
const end = rest.indexOf('"""', 3);
|
|
16
|
+
rest = end === -1 ? "" : rest.slice(end + 3);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
return /^mutation\b/i.test(rest);
|
|
22
|
+
}
|
|
23
|
+
function hasServerBackoff(err) {
|
|
24
|
+
if (err.retryAfterSeconds !== undefined)
|
|
25
|
+
return true;
|
|
26
|
+
return err.rateLimit !== undefined && err.rateLimit.remaining <= 0;
|
|
27
|
+
}
|
|
28
|
+
/** Retry GitHub GraphQL engine crashes (HTTP 200, data: null, INTERNAL) on reads. */
|
|
29
|
+
export async function withGraphQlInternalRetry(document, run) {
|
|
30
|
+
let lastErr;
|
|
31
|
+
for (let attempt = 1; attempt <= GRAPHQL_INTERNAL_RETRY_DELAYS.length + 1; attempt++) {
|
|
32
|
+
try {
|
|
33
|
+
return await run();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (!(err instanceof GitHubRequestError) ||
|
|
37
|
+
!isRetryableGraphQlInternal(err.graphqlErrors) ||
|
|
38
|
+
isGraphQlMutationDocument(document) ||
|
|
39
|
+
hasServerBackoff(err)) {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
lastErr = err;
|
|
43
|
+
const delay = GRAPHQL_INTERNAL_RETRY_DELAYS[attempt - 1];
|
|
44
|
+
if (delay === undefined)
|
|
45
|
+
break;
|
|
46
|
+
await sleep(delay);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw lastErr;
|
|
50
|
+
}
|
|
@@ -13,7 +13,7 @@ export function parseGraphQlPayload(parsed, status, rateLimit, retryAfterSeconds
|
|
|
13
13
|
typeof error["message"] === "string")) {
|
|
14
14
|
throw malformedGraphQlResponse("errors field is not an array of GraphQL errors", status, rateLimit, retryAfterSeconds);
|
|
15
15
|
}
|
|
16
|
-
errors = record["errors"];
|
|
16
|
+
errors = record["errors"].map((error) => parseGraphQlError(error));
|
|
17
17
|
}
|
|
18
18
|
if (!("data" in record)) {
|
|
19
19
|
if (errors?.length)
|
|
@@ -26,6 +26,16 @@ export function parseGraphQlPayload(parsed, status, rateLimit, retryAfterSeconds
|
|
|
26
26
|
}
|
|
27
27
|
return { data: record["data"] ?? null, errors };
|
|
28
28
|
}
|
|
29
|
+
function parseGraphQlError(error) {
|
|
30
|
+
const parsed = { message: error["message"] };
|
|
31
|
+
if ("path" in error)
|
|
32
|
+
parsed.path = error["path"];
|
|
33
|
+
if (typeof error["type"] === "string")
|
|
34
|
+
parsed.type = error["type"];
|
|
35
|
+
if (error["extensions"] !== undefined)
|
|
36
|
+
parsed.extensions = error["extensions"];
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
29
39
|
export function formatGraphQlErrors(errors) {
|
|
30
40
|
return (errors ?? [])
|
|
31
41
|
.map((error) => {
|
|
@@ -41,6 +41,7 @@ function parseThread(raw, pr, threadId) {
|
|
|
41
41
|
...(comment?.authorAssociation !== undefined && {
|
|
42
42
|
authorAssociation: comment.authorAssociation,
|
|
43
43
|
}),
|
|
44
|
+
...(comment?.viewerDidAuthor === true && { viewerDidAuthor: true }),
|
|
44
45
|
body: comment?.body ?? "",
|
|
45
46
|
url: comment?.url ?? "",
|
|
46
47
|
createdAtUnix: comment?.createdAt ? parseCreatedAt(comment.createdAt) : 0,
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/rep
|
|
|
11
11
|
const pr = z
|
|
12
12
|
.string()
|
|
13
13
|
.refine(isRepositoryQualifiedPrReference, { message: QUALIFIED_PR_ERROR })
|
|
14
|
-
.describe("GitHub pull-request URL or owner/repo#number");
|
|
14
|
+
.describe("GitHub pull-request URL or owner/repo#number; the explicit repository may differ from the server working directory");
|
|
15
15
|
const ids = z.array(z.string().min(1)).optional();
|
|
16
16
|
const iterateInputSchema = z.object({
|
|
17
17
|
pr,
|
|
@@ -19,8 +19,14 @@ const iterateInputSchema = z.object({
|
|
|
19
19
|
stallTimeoutSeconds: z.number().nonnegative().optional(),
|
|
20
20
|
noAutoMarkReady: z.boolean().optional(),
|
|
21
21
|
merge: z.boolean().optional(),
|
|
22
|
-
noAutoCancelActionable: z
|
|
23
|
-
|
|
22
|
+
noAutoCancelActionable: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Deprecated no-op; Shepherd never cancels workflow runs."),
|
|
26
|
+
neverCancelRuns: z
|
|
27
|
+
.array(z.string())
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Deprecated per-call no-op retained for compatibility."),
|
|
24
30
|
});
|
|
25
31
|
const reviewMutationsOperationSchema = z.object({
|
|
26
32
|
type: z.literal("review_mutations"),
|
|
@@ -34,7 +40,7 @@ const reviewMutationsOperationSchema = z.object({
|
|
|
34
40
|
const markFilesViewedOperationSchema = z.object({
|
|
35
41
|
type: z.literal("mark_files_viewed"),
|
|
36
42
|
files: z.array(z.string().min(1)).optional(),
|
|
37
|
-
tests: z.boolean().optional(),
|
|
43
|
+
tests: z.boolean().optional().describe("Select changed test files; no mutation occurs."),
|
|
38
44
|
matchPatterns: z.array(z.string().min(1)).optional(),
|
|
39
45
|
});
|
|
40
46
|
const appendJournalOperationSchema = z.object({
|
|
@@ -73,7 +79,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
73
79
|
const shepherd = options.shepherd ?? createPrShepherd({ cwd: options.cwd });
|
|
74
80
|
const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
|
|
75
81
|
server.registerTool("iterate", {
|
|
76
|
-
description: "Inspect the
|
|
82
|
+
description: "Inspect the specified pull request and return the next Shepherd state.",
|
|
77
83
|
inputSchema: iterateInputSchema,
|
|
78
84
|
annotations: {
|
|
79
85
|
readOnlyHint: false,
|
|
@@ -83,7 +89,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
83
89
|
},
|
|
84
90
|
}, async (input) => runTool(() => shepherd.iterate(requireRepositoryQualifiedPr(input)), formatIterateResult));
|
|
85
91
|
server.registerTool("apply", {
|
|
86
|
-
description: "Apply ordered review, file-view,
|
|
92
|
+
description: "Apply ordered authorized review and journal operations, or run selection-only file-view diagnostics, after prevalidation.",
|
|
87
93
|
inputSchema: applyInputSchema,
|
|
88
94
|
annotations: {
|
|
89
95
|
readOnlyHint: false,
|
package/bin/pr-reference.d.mts
CHANGED
|
@@ -2,6 +2,19 @@ export interface ParsedPrReference {
|
|
|
2
2
|
number?: number;
|
|
3
3
|
repository?: string;
|
|
4
4
|
}
|
|
5
|
+
export interface ResolvedPrTarget {
|
|
6
|
+
prNumber?: number;
|
|
7
|
+
targetRepository?: {
|
|
8
|
+
owner: string;
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
5
12
|
/** Parses API PR references without performing repository or GitHub I/O. */
|
|
6
13
|
export declare function parsePrReference(pr: number | string | undefined): ParsedPrReference | null;
|
|
14
|
+
/** Parses a positional CLI PR reference, including a bare numeric string. */
|
|
15
|
+
export declare function parseCliPrReference(value: string): ParsedPrReference | null;
|
|
16
|
+
/** Converts a validated parsed reference into the command-layer target shape. */
|
|
17
|
+
export declare function resolveParsedPrTarget(parsed: ParsedPrReference): ResolvedPrTarget;
|
|
18
|
+
/** Canonical collision-safe PR reference for generated commands and handoffs. */
|
|
19
|
+
export declare function formatPrUrl(repository: string, prNumber: number): string;
|
|
7
20
|
export declare function isRepositoryQualifiedPrReference(pr: unknown): pr is string;
|
package/bin/pr-reference.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export function parsePrReference(pr) {
|
|
|
18
18
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
19
19
|
if ((url.protocol === "https:" || url.protocol === "http:") &&
|
|
20
20
|
(url.hostname === "github.com" || url.hostname === "www.github.com") &&
|
|
21
|
-
parts.length
|
|
21
|
+
parts.length >= 4 &&
|
|
22
22
|
parts[2] === "pull" &&
|
|
23
23
|
/^[1-9][0-9]*$/.test(parts[3])) {
|
|
24
24
|
return { number: Number(parts[3]), repository: `${parts[0]}/${parts[1]}` };
|
|
@@ -29,6 +29,28 @@ export function parsePrReference(pr) {
|
|
|
29
29
|
}
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
+
/** Parses a positional CLI PR reference, including a bare numeric string. */
|
|
33
|
+
export function parseCliPrReference(value) {
|
|
34
|
+
if (/^[0-9]+$/.test(value))
|
|
35
|
+
return { number: Number(value) };
|
|
36
|
+
return parsePrReference(value);
|
|
37
|
+
}
|
|
38
|
+
/** Converts a validated parsed reference into the command-layer target shape. */
|
|
39
|
+
export function resolveParsedPrTarget(parsed) {
|
|
40
|
+
if (parsed.repository === undefined)
|
|
41
|
+
return { prNumber: parsed.number };
|
|
42
|
+
const [owner, name] = parsed.repository.split("/");
|
|
43
|
+
if (!owner || !name)
|
|
44
|
+
throw new Error(`Invalid repository reference: ${parsed.repository}`);
|
|
45
|
+
return {
|
|
46
|
+
prNumber: parsed.number,
|
|
47
|
+
targetRepository: { owner, name },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Canonical collision-safe PR reference for generated commands and handoffs. */
|
|
51
|
+
export function formatPrUrl(repository, prNumber) {
|
|
52
|
+
return `https://github.com/${repository}/pull/${prNumber}`;
|
|
53
|
+
}
|
|
32
54
|
export function isRepositoryQualifiedPrReference(pr) {
|
|
33
55
|
if (typeof pr !== "string")
|
|
34
56
|
return false;
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -13,6 +13,8 @@ export function toAgentThread(t) {
|
|
|
13
13
|
const suggestion = extractSuggestion(t) ?? undefined;
|
|
14
14
|
return {
|
|
15
15
|
id: t.id,
|
|
16
|
+
...(t.viewerCanReply === false && { viewerCanReply: false }),
|
|
17
|
+
...(t.viewerCanResolve === false && { viewerCanResolve: false }),
|
|
16
18
|
...(t.reviewId !== undefined && { reviewId: t.reviewId }),
|
|
17
19
|
path: t.path,
|
|
18
20
|
line: t.line,
|
|
@@ -22,6 +24,7 @@ export function toAgentThread(t) {
|
|
|
22
24
|
author: t.author,
|
|
23
25
|
...(t.authorType !== undefined && { authorType: t.authorType }),
|
|
24
26
|
...(t.authorAssociation !== undefined && { authorAssociation: t.authorAssociation }),
|
|
27
|
+
...(t.viewerDidAuthor === true && { viewerDidAuthor: true }),
|
|
25
28
|
body: t.body,
|
|
26
29
|
url: t.url,
|
|
27
30
|
...(t.edited === true && { edited: true }),
|
|
@@ -31,6 +34,7 @@ export function toAgentThread(t) {
|
|
|
31
34
|
author: c.author,
|
|
32
35
|
...(c.authorType !== undefined && { authorType: c.authorType }),
|
|
33
36
|
...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
|
|
37
|
+
...(c.viewerDidAuthor === true && { viewerDidAuthor: true }),
|
|
34
38
|
body: c.body,
|
|
35
39
|
url: c.url,
|
|
36
40
|
})),
|
|
@@ -41,6 +45,7 @@ export function toAgentThread(t) {
|
|
|
41
45
|
export function toAgentComment(c) {
|
|
42
46
|
return {
|
|
43
47
|
id: c.id,
|
|
48
|
+
...(c.viewerCanMinimize === false && { viewerCanMinimize: false }),
|
|
44
49
|
author: c.author,
|
|
45
50
|
...(c.authorType !== undefined && { authorType: c.authorType }),
|
|
46
51
|
...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
|
|
@@ -4,11 +4,13 @@ export declare function threadComments(thread: {
|
|
|
4
4
|
author: string;
|
|
5
5
|
authorType?: ReviewThreadComment["authorType"];
|
|
6
6
|
authorAssociation?: ReviewThreadComment["authorAssociation"];
|
|
7
|
+
viewerDidAuthor?: true;
|
|
7
8
|
url?: string;
|
|
8
9
|
createdAtUnix?: number;
|
|
9
10
|
comments?: Array<Pick<ReviewThreadComment, "id" | "author" | "body" | "url"> & {
|
|
10
11
|
authorType?: ReviewThreadComment["authorType"];
|
|
11
12
|
authorAssociation?: ReviewThreadComment["authorAssociation"];
|
|
13
|
+
viewerDidAuthor?: true;
|
|
12
14
|
} & Partial<Pick<ReviewThreadComment, "isMinimized" | "createdAtUnix">>>;
|
|
13
15
|
}): ReviewThreadComment[];
|
|
14
16
|
export declare function threadTranscriptBody(thread: ReviewThread, appendedBodies?: string[]): string;
|