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.
Files changed (104) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +18 -17
  3. package/bin/api.d.mts +3 -2
  4. package/bin/api.mjs +13 -18
  5. package/bin/cli/args.mjs +13 -9
  6. package/bin/cli/fix-formatter.mjs +8 -6
  7. package/bin/cli/help-command-pages.d.mts +9 -6
  8. package/bin/cli/help-command-pages.mjs +7 -4
  9. package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
  10. package/bin/cli/help-iterate-poll-pages.mjs +2 -2
  11. package/bin/cli/help-top-page.d.mts +1 -1
  12. package/bin/cli/help-top-page.mjs +2 -2
  13. package/bin/cli/help.d.mts +10 -7
  14. package/bin/cli/iterate-formatter.d.mts +1 -1
  15. package/bin/cli/iterate-formatter.mjs +5 -5
  16. package/bin/cli/iterate-lean.mjs +4 -0
  17. package/bin/cli/journal-formatter.mjs +3 -0
  18. package/bin/cli/journal-handler.mjs +11 -7
  19. package/bin/cli/list-formatters.d.mts +3 -1
  20. package/bin/cli/list-formatters.mjs +9 -7
  21. package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -1
  22. package/bin/cli/mutate-formatter.mjs +4 -0
  23. package/bin/commands/check.mjs +22 -6
  24. package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
  25. package/bin/commands/commit-suggestion-instruction.mjs +2 -2
  26. package/bin/commands/commit-suggestion.mjs +3 -1
  27. package/bin/commands/iterate/check-instructions.d.mts +5 -19
  28. package/bin/commands/iterate/check-instructions.mjs +24 -25
  29. package/bin/commands/iterate/classify.d.mts +2 -2
  30. package/bin/commands/iterate/classify.mjs +43 -25
  31. package/bin/commands/iterate/escalate.d.mts +1 -1
  32. package/bin/commands/iterate/escalate.mjs +11 -0
  33. package/bin/commands/iterate/fix-code.mjs +81 -42
  34. package/bin/commands/iterate/helpers.d.mts +0 -2
  35. package/bin/commands/iterate/helpers.mjs +0 -15
  36. package/bin/commands/iterate/index.mjs +6 -12
  37. package/bin/commands/iterate/mark-ready.d.mts +2 -0
  38. package/bin/commands/iterate/mark-ready.mjs +38 -0
  39. package/bin/commands/iterate/merge-state.mjs +38 -9
  40. package/bin/commands/iterate/merge.mjs +0 -1
  41. package/bin/commands/iterate/render.d.mts +2 -2
  42. package/bin/commands/iterate/render.mjs +19 -20
  43. package/bin/commands/iterate/stall.mjs +4 -2
  44. package/bin/commands/iterate/thread-mutation-routing.d.mts +9 -0
  45. package/bin/commands/iterate/thread-mutation-routing.mjs +29 -0
  46. package/bin/commands/journal/index.d.mts +5 -0
  47. package/bin/commands/journal/index.mjs +11 -2
  48. package/bin/commands/mark-files-as-viewed.d.mts +2 -1
  49. package/bin/commands/mark-files-as-viewed.mjs +5 -92
  50. package/bin/commands/resolve-mutate.mjs +51 -9
  51. package/bin/commands/shepherd-journal.d.mts +1 -1
  52. package/bin/commands/shepherd-journal.mjs +2 -2
  53. package/bin/commands/suggestion-patch-item.d.mts +1 -1
  54. package/bin/commands/suggestion-patch-item.mjs +3 -11
  55. package/bin/commands/suggestion-patches.mjs +3 -3
  56. package/bin/comments/authors.d.mts +6 -0
  57. package/bin/comments/authors.mjs +5 -0
  58. package/bin/comments/marker.d.mts +6 -1
  59. package/bin/comments/marker.mjs +8 -1
  60. package/bin/comments/resolve.d.mts +4 -0
  61. package/bin/comments/thread-visibility.mjs +10 -10
  62. package/bin/comments/visible-comments.mjs +2 -1
  63. package/bin/config/load.d.mts +1 -1
  64. package/bin/exit-codes.d.mts +1 -1
  65. package/bin/exit-codes.mjs +1 -1
  66. package/bin/github/batch-parsers.d.mts +2 -2
  67. package/bin/github/batch-parsers.mjs +17 -1
  68. package/bin/github/batch-raw-types.d.mts +12 -0
  69. package/bin/github/batch.mjs +1 -1
  70. package/bin/github/client.d.mts +1 -0
  71. package/bin/github/client.mjs +8 -1
  72. package/bin/github/errors.d.mts +6 -0
  73. package/bin/github/errors.mjs +27 -1
  74. package/bin/github/gql/batch-pr-page.gql +6 -0
  75. package/bin/github/gql/batch-pr.gql +13 -0
  76. package/bin/github/gql/get-pr-body.gql +1 -0
  77. package/bin/github/gql/review-thread-comments.gql +1 -0
  78. package/bin/github/gql/suggestion-threads.gql +1 -0
  79. package/bin/github/graphql-http.mjs +3 -2
  80. package/bin/github/graphql-internal-retry.d.mts +2 -0
  81. package/bin/github/graphql-internal-retry.mjs +50 -0
  82. package/bin/github/graphql-response.mjs +11 -1
  83. package/bin/github/suggestion-thread.mjs +1 -0
  84. package/bin/mcp/server.mjs +12 -6
  85. package/bin/pr-reference.d.mts +13 -0
  86. package/bin/pr-reference.mjs +23 -1
  87. package/bin/reporters/agent.mjs +5 -0
  88. package/bin/threads/transcript.d.mts +2 -0
  89. package/bin/threads/transcript.mjs +2 -0
  90. package/bin/types/agent-thread.d.mts +1 -0
  91. package/bin/types/escalate.d.mts +6 -1
  92. package/bin/types/github.d.mts +19 -0
  93. package/bin/types/iterate.d.mts +9 -8
  94. package/bin/types/report.d.mts +14 -1
  95. package/bin/types/review-thread.d.mts +1 -0
  96. package/bin/types/suggestion-patch.d.mts +1 -1
  97. package/package.json +1 -1
  98. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  99. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  100. package/plugins/pr-shepherd/.mcp.json +1 -1
  101. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +3 -3
  102. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +15 -13
  103. package/bin/commands/iterate/reruns.d.mts +0 -20
  104. package/bin/commands/iterate/reruns.mjs +0 -105
