@henryqw/pi-pr 4.0.5 → 4.0.7

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
@@ -70,7 +70,7 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
70
70
  | Base update required or merge conflict | Update from the base branch's current target when the tree is clean and local HEAD equals the PR head. |
71
71
  | GitHub Actions job failed | Run the CI fix workflow when the same local prerequisite holds. |
72
72
  | External check or commit status failed | Show `CI failed` as a no-action blocker. |
73
- | Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
73
+ | Changes requested or unresolved review threads | Start or resume the package comment sweep when the same local prerequisite holds. |
74
74
  | No-action state | Report the state without taking action. |
75
75
  | Merge-ready pull request | Ask for final confirmation, recheck fresh state, and squash-merge if confirmed. |
76
76
 
@@ -90,6 +90,10 @@ The creation workflow repeats destination, remote OID, PR, and configuration che
90
90
 
91
91
  Each helper workflow receives a random run ID and its first action. The run stays bound to one session, canonical worktree, route, and fresh authority. Helper calls from another run, session, worktree, or route fail.
92
92
 
93
+ For comment sweeps, `/pr` checks the package recovery file without changing it. It selects `start` when recovery is absent. It selects `resume` only when valid recovery matches the fresh route authority. Invalid recovery stays unchanged and blocks dispatch with its path and reason.
94
+
95
+ Direct skill or `pi_pr_*` tool calls cannot create route authority. Run `/pr` to reserve a fresh route.
96
+
93
97
  Only one helper run can exist at a time. Most runs expire when the agent settles. A create or branch-update conflict stays available for one user-guided continuation, then expires after that continuation settles. Session replacement and shutdown forget the run without aborting or cleaning a pending merge.
94
98
 
95
99
  After a `/pr` create workflow settles, the extension waits for a refresh that finds a configured current PR. It then prefixes the Herdr workspace label with `#<number> • `.
@@ -129,17 +133,17 @@ The sweep runs existing non-destructive checks on the clean committed `HEAD` bef
129
133
 
130
134
  ### Refresh
131
135
 
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.
136
+ 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
137
 
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.
138
+ 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
139
 
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.
140
+ 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
141
 
138
142
  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
143
 
140
144
  ### Session identity
141
145
 
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.
146
+ 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
147
 
144
148
  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
149
 
@@ -149,7 +153,7 @@ The GitHub response must match the observed URL, host, repository, head ref, hea
149
153
 
150
154
  - `/pr` accepts creation syntax only as a leading `--base BRANCH`, followed by optional creation guidance. It does not open a browser.
151
155
  - 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.
156
+ - Presentation refreshes do not auto-triage comments or start a workflow. The package comment sweep starts or resumes only when an explicit `/pr` selects it.
153
157
  - It does not enable auto-merge or add a merge queue.
154
158
  - 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
159
  - Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
