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,22 @@
1
+ export interface AppendResult {
2
+ body: string;
3
+ mutated: boolean;
4
+ sectionExisted: boolean;
5
+ }
6
+ type ValidationOk = {
7
+ ok: true;
8
+ item: string;
9
+ };
10
+ type ValidationError = {
11
+ ok: false;
12
+ error: string;
13
+ };
14
+ export type ValidationResult = ValidationOk | ValidationError;
15
+ /** Validates that the input is a properly formed markdown list item. */
16
+ export declare function validateJournalItem(input: string): ValidationResult;
17
+ /**
18
+ * Appends a validated list item to the ## Shepherd Journal section of a PR body.
19
+ * Creates the section at the end if absent. Skips if the exact item is already present (idempotent).
20
+ */
21
+ export declare function appendJournalItem(body: string, item: string): AppendResult;
22
+ export {};
@@ -0,0 +1,5 @@
1
+ interface LogFileResult {
2
+ path: string;
3
+ }
4
+ export declare function runLogFile(): Promise<LogFileResult>;
5
+ export {};
@@ -0,0 +1,26 @@
1
+ import { type ResolveRateLimitStop } from "../comments/rate-limit.mts";
2
+ import type { GlobalOptions } from "../types.mts";
3
+ export interface MarkFilesAsViewedOptions extends GlobalOptions {
4
+ prNumber?: number;
5
+ files: string[];
6
+ tests?: boolean;
7
+ matchPatterns?: string[];
8
+ }
9
+ export interface MarkFilesAsViewedResult {
10
+ repo: string;
11
+ prNumber: number;
12
+ pullRequestId: string;
13
+ requestedPaths: string[];
14
+ testSelector: boolean;
15
+ matchPatterns: string[];
16
+ matchedPaths: string[];
17
+ markedPaths: string[];
18
+ alreadyViewedPaths: string[];
19
+ missingPaths: string[];
20
+ unmatchedSelectors: string[];
21
+ errors: string[];
22
+ rateLimit?: ResolveRateLimitStop;
23
+ unmarkedPaths?: string[];
24
+ }
25
+ /** @deprecated Hidden implementation for `mark-files-as-viewed`; use `apply files`. */
26
+ export declare function runMarkFilesAsViewed(opts: MarkFilesAsViewedOptions): Promise<MarkFilesAsViewedResult>;
@@ -2,6 +2,7 @@
2
2
  import { graphql, graphqlWithRateLimit, getCurrentPrNumber, getRepoInfo, } from "../github/client.mjs";
3
3
  import { paginateForward } from "../github/pagination.mjs";
4
4
  import { isRateLimitMessage, rateLimitFromError, rateLimitFromGraphQlResult, } from "../comments/rate-limit.mjs";