@@ -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, `Retry-After` present. */
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;
@@ -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, `Retry-After` present. */
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;
@@ -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,
@@ -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>;
@@ -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 { nodeId: pullRequest.id, body: pullRequest.body ?? "" };
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) {
@@ -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;
@@ -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 || status >= 500 || retryAfterSeconds !== undefined || rateLimitExhausted) {
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
@@ -3,6 +3,7 @@ query GetPrBody($owner: String!, $repo: String!, $pr: Int!) {
3
3
  pullRequest(number: $pr) {
4
4
  id
5
5
  body
6
+ viewerCanUpdate
6
7
  }
7
8
  }
8
9
  }
@@ -12,6 +12,7 @@ query ReviewThreadComments($threadId: ID!, $commentsCursor: String) {
12
12
  isMinimized
13
13
  url
14
14
  authorAssociation
15
+ viewerDidAuthor
15
16
  author {
16
17
  __typename
17
18
  login
@@ -24,6 +24,7 @@ query SuggestionThreads($owner: String!, $repo: String!, $pr: Int!, $threadIds:
24
24
  isMinimized
25
25
  url
26
26
  authorAssociation
27
+ viewerDidAuthor
27
28
  author {
28
29
  __typename
29
30
  login
@@ -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,2 @@
1
+ /** Retry GitHub GraphQL engine crashes (HTTP 200, data: null, INTERNAL) on reads. */
2
+ export declare function withGraphQlInternalRetry<T>(document: string, run: () => Promise<T>): Promise<T>;
@@ -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,
@@ -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.boolean().optional(),
23
- neverCancelRuns: z.array(z.string()).optional(),
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 current pull request and return the next Shepherd state.",
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, and journal operations after prevalidation.",
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,
@@ -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;
@@ -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 === 4 &&
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;
@@ -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;