@@ -17,15 +17,17 @@ import {
17
17
 
18
18
  export type WorkflowNextStep = Extract<NextStep, "create" | "update-branch" | "sweep" | "fix-ci">;
19
19
 
20
- const WORKFLOWS: Record<WorkflowNextStep, { command: string; action: string }> = {
21
- create: { command: "skill:pi-pr-create", action: "prepare" },
22
- "update-branch": { command: "skill:pi-pr-update-branch", action: "merge" },
23
- sweep: { command: "skill:pi-pr-comment-sweep", action: "start" },
24
- "fix-ci": { command: "skill:pi-pr-fix-ci", action: "collect" },
20
+ const WORKFLOWS: Record<WorkflowNextStep, { command: string }> = {
21
+ create: { command: "skill:pi-pr-create" },
22
+ "update-branch": { command: "skill:pi-pr-update-branch" },
23
+ sweep: { command: "skill:pi-pr-comment-sweep" },
24
+ "fix-ci": { command: "skill:pi-pr-fix-ci" },
25
25
  };
26
26
  export type WorkflowReservation =
27
27
  | { route: "create"; target: PullRequestTarget; base?: string }
28
28
  | { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
29
+ export type WorkflowLaunchAction = "prepare" | "merge" | "start" | "resume" | "collect";
30
+ export type WorkflowReservationResult = { runId: string; action: WorkflowLaunchAction };
29
31
 
30
32
  type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
31
33
  export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
@@ -42,7 +44,7 @@ export type WorkflowPromptIdentity = Readonly<{
42
44
  route: WorkflowNextStep;
43
45
  skill: string;
44
46
  runId: string;
45
- action: string;
47
+ action: WorkflowLaunchAction;
46
48
  }>;
47
49
 
48
50
  export type PrCommandDependencies = {
@@ -52,7 +54,7 @@ export type PrCommandDependencies = {
52
54
  reservation: WorkflowReservation,
53
55
  ctx: ExtensionCommandContext,
54
56
  invocation?: PrCommandInvocation,
55
- ) => Promise<string>;
57
+ ) => Promise<WorkflowReservationResult>;
56
58
  markWorkflowPromptQueued?: (identity: WorkflowPromptIdentity, queued: boolean) => void;
57
59
  releaseWorkflow?: (runId: string, invocation?: PrCommandInvocation) => void;
58
60
  };
@@ -96,7 +98,7 @@ function packageWorkflowCommand(pi: PrCommandPi, route: WorkflowNextStep) {
96
98
  candidate.sourceInfo.origin === "package"
97
99
  );
98
100
  if (!command) throw new Error(`${workflow.command} failed: bundled workflow is unavailable`);
99
- return { command, action: workflow.action };
101
+ return command;
100
102
  }
101
103
 
102
104
  async function dispatchWorkflow(
@@ -113,10 +115,11 @@ async function dispatchWorkflow(
113
115
  const workflow = packageWorkflowCommand(pi, route);
114
116
  let runId: string | undefined;
115
117
  try {
116
- runId = await reserve(reservation, ctx, invocation);
118
+ const reserved = await reserve(reservation, ctx, invocation);
119
+ runId = reserved.runId;
117
120
  invocation?.assertCurrent();
118
121
  const queued = !ctx.isIdle();
119
- const identity = { route, skill: workflow.command.name, runId, action: workflow.action };
122
+ const identity = { route, skill: workflow.name, runId, action: reserved.action };
120
123
  markPromptQueued(identity, queued);
121
124
  const options = queued
122
125
  ? { deliverAs: "followUp" as const, expandPromptTemplates: true }
@@ -231,6 +231,16 @@ function sameLinkage(expected: SweepAuthority, current: SweepAuthority, remoteHe
231
231
  current.target.remoteOid === remoteHead;
232
232
  }
233
233
 
234
+ function recoveryMatchesRouteAuthority(state: SweepState, suppliedAuthority: SweepAuthority): boolean {
235
+ const permittedHeads = new Set<string>();
236
+ if (state.attempts.push.state !== "applied") permittedHeads.add(state.original.lease);
237
+ if (
238
+ (state.attempts.push.state === "attempting" || state.attempts.push.state === "unknown" || state.attempts.push.state === "applied") &&
239
+ state.publicationHead
240
+ ) permittedHeads.add(state.publicationHead);
241
+ return [...permittedHeads].some((head) => sameLinkage(state.authority, suppliedAuthority, head));
242
+ }
243
+
234
244
  function feedbackMatchesAuthority(snapshot: FeedbackSnapshot, authority: SweepAuthority, head: string): boolean {
235
245
  const current = snapshot.pullRequest;
236
246
  return current.id === authority.id && current.number === authority.number && current.url === authority.url &&
@@ -575,6 +585,17 @@ export class PullRequestCommentSweep {
575
585
  return (await this.location()).path;
576
586
  }
577
587
 
588
+ async recoveryLaunchAction(): Promise<"start" | "resume"> {
589
+ if (!this.suppliedAuthority) throw new Error("Comment sweep recovery inspection requires route authority");
590
+ const location = await this.location();
591
+ const state = await this.loadIfPresent(location);
592
+ if (!state) return "start";
593
+ if (!recoveryMatchesRouteAuthority(state, this.suppliedAuthority)) {
594
+ throw new Error(`Comment sweep recovery is preserved at ${location.path}: recovery does not match freshly discovered route authority`);
595
+ }
596
+ return "resume";
597
+ }
598
+
578
599
  private async loadState(location: Awaited<ReturnType<PullRequestCommentSweep["location"]>>): Promise<SweepState> {
579
600
  const raw = await readTextFileBounded(location.path, SWEEP_RECOVERY_MAX_BYTES, { signal: this.signal });
580
601
  let value: unknown;
@@ -789,17 +810,10 @@ export class PullRequestCommentSweep {
789
810
  async resume(): Promise<SweepStatus> {
790
811
  return await withWorktreeLock(this.cwd, async () => {
791
812
  if (!this.suppliedAuthority) throw new Error("Comment sweep resume requires route authority");
792
- const suppliedAuthority = this.suppliedAuthority;
793
813
  const location = await this.location();
794
814
  const state = await this.loadState(location);
795
- const permittedHeads = new Set<string>();
796
- if (state.attempts.push.state !== "applied") permittedHeads.add(state.original.lease);
797
- if (
798
- (state.attempts.push.state === "attempting" || state.attempts.push.state === "unknown" || state.attempts.push.state === "applied") &&
799
- state.publicationHead
800
- ) permittedHeads.add(state.publicationHead);
801
- if (![...permittedHeads].some((head) => sameLinkage(state.authority, suppliedAuthority, head))) {
802
- throw new Error("Comment sweep recovery does not match supplied route authority");
815
+ if (!recoveryMatchesRouteAuthority(state, this.suppliedAuthority)) {
816
+ throw new Error(`Comment sweep recovery is preserved at ${location.path}: recovery does not match supplied route authority`);
803
817
  }
804
818
  await this.reconcile(state);
805
819
  state.attempts.resolutions = state.attempts.resolutions.filter(({ state: attempt }) => attempt !== "blocked");
@@ -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…";
@@ -140,7 +140,7 @@ const FixCiParameters = Type.Union([
140
140
 
141
141
  type UpdateBranchWorkflow = Pick<PullRequestBranchUpdater, "state" | "merge" | "continue" | "publish">;
142
142
  type CreateWorkflow = Pick<PullRequestCreator, "state" | "prepare" | "merge" | "continue" | "push" | "publish">;
143
- type SweepWorkflow = Pick<PullRequestCommentSweep, "start" | "resume" | "show" | "record" | "publish" | "refresh" | "resolve" | "finalize">;
143
+ type SweepWorkflow = Pick<PullRequestCommentSweep, "recoveryLaunchAction" | "start" | "resume" | "show" | "record" | "publish" | "refresh" | "resolve" | "finalize">;
144
144
  type FixCiWorkflow = Pick<PullRequestCiFixer, "collect" | "publish">;
145
145
 
146
146
  type WorkflowContextBase = {
@@ -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;
@@ -338,7 +337,7 @@ export default function pullRequestExtension(
338
337
  loadCurrentPullRequest: load,
339
338
  }),
340
339
  };
341
- break;
340
+ return { runId, action: "merge" };
342
341
  case "create":
343
342
  workflowContext = {
344
343
  ...common,
@@ -351,9 +350,9 @@ export default function pullRequestExtension(
351
350
  loadCurrentPullRequest: load,
352
351
  }),
353
352
  };
354
- break;
355
- case "sweep":
356
- workflowContext = {
353
+ return { runId, action: "prepare" };
354
+ case "sweep": {
355
+ const selected: Extract<WorkflowContext, { route: "sweep" }> = {
357
356
  ...common,
358
357
  route: "sweep",
359
358
  workflow: createCommentSweep({
@@ -363,7 +362,17 @@ export default function pullRequestExtension(
363
362
  loadCurrentPullRequest: load,
364
363
  }),
365
364
  };
366
- break;
365
+ workflowContext = selected;
366
+ try {
367
+ const action = await selected.workflow.recoveryLaunchAction();
368
+ invocation.assertCurrent();
369
+ if (workflowContext !== selected) throw new Error("PR workflow session changed during recovery inspection");
370
+ return { runId, action };
371
+ } catch (error) {
372
+ clearWorkflow(selected);
373
+ throw error;
374
+ }
375
+ }
367
376
  case "fix-ci":
368
377
  workflowContext = {
369
378
  ...common,
@@ -375,9 +384,8 @@ export default function pullRequestExtension(
375
384
  loadCurrentPullRequest: load,
376
385
  }),
377
386
  };
378
- break;
387
+ return { runId, action: "collect" };
379
388
  }
380
- return common.runId;
381
389
  };
382
390
 
383
391
  const markWorkflowPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]> = (identity, queued) => {
@@ -404,7 +412,7 @@ export default function pullRequestExtension(
404
412
  ) => {
405
413
  signal?.throwIfAborted();
406
414
  const selected = workflowContext;
407
- if (!selected) throw new Error("No PR workflow is active");
415
+ if (!selected) throw new Error("No PR workflow is active; run /pr to discover and reserve the current route");
408
416
  if (selected.runId !== runId) throw new Error("PR workflow runId is wrong or stale");
409
417
  if (selected.sessionGeneration !== sessionGeneration) throw new Error("PR workflow session is stale");
410
418
  if (selected.route !== route) throw new Error(`PR workflow route is ${selected.route}, not ${route}`);
@@ -561,8 +569,6 @@ export default function pullRequestExtension(
561
569
  discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
562
570
  ): void => {
563
571
  if (discovery.kind === "inactive") {
564
- if (timer !== undefined) clearInterval(timer);
565
- timer = undefined;
566
572
  displayEstablished = true;
567
573
  lastDiscovery = "inactive";
568
574
  displayedWidget = undefined;
@@ -600,7 +606,7 @@ export default function pullRequestExtension(
600
606
  context = undefined;
601
607
  observation = undefined;
602
608
  queued = false;
603
- refreshFailureReported = false;
609
+ reportedRefreshFailure = undefined;
604
610
  displayEstablished = false;
605
611
  lastDiscovery = undefined;
606
612
  lastBlockedIssueKey = undefined;
@@ -612,18 +618,22 @@ export default function pullRequestExtension(
612
618
  activeInvocations.clear();
613
619
  stopRoutingSpinner();
614
620
  widgetKind = "presentation";
615
- if (timer !== undefined) clearInterval(timer);
616
- timer = undefined;
617
621
  active?.abort();
618
622
  active = undefined;
619
623
  };
620
624
 
621
- const reportRefreshFailure = (): void => {
625
+ const reportRefreshFailure = (error: unknown): void => {
622
626
  const ctx = context;
623
- if (!ctx || refreshFailureReported) return;
624
- refreshFailureReported = true;
627
+ const category = error instanceof GitHubRateLimitError ? "quota" : "generic";
628
+ if (!ctx || reportedRefreshFailure === category || reportedRefreshFailure === "quota") return;
629
+ reportedRefreshFailure = category;
625
630
  try {
626
- ctx.ui.notify("PR status refresh failed: status unavailable", "error");
631
+ ctx.ui.notify(
632
+ error instanceof GitHubRateLimitError
633
+ ? error.message
634
+ : "PR status refresh failed: status unavailable",
635
+ "error",
636
+ );
627
637
  } catch {
628
638
  console.error("PR status refresh failed and could not be reported");
629
639
  }
@@ -655,7 +665,7 @@ export default function pullRequestExtension(
655
665
  try {
656
666
  discovery = await load(pi, loadContext);
657
667
  if (controller.signal.aborted || sessionGeneration !== generation) return;
658
- } catch {
668
+ } catch (error) {
659
669
  // Keep an established footer. A refresh failure must not leave a stale action hint.
660
670
  if (!controller.signal.aborted && sessionGeneration === generation) {
661
671
  displayedWidget = undefined;
@@ -665,13 +675,13 @@ export default function pullRequestExtension(
665
675
  ctx.ui.setStatus(UI_KEY, formatPrFooter(unavailable, ctx.ui.theme));
666
676
  displayEstablished = true;
667
677
  }
668
- reportRefreshFailure();
678
+ reportRefreshFailure(error);
669
679
  }
670
680
  return;
671
681
  }
672
682
  if (controller.signal.aborted || sessionGeneration !== generation) return;
673
683
  render(ctx, discovery);
674
- refreshFailureReported = false;
684
+ reportedRefreshFailure = undefined;
675
685
 
676
686
  const pullRequest = discovery.kind === "current" ? discovery.pullRequest : undefined;
677
687
  if (pendingWorkspaceRename && pullRequest?.target.provenance === "configured") {
@@ -716,14 +726,10 @@ export default function pullRequestExtension(
716
726
 
717
727
  pi.on("session_start", async (_event, ctx) => {
718
728
  stop();
719
- const generation = sessionGeneration;
720
729
  observation = latestObservation(ctx);
721
730
  if (!ctx.hasUI) return;
722
731
  context = ctx;
723
732
  await refresh();
724
- if (sessionGeneration === generation && lastDiscovery !== "inactive") {
725
- timer = setInterval(refreshInBackground, POLL_INTERVAL_MS);
726
- }
727
733
  });
728
734
 
729
735
  pi.on("session_shutdown", stop);
@@ -805,8 +811,14 @@ export default function pullRequestExtension(
805
811
  if (sessionGeneration === generation) {
806
812
  cancelRefresh();
807
813
  activeInvocations.delete(invocation);
808
- reconcileWidget(ctx);
809
- refreshInBackground();
814
+ if (error instanceof GitHubRateLimitError) {
815
+ displayedWidget = undefined;
816
+ reconcileWidget(ctx);
817
+ reportRefreshFailure(error);
818
+ } else {
819
+ reconcileWidget(ctx);
820
+ refreshInBackground();
821
+ }
810
822
  }
811
823
  throw error;
812
824
  }
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.7",
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",
@@ -9,9 +9,10 @@ Use the package-owned comment-sweep workflow. It exposes these closed actions:
9
9
  `start`, `resume`, `show`, `record`, `publish`, `refresh`, `resolve`, and
10
10
  `finalize`.
11
11
 
12
- 1. Call `start` for a fresh current-branch pull request. Call `resume` only for
13
- saved work. Never replace or delete blocked recovery state by hand. See
14
- [Sweep recovery](references/recovery.md).
12
+ 1. Use the `start` or `resume` action supplied by `/pr`. `/pr` checks saved work
13
+ and chooses the action. Do not change it. Direct skill or tool calls cannot
14
+ create route authority; run `/pr` instead. Never replace or delete blocked
15
+ recovery state by hand. See [Sweep recovery](references/recovery.md).
15
16
  2. Use `show` for one feedback ID at a time. Inspect every conversation
16
17
  comment, review, thread, and thread comment. Follow
17
18
  [Thread triage](references/thread-triage.md).
@@ -10,10 +10,14 @@ The file is private, bounded to 1 MiB, and replaced atomically. It contains the
10
10
  frozen PR authority, original head and lease, complete feedback, exact ledger,
11
11
  owned paths, and mutation attempts.
12
12
 
13
- Use `resume` when this file exists. Resume checks the canonical worktree, local
14
- changes, PR linkage, and remote head. It reconciles an attempted push or thread
15
- resolution before issuing a new epoch and run ID. Calls from the old run then
16
- fail.
13
+ Run `/pr` to enter recovery. After fresh route discovery, `/pr` checks this file
14
+ without changing it. It selects `start` when the file is absent. It selects
15
+ `resume` only when valid recovery matches the fresh route authority.
16
+
17
+ Resume checks the canonical worktree, local changes, PR linkage, and remote
18
+ head again under its lock. It reconciles an attempted push or thread resolution
19
+ before issuing a new epoch and run ID. Calls from the old run then fail. Direct
20
+ skill or tool calls cannot create route authority.
17
21
 
18
22
  A completed post-publish `refresh` stores the new complete snapshot before any
19
23
  replacement ledger. Recovery keeps that snapshot in `refresh-pending`, with its
@@ -22,7 +26,9 @@ Use `show` with the resumed guard to inspect each frozen item. Then use `record`
22
26
  without `ownedPaths` to supply exact complete coverage for that snapshot.
23
27
  Resolution and finalization remain blocked until this record succeeds.
24
28
 
25
- Malformed or oversized recovery is preserved and blocks the workflow. Never
26
- repair, move, replace, or delete it automatically. An unknown mutation is never
27
- replayed. If reconciliation cannot prove its exact result, stop and report the
28
- state path and blocker.
29
+ Malformed, oversized, obsolete, wrong-worktree, or route-mismatched recovery is
30
+ preserved and blocks dispatch. Never repair, move, replace, or delete it
31
+ automatically. Report the state path and exact blocker.
32
+
33
+ An unknown mutation is never replayed. If reconciliation cannot prove its exact
34
+ result, stop and report the state path and blocker.