5
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
5
6
  const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) {
6
7
  repository(owner: $owner, name: $repo) {
7
8
  pullRequest(number: $pr) {
@@ -22,11 +23,13 @@ const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr
22
23
  }`;
23
24
  const TEST_FILE_RE = /(^|\/)(tests?|__tests__|spec)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|_tests?\.rs$|(^|\/)tests?\.rs$/i;
24
25
  const BULK_CHUNK_SIZE = 10;
26
+ /** @deprecated Hidden implementation for `mark-files-as-viewed`; use `apply files`. */
25
27
  export async function runMarkFilesAsViewed(opts) {
26
28
  const repo = await getRepoInfo();
27
29
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
28
- if (!prNumber)
29
- throw new Error("No PR number provided and no current branch PR found");
30
+ if (!prNumber) {
31
+ throw new ShepherdError("No PR number provided and no current branch PR found", EXIT.UNAVAILABLE);
32
+ }
30
33
  const matchPatterns = opts.matchPatterns ?? [];
31
34
  const matchRegexes = matchPatterns.map((pattern) => compilePattern(pattern));
32
35
  const fetched = await fetchPullRequestFiles(prNumber, repo);
@@ -61,7 +64,7 @@ async function fetchPullRequestFiles(pr, repo) {
61
64
  });
62
65
  const raw = first.data.repository?.pullRequest;
63
66
  if (!raw)
64
- throw new Error(`PR #${pr} not found`);
67
+ throw new ShepherdError(`PR #${pr} not found`, EXIT.UNAVAILABLE);
65
68
  let files = raw.files.nodes;
66
69
  if (raw.files.pageInfo.hasNextPage && raw.files.pageInfo.endCursor) {
67
70
  const extra = await paginateForward(async (cursor) => {
@@ -73,7 +76,7 @@ async function fetchPullRequestFiles(pr, repo) {
73
76
  });
74
77
  const pr2 = res.data.repository?.pullRequest;
75
78
  if (!pr2)
76
- throw new Error(`PR #${pr} not found`);
79
+ throw new ShepherdError(`PR #${pr} not found`, EXIT.UNAVAILABLE);
77
80
  return pr2.files;
78
81
  }, raw.files.pageInfo.endCursor);
79
82
  files = [...files, ...extra];
@@ -86,7 +89,7 @@ function compilePattern(pattern) {
86
89
  }
87
90
  catch (e) {
88
91
  const msg = e instanceof Error ? e.message : String(e);
89
- throw new Error(`Invalid --match regex ${JSON.stringify(pattern)}: ${msg}`);
92
+ throw new ShepherdError(`Invalid --match regex ${JSON.stringify(pattern)}: ${msg}`, EXIT.USAGE);
90
93
  }
91
94
  }
92
95
  function selectChangedFiles(changedFiles, opts) {
@@ -0,0 +1,10 @@
1
+ import type { IterateCommandOptions, IterateResult } from "../types.mts";
2
+ interface PollCommandOptions extends IterateCommandOptions {
3
+ intervalSeconds: number;
4
+ timeoutSeconds: number;
5
+ quietStatus?: boolean;
6
+ untilTerminal?: boolean;
7
+ }
8
+ /** @deprecated Hidden implementation for the legacy `poll` alias. */
9
+ export declare function runPoll(opts: PollCommandOptions): Promise<IterateResult>;
10
+ export {};
@@ -54,6 +54,7 @@ function writeWaitProgress(opts) {
54
54
  }
55
55
  return signature;
56
56
  }
57
+ /** @deprecated Hidden implementation for the legacy `poll` alias. */
57
58
  export async function runPoll(opts) {
58
59
  const { intervalSeconds, timeoutSeconds, quietStatus: quietStatusOpt, untilTerminal: untilTerminalOpt, ...iterateOpts } = opts;
59
60
  const intervalMs = Math.min(intervalSeconds * 1000, MAX_TIMER_MS);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Ready-delay state machine for the shepherd iterate loop.
3
+ *
4
+ * When all READY conditions hold, shepherd writes a `ready-since.txt` marker
5
+ * to the state dir. The loop continues until the PR has been READY for
6
+ * `readyDelaySeconds` consecutively. Any not-READY result resets the timer.
7
+ */
8
+ interface ReadyDelayState {
9
+ isReady: boolean;
10
+ /**
11
+ * When true, the loop should cancel itself — the PR has been READY for
12
+ * longer than the configured ready-delay.
13
+ */
14
+ shouldCancel: boolean;
15
+ /** How many seconds remain in the ready-delay. */
16
+ remainingSeconds: number;
17
+ }
18
+ /**
19
+ * Update the ready-delay state machine and return the current decision.
20
+ *
21
+ * Call this at the end of each sweep iteration:
22
+ * - If `isReady == true`: start or continue the ready timer.
23
+ * - If `isReady == false`: reset the timer.
24
+ *
25
+ * When `shouldCancel == true`, the formatter tells loop-capable agents to cancel
26
+ * the loop and tells one-shot agents to stop.
27
+ */
28
+ export declare function updateReadyDelay(prNumber: number, isReady: boolean, readyDelaySeconds: number, owner: string, repo: string): Promise<ReadyDelayState>;
29
+ export {};
@@ -0,0 +1,15 @@
1
+ import { type RepoInfo } from "../github/client.mts";
2
+ import type { CiVerdict } from "../checks/classify.mts";
3
+ import type { BatchPrData, MergeStatusResult, ShepherdStatus } from "../types.mts";
4
+ interface ReadyMergeabilityRefresh {
5
+ batchData: BatchPrData;
6
+ mergeStatus: MergeStatusResult;
7
+ status: ShepherdStatus;
8
+ }
9
+ export declare function refreshUnknownMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData): Promise<{
10
+ batchData: BatchPrData;
11
+ didRefresh: boolean;
12
+ }>;
13
+ export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number): Promise<ReadyMergeabilityRefresh>;
14
+ export declare function isBlockedByFilteredCheck(mergeStatus: MergeStatusResult, verdict: CiVerdict): boolean;
15
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ResolveOptions } from "../types.mts";
2
+ import type { ResolveCommandOptions } from "./resolve.mts";
3
+ /** @deprecated Hidden implementation for `resolve`; use `apply review`. */
4
+ export declare function runResolveMutate(opts: ResolveCommandOptions & ResolveOptions): Promise<import("../comments/resolve.mts").ResolveResult>;
@@ -6,11 +6,13 @@ import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "..
6
6
  import { markReplySeen } from "../state/seen-comments.mjs";
