pr-shepherd 0.52.1 → 0.53.1
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 +15 -13
- package/bin/cli/api-usage-formatter.mjs +17 -3
- package/bin/cli/iterate-instructions.mjs +2 -21
- package/bin/cli/poll-summary-formatter.mjs +1 -2
- package/bin/commands/iterate/api-usage.mjs +4 -3
- package/bin/commands/iterate/index.mjs +3 -12
- package/bin/commands/iterate/parent-first.d.mts +6 -23
- package/bin/commands/iterate/parent-first.mjs +7 -64
- package/bin/commands/poll-quota.d.mts +18 -6
- package/bin/commands/poll-quota.mjs +55 -17
- package/bin/commands/poll-summary-instructions.mjs +26 -41
- package/bin/commands/poll-summary.mjs +6 -6
- package/bin/commands/poll.mjs +8 -10
- package/bin/commands/quota-selection.d.mts +7 -0
- package/bin/commands/quota-selection.mjs +20 -0
- package/bin/commands/stack-drain.d.mts +5 -5
- package/bin/commands/stack-drain.mjs +39 -30
- package/bin/commands/stack-layer-readiness.d.mts +2 -3
- package/bin/commands/stack-layer-readiness.mjs +4 -5
- package/bin/commands/stack-stall.mjs +0 -1
- package/bin/commands/stack-work.d.mts +4 -4
- package/bin/commands/stack-work.mjs +5 -6
- package/bin/github/api-telemetry-aggregate.d.mts +1 -0
- package/bin/github/api-telemetry-aggregate.mjs +8 -2
- package/bin/github/api-telemetry.d.mts +4 -0
- package/bin/github/api-telemetry.mjs +12 -0
- package/bin/github/graphql-http.mjs +6 -0
- package/bin/github/http-auth.d.mts +3 -0
- package/bin/github/http-auth.mjs +6 -0
- package/bin/github/http-intermediate.d.mts +1 -0
- package/bin/github/http-intermediate.mjs +3 -0
- package/bin/quota-budgets.d.mts +16 -0
- package/bin/quota-budgets.mjs +65 -0
- package/bin/quota-warning.mjs +11 -1
- package/bin/state/graphql-quota-policy.d.mts +7 -1
- package/bin/state/graphql-quota-policy.mjs +42 -15
- package/bin/state/graphql-quota-warnings.d.mts +2 -2
- package/bin/state/graphql-quota-warnings.mjs +7 -4
- package/bin/types/api-usage.d.mts +14 -1
- package/bin/types/merge-requirements.d.mts +3 -13
- package/bin/types/poll-summary.d.mts +0 -2
- 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
|
@@ -4,40 +4,67 @@ export function evaluateGraphqlQuotaWarning(bands, sample, previous, observedAt
|
|
|
4
4
|
previous.resource !== sample.resource ||
|
|
5
5
|
previous.limit !== sample.limit ||
|
|
6
6
|
windowRolled ||
|
|
7
|
-
(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
fingerprintChanged(previous, sample);
|
|
8
|
+
const effective = !rearm && previous !== null && usageRegressed(previous, sample)
|
|
9
|
+
? newerSavedSample(previous)
|
|
10
|
+
: sample;
|
|
11
|
+
const warned = new Set(rearm || previous === null ? [] : previous.warnedThresholds);
|
|
11
12
|
const rearmEpoch = rearm ? (previous?.rearmEpoch ?? 0) + 1 : (previous?.rearmEpoch ?? 0);
|
|
12
|
-
const crossed = bands.filter((band) =>
|
|
13
|
+
const crossed = bands.filter((band) => effective.remaining * 100 <= effective.limit * band.remainingPercent);
|
|
13
14
|
const newCrossed = crossed.filter((band) => !warned.has(band.remainingPercent));
|
|
14
15
|
for (const band of crossed)
|
|
15
16
|
warned.add(band.remainingPercent);
|
|
16
17
|
const active = newCrossed.at(-1);
|
|
18
|
+
const credentialFingerprint = effective.credentialFingerprint ?? previous?.credentialFingerprint;
|
|
17
19
|
const state = {
|
|
18
|
-
resource:
|
|
19
|
-
limit:
|
|
20
|
-
...(
|
|
21
|
-
lastRemaining:
|
|
22
|
-
resetAt:
|
|
20
|
+
resource: effective.resource,
|
|
21
|
+
limit: effective.limit,
|
|
22
|
+
...(effective.used !== undefined && { lastUsed: effective.used }),
|
|
23
|
+
lastRemaining: effective.remaining,
|
|
24
|
+
resetAt: effective.resetAt,
|
|
23
25
|
warnedThresholds: bands
|
|
24
26
|
.map((band) => band.remainingPercent)
|
|
25
27
|
.filter((threshold) => warned.has(threshold)),
|
|
28
|
+
...(credentialFingerprint !== undefined && { credentialFingerprint }),
|
|
26
29
|
rearmEpoch,
|
|
27
30
|
};
|
|
28
31
|
if (active === undefined)
|
|
29
32
|
return { state };
|
|
30
33
|
return {
|
|
31
34
|
warning: {
|
|
32
|
-
resource: "graphql",
|
|
35
|
+
resource: sample.resource === "core" ? "core" : "graphql",
|
|
33
36
|
thresholdPercent: active.remainingPercent,
|
|
34
|
-
remaining:
|
|
35
|
-
limit:
|
|
36
|
-
...(
|
|
37
|
-
resetAt:
|
|
37
|
+
remaining: effective.remaining,
|
|
38
|
+
limit: effective.limit,
|
|
39
|
+
...(effective.used !== undefined && { used: effective.used }),
|
|
40
|
+
resetAt: effective.resetAt,
|
|
38
41
|
pollIntervalMinutes: active.pollIntervalMinutes,
|
|
39
42
|
pollTimeoutMinutes: active.pollIntervalMinutes * 2,
|
|
40
43
|
},
|
|
41
44
|
state,
|
|
42
45
|
};
|
|
43
46
|
}
|
|
47
|
+
function fingerprintChanged(previous, sample) {
|
|
48
|
+
return (previous.credentialFingerprint !== undefined &&
|
|
49
|
+
sample.credentialFingerprint !== undefined &&
|
|
50
|
+
previous.credentialFingerprint !== sample.credentialFingerprint);
|
|
51
|
+
}
|
|
52
|
+
/** An older observation of the same window: lower used, or higher remaining when used is absent. */
|
|
53
|
+
function usageRegressed(previous, sample) {
|
|
54
|
+
if (sample.used !== undefined && previous.lastUsed !== undefined) {
|
|
55
|
+
return sample.used < previous.lastUsed;
|
|
56
|
+
}
|
|
57
|
+
return sample.remaining > previous.lastRemaining;
|
|
58
|
+
}
|
|
59
|
+
function newerSavedSample(previous) {
|
|
60
|
+
return {
|
|
61
|
+
resource: previous.resource,
|
|
62
|
+
limit: previous.limit,
|
|
63
|
+
...(previous.lastUsed !== undefined && { used: previous.lastUsed }),
|
|
64
|
+
remaining: previous.lastRemaining,
|
|
65
|
+
resetAt: previous.resetAt,
|
|
66
|
+
...(previous.credentialFingerprint !== undefined && {
|
|
67
|
+
credentialFingerprint: previous.credentialFingerprint,
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GraphqlQuotaWarningBand } from "../config/load.mts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ApiResourceUsage, GraphqlQuotaWarning } from "../types.mts";
|
|
3
3
|
export declare function evaluateWorktreeGraphqlQuotaWarning(key: {
|
|
4
4
|
owner: string;
|
|
5
5
|
repo: string;
|
|
6
|
-
}, bands: GraphqlQuotaWarningBand[], sample:
|
|
6
|
+
}, bands: GraphqlQuotaWarningBand[], sample: ApiResourceUsage, persist: boolean, now?: number): Promise<GraphqlQuotaWarning | undefined>;
|
|
@@ -10,9 +10,10 @@ const sessionStates = new Map();
|
|
|
10
10
|
export async function evaluateWorktreeGraphqlQuotaWarning(key, bands, sample, persist, now = Date.now() / 1000) {
|
|
11
11
|
if (bands.length === 0)
|
|
12
12
|
return undefined;
|
|
13
|
-
const
|
|
13
|
+
const quotaResource = sample.resource === "core" ? "core" : "graphql";
|
|
14
|
+
const path = await warningStatePath(key, quotaResource);
|
|
14
15
|
if (path === undefined) {
|
|
15
|
-
const sessionKey = `${key.owner}/${key.repo}`;
|
|
16
|
+
const sessionKey = `${key.owner}/${key.repo}/${quotaResource}`;
|
|
16
17
|
if (!persist) {
|
|
17
18
|
return evaluateGraphqlQuotaWarning(bands, sample, sessionStates.get(sessionKey) ?? null, now)
|
|
18
19
|
.warning;
|
|
@@ -50,7 +51,7 @@ async function serializeStateUpdate(key, update) {
|
|
|
50
51
|
});
|
|
51
52
|
return result;
|
|
52
53
|
}
|
|
53
|
-
async function warningStatePath(key) {
|
|
54
|
+
async function warningStatePath(key, resource) {
|
|
54
55
|
let worktreeKey;
|
|
55
56
|
try {
|
|
56
57
|
worktreeKey = await getWorktreeKey();
|
|
@@ -58,7 +59,7 @@ async function warningStatePath(key) {
|
|
|
58
59
|
catch {
|
|
59
60
|
return undefined;
|
|
60
61
|
}
|
|
61
|
-
return join(resolveRepoStateDir(key), "worktrees", `${worktreeKey}-
|
|
62
|
+
return join(resolveRepoStateDir(key), "worktrees", `${worktreeKey}-${resource}-quota-warnings.json`);
|
|
62
63
|
}
|
|
63
64
|
async function readState(path) {
|
|
64
65
|
try {
|
|
@@ -70,6 +71,8 @@ async function readState(path) {
|
|
|
70
71
|
!Array.isArray(parsed.warnedThresholds)) {
|
|
71
72
|
return null;
|
|
72
73
|
}
|
|
74
|
+
if (typeof parsed.credentialFingerprint !== "string")
|
|
75
|
+
delete parsed.credentialFingerprint;
|
|
73
76
|
return parsed;
|
|
74
77
|
}
|
|
75
78
|
catch {
|
|
@@ -5,6 +5,8 @@ export interface ApiResourceUsage {
|
|
|
5
5
|
used?: number;
|
|
6
6
|
remaining: number;
|
|
7
7
|
resetAt: number;
|
|
8
|
+
/** Truncated SHA-256 of the credential. Quota state only; summarized usage omits it. */
|
|
9
|
+
credentialFingerprint?: string;
|
|
8
10
|
}
|
|
9
11
|
export interface GraphqlApiUsage extends ApiResourceUsage {
|
|
10
12
|
/** Exact sum reported by rateLimit.cost for GraphQL queries in this command. */
|
|
@@ -14,13 +16,22 @@ export interface GraphqlApiUsage extends ApiResourceUsage {
|
|
|
14
16
|
/** Exact sum reported by rateLimit.nodeCount for measured GraphQL queries. */
|
|
15
17
|
nodeCount: number;
|
|
16
18
|
}
|
|
19
|
+
/** One budget inside a combined GraphQL and REST core warning. */
|
|
20
|
+
export interface QuotaWarningBudget {
|
|
21
|
+
resource: "graphql" | "core";
|
|
22
|
+
thresholdPercent: number;
|
|
23
|
+
remaining: number;
|
|
24
|
+
limit: number;
|
|
25
|
+
used?: number;
|
|
26
|
+
resetAt: number;
|
|
27
|
+
}
|
|
17
28
|
export interface ApiUsage {
|
|
18
29
|
credentialSources: string[];
|
|
19
30
|
graphql?: GraphqlApiUsage;
|
|
20
31
|
rest?: ApiResourceUsage[];
|
|
21
32
|
}
|
|
22
33
|
export interface GraphqlQuotaWarning {
|
|
23
|
-
resource: "graphql";
|
|
34
|
+
resource: "graphql" | "core" | "combined";
|
|
24
35
|
thresholdPercent: number;
|
|
25
36
|
remaining: number;
|
|
26
37
|
limit: number;
|
|
@@ -28,4 +39,6 @@ export interface GraphqlQuotaWarning {
|
|
|
28
39
|
resetAt: number;
|
|
29
40
|
pollIntervalMinutes: number;
|
|
30
41
|
pollTimeoutMinutes: number;
|
|
42
|
+
/** Both budgets when GraphQL and REST core are low together. */
|
|
43
|
+
budgets?: QuotaWarningBudget[];
|
|
31
44
|
}
|
|
@@ -39,23 +39,13 @@ export interface StackStatus {
|
|
|
39
39
|
position: number;
|
|
40
40
|
baseRefName: string;
|
|
41
41
|
}
|
|
42
|
-
/** Why an open native stack layer
|
|
42
|
+
/** Why an open native stack layer is not ready to merge. */
|
|
43
43
|
export type StackLayerBlockReason = "closed" | "draft" | "conflicting" | "queue-removal" | "failing-checks" | "review-work" | "checks-in-progress" | "merge-state" | "no-ready-receipt" | "stale-ancestry";
|
|
44
|
-
/** The lowest native stack layer that keeps an upper draft from being marked ready. */
|
|
45
|
-
export interface StackLowerLayerBlock {
|
|
46
|
-
pr: number;
|
|
47
|
-
reason: StackLayerBlockReason;
|
|
48
|
-
}
|
|
49
44
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `lowerLayer` is the layer to advance first; without one, the lower layers could not
|
|
53
|
-
* be verified.
|
|
45
|
+
* A native stack draft stays in draft on a WAIT tick because this session will not
|
|
46
|
+
* mark it ready. The caller returns to the stack selector.
|
|
54
47
|
*/
|
|
55
48
|
export type StackDraftHold = {
|
|
56
|
-
kind: "lower-layer-not-ready";
|
|
57
|
-
lowerLayer?: StackLowerLayerBlock;
|
|
58
|
-
} | {
|
|
59
49
|
kind: "auto-mark-ready-disabled";
|
|
60
50
|
};
|
|
61
51
|
/** Extra batch-PR fields for merge-queue, stacks, and folded branch rules. */
|
|
@@ -65,8 +65,6 @@ export interface PollSummaryItem {
|
|
|
65
65
|
pollProbe?: true;
|
|
66
66
|
/** A current one-PR READY-after-delay completion was verified. */
|
|
67
67
|
readyReceipt?: true;
|
|
68
|
-
/** Lowest unready ancestor that prevents this layer from being stack-mergeable. */
|
|
69
|
-
blockedByPr?: number;
|
|
70
68
|
}
|
|
71
69
|
export type PollSummarySelection = {
|
|
72
70
|
kind: "prs";
|
package/package.json
CHANGED