pr-shepherd 0.42.0 → 0.44.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 (145) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +24 -18
  3. package/bin/api.d.mts +3 -2
  4. package/bin/api.mjs +13 -18
  5. package/bin/checks/classify.d.mts +3 -1
  6. package/bin/checks/classify.mjs +5 -2
  7. package/bin/cli/args.mjs +14 -9
  8. package/bin/cli/default-poll.mjs +1 -0
  9. package/bin/cli/fix-formatter.mjs +20 -8
  10. package/bin/cli/handlers.mjs +1 -0
  11. package/bin/cli/help-command-pages.d.mts +9 -6
  12. package/bin/cli/help-command-pages.mjs +7 -4
  13. package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
  14. package/bin/cli/help-iterate-poll-pages.mjs +9 -4
  15. package/bin/cli/help-top-page.d.mts +1 -1
  16. package/bin/cli/help-top-page.mjs +2 -2
  17. package/bin/cli/help.d.mts +10 -7
  18. package/bin/cli/iterate-flags.d.mts +1 -0
  19. package/bin/cli/iterate-flags.mjs +2 -0
  20. package/bin/cli/iterate-formatter.d.mts +1 -1
  21. package/bin/cli/iterate-formatter.mjs +9 -6
  22. package/bin/cli/iterate-instructions.mjs +15 -0
  23. package/bin/cli/iterate-lean.d.mts +1 -2
  24. package/bin/cli/iterate-lean.mjs +16 -17
  25. package/bin/cli/iterate-merge-formatter.d.mts +3 -0
  26. package/bin/cli/iterate-merge-formatter.mjs +52 -0
  27. package/bin/cli/iterate-verbose.d.mts +6 -0
  28. package/bin/cli/iterate-verbose.mjs +14 -0
  29. package/bin/cli/journal-formatter.mjs +3 -0
  30. package/bin/cli/journal-handler.mjs +11 -7
  31. package/bin/cli/list-formatters.d.mts +3 -1
  32. package/bin/cli/list-formatters.mjs +9 -7
  33. package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -1
  34. package/bin/cli/mutate-formatter.mjs +4 -0
  35. package/bin/cli/poll-handler.mjs +1 -0
  36. package/bin/cli/runner.mjs +4 -3
  37. package/bin/commands/check-terminal-report.mjs +1 -0
  38. package/bin/commands/check.mjs +64 -7
  39. package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
  40. package/bin/commands/commit-suggestion-instruction.mjs +2 -2
  41. package/bin/commands/commit-suggestion.mjs +3 -1
  42. package/bin/commands/iterate/base.d.mts +5 -0
  43. package/bin/commands/iterate/base.mjs +25 -0
  44. package/bin/commands/iterate/check-instructions.d.mts +5 -19
  45. package/bin/commands/iterate/check-instructions.mjs +24 -25
  46. package/bin/commands/iterate/classify.d.mts +2 -2
  47. package/bin/commands/iterate/classify.mjs +43 -25
  48. package/bin/commands/iterate/escalate.d.mts +3 -1
  49. package/bin/commands/iterate/escalate.mjs +29 -2
  50. package/bin/commands/iterate/fix-code.mjs +81 -26
  51. package/bin/commands/iterate/helpers.d.mts +0 -2
  52. package/bin/commands/iterate/helpers.mjs +6 -17
  53. package/bin/commands/iterate/index.mjs +31 -43
  54. package/bin/commands/iterate/mark-ready.d.mts +2 -0
  55. package/bin/commands/iterate/mark-ready.mjs +38 -0
  56. package/bin/commands/iterate/merge-state.d.mts +15 -0
  57. package/bin/commands/iterate/merge-state.mjs +82 -0
  58. package/bin/commands/iterate/merge.d.mts +13 -0
  59. package/bin/commands/iterate/merge.mjs +45 -0
  60. package/bin/commands/iterate/render.d.mts +2 -2
  61. package/bin/commands/iterate/render.mjs +19 -20
  62. package/bin/commands/iterate/stall.mjs +4 -2
  63. package/bin/commands/iterate/thread-mutation-routing.d.mts +9 -0
  64. package/bin/commands/iterate/thread-mutation-routing.mjs +29 -0
  65. package/bin/commands/journal/index.d.mts +5 -0
  66. package/bin/commands/journal/index.mjs +11 -2
  67. package/bin/commands/mark-files-as-viewed.d.mts +2 -1
  68. package/bin/commands/mark-files-as-viewed.mjs +5 -92
  69. package/bin/commands/poll.mjs +3 -1
  70. package/bin/commands/resolve-mutate.mjs +51 -9
  71. package/bin/commands/shepherd-journal.d.mts +1 -1
  72. package/bin/commands/shepherd-journal.mjs +2 -2
  73. package/bin/commands/suggestion-patch-item.d.mts +1 -1
  74. package/bin/commands/suggestion-patch-item.mjs +3 -11
  75. package/bin/commands/suggestion-patches.mjs +3 -3
  76. package/bin/comments/authors.d.mts +6 -0
  77. package/bin/comments/authors.mjs +5 -0
  78. package/bin/comments/marker.d.mts +6 -1
  79. package/bin/comments/marker.mjs +8 -1
  80. package/bin/comments/resolve.d.mts +4 -0
  81. package/bin/comments/thread-visibility.mjs +10 -10
  82. package/bin/comments/visible-comments.mjs +2 -1
  83. package/bin/config/load.d.mts +5 -1
  84. package/bin/config/load.mjs +34 -0
  85. package/bin/config/merge-command-args.d.mts +2 -0
  86. package/bin/config/merge-command-args.mjs +44 -0
  87. package/bin/config.json +3 -0
  88. package/bin/exit-codes.d.mts +2 -0
  89. package/bin/exit-codes.mjs +4 -0
  90. package/bin/github/batch-parse-checks.d.mts +3 -0
  91. package/bin/github/batch-parse-checks.mjs +29 -0
  92. package/bin/github/batch-parsers-rules.d.mts +3 -1
  93. package/bin/github/batch-parsers-rules.mjs +33 -0
  94. package/bin/github/batch-parsers.d.mts +2 -2
  95. package/bin/github/batch-parsers.mjs +40 -27
  96. package/bin/github/batch-raw-rules.d.mts +48 -0
  97. package/bin/github/batch-raw-types.d.mts +12 -0
  98. package/bin/github/batch-raw-types.mjs +0 -1
  99. package/bin/github/batch.mjs +3 -1
  100. package/bin/github/client.d.mts +1 -0
  101. package/bin/github/client.mjs +8 -1
  102. package/bin/github/gql/batch-pr-page.gql +6 -0
  103. package/bin/github/gql/batch-pr.gql +105 -0
  104. package/bin/github/gql/commit-check-contexts.gql +58 -0
  105. package/bin/github/gql/get-pr-body.gql +1 -0
  106. package/bin/github/gql/review-thread-comments.gql +1 -0
  107. package/bin/github/gql/suggestion-threads.gql +1 -0
  108. package/bin/github/merge-queue-checks.d.mts +4 -0
  109. package/bin/github/merge-queue-checks.mjs +48 -0
  110. package/bin/github/queries.d.mts +2 -0
  111. package/bin/github/queries.mjs +2 -0
  112. package/bin/github/suggestion-thread.mjs +1 -0
  113. package/bin/mcp/server.mjs +13 -6
  114. package/bin/pr-reference.d.mts +13 -0
  115. package/bin/pr-reference.mjs +23 -1
  116. package/bin/reporters/agent.mjs +11 -3
  117. package/bin/threads/transcript.d.mts +2 -0
  118. package/bin/threads/transcript.mjs +2 -0
  119. package/bin/types/activity.d.mts +2 -0
  120. package/bin/types/agent-thread.d.mts +1 -0
  121. package/bin/types/escalate.d.mts +35 -0
  122. package/bin/types/escalate.mjs +1 -0
  123. package/bin/types/github.d.mts +27 -0
  124. package/bin/types/github.mjs +0 -3
  125. package/bin/types/iterate.d.mts +23 -39
  126. package/bin/types/merge-action.d.mts +12 -0
  127. package/bin/types/merge-action.mjs +1 -0
  128. package/bin/types/merge-queue.d.mts +13 -0
  129. package/bin/types/merge-queue.mjs +1 -0
  130. package/bin/types/merge-requirements.d.mts +17 -0
  131. package/bin/types/report.d.mts +22 -1
  132. package/bin/types/review-thread.d.mts +1 -0
  133. package/bin/types/suggestion-patch.d.mts +1 -1
  134. package/bin/types.d.mts +3 -0
  135. package/bin/types.mjs +3 -0
  136. package/bin/util/markdown.d.mts +2 -0
  137. package/bin/util/markdown.mjs +7 -0
  138. package/package.json +1 -1
  139. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  140. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  141. package/plugins/pr-shepherd/.mcp.json +1 -1
  142. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +3 -3
  143. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +17 -15
  144. package/bin/commands/iterate/reruns.d.mts +0 -20
  145. package/bin/commands/iterate/reruns.mjs +0 -105
