pr-shepherd 0.55.2 → 0.56.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 +6 -2
- package/bin/api.d.mts +5 -0
- package/bin/api.mjs +12 -1
- package/bin/checks/job-log.d.mts +9 -0
- package/bin/checks/job-log.mjs +30 -0
- package/bin/checks/jobs-types.d.mts +14 -0
- package/bin/checks/jobs-types.mjs +7 -0
- package/bin/checks/related-jobs.d.mts +11 -0
- package/bin/checks/related-jobs.mjs +32 -0
- package/bin/checks/triage-budget.d.mts +17 -0
- package/bin/checks/triage-budget.mjs +49 -0
- package/bin/checks/triage.d.mts +6 -3
- package/bin/checks/triage.mjs +89 -61
- package/bin/cli/api-usage-formatter.mjs +2 -2
- package/bin/cli/fix-formatter.mjs +2 -0
- package/bin/cli/iterate-checks-formatter.mjs +2 -0
- package/bin/cli/related-jobs-format.d.mts +3 -0
- package/bin/cli/related-jobs-format.mjs +16 -0
- package/bin/commands/check-execution-context.d.mts +12 -0
- package/bin/commands/check-execution-context.mjs +38 -0
- package/bin/commands/check-fingerprint.mjs +14 -8
- package/bin/commands/check-unreported.d.mts +3 -2
- package/bin/commands/check-unreported.mjs +4 -4
- package/bin/commands/check.d.mts +2 -1
- package/bin/commands/check.mjs +21 -6
- package/bin/commands/iterate/check-instructions.d.mts +6 -3
- package/bin/commands/iterate/check-instructions.mjs +6 -5
- package/bin/commands/iterate/escalate.mjs +3 -0
- package/bin/commands/iterate/fix-code.mjs +6 -2
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/index.mjs +20 -8
- package/bin/commands/iterate/render.mjs +11 -3
- package/bin/commands/iterate/stale-ancestry.d.mts +2 -1
- package/bin/commands/iterate/stale-ancestry.mjs +3 -2
- package/bin/commands/poll-quota.d.mts +2 -0
- package/bin/commands/poll-quota.mjs +18 -25
- package/bin/commands/poll-rate-limit-wait.mjs +13 -8
- package/bin/commands/ready-delay.d.mts +2 -0
- package/bin/commands/ready-delay.mjs +18 -0
- package/bin/commands/resolve-mutate.mjs +8 -9
- package/bin/github/batch-raw-types.d.mts +2 -0
- package/bin/github/batch-receipt-evidence.d.mts +5 -0
- package/bin/github/batch-receipt-evidence.mjs +61 -0
- package/bin/github/batch.d.mts +4 -0
- package/bin/github/batch.mjs +42 -8
- package/bin/github/errors.d.mts +3 -0
- package/bin/github/errors.mjs +15 -6
- package/bin/github/gql/batch-pr-page.gql +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +8 -0
- package/bin/github/gql/reply-thread-comments.gql +25 -0
- package/bin/github/gql/reply-thread-transcripts.gql +31 -0
- package/bin/github/merge-queue-checks.d.mts +2 -1
- package/bin/github/merge-queue-checks.mjs +18 -8
- package/bin/github/merge-target-rules.d.mts +2 -1
- package/bin/github/merge-target-rules.mjs +4 -4
- package/bin/github/poll-summary-annotation-probe.d.mts +2 -0
- package/bin/github/poll-summary-annotation-probe.mjs +7 -0
- package/bin/github/queries.d.mts +5 -0
- package/bin/github/queries.mjs +5 -0
- package/bin/github/rate-limit-kind.d.mts +13 -0
- package/bin/github/rate-limit-kind.mjs +25 -0
- package/bin/github/reply-thread-transcripts.d.mts +3 -0
- package/bin/github/reply-thread-transcripts.mjs +89 -0
- package/bin/github/rest-http.mjs +1 -0
- package/bin/github/rest-text.d.mts +2 -1
- package/bin/github/rest-text.mjs +5 -2
- package/bin/github/thread-comments.d.mts +5 -1
- package/bin/github/thread-comments.mjs +30 -6
- package/bin/mcp/server.mjs +32 -5
- package/bin/quota-warning.mjs +2 -2
- package/bin/reporters/agent.mjs +1 -0
- package/bin/threads/transcript.d.mts +1 -0
- package/bin/threads/transcript.mjs +4 -1
- package/bin/types/check-classification.d.mts +10 -0
- package/bin/types/report.d.mts +4 -1
- 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
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +2 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { graphqlWithRateLimit } from "./client.mjs";
|
|
2
|
+
import { GitHubRequestError } from "./errors.mjs";
|
|
3
|
+
import { rateLimitKind } from "./rate-limit-kind.mjs";
|
|
4
|
+
import { REPLY_THREAD_COMMENTS_QUERY, REPLY_THREAD_TRANSCRIPTS_QUERY } from "./queries.mjs";
|
|
5
|
+
import { threadTranscriptBodies } from "../threads/transcript.mjs";
|
|
6
|
+
import { mapPool } from "../util/pool.mjs";
|
|
7
|
+
const IDS_PER_REQUEST = 20;
|
|
8
|
+
const CONCURRENCY = 4;
|
|
9
|
+
const MAX_COMMENT_PAGES = 100;
|
|
10
|
+
/** Best-effort transcript evidence; it never filters user-supplied mutation IDs. */
|
|
11
|
+
export async function fetchReplyThreadTranscripts(pr, repo, requestedIds) {
|
|
12
|
+
const ids = [...new Set(requestedIds)];
|
|
13
|
+
if (ids.length === 0)
|
|
14
|
+
return new Map();
|
|
15
|
+
const expectedRepo = `${repo.owner}/${repo.name}`.toLowerCase();
|
|
16
|
+
const requested = new Set(ids);
|
|
17
|
+
const chunks = [];
|
|
18
|
+
for (let offset = 0; offset < ids.length; offset += IDS_PER_REQUEST) {
|
|
19
|
+
chunks.push(ids.slice(offset, offset + IDS_PER_REQUEST));
|
|
20
|
+
}
|
|
21
|
+
let stopped = false;
|
|
22
|
+
const firstPages = await mapPool(chunks, CONCURRENCY, async (chunk) => {
|
|
23
|
+
if (stopped)
|
|
24
|
+
return [];
|
|
25
|
+
try {
|
|
26
|
+
const result = await graphqlWithRateLimit(REPLY_THREAD_TRANSCRIPTS_QUERY, {
|
|
27
|
+
ids: chunk,
|
|
28
|
+
});
|
|
29
|
+
if (result.rateLimit?.remaining === 0)
|
|
30
|
+
stopped = true;
|
|
31
|
+
return result.data.nodes.filter((node) => node?.__typename === "PullRequestReviewThread" &&
|
|
32
|
+
node.id !== undefined &&
|
|
33
|
+
requested.has(node.id) &&
|
|
34
|
+
node.pullRequest?.number === pr &&
|
|
35
|
+
node.pullRequest.repository.nameWithOwner.toLowerCase() === expectedRepo &&
|
|
36
|
+
node.comments !== undefined);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isThrottle(error))
|
|
40
|
+
stopped = true;
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const threads = firstPages.flat();
|
|
45
|
+
const completed = await mapPool(threads, CONCURRENCY, async (thread) => {
|
|
46
|
+
try {
|
|
47
|
+
return await completeThread(thread, () => stopped, () => {
|
|
48
|
+
stopped = true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isThrottle(error))
|
|
53
|
+
stopped = true;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return new Map(completed.filter((entry) => entry !== null));
|
|
58
|
+
}
|
|
59
|
+
async function completeThread(thread, isStopped, stop) {
|
|
60
|
+
if (!thread.id || !thread.comments)
|
|
61
|
+
return null;
|
|
62
|
+
const bodies = thread.comments.nodes.map((comment) => comment.body);
|
|
63
|
+
let page = thread.comments;
|
|
64
|
+
const seenCursors = new Set();
|
|
65
|
+
let pages = 1;
|
|
66
|
+
while (page.pageInfo.hasNextPage) {
|
|
67
|
+
const cursor = page.pageInfo.endCursor;
|
|
68
|
+
if (!cursor || seenCursors.has(cursor) || pages >= MAX_COMMENT_PAGES || isStopped())
|
|
69
|
+
return null;
|
|
70
|
+
seenCursors.add(cursor);
|
|
71
|
+
const result = await graphqlWithRateLimit(REPLY_THREAD_COMMENTS_QUERY, {
|
|
72
|
+
id: thread.id,
|
|
73
|
+
cursor,
|
|
74
|
+
});
|
|
75
|
+
if (result.rateLimit?.remaining === 0)
|
|
76
|
+
stop();
|
|
77
|
+
const next = result.data.node;
|
|
78
|
+
if (next?.__typename !== "PullRequestReviewThread" || next.id !== thread.id || !next.comments) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
page = next.comments;
|
|
82
|
+
bodies.push(...page.nodes.map((comment) => comment.body));
|
|
83
|
+
pages += 1;
|
|
84
|
+
}
|
|
85
|
+
return bodies.length > 0 ? [thread.id, threadTranscriptBodies(bodies)] : null;
|
|
86
|
+
}
|
|
87
|
+
function isThrottle(error) {
|
|
88
|
+
return error instanceof GitHubRequestError && rateLimitKind(error) !== null;
|
|
89
|
+
}
|
package/bin/github/rest-http.mjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type RateLimitInfo } from "./http-utils.mts";
|
|
2
|
+
export declare function restText(path: string, onRateLimit?: (rateLimit?: RateLimitInfo) => void): Promise<string>;
|
package/bin/github/rest-text.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
|
3
3
|
import { GitHubRequestError } from "./errors.mjs";
|
|
4
4
|
import { makeAuthHeaders } from "./http-auth.mjs";
|
|
5
5
|
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
6
|
-
import { parseRateLimit, parseRetryAfter, redactUrl, sanitizeBody } from "./http-utils.mjs";
|
|
6
|
+
import { parseRateLimit, parseRetryAfter, redactUrl, sanitizeBody, } from "./http-utils.mjs";
|
|
7
7
|
import { recordApiTelemetry } from "./api-telemetry.mjs";
|
|
8
8
|
import { recordIntermediateResponse } from "./http-intermediate.mjs";
|
|
9
9
|
const BASE_URL = "https://api.github.com";
|
|
10
|
-
export async function restText(path) {
|
|
10
|
+
export async function restText(path, onRateLimit) {
|
|
11
11
|
const url = `${BASE_URL}${path}`;
|
|
12
12
|
const n = nextEntry();
|
|
13
13
|
appendEntry(formatRequestEntry({ n, kind: "restText", method: "GET", url }));
|
|
@@ -28,6 +28,7 @@ export async function restText(path) {
|
|
|
28
28
|
}));
|
|
29
29
|
const durationMs = Math.round(performance.now() - retryT0);
|
|
30
30
|
const rateLimit = parseRateLimit(res.headers) ?? undefined;
|
|
31
|
+
onRateLimit?.(rateLimit);
|
|
31
32
|
const retryAfterSeconds = parseRetryAfter(res.headers);
|
|
32
33
|
recordApiTelemetry({ kind: "REST", method: "GET", authSource, rateLimit });
|
|
33
34
|
if ([301, 302, 307, 308].includes(res.status)) {
|
|
@@ -62,6 +63,7 @@ export async function restText(path) {
|
|
|
62
63
|
rateLimit,
|
|
63
64
|
retryAfterSeconds,
|
|
64
65
|
authSource,
|
|
66
|
+
responseMessage: sanitizeBody(text),
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
appendEntry(formatResponseEntry({
|
|
@@ -114,6 +116,7 @@ async function followRestTextRedirect(res, entry) {
|
|
|
114
116
|
status: redirectRes.status,
|
|
115
117
|
rateLimit: parseRateLimit(redirectRes.headers) ?? undefined,
|
|
116
118
|
retryAfterSeconds: parseRetryAfter(redirectRes.headers),
|
|
119
|
+
responseMessage: "",
|
|
117
120
|
});
|
|
118
121
|
}
|
|
119
122
|
return redirectRes.text();
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
import { type RateLimitInfo } from "./client.mts";
|
|
1
2
|
import type { RawThread } from "./batch-raw-types.mts";
|
|
2
|
-
export declare function hydrateThreadCommentPages(threads: RawThread[]): Promise<
|
|
3
|
+
export declare function hydrateThreadCommentPages(threads: RawThread[], initialRateLimit?: RateLimitInfo): Promise<{
|
|
4
|
+
threads: RawThread[];
|
|
5
|
+
rateLimit?: RateLimitInfo;
|
|
6
|
+
}>;
|
|
@@ -4,23 +4,47 @@ import { paginateForward } from "./pagination.mjs";
|
|
|
4
4
|
import { REVIEW_THREAD_COMMENTS_QUERY } from "./queries.mjs";
|
|
5
5
|
import { mapPool } from "../util/pool.mjs";
|
|
6
6
|
const THREAD_COMMENT_PAGE_CONCURRENCY = 4;
|
|
7
|
-
export async function hydrateThreadCommentPages(threads) {
|
|
8
|
-
const gate = {
|
|
9
|
-
|
|
7
|
+
export async function hydrateThreadCommentPages(threads, initialRateLimit) {
|
|
8
|
+
const gate = {
|
|
9
|
+
rateLimit: initialRateLimit,
|
|
10
|
+
exhausted: initialRateLimit?.remaining === 0,
|
|
11
|
+
};
|
|
12
|
+
const hydrated = await mapPool(threads, THREAD_COMMENT_PAGE_CONCURRENCY, async (thread) => {
|
|
13
|
+
if (gate.error !== undefined)
|
|
14
|
+
return thread;
|
|
15
|
+
try {
|
|
16
|
+
return await hydrateThreadCommentPage(thread, gate);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
gate.error ??= error;
|
|
20
|
+
return thread;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (gate.error !== undefined)
|
|
24
|
+
throw gate.error;
|
|
25
|
+
return { threads: hydrated, rateLimit: gate.rateLimit };
|
|
10
26
|
}
|
|
11
27
|
async function hydrateThreadCommentPage(thread, gate) {
|
|
12
28
|
const pageInfo = thread.comments.pageInfo;
|
|
13
29
|
if (!pageInfo?.hasNextPage || !pageInfo.endCursor)
|
|
14
30
|
return thread;
|
|
15
31
|
const extra = await paginateForward(async (cursor) => {
|
|
16
|
-
if (gate.
|
|
17
|
-
throw
|
|
32
|
+
if (gate.error !== undefined)
|
|
33
|
+
throw gate.error;
|
|
34
|
+
if (gate.exhausted) {
|
|
35
|
+
throw new GitHubRequestError("GitHub GraphQL rate limit remaining is 0; thread comment pagination incomplete", { status: 403, rateLimit: gate.rateLimit });
|
|
18
36
|
}
|
|
19
37
|
const res = await graphqlWithRateLimit(REVIEW_THREAD_COMMENTS_QUERY, {
|
|
20
38
|
threadId: thread.id,
|
|
21
39
|
...(cursor ? { commentsCursor: cursor } : {}),
|
|
22
40
|
});
|
|
23
|
-
|
|
41
|
+
if (res.rateLimit?.remaining === 0) {
|
|
42
|
+
gate.rateLimit = res.rateLimit;
|
|
43
|
+
gate.exhausted = true;
|
|
44
|
+
}
|
|
45
|
+
else if (!gate.exhausted) {
|
|
46
|
+
gate.rateLimit = res.rateLimit ?? gate.rateLimit;
|
|
47
|
+
}
|
|
24
48
|
const node = res.data.node;
|
|
25
49
|
if (!node?.comments) {
|
|
26
50
|
const nodeType = node?.__typename ?? "null";
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { formatPollSummaryResult } from "../cli/poll-summary-formatter.mjs";
|
|
|
10
10
|
import { projectStackOverview } from "../cli/stack-overview.mjs";
|
|
11
11
|
import { formatCliError, serializeGitHubRequestErrorDetails } from "../cli/error-format.mjs";
|
|
12
12
|
import { errorToExitCode, EXIT } from "../exit-codes.mjs";
|
|
13
|
+
import { extractShepherdJournal } from "../journal/index.mjs";
|
|
13
14
|
const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/repo#number reference";
|
|
14
15
|
const pr = z
|
|
15
16
|
.string()
|
|
@@ -87,7 +88,8 @@ const suggestionPatchesInputSchema = z.object({
|
|
|
87
88
|
});
|
|
88
89
|
/** Creates a local-only MCP server with Shepherd's public operations. */
|
|
89
90
|
export function createPrShepherdMcpServer(options = {}) {
|
|
90
|
-
|
|
91
|
+
let shepherd = options.shepherd;
|
|
92
|
+
const getShepherd = () => (shepherd ??= createPrShepherd({ cwd: options.cwd }));
|
|
91
93
|
const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
|
|
92
94
|
server.registerTool("iterate", {
|
|
93
95
|
description: "Inspect one pull request, an explicit same-repository set, or a native stack and return one Shepherd tick.",
|
|
@@ -103,7 +105,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
103
105
|
const opts = {
|
|
104
106
|
readyDelaySuffix: input.readyDelaySeconds === undefined ? undefined : `${input.readyDelaySeconds}s`,
|
|
105
107
|
};
|
|
106
|
-
return runTool(() => runIterateSelector(
|
|
108
|
+
return runTool(() => runIterateSelector(getShepherd(), requireRepositoryQualifiedIterate(input)), (result) => isPollSummary(result)
|
|
107
109
|
? formatPollSummaryResult(result)
|
|
108
110
|
: formatIterateResult(result, opts), (result) => isPollSummary(result)
|
|
109
111
|
? result.selection.kind === "stack"
|
|
@@ -120,7 +122,32 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
120
122
|
idempotentHint: false,
|
|
121
123
|
openWorldHint: true,
|
|
122
124
|
},
|
|
123
|
-
}, async (input) => runTool(() =>
|
|
125
|
+
}, async (input) => runTool(() => getShepherd().apply(requireRepositoryQualifiedPr(input)), formatApplyResult));
|
|
126
|
+
server.registerTool("extract_journal", {
|
|
127
|
+
description: "Extract the validated Shepherd Journal from a supplied Markdown body without I/O.",
|
|
128
|
+
inputSchema: z.object({ body: z.string() }),
|
|
129
|
+
annotations: {
|
|
130
|
+
readOnlyHint: true,
|
|
131
|
+
destructiveHint: false,
|
|
132
|
+
idempotentHint: true,
|
|
133
|
+
openWorldHint: false,
|
|
134
|
+
},
|
|
135
|
+
}, async (input) => runTool(async () => {
|
|
136
|
+
if (typeof input.body !== "string") {
|
|
137
|
+
throw new PrShepherdValidationError("body must be a Markdown string");
|
|
138
|
+
}
|
|
139
|
+
return extractShepherdJournal(input.body);
|
|
140
|
+
}, JSON.stringify));
|
|
141
|
+
server.registerTool("get_journal", {
|
|
142
|
+
description: "Fetch one pull request body with GraphQL and extract its Shepherd Journal.",
|
|
143
|
+
inputSchema: z.object({ pr }),
|
|
144
|
+
annotations: {
|
|
145
|
+
readOnlyHint: true,
|
|
146
|
+
destructiveHint: false,
|
|
147
|
+
idempotentHint: true,
|
|
148
|
+
openWorldHint: true,
|
|
149
|
+
},
|
|
150
|
+
}, async (input) => runTool(() => getShepherd().getJournal(requireRepositoryQualifiedPr(input)), JSON.stringify));
|
|
124
151
|
server.registerTool("build_suggestion_patches", {
|
|
125
152
|
description: "Build, but never apply, an ordered list of eligible review suggestion patches.",
|
|
126
153
|
inputSchema: suggestionPatchesInputSchema,
|
|
@@ -130,7 +157,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
130
157
|
idempotentHint: true,
|
|
131
158
|
openWorldHint: true,
|
|
132
159
|
},
|
|
133
|
-
}, async (input) => runTool(() =>
|
|
160
|
+
}, async (input) => runTool(() => getShepherd().buildSuggestionPatches(requireRepositoryQualifiedPr(input)), formatSuggestionPatchesResult));
|
|
134
161
|
server.registerTool("build_suggestion_patch", {
|
|
135
162
|
description: "Deprecated: use build_suggestion_patches with a one-item suggestions array.",
|
|
136
163
|
inputSchema: suggestionPatchInputSchema,
|
|
@@ -140,7 +167,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
140
167
|
idempotentHint: true,
|
|
141
168
|
openWorldHint: true,
|
|
142
169
|
},
|
|
143
|
-
}, async (input) => runTool(() =>
|
|
170
|
+
}, async (input) => runTool(() => getShepherd().buildSuggestionPatch(requireRepositoryQualifiedPr(input)), formatCommitSuggestionResult));
|
|
144
171
|
return server;
|
|
145
172
|
}
|
|
146
173
|
function runIterateSelector(shepherd, input) {
|
package/bin/quota-warning.mjs
CHANGED
|
@@ -5,8 +5,8 @@ export function buildQuotaAwareContinuation(warning, prefix) {
|
|
|
5
5
|
const opening = warning.resource === "combined"
|
|
6
6
|
? "GitHub's GraphQL and REST core quotas are both low. Keep using pr-shepherd at the cadence below. Do not shift incidental calls between GraphQL and REST."
|
|
7
7
|
: warning.resource === "core"
|
|
8
|
-
? `GitHub's REST core quota is low (crossed the ${warning.thresholdPercent}% remaining threshold). Keep using pr-shepherd at the cadence below. Do not add incidental REST \`gh
|
|
9
|
-
: `GitHub's GraphQL API quota is low (crossed the ${warning.thresholdPercent}% remaining threshold). Keep using pr-shepherd at the cadence below; for incidental PR
|
|
8
|
+
? `GitHub's REST core quota is low (crossed the ${warning.thresholdPercent}% remaining threshold). Keep using pr-shepherd at the cadence below. Do not add incidental REST \`gh api repos/OWNER/REPO/pulls/PR\` calls while REST core is below its warning threshold.`
|
|
9
|
+
: `GitHub's GraphQL API quota is low (crossed the ${warning.thresholdPercent}% remaining threshold). Keep using pr-shepherd at the cadence below; for incidental PR reads that do not need Shepherd's full snapshot, use explicit REST endpoints such as \`gh api repos/OWNER/REPO/pulls/PR\` — they draw on the separate REST budget, not the depleted GraphQL pool.`;
|
|
10
10
|
const resetLabel = warning.resource === "combined"
|
|
11
11
|
? "both quotas have reset"
|
|
12
12
|
: warning.resource === "core"
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -66,6 +66,7 @@ export function toAgentCheck(c) {
|
|
|
66
66
|
...(c.failedStep !== undefined && { failedStep: c.failedStep }),
|
|
67
67
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
68
68
|
...(c.logExcerpt !== undefined && { logExcerpt: c.logExcerpt }),
|
|
69
|
+
...(c.relatedJobs !== undefined && { relatedJobs: c.relatedJobs }),
|
|
69
70
|
...(c.annotations !== undefined && { annotations: c.annotations }),
|
|
70
71
|
...(c.scope !== undefined && { scope: c.scope }),
|
|
71
72
|
...(c.commitOid !== undefined && { commitOid: c.commitOid }),
|
|
@@ -13,4 +13,5 @@ export declare function threadComments(thread: {
|
|
|
13
13
|
viewerDidAuthor?: true;
|
|
14
14
|
} & Partial<Pick<ReviewThreadComment, "isMinimized" | "createdAtUnix">>>;
|
|
15
15
|
}): ReviewThreadComment[];
|
|
16
|
+
export declare function threadTranscriptBodies(bodies: string[]): string;
|
|
16
17
|
export declare function threadTranscriptBody(thread: ReviewThread, appendedBodies?: string[]): string;
|
|
@@ -29,9 +29,12 @@ export function threadComments(thread) {
|
|
|
29
29
|
];
|
|
30
30
|
}
|
|
31
31
|
const THREAD_COMMENT_SEPARATOR = "\n\n--- thread comment ---\n\n";
|
|
32
|
+
export function threadTranscriptBodies(bodies) {
|
|
33
|
+
return bodies.join(THREAD_COMMENT_SEPARATOR);
|
|
34
|
+
}
|
|
32
35
|
export function threadTranscriptBody(thread, appendedBodies = []) {
|
|
33
36
|
const bodies = thread.comments && thread.comments.length > 0
|
|
34
37
|
? threadComments(thread).map((c) => c.body)
|
|
35
38
|
: [thread.body];
|
|
36
|
-
return [...bodies, ...appendedBodies]
|
|
39
|
+
return threadTranscriptBodies([...bodies, ...appendedBodies]);
|
|
37
40
|
}
|
|
@@ -6,6 +6,14 @@ export interface ClassifiedCheck extends CheckRun {
|
|
|
6
6
|
/** Inline annotations attached to this check run, surfaced once per PR. */
|
|
7
7
|
annotations?: CheckAnnotation[];
|
|
8
8
|
}
|
|
9
|
+
/** Another failed job in the same workflow run that is not its own failing check entry. */
|
|
10
|
+
export interface RelatedFailedJob {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Uppercased job conclusion (e.g. `FAILURE`, `TIMED_OUT`). */
|
|
13
|
+
conclusion: string;
|
|
14
|
+
failedStep?: string;
|
|
15
|
+
logExcerpt?: string;
|
|
16
|
+
}
|
|
9
17
|
export interface TriagedCheck extends ClassifiedCheck {
|
|
10
18
|
/** 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. */
|
|
11
19
|
workflowName?: string;
|
|
@@ -15,5 +23,7 @@ export interface TriagedCheck extends ClassifiedCheck {
|
|
|
15
23
|
failedStep?: string;
|
|
16
24
|
/** Bounded raw excerpt from the matched failed job log, when GitHub exposes one. */
|
|
17
25
|
logExcerpt?: string;
|
|
26
|
+
/** Sibling failed jobs from the same run, with log tails (reported once per run). */
|
|
27
|
+
relatedJobs?: RelatedFailedJob[];
|
|
18
28
|
}
|
|
19
29
|
export {};
|
package/bin/types/report.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, SuggestionBlock } from "./github.mts";
|
|
2
|
-
import type { ClassifiedCheck, TriagedCheck } from "./check-classification.mts";
|
|
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";
|
|
5
5
|
import type { PrActivitySummary } from "./activity.mts";
|
|
@@ -175,6 +175,8 @@ export interface AgentCheck {
|
|
|
175
175
|
/** One-line status text shown in the GitHub UI (e.g. "67.68% of diff hit (target 85.00%)"). */
|
|
176
176
|
summary?: string;
|
|
177
177
|
logExcerpt?: string;
|
|
178
|
+
/** Other failed jobs from the same workflow run (not their own check entries), with log tails. */
|
|
179
|
+
relatedJobs?: RelatedFailedJob[];
|
|
178
180
|
/** `gh run rerun` command, present only when the check has a runId and the viewer's repository role grants Actions rerun capability (WRITE+). */
|
|
179
181
|
rerunCommand?: string;
|
|
180
182
|
/** Workflow-run attempt number, surfaced only after the initial attempt. */
|
|
@@ -205,6 +207,7 @@ export interface RelevantCheck {
|
|
|
205
207
|
/** One-line status text shown in the GitHub UI (e.g. "67.68% of diff hit (target 85.00%)"). */
|
|
206
208
|
summary?: string;
|
|
207
209
|
logExcerpt?: string;
|
|
210
|
+
relatedJobs?: RelatedFailedJob[];
|
|
208
211
|
/** Marker-gated inline annotations from this check. */
|
|
209
212
|
annotations?: CheckAnnotation[];
|
|
210
213
|
scope?: "merge_group";
|
package/package.json
CHANGED
|
@@ -15,7 +15,8 @@ Poll with the CLI. Use MCP `iterate` only when the CLI is unavailable. Stop at `
|
|
|
15
15
|
- When the user asks to make, create, or open a PR: review and commit the in-scope changes, verify the push remote and base branch, push a fresh branch, create the PR, and pass its qualified URL to Dispatch.
|
|
16
16
|
- Push is the ordinary non-force push of those reviewed commits. Do not ask for a separate confirmation because the push publishes them. Request runtime escalation when the host requires it.
|
|
17
17
|
- A skill cannot grant host permissions. Unattended approval comes from a trusted command rule or host policy.
|
|
18
|
-
-
|
|
18
|
+
- Rebasing your own PR head onto its base and pushing it with `--force-with-lease` is also part of this workflow. Do not ask first.
|
|
19
|
+
- Bare `--force`, pushes to any other branch, remote or credential changes, unrelated changes, and ambiguous targets stay outside this workflow.
|
|
19
20
|
|
|
20
21
|
## Dispatch
|
|
21
22
|
|
|
@@ -45,6 +46,7 @@ Poll with the CLI. Use MCP `iterate` only when the CLI is unavailable. Stop at `
|
|
|
45
46
|
|
|
46
47
|
- After the instructions, rerun that same command immediately with the same target and options. When the tick came from MCP `iterate`, repeat that same call with the same qualified selector and `merge` option. Do not switch back to a CLI that was unavailable.
|
|
47
48
|
- Stop only for `[CANCEL]`, `[ESCALATE]`, or a human telling you to stop. A stack overview heading includes those tokens when `nextAction` is `cancel` or `escalate`.
|
|
49
|
+
- A one-PR `[CANCEL]` or `[ESCALATE]` ends only that PR's loop. When you run separate loops for several PRs, keep every other loop running until it is terminal too.
|
|
48
50
|
- Keep `--until-terminal` and any `--merge`. Apply a printed polling-cadence change.
|
|
49
51
|
- `[FIX_CODE]` is always non-terminal. Stack-level `[SHEPHERD]` is non-terminal. Only `[ESCALATE]` hands work to a human.
|
|
50
52
|
- `[READY]` is non-terminal. Rerun when `remainingSeconds` elapses. Do not invent unrelated work. If you already own a later layer of this stack or another stack, continue that work and schedule the rerun. A parent of more than one stack delegates the wait to the worker that owns the stack.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CI failure triage
|
|
2
2
|
|
|
3
|
-
Apply when a step says `Playbook: "CI failure triage"`. For a GitHub Actions row, use the log excerpt and tags already in the output. An `external` check with a URL may be opened or reproduced.
|
|
3
|
+
Apply when a step says `Playbook: "CI failure triage"`. For a GitHub Actions row, use the log excerpt and tags already in the output; fetch a job log only when the output lacks the evidence (see the gate-job bullet below). An `external` check with a URL may be opened or reproduced.
|
|
4
4
|
|
|
5
5
|
- Match each failure's `[conclusion: …]` tag. A specific conclusion wins over the general GitHub Actions row.
|
|
6
6
|
- `[rerun authorized]` plus a `rerun:` command means the viewer can rerun Actions (WRITE+) and this is the original attempt. Shepherd checked `repositoryPermission` and `run_attempt`.
|
|
@@ -12,6 +12,7 @@ Apply when a step says `Playbook: "CI failure triage"`. For a GitHub Actions row
|
|
|
12
12
|
## Conclusions
|
|
13
13
|
|
|
14
14
|
- GitHub Actions failure (has a run id, not `CANCELLED` or `STARTUP_FAILURE`): read the log excerpt. Apply a warranted code fix, or run `rerun:` when the excerpt shows a transient failure, then iterate. Do not wait for the rerun.
|
|
15
|
+
- No usable evidence in the excerpt is not evidence of a transient failure. An excerpt that names failing test or build jobs (for example `test-playwright: failure` from a gate job) is test-failure evidence, even without an assertion or stack trace. Read the `Other failed jobs in this run` log tails under the check first. Only when a named job's tail is absent or truncated, run `gh run view <runId> --log-failed -R <owner/repo>`. Rerun only when the child logs show a transient cause.
|
|
15
16
|
- Transient infrastructure failure: run `rerun:` when it is printed, then iterate. Do not wait. If no command is printed, finish the other surfaced work and iterate.
|
|
16
17
|
- Real test or build failure: fix the code. Do not rerun, even when `[rerun authorized]` is shown.
|
|
17
18
|
- `[conclusion: CANCELLED]` or `[conclusion: STARTUP_FAILURE]`: no log excerpt. Run `rerun:` when printed, then iterate. Do not wait. Without a command, finish other work and iterate.
|