@henryqw/pi-pr 4.0.5 → 4.0.6

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/README.md CHANGED
@@ -129,17 +129,17 @@ The sweep runs existing non-destructive checks on the clean committed `HEAD` bef
129
129
 
130
130
  ### Refresh
131
131
 
132
- The footer and widget load at session start. A directory outside a Git worktree stays silent and does not start polling. The UI shows `PR · status unavailable` for other discovery failures and reports only a generic error.
132
+ The footer and widget load once at session start. A directory outside a Git worktree stays silent. The UI shows `PR · status unavailable` for other discovery failures and reports only a generic error.
133
133
 
134
- They refresh after local commits, PR creation, pushes, and each dispatched workflow settles. During creation, intermediate refreshes wait until the workflow settles. They also refresh after any successful delegated task settles. Active Git worktrees poll every 30 seconds. Polling updates presentation only and may be stale.
134
+ They refresh after local commits, PR creation, pushes, and each dispatched workflow settles. During creation, intermediate refreshes wait until the workflow settles. They also refresh after any successful delegated task settles. There is no periodic presentation refresh, so external changes may leave the footer and widget stale indefinitely. `/pr` reads fresh state before routing or acting and remains authoritative.
135
135
 
136
- The create widget stays hidden until the local branch has a commit beyond its creation point. `/pr` replaces any hint with routing feedback while it selects a route. The feedback clears before route interaction. A dispatched workflow keeps the widget hidden until the agent settles. Direct and no-action routes refresh it after completion. A failed command restores the prior hint and schedules a refresh.
136
+ The create widget stays hidden until the local branch has a commit beyond its creation point. `/pr` replaces any hint with routing feedback while it selects a route. The feedback clears before route interaction. A dispatched workflow keeps the widget hidden until the agent settles. Direct and no-action routes refresh it after completion. A failed command restores the prior hint and schedules a refresh, except when fresh lookup hits the GitHub API quota: it shows the sanitized message `GitHub API rate limit exhausted; retry after GitHub resets it` and does not immediately retry.
137
137
 
138
138
  Presentation uses route priority, so draft appears before running CI. `/pr` reads fresh state before routing or merging. The command is authoritative for actions.
139
139
 
140
140
  ### Session identity
141
141
 
142
- The extension records one configured PR identity in the Pi session. It stores only the PR URL, number, host, head identity, and configured target identity. It does not store lifecycle, CI, review, readiness, or base state. Repeated polling does not add duplicate entries, and no repository cache file is created.
142
+ The extension records one configured PR identity in the Pi session. It stores only the PR URL, number, host, head identity, and configured target identity. It does not store lifecycle, CI, review, readiness, or base state. Event-driven refreshes do not add duplicate entries, and no repository cache file is created.
143
143
 
144
144
  Normal discovery always runs first. If the configured remote ref was deleted, the footer and `/pr` may reload the exact observed PR URL. The current host, repository, branch, remote, ref, and local HEAD must still match the observation. Repository names use case-insensitive GitHub matching.
145
145
 
@@ -149,7 +149,7 @@ The GitHub response must match the observed URL, host, repository, head ref, hea
149
149
 
150
150
  - `/pr` accepts creation syntax only as a leading `--base BRANCH`, followed by optional creation guidance. It does not open a browser.
151
151
  - It does not run `/done` or `/sweep`.
152
- - Polling does not auto-triage comments or start a workflow. The package comment sweep runs only when an explicit `/pr` selects it.
152
+ - Presentation refreshes do not auto-triage comments or start a workflow. The package comment sweep runs only when an explicit `/pr` selects it.
153
153
  - It does not enable auto-merge or add a merge queue.
154
154
  - It does not rebase the local branch, overwrite concurrent remote updates, delete branches, or clean up worktrees. Creation uses exact leases plus ancestry checks; an empty lease is only an atomic absence check.
155
155
  - Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
@@ -66,6 +66,13 @@ export class PullRequestLoadError extends Error {
66
66
  }
67
67
  }
68
68
 