@@ -34,6 +34,10 @@ export function formatMutateResult(result) {
34
34
  pushIds(lines, "Skipped human minimizes", result.skippedHumanMinimizes);
35
35
  pushIds(lines, "Skipped human review dismissals", result.skippedHumanDismissals);
36
36
  pushIds(lines, "Skipped non-human/unknown thread replies", result.skippedNonHumanReplies);
37
+ pushIds(lines, "Authorization denied/unverifiable for thread replies", result.skippedUnauthorizedReplies);
38
+ pushIds(lines, "Authorization denied/unverifiable for thread resolves", result.skippedUnauthorizedResolves);
39
+ pushIds(lines, "Authorization denied/unverifiable for minimizes", result.skippedUnauthorizedMinimizes);
40
+ pushIds(lines, "Authorization denied/unverifiable for review dismissals", result.skippedUnauthorizedDismissals);
37
41
  const rateLimit = formatRateLimit(result);
38
42
  if (rateLimit)
39
43
  lines.push(rateLimit);
@@ -36,6 +36,7 @@ export async function handlePoll(args) {
36
36
  stallTimeoutSeconds: flags.stallTimeoutSeconds,
37
37
  noAutoMarkReady: flags.noAutoMarkReady,
38
38
  noAutoCancelActionable: flags.noAutoCancelActionable,
39
+ merge: flags.merge,
39
40
  intervalSeconds,
40
41
  timeoutSeconds,
41
42
  debounceSeconds,
@@ -8,11 +8,12 @@ export function renderShellCommand(argv) {
8
8
  function renderShellArg(arg) {
9
9
  if (/^\$[A-Z_][A-Z0-9_]*$/.test(arg))
10
10
  return `"${arg}"`;
11
+ const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=(\$[A-Z_][A-Z0-9_]*)$/.exec(arg);
12
+ if (assignment)
13
+ return `${assignment[1]}="${assignment[2]}"`;
11
14
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg))
12
15
  return arg;
13
16
  if (!/["$`\\]/.test(arg))
14
17
  return `"${arg}"`;
15
- if (!arg.includes("'"))
16
- return `'${arg}'`;
17
- throw new Error(`Unexpected character in shell arg: ${JSON.stringify(arg)}`);
18
+ return `'${arg.replaceAll("'", `'"'"'`)}'`;
18
19
  }
@@ -2,6 +2,7 @@ export function buildTerminalReport(prNumber, repo, batchData, mergeStatus, stat
2
2
  return {
3
3
  pr: prNumber,
4
4
  nodeId: batchData.nodeId,
5
+ headSha: batchData.headRefOid,
5
6
  repo: `${repo.owner}/${repo.name}`,
6
7
  status,
7
8
  baseBranch: batchData.baseRefName,
@@ -22,7 +22,7 @@ import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
22
22
  import { EXIT, ShepherdError } from "../exit-codes.mjs";
23
23
  import { getEffectiveCwd } from "../execution-context.mjs";
24
24
  export async function runCheck(opts) {
25
- const repo = await getRepoInfo();
25
+ const repo = opts.targetRepository ?? (await getRepoInfo());
26
26
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
27
27
  if (prNumber === null) {
28
28
  throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
@@ -42,7 +42,22 @@ export async function runCheck(opts) {
42
42
  ? []
43
43
  : await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
44
44
  const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
45
- const classifiedChecks = classifyChecks(allChecks);
45
+ const classifiedPrChecks = classifyChecks(allChecks);
46
+ const latestRemoval = batchData.latestMergeQueueRemoval;
47
+ const headUpdatedAfterRemoval = Boolean(latestRemoval &&
48
+ latestRemoval.beforeCommitParentOids &&
49
+ !latestRemoval.beforeCommitParentOids.includes(batchData.headRefOid));
50
+ const queueRawChecks = batchData.isInMergeQueue
51
+ ? (batchData.mergeQueueChecks ?? [])
52
+ : latestRemoval && !headUpdatedAfterRemoval
53
+ ? (batchData.removedMergeQueueChecks ?? [])
54
+ : [];
55
+ // Keep supersession grouping commit-local, but accept merge_group only for the
56
+ // synthetic queue-commit source.
57
+ const classifiedQueueChecks = classifyChecks(queueRawChecks, {
58
+ additionalRelevantEvents: ["merge_group"],
59
+ });
60
+ const classifiedChecks = [...classifiedPrChecks, ...classifiedQueueChecks];
46
61
  const verdict = getCiVerdict(classifiedChecks);
47
62
  const passing = classifiedChecks.filter((c) => c.category === "passed");
48
63
  const failing = classifiedChecks.filter((c) => c.category === "failing");
@@ -60,8 +75,10 @@ export async function runCheck(opts) {
60
75
  const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered, ignored }, seenMap, prNumber);
61
76
  const ignoredAnnotated = merged.ignored.filter((c) => (c.annotations?.length ?? 0) > 0);
62
77
  const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized && !partition.suppressedCommentIds.has(c.id));
63
- const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id)), seenMap, config.iterate.minimizeComments, botUsernames);
64
- const threadVisibility = classifyThreadVisibility(batchData.reviewThreads.filter((t) => !partition.suppressedThreadIds.has(t.id)), seenMap, botUsernames);
78
+ const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize !== true));
79
+ const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate.minimizeComments, botUsernames);
80
+ const deniedRuleAutoResolveThreadIds = new Set(partition.ruleAutoResolveThreadIds.filter((id) => batchData.reviewThreads.find((thread) => thread.id === id)?.viewerCanResolve !== true));
81
+ const threadVisibility = classifyThreadVisibility(batchData.reviewThreads.filter((t) => !partition.suppressedThreadIds.has(t.id) || deniedRuleAutoResolveThreadIds.has(t.id)), seenMap, botUsernames);
65
82
  const firstLookComments = minimizedCommentCandidates.flatMap((c) => {
66
83
  const cls = classifyItem(c.id, c.body, seenMap);
67
84
  if (cls === "unchanged")
@@ -72,7 +89,9 @@ export async function runCheck(opts) {
72
89
  const firstLookSummaries = [];
73
90
  const editedSummaries = [];
74
91
  const seenSummaries = [];
75
- const unseenReviewSummaries = batchData.reviewSummaries.filter((r) => !partition.suppressedReviewSummaryIds.has(r.id));
92
+ const deniedRuleAutoResolveReviewSummaryIds = new Set(partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize !== true));
93
+ const unseenReviewSummaries = batchData.reviewSummaries.filter((r) => !partition.suppressedReviewSummaryIds.has(r.id) ||
94
+ deniedRuleAutoResolveReviewSummaryIds.has(r.id));
76
95
  for (const r of unseenReviewSummaries) {
77
96
  const cls = classifyItem(r.id, r.body, seenMap);
78
97
  if (cls === "new")
@@ -108,7 +127,8 @@ export async function runCheck(opts) {
108
127
  .filter((t) => partition.suppressedThreadIds.has(t.id))
109
128
  .map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
110
129
  ...batchData.reviewSummaries
111
- .filter((r) => partition.suppressedReviewSummaryIds.has(r.id))
130
+ .filter((r) => partition.suppressedReviewSummaryIds.has(r.id) &&
131
+ !deniedRuleAutoResolveReviewSummaryIds.has(r.id))
112
132
  .map((r) => markSeen(stateKey, r.id, r.body)),
113
133
  ...batchData.changesRequestedReviews
114
134
  .filter((r) => partition.suppressedChangesRequestedIds.has(r.id))
@@ -116,7 +136,17 @@ export async function runCheck(opts) {
116
136
  ]);
117
137
  await markReviewInlineThreadMarkers(stateKey, batchData.reviewThreads);
118
138
  }
119
- const { threadIds: ruleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(partition, opts.autoMinimizeSuppressed);
139
+ const authorizedPartition = {
140
+ ...partition,
141
+ ruleAutoResolveThreadIds: partition.ruleAutoResolveThreadIds.filter((id) => batchData.reviewThreads.find((thread) => thread.id === id)?.viewerCanResolve === true),
142
+ ruleAutoResolveCommentIds: partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize === true),
143
+ ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize === true),
144
+ };
145
+ const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(authorizedPartition, opts.autoMinimizeSuppressed);
146
+ const ruleAutoResolveThreadIds = [
147
+ ...authorizedRuleAutoResolveThreadIds,
148
+ ...deniedRuleAutoResolveThreadIds,
149
+ ];
120
150
  const changesRequestedReviews = changesRequestedReviewVisibility.visible;
121
151
  const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)).length;
