pr-shepherd 0.54.1 → 0.55.1

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 (121) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/bin/checks/unreported-required.d.mts +29 -0
  4. package/bin/checks/unreported-required.mjs +52 -0
  5. package/bin/classify/apply.d.mts +2 -0
  6. package/bin/classify/apply.mjs +31 -31
  7. package/bin/classify/rule-action.d.mts +8 -0
  8. package/bin/classify/rule-action.mjs +29 -0
  9. package/bin/classify/types.d.mts +1 -1
  10. package/bin/cli/help-command-pages.d.mts +2 -2
  11. package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
  12. package/bin/cli/help-iterate-poll-pages.mjs +6 -5
  13. package/bin/cli/help-top-page.d.mts +1 -1
  14. package/bin/cli/help-top-page.mjs +2 -2
  15. package/bin/cli/help.d.mts +3 -3
  16. package/bin/cli/iterate-formatter.mjs +32 -9
  17. package/bin/cli/iterate-instructions.mjs +6 -0
  18. package/bin/cli/iterate-lean.mjs +17 -0
  19. package/bin/cli/poll-summary-emitter.mjs +4 -1
  20. package/bin/cli/poll-summary-formatter.mjs +10 -3
  21. package/bin/cli/stack-overview.d.mts +48 -0
  22. package/bin/cli/stack-overview.mjs +138 -0
  23. package/bin/commands/check-fingerprint.mjs +2 -1
  24. package/bin/commands/check-status.d.mts +1 -1
  25. package/bin/commands/check-status.mjs +8 -3
  26. package/bin/commands/check-unreported.d.mts +28 -0
  27. package/bin/commands/check-unreported.mjs +101 -0
  28. package/bin/commands/check.mjs +45 -38
  29. package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
  30. package/bin/commands/commit-suggestion-instruction.mjs +1 -1
  31. package/bin/commands/iterate/base.mjs +11 -0
  32. package/bin/commands/iterate/check-instructions.d.mts +1 -1
  33. package/bin/commands/iterate/check-instructions.mjs +1 -1
  34. package/bin/commands/iterate/escalate.mjs +4 -0
  35. package/bin/commands/iterate/index.mjs +69 -3
  36. package/bin/commands/iterate/merge-state.mjs +12 -12
  37. package/bin/commands/iterate/merge.d.mts +12 -2
  38. package/bin/commands/iterate/merge.mjs +32 -2
  39. package/bin/commands/iterate/unreported-required.d.mts +18 -0
  40. package/bin/commands/iterate/unreported-required.mjs +136 -0
  41. package/bin/commands/poll-summary.mjs +1 -0
  42. package/bin/commands/poll.mjs +9 -2
  43. package/bin/commands/ready-mergeability.d.mts +1 -1
  44. package/bin/commands/ready-mergeability.mjs +2 -2
  45. package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
  46. package/bin/commands/rule-auto-resolve-format.mjs +151 -0
  47. package/bin/commands/rule-auto-resolve.d.mts +21 -0
  48. package/bin/commands/rule-auto-resolve.mjs +113 -0
  49. package/bin/commands/shepherd-journal.d.mts +1 -1
  50. package/bin/commands/shepherd-journal.mjs +1 -1
  51. package/bin/commands/stack-drain.d.mts +0 -5
  52. package/bin/commands/stack-drain.mjs +26 -16
  53. package/bin/commands/stack-merge-flag.d.mts +7 -0
  54. package/bin/commands/stack-merge-flag.mjs +6 -0
  55. package/bin/config/load.d.mts +3 -0
  56. package/bin/config/load.mjs +9 -2
  57. package/bin/config/merge-method.d.mts +27 -0
  58. package/bin/config/merge-method.mjs +56 -0
  59. package/bin/exit-codes.d.mts +1 -1
  60. package/bin/exit-codes.mjs +2 -1
  61. package/bin/github/batch-parse-suites.d.mts +3 -0
  62. package/bin/github/batch-parse-suites.mjs +15 -0
  63. package/bin/github/batch-parsers.d.mts +1 -1
  64. package/bin/github/batch-parsers.mjs +7 -0
  65. package/bin/github/batch-raw-types.d.mts +5 -0
  66. package/bin/github/batch.d.mts +3 -0
  67. package/bin/github/batch.mjs +9 -1
  68. package/bin/github/errors.d.mts +6 -0
  69. package/bin/github/errors.mjs +23 -5
  70. package/bin/github/fingerprint-fields.d.mts +1 -0
  71. package/bin/github/fingerprint-fields.mjs +1 -1
  72. package/bin/github/gql/base-behind.gql +20 -0
  73. package/bin/github/gql/batch-pr-page.gql +2 -0
  74. package/bin/github/gql/batch-pr.gql +5 -0
  75. package/bin/github/gql/commit-check-suites.gql +1 -0
  76. package/bin/github/gql/poll-stack-summary.gql +6 -0
  77. package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
  78. package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
  79. package/bin/github/gql/poll-summary-fragment.gql +16 -0
  80. package/bin/github/gql/pr-merge-policy.gql +1 -44
  81. package/bin/github/gql/ref-rules-query.gql +18 -0
  82. package/bin/github/gql/ref-rules.gql +46 -0
  83. package/bin/github/graphql-internal-retry.d.mts +1 -1
  84. package/bin/github/graphql-internal-retry.mjs +4 -3
  85. package/bin/github/merge-queue-checks.mjs +7 -3
  86. package/bin/github/merge-target-rules.d.mts +28 -0
  87. package/bin/github/merge-target-rules.mjs +77 -0
  88. package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
  89. package/bin/github/poll-summary-annotation-probe.mjs +88 -0
  90. package/bin/github/poll-summary-fingerprint.mjs +1 -1
  91. package/bin/github/poll-summary-projector.d.mts +1 -1
  92. package/bin/github/poll-summary-projector.mjs +19 -12
  93. package/bin/github/poll-summary-queue-removal.mjs +7 -3
  94. package/bin/github/poll-summary-raw.d.mts +16 -0
  95. package/bin/github/poll-summary-readiness.mjs +1 -0
  96. package/bin/github/poll-summary-route.mjs +5 -0
  97. package/bin/github/poll-summary-unreported.d.mts +6 -0
  98. package/bin/github/poll-summary-unreported.mjs +51 -0
  99. package/bin/github/poll-summary.d.mts +1 -0
  100. package/bin/github/poll-summary.mjs +28 -3
  101. package/bin/github/queries.d.mts +9 -0
  102. package/bin/github/queries.mjs +11 -2
  103. package/bin/github/queue-removal-freshness.d.mts +20 -5
  104. package/bin/github/queue-removal-freshness.mjs +37 -10
  105. package/bin/github/stack-read.d.mts +4 -0
  106. package/bin/github/stack-read.mjs +13 -1
  107. package/bin/mcp/server.mjs +6 -1
  108. package/bin/state/ci-retrigger.d.mts +25 -0
  109. package/bin/state/ci-retrigger.mjs +48 -0
  110. package/bin/types/escalate.d.mts +1 -1
  111. package/bin/types/github.d.mts +11 -0
  112. package/bin/types/iterate.d.mts +16 -3
  113. package/bin/types/poll-summary.d.mts +13 -0
  114. package/bin/types/report.d.mts +34 -1
  115. package/package.json +1 -1
  116. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  117. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  118. package/plugins/pr-shepherd/.mcp.json +1 -1
  119. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -3
  120. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  121. package/src/classify/types.mts +1 -1
