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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Process exit codes for the pr-shepherd CLI.
3
+ *
4
+ * Three bands:
5
+ * 0 done — shepherd finished and the PR is in a good terminal state
6
+ * 10-19 shepherd RAN successfully; the code reports PR state
7
+ * 64-78 shepherd FAILED (BSD `sysexits.h` codes)
8
+ *
9
+ * Caller rule: `$? >= 64` means shepherd itself failed. `0` or `10-19` means it
10
+ * ran to completion and is reporting PR state. See docs/exit-codes.md.
11
+ */
12
+ import type { IterateResult } from "./types.mts";
13
+ export declare const EXIT: Readonly<{
14
+ /** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
15
+ readonly OK: 0;
16
+ /** Nothing to do yet; CI still in progress. */
17
+ readonly WAIT: 10;
18
+ /** Draft PR converted to ready for review. */
19
+ readonly MARK_READY: 11;
20
+ /** Agent work required. */
21
+ readonly FIX_CODE: 12;
22
+ /** Human attention required. */
23
+ readonly ESCALATE: 13;
24
+ /** `cancel` + `closed` — PR closed without merging. */
25
+ readonly CLOSED: 14;
26
+ /** Bad/unknown flag, unknown subcommand, missing required arg, invalid duration. */
27
+ readonly USAGE: 64;
28
+ /** Malformed caller data: bad `--require-sha`, `PRRC_*` IDs, bad repo string. */
29
+ readonly DATAERR: 65;
30
+ /** Input file/stdin could not be read. */
31
+ readonly NOINPUT: 66;
32
+ /** Precondition unmet: no open PR for branch, thread not eligible, unclassified 4xx. */
33
+ readonly UNAVAILABLE: 69;
34
+ /** Unexpected/unclassified internal error — the fallback. */
35
+ readonly SOFTWARE: 70;
36
+ /** Retryable GitHub failure: 429, 5xx, rate limit exhausted, `Retry-After` present. */
37
+ readonly TEMPFAIL: 75;
38
+ /** GitHub 401/403 — missing token or insufficient PAT scopes. */
39
+ readonly NOPERM: 77;
40
+ /** `.pr-shepherdrc.yml` validation failure. */
41
+ readonly CONFIG: 78;
42
+ }>;
43
+ /** An error that carries its own exit code, so the top-level handler doesn't have to guess. */
44
+ export declare class ShepherdError extends Error {
45
+ readonly exitCode: number;
46
+ constructor(message: string, exitCode: number, opts?: {
47
+ cause?: unknown;
48
+ });
49
+ }
50
+ export declare function iterateResultToExitCode(result: IterateResult): number;
51
+ export declare function errorToExitCode(err: unknown): number;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Process exit codes for the pr-shepherd CLI.
3
+ *
4
+ * Three bands:
5
+ * 0 done — shepherd finished and the PR is in a good terminal state
6
+ * 10-19 shepherd RAN successfully; the code reports PR state
7
+ * 64-78 shepherd FAILED (BSD `sysexits.h` codes)
8
+ *
9
+ * Caller rule: `$? >= 64` means shepherd itself failed. `0` or `10-19` means it
10
+ * ran to completion and is reporting PR state. See docs/exit-codes.md.
11
+ */
12
+ export const EXIT = Object.freeze({
13
+ /** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
14
+ OK: 0,
15
+ /** Nothing to do yet; CI still in progress. */
16
+ WAIT: 10,
17
+ /** Draft PR converted to ready for review. */
18
+ MARK_READY: 11,
19
+ /** Agent work required. */
20
+ FIX_CODE: 12,
21
+ /** Human attention required. */
22
+ ESCALATE: 13,
23
+ /** `cancel` + `closed` — PR closed without merging. */
24
+ CLOSED: 14,
25
+ /** Bad/unknown flag, unknown subcommand, missing required arg, invalid duration. */
26
+ USAGE: 64,
27
+ /** Malformed caller data: bad `--require-sha`, `PRRC_*` IDs, bad repo string. */
28
+ DATAERR: 65,
29
+ /** Input file/stdin could not be read. */
30
+ NOINPUT: 66,
31
+ /** Precondition unmet: no open PR for branch, thread not eligible, unclassified 4xx. */
32
+ UNAVAILABLE: 69,
33
+ /** Unexpected/unclassified internal error — the fallback. */
34
+ SOFTWARE: 70,
35
+ /** Retryable GitHub failure: 429, 5xx, rate limit exhausted, `Retry-After` present. */
36
+ TEMPFAIL: 75,
37
+ /** GitHub 401/403 — missing token or insufficient PAT scopes. */
38
+ NOPERM: 77,
39
+ /** `.pr-shepherdrc.yml` validation failure. */
40
+ CONFIG: 78,
41
+ });
42
+ /** An error that carries its own exit code, so the top-level handler doesn't have to guess. */
43
+ export class ShepherdError extends Error {
44
+ exitCode;
45
+ constructor(message, exitCode, opts) {
46
+ super(message, opts);
47
+ this.name = "ShepherdError";
48
+ this.exitCode = exitCode;
49
+ }
50
+ }
51
+ const CANCEL_REASON_EXIT_CODE = {
52
+ merged: EXIT.OK,
53
+ "ready-delay-elapsed": EXIT.OK,
54
+ closed: EXIT.CLOSED,
55
+ };
56
+ export function iterateResultToExitCode(result) {
57
+ switch (result.action) {
58
+ case "cancel":
59
+ return CANCEL_REASON_EXIT_CODE[result.reason];
60
+ case "wait":
61
+ return EXIT.WAIT;
62
+ case "mark_ready":
63
+ return EXIT.MARK_READY;
64
+ case "fix_code":
65
+ return EXIT.FIX_CODE;
66
+ case "escalate":
67
+ return EXIT.ESCALATE;
68
+ }
69
+ }
70
+ export function errorToExitCode(err) {
71
+ if (err instanceof ShepherdError)
72
+ return err.exitCode;
73
+ return EXIT.SOFTWARE;
74
+ }
@@ -0,0 +1,3 @@
1
+ import type { PrActivitySummary, PrComment, Review, ReviewThread } from "../types.mts";
2
+ import type { RawPr } from "./batch-raw-types.mts";
3
+ export declare function buildPrActivitySummary(raw: RawPr, comments: PrComment[], reviewThreads: ReviewThread[], reviewSummaries: Review[], changesRequestedReviews: Review[], approvedReviews: Review[]): PrActivitySummary;
@@ -8,6 +8,7 @@ function reviewActivityItems(reviews, latestCommitCommittedAtUnix, kind) {
8
8
  id: r.id,
9
9
  author: r.author,
10
10
  authorType: r.authorType,
11
+ ...(r.authorAssociation !== undefined && { authorAssociation: r.authorAssociation }),
11
12
  body: r.body,
12
13
  createdAtUnix: r.createdAtUnix ?? 0,
13
14
  }));
