pr-shepherd 1.1.2 → 1.2.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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +17 -17
  3. package/bin/classify/standard/coderabbit-notices.mjs +17 -1
  4. package/bin/classify/standard/codex-notices.mjs +5 -3
  5. package/bin/cli/iterate-instructions.mjs +3 -1
  6. package/bin/cli/iterate-lean.mjs +8 -1
  7. package/bin/cli/triage-formatter.mjs +5 -1
  8. package/bin/commands/check-annotations.d.mts +8 -0
  9. package/bin/commands/check-annotations.mjs +10 -0
  10. package/bin/commands/check.mjs +15 -11
  11. package/bin/commands/iterate/check-blocker-gate.d.mts +2 -0
  12. package/bin/commands/iterate/check-blocker-gate.mjs +3 -1
  13. package/bin/commands/iterate/check-instructions.d.mts +7 -1
  14. package/bin/commands/iterate/check-instructions.mjs +10 -3
  15. package/bin/commands/iterate/index.mjs +17 -7
  16. package/bin/commands/iterate/merge-plan.d.mts +2 -0
  17. package/bin/commands/iterate/merge-plan.mjs +5 -0
  18. package/bin/commands/iterate/merge-state.d.mts +10 -1
  19. package/bin/commands/iterate/merge-state.mjs +1 -1
  20. package/bin/commands/iterate/native-stack-rebase.d.mts +13 -2
  21. package/bin/commands/iterate/native-stack-rebase.mjs +23 -3
  22. package/bin/commands/iterate/queued-conflict-deferral.d.mts +15 -0
  23. package/bin/commands/iterate/queued-conflict-deferral.mjs +20 -0
  24. package/bin/commands/iterate/render.d.mts +2 -1
  25. package/bin/commands/iterate/render.mjs +18 -12
  26. package/bin/commands/iterate/rerun-command.d.mts +7 -0
  27. package/bin/commands/iterate/rerun-command.mjs +22 -0
  28. package/bin/commands/iterate/run.mjs +2 -1
  29. package/bin/commands/iterate/triage.d.mts +5 -1
  30. package/bin/commands/iterate/triage.mjs +32 -11
  31. package/bin/commands/poll-summary-explicit-instructions.mjs +3 -0
  32. package/bin/commands/poll-summary-instructions.mjs +1 -1
  33. package/bin/comments/resolve.d.mts +2 -0
  34. package/bin/comments/resolve.mjs +7 -0
  35. package/bin/comments/rest-review-mutations.d.mts +1 -0
  36. package/bin/comments/rest-review-mutations.mjs +3 -1
  37. package/bin/types/iterate.d.mts +2 -0
  38. package/bin/types/report.d.mts +2 -0
  39. package/package.json +2 -2
  40. package/plugins/pr-shepherd/.claude-plugin/icon.png +0 -0
  41. package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
  42. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  43. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  44. package/plugins/pr-shepherd/.mcp.json +1 -1
  45. package/plugins/pr-shepherd/package-lock.json +1278 -0
  46. package/plugins/pr-shepherd/package.json +9 -0
  47. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +5 -4
  48. package/plugins/pr-shepherd/skills/pr-shepherd/references/branch-update.md +1 -1
  49. package/plugins/pr-shepherd/skills/pr-shepherd/references/event-loop.md +1 -1
@@ -1,9 +1,9 @@
1
1
  import { buildPrShepherdCommand, renderShellCommand } from "../../cli/runner.mjs";
2
- import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintInstruction, buildRepeatedWorkflowBranchRecoveryInstructions, buildResolveCommandInstruction, buildFixCompletionInstruction, } from "./check-instructions.mjs";
2
+ import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintInstruction, buildRepeatedWorkflowBranchRecoveryInstructions, buildResolveCommandInstruction, buildFixCompletionInstruction, QUEUED_CONFLICT_CONTINUATION, } from "./check-instructions.mjs";
3
3
  import { isFailingAgentCheck } from "../../checks/conclusions.mjs";
4
4
  import { buildCommitSuggestionInstruction } from "../commit-suggestion-instruction.mjs";