7
7
  import { threadTranscriptBody } from "../threads/transcript.mjs";
8
8
  import { addPrShepherdMarker } from "../comments/marker.mjs";
9
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
10
+ /** @deprecated Hidden implementation for `resolve`; use `apply review`. */
9
11
  export async function runResolveMutate(opts) {
10
12
  const repo = await getRepoInfo();
11
13
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
12
14
  if (prNumber === null) {
13
- throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
15
+ throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
14
16
  }
15
17
  const { data } = await fetchPrBatch(prNumber, repo, { paginateApprovedReviews: true });
16
18
  const config = loadConfig();
@@ -0,0 +1,4 @@
1
+ import type { GlobalOptions } from "../types.mts";
2
+ export { runResolveMutate } from "./resolve-mutate.mts";
3
+ export interface ResolveCommandOptions extends GlobalOptions {
4
+ }
@@ -0,0 +1,7 @@
1
+ export declare const SHEPHERD_JOURNAL_SECTION = "## Shepherd Journal";
2
+ export declare const SHEPHERD_JOURNAL_SECTION_PATTERN: RegExp;
3
+ export declare const SHEPHERD_JOURNAL_APPEND_HINT = "If this section already exists, append your entries under it instead of creating a duplicate heading.";
4
+ export declare const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` \u2014 you are seeing these for the first time. Eligible non-human IDs, when present, are already included in `--minimize-comment-ids` in the `apply review:` or `resolve-only:` command above; if any warrants a Shepherd Journal note, append it before applying review mutations.";
5
+ export declare function buildShepherdJournalInstruction(prNumber: number, itemReferenceGuidance: string): string;
6
+ export declare const SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEM_HEADINGS = "For threads and comments, use the markdown link shown in its heading above; for reviews, reference the review ID.";
7
+ export declare const SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS = "For threads and comments, use the markdown link shown in each item's bullet above; for reviews, reference the review ID.";
@@ -1,10 +1,10 @@
1
1
  export const SHEPHERD_JOURNAL_SECTION = "## Shepherd Journal";
2
2
  export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
3
3
  export const SHEPHERD_JOURNAL_APPEND_HINT = "If this section already exists, append your entries under it instead of creating a duplicate heading.";
4
- export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time. Eligible non-human IDs, when present, are already included in `--minimize-comment-ids` in the resolve or resolve-only command above; if any warrants a Shepherd Journal note, append it before running resolve.";
4
+ export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time. Eligible non-human IDs, when present, are already included in `--minimize-comment-ids` in the `apply review:` or `resolve-only:` command above; if any warrants a Shepherd Journal note, append it before applying review mutations.";
5
5
  export function buildShepherdJournalInstruction(prNumber, itemReferenceGuidance) {
6
6
  return [
7
- `For any large decisions or rejections you made this iteration, run \`pr-shepherd journal ${prNumber} '- <decision>'\` to append an entry to the \`${SHEPHERD_JOURNAL_SECTION}\` section.`,
7
+ `For any large decisions or rejections you made this iteration, run \`pr-shepherd apply journal ${prNumber} '- <decision>'\` to append an entry to the \`${SHEPHERD_JOURNAL_SECTION}\` section.`,
8
8
  itemReferenceGuidance,
9
9
  `The command is idempotent — re-running with the same text is a no-op.`,
10
10
  ].join(" ");