@@ -1,5 +1,7 @@
1
+ import { ruleAutoResolveFromReport } from "../rule-auto-resolve-format.mjs";
1
2
  import { buildActiveChecks, buildRelevantChecks, buildSummary, buildSuppressedCheckFields, } from "./helpers.mjs";
2
3
  export function buildIterateBase(report, readyState) {
4
+ const ruleAutoResolve = ruleAutoResolveFromReport(report);
3
5
  return {
4
6
  pr: report.pr,
5
7
  repo: report.repo,
@@ -21,6 +23,15 @@ export function buildIterateBase(report, readyState) {
21
23
  ...buildSuppressedCheckFields(report),
22
24
  activity: report.activity,
23
25
  mergeQueue: report.mergeQueue,
26
+ ...(report.unreportedRequiredChecks &&
27
+ report.unreportedRequiredChecks.length > 0 && {
28
+ unreportedRequiredChecks: report.unreportedRequiredChecks,
29
+ }),
30
+ ...(report.trunkBehindBy !== undefined &&
31
+ report.trunkBehindBy > 0 && { trunkBehindBy: report.trunkBehindBy }),
32
+ ...(report.baseBehindBy !== undefined &&
33
+ report.baseBehindBy > 0 && { baseBehindBy: report.baseBehindBy }),
24
34
  ...(report.fingerprintReused === true && { fingerprintReused: true }),
35
+ ...(ruleAutoResolve && { ruleAutoResolve }),
25
36
  };
26
37
  }
@@ -4,7 +4,7 @@ export declare function buildCrStaleClause(reviews: Review[]): string;
4
4
  /**
5
5
  * Build the optional behind-base push hint. Empty unless the branch is actually behind its base
6
6
  * and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
7
- * rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in CLAUDE.md); this
7
+ * rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in AGENTS.md); this
8
8
  * only echoes back the caller's own configured pointer. `hint` is trimmed and type-checked at the
9
9
  * point of use (rather than at config load) so a malformed rc file value (non-string, or
10
10
  * whitespace-only) degrades to "no hint" instead of rendering garbage into agent-facing text or
@@ -8,7 +8,7 @@ export function buildCrStaleClause(reviews) {
8
8
  /**
9
9
  * Build the optional behind-base push hint. Empty unless the branch is actually behind its base
10
10
  * and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
11
- * rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in CLAUDE.md); this
11
+ * rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in AGENTS.md); this
12
12
  * only echoes back the caller's own configured pointer. `hint` is trimmed and type-checked at the
13
13
  * point of use (rather than at config load) so a malformed rc file value (non-string, or
14
14
  * whitespace-only) degrades to "no hint" instead of rendering garbage into agent-facing text or
@@ -240,6 +240,10 @@ export function buildEscalateSuggestion(triggers, detail) {
240
240
  const duration = detail ?? "60 minutes";
241
241
  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.`;
242
242
  }
243
+ if (triggers.includes("required-checks-unreported")) {
244
+ const names = detail ? ` (${detail})` : "";
245
+ return `Required checks are still unreported${names}, and no CI is running. Rebase and push is the fix when the branch is behind. This head is current, and another reopen will not create a job the workflow does not emit. Path filters are the usual cause. Investigate that, then handle the missing checks manually.`;
246
+ }
243
247
  if (triggers.includes("stall-state-unavailable")) {
244
248
  const reason = detail ?? "unknown error";
245
249
  return `The stall timer could not be saved (${reason}). Fix PR_SHEPHERD_STATE_DIR or the directory permissions, then resume. Automated polling is paused because a stuck loop would not be detected.`;
@@ -18,6 +18,7 @@ import { markReadyIfAuthorized } from "./mark-ready.mjs";
18
18
  import { withIterateApiUsage } from "./run.mjs";
19
19
  import { fetchRawSummaryPr } from "../../github/poll-summary.mjs";
20
20
  import { summarizePollSummaryPr } from "../../github/poll-summary-projector.mjs";
21
+ import { annotationProbeUnavailable } from "../../github/poll-summary-annotation-probe.mjs";
21
22
  import { fingerprintRawSummaryPr } from "../../github/poll-summary-fingerprint.mjs";
22
23
  import { currentQueueRemovalEvent } from "../../github/poll-summary-queue-removal.mjs";
23
24
  import { isCurrentSummaryReady } from "../../github/poll-summary-readiness.mjs";
@@ -25,6 +26,7 @@ import { clearReadyReceipt, isReadyReceiptCurrent, readReadyReceipt, writeReadyR
25
26
  import { stackDraftHold } from "./parent-first.mjs";
26
27
  import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
27
28
  import { annotateBlockedWait, resolveCheckBlockerGate } from "./check-blocker-gate.mjs";
29
+ import { buildUnreportedFixResult, planUnreportedRequired } from "./unreported-required.mjs";
28
30
  export function runIterate(opts) {
29
31
  return withIterateApiUsage(opts, () => runIterateCore(opts));
30
32
  }
@@ -122,6 +124,16 @@ async function runIterateCore(opts) {
122
124
  // readiness evidence, so a rerun (e.g. with --merge) does not wait again.
123
125
  const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName, { headSha, alreadyElapsed: receiptCurrent });
124
126
  const base = buildIterateBase(reportForWork, readyState);
127
+ const unreportedPlan = await planUnreportedRequired({
128
+ report,
129
+ base,
130
+ stateKey: stallKey,
131
+ headSha,
132
+ otherAutonomousWork: hasActionableWork || staleAncestry !== null,
133
+ });
134
+ if (unreportedPlan.escalate)
135
+ return unreportedPlan.escalate;
136
+ const unreportedRepair = unreportedPlan.repairInstructions ?? [];
125
137
  // Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
126
138
  // GitHub itself is already acting on — the queue will eject the PR for these regardless of
127
139
  // what Shepherd does, so they always surface immediately. Only review threads/comments/
@@ -148,9 +160,36 @@ async function runIterateCore(opts) {
148
160
  surfacedApprovals,
149
161
  botUsernames,
150
162
  releasedCheckNames: blockerGate?.releasedNames,
163
+ ...((unreportedRepair.length > 0 || staleAncestry !== null) && {
164
+ repairInstructions: [...unreportedRepair, ...(staleAncestry?.instructions ?? [])],
165
+ }),
166
+ ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
167
+ });
168
+ }
169
+ if (unreportedRepair.length > 0 && staleAncestry) {
170
+ return handleFixCode({
171
+ base,
172
+ report: reportForWork,
173
+ opts: { ...opts, prNumber, neverCancelRuns },
174
+ headSha,
175
+ stallKey,
176
+ prNumber,
177
+ stallTimeoutSeconds,
178
+ repoOwner,
179
+ repoName,
180
+ reviewSummaryIds,
181
+ firstLookSummaries,
182
+ editedSummaries,
183
+ surfacedApprovals,
184
+ botUsernames,
185
+ releasedCheckNames: blockerGate?.releasedNames,
186
+ repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
151
187
  ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
152
188
  });
153
189
  }
190
+ if (unreportedRepair.length > 0) {
191
+ return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, buildUnreportedFixResult(base, report, unreportedRepair), reportForWork, reviewSummaryIds), blockerGate);
192
+ }
154
193
  const mergeStateResult = await handleActiveMergeState({
155
194
  enabled: opts.merge,
156
195
  active: activeMerge,
@@ -182,7 +221,7 @@ async function runIterateCore(opts) {
182
221
  surfacedApprovals,
183
222
  botUsernames,
184
223
  releasedCheckNames: blockerGate?.releasedNames,
185
- repairInstructions: staleAncestry.instructions,
224
+ repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
186
225
  ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
187
226
  });
188
227
  }
@@ -220,6 +259,17 @@ async function runIterateCore(opts) {
220
259
  log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
221
260
  };
222
261
  }
262
+ if (isCleanReadyState &&
263
+ readyState.isReady &&
264
+ !readyState.shouldCancel &&
265
+ readyState.remainingSeconds > 0) {
266
+ await clearStallState(stallKey);
267
+ return {
268
+ ...base,
269
+ action: "ready",
270
+ log: `READY: PR #${base.pr} is ready — ${readyState.remainingSeconds}s of ready-delay remaining — ${buildWaitLog(base).slice("WAIT: ".length)}`,
271
+ };
272
+ }
223
273
  const hold = stackDraftHold(report, autoMarkReady);
224
274
  const wait = {
225
275
  ...base,
@@ -238,14 +288,19 @@ async function recordReadyReceipt(key, report) {
238
288
  return false;
239
289
  try {
240
290
  const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
241
- const fingerprint = fingerprintRawSummaryPr(raw);
242
- if (fingerprint === null ||
291
+ if (fingerprintRawSummaryPr(raw) === null ||
243
292
  raw.state !== "OPEN" ||
244
293
  raw.isDraft ||
245
294
  raw.headRefOid !== report.headSha ||
246
295
  raw.baseRefOid !== report.baseRefOid)
247
296
  return false;
297
+ // Summarize hydrates annotation totals onto the snapshot before the stored hash.
248
298
  const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
299
+ if (annotationProbeUnavailable(raw))
300
+ return false;
301
+ const fingerprint = fingerprintRawSummaryPr(raw);
302
+ if (fingerprint === null)
303
+ return false;
249
304
  if (!isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {}))
250
305
  return false;
251
306
  const removalEvent = currentQueueRemovalEvent(raw);
@@ -290,6 +345,17 @@ async function revalidateReadyReceipt(key, report, hasReadinessWork) {
290
345
  }
291
346
  try {
292
347
  const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
348
+ const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
349
+ if (annotationProbeUnavailable(raw)) {
350
+ const sameHead = raw.headRefOid === report.headSha;
351
+ const sameBase = retainQueuedReceipt || raw.baseRefOid === report.baseRefOid;
352
+ const stillReady = isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {});
353
+ if (!sameHead || !sameBase || !stillReady) {
354
+ await clearReadyReceipt(key);
355
+ return false;
356
+ }
357
+ return true;
358
+ }
293
359
  // Queue predecessors can advance the target branch without changing this
294
360
  // PR's source. Compare against the receipt's base only while both fresh
295
361
  // views still place the PR in the queue.
@@ -1,6 +1,6 @@
1
1
  import { clearStallState } from "../../state/iterate-stall.mjs";
2
2
  import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
3
- import { buildMergeCommandPlan } from "./merge.mjs";
3
+ import { buildMergeCommandPlan, unavailableMergeResult } from "./merge.mjs";
4
4
  import { formatPrUrl } from "../../pr-reference.mjs";
5
5
  import { buildPrShepherdCommand } from "../../cli/runner.mjs";
6
6
  import { inlineCode } from "../../util/markdown.mjs";
@@ -49,17 +49,17 @@ export function buildReadyMergeOutcome(enabled, readyElapsed, base, report) {
49
49
  return buildStackedRouteResult(base, report, stack);
50
50
  const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
51
51
  report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
52
- return {
53
- ...base,
54
- action: "merge",
55
- merge: buildMergeCommandPlan({
56
- pr: report.pr,
57
- repo: report.repo,
58
- nodeId: report.nodeId,
59
- headSha: report.headSha ?? "unknown",
60
- queue,
61
- }),
62
- };
52
+ const plan = buildMergeCommandPlan({
53
+ pr: report.pr,
54
+ repo: report.repo,
55
+ nodeId: report.nodeId,
56
+ headSha: report.headSha ?? "unknown",
57
+ queue,
58
+ ...(report.allowedMergeMethods && { allowedMergeMethods: report.allowedMergeMethods }),
59
+ });
60
+ if ("unavailable" in plan)
61
+ return unavailableMergeResult(base, report, plan.unavailable);
62
+ return { ...base, action: "merge", merge: plan };
63
63
  }
64
64
  /** Raw counts of non-CI actionable work held back for one queued-PR wait tick. Omitted (all zero) when empty. */
65
65
  function buildDeferredWork(input) {
@@ -1,13 +1,23 @@
1
- import type { MergeCommandPlan } from "../../types.mts";
1
+ import { type MergeMethod } from "../../config/merge-method.mts";
2
+ import type { IterateResult, IterateResultBase, MergeCommandPlan } from "../../types.mts";
2
3
  interface MergePlanInput {
3
4
  pr: number;
4
5
  repo: string;
5
6
  nodeId: string;
6
7
  headSha: string;
7
8
  queue: boolean;
9
+ /** Omitted when the batch did not select repository merge settings. */
10
+ allowedMergeMethods?: readonly MergeMethod[];
8
11
  }
9
- export declare function buildMergeCommandPlan(input: MergePlanInput): MergeCommandPlan;
12
+ export interface MergeMethodUnavailable {
13
+ unavailable: string;
14
+ }
15
+ export declare function buildMergeCommandPlan(input: MergePlanInput): MergeCommandPlan | MergeMethodUnavailable;
10
16
  export declare function renderMergeCommand(command: {
11
17
  argv: string[];
12
18
  }): string;
19
+ export declare function unavailableMergeResult(base: IterateResultBase, report: {
20
+ pr: number;
21
+ repo: string;
22
+ }, unavailable: string): IterateResult;
13
23
  export {};
@@ -1,6 +1,9 @@
1
1
  import { loadConfig } from "../../config/load.mjs";
2
2
  import { findMergeStrategies } from "../../config/merge-command-args.mjs";
3
+ import { chooseMergeMethod, configuredMergeMethod, } from "../../config/merge-method.mjs";
4
+ import { formatPrUrl } from "../../pr-reference.mjs";
3
5
  import { renderShellCommand } from "../../cli/runner.mjs";
6
+ import { buildEscalateHumanMessage } from "./escalate.mjs";
4
7
  const ENQUEUE_MUTATION = "mutation EnqueuePullRequest($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }";
5
8
  export function buildMergeCommandPlan(input) {
6
9
  const base = [
@@ -32,9 +35,17 @@ export function buildMergeCommandPlan(input) {
32
35
  },
33
36
  };
34
37
  }
35
- const configuredArgs = loadConfig().merge?.commandArgs ?? [];
38
+ const mergeConfig = loadConfig().merge;
39
+ const configuredArgs = mergeConfig?.commandArgs ?? [];
40
+ const decision = chooseMergeMethod({
41
+ allowed: input.allowedMergeMethods,
42
+ configured: configuredMergeMethod(mergeConfig ?? {}),
43
+ fallback: "merge",
44
+ });
45
+ if ("unavailable" in decision)
46
+ return decision;
36
47
  const hasStrategy = findMergeStrategies(configuredArgs).length > 0;
37
- const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs, "--merge"];
48
+ const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs, `--${decision.method}`];
38
49
  return {
39
50
  mode: "auto",
40
51
  command: { argv: [...base, "--auto", ...commandArgs] },
@@ -44,3 +55,22 @@ export function buildMergeCommandPlan(input) {
44
55
  export function renderMergeCommand(command) {
45
56
  return renderShellCommand(command.argv);
46
57
  }
58
+ export function unavailableMergeResult(base, report, unavailable) {
59
+ const escalateBase = {
60
+ triggers: ["merge-method-unavailable"],
61
+ unresolvedThreads: [],
62
+ ambiguousComments: [],
63
+ changesRequestedReviews: [],
64
+ suggestion: unavailable,
65
+ };
66
+ return {
67
+ ...base,
68
+ action: "escalate",
69
+ escalate: {
70
+ ...escalateBase,
71
+ humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
72
+ merge: true,
73
+ }),
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,18 @@
1
+ import type { IterateResult, IterateResultBase, ShepherdReport } from "../../types.mts";
2
+ interface UnreportedPlan {
3
+ escalate?: IterateResult;
4
+ repairInstructions?: string[];
5
+ }
6
+ export declare function planUnreportedRequired(input: {
7
+ report: ShepherdReport;
8
+ base: IterateResultBase;
9
+ stateKey: {
10
+ owner: string;
11
+ repo: string;
12
+ pr: number;
13
+ };
14
+ headSha: string;
15
+ otherAutonomousWork: boolean;
16
+ }): Promise<UnreportedPlan>;
17
+ export declare function buildUnreportedFixResult(base: IterateResultBase, report: ShepherdReport, instructions: string[]): IterateResult;
18
+ export {};
@@ -0,0 +1,136 @@
1
+ import { buildPrShepherdCommand } from "../../cli/runner.mjs";
2
+ import { formatPrUrl } from "../../pr-reference.mjs";
3
+ import { readCiRetrigger, sameCiRetrigger, writeCiRetrigger } from "../../state/ci-retrigger.mjs";
4
+ import { buildFixCompletionInstruction } from "./check-instructions.mjs";
5
+ import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
6
+ import { buildNativeStackRebaseInstruction } from "./native-stack-rebase.mjs";
7
+ /** Rebase when the trunk or the PR base is behind; otherwise close and reopen once. */
8
+ function decideUnreportedRequired(input) {
9
+ if (input.behind)
10
+ return "rebase";
11
+ if (input.alreadyRetriggered)
12
+ return input.otherAutonomousWork ? "none" : "escalate";
13
+ return "reopen";
14
+ }
15
+ function listedChecks(names) {
16
+ return names.map((name) => `\`${name}\``).join(", ");
17
+ }
18
+ /** Facts, then rebase-and-push, then investigate or escalate if that push does not start CI. */
19
+ function buildUnreportedRequiredInstructions(input) {
20
+ const names = listedChecks(input.names);
21
+ const gap = input.behindBy > 0 && input.trunk
22
+ ? `The stack trunk is behind by ${input.behindBy} commits.`
23
+ : input.behindBy > 0
24
+ ? `This branch is behind \`${input.baseBranch}\` by ${input.behindBy} commits.`
25
+ : input.behind
26
+ ? "This PR's derived merge status is BEHIND."
27
+ : `This branch is not behind \`${input.baseBranch}\`.`;
28
+ const facts = `${gap} No CI checks are running, and required checks have not passed: ${names}.`;
29
+ if (!input.behind) {
30
+ return [
31
+ facts,
32
+ `Retrigger workflows once for this head with \`gh pr close ${input.pr} -R ${input.repo}\` then \`gh pr reopen ${input.pr} -R ${input.repo}\`.`,
33
+ "If those checks are still missing on the next poll, investigate why the workflows did not start. Do not close the PR again. Shepherd escalates with `required-checks-unreported` when this remains the only blocker.",
34
+ ];
35
+ }
36
+ const rebase = input.stackRebase
37
+ ? `${input.stackRebase} Then push the rewritten stack with \`gh stack push\`.`
38
+ : `Rebase onto \`${input.baseBranch}\` and push.`;
39
+ return [
40
+ facts,
41
+ `${rebase} Rebase and push is how these checks start.`,
42
+ "If that push does not start the checks, investigate why the workflows did not run. Shepherd escalates with `required-checks-unreported` when the branch is current and this remains the only blocker.",
43
+ ];
44
+ }
45
+ export async function planUnreportedRequired(input) {
46
+ const names = input.report.unreportedRequiredChecks ?? [];
47
+ const actionsRunning = input.report.actionsWorkflowInProgress === true || input.report.checks.inProgress.length > 0;
48
+ const failing = input.report.checks.failing.length > 0;
49
+ const conflicts = input.report.mergeStatus.status === "CONFLICTS";
50
+ const inQueue = input.report.mergeQueue?.inQueue === true;
51
+ if (names.length === 0 || actionsRunning || failing || conflicts || inQueue)
52
+ return {};
53
+ const behindBy = input.report.trunkBehindBy ?? input.report.baseBehindBy ?? 0;
54
+ const behind = behindBy > 0 || input.report.mergeStatus.status === "BEHIND";
55
+ const decision = decideUnreportedRequired({
56
+ behind,
57
+ alreadyRetriggered: sameCiRetrigger(await readCiRetrigger(input.stateKey), input.headSha, names),
58
+ otherAutonomousWork: input.otherAutonomousWork,
59
+ });
60
+ if (decision === "none")
61
+ return {};
62
+ if (decision === "escalate")
63
+ return { escalate: escalateUnreported(input.base, input.report, names) };
64
+ const rebase = stackRebase(input.report);
65
+ const instructions = buildUnreportedRequiredInstructions({
66
+ names,
67
+ repo: input.report.repo,
68
+ pr: input.report.pr,
69
+ baseBranch: input.report.baseBranch,
70
+ behind,
71
+ behindBy,
72
+ trunk: (input.report.trunkBehindBy ?? 0) > 0,
73
+ ...(rebase && { stackRebase: rebase }),
74
+ });
75
+ if (decision === "reopen") {
76
+ await writeCiRetrigger(input.stateKey, { headSha: input.headSha, contexts: names });
77
+ }
78
+ return { repairInstructions: instructions };
79
+ }
80
+ export function buildUnreportedFixResult(base, report, instructions) {
81
+ const prUrl = formatPrUrl(report.repo, report.pr);
82
+ return {
83
+ ...base,
84
+ action: "fix_code",
85
+ fix: {
86
+ threads: [],
87
+ resolutionOnlyThreads: [],
88
+ actionableComments: [],
89
+ reviewSummaryIds: [],
90
+ firstLookSummaries: [],
91
+ editedSummaries: [],
92
+ surfacedApprovals: [],
93
+ checks: [],
94
+ changesRequestedReviews: [],
95
+ resolveCommand: {
96
+ argv: buildPrShepherdCommand(["apply", "review", prUrl]).argv,
97
+ requiresHeadSha: false,
98
+ requiresDismissMessage: false,
99
+ hasMutations: false,
100
+ },
101
+ instructions: [...instructions, buildFixCompletionInstruction([])],
102
+ inProgressRunIds: [],
103
+ protectedRuns: [],
104
+ firstLookThreads: [],
105
+ firstLookComments: [],
106
+ },
107
+ cancelled: [],
108
+ };
109
+ }
110
+ function stackRebase(report) {
111
+ const stack = report.mergeStatus.mergeRequirements?.stack;
112
+ if (!stack)
113
+ return undefined;
114
+ const start = report.stackBottomPr !== undefined
115
+ ? { bottomPr: report.stackBottomPr }
116
+ : { trunk: stack.baseRefName };
117
+ return buildNativeStackRebaseInstruction(report.repo, stack.number, start);
118
+ }
119
+ function escalateUnreported(base, report, names) {
120
+ const detail = names.map((name) => `\`${name}\``).join(", ");
121
+ const escalateBase = {
122
+ triggers: ["required-checks-unreported"],
123
+ unresolvedThreads: [],
124
+ ambiguousComments: [],
125
+ changesRequestedReviews: [],
126
+ suggestion: buildEscalateSuggestion(["required-checks-unreported"], detail),
127
+ };
128
+ return {
129
+ ...base,
130
+ action: "escalate",
131
+ escalate: {
132
+ ...escalateBase,
133
+ humanMessage: buildEscalateHumanMessage(escalateBase, report.pr),
134
+ },
135
+ };
136
+ }
@@ -31,6 +31,7 @@ async function runPollSummaryCore(opts) {
31
31
  reason: allTerminal ? "all_terminal" : actionable ? "actionable" : "waiting",
32
32
  prs: fetched.prs,
33
33
  ...(fetched.stackAncestry?.length && { stackAncestry: fetched.stackAncestry }),
34
+ ...(fetched.allowedMergeMethods && { allowedMergeMethods: fetched.allowedMergeMethods }),
34
35
  }, opts.merge === true);
35
36
  return applyStackStallGuard(planned, repo, opts.stallTimeoutSeconds ?? loadConfig().iterate.stallTimeoutMinutes * 60);
36
37
  }
@@ -6,6 +6,10 @@ import { quotaPollIntervalMs } from "./poll-quota.mjs";
6
6
  import { onePrCancelFromPulls, onePrRateLimitTargets } from "./poll-rate-limit-cancel.mjs";
7
7
  import { createUntilTerminalRateLimitRetry } from "./poll-rate-limit-wait.mjs";
8
8
  import { writeDebounceProgress, writeWaitProgress } from "./poll-progress.mjs";
9
+ function noteRuleAutoResolve(result) {
10
+ if (result.ruleAutoResolve?.summary)
11
+ process.stderr.write(`${result.ruleAutoResolve.summary}\n`);
12
+ }
9
13
  const DEFAULT_POLL_DEBOUNCE_SECONDS = 60;
10
14
  const MAX_TIMER_MS = 2 ** 31 - 1;
11
15
  const TIMER_DRIFT_TOLERANCE_MS = 500;
@@ -38,8 +42,7 @@ async function runPollCore(opts) {
38
42
  const remainingBefore = untilTerminal
39
43
  ? Number.POSITIVE_INFINITY
40
44
  : timeoutMs - (Date.now() - start);
41
- // Cache only internal continuation ticks. Last bounded tick, FIX_CODE debounce,
42
- // and any tick we return to the caller must fetch BatchPr.
45
+ // Cache only internal continuation ticks; returned ticks must fetch BatchPr.
43
46
  const allowCache = debounceUntil === null && remainingBefore + TIMER_DRIFT_TOLERANCE_MS >= intervalMs;
44
47
  const iterateTick = (fingerprintCache) => runIterate({
45
48
  ...iterateOpts,
@@ -89,6 +92,7 @@ async function runPollCore(opts) {
89
92
  if (lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) {
90
93
  debounceUntil ??= Date.now() + debounceMs;
91
94
  const remainingMs = Math.max(debounceUntil - Date.now(), 0);
95
+ noteRuleAutoResolve(lastResult);
92
96
  writeDebounceProgress(tick, Date.now() - start, remainingMs);
93
97
  await sleep(Math.min(intervalMs, remainingMs));
94
98
  continue;
@@ -123,6 +127,7 @@ async function runPollCore(opts) {
123
127
  verbose,
124
128
  lastWaitSignature,
125
129
  });
130
+ noteRuleAutoResolve(lastResult);
126
131
  await sleep(sleepMs);
127
132
  continue;
128
133
  }
@@ -139,10 +144,12 @@ async function runPollCore(opts) {
139
144
  break;
140
145
  }
141
146
  }
147
+ noteRuleAutoResolve(lastResult);
142
148
  await sleep(sleepMs);
143
149
  continue;
144
150
  }
145
151
  if (lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) {
152
+ noteRuleAutoResolve(lastResult);
146
153
  debounceUntil ??= Date.now() + debounceMs;
147
154
  const remainingMs = debounceUntil - Date.now();
148
155
  if (remainingMs > 0) {
@@ -10,6 +10,6 @@ export declare function refreshUnknownMergeability(prNumber: number, repo: RepoI
10
10
  batchData: BatchPrData;
11
11
  didRefresh: boolean;
12
12
  }>;
13
- export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number): Promise<ReadyMergeabilityRefresh>;
13
+ export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number, hasUnreportedRequired?: boolean): Promise<ReadyMergeabilityRefresh>;
14
14
  export declare function isBlockedByFilteredCheck(mergeStatus: MergeStatusResult, verdict: CiVerdict): boolean;
15
15
  export {};
@@ -8,10 +8,10 @@ export async function refreshUnknownMergeability(prNumber, repo, batchData) {
8
8
  }
9
9
  return { batchData: await readMergeability(prNumber, repo, batchData), didRefresh: true };
10
10
  }
11
- export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount) {
11
+ export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount, hasUnreportedRequired = false) {
12
12
  const refreshedBatchData = await readMergeability(prNumber, repo, batchData);
13
13
  const mergeStatus = deriveMergeStatus(refreshedBatchData);
14
- const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount);
14
+ const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount, hasUnreportedRequired);
15
15
  return { batchData: refreshedBatchData, mergeStatus, status };