5
5
  import { partitionFixThreads } from "./fix-instruction-threads.mjs";
6
- import { buildBranchPushInstruction, buildConflictInstruction } from "./native-stack-rebase.mjs";
6
+ import { buildBranchPushInstruction, buildConflictInstruction, buildQueuedConflictChoice, } from "./native-stack-rebase.mjs";
7
7
  import { queueEjectionSteps } from "./queue-recovery-instructions.mjs";
8
8
  import { buildPushSteps } from "./triage-loop-instruction.mjs";
9
9
  /**
@@ -19,7 +19,8 @@ const REQUIRE_HEAD_SHA = "--require-sha HEAD";
19
19
  export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behind-base-hint — see buildBehindBaseHintInstruction
20
20
  isBehind = false, _viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, stackRebase, // native stack layers rebase with gh-stack, not branch by branch
21
21
  queueEjection, // current merge-queue removal with failing queue checks
22
- instructionStyle = "inline") {
22
+ instructionStyle = "inline", // see iterate.instructions
23
+ dequeueCommand) {
23
24
  const instructions = [];
24
25
  const { locatedThreads, unlocatedThreads } = partitionFixThreads(threads, resolveCommand, resolveOnlyCommand);
25
26
  const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
@@ -32,18 +33,23 @@ instructionStyle = "inline") {
32
33
  changesRequestedReviews.length > 0 ||
33
34
  actionableComments.length > 0;
34
35
  // Start with interpretation. The agent decides what raw feedback warrants a code change.
36
+ // A queued conflict offers a wait path that publishes nothing, so fixes, branch updates,
37
+ // pushes, and review mutations (which must follow a push) become path (a).
38
+ const pathA = (step) => (dequeueCommand ? `If you chose (a): ${step}` : step);
35
39
  if (hasNonConflictHints)
36
- instructions.push("Fix each warranted item.");
40
+ instructions.push(pathA("Fix each warranted item."));
37
41
  // A conflicting native stack layer follows the printed gh-stack route, so it omits the hint.
38
42
  const branchUpdateHint = buildBehindBaseHintInstruction(baseBranch, behindBaseHint, {
39
43
  isBehind,
40
44
  hasConflicts: hasConflicts && !stackRebase,
41
- });
45
+ }).map(pathA);
42
46
  // The conflict hint belongs with the conflict step; otherwise it precedes the push step.
43
47
  const hintWithConflictStep = hasConflicts && !hasRepeatedWorkflowBranchRecovery;
44
48
  const branchRecovery = hasConflicts || hasRepeatedWorkflowBranchRecovery;
45
49
  if (hintWithConflictStep)
46
- instructions.push(buildConflictInstruction(stackRebase), ...branchUpdateHint);
50
+ instructions.push(buildConflictInstruction(stackRebase, dequeueCommand), ...branchUpdateHint);
51
+ else if (dequeueCommand && hasRepeatedWorkflowBranchRecovery)
52
+ instructions.push(`The branch has merge conflicts (see \`**branch**\` above). ${buildQueuedConflictChoice(dequeueCommand, "Follow the steps marked `If you chose (a):` below.", stackRebase !== undefined)}`);
47
53
  const firstLookTotal = firstLookThreads.length + firstLookComments.length;
48
54
  if (firstLookTotal > 0)
49
55
  instructions.push("Review every item under `## First-look items` before acting.");
@@ -56,12 +62,12 @@ instructionStyle = "inline") {
56
62
  instructions.push("Acknowledge each item under `## Unlocated review threads (logged once — no mutation)`. Shepherd cannot route a code fix or review mutation without a path and line; the unchanged item will be skipped on later ticks.");
57
63
  const hasSuggestions = locatedThreads.some((t) => t.suggestion);
58
64
  if (hasSuggestions)
59
- instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
65
+ instructions.push(pathA(buildCommitSuggestionInstruction(prReference, "## Review threads")));
60
66
  if (resolutionOnlyThreads.length > 0)
61
67
  instructions.push("Review the threads under `## Review threads to resolve` before running the generated mutations.");
62
- instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions, ...queueEjectionSteps(queueEjection, stackRebase, branchRecovery));
68
+ instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions.map(pathA), ...queueEjectionSteps(queueEjection, stackRebase, branchRecovery));
63
69
  if (hasAnnotations)
64
- instructions.push("Inspect every referenced range under `## Check annotations` and apply any warranted change.");
70
+ instructions.push(pathA("Inspect every referenced range under `## Check annotations` and apply any warranted change."));
65
71
  // The first step already covers applying changes-requested bodies; only stale CRs add a step.
66
72
  const staleClause = buildCrStaleClause(changesRequestedReviews);
67
73
  if (staleClause)
@@ -83,11 +89,11 @@ instructionStyle = "inline") {
83
89
  const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
84
90
  const genericPush = !(branchRecovery || queueEjection) && hasNonConflictHints;
85
91
  if (branchRecovery || queueEjection) {
86
- instructions.push(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix, !branchRecovery));
92
+ instructions.push(pathA(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix, !branchRecovery)));
87
93
  }
88
94
  instructions.push(...buildPushSteps(instructionStyle, genericPush));
89
95
  if (resolveOnlyCommand?.hasMutations)
90
- instructions.push(`Run: \`${renderResolveCommand(resolveOnlyCommand)}\``);
91
- instructions.push(...buildResolveCommandInstruction(resolveCommand, renderResolveCommand(resolveCommand)), buildFixCompletionInstruction());
96
+ instructions.push(pathA(`Run: \`${renderResolveCommand(resolveOnlyCommand)}\``));
97
+ instructions.push(...buildResolveCommandInstruction(resolveCommand, renderResolveCommand(resolveCommand)).map(pathA), dequeueCommand ? QUEUED_CONFLICT_CONTINUATION : buildFixCompletionInstruction());
92
98
  return instructions;
93
99
  }
@@ -0,0 +1,7 @@
1
+ import type { IterateResult } from "../../types.mts";
2
+ /**
3
+ * When the caller passed a qualified PR, name it in the plain `[TRIAGE]` recurrence step, so the
4
+ * caller reruns the same target instead of shortening it to a bare number that resolves in the
5
+ * local checkout. A bare target already reruns correctly. The caller owns the mode and flags.
6
+ */
7
+ export declare function withTriageRerunTarget(result: IterateResult, qualified: boolean): IterateResult;
@@ -0,0 +1,22 @@
1
+ import { formatPrUrl } from "../../pr-reference.mjs";
2
+ import { TRIAGE_CONTINUATION } from "./check-instructions.mjs";
3
+ /**
4
+ * When the caller passed a qualified PR, name it in the plain `[TRIAGE]` recurrence step, so the
5
+ * caller reruns the same target instead of shortening it to a bare number that resolves in the
6
+ * local checkout. A bare target already reruns correctly. The caller owns the mode and flags.
7
+ */
8
+ export function withTriageRerunTarget(result, qualified) {
9
+ if (!qualified || result.action !== "triage")
10
+ return result;
11
+ const instructions = result.triage.instructions;
12
+ if (instructions.at(-1) !== TRIAGE_CONTINUATION)
13
+ return result;
14
+ const target = formatPrUrl(result.repo, result.pr);
15
+ return {
16
+ ...result,
17
+ triage: {
18
+ ...result.triage,
19
+ instructions: [...instructions.slice(0, -1), `Rerun Shepherd now on \`${target}\`.`],
20
+ },
21
+ };
22
+ }
@@ -1,8 +1,9 @@
1
1
  import { withApiTelemetryScope } from "../../github/api-telemetry.mjs";
