pr-shepherd 0.34.0 → 0.36.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 (231) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.grok-plugin/marketplace.json +17 -0
  3. package/README.md +80 -43
  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/checks/triage.mjs +11 -3
  11. package/bin/classify/apply.d.mts +18 -0
  12. package/bin/classify/apply.mjs +4 -0
  13. package/bin/classify/loader.d.mts +10 -0
  14. package/bin/classify/types.d.mts +35 -0
  15. package/bin/cli/args.d.mts +18 -0
  16. package/bin/cli/args.mjs +1 -0
  17. package/bin/cli/clean-formatter.d.mts +2 -0
  18. package/bin/cli/default-poll.d.mts +2 -0
  19. package/bin/cli/default-poll.mjs +1 -0
  20. package/bin/cli/duration-flag.d.mts +2 -0
  21. package/bin/cli/duration.d.mts +13 -0
  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 +11 -10
  29. package/bin/cli/help-command-pages.d.mts +231 -0
  30. package/bin/cli/help-command-pages.mjs +106 -1
  31. package/bin/cli/help-iterate-poll-pages.d.mts +4 -0
  32. package/bin/cli/help-iterate-poll-pages.mjs +14 -7
  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 +16 -16
  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-flags.d.mts +11 -0
  40. package/bin/cli/iterate-formatter.d.mts +19 -0
  41. package/bin/cli/iterate-formatter.mjs +11 -2
  42. package/bin/cli/iterate-instructions.d.mts +6 -0
  43. package/bin/cli/iterate-lean.d.mts +12 -0
  44. package/bin/cli/iterate-lean.mjs +1 -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 +7 -20
  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 +9 -2
  56. package/bin/cli/resolve-validators.d.mts +3 -0
  57. package/bin/cli/resolve-validators.mjs +3 -3
  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 +72 -16
  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 +7 -0
  67. package/bin/commands/check.mjs +28 -23
  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 +20 -13
  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/fix-code.mjs +1 -1
  81. package/bin/commands/iterate/helpers.d.mts +14 -0
  82. package/bin/commands/iterate/helpers.mjs +19 -7
  83. package/bin/commands/iterate/index.d.mts +2 -0
  84. package/bin/commands/iterate/index.mjs +8 -12
  85. package/bin/commands/iterate/render.d.mts +5 -0
  86. package/bin/commands/iterate/render.mjs +8 -6
  87. package/bin/commands/iterate/reruns.d.mts +20 -0
  88. package/bin/commands/iterate/stall.d.mts +6 -0
  89. package/bin/commands/journal/index.d.mts +14 -0
  90. package/bin/commands/journal/index.mjs +1 -0
  91. package/bin/commands/journal/transform.d.mts +22 -0
  92. package/bin/commands/log-file.d.mts +5 -0
  93. package/bin/commands/mark-files-as-viewed.d.mts +26 -0
  94. package/bin/commands/mark-files-as-viewed.mjs +1 -0
  95. package/bin/commands/poll.d.mts +12 -0
  96. package/bin/commands/poll.mjs +52 -17
  97. package/bin/commands/ready-delay.d.mts +29 -0
  98. package/bin/commands/ready-delay.mjs +3 -13
  99. package/bin/commands/ready-mergeability.d.mts +15 -0
  100. package/bin/commands/resolve-mutate.d.mts +4 -0
  101. package/bin/commands/resolve-mutate.mjs +1 -0
  102. package/bin/commands/resolve.d.mts +4 -0
  103. package/bin/commands/shepherd-journal.d.mts +7 -0
  104. package/bin/commands/shepherd-journal.mjs +2 -2
  105. package/bin/comments/authors.d.mts +14 -0
  106. package/bin/comments/marker.d.mts +2 -0
  107. package/bin/comments/minimize-policy.d.mts +4 -0
  108. package/bin/comments/pending-ops.d.mts +15 -0
  109. package/bin/comments/rate-limit.d.mts +18 -0
  110. package/bin/comments/resolve.d.mts +34 -0
  111. package/bin/comments/resolve.mjs +1 -0
  112. package/bin/comments/review-thread-markers.d.mts +8 -0
  113. package/bin/comments/review-visibility.d.mts +28 -0
  114. package/bin/comments/sha-poll.d.mts +2 -0
  115. package/bin/comments/thread-visibility.d.mts +11 -0
  116. package/bin/comments/visible-comments.d.mts +11 -0
  117. package/bin/config/load.d.mts +60 -0
  118. package/bin/config/load.mjs +129 -16
  119. package/bin/config.json +0 -2
  120. package/bin/execution-context.d.mts +9 -0
  121. package/bin/execution-context.mjs +19 -0
  122. package/bin/exit-codes.d.mts +51 -0
  123. package/bin/github/activity.d.mts +3 -0
  124. package/bin/github/activity.mjs +7 -0
  125. package/bin/github/batch-page-helpers.d.mts +45 -0
  126. package/bin/github/batch-page-helpers.mjs +63 -0
  127. package/bin/github/batch-page.d.mts +14 -0
  128. package/bin/github/batch-page.mjs +62 -0
  129. package/bin/github/batch-parse-suites.d.mts +4 -0
  130. package/bin/github/batch-parse-suites.mjs +25 -0
  131. package/bin/github/batch-parser-helpers.d.mts +22 -0
  132. package/bin/github/batch-parsers-rules.d.mts +6 -0
  133. package/bin/github/batch-parsers-rules.mjs +122 -0
  134. package/bin/github/batch-parsers.d.mts +3 -0
  135. package/bin/github/batch-parsers.mjs +12 -0
  136. package/bin/github/batch-raw-rules.d.mts +59 -0
  137. package/bin/github/batch-raw-rules.mjs +1 -0
  138. package/bin/github/batch-raw-types.d.mts +216 -0
  139. package/bin/github/batch-response.d.mts +4 -0
  140. package/bin/github/batch.d.mts +24 -0
  141. package/bin/github/batch.mjs +14 -120
  142. package/bin/github/branch-protection.d.mts +3 -0
  143. package/bin/github/check-annotations.d.mts +2 -0
  144. package/bin/github/client.d.mts +46 -0
  145. package/bin/github/client.mjs +7 -2
  146. package/bin/github/errors.d.mts +25 -0
  147. package/bin/github/gql/batch-pr-page.gql +189 -0
  148. package/bin/github/gql/batch-pr.gql +79 -21
  149. package/bin/github/gql/commit-suggestion-thread.gql +40 -0
  150. package/bin/github/gql/review-thread-comments.gql +1 -0
  151. package/bin/github/graphql-http.d.mts +19 -0
  152. package/bin/github/graphql-response.d.mts +7 -0
  153. package/bin/github/http-auth.d.mts +4 -0
  154. package/bin/github/http-request.d.mts +7 -0
  155. package/bin/github/http-utils.d.mts +11 -0
  156. package/bin/github/http.d.mts +6 -0
  157. package/bin/github/http.mjs +2 -1
  158. package/bin/github/pagination.d.mts +46 -0
  159. package/bin/github/pagination.mjs +3 -2
  160. package/bin/github/queries.d.mts +28 -0
  161. package/bin/github/queries.mjs +4 -0
  162. package/bin/github/rest-http.d.mts +7 -0
  163. package/bin/github/rest-http.mjs +25 -86
  164. package/bin/github/rest-text.d.mts +1 -0
  165. package/bin/github/rest-text.mjs +88 -0
  166. package/bin/github/suggestion-thread.d.mts +9 -0
  167. package/bin/github/suggestion-thread.mjs +45 -0
  168. package/bin/github/thread-comments.d.mts +2 -0
  169. package/bin/github/thread-comments.mjs +12 -8
  170. package/bin/index.d.mts +10 -0
  171. package/bin/index.mjs +1 -1
  172. package/bin/log/log-file.d.mts +28 -0
  173. package/bin/log/session.d.mts +31 -0
  174. package/bin/log/setup.d.mts +6 -0
  175. package/bin/mcp/index.d.mts +5 -0
  176. package/bin/mcp/index.mjs +8 -0
  177. package/bin/mcp/server.d.mts +8 -0
  178. package/bin/mcp/server.mjs +157 -0
  179. package/bin/mcp-stdio.d.mts +2 -0
  180. package/bin/mcp-stdio.mjs +7 -0
  181. package/bin/merge-status/derive.d.mts +19 -0
  182. package/bin/merge-status/derive.mjs +2 -0
  183. package/bin/merge-status/requirements-format.d.mts +3 -0
  184. package/bin/merge-status/requirements-format.mjs +88 -0
  185. package/bin/merge-status/requirements.d.mts +2 -0
  186. package/bin/merge-status/requirements.mjs +51 -0
  187. package/bin/reporters/agent.d.mts +23 -0
  188. package/bin/reporters/agent.mjs +3 -0
  189. package/bin/state/base.d.mts +10 -0
  190. package/bin/state/base.mjs +23 -0
  191. package/bin/state/bot-cr-seen.d.mts +51 -0
  192. package/bin/state/bot-cr-seen.mjs +4 -14
  193. package/bin/state/fix-attempts.d.mts +27 -0
  194. package/bin/state/fix-attempts.mjs +3 -13
  195. package/bin/state/iterate-stall.d.mts +27 -0
  196. package/bin/state/iterate-stall.mjs +3 -13
  197. package/bin/state/seen-comments.d.mts +62 -0
  198. package/bin/state/seen-comments.mjs +6 -13
  199. package/bin/suggestions/extract.d.mts +8 -0
  200. package/bin/suggestions/parse.d.mts +48 -0
  201. package/bin/suggestions/patch.d.mts +14 -0
  202. package/bin/threads/transcript.d.mts +14 -0
  203. package/bin/threads/transcript.mjs +4 -0
  204. package/bin/types/activity.d.mts +30 -0
  205. package/bin/types/agent-thread.d.mts +9 -0
  206. package/bin/types/check-annotations.d.mts +14 -0
  207. package/bin/types/check-classification.d.mts +19 -0
  208. package/bin/types/github.d.mts +136 -0
  209. package/bin/types/iterate.d.mts +156 -0
  210. package/bin/types/merge-requirements.d.mts +82 -0
  211. package/bin/types/merge-requirements.mjs +2 -0
  212. package/bin/types/protected-run.d.mts +6 -0
  213. package/bin/types/report.d.mts +176 -0
  214. package/bin/types/review-thread.d.mts +12 -0
  215. package/bin/types.d.mts +10 -0
  216. package/bin/types.mjs +1 -0
  217. package/bin/util/markdown.d.mts +1 -0
  218. package/bin/util/path-segment.d.mts +4 -0
  219. package/bin/util/path-segment.mjs +2 -0
  220. package/bin/util/pool.d.mts +2 -0
  221. package/bin/util/pool.mjs +18 -0
  222. package/bin/util/sleep.d.mts +1 -0
  223. package/bin/util/worktree.d.mts +9 -0
  224. package/bin/util/worktree.mjs +4 -1
  225. package/package.json +51 -37
  226. package/plugins/pr-shepherd/.codex-plugin/plugin.json +3 -2
  227. package/plugins/pr-shepherd/.codex.mcp.json +8 -0
  228. package/plugins/pr-shepherd/.mcp.json +6 -0
  229. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +5 -19
  230. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -15
  231. package/src/classify/types.mts +12 -0
@@ -0,0 +1,28 @@
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
+ /** Slim @include follow-up for outstanding batch-query connections. */
12
+ export declare const BATCH_PR_PAGE_QUERY: string;
13
+ /** PR head fields plus a single review thread for `commit-suggestion`. */
14
+ export declare const COMMIT_SUGGESTION_THREAD_QUERY: string;
15
+ /** Fetches additional comments for a single review thread when its nested connection paginates. */
16
+ export declare const REVIEW_THREAD_COMMENTS_QUERY: string;
17
+ /** Fetch inline annotations for a single CheckRun by node ID. */
18
+ export declare const CHECK_RUN_ANNOTATIONS_QUERY: string;
19
+ /** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
20
+ export declare const GET_PR_HEAD_SHA_QUERY: string;
21
+ /** Look up PR number by branch name (for getCurrentPrNumber). */
22
+ export declare const PR_NUMBER_BY_BRANCH_QUERY: string;
23
+ /** Convert a draft PR to ready for review. */
24
+ export declare const MARK_PR_READY_MUTATION: string;
25
+ /** Fetch the PR body and node ID. */
26
+ export declare const GET_PR_BODY_QUERY: string;
27
+ /** Update the PR body. */
28
+ export declare const UPDATE_PR_BODY_MUTATION: string;
@@ -11,6 +11,10 @@ import { join } from "node:path";
11
11
  const gql = (name) => readFileSync(join(import.meta.dirname, "gql", name), "utf8");
12
12
  /** The primary batch query that fetches CI + comments + merge status in one round-trip. */
13
13
  export const BATCH_PR_QUERY = gql("batch-pr.gql");
14
+ /** Slim @include follow-up for outstanding batch-query connections. */
15
+ export const BATCH_PR_PAGE_QUERY = gql("batch-pr-page.gql");
16
+ /** PR head fields plus a single review thread for `commit-suggestion`. */
17
+ export const COMMIT_SUGGESTION_THREAD_QUERY = gql("commit-suggestion-thread.gql");
14
18
  /** Fetches additional comments for a single review thread when its nested connection paginates. */
15
19
  export const REVIEW_THREAD_COMMENTS_QUERY = gql("review-thread-comments.gql");
16
20
  /** Fetch inline annotations for a single CheckRun by node ID. */
@@ -0,0 +1,7 @@
1
+ import { type RateLimitInfo } from "./http-utils.mts";
2
+ export interface RestResult<T = unknown> {
3
+ data: T;
4
+ rateLimit?: RateLimitInfo;
5
+ }
6
+ export declare function rest<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
7
+ export declare function restWithRateLimit<T = unknown>(method: string, path: string, body?: unknown): Promise<RestResult<T>>;
@@ -3,10 +3,29 @@ import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
3
3
  import { GitHubRequestError } from "./errors.mjs";
4
4
  import { makeHeaders } from "./http-auth.mjs";
5
5
  import { requestWithTokenRetry } from "./http-request.mjs";
6
- import { parseRateLimit, parseRetryAfter, redactToken, redactUrl, sanitizeBody, } from "./http-utils.mjs";
6
+ import { parseRateLimit, parseRetryAfter, redactToken, sanitizeBody, } from "./http-utils.mjs";
7
7
  const BASE_URL = "https://api.github.com";
8
+ const SAFE_GITHUB_REST_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/?-]*$/;
9
+ function githubApiUrl(path) {
10
+ if (!SAFE_GITHUB_REST_PATH.test(path) || path.includes("://")) {
11
+ throw new Error(`Invalid GitHub REST path: ${path}`);
12
+ }
13
+ const url = new URL(path, `${BASE_URL}/`);
14
+ if (url.origin !== BASE_URL) {
15
+ throw new Error(`Invalid GitHub REST URL origin: ${url.origin}`);
16
+ }
17
+ const pathname = path.split("?")[0] ?? path;
18
+ if (pathname.split("/").some((seg) => seg === ".." || seg === ".")) {
19
+ throw new Error(`Invalid GitHub REST path: ${path}`);
20
+ }
21
+ return url.href;
22
+ }
8
23
  export async function rest(method, path, body) {
9
- const url = `${BASE_URL}${path}`;
24
+ const { data } = await restWithRateLimit(method, path, body);
25
+ return data;
26
+ }
27
+ export async function restWithRateLimit(method, path, body) {
28
+ const url = githubApiUrl(path);
10
29
  const n = nextEntry();
11
30
  appendEntry(formatRequestEntry({ n, kind: "REST", method, url, body }));
12
31
  const t0 = performance.now();
@@ -17,6 +36,7 @@ export async function rest(method, path, body) {
17
36
  }), t0, (status, durationMs) => appendEntry(formatResponseEntry({ n, kind: "REST", method, url, status, durationMs })));
18
37
  const durationMs = Math.round(performance.now() - retryT0);
19
38
  const ct = res.headers.get("content-type") ?? "";
39
+ const rateLimit = parseRateLimit(res.headers) ?? undefined;
20
40
  if (!res.ok) {
21
41
  const text = await res.text();
22
42
  appendEntry(formatResponseEntry({
@@ -31,7 +51,7 @@ export async function rest(method, path, body) {
31
51
  }));
32
52
  throw new GitHubRequestError(`GitHub REST ${method} ${path} failed: ${res.status} ${sanitizeBody(text)}`, {
33
53
  status: res.status,
34
- rateLimit: parseRateLimit(res.headers) ?? undefined,
54
+ rateLimit,
35
55
  retryAfterSeconds: parseRetryAfter(res.headers),
36
56
  });
37
57
  }
@@ -48,7 +68,7 @@ export async function rest(method, path, body) {
48
68
  body: json,
49
69
  attempt: attempt > 1 ? attempt : undefined,
50
70
  }));
