pr-shepherd 0.44.0 → 0.45.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 +4 -4
- package/bin/checks/conclusions.d.mts +24 -1
- package/bin/checks/conclusions.mjs +39 -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 +12 -1
- package/bin/cli/iterate-activity-formatter.d.mts +2 -0
- package/bin/cli/iterate-activity-formatter.mjs +26 -0
- package/bin/cli/iterate-formatter.mjs +11 -28
- package/bin/cli/iterate-instructions.mjs +14 -1
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/mutate-formatter.mjs +2 -0
- package/bin/commands/check-annotations.d.mts +2 -1
- package/bin/commands/check-annotations.mjs +7 -7
- package/bin/commands/check.mjs +6 -4
- package/bin/commands/iterate/api-usage.d.mts +2 -0
- package/bin/commands/iterate/api-usage.mjs +42 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +27 -8
- package/bin/commands/iterate/fix-code.mjs +42 -8
- package/bin/commands/iterate/index.mjs +6 -3
- package/bin/commands/iterate/merge-state.mjs +5 -2
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +9 -5
- 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 +29 -10
- package/bin/comments/rate-limit.d.mts +4 -0
- package/bin/comments/rate-limit.mjs +6 -0
- package/bin/config/load.d.mts +6 -1
- package/bin/config/load.mjs +33 -1
- package/bin/config.json +6 -1
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +1 -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/errors.d.mts +8 -0
- package/bin/github/errors.mjs +29 -1
- 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 +66 -10
- package/bin/github/graphql-internal-retry.d.mts +2 -0
- package/bin/github/graphql-internal-retry.mjs +50 -0
- package/bin/github/graphql-response.mjs +11 -1
- 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/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/iterate.d.mts +4 -1
- package/bin/types/report.d.mts +2 -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 +14 -9
package/bin/commands/check.mjs
CHANGED
|
@@ -27,6 +27,7 @@ export async function runCheck(opts) {
|
|
|
27
27
|
if (prNumber === null) {
|
|
28
28
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
29
29
|
}
|
|
30
|
+
const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
|
|
30
31
|
const config = loadConfig();
|
|
31
32
|
const paginateApprovedReviews = config.iterate.minimizeApprovals;
|
|
32
33
|
const result = await fetchPrBatch(prNumber, repo, { paginateApprovedReviews });
|
|
@@ -40,7 +41,7 @@ export async function runCheck(opts) {
|
|
|
40
41
|
}
|
|
41
42
|
const startupFailureChecks = result.checkSuitesComplete
|
|
42
43
|
? []
|
|
43
|
-
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
|
|
44
|
+
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber, stateKey);
|
|
44
45
|
const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
|
|
45
46
|
const classifiedPrChecks = classifyChecks(allChecks);
|
|
46
47
|
const latestRemoval = batchData.latestMergeQueueRemoval;
|
|
@@ -65,14 +66,15 @@ export async function runCheck(opts) {
|
|
|
65
66
|
const skipped = classifiedChecks.filter((c) => c.category === "skipped");
|
|
66
67
|
const filtered = classifiedChecks.filter((c) => c.category === "filtered");
|
|
67
68
|
const ignored = classifiedChecks.filter((c) => c.category === "ignored");
|
|
68
|
-
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
69
|
-
|
|
69
|
+
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
70
|
+
? await triageFailingChecks(failing, repo, stateKey)
|
|
71
|
+
: failing;
|
|
70
72
|
const seenMap = await loadSeenMap(stateKey);
|
|
71
73
|
const botUsernames = normalizeBotUsernames(config.botUsernames);
|
|
72
74
|
const ruleSet = await loadRules(discoverRuleFiles(getEffectiveCwd()));
|
|
73
75
|
const classifyIndex = buildClassifyIndex(ruleSet, batchData);
|
|
74
76
|
const partition = partitionBatch(classifyIndex, batchData);
|
|
75
|
-
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered, ignored }, seenMap, prNumber);
|
|
77
|
+
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered, ignored }, seenMap, prNumber, { stateKey, headSha: batchData.headRefOid });
|
|
76
78
|
const ignoredAnnotated = merged.ignored.filter((c) => (c.annotations?.length ?? 0) > 0);
|
|
77
79
|
const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized && !partition.suppressedCommentIds.has(c.id));
|
|
78
80
|
const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize !== true));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { loadConfig } from "../../config/load.mjs";
|
|
2
|
+
import { summarizeApiTelemetry } from "../../github/api-telemetry.mjs";
|
|
3
|
+
import { evaluateWorktreeGraphqlQuotaWarning } from "../../state/graphql-quota-warnings.mjs";
|
|
4
|
+
import { buildQuotaAwareContinuation } from "../../quota-warning.mjs";
|
|
5
|
+
function shouldWarn(result) {
|
|
6
|
+
if (["wait", "mark_ready", "merge"].includes(result.action))
|
|
7
|
+
return true;
|
|
8
|
+
if (result.action !== "fix_code")
|
|
9
|
+
return false;
|
|
10
|
+
return !result.fix.instructions.some((instruction) => /stop polling|human direction/i.test(instruction));
|
|
11
|
+
}
|
|
12
|
+
export async function attachApiUsage(result, persistWarning, preservePersistedWarning = false) {
|
|
13
|
+
const apiUsage = summarizeApiTelemetry();
|
|
14
|
+
if (apiUsage === undefined)
|
|
15
|
+
return result;
|
|
16
|
+
let quotaWarning = preservePersistedWarning ? result.quotaWarning : undefined;
|
|
17
|
+
if (quotaWarning === undefined && apiUsage.graphql !== undefined && shouldWarn(result)) {
|
|
18
|
+
const [owner, repo] = result.repo.split("/");
|
|
19
|
+
if (owner && repo) {
|
|
20
|
+
quotaWarning = await evaluateWorktreeGraphqlQuotaWarning({ owner, repo }, loadConfig().watch.graphqlQuotaWarnings, apiUsage.graphql, persistWarning);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const { quotaWarning: _deferredWarning, ...baseResult } = result;
|
|
24
|
+
const withWarning = {
|
|
25
|
+
...baseResult,
|
|
26
|
+
apiUsage,
|
|
27
|
+
...(quotaWarning !== undefined && { quotaWarning }),
|
|
28
|
+
};
|
|
29
|
+
if (quotaWarning === undefined || withWarning.action !== "fix_code")
|
|
30
|
+
return withWarning;
|
|
31
|
+
const instructions = [...withWarning.fix.instructions];
|
|
32
|
+
const completion = instructions.at(-1);
|
|
33
|
+
if (completion !== undefined) {
|
|
34
|
+
if (/\[FIX_CODE\].*non-terminal/i.test(completion)) {
|
|
35
|
+
instructions[instructions.length - 1] = buildQuotaAwareContinuation(quotaWarning, "`[FIX_CODE]` is non-terminal. After completing these steps,");
|
|
36
|
+
}
|
|
37
|
+
else if (/\[FIX_CODE\].*is conditional:.*if you did not change code,/i.test(completion)) {
|
|
38
|
+
instructions[instructions.length - 1] = completion.replace(/if you did not change code,(.*?)\s+and iterate again with the same options\./i, (_, before) => `if you did not change code,${before.trimEnd()}. ${buildQuotaAwareContinuation(quotaWarning, "").trimStart()}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { ...withWarning, fix: { ...withWarning.fix, instructions } };
|
|
42
|
+
}
|
|
@@ -27,7 +27,7 @@ export declare function buildBehindBaseHintInstruction(baseBranch: string, hint:
|
|
|
27
27
|
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
28
28
|
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
29
29
|
*/
|
|
30
|
-
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
30
|
+
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand, pushAuthorized?: boolean): string[];
|
|
31
31
|
/** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
|
|
32
32
|
export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
|
|
33
|
-
export declare function buildFixCompletionInstruction(checks: AgentCheck[], requiresRemoteUpdateAuthorization?: boolean, hasShaGatedReviewMutations?: boolean): string;
|
|
33
|
+
export declare function buildFixCompletionInstruction(checks: AgentCheck[], requiresRemoteUpdateAuthorization?: boolean, hasShaGatedReviewMutations?: boolean, pushAuthorized?: boolean): string;
|
|
@@ -36,7 +36,7 @@ export function buildBehindBaseHintInstruction(baseBranch, hint, isBehind) {
|
|
|
36
36
|
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
37
37
|
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
38
38
|
*/
|
|
39
|
-
export function buildResolveCommandInstruction(resolveCommand) {
|
|
39
|
+
export function buildResolveCommandInstruction(resolveCommand, pushAuthorized = false) {
|
|
40
40
|
if (!resolveCommand.hasMutations)
|
|
41
41
|
return [];
|
|
42
42
|
const instructions = [];
|
|
@@ -44,7 +44,9 @@ export function buildResolveCommandInstruction(resolveCommand) {
|
|
|
44
44
|
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked viewer-authored human thread is emitted resolve-only, not for another reply.");
|
|
45
45
|
}
|
|
46
46
|
if (resolveCommand.requiresHeadSha) {
|
|
47
|
-
instructions.push(
|
|
47
|
+
instructions.push(pushAuthorized
|
|
48
|
+
? "If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA."
|
|
49
|
+
: "If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, do not run this command until an authorized push updates the remote PR head; then replace `$HEAD_SHA` with that pushed commit SHA.");
|
|
48
50
|
}
|
|
49
51
|
if (resolveCommand.requiresDismissMessage) {
|
|
50
52
|
instructions.push("Replace `$DISMISS_MESSAGE` with one sentence describing what changed.");
|
|
@@ -58,32 +60,49 @@ export function buildFailingCheckInstructions(checks) {
|
|
|
58
60
|
return [];
|
|
59
61
|
const hasBare = checks.some((c) => !c.runId && !c.detailsUrl);
|
|
60
62
|
const hasTriageable = checks.some((c) => c.runId || c.detailsUrl);
|
|
63
|
+
const hasRerunAuthorized = checks.some((c) => c.rerunCommand);
|
|
61
64
|
const instructions = [];
|
|
62
65
|
if (hasTriageable) {
|
|
63
66
|
instructions.push('Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.');
|
|
64
67
|
}
|
|
68
|
+
if (hasRerunAuthorized) {
|
|
69
|
+
instructions.push('A `[rerun authorized]` check includes a `rerun:` command. See "CI failure triage" in the pr-shepherd skill for which conclusions warrant a rerun versus a code fix.');
|
|
70
|
+
}
|
|
65
71
|
if (hasBare) {
|
|
66
72
|
instructions.push("For each `(no runId)` failure, escalate to a human because no log or URL is available.");
|
|
67
73
|
}
|
|
68
74
|
return instructions;
|
|
69
75
|
}
|
|
70
|
-
export function buildFixCompletionInstruction(checks, requiresRemoteUpdateAuthorization = false, hasShaGatedReviewMutations = false) {
|
|
76
|
+
export function buildFixCompletionInstruction(checks, requiresRemoteUpdateAuthorization = false, hasShaGatedReviewMutations = false, pushAuthorized = false) {
|
|
71
77
|
if (requiresRemoteUpdateAuthorization) {
|
|
78
|
+
if (pushAuthorized) {
|
|
79
|
+
return "`[FIX_CODE]` is non-terminal: resolve the conflicts, commit, push to the PR head branch, then iterate again with the same options.";
|
|
80
|
+
}
|
|
72
81
|
return "`[FIX_CODE]` requires a human handoff for an authorized push after conflict resolution. Shepherd cannot verify the Git credential's push authorization. Stop polling after committing, and resume only after the remote PR head changes.";
|
|
73
82
|
}
|
|
74
83
|
const hasUninspectableFailure = checks.some((check) => !check.runId && !check.detailsUrl);
|
|
75
|
-
const hasCiAuthorizationHandoff = checks.some((check) => check.conclusion === "CANCELLED" ||
|
|
76
|
-
check.conclusion === "STARTUP_FAILURE" ||
|
|
77
|
-
(check.runId === null && Boolean(check.detailsUrl)) ||
|
|
78
|
-
(check.runId !== null && !check.logExcerpt?.trim()));
|
|
79
84
|
if (hasUninspectableFailure) {
|
|
80
85
|
return "`[FIX_CODE]` requires a human handoff for an uninspectable failing check. Stop polling after escalating, and resume only after human direction.";
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
// Checks that would otherwise need a workflow-run mutation (rerun) to move forward.
|
|
88
|
+
// `rerunCommand` is only populated when the viewer's repository role grants Actions
|
|
89
|
+
// rerun capability (see canRerunWorkflows) — unauthorized checks stay a terminal handoff.
|
|
90
|
+
const ciHandoffChecks = checks.filter((check) => check.conclusion === "CANCELLED" ||
|
|
91
|
+
check.conclusion === "STARTUP_FAILURE" ||
|
|
92
|
+
(check.runId === null && Boolean(check.detailsUrl)) ||
|
|
93
|
+
(check.runId !== null && !check.logExcerpt?.trim()));
|
|
94
|
+
const hasUnauthorizedCiHandoff = ciHandoffChecks.some((check) => !check.rerunCommand);
|
|
95
|
+
if (hasUnauthorizedCiHandoff) {
|
|
83
96
|
return "`[FIX_CODE]` requires a human handoff for a failing check with no authorized follow-up action. Stop polling after escalating, and resume only after human direction.";
|
|
84
97
|
}
|
|
85
98
|
if (hasShaGatedReviewMutations) {
|
|
99
|
+
if (pushAuthorized) {
|
|
100
|
+
return "`[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run the review mutations using the pushed commit SHA and iterate again with the same options; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
|
|
101
|
+
}
|
|
86
102
|
return "`[FIX_CODE]` is conditional: if you changed code, stop after committing and resume only after an authorized push changes the remote PR head; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
|
|
87
103
|
}
|
|
104
|
+
if (ciHandoffChecks.some((check) => check.rerunCommand)) {
|
|
105
|
+
return "`[FIX_CODE]` is non-terminal. Run any warranted reruns for `[rerun authorized]` checks (or apply code fixes for real failures), then iterate again with the same options to continue.";
|
|
106
|
+
}
|
|
88
107
|
return "`[FIX_CODE]` is non-terminal. After completing these steps, iterate again with the same options to continue.";
|
|
89
108
|
}
|
|
@@ -11,6 +11,7 @@ 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, canPushToHead } from "../../checks/conclusions.mjs";
|
|
14
15
|
import { loadConfig } from "../../config/load.mjs";
|
|
15
16
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
16
17
|
function nextFixAttempts(stored, headSha, threads) {
|
|
@@ -144,14 +145,39 @@ export async function handleFixCode(ctx) {
|
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
147
|
-
// GitHub does not expose a per-run viewer capability for cancellation
|
|
148
|
-
//
|
|
148
|
+
// GitHub does not expose a per-run viewer capability for cancellation, so Shepherd never
|
|
149
|
+
// issues or recommends a cancellation regardless of repository role. A rerun is different:
|
|
150
|
+
// GitHub's Actions rerun API requires actions:write, which rides with WRITE+ repo access, so
|
|
151
|
+
// repositoryPermission is a proxy for account-level rerun capability (see canRerunWorkflows) —
|
|
152
|
+
// it does not confirm the credential executing `gh` has that scope; an unauthorized rerun
|
|
153
|
+
// simply fails when the agent runs it, the same residual risk as every other CLI-recommended
|
|
154
|
+
// git/gh mutation in this codebase.
|
|
149
155
|
const cancelled = [];
|
|
150
156
|
const baseLookup = validateBaseBranch(report.baseBranch);
|
|
151
157
|
const threads = report.threads.actionable.map(toAgentThread);
|
|
152
158
|
const resolutionOnlyThreads = report.threads.resolutionOnly;
|
|
153
159
|
const actionableComments = report.comments.actionable.map(toAgentComment);
|
|
154
|
-
const
|
|
160
|
+
const rerunAuthorized = canRerunWorkflows(report.viewerAuthorization);
|
|
161
|
+
// A workflow run can only be rerun once it has fully completed; a runId still present among
|
|
162
|
+
// in-progress checks (a sibling job from the same run) is not yet eligible.
|
|
163
|
+
const inProgressWorkflowRunIds = new Set(report.checks.inProgress.flatMap((c) => (c.runId !== null ? [c.runId] : [])));
|
|
164
|
+
// Confirmed GitHub Actions provenance for a runId: `source: "startup_failure"` checks are
|
|
165
|
+
// fetched directly from the Actions REST API (always genuine), while `source: "check_run"`
|
|
166
|
+
// checks need `workflowName` — derived from the same `checkSuite.workflowRun.workflow` GraphQL
|
|
167
|
+
// path as the run's numeric ID — because a third-party GitHub App's CheckRun can carry a
|
|
168
|
+
// details-URL-parsed runId that merely looks like an Actions run number.
|
|
169
|
+
const actionsRunIds = new Set(failingChecks.flatMap((c) => c.runId !== null && (c.source === "startup_failure" || c.workflowName !== undefined)
|
|
170
|
+
? [c.runId]
|
|
171
|
+
: []));
|
|
172
|
+
const failingAgentChecks = toAgentChecks(failingChecks).map((c) => rerunAuthorized &&
|
|
173
|
+
c.runId &&
|
|
174
|
+
actionsRunIds.has(c.runId) &&
|
|
175
|
+
// ACTION_REQUIRED means the run is paused pending manual workflow approval; rerunning does
|
|
176
|
+
// not grant that approval, so no rerun command applies.
|
|
177
|
+
c.conclusion !== "ACTION_REQUIRED" &&
|
|
178
|
+
!inProgressWorkflowRunIds.has(c.runId)
|
|
179
|
+
? { ...c, rerunCommand: `gh run rerun ${c.runId} -R ${report.repo}` }
|
|
180
|
+
: c);
|
|
155
181
|
const checks = [
|
|
156
182
|
...failingAgentChecks,
|
|
157
183
|
...toAgentChecks(annotatedExtra).map((c) => ({ ...c, annotationOnly: true })),
|
|
@@ -160,16 +186,24 @@ export async function handleFixCode(ctx) {
|
|
|
160
186
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
161
187
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
162
188
|
const { behindBaseHint } = loadConfig().iterate;
|
|
189
|
+
// Whether GitHub reports the viewer can push to the PR head branch (own-repo write
|
|
190
|
+
// access, or a fork with viewerCanEditFiles/head-repo write for a fork PR). Drives
|
|
191
|
+
// whether the fix instructions push autonomously or hand off for an authorized push —
|
|
192
|
+
// see canPushToHead.
|
|
193
|
+
const pushAuthorized = canPushToHead(report.viewerAuthorization);
|
|
163
194
|
// Only surface in-progress runs when a push is plausible — resolution-only and
|
|
164
195
|
// summary-only iterations have no path to a push, so listing runs would prompt
|
|
165
196
|
// unnecessary cancellation.
|
|
166
197
|
const inProgressRunIds = [];
|
|
167
198
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
168
199
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
169
|
-
// Conflict resolution necessarily changes the branch head
|
|
170
|
-
// authorization for the local Git credential that would publish
|
|
171
|
-
// until an authorized push updates the PR and a
|
|
172
|
-
|
|
200
|
+
// Conflict resolution necessarily changes the branch head. When the viewer cannot push,
|
|
201
|
+
// Shepherd cannot verify authorization for the local Git credential that would publish
|
|
202
|
+
// it, so review mutations are deferred until an authorized push updates the PR and a
|
|
203
|
+
// fresh iteration can rebuild SHA-safe commands. When the viewer can push, the agent
|
|
204
|
+
// pushes as part of this fix_code tick, so mutations can be built normally (SHA-gated
|
|
205
|
+
// like any other push-requiring tick).
|
|
206
|
+
const { resolveCommand, resolveOnlyCommand } = hasConflicts && !pushAuthorized
|
|
173
207
|
? buildResolveCommand([], [], [], [], [], prReference, botUsernames, [], undefined, [])
|
|
174
208
|
: buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads);
|
|
175
209
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
@@ -202,7 +236,7 @@ export async function handleFixCode(ctx) {
|
|
|
202
236
|
}
|
|
203
237
|
const firstLookThreads = report.threads.firstLook;
|
|
204
238
|
const firstLookComments = report.comments.firstLook;
|
|
205
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true);
|
|
239
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true, pushAuthorized);
|
|
206
240
|
const prospectiveResult = {
|
|
207
241
|
...base,
|
|
208
242
|
baseBranch: baseLookup.branch,
|
|
@@ -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
|
|
@@ -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
|
}
|
|
@@ -2,4 +2,4 @@ import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, Fir
|
|
|
2
2
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
3
3
|
export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
4
4
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
5
|
-
isBehind?: boolean, viewerCanUpdate?: boolean): string[];
|
|
5
|
+
isBehind?: boolean, viewerCanUpdate?: boolean, pushAuthorized?: boolean): string[];
|
|
@@ -11,7 +11,7 @@ export function renderResolveCommand(rc) {
|
|
|
11
11
|
return renderShellCommand(parts);
|
|
12
12
|
}
|
|
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
|
-
isBehind = false, viewerCanUpdate = false) {
|
|
14
|
+
isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
15
15
|
const instructions = [];
|
|
16
16
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
17
17
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
@@ -81,10 +81,14 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
81
81
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
82
82
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
83
83
|
if (hasConflicts) {
|
|
84
|
-
instructions.push(
|
|
84
|
+
instructions.push(pushAuthorized
|
|
85
|
+
? `Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`
|
|
86
|
+
: `Commit any remaining conflict-resolution changes${mutationSuffix}.`);
|
|
85
87
|
}
|
|
86
88
|
else if (hasNonConflictHints) {
|
|
87
|
-
instructions.push(
|
|
89
|
+
instructions.push(pushAuthorized
|
|
90
|
+
? "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."
|
|
91
|
+
: "If you changed code, commit any remaining changes, then stop and hand off for a push whose authorization is established outside Shepherd; do not run the remaining review mutations or iterate until the remote PR head changes. Shepherd cannot verify the Git credential's push authorization. If you did not change code, do not commit and continue with the remaining steps.");
|
|
88
92
|
}
|
|
89
93
|
if (viewerCanUpdate &&
|
|
90
94
|
(hasReviewMutations ||
|
|
@@ -96,7 +100,7 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
96
100
|
}
|
|
97
101
|
if (resolveOnlyCommand?.hasMutations)
|
|
98
102
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
99
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand));
|
|
100
|
-
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
103
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand, pushAuthorized));
|
|
104
|
+
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha, pushAuthorized));
|
|
101
105
|
return instructions;
|
|
102
106
|
}
|
|
@@ -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,31 @@ 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
|
+
const terminalOrHandoff = ["cancel", "escalate"].includes(lastResult.action) ||
|
|
103
|
+
(lastResult.action === "fix_code" &&
|
|
104
|
+
lastResult.fix.instructions.some((instruction) => /stop polling|human direction/i.test(instruction)));
|
|
105
|
+
if (terminalOrHandoff) {
|
|
106
|
+
const { quotaWarning: _quotaWarning, ...withoutQuotaWarning } = lastResult;
|
|
107
|
+
lastResult = withoutQuotaWarning;
|
|
108
|
+
}
|
|
109
|
+
else if (lastResult.quotaWarning === undefined) {
|
|
110
|
+
lastResult = { ...lastResult, quotaWarning: pendingQuotaWarning };
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
93
114
|
if (lastResult.action === "wait" && !pastDebounce) {
|
|
94
|
-
|
|
115
|
+
if (pendingQuotaWarning === undefined)
|
|
116
|
+
debounceUntil = null;
|
|
95
117
|
const elapsedMs = Date.now() - start;
|
|
96
118
|
if (!untilTerminal) {
|
|
97
119
|
const remainingMs = timeoutMs - elapsedMs;
|
|
@@ -126,9 +148,6 @@ export async function runPoll(opts) {
|
|
|
126
148
|
writeDebounceProgress(tick, Date.now() - start, remainingMs);
|
|
127
149
|
await sleep(Math.min(intervalMs, remainingMs));
|
|
128
150
|
}
|
|
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
151
|
continue;
|
|
133
152
|
}
|
|
134
153
|
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;
|
|
@@ -26,15 +26,21 @@ function buildRateLimitStop(message, meta) {
|
|
|
26
26
|
const retryAfterSeconds = finiteNumber(meta.retryAfterSeconds);
|
|
27
27
|
const remaining = finiteNumber(meta.rateLimit?.remaining);
|
|
28
28
|
const limit = finiteNumber(meta.rateLimit?.limit);
|
|
29
|
+
const used = finiteNumber(meta.rateLimit?.used);
|
|
29
30
|
const resetAt = finiteNumber(meta.rateLimit?.resetAt);
|
|
31
|
+
const resource = typeof meta.rateLimit?.resource === "string" ? meta.rateLimit.resource : undefined;
|
|
30
32
|
if (retryAfterSeconds !== undefined)
|
|
31
33
|
stop.retryAfterSeconds = retryAfterSeconds;
|
|
32
34
|
if (limit !== undefined)
|
|
33
35
|
stop.limit = limit;
|
|
36
|
+
if (used !== undefined)
|
|
37
|
+
stop.used = used;
|
|
34
38
|
if (remaining !== undefined)
|
|
35
39
|
stop.remaining = remaining;
|
|
36
40
|
if (resetAt !== undefined)
|
|
37
41
|
stop.resetAt = resetAt;
|
|
42
|
+
if (resource !== undefined)
|
|
43
|
+
stop.resource = resource;
|
|
38
44
|
return stop;
|
|
39
45
|
}
|
|
40
46
|
function finiteNumber(value) {
|
package/bin/config/load.d.mts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
declare const MINIMIZE_COMMENTS_POLICIES: readonly ["all", "bots", "users", "none"];
|
|
2
2
|
export type MinimizeCommentsPolicy = (typeof MINIMIZE_COMMENTS_POLICIES)[number];
|
|
3
|
-
interface
|
|
3
|
+
export interface GraphqlQuotaWarningBand {
|
|
4
|
+
remainingPercent: number;
|
|
5
|
+
pollIntervalMinutes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PrShepherdConfig {
|
|
4
8
|
/** Optional user classification configuration; preserved for rule consumers. */
|
|
5
9
|
classify?: unknown;
|
|
6
10
|
/** GitHub logins that should be treated as bots even when GitHub reports User/Unknown. */
|
|
@@ -30,6 +34,7 @@ interface PrShepherdConfig {
|
|
|
30
34
|
};
|
|
31
35
|
watch: {
|
|
32
36
|
readyDelayMinutes: number;
|
|
37
|
+
graphqlQuotaWarnings: GraphqlQuotaWarningBand[];
|
|
33
38
|
};
|
|
34
39
|
resolve: {
|
|
35
40
|
shaPoll: {
|
package/bin/config/load.mjs
CHANGED
|
@@ -115,6 +115,37 @@ function parseMergeCommandArgs(value) {
|
|
|
115
115
|
}
|
|
116
116
|
return strategies.length === 0 ? [...value, "--merge"] : [...value];
|
|
117
117
|
}
|
|
118
|
+
function parseGraphqlQuotaWarnings(value) {
|
|
119
|
+
if (!Array.isArray(value)) {
|
|
120
|
+
throw new Error("Invalid config: watch.graphqlQuotaWarnings must be an array");
|
|
121
|
+
}
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const parsed = value.map((item, index) => {
|
|
124
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
125
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}] must be an object`);
|
|
126
|
+
}
|
|
127
|
+
const record = item;
|
|
128
|
+
const remainingPercent = record["remainingPercent"];
|
|
129
|
+
const pollIntervalMinutes = record["pollIntervalMinutes"];
|
|
130
|
+
if (typeof remainingPercent !== "number" ||
|
|
131
|
+
!Number.isInteger(remainingPercent) ||
|
|
132
|
+
remainingPercent < 1 ||
|
|
133
|
+
remainingPercent > 100) {
|
|
134
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}].remainingPercent must be an integer from 1 to 100`);
|
|
135
|
+
}
|
|
136
|
+
if (typeof pollIntervalMinutes !== "number" ||
|
|
137
|
+
!Number.isFinite(pollIntervalMinutes) ||
|
|
138
|
+
pollIntervalMinutes <= 0) {
|
|
139
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings[${index}].pollIntervalMinutes must be a positive number`);
|
|
140
|
+
}
|
|
141
|
+
if (seen.has(remainingPercent)) {
|
|
142
|
+
throw new Error(`Invalid config: watch.graphqlQuotaWarnings has duplicate remainingPercent ${remainingPercent}`);
|
|
143
|
+
}
|
|
144
|
+
seen.add(remainingPercent);
|
|
145
|
+
return { remainingPercent, pollIntervalMinutes };
|
|
146
|
+
});
|
|
147
|
+
return parsed.sort((left, right) => right.remainingPercent - left.remainingPercent);
|
|
148
|
+
}
|
|
118
149
|
const KNOWN_CONFIG_KEYS = new Set([
|
|
119
150
|
"classify",
|
|
120
151
|
"botUsernames",
|
|
@@ -135,7 +166,7 @@ const KNOWN_NESTED_KEYS = {
|
|
|
135
166
|
"minimizeComments",
|
|
136
167
|
"behindBaseHint",
|
|
137
168
|
]),
|
|
138
|
-
watch: new Set(["readyDelayMinutes"]),
|
|
169
|
+
watch: new Set(["readyDelayMinutes", "graphqlQuotaWarnings"]),
|
|
139
170
|
resolve: new Set(["shaPoll"]),
|
|
140
171
|
checks: new Set(["ciTriggerEvents"]),
|
|
141
172
|
mergeStatus: new Set(["blockingReviewerLogins"]),
|
|
@@ -228,6 +259,7 @@ export function loadConfig() {
|
|
|
228
259
|
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
229
260
|
if (config.merge)
|
|
230
261
|
config.merge.commandArgs = parseMergeCommandArgs(config.merge.commandArgs);
|
|
262
|
+
config.watch.graphqlQuotaWarnings = parseGraphqlQuotaWarnings(config.watch.graphqlQuotaWarnings);
|
|
231
263
|
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
232
264
|
configCache.set(cwd, config);
|
|
233
265
|
return config;
|
package/bin/config.json
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
"behindBaseHint": ""
|
|
23
23
|
},
|
|
24
24
|
"watch": {
|
|
25
|
-
"readyDelayMinutes": 10
|
|
25
|
+
"readyDelayMinutes": 10,
|
|
26
|
+
"graphqlQuotaWarnings": [
|
|
27
|
+
{ "remainingPercent": 30, "pollIntervalMinutes": 2 },
|
|
28
|
+
{ "remainingPercent": 20, "pollIntervalMinutes": 5 },
|
|
29
|
+
{ "remainingPercent": 10, "pollIntervalMinutes": 10 }
|
|
30
|
+
]
|
|
26
31
|
},
|
|
27
32
|
"resolve": {
|
|
28
33
|
"shaPoll": {
|
package/bin/exit-codes.d.mts
CHANGED
|
@@ -35,7 +35,7 @@ export declare const EXIT: Readonly<{
|
|
|
35
35
|
readonly UNAVAILABLE: 69;
|
|
36
36
|
/** Unexpected/unclassified internal error — the fallback. */
|
|
37
37
|
readonly SOFTWARE: 70;
|
|
38
|
-
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted,
|
|
38
|
+
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted, Retry-After, GraphQL INTERNAL. */
|
|
39
39
|
readonly TEMPFAIL: 75;
|
|
40
40
|
/** GitHub 401/403 — missing token or insufficient PAT scopes. */
|
|
41
41
|
readonly NOPERM: 77;
|