@@ -0,0 +1,14 @@
1
+ import type { AuthorType } from "../types.mts";
2
+ export type NormalizedBotUsernames = ReadonlySet<string>;
3
+ export declare function normalizeBotUsernames(botUsernames?: readonly string[] | undefined | null): NormalizedBotUsernames;
4
+ export declare function normalizeAuthorType(typeName: string | undefined | null, login: string | undefined | null): AuthorType;
5
+ export declare function isHumanAuthor(author: {
6
+ author?: string;
7
+ login?: string;
8
+ authorType?: AuthorType;
9
+ }): boolean;
10
+ export declare function isConfiguredBotAuthor(author: {
11
+ author?: string;
12
+ login?: string;
13
+ authorType?: AuthorType;
14
+ }, botUsernames?: NormalizedBotUsernames): boolean;
@@ -0,0 +1,2 @@
1
+ export declare function hasPrShepherdMarker(body: string): boolean;
2
+ export declare function addPrShepherdMarker(body: string): string;
@@ -0,0 +1,4 @@
1
+ import type { MinimizeCommentsPolicy } from "../config/load.mts";
2
+ import type { AuthorType } from "../types.mts";
3
+ import { type NormalizedBotUsernames } from "./authors.mts";
4
+ export declare function shouldMinimizeAuthor(authorType: AuthorType | undefined, policy: MinimizeCommentsPolicy | undefined, author?: string, botUsernames?: NormalizedBotUsernames): boolean;
@@ -0,0 +1,15 @@
1
+ import type { ResolveResult } from "./resolve.mts";
2
+ export type ResolveMutationOp = {
3
+ kind: "p";
4
+ id: string;
5
+ } | {
6
+ kind: "r";
7
+ id: string;
8
+ } | {
9
+ kind: "m";
10
+ id: string;
11
+ } | {
12
+ kind: "d";
13
+ id: string;
14
+ };
15
+ export declare function setPendingOps(result: ResolveResult, ops: ResolveMutationOp[]): void;
@@ -0,0 +1,18 @@
1
+ export interface ResolveRateLimitStop {
2
+ message: string;
3
+ retryAfterSeconds?: number;
4
+ limit?: number;
5
+ remaining?: number;
6
+ resetAt?: number;
7
+ }
8
+ export declare function rateLimitFromError(err: unknown, fallbackMessage: string): ResolveRateLimitStop | null;
9
+ export declare function rateLimitFromGraphQlResult(messages: string[], meta: {
10
+ rateLimit?: {
11
+ remaining?: unknown;
12
+ limit?: unknown;
13
+ resetAt?: unknown;
14
+ };
15
+ retryAfterSeconds?: unknown;
16
+ stopOnZeroRemaining?: boolean;
17
+ }): ResolveRateLimitStop | undefined;
18
+ export declare function isRateLimitMessage(message: string): boolean;
@@ -0,0 +1,34 @@
1
+ import { type RepoInfo } from "../github/client.mts";
2
+ import type { ResolveOptions } from "../types.mts";
3
+ import { type ResolveRateLimitStop } from "./rate-limit.mts";
4
+ export interface ResolveResult {
5
+ repliedThreads: string[];
6
+ resolvedThreads: string[];
7
+ minimizedComments: string[];
8
+ dismissedReviews: string[];
9
+ errors: string[];
10
+ skippedDismissals?: string[];
11
+ skippedHumanResolves?: string[];
12
+ skippedHumanMinimizes?: string[];
13
+ skippedHumanDismissals?: string[];
14
+ skippedNonHumanReplies?: string[];
15
+ rateLimit?: ResolveRateLimitStop;
16
+ unrepliedThreads?: string[];
17
+ unresolvedThreads?: string[];
18
+ unminimizedComments?: string[];
19
+ undismissedReviews?: string[];
20
+ }
21
+ export declare function applyResolveOptions(pr: number, repo: RepoInfo, opts: ResolveOptions): Promise<ResolveResult>;
22
+ /** @deprecated Compatibility alias; use `autoResolveThreads`. */
23
+ export declare function autoResolveOutdated(threadIds: string[]): Promise<{
24
+ resolved: string[];
25
+ errors: string[];
26
+ }>;
27
+ export declare function autoResolveThreads(threadIds: string[]): Promise<{
28
+ resolved: string[];
29
+ errors: string[];
30
+ }>;
31
+ export declare function autoMinimizeComments(minimizeIds: string[]): Promise<{
32
+ minimized: string[];
33
+ errors: string[];
34
+ }>;
@@ -56,6 +56,7 @@ export async function applyResolveOptions(pr, repo, opts) {
56
56
  await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds, filteredDismissReviewIds, opts.dismissMessage ?? "", result);
