pr-shepherd 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.grok-plugin/marketplace.json +17 -0
  3. package/README.md +63 -41
  4. package/bin/api.d.mts +80 -0
  5. package/bin/api.mjs +236 -0
  6. package/bin/checks/classify.d.mts +41 -0
  7. package/bin/checks/startup-failures.d.mts +2 -0
  8. package/bin/checks/superseded.d.mts +21 -0
  9. package/bin/checks/triage.d.mts +4 -0
  10. package/bin/classify/apply.d.mts +18 -0
  11. package/bin/classify/apply.mjs +4 -0
  12. package/bin/classify/loader.d.mts +10 -0
  13. package/bin/classify/types.d.mts +35 -0
  14. package/bin/cli/args.d.mts +18 -0
  15. package/bin/cli/clean-formatter.d.mts +2 -0
  16. package/bin/cli/default-poll.d.mts +2 -0
  17. package/bin/cli/default-poll.mjs +2 -1
  18. package/bin/cli/duration-flag.d.mts +2 -0
  19. package/bin/cli/duration-flag.mjs +5 -4
  20. package/bin/cli/duration.d.mts +13 -0
  21. package/bin/cli/{exit-codes.mjs → duration.mjs} +0 -26
  22. package/bin/cli/fence.d.mts +1 -0
  23. package/bin/cli/fix-formatter-extra.d.mts +3 -0
  24. package/bin/cli/fix-formatter.d.mts +2 -0
  25. package/bin/cli/fix-formatter.mjs +6 -6
  26. package/bin/cli/formatters.d.mts +7 -0
  27. package/bin/cli/handlers.d.mts +4 -0
  28. package/bin/cli/handlers.mjs +20 -18
  29. package/bin/cli/help-command-pages.d.mts +231 -0
  30. package/bin/cli/help-command-pages.mjs +116 -72
  31. package/bin/cli/help-iterate-poll-pages.d.mts +4 -0
  32. package/bin/cli/help-iterate-poll-pages.mjs +74 -0
  33. package/bin/cli/help-log-file-page.d.mts +1 -0
  34. package/bin/cli/help-top-page.d.mts +1 -0
  35. package/bin/cli/help-top-page.mjs +22 -20
  36. package/bin/cli/help.d.mts +236 -0
  37. package/bin/cli/help.mjs +17 -0
  38. package/bin/cli/iterate-emitter.d.mts +8 -0
  39. package/bin/cli/iterate-emitter.mjs +2 -2
  40. package/bin/cli/iterate-flags.d.mts +11 -0
  41. package/bin/cli/iterate-flags.mjs +1 -1
  42. package/bin/cli/iterate-formatter.d.mts +19 -0
  43. package/bin/cli/iterate-instructions.d.mts +6 -0
  44. package/bin/cli/iterate-lean.d.mts +12 -0
  45. package/bin/cli/journal-formatter.d.mts +2 -0
  46. package/bin/cli/journal-formatter.mjs +15 -0
  47. package/bin/cli/journal-handler.d.mts +1 -0
  48. package/bin/cli/journal-handler.mjs +13 -25
  49. package/bin/cli/list-formatters.d.mts +76 -0
  50. package/bin/cli/list-formatters.mjs +9 -9
  51. package/bin/cli/mark-files-as-viewed-flags.d.mts +11 -0
  52. package/bin/cli/mark-files-as-viewed-formatter.d.mts +2 -0
  53. package/bin/cli/mutate-formatter.d.mts +2 -0
  54. package/bin/cli/poll-handler.d.mts +1 -0
  55. package/bin/cli/poll-handler.mjs +3 -3
  56. package/bin/cli/resolve-validators.d.mts +3 -0
  57. package/bin/cli/resolve-validators.mjs +6 -5
  58. package/bin/cli/runner.d.mts +7 -0
  59. package/bin/cli/suggestion-renderer.d.mts +3 -0
  60. package/bin/cli/validate-default-args.d.mts +6 -0
  61. package/bin/cli-parser.d.mts +2 -0
  62. package/bin/cli-parser.mjs +77 -20
  63. package/bin/commands/check-annotations.d.mts +5 -0
  64. package/bin/commands/check-status.d.mts +3 -0
  65. package/bin/commands/check-terminal-report.d.mts +5 -0
  66. package/bin/commands/check.d.mts +6 -0
  67. package/bin/commands/check.mjs +4 -2
  68. package/bin/commands/clean.d.mts +21 -0
  69. package/bin/commands/commit-suggestion-instruction.d.mts +8 -0
  70. package/bin/commands/commit-suggestion-instruction.mjs +3 -3
  71. package/bin/commands/commit-suggestion.d.mts +8 -0
  72. package/bin/commands/commit-suggestion.mjs +28 -26
  73. package/bin/commands/iterate/check-instructions.d.mts +16 -0
  74. package/bin/commands/iterate/check-instructions.mjs +3 -3
  75. package/bin/commands/iterate/classify.d.mts +18 -0
  76. package/bin/commands/iterate/classify.mjs +4 -4
  77. package/bin/commands/iterate/escalate.d.mts +31 -0
  78. package/bin/commands/iterate/escalate.mjs +7 -4
  79. package/bin/commands/iterate/fix-code.d.mts +25 -0
  80. package/bin/commands/iterate/helpers.d.mts +13 -0
  81. package/bin/commands/iterate/helpers.mjs +4 -1
  82. package/bin/commands/iterate/index.d.mts +2 -0
  83. package/bin/commands/iterate/index.mjs +10 -5
  84. package/bin/commands/iterate/render.d.mts +5 -0
  85. package/bin/commands/iterate/render.mjs +8 -6
  86. package/bin/commands/iterate/reruns.d.mts +20 -0
  87. package/bin/commands/iterate/stall.d.mts +6 -0
  88. package/bin/commands/journal/index.d.mts +14 -0
  89. package/bin/commands/journal/index.mjs +1 -0
  90. package/bin/commands/journal/transform.d.mts +22 -0
  91. package/bin/commands/log-file.d.mts +5 -0
  92. package/bin/commands/mark-files-as-viewed.d.mts +26 -0
  93. package/bin/commands/mark-files-as-viewed.mjs +8 -5
  94. package/bin/commands/poll.d.mts +10 -0
  95. package/bin/commands/poll.mjs +1 -0
  96. package/bin/commands/ready-delay.d.mts +29 -0
  97. package/bin/commands/ready-mergeability.d.mts +15 -0
  98. package/bin/commands/resolve-mutate.d.mts +4 -0
  99. package/bin/commands/resolve-mutate.mjs +3 -1
  100. package/bin/commands/resolve.d.mts +4 -0
  101. package/bin/commands/shepherd-journal.d.mts +7 -0
  102. package/bin/commands/shepherd-journal.mjs +2 -2
  103. package/bin/comments/authors.d.mts +14 -0
  104. package/bin/comments/marker.d.mts +2 -0
  105. package/bin/comments/minimize-policy.d.mts +4 -0
  106. package/bin/comments/pending-ops.d.mts +15 -0
  107. package/bin/comments/rate-limit.d.mts +18 -0
  108. package/bin/comments/resolve.d.mts +34 -0
  109. package/bin/comments/resolve.mjs +1 -0
  110. package/bin/comments/review-thread-markers.d.mts +8 -0
  111. package/bin/comments/review-visibility.d.mts +28 -0
  112. package/bin/comments/sha-poll.d.mts +2 -0
  113. package/bin/comments/thread-visibility.d.mts +11 -0
  114. package/bin/comments/visible-comments.d.mts +11 -0
  115. package/bin/config/load.d.mts +60 -0
  116. package/bin/config/load.mjs +72 -1
  117. package/bin/config.json +0 -2
  118. package/bin/execution-context.d.mts +9 -0
  119. package/bin/execution-context.mjs +19 -0
  120. package/bin/exit-codes.d.mts +51 -0
  121. package/bin/exit-codes.mjs +74 -0
  122. package/bin/github/activity.d.mts +3 -0
  123. package/bin/github/activity.mjs +7 -0
  124. package/bin/github/batch-parser-helpers.d.mts +22 -0
  125. package/bin/github/batch-parsers.d.mts +3 -0
  126. package/bin/github/batch-parsers.mjs +6 -0
  127. package/bin/github/batch-raw-types.d.mts +207 -0
  128. package/bin/github/batch-response.d.mts +4 -0
  129. package/bin/github/batch-response.mjs +8 -3
  130. package/bin/github/batch.d.mts +22 -0
  131. package/bin/github/branch-protection.d.mts +3 -0
  132. package/bin/github/check-annotations.d.mts +2 -0
  133. package/bin/github/client.d.mts +46 -0
  134. package/bin/github/client.mjs +7 -2
  135. package/bin/github/errors.d.mts +25 -0
  136. package/bin/github/errors.mjs +26 -2
  137. package/bin/github/gql/batch-pr.gql +5 -0
  138. package/bin/github/gql/review-thread-comments.gql +1 -0
  139. package/bin/github/graphql-http.d.mts +19 -0
  140. package/bin/github/graphql-response.d.mts +7 -0
  141. package/bin/github/graphql-response.mjs +5 -0
  142. package/bin/github/http-auth.d.mts +4 -0
  143. package/bin/github/http-auth.mjs +2 -1
  144. package/bin/github/http-request.d.mts +7 -0
  145. package/bin/github/http-utils.d.mts +11 -0
  146. package/bin/github/http.d.mts +5 -0
  147. package/bin/github/pagination.d.mts +45 -0
  148. package/bin/github/queries.d.mts +24 -0
  149. package/bin/github/rest-http.d.mts +2 -0
  150. package/bin/github/rest-http.mjs +19 -5
  151. package/bin/github/thread-comments.d.mts +2 -0
  152. package/bin/index.d.mts +10 -0
  153. package/bin/index.mjs +3 -2
  154. package/bin/log/log-file.d.mts +28 -0
  155. package/bin/log/session.d.mts +31 -0
  156. package/bin/log/setup.d.mts +6 -0
  157. package/bin/mcp/index.d.mts +5 -0
  158. package/bin/mcp/index.mjs +8 -0
  159. package/bin/mcp/server.d.mts +8 -0
  160. package/bin/mcp/server.mjs +157 -0
  161. package/bin/mcp-stdio.d.mts +2 -0
  162. package/bin/mcp-stdio.mjs +7 -0
  163. package/bin/merge-status/derive.d.mts +19 -0
  164. package/bin/reporters/agent.d.mts +23 -0
  165. package/bin/reporters/agent.mjs +3 -0
  166. package/bin/state/base.d.mts +1 -0
  167. package/bin/state/bot-cr-seen.d.mts +51 -0
  168. package/bin/state/bot-cr-seen.mjs +1 -1
  169. package/bin/state/fix-attempts.d.mts +27 -0
  170. package/bin/state/iterate-stall.d.mts +27 -0
  171. package/bin/state/seen-comments.d.mts +62 -0
  172. package/bin/suggestions/extract.d.mts +8 -0
  173. package/bin/suggestions/parse.d.mts +48 -0
  174. package/bin/suggestions/patch.d.mts +14 -0
  175. package/bin/threads/transcript.d.mts +14 -0
  176. package/bin/threads/transcript.mjs +4 -0
  177. package/bin/types/activity.d.mts +30 -0
  178. package/bin/types/agent-thread.d.mts +9 -0
  179. package/bin/types/check-annotations.d.mts +14 -0
  180. package/bin/types/check-classification.d.mts +19 -0
  181. package/bin/types/github.d.mts +139 -0
  182. package/bin/types/iterate.d.mts +157 -0
  183. package/bin/types/protected-run.d.mts +6 -0
  184. package/bin/types/report.d.mts +176 -0
  185. package/bin/types/review-thread.d.mts +12 -0
  186. package/bin/types.d.mts +9 -0
  187. package/bin/util/markdown.d.mts +1 -0
  188. package/bin/util/path-segment.d.mts +2 -0
  189. package/bin/util/sleep.d.mts +1 -0
  190. package/bin/util/worktree.d.mts +9 -0
  191. package/bin/util/worktree.mjs +4 -1
  192. package/package.json +51 -37
  193. package/plugins/pr-shepherd/.codex-plugin/plugin.json +3 -2
  194. package/plugins/pr-shepherd/.codex.mcp.json +8 -0
  195. package/plugins/pr-shepherd/.mcp.json +6 -0
  196. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +5 -19
  197. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -15
  198. package/src/classify/types.mts +12 -0
@@ -11,6 +11,7 @@ query ReviewThreadComments($threadId: ID!, $commentsCursor: String) {
11
11
  id
12
12
  isMinimized
13
13
  url
14
+ authorAssociation
14
15
  author {
15
16
  __typename
16
17
  login
@@ -0,0 +1,19 @@
1
+ import { type GitHubGraphQlError } from "./errors.mts";
2
+ import { type RateLimitInfo } from "./http-utils.mts";
3
+ interface GraphQlResult<T = unknown> {
4
+ data: T;
5
+ rateLimit?: RateLimitInfo;
6
+ retryAfterSeconds?: number;
7
+ errors?: GitHubGraphQlError[];
8
+ }
9
+ export interface GraphQlRequestOptions {
10
+ /**
11
+ * Mutation batches use partial GraphQL data to preserve per-alias successes.
12
+ * Read paths must keep the strict default so incomplete snapshots never drive
13
+ * PR state transitions.
14
+ */
15
+ allowPartialData?: boolean;
16
+ }
17
+ export declare function graphql<T = unknown>(query: string, vars?: Record<string, unknown>, opts?: GraphQlRequestOptions): Promise<GraphQlResult<T>>;
18
+ export declare function graphqlWithRateLimit<T = unknown>(query: string, vars?: Record<string, unknown>, opts?: GraphQlRequestOptions): Promise<GraphQlResult<T>>;
19
+ export {};
@@ -0,0 +1,7 @@
1
+ import { type GitHubGraphQlError } from "./errors.mts";
2
+ import type { RateLimitInfo } from "./http-utils.mts";
3
+ export declare function parseGraphQlPayload<T>(parsed: unknown, status: number, rateLimit: RateLimitInfo | null, retryAfterSeconds: number | undefined): {
4
+ data: T | null;
5
+ errors?: GitHubGraphQlError[];
6
+ };
7
+ export declare function formatGraphQlErrors(errors: GitHubGraphQlError[] | undefined): string;
@@ -1,3 +1,4 @@
1
+ import { EXIT } from "../exit-codes.mjs";
1
2
  import { GitHubRequestError } from "./errors.mjs";
2
3
  export function parseGraphQlPayload(parsed, status, rateLimit, retryAfterSeconds) {
3
4
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -34,9 +35,13 @@ export function formatGraphQlErrors(errors) {
34
35
  .join("; ");
35
36
  }
36
37
  function malformedGraphQlResponse(detail, status, rateLimit, retryAfterSeconds) {
38
+ // A response that fails to parse as valid GraphQL shape is an internal/unexpected
39
+ // failure, not a precondition or permission problem — force EX_SOFTWARE rather
40
+ // than letting the (likely 200) status fall through to EX_UNAVAILABLE.
37
41
  return new GitHubRequestError(`Malformed GitHub GraphQL response: ${detail}`, {
38
42
  status,
39
43
  rateLimit: rateLimit ?? undefined,
40
44
  retryAfterSeconds,
45
+ exitCodeOverride: EXIT.SOFTWARE,
41
46
  });
42
47
  }
@@ -0,0 +1,4 @@
1
+ export declare function _resetTokenCache(): void;
2
+ export declare function hasCachedToken(): boolean;
3
+ export declare function clearTokenCache(): void;
4
+ export declare function makeHeaders(): Promise<Record<string, string>>;
@@ -1,5 +1,6 @@
1
1
  import { execFile as execFileCb } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
3
4
  const execFile = promisify(execFileCb);
4
5
  let _token;
5
6
  export function _resetTokenCache() {
@@ -35,7 +36,7 @@ async function resolveToken() {
35
36
  _token = codexToken;
36
37
  return _token;
37
38
  }
38
- throw new Error("No GitHub token found. Set GH_TOKEN, GITHUB_TOKEN, or GITHUB_PERSONAL_ACCESS_TOKEN, or run `gh auth login`.");
39
+ throw new ShepherdError("No GitHub token found. Set GH_TOKEN, GITHUB_TOKEN, or GITHUB_PERSONAL_ACCESS_TOKEN, or run `gh auth login`.", EXIT.NOPERM);
39
40
  }
40
41
  export async function makeHeaders() {
41
42
  return {
@@ -0,0 +1,7 @@
1
+ type RetryLogFn = (status: number, durationMs: number) => void;
2
+ export declare function requestWithTokenRetry(fn: () => Promise<Response>, t0: number, onIntermediate?: RetryLogFn): Promise<{
3
+ res: Response;
4
+ attempt: number;
5
+ retryT0: number;
6
+ }>;
7
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare function isTransportError(err: unknown): boolean;
2
+ export interface RateLimitInfo {
3
+ remaining: number;
4
+ limit: number;
5
+ resetAt: number;
6
+ }
7
+ export declare function sanitizeBody(body: string): string;
8
+ export declare function redactToken(body: string): string;
9
+ export declare function redactUrl(url: string): string;
10
+ export declare function parseRateLimit(headers: Headers): RateLimitInfo | null;
11
+ export declare function parseRetryAfter(headers: Headers): number | undefined;
@@ -0,0 +1,5 @@
1
+ export { GitHubRequestError } from "./errors.mts";
2
+ export { _resetTokenCache } from "./http-auth.mts";
3
+ export { graphql, graphqlWithRateLimit } from "./graphql-http.mts";
4
+ export { rest, restText } from "./rest-http.mts";
5
+ export type { RateLimitInfo } from "./http-utils.mts";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Generic GraphQL cursor-based paginator for shepherd.
3
+ *
4
+ * Both paginators accept a `fetchFn` instead of calling `graphql` directly,
5
+ * which makes them testable without any mocking — tests supply a pure function
6
+ * that returns pages of fake data.
7
+ *
8
+ * GitHub's GraphQL connections support two cursor directions:
9
+ * - Forward (`after` + `first`) — used by check contexts.
10
+ * - Backward (`before` + `last`) — used by reviewThreads (default GitHub order).
11
+ */
12
+ interface PageInfo {
13
+ hasNextPage?: boolean;
14
+ hasPreviousPage?: boolean;
15
+ endCursor?: string | null;
16
+ startCursor?: string | null;
17
+ }
18
+ export interface Connection<T> {
19
+ pageInfo: PageInfo;
20
+ nodes: T[];
21
+ }
22
+ /**
23
+ * Paginate forward through a GraphQL connection (`first` / `after` cursors).
24
+ *
25
+ * @param fetchFn Called once per page. Receives the cursor (or null for
26
+ * the very first page) and returns a Connection<T>.
27
+ * @param initialCursor Start from this cursor instead of null. Pass the
28
+ * `endCursor` of an already-fetched page to fetch only
29
+ * the pages *after* it, avoiding a duplicate re-fetch.
30
+ */
31
+ export declare function paginateForward<T>(fetchFn: (cursor: string | null) => Promise<Connection<T>>, initialCursor?: string | null): Promise<T[]>;
32
+ /**
33
+ * Paginate backward through a GraphQL connection (`last` / `before` cursors).
34
+ *
35
+ * Used for `reviewThreads(last: 100, before: $before)`.
36
+ *
37
+ * @param fetchFn Called once per page. Receives the cursor (or null for
38
+ * the very first page). Returns a Connection<T> with
39
+ * `hasPreviousPage` and `startCursor` in pageInfo.
40
+ * @param initialCursor Start from this cursor instead of null. Pass the
41
+ * `startCursor` of an already-fetched page to avoid
42
+ * re-fetching it (fetch only the pages *before* it).
43
+ */
44
+ export declare function paginateBackward<T>(fetchFn: (cursor: string | null) => Promise<Connection<T>>, initialCursor?: string | null): Promise<T[]>;
45
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * GraphQL query strings used by pr-shepherd.
3
+ *
4
+ * Static query strings live in src/github/gql/*.gql and are loaded here.
5
+ * Dynamic documents whose content varies per call (e.g. BulkApply in
6
+ * src/comments/resolve.mts) are built at runtime and are exempt — they
7
+ * cannot be expressed as static files.
8
+ */
9
+ /** The primary batch query that fetches CI + comments + merge status in one round-trip. */
10
+ export declare const BATCH_PR_QUERY: string;
11
+ /** Fetches additional comments for a single review thread when its nested connection paginates. */
12
+ export declare const REVIEW_THREAD_COMMENTS_QUERY: string;
13
+ /** Fetch inline annotations for a single CheckRun by node ID. */
14
+ export declare const CHECK_RUN_ANNOTATIONS_QUERY: string;
15
+ /** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
16
+ export declare const GET_PR_HEAD_SHA_QUERY: string;
17
+ /** Look up PR number by branch name (for getCurrentPrNumber). */
18
+ export declare const PR_NUMBER_BY_BRANCH_QUERY: string;
19
+ /** Convert a draft PR to ready for review. */
20
+ export declare const MARK_PR_READY_MUTATION: string;
21
+ /** Fetch the PR body and node ID. */
22
+ export declare const GET_PR_BODY_QUERY: string;
23
+ /** Update the PR body. */
24
+ export declare const UPDATE_PR_BODY_MUTATION: string;
@@ -0,0 +1,2 @@
1
+ export declare function rest<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
2
+ export declare function restText(path: string): Promise<string>;
@@ -1,8 +1,9 @@
1
1
  import { appendEntry, nextEntry } from "../log/log-file.mjs";
2
2
  import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
3
+ import { GitHubRequestError } from "./errors.mjs";
3
4
  import { makeHeaders } from "./http-auth.mjs";
4
5
  import { requestWithTokenRetry } from "./http-request.mjs";
5
- import { redactToken, redactUrl, sanitizeBody } from "./http-utils.mjs";
6
+ import { parseRateLimit, parseRetryAfter, redactToken, redactUrl, sanitizeBody, } from "./http-utils.mjs";
6
7
  const BASE_URL = "https://api.github.com";
7
8
  export async function rest(method, path, body) {
8
9
  const url = `${BASE_URL}${path}`;
@@ -28,7 +29,11 @@ export async function rest(method, path, body) {
28
29
  textBody: redactToken(text),
29
30
  attempt: attempt > 1 ? attempt : undefined,
30
31
  }));
31
- throw new Error(`GitHub REST ${method} ${path} failed: ${res.status} ${sanitizeBody(text)}`);
32
+ throw new GitHubRequestError(`GitHub REST ${method} ${path} failed: ${res.status} ${sanitizeBody(text)}`, {
33
+ status: res.status,
34
+ rateLimit: parseRateLimit(res.headers) ?? undefined,
35
+ retryAfterSeconds: parseRetryAfter(res.headers),
36
+ });
32
37
  }
33
38
  if (ct.includes("application/json")) {
34
39
  const json = (await res.json());
@@ -80,7 +85,11 @@ export async function restText(path) {
80
85
  durationMs,
81
86
  attempt: attempt > 1 ? attempt : undefined,
82
87
  }));
83
- throw new Error(`GitHub REST GET ${path} failed: ${res.status} ${sanitizeBody(text)}`);
88
+ throw new GitHubRequestError(`GitHub REST GET ${path} failed: ${res.status} ${sanitizeBody(text)}`, {
89
+ status: res.status,
90
+ rateLimit: parseRateLimit(res.headers) ?? undefined,
91
+ retryAfterSeconds: parseRetryAfter(res.headers),
92
+ });
84
93
  }
85
94
  appendEntry(formatResponseEntry({
86
95
  n,
@@ -121,8 +130,13 @@ async function followRestTextRedirect(res, entry) {
121
130
  durationMs: Math.round(performance.now() - t1),
122
131
  contentLength: parseContentLength(redirectRes.headers),
123
132
  }));
124
- if (!redirectRes.ok)
125
- throw new Error(`redirect target ${location} failed: ${redirectRes.status}`);
133
+ if (!redirectRes.ok) {
134
+ throw new GitHubRequestError(`redirect target ${location} failed: ${redirectRes.status}`, {
135
+ status: redirectRes.status,
136
+ rateLimit: parseRateLimit(redirectRes.headers) ?? undefined,
137
+ retryAfterSeconds: parseRetryAfter(redirectRes.headers),
138
+ });
139
+ }
126
140
  return redirectRes.text();
127
141
  }
128
142
  function parseContentLength(headers) {
@@ -0,0 +1,2 @@
1
+ import type { RawThread } from "./batch-raw-types.mts";
2
+ export declare function hydrateThreadCommentPages(threads: RawThread[]): Promise<RawThread[]>;
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pr-shepherd — unified GitHub PR status + auto-resolve CLI
4
+ *
5
+ * Usage:
6
+ * pr-shepherd [PR]
7
+ * pr-shepherd apply review [PR]
8
+ * pr-shepherd iterate [PR]
9
+ */
10
+ export {};
package/bin/index.mjs CHANGED
@@ -4,10 +4,11 @@
4
4
  *
5
5
  * Usage:
6
6
  * pr-shepherd [PR]
7
- * pr-shepherd resolve [PR]
7
+ * pr-shepherd apply review [PR]
8
8
  * pr-shepherd iterate [PR]
9
9
  */
10
10
  import { main } from "./cli-parser.mjs";
11
+ import { errorToExitCode } from "./exit-codes.mjs";
11
12
  function formatCause(cause, seen = new Set(), depth = 0) {
12
13
  if (depth > 5 || seen.has(cause))
13
14
  return "[circular or deep cause chain]";
@@ -23,5 +24,5 @@ main(process.argv).catch((err) => {
23
24
  const msg = err instanceof Error ? err.message : String(err);
24
25
  const causeStr = err instanceof Error && err.cause != null ? formatCause(err.cause) : null;
25
26
  process.stderr.write(`pr-shepherd error: ${msg}${causeStr !== null ? ` (cause: ${causeStr})` : ""}\n`);
26
- process.exit(1);
27
+ process.exit(errorToExitCode(err));
27
28
  });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Append-only per-worktree markdown log.
3
+ *
4
+ * Log path: $PR_SHEPHERD_STATE_DIR/<owner>-<repo>/worktrees/<basename>-<sha8>.md
5
+ *
6
+ * Always-on by default. Set PR_SHEPHERD_LOG_DISABLED=1 or CI=true to disable.
7
+ * Write failures flip an internal disabled flag so the CLI never crashes because
8
+ * logging failed.
9
+ */
10
+ /** Returns the next monotonically-increasing entry number for the current session. */
11
+ export declare function nextEntry(): number;
12
+ interface RepoKey {
13
+ owner: string;
14
+ repo: string;
15
+ }
16
+ export declare function getLogFilePath(key: RepoKey): string;
17
+ /**
18
+ * Initialize the log for this process. Must be called before appendEntry().
19
+ * Silently disables logging on any error (no git repo, bad repo name, etc.).
20
+ */
21
+ export declare function initLog(repoKey: RepoKey): Promise<string | null>;
22
+ /** Append a pre-formatted markdown chunk to the log. No-op if disabled. */
23
+ export declare function appendEntry(markdown: string): void;
24
+ /** Resolve the log path without initializing (for the log-file subcommand). */
25
+ export declare function resolveLogPath(repoKey: RepoKey): Promise<string>;
26
+ /** Exposed for tests to reset module state. */
27
+ export declare function _resetLogState(): void;
28
+ export {};
@@ -0,0 +1,31 @@
1
+ /** Builds the session header markdown block. */
2
+ export declare function buildSessionHeader(argv: string[]): {
3
+ markdown: string;
4
+ };
5
+ interface HttpRequestEntry {
6
+ n: number;
7
+ kind: "GraphQL" | "REST" | "restText";
8
+ method: string;
9
+ url: string;
10
+ body?: unknown;
11
+ }
12
+ interface HttpResponseEntry {
13
+ n: number;
14
+ kind: "GraphQL" | "REST" | "restText";
15
+ method: string;
16
+ url: string;
17
+ status: number;
18
+ durationMs: number;
19
+ /** Parsed response body. Omit for restText. */
20
+ body?: unknown;
21
+ /** Raw text response body. Omit for restText. */
22
+ textBody?: string;
23
+ contentType?: string;
24
+ contentLength?: number;
25
+ /** Set on 401-retry invocations. */
26
+ attempt?: number;
27
+ }
28
+ export declare function formatRequestEntry(entry: HttpRequestEntry): string;
29
+ export declare function formatResponseEntry(entry: HttpResponseEntry): string;
30
+ export declare function formatOutputEntry(text: string, format: "text" | "json"): string;
31
+ export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Initialize the per-worktree log, write the session header, and install a
3
+ * stdout tee that routes all CLI output to the log. No-op after the first call.
4
+ * Silently skips logging when not in a git repo or on any other error.
5
+ */
6
+ export declare function setupLog(argv: string[]): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { type CreatePrShepherdMcpServerOptions } from "./server.mts";
2
+ export { createPrShepherdMcpServer } from "./server.mts";
3
+ export type { CreatePrShepherdMcpServerOptions } from "./server.mts";
4
+ /** Starts the local stdio-only MCP transport. */
5
+ export declare function runPrShepherdMcpStdio(options?: CreatePrShepherdMcpServerOptions): Promise<void>;
@@ -0,0 +1,8 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createPrShepherdMcpServer } from "./server.mjs";
3
+ export { createPrShepherdMcpServer } from "./server.mjs";
4
+ /** Starts the local stdio-only MCP transport. */
5
+ export async function runPrShepherdMcpStdio(options = {}) {
6
+ const server = createPrShepherdMcpServer(options);
7
+ await server.connect(new StdioServerTransport());
8
+ }
@@ -0,0 +1,8 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type CreatePrShepherdOptions, type PrShepherd } from "../api.mts";
3
+ export interface CreatePrShepherdMcpServerOptions extends CreatePrShepherdOptions {
4
+ /** Optional injection point for embedding hosts and focused tests. */
5
+ shepherd?: PrShepherd;
6
+ }
7
+ /** Creates a local-only MCP server with the three public Shepherd operations. */
8
+ export declare function createPrShepherdMcpServer(options?: CreatePrShepherdMcpServerOptions): McpServer;
@@ -0,0 +1,157 @@
1
+ /* eslint-disable max-lines */
2
+ import { readFileSync } from "node:fs";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { z } from "zod";
5
+ import { createPrShepherd, PartialApplyError, PrShepherdValidationError, } from "../api.mjs";
6
+ import { formatJournalResult } from "../cli/journal-formatter.mjs";
7
+ import { formatCommitSuggestionResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, } from "../cli/formatters.mjs";
8
+ import { errorToExitCode, EXIT } from "../exit-codes.mjs";
9
+ const pr = z.union([z.number().int().positive(), z.string().url()]).optional();
10
+ const ids = z.array(z.string().min(1)).optional();
11
+ const iterateInputSchema = z.object({
12
+ pr,
13
+ readyDelaySeconds: z.number().nonnegative().optional(),
14
+ stallTimeoutSeconds: z.number().nonnegative().optional(),
15
+ noAutoMarkReady: z.boolean().optional(),
16
+ noAutoCancelActionable: z.boolean().optional(),
17
+ neverCancelRuns: z.array(z.string()).optional(),
18
+ });
19
+ const reviewMutationsOperationSchema = z.object({
20
+ type: z.literal("review_mutations"),
21
+ resolveThreadIds: ids,
22
+ replyThreadIds: ids,
23
+ minimizeCommentIds: ids,
24
+ dismissReviewIds: ids,
25
+ message: z.string().optional(),
26
+ requireSha: z.string().optional(),
27
+ });
28
+ const markFilesViewedOperationSchema = z.object({
29
+ type: z.literal("mark_files_viewed"),
30
+ files: z.array(z.string().min(1)).optional(),
31
+ tests: z.boolean().optional(),
32
+ matchPatterns: z.array(z.string().min(1)).optional(),
33
+ });
34
+ const appendJournalOperationSchema = z.object({
35
+ type: z.literal("append_journal"),
36
+ item: z.string(),
37
+ dryRun: z.boolean().optional(),
38
+ });
39
+ const applyInputSchema = z.object({
40
+ pr,
41
+ operations: z
42
+ .array(z.discriminatedUnion("type", [
43
+ reviewMutationsOperationSchema,
44
+ markFilesViewedOperationSchema,
45
+ appendJournalOperationSchema,
46
+ ]))
47
+ .min(1),
48
+ });
49
+ const suggestionPatchInputSchema = z.object({
50
+ pr,
51
+ threadId: z.string().min(1),
52
+ message: z.string().min(1),
53
+ description: z.string().optional(),
54
+ });
55
+ /** Creates a local-only MCP server with the three public Shepherd operations. */
56
+ export function createPrShepherdMcpServer(options = {}) {
57
+ const shepherd = options.shepherd ?? createPrShepherd({ cwd: options.cwd });
58
+ const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
59
+ server.registerTool("iterate", {
60
+ description: "Inspect the current pull request and return the next Shepherd state.",
61
+ inputSchema: iterateInputSchema,
62
+ annotations: {
63
+ readOnlyHint: false,
64
+ destructiveHint: true,
65
+ idempotentHint: false,
66
+ openWorldHint: true,
67
+ },
68
+ }, async (input) => runTool(() => shepherd.iterate(input), formatIterateResult));
69
+ server.registerTool("apply", {
70
+ description: "Apply ordered review, file-view, and journal operations after prevalidation.",
71
+ inputSchema: applyInputSchema,
72
+ annotations: {
73
+ readOnlyHint: false,
74
+ destructiveHint: true,
75
+ idempotentHint: false,
76
+ openWorldHint: true,
77
+ },
78
+ }, async (input) => runTool(() => shepherd.apply(input), formatApplyResult));
79
+ server.registerTool("build_suggestion_patch", {
80
+ description: "Build, but never apply, a patch from an eligible review suggestion.",
81
+ inputSchema: suggestionPatchInputSchema,
82
+ annotations: {
83
+ readOnlyHint: true,
84
+ destructiveHint: false,
85
+ idempotentHint: true,
86
+ openWorldHint: true,
87
+ },
88
+ }, async (input) => runTool(() => shepherd.buildSuggestionPatch(input), formatCommitSuggestionResult));
89
+ return server;
90
+ }
91
+ function readPackageVersion() {
92
+ const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
93
+ return packageJson.version;
94
+ }
95
+ function toolResult(result, text) {
96
+ return {
97
+ content: [{ type: "text", text }],
98
+ structuredContent: result,
99
+ };
100
+ }
101
+ async function runTool(work, format) {
102
+ try {
103
+ const result = await work();
104
+ return toolResult(result, format(result));
105
+ }
106
+ catch (error) {
107
+ return toolError(error);
108
+ }
109
+ }
110
+ function formatApplyResult(result) {
111
+ return result.operations
112
+ .map((operation, index) => {
113
+ const heading = `## Operation ${index + 1}: ${operation.type}`;
114
+ switch (operation.type) {
115
+ case "review_mutations":
116
+ return `${heading}\n\n${formatMutateResult(operation.result)}`;
117
+ case "mark_files_viewed":
118
+ return `${heading}\n\n${formatMarkFilesAsViewedResult(operation.result)}`;
119
+ case "append_journal":
120
+ return `${heading}\n\n${formatJournalResult(operation.result)}`;
121
+ }
122
+ })
123
+ .join("\n\n");
124
+ }
125
+ function toolError(error) {
126
+ const code = error instanceof PrShepherdValidationError
127
+ ? EXIT.USAGE
128
+ : error instanceof PartialApplyError
129
+ ? errorToExitCode(error.cause)
130
+ : errorToExitCode(error);
131
+ const details = error instanceof PartialApplyError
132
+ ? { failedIndex: error.failedIndex, completed: redactValue(error.completed) }
133
+ : error instanceof PrShepherdValidationError
134
+ ? { validation: true }
135
+ : {};
136
+ const message = redactErrorMessage(error instanceof Error ? error.message : String(error));
137
+ return {
138
+ isError: true,
139
+ content: [{ type: "text", text: `pr-shepherd error (${code}): ${message}` }],
140
+ structuredContent: { code, message, details },
141
+ };
142
+ }
143
+ function redactErrorMessage(message) {
144
+ return message
145
+ .replace(/(?:ghp|github_pat)_[A-Za-z0-9_]+/g, "[redacted]")
146
+ .replace(/(authorization:\s*bearer\s+)[^\s]+/gi, "$1[redacted]");
147
+ }
148
+ function redactValue(value) {
149
+ if (typeof value === "string")
150
+ return redactErrorMessage(value);
151
+ if (Array.isArray(value))
152
+ return value.map(redactValue);
153
+ if (value !== null && typeof value === "object") {
154
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactValue(item)]));
155
+ }
156
+ return value;
157
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runPrShepherdMcpStdio } from "./mcp/index.mjs";
3
+ runPrShepherdMcpStdio().catch((error) => {
4
+ const message = error instanceof Error ? error.message : String(error);
5
+ process.stderr.write(`pr-shepherd-mcp error: ${message}\n`);
6
+ process.exitCode = 1;
7
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Derives a shepherd `MergeStatusResult` from raw PR data.
3
+ *
4
+ * `pr.state` is passed through unchanged; `iterate` handles the cancel action
5
+ * for non-OPEN (merged/closed) PRs — this function does not branch on it.
6
+ *
7
+ * Interpretation order for `status` — first match wins:
8
+ * 1. mergeable == CONFLICTING → CONFLICTS (hard conflict even for drafts)
9
+ * 2. blockingBotReviewInProgress → BLOCKED
10
+ * 3. mergeStateStatus DIRTY → CONFLICTS (GitHub merge conflicts, even for drafts)
11
+ * 4. isDraft → DRAFT
12
+ * 5. mergeStateStatus BEHIND → BEHIND
13
+ * 6. mergeStateStatus BLOCKED / HAS_HOOKS → BLOCKED
14
+ * 7. mergeStateStatus UNSTABLE → UNSTABLE
15
+ * 8. mergeStateStatus UNKNOWN → UNKNOWN
16
+ * 9. mergeStateStatus CLEAN → CLEAN
17
+ */
18
+ import type { BatchPrData, MergeStatusResult } from "../types.mts";
19
+ export declare function deriveMergeStatus(pr: BatchPrData): MergeStatusResult;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Projections for the agent-facing iterate output.
3
+ *
4
+ * These strip fields that are always-false by the time items reach iterate
5
+ * (isResolved, isOutdated, isMinimized, createdAtUnix) and check metadata the
6
+ * agent/iterate prompt never reads (event, status, category).
7
+ * conclusion is preserved on AgentCheck so the formatter can branch on run-level conclusions.
8
+ * detailsUrl is preserved in AgentCheck as a fallback for external status checks.
9
+ * The original domain types are preserved as internal snapshot types.
10
+ */
11
+ import type { ReviewThread, PrComment, TriagedCheck, ClassifiedCheck, AgentThread, AgentComment, AgentCheck, AgentStalledCheck } from "../types.mts";
12
+ export declare function toAgentThread(t: ReviewThread): AgentThread;
13
+ export declare function toAgentComment(c: PrComment & {
14
+ edited?: boolean;
15
+ }): AgentComment;
16
+ export declare function toAgentCheck(c: TriagedCheck): AgentCheck;
17
+ export declare function toAgentStalledCheck(c: ClassifiedCheck, nowSeconds: number): AgentStalledCheck;
18
+ /**
19
+ * Project failing checks for the agent. Deduplicates only null-runId external
20
+ * checks by name — when runId is present each check may have a distinct job and
21
+ * log tail, so they are all kept.
22
+ */
23
+ export declare function toAgentChecks(checks: TriagedCheck[]): AgentCheck[];
@@ -21,6 +21,7 @@ export function toAgentThread(t) {
21
21
  t.startLine !== t.line && { startLine: t.startLine }),
22
22
  author: t.author,
23
23
  ...(t.authorType !== undefined && { authorType: t.authorType }),
24
+ ...(t.authorAssociation !== undefined && { authorAssociation: t.authorAssociation }),
24
25
  body: t.body,
25
26
  url: t.url,
26
27
  ...(t.edited === true && { edited: true }),
@@ -29,6 +30,7 @@ export function toAgentThread(t) {
29
30
  id: c.id,
30
31
  author: c.author,
31
32
  ...(c.authorType !== undefined && { authorType: c.authorType }),
33
+ ...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
32
34
  body: c.body,
33
35
  url: c.url,
34
36
  })),
@@ -41,6 +43,7 @@ export function toAgentComment(c) {
41
43
  id: c.id,
42
44
  author: c.author,
43
45
  ...(c.authorType !== undefined && { authorType: c.authorType }),
46
+ ...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
44
47
  body: c.body,
45
48
  url: c.url,
46
49
  ...(c.edited === true && { edited: true }),
@@ -0,0 +1 @@
1
+ export declare function resolveStateBase(): string;