122
152
  const approvedReviews = approvedReviewVisibility.visible;
@@ -131,10 +161,19 @@ export async function runCheck(opts) {
131
161
  }
132
162
  }
133
163
  const blockedByFilteredCheck = isBlockedByFilteredCheck(mergeStatus, verdict);
164
+ const queueCommit = batchData.isInMergeQueue
165
+ ? batchData.mergeQueueEntry?.headCommitOid
166
+ : batchData.latestMergeQueueRemoval?.beforeCommitOid;
167
+ const hasQueueState = Boolean(batchData.isMergeQueueEnabled ||
168
+ batchData.isInMergeQueue ||
169
+ batchData.autoMergeRequest ||
170
+ batchData.latestMergeQueueRemoval);
134
171
  return {
135
172
  pr: prNumber,
136
173
  nodeId: batchData.nodeId,
174
+ headSha: batchData.headRefOid,
137
175
  repo: `${repo.owner}/${repo.name}`,
176
+ ...(batchData.viewerAuthorization && { viewerAuthorization: batchData.viewerAuthorization }),
138
177
  status,
139
178
  baseBranch: batchData.baseRefName,
140
179
  mergeStatus,
@@ -175,6 +214,24 @@ export async function runCheck(opts) {
175
214
  : undefined),
176
215
  branchProtection: batchData.branchProtection,
177
216
  activity: batchData.activity,
217
+ ...(hasQueueState && {
218
+ mergeQueue: {
219
+ enabled: Boolean(batchData.isMergeQueueEnabled),
220
+ inQueue: Boolean(batchData.isInMergeQueue),
221
+ ...(batchData.autoMergeRequest && { autoMergeRequest: batchData.autoMergeRequest }),
222
+ ...(batchData.mergeQueueEntry && { entry: batchData.mergeQueueEntry }),
223
+ ...(batchData.latestMergeQueueRemoval && {
224
+ latestRemoval: batchData.latestMergeQueueRemoval,
225
+ }),
226
+ ...(queueCommit && { checkCommitOid: queueCommit }),
227
+ ...((batchData.isInMergeQueue
228
+ ? batchData.mergeQueueChecksIncomplete
229
+ : batchData.removedMergeQueueChecksIncomplete) && {
230
+ checksIncomplete: true,
231
+ }),
232
+ ...(headUpdatedAfterRemoval && { headUpdatedAfterRemoval: true }),
233
+ },
234
+ }),
178
235
  };
179
236
  }
180
237
  async function remainingRuleAutoResolveIds(partition, autoMinimizeSuppressed = false) {
@@ -8,4 +8,4 @@
8
8
  * @param sectionName - The markdown section heading where suggestion threads appear,
9
9
  * e.g. `"## Review threads"`.
10
10
  */
11
- export declare function buildCommitSuggestionInstruction(prNumber: number, sectionName: string): string;
11
+ export declare function buildCommitSuggestionInstruction(prReference: string | number, sectionName: string): string;
@@ -9,10 +9,10 @@ import { buildPrShepherdCommand } from "../cli/runner.mjs";
9
9
  * @param sectionName - The markdown section heading where suggestion threads appear,
10
10
  * e.g. `"## Review threads"`.
11
11
  */
12
- export function buildCommitSuggestionInstruction(prNumber, sectionName) {
12
+ export function buildCommitSuggestionInstruction(prReference, sectionName) {
13
13
  const command = buildPrShepherdCommand([
14
14
  "build-suggestion-patches",
15
- String(prNumber),
15
+ String(prReference),
16
16
  "--thread-id",
17
17
  "<id>",
18
18
  "--message",
@@ -1,9 +1,11 @@
1
1
  import { buildSingularInstructions } from "./suggestion-patch-item.mjs";
2
2
  import { runSuggestionPatches } from "./suggestion-patches.mjs";
3
+ import { formatPrUrl } from "../pr-reference.mjs";
3
4
  /** @deprecated Use runSuggestionPatches. */
4
5
  export async function runCommitSuggestion(opts) {
5
6
  const result = await runSuggestionPatches({
6
7
  prNumber: opts.prNumber,
8
+ targetRepository: opts.targetRepository,
7
9
  format: opts.format,
8
10
  verbose: opts.verbose,
9
11
  suggestions: [
@@ -19,6 +21,6 @@ export async function runCommitSuggestion(opts) {
19
21
  ...patch,
20
22
  pr: result.pr,
21
23
  repo: result.repo,
22
- postActionInstructions: buildSingularInstructions(patch, result.pr),
24
+ postActionInstructions: buildSingularInstructions(patch, formatPrUrl(result.repo, result.pr)),
23
25
  };
24
26
  }
@@ -0,0 +1,5 @@
1
+ import type { IterateResultBase, ShepherdReport } from "../../types.mts";
2
+ export declare function buildIterateBase(report: ShepherdReport, readyState: {
3
+ shouldCancel: boolean;
4
+ remainingSeconds: number;
5
+ }): IterateResultBase;
@@ -0,0 +1,25 @@
1
+ import { buildActiveChecks, buildRelevantChecks, buildSummary, buildSuppressedCheckFields, } from "./helpers.mjs";
2
+ export function buildIterateBase(report, readyState) {
3
+ return {
4
+ pr: report.pr,
5
+ repo: report.repo,
6
+ status: report.status,
7
+ state: report.mergeStatus.state,
8
+ mergeStateStatus: report.mergeStatus.mergeStateStatus,
9
+ mergeStatus: report.mergeStatus.status,
10
+ reviewDecision: report.mergeStatus.reviewDecision,
11
+ blockingBotReviewInProgress: report.mergeStatus.blockingBotReviewInProgress,
12
+ isDraft: report.mergeStatus.isDraft,
13
+ shouldCancel: readyState.shouldCancel,
14
+ remainingSeconds: readyState.remainingSeconds,
15
+ summary: buildSummary(report),
16
+ baseBranch: report.baseBranch,
17
+ branchProtection: report.branchProtection,
18
+ mergeRequirements: report.mergeStatus.mergeRequirements,
19
+ checks: buildRelevantChecks(report),
20
+ inProgressChecks: buildActiveChecks(report),
21
+ ...buildSuppressedCheckFields(report),
22
+ activity: report.activity,
23
+ mergeQueue: report.mergeQueue,
24
+ };
25
+ }
@@ -17,11 +17,9 @@ export declare function buildBehindBaseHintInstruction(baseBranch: string, hint:
17
17
  *
18
18
  * - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
19
19
  * empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
20
- * - Self-reply exclusion: a human thread whose latest visible comment is already Shepherd's
21
- * own prior reply still has its ID in `--reply-thread-ids` by default. Running the
22
- * printed command as-is replies to Shepherd's own reply, which can re-surface the thread
23
- * and produce a self-perpetuating reply loop — worse than a rejected mutation, and not
24
- * something a caller can discover by inspecting the command alone.
20
+ * Marker-based self-reply routing is already reflected in the generated IDs. The instruction
21
+ * below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
22
+ * not mistaken for an automated reply merely because the GitHub login matches.
25
23
  *
26
24
  * Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
27
25
  * dismiss-ID retention and the first-look/annotation ID-exclusion rules. Those only matter
@@ -30,18 +28,6 @@ export declare function buildBehindBaseHintInstruction(baseBranch: string, hint:
30
28
  * without it, nothing in CLI output tells the agent that playbook exists.
31
29
  */
32
30
  export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
33
- /**
34
- * Build the CI-triage instruction. The per-conclusion rerun policy (GitHub Actions log
35
- * excerpts, `gh run view`/`gh run rerun` rules for CANCELLED/STARTUP_FAILURE/external
36
- * failures) is invariant text keyed on the `[conclusion: …]` tags already rendered in
37
- * `## Failing checks` — it lives in the pr-shepherd skill's "CI failure triage" playbook
38
- * instead of being re-emitted every tick. This supersedes the "CI budget rules" example in
39
- * CLAUDE.md's "Keep skills and loop prompts minimal" section (see that section's amendment
40
- * note). The `(no runId)` case stays here because it flips `buildFixCompletionInstruction`
41
- * to a human-handoff terminal state — that trigger, unlike the others, is CLI-decided. The
42
- * CLI sentence does not claim every failure has a log excerpt to read (only GitHub Actions
43
- * checks with a runId do — CANCELLED, STARTUP_FAILURE, and external checks may not); that
44
- * per-kind detail is exactly what the skill playbook table disambiguates.
45
- */
31
+ /** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
46
32
  export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
47
- export declare function buildFixCompletionInstruction(checks: AgentCheck[]): string;
33
+ export declare function buildFixCompletionInstruction(checks: AgentCheck[], requiresRemoteUpdateAuthorization?: boolean, hasShaGatedReviewMutations?: boolean): string;
@@ -18,7 +18,7 @@ export function buildBehindBaseHintInstruction(baseBranch, hint, isBehind) {
18
18
  const trimmedHint = typeof hint === "string" ? hint.trim() : "";
19
19
  if (!isBehind || trimmedHint === "")
20
20
  return [];
21
- return [`The branch is behind \`origin/${baseBranch}\`. ${trimmedHint} before pushing.`];
21
+ return [`The branch is behind PR base branch \`${baseBranch}\`. ${trimmedHint} before pushing.`];
22
22
  }
23
23
  /**
24
24
  * Build the `Run the apply review: command` instruction. Steps stay here (not in the skill)
@@ -26,11 +26,9 @@ export function buildBehindBaseHintInstruction(baseBranch, hint, isBehind) {
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
- * - Self-reply exclusion: a human thread whose latest visible comment is already Shepherd's
30
- * own prior reply still has its ID in `--reply-thread-ids` by default. Running the
31
- * printed command as-is replies to Shepherd's own reply, which can re-surface the thread
32
- * and produce a self-perpetuating reply loop — worse than a rejected mutation, and not
33
- * something a caller can discover by inspecting the command alone.
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.
34
32
  *
35
33
  * Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
36
34
  * dismiss-ID retention and the first-look/annotation ID-exclusion rules. Those only matter
@@ -43,10 +41,10 @@ export function buildResolveCommandInstruction(resolveCommand) {
43
41
  return [];
44
42
  const instructions = [];
45
43
  if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
46
- instructions.push("Before `apply review:`, remove any `--reply-thread-ids` entry whose latest visible comment is your own Shepherd reply. Do not reply to yourself.");
44
+ instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked viewer-authored human thread is emitted resolve-only, not for another reply.");
47
45
  }
48
46
  if (resolveCommand.requiresHeadSha) {
49
- instructions.push("Replace `$HEAD_SHA` with the pushed commit SHA, or `$(git rev-parse HEAD)` if you did not push.");
47
+ 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, do not run this command until an authorized push updates the remote PR head; then replace `$HEAD_SHA` with that pushed commit SHA.");
50
48
  }
51
49
  if (resolveCommand.requiresDismissMessage) {
52
50
  instructions.push("Replace `$DISMISS_MESSAGE` with one sentence describing what changed.");
@@ -54,19 +52,7 @@ export function buildResolveCommandInstruction(resolveCommand) {
54
52
  instructions.push('Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.');
55
53
  return instructions;
56
54
  }
57
- /**
58
- * Build the CI-triage instruction. The per-conclusion rerun policy (GitHub Actions log
59
- * excerpts, `gh run view`/`gh run rerun` rules for CANCELLED/STARTUP_FAILURE/external
60
- * failures) is invariant text keyed on the `[conclusion: …]` tags already rendered in
61
- * `## Failing checks` — it lives in the pr-shepherd skill's "CI failure triage" playbook
62
- * instead of being re-emitted every tick. This supersedes the "CI budget rules" example in
63
- * CLAUDE.md's "Keep skills and loop prompts minimal" section (see that section's amendment
64
- * note). The `(no runId)` case stays here because it flips `buildFixCompletionInstruction`
65
- * to a human-handoff terminal state — that trigger, unlike the others, is CLI-decided. The
66
- * CLI sentence does not claim every failure has a log excerpt to read (only GitHub Actions
67
- * checks with a runId do — CANCELLED, STARTUP_FAILURE, and external checks may not); that
68
- * per-kind detail is exactly what the skill playbook table disambiguates.
69
- */
55
+ /** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
70
56
  export function buildFailingCheckInstructions(checks) {
71
57
  if (checks.length === 0)
72
58
  return [];
@@ -74,17 +60,30 @@ export function buildFailingCheckInstructions(checks) {
74
60
  const hasTriageable = checks.some((c) => c.runId || c.detailsUrl);
75
61
  const instructions = [];
76
62
  if (hasTriageable) {
77
- instructions.push('Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for `gh run view` / `gh run rerun` rules.');
63
+ instructions.push('Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.');
78
64
  }
79
65
  if (hasBare) {
80
66
  instructions.push("For each `(no runId)` failure, escalate to a human because no log or URL is available.");
81
67
  }
82
68
  return instructions;
83
69
  }
84
- export function buildFixCompletionInstruction(checks) {
85
- const requiresHumanHandoff = checks.some((check) => !check.runId && !check.detailsUrl);
86
- if (requiresHumanHandoff) {
70
+ export function buildFixCompletionInstruction(checks, requiresRemoteUpdateAuthorization = false, hasShaGatedReviewMutations = false) {
71
+ if (requiresRemoteUpdateAuthorization) {
72
+ return "`[FIX_CODE]` requires a human handoff for an authorized push after conflict resolution. Shepherd cannot verify the Git credential's push authorization. Stop polling after committing, and resume only after the remote PR head changes.";
73
+ }
74
+ const hasUninspectableFailure = checks.some((check) => !check.runId && !check.detailsUrl);
75
+ const hasCiAuthorizationHandoff = checks.some((check) => check.conclusion === "CANCELLED" ||
76
+ check.conclusion === "STARTUP_FAILURE" ||
77
+ (check.runId === null && Boolean(check.detailsUrl)) ||
78
+ (check.runId !== null && !check.logExcerpt?.trim()));
79
+ if (hasUninspectableFailure) {
87
80
  return "`[FIX_CODE]` requires a human handoff for an uninspectable failing check. Stop polling after escalating, and resume only after human direction.";
88
81
  }
82
+ if (hasCiAuthorizationHandoff) {
83
+ return "`[FIX_CODE]` requires a human handoff for a failing check with no authorized follow-up action. Stop polling after escalating, and resume only after human direction.";
84
+ }
85
+ if (hasShaGatedReviewMutations) {
86
+ return "`[FIX_CODE]` is conditional: if you changed code, stop after committing and resume only after an authorized push changes the remote PR head; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
87
+ }
89
88
  return "`[FIX_CODE]` is non-terminal. After completing these steps, iterate again with the same options to continue.";
90
89
  }
@@ -1,4 +1,4 @@
1
- import type { AgentThread, Review, ResolveCommand, AgentCheck, ReviewThread } from "../../types.mts";
1
+ import type { AgentThread, Review, ResolveCommand, AgentCheck, ReviewThread, ViewerAuthorization } from "../../types.mts";
2
2
  import { type NormalizedBotUsernames } from "../../comments/authors.mts";
3
3
  import type { MinimizeCommentsPolicy } from "../../config/load.mts";
4
4
  export declare function classifyReviewSummaries(summaries: {
@@ -12,7 +12,7 @@ export declare function classifyReviewSummaries(summaries: {
12
12
  editedSummaries: Review[];
13
13
  surfacedApprovals: Review[];
14
14
  };
15
- export declare function buildResolveCommand(threads: AgentThread[], resolutionOnlyThreads: ReviewThread[], allCommentIds: string[], reviews: Review[], checks: AgentCheck[], prNumber: number, botUsernames?: NormalizedBotUsernames, ruleAutoResolveThreadIds?: string[]): {
15
+ export declare function buildResolveCommand(threads: AgentThread[], resolutionOnlyThreads: ReviewThread[], allCommentIds: string[], reviews: Review[], checks: AgentCheck[], prReference: string | number, botUsernames?: NormalizedBotUsernames, ruleAutoResolveThreadIds?: string[], viewerAuthorization?: ViewerAuthorization, authorizationThreads?: ReviewThread[]): {
16
16
  resolveCommand: ResolveCommand;
17
17
  resolveOnlyCommand?: ResolveCommand;
18
18
  };
@@ -1,6 +1,7 @@
1
1
  import { buildPrShepherdCommand } from "../../cli/runner.mjs";
2
2
  import { shouldMinimizeAuthor } from "../../comments/minimize-policy.mjs";
3
3
  import { isConfiguredBotAuthor, isHumanAuthor, } from "../../comments/authors.mjs";
4
+ import { buildThreadMutationRouting } from "./thread-mutation-routing.mjs";
4
5
  function dedupeIds(ids) {
5
6
  const seen = new Set();
6
7
  const out = [];
@@ -14,7 +15,8 @@ function dedupeIds(ids) {
14
15
  }
15
16
  export function classifyReviewSummaries(summaries, approvals, minimizeApprovals, minimizeComments = "all", botUsernames = new Set(), unresolvedThreads = [], ruleAutoResolveIds = []) {
16
17
  const blockedReviewIds = new Set(unresolvedThreads.flatMap((t) => (t.reviewId !== undefined ? [t.reviewId] : [])));
17
- const eligible = (r) => shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames) &&
18
+ const eligible = (r) => r.viewerCanMinimize === true &&
19
+ shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames) &&
18
20
  !blockedReviewIds.has(r.id);
19
21
  // First-look summaries still need one tick to surface their body to the agent,
20
22
  // so their minimize IDs ride in the agent-facing apply command. Seen summaries
@@ -34,7 +36,8 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
34
36
  if (minimizeApprovals) {
35
37
  const surfacedApprovals = [];
36
38
  for (const r of approvals) {
37
- if (shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames))
39
+ if (r.viewerCanMinimize === true &&
40
+ shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames))
38
41
  minimizeIds.push(r.id);
39
42
  else
40
43
  surfacedApprovals.push(r);
@@ -55,35 +58,45 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
55
58
  surfacedApprovals: approvals,
56
59
  };
57
60
  }
58
- export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber, botUsernames = new Set(), ruleAutoResolveThreadIds = []) {
61
+ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prReference, botUsernames = new Set(), ruleAutoResolveThreadIds = [], viewerAuthorization, authorizationThreads = []) {
59
62
  const allThreads = [...threads, ...resolutionOnlyThreads];
60
- const replyThreadIds = dedupeIds(allThreads
61
- .filter((t) => isHumanAuthor(t) && !isConfiguredBotAuthor(t, botUsernames))
62
- .map((t) => t.id));
63
- // Rule-matched threads bypass the author check (human-author guard still applies in resolve-mutate).
64
- const resolveThreadIds = dedupeIds([
65
- ...allThreads
66
- .filter((t) => !isHumanAuthor(t) || isConfiguredBotAuthor(t, botUsernames))
67
- .map((t) => t.id),
68
- ...ruleAutoResolveThreadIds,
69
- ]);
63
+ const routed = buildThreadMutationRouting(allThreads, botUsernames, ruleAutoResolveThreadIds);
64
+ const canReply = new Set(authorizationThreads
65
+ .filter((thread) => thread.viewerCanReply === true)
66
+ .map((thread) => thread.id));
67
+ const canResolve = new Set(authorizationThreads
68
+ .filter((thread) => thread.viewerCanResolve === true)
69
+ .map((thread) => thread.id));
70
+ const replyThreadIds = routed.replyThreadIds.filter((id) => canReply.has(id));
71
+ // Viewer-authored human resolves stay paired with an authorized reply. Marker-ended
72
+ // viewer-authored retries and bot/non-human resolves need only resolve authorization.
73
+ const pairedResolveThreadIds = routed.pairedResolveThreadIds.filter((id) => canReply.has(id) && canResolve.has(id));
74
+ const standaloneResolveThreadIds = routed.standaloneResolveThreadIds.filter((id) => canResolve.has(id));
75
+ const resolveThreadIds = dedupeIds([...pairedResolveThreadIds, ...standaloneResolveThreadIds]);
70
76
  // Bot/non-human CHANGES_REQUESTED reviews are auto-dismissed after the agent pushes a fix.
71
77
  // Human reviews are left for the reviewer to re-review or dismiss themselves.
72
- const dismissReviewIds = dedupeIds(reviews
73
- .filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames))
74
- .map((r) => r.id));
78
+ const dismissReviewIds = dedupeIds(viewerAuthorization?.viewerCanAdminister === true
79
+ ? reviews
80
+ .filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames))
81
+ .map((r) => r.id)
82
+ : []);
75
83
  const hasReply = replyThreadIds.length > 0;
76
84
  const hasDismiss = dismissReviewIds.length > 0;
77
85
  // Mutations that require --message: replies (to human threads) and dismissals (of bot CR reviews).
78
86
  const hasMessageMutations = hasReply || hasDismiss;
79
87
  const hasResolveOrMinimize = resolveThreadIds.length > 0 || allCommentIds.length > 0;
80
- if (hasMessageMutations && hasResolveOrMinimize) {
81
- // Split: message-bearing mutations (replies + dismissals) ride in resolveArgv;
82
- // resolve/minimize mutations go in resolveOnlyArgv so they can run without SHA or message.
83
- const resolveArgv = buildPrShepherdCommand(["apply", "review", String(prNumber)]).argv;
88
+ const hasStandaloneResolveOrMinimize = standaloneResolveThreadIds.length > 0 || allCommentIds.length > 0;
89
+ if (hasMessageMutations && hasStandaloneResolveOrMinimize) {
90
+ // Split: message-bearing mutations plus their paired viewer-authored resolves ride in
91
+ // resolveArgv; standalone resolve/minimize mutations go in resolveOnlyArgv so they can
92
+ // run without SHA or message.
93
+ const resolveArgv = buildPrShepherdCommand(["apply", "review", String(prReference)]).argv;
84
94
  if (replyThreadIds.length > 0) {
85
95
  resolveArgv.push("--reply-thread-ids", replyThreadIds.join(","));
86
96
  }
97
+ if (pairedResolveThreadIds.length > 0) {
98
+ resolveArgv.push("--resolve-thread-ids", pairedResolveThreadIds.join(","));
99
+ }
87
100
  resolveArgv.push("--message", "$DISMISS_MESSAGE");
88
101
  if (hasDismiss) {
89
102
  resolveArgv.push("--dismiss-review-ids", dismissReviewIds.join(","));
@@ -96,12 +109,15 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
96
109
  requiresHeadSha,
97
110
  requiresDismissMessage: true,
98
111
  ...(replyThreadIds.length > 0 ? { replyThreadIds } : undefined),
112
+ ...(pairedResolveThreadIds.length > 0
113
+ ? { resolveThreadIds: pairedResolveThreadIds }
114
+ : undefined),
99
115
  ...(hasDismiss ? { dismissReviewIds } : undefined),
100
116
  hasMutations: true,
101
117
  };
102
- const resolveOnlyArgv = buildPrShepherdCommand(["apply", "review", String(prNumber)]).argv;
103
- if (resolveThreadIds.length > 0) {
104
- resolveOnlyArgv.push("--resolve-thread-ids", resolveThreadIds.join(","));
118
+ const resolveOnlyArgv = buildPrShepherdCommand(["apply", "review", String(prReference)]).argv;
119
+ if (standaloneResolveThreadIds.length > 0) {
120
+ resolveOnlyArgv.push("--resolve-thread-ids", standaloneResolveThreadIds.join(","));
105
121
  }
106
122
  if (allCommentIds.length > 0) {
107
123
  resolveOnlyArgv.push("--minimize-comment-ids", allCommentIds.join(","));
@@ -110,13 +126,15 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
110
126
  argv: resolveOnlyArgv,
111
127
  requiresHeadSha: false,
112
128
  requiresDismissMessage: false,
113
- ...(resolveThreadIds.length > 0 ? { resolveThreadIds } : undefined),
129
+ ...(standaloneResolveThreadIds.length > 0
130
+ ? { resolveThreadIds: standaloneResolveThreadIds }
131
+ : undefined),
114
132
  hasMutations: true,
115
133
  };
116
134
  return { resolveCommand, resolveOnlyCommand };
117
135
  }
118
136
  // Single command: all mutations combined (or only one category present).
119
- const argv = buildPrShepherdCommand(["apply", "review", String(prNumber)]).argv;
137
+ const argv = buildPrShepherdCommand(["apply", "review", String(prReference)]).argv;
120
138
  if (replyThreadIds.length > 0) {
121
139
  argv.push("--reply-thread-ids", replyThreadIds.join(","));
122
140
  argv.push("--message", "$DISMISS_MESSAGE");
@@ -26,6 +26,8 @@ interface BaseBranchLookup {
26
26
  */
27
27
  export declare function formatDurationApprox(seconds: number): string;
28
28
  export declare function validateBaseBranch(raw: string): BaseBranchLookup;
29
- export declare function buildEscalateHumanMessage(escalate: Omit<EscalateDetails, "humanMessage">, pr: number): string;
29
+ export declare function buildEscalateHumanMessage(escalate: Omit<EscalateDetails, "humanMessage">, pr: string | number, opts?: {
30
+ merge?: boolean;
31
+ }): string;
30
32
  export declare function buildEscalateSuggestion(triggers: EscalateTrigger[], detail?: string): string;
31
33
  export {};
@@ -59,7 +59,7 @@ export function validateBaseBranch(raw) {
59
59
  }
60
60
  return { branch: trimmed, isFallback: false };
61
61
  }
62
- export function buildEscalateHumanMessage(escalate, pr) {
62
+ export function buildEscalateHumanMessage(escalate, pr, opts) {
63
63
  const lines = [];
64
64
  lines.push("⚠️ /pr-shepherd:pr-shepherd paused — manual intervention required");
65
65
  lines.push("");
@@ -109,6 +109,26 @@ export function buildEscalateHumanMessage(escalate, pr) {
109
109
  lines.push("");
110
110
  }
111
111
  }
112
+ if (escalate.mergeQueueRemoval) {
113
+ const removal = escalate.mergeQueueRemoval;
114
+ lines.push("");
115
+ lines.push("## Merge queue removal");
116
+ lines.push("");
117
+ lines.push(`- reason: \`${removal.reason ?? "not provided"}\``);
118
+ if (removal.actor)
119
+ lines.push(`- actor: \`@${removal.actor}\``);
120
+ lines.push(`- createdAtUnix: \`${removal.createdAtUnix}\``);
121
+ if (removal.beforeCommitOid)
122
+ lines.push(`- queue commit: \`${removal.beforeCommitOid}\``);
123
+ }
124
+ if (escalate.authorization && escalate.authorization.length > 0) {
125
+ lines.push("");
126
+ lines.push("## Authorization");
127
+ lines.push("");
128
+ for (const item of escalate.authorization) {
129
+ lines.push(`- ${item.action}: ${item.targetIds.map((id) => `\`${id}\``).join(", ")} — GitHub denied the action or did not expose a confirming capability`);
130
+ }
131
+ }
112
132
  if (escalate.thrashHistory && escalate.thrashHistory.length > 0) {
113
133
  lines.push("");
114
134
  lines.push("## Fix attempts");
@@ -120,10 +140,17 @@ export function buildEscalateHumanMessage(escalate, pr) {
120
140
  lines.push("");
121
141
  lines.push("---");
122
142
  lines.push("");
123
- lines.push(`After completing manual fixes (and pushing if required), rerun \`/pr-shepherd:pr-shepherd ${pr}\` to resume.`);
143
+ lines.push(`After completing manual fixes (and pushing if required), rerun \`/pr-shepherd:pr-shepherd ${pr}${opts?.merge ? " --merge" : ""}\` to resume.`);
124
144
  return lines.join("\n");
125
145
  }
126
146
  export function buildEscalateSuggestion(triggers, detail) {
147
+ if (triggers.includes("authorization-required")) {
148
+ return "GitHub did not confirm that the current viewer may perform one or more required actions. Ask a repository maintainer to handle the listed items; Shepherd will not recommend commands that would be denied.";
149
+ }
150
+ if (triggers.includes("merge-queue-removed")) {
151
+ const reason = detail ? ` GitHub reason: ${detail}.` : "";
152
+ return `The PR left the merge queue without an actionable check failure.${reason} Confirm the removal was not intentional before adding it again.`;
153
+ }
127
154
  if (triggers.includes("stall-timeout")) {
128
155
  const duration = detail ?? "60 minutes";
129
156
  return `No progress detected for ${duration} — state has not changed. This is a manual checkpoint: inspect the PR and apply a manual fix before resuming.`;