pr-shepherd 0.35.0 → 0.37.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 (91) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +32 -12
  3. package/bin/checks/conclusions.d.mts +6 -0
  4. package/bin/checks/conclusions.mjs +9 -0
  5. package/bin/checks/triage.mjs +11 -3
  6. package/bin/cli/args.mjs +1 -0
  7. package/bin/cli/default-poll.mjs +1 -0
  8. package/bin/cli/fix-formatter.mjs +4 -2
  9. package/bin/cli/help-command-pages.d.mts +1 -1
  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 +2 -1
  14. package/bin/cli/help.d.mts +2 -2
  15. package/bin/cli/iterate-formatter.mjs +11 -2
  16. package/bin/cli/iterate-lean.mjs +1 -0
  17. package/bin/cli/poll-handler.mjs +7 -0
  18. package/bin/commands/check-annotations.d.mts +16 -3
  19. package/bin/commands/check-annotations.mjs +37 -1
  20. package/bin/commands/check.d.mts +1 -0
  21. package/bin/commands/check.mjs +35 -28
  22. package/bin/commands/commit-suggestion.mjs +11 -5
  23. package/bin/commands/iterate/fix-code.mjs +13 -6
  24. package/bin/commands/iterate/helpers.d.mts +1 -0
  25. package/bin/commands/iterate/helpers.mjs +15 -6
  26. package/bin/commands/iterate/index.mjs +5 -10
  27. package/bin/commands/iterate/render.mjs +9 -5
  28. package/bin/commands/iterate/stall.mjs +5 -0
  29. package/bin/commands/poll.d.mts +2 -0
  30. package/bin/commands/poll.mjs +51 -17
  31. package/bin/commands/ready-delay.mjs +3 -13
  32. package/bin/config/load.mjs +67 -25
  33. package/bin/github/batch-page-helpers.d.mts +45 -0
  34. package/bin/github/batch-page-helpers.mjs +63 -0
  35. package/bin/github/batch-page.d.mts +14 -0
  36. package/bin/github/batch-page.mjs +62 -0
  37. package/bin/github/batch-parse-suites.d.mts +4 -0
  38. package/bin/github/batch-parse-suites.mjs +25 -0
  39. package/bin/github/batch-parser-helpers.mjs +1 -0
  40. package/bin/github/batch-parsers-rules.d.mts +6 -0
  41. package/bin/github/batch-parsers-rules.mjs +122 -0
  42. package/bin/github/batch-parsers.mjs +6 -0
  43. package/bin/github/batch-raw-rules.d.mts +59 -0
  44. package/bin/github/batch-raw-rules.mjs +1 -0
  45. package/bin/github/batch-raw-types.d.mts +25 -11
  46. package/bin/github/batch.d.mts +2 -0
  47. package/bin/github/batch.mjs +14 -120
  48. package/bin/github/gql/batch-pr-page.gql +194 -0
  49. package/bin/github/gql/batch-pr.gql +79 -21
  50. package/bin/github/gql/commit-suggestion-thread.gql +40 -0
  51. package/bin/github/http.d.mts +2 -1
  52. package/bin/github/http.mjs +2 -1
  53. package/bin/github/pagination.d.mts +3 -2
  54. package/bin/github/pagination.mjs +3 -2
  55. package/bin/github/queries.d.mts +4 -0
  56. package/bin/github/queries.mjs +4 -0
  57. package/bin/github/rest-http.d.mts +6 -1
  58. package/bin/github/rest-http.mjs +25 -86
  59. package/bin/github/rest-text.d.mts +1 -0
  60. package/bin/github/rest-text.mjs +88 -0
  61. package/bin/github/suggestion-thread.d.mts +9 -0
  62. package/bin/github/suggestion-thread.mjs +45 -0
  63. package/bin/github/thread-comments.mjs +12 -8
  64. package/bin/merge-status/derive.mjs +2 -0
  65. package/bin/merge-status/requirements-format.d.mts +3 -0
  66. package/bin/merge-status/requirements-format.mjs +88 -0
  67. package/bin/merge-status/requirements.d.mts +2 -0
  68. package/bin/merge-status/requirements.mjs +51 -0
  69. package/bin/reporters/agent.mjs +0 -3
  70. package/bin/state/base.d.mts +9 -0
  71. package/bin/state/base.mjs +23 -0
  72. package/bin/state/bot-cr-seen.mjs +3 -13
  73. package/bin/state/fix-attempts.mjs +3 -13
  74. package/bin/state/iterate-stall.mjs +3 -13
  75. package/bin/state/seen-comments.mjs +6 -13
  76. package/bin/types/check-classification.d.mts +2 -2
  77. package/bin/types/github.d.mts +6 -7
  78. package/bin/types/iterate.d.mts +6 -7
  79. package/bin/types/merge-requirements.d.mts +82 -0
  80. package/bin/types/merge-requirements.mjs +2 -0
  81. package/bin/types/report.d.mts +5 -5
  82. package/bin/types.d.mts +1 -0
  83. package/bin/types.mjs +1 -0
  84. package/bin/util/path-segment.d.mts +2 -0
  85. package/bin/util/path-segment.mjs +2 -0
  86. package/bin/util/pool.d.mts +2 -0
  87. package/bin/util/pool.mjs +18 -0
  88. package/package.json +1 -1
  89. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  90. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  91. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -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
  }
