pr-shepherd 0.48.0 → 0.50.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 +14 -2
- package/bin/api.d.mts +19 -3
- package/bin/api.mjs +57 -8
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +1 -1
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/help-command-pages.d.mts +9 -9
- package/bin/cli/help-command-pages.mjs +8 -8
- 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 +5 -3
- package/bin/cli/help.d.mts +10 -10
- package/bin/cli/iterate-instructions.mjs +9 -2
- package/bin/cli/iterate-lean.mjs +11 -0
- package/bin/cli/poll-handler.mjs +25 -4
- package/bin/cli/poll-summary-emitter.d.mts +4 -0
- package/bin/cli/poll-summary-emitter.mjs +22 -0
- package/bin/cli/poll-summary-formatter.d.mts +2 -0
- package/bin/cli/poll-summary-formatter.mjs +96 -0
- package/bin/cli/poll-targets.d.mts +15 -0
- package/bin/cli/poll-targets.mjs +114 -0
- package/bin/cli/validate-default-args.mjs +1 -4
- 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-summary.d.mts +10 -0
- package/bin/commands/poll-summary.mjs +163 -0
- package/bin/commands/ready-delay.d.mts +3 -1
- package/bin/commands/ready-delay.mjs +3 -2
- 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-stack-summary.gql +33 -0
- package/bin/github/gql/poll-summary-fragment.gql +198 -0
- package/bin/github/poll-summary-checks.d.mts +3 -0
- package/bin/github/poll-summary-checks.mjs +61 -0
- package/bin/github/poll-summary-projector.d.mts +4 -0
- package/bin/github/poll-summary-projector.mjs +81 -0
- package/bin/github/poll-summary-raw.d.mts +134 -0
- package/bin/github/poll-summary-raw.mjs +1 -0
- package/bin/github/poll-summary-review.d.mts +4 -0
- package/bin/github/poll-summary-review.mjs +85 -0
- package/bin/github/poll-summary-route.d.mts +4 -0
- package/bin/github/poll-summary-route.mjs +47 -0
- package/bin/github/poll-summary.d.mts +7 -0
- package/bin/github/poll-summary.mjs +110 -0
- package/bin/github/queries.d.mts +4 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/mcp/server.mjs +39 -6
- package/bin/pr-reference.d.mts +2 -0
- package/bin/pr-reference.mjs +4 -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 +82 -0
- package/bin/types/poll-summary.mjs +1 -0
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -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 +5 -10
|
@@ -69,7 +69,7 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
69
69
|
instructions.push(`Apply every warranted review fix in ${filesRef}.`);
|
|
70
70
|
}
|
|
71
71
|
if (resolutionOnlyThreads.length > 0) {
|
|
72
|
-
instructions.push(
|
|
72
|
+
instructions.push("Review the threads under `## Review threads to resolve` before running the generated mutations.");
|
|
73
73
|
}
|
|
74
74
|
instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions);
|
|
75
75
|
if (hasAnnotations) {
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import { readStallState, writeStallState } from "../../state/iterate-stall.mjs";
|
|
2
3
|
import { toAgentThread, toAgentComment, toAgentStalledCheck } from "../../reporters/agent.mjs";
|
|
3
4
|
import { buildEscalateSuggestion, buildEscalateHumanMessage, formatDurationApprox, } from "./escalate.mjs";
|
|
4
5
|
import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
5
6
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
7
|
+
function pendingReviewCommandsFromResult(result) {
|
|
8
|
+
if (result.action !== "fix_code")
|
|
9
|
+
return undefined;
|
|
10
|
+
const pending = {
|
|
11
|
+
...(result.fix.resolveOnlyCommand?.hasMutations && {
|
|
12
|
+
resolveOnlyCommand: result.fix.resolveOnlyCommand,
|
|
13
|
+
}),
|
|
14
|
+
...(result.fix.resolveCommand.hasMutations && { resolveCommand: result.fix.resolveCommand }),
|
|
15
|
+
};
|
|
16
|
+
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
17
|
+
}
|
|
18
|
+
function surfacedSummariesFromResult(result) {
|
|
19
|
+
if (result.action !== "fix_code")
|
|
20
|
+
return {};
|
|
21
|
+
return {
|
|
22
|
+
...(result.fix.firstLookSummaries.length > 0 && {
|
|
23
|
+
firstLookSummaries: result.fix.firstLookSummaries,
|
|
24
|
+
}),
|
|
25
|
+
...(result.fix.editedSummaries.length > 0 && {
|
|
26
|
+
editedSummaries: result.fix.editedSummaries,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
6
30
|
function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds) {
|
|
7
31
|
const checks = [
|
|
8
32
|
...report.checks.failing.map((f) => `failing:${f.name}:${f.conclusion}:${f.runId ?? "no-run"}:${f.runAttempt ?? "unknown"}`),
|
|
@@ -44,13 +68,16 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
44
68
|
action: prospectiveResult.action,
|
|
45
69
|
});
|
|
46
70
|
if (stalledChecks.length > 0) {
|
|
71
|
+
const pending = pendingReviewCommandsFromResult(prospectiveResult);
|
|
47
72
|
const stalledDuration = formatDurationApprox(Math.max(...stalledChecks.map((c) => c.ageSeconds)));
|
|
48
73
|
const escalateBase = {
|
|
49
74
|
triggers: ["stall-timeout"],
|
|
50
75
|
unresolvedThreads: [],
|
|
51
76
|
ambiguousComments: [],
|
|
52
77
|
changesRequestedReviews: [],
|
|
78
|
+
...surfacedSummariesFromResult(prospectiveResult),
|
|
53
79
|
stalledChecks,
|
|
80
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
54
81
|
suggestion: buildEscalateSuggestion(["stall-timeout"], stalledDuration),
|
|
55
82
|
};
|
|
56
83
|
return {
|
|
@@ -76,11 +103,14 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
76
103
|
}
|
|
77
104
|
else if (ageSeconds >= stallTimeoutSeconds) {
|
|
78
105
|
const stalledDuration = formatDurationApprox(ageSeconds);
|
|
106
|
+
const pending = pendingReviewCommandsFromResult(prospectiveResult);
|
|
79
107
|
const escalateBase = {
|
|
80
108
|
triggers: ["stall-timeout"],
|
|
81
109
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
82
110
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
83
111
|
changesRequestedReviews: report.changesRequestedReviews,
|
|
112
|
+
...surfacedSummariesFromResult(prospectiveResult),
|
|
113
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
84
114
|
suggestion: buildEscalateSuggestion(["stall-timeout"], stalledDuration),
|
|
85
115
|
};
|
|
86
116
|
return {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { type NormalizedBotUsernames } from "../../comments/authors.mts";
|
|
2
|
-
import { shouldResolveOtherHumanThread } from "../../comments/thread-resolve-policy.mts";
|
|
3
2
|
import type { ResolveOtherHumanThreads } from "../../config/load.mts";
|
|
4
3
|
import type { AgentThread, ReviewThread } from "../../types.mts";
|
|
5
|
-
export { shouldResolveOtherHumanThread };
|
|
6
4
|
export type RoutableThread = AgentThread | ReviewThread;
|
|
7
5
|
export interface ThreadMutationRouting {
|
|
8
6
|
replyThreadIds: string[];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "../../comments/authors.mjs";
|
|
2
2
|
import { threadEndedByShepherd } from "../../comments/marker.mjs";
|
|
3
3
|
import { shouldResolveOtherHumanThread } from "../../comments/thread-resolve-policy.mjs";
|
|
4
|
-
export { shouldResolveOtherHumanThread };
|
|
5
4
|
function dedupeIds(ids) {
|
|
6
5
|
return [...new Set(ids)];
|
|
7
6
|
}
|
|
@@ -32,7 +31,7 @@ export function buildThreadMutationRouting(threads, botUsernames, ruleAutoResolv
|
|
|
32
31
|
.filter((thread) => shouldPairResolve(thread, botUsernames, policy) && !threadEndedByShepherd(thread))
|
|
33
32
|
.map((thread) => thread.id));
|
|
34
33
|
const pairedResolveIdSet = new Set(pairedResolveThreadIds);
|
|
35
|
-
// Rule-matched threads bypass
|
|
34
|
+
// Rule-matched threads bypass the generated-command author routing.
|
|
36
35
|
const standaloneResolveThreadIds = dedupeIds([
|
|
37
36
|
...threads
|
|
38
37
|
.filter((thread) => shouldPairResolve(thread, botUsernames, policy) && threadEndedByShepherd(thread))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PollSummaryCommandOptions, PollSummaryResult } from "../types.mts";
|
|
2
|
+
export interface AggregatePollCommandOptions extends PollSummaryCommandOptions {
|
|
3
|
+
intervalSeconds: number;
|
|
4
|
+
timeoutSeconds: number;
|
|
5
|
+
debounceSeconds?: number;
|
|
6
|
+
quietStatus?: boolean;
|
|
7
|
+
untilTerminal?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function runPollSummary(opts: PollSummaryCommandOptions): Promise<PollSummaryResult>;
|
|
10
|
+
export declare function runAggregatePoll(opts: AggregatePollCommandOptions): Promise<PollSummaryResult>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
import { ShepherdError } from "../exit-codes.mjs";
|
|
3
|
+
import { getRepoInfo } from "../github/client.mjs";
|
|
4
|
+
import { withApiTelemetryScope, summarizeApiTelemetry } from "../github/api-telemetry.mjs";
|
|
5
|
+
import { fetchPollSummary } from "../github/poll-summary.mjs";
|
|
6
|
+
import { sleep } from "../util/sleep.mjs";
|
|
7
|
+
import { graphqlQuotaPollIntervalMs, pollGraphQlRetryAfterMs } from "./poll-quota.mjs";
|
|
8
|
+
import { evaluateWorktreeGraphqlQuotaWarning } from "../state/graphql-quota-warnings.mjs";
|
|
9
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
10
|
+
const TIMER_DRIFT_TOLERANCE_MS = 500;
|
|
11
|
+
export function runPollSummary(opts) {
|
|
12
|
+
return withApiTelemetryScope(async () => attachUsage(await runPollSummaryCore(opts)));
|
|
13
|
+
}
|
|
14
|
+
export function runAggregatePoll(opts) {
|
|
15
|
+
return withApiTelemetryScope(() => runAggregatePollCore(opts));
|
|
16
|
+
}
|
|
17
|
+
async function runPollSummaryCore(opts) {
|
|
18
|
+
const repo = opts.targetRepository ?? (await getRepoInfo());
|
|
19
|
+
const fetched = await fetchPollSummary(opts, repo);
|
|
20
|
+
const allTerminal = fetched.prs.every((item) => item.action === "cancel");
|
|
21
|
+
const actionable = fetched.prs.some((item) => item.action !== "wait" && item.action !== "cancel");
|
|
22
|
+
return {
|
|
23
|
+
mode: "summary",
|
|
24
|
+
repo: `${repo.owner}/${repo.name}`,
|
|
25
|
+
selection: fetched.selection,
|
|
26
|
+
reason: allTerminal ? "all_terminal" : actionable ? "actionable" : "waiting",
|
|
27
|
+
prs: fetched.prs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function runAggregatePollCore(opts) {
|
|
31
|
+
const intervalMs = Math.min(opts.intervalSeconds * 1000, MAX_TIMER_MS);
|
|
32
|
+
const timeoutMs = Math.min(opts.timeoutSeconds * 1000, MAX_TIMER_MS);
|
|
33
|
+
const debounceMs = Math.min((opts.debounceSeconds ?? 60) * 1000, MAX_TIMER_MS);
|
|
34
|
+
const quotaBands = loadConfig().watch.graphqlQuotaWarnings;
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
let tick = 0;
|
|
37
|
+
let debounceUntil = null;
|
|
38
|
+
let last;
|
|
39
|
+
let lastStatusSignature = null;
|
|
40
|
+
let rateLimitRetries = 0;
|
|
41
|
+
let pendingQuotaWarning;
|
|
42
|
+
while (true) {
|
|
43
|
+
tick += 1;
|
|
44
|
+
try {
|
|
45
|
+
last = await runPollSummaryCore(opts);
|
|
46
|
+
rateLimitRetries = 0;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (last?.selection.kind === "stack" && isMissingStack(error)) {
|
|
50
|
+
const explicit = await runPollSummaryCore({
|
|
51
|
+
...opts,
|
|
52
|
+
stackPrNumber: undefined,
|
|
53
|
+
prNumbers: last.prs.map((item) => item.pr),
|
|
54
|
+
});
|
|
55
|
+
if (explicit.prs.every((item) => item.action === "cancel")) {
|
|
56
|
+
const warning = await aggregateQuotaWarning(explicit, quotaBands, opts.intervalSeconds);
|
|
57
|
+
if (warning)
|
|
58
|
+
pendingQuotaWarning = warning;
|
|
59
|
+
return attachUsage({
|
|
60
|
+
...explicit,
|
|
61
|
+
reason: "all_terminal",
|
|
62
|
+
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const retryMs = opts.untilTerminal ? pollGraphQlRetryAfterMs(error) : null;
|
|
67
|
+
if (retryMs === null || rateLimitRetries >= 1)
|
|
68
|
+
throw error;
|
|
69
|
+
rateLimitRetries += 1;
|
|
70
|
+
process.stderr.write(`[aggregate poll tick ${tick} / +${Math.round((Date.now() - start) / 1000)}s] GraphQL rate limit — retrying in ${Math.round(retryMs / 1000)}s\n`);
|
|
71
|
+
await sleep(retryMs);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const allTerminal = last.prs.every((item) => item.action === "cancel");
|
|
75
|
+
const immediate = last.prs.some((item) => ["escalate", "merge", "mark_ready"].includes(item.action));
|
|
76
|
+
const hasFix = last.prs.some((item) => item.action === "fix_code");
|
|
77
|
+
const warning = await aggregateQuotaWarning(last, quotaBands, opts.intervalSeconds);
|
|
78
|
+
if (warning)
|
|
79
|
+
pendingQuotaWarning = warning;
|
|
80
|
+
if (allTerminal) {
|
|
81
|
+
return attachUsage({
|
|
82
|
+
...last,
|
|
83
|
+
reason: "all_terminal",
|
|
84
|
+
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (immediate) {
|
|
88
|
+
return attachUsage({
|
|
89
|
+
...last,
|
|
90
|
+
reason: "actionable",
|
|
91
|
+
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (hasFix) {
|
|
95
|
+
if (debounceMs === 0)
|
|
96
|
+
return attachUsage({
|
|
97
|
+
...last,
|
|
98
|
+
reason: "actionable",
|
|
99
|
+
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
100
|
+
});
|
|
101
|
+
debounceUntil ??= Date.now() + debounceMs;
|
|
102
|
+
if (Date.now() >= debounceUntil)
|
|
103
|
+
return attachUsage({
|
|
104
|
+
...last,
|
|
105
|
+
reason: "actionable",
|
|
106
|
+
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
debounceUntil = null;
|
|
111
|
+
if (opts.untilTerminal && pendingQuotaWarning) {
|
|
112
|
+
return attachUsage({ ...last, quotaWarning: pendingQuotaWarning });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const elapsedMs = Date.now() - start;
|
|
116
|
+
const sleepMs = debounceUntil
|
|
117
|
+
? Math.min(intervalMs, Math.max(debounceUntil - Date.now(), 0))
|
|
118
|
+
: graphqlQuotaPollIntervalMs(quotaBands, summarizeApiTelemetry()?.graphql, intervalMs, MAX_TIMER_MS);
|
|
119
|
+
if (!opts.untilTerminal && debounceUntil === null) {
|
|
120
|
+
const remainingMs = timeoutMs - elapsedMs;
|
|
121
|
+
if (remainingMs <= 0 || remainingMs + TIMER_DRIFT_TOLERANCE_MS < sleepMs) {
|
|
122
|
+
return attachUsage({ ...last, reason: "timeout" });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const statusSignature = summaryStatusSignature(last);
|
|
126
|
+
if (!opts.quietStatus || hasFix || statusSignature !== lastStatusSignature) {
|
|
127
|
+
process.stderr.write(`[aggregate poll tick ${tick} / +${Math.round(elapsedMs / 1000)}s] ${last.prs
|
|
128
|
+
.map((item) => `#${item.pr} ${item.action.toUpperCase()}`)
|
|
129
|
+
.join(", ")}\n`);
|
|
130
|
+
}
|
|
131
|
+
lastStatusSignature = statusSignature;
|
|
132
|
+
await sleep(sleepMs);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
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
|
+
function isMissingStack(error) {
|
|
158
|
+
return (error instanceof ShepherdError && error.message.includes("not part of a native GitHub stack"));
|
|
159
|
+
}
|
|
160
|
+
function attachUsage(result) {
|
|
161
|
+
const apiUsage = summarizeApiTelemetry();
|
|
162
|
+
return apiUsage ? { ...result, apiUsage } : result;
|
|
163
|
+
}
|
|
@@ -25,5 +25,7 @@ interface ReadyDelayState {
|
|
|
25
25
|
* When `shouldCancel == true`, the formatter tells loop-capable agents to cancel
|
|
26
26
|
* the loop and tells one-shot agents to stop.
|
|
27
27
|
*/
|
|
28
|
-
export declare function updateReadyDelay(prNumber: number, isReady: boolean, readyDelaySeconds: number, owner: string, repo: string
|
|
28
|
+
export declare function updateReadyDelay(prNumber: number, isReady: boolean, readyDelaySeconds: number, owner: string, repo: string, options?: {
|
|
29
|
+
retainElapsed?: boolean;
|
|
30
|
+
}): Promise<ReadyDelayState>;
|
|
29
31
|
export {};
|
|
@@ -18,7 +18,7 @@ import { resolvePrStatePath } from "../state/base.mjs";
|
|
|
18
18
|
* When `shouldCancel == true`, the formatter tells loop-capable agents to cancel
|
|
19
19
|
* the loop and tells one-shot agents to stop.
|
|
20
20
|
*/
|
|
21
|
-
export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, owner, repo) {
|
|
21
|
+
export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, owner, repo, options = {}) {
|
|
22
22
|
const markerPath = readySincePath(prNumber, owner, repo);
|
|
23
23
|
if (!isReady) {
|
|
24
24
|
// Reset the timer.
|
|
@@ -47,7 +47,8 @@ export async function updateReadyDelay(prNumber, isReady, readyDelaySeconds, own
|
|
|
47
47
|
const elapsed = now - readySince;
|
|
48
48
|
const remaining = readyDelaySeconds - elapsed;
|
|
49
49
|
if (remaining <= 0) {
|
|
50
|
-
|
|
50
|
+
if (!options.retainElapsed)
|
|
51
|
+
await safeUnlink(markerPath);
|
|
51
52
|
return { isReady: true, shouldCancel: true, remainingSeconds: 0 };
|
|
52
53
|
}
|
|
53
54
|
return { isReady: true, shouldCancel: false, remainingSeconds: remaining };
|
|
@@ -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`. */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
query PollStackSummary($owner: String!, $repo: String!, $anchor: Int!, $after: String) {
|
|
2
|
+
_shepherdRateLimit: rateLimit {
|
|
3
|
+
cost
|
|
4
|
+
limit
|
|
5
|
+
nodeCount
|
|
6
|
+
remaining
|
|
7
|
+
resetAt
|
|
8
|
+
used
|
|
9
|
+
}
|
|
10
|
+
repository(owner: $owner, name: $repo) {
|
|
11
|
+
viewerCanAdminister
|
|
12
|
+
pullRequest(number: $anchor) {
|
|
13
|
+
stack {
|
|
14
|
+
id
|
|
15
|
+
number
|
|
16
|
+
size
|
|
17
|
+
baseRefName
|
|
18
|
+
entries(first: 50, after: $after) {
|
|
19
|
+
pageInfo {
|
|
20
|
+
hasNextPage
|
|
21
|
+
endCursor
|
|
22
|
+
}
|
|
23
|
+
nodes {
|
|
24
|
+
position
|
|
25
|
+
pullRequest {
|
|
26
|
+
...PollSummaryPr
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|