69
+ export class GitHubRateLimitError extends PullRequestLoadError {
70
+ constructor() {
71
+ super("GitHub API rate limit exhausted; retry after GitHub resets it");
72
+ this.name = "GitHubRateLimitError";
73
+ }
74
+ }
75
+
69
76
  export type PullRequestRef = {
70
77
  repository: string;
71
78
  ref: string;
@@ -369,7 +376,11 @@ async function invoke(
369
376
  return parseCommandOutput(result, action);
370
377
  }
371
378
 
372
- function commandFailure(action: string, result: CommandOutput): never {
379
+ function commandFailure(action: string, result: CommandOutput, command?: string): never {
380
+ if (
381
+ command === "gh" && !result.killed && result.code !== 0 &&
382
+ result.stderr.includes("GraphQL: API rate limit exceeded")
383
+ ) throw new GitHubRateLimitError();
373
384
  fail(action, result.killed ? "command was cancelled" : `exit code ${result.code}`);
374
385
  }
375
386
 
@@ -381,7 +392,7 @@ async function execute(
381
392
  args: string[],
382
393
  ): Promise<CommandOutput> {
383
394
  const result = await invoke(pi, context, action, command, args);
384
- if (result.killed || result.code !== 0) commandFailure(action, result);
395
+ if (result.killed || result.code !== 0) commandFailure(action, result, command);
385
396
  return result;
386
397
  }
387
398
 
@@ -1265,6 +1276,7 @@ async function readRemoteAuthority(
1265
1276
  ) fail("Read fetch repository", "fetch and push repositories do not match");
1266
1277
  return { fetchSource: pushUrl.fetchSource, repository: pushRepository };
1267
1278
  } catch (error) {
1279
+ if (error instanceof GitHubRateLimitError) throw error;
1268
1280
  if (!strict && error instanceof PullRequestLoadError) return null;
1269
1281
  throw error;
1270
1282
  }
package/extensions/pr.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  } from "./pr-command.ts";
20
20
  import { PullRequestCreator, type CreatePullRequestOptions } from "./pr-create.ts";
