pr-shepherd 0.49.0 → 0.51.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 +13 -6
- package/bin/cli/help-command-pages.d.mts +8 -8
- package/bin/cli/help-command-pages.mjs +8 -8
- package/bin/cli/help.d.mts +8 -8
- package/bin/cli/iterate-instructions.mjs +9 -2
- package/bin/cli/iterate-lean.mjs +11 -0
- package/bin/cli/poll-summary-emitter.mjs +15 -0
- package/bin/cli/poll-summary-formatter.mjs +10 -24
- package/bin/cli-parser.mjs +3 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -8
- package/bin/commands/iterate/check-instructions.mjs +2 -11
- package/bin/commands/iterate/escalate.mjs +30 -2
- package/bin/commands/iterate/fix-code.mjs +62 -20
- package/bin/commands/iterate/render.mjs +1 -1
- package/bin/commands/iterate/stall.mjs +30 -0
- package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
- package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
- package/bin/commands/poll-quota.d.mts +2 -1
- package/bin/commands/poll-quota.mjs +12 -0
- package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
- package/bin/commands/poll-summary-explicit-instructions.mjs +22 -0
- package/bin/commands/poll-summary-instructions.d.mts +3 -0
- package/bin/commands/poll-summary-instructions.mjs +139 -0
- package/bin/commands/poll-summary-signature.d.mts +2 -0
- package/bin/commands/poll-summary-signature.mjs +16 -0
- package/bin/commands/poll-summary.mjs +25 -39
- package/bin/commands/resolve-mutate.mjs +22 -68
- package/bin/comments/resolve.d.mts +5 -0
- package/bin/comments/resolve.mjs +2 -11
- package/bin/github/gql/poll-summary-fragment.gql +1 -0
- package/bin/github/poll-summary-projector.mjs +2 -1
- package/bin/github/poll-summary-raw.d.mts +1 -0
- package/bin/github/poll-summary-route.mjs +4 -1
- package/bin/github/poll-summary.d.mts +2 -1
- package/bin/github/poll-summary.mjs +19 -0
- package/bin/state/fix-attempts.d.mts +3 -4
- package/bin/state/fix-attempts.mjs +2 -3
- package/bin/types/escalate.d.mts +10 -0
- package/bin/types/poll-summary.d.mts +14 -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
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -11
- package/plugins/pr-shepherd/skills/reduce-pr-noise/SKILL.md +17 -0
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +24 -0
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/settings.md +27 -0
|
@@ -4,12 +4,13 @@ import { getRepoInfo } from "../github/client.mjs";
|
|
|
4
4
|
import { withApiTelemetryScope, summarizeApiTelemetry } from "../github/api-telemetry.mjs";
|
|
5
5
|
import { fetchPollSummary } from "../github/poll-summary.mjs";
|
|
6
6
|
import { sleep } from "../util/sleep.mjs";
|
|
7
|
-
import { graphqlQuotaPollIntervalMs, pollGraphQlRetryAfterMs } from "./poll-quota.mjs";
|
|
8
|
-
import {
|
|
7
|
+
import { aggregateQuotaWarning, graphqlQuotaPollIntervalMs, pollGraphQlRetryAfterMs, } from "./poll-quota.mjs";
|
|
8
|
+
import { withPollSummaryInstructions } from "./poll-summary-instructions.mjs";
|
|
9
|
+
import { summaryStatusSignature } from "./poll-summary-signature.mjs";
|
|
9
10
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
10
11
|
const TIMER_DRIFT_TOLERANCE_MS = 500;
|
|
11
12
|
export function runPollSummary(opts) {
|
|
12
|
-
return withApiTelemetryScope(async () => attachUsage(await runPollSummaryCore(opts)));
|
|
13
|
+
return withApiTelemetryScope(async () => attachUsage(await runPollSummaryCore(opts), opts.merge));
|
|
13
14
|
}
|
|
14
15
|
export function runAggregatePoll(opts) {
|
|
15
16
|
return withApiTelemetryScope(() => runAggregatePollCore(opts));
|
|
@@ -19,13 +20,14 @@ async function runPollSummaryCore(opts) {
|
|
|
19
20
|
const fetched = await fetchPollSummary(opts, repo);
|
|
20
21
|
const allTerminal = fetched.prs.every((item) => item.action === "cancel");
|
|
21
22
|
const actionable = fetched.prs.some((item) => item.action !== "wait" && item.action !== "cancel");
|
|
22
|
-
return {
|
|
23
|
+
return withPollSummaryInstructions({
|
|
23
24
|
mode: "summary",
|
|
24
25
|
repo: `${repo.owner}/${repo.name}`,
|
|
25
26
|
selection: fetched.selection,
|
|
26
27
|
reason: allTerminal ? "all_terminal" : actionable ? "actionable" : "waiting",
|
|
27
28
|
prs: fetched.prs,
|
|
28
|
-
|
|
29
|
+
...(fetched.stackAncestry?.length && { stackAncestry: fetched.stackAncestry }),
|
|
30
|
+
}, opts.merge === true);
|
|
29
31
|
}
|
|
30
32
|
async function runAggregatePollCore(opts) {
|
|
31
33
|
const intervalMs = Math.min(opts.intervalSeconds * 1000, MAX_TIMER_MS);
|
|
@@ -60,7 +62,7 @@ async function runAggregatePollCore(opts) {
|
|
|
60
62
|
...explicit,
|
|
61
63
|
reason: "all_terminal",
|
|
62
64
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
63
|
-
});
|
|
65
|
+
}, opts.merge);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
const retryMs = opts.untilTerminal ? pollGraphQlRetryAfterMs(error) : null;
|
|
@@ -71,9 +73,15 @@ async function runAggregatePollCore(opts) {
|
|
|
71
73
|
await sleep(retryMs);
|
|
72
74
|
continue;
|
|
73
75
|
}
|
|
74
|
-
const allTerminal = last.
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const allTerminal = last.selection.kind === "stack"
|
|
77
|
+
? last.nextAction === "cancel"
|
|
78
|
+
: last.prs.every((item) => item.action === "cancel");
|
|
79
|
+
const immediate = last.selection.kind === "stack"
|
|
80
|
+
? ["escalate", "merge", "mark_ready"].includes(last.nextAction ?? "wait")
|
|
81
|
+
: last.prs.some((item) => ["escalate", "merge", "mark_ready"].includes(item.action));
|
|
82
|
+
const hasFix = last.selection.kind === "stack"
|
|
83
|
+
? last.nextAction === "fix_code"
|
|
84
|
+
: last.prs.some((item) => item.action === "fix_code");
|
|
77
85
|
const warning = await aggregateQuotaWarning(last, quotaBands, opts.intervalSeconds);
|
|
78
86
|
if (warning)
|
|
79
87
|
pendingQuotaWarning = warning;
|
|
@@ -82,14 +90,14 @@ async function runAggregatePollCore(opts) {
|
|
|
82
90
|
...last,
|
|
83
91
|
reason: "all_terminal",
|
|
84
92
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
85
|
-
});
|
|
93
|
+
}, opts.merge);
|
|
86
94
|
}
|
|
87
95
|
if (immediate) {
|
|
88
96
|
return attachUsage({
|
|
89
97
|
...last,
|
|
90
98
|
reason: "actionable",
|
|
91
99
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
92
|
-
});
|
|
100
|
+
}, opts.merge);
|
|
93
101
|
}
|
|
94
102
|
if (hasFix) {
|
|
95
103
|
if (debounceMs === 0)
|
|
@@ -97,19 +105,19 @@ async function runAggregatePollCore(opts) {
|
|
|
97
105
|
...last,
|
|
98
106
|
reason: "actionable",
|
|
99
107
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
100
|
-
});
|
|
108
|
+
}, opts.merge);
|
|
101
109
|
debounceUntil ??= Date.now() + debounceMs;
|
|
102
110
|
if (Date.now() >= debounceUntil)
|
|
103
111
|
return attachUsage({
|
|
104
112
|
...last,
|
|
105
113
|
reason: "actionable",
|
|
106
114
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
107
|
-
});
|
|
115
|
+
}, opts.merge);
|
|
108
116
|
}
|
|
109
117
|
else {
|
|
110
118
|
debounceUntil = null;
|
|
111
119
|
if (opts.untilTerminal && pendingQuotaWarning) {
|
|
112
|
-
return attachUsage({ ...last, quotaWarning: pendingQuotaWarning });
|
|
120
|
+
return attachUsage({ ...last, quotaWarning: pendingQuotaWarning }, opts.merge);
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
const elapsedMs = Date.now() - start;
|
|
@@ -119,7 +127,7 @@ async function runAggregatePollCore(opts) {
|
|
|
119
127
|
if (!opts.untilTerminal && debounceUntil === null) {
|
|
120
128
|
const remainingMs = timeoutMs - elapsedMs;
|
|
121
129
|
if (remainingMs <= 0 || remainingMs + TIMER_DRIFT_TOLERANCE_MS < sleepMs) {
|
|
122
|
-
return attachUsage({ ...last, reason: "timeout" });
|
|
130
|
+
return attachUsage({ ...last, reason: "timeout" }, opts.merge);
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
const statusSignature = summaryStatusSignature(last);
|
|
@@ -132,32 +140,10 @@ async function runAggregatePollCore(opts) {
|
|
|
132
140
|
await sleep(sleepMs);
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
|
-
async function aggregateQuotaWarning(result, bands, intervalSeconds) {
|
|
136
|
-
const usage = summarizeApiTelemetry()?.graphql;
|
|
137
|
-
const [owner, repo] = result.repo.split("/");
|
|
138
|
-
if (!usage || !owner || !repo)
|
|
139
|
-
return undefined;
|
|
140
|
-
return evaluateWorktreeGraphqlQuotaWarning({ owner, repo }, bands.map((band) => ({
|
|
141
|
-
...band,
|
|
142
|
-
pollIntervalMinutes: Math.max(band.pollIntervalMinutes, intervalSeconds / 60),
|
|
143
|
-
})), usage, true);
|
|
144
|
-
}
|
|
145
|
-
function summaryStatusSignature(result) {
|
|
146
|
-
return JSON.stringify(result.prs.map((item) => ({
|
|
147
|
-
pr: item.pr,
|
|
148
|
-
action: item.action,
|
|
149
|
-
state: item.state,
|
|
150
|
-
mergeable: item.mergeable,
|
|
151
|
-
mergeStateStatus: item.mergeStateStatus,
|
|
152
|
-
reviewDecision: item.reviewDecision,
|
|
153
|
-
checks: item.checks,
|
|
154
|
-
review: item.review,
|
|
155
|
-
})));
|
|
156
|
-
}
|
|
157
143
|
function isMissingStack(error) {
|
|
158
144
|
return (error instanceof ShepherdError && error.message.includes("not part of a native GitHub stack"));
|
|
159
145
|
}
|
|
160
|
-
function attachUsage(result) {
|
|
146
|
+
function attachUsage(result, mergeRequested) {
|
|
161
147
|
const apiUsage = summarizeApiTelemetry();
|
|
162
|
-
return apiUsage ? { ...result, apiUsage } : result;
|
|
148
|
+
return withPollSummaryInstructions(apiUsage ? { ...result, apiUsage } : result, mergeRequested === true);
|
|
163
149
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
2
2
|
import { applyResolveOptions } from "../comments/resolve.mjs";
|
|
3
3
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
4
|
-
import { loadConfig } from "../config/load.mjs";
|
|
5
|
-
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
6
|
-
import { shouldResolveOtherHumanThread } from "./iterate/thread-mutation-routing.mjs";
|
|
7
4
|
import { markReplySeen } from "../state/seen-comments.mjs";
|
|
8
5
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
9
|
-
import { addPrShepherdMarker
|
|
6
|
+
import { addPrShepherdMarker } from "../comments/marker.mjs";
|
|
10
7
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
11
8
|
/** @deprecated Hidden implementation for `resolve`; use `apply review`. */
|
|
12
9
|
export async function runResolveMutate(opts) {
|
|
@@ -15,73 +12,30 @@ export async function runResolveMutate(opts) {
|
|
|
15
12
|
if (prNumber === null) {
|
|
16
13
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// authoritative and this path must not second-guess that intent.
|
|
34
|
-
const requestedReplyIds = new Set(opts.replyThreadIds ?? []);
|
|
35
|
-
const policy = config.iterate?.resolveOtherHumanThreads ?? "none";
|
|
36
|
-
const allowedHumanResolveIds = new Set(data.reviewThreads
|
|
37
|
-
.filter((thread) => {
|
|
38
|
-
if (!humanThreadIds.has(thread.id))
|
|
39
|
-
return false;
|
|
40
|
-
const paired = requestedReplyIds.has(thread.id) || threadEndedByShepherd(thread);
|
|
41
|
-
if (!paired)
|
|
42
|
-
return false;
|
|
43
|
-
if (isViewerAuthoredHuman(thread, botUsernames))
|
|
44
|
-
return true;
|
|
45
|
-
return shouldResolveOtherHumanThread(thread, policy);
|
|
46
|
-
})
|
|
47
|
-
.map((thread) => thread.id));
|
|
48
|
-
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) => !humanThreadIds.has(id) || allowedHumanResolveIds.has(id));
|
|
49
|
-
const skippedHumanResolves = (opts.resolveThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !allowedHumanResolveIds.has(id));
|
|
50
|
-
const knownThreadIds = new Set(data.reviewThreads.map((thread) => thread.id));
|
|
51
|
-
const replyThreadIds = opts.replyThreadIds?.filter((id) => knownThreadIds.has(id));
|
|
52
|
-
const skippedNonHumanReplies = (opts.replyThreadIds ?? []).filter((id) => !knownThreadIds.has(id));
|
|
53
|
-
const minimizeCommentIds = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id));
|
|
54
|
-
const skippedHumanMinimizes = (opts.minimizeCommentIds ?? []).filter((id) => humanCommentIds.has(id) || humanReviewIds.has(id));
|
|
55
|
-
const dismissReviewIds = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && data.changesRequestedReviews.some((review) => review.id === id));
|
|
56
|
-
const skippedHumanDismissals = (opts.dismissReviewIds ?? []).filter((id) => humanReviewIds.has(id));
|
|
57
|
-
const skippedIneligibleDismissals = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && !dismissReviewIds.includes(id));
|
|
58
|
-
const hasMutation = resolveThreadIds.length > 0 ||
|
|
59
|
-
(replyThreadIds?.length ?? 0) > 0 ||
|
|
60
|
-
minimizeCommentIds.length > 0 ||
|
|
61
|
-
dismissReviewIds.length > 0;
|
|
15
|
+
// Fetch only to retain the pre-reply transcript for successful-reply seen
|
|
16
|
+
// markers. It never determines which user-supplied IDs are sent to GitHub.
|
|
17
|
+
let threadById;
|
|
18
|
+
if (opts.replyThreadIds?.length) {
|
|
19
|
+
try {
|
|
20
|
+
threadById = new Map((await fetchPrBatch(prNumber, repo, { paginateApprovedReviews: true })).data.reviewThreads.map((thread) => [thread.id, thread]));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Seen-marker bookkeeping is best-effort. A failed read must not block
|
|
24
|
+
// the explicit mutation request; GitHub's mutation response is authoritative.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Iterate capability-checks and routes its generated commands. Direct apply
|
|
28
|
+
// requests are user-directed: forward every supplied ID unchanged and let
|
|
29
|
+
// GitHub report whether each requested mutation is permitted or applicable.
|
|
62
30
|
const result = await applyResolveOptions(prNumber, repo, {
|
|
63
|
-
resolveThreadIds,
|
|
64
|
-
replyThreadIds,
|
|
65
|
-
minimizeCommentIds,
|
|
66
|
-
dismissReviewIds,
|
|
31
|
+
resolveThreadIds: opts.resolveThreadIds,
|
|
32
|
+
replyThreadIds: opts.replyThreadIds,
|
|
33
|
+
minimizeCommentIds: opts.minimizeCommentIds,
|
|
34
|
+
dismissReviewIds: opts.dismissReviewIds,
|
|
67
35
|
dismissMessage: opts.dismissMessage,
|
|
68
|
-
requireSha:
|
|
36
|
+
requireSha: opts.requireSha,
|
|
69
37
|
});
|
|
70
|
-
if (
|
|
71
|
-
result.skippedHumanResolves = skippedHumanResolves;
|
|
72
|
-
if (skippedHumanMinimizes.length > 0)
|
|
73
|
-
result.skippedHumanMinimizes = skippedHumanMinimizes;
|
|
74
|
-
if (skippedHumanDismissals.length > 0)
|
|
75
|
-
result.skippedHumanDismissals = skippedHumanDismissals;
|
|
76
|
-
if (skippedNonHumanReplies.length > 0)
|
|
77
|
-
result.skippedNonHumanReplies = skippedNonHumanReplies;
|
|
78
|
-
if (skippedIneligibleDismissals.length > 0) {
|
|
79
|
-
result.skippedDismissals = [
|
|
80
|
-
...(result.skippedDismissals ?? []),
|
|
81
|
-
...skippedIneligibleDismissals,
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
if (opts.dismissMessage) {
|
|
38
|
+
if (opts.dismissMessage && threadById) {
|
|
85
39
|
const markedMessage = addPrShepherdMarker(opts.dismissMessage);
|
|
86
40
|
await Promise.all(result.repliedThreads.map((id) => {
|
|
87
41
|
const thread = threadById.get(id);
|
|
@@ -7,10 +7,15 @@ export interface ResolveResult {
|
|
|
7
7
|
minimizedComments: string[];
|
|
8
8
|
dismissedReviews: string[];
|
|
9
9
|
errors: string[];
|
|
10
|
+
/** @deprecated Direct apply forwards every supplied dismissal ID to GitHub. */
|
|
10
11
|
skippedDismissals?: string[];
|
|
12
|
+
/** @deprecated Direct apply no longer applies author policy. */
|
|
11
13
|
skippedHumanResolves?: string[];
|
|
14
|
+
/** @deprecated Direct apply no longer applies author policy. */
|
|
12
15
|
skippedHumanMinimizes?: string[];
|
|
16
|
+
/** @deprecated Direct apply no longer applies author policy. */
|
|
13
17
|
skippedHumanDismissals?: string[];
|
|
18
|
+
/** @deprecated Direct apply forwards every supplied reply ID to GitHub. */
|
|
14
19
|
skippedNonHumanReplies?: string[];
|
|
15
20
|
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
16
21
|
skippedUnauthorizedReplies?: string[];
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -35,9 +35,6 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
35
35
|
const replyThreadIds = dedupeIds(opts.replyThreadIds ?? []);
|
|
36
36
|
const minimizeCommentIds = opts.minimizeCommentIds ?? [];
|
|
37
37
|
const dismissReviewIds = dedupeIds(opts.dismissReviewIds ?? []);
|
|
38
|
-
const minimizeCommentIdSet = new Set(minimizeCommentIds);
|
|
39
|
-
const filteredDismissReviewIds = dismissReviewIds.filter((id) => !minimizeCommentIdSet.has(id));
|
|
40
|
-
const overlappingDismissIds = dismissReviewIds.filter((id) => minimizeCommentIdSet.has(id));
|
|
41
38
|
const result = {
|
|
42
39
|
repliedThreads: [],
|
|
43
40
|
resolvedThreads: [],
|
|
@@ -45,13 +42,7 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
45
42
|
dismissedReviews: [],
|
|
46
43
|
errors: [],
|
|
47
44
|
};
|
|
48
|
-
if (
|
|
49
|
-
result.skippedDismissals = [];
|
|
50
|
-
for (const id of overlappingDismissIds) {
|
|
51
|
-
result.skippedDismissals.push(id);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if ((filteredDismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
|
|
45
|
+
if ((dismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
|
|
55
46
|
throw new Error("--message is required when replying to threads or dismissing reviews");
|
|
56
47
|
}
|
|
57
48
|
if (opts.requireSha) {
|
|
@@ -59,7 +50,7 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
59
50
|
// before reviewers see the fix.
|
|
60
51
|
await waitForSha(pr, repo, opts.requireSha);
|
|
61
52
|
}
|
|
62
|
-
await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds,
|
|
53
|
+
await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds, dismissReviewIds, opts.dismissMessage ?? "", result);
|
|
63
54
|
return result;
|
|
64
55
|
}
|
|
65
56
|
/** @deprecated Compatibility alias; use `autoResolveThreads`. */
|
|
@@ -55,7 +55,8 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
55
55
|
...(Object.keys(checks).length > 0 && { checks }),
|
|
56
56
|
...(Object.keys(review).length > 0 && { review }),
|
|
57
57
|
...(stack && { stack }),
|
|
58
|
-
...(!["wait", "cancel"].includes(action) &&
|
|
58
|
+
...(!["wait", "cancel"].includes(action) &&
|
|
59
|
+
!(opts.stackPrNumber !== undefined && raw.stack && action === "merge") && {
|
|
59
60
|
pollCommand: buildPollCommand(repoName, raw.number, opts),
|
|
60
61
|
}),
|
|
61
62
|
};
|
|
@@ -35,9 +35,12 @@ export function routePollSummary(raw, checks, review, opts) {
|
|
|
35
35
|
if (opts.merge && raw.isInMergeQueue) {
|
|
36
36
|
return { action: "wait", reasons: ["already-in-merge-queue"] };
|
|
37
37
|
}
|
|
38
|
-
if (opts.merge && raw.stack) {
|
|
38
|
+
if (opts.merge && raw.stack && opts.stackPrNumber === undefined) {
|
|
39
39
|
return { action: "fix_code", reasons: ["authoritative-poll-required"] };
|
|
40
40
|
}
|
|
41
|
+
if (opts.merge && raw.stack) {
|
|
42
|
+
return { action: "merge", reasons: ["appears-ready"] };
|
|
43
|
+
}
|
|
41
44
|
if (opts.merge && !raw.stack)
|
|
42
45
|
return { action: "merge", reasons: ["appears-ready"] };
|
|
43
46
|
return { action: "cancel", reasons: ["appears-ready"] };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { PollSummaryCommandOptions, PollSummaryItem, PollSummarySelection } from "../types.mts";
|
|
1
|
+
import type { PollSummaryCommandOptions, PollSummaryItem, PollSummarySelection, PollSummaryStackAncestry } from "../types.mts";
|
|
2
2
|
import { type RepoInfo } from "./client.mts";
|
|
3
3
|
export interface FetchedPollSummary {
|
|
4
4
|
selection: PollSummarySelection;
|
|
5
5
|
prs: PollSummaryItem[];
|
|
6
|
+
stackAncestry?: PollSummaryStackAncestry[];
|
|
6
7
|
}
|
|
7
8
|
export declare function fetchPollSummary(opts: PollSummaryCommandOptions, repo: RepoInfo): Promise<FetchedPollSummary>;
|
|
@@ -97,9 +97,28 @@ async function fetchStackSummary(opts, repo) {
|
|
|
97
97
|
throw new ShepherdError(`GitHub returned incomplete stack membership (${unique.size} of ${stackSize} entries)`, EXIT.TEMPFAIL);
|
|
98
98
|
}
|
|
99
99
|
const ordered = [...unique.values()].sort((left, right) => left.position - right.position);
|
|
100
|
+
const stackAncestry = [];
|
|
101
|
+
for (let index = 1; index < ordered.length; index++) {
|
|
102
|
+
const parent = ordered[index - 1].pullRequest;
|
|
103
|
+
const child = ordered[index].pullRequest;
|
|
104
|
+
if (parent.state !== "OPEN" || child.state !== "OPEN")
|
|
105
|
+
continue;
|
|
106
|
+
if (child.baseRefName === parent.headRefName && child.baseRefOid === parent.headRefOid) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
stackAncestry.push({
|
|
110
|
+
parentPr: parent.number,
|
|
111
|
+
parentHeadRefName: parent.headRefName,
|
|
112
|
+
parentHeadRefOid: parent.headRefOid,
|
|
113
|
+
childPr: child.number,
|
|
114
|
+
childBaseRefName: child.baseRefName,
|
|
115
|
+
childBaseRefOid: child.baseRefOid,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
100
118
|
return {
|
|
101
119
|
selection: { kind: "stack", anchor, stackNumber, stackSize },
|
|
102
120
|
prs: await Promise.all(ordered.map((entry) => summarizePollSummaryPr(entry.pullRequest, repo, opts, viewerCanAdminister))),
|
|
121
|
+
...(stackAncestry.length > 0 && { stackAncestry }),
|
|
103
122
|
};
|
|
104
123
|
}
|
|
105
124
|
function deduplicate(values) {
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent attempt counter for the iterate escalation guard.
|
|
3
3
|
*
|
|
4
|
-
* Tracks how many times each review thread has been dispatched to
|
|
5
|
-
* handler without being resolved.
|
|
6
|
-
* commit SHA changes (i.e. a new push landed).
|
|
4
|
+
* Tracks how many caller-visible times each review thread has been dispatched to
|
|
5
|
+
* the fix_code handler without being resolved. Body edits reset the count.
|
|
7
6
|
*
|
|
8
7
|
* State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
|
|
9
8
|
*/
|
|
10
9
|
export interface FixAttemptsState {
|
|
11
|
-
/** HEAD SHA at the time the counts were last written
|
|
10
|
+
/** HEAD SHA at the time the counts were last written, retained for observability/compatibility. */
|
|
12
11
|
headSha: string;
|
|
13
12
|
/** Map from thread ID → number of fix_code dispatches that included this thread. */
|
|
14
13
|
threadAttempts: Record<string, number>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent attempt counter for the iterate escalation guard.
|
|
3
3
|
*
|
|
4
|
-
* Tracks how many times each review thread has been dispatched to
|
|
5
|
-
* handler without being resolved.
|
|
6
|
-
* commit SHA changes (i.e. a new push landed).
|
|
4
|
+
* Tracks how many caller-visible times each review thread has been dispatched to
|
|
5
|
+
* the fix_code handler without being resolved. Body edits reset the count.
|
|
7
6
|
*
|
|
8
7
|
* State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
|
|
9
8
|
*/
|
package/bin/types/escalate.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentCheck, AgentComment, AgentThread } from "./report.mts";
|
|
2
|
+
import type { ResolveCommand } from "./iterate.mts";
|
|
2
3
|
import type { CheckStatus, Review } from "./github.mts";
|
|
3
4
|
import type { MergeQueueRemovalStatus, StackStatus } from "./merge-requirements.mts";
|
|
4
5
|
export type EscalateTrigger = "fix-thrash" | "base-branch-unknown" | "stall-timeout" | "check-follow-up-unavailable" | "authorization-required" | "bot-cr-not-dismissed" | "merge-queue-removed" | "stacked-pr";
|
|
@@ -19,6 +20,10 @@ export interface EscalateDetails {
|
|
|
19
20
|
unresolvedThreads: AgentThread[];
|
|
20
21
|
ambiguousComments: AgentComment[];
|
|
21
22
|
changesRequestedReviews: Review[];
|
|
23
|
+
/** First-look review summaries that must be shown before any pending minimization. */
|
|
24
|
+
firstLookSummaries?: Review[];
|
|
25
|
+
/** Previously seen review summaries whose edited bodies must be shown again. */
|
|
26
|
+
editedSummaries?: Review[];
|
|
22
27
|
/** Failing checks whose next step requires human attention. */
|
|
23
28
|
checks?: AgentCheck[];
|
|
24
29
|
stalledChecks?: AgentStalledCheck[];
|
|
@@ -26,6 +31,11 @@ export interface EscalateDetails {
|
|
|
26
31
|
threadId: string;
|
|
27
32
|
attempts: number;
|
|
28
33
|
}>;
|
|
34
|
+
/** Review mutations generated for this tick, retained so an escalation cannot strand them. */
|
|
35
|
+
pendingReviewCommands?: {
|
|
36
|
+
resolveOnlyCommand?: ResolveCommand;
|
|
37
|
+
resolveCommand?: ResolveCommand;
|
|
38
|
+
};
|
|
29
39
|
suggestion: string;
|
|
30
40
|
humanMessage: string;
|
|
31
41
|
mergeQueueRemoval?: MergeQueueRemovalStatus;
|
|
@@ -18,6 +18,15 @@ export interface PollSummaryReview {
|
|
|
18
18
|
actionable?: number;
|
|
19
19
|
incomplete?: true;
|
|
20
20
|
}
|
|
21
|
+
/** The two GitHub refs at an adjacent, still-open stack boundary. */
|
|
22
|
+
export interface PollSummaryStackAncestry {
|
|
23
|
+
parentPr: number;
|
|
24
|
+
parentHeadRefName: string;
|
|
25
|
+
parentHeadRefOid: string;
|
|
26
|
+
childPr: number;
|
|
27
|
+
childBaseRefName: string;
|
|
28
|
+
childBaseRefOid: string;
|
|
29
|
+
}
|
|
21
30
|
interface PollSummaryStack {
|
|
22
31
|
number: number;
|
|
23
32
|
size: number;
|
|
@@ -63,6 +72,11 @@ export interface PollSummaryResult {
|
|
|
63
72
|
selection: PollSummarySelection;
|
|
64
73
|
reason: "actionable" | "all_terminal" | "waiting" | "timeout";
|
|
65
74
|
prs: PollSummaryItem[];
|
|
75
|
+
/** Present only for native-stack boundaries whose recorded refs differ. */
|
|
76
|
+
stackAncestry?: PollSummaryStackAncestry[];
|
|
77
|
+
/** The next stack-level transition, which may differ from an individual row's hint. */
|
|
78
|
+
nextAction?: ShepherdAction;
|
|
79
|
+
instructions?: string[];
|
|
66
80
|
apiUsage?: ApiUsage;
|
|
67
81
|
quotaWarning?: GraphqlQuotaWarning;
|
|
68
82
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pr-shepherd
|
|
3
3
|
description: 'Create or iterate a GitHub pull request with pr-shepherd (MCP or CLI). Use for requests like "make a PR and use pr-shepherd", "iterate PR #123", or "run pr-shepherd until this PR is ready".'
|
|
4
4
|
user-invocable: true
|
|
5
|
-
argument-hint: "[PR number or URL] [--merge]"
|
|
5
|
+
argument-hint: "[PR number or URL | --stack PR] [--merge]"
|
|
6
6
|
allowed-tools: ["MCP", "Bash", "Read", "Grep", "Glob", "Edit", "Write"]
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -18,9 +18,9 @@ If the requested PR does not exist yet, review and commit the in-scope changes,
|
|
|
18
18
|
|
|
19
19
|
## Arguments: $ARGUMENTS
|
|
20
20
|
|
|
21
|
-
1. Parse optional PR numbers, repository-qualified `owner/repo#N` references, or GitHub PR URLs and an optional `--merge` flag from `$ARGUMENTS`; alternatively parse one `--stack PR` selector. Otherwise let pr-shepherd infer the current branch PR. Reject any remaining argument. Follow the target repository's local `AGENTS.md` and `CLAUDE.md` standards while making changes.
|
|
21
|
+
1. Parse optional PR numbers, repository-qualified `owner/repo#N` references, or GitHub PR URLs and an optional `--merge` flag from `$ARGUMENTS`; alternatively parse one `--stack PR` selector. A clear request to merge, land, or enqueue the selected PR or stack also opts into `--merge` without a literal flag; a request only to create or open a PR does not. When the user asks to shepherd or merge a native stack and supplies an anchor PR without a literal `--stack`, use that PR as the `--stack` selector. Otherwise let pr-shepherd infer the current branch PR. Reject any remaining argument. Follow the target repository's local `AGENTS.md` and `CLAUDE.md` standards while making changes.
|
|
22
22
|
|
|
23
|
-
2. For the CLI, convert supplied `owner/repo#N` references to `https://github.com/owner/repo/pull/N`; otherwise pass supplied URLs or bare numbers unchanged, then run `pr-shepherd [PR ...] --until-terminal`, or `pr-shepherd --stack PR --until-terminal` for a stack, omitting `[PR ...]` when none was supplied and appending `--merge` when requested. This command keeps ordinary `[WAIT]` and `[MARK_READY]` ticks inside the same invocation; aggregate selectors return when any row needs work or all rows are terminal. It also returns for a quota warning or an emitted `[MERGE]` command, which is non-terminal and must run before the next invocation. A qualified reference may name a fork or upstream repository: it is the GitHub target, while the current checkout continues to supply local git/config/rules context. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, first repository-qualify every supplied reference with its GitHub URL or `owner/repo#N`; resolve bare numbers through `gh pr view <number> --json url --jq .url`, and resolve an omitted target with `gh pr view --json url --jq .url`. If that does not produce the required qualified selector, stop and report that MCP cannot safely determine it. Otherwise call `iterate` with `pr`, `prs`, or `stack` as selected, plus `merge: true` when
|
|
23
|
+
2. For the CLI, convert supplied `owner/repo#N` references to `https://github.com/owner/repo/pull/N`; otherwise pass supplied URLs or bare numbers unchanged, then run `pr-shepherd [PR ...] --until-terminal`, or `pr-shepherd --stack PR --until-terminal` for a stack, omitting `[PR ...]` when none was supplied and appending `--merge` when requested. This command keeps ordinary `[WAIT]` and `[MARK_READY]` ticks inside the same invocation; aggregate selectors return when any row needs work or all rows are terminal. It also returns for a quota warning or an emitted `[MERGE]` command, which is non-terminal and must run before the next invocation. A qualified reference may name a fork or upstream repository: it is the GitHub target, while the current checkout continues to supply local git/config/rules context. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, first repository-qualify every supplied reference with its GitHub URL or `owner/repo#N`; resolve bare numbers through `gh pr view <number> --json url --jq .url`, and resolve an omitted target with `gh pr view --json url --jq .url`. If that does not produce the required qualified selector, stop and report that MCP cannot safely determine it. Otherwise call `iterate` with `pr`, `prs`, or `stack` as selected, plus `merge: true` when merge intent was requested, and print its full result.
|
|
24
24
|
|
|
25
25
|
3. Print the full result and follow every returned `## Instructions` step exactly. For CLI output, run each printed mutation command when instructed. For MCP output, use MCP `apply` and `build_suggestion_patches` with the same qualified PR reference; do not run a shell `pr-shepherd apply` command.
|
|
26
26
|
|
|
@@ -51,7 +51,7 @@ annotations, or CI log excerpts.
|
|
|
51
51
|
- The command builds from the fetched PR head and accepts a clean local descendant only when the complete ordered patch stream passes `git apply --check`.
|
|
52
52
|
- If the command refuses because a suggestion is unsafe or no longer applies, inspect the current source, the displayed replacement block, and reviewer intent before editing manually. Do not apply a stale numeric range blindly or retry unchanged input.
|
|
53
53
|
- A returned patch was checked against the then-current worktree. If it later fails, re-inspect the worktree because it changed after validation.
|
|
54
|
-
-
|
|
54
|
+
- Use the generated thread IDs and flag placement returned with the patch command.
|
|
55
55
|
|
|
56
56
|
### CI failure triage
|
|
57
57
|
|
|
@@ -77,16 +77,11 @@ When several bullets share one runId (matrix jobs from the same run), the `rerun
|
|
|
77
77
|
|
|
78
78
|
Applies to every `apply review:` / `resolve-only:` command the CLI prints. Covers only what stays safe if you run the printed command **unmodified** — `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution remains a separate CLI-printed step because the command is unsafe by default without those placeholders.
|
|
79
79
|
|
|
80
|
-
The CLI only includes IDs whose per-object GitHub viewer capability and semantic routing authorize the corresponding generated action.
|
|
80
|
+
The CLI only includes IDs whose per-object GitHub viewer capability and semantic routing authorize the corresponding generated action. Generated commands are pre-populated; omission is not a prohibition. A separate, user-directed `apply review` request may supply any reply, resolve, minimize, or dismiss IDs; it forwards them without Shepherd author, capability, or current-state filtering, and GitHub's per-operation response is authoritative.
|
|
81
81
|
|
|
82
|
-
-
|
|
83
|
-
- Never add first-look-only or check-annotation IDs to `--reply-thread-ids`, `--resolve-thread-ids`, `--dismiss-review-ids`, or `--minimize-comment-ids` — those flags are pre-populated by the CLI.
|
|
82
|
+
- When `## Instructions` says to run a generated `apply review:` / `resolve-only:` command, run it even when no code change is warranted. An `[ESCALATE]` instruction may require user direction first. The command records the agent's disposition of the included review items; skipping it leaves authorized threads active and can eventually trigger `fix-thrash`.
|
|
84
83
|
- Keep every existing `--dismiss-review-ids` ID the CLI already included. Each is a bot or non-human review that must be dismissed; omitting one leaves the PR in `CHANGES_REQUESTED`.
|
|
85
84
|
|
|
86
|
-
### Review-mutation routing
|
|
87
|
-
|
|
88
|
-
For threads under both `## Review threads` and `## Review threads to resolve`, evaluate every thread before running mutations. Keep unmarked bot/non-human and viewer-authored IDs in both `--reply-thread-ids` and `--resolve-thread-ids`, including when the feedback is advisory, already satisfied, or otherwise warrants no code change: the reply runs before the resolve. Unmarked other-human IDs use `--reply-thread-ids` only unless the CLI also put them in `--resolve-thread-ids`. When the latest comment begins `<!-- pr-shepherd -->`, it is an established Shepherd reply—not merely a same-account comment. A marked thread that is still being resolved is resolve-only for retry. Do not add IDs the CLI omitted, and do not move IDs between flags.
|
|
89
|
-
|
|
90
85
|
### Shepherd Journal
|
|
91
86
|
|
|
92
87
|
Link threads and comments in a journal entry from their headings in the CLI output. Cite reviews by ID.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reduce-pr-noise
|
|
3
|
+
description: Reduce repetitive pr-shepherd output by configuring bot-comment classification rules or existing noise settings. Use for requests to silence a specific bot notice, quiet polling, trim CI logs, or tune comment visibility; use pr-shepherd for PR iteration itself.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: "[noisy bot comment or output]"
|
|
6
|
+
allowed-tools: ["Bash", "Read", "Grep", "Glob", "Edit", "Write"]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Reduce pr-shepherd noise
|
|
10
|
+
|
|
11
|
+
Identify the unwanted output from the request, a representative Shepherd result, and the current configuration. If a content-specific rule needs a message pattern and none is available, ask for an example before writing that rule.
|
|
12
|
+
|
|
13
|
+
- For a recurring bot message identified by its author and text, read [bot-comment classifiers](references/classifiers.md).
|
|
14
|
+
- For polling status, CI checks or logs, and broad comment visibility, read [noise settings](references/settings.md).
|
|
15
|
+
- Read both references only when the request needs both kinds of change.
|
|
16
|
+
|
|
17
|
+
Make the smallest change that addresses the observed noise. Use project-local files for project-specific policy; use the user's home configuration only when they request a personal default. Validate the match or setting and explain what will become less visible. If the user asks only how to configure it, give the relevant instructions without editing files.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Bot-comment classifiers
|
|
2
|
+
|
|
3
|
+
Use a classification rule when the unwanted item has a recognizable author and message pattern. Inspect a representative item first so the rule does not hide other feedback from the same bot. Prefer an existing settings control for polling- or output-wide behavior.
|
|
4
|
+
|
|
5
|
+
Place an `.mts` file in `.pr-shepherd/classification/` in the project. Shepherd uses the first classification directory on the working directory's ancestor chain; it stops at the home directory if reached, otherwise at the filesystem root. A home-level rule directory is not discovered when the project is outside the home tree. Rule directories do not merge. Files ending in `.ts`, `.mts`, `.mjs`, or `.js` load, except names beginning with `_` or `.`. An `.mts` rule can use erasable TypeScript syntax and `import type`, without transpilation-only features such as enums.
|
|
6
|
+
|
|
7
|
+
Each file default-exports a `ClassifyRule` from `pr-shepherd/classify`. A rule receives one of `review-thread`, `pr-comment`, `review-summary`, or `changes-requested`, with `author`, `authorType`, `body`, `id`, and optional `url` or thread `path`. Match the observed `kind`, login, and distinctive body text. For example:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import type { ClassifyRule } from "pr-shepherd/classify";
|
|
11
|
+
|
|
12
|
+
const rule: ClassifyRule = (item) => {
|
|
13
|
+
if (item.kind !== "pr-comment" && item.kind !== "review-summary") return null;
|
|
14
|
+
if (item.author.toLowerCase() !== "gemini-code-assist") return null;
|
|
15
|
+
if (!/^You have reached your daily quota limit\b/i.test(item.body)) return null;
|
|
16
|
+
return { suppress: true };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default rule;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`suppress: true` removes a matched item from agent output. Add `autoResolve: true` only when the requested policy also calls for resolving its thread or minimizing its comment or review summary. It is unsupported for `changes-requested` reviews, whose dismissal needs a message. With both flags, `actions.autoMinimizeSuppressed: true` lets Shepherd perform the authorized mutation silently; when GitHub does not confirm capability, the item returns to normal first-look visibility. Matching rules combine their flags, so inspect existing rules before adding one.
|
|
23
|
+
|
|
24
|
+
Exercise the exported rule against the observed item and negative examples: a different author, kind, and substantive message from the same bot. Confirm only the intended item matches before enabling automatic resolution. After editing a rule, restart a persistent MCP server or long-running poll process; it caches loaded rule modules. A new CLI process loads the change. A classifier does not retroactively remove already displayed output.
|