57
57
  return result;
58
58
  }
59
+ /** @deprecated Compatibility alias; use `autoResolveThreads`. */
59
60
  export async function autoResolveOutdated(threadIds) {
60
61
  return autoResolveThreads(threadIds);
61
62
  }
@@ -0,0 +1,8 @@
1
+ import type { ReviewThread } from "../types.mts";
2
+ interface StateKey {
3
+ owner: string;
4
+ repo: string;
5
+ pr: number;
6
+ }
7
+ export declare function markReviewInlineThreadMarkers(key: StateKey, threads: readonly ReviewThread[]): Promise<void>;
8
+ export {};
@@ -0,0 +1,28 @@
1
+ import { type SeenMarker } from "../state/seen-comments.mts";
2
+ import { type NormalizedBotUsernames } from "./authors.mts";
3
+ import type { Review } from "../types.mts";
4
+ interface ReviewVisibility {
5
+ visible: Review[];
6
+ toMarkSeen: Review[];
7
+ }
8
+ export declare function classifyReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>): ReviewVisibility;
9
+ /**
10
+ * Bot CHANGES_REQUESTED reviews are different from every other surfaceable
11
+ * item: the agent — not the author — is responsible for dismissing them via
12
+ * `--dismiss-review-ids` after a fix push. If the agent forgets, the review
13
+ * stays in `CHANGES_REQUESTED` state and silently blocks the PR.
14
+ *
15
+ * The standard seen-gate (`classifyReviewsForDisplay`) suppresses items with
16
+ * unchanged bodies, which would also drop the bot CR ID from the
17
+ * `--dismiss-review-ids` flag — making the bug irrecoverable. This function
18
+ * keeps every bot CR in the visible set on every tick, using the seen-map
19
+ * only to pick the render form:
20
+ *
21
+ * - `new` / `edited` → full bullet (caller renders body normally).
22
+ * - `unchanged` → flagged `staleBotCr: true` so the formatter emits a terse
23
+ * one-line reminder.
24
+ *
25
+ * Human-authored CR reviews continue to flow through the standard seen-gate.
26
+ */
27
+ export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames): ReviewVisibility;
28
+ export {};
@@ -0,0 +1,2 @@
1
+ import { type RepoInfo } from "../github/client.mts";
2
+ export declare function waitForSha(pr: number, repo: RepoInfo, expectedSha: string): Promise<void>;
@@ -0,0 +1,11 @@
1
+ import { type SeenMarker } from "../state/seen-comments.mts";
2
+ import { type NormalizedBotUsernames } from "./authors.mts";
3
+ import type { FirstLookThread, ReviewThread } from "../types.mts";
4
+ interface ThreadVisibility {
5
+ activeThreads: ReviewThread[];
6
+ resolutionOnlyThreads: ReviewThread[];
7
+ firstLookThreads: FirstLookThread[];
8
+ toMarkSeen: ReviewThread[];
9
+ }
10
+ export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames): ThreadVisibility;
11
+ export {};
@@ -0,0 +1,11 @@
1
+ import { type SeenMarker } from "../state/seen-comments.mts";
2
+ import type { MinimizeCommentsPolicy } from "../config/load.mts";
3
+ import type { ActionableComment, PrComment } from "../types.mts";
4
+ import type { NormalizedBotUsernames } from "./authors.mts";
5
+ interface VisibleCommentClassification {
6
+ actionable: ActionableComment[];
7
+ minimizeIds: string[];
8
+ toMarkSeen: ActionableComment[];
9
+ }
10
+ export declare function classifyVisibleComments(comments: PrComment[], seenMap: Map<string, SeenMarker>, minimizeComments: MinimizeCommentsPolicy | undefined, botUsernames?: NormalizedBotUsernames): VisibleCommentClassification;
11
+ export {};
@@ -0,0 +1,60 @@
1
+ declare const MINIMIZE_COMMENTS_POLICIES: readonly ["all", "bots", "users", "none"];
2
+ export type MinimizeCommentsPolicy = (typeof MINIMIZE_COMMENTS_POLICIES)[number];
3
+ interface PrShepherdConfig {
4
+ /** Optional user classification configuration; preserved for rule consumers. */
5
+ classify?: unknown;
6
+ /** GitHub logins that should be treated as bots even when GitHub reports User/Unknown. */
7
+ botUsernames: string[];
8
+ /** Case-insensitive glob patterns for check/status context names Shepherd should ignore. */
9
+ ignoreChecks: string[];
10
+ iterate: {
11
+ fixAttemptsPerThread: number;
12
+ stallTimeoutMinutes: number;
13
+ /**
14
+ * When `true`, APPROVED-state reviews are also eligible for minimization — defaults to `false`
15
+ * so approvals stay visible. `minimizeComments` still filters by GitHub author type.
16
+ */
17
+ minimizeApprovals: boolean;
18
+ /**
19
+ * Which GitHub author classes should be auto-minimized for minimizable PR comments and review
20
+ * summaries. Items excluded by this policy are still surfaced once (and after edits) via seen
21
+ * markers so they do not repeat forever.
22
+ */
23
+ minimizeComments: MinimizeCommentsPolicy;
24
+ /**
25
+ * One-liner hint appended to the `fix_code` push instruction when the branch is behind its
26
+ * base — e.g. "rebase --force-with-lease" or "see .agents/skills/git-and-prs.md". Empty
27
+ * (default) omits the hint entirely; the CLI never prescribes rebase/merge mechanics itself.
28
+ */
29
+ behindBaseHint: string;
30
+ };
31
+ watch: {
32
+ readyDelayMinutes: number;
33
+ };
34
+ resolve: {
35
+ shaPoll: {
36
+ intervalMs: number;
37
+ maxAttempts: number;
38
+ };
39
+ };
40
+ checks: {
41
+ ciTriggerEvents: string[];
42
+ };
43
+ mergeStatus: {
44
+ blockingReviewerLogins: string[];
45
+ };
46
+ actions: {
47
+ autoMinimizeSuppressed: boolean;
48
+ autoMarkReady: boolean;
49
+ /** Case-insensitive glob patterns for workflow/check names Shepherd must not cancel. */
50
+ neverCancelRuns: string[];
51
+ /** @deprecated Accepted for compatibility, ignored by the loader. */
52
+ autoResolveOutdated?: boolean;
53
+ /** @deprecated Accepted for compatibility, ignored by the loader. */
54
+ commitSuggestions?: boolean;
55
+ };
56
+ }
57
+ export declare function loadConfig(): PrShepherdConfig;
58
+ /** Reset the config cache — for use in tests that change directories. */
59
+ export declare function _resetConfigCache(): void;
60
+ export {};
@@ -1,8 +1,10 @@
1
+ /* eslint-disable max-lines */
1
2
  import { readFileSync, statSync } from "node:fs";
2
3
  import { join, dirname } 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
10
  function findRcFile(startDir) {
@@ -41,6 +43,10 @@ function isMinimizeCommentsPolicy(value) {
41
43
  return MINIMIZE_COMMENTS_POLICIES.some((policy) => policy === value);
42
44
  }
43
45
  function parseMinimizeCommentsPolicy(value) {
46
+ if (value === "users") {
47
+ process.stderr.write('pr-shepherd: config: iterate.minimizeComments: "users" is deprecated and is now treated as "none".\n');
48
+ return "none";
49
+ }
44
50
  if (isMinimizeCommentsPolicy(value))
45
51
  return value;
46
52
  throw new Error(`Invalid config: iterate.minimizeComments must be one of "all", "bots", "users", or "none", got ${JSON.stringify(value)}`);
@@ -63,10 +69,64 @@ function parseNeverCancelRuns(value) {
63
69
  }
64
70
  return value;
65
71
  }
72
+ const KNOWN_CONFIG_KEYS = new Set([
73
+ "classify",
74
+ "botUsernames",
75
+ "ignoreChecks",
76
+ "iterate",
77
+ "watch",
78
+ "resolve",
79
+ "checks",
80
+ "mergeStatus",
81
+ "actions",
82
+ ]);
83
+ const KNOWN_NESTED_KEYS = {
84
+ iterate: new Set([
85
+ "fixAttemptsPerThread",
86
+ "stallTimeoutMinutes",
87
+ "minimizeApprovals",
88
+ "minimizeComments",
89
+ "behindBaseHint",
90
+ ]),
91
+ watch: new Set(["readyDelayMinutes"]),
92
+ resolve: new Set(["shaPoll"]),
93
+ checks: new Set(["ciTriggerEvents"]),
94
+ mergeStatus: new Set(["blockingReviewerLogins"]),
95
+ actions: new Set([
96
+ "autoMinimizeSuppressed",
97
+ "autoMarkReady",
98
+ "neverCancelRuns",
99
+ "autoResolveOutdated",
100
+ "commitSuggestions",
101
+ ]),
102
+ };
103
+ function warnUnknownConfigKeys(config) {
104
+ for (const key of Object.keys(config)) {
105
+ if (!KNOWN_CONFIG_KEYS.has(key)) {
106
+ process.stderr.write(`pr-shepherd: config: unknown key "${key}" ignored.\n`);
107
+ delete config[key];
108
+ continue;
109
+ }
110
+ const value = config[key];
111
+ const nested = KNOWN_NESTED_KEYS[key];
112
+ if (nested === undefined ||
113
+ value === null ||
114
+ typeof value !== "object" ||
115
+ Array.isArray(value)) {
116
+ continue;
117
+ }
118
+ for (const child of Object.keys(value)) {
119
+ if (!nested.has(child)) {
120
+ process.stderr.write(`pr-shepherd: config: unknown key "${key}.${child}" ignored.\n`);
121
+ delete value[child];
122
+ }
123
+ }
124
+ }
125
+ }
66
126
  const defaults = builtins;
67
127
  const configCache = new Map();
68
128
  export function loadConfig() {
69
- const cwd = process.cwd();
129
+ const cwd = getEffectiveCwd();
70
130
  if (configCache.has(cwd))
71
131
  return configCache.get(cwd);
72
132
  const rcPath = findRcFile(cwd);
@@ -77,6 +137,17 @@ export function loadConfig() {
77
137
  try {
78
138
  const raw = readFileSync(rcPath, "utf8");
79
139
  const parsed = (parse(raw) ?? {});
140
+ const rawActions = parsed.actions;
141
+ if (rawActions !== null && typeof rawActions === "object" && !Array.isArray(rawActions)) {
142
+ const actions = rawActions;
143
+ for (const key of ["autoResolveOutdated", "commitSuggestions"]) {
144
+ if (key in actions) {
145
+ process.stderr.write(`pr-shepherd: config: actions.${key} is deprecated and ignored.\n`);
146
+ delete actions[key];
147
+ }
148
+ }
149
+ }
150
+ warnUnknownConfigKeys(parsed);
80
151
  const config = deepMerge(defaults, parsed);
81
152
  config.botUsernames = parseBotUsernames(config.botUsernames);
82
153
  config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
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
+ }