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
@@ -1,22 +1,40 @@
1
+ /* eslint-disable max-lines */
1
2
  import { readFileSync, statSync } from "node:fs";
2
- import { join, dirname } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
3
4
  import { homedir } from "node:os";
4
5
  import { parse } from "yaml";
5
6
  import builtins from "../config.json" with { type: "json" };
7
+ import { getEffectiveCwd } from "../execution-context.mjs";
6
8
  const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
7
9
  const RC_FILENAME = ".pr-shepherdrc.yml";
8
- function findRcFile(startDir) {
9
- const home = homedir();
10
- let current = startDir;
11
- while (true) {
12
- const candidate = join(current, RC_FILENAME);
10
+ /**
11
+ * Collect `.pr-shepherdrc.yml` files from `startDir` toward `$HOME` (closest first).
12
+ * `$HOME/.pr-shepherdrc.yml` is always included when it exists, even if cwd is
13
+ * outside the home directory. The walk never includes the filesystem root, so a
14
+ * `/.pr-shepherdrc.yml` cannot override the user-level file.
15
+ */
16
+ function collectRcFiles(startDir) {
17
+ const home = resolve(homedir());
18
+ const seen = new Set();
19
+ const files = [];
20
+ const add = (dir) => {
21
+ const candidate = resolve(join(dir, RC_FILENAME));
22
+ if (seen.has(candidate))
23
+ return;
13
24
  if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) {
14
- return candidate;
25
+ seen.add(candidate);
26
+ files.push(candidate);
15
27
  }
16
- if (current === home || current === dirname(current))
17
- return null;
28
+ };
29
+ let current = resolve(startDir);
30
+ while (current !== dirname(current)) {
31
+ add(current);
32
+ if (current === home)
33
+ break;
18
34
  current = dirname(current);
19
35
  }
36
+ add(home);
37
+ return files;
20
38
  }
21
39
  function deepMerge(base, override) {
22
40
  const result = { ...base };
@@ -41,6 +59,10 @@ function isMinimizeCommentsPolicy(value) {
41
59
  return MINIMIZE_COMMENTS_POLICIES.some((policy) => policy === value);
42
60
  }
43
61
  function parseMinimizeCommentsPolicy(value) {
62
+ if (value === "users") {
63
+ process.stderr.write('pr-shepherd: config: iterate.minimizeComments: "users" is deprecated and is now treated as "none".\n');
64
+ return "none";
65
+ }
44
66
  if (isMinimizeCommentsPolicy(value))
45
67
  return value;
46
68
  throw new Error(`Invalid config: iterate.minimizeComments must be one of "all", "bots", "users", or "none", got ${JSON.stringify(value)}`);
@@ -63,21 +85,112 @@ function parseNeverCancelRuns(value) {
63
85
  }
64
86
  return value;
65
87
  }
88
+ const KNOWN_CONFIG_KEYS = new Set([
89
+ "classify",
90
+ "botUsernames",
91
+ "ignoreChecks",
92
+ "iterate",
93
+ "watch",
94
+ "resolve",
95
+ "checks",
96
+ "mergeStatus",
97
+ "actions",
98
+ ]);
99
+ const KNOWN_NESTED_KEYS = {
100
+ iterate: new Set([
101
+ "fixAttemptsPerThread",
102
+ "stallTimeoutMinutes",
103
+ "minimizeApprovals",
104
+ "minimizeComments",
105
+ "behindBaseHint",
106
+ ]),
107
+ watch: new Set(["readyDelayMinutes"]),
108
+ resolve: new Set(["shaPoll"]),
109
+ checks: new Set(["ciTriggerEvents"]),
110
+ mergeStatus: new Set(["blockingReviewerLogins"]),
111
+ actions: new Set([
112
+ "autoMinimizeSuppressed",
113
+ "autoMarkReady",
114
+ "neverCancelRuns",
115
+ "autoResolveOutdated",
116
+ "commitSuggestions",
117
+ ]),
118
+ };
119
+ function warnUnknownConfigKeys(config) {
120
+ for (const key of Object.keys(config)) {
121
+ if (!KNOWN_CONFIG_KEYS.has(key)) {
122
+ process.stderr.write(`pr-shepherd: config: unknown key "${key}" ignored.\n`);
123
+ delete config[key];
124
+ continue;
125
+ }
126
+ const value = config[key];
127
+ const nested = KNOWN_NESTED_KEYS[key];
128
+ if (nested === undefined ||
129
+ value === null ||
130
+ typeof value !== "object" ||
131
+ Array.isArray(value)) {
132
+ continue;
133
+ }
134
+ for (const child of Object.keys(value)) {
135
+ if (!nested.has(child)) {
136
+ process.stderr.write(`pr-shepherd: config: unknown key "${key}.${child}" ignored.\n`);
137
+ delete value[child];
138
+ }
139
+ }
140
+ }
141
+ }
66
142
  const defaults = builtins;
67
143
  const configCache = new Map();
144
+ function stripDeprecatedActionKeys(parsed) {
145
+ const rawActions = parsed.actions;
146
+ if (rawActions === null || typeof rawActions !== "object" || Array.isArray(rawActions))
147
+ return;
148
+ const actions = rawActions;
149
+ for (const key of ["autoResolveOutdated", "commitSuggestions"]) {
150
+ if (key in actions) {
151
+ process.stderr.write(`pr-shepherd: config: actions.${key} is deprecated and ignored.\n`);
152
+ delete actions[key];
153
+ }
154
+ }
155
+ }
156
+ function readRcFile(rcPath) {
157
+ try {
158
+ const raw = readFileSync(rcPath, "utf8");
159
+ const parsed = (parse(raw) ?? {});
160
+ stripDeprecatedActionKeys(parsed);
161
+ warnUnknownConfigKeys(parsed);
162
+ return parsed;
163
+ }
164
+ catch (err) {
165
+ process.stderr.write(`pr-shepherd: failed to parse ${rcPath}: ${err instanceof Error ? err.message : String(err)}\n`);
166
+ return null;
167
+ }
168
+ }
68
169
  export function loadConfig() {
69
- const cwd = process.cwd();
170
+ const cwd = getEffectiveCwd();
70
171
  if (configCache.has(cwd))
71
172
  return configCache.get(cwd);
72
- const rcPath = findRcFile(cwd);
73
- if (!rcPath) {
173
+ const rcPaths = collectRcFiles(cwd);
174
+ if (rcPaths.length === 0) {
74
175
  configCache.set(cwd, defaults);
75
176
  return defaults;
76
177
  }
77
178
  try {
78
- const raw = readFileSync(rcPath, "utf8");
79
- const parsed = (parse(raw) ?? {});
80
- const config = deepMerge(defaults, parsed);
179
+ // Farthest file first so closer directories override, ESLint-style.
180
+ let overlay = {};
181
+ let loaded = false;
182
+ for (const rcPath of [...rcPaths].reverse()) {
183
+ const parsed = readRcFile(rcPath);
184
+ if (parsed === null)
185
+ continue;
186
+ overlay = deepMerge(overlay, parsed);
187
+ loaded = true;
188
+ }
189
+ if (!loaded) {
190
+ configCache.set(cwd, defaults);
191
+ return defaults;
192
+ }
193
+ const config = deepMerge(structuredClone(defaults), overlay);
81
194
  config.botUsernames = parseBotUsernames(config.botUsernames);
82
195
  config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
83
196
  config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
@@ -86,7 +199,7 @@ export function loadConfig() {
86
199
  return config;
87
200
  }
88
201
  catch (err) {
89
- process.stderr.write(`pr-shepherd: failed to parse ${rcPath}: ${err instanceof Error ? err.message : String(err)}\n`);
202
+ process.stderr.write(`pr-shepherd: failed to parse ${rcPaths[0]}: ${err instanceof Error ? err.message : String(err)}\n`);
90
203
  const fallback = { ...defaults };
91
204
  configCache.set(cwd, fallback);
92
205
  return fallback;
package/bin/config.json CHANGED
@@ -37,10 +37,8 @@
37
37
  "blockingReviewerLogins": ["copilot"]
38
38
  },
39
39
  "actions": {
40
- "autoResolveOutdated": true,
41
40
  "autoMinimizeSuppressed": true,
42
41
  "autoMarkReady": true,
43
- "commitSuggestions": true,
44
42
  "neverCancelRuns": []
45
43
  }
46
44
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Runs work with an optional working directory without changing Node's global
3
+ * process state. This keeps independent API/MCP requests safe to run together.
4
+ */
5
+ export declare function runWithExecutionCwd<T>(cwd: string | undefined, work: () => T): T;
6
+ /** The caller-scoped working directory, when one was supplied. */
7
+ export declare function getExecutionCwd(): string | undefined;
8
+ /** The caller-scoped working directory, falling back to the CLI process cwd. */
9
+ export declare function getEffectiveCwd(): string;
@@ -0,0 +1,19 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const executionContext = new AsyncLocalStorage();
3
+ /**
4
+ * Runs work with an optional working directory without changing Node's global
5
+ * process state. This keeps independent API/MCP requests safe to run together.
6
+ */
7
+ export function runWithExecutionCwd(cwd, work) {
8
+ if (cwd === undefined)
9
+ return work();
10
+ return executionContext.run({ cwd }, work);
11
+ }
12
+ /** The caller-scoped working directory, when one was supplied. */
13
+ export function getExecutionCwd() {
14
+ return executionContext.getStore()?.cwd;
15
+ }
16
+ /** The caller-scoped working directory, falling back to the CLI process cwd. */
17
+ export function getEffectiveCwd() {
18
+ return getExecutionCwd() ?? process.cwd();
19
+ }
@@ -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,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,45 @@
1
+ import type { RawContextNode, RawComment, RawPr, RawReview, RawReviewSummary, RawThread } from "./batch-raw-types.mts";
2
+ export interface RawPageResponse {
3
+ repository: {
4
+ pullRequest: {
5
+ reviewThreads?: RawPr["reviewThreads"];
6
+ comments?: RawPr["comments"];
7
+ changesRequestedReviews?: RawPr["changesRequestedReviews"];
8
+ reviewSummaries?: RawPr["reviewSummaries"];
9
+ approvedReviews?: RawPr["approvedReviews"];
10
+ commits?: RawPr["commits"];
11
+ } | null;
12
+ };
13
+ }
14
+ export declare function backwardCursor(conn: {
15
+ pageInfo: {
16
+ hasPreviousPage: boolean;
17
+ startCursor: string | null;
18
+ };
19
+ } | undefined): string | undefined;
20
+ export declare function forwardCursor(conn: {
21
+ pageInfo: {
22
+ hasNextPage: boolean;
23
+ endCursor: string | null;
24
+ };
25
+ } | undefined): string | undefined;
26
+ export interface PageCursors {
27
+ threads?: string;
28
+ comments?: string;
29
+ changesRequested?: string;
30
+ reviewSummaries?: string;
31
+ approvedReviews?: string;
32
+ checks?: string;
33
+ }
34
+ export interface PageAccumulator {
35
+ threads: RawThread[];
36
+ comments: RawComment[];
37
+ changesRequested: RawReview[];
38
+ reviewSummaries: RawReviewSummary[];
39
+ approvedReviews: RawReviewSummary[];
40
+ checks: RawContextNode[];
41
+ }
42
+ export declare function applyIncludedPage(dest: PageAccumulator, pr2: NonNullable<RawPageResponse["repository"]["pullRequest"]>, cursors: PageCursors, firstOid: string | undefined, pageCount: number, pr: number): {
43
+ cursors: PageCursors;
44
+ pageCount: number;
45
+ };
@@ -0,0 +1,63 @@
1
+ import { requireContextNodes } from "./batch-response.mjs";
2
+ export function backwardCursor(conn) {
3
+ if (conn?.pageInfo.hasPreviousPage === true && conn.pageInfo.startCursor) {
4
+ return conn.pageInfo.startCursor;
5
+ }
6
+ return undefined;
7
+ }
8
+ export function forwardCursor(conn) {
9
+ if (conn?.pageInfo.hasNextPage === true && conn.pageInfo.endCursor) {
10
+ return conn.pageInfo.endCursor;
11
+ }
12
+ return undefined;
13
+ }
14
+ function prependConnection(dest, conn, pr) {
15
+ if (!conn)
16
+ throw new Error(`PR #${pr} not found`);
17
+ dest.unshift(...conn.nodes);
18
+ }
19
+ function takeCheckPage(pr2, firstOid, pageCount) {
20
+ if (!pr2.commits?.nodes[0]?.commit.statusCheckRollup) {
21
+ throw new Error(`Check-context pagination interrupted: statusCheckRollup disappeared on page ${pageCount + 1} (possible force-push race). Retry after the push stabilizes.`);
22
+ }
23
+ const currentOid = pr2.commits.nodes[0]?.commit.oid;
24
+ if (firstOid !== undefined && currentOid !== undefined && currentOid !== firstOid) {
25
+ throw new Error(`Check-context pagination interrupted: head commit changed from ${firstOid} to ${currentOid} between pages (force-push race). Retry.`);
26
+ }
27
+ const ctxs = pr2.commits.nodes[0]?.commit.statusCheckRollup?.contexts;
28
+ if (!ctxs) {
29
+ return { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
30
+ }
31
+ return { ...ctxs, nodes: requireContextNodes(ctxs.nodes) };
32
+ }
33
+ export function applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr) {
34
+ const next = {};
35
+ let nextPageCount = pageCount;
36
+ if (cursors.threads !== undefined) {
37
+ prependConnection(dest.threads, pr2.reviewThreads, pr);
38
+ next.threads = backwardCursor(pr2.reviewThreads);
39
+ }
40
+ if (cursors.comments !== undefined) {
41
+ prependConnection(dest.comments, pr2.comments, pr);
42
+ next.comments = backwardCursor(pr2.comments);
43
+ }
44
+ if (cursors.changesRequested !== undefined) {
45
+ prependConnection(dest.changesRequested, pr2.changesRequestedReviews, pr);
46
+ next.changesRequested = backwardCursor(pr2.changesRequestedReviews);
47
+ }
48
+ if (cursors.reviewSummaries !== undefined) {
49
+ prependConnection(dest.reviewSummaries, pr2.reviewSummaries, pr);
50
+ next.reviewSummaries = backwardCursor(pr2.reviewSummaries);
51
+ }
52
+ if (cursors.approvedReviews !== undefined) {
53
+ prependConnection(dest.approvedReviews, pr2.approvedReviews, pr);
54
+ next.approvedReviews = backwardCursor(pr2.approvedReviews);
55
+ }
56
+ if (cursors.checks !== undefined) {
57
+ nextPageCount++;
58
+ const extra = takeCheckPage(pr2, firstOid, nextPageCount);
59
+ dest.checks.push(...extra.nodes);
60
+ next.checks = forwardCursor(extra);
61
+ }
62
+ return { cursors: next, pageCount: nextPageCount };
63
+ }
@@ -0,0 +1,14 @@
1
+ import { type RateLimitInfo, type RepoInfo } from "./client.mts";
2
+ import { type PageAccumulator } from "./batch-page-helpers.mts";
3
+ import type { RawPr } from "./batch-raw-types.mts";
4
+ export interface PaginatedBatchNodes extends PageAccumulator {
5
+ rateLimit?: RateLimitInfo;
6
+ }
7
+ export interface PaginateBatchOptions {
8
+ paginateApprovedReviews?: boolean;
9
+ }
10
+ /**
11
+ * Fetch remaining connection pages with a slim `@include` query. Independent
12
+ * connections that still have cursors are combined into one request per round.
13
+ */
14
+ export declare function paginateBatchConnections(pr: number, repo: RepoInfo, raw: RawPr, opts: PaginateBatchOptions, initialRateLimit?: RateLimitInfo): Promise<PaginatedBatchNodes>;
@@ -0,0 +1,62 @@
1
+ import { graphqlWithRateLimit } from "./client.mjs";
2
+ import { GitHubRequestError } from "./errors.mjs";
3
+ import { BATCH_PR_PAGE_QUERY } from "./queries.mjs";
4
+ import { requireContextNodes } from "./batch-response.mjs";
5
+ import { applyIncludedPage, backwardCursor, forwardCursor, } from "./batch-page-helpers.mjs";
6
+ /**
7
+ * Fetch remaining connection pages with a slim `@include` query. Independent
8
+ * connections that still have cursors are combined into one request per round.
9
+ */
10
+ export async function paginateBatchConnections(pr, repo, raw, opts, initialRateLimit) {
11
+ const dest = {
12
+ threads: [...raw.reviewThreads.nodes],
13
+ comments: [...raw.comments.nodes],
14
+ changesRequested: [...raw.changesRequestedReviews.nodes],
15
+ reviewSummaries: [...raw.reviewSummaries.nodes],
16
+ approvedReviews: [...raw.approvedReviews.nodes],
17
+ checks: [
18
+ ...requireContextNodes(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? []),
19
+ ],
20
+ };
21
+ const firstOid = raw.commits.nodes[0]?.commit.oid;
22
+ let cursors = {
23
+ threads: backwardCursor(raw.reviewThreads),
24
+ comments: backwardCursor(raw.comments),
25
+ changesRequested: backwardCursor(raw.changesRequestedReviews),
26
+ reviewSummaries: backwardCursor(raw.reviewSummaries),
27
+ approvedReviews: opts.paginateApprovedReviews === true ? backwardCursor(raw.approvedReviews) : undefined,
28
+ checks: forwardCursor(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts),
29
+ };
30
+ let rateLimit = initialRateLimit;
31
+ let pageCount = 0;
32
+ while (Object.values(cursors).some((cursor) => cursor !== undefined)) {
33
+ if (rateLimit?.remaining === 0) {
34
+ throw new GitHubRequestError("GitHub GraphQL rate limit remaining is 0; pagination incomplete", { status: 403, rateLimit });
35
+ }
36
+ const res = await graphqlWithRateLimit(BATCH_PR_PAGE_QUERY, {
37
+ owner: repo.owner,
38
+ repo: repo.name,
39
+ pr,
40
+ includeThreads: cursors.threads !== undefined,
41
+ threadsCursor: cursors.threads ?? null,
42
+ includeComments: cursors.comments !== undefined,
43
+ commentsCursor: cursors.comments ?? null,
44
+ includeChangesRequested: cursors.changesRequested !== undefined,
45
+ changesRequestedCursor: cursors.changesRequested ?? null,
46
+ includeReviewSummaries: cursors.reviewSummaries !== undefined,
47
+ reviewSummariesCursor: cursors.reviewSummaries ?? null,
48
+ includeApprovedReviews: cursors.approvedReviews !== undefined,
49
+ approvedReviewsCursor: cursors.approvedReviews ?? null,
50
+ includeChecks: cursors.checks !== undefined,
51
+ checksCursor: cursors.checks ?? null,
52
+ });
53
+ rateLimit = res.rateLimit ?? rateLimit;
54
+ const pr2 = res.data.repository.pullRequest;
55
+ if (!pr2)
56
+ throw new Error(`PR #${pr} not found`);
57
+ const applied = applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr);
58
+ cursors = applied.cursors;
59
+ pageCount = applied.pageCount;
60
+ }
61
+ return { ...dest, rateLimit };
62
+ }
@@ -0,0 +1,4 @@
1
+ import type { CheckRun } from "../types.mts";
2
+ import type { RawPr } from "./batch-raw-types.mts";
3
+ export declare function parseCheckSuitesComplete(raw: RawPr): boolean;
4
+ export declare function parseSuiteStartupFailures(raw: RawPr): CheckRun[];
@@ -0,0 +1,25 @@
1
+ export function parseCheckSuitesComplete(raw) {
2
+ return raw.commits.nodes[0]?.commit.checkSuites?.pageInfo.hasNextPage === false;
3
+ }
4
+ export function parseSuiteStartupFailures(raw) {
5
+ const suites = raw.commits.nodes[0]?.commit.checkSuites;
6
+ if (!suites)
7
+ return [];
8
+ return suites.nodes.flatMap((node) => {
9
+ if (node.conclusion !== "STARTUP_FAILURE" || node.workflowRun == null)
10
+ return [];
11
+ const runId = node.workflowRun.databaseId != null ? String(node.workflowRun.databaseId) : null;
12
+ const name = node.workflowRun.workflow?.name?.trim() || (runId ? `workflow run ${runId}` : "workflow run");
13
+ return [
14
+ {
15
+ name,
16
+ status: "COMPLETED",
17
+ conclusion: "STARTUP_FAILURE",
18
+ source: "startup_failure",
19
+ detailsUrl: node.workflowRun.url ?? "",
20
+ event: node.workflowRun.event,
21
+ runId,
22
+ },
23
+ ];
24
+ });
25
+ }
@@ -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,6 @@
1
+ import type { BranchRules, MergeQueueEntryStatus, StackStatus } from "../types.mts";
2
+ import type { RawBaseRef, RawPrMergeFields } from "./batch-raw-rules.mts";
3
+ export declare const EMPTY_BRANCH_RULES: BranchRules;
4
+ export declare function parseBranchRules(baseRef: RawBaseRef | null | undefined): BranchRules;
5
+ export declare function parseMergeQueueEntry(raw: RawPrMergeFields["mergeQueueEntry"]): MergeQueueEntryStatus | null;
6
+ export declare function parseStack(raw: RawPrMergeFields): StackStatus | null;