16
16
  }
17
17
  export function isBlockedByFilteredCheck(mergeStatus, verdict) {
@@ -0,0 +1,22 @@
1
+ import type { AutoMinimizedItem, AutoResolvedThread, RuleAutoResolveReport, ShepherdReport } from "../types.mts";
2
+ export declare function uniqueReasons(reasons: readonly string[]): string[];
3
+ export declare function reasonClause(reasons: readonly string[]): string | undefined;
4
+ export declare function ruleAutoResolveBody(counts: {
5
+ threads: number;
6
+ comments: number;
7
+ reviewSummaries: number;
8
+ }): string | undefined;
9
+ export declare function formatRuleAutoResolveJournalItem(input: {
10
+ body: string;
11
+ reasons: readonly string[];
12
+ viewerLogin?: string;
13
+ urls: readonly string[];
14
+ }): string;
15
+ export declare function decorateAutoResolveError(error: string, reasons: readonly string[]): string;
16
+ export declare function reasonsForError(error: string, ids: readonly string[], ruleReasons: ReadonlyMap<string, readonly string[]>): string[];
17
+ export declare function ruleAutoResolveFromReport(report: ShepherdReport): RuleAutoResolveReport | undefined;
18
+ export declare function projectRuleAutoResolve(event: RuleAutoResolveReport): Record<string, unknown>;
19
+ export declare function insertRuleAutoResolveSection(text: string, event: RuleAutoResolveReport | undefined): string;
20
+ export declare function stripReplayedRuleAutoResolve(report: ShepherdReport): ShepherdReport;
21
+ export declare function minimizedItem(kind: AutoMinimizedItem["kind"], id: string, url: string | undefined, reasons: readonly string[]): AutoMinimizedItem;
22
+ export declare function resolvedThread(thread: AutoResolvedThread, reasons: readonly string[]): AutoResolvedThread;