pr-shepherd 0.35.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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +32 -12
  3. package/bin/checks/triage.mjs +11 -3
  4. package/bin/cli/args.mjs +1 -0
  5. package/bin/cli/default-poll.mjs +1 -0
  6. package/bin/cli/help-command-pages.d.mts +1 -1
  7. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  8. package/bin/cli/help-iterate-poll-pages.mjs +10 -5
  9. package/bin/cli/help-top-page.d.mts +1 -1
  10. package/bin/cli/help-top-page.mjs +2 -1
  11. package/bin/cli/help.d.mts +2 -2
  12. package/bin/cli/iterate-formatter.mjs +11 -2
  13. package/bin/cli/iterate-lean.mjs +1 -0
  14. package/bin/cli/poll-handler.mjs +7 -0
  15. package/bin/commands/check.d.mts +1 -0
  16. package/bin/commands/check.mjs +26 -22
  17. package/bin/commands/commit-suggestion.mjs +11 -5
  18. package/bin/commands/iterate/fix-code.mjs +1 -1
  19. package/bin/commands/iterate/helpers.d.mts +1 -0
  20. package/bin/commands/iterate/helpers.mjs +15 -6
  21. package/bin/commands/iterate/index.mjs +3 -10
  22. package/bin/commands/poll.d.mts +2 -0
  23. package/bin/commands/poll.mjs +51 -17
  24. package/bin/commands/ready-delay.mjs +3 -13
  25. package/bin/config/load.mjs +67 -25
  26. package/bin/github/batch-page-helpers.d.mts +45 -0
  27. package/bin/github/batch-page-helpers.mjs +63 -0
  28. package/bin/github/batch-page.d.mts +14 -0
  29. package/bin/github/batch-page.mjs +62 -0
  30. package/bin/github/batch-parse-suites.d.mts +4 -0
  31. package/bin/github/batch-parse-suites.mjs +25 -0
  32. package/bin/github/batch-parsers-rules.d.mts +6 -0
  33. package/bin/github/batch-parsers-rules.mjs +122 -0
  34. package/bin/github/batch-parsers.mjs +6 -0
  35. package/bin/github/batch-raw-rules.d.mts +59 -0
  36. package/bin/github/batch-raw-rules.mjs +1 -0
  37. package/bin/github/batch-raw-types.d.mts +20 -11
  38. package/bin/github/batch.d.mts +2 -0
  39. package/bin/github/batch.mjs +14 -120
  40. package/bin/github/gql/batch-pr-page.gql +189 -0
  41. package/bin/github/gql/batch-pr.gql +74 -21
  42. package/bin/github/gql/commit-suggestion-thread.gql +40 -0
  43. package/bin/github/http.d.mts +2 -1
  44. package/bin/github/http.mjs +2 -1
  45. package/bin/github/pagination.d.mts +3 -2
  46. package/bin/github/pagination.mjs +3 -2
  47. package/bin/github/queries.d.mts +4 -0
  48. package/bin/github/queries.mjs +4 -0
  49. package/bin/github/rest-http.d.mts +6 -1
  50. package/bin/github/rest-http.mjs +25 -86
  51. package/bin/github/rest-text.d.mts +1 -0
  52. package/bin/github/rest-text.mjs +88 -0
  53. package/bin/github/suggestion-thread.d.mts +9 -0
  54. package/bin/github/suggestion-thread.mjs +45 -0
  55. package/bin/github/thread-comments.mjs +12 -8
  56. package/bin/merge-status/derive.mjs +2 -0
  57. package/bin/merge-status/requirements-format.d.mts +3 -0
  58. package/bin/merge-status/requirements-format.mjs +88 -0
  59. package/bin/merge-status/requirements.d.mts +2 -0
  60. package/bin/merge-status/requirements.mjs +51 -0
  61. package/bin/state/base.d.mts +9 -0
  62. package/bin/state/base.mjs +23 -0
  63. package/bin/state/bot-cr-seen.mjs +3 -13
  64. package/bin/state/fix-attempts.mjs +3 -13
  65. package/bin/state/iterate-stall.mjs +3 -13
  66. package/bin/state/seen-comments.mjs +6 -13
  67. package/bin/types/github.d.mts +4 -7
  68. package/bin/types/iterate.d.mts +6 -7
  69. package/bin/types/merge-requirements.d.mts +82 -0
  70. package/bin/types/merge-requirements.mjs +2 -0
  71. package/bin/types.d.mts +1 -0
  72. package/bin/types.mjs +1 -0
  73. package/bin/util/path-segment.d.mts +2 -0
  74. package/bin/util/path-segment.mjs +2 -0
  75. package/bin/util/pool.d.mts +2 -0
  76. package/bin/util/pool.mjs +18 -0
  77. package/package.json +1 -1
  78. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  79. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  80. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -9,9 +9,8 @@
9
9
  */
10
10
  import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
11
11
  import { randomUUID } from "node:crypto";
12
- import { join, dirname } from "node:path";
13
- import { SAFE_SEGMENT } from "../util/path-segment.mjs";
14
- import { resolveStateBase } from "./base.mjs";
12
+ import { dirname } from "node:path";
13
+ import { resolvePrStatePath } from "./base.mjs";
15
14
  // ---------------------------------------------------------------------------
16
15
  // Public API
17
16
  // ---------------------------------------------------------------------------
@@ -54,14 +53,5 @@ export async function writeFixAttempts(key, state) {
54
53
  // Helpers
55
54
  // ---------------------------------------------------------------------------
56
55
  function resolvePath(key) {
57
- for (const [field, value] of [
58
- ["owner", key.owner],
59
- ["repo", key.repo],
60
- ]) {
61
- if (!SAFE_SEGMENT.test(value)) {
62
- throw new Error(`Invalid state key segment "${field}": ${value}`);
63
- }
64
- }
65
- const base = resolveStateBase();
66
- return join(base, `${key.owner}-${key.repo}`, String(key.pr), "fix-attempts.json");
56
+ return resolvePrStatePath(key, "fix-attempts.json");
67
57
  }
@@ -9,9 +9,8 @@
9
9
  */
10
10
  import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
11
11
  import { randomUUID } from "node:crypto";
12
- import { join, dirname } from "node:path";
13
- import { SAFE_SEGMENT } from "../util/path-segment.mjs";
14
- import { resolveStateBase } from "./base.mjs";
12
+ import { dirname } from "node:path";
13
+ import { resolvePrStatePath } from "./base.mjs";
15
14
  // ---------------------------------------------------------------------------
16
15
  // Public API
17
16
  // ---------------------------------------------------------------------------
@@ -70,14 +69,5 @@ export async function writeStallState(key, state) {
70
69
  // Helpers
71
70
  // ---------------------------------------------------------------------------
72
71
  function resolvePath(key) {
73
- for (const [field, value] of [
74
- ["owner", key.owner],
75
- ["repo", key.repo],
76
- ]) {
77
- if (!SAFE_SEGMENT.test(value)) {
78
- throw new Error(`Invalid state key segment "${field}": ${value}`);
79
- }
80
- }
81
- const base = resolveStateBase();
82
- return join(base, `${key.owner}-${key.repo}`, String(key.pr), "iterate-stall.json");
72
+ return resolvePrStatePath(key, "iterate-stall.json");
83
73
  }
@@ -3,7 +3,7 @@ import { readFile, writeFile, rename, unlink, mkdir, access, readdir } from "nod
3
3
  import { join, dirname } from "node:path";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { SAFE_SEGMENT } from "../util/path-segment.mjs";
6
- import { resolveStateBase } from "./base.mjs";
6
+ import { resolvePrStatePath } from "./base.mjs";
7
7
  // ---------------------------------------------------------------------------
8
8
  // Public API
9
9
  // ---------------------------------------------------------------------------
@@ -57,7 +57,9 @@ export async function loadSeenMap(key) {
57
57
  try {
58
58
  const dir = resolveDir(key);
59
59
  const entries = await readdir(dir);
60
- for (const entry of entries.filter((e) => e.endsWith(".json"))) {
60
+ for (const entry of entries) {
61
+ if (!entry.endsWith(".json") || !SAFE_SEGMENT.test(entry))
62
+ continue;
61
63
  try {
62
64
  const raw = await readFile(join(dir, entry), "utf8");
63
65
  const marker = JSON.parse(raw);
@@ -182,16 +184,7 @@ export async function readSeenMarker(key, id) {
182
184
  // Helpers
183
185
  // ---------------------------------------------------------------------------
184
186
  function resolveDir(key) {
185
- for (const [field, value] of [
186
- ["owner", key.owner],
187
- ["repo", key.repo],
188
- ]) {
189
- if (!SAFE_SEGMENT.test(value)) {
190
- throw new Error(`Invalid state key segment "${field}": ${value}`);
191
- }
192
- }
193
- const base = resolveStateBase();
194
- return join(base, `${key.owner}-${key.repo}`, String(key.pr), "seen");
187
+ return resolvePrStatePath(key, "seen");
195
188
  }
196
189
  function resolvePath(key, id) {
197
190
  if (!SAFE_SEGMENT.test(id)) {
@@ -203,5 +196,5 @@ function resolvePath(key, id) {
203
196
  // causing seen-markers to overwrite each other and items to re-surface every
204
197
  // tick. SHA-256 is case-sensitive so distinct IDs get distinct files.
205
198
  const hash = createHash("sha256").update(id, "utf8").digest("hex");
206
- return join(resolveDir(key), `${hash}.json`);
199
+ return resolvePrStatePath(key, "seen", `${hash}.json`);
207
200
  }
@@ -1,5 +1,6 @@
1
1
  import type { ReviewThreadComment } from "./review-thread.mts";
2
2
  import type { PrActivitySummary } from "./activity.mts";
3
+ import type { BatchPrMergeFields, MergeRequirements } from "./merge-requirements.mts";
3
4
  export type CheckConclusion = "ACTION_REQUIRED" | "CANCELLED" | "FAILURE" | "NEUTRAL" | "SKIPPED" | "STALE" | "STARTUP_FAILURE" | "SUCCESS" | "TIMED_OUT" | null;
4
5
  export type CheckStatus = "COMPLETED" | "IN_PROGRESS" | "PENDING" | "QUEUED" | "REQUESTED" | "WAITING";
5
6
  export type MergeableState = "CONFLICTING" | "MERGEABLE" | "UNKNOWN";
@@ -49,12 +50,7 @@ export interface ReviewThread {
49
50
  /** True when Shepherd re-surfaced this thread because its body changed after first look. */
50
51
  edited?: boolean;
51
52
  }
52
- /**
53
- * Parsed GitHub ```suggestion block, attached to a review thread when the
54
- * reviewer left a machine-applicable replacement. The `lines` are the
55
- * exact text the agent (or the CLI's commit-suggestions path) would write
56
- * into the file in place of lines [startLine..endLine].
57
- */
53
+ /** Parsed GitHub ```suggestion block attached to a review thread. */
58
54
  export interface SuggestionBlock {
59
55
  /** 1-indexed inclusive start line. Equal to `endLine` for single-line suggestions. */
60
56
  startLine: number;
@@ -107,8 +103,9 @@ export interface MergeStatusResult {
107
103
  reviewDecision: ReviewDecision;
108
104
  blockingBotReviewInProgress: boolean;
109
105
  mergeStateStatus: MergeStateStatus;
106
+ mergeRequirements?: MergeRequirements;
110
107
  }
111
- export interface BatchPrData {
108
+ export interface BatchPrData extends BatchPrMergeFields {
112
109
  nodeId: string;
113
110
  number: number;
114
111
  state: "OPEN" | "CLOSED" | "MERGED";
@@ -1,6 +1,7 @@
1
1
  import type { AgentThread, AgentComment, AgentCheck, GlobalOptions, RelevantCheck, ShepherdStatus, FirstLookThread, FirstLookComment } from "./report.mts";
2
2
  import type { ActiveCheck, PrActivitySummary } from "./activity.mts";
3
3
  import type { BranchProtection, CheckStatus, MergeStateStatus, Review, ReviewDecision, ReviewThread, ShepherdMergeStatus } from "./github.mts";
4
+ import type { MergeRequirements } from "./merge-requirements.mts";
4
5
  import type { ProtectedRun } from "./protected-run.mts";
5
6
  export type ShepherdAction = "wait" | "fix_code" | "mark_ready" | "cancel" | "escalate";
6
7
  export type EscalateTrigger = "fix-thrash" | "base-branch-unknown" | "stall-timeout" | "thread-missing-location" | "bot-cr-not-dismissed";
@@ -53,15 +54,12 @@ export interface IterateResultBase {
53
54
  summary: IterateResultSummary;
54
55
  /** Validated base branch (e.g. "main") for this PR. */
55
56
  baseBranch: string;
56
- /** Branch protection rule for the PR's base branch. Null when no rule exists or the base ref is unavailable. */
57
+ /** Null when no classic protection rule exists or the base ref is unavailable. */
57
58
  branchProtection: BranchProtection | null;
59
+ mergeRequirements?: MergeRequirements;
58
60
  /**
59
- * All CI checks that are relevant to PR readiness: triggered by a PR event
60
- * (pull_request / pull_request_target, or StatusContext with null event),
61
- * completed (status === COMPLETED), and not skipped/neutral.
62
- *
63
- * Includes both passing and failing checks. Failing entries carry
64
- * `workflowName`, `jobName`, `failedStep`, and `summary`.
61
+ * PR-event checks that completed and are not skipped/neutral.
62
+ * Failing entries carry `workflowName`, `jobName`, `failedStep`, and `summary`.
65
63
  */
66
64
  checks: RelevantCheck[];
67
65
  inProgressChecks?: ActiveCheck[];
@@ -153,5 +151,6 @@ export interface IterateCommandOptions extends GlobalOptions {
153
151
  stallTimeoutSeconds?: number;
154
152
  /** Case-insensitive workflow/check glob patterns Shepherd must not cancel. */
155
153
  neverCancelRuns?: string[];
154
+ persistSeen?: boolean;
156
155
  }
157
156
  export {};
@@ -0,0 +1,82 @@
1
+ /** Branch-rule and "why can't I merge" types shared by batch data and merge status. */
2
+ export interface BranchRules {
3
+ requiredApprovingReviewCount: number;
4
+ requiresConversationResolution: boolean;
5
+ requiresCodeOwnerReviews: boolean;
6
+ requiresLastPushApproval: boolean;
7
+ requiresCommitSignatures: boolean;
8
+ requiresLinearHistory: boolean;
9
+ requiresStrictStatusChecks: boolean;
10
+ requiredStatusCheckContexts: string[];
11
+ requiredDeploymentEnvironments: string[];
12
+ requiresMergeQueue: boolean;
13
+ requiresWorkflows: boolean;
14
+ requiresCodeScanning: boolean;
15
+ }
16
+ export interface MergeQueueEntryStatus {
17
+ position: number;
18
+ state: string;
19
+ estimatedTimeToMerge: number | null;
20
+ }
21
+ export interface StackStatus {
22
+ number: number;
23
+ size: number;
24
+ position: number;
25
+ baseRefName: string;
26
+ }
27
+ /** Extra batch-PR fields for merge-queue, stacks, and folded branch rules. */
28
+ export interface BatchPrMergeFields {
29
+ branchRules?: BranchRules;
30
+ isInMergeQueue?: boolean;
31
+ isMergeQueueEnabled?: boolean;
32
+ mergeQueueEntry?: MergeQueueEntryStatus | null;
33
+ stack?: StackStatus | null;
34
+ }
35
+ /** Snapshot of "why can't I merge" requirements vs current PR state. */
36
+ export interface MergeRequirements {
37
+ approvals: {
38
+ current: number;
39
+ requiredCount: number;
40
+ };
41
+ conversationsResolved: {
42
+ resolved: boolean;
43
+ unresolvedCount: number;
44
+ required: boolean;
45
+ };
46
+ codeOwnerReview?: {
47
+ required: true;
48
+ };
49
+ lastPushApproval?: {
50
+ required: true;
51
+ };
52
+ signedCommits?: {
53
+ required: true;
54
+ };
55
+ linearHistory?: {
56
+ required: true;
57
+ };
58
+ branchUpToDate?: {
59
+ current: boolean;
60
+ required: true;
61
+ };
62
+ requiredStatusChecks?: {
63
+ contexts: string[];
64
+ };
65
+ requiredDeployments?: {
66
+ environments: string[];
67
+ };
68
+ requiredWorkflows?: {
69
+ required: true;
70
+ };
71
+ codeScanning?: {
72
+ required: true;
73
+ };
74
+ mergeQueue?: {
75
+ required: boolean;
76
+ enabled: boolean;
77
+ inQueue: boolean;
78
+ position?: number;
79
+ state?: string;
80
+ };
81
+ stack?: StackStatus;
82
+ }
@@ -0,0 +1,2 @@
1
+ /** Branch-rule and "why can't I merge" types shared by batch data and merge status. */
2
+ export {};
package/bin/types.d.mts CHANGED
@@ -7,3 +7,4 @@ export * from "./types/agent-thread.mts";
7
7
  export * from "./types/check-annotations.mts";
8
8
  export * from "./types/report.mts";
9
9
  export * from "./types/iterate.mts";
10
+ export * from "./types/merge-requirements.mts";
package/bin/types.mjs CHANGED
@@ -7,3 +7,4 @@ export * from "./types/agent-thread.mjs";
7
7
  export * from "./types/check-annotations.mjs";
8
8
  export * from "./types/report.mjs";
9
9
  export * from "./types/iterate.mjs";
10
+ export * from "./types/merge-requirements.mjs";
@@ -1,2 +1,4 @@
1
1
  /** Allowed characters: alphanumeric, hyphen, underscore, dot. Prevents path traversal. */
2
2
  export declare const SAFE_SEGMENT: RegExp;
3
+ /** Positive decimal integer. Rejects traversal payloads that stringify as non-digits. */
4
+ export declare const SAFE_PR_NUMBER: RegExp;
@@ -1,2 +1,4 @@
1
1
  /** Allowed characters: alphanumeric, hyphen, underscore, dot. Prevents path traversal. */
2
2
  export const SAFE_SEGMENT = /^[a-zA-Z0-9._-]+$/;
3
+ /** Positive decimal integer. Rejects traversal payloads that stringify as non-digits. */
4
+ export const SAFE_PR_NUMBER = /^[1-9][0-9]*$/;
@@ -0,0 +1,2 @@
1
+ /** Bounded concurrent map — used so follow-up GraphQL pages do not burst N posts. */
2
+ export declare function mapPool<T, R>(items: readonly T[], concurrency: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,18 @@
1
+ /** Bounded concurrent map — used so follow-up GraphQL pages do not burst N posts. */
2
+ export async function mapPool(items, concurrency, fn) {
3
+ if (items.length === 0)
4
+ return [];
5
+ const results = Array.from({ length: items.length });
6
+ let next = 0;
7
+ const workerCount = Math.min(Math.max(1, concurrency), items.length);
8
+ await Promise.all(Array.from({ length: workerCount }, async () => {
9
+ for (;;) {
10
+ const i = next;
11
+ next += 1;
12
+ if (i >= items.length)
13
+ return;
14
+ results[i] = await fn(items[i], i);
15
+ }
16
+ }));
17
+ return results;
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.35.0",
3
+ "version": "0.36.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.35.0",
3
+ "version": "0.36.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.35.0", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.36.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.35.0", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.36.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }