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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -13
  3. package/bin/cli/api-usage-formatter.mjs +17 -3
  4. package/bin/cli/iterate-instructions.mjs +2 -21
  5. package/bin/cli/poll-summary-formatter.mjs +1 -2
  6. package/bin/commands/iterate/api-usage.mjs +4 -3
  7. package/bin/commands/iterate/index.mjs +3 -12
  8. package/bin/commands/iterate/parent-first.d.mts +6 -23
  9. package/bin/commands/iterate/parent-first.mjs +7 -64
  10. package/bin/commands/poll-quota.d.mts +18 -6
  11. package/bin/commands/poll-quota.mjs +55 -17
  12. package/bin/commands/poll-summary-instructions.mjs +26 -41
  13. package/bin/commands/poll-summary.mjs +6 -6
  14. package/bin/commands/poll.mjs +8 -10
  15. package/bin/commands/quota-selection.d.mts +7 -0
  16. package/bin/commands/quota-selection.mjs +20 -0
  17. package/bin/commands/stack-drain.d.mts +5 -5
  18. package/bin/commands/stack-drain.mjs +39 -30
  19. package/bin/commands/stack-layer-readiness.d.mts +2 -3
  20. package/bin/commands/stack-layer-readiness.mjs +4 -5
  21. package/bin/commands/stack-stall.mjs +0 -1
  22. package/bin/commands/stack-work.d.mts +4 -4
  23. package/bin/commands/stack-work.mjs +5 -6
  24. package/bin/github/api-telemetry-aggregate.d.mts +1 -0
  25. package/bin/github/api-telemetry-aggregate.mjs +8 -2
  26. package/bin/github/api-telemetry.d.mts +4 -0
  27. package/bin/github/api-telemetry.mjs +12 -0
  28. package/bin/github/graphql-http.mjs +6 -0
  29. package/bin/github/http-auth.d.mts +3 -0
  30. package/bin/github/http-auth.mjs +6 -0
  31. package/bin/github/http-intermediate.d.mts +1 -0
  32. package/bin/github/http-intermediate.mjs +3 -0
  33. package/bin/quota-budgets.d.mts +16 -0
  34. package/bin/quota-budgets.mjs +65 -0
  35. package/bin/quota-warning.mjs +11 -1
  36. package/bin/state/graphql-quota-policy.d.mts +7 -1
  37. package/bin/state/graphql-quota-policy.mjs +42 -15
  38. package/bin/state/graphql-quota-warnings.d.mts +2 -2
  39. package/bin/state/graphql-quota-warnings.mjs +7 -4
  40. package/bin/types/api-usage.d.mts +14 -1
  41. package/bin/types/merge-requirements.d.mts +3 -13
  42. package/bin/types/poll-summary.d.mts +0 -2
  43. package/package.json +1 -1
  44. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  45. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  46. 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
- (sample.used !== undefined && previous.lastUsed !== undefined
8
- ? sample.used < previous.lastUsed
9
- : sample.remaining > previous.lastRemaining);
10
- const warned = new Set(rearm ? [] : previous.warnedThresholds);
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) => sample.remaining * 100 <= sample.limit * band.remainingPercent);
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: sample.resource,
19
- limit: sample.limit,
20
- ...(sample.used !== undefined && { lastUsed: sample.used }),
21
- lastRemaining: sample.remaining,
22
- resetAt: sample.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: sample.remaining,
35
- limit: sample.limit,
36
- ...(sample.used !== undefined && { used: sample.used }),
37
- resetAt: sample.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 { GraphqlQuotaWarning, GraphqlApiUsage } from "../types.mts";
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: GraphqlApiUsage, persist: boolean, now?: number): Promise<GraphqlQuotaWarning | undefined>;
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 path = await warningStatePath(key);
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}-graphql-quota-warnings.json`);
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 does not yet let the layers above it advance. */
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
- * Why a native stack layer stays in draft on a WAIT tick. Repeating the same one-PR
51
- * session cannot advance it, so the caller returns to the stack selector. A named
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.52.1",
3
+ "version": "0.53.1",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.52.1",
3
+ "version": "0.53.1",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pr-shepherd": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "pr-shepherd@0.52.1", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.53.1", "pr-shepherd-mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pr-shepherd": {
3
3
  "command": "npx",
4
- "args": ["--yes", "--package", "pr-shepherd@0.52.1", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.53.1", "pr-shepherd-mcp"]
5
5
  }
6
6
  }