pr-shepherd 0.53.1 → 0.54.1

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 (88) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/bin/cli/args.mjs +3 -0
  4. package/bin/cli/check-blocker-handler.d.mts +2 -0
  5. package/bin/cli/check-blocker-handler.mjs +82 -0
  6. package/bin/cli/help-command-pages.d.mts +22 -3
  7. package/bin/cli/help-command-pages.mjs +21 -2
  8. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  9. package/bin/cli/help-iterate-poll-pages.mjs +1 -1
  10. package/bin/cli/help-top-page.d.mts +1 -1
  11. package/bin/cli/help-top-page.mjs +3 -1
  12. package/bin/cli/help.d.mts +23 -4
  13. package/bin/cli/help.mjs +2 -0
  14. package/bin/cli/iterate-branch-segment.d.mts +3 -0
  15. package/bin/cli/iterate-branch-segment.mjs +13 -0
  16. package/bin/cli/iterate-formatter.mjs +5 -13
  17. package/bin/cli/iterate-lean.mjs +1 -0
  18. package/bin/cli/poll-handler.mjs +5 -1
  19. package/bin/cli-parser.mjs +4 -0
  20. package/bin/commands/apply-check-blocker.d.mts +22 -0
  21. package/bin/commands/apply-check-blocker.mjs +36 -0
  22. package/bin/commands/check-annotations.d.mts +1 -1
  23. package/bin/commands/check-annotations.mjs +27 -17
  24. package/bin/commands/check-blocker-ref.d.mts +3 -0
  25. package/bin/commands/check-blocker-ref.mjs +50 -0
  26. package/bin/commands/check.mjs +1 -0
  27. package/bin/commands/iterate/check-blocker-gate.d.mts +18 -0
  28. package/bin/commands/iterate/check-blocker-gate.mjs +115 -0
  29. package/bin/commands/iterate/check-instructions.d.mts +2 -0
  30. package/bin/commands/iterate/check-instructions.mjs +4 -0
  31. package/bin/commands/iterate/conflicting-head-ci.d.mts +18 -0
  32. package/bin/commands/iterate/conflicting-head-ci.mjs +32 -0
  33. package/bin/commands/iterate/fix-code.d.mts +2 -0
  34. package/bin/commands/iterate/fix-code.mjs +61 -16
  35. package/bin/commands/iterate/index.mjs +22 -8
  36. package/bin/commands/iterate/native-stack-rebase.d.mts +8 -2
  37. package/bin/commands/iterate/native-stack-rebase.mjs +14 -4
  38. package/bin/commands/iterate/stack-trunk-conflict.d.mts +13 -0
  39. package/bin/commands/iterate/stack-trunk-conflict.mjs +51 -0
  40. package/bin/commands/poll-quota.d.mts +2 -0
  41. package/bin/commands/poll-quota.mjs +25 -12
  42. package/bin/commands/poll-rate-limit-cancel.d.mts +35 -0
  43. package/bin/commands/poll-rate-limit-cancel.mjs +101 -0
  44. package/bin/commands/poll-rate-limit-delay.d.mts +1 -0
  45. package/bin/commands/poll-rate-limit-delay.mjs +7 -0
  46. package/bin/commands/poll-rate-limit-wait.d.mts +14 -0
  47. package/bin/commands/poll-rate-limit-wait.mjs +125 -0
  48. package/bin/commands/poll-summary.mjs +21 -9
  49. package/bin/commands/poll.mjs +22 -17
  50. package/bin/config/load.d.mts +2 -0
  51. package/bin/config/load.mjs +24 -2
  52. package/bin/config.json +1 -0
  53. package/bin/github/batch-parse-suites.d.mts +2 -0
  54. package/bin/github/batch-parse-suites.mjs +5 -0
  55. package/bin/github/batch.d.mts +2 -0
  56. package/bin/github/batch.mjs +2 -1
  57. package/bin/github/check-annotation-cache.d.mts +8 -0
  58. package/bin/github/check-annotation-cache.mjs +23 -0
  59. package/bin/github/check-annotation-pages.d.mts +11 -0
  60. package/bin/github/check-annotation-pages.mjs +36 -0
  61. package/bin/github/check-annotation-shape.d.mts +21 -0
  62. package/bin/github/check-annotation-shape.mjs +50 -0
  63. package/bin/github/check-annotations-batch.d.mts +17 -0
  64. package/bin/github/check-annotations-batch.mjs +92 -0
  65. package/bin/github/check-annotations.d.mts +8 -11
  66. package/bin/github/check-annotations.mjs +14 -103
  67. package/bin/github/gql/batch-pr.gql +1 -1
  68. package/bin/github/gql/check-run-annotations-batch.gql +41 -0
  69. package/bin/github/gql/upper-layer-conflict-target.gql +36 -0
  70. package/bin/github/pagination.d.mts +1 -1
  71. package/bin/github/pagination.mjs +1 -1
  72. package/bin/github/poll-summary-check-blockers.d.mts +16 -0
  73. package/bin/github/poll-summary-check-blockers.mjs +25 -0
  74. package/bin/github/poll-summary-checks.d.mts +2 -0
  75. package/bin/github/poll-summary-checks.mjs +35 -20
  76. package/bin/github/poll-summary-projector.mjs +5 -0
  77. package/bin/github/queries.d.mts +11 -0
  78. package/bin/github/queries.mjs +11 -0
  79. package/bin/state/check-blockers.d.mts +30 -0
  80. package/bin/state/check-blockers.mjs +88 -0
  81. package/bin/state/conflicting-head-seen.d.mts +13 -0
  82. package/bin/state/conflicting-head-seen.mjs +44 -0
  83. package/bin/types/iterate.d.mts +5 -0
  84. package/bin/types/report.d.mts +2 -0
  85. package/package.json +1 -1
  86. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  87. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  88. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -9,6 +9,7 @@ import { summarizePollSummaryReview } from "./poll-summary-review.mjs";
9
9
  import { fingerprintRawSummaryPr } from "./poll-summary-fingerprint.mjs";
10
10
  import { currentQueueRemovalEvent } from "./poll-summary-queue-removal.mjs";
11
11
  import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
12
+ import { applyOpenCheckBlockers } from "./poll-summary-check-blockers.mjs";
12
13
  import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
13
14
  export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false) {
14
15
  const repoName = `${repo.owner}/${repo.name}`;
@@ -69,6 +70,9 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
69
70
  : removalEvent
70
71
  ? projectQueueRemoval(removalEvent)
71
72
  : undefined;
73
+ const blocked = await applyOpenCheckBlockers(raw, repo, { action, reasons }, checks, review, opts.stackPrNumber !== undefined);
74
+ action = blocked.action;
75
+ reasons = blocked.reasons;
72
76
  return {
73
77
  pr: raw.number,
74
78
  repo: repoName,
@@ -97,6 +101,7 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
97
101
  !(opts.stackPrNumber !== undefined && raw.stack && action === "merge"))
98
102
  ? pollCommandFields(repoName, raw.number, raw.isDraft, opts)
99
103
  : {}),
104
+ ...(blocked.pollProbe ? { pollProbe: true } : {}),
100
105
  };
101
106
  }
102
107
  function projectQueueRemoval(removal) {
@@ -23,12 +23,23 @@ export declare const POLL_SUMMARY_CHECK_PAGE_QUERY: string;
23
23
  export declare const POLL_STACK_SUMMARY_QUERY: string;
24
24
  /** Reads only native-stack membership and the refs that link adjacent entries. */
25
25
  export declare const POLL_STACK_TOPOLOGY_QUERY: string;
26
+ /**
27
+ * Whether a conflicting upper stack layer already contains its PR base, plus
28
+ * the open stack entries needed to name the bottom layer. Not part of BatchPr.
29
+ */
30
+ export declare const UPPER_LAYER_CONFLICT_TARGET_QUERY: string;
26
31
  /** PR head fields plus a single review thread for `commit-suggestion`. */
27
32
  export declare const SUGGESTION_THREADS_QUERY: string;
28
33
  /** Fetches additional comments for a single review thread when its nested connection paginates. */
29
34
  export declare const REVIEW_THREAD_COMMENTS_QUERY: string;
30
35
  /** Fetch inline annotations for a single CheckRun by node ID. */
31
36
  export declare const CHECK_RUN_ANNOTATIONS_QUERY: string;
37
+ /**
38
+ * First annotation page for many CheckRuns. Chunked at 20 ids so the nested
39
+ * `annotations` connections stay a 1-point query. Further pages use
40
+ * `CHECK_RUN_ANNOTATIONS_QUERY`.
41
+ */
42
+ export declare const CHECK_RUN_ANNOTATIONS_BATCH_QUERY: string;
32
43
  /** Fetches additional status contexts for a merge queue synthetic commit. */
33
44
  export declare const COMMIT_CHECK_CONTEXTS_QUERY: string;
34
45
  /** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
@@ -28,12 +28,23 @@ export const POLL_SUMMARY_CHECK_PAGE_QUERY = `${POLL_SUMMARY_CHECK_CONTEXTS_FRAG
28
28
  export const POLL_STACK_SUMMARY_QUERY = `${POLL_SUMMARY_FRAGMENT}\n${gql("poll-stack-summary.gql")}`;
29
29
  /** Reads only native-stack membership and the refs that link adjacent entries. */
30
30
  export const POLL_STACK_TOPOLOGY_QUERY = gql("poll-stack-topology.gql");
31
+ /**
32
+ * Whether a conflicting upper stack layer already contains its PR base, plus
33
+ * the open stack entries needed to name the bottom layer. Not part of BatchPr.
34
+ */
35
+ export const UPPER_LAYER_CONFLICT_TARGET_QUERY = gql("upper-layer-conflict-target.gql");
31
36
  /** PR head fields plus a single review thread for `commit-suggestion`. */
32
37
  export const SUGGESTION_THREADS_QUERY = gql("suggestion-threads.gql");
33
38
  /** Fetches additional comments for a single review thread when its nested connection paginates. */
34
39
  export const REVIEW_THREAD_COMMENTS_QUERY = gql("review-thread-comments.gql");
35
40
  /** Fetch inline annotations for a single CheckRun by node ID. */
36
41
  export const CHECK_RUN_ANNOTATIONS_QUERY = gql("check-run-annotations.gql");
42
+ /**
43
+ * First annotation page for many CheckRuns. Chunked at 20 ids so the nested
44
+ * `annotations` connections stay a 1-point query. Further pages use
45
+ * `CHECK_RUN_ANNOTATIONS_QUERY`.
46
+ */
47
+ export const CHECK_RUN_ANNOTATIONS_BATCH_QUERY = gql("check-run-annotations-batch.gql");
37
48
  /** Fetches additional status contexts for a merge queue synthetic commit. */
38
49
  export const COMMIT_CHECK_CONTEXTS_QUERY = gql("commit-check-contexts.gql");
39
50
  /** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Per-PR records that a failing check is blocked on an external PR or issue.
3
+ *
4
+ * `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/check-blockers.json`
5
+ */
6
+ export interface CheckBlockerRef {
7
+ owner: string;
8
+ name: string;
9
+ number: number;
10
+ kind: "pull" | "issue";
11
+ }
12
+ export interface CheckBlockerRecord {
13
+ checkName: string;
14
+ blocker: CheckBlockerRef;
15
+ /** Unix seconds. */
16
+ recordedAt: number;
17
+ }
18
+ interface StateKey {
19
+ owner: string;
20
+ repo: string;
21
+ pr: number;
22
+ }
23
+ export declare function formatCheckBlockerRef(blocker: CheckBlockerRef): string;
24
+ /** Missing, unreadable, or malformed state is an empty list. Does not throw. */
25
+ export declare function readCheckBlockers(key: StateKey): Promise<CheckBlockerRecord[]>;
26
+ /** Insert or replace the record for `checkName`. Returns false when the file cannot be written. */
27
+ export declare function writeCheckBlocker(key: StateKey, record: CheckBlockerRecord): Promise<boolean>;
28
+ /** Drop one check's record. A missing record is success and does not create a file. */
29
+ export declare function clearCheckBlocker(key: StateKey, checkName: string): Promise<boolean>;
30
+ export {};
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Per-PR records that a failing check is blocked on an external PR or issue.
3
+ *
4
+ * `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/check-blockers.json`
5
+ */
6
+ import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
7
+ import { randomUUID } from "node:crypto";
8
+ import { dirname } from "node:path";
9
+ import { resolvePrStatePath } from "./base.mjs";
10
+ const FILE = "check-blockers.json";
11
+ export function formatCheckBlockerRef(blocker) {
12
+ return `${blocker.owner}/${blocker.name}#${blocker.number}`;
13
+ }
14
+ /** Missing, unreadable, or malformed state is an empty list. Does not throw. */
15
+ export async function readCheckBlockers(key) {
16
+ try {
17
+ const parsed = JSON.parse(await readFile(resolvePrStatePath(key, FILE), "utf8"));
18
+ if (parsed === null || typeof parsed !== "object")
19
+ return [];
20
+ const blockers = parsed.blockers;
21
+ if (!Array.isArray(blockers))
22
+ return [];
23
+ return blockers.filter(isRecord);
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ }
29
+ /** Insert or replace the record for `checkName`. Returns false when the file cannot be written. */
30
+ export async function writeCheckBlocker(key, record) {
31
+ const blockers = (await readCheckBlockers(key)).filter((item) => item.checkName !== record.checkName);
32
+ blockers.push(record);
33
+ return writeAll(key, blockers);
34
+ }
35
+ /** Drop one check's record. A missing record is success and does not create a file. */
36
+ export async function clearCheckBlocker(key, checkName) {
37
+ const current = await readCheckBlockers(key);
38
+ const blockers = current.filter((item) => item.checkName !== checkName);
39
+ if (blockers.length === current.length)
40
+ return true;
41
+ return writeAll(key, blockers);
42
+ }
43
+ async function writeAll(key, blockers) {
44
+ let tmp;
45
+ try {
46
+ const path = resolvePrStatePath(key, FILE);
47
+ tmp = `${path}.${randomUUID()}.tmp`;
48
+ await mkdir(dirname(path), { recursive: true });
49
+ await writeFile(tmp, `${JSON.stringify({ blockers })}\n`, "utf8");
50
+ await rename(tmp, path);
51
+ tmp = undefined;
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ finally {
58
+ if (tmp !== undefined) {
59
+ try {
60
+ await unlink(tmp);
61
+ }
62
+ catch {
63
+ // The failed write may not have created the temp file.
64
+ }
65
+ }
66
+ }
67
+ }
68
+ function isRecord(value) {
69
+ if (value === null || typeof value !== "object")
70
+ return false;
71
+ const record = value;
72
+ return (typeof record.checkName === "string" &&
73
+ record.checkName.length > 0 &&
74
+ typeof record.recordedAt === "number" &&
75
+ Number.isFinite(record.recordedAt) &&
76
+ isBlocker(record.blocker));
77
+ }
78
+ function isBlocker(value) {
79
+ if (value === null || typeof value !== "object")
80
+ return false;
81
+ const blocker = value;
82
+ return ((blocker.kind === "pull" || blocker.kind === "issue") &&
83
+ typeof blocker.owner === "string" &&
84
+ typeof blocker.name === "string" &&
85
+ typeof blocker.number === "number" &&
86
+ Number.isInteger(blocker.number) &&
87
+ blocker.number > 0);
88
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * When Shepherd first saw a conflicting head that had no CI.
3
+ *
4
+ * `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/conflicting-head.json`
5
+ * GitHub's Commit object has no push timestamp, so the missing-CI grace
6
+ * starts at this observation instead of `committedDate`.
7
+ */
8
+ /** Unix seconds of the first observation of `headSha`, or undefined when state cannot be used. */
9
+ export declare function conflictingHeadFirstSeenUnix(key: {
10
+ owner: string;
11
+ repo: string;
12
+ pr: number;
13
+ }, headSha: string, nowMs: number): Promise<number | undefined>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * When Shepherd first saw a conflicting head that had no CI.
3
+ *
4
+ * `$PR_SHEPHERD_STATE_DIR/<owner>/<repo>/<pr>/conflicting-head.json`
5
+ * GitHub's Commit object has no push timestamp, so the missing-CI grace
6
+ * starts at this observation instead of `committedDate`.
7
+ */
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { dirname } from "node:path";
10
+ import { resolvePrStatePath } from "./base.mjs";
11
+ /** Unix seconds of the first observation of `headSha`, or undefined when state cannot be used. */
12
+ export async function conflictingHeadFirstSeenUnix(key, headSha, nowMs) {
13
+ const nowUnix = Math.floor(nowMs / 1000);
14
+ try {
15
+ const path = resolvePrStatePath(key, "conflicting-head.json");
16
+ const existing = await readSeen(path);
17
+ if (existing?.headSha === headSha)
18
+ return existing.firstSeenAtUnix;
19
+ await mkdir(dirname(path), { recursive: true });
20
+ const next = { headSha, firstSeenAtUnix: nowUnix };
21
+ await writeFile(path, JSON.stringify(next));
22
+ return nowUnix;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ async function readSeen(path) {
29
+ try {
30
+ const parsed = JSON.parse(await readFile(path, "utf8"));
31
+ if (parsed === null || typeof parsed !== "object")
32
+ return undefined;
33
+ const headSha = parsed.headSha;
34
+ const firstSeenAtUnix = parsed.firstSeenAtUnix;
35
+ if (typeof headSha !== "string" || typeof firstSeenAtUnix !== "number")
36
+ return undefined;
37
+ if (!Number.isFinite(firstSeenAtUnix))
38
+ return undefined;
39
+ return { headSha, firstSeenAtUnix };
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
@@ -29,6 +29,11 @@ export interface IterateResultBase {
29
29
  summary: IterateResultSummary;
30
30
  /** Validated base branch (e.g. "main") for this PR. */
31
31
  baseBranch: string;
32
+ /**
33
+ * Set when a native-stack upper layer is conflicting but already contains its
34
+ * PR base, so the dirty state is against the stack trunk rather than that base.
35
+ */
36
+ stackTrunkConflict?: string;
32
37
  /** Null when no classic protection rule exists or the base ref is unavailable. */
33
38
  branchProtection: BranchProtection | null;
34
39
  mergeRequirements?: MergeRequirements;
@@ -33,6 +33,8 @@ export interface ShepherdReport {
33
33
  baseBranch: string;
34
34
  /** Base commit GitHub recorded for this PR (`PullRequest.baseRefOid`), not the branch's live tip. */
35
35
  baseRefOid?: string;
36
+ /** True when the head commit's check-suite page is complete and has no suites. */
37
+ headCheckSuitesEmpty?: true;
36
38
  mergeStatus: MergeStatusResult;
37
39
  checks: {
38
40
  passing: ClassifiedCheck[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.53.1",
3
+ "version": "0.54.1",
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.53.1",
3
+ "version": "0.54.1",
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.53.1", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.54.1", "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.53.1", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.54.1", "pr-shepherd-mcp"]
5
5
  }
6
6
  }