pr-shepherd 0.49.0 → 0.51.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 (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +13 -6
  3. package/bin/cli/help-command-pages.d.mts +8 -8
  4. package/bin/cli/help-command-pages.mjs +8 -8
  5. package/bin/cli/help.d.mts +8 -8
  6. package/bin/cli/iterate-instructions.mjs +9 -2
  7. package/bin/cli/iterate-lean.mjs +11 -0
  8. package/bin/cli/poll-summary-emitter.mjs +15 -0
  9. package/bin/cli/poll-summary-formatter.mjs +10 -24
  10. package/bin/cli-parser.mjs +3 -0
  11. package/bin/commands/iterate/check-instructions.d.mts +2 -8
  12. package/bin/commands/iterate/check-instructions.mjs +2 -11
  13. package/bin/commands/iterate/escalate.mjs +30 -2
  14. package/bin/commands/iterate/fix-code.mjs +62 -20
  15. package/bin/commands/iterate/render.mjs +1 -1
  16. package/bin/commands/iterate/stall.mjs +30 -0
  17. package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
  18. package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
  19. package/bin/commands/poll-quota.d.mts +2 -1
  20. package/bin/commands/poll-quota.mjs +12 -0
  21. package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
  22. package/bin/commands/poll-summary-explicit-instructions.mjs +22 -0
  23. package/bin/commands/poll-summary-instructions.d.mts +3 -0
  24. package/bin/commands/poll-summary-instructions.mjs +139 -0
  25. package/bin/commands/poll-summary-signature.d.mts +2 -0
  26. package/bin/commands/poll-summary-signature.mjs +16 -0
  27. package/bin/commands/poll-summary.mjs +25 -39
  28. package/bin/commands/resolve-mutate.mjs +22 -68
  29. package/bin/comments/resolve.d.mts +5 -0
  30. package/bin/comments/resolve.mjs +2 -11
  31. package/bin/github/gql/poll-summary-fragment.gql +1 -0
  32. package/bin/github/poll-summary-projector.mjs +2 -1
  33. package/bin/github/poll-summary-raw.d.mts +1 -0
  34. package/bin/github/poll-summary-route.mjs +4 -1
  35. package/bin/github/poll-summary.d.mts +2 -1
  36. package/bin/github/poll-summary.mjs +19 -0
  37. package/bin/state/fix-attempts.d.mts +3 -4
  38. package/bin/state/fix-attempts.mjs +2 -3
  39. package/bin/types/escalate.d.mts +10 -0
  40. package/bin/types/poll-summary.d.mts +14 -0
  41. package/package.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/skills/pr-shepherd/SKILL.md +6 -11
  46. package/plugins/pr-shepherd/skills/reduce-pr-noise/SKILL.md +17 -0
  47. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +24 -0
  48. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/settings.md +27 -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": "0.49.0",
4
+ "version": "0.51.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -72,7 +72,7 @@ Conversations Resolved: No [Not Required]
72
72
  2. Apply every warranted review fix in each file referenced above.
73
73
  3. Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.
74
74
  4. If you changed code, commit any remaining changes and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate immediately with the same options. If you did not change code, do not commit and continue.
75
- 5. Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an earlier Shepherd reply: a marked thread that is still being resolved is emitted resolve-only when authorized.
75
+ 5. Substitute any command placeholders and run the generated review mutations.
76
76
  6. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.
77
77
  7. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
78
78
  8. Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.
@@ -92,7 +92,7 @@ This system is opinionated and works best with PRs that use required status chec
92
92
  - Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
93
93
  - With `--merge`, actionable review threads/comments/reviews/summaries are held back (`WAIT`, with raw deferred-work counts) while a PR sits in the merge queue, since a Shepherd-initiated push would eject it; set `actions.workWhileQueued: true` to act on them immediately instead. Failing checks and merge conflicts are never deferred.
94
94
  - The CLI never performs git mutations itself — it only emits commit/push instructions for the agent to run. Push access to the PR head is a usage precondition; GitHub viewer fields do not create a separate push-authorization handoff.
95
- - Generated iterate mutations and automatic actions are capability-aware and omit unauthorized commands. Explicit `apply` operations honor the caller's intent and surface GitHub's result; semantic human-content protections still apply.
95
+ - Generated iterate mutations and automatic actions are capability-aware and omit unauthorized commands. Explicit `apply` operations forward the caller's requested IDs without iterate's author or capability policy and surface GitHub's result.
96
96
  - `build_suggestion_patches` turns one or more ordered GitHub suggestion threads into checked patches and commit metadata, but never edits the working tree or git history. Local HEAD may be ahead when the live PR head is its ancestor.
97
97
 
98
98
  ## Usage
@@ -144,10 +144,15 @@ needs agent work, all rows are terminal, the bounded timeout expires, or `--unti
144
144
  a configured GraphQL quota-warning band. Check counts use the same ignored, protected-run,
145
145
  superseded-run, and event rules as singular iteration and include active merge-queue commit checks.
146
146
  Bounded review/check overflow remains visible without permanently forcing work, and clean rows use
147
- the configured ready-delay before becoming terminal. Each actionable row includes
148
- an exact single-PR `pollCommand`; run independent actionable rows, then invoke the aggregate selector again.
149
- Stack rows are ordered bottom-to-top. API and MCP aggregate calls perform one summary tick and leave
150
- recurrence to the caller.
147
+ the configured ready-delay before becoming terminal. Explicit PR sets give each actionable row an
148
+ exact single-PR `pollCommand`, so independent rows can proceed before the next aggregate poll.
149
+ Stack rows are ordered bottom-to-top and follow the one ordered stack instruction block instead.
150
+ The summary also checks that every open child was based on
151
+ its direct parent's current head. A stale child/parent OID pair is actionable even when GitHub
152
+ reports both PRs clean: without `--merge`, rebase the upstack branches from their parent and push
153
+ them with the emitted `gh stack` commands; with `--merge`, finish the contiguous ready lower
154
+ layers with the emitted `gh stack merge --squash` command, recheck, then repair the child. API and
155
+ MCP aggregate calls perform one summary tick and leave recurrence to the caller.
151
156
 
152
157
  Polling defaults can be set under `poll` in `.pr-shepherdrc.yml`: `intervalSeconds`, `timeoutSeconds`, `debounceSeconds`, and `quietStatus`. Explicit flags override configuration, including `--no-quiet-status` when a shared config enables quiet output. Quiet status remains off by default.
153
158
 
@@ -286,6 +291,8 @@ Environment variables:
286
291
 
287
292
  See [docs/configuration.md](docs/configuration.md) for the full reference.
288
293
 
294
+ For guided noise reduction, use the plugin's `reduce-pr-noise` skill. It loads focused guidance for bot-comment classifiers or settings only when relevant; see [docs/skills.md](docs/skills.md).
295
+
289
296
  ### Classification rules
290
297
 
291
298
  Drop `.ts` / `.mts` / `.mjs` / `.js` files under `.pr-shepherd/classification/` to suppress and/or auto-resolve specific bot comments — useful for silencing repetitive noise like rate-limit notices from `gemini-code-assist` or "Reviews paused" from `coderabbitai`.
@@ -14,7 +14,8 @@ Run 'pr-shepherd apply <review|files|journal> --help' for command-specific detai
14
14
  --help, -h Print this help and exit before GitHub I/O.`;
15
15
  readonly "apply review": `pr-shepherd apply review
16
16
 
17
- Apply GitHub review-state mutations after fixes.
17
+ Apply user-directed GitHub review-state mutations after fixes. Supplied IDs are forwarded to GitHub;
18
+ GitHub accepts or rejects each mutation.
18
19
 
19
20
  Usage:
20
21
  pr-shepherd apply review [PR] --reply-thread-ids A,B --message MSG
@@ -24,10 +25,9 @@ Usage:
24
25
 
25
26
  Flags:
26
27
  --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
27
- Human-authored thread IDs are skipped; use --reply-thread-ids.
28
- --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
28
+ --reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
29
29
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
30
- --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
30
+ --dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
31
31
  --message <text> Reply/dismiss message. Required with reply or dismiss IDs.
32
32
  --require-sha <sha> Wait for this full 40-character lowercase PR head SHA.
33
33
  --format text|json Output format. Default: text.
@@ -121,7 +121,8 @@ Use --dry-run to preview paths without removing them.
121
121
  readonly "admin log-file": string;
122
122
  readonly resolve: `pr-shepherd resolve
123
123
 
124
- Apply GitHub review-state mutations after fixes.
124
+ Deprecated compatibility alias for user-directed GitHub review-state mutations. Supplied IDs are forwarded to GitHub;
125
+ GitHub accepts or rejects each mutation.
125
126
 
126
127
  Usage:
127
128
  pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
@@ -131,11 +132,10 @@ Usage:
131
132
 
132
133
  Flags:
133
134
  --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
134
- Human-authored thread IDs are skipped; use --reply-thread-ids.
135
135
  Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
136
- --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
136
+ --reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
137
137
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
138
- --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
138
+ --dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
139
139
  --message <text> Reply/dismiss message. Required with --reply-thread-ids
140
140
  or --dismiss-review-ids.
141
141
  --require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
@@ -17,7 +17,8 @@ Run 'pr-shepherd apply <review|files|journal> --help' for command-specific detai
17
17
  --help, -h Print this help and exit before GitHub I/O.`,
18
18
  "apply review": `pr-shepherd apply review
19
19
 
20
- Apply GitHub review-state mutations after fixes.
20
+ Apply user-directed GitHub review-state mutations after fixes. Supplied IDs are forwarded to GitHub;
21
+ GitHub accepts or rejects each mutation.
21
22
 
22
23
  Usage:
23
24
  pr-shepherd apply review [PR] --reply-thread-ids A,B --message MSG
@@ -27,10 +28,9 @@ Usage:
27
28
 
28
29
  Flags:
29
30
  --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
30
- Human-authored thread IDs are skipped; use --reply-thread-ids.
31
- --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
31
+ --reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
32
32
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
33
- --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
33
+ --dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
34
34
  --message <text> Reply/dismiss message. Required with reply or dismiss IDs.
35
35
  --require-sha <sha> Wait for this full 40-character lowercase PR head SHA.
36
36
  --format text|json Output format. Default: text.
@@ -124,7 +124,8 @@ Use --dry-run to preview paths without removing them.
124
124
  "admin log-file": LOG_FILE_USAGE.replaceAll("pr-shepherd log-file", "pr-shepherd admin log-file"),
125
125
  resolve: `pr-shepherd resolve
126
126
 
127
- Apply GitHub review-state mutations after fixes.
127
+ Deprecated compatibility alias for user-directed GitHub review-state mutations. Supplied IDs are forwarded to GitHub;
128
+ GitHub accepts or rejects each mutation.
128
129
 
129
130
  Usage:
130
131
  pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
@@ -134,11 +135,10 @@ Usage:
134
135
 
135
136
  Flags:
136
137
  --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
137
- Human-authored thread IDs are skipped; use --reply-thread-ids.
138
138
  Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
139
- --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
139
+ --reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
140
140
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
141
- --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
141
+ --dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
142
142
  --message <text> Reply/dismiss message. Required with --reply-thread-ids
143
143
  or --dismiss-review-ids.
144
144
  --require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
@@ -14,7 +14,8 @@ Run 'pr-shepherd apply <review|files|journal> --help' for command-specific detai
14
14
  --help, -h Print this help and exit before GitHub I/O.`;
15
15
  readonly "apply review": `pr-shepherd apply review
16
16
 
17
- Apply GitHub review-state mutations after fixes.
17
+ Apply user-directed GitHub review-state mutations after fixes. Supplied IDs are forwarded to GitHub;
18
+ GitHub accepts or rejects each mutation.
18
19
 
19
20
  Usage:
20
21
  pr-shepherd apply review [PR] --reply-thread-ids A,B --message MSG
@@ -24,10 +25,9 @@ Usage:
24
25
 
25
26
  Flags:
26
27
  --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
27
- Human-authored thread IDs are skipped; use --reply-thread-ids.
28
- --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
28
+ --reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
29
29
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
30
- --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
30
+ --dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
31
31
  --message <text> Reply/dismiss message. Required with reply or dismiss IDs.
32
32
  --require-sha <sha> Wait for this full 40-character lowercase PR head SHA.
33
33
  --format text|json Output format. Default: text.
@@ -121,7 +121,8 @@ Use --dry-run to preview paths without removing them.
121
121
  readonly "admin log-file": string;
122
122
  readonly resolve: `pr-shepherd resolve
123
123
 
124
- Apply GitHub review-state mutations after fixes.
124
+ Deprecated compatibility alias for user-directed GitHub review-state mutations. Supplied IDs are forwarded to GitHub;
125
+ GitHub accepts or rejects each mutation.
125
126
 
126
127
  Usage:
127
128
  pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
@@ -131,11 +132,10 @@ Usage:
131
132
 
132
133
  Flags:
133
134
  --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
134
- Human-authored thread IDs are skipped; use --reply-thread-ids.
135
135
  Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
136
- --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
136
+ --reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
137
137
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
138
- --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
138
+ --dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
139
139
  --message <text> Reply/dismiss message. Required with --reply-thread-ids
140
140
  or --dismiss-review-ids.
141
141
  --require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
@@ -38,8 +38,15 @@ export function buildSimpleIterateInstructions(result) {
38
38
  }
39
39
  case "cancel":
40
40
  return ["Stop — the PR loop is complete. No further polling is needed."];
41
- case "escalate":
42
- return ["Stop human direction is required before automated polling can resume."];
41
+ case "escalate": {
42
+ const pending = result.escalate.pendingReviewCommands;
43
+ if (!pending)
44
+ return ["Stop — human direction is required before automated polling can resume."];
45
+ return [
46
+ "Stop polling. Ask the user whether to run the pending review commands shown above.",
47
+ "If yes, replace any `$HEAD_SHA` with the full 40-character pushed PR-head SHA and any `$DISMISS_MESSAGE` with a one-sentence disposition, run every pending command, then rerun Shepherd with the same options.",
48
+ ];
49
+ }
43
50
  }
44
51
  }
45
52
  export function adaptIterateLog(log) {
@@ -160,6 +160,14 @@ export function projectIterateLean(result, opts) {
160
160
  ...(result.escalate.changesRequestedReviews.length > 0 && {
161
161
  changesRequestedReviews: result.escalate.changesRequestedReviews,
162
162
  }),
163
+ ...(result.escalate.firstLookSummaries &&
164
+ result.escalate.firstLookSummaries.length > 0 && {
165
+ firstLookSummaries: result.escalate.firstLookSummaries,
166
+ }),
167
+ ...(result.escalate.editedSummaries &&
168
+ result.escalate.editedSummaries.length > 0 && {
169
+ editedSummaries: result.escalate.editedSummaries,
170
+ }),
163
171
  ...(result.escalate.checks &&
164
172
  result.escalate.checks.length > 0 && {
165
173
  checks: result.escalate.checks,
@@ -182,6 +190,9 @@ export function projectIterateLean(result, opts) {
182
190
  result.escalate.authorization.length > 0 && {
183
191
  authorization: result.escalate.authorization,
184
192
  }),
193
+ ...(result.escalate.pendingReviewCommands && {
194
+ pendingReviewCommands: result.escalate.pendingReviewCommands,
195
+ }),
185
196
  suggestion: result.escalate.suggestion,
186
197
  humanMessage: result.escalate.humanMessage,
187
198
  },
@@ -5,6 +5,21 @@ export function emitPollSummaryResult(result, opts) {
5
5
  process.exitCode = pollSummaryExitCode(result);
6
6
  }
7
7
  function pollSummaryExitCode(result) {
8
+ if (result.nextAction === "cancel" &&
9
+ result.prs.some((item) => item.reasons.includes("closed"))) {
10
+ return EXIT.CLOSED;
11
+ }
12
+ if (result.nextAction) {
13
+ const stackExitCode = {
14
+ escalate: EXIT.ESCALATE,
15
+ fix_code: EXIT.FIX_CODE,
16
+ merge: EXIT.MERGE,
17
+ mark_ready: EXIT.MARK_READY,
18
+ wait: EXIT.WAIT,
19
+ cancel: EXIT.OK,
20
+ };
21
+ return stackExitCode[result.nextAction] ?? EXIT.OK;
22
+ }
8
23
  const actions = new Set(result.prs.map((item) => item.action));
9
24
  if (actions.has("escalate"))
10
25
  return EXIT.ESCALATE;
@@ -1,5 +1,5 @@
1
1
  import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
2
- import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
2
+ import { withPollSummaryInstructions } from "../commands/poll-summary-instructions.mjs";
3
3
  export function formatPollSummaryResult(result) {
4
4
  const selection = result.selection.kind === "stack"
5
5
  ? `stack #${result.selection.stackNumber} anchored at PR #${result.selection.anchor} (${result.selection.stackSize} PRs)`
@@ -7,42 +7,28 @@ export function formatPollSummaryResult(result) {
7
7
  const lines = [
8
8
  `# Poll summary [${result.reason.toUpperCase()}]`,
9
9
  "",
10
- `**repo** \`${result.repo}\` · **selection** ${selection} · **mode** \`${result.mode}\``,
10
+ `**repo** \`${result.repo}\` · **selection** ${selection} · **mode** \`${result.mode}\`${result.nextAction ? ` · **next action** \`${result.nextAction}\`` : ""}`,
11
11
  "",
12
12
  "## Pull requests",
13
13
  "",
14
14
  ...result.prs.map(formatItem),
15
15
  ];
16
+ if (result.stackAncestry?.length) {
17
+ lines.push("", "## Stack ancestry", "");
18
+ for (const pair of result.stackAncestry) {
19
+ lines.push(`- PR #${pair.childPr} base \`${pair.childBaseRefName}\` at \`${pair.childBaseRefOid}\` differs from parent PR #${pair.parentPr} head \`${pair.parentHeadRefName}\` at \`${pair.parentHeadRefOid}\`.`);
20
+ }
21
+ }
16
22
  const apiUsage = result.apiUsage ? formatApiUsage(result.apiUsage) : null;
17
23
  const quotaWarning = formatQuotaWarning(result.quotaWarning);
18
24
  if (quotaWarning)
19
25
  lines.push("", quotaWarning);
20
26
  if (apiUsage)
21
27
  lines.push("", apiUsage);
22
- lines.push("", "## Instructions", "", ...formatInstructions(result));
28
+ const instructions = result.instructions ?? withPollSummaryInstructions(result, false).instructions ?? [];
29
+ lines.push("", "## Instructions", "", ...instructions);
23
30
  return lines.join("\n");
24
31
  }
25
- function formatInstructions(result) {
26
- if (result.reason === "all_terminal")
27
- return ["1. Stop — every selected PR is terminal."];
28
- if (result.quotaWarning && result.reason !== "actionable") {
29
- return [
30
- buildQuotaAwareContinuation(result.quotaWarning, "1. This aggregate selection is non-terminal. Before continuing,"),
31
- ];
32
- }
33
- if (result.reason === "waiting" || result.reason === "timeout") {
34
- return ["1. Run this aggregate selector again when the caller is ready to recheck."];
35
- }
36
- const instructions = [
37
- "1. Choose each non-WAIT, non-CANCEL row that can proceed independently and run or delegate its exact `pollCommand`.",
38
- "2. Follow each selected one-PR poll's `## Instructions` until it returns `CANCEL` or `ESCALATE`.",
39
- "3. Run this aggregate poll again after selected work completes; one row's `ESCALATE` does not stop work on other rows.",
40
- ];
41
- if (result.quotaWarning) {
42
- instructions[2] = buildQuotaAwareContinuation(result.quotaWarning, "3. After selected work completes,");
43
- }
44
- return instructions;
45
- }
46
32
  function formatItem(item) {
47
33
  const flags = [item.isDraft ? "draft" : null, item.isInMergeQueue ? "queued" : null]
48
34
  .filter((value) => value !== null)
@@ -213,4 +213,7 @@ async function handleResolve(args, command = "apply review") {
213
213
  process.stdout.write(globalOpts.format === "json"
214
214
  ? `${JSON.stringify(result, null, 2)}\n`
215
215
  : `${formatMutateResult(result)}\n`);
216
+ if (result.errors.length > 0) {
217
+ process.exitCode = result.rateLimit ? EXIT.TEMPFAIL : EXIT.UNAVAILABLE;
218
+ }
216
219
  }
@@ -26,15 +26,9 @@ export declare function buildRepeatedWorkflowBranchRecoveryInstructions(baseBran
26
26
  *
27
27
  * - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
28
28
  * empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
29
- * Marker-based self-reply routing is already reflected in the generated IDs. The instruction
30
- * below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
31
- * not mistaken for an automated reply merely because the GitHub login matches.
32
- *
33
29
  * Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
34
- * dismiss-ID retention and the first-look/annotation ID-exclusion rules. Those only matter
35
- * if the caller *edits* the printed command (removes an ID, or adds one back); the printed
36
- * command run unmodified is already correct for them. The pointer below is load-bearing:
37
- * without it, nothing in CLI output tells the agent that playbook exists.
30
+ * dismiss-ID retention. The pointer below is load-bearing: without it, nothing in CLI output
31
+ * tells the agent that playbook exists.
38
32
  */
39
33
  export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
40
34
  /** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
@@ -43,23 +43,14 @@ export function buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasE
43
43
  *
44
44
  * - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
45
45
  * empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
46
- * Marker-based self-reply routing is already reflected in the generated IDs. The instruction
47
- * below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
48
- * not mistaken for an automated reply merely because the GitHub login matches.
49
- *
50
46
  * Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
51
- * dismiss-ID retention and the first-look/annotation ID-exclusion rules. Those only matter
52
- * if the caller *edits* the printed command (removes an ID, or adds one back); the printed
53
- * command run unmodified is already correct for them. The pointer below is load-bearing:
54
- * without it, nothing in CLI output tells the agent that playbook exists.
47
+ * dismiss-ID retention. The pointer below is load-bearing: without it, nothing in CLI output
48
+ * tells the agent that playbook exists.
55
49
  */
56
50
  export function buildResolveCommandInstruction(resolveCommand) {
57
51
  if (!resolveCommand.hasMutations)
58
52
  return [];
59
53
  const instructions = [];
60
- if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
61
- instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked thread that is still being resolved is emitted resolve-only, not for another reply.");
62
- }
63
54
  if (resolveCommand.requiresHeadSha) {
64
55
  instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.");
65
56
  }
@@ -1,5 +1,6 @@
1
1
  import { loadConfig } from "../../config/load.mjs";
2
2
  import { inlineCode } from "../../util/markdown.mjs";
3
+ import { renderResolveCommand } from "./render.mjs";
3
4
  function renderEscalateAuthor(item) {
4
5
  return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
5
6
  }
@@ -121,6 +122,8 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
121
122
  const hasItems = escalate.unresolvedThreads.length > 0 ||
122
123
  escalate.changesRequestedReviews.length > 0 ||
123
124
  escalate.ambiguousComments.length > 0 ||
125
+ (escalate.firstLookSummaries?.length ?? 0) > 0 ||
126
+ (escalate.editedSummaries?.length ?? 0) > 0 ||
124
127
  (escalate.checks?.length ?? 0) > 0 ||
125
128
  (escalate.stalledChecks?.length ?? 0) > 0;
126
129
  if (hasItems) {
@@ -157,6 +160,20 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
157
160
  lines.push(` > ${bodyLine}`);
158
161
  lines.push("");
159
162
  }
163
+ for (const [heading, summaries] of [
164
+ ["Review summaries (first look)", escalate.firstLookSummaries ?? []],
165
+ ["Review summaries (edited since first look)", escalate.editedSummaries ?? []],
166
+ ]) {
167
+ if (summaries.length === 0)
168
+ continue;
169
+ lines.push(`### ${heading}`, "");
170
+ for (const summary of summaries) {
171
+ lines.push(`- review \`${summary.id}\` (${renderEscalateAuthor(summary)}):`, "");
172
+ for (const bodyLine of summary.body.split("\n"))
173
+ lines.push(` > ${bodyLine}`);
174
+ lines.push("");
175
+ }
176
+ }
160
177
  for (const c of escalate.ambiguousComments) {
161
178
  lines.push(`- comment \`${c.id}\` (${renderEscalateAuthor(c)}):`);
162
179
  lines.push("");
@@ -194,7 +211,17 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
194
211
  lines.push("## Fix attempts");
195
212
  lines.push("");
196
213
  for (const a of escalate.thrashHistory) {
197
- lines.push(`- thread \`${a.threadId}\` attempted ${a.attempts} times`);
214
+ lines.push(`- thread \`${a.threadId}\` pending commands returned ${a.attempts} times`);
215
+ }
216
+ }
217
+ const pending = escalate.pendingReviewCommands;
218
+ if (pending?.resolveOnlyCommand?.hasMutations || pending?.resolveCommand?.hasMutations) {
219
+ lines.push("", "## Pending review commands", "");
220
+ if (pending.resolveOnlyCommand?.hasMutations) {
221
+ lines.push(`- resolve-only: \`${renderResolveCommand(pending.resolveOnlyCommand)}\``);
222
+ }
223
+ if (pending.resolveCommand?.hasMutations) {
224
+ lines.push(`- apply review: \`${renderResolveCommand(pending.resolveCommand)}\``);
198
225
  }
199
226
  }
200
227
  lines.push("");
@@ -227,7 +254,8 @@ export function buildEscalateSuggestion(triggers, detail) {
227
254
  return `Could not determine the PR's base branch${reason} — automated rebases are paused because branch safety is unclear. Run the rebase manually against the PR's real target branch.`;
228
255
  }
229
256
  if (triggers.includes("fix-thrash")) {
230
- return "Same thread(s) reached the automated attempt limit — treat this as a manual handoff. Apply the fix by hand.";
257
+ const attempts = loadConfig().iterate.fixAttemptsPerThread;
258
+ return `The same thread(s) remain unresolved after their pending review commands were returned for ${attempts} FIX_CODE ticks. Automated iteration is paused for a manual decision.`;
231
259
  }
232
260
  if (triggers.includes("bot-cr-not-dismissed")) {
233
261
  const ids = detail ? ` (review IDs: ${detail})` : "";