@@ -26,6 +27,9 @@ export function buildPrActivitySummary(raw, comments, reviewThreads, reviewSumma
26
27
  id: c.id,
27
28
  author: c.author,
28
29
  authorType: c.authorType,
30
+ ...(c.authorAssociation !== undefined && {
31
+ authorAssociation: c.authorAssociation,
32
+ }),
29
33
  body: c.body,
30
34
  url: c.url,
31
35
  createdAtUnix: c.createdAtUnix,
@@ -37,6 +41,9 @@ export function buildPrActivitySummary(raw, comments, reviewThreads, reviewSumma
37
41
  id: c.id,
38
42
  author: c.author,
39
43
  authorType: c.authorType,
44
+ ...(c.authorAssociation !== undefined && {
45
+ authorAssociation: c.authorAssociation,
46
+ }),
40
47
  body: c.body,
41
48
  url: c.url,
42
49
  createdAtUnix: c.createdAtUnix,
@@ -0,0 +1,22 @@
1
+ import type { AuthorType, CheckConclusion, CheckRun, CheckStatus, Review, ReviewThread } from "../types.mts";
2
+ import type { RawContextNode } from "./batch-raw-types.mts";
3
+ export declare function mapAuthorType(typeName: string | undefined | null, login?: string | undefined | null): AuthorType;
4
+ export declare function parseCreatedAt(iso: string): number;
5
+ /** Map a GraphQL CheckRun context node to a CheckRun. */
6
+ export declare function mapCheckRunNode(node: Extract<RawContextNode, {
7
+ __typename: "CheckRun";
8
+ }>): CheckRun;
9
+ export declare function latestApprovedLogins(latest: Array<{
10
+ login: string;
11
+ state: string;
12
+ }>): Set<string>;
13
+ /**
14
+ * A CR review is stale when its commit.oid differs from the PR head AND every
15
+ * associated review thread is resolved or outdated. Reviews with no associated
16
+ * threads are treated conservatively (not marked stale).
17
+ */
18
+ export declare function isReviewStale(review: Review, headRefOid: string, reviewThreads: ReviewThread[]): boolean;
19
+ export declare function mapStatusContextState(state: string): {
20
+ status: CheckStatus;
21
+ conclusion: CheckConclusion;
22
+ };
@@ -0,0 +1,3 @@
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;
@@ -6,6 +6,7 @@ function parseReviewNode(r) {
6
6
  id: r.id,
7
7
  author: r.author?.login ?? "unknown",
8
8
  authorType: mapAuthorType(r.author?.__typename, r.author?.login),
9
+ ...(r.authorAssociation !== undefined && { authorAssociation: r.authorAssociation }),
9
10
  body: r.body,
10
11
  createdAtUnix: r.createdAt ? parseCreatedAt(r.createdAt) : 0,
11
12
  };
@@ -32,6 +33,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
32
33
  ...(c.pullRequestReview?.id ? { reviewId: c.pullRequestReview.id } : undefined),
33
34
  author: c.author?.login ?? "unknown",
34
35
  authorType: mapAuthorType(c.author?.__typename, c.author?.login),
36
+ ...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
35
37
  body: c.body,
36
38
  url: c.url,
37
39
  createdAtUnix: c.createdAt ? parseCreatedAt(c.createdAt) : 0,
@@ -47,6 +49,9 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
47
49
  ...(comment?.pullRequestReview?.id ? { reviewId: comment.pullRequestReview.id } : undefined),
48
50
  author: comment?.author?.login ?? "unknown",
49
51
  authorType: mapAuthorType(comment?.author?.__typename, comment?.author?.login),
52
+ ...(comment?.authorAssociation !== undefined && {
53
+ authorAssociation: comment.authorAssociation,
54
+ }),
50
55
  body: comment?.body ?? "",
51
56
  url: comment?.url ?? "",
52
57
  createdAtUnix: comment?.createdAt ? parseCreatedAt(comment.createdAt) : 0,
@@ -58,6 +63,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
58
63
  isMinimized: c.isMinimized,
59
64
  author: c.author?.login ?? "unknown",
60
65
  authorType: mapAuthorType(c.author?.__typename, c.author?.login),
66
+ ...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
61
67
  body: c.body,
62
68
  url: c.url,
63
69
  createdAtUnix: c.createdAt ? parseCreatedAt(c.createdAt) : 0,
@@ -0,0 +1,207 @@
1
+ import type { CommentAuthorAssociation } from "../types/github.mts";
2
+ export interface RawBatchResponse {
3
+ repository: {
4
+ pullRequest: RawPr | null;
5
+ } | null;
6
+ }
7
+ export interface RawPr {
8
+ id: string;
9
+ number: number;
10
+ state: string;
11
+ isDraft: boolean;
12
+ mergeable: string;
13
+ mergeStateStatus: string;
14
+ reviewDecision: string | null;
15
+ headRefOid: string;
16
+ headRefName: string;
17
+ headRepository: {
18
+ nameWithOwner: string;
19
+ } | null;
20
+ baseRefName: string;
21
+ baseRef: {
22
+ branchProtectionRule: RawBranchProtectionRule | null;
23
+ } | null;
24
+ reviewRequests: {
25
+ nodes: Array<{
26
+ requestedReviewer: {
27
+ login?: string;
28
+ name?: string;
29
+ } | null;
30
+ }>;
31
+ };
32
+ latestReviews: {
33
+ nodes: Array<{
34
+ author: RawAuthor | null;
35
+ state: string;
36
+ }>;
37
+ };
38
+ reviewThreads: {
39
+ pageInfo: {
40
+ hasPreviousPage: boolean;
41
+ startCursor: string | null;
42
+ };
43
+ nodes: RawThread[];
44
+ };
45
+ comments: {
46
+ pageInfo: {
47
+ hasPreviousPage: boolean;
48
+ startCursor: string | null;
49
+ };
50
+ nodes: RawComment[];
51
+ };
52
+ changesRequestedReviews: {
53
+ pageInfo: {
54
+ hasPreviousPage: boolean;
55
+ startCursor: string | null;
56
+ };
57
+ nodes: RawReview[];
58
+ };
59
+ reviewSummaries: {
60
+ pageInfo: {
61
+ hasPreviousPage: boolean;
62
+ startCursor: string | null;
63
+ };
64
+ nodes: RawReviewSummary[];
65
+ };
66
+ allReviews?: {
67
+ totalCount: number;
68
+ };
69
+ approvedReviews: {
70
+ pageInfo: {
71
+ hasPreviousPage: boolean;
72
+ startCursor: string | null;
73
+ };
74
+ nodes: RawReviewSummary[];
75
+ };
76
+ commits: {
77
+ totalCount?: number;
78
+ nodes: Array<{
79
+ commit: {
80
+ oid: string;
81
+ committedDate?: string;
82
+ statusCheckRollup: {
83
+ contexts: {
84
+ pageInfo: {
85
+ hasNextPage: boolean;
86
+ endCursor: string | null;
87
+ };
88
+ nodes: Array<RawContextNode | null>;
89
+ };
90
+ } | null;
91
+ };
92
+ }>;
93
+ };
94
+ }
95
+ interface RawAuthor {
96
+ __typename?: string;
97
+ login: string;
98
+ }
99
+ export interface RawThreadComment {
100
+ id: string;
101
+ isMinimized: boolean;
102
+ url: string;
103
+ authorAssociation?: CommentAuthorAssociation;
104
+ author: RawAuthor | null;
105
+ pullRequestReview?: {
106
+ id: string;
107
+ } | null;
108
+ body: string;
109
+ path: string | null;
110
+ line: number | null;
111
+ startLine: number | null;
112
+ createdAt?: string;
113
+ }
114
+ export interface RawThread {
115
+ id: string;
116
+ isResolved: boolean;
117
+ isOutdated: boolean;
118
+ path?: string | null;
119
+ line?: number | null;
120
+ startLine?: number | null;
121
+ comments: {
122
+ pageInfo?: {
123
+ hasNextPage: boolean;
124
+ endCursor: string | null;
125
+ };
126
+ nodes: RawThreadComment[];
127
+ };
128
+ }
129
+ export interface RawReviewThreadCommentsResponse {
130
+ node: {
131
+ __typename?: string;
132
+ comments: {
133
+ pageInfo: {
134
+ hasNextPage: boolean;
135
+ endCursor: string | null;
136
+ };
137
+ nodes: RawThreadComment[];
138
+ } | null;
139
+ } | null;
140
+ }
141
+ export interface RawComment {
142
+ id: string;
143
+ isMinimized: boolean;
144
+ url: string;
145
+ authorAssociation?: CommentAuthorAssociation;
146
+ author: RawAuthor | null;
147
+ body: string;
148
+ createdAt?: string;
149
+ }
150
+ export interface RawReview {
151
+ id: string;
152
+ authorAssociation?: CommentAuthorAssociation;
153
+ author: RawAuthor | null;
154
+ body: string;
155
+ createdAt?: string;
156
+ commit?: {
157
+ oid: string;
158
+ };
159
+ }
160
+ export interface RawReviewSummary {
161
+ id: string;
162
+ isMinimized: boolean;
163
+ authorAssociation?: CommentAuthorAssociation;
164
+ author: RawAuthor | null;
165
+ body: string;
166
+ createdAt?: string;
167
+ }
168
+ interface RawBranchProtectionRule {
169
+ requiresApprovingReviews: boolean;
170
+ requiredApprovingReviewCount: number;
171
+ requiresConversationResolution: boolean;
172
+ requiresStatusChecks: boolean;
173
+ requiredStatusCheckContexts: string[] | null;
174
+ }
175
+ export type RawContextNode = {
176
+ __typename: "CheckRun";
177
+ id: string;
178
+ name: string;
179
+ status: string;
180
+ conclusion: string | null;
181
+ detailsUrl: string | null;
182
+ completedAt?: string | null;
183
+ startedAt?: string | null;
184
+ title: string | null;
185
+ summary: string | null;
186
+ checkSuite: {
187
+ createdAt?: string;
188
+ updatedAt?: string;
189
+ workflowRun: {
190
+ event: string;
191
+ createdAt?: string;
192
+ updatedAt?: string;
193
+ workflow?: {
194
+ name: string;
195
+ databaseId?: number | null;
196
+ } | null;
197
+ } | null;
198
+ } | null;
199
+ } | {
200
+ __typename: "StatusContext";
201
+ context: string;
202
+ state: string;
203
+ createdAt?: string;
204
+ targetUrl: string | null;
205
+ description: string | null;
206
+ };
207
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { RepoInfo } from "./client.mts";
2
+ import type { RawBatchResponse, RawContextNode, RawPr } from "./batch-raw-types.mts";
3
+ export declare function requireRawPr(response: RawBatchResponse | null | undefined, pr: number, repo: RepoInfo): RawPr;
4
+ export declare function requireContextNodes(nodes: Array<RawContextNode | null>): RawContextNode[];
@@ -1,16 +1,21 @@
1
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
1
2
  import { GitHubRequestError } from "./errors.mjs";
2
3
  export function requireRawPr(response, pr, repo) {
3
4
  if (!response?.repository) {
4
5
  throw new GitHubRequestError(`GitHub GraphQL response did not include repository ${repo.owner}/${repo.name} (not found or access denied)`, { status: 200 });
5
6
  }
6
- if (!response.repository.pullRequest)
7
- throw new Error(`PR #${pr} not found`);
7
+ if (!response.repository.pullRequest) {
8
+ throw new ShepherdError(`PR #${pr} not found`, EXIT.UNAVAILABLE);
9
+ }
8
10
  return response.repository.pullRequest;
9
11
  }
10
12
  export function requireContextNodes(nodes) {
11
13
  const nullIndex = nodes.findIndex((node) => node === null);
12
14
  if (nullIndex !== -1) {
13
- throw new GitHubRequestError(`Malformed GitHub GraphQL response: null check context at repository.pullRequest.commits.nodes.0.commit.statusCheckRollup.contexts.nodes.${nullIndex}`, { status: 200 });
15
+ // A null context node is an unexpected/malformed shape, not a precondition or
16
+ // permission problem — force EX_SOFTWARE rather than falling through to the
17
+ // (200-status-derived) EX_UNAVAILABLE default.
18
+ throw new GitHubRequestError(`Malformed GitHub GraphQL response: null check context at repository.pullRequest.commits.nodes.0.commit.statusCheckRollup.contexts.nodes.${nullIndex}`, { status: 200, exitCodeOverride: EXIT.SOFTWARE });
14
19
  }
15
20
  return nodes;
16
21
  }
@@ -0,0 +1,22 @@
1
+ import { type RateLimitInfo, type RepoInfo } from "./client.mts";
2
+ import type { BatchPrData } from "../types.mts";
3
+ interface BatchResult {
4
+ data: BatchPrData;
5
+ rateLimit?: RateLimitInfo;
6
+ }
7
+ interface FetchPrBatchOptions {
8
+ /**
9
+ * When false (default), the first-page approvedReviews are returned but
10
+ * backward pagination is skipped. Iterate's approvals-minimize flow sets this
11
+ * to true only when the user opts in, so long-lived PRs with > 50 approvals
12
+ * don't pay extra GraphQL round-trips per iterate call for data no consumer
13
+ * currently uses. The first page is free — already inside the one batch
14
+ * request — so there's no need to conditionally omit the field itself.
15
+ */
16
+ paginateApprovedReviews?: boolean;
17
+ }
18
+ /**
19
+ * Fetch all PR data needed for a `shepherd check` in one (or a few, if paginating) GraphQL requests.
20
+ */
21
+ export declare function fetchPrBatch(pr: number, repo: RepoInfo, opts?: FetchPrBatchOptions): Promise<BatchResult>;
22
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { BatchPrData } from "../types.mts";
2
+ import type { RawPr } from "./batch-raw-types.mts";
3
+ export declare function parseBranchProtection(raw: RawPr): BatchPrData["branchProtection"];
@@ -0,0 +1,2 @@
1
+ import type { CheckAnnotation } from "../types.mts";
2
+ export declare function fetchCheckRunAnnotations(checkRunId: string): Promise<CheckAnnotation[]>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * High-level GitHub client — wraps http.mts for application-level concerns.
3
+ *
4
+ * All GitHub I/O goes through native fetch (via http.mts); the `gh` CLI is no
5
+ * longer used for GitHub API calls, but may be invoked as an auth-token
6
+ * fallback via `gh auth token` when neither GH_TOKEN nor GITHUB_TOKEN is set.
7
+ */
8
+ import type { MergeableState, MergeStateStatus } from "../types.mts";
9
+ export type { RateLimitInfo } from "./http.mts";
10
+ export { graphql, graphqlWithRateLimit } from "./http.mts";
11
+ export interface RepoInfo {
12
+ owner: string;
13
+ name: string;
14
+ }
15
+ /**
16
+ * Returns the current repo's owner and name by parsing `git remote get-url origin`.
17
+ * Handles https://, git@, and ssh:// remote URL formats.
18
+ */
19
+ export declare function getRepoInfo(): Promise<RepoInfo>;
20
+ /**
21
+ * Derives the PR number for the current HEAD branch.
22
+ * Returns null if no open PR is found.
23
+ */
24
+ export declare function getCurrentPrNumber(): Promise<number | null>;
25
+ /** Returns the PR number for a given branch, or null if no open PR is found. */
26
+ export declare function getPrNumberForBranch(branch: string, owner: string, repo: string): Promise<number | null>;
27
+ /** Returns the `headRefOid` (commit SHA) of the given PR as reported by GitHub. */
28
+ export declare function getPrHeadSha(pr: number, owner: string, name: string): Promise<string>;
29
+ /** Fetches the node ID and body text for a PR. GitHub returns null body for empty bodies — coerced to "". */
30
+ export declare function getPullRequestBody(pr: number, owner: string, name: string): Promise<{
31
+ nodeId: string;
32
+ body: string;
33
+ }>;
34
+ /** Overwrites the PR body. */
35
+ export declare function updatePullRequestBody(pullRequestId: string, body: string): Promise<void>;
36
+ /**
37
+ * Fetches `mergeable` and `mergeStateStatus` via the REST API.
38
+ *
39
+ * Used as a fallback when the GraphQL API returns `UNKNOWN` for these fields —
40
+ * a known GitHub quirk where GraphQL lags behind the REST layer.
41
+ */
42
+ export declare function getMergeableState(pr: number, owner: string, repo: string): Promise<{
43
+ mergeable: MergeableState;
44
+ mergeStateStatus: MergeStateStatus;
45
+ }>;
46
+ export declare function getCurrentBranch(): Promise<string>;
@@ -9,6 +9,7 @@ import { execFile as execFileCb } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
10
  import { graphql as httpGraphql, rest } from "./http.mjs";
11
11
  import { PR_NUMBER_BY_BRANCH_QUERY, GET_PR_HEAD_SHA_QUERY, GET_PR_BODY_QUERY, UPDATE_PR_BODY_MUTATION, } from "./queries.mjs";
12
+ import { getExecutionCwd } from "../execution-context.mjs";
12
13
  const execFile = promisify(execFileCb);
13
14
  // ---------------------------------------------------------------------------
14
15
  // GraphQL — thin re-exports so callers don't need to import http.mts directly
@@ -19,7 +20,9 @@ export { graphql, graphqlWithRateLimit } from "./http.mjs";
19
20
  * Handles https://, git@, and ssh:// remote URL formats.
20
21
  */
21
22
  export async function getRepoInfo() {
22
- const { stdout } = await execFile("git", ["remote", "get-url", "origin"]);
23
+ const { stdout } = await execFile("git", ["remote", "get-url", "origin"], {
24
+ cwd: getExecutionCwd(),
25
+ });
23
26
  const url = stdout.trim();
24
27
  return parseRemoteUrl(url);
25
28
  }
@@ -95,7 +98,9 @@ export async function getMergeableState(pr, owner, repo) {
95
98
  // Internal helpers
96
99
  // ---------------------------------------------------------------------------
97
100
  export async function getCurrentBranch() {
98
- const { stdout } = await execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
101
+ const { stdout } = await execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
102
+ cwd: getExecutionCwd(),
103
+ });
99
104
  return stdout.trim();
100
105
  }
101
106
  function parseRemoteUrl(url) {
@@ -0,0 +1,25 @@
1
+ import { ShepherdError } from "../exit-codes.mts";
2
+ import type { RateLimitInfo } from "./http.mts";
3
+ export interface GitHubGraphQlError {
4
+ message: string;
5
+ path?: unknown;
6
+ }
7
+ export declare class GitHubRequestError extends ShepherdError {
8
+ readonly status: number;
9
+ readonly rateLimit?: RateLimitInfo;
10
+ readonly retryAfterSeconds?: number;
11
+ readonly graphqlErrors?: GitHubGraphQlError[];
12
+ constructor(message: string, opts: {
13
+ status: number;
14
+ rateLimit?: RateLimitInfo;
15
+ retryAfterSeconds?: number;
16
+ graphqlErrors?: GitHubGraphQlError[];
17
+ /**
18
+ * Bypasses status-based classification entirely — for callers that already
19
+ * know the failure kind better than the HTTP status can express (e.g. a
20
+ * response that failed to parse at all, which is an internal failure, not
21
+ * an availability or permission problem).
22
+ */
23
+ exitCodeOverride?: number;
24
+ });
25
+ }
@@ -1,10 +1,34 @@
1
- export class GitHubRequestError extends Error {
1
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
2
+ // GitHub's GraphQL API reports field-level permission failures (e.g. a fine-grained
3
+ // PAT missing a scope) as an `errors[].message` entry at HTTP 200, not as an HTTP
4
+ // 401/403 — the transport-level request succeeded even though one field could not
5
+ // be resolved. Status alone can't see this, so classification must also inspect the
6
+ // GraphQL error messages themselves.
7
+ const GRAPHQL_PERMISSION_ERROR = /resource not accessible/i;
8
+ function hasPermissionError(graphqlErrors) {
9
+ return graphqlErrors?.some((e) => GRAPHQL_PERMISSION_ERROR.test(e.message)) ?? false;
10
+ }
11
+ function classifyStatus(status, rateLimit, retryAfterSeconds, graphqlErrors) {
12
+ // Retry signals take priority over everything else: GitHub's secondary rate limit
13
+ // returns 403 with a Retry-After header, which is a transient throttle — not the
14
+ // permission-denied 403 a bad/missing token produces. Treat any retry signal as
15
+ // TEMPFAIL first so it isn't shadowed by the checks below.
16
+ const rateLimitExhausted = rateLimit !== undefined && rateLimit.remaining <= 0;
17
+ if (status === 429 || status >= 500 || retryAfterSeconds !== undefined || rateLimitExhausted) {
18
+ return EXIT.TEMPFAIL;
19
+ }
20
+ if (status === 401 || status === 403 || hasPermissionError(graphqlErrors))
21
+ return EXIT.NOPERM;
22
+ return EXIT.UNAVAILABLE;
23
+ }
24
+ export class GitHubRequestError extends ShepherdError {
2
25
  status;
3
26
  rateLimit;
4
27
  retryAfterSeconds;
5
28
  graphqlErrors;
6
29
  constructor(message, opts) {
7
- super(message);
30
+ super(message, opts.exitCodeOverride ??
31
+ classifyStatus(opts.status, opts.rateLimit, opts.retryAfterSeconds, opts.graphqlErrors));
8
32
  this.name = "GitHubRequestError";
9
33
  this.status = opts.status;
10
34
  this.rateLimit = opts.rateLimit;
@@ -82,6 +82,7 @@ query BatchPr(
82
82
  id
83
83
  isMinimized
84
84
  url
85
+ authorAssociation
85
86
  author {
86
87
  __typename
87
88
  login
@@ -107,6 +108,7 @@ query BatchPr(
107
108
  id
108
109
  isMinimized
109
110
  url
111
+ authorAssociation
110
112
  author {
111
113
  __typename
112
114
  login
@@ -126,6 +128,7 @@ query BatchPr(
126
128
  }
127
129
  nodes {
128
130
  id
131
+ authorAssociation
129
132
  author {
130
133
  __typename
131
134
  login
@@ -145,6 +148,7 @@ query BatchPr(
145
148
  nodes {
146
149
  id
147
150
  isMinimized
151
+ authorAssociation
148
152
  author {
149
153
  __typename
150
154
  login
@@ -164,6 +168,7 @@ query BatchPr(
164
168
  nodes {
165
169
  id
166
170
  isMinimized
171
+ authorAssociation
167
172
  author {
168
173
  __typename
169
174
  login