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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -12
- 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/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.d.mts +1 -0
- package/bin/commands/check.mjs +26 -22
- package/bin/commands/commit-suggestion.mjs +11 -5
- package/bin/commands/iterate/fix-code.mjs +1 -1
- package/bin/commands/iterate/helpers.d.mts +1 -0
- package/bin/commands/iterate/helpers.mjs +15 -6
- package/bin/commands/iterate/index.mjs +3 -10
- 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-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 +20 -11
- package/bin/github/batch.d.mts +2 -0
- package/bin/github/batch.mjs +14 -120
- package/bin/github/gql/batch-pr-page.gql +189 -0
- package/bin/github/gql/batch-pr.gql +74 -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/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/github.d.mts +4 -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.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
|
@@ -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 {
|
|
13
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
13
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|
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";
|
|
@@ -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";
|
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.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