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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -12
- package/bin/checks/conclusions.d.mts +6 -0
- package/bin/checks/conclusions.mjs +9 -0
- package/bin/checks/triage.mjs +11 -3
- package/bin/cli/args.mjs +1 -0
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/fix-formatter.mjs +4 -2
- package/bin/cli/help-command-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +10 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -1
- package/bin/cli/help.d.mts +2 -2
- package/bin/cli/iterate-formatter.mjs +11 -2
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/poll-handler.mjs +7 -0
- package/bin/commands/check-annotations.d.mts +16 -3
- package/bin/commands/check-annotations.mjs +37 -1
- package/bin/commands/check.d.mts +1 -0
- package/bin/commands/check.mjs +35 -28
- package/bin/commands/commit-suggestion.mjs +11 -5
- package/bin/commands/iterate/fix-code.mjs +13 -6
- package/bin/commands/iterate/helpers.d.mts +1 -0
- package/bin/commands/iterate/helpers.mjs +15 -6
- package/bin/commands/iterate/index.mjs +5 -10
- package/bin/commands/iterate/render.mjs +9 -5
- package/bin/commands/iterate/stall.mjs +5 -0
- package/bin/commands/poll.d.mts +2 -0
- package/bin/commands/poll.mjs +51 -17
- package/bin/commands/ready-delay.mjs +3 -13
- package/bin/config/load.mjs +67 -25
- package/bin/github/batch-page-helpers.d.mts +45 -0
- package/bin/github/batch-page-helpers.mjs +63 -0
- package/bin/github/batch-page.d.mts +14 -0
- package/bin/github/batch-page.mjs +62 -0
- package/bin/github/batch-parse-suites.d.mts +4 -0
- package/bin/github/batch-parse-suites.mjs +25 -0
- package/bin/github/batch-parser-helpers.mjs +1 -0
- package/bin/github/batch-parsers-rules.d.mts +6 -0
- package/bin/github/batch-parsers-rules.mjs +122 -0
- package/bin/github/batch-parsers.mjs +6 -0
- package/bin/github/batch-raw-rules.d.mts +59 -0
- package/bin/github/batch-raw-rules.mjs +1 -0
- package/bin/github/batch-raw-types.d.mts +25 -11
- package/bin/github/batch.d.mts +2 -0
- package/bin/github/batch.mjs +14 -120
- package/bin/github/gql/batch-pr-page.gql +194 -0
- package/bin/github/gql/batch-pr.gql +79 -21
- package/bin/github/gql/commit-suggestion-thread.gql +40 -0
- package/bin/github/http.d.mts +2 -1
- package/bin/github/http.mjs +2 -1
- package/bin/github/pagination.d.mts +3 -2
- package/bin/github/pagination.mjs +3 -2
- package/bin/github/queries.d.mts +4 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/github/rest-http.d.mts +6 -1
- package/bin/github/rest-http.mjs +25 -86
- package/bin/github/rest-text.d.mts +1 -0
- package/bin/github/rest-text.mjs +88 -0
- package/bin/github/suggestion-thread.d.mts +9 -0
- package/bin/github/suggestion-thread.mjs +45 -0
- package/bin/github/thread-comments.mjs +12 -8
- package/bin/merge-status/derive.mjs +2 -0
- package/bin/merge-status/requirements-format.d.mts +3 -0
- package/bin/merge-status/requirements-format.mjs +88 -0
- package/bin/merge-status/requirements.d.mts +2 -0
- package/bin/merge-status/requirements.mjs +51 -0
- package/bin/reporters/agent.mjs +0 -3
- package/bin/state/base.d.mts +9 -0
- package/bin/state/base.mjs +23 -0
- package/bin/state/bot-cr-seen.mjs +3 -13
- package/bin/state/fix-attempts.mjs +3 -13
- package/bin/state/iterate-stall.mjs +3 -13
- package/bin/state/seen-comments.mjs +6 -13
- package/bin/types/check-classification.d.mts +2 -2
- package/bin/types/github.d.mts +6 -7
- package/bin/types/iterate.d.mts +6 -7
- package/bin/types/merge-requirements.d.mts +82 -0
- package/bin/types/merge-requirements.mjs +2 -0
- package/bin/types/report.d.mts +5 -5
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/bin/util/path-segment.d.mts +2 -0
- package/bin/util/path-segment.mjs +2 -0
- package/bin/util/pool.d.mts +2 -0
- package/bin/util/pool.mjs +18 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {};
|
package/bin/types/github.d.mts
CHANGED
|
@@ -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";
|
package/bin/types/iterate.d.mts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
+
}
|
package/bin/types/report.d.mts
CHANGED
|
@@ -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`.
|
|
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:
|
|
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
package/bin/types.mjs
CHANGED
|
@@ -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,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