21
21
  import {
22
+ GitHubRateLimitError,
22
23
  loadCurrentPullRequest,
23
24
  parsePullRequestObservation,
24
25
  pullRequestObservation,
@@ -37,7 +38,6 @@ import {
37
38
  } from "./pr-ui.ts";
38
39
  import { PullRequestBranchUpdater, type UpdateBranchOptions } from "./pr-update-branch.ts";
39
40
 
40
- const POLL_INTERVAL_MS = 30_000;
41
41
  const ROUTING_SPINNER_INTERVAL_MS = 80;
42
42
  const ROUTING_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
43
43
  const ROUTING_WIDGET_TEXT = "Checking pull request…";
@@ -282,10 +282,9 @@ export default function pullRequestExtension(
282
282
  return discovery;
283
283
  };
284
284
  let sessionGeneration = 0;
285
- let timer: ReturnType<typeof setInterval> | undefined;
286
285
  let active: AbortController | undefined;
287
286
  let queued = false;
288
- let refreshFailureReported = false;
287
+ let reportedRefreshFailure: "generic" | "quota" | undefined;
289
288
  let displayEstablished = false;
290
289
  let lastDiscovery: "configured" | "inferred" | "absent" | "blocked" | "inactive" | undefined;
291
290
  let lastBlockedIssueKey: string | undefined;
@@ -561,8 +560,6 @@ export default function pullRequestExtension(
561
560
  discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
562
561
  ): void => {
563
562
  if (discovery.kind === "inactive") {
564
- if (timer !== undefined) clearInterval(timer);
565
- timer = undefined;
566
563
  displayEstablished = true;
567
564
  lastDiscovery = "inactive";
568
565
  displayedWidget = undefined;
@@ -600,7 +597,7 @@ export default function pullRequestExtension(
600
597
  context = undefined;
601
598
  observation = undefined;
602
599
  queued = false;
603
- refreshFailureReported = false;
600
+ reportedRefreshFailure = undefined;
604
601
  displayEstablished = false;
605
602
  lastDiscovery = undefined;
606
603
  lastBlockedIssueKey = undefined;
@@ -612,18 +609,22 @@ export default function pullRequestExtension(
612
609
  activeInvocations.clear();
613
610
  stopRoutingSpinner();
614
611
  widgetKind = "presentation";
615
- if (timer !== undefined) clearInterval(timer);
616
- timer = undefined;
617
612
  active?.abort();
618
613
  active = undefined;
619
614
  };
620
615
 
621
- const reportRefreshFailure = (): void => {
616
+ const reportRefreshFailure = (error: unknown): void => {
622
617
  const ctx = context;
623
- if (!ctx || refreshFailureReported) return;
624
- refreshFailureReported = true;
618
+ const category = error instanceof GitHubRateLimitError ? "quota" : "generic";
619
+ if (!ctx || reportedRefreshFailure === category || reportedRefreshFailure === "quota") return;
620
+ reportedRefreshFailure = category;
625
621
  try {
626
- ctx.ui.notify("PR status refresh failed: status unavailable", "error");
622
+ ctx.ui.notify(
623
+ error instanceof GitHubRateLimitError
624
+ ? error.message
625
+ : "PR status refresh failed: status unavailable",
626
+ "error",
627
+ );
627
628
  } catch {
628
629
  console.error("PR status refresh failed and could not be reported");
629
630
  }
@@ -655,7 +656,7 @@ export default function pullRequestExtension(
655
656
  try {
656
657
  discovery = await load(pi, loadContext);
657
658
  if (controller.signal.aborted || sessionGeneration !== generation) return;
658
- } catch {
659
+ } catch (error) {
659
660
  // Keep an established footer. A refresh failure must not leave a stale action hint.
660
661
  if (!controller.signal.aborted && sessionGeneration === generation) {
661
662
  displayedWidget = undefined;
@@ -665,13 +666,13 @@ export default function pullRequestExtension(
665
666
  ctx.ui.setStatus(UI_KEY, formatPrFooter(unavailable, ctx.ui.theme));
666
667
  displayEstablished = true;
667
668
  }
668
- reportRefreshFailure();
669
+ reportRefreshFailure(error);
669
670
  }
670
671
  return;
671
672
  }
672
673
  if (controller.signal.aborted || sessionGeneration !== generation) return;
673
674
  render(ctx, discovery);
674
- refreshFailureReported = false;
675
+ reportedRefreshFailure = undefined;
675
676
 
676
677
  const pullRequest = discovery.kind === "current" ? discovery.pullRequest : undefined;
677
678
  if (pendingWorkspaceRename && pullRequest?.target.provenance === "configured") {
@@ -716,14 +717,10 @@ export default function pullRequestExtension(
716
717
 
717
718
  pi.on("session_start", async (_event, ctx) => {
718
719
  stop();
719
- const generation = sessionGeneration;
720
720
  observation = latestObservation(ctx);
721
721
  if (!ctx.hasUI) return;
722
722
  context = ctx;
723
723
  await refresh();
724
- if (sessionGeneration === generation && lastDiscovery !== "inactive") {
725
- timer = setInterval(refreshInBackground, POLL_INTERVAL_MS);
726
- }
727
724
  });
728
725
 
729
726
  pi.on("session_shutdown", stop);
@@ -805,8 +802,14 @@ export default function pullRequestExtension(
805
802
  if (sessionGeneration === generation) {
806
803
  cancelRefresh();
807
804
  activeInvocations.delete(invocation);
808
- reconcileWidget(ctx);
809
- refreshInBackground();
805
+ if (error instanceof GitHubRateLimitError) {
806
+ displayedWidget = undefined;
807
+ reconcileWidget(ctx);
808
+ reportRefreshFailure(error);
809
+ } else {
810
+ reconcileWidget(ctx);
811
+ refreshInBackground();
812
+ }
810
813
  }
811
814
  throw error;
812
815
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-pr",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "description": "Run /pr to safely discover or link the current pull request, then create, update, address feedback, fix CI, or merge when ready.",
5
5
  "keywords": [
6
6
  "pi-package",