2
2
  import { attachApiUsage } from "./api-usage.mjs";
3
+ import { withTriageRerunTarget } from "./rerun-command.mjs";
3
4
  export function withIterateApiUsage(opts, runCore) {
4
5
  return withApiTelemetryScope(async () => {
5
6
  const result = await runCore();
6
- return attachApiUsage(result, opts.deferQuotaWarning !== true, false, opts.quotaWarningMinimumPollIntervalMinutes);
7
+ return attachApiUsage(withTriageRerunTarget(result, opts.targetRepository !== undefined), opts.deferQuotaWarning !== true, false, opts.quotaWarningMinimumPollIntervalMinutes);
7
8
  });
8
9
  }
@@ -1,4 +1,4 @@
1
- import type { IterateCommandOptions, IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
1
+ import type { IterateCommandOptions, IterateDeferredWork, IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
2
2
  import type { NormalizedBotUsernames } from "../../comments/authors.mts";
3
3
  interface HandleTriageContext {
4
4
  base: IterateResultBase;
@@ -21,9 +21,13 @@ interface HandleTriageContext {
21
21
  botUsernames: NormalizedBotUsernames;
22
22
  /** Failing checks whose external blocker has merged or closed. */
23
23
  releasedCheckNames?: ReadonlySet<string>;
24
+ /** Released check name → the blocker that merged or closed, shown on the check. */
25
+ releasedCheckBlockers?: ReadonlyMap<string, string>;
24
26
  ruleAutoResolveThreadIds?: string[];
25
27
  /** Verified stack-repair guidance, when ancestry is stale. */
26
28
  repairInstructions?: string[];
29
+ /** Review work a queued conflict holds back until the PR leaves the queue. */
30
+ deferredWork?: IterateDeferredWork;
27
31
  }
28
32
  export declare function handleTriage(ctx: HandleTriageContext): Promise<IterateResult>;
29
33
  export {};
@@ -11,12 +11,14 @@ import { hasLogEvidence } from "./check-evidence.mjs";
11
11
  import { currentEjectionCommit, } from "./queue-recovery-instructions.mjs";
12
12
  import { buildReleasedBlockerInstruction, buildReviewSubsetInstruction, } from "./check-instructions.mjs";
13
13
  import { buildNativeStackLayerRebase } from "./native-stack-rebase.mjs";
14
+ import { buildDequeueCommand } from "./merge-plan.mjs";
15
+ import { renderShellCommand } from "../../cli/runner.mjs";
14
16
  import { lookupUpperLayerTrunkConflict } from "./stack-trunk-conflict.mjs";
15
17
  import { lookupMergedBasePullRequests } from "./merged-base-pull-requests.mjs";
16
18
  import { conflictingHeadCiNote, countReportedChecks, insertConflictingHeadCiNote, } from "./conflicting-head-ci.mjs";
17
19
  import { conflictingHeadFirstSeenUnix } from "../../state/conflicting-head-seen.mjs";
18
20
  import { applyStallGuard } from "./stall.mjs";
19
- import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
21
+ import { annotationMarkerBody, checksWithActionableAnnotations, isQueuedConflict, } from "../check-annotations.mjs";
20
22
  import { threadTranscriptBody } from "../../threads/transcript.mjs";
21
23
  import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs";
22
24
  import { canRerunWorkflows } from "../../checks/conclusions.mjs";
@@ -86,7 +88,7 @@ export async function handleTriage(ctx) {
86
88
  return { ...result, escalate: { ...result.escalate, reviewItems } };
87
89
  }
88
90
  async function handleTriageTick(ctx) {
89
- const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, releasedCheckNames = EMPTY_RELEASED, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
91
+ const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, releasedCheckNames = EMPTY_RELEASED, releasedCheckBlockers, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
90
92
  const prReference = formatPrUrl(report.repo, prNumber);
91
93
  const failingChecks = report.checks.failing;
92
94
  const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
@@ -178,8 +180,10 @@ async function handleTriageTick(ctx) {
178
180
  : []));
179
181
  const initialAttemptRunIds = new Set(failingChecks.flatMap((c) => (c.runId !== null && c.runAttempt === 1 ? [c.runId] : [])));
180
182
  const failingAgentChecks = toAgentChecks(failingChecks).map((c) => {
181
- if (releasedCheckNames.has(c.name))
182
- return c;
183
+ if (releasedCheckNames.has(c.name)) {
184
+ const releasedBlocker = releasedCheckBlockers?.get(c.name);
185
+ return releasedBlocker ? { ...c, releasedBlocker } : c;
186
+ }
183
187
  return rerunAuthorized &&
184
188
  // Rerunning queue CI cannot restore a removed entry and overwrites its failure evidence.
185
189
  c.scope !== "merge_group" &&
@@ -201,6 +205,7 @@ async function handleTriageTick(ctx) {
201
205
  ];
202
206
  const { changesRequestedReviews } = report;
203
207
  const hasConflicts = report.mergeStatus.status === "CONFLICTS";
208
+ const queuedConflict = isQueuedConflict(report.mergeQueue?.inQueue, report.mergeStatus.status);
204
209
  const isBehind = report.mergeStatus.status === "BEHIND";
205
210
  const { "behind-base-hint": behindBaseHint, instructions: configuredInstructions } = loadConfig().iterate;
206
211
  const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
@@ -340,14 +345,26 @@ async function handleTriageTick(ctx) {
340
345
  const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0) || queueEjection
341
346
  ? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict, report.stackBottomPr)
342
347
  : undefined;
343
- const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, canGenerateGithubMutation(report.viewerAuthorization?.viewerCanUpdate, "ready"), exhaustedAttempts.length > 0, stackRebase, queueEjection, opts.instructions ?? configuredInstructions);
348
+ const dequeueCommand = queuedConflict
349
+ ? renderShellCommand(buildDequeueCommand(report.nodeId))
350
+ : undefined;
351
+ const instructions = buildFixInstructions(threads, actionableComments, checks.filter((check) => !releasedCheckNames.has(check.name)), changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, canGenerateGithubMutation(report.viewerAuthorization?.viewerCanUpdate, "ready"), exhaustedAttempts.length > 0, stackRebase, queueEjection, opts.instructions ?? configuredInstructions, dequeueCommand);
344
352
  if (recovery && "unavailable" in recovery)
345
353
  instructions.unshift(recovery.unavailable);
346
354
  if (acknowledgmentRecovery && "unavailable" in acknowledgmentRecovery)
347
355
  instructions.unshift(acknowledgmentRecovery.unavailable);
356
+ // A queued conflict offers a wait path, so every later branch-mutating step is path (a), and
357
+ // leading repair steps follow the dequeue-or-wait choice instead of preceding it.
358
+ const pathA = (step) => (dequeueCommand ? `If you chose (a): ${step}` : step);
359
+ const prependBranchSteps = (steps) => {
360
+ if (!dequeueCommand)
361
+ return void instructions.unshift(...steps);
362
+ const choice = instructions.findIndex((step) => step.includes(dequeueCommand));
363
+ instructions.splice(choice + 1, 0, ...steps.map(pathA));
364
+ };
348
365
  if (failingAgentChecks.some((check) => releasedCheckNames.has(check.name))) {
349
366
  const completion = instructions.pop();
350
- instructions.push(buildReleasedBlockerInstruction(prNumber));
367
+ instructions.push(pathA(buildReleasedBlockerInstruction(prNumber)));
351
368
  if (completion !== undefined)
352
369
  instructions.push(completion);
353
370
  }
@@ -357,15 +374,16 @@ async function handleTriageTick(ctx) {
357
374
  if (completion !== undefined)
358
375
  instructions.push(completion);
359
376
  }
360
- if (repairInstructions && repairInstructions.length > 0) {
361
- instructions.unshift(...repairInstructions);
362
- }
377
+ if (repairInstructions && repairInstructions.length > 0)
378
+ prependBranchSteps(repairInstructions);
363
379
  if (mergedBasePullRequests.length > 0) {
364
380
  // `gh pr edit` goes through GraphQL; REST callers retarget with the pulls PATCH endpoint.
365
381
  const retarget = report.transport === "rest"
366
382
  ? `gh api --method PATCH repos/${report.repo}/pulls/${prNumber} -f base=<verified-parent-base>`
367
383
  : `gh pr edit ${prReference} --base <verified-parent-base>`;
368
- instructions.unshift(`Inspect every PR under \`## Merged PRs matching the current base\`. If this PR is the remaining layer intended for a merged parent's base branch, run \`${retarget}\` after replacing \`<verified-parent-base>\` with that parent's shell-quoted base branch, then rerun Shepherd immediately and follow its fresh instructions instead of the remaining steps here. Otherwise keep the current base and follow the remaining conflict-resolution steps.`);
384
+ prependBranchSteps([
385
+ `Inspect every PR under \`## Merged PRs matching the current base\`. If this PR is the remaining layer intended for a merged parent's base branch, run \`${retarget}\` after replacing \`<verified-parent-base>\` with that parent's shell-quoted base branch, then rerun Shepherd immediately and follow its fresh instructions instead of the remaining steps here. Otherwise keep the current base and follow the remaining conflict-resolution steps.`,
386
+ ]);
369
387
  }
370
388
  const checkRunCount = countReportedChecks(report.checks);
371
389
  const suitesEmpty = report.headCheckSuitesEmpty === true;
@@ -404,12 +422,15 @@ async function handleTriageTick(ctx) {
404
422
  firstLookThreads,
405
423
  firstLookComments,
406
424
  ...(report.reviewItems && { reviewItems: report.reviewItems }),
425
+ ...(ctx.deferredWork && { deferredWork: ctx.deferredWork }),
407
426
  },
408
427
  };
409
428
  const result = await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds);
410
429
  if (result.action === "triage" && opts.persistSeen !== false) {
411
430
  await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
412
- await Promise.allSettled(result.triage.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
431
+ // A queued conflict's annotation fixes are path (a); waiting keeps them unseen.
432
+ if (!queuedConflict)
433
+ await Promise.allSettled(result.triage.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
413
434
  }
414
435
  return result;
415
436
  }
@@ -35,6 +35,9 @@ export function explicitInstructions(result, rerunCommand) {
35
35
  "2. Follow each selected one-PR poll's `## Instructions` until it returns `CANCEL` or `ESCALATE`.",
36
36
  `3. Run ${again} again after selected work completes; one row's \`ESCALATE\` does not stop work on other rows. ${KEEP_WATCHING}`,
37
37
  ];
38
+ if (result.prs.some((item) => item.action === "escalate") && !isHandedOffSelection(result)) {
39
+ instructions[0] = `${instructions[0]} Report each \`ESCALATE\` row's reasons to the user now and leave that PR alone; keep working on the other rows.`;
40
+ }
38
41
  if (onlyDraftVerificationSelection(result)) {
39
42
  instructions[2] = `3. If every draft verification probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, stop this selection and report any \`ESCALATE\` rows; leave the drafts unchanged. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow the probes' instructions and rerun ${again} when that work completes.`;
40
43
  }
@@ -140,7 +140,7 @@ function planStack(result, mergeRequested, rerunCommand) {
140
140
  action: "cancel",
141
141
  stackMergeable: false,
142
142
  instructions: [
143
- `1. Stop shepherding this stack. Automatic mark-ready is disabled and ${work.held.map((item) => `PR #${item.pr}`).join(", ")} ${work.held.length === 1 ? "is" : "are"} clean but still in draft.`,
143
+ `1. Stop shepherding this stack. Automatic mark-ready is disabled and ${work.held.map((item) => `PR #${item.pr}`).join(", ")} ${work.held.length === 1 ? "is" : "are"} clean but still in draft. Leave ${work.held.length === 1 ? "it" : "them"} in draft and end the turn; do not mark ${work.held.length === 1 ? "it" : "them"} ready or suggest doing so, because that is the human's decision.`,
144
144
  ],
145
145
  };
146
146
  }
@@ -11,6 +11,8 @@ export interface ResolveResult {
11
11
  rateLimit?: ResolveRateLimitStop;
12
12
  sessionRefusal?: string;
13
13
  instructions?: string[];
14
+ /** IDs GitHub denied (403/404); Shepherd omits them from later commands until edited. */
15
+ deniedIds?: string[];
14
16
  unrepliedThreads?: string[];
15
17
  unresolvedThreads?: string[];
16
18
  unminimizedComments?: string[];
@@ -86,6 +86,11 @@ export async function applyResolveOptions(pr, repo, opts, preflight) {
86
86
  "Restore GitHub access for this session using the proxy instructions above.",
87
87
  "Retry only the pending IDs listed above.",
88
88
  ];
89
+ else if (result.deniedIds?.length)
90
+ result.instructions = [
91
+ `GitHub denied ${result.deniedIds.map((id) => `\`${id}\``).join(", ")}. Shepherd omits ${result.deniedIds.length === 1 ? "it" : "them"} from later commands until edited, so do not retry, escalate, or route around ${result.deniedIds.length === 1 ? "it" : "them"}.`,
92
+ "Rerun Shepherd with the same options and continue from its output.",
93
+ ];
89
94
  return result;
90
95
  }
91
96
  export async function autoResolveThreads(threadIds) {
@@ -192,6 +197,8 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
192
197
  }), { mutation: true });
193
198
  if ("restStopped" in resp)
194
199
  restStopped = resp.restStopped === true;
200
+ if ("deniedIds" in resp && Array.isArray(resp.deniedIds) && resp.deniedIds.length)
201
+ (result.deniedIds ??= []).push(...resp.deniedIds);
195
202
  if ("sessionRefusal" in resp && typeof resp.sessionRefusal === "string")
196
203
  result.sessionRefusal = resp.sessionRefusal;
197
204
  data = resp.data;
@@ -18,5 +18,6 @@ export declare function applyRestReviewChunk(input: {
18
18
  retryAfterSeconds?: number;
19
19
  restStopped?: true;
20
20
  sessionRefusal?: string;
21
+ deniedIds?: string[];
21
22
  }>;
22
23
  export { isAmbiguousMutationError } from "./rest-reply.mts";
@@ -108,8 +108,10 @@ export async function applyRestReviewChunk(input) {
108
108
  const message = error instanceof Error ? error.message : String(error);
109
109
  result.errors.push({ message, path: [op.alias] });
110
110
  const stop = rateLimitFromError(error, message);
111
- if (!stop && error instanceof GitHubRequestError && [403, 404].includes(error.status))
111
+ if (!stop && error instanceof GitHubRequestError && [403, 404].includes(error.status)) {
112
112
  await rememberDenied(input, op.id, op.kind);
113
+ (result.deniedIds ??= []).push(op.id);
114
+ }
113
115
  if (error instanceof GitHubRequestError) {
114
116
  result.rateLimit = error.rateLimit;
115
117
  result.retryAfterSeconds = error.retryAfterSeconds;
@@ -149,6 +149,8 @@ interface TriageDetails {
149
149
  shown: number;
150
150
  total: number;
151
151
  };
152
+ /** Review work a queued conflicting PR holds back until it leaves the queue. Omitted when empty. */
153
+ deferredWork?: import("./merge-queue.mts").IterateDeferredWork;
152
154
  }
153
155
  export interface IterateResultTriage extends IterateResultBase {
154
156
  action: "triage";
@@ -213,6 +213,8 @@ export interface AgentCheck {
213
213
  rerunCommand?: string;
214
214
  /** Workflow-run attempt number, surfaced only after the initial attempt. */
215
215
  runAttempt?: number;
216
+ /** `owner/name#number` of a recorded blocker that has merged or closed. */
217
+ releasedBlocker?: string;
216
218
  annotations?: CheckAnnotation[];
217
219
  annotationOnly?: true;
218
220
  scope?: "merge_group";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -91,7 +91,7 @@
91
91
  "husky": "^9.1.7",
92
92
  "knip": "^6.14.1",
93
93
  "marked": "^18.0.11",
94
- "oxfmt": "^0.73.0",
94
+ "oxfmt": "^0.75.0",
95
95
  "oxlint": "^1.60.0",
96
96
  "typescript": "^7.0.2",
97
97
  "vitest": "^5.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
- "version": "1.1.2",
4
+ "version": "1.2.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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@1.1.2", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@1.2.0", "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@1.1.2", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@1.2.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }