pr-shepherd 0.48.0 → 0.50.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 (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -2
  3. package/bin/api.d.mts +19 -3
  4. package/bin/api.mjs +57 -8
  5. package/bin/classify/apply.d.mts +2 -0
  6. package/bin/classify/apply.mjs +1 -1
  7. package/bin/cli/default-poll.mjs +1 -0
  8. package/bin/cli/help-command-pages.d.mts +9 -9
  9. package/bin/cli/help-command-pages.mjs +8 -8
  10. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  11. package/bin/cli/help-iterate-poll-pages.mjs +10 -5
  12. package/bin/cli/help-top-page.d.mts +1 -1
  13. package/bin/cli/help-top-page.mjs +5 -3
  14. package/bin/cli/help.d.mts +10 -10
  15. package/bin/cli/iterate-instructions.mjs +9 -2
  16. package/bin/cli/iterate-lean.mjs +11 -0
  17. package/bin/cli/poll-handler.mjs +25 -4
  18. package/bin/cli/poll-summary-emitter.d.mts +4 -0
  19. package/bin/cli/poll-summary-emitter.mjs +22 -0
  20. package/bin/cli/poll-summary-formatter.d.mts +2 -0
  21. package/bin/cli/poll-summary-formatter.mjs +96 -0
  22. package/bin/cli/poll-targets.d.mts +15 -0
  23. package/bin/cli/poll-targets.mjs +114 -0
  24. package/bin/cli/validate-default-args.mjs +1 -4
  25. package/bin/cli-parser.mjs +3 -0
  26. package/bin/commands/iterate/check-instructions.d.mts +2 -8
  27. package/bin/commands/iterate/check-instructions.mjs +2 -11
  28. package/bin/commands/iterate/escalate.mjs +30 -2
  29. package/bin/commands/iterate/fix-code.mjs +62 -20
  30. package/bin/commands/iterate/render.mjs +1 -1
  31. package/bin/commands/iterate/stall.mjs +30 -0
  32. package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
  33. package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
  34. package/bin/commands/poll-summary.d.mts +10 -0
  35. package/bin/commands/poll-summary.mjs +163 -0
  36. package/bin/commands/ready-delay.d.mts +3 -1
  37. package/bin/commands/ready-delay.mjs +3 -2
  38. package/bin/commands/resolve-mutate.mjs +22 -68
  39. package/bin/comments/resolve.d.mts +5 -0
  40. package/bin/comments/resolve.mjs +2 -11
  41. package/bin/github/gql/poll-stack-summary.gql +33 -0
  42. package/bin/github/gql/poll-summary-fragment.gql +198 -0
  43. package/bin/github/poll-summary-checks.d.mts +3 -0
  44. package/bin/github/poll-summary-checks.mjs +61 -0
  45. package/bin/github/poll-summary-projector.d.mts +4 -0
  46. package/bin/github/poll-summary-projector.mjs +81 -0
  47. package/bin/github/poll-summary-raw.d.mts +134 -0
  48. package/bin/github/poll-summary-raw.mjs +1 -0
  49. package/bin/github/poll-summary-review.d.mts +4 -0
  50. package/bin/github/poll-summary-review.mjs +85 -0
  51. package/bin/github/poll-summary-route.d.mts +4 -0
  52. package/bin/github/poll-summary-route.mjs +47 -0
  53. package/bin/github/poll-summary.d.mts +7 -0
  54. package/bin/github/poll-summary.mjs +110 -0
  55. package/bin/github/queries.d.mts +4 -0
  56. package/bin/github/queries.mjs +4 -0
  57. package/bin/mcp/server.mjs +39 -6
  58. package/bin/pr-reference.d.mts +2 -0
  59. package/bin/pr-reference.mjs +4 -0
  60. package/bin/state/fix-attempts.d.mts +3 -4
  61. package/bin/state/fix-attempts.mjs +2 -3
  62. package/bin/types/escalate.d.mts +10 -0
  63. package/bin/types/poll-summary.d.mts +82 -0
  64. package/bin/types/poll-summary.mjs +1 -0
  65. package/bin/types.d.mts +1 -0
  66. package/bin/types.mjs +1 -0
  67. package/package.json +1 -1
  68. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  69. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  70. package/plugins/pr-shepherd/.mcp.json +1 -1
  71. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +5 -10
@@ -0,0 +1,110 @@
1
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
2
+ import { graphqlWithRateLimit } from "./client.mjs";
3
+ import { GitHubRequestError } from "./errors.mjs";
4
+ import { summarizePollSummaryPr } from "./poll-summary-projector.mjs";
5
+ import { POLL_STACK_SUMMARY_QUERY, POLL_SUMMARY_FRAGMENT } from "./queries.mjs";
6
+ const MAX_EXPLICIT_PRS_PER_QUERY = 50;
7
+ export async function fetchPollSummary(opts, repo) {
8
+ if (opts.stackPrNumber !== undefined)
9
+ return fetchStackSummary(opts, repo);
10
+ const requested = deduplicate(opts.prNumbers ?? []);
11
+ if (requested.length === 0) {
12
+ throw new ShepherdError("Aggregate poll requires at least two PRs or --stack <PR>", EXIT.USAGE);
13
+ }
14
+ const raw = [];
15
+ let viewerCanAdminister = false;
16
+ for (let offset = 0; offset < requested.length; offset += MAX_EXPLICIT_PRS_PER_QUERY) {
17
+ const chunk = requested.slice(offset, offset + MAX_EXPLICIT_PRS_PER_QUERY);
18
+ const fetched = await fetchExplicitChunk(chunk, repo);
19
+ viewerCanAdminister = fetched.viewerCanAdminister;
20
+ raw.push(...fetched.prs);
21
+ }
22
+ return {
23
+ selection: { kind: "prs", requested },
24
+ prs: await Promise.all(raw.map((pr) => summarizePollSummaryPr(pr, repo, opts, viewerCanAdminister))),
25
+ };
26
+ }
27
+ async function fetchExplicitChunk(prs, repo) {
28
+ const declarations = prs.map((_, index) => `$pr${index}: Int!`).join(", ");
29
+ const aliases = prs
30
+ .map((_, index) => `pr${index}: pullRequest(number: $pr${index}) { ...PollSummaryPr }`)
31
+ .join("\n");
32
+ const query = `${POLL_SUMMARY_FRAGMENT}\nquery PollSummary($owner: String!, $repo: String!, ${declarations}) {\n _shepherdRateLimit: rateLimit { cost limit nodeCount remaining resetAt used }\n repository(owner: $owner, name: $repo) {\n viewerCanAdminister\n ${aliases}\n }\n}`;
33
+ const variables = Object.fromEntries(prs.map((pr, index) => [`pr${index}`, pr]));
34
+ const result = await graphqlWithRateLimit(query, {
35
+ owner: repo.owner,
36
+ repo: repo.name,
37
+ ...variables,
38
+ });
39
+ if (!result.data.repository)
40
+ throw missingRepository(repo);
41
+ const rawPrs = prs.map((pr, index) => {
42
+ const raw = result.data.repository[`pr${index}`];
43
+ if (!raw)
44
+ throw new ShepherdError(`PR #${pr} not found`, EXIT.UNAVAILABLE);
45
+ return raw;
46
+ });
47
+ return { prs: rawPrs, viewerCanAdminister: result.data.repository.viewerCanAdminister };
48
+ }
49
+ async function fetchStackSummary(opts, repo) {
50
+ const anchor = opts.stackPrNumber;
51
+ let after = null;
52
+ let stackId = null;
53
+ let stackNumber = 0;
54
+ let stackSize = 0;
55
+ let viewerCanAdminister = false;
56
+ const entries = [];
57
+ do {
58
+ const response = (await graphqlWithRateLimit(POLL_STACK_SUMMARY_QUERY, {
59
+ owner: repo.owner,
60
+ repo: repo.name,
61
+ anchor,
62
+ after,
63
+ }));
64
+ const repository = response.data.repository;
65
+ if (!repository)
66
+ throw missingRepository(repo);
67
+ viewerCanAdminister = repository.viewerCanAdminister;
68
+ if (!repository.pullRequest) {
69
+ throw new ShepherdError(`PR #${anchor} not found`, EXIT.UNAVAILABLE);
70
+ }
71
+ const stack = repository.pullRequest.stack;
72
+ if (!stack) {
73
+ throw new ShepherdError(`PR #${anchor} is not part of a native GitHub stack`, EXIT.UNAVAILABLE);
74
+ }
75
+ if (stackId !== null && stack.id !== stackId) {
76
+ throw new ShepherdError("GitHub stack membership changed while it was being fetched; retry", EXIT.TEMPFAIL);
77
+ }
78
+ stackId = stack.id;
79
+ stackNumber = stack.number;
80
+ stackSize = stack.size;
81
+ for (const entry of stack.entries.nodes) {
82
+ if (!entry.pullRequest) {
83
+ throw new ShepherdError("GitHub returned an incomplete pull-request stack entry", EXIT.TEMPFAIL);
84
+ }
85
+ entries.push({ position: entry.position, pullRequest: entry.pullRequest });
86
+ }
87
+ const pageInfo = stack.entries.pageInfo;
88
+ if (pageInfo.hasNextPage && !pageInfo.endCursor) {
89
+ throw new ShepherdError("GitHub stack pagination did not include an end cursor", EXIT.TEMPFAIL);
90
+ }
91
+ after = pageInfo.hasNextPage ? pageInfo.endCursor : null;
92
+ } while (after !== null);
93
+ const unique = new Map();
94
+ for (const entry of entries)
95
+ unique.set(entry.pullRequest.number, entry);
96
+ if (!unique.has(anchor) || unique.size !== stackSize) {
97
+ throw new ShepherdError(`GitHub returned incomplete stack membership (${unique.size} of ${stackSize} entries)`, EXIT.TEMPFAIL);
98
+ }
99
+ const ordered = [...unique.values()].sort((left, right) => left.position - right.position);
100
+ return {
101
+ selection: { kind: "stack", anchor, stackNumber, stackSize },
102
+ prs: await Promise.all(ordered.map((entry) => summarizePollSummaryPr(entry.pullRequest, repo, opts, viewerCanAdminister))),
103
+ };
104
+ }
105
+ function deduplicate(values) {
106
+ return [...new Set(values)];
107
+ }
108
+ function missingRepository(repo) {
109
+ return new GitHubRequestError(`GitHub GraphQL response did not include repository ${repo.owner}/${repo.name} (not found or access denied)`, { status: 200 });
110
+ }
@@ -12,6 +12,10 @@ export declare const BATCH_PR_QUERY: string;
12
12
  export declare const BATCH_PR_PAGE_QUERY: string;
13
13
  /** Cheap PR fingerprint used to skip an unchanged BatchPr snapshot. */
14
14
  export declare const PR_FINGERPRINT_QUERY: string;
15
+ /** Compact per-PR fields shared by explicit-list and native-stack summary queries. */
16
+ export declare const POLL_SUMMARY_FRAGMENT: string;
17
+ /** Discovers and summarizes every entry in one native GitHub pull-request stack. */
18
+ export declare const POLL_STACK_SUMMARY_QUERY: string;
15
19
  /** PR head fields plus a single review thread for `commit-suggestion`. */
16
20
  export declare const SUGGESTION_THREADS_QUERY: string;
17
21
  /** Fetches additional comments for a single review thread when its nested connection paginates. */
@@ -16,6 +16,10 @@ export const BATCH_PR_QUERY = withSharedFragments(gql("batch-pr.gql"));
16
16
  export const BATCH_PR_PAGE_QUERY = gql("batch-pr-page.gql");
17
17
  /** Cheap PR fingerprint used to skip an unchanged BatchPr snapshot. */
18
18
  export const PR_FINGERPRINT_QUERY = withSharedFragments(gql("pr-fingerprint.gql"));
19
+ /** Compact per-PR fields shared by explicit-list and native-stack summary queries. */
20
+ export const POLL_SUMMARY_FRAGMENT = gql("poll-summary-fragment.gql");
21
+ /** Discovers and summarizes every entry in one native GitHub pull-request stack. */
22
+ export const POLL_STACK_SUMMARY_QUERY = `${POLL_SUMMARY_FRAGMENT}\n${gql("poll-stack-summary.gql")}`;
19
23
  /** PR head fields plus a single review thread for `commit-suggestion`. */
20
24
  export const SUGGESTION_THREADS_QUERY = gql("suggestion-threads.gql");
21
25
  /** Fetches additional comments for a single review thread when its nested connection paginates. */
@@ -3,9 +3,10 @@ import { readFileSync } from "node:fs";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { z } from "zod";
5
5
  import { createPrShepherd, PartialApplyError, PrShepherdValidationError, } from "../api.mjs";
6
- import { isRepositoryQualifiedPrReference } from "../pr-reference.mjs";
6
+ import { isRepositoryQualifiedPrReference, normalizeRepositoryIdentity, parsePrReference, } from "../pr-reference.mjs";
7
7
  import { formatJournalResult } from "../cli/journal-formatter.mjs";
8
8
  import { formatCommitSuggestionResult, formatSuggestionPatchesResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, projectIterateLean, } from "../cli/formatters.mjs";
9
+ import { formatPollSummaryResult } from "../cli/poll-summary-formatter.mjs";
9
10
  import { formatCliError, serializeGitHubRequestErrorDetails } from "../cli/error-format.mjs";
10
11
  import { errorToExitCode, EXIT } from "../exit-codes.mjs";
11
12
  const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/repo#number reference";
@@ -14,8 +15,11 @@ const pr = z
14
15
  .refine(isRepositoryQualifiedPrReference, { message: QUALIFIED_PR_ERROR })
15
16
  .describe("GitHub pull-request URL or owner/repo#number; the explicit repository may differ from the server working directory");
16
17
  const ids = z.array(z.string().min(1)).optional();
17
- const iterateInputSchema = z.object({
18
- pr,
18
+ const iterateInputSchema = z
19
+ .object({
20
+ pr: pr.optional(),
21
+ prs: z.array(pr).min(1).optional(),
22
+ stack: pr.optional(),
19
23
  readyDelaySeconds: z.number().nonnegative().optional(),
20
24
  stallTimeoutSeconds: z.number().nonnegative().optional(),
21
25
  noAutoMarkReady: z.boolean().optional(),
@@ -28,7 +32,12 @@ const iterateInputSchema = z.object({
28
32
  .array(z.string())
29
33
  .optional()
30
34
  .describe("Deprecated per-call no-op retained for compatibility."),
31
- });
35
+ })
36
+ .refine((input) => [input.pr, input.prs, input.stack].filter((value) => value !== undefined).length === 1, {
37
+ message: "exactly one of pr, prs, or stack is required",
38
+ })
39
+ .refine((input) => !input.prs ||
40
+ new Set(input.prs.map((ref) => normalizeRepositoryIdentity(parsePrReference(ref)?.repository ?? ""))).size === 1, { message: "all prs must belong to the same repository" });
32
41
  const reviewMutationsOperationSchema = z.object({
33
42
  type: z.literal("review_mutations"),
34
43
  resolveThreadIds: ids,
@@ -80,7 +89,7 @@ export function createPrShepherdMcpServer(options = {}) {
80
89
  const shepherd = options.shepherd ?? createPrShepherd({ cwd: options.cwd });
81
90
  const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
82
91
  server.registerTool("iterate", {
83
- description: "Inspect the specified pull request and return the next Shepherd state.",
92
+ description: "Inspect one pull request, an explicit same-repository set, or a native stack and return one Shepherd tick.",
84
93
  inputSchema: iterateInputSchema,
85
94
  annotations: {
86
95
  readOnlyHint: false,
@@ -93,7 +102,9 @@ export function createPrShepherdMcpServer(options = {}) {
93
102
  const opts = {
94
103
  readyDelaySuffix: input.readyDelaySeconds === undefined ? undefined : `${input.readyDelaySeconds}s`,
95
104
  };
96
- return runTool(() => shepherd.iterate(requireRepositoryQualifiedPr(input)), (result) => formatIterateResult(result, opts), (result) => projectIterateLean(result, opts));
105
+ return runTool(() => runIterateSelector(shepherd, requireRepositoryQualifiedIterate(input)), (result) => isPollSummary(result)
106
+ ? formatPollSummaryResult(result)
107
+ : formatIterateResult(result, opts), (result) => isPollSummary(result) ? result : projectIterateLean(result, opts));
97
108
  });
98
109
  server.registerTool("apply", {
99
110
  description: "Apply ordered review, journal, and file-view operations after prevalidation; explicit requests rely on GitHub's mutation response.",
@@ -127,6 +138,28 @@ export function createPrShepherdMcpServer(options = {}) {
127
138
  }, async (input) => runTool(() => shepherd.buildSuggestionPatch(requireRepositoryQualifiedPr(input)), formatCommitSuggestionResult));
128
139
  return server;
129
140
  }
141
+ function runIterateSelector(shepherd, input) {
142
+ return "prs" in input || "stack" in input
143
+ ? shepherd.iterate(input)
144
+ : shepherd.iterate(input);
145
+ }
146
+ function requireRepositoryQualifiedIterate(input) {
147
+ if ([input.pr, input.prs, input.stack].filter((value) => value !== undefined).length !== 1) {
148
+ throw new PrShepherdValidationError("exactly one of pr, prs, or stack is required");
149
+ }
150
+ const refs = Array.isArray(input.prs) ? input.prs : [input.pr ?? input.stack];
151
+ if (refs.length === 0 || refs.some((ref) => !isRepositoryQualifiedPrReference(ref))) {
152
+ throw new PrShepherdValidationError(QUALIFIED_PR_ERROR);
153
+ }
154
+ const repositories = new Set(refs.map((ref) => normalizeRepositoryIdentity(parsePrReference(ref)?.repository ?? "")));
155
+ if (repositories.size !== 1) {
156
+ throw new PrShepherdValidationError("all prs must belong to the same repository");
157
+ }
158
+ return input;
159
+ }
160
+ function isPollSummary(result) {
161
+ return "mode" in result && result.mode === "summary";
162
+ }
130
163
  function requireRepositoryQualifiedPr(input) {
131
164
  if (!isRepositoryQualifiedPrReference(input.pr)) {
132
165
  throw new PrShepherdValidationError(QUALIFIED_PR_ERROR);
@@ -18,3 +18,5 @@ export declare function resolveParsedPrTarget(parsed: ParsedPrReference): Resolv
18
18
  /** Canonical collision-safe PR reference for generated commands and escalation messages. */
19
19
  export declare function formatPrUrl(repository: string, prNumber: number): string;
20
20
  export declare function isRepositoryQualifiedPrReference(pr: unknown): pr is string;
21
+ /** GitHub owner and repository names are case-insensitive. */
22
+ export declare function normalizeRepositoryIdentity(repository: string): string;
@@ -56,3 +56,7 @@ export function isRepositoryQualifiedPrReference(pr) {
56
56
  return false;
57
57
  return parsePrReference(pr)?.repository !== undefined;
58
58
  }
59
+ /** GitHub owner and repository names are case-insensitive. */
60
+ export function normalizeRepositoryIdentity(repository) {
61
+ return repository.toLowerCase();
62
+ }
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Persistent attempt counter for the iterate escalation guard.
3
3
  *
4
- * Tracks how many times each review thread has been dispatched to the fix_code
5
- * handler without being resolved. Counts are reset automatically when the HEAD
6
- * commit SHA changes (i.e. a new push landed).
4
+ * Tracks how many caller-visible times each review thread has been dispatched to
5
+ * the fix_code handler without being resolved. Body edits reset the count.
7
6
  *
8
7
  * State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
9
8
  */
10
9
  export interface FixAttemptsState {
11
- /** HEAD SHA at the time the counts were last written. Reset key. */
10
+ /** HEAD SHA at the time the counts were last written, retained for observability/compatibility. */
12
11
  headSha: string;
13
12
  /** Map from thread ID → number of fix_code dispatches that included this thread. */
14
13
  threadAttempts: Record<string, number>;
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Persistent attempt counter for the iterate escalation guard.
3
3
  *
4
- * Tracks how many times each review thread has been dispatched to the fix_code
5
- * handler without being resolved. Counts are reset automatically when the HEAD
6
- * commit SHA changes (i.e. a new push landed).
4
+ * Tracks how many caller-visible times each review thread has been dispatched to
5
+ * the fix_code handler without being resolved. Body edits reset the count.
7
6
  *
8
7
  * State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
9
8
  */
@@ -1,4 +1,5 @@
1
1
  import type { AgentCheck, AgentComment, AgentThread } from "./report.mts";
2
+ import type { ResolveCommand } from "./iterate.mts";
2
3
  import type { CheckStatus, Review } from "./github.mts";
3
4
  import type { MergeQueueRemovalStatus, StackStatus } from "./merge-requirements.mts";
4
5
  export type EscalateTrigger = "fix-thrash" | "base-branch-unknown" | "stall-timeout" | "check-follow-up-unavailable" | "authorization-required" | "bot-cr-not-dismissed" | "merge-queue-removed" | "stacked-pr";
@@ -19,6 +20,10 @@ export interface EscalateDetails {
19
20
  unresolvedThreads: AgentThread[];
20
21
  ambiguousComments: AgentComment[];
21
22
  changesRequestedReviews: Review[];
23
+ /** First-look review summaries that must be shown before any pending minimization. */
24
+ firstLookSummaries?: Review[];
25
+ /** Previously seen review summaries whose edited bodies must be shown again. */
26
+ editedSummaries?: Review[];
22
27
  /** Failing checks whose next step requires human attention. */
23
28
  checks?: AgentCheck[];
24
29
  stalledChecks?: AgentStalledCheck[];
@@ -26,6 +31,11 @@ export interface EscalateDetails {
26
31
  threadId: string;
27
32
  attempts: number;
28
33
  }>;
34
+ /** Review mutations generated for this tick, retained so an escalation cannot strand them. */
35
+ pendingReviewCommands?: {
36
+ resolveOnlyCommand?: ResolveCommand;
37
+ resolveCommand?: ResolveCommand;
38
+ };
29
39
  suggestion: string;
30
40
  humanMessage: string;
31
41
  mergeQueueRemoval?: MergeQueueRemovalStatus;
@@ -0,0 +1,82 @@
1
+ import type { ApiUsage, GraphqlQuotaWarning } from "./api-usage.mts";
2
+ import type { MergeableState, MergeStateStatus, ReviewDecision } from "./github.mts";
3
+ import type { ShepherdAction } from "./iterate.mts";
4
+ export interface PollSummaryChecks {
5
+ passing?: number;
6
+ failing?: number;
7
+ inProgress?: number;
8
+ skipped?: number;
9
+ filtered?: number;
10
+ ignored?: number;
11
+ superseded?: number;
12
+ incomplete?: true;
13
+ }
14
+ export interface PollSummaryReview {
15
+ comments?: number;
16
+ reviews?: number;
17
+ threads?: number;
18
+ actionable?: number;
19
+ incomplete?: true;
20
+ }
21
+ interface PollSummaryStack {
22
+ number: number;
23
+ size: number;
24
+ position: number;
25
+ baseRefName: string;
26
+ }
27
+ export interface PollSummaryItem {
28
+ pr: number;
29
+ repo: string;
30
+ title: string;
31
+ url: string;
32
+ /** Conservative routing hint; the selected one-PR poll makes the authoritative decision. */
33
+ action: ShepherdAction;
34
+ reasons: string[];
35
+ state: "OPEN" | "CLOSED" | "MERGED" | "UNKNOWN";
36
+ mergeable: MergeableState;
37
+ mergeStateStatus: MergeStateStatus;
38
+ reviewDecision?: ReviewDecision;
39
+ headRefName: string;
40
+ headRefOid: string;
41
+ baseRefName: string;
42
+ isDraft?: true;
43
+ isInMergeQueue?: true;
44
+ blockingReviewerInProgress?: true;
45
+ remainingSeconds?: number;
46
+ checks?: PollSummaryChecks;
47
+ review?: PollSummaryReview;
48
+ stack?: PollSummaryStack;
49
+ pollCommand?: string;
50
+ }
51
+ export type PollSummarySelection = {
52
+ kind: "prs";
53
+ requested: number[];
54
+ } | {
55
+ kind: "stack";
56
+ anchor: number;
57
+ stackNumber: number;
58
+ stackSize: number;
59
+ };
60
+ export interface PollSummaryResult {
61
+ mode: "summary";
62
+ repo: string;
63
+ selection: PollSummarySelection;
64
+ reason: "actionable" | "all_terminal" | "waiting" | "timeout";
65
+ prs: PollSummaryItem[];
66
+ apiUsage?: ApiUsage;
67
+ quotaWarning?: GraphqlQuotaWarning;
68
+ }
69
+ export interface PollSummaryCommandOptions {
70
+ prNumbers?: number[];
71
+ stackPrNumber?: number;
72
+ targetRepository?: {
73
+ owner: string;
74
+ name: string;
75
+ };
76
+ merge?: boolean;
77
+ readyDelaySeconds?: number;
78
+ stallTimeoutSeconds?: number;
79
+ noAutoMarkReady?: boolean;
80
+ noAutoCancelActionable?: boolean;
81
+ }
82
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/bin/types.d.mts CHANGED
@@ -13,3 +13,4 @@ export * from "./types/merge-action.mts";
13
13
  export * from "./types/escalate.mts";
14
14
  export * from "./types/merge-queue.mts";
15
15
  export * from "./types/api-usage.mts";
16
+ export * from "./types/poll-summary.mts";
package/bin/types.mjs CHANGED
@@ -13,3 +13,4 @@ export * from "./types/merge-action.mjs";
13
13
  export * from "./types/escalate.mjs";
14
14
  export * from "./types/merge-queue.mjs";
15
15
  export * from "./types/api-usage.mjs";
16
+ export * from "./types/poll-summary.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pr-shepherd": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "pr-shepherd@0.48.0", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.50.0", "pr-shepherd-mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pr-shepherd": {
3
3
  "command": "npx",
4
- "args": ["--yes", "--package", "pr-shepherd@0.48.0", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.50.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }
@@ -18,9 +18,9 @@ If the requested PR does not exist yet, review and commit the in-scope changes,
18
18
 
19
19
  ## Arguments: $ARGUMENTS
20
20
 
21
- 1. Parse an optional PR number, repository-qualified `owner/repo#N`, or GitHub PR URL and an optional `--merge` flag from `$ARGUMENTS`; otherwise let pr-shepherd infer the current branch PR. Reject any remaining argument. Follow the target repository's local `AGENTS.md` and `CLAUDE.md` standards while making changes.
21
+ 1. Parse optional PR numbers, repository-qualified `owner/repo#N` references, or GitHub PR URLs and an optional `--merge` flag from `$ARGUMENTS`; alternatively parse one `--stack PR` selector. Otherwise let pr-shepherd infer the current branch PR. Reject any remaining argument. Follow the target repository's local `AGENTS.md` and `CLAUDE.md` standards while making changes.
22
22
 
23
- 2. For the CLI, convert supplied `owner/repo#N` to `https://github.com/owner/repo/pull/N`; otherwise pass the supplied URL or bare number unchanged, then run the canonical poll command `pr-shepherd [PR] --until-terminal`, omitting `[PR]` when none was supplied and appending `--merge` when requested. This command keeps ordinary `[WAIT]` and `[MARK_READY]` ticks inside the same invocation; it returns for agent-facing work (including an emitted `[MERGE]` command, which is non-terminal and must run before the next invocation), a quota warning, `[CANCEL]`, or `[ESCALATE]`. A qualified reference may name a fork or upstream repository: it is the GitHub target, while the current checkout continues to supply local git/config/rules context. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, first obtain a repository-qualified reference: use a supplied GitHub PR URL or `owner/repo#N` unchanged; for a bare number, run `gh pr view <number> --json url --jq .url`; when omitted, run `gh pr view --json url --jq .url`. If that does not produce one qualified PR reference, stop and report that MCP cannot safely determine the PR. Otherwise call `iterate` with that qualified reference, plus `merge: true` when `--merge` was supplied, and print its full result.
23
+ 2. For the CLI, convert supplied `owner/repo#N` references to `https://github.com/owner/repo/pull/N`; otherwise pass supplied URLs or bare numbers unchanged, then run `pr-shepherd [PR ...] --until-terminal`, or `pr-shepherd --stack PR --until-terminal` for a stack, omitting `[PR ...]` when none was supplied and appending `--merge` when requested. This command keeps ordinary `[WAIT]` and `[MARK_READY]` ticks inside the same invocation; aggregate selectors return when any row needs work or all rows are terminal. It also returns for a quota warning or an emitted `[MERGE]` command, which is non-terminal and must run before the next invocation. A qualified reference may name a fork or upstream repository: it is the GitHub target, while the current checkout continues to supply local git/config/rules context. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, first repository-qualify every supplied reference with its GitHub URL or `owner/repo#N`; resolve bare numbers through `gh pr view <number> --json url --jq .url`, and resolve an omitted target with `gh pr view --json url --jq .url`. If that does not produce the required qualified selector, stop and report that MCP cannot safely determine it. Otherwise call `iterate` with `pr`, `prs`, or `stack` as selected, plus `merge: true` when `--merge` was supplied, and print its full result.
24
24
 
25
25
  3. Print the full result and follow every returned `## Instructions` step exactly. For CLI output, run each printed mutation command when instructed. For MCP output, use MCP `apply` and `build_suggestion_patches` with the same qualified PR reference; do not run a shell `pr-shepherd apply` command.
26
26
 
@@ -51,7 +51,7 @@ annotations, or CI log excerpts.
51
51
  - The command builds from the fetched PR head and accepts a clean local descendant only when the complete ordered patch stream passes `git apply --check`.
52
52
  - If the command refuses because a suggestion is unsafe or no longer applies, inspect the current source, the displayed replacement block, and reviewer intent before editing manually. Do not apply a stale numeric range blindly or retry unchanged input.
53
53
  - A returned patch was checked against the then-current worktree. If it later fails, re-inspect the worktree because it changed after validation.
54
- - Keep the generated thread IDs and flag placement unchanged. Viewer-authored human feedback may intentionally appear in both reply and resolve flags; unmarked other-human feedback remains reply-only. Marker-ended other-human feedback is already acknowledged and has no generated mutation.
54
+ - Use the generated thread IDs and flag placement returned with the patch command.
55
55
 
56
56
  ### CI failure triage
57
57
 
@@ -77,16 +77,11 @@ When several bullets share one runId (matrix jobs from the same run), the `rerun
77
77
 
78
78
  Applies to every `apply review:` / `resolve-only:` command the CLI prints. Covers only what stays safe if you run the printed command **unmodified** — `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution remains a separate CLI-printed step because the command is unsafe by default without those placeholders.
79
79
 
80
- The CLI only includes IDs whose per-object GitHub viewer capability and semantic routing authorize the corresponding generated action. Direct `apply review` honors those emitted IDs without a second authorization preflight and surfaces GitHub's per-operation result. Do not reconstruct omitted review reply, thread resolution, or bot-review dismissal IDs and do not hand them off: denied or unverifiable generated mutations are one-look skips that Shepherd suppresses until the item is edited. Location is not required for generated reply/resolve mutations; unauthorized threads without a path or line remain one-look skips.
80
+ The CLI only includes IDs whose per-object GitHub viewer capability and semantic routing authorize the corresponding generated action. Generated commands are pre-populated; omission is not a prohibition. A separate, user-directed `apply review` request may supply any reply, resolve, minimize, or dismiss IDs; it forwards them without Shepherd author, capability, or current-state filtering, and GitHub's per-operation response is authoritative.
81
81
 
82
- - Run every generated `apply review:` / `resolve-only:` command even when no code change is warranted. The command records the agent's disposition of the included review items; skipping it leaves authorized threads active and can eventually trigger `fix-thrash`.
83
- - Never add first-look-only or check-annotation IDs to `--reply-thread-ids`, `--resolve-thread-ids`, `--dismiss-review-ids`, or `--minimize-comment-ids` — those flags are pre-populated by the CLI.
82
+ - When `## Instructions` says to run a generated `apply review:` / `resolve-only:` command, run it even when no code change is warranted. An `[ESCALATE]` instruction may require user direction first. The command records the agent's disposition of the included review items; skipping it leaves authorized threads active and can eventually trigger `fix-thrash`.
84
83
  - Keep every existing `--dismiss-review-ids` ID the CLI already included. Each is a bot or non-human review that must be dismissed; omitting one leaves the PR in `CHANGES_REQUESTED`.
85
84
 
86
- ### Review-mutation routing
87
-
88
- For threads under both `## Review threads` and `## Review threads to resolve`, evaluate every thread before running mutations. Keep unmarked bot/non-human and viewer-authored IDs in both `--reply-thread-ids` and `--resolve-thread-ids`, including when the feedback is advisory, already satisfied, or otherwise warrants no code change: the reply runs before the resolve. Unmarked other-human IDs use `--reply-thread-ids` only unless the CLI also put them in `--resolve-thread-ids`. When the latest comment begins `<!-- pr-shepherd -->`, it is an established Shepherd reply—not merely a same-account comment. A marked thread that is still being resolved is resolve-only for retry. Do not add IDs the CLI omitted, and do not move IDs between flags.
89
-
90
85
  ### Shepherd Journal
91
86
 
92
87
  Link threads and comments in a journal entry from their headings in the CLI output. Cite reviews by ID.