51
- return json;
71
+ return { data: json, rateLimit };
52
72
  }
53
73
  appendEntry(formatResponseEntry({
54
74
  n,
@@ -60,86 +80,5 @@ export async function rest(method, path, body) {
60
80
  contentType: ct || undefined,
61
81
  attempt: attempt > 1 ? attempt : undefined,
62
82
  }));
63
- return undefined;
64
- }
65
- export async function restText(path) {
66
- const url = `${BASE_URL}${path}`;
67
- const n = nextEntry();
68
- appendEntry(formatRequestEntry({ n, kind: "restText", method: "GET", url }));
69
- const t0 = performance.now();
70
- const { res, attempt, retryT0 } = await requestWithTokenRetry(async () => fetch(url, { method: "GET", headers: await makeHeaders(), redirect: "manual" }), t0, (status, durationMs) => appendEntry(formatResponseEntry({ n, kind: "restText", method: "GET", url, status, durationMs })));
71
- const durationMs = Math.round(performance.now() - retryT0);
72
- if ([301, 302, 307, 308].includes(res.status)) {
73
- const redirected = await followRestTextRedirect(res, { n, url, durationMs, attempt });
74
- if (redirected !== null)
75
- return redirected;
76
- }
77
- if (!res.ok) {
78
- const text = await res.text();
79
- appendEntry(formatResponseEntry({
80
- n,
81
- kind: "restText",
82
- method: "GET",
83
- url,
84
- status: res.status,
85
- durationMs,
86
- attempt: attempt > 1 ? attempt : undefined,
87
- }));
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
- });
93
- }
94
- appendEntry(formatResponseEntry({
95
- n,
96
- kind: "restText",
97
- method: "GET",
98
- url,
99
- status: res.status,
100
- durationMs,
101
- contentLength: parseContentLength(res.headers),
102
- attempt: attempt > 1 ? attempt : undefined,
103
- }));
104
- return res.text();
105
- }
106
- async function followRestTextRedirect(res, entry) {
107
- appendEntry(formatResponseEntry({
108
- n: entry.n,
109
- kind: "restText",
110
- method: "GET",
111
- url: entry.url,
112
- status: res.status,
113
- durationMs: entry.durationMs,
114
- attempt: entry.attempt > 1 ? entry.attempt : undefined,
115
- }));
116
- const location = res.headers.get("location");
117
- if (!location)
118
- return null;
119
- const n2 = nextEntry();
120
- const logUrl = redactUrl(location);
121
- appendEntry(formatRequestEntry({ n: n2, kind: "restText", method: "GET", url: logUrl }));
122
- const t1 = performance.now();
123
- const redirectRes = await fetch(location);
124
- appendEntry(formatResponseEntry({
125
- n: n2,
126
- kind: "restText",
127
- method: "GET",
128
- url: logUrl,
129
- status: redirectRes.status,
130
- durationMs: Math.round(performance.now() - t1),
131
- contentLength: parseContentLength(redirectRes.headers),
132
- }));
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
- }
140
- return redirectRes.text();
141
- }
142
- function parseContentLength(headers) {
143
- const raw = headers.get("content-length");
144
- return raw !== null && Number.isFinite(Number(raw)) ? Number(raw) : undefined;
83
+ return { data: undefined, rateLimit };
145
84
  }
@@ -0,0 +1 @@
1
+ export declare function restText(path: string): Promise<string>;
@@ -0,0 +1,88 @@
1
+ import { appendEntry, nextEntry } from "../log/log-file.mjs";
2
+ import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
3
+ import { GitHubRequestError } from "./errors.mjs";
4
+ import { makeHeaders } from "./http-auth.mjs";
5
+ import { requestWithTokenRetry } from "./http-request.mjs";
6
+ import { parseRateLimit, parseRetryAfter, redactUrl, sanitizeBody } from "./http-utils.mjs";
7
+ const BASE_URL = "https://api.github.com";
8
+ export async function restText(path) {
9
+ const url = `${BASE_URL}${path}`;
10
+ const n = nextEntry();
11
+ appendEntry(formatRequestEntry({ n, kind: "restText", method: "GET", url }));
12
+ const t0 = performance.now();
13
+ const { res, attempt, retryT0 } = await requestWithTokenRetry(async () => fetch(url, { method: "GET", headers: await makeHeaders(), redirect: "manual" }), t0, (status, durationMs) => appendEntry(formatResponseEntry({ n, kind: "restText", method: "GET", url, status, durationMs })));
14
+ const durationMs = Math.round(performance.now() - retryT0);
15
+ if ([301, 302, 307, 308].includes(res.status)) {
16
+ const redirected = await followRestTextRedirect(res, { n, url, durationMs, attempt });
17
+ if (redirected !== null)
18
+ return redirected;
19
+ }
20
+ if (!res.ok) {
21
+ const text = await res.text();
22
+ appendEntry(formatResponseEntry({
23
+ n,
24
+ kind: "restText",
25
+ method: "GET",
26
+ url,
27
+ status: res.status,
28
+ durationMs,
29
+ attempt: attempt > 1 ? attempt : undefined,
30
+ }));
31
+ throw new GitHubRequestError(`GitHub REST GET ${path} failed: ${res.status} ${sanitizeBody(text)}`, {
32
+ status: res.status,
33
+ rateLimit: parseRateLimit(res.headers) ?? undefined,
34
+ retryAfterSeconds: parseRetryAfter(res.headers),
35
+ });
36
+ }
37
+ appendEntry(formatResponseEntry({
38
+ n,
39
+ kind: "restText",
40
+ method: "GET",
41
+ url,
42
+ status: res.status,
43
+ durationMs,
44
+ contentLength: parseContentLength(res.headers),
45
+ attempt: attempt > 1 ? attempt : undefined,
46
+ }));
47
+ return res.text();
48
+ }
49
+ async function followRestTextRedirect(res, entry) {
50
+ appendEntry(formatResponseEntry({
51
+ n: entry.n,
52
+ kind: "restText",
53
+ method: "GET",
54
+ url: entry.url,
55
+ status: res.status,
56
+ durationMs: entry.durationMs,
57
+ attempt: entry.attempt > 1 ? entry.attempt : undefined,
58
+ }));
59
+ const location = res.headers.get("location");
60
+ if (!location)
61
+ return null;
62
+ const n2 = nextEntry();
63
+ const logUrl = redactUrl(location);
64
+ appendEntry(formatRequestEntry({ n: n2, kind: "restText", method: "GET", url: logUrl }));
65
+ const t1 = performance.now();
66
+ const redirectRes = await fetch(location);
67
+ appendEntry(formatResponseEntry({
68
+ n: n2,
69
+ kind: "restText",
70
+ method: "GET",
71
+ url: logUrl,
72
+ status: redirectRes.status,
73
+ durationMs: Math.round(performance.now() - t1),
74
+ contentLength: parseContentLength(redirectRes.headers),
75
+ }));
76
+ if (!redirectRes.ok) {
77
+ throw new GitHubRequestError(`redirect target ${logUrl} failed: ${redirectRes.status}`, {
78
+ status: redirectRes.status,
79
+ rateLimit: parseRateLimit(redirectRes.headers) ?? undefined,
80
+ retryAfterSeconds: parseRetryAfter(redirectRes.headers),
81
+ });
82
+ }
83
+ return redirectRes.text();
84
+ }
85
+ function parseContentLength(headers) {
86
+ const raw = headers.get("content-length");
87
+ return raw !== null && Number.isFinite(Number(raw)) ? Number(raw) : undefined;
88
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReviewThread } from "../types.mts";
2
+ import type { RepoInfo } from "./client.mts";
3
+ export interface SuggestionThreadResult {
4
+ headRefOid: string;
5
+ headRefName: string;
6
+ headRepoWithOwner: string | null;
7
+ thread: ReviewThread | null;
8
+ }
9
+ export declare function fetchSuggestionThread(pr: number, repo: RepoInfo, threadId: string): Promise<SuggestionThreadResult>;
@@ -0,0 +1,45 @@
1
+ import { graphql } from "./client.mjs";
2
+ import { COMMIT_SUGGESTION_THREAD_QUERY } from "./queries.mjs";
3
+ import { mapAuthorType, parseCreatedAt } from "./batch-parser-helpers.mjs";
4
+ export async function fetchSuggestionThread(pr, repo, threadId) {
5
+ const result = await graphql(COMMIT_SUGGESTION_THREAD_QUERY, {
6
+ owner: repo.owner,
7
+ repo: repo.name,
8
+ pr,
9
+ threadId,
10
+ });
11
+ const pull = result.data.repository.pullRequest;
12
+ if (!pull) {
13
+ throw new Error(`PR #${pr} not found`);
14
+ }
15
+ return {
16
+ headRefOid: pull.headRefOid,
17
+ headRefName: pull.headRefName,
18
+ headRepoWithOwner: pull.headRepository?.nameWithOwner ?? null,
19
+ thread: parseThread(result.data.node, pr, threadId),
20
+ };
21
+ }
22
+ function parseThread(raw, pr, threadId) {
23
+ if (!raw?.id || !raw.comments || raw.id !== threadId)
24
+ return null;
25
+ if (raw.pullRequest?.number !== pr)
26
+ return null;
27
+ const comment = raw.comments.nodes[0];
28
+ return {
29
+ id: raw.id,
30
+ isResolved: raw.isResolved,
31
+ isOutdated: raw.isOutdated,
32
+ isMinimized: comment?.isMinimized ?? false,
33
+ path: raw.path ?? comment?.path ?? null,
34
+ line: raw.line ?? comment?.line ?? null,
35
+ startLine: raw.startLine ?? comment?.startLine ?? null,
36
+ author: comment?.author?.login ?? "unknown",
37
+ authorType: mapAuthorType(comment?.author?.__typename, comment?.author?.login),
38
+ ...(comment?.authorAssociation !== undefined && {
39
+ authorAssociation: comment.authorAssociation,
40
+ }),
41
+ body: comment?.body ?? "",
42
+ url: comment?.url ?? "",
43
+ createdAtUnix: comment?.createdAt ? parseCreatedAt(comment.createdAt) : 0,
44
+ };
45
+ }
@@ -0,0 +1,2 @@
1
+ import type { RawThread } from "./batch-raw-types.mts";
2
+ export declare function hydrateThreadCommentPages(threads: RawThread[]): Promise<RawThread[]>;
@@ -1,22 +1,26 @@
1
- import { graphql } from "./client.mjs";
1
+ import { graphqlWithRateLimit } from "./client.mjs";
2
+ import { GitHubRequestError } from "./errors.mjs";
2
3
  import { paginateForward } from "./pagination.mjs";
3
4
  import { REVIEW_THREAD_COMMENTS_QUERY } from "./queries.mjs";
5
+ import { mapPool } from "../util/pool.mjs";
6
+ const THREAD_COMMENT_PAGE_CONCURRENCY = 4;
4
7
  export async function hydrateThreadCommentPages(threads) {
5
- const hydrated = [];
6
- for (const thread of threads) {
7
- hydrated.push(await hydrateThreadCommentPage(thread));
8
- }
9
- return hydrated;
8
+ const gate = {};
9
+ return mapPool(threads, THREAD_COMMENT_PAGE_CONCURRENCY, (thread) => hydrateThreadCommentPage(thread, gate));
10
10
  }
11
- async function hydrateThreadCommentPage(thread) {
11
+ async function hydrateThreadCommentPage(thread, gate) {
12
12
  const pageInfo = thread.comments.pageInfo;
13
13
  if (!pageInfo?.hasNextPage || !pageInfo.endCursor)
14
14
  return thread;
15
15
  const extra = await paginateForward(async (cursor) => {
16
- const res = await graphql(REVIEW_THREAD_COMMENTS_QUERY, {
16
+ if (gate.remaining === 0) {
17
+ throw new GitHubRequestError("GitHub GraphQL rate limit remaining is 0; thread comment pagination incomplete", { status: 403 });
18
+ }
19
+ const res = await graphqlWithRateLimit(REVIEW_THREAD_COMMENTS_QUERY, {
17
20
  threadId: thread.id,
18
21
  ...(cursor ? { commentsCursor: cursor } : {}),
19
22
  });
23
+ gate.remaining = res.rateLimit?.remaining;
20
24
  const node = res.data.node;
21
25
  if (!node?.comments) {
22
26
  const nodeType = node?.__typename ?? "null";
@@ -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,7 +4,7 @@
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";
@@ -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 {};