@@ -3,6 +3,8 @@ import type { CheckRun } from "./github.mts";
3
3
  type CheckCategory = "passed" | "failing" | "in_progress" | "skipped" | "filtered" | "ignored" | "superseded";
4
4
  export interface ClassifiedCheck extends CheckRun {
5
5
  category: CheckCategory;
6
+ /** Inline annotations attached to this check run, surfaced once per PR. */
7
+ annotations?: CheckAnnotation[];
6
8
  }
7
9
  export interface TriagedCheck extends ClassifiedCheck {
8
10
  /** Workflow display name (e.g. `"CI"`). Populated when available from the jobs API; may be `undefined` on fetch failure or when no matching job is found. */
@@ -13,7 +15,5 @@ export interface TriagedCheck extends ClassifiedCheck {
13
15
  failedStep?: string;
14
16
  /** Bounded raw excerpt from the matched failed job log, when GitHub exposes one. */
15
17
  logExcerpt?: string;
16
- /** Inline annotations attached to this failing check run, surfaced once per PR. */
17
- annotations?: CheckAnnotation[];
18
18
  }
19
19
  export {};
@@ -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";
@@ -27,6 +28,8 @@ export interface CheckRun {
27
28
  /** Workflow display name for GitHub Actions check runs, when GraphQL exposes it. */
28
29
  workflowName?: string;
29
30
  workflowId?: string;
31
+ /** True when GraphQL reported at least one CheckRun annotation. Omitted when false. */
32
+ hasAnnotations?: boolean;
30
33
  }
31
34
  export interface ReviewThread {
32
35
  id: string;
@@ -49,12 +52,7 @@ export interface ReviewThread {
49
52
  /** True when Shepherd re-surfaced this thread because its body changed after first look. */
50
53
  edited?: boolean;
51
54
  }
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
- */
55
+ /** Parsed GitHub ```suggestion block attached to a review thread. */
58
56
  export interface SuggestionBlock {
59
57
  /** 1-indexed inclusive start line. Equal to `endLine` for single-line suggestions. */
60
58
  startLine: number;
@@ -107,8 +105,9 @@ export interface MergeStatusResult {
107
105
  reviewDecision: ReviewDecision;
108
106
  blockingBotReviewInProgress: boolean;
109
107
  mergeStateStatus: MergeStateStatus;
108
+ mergeRequirements?: MergeRequirements;
110
109
  }
111
- export interface BatchPrData {
110
+ export interface BatchPrData extends BatchPrMergeFields {
112
111
  nodeId: string;
113
112
  number: number;
114
113
  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 {};
@@ -32,6 +32,8 @@ export interface ShepherdReport {
32
32
  skipped: ClassifiedCheck[];
33
33
  /** Checks filtered out because they were triggered by a non-PR event (push, schedule, etc.). */
34
34
  filtered: ClassifiedCheck[];
35
+ /** Ignored checks with unseen annotations; omitted when empty. */
36
+ ignored?: ClassifiedCheck[];
35
37
  filteredNames: string[];
36
38
  blockedByFilteredCheck: boolean;
37
39
  ignoredNames?: string[];
@@ -104,16 +106,14 @@ export interface AgentComment {
104
106
  url: string;
105
107
  edited?: boolean;
106
108
  }
107
- /** Check shape emitted to the iterate agent under `fix_code`. Cancelled checks
108
- * should be handled from `name`/`runId`/`detailsUrl`/`conclusion`; optional
109
- * workflow/job/step metadata may still be present when available. */
109
+ /** Check shape emitted to the iterate agent under `fix_code`. */
110
110
  export interface AgentCheck {
111
111
  name: string;
112
112
  runId: string | null;
113
113
  /** Fallback for checks where runId is null (e.g. external status checks). */
114
114
  detailsUrl: string | null;
115
115
  /** Raw GitHub check conclusion; may be null for some completed checks from upstream data. */
116
- conclusion: Exclude<CheckConclusion, "SKIPPED" | "NEUTRAL">;
116
+ conclusion: CheckConclusion;
117
117
  /** Workflow display name (e.g. `"CI"`). Populated on a best-effort basis when available from the jobs API. */
118
118
  workflowName?: string;
119
119
  /** Name of the matched job (e.g. `"tests (ubuntu)"`). Distinct from check name for matrix builds. */
@@ -123,8 +123,8 @@ export interface AgentCheck {
123
123
  /** One-line status text shown in the GitHub UI (e.g. "67.68% of diff hit (target 85.00%)"). */
124
124
  summary?: string;
125
125
  logExcerpt?: string;
126
- /** Marker-gated inline annotations from this failing check. */
127
126
  annotations?: CheckAnnotation[];
127
+ annotationOnly?: true;
128
128
  }
129
129
  /**
130
130
  * A single CI check that is relevant to PR readiness — triggered by a PR event
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.37.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.37.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.37.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.37.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }