pr-shepherd 0.44.1 → 0.46.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 +19 -8
- package/bin/checks/conclusions.d.mts +8 -1
- package/bin/checks/conclusions.mjs +12 -0
- package/bin/checks/triage.d.mts +3 -2
- package/bin/checks/triage.mjs +44 -15
- package/bin/cli/api-usage-formatter.d.mts +3 -0
- package/bin/cli/api-usage-formatter.mjs +36 -0
- package/bin/cli/error-format.d.mts +13 -0
- package/bin/cli/error-format.mjs +41 -0
- package/bin/cli/fix-formatter.mjs +22 -5
- package/bin/cli/help-command-pages.d.mts +13 -0
- package/bin/cli/help-command-pages.mjs +13 -0
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +14 -1
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-activity-formatter.d.mts +2 -0
- package/bin/cli/iterate-activity-formatter.mjs +26 -0
- package/bin/cli/iterate-checks-formatter.d.mts +2 -0
- package/bin/cli/iterate-checks-formatter.mjs +55 -0
- package/bin/cli/iterate-formatter.mjs +17 -29
- package/bin/cli/iterate-instructions.mjs +14 -1
- package/bin/cli/iterate-lean.mjs +5 -0
- package/bin/cli/journal-extract-handler.d.mts +2 -0
- package/bin/cli/journal-extract-handler.mjs +48 -0
- package/bin/cli/mutate-formatter.mjs +2 -0
- package/bin/cli/safe-body-file.d.mts +14 -0
- package/bin/cli/safe-body-file.mjs +42 -0
- package/bin/cli-parser.mjs +7 -0
- package/bin/commands/check-annotations.d.mts +2 -1
- package/bin/commands/check-annotations.mjs +7 -7
- package/bin/commands/check-status.mjs +4 -4
- package/bin/commands/check.mjs +31 -9
- package/bin/commands/iterate/api-usage.d.mts +2 -0
- package/bin/commands/iterate/api-usage.mjs +35 -0
- package/bin/commands/iterate/check-instructions.d.mts +1 -1
- package/bin/commands/iterate/check-instructions.mjs +13 -18
- package/bin/commands/iterate/classify.mjs +2 -1
- package/bin/commands/iterate/escalate.mjs +63 -9
- package/bin/commands/iterate/fix-code.mjs +110 -73
- package/bin/commands/iterate/index.mjs +8 -5
- package/bin/commands/iterate/merge-state.mjs +5 -2
- package/bin/commands/iterate/render.mjs +13 -6
- package/bin/commands/iterate/run.d.mts +2 -0
- package/bin/commands/iterate/run.mjs +8 -0
- package/bin/commands/mark-files-as-viewed.mjs +8 -0
- package/bin/commands/poll-run.d.mts +2 -0
- package/bin/commands/poll-run.mjs +8 -0
- package/bin/commands/poll.d.mts +1 -2
- package/bin/commands/poll.mjs +26 -10
- package/bin/comments/rate-limit.d.mts +4 -0
- package/bin/comments/rate-limit.mjs +6 -0
- package/bin/comments/review-visibility.d.mts +1 -1
- package/bin/comments/review-visibility.mjs +3 -2
- package/bin/comments/thread-visibility.d.mts +1 -1
- package/bin/comments/thread-visibility.mjs +9 -3
- package/bin/config/load.d.mts +6 -1
- package/bin/config/load.mjs +33 -1
- package/bin/config.json +6 -1
- package/bin/github/api-telemetry-aggregate.d.mts +32 -0
- package/bin/github/api-telemetry-aggregate.mjs +84 -0
- package/bin/github/api-telemetry.d.mts +13 -0
- package/bin/github/api-telemetry.mjs +128 -0
- package/bin/github/check-annotations.d.mts +14 -1
- package/bin/github/check-annotations.mjs +29 -2
- package/bin/github/client.d.mts +1 -1
- package/bin/github/client.mjs +1 -1
- package/bin/github/errors.d.mts +2 -0
- package/bin/github/errors.mjs +2 -0
- package/bin/github/gql/batch-pr-page.gql +8 -0
- package/bin/github/gql/batch-pr.gql +8 -0
- package/bin/github/gql/check-run-annotations.gql +8 -0
- package/bin/github/gql/commit-check-contexts.gql +8 -0
- package/bin/github/gql/get-pr-body.gql +8 -0
- package/bin/github/gql/get-pr-head-sha.gql +8 -0
- package/bin/github/gql/pr-number-by-branch.gql +8 -0
- package/bin/github/gql/review-thread-comments.gql +8 -0
- package/bin/github/gql/suggestion-threads.gql +8 -0
- package/bin/github/graphql-http.mjs +63 -8
- package/bin/github/http-auth.d.mts +9 -1
- package/bin/github/http-auth.mjs +35 -14
- package/bin/github/http-intermediate.d.mts +9 -0
- package/bin/github/http-intermediate.mjs +20 -0
- package/bin/github/http-request.d.mts +1 -1
- package/bin/github/http-request.mjs +1 -1
- package/bin/github/http-utils.d.mts +6 -0
- package/bin/github/http-utils.mjs +10 -1
- package/bin/github/rest-http.d.mts +16 -1
- package/bin/github/rest-http.mjs +71 -8
- package/bin/github/rest-text.mjs +41 -5
- package/bin/index.mjs +2 -1
- package/bin/log/session.d.mts +11 -0
- package/bin/log/session.mjs +22 -0
- package/bin/mcp/server.mjs +16 -2
- package/bin/pr-reference.d.mts +1 -1
- package/bin/pr-reference.mjs +1 -1
- package/bin/quota-warning.d.mts +2 -0
- package/bin/quota-warning.mjs +6 -0
- package/bin/state/graphql-quota-claims.d.mts +2 -0
- package/bin/state/graphql-quota-claims.mjs +61 -0
- package/bin/state/graphql-quota-policy.d.mts +17 -0
- package/bin/state/graphql-quota-policy.mjs +43 -0
- package/bin/state/graphql-quota-warnings.d.mts +6 -0
- package/bin/state/graphql-quota-warnings.mjs +105 -0
- package/bin/state/rest-cache.d.mts +48 -0
- package/bin/state/rest-cache.mjs +91 -0
- package/bin/types/api-usage.d.mts +31 -0
- package/bin/types/api-usage.mjs +1 -0
- package/bin/types/escalate.d.mts +5 -3
- package/bin/types/iterate.d.mts +4 -1
- package/bin/types/report.d.mts +3 -1
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/package.json +2 -2
- 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 +24 -13
|
@@ -2,6 +2,61 @@ import { loadConfig } from "../../config/load.mjs";
|
|
|
2
2
|
function renderEscalateAuthor(item) {
|
|
3
3
|
return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
|
|
4
4
|
}
|
|
5
|
+
function renderCheckTarget(check) {
|
|
6
|
+
const parts = [];
|
|
7
|
+
if (check.runId)
|
|
8
|
+
parts.push(`run \`${check.runId}\``);
|
|
9
|
+
if (check.detailsUrl)
|
|
10
|
+
parts.push(`URL \`${check.detailsUrl}\``);
|
|
11
|
+
return parts.length > 0 ? parts.join(", ") : "no run ID or URL";
|
|
12
|
+
}
|
|
13
|
+
function renderCheckScope(check) {
|
|
14
|
+
if (!check.scope)
|
|
15
|
+
return "";
|
|
16
|
+
const commit = check.commitOid ? ` at \`${check.commitOid}\`` : "";
|
|
17
|
+
return `, scope \`${check.scope}\`${commit}`;
|
|
18
|
+
}
|
|
19
|
+
function renderBlockquoteLines(value, indent) {
|
|
20
|
+
return value.split("\n").map((line) => `${indent}> ${line}`);
|
|
21
|
+
}
|
|
22
|
+
function renderEscalateAnnotation(annotation) {
|
|
23
|
+
const start = annotation.startLine ?? annotation.endLine ?? "?";
|
|
24
|
+
const end = annotation.endLine ?? annotation.startLine ?? "?";
|
|
25
|
+
const range = start === end ? String(start) : `${start}-${end}`;
|
|
26
|
+
const columns = annotation.startColumn == null && annotation.endColumn == null
|
|
27
|
+
? ""
|
|
28
|
+
: `, columns ${annotation.startColumn ?? "?"}-${annotation.endColumn ?? "?"}`;
|
|
29
|
+
const title = annotation.title ? ` — ${annotation.title}` : "";
|
|
30
|
+
const link = annotation.blobUrl ? ` [source](${annotation.blobUrl})` : "";
|
|
31
|
+
const lines = [
|
|
32
|
+
` - annotation \`${annotation.id}\`${link} — \`${annotation.path}:${range}\` [${annotation.level}${columns}]${title}`,
|
|
33
|
+
...renderBlockquoteLines(annotation.message, " "),
|
|
34
|
+
];
|
|
35
|
+
if (annotation.rawDetails) {
|
|
36
|
+
lines.push(...renderBlockquoteLines(annotation.rawDetails, " "));
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
function renderEscalateCheck(check) {
|
|
41
|
+
const workflowPrefix = check.workflowName ? `${check.workflowName} › ` : "";
|
|
42
|
+
const jobLabel = check.jobName ?? check.name;
|
|
43
|
+
const conclusion = check.conclusion ?? "UNKNOWN";
|
|
44
|
+
const lines = [
|
|
45
|
+
`- ${renderCheckTarget(check)} — \`${workflowPrefix}${jobLabel}\` [conclusion: ${conclusion}]${renderCheckScope(check)}`,
|
|
46
|
+
];
|
|
47
|
+
if (check.failedStep)
|
|
48
|
+
lines.push(` > failed step: ${check.failedStep}`);
|
|
49
|
+
if (check.summary)
|
|
50
|
+
lines.push(` > ${check.summary}`);
|
|
51
|
+
if (check.logExcerpt)
|
|
52
|
+
lines.push(...renderBlockquoteLines(check.logExcerpt, " "));
|
|
53
|
+
if (check.rerunCommand)
|
|
54
|
+
lines.push(` rerun: \`${check.rerunCommand}\``);
|
|
55
|
+
for (const annotation of check.annotations ?? []) {
|
|
56
|
+
lines.push(...renderEscalateAnnotation(annotation));
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
5
60
|
export function checkEscalateTriggers(actionableThreads, threadAttempts) {
|
|
6
61
|
const triggers = [];
|
|
7
62
|
const maxAttempts = loadConfig().iterate.fixAttemptsPerThread;
|
|
@@ -10,11 +65,6 @@ export function checkEscalateTriggers(actionableThreads, threadAttempts) {
|
|
|
10
65
|
if (thrashThreads.length > 0) {
|
|
11
66
|
triggers.push("fix-thrash");
|
|
12
67
|
}
|
|
13
|
-
// Trigger 2: actionable thread has no file/line — cannot locate code to edit.
|
|
14
|
-
const unlocatable = actionableThreads.filter((t) => t.path === null || t.line === null);
|
|
15
|
-
if (unlocatable.length > 0) {
|
|
16
|
-
triggers.push("thread-missing-location");
|
|
17
|
-
}
|
|
18
68
|
return {
|
|
19
69
|
triggers,
|
|
20
70
|
thrashHistory: thrashThreads.length > 0
|
|
@@ -69,6 +119,7 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
69
119
|
const hasItems = escalate.unresolvedThreads.length > 0 ||
|
|
70
120
|
escalate.changesRequestedReviews.length > 0 ||
|
|
71
121
|
escalate.ambiguousComments.length > 0 ||
|
|
122
|
+
(escalate.checks?.length ?? 0) > 0 ||
|
|
72
123
|
(escalate.stalledChecks?.length ?? 0) > 0;
|
|
73
124
|
if (hasItems) {
|
|
74
125
|
lines.push("");
|
|
@@ -86,6 +137,9 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
86
137
|
}
|
|
87
138
|
if ((escalate.stalledChecks?.length ?? 0) > 0)
|
|
88
139
|
lines.push("");
|
|
140
|
+
for (const check of escalate.checks ?? []) {
|
|
141
|
+
lines.push(...renderEscalateCheck(check), "");
|
|
142
|
+
}
|
|
89
143
|
for (const t of escalate.unresolvedThreads) {
|
|
90
144
|
const loc = t.path ? `\`${t.path}:${t.line ?? "?"}\`` : "(no location)";
|
|
91
145
|
lines.push(`- thread \`${t.id}\` — ${loc} (${renderEscalateAuthor(t)}):`);
|
|
@@ -144,8 +198,11 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
144
198
|
return lines.join("\n");
|
|
145
199
|
}
|
|
146
200
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
201
|
+
if (triggers.includes("check-follow-up-unavailable")) {
|
|
202
|
+
return "One or more failing checks have no autonomous follow-up available. Use the displayed conclusion, run or URL, and included evidence to handle them manually.";
|
|
203
|
+
}
|
|
147
204
|
if (triggers.includes("authorization-required")) {
|
|
148
|
-
return "GitHub did not confirm that the current viewer may perform
|
|
205
|
+
return "GitHub did not confirm that the current viewer may perform the requested mark-ready or merge/enqueue operation. Ask a repository maintainer to perform that state change; Shepherd will not recommend a command that would be denied.";
|
|
149
206
|
}
|
|
150
207
|
if (triggers.includes("merge-queue-removed")) {
|
|
151
208
|
const reason = detail ? ` GitHub reason: ${detail}.` : "";
|
|
@@ -162,9 +219,6 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
162
219
|
if (triggers.includes("fix-thrash")) {
|
|
163
220
|
return "Same thread(s) reached the automated attempt limit — treat this as a manual handoff. Apply the fix by hand.";
|
|
164
221
|
}
|
|
165
|
-
if (triggers.includes("thread-missing-location")) {
|
|
166
|
-
return "Review thread has no file/line reference — automated location routing failed and manual handling is required.";
|
|
167
|
-
}
|
|
168
222
|
if (triggers.includes("bot-cr-not-dismissed")) {
|
|
169
223
|
const ids = detail ? ` (review IDs: ${detail})` : "";
|
|
170
224
|
return `Bot CHANGES_REQUESTED review(s) remained undismissed past the stall window${ids}. The agent likely dropped \`--dismiss-review-ids\` from a prior apply command. Dismiss the review(s) manually (or re-run \`pr-shepherd apply review\` with the IDs) to unblock the PR.`;
|
|
@@ -11,8 +11,23 @@ import { applyStallGuard } from "./stall.mjs";
|
|
|
11
11
|
import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
12
12
|
import { threadTranscriptBody } from "../../threads/transcript.mjs";
|
|
13
13
|
import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs";
|
|
14
|
+
import { canRerunWorkflows } from "../../checks/conclusions.mjs";
|
|
14
15
|
import { loadConfig } from "../../config/load.mjs";
|
|
15
16
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
17
|
+
function checkRequiresHumanFollowUp(check) {
|
|
18
|
+
if (check.rerunCommand)
|
|
19
|
+
return false;
|
|
20
|
+
if (check.conclusion === "ACTION_REQUIRED" ||
|
|
21
|
+
check.conclusion === "CANCELLED" ||
|
|
22
|
+
check.conclusion === "STARTUP_FAILURE")
|
|
23
|
+
return true;
|
|
24
|
+
// An external check's direct URL is actionable evidence: the agent can inspect the
|
|
25
|
+
// provider and/or reproduce the reported failure locally. Only a truly bare check
|
|
26
|
+
// has no autonomous investigation path.
|
|
27
|
+
if (check.runId === null)
|
|
28
|
+
return !check.detailsUrl?.trim();
|
|
29
|
+
return !check.logExcerpt?.trim();
|
|
30
|
+
}
|
|
16
31
|
function nextFixAttempts(stored, headSha, threads) {
|
|
17
32
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
18
33
|
const threadBodyHashes = stored?.threadBodyHashes
|
|
@@ -40,66 +55,17 @@ export async function handleFixCode(ctx) {
|
|
|
40
55
|
]);
|
|
41
56
|
const replyIdSet = new Set(routedThreadMutations.replyThreadIds);
|
|
42
57
|
const resolveIdSet = new Set(routedThreadMutations.resolveThreadIds);
|
|
43
|
-
const unauthorizedReplies = allThreads.filter((thread) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const unauthorizedResolves = allThreads.filter((thread) => report.viewerAuthorization !== undefined &&
|
|
47
|
-
resolveIdSet.has(thread.id) &&
|
|
48
|
-
thread.viewerCanResolve !== true);
|
|
49
|
-
const unauthorizedDismissals = report.changesRequestedReviews.filter((review) => report.viewerAuthorization !== undefined &&
|
|
50
|
-
(!isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames)) &&
|
|
58
|
+
const unauthorizedReplies = allThreads.filter((thread) => replyIdSet.has(thread.id) && thread.viewerCanReply !== true);
|
|
59
|
+
const unauthorizedResolves = allThreads.filter((thread) => resolveIdSet.has(thread.id) && thread.viewerCanResolve !== true);
|
|
60
|
+
const unauthorizedDismissals = report.changesRequestedReviews.filter((review) => (!isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames)) &&
|
|
51
61
|
report.viewerAuthorization?.viewerCanAdminister !== true);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
? [
|
|
55
|
-
{
|
|
56
|
-
action: "reply-thread",
|
|
57
|
-
targetIds: unauthorizedReplies.map((thread) => thread.id),
|
|
58
|
-
reason: "denied-or-unverifiable",
|
|
59
|
-
},
|
|
60
|
-
]
|
|
61
|
-
: []),
|
|
62
|
-
...(unauthorizedResolves.length > 0
|
|
63
|
-
? [
|
|
64
|
-
{
|
|
65
|
-
action: "resolve-thread",
|
|
66
|
-
targetIds: unauthorizedResolves.map((thread) => thread.id),
|
|
67
|
-
reason: "denied-or-unverifiable",
|
|
68
|
-
},
|
|
69
|
-
]
|
|
70
|
-
: []),
|
|
71
|
-
...(unauthorizedDismissals.length > 0
|
|
72
|
-
? [
|
|
73
|
-
{
|
|
74
|
-
action: "dismiss-review",
|
|
75
|
-
targetIds: unauthorizedDismissals.map((review) => review.id),
|
|
76
|
-
reason: "denied-or-unverifiable",
|
|
77
|
-
},
|
|
78
|
-
]
|
|
79
|
-
: []),
|
|
80
|
-
];
|
|
81
|
-
if (authorization.length > 0) {
|
|
82
|
-
const authorizationEscalateBase = {
|
|
83
|
-
triggers: ["authorization-required"],
|
|
84
|
-
unresolvedThreads: allThreads.map(toAgentThread),
|
|
85
|
-
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
86
|
-
changesRequestedReviews: report.changesRequestedReviews,
|
|
87
|
-
authorization,
|
|
88
|
-
suggestion: buildEscalateSuggestion(["authorization-required"]),
|
|
89
|
-
};
|
|
90
|
-
return {
|
|
91
|
-
...base,
|
|
92
|
-
action: "escalate",
|
|
93
|
-
escalate: {
|
|
94
|
-
...authorizationEscalateBase,
|
|
95
|
-
humanMessage: buildEscalateHumanMessage(authorizationEscalateBase, prReference),
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
62
|
+
const skippedThreadIds = new Set([...unauthorizedReplies, ...unauthorizedResolves].map((thread) => thread.id));
|
|
63
|
+
const retryableActionableThreads = report.threads.actionable.filter((thread) => !skippedThreadIds.has(thread.id) && thread.path !== null && thread.line !== null);
|
|
99
64
|
const protectedRuns = [];
|
|
100
65
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
101
|
-
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha,
|
|
102
|
-
const botCrReviews = report.changesRequestedReviews.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames))
|
|
66
|
+
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha, retryableActionableThreads);
|
|
67
|
+
const botCrReviews = report.changesRequestedReviews.filter((r) => (!isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames)) &&
|
|
68
|
+
report.viewerAuthorization?.viewerCanAdminister === true);
|
|
103
69
|
const botCrStateKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
104
70
|
const previousBotCrState = await readBotCrSeenState(botCrStateKey);
|
|
105
71
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
@@ -120,11 +86,13 @@ export async function handleFixCode(ctx) {
|
|
|
120
86
|
action: "escalate",
|
|
121
87
|
escalate: {
|
|
122
88
|
...escalateBase,
|
|
123
|
-
humanMessage: buildEscalateHumanMessage(escalateBase, prReference
|
|
89
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference, {
|
|
90
|
+
merge: opts.merge,
|
|
91
|
+
}),
|
|
124
92
|
},
|
|
125
93
|
};
|
|
126
94
|
}
|
|
127
|
-
const escalateTriggers = checkEscalateTriggers(
|
|
95
|
+
const escalateTriggers = checkEscalateTriggers(retryableActionableThreads, threadAttempts);
|
|
128
96
|
if (escalateTriggers.triggers.length > 0) {
|
|
129
97
|
const escalateBase = {
|
|
130
98
|
triggers: escalateTriggers.triggers,
|
|
@@ -139,24 +107,57 @@ export async function handleFixCode(ctx) {
|
|
|
139
107
|
action: "escalate",
|
|
140
108
|
escalate: {
|
|
141
109
|
...escalateBase,
|
|
142
|
-
humanMessage: buildEscalateHumanMessage(escalateBase, prReference
|
|
110
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference, {
|
|
111
|
+
merge: opts.merge,
|
|
112
|
+
}),
|
|
143
113
|
},
|
|
144
114
|
};
|
|
145
115
|
}
|
|
146
116
|
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
147
|
-
// GitHub does not expose a per-run viewer capability for cancellation
|
|
148
|
-
//
|
|
117
|
+
// GitHub does not expose a per-run viewer capability for cancellation, so Shepherd never
|
|
118
|
+
// issues or recommends a cancellation regardless of repository role. A rerun is different:
|
|
119
|
+
// GitHub's Actions rerun API requires actions:write, which rides with WRITE+ repo access, so
|
|
120
|
+
// repositoryPermission is a proxy for account-level rerun capability (see canRerunWorkflows) —
|
|
121
|
+
// it does not confirm the credential executing `gh` has that scope; an unauthorized rerun
|
|
122
|
+
// simply fails when the agent runs it, the same residual risk as every other CLI-recommended
|
|
123
|
+
// git/gh mutation in this codebase.
|
|
149
124
|
const cancelled = [];
|
|
150
125
|
const baseLookup = validateBaseBranch(report.baseBranch);
|
|
151
126
|
const threads = report.threads.actionable.map(toAgentThread);
|
|
152
127
|
const resolutionOnlyThreads = report.threads.resolutionOnly;
|
|
153
128
|
const actionableComments = report.comments.actionable.map(toAgentComment);
|
|
154
|
-
const
|
|
129
|
+
const rerunAuthorized = canRerunWorkflows(report.viewerAuthorization);
|
|
130
|
+
// A workflow run can only be rerun once it has fully completed; a runId still present among
|
|
131
|
+
// in-progress checks (a sibling job from the same run) is not yet eligible.
|
|
132
|
+
const inProgressWorkflowRunIds = new Set(report.checks.inProgress.flatMap((c) => (c.runId !== null ? [c.runId] : [])));
|
|
133
|
+
// Confirmed GitHub Actions provenance for a runId: `source: "startup_failure"` checks are
|
|
134
|
+
// fetched directly from the Actions REST API (always genuine), while `source: "check_run"`
|
|
135
|
+
// checks need `workflowName` — derived from the same `checkSuite.workflowRun.workflow` GraphQL
|
|
136
|
+
// path as the run's numeric ID — because a third-party GitHub App's CheckRun can carry a
|
|
137
|
+
// details-URL-parsed runId that merely looks like an Actions run number.
|
|
138
|
+
const actionsRunIds = new Set(failingChecks.flatMap((c) => c.runId !== null && (c.source === "startup_failure" || c.workflowName !== undefined)
|
|
139
|
+
? [c.runId]
|
|
140
|
+
: []));
|
|
141
|
+
const failingAgentChecks = toAgentChecks(failingChecks).map((c) => rerunAuthorized &&
|
|
142
|
+
c.runId &&
|
|
143
|
+
actionsRunIds.has(c.runId) &&
|
|
144
|
+
// ACTION_REQUIRED means the run is paused pending manual workflow approval; rerunning does
|
|
145
|
+
// not grant that approval, so no rerun command applies.
|
|
146
|
+
c.conclusion !== "ACTION_REQUIRED" &&
|
|
147
|
+
!inProgressWorkflowRunIds.has(c.runId)
|
|
148
|
+
? { ...c, rerunCommand: `gh run rerun ${c.runId} -R ${report.repo}` }
|
|
149
|
+
: c);
|
|
155
150
|
const checks = [
|
|
156
151
|
...failingAgentChecks,
|
|
157
152
|
...toAgentChecks(annotatedExtra).map((c) => ({ ...c, annotationOnly: true })),
|
|
158
153
|
];
|
|
159
154
|
const { changesRequestedReviews } = report;
|
|
155
|
+
const actionableChangesRequestedReviews = changesRequestedReviews.filter((review) => review.staleReview !== true ||
|
|
156
|
+
!isHumanAuthor(review) ||
|
|
157
|
+
isConfiguredBotAuthor(review, botUsernames));
|
|
158
|
+
const skippedDismissalIds = new Set(unauthorizedDismissals.map((review) => review.id));
|
|
159
|
+
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !skippedDismissalIds.has(review.id));
|
|
160
|
+
const resolutionOnlyThreadsForWork = resolutionOnlyThreads.filter((thread) => !skippedThreadIds.has(thread.id) && thread.path !== null && thread.line !== null);
|
|
160
161
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
161
162
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
162
163
|
const { behindBaseHint } = loadConfig().iterate;
|
|
@@ -166,23 +167,57 @@ export async function handleFixCode(ctx) {
|
|
|
166
167
|
const inProgressRunIds = [];
|
|
167
168
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
168
169
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
171
|
+
const manualFollowUpChecks = failingAgentChecks.filter((check) => !belongsToActiveWorkflowRun(check) && checkRequiresHumanFollowUp(check));
|
|
172
|
+
const hasAutonomousWork = hasConflicts ||
|
|
173
|
+
threads.length > 0 ||
|
|
174
|
+
resolutionOnlyThreads.length > 0 ||
|
|
175
|
+
actionableComments.length > 0 ||
|
|
176
|
+
commentMinimizeIds.length > 0 ||
|
|
177
|
+
actionableChangesRequestedReviews.length > 0 ||
|
|
178
|
+
reviewSummaryIds.length > 0 ||
|
|
179
|
+
firstLookSummaries.length > 0 ||
|
|
180
|
+
editedSummaries.length > 0 ||
|
|
181
|
+
report.threads.firstLook.length > 0 ||
|
|
182
|
+
report.comments.firstLook.length > 0 ||
|
|
183
|
+
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
184
|
+
failingAgentChecks.some((check) => belongsToActiveWorkflowRun(check) || !checkRequiresHumanFollowUp(check));
|
|
185
|
+
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
186
|
+
const checkEscalateBase = {
|
|
187
|
+
triggers: ["check-follow-up-unavailable"],
|
|
188
|
+
unresolvedThreads: [],
|
|
189
|
+
ambiguousComments: [],
|
|
190
|
+
changesRequestedReviews,
|
|
191
|
+
checks: manualFollowUpChecks,
|
|
192
|
+
suggestion: buildEscalateSuggestion(["check-follow-up-unavailable"]),
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
action: "escalate",
|
|
197
|
+
escalate: {
|
|
198
|
+
...checkEscalateBase,
|
|
199
|
+
humanMessage: buildEscalateHumanMessage(checkEscalateBase, prReference, {
|
|
200
|
+
merge: opts.merge,
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Push access to the PR head branch is a usage precondition. Build review mutations for
|
|
206
|
+
// conflict ticks normally so the caller can push and complete the same fix_code cycle.
|
|
207
|
+
const retryableActionableIds = new Set(retryableActionableThreads.map((thread) => thread.id));
|
|
208
|
+
const retryableAgentThreads = threads.filter((thread) => retryableActionableIds.has(thread.id));
|
|
209
|
+
const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(retryableAgentThreads, resolutionOnlyThreadsForWork, allCommentIds, changesRequestedReviewsForWork, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads);
|
|
175
210
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
176
211
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
177
212
|
// prediction that the agent *will* push. Intentionally broader than `pushLikely` above:
|
|
178
213
|
// resolution-only threads also need a known base in case the agent does push.
|
|
179
|
-
const pushIsPlausible =
|
|
214
|
+
const pushIsPlausible = retryableActionableThreads.length > 0 ||
|
|
180
215
|
failingAgentChecks.length > 0 ||
|
|
181
216
|
annotatedExtra.length > 0 ||
|
|
182
217
|
hasConflicts ||
|
|
183
|
-
|
|
218
|
+
changesRequestedReviewsForWork.length > 0 ||
|
|
184
219
|
actionableComments.length > 0 ||
|
|
185
|
-
|
|
220
|
+
resolutionOnlyThreadsForWork.length > 0;
|
|
186
221
|
if (baseLookup.isFallback && pushIsPlausible) {
|
|
187
222
|
const fallbackEscalateBase = {
|
|
188
223
|
triggers: ["base-branch-unknown"],
|
|
@@ -196,7 +231,9 @@ export async function handleFixCode(ctx) {
|
|
|
196
231
|
action: "escalate",
|
|
197
232
|
escalate: {
|
|
198
233
|
...fallbackEscalateBase,
|
|
199
|
-
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase, prReference
|
|
234
|
+
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase, prReference, {
|
|
235
|
+
merge: opts.merge,
|
|
236
|
+
}),
|
|
200
237
|
},
|
|
201
238
|
};
|
|
202
239
|
}
|
|
@@ -14,7 +14,11 @@ import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
|
14
14
|
import { buildReadyMergeResult, handleActiveMergeState } from "./merge-state.mjs";
|
|
15
15
|
import { buildIterateBase } from "./base.mjs";
|
|
16
16
|
import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
17
|
-
|
|
17
|
+
import { withIterateApiUsage } from "./run.mjs";
|
|
18
|
+
export function runIterate(opts) {
|
|
19
|
+
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
20
|
+
}
|
|
21
|
+
async function runIterateCore(opts) {
|
|
18
22
|
const config = loadConfig();
|
|
19
23
|
const botUsernames = normalizeBotUsernames(config.botUsernames);
|
|
20
24
|
const readyDelaySeconds = opts.readyDelaySeconds ?? config.watch.readyDelayMinutes * 60;
|
|
@@ -48,8 +52,7 @@ export async function runIterate(opts) {
|
|
|
48
52
|
seen: report.reviewSummaries,
|
|
49
53
|
edited: report.editedSummaries,
|
|
50
54
|
}, report.approvedReviews, config.iterate.minimizeApprovals, config.iterate.minimizeComments, botUsernames, [...report.threads.actionable, ...report.threads.resolutionOnly], report.ruleAutoResolveReviewSummaryIds);
|
|
51
|
-
//
|
|
52
|
-
// in-process so they never register as agent-facing actionable work (#313).
|
|
55
|
+
// Minimize already-seen review summaries in-process so they never become agent-facing work.
|
|
53
56
|
// GitHub can still return a null/error/rate-limit result per ID without
|
|
54
57
|
// throwing (autoMinimizeComments reports this via `errors`, not a rejection);
|
|
55
58
|
// any ID it did not confirm minimized falls back into the agent-facing set so
|
|
@@ -78,8 +81,8 @@ export async function runIterate(opts) {
|
|
|
78
81
|
editedSummaries.length > 0 ||
|
|
79
82
|
(config.iterate.minimizeApprovals && surfacedApprovals.length > 0);
|
|
80
83
|
const activeMerge = Boolean(opts.merge && (report.mergeQueue?.inQueue || report.mergeQueue?.autoMergeRequest));
|
|
81
|
-
const
|
|
82
|
-
const readyState = await updateReadyDelay(report.pr,
|
|
84
|
+
const isCleanReadyState = report.status === "READY" && !hasActionableWork && !activeMerge;
|
|
85
|
+
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName);
|
|
83
86
|
const base = buildIterateBase(report, readyState);
|
|
84
87
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
85
88
|
if (hasActionableWork) {
|
|
@@ -7,7 +7,10 @@ export function buildReadyMergeResult(enabled, readyElapsed, base, report) {
|
|
|
7
7
|
return null;
|
|
8
8
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
9
9
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
10
|
-
|
|
10
|
+
// A merge queue is enrolled the same way auto-merge is granted: enabling
|
|
11
|
+
// auto-merge on a queue-required PR is what adds it to the queue, so
|
|
12
|
+
// viewerCanEnableAutoMerge authorizes both the plain-merge and enqueue paths.
|
|
13
|
+
if (report.viewerAuthorization?.viewerCanEnableAutoMerge === true) {
|
|
11
14
|
return {
|
|
12
15
|
...base,
|
|
13
16
|
action: "merge",
|
|
@@ -16,7 +19,7 @@ export function buildReadyMergeResult(enabled, readyElapsed, base, report) {
|
|
|
16
19
|
repo: report.repo,
|
|
17
20
|
nodeId: report.nodeId,
|
|
18
21
|
headSha: report.headSha ?? "unknown",
|
|
19
|
-
queue
|
|
22
|
+
queue,
|
|
20
23
|
}),
|
|
21
24
|
};
|
|
22
25
|
}
|
|
@@ -13,6 +13,8 @@ export function renderResolveCommand(rc) {
|
|
|
13
13
|
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
14
14
|
isBehind = false, viewerCanUpdate = false) {
|
|
15
15
|
const instructions = [];
|
|
16
|
+
const locatedThreads = threads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
17
|
+
const unlocatedThreads = threads.filter((thread) => thread.path === null || thread.line === null);
|
|
16
18
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
17
19
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
18
20
|
const hasNonConflictHints = threads.length > 0 ||
|
|
@@ -23,8 +25,10 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
23
25
|
// Start with interpretation. The agent decides what raw feedback warrants a code change.
|
|
24
26
|
if (hasNonConflictHints) {
|
|
25
27
|
const actionableSections = [];
|
|
26
|
-
if (
|
|
28
|
+
if (locatedThreads.length > 0)
|
|
27
29
|
actionableSections.push("`## Review threads`");
|
|
30
|
+
if (unlocatedThreads.length > 0)
|
|
31
|
+
actionableSections.push("`## Unlocated review threads (logged once — no mutation)`");
|
|
28
32
|
if (actionableComments.length > 0)
|
|
29
33
|
actionableSections.push("`## Actionable comments`");
|
|
30
34
|
if (failingChecks.length > 0)
|
|
@@ -53,17 +57,20 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
53
57
|
if (editedTotal > 0) {
|
|
54
58
|
instructions.push("Read every item marked `[edited since first look]`, including edited summaries and edited first-look bullets, before deciding whether to resolve a matching thread.");
|
|
55
59
|
}
|
|
60
|
+
if (unlocatedThreads.length > 0) {
|
|
61
|
+
instructions.push("Acknowledge each item under `## Unlocated review threads (logged once — no mutation)`. Shepherd cannot route a code fix or review mutation without a path and line; the unchanged item will be skipped on later ticks.");
|
|
62
|
+
}
|
|
56
63
|
// GitHub exposes no exact viewer capability for workflow-run cancellation, so the
|
|
57
64
|
// informational run lists never produce a cancellation recommendation.
|
|
58
65
|
void inProgressRunIds;
|
|
59
66
|
void cancelledCount;
|
|
60
|
-
const hasSuggestions =
|
|
67
|
+
const hasSuggestions = locatedThreads.some((t) => t.suggestion);
|
|
61
68
|
if (hasSuggestions)
|
|
62
69
|
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
63
|
-
if (
|
|
70
|
+
if (locatedThreads.length > 0 || actionableComments.length > 0) {
|
|
64
71
|
// Actionable comments carry no file/line location (unlike threads), so "referenced above"
|
|
65
72
|
// is only accurate when threads are present.
|
|
66
|
-
const filesRef =
|
|
73
|
+
const filesRef = locatedThreads.length > 0 ? "each file referenced above" : "the relevant files";
|
|
67
74
|
instructions.push(`Apply every warranted review fix in ${filesRef}.`);
|
|
68
75
|
}
|
|
69
76
|
if (resolutionOnlyThreads.length > 0) {
|
|
@@ -81,10 +88,10 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
81
88
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
82
89
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
83
90
|
if (hasConflicts) {
|
|
84
|
-
instructions.push(`Commit any remaining conflict-resolution changes${mutationSuffix}.`);
|
|
91
|
+
instructions.push(`Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`);
|
|
85
92
|
}
|
|
86
93
|
else if (hasNonConflictHints) {
|
|
87
|
-
instructions.push("If you changed code, commit any remaining changes
|
|
94
|
+
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue with the remaining steps.");
|
|
88
95
|
}
|
|
89
96
|
if (viewerCanUpdate &&
|
|
90
97
|
(hasReviewMutations ||
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { withApiTelemetryScope } from "../../github/api-telemetry.mjs";
|
|
2
|
+
import { attachApiUsage } from "./api-usage.mjs";
|
|
3
|
+
export function withIterateApiUsage(opts, runCore) {
|
|
4
|
+
return withApiTelemetryScope(async () => {
|
|
5
|
+
const result = await runCore();
|
|
6
|
+
return attachApiUsage(result, opts.deferQuotaWarning !== true);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -3,6 +3,14 @@ import { graphql, getCurrentPrNumber, getRepoInfo } from "../github/client.mjs";
|
|
|
3
3
|
import { paginateForward } from "../github/pagination.mjs";
|
|
4
4
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
5
5
|
const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) {
|
|
6
|
+
_shepherdRateLimit: rateLimit {
|
|
7
|
+
cost
|
|
8
|
+
limit
|
|
9
|
+
nodeCount
|
|
10
|
+
remaining
|
|
11
|
+
resetAt
|
|
12
|
+
used
|
|
13
|
+
}
|
|
6
14
|
repository(owner: $owner, name: $repo) {
|
|
7
15
|
pullRequest(number: $pr) {
|
|
8
16
|
id
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { withApiTelemetryScope } from "../github/api-telemetry.mjs";
|
|
2
|
+
import { attachApiUsage } from "./iterate/api-usage.mjs";
|
|
3
|
+
export function withPollApiUsage(runCore, preservePersistedWarning) {
|
|
4
|
+
return withApiTelemetryScope(async () => {
|
|
5
|
+
const result = await runCore();
|
|
6
|
+
return attachApiUsage(result, true, preservePersistedWarning);
|
|
7
|
+
});
|
|
8
|
+
}
|
package/bin/commands/poll.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IterateCommandOptions, IterateResult } from "../types.mts";
|
|
2
|
-
interface PollCommandOptions extends IterateCommandOptions {
|
|
2
|
+
export interface PollCommandOptions extends IterateCommandOptions {
|
|
3
3
|
intervalSeconds: number;
|
|
4
4
|
timeoutSeconds: number;
|
|
5
5
|
/** Settle window after first FIX_CODE before returning. Default 60. 0 disables. */
|
|
@@ -9,4 +9,3 @@ interface PollCommandOptions extends IterateCommandOptions {
|
|
|
9
9
|
}
|
|
10
10
|
/** @deprecated Hidden implementation for the legacy `poll` alias. */
|
|
11
11
|
export declare function runPoll(opts: PollCommandOptions): Promise<IterateResult>;
|
|
12
|
-
export {};
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runIterate } from "./iterate/index.mjs";
|
|
2
2
|
import { sleep } from "../util/sleep.mjs";
|
|
3
|
+
import { withPollApiUsage } from "./poll-run.mjs";
|
|
3
4
|
const DEFAULT_POLL_DEBOUNCE_SECONDS = 60;
|
|
4
5
|
function writeTickProgress(tick, elapsedSeconds, sleepSeconds, verbose) {
|
|
5
6
|
if (verbose) {
|
|
@@ -61,7 +62,10 @@ function writeDebounceProgress(tick, elapsedMs, remainingMs) {
|
|
|
61
62
|
process.stderr.write(`[poll tick ${tick} / +${elapsedSeconds}s] FIX_CODE — debounce ${remainingSeconds}s remaining\n`);
|
|
62
63
|
}
|
|
63
64
|
/** @deprecated Hidden implementation for the legacy `poll` alias. */
|
|
64
|
-
export
|
|
65
|
+
export function runPoll(opts) {
|
|
66
|
+
return withPollApiUsage(() => runPollCore(opts), opts.untilTerminal === true);
|
|
67
|
+
}
|
|
68
|
+
async function runPollCore(opts) {
|
|
65
69
|
const { intervalSeconds, timeoutSeconds, debounceSeconds: debounceSecondsOpt, quietStatus: quietStatusOpt, untilTerminal: untilTerminalOpt, ...iterateOpts } = opts;
|
|
66
70
|
const intervalMs = Math.min(intervalSeconds * 1000, MAX_TIMER_MS);
|
|
67
71
|
const timeoutMs = Math.min(timeoutSeconds * 1000, MAX_TIMER_MS);
|
|
@@ -74,11 +78,8 @@ export async function runPoll(opts) {
|
|
|
74
78
|
const quietStatus = quietStatusOpt === true;
|
|
75
79
|
const untilTerminal = untilTerminalOpt === true;
|
|
76
80
|
let lastWaitSignature = null;
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
// the monitored PR merges it returns nothing and runIterate throws instead of reporting the
|
|
80
|
-
// terminal CANCEL/merged result. Pin the PR resolved by the first tick so later ticks target it
|
|
81
|
-
// directly and never re-run branch discovery.
|
|
81
|
+
let pendingQuotaWarning;
|
|
82
|
+
// Pin the PR resolved by the first tick; branch inference only matches OPEN PRs.
|
|
82
83
|
let prNumber = opts.prNumber;
|
|
83
84
|
let debounceUntil = null;
|
|
84
85
|
while (true) {
|
|
@@ -88,10 +89,28 @@ export async function runPoll(opts) {
|
|
|
88
89
|
...iterateOpts,
|
|
89
90
|
prNumber,
|
|
90
91
|
persistSeen: debounceSeconds === 0 || pastDebounce,
|
|
92
|
+
// Until-terminal must persist before deciding to return.
|
|
93
|
+
deferQuotaWarning: !untilTerminal,
|
|
91
94
|
});
|
|
92
95
|
prNumber ??= lastResult.pr;
|
|
96
|
+
if (lastResult.quotaWarning !== undefined)
|
|
97
|
+
pendingQuotaWarning = lastResult.quotaWarning;
|
|
98
|
+
if (untilTerminal &&
|
|
99
|
+
pendingQuotaWarning !== undefined &&
|
|
100
|
+
!(lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) &&
|
|
101
|
+
!(debounceUntil !== null && !pastDebounce)) {
|
|
102
|
+
if (["cancel", "escalate"].includes(lastResult.action)) {
|
|
103
|
+
const { quotaWarning: _quotaWarning, ...withoutQuotaWarning } = lastResult;
|
|
104
|
+
lastResult = withoutQuotaWarning;
|
|
105
|
+
}
|
|
106
|
+
else if (lastResult.quotaWarning === undefined) {
|
|
107
|
+
lastResult = { ...lastResult, quotaWarning: pendingQuotaWarning };
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
93
111
|
if (lastResult.action === "wait" && !pastDebounce) {
|
|
94
|
-
|
|
112
|
+
if (pendingQuotaWarning === undefined)
|
|
113
|
+
debounceUntil = null;
|
|
95
114
|
const elapsedMs = Date.now() - start;
|
|
96
115
|
if (!untilTerminal) {
|
|
97
116
|
const remainingMs = timeoutMs - elapsedMs;
|
|
@@ -126,9 +145,6 @@ export async function runPoll(opts) {
|
|
|
126
145
|
writeDebounceProgress(tick, Date.now() - start, remainingMs);
|
|
127
146
|
await sleep(Math.min(intervalMs, remainingMs));
|
|
128
147
|
}
|
|
129
|
-
// Always continue so a tick that *started* before debounceUntil but *finished*
|
|
130
|
-
// after it cannot return with persistSeen: false. The next loop iteration
|
|
131
|
-
// sees pastDebounce and persists.
|
|
132
148
|
continue;
|
|
133
149
|
}
|
|
134
150
|
break;
|
|
@@ -2,15 +2,19 @@ export interface ResolveRateLimitStop {
|
|
|
2
2
|
message: string;
|
|
3
3
|
retryAfterSeconds?: number;
|
|
4
4
|
limit?: number;
|
|
5
|
+
used?: number;
|
|
5
6
|
remaining?: number;
|
|
6
7
|
resetAt?: number;
|
|
8
|
+
resource?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare function rateLimitFromError(err: unknown, fallbackMessage: string): ResolveRateLimitStop | null;
|
|
9
11
|
export declare function rateLimitFromGraphQlResult(messages: string[], meta: {
|
|
10
12
|
rateLimit?: {
|
|
11
13
|
remaining?: unknown;
|
|
12
14
|
limit?: unknown;
|
|
15
|
+
used?: unknown;
|
|
13
16
|
resetAt?: unknown;
|
|
17
|
+
resource?: unknown;
|
|
14
18
|
};
|
|
15
19
|
retryAfterSeconds?: unknown;
|
|
16
20
|
stopOnZeroRemaining?: boolean;
|