pr-shepherd 1.0.0 → 1.1.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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/checks/classify.d.mts +3 -1
- package/bin/checks/classify.mjs +8 -1
- package/bin/cli/poll-summary-formatter.mjs +3 -0
- package/bin/cli/rule-auto-resolve-lines.d.mts +3 -0
- package/bin/cli/rule-auto-resolve-lines.mjs +12 -0
- package/bin/cli/stack-layer-view.d.mts +2 -1
- package/bin/cli/stack-overview.mjs +3 -0
- package/bin/cli/triage-formatter.mjs +5 -1
- package/bin/commands/check-activity.d.mts +8 -0
- package/bin/commands/check-activity.mjs +26 -0
- package/bin/commands/check-annotations.mjs +2 -1
- package/bin/commands/check-status.d.mts +6 -1
- package/bin/commands/check-status.mjs +9 -4
- package/bin/commands/check-wait-detectors.mjs +11 -0
- package/bin/commands/check.mjs +10 -3
- package/bin/commands/iterate/escalate.mjs +4 -2
- package/bin/commands/poll-summary-explicit-instructions.d.mts +1 -1
- package/bin/commands/poll-summary-explicit-instructions.mjs +9 -7
- package/bin/commands/poll-summary-instructions.d.mts +2 -2
- package/bin/commands/poll-summary-instructions.mjs +16 -15
- package/bin/commands/poll-summary-signature.d.mts +6 -0
- package/bin/commands/poll-summary-signature.mjs +19 -2
- package/bin/commands/poll-summary.mjs +22 -14
- package/bin/commands/ready-delay.d.mts +1 -0
- package/bin/commands/ready-delay.mjs +8 -3
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +7 -0
- package/bin/commands/rule-auto-resolve-format.mjs +11 -4
- package/bin/commands/rule-auto-resolve.d.mts +14 -2
- package/bin/commands/stack-drain.d.mts +1 -1
- package/bin/commands/stack-drain.mjs +3 -3
- package/bin/commands/stack-work.d.mts +4 -1
- package/bin/commands/stack-work.mjs +10 -2
- package/bin/commands/tick.mjs +1 -0
- package/bin/comments/thread-visibility.d.mts +6 -0
- package/bin/comments/thread-visibility.mjs +30 -15
- package/bin/github/batch-parse-suites.d.mts +14 -0
- package/bin/github/batch-parse-suites.mjs +4 -1
- package/bin/github/batch-parser-helpers.mjs +5 -0
- package/bin/github/batch-raw-types.d.mts +1 -0
- package/bin/github/check-annotation-cache.d.mts +5 -0
- package/bin/github/check-annotation-cache.mjs +6 -3
- package/bin/github/fingerprint.mjs +4 -0
- package/bin/github/github-fetch.d.mts +6 -2
- package/bin/github/github-fetch.mjs +37 -2
- package/bin/github/github-timeouts.d.mts +8 -0
- package/bin/github/github-timeouts.mjs +8 -0
- package/bin/github/gql/batch-pr-page.gql +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/commit-check-contexts.gql +1 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +1 -0
- package/bin/github/http-utils.mjs +3 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +5 -0
- package/bin/github/poll-summary-annotation-probe.mjs +17 -3
- package/bin/github/poll-summary-annotations.d.mts +14 -0
- package/bin/github/poll-summary-annotations.mjs +75 -0
- package/bin/github/poll-summary-blocking-reviewer.d.mts +2 -0
- package/bin/github/poll-summary-blocking-reviewer.mjs +9 -0
- package/bin/github/poll-summary-checks.d.mts +1 -0
- package/bin/github/poll-summary-checks.mjs +21 -2
- package/bin/github/poll-summary-command.d.mts +7 -1
- package/bin/github/poll-summary-command.mjs +29 -0
- package/bin/github/poll-summary-fingerprint.d.mts +7 -0
- package/bin/github/poll-summary-fingerprint.mjs +7 -0
- package/bin/github/poll-summary-projector.d.mts +3 -1
- package/bin/github/poll-summary-projector.mjs +15 -25
- package/bin/github/poll-summary-queue-removal.d.mts +2 -0
- package/bin/github/poll-summary-queue-removal.mjs +11 -0
- package/bin/github/poll-summary-raw.d.mts +1 -0
- package/bin/github/poll-summary-readiness.mjs +3 -0
- package/bin/github/poll-summary-review-parity.d.mts +24 -0
- package/bin/github/poll-summary-review-parity.mjs +82 -0
- package/bin/github/poll-summary-review.d.mts +11 -2
- package/bin/github/poll-summary-review.mjs +87 -59
- package/bin/github/poll-summary-route.mjs +2 -0
- package/bin/github/poll-summary-row-error.d.mts +16 -0
- package/bin/github/poll-summary-row-error.mjs +74 -0
- package/bin/github/poll-summary-rule-auto-resolve.d.mts +32 -0
- package/bin/github/poll-summary-rule-auto-resolve.mjs +119 -0
- package/bin/github/poll-summary-rule-handoff.d.mts +7 -0
- package/bin/github/poll-summary-rule-handoff.mjs +20 -0
- package/bin/github/poll-summary-rule-items.d.mts +20 -0
- package/bin/github/poll-summary-rule-items.mjs +28 -0
- package/bin/github/poll-summary-unreported.d.mts +7 -0
- package/bin/github/poll-summary-unreported.mjs +16 -0
- package/bin/github/poll-summary.mjs +15 -7
- package/bin/github/rest-check-read.mjs +3 -1
- package/bin/github/rest-reader-core.mjs +13 -2
- package/bin/github/rest-summary-read.mjs +12 -1
- package/bin/github/rest-text.mjs +3 -2
- package/bin/github/stack-handoff-page.mjs +1 -0
- package/bin/reporters/agent.mjs +2 -0
- package/bin/types/github.d.mts +8 -0
- package/bin/types/poll-summary.d.mts +23 -0
- package/bin/types/report.d.mts +5 -1
- package/package.json +2 -2
- package/plugins/pr-shepherd/.claude-plugin/plugin.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
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -0
|
@@ -3,8 +3,9 @@ import { recordRestIdentity } from "./rest-identities.mjs";
|
|
|
3
3
|
import { readRest as rest } from "./rest-reader-core.mjs";
|
|
4
4
|
export async function readRestCommitChecks(oid, repo, pr = 0) {
|
|
5
5
|
const prefix = restRepoPath(repo);
|
|
6
|
-
|
|
6
|
+
// Suites first: a COMPLETED suite read before the runs means a run still not COMPLETED is stuck.
|
|
7
7
|
const suites = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-suites`, (body) => restCollection(body, "check_suites"));
|
|
8
|
+
const checks = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-runs?filter=latest`, (body) => restCollection(body, "check_runs"));
|
|
8
9
|
const statuses = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/statuses`);
|
|
9
10
|
const workflowRuns = await readRestPages(`${prefix}/actions/runs?head_sha=${encodeURIComponent(oid)}`, (body) => restCollection(body, "workflow_runs"));
|
|
10
11
|
for (const suite of suites.nodes) {
|
|
@@ -64,6 +65,7 @@ export async function readRestCommitChecks(oid, repo, pr = 0) {
|
|
|
64
65
|
annotations: { totalCount: check.output.annotations_count, nodes: [] },
|
|
65
66
|
checkSuite: suite
|
|
66
67
|
? {
|
|
68
|
+
status: suite.status.toUpperCase(),
|
|
67
69
|
createdAt: suite.created_at,
|
|
68
70
|
updatedAt: suite.updated_at,
|
|
69
71
|
workflowRun: run
|
|
@@ -101,7 +101,9 @@ function nextRestPage(link, original) {
|
|
|
101
101
|
if (!match)
|
|
102
102
|
malformedRest(`invalid pagination link for ${original}`);
|
|
103
103
|
const url = new URL(match[1], "https://api.github.com");
|
|
104
|
-
|
|
104
|
+
const originalPath = original.split("?")[0];
|
|
105
|
+
if (url.origin !== "https://api.github.com" ||
|
|
106
|
+
!samePaginationResource(url.pathname, originalPath))
|
|
105
107
|
malformedRest(`unsafe pagination link for ${original}`);
|
|
106
108
|
if (url.searchParams.get("per_page") !== "100")
|
|
107
109
|
malformedRest(`changed page size for ${original}`);
|
|
@@ -113,7 +115,16 @@ function nextRestPage(link, original) {
|
|
|
113
115
|
JSON.stringify(source.searchParams.getAll(key)))
|
|
114
116
|
malformedRest(`changed pagination filter for ${original}`);
|
|
115
117
|
}
|
|
116
|
-
|
|
118
|
+
// Rebuild on the requested path so the repository id in a /repositories/{id} link is never trusted.
|
|
119
|
+
return `${originalPath}${url.search}`;
|
|
120
|
+
}
|
|
121
|
+
/** GitHub may link `/repos/{owner}/{repo}/…` pages through `/repositories/{id}/…`. */
|
|
122
|
+
function samePaginationResource(linkPath, originalPath) {
|
|
123
|
+
if (linkPath === originalPath)
|
|
124
|
+
return true;
|
|
125
|
+
const linkSuffix = /^\/repositories\/\d+(\/.*)$/.exec(linkPath)?.[1];
|
|
126
|
+
const originalSuffix = /^\/repos\/[^/]+\/[^/]+(\/.*)$/.exec(originalPath)?.[1];
|
|
127
|
+
return linkSuffix !== undefined && linkSuffix === originalSuffix;
|
|
117
128
|
}
|
|
118
129
|
/** A pull request whose mergeability GitHub is still computing must be re-read, never replayed. */
|
|
119
130
|
function isSettledBody(body) {
|
|
@@ -99,7 +99,7 @@ export function restSummary(data, pull, feedback, checks, baseRef) {
|
|
|
99
99
|
nodes: checks.suites.map((suite) => ({
|
|
100
100
|
status: suite.status.toUpperCase(),
|
|
101
101
|
conclusion: suite.conclusion?.toUpperCase() ?? null,
|
|
102
|
-
workflowRun: checks.workflowRuns.find((run) => run.check_suite_id === suite.id)
|
|
102
|
+
workflowRun: restSuiteWorkflowRun(checks.workflowRuns.find((run) => run.check_suite_id === suite.id)),
|
|
103
103
|
})),
|
|
104
104
|
},
|
|
105
105
|
},
|
|
@@ -110,3 +110,14 @@ export function restSummary(data, pull, feedback, checks, baseRef) {
|
|
|
110
110
|
transportUnavailable: data.transportUnavailable,
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
/** The GraphQL `workflowRun` shape, so summary readers see one suite format on either transport. */
|
|
114
|
+
function restSuiteWorkflowRun(run) {
|
|
115
|
+
if (!run)
|
|
116
|
+
return null;
|
|
117
|
+
return {
|
|
118
|
+
databaseId: run.id,
|
|
119
|
+
event: run.event,
|
|
120
|
+
url: run.html_url,
|
|
121
|
+
workflow: { name: run.name },
|
|
122
|
+
};
|
|
123
|
+
}
|
package/bin/github/rest-text.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { appendEntry, nextEntry } from "../log/log-file.mjs";
|
|
|
2
2
|
import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
3
3
|
import { GitHubRequestError } from "./errors.mjs";
|
|
4
4
|
import { githubFetch } from "./github-fetch.mjs";
|
|
5
|
+
import { GITHUB_LOG_TIMEOUT_MS } from "./github-timeouts.mjs";
|
|
5
6
|
import { makeAuthHeaders } from "./http-auth.mjs";
|
|
6
7
|
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
7
8
|
import { parseRateLimit, parseRetryAfter, redactUrl, sanitizeBody, } from "./http-utils.mjs";
|
|
@@ -19,7 +20,7 @@ export async function restText(path, onRateLimit) {
|
|
|
19
20
|
const auth = await makeAuthHeaders();
|
|
20
21
|
authSource = auth.source;
|
|
21
22
|
credentialFingerprint = auth.fingerprint;
|
|
22
|
-
return githubFetch(url, { method: "GET", headers: auth.headers, redirect: "manual" });
|
|
23
|
+
return githubFetch(url, { method: "GET", headers: auth.headers, redirect: "manual" }, GITHUB_LOG_TIMEOUT_MS);
|
|
23
24
|
}, t0, (response, durationMs) => recordIntermediateResponse({
|
|
24
25
|
n,
|
|
25
26
|
kind: "restText",
|
|
@@ -105,7 +106,7 @@ async function followRestTextRedirect(res, entry) {
|
|
|
105
106
|
const logUrl = redactUrl(location);
|
|
106
107
|
appendEntry(formatRequestEntry({ n: n2, kind: "restText", method: "GET", url: logUrl }));
|
|
107
108
|
const t1 = performance.now();
|
|
108
|
-
const redirectRes = await githubFetch(location);
|
|
109
|
+
const redirectRes = await githubFetch(location, undefined, GITHUB_LOG_TIMEOUT_MS);
|
|
109
110
|
appendEntry(formatResponseEntry({
|
|
110
111
|
n: n2,
|
|
111
112
|
kind: "restText",
|
|
@@ -90,6 +90,7 @@ export function batchResponseFromSummary(layer, source, runs) {
|
|
|
90
90
|
...(run.annotations && { annotations: run.annotations }),
|
|
91
91
|
checkSuite: suite
|
|
92
92
|
? {
|
|
93
|
+
...(suite.status && { status: suite.status }),
|
|
93
94
|
...(suite.createdAt && { createdAt: suite.createdAt }),
|
|
94
95
|
...(suite.updatedAt && { updatedAt: suite.updatedAt }),
|
|
95
96
|
workflowRun: workflowRun
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -59,6 +59,8 @@ export function toAgentCheck(c) {
|
|
|
59
59
|
runId: c.runId,
|
|
60
60
|
detailsUrl: c.detailsUrl,
|
|
61
61
|
conclusion: c.conclusion,
|
|
62
|
+
...(c.status !== "COMPLETED" &&
|
|
63
|
+
c.suiteStatus !== undefined && { status: c.status, suiteStatus: c.suiteStatus }),
|
|
62
64
|
...(c.runAttempt !== undefined && c.runAttempt > 1 && { runAttempt: c.runAttempt }),
|
|
63
65
|
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
64
66
|
...(c.jobName !== undefined && { jobName: c.jobName }),
|
package/bin/types/github.d.mts
CHANGED
|
@@ -57,9 +57,17 @@ export interface CheckRun {
|
|
|
57
57
|
summary?: string;
|
|
58
58
|
/** Workflow display name for GitHub Actions check runs, when GraphQL exposes it. */
|
|
59
59
|
workflowName?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Raw status of the check suite this run belongs to. Absent when GitHub did not report one
|
|
62
|
+
* (status contexts, synthetic checks, or a transport that cannot read it). A `COMPLETED`
|
|
63
|
+
* suite with a run that is not `COMPLETED` is a stuck check.
|
|
64
|
+
*/
|
|
65
|
+
suiteStatus?: string;
|
|
60
66
|
workflowId?: string;
|
|
61
67
|
/** True when GraphQL reported at least one CheckRun annotation. Omitted when false. */
|
|
62
68
|
hasAnnotations?: boolean;
|
|
69
|
+
/** Annotation total GraphQL reported for this CheckRun. Internal: validates cached lists. */
|
|
70
|
+
annotationCount?: number;
|
|
63
71
|
/**
|
|
64
72
|
* Every annotation on this run, when BatchPr's `annotations(first: 1)` page already held all
|
|
65
73
|
* of them. Internal: lets the check path skip `CheckRunAnnotationsBatch`; never rendered.
|
|
@@ -3,6 +3,7 @@ import type { MergeableState, MergeStateStatus, ReviewDecision } from "./github.
|
|
|
3
3
|
import type { MergeQueueRemovalStatus } from "./merge-requirements.mts";
|
|
4
4
|
import type { ShepherdAction } from "./iterate.mts";
|
|
5
5
|
import type { NextCheck } from "./next-check.mts";
|
|
6
|
+
import type { AutoMinimizedItem } from "./report.mts";
|
|
6
7
|
export interface PollSummaryChecks {
|
|
7
8
|
passing?: number;
|
|
8
9
|
failing?: number;
|
|
@@ -12,11 +13,26 @@ export interface PollSummaryChecks {
|
|
|
12
13
|
ignored?: number;
|
|
13
14
|
superseded?: number;
|
|
14
15
|
incomplete?: true;
|
|
16
|
+
/**
|
|
17
|
+
* Non-SUCCESS checks with annotations the one-PR check has not shown yet (or that could not be
|
|
18
|
+
* read). Only set on a row that would otherwise claim ready; it routes to `triage`.
|
|
19
|
+
*/
|
|
20
|
+
annotated?: number;
|
|
15
21
|
/** Required contexts with no check run and no status context. Omitted when empty. */
|
|
16
22
|
unreportedRequired?: string[];
|
|
17
23
|
/** A relevant Actions workflow suite has not completed. */
|
|
18
24
|
actionsWorkflowInProgress?: true;
|
|
19
25
|
}
|
|
26
|
+
/** The one-PR `ruleAutoResolve` content, with threads reduced to what a row needs: ID and link. */
|
|
27
|
+
export interface PollSummaryRuleAutoResolve {
|
|
28
|
+
summary: string;
|
|
29
|
+
threads?: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
}>;
|
|
33
|
+
minimized?: AutoMinimizedItem[];
|
|
34
|
+
errors?: string[];
|
|
35
|
+
}
|
|
20
36
|
export interface PollSummaryReview {
|
|
21
37
|
comments?: number;
|
|
22
38
|
reviews?: number;
|
|
@@ -56,6 +72,8 @@ export interface PollSummaryItem {
|
|
|
56
72
|
/** Conservative routing hint; the selected one-PR poll makes the authoritative decision. */
|
|
57
73
|
action: ShepherdAction;
|
|
58
74
|
reasons: string[];
|
|
75
|
+
/** Raw message of the error that stopped this PR's summary read (`summary-read-failed`). */
|
|
76
|
+
error?: string;
|
|
59
77
|
state: "OPEN" | "CLOSED" | "MERGED" | "UNKNOWN";
|
|
60
78
|
mergeable: MergeableState;
|
|
61
79
|
mergeStateStatus: MergeStateStatus;
|
|
@@ -72,6 +90,11 @@ export interface PollSummaryItem {
|
|
|
72
90
|
remainingSeconds?: number;
|
|
73
91
|
checks?: PollSummaryChecks;
|
|
74
92
|
review?: PollSummaryReview;
|
|
93
|
+
/**
|
|
94
|
+
* Rule-driven auto-resolve this tick performed or failed, in the one-PR `ruleAutoResolve`
|
|
95
|
+
* shape. Kept out of `review` so a one-tick event does not churn the status signature.
|
|
96
|
+
*/
|
|
97
|
+
ruleAutoResolve?: PollSummaryRuleAutoResolve;
|
|
75
98
|
stack?: PollSummaryStack;
|
|
76
99
|
pollCommand?: string;
|
|
77
100
|
/**
|
package/bin/types/report.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, SuggestionBlock } from "./github.mts";
|
|
1
|
+
import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, CheckStatus, SuggestionBlock } from "./github.mts";
|
|
2
2
|
import type { ClassifiedCheck, RelatedFailedJob, TriagedCheck } from "./check-classification.mts";
|
|
3
3
|
import type { AgentThreadComment } from "./agent-thread.mts";
|
|
4
4
|
import type { CheckAnnotation } from "./check-annotations.mts";
|
|
@@ -196,6 +196,10 @@ export interface AgentCheck {
|
|
|
196
196
|
detailsUrl: string | null;
|
|
197
197
|
/** Raw GitHub check conclusion; may be null for some completed checks from upstream data. */
|
|
198
198
|
conclusion: CheckConclusion;
|
|
199
|
+
/** Raw check status, present only for a stuck check: not `COMPLETED` while its suite is. */
|
|
200
|
+
status?: CheckStatus;
|
|
201
|
+
/** Raw check-suite status (`COMPLETED`), present only with `status` for a stuck check. */
|
|
202
|
+
suiteStatus?: string;
|
|
199
203
|
/** Workflow display name (e.g. `"CI"`). Populated on a best-effort basis when available from the jobs API. */
|
|
200
204
|
workflowName?: string;
|
|
201
205
|
/** Name of the matched job (e.g. `"tests (ubuntu)"`). Distinct from check name for matrix builds. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"husky": "^9.1.7",
|
|
92
92
|
"knip": "^6.14.1",
|
|
93
93
|
"marked": "^18.0.11",
|
|
94
|
-
"oxfmt": "^0.
|
|
94
|
+
"oxfmt": "^0.73.0",
|
|
95
95
|
"oxlint": "^1.60.0",
|
|
96
96
|
"typescript": "^7.0.2",
|
|
97
97
|
"vitest": "^5.0.0"
|
|
@@ -13,6 +13,7 @@ Apply when a step says `Playbook: "CI failure triage"`. Use the excerpts and tag
|
|
|
13
13
|
- Actions failure with a run id: read the excerpt. Fix a real test or build failure in code and never rerun it, even with a printed `rerun:`. Run a printed `rerun:` only for a transient cause; without one, finish the other work. Then iterate without waiting.
|
|
14
14
|
- An empty excerpt is not evidence of a transient failure. A gate-job excerpt naming failed jobs (for example `test-playwright: failure`) is test-failure evidence even without a stack trace: read the `Other failed jobs in this run` tails first, and only when a named job's tail is absent or truncated run `gh run view <runId> --log-failed -R <owner/repo>`. Rerun only when those logs show a transient cause.
|
|
15
15
|
- `CANCELLED` or `STARTUP_FAILURE` (no excerpt): run a printed `rerun:`, else finish the other work; iterate without waiting.
|
|
16
|
+
- `[status: …, suite: COMPLETED]` (stuck run): the workflow finished but this check never completed, so there is nothing to fix in code. Run a printed `rerun:`, else finish the other work; iterate without waiting.
|
|
16
17
|
- `ACTION_REQUIRED`: finish the other work and iterate. Shepherd escalates if manual approval is still required.
|
|
17
18
|
- `external` with a URL: inspect the provider or reproduce locally, fix, and iterate. The URL alone is not `[ESCALATE]`.
|
|
18
19
|
- `(no runId)` without a URL: keep the metadata. Shepherd escalates when nothing else remains.
|