pr-shepherd 0.54.0 → 0.55.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 (126) 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-branch-segment.d.mts +3 -0
  17. package/bin/cli/iterate-branch-segment.mjs +13 -0
  18. package/bin/cli/iterate-formatter.mjs +34 -22
  19. package/bin/cli/iterate-instructions.mjs +6 -0
  20. package/bin/cli/iterate-lean.mjs +16 -0
  21. package/bin/cli/poll-summary-emitter.mjs +4 -1
  22. package/bin/cli/poll-summary-formatter.mjs +10 -3
  23. package/bin/cli/stack-overview.d.mts +48 -0
  24. package/bin/cli/stack-overview.mjs +138 -0
  25. package/bin/commands/check-fingerprint.mjs +2 -1
  26. package/bin/commands/check-status.d.mts +1 -1
  27. package/bin/commands/check-status.mjs +8 -3
  28. package/bin/commands/check-unreported.d.mts +26 -0
  29. package/bin/commands/check-unreported.mjs +73 -0
  30. package/bin/commands/check.mjs +45 -38
  31. package/bin/commands/iterate/base.mjs +9 -0
  32. package/bin/commands/iterate/conflicting-head-ci.d.mts +18 -0
  33. package/bin/commands/iterate/conflicting-head-ci.mjs +32 -0
  34. package/bin/commands/iterate/escalate.mjs +4 -0
  35. package/bin/commands/iterate/fix-code.mjs +30 -1
  36. package/bin/commands/iterate/index.mjs +69 -3
  37. package/bin/commands/iterate/merge-state.mjs +12 -12
  38. package/bin/commands/iterate/merge.d.mts +12 -2
  39. package/bin/commands/iterate/merge.mjs +32 -2
  40. package/bin/commands/iterate/native-stack-rebase.d.mts +8 -2
  41. package/bin/commands/iterate/native-stack-rebase.mjs +14 -4
  42. package/bin/commands/iterate/stack-trunk-conflict.d.mts +13 -0
  43. package/bin/commands/iterate/stack-trunk-conflict.mjs +51 -0
  44. package/bin/commands/iterate/unreported-required.d.mts +18 -0
  45. package/bin/commands/iterate/unreported-required.mjs +122 -0
  46. package/bin/commands/poll-summary.mjs +1 -0
  47. package/bin/commands/poll.mjs +9 -2
  48. package/bin/commands/ready-mergeability.d.mts +1 -1
  49. package/bin/commands/ready-mergeability.mjs +2 -2
  50. package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
  51. package/bin/commands/rule-auto-resolve-format.mjs +151 -0
  52. package/bin/commands/rule-auto-resolve.d.mts +21 -0
  53. package/bin/commands/rule-auto-resolve.mjs +113 -0
  54. package/bin/commands/stack-drain.d.mts +0 -5
  55. package/bin/commands/stack-drain.mjs +26 -16
  56. package/bin/commands/stack-merge-flag.d.mts +7 -0
  57. package/bin/commands/stack-merge-flag.mjs +6 -0
  58. package/bin/config/load.d.mts +3 -0
  59. package/bin/config/load.mjs +9 -2
  60. package/bin/config/merge-method.d.mts +27 -0
  61. package/bin/config/merge-method.mjs +56 -0
  62. package/bin/exit-codes.d.mts +1 -1
  63. package/bin/exit-codes.mjs +2 -1
  64. package/bin/github/batch-parse-suites.d.mts +5 -0
  65. package/bin/github/batch-parse-suites.mjs +20 -0
  66. package/bin/github/batch-parsers.d.mts +1 -1
  67. package/bin/github/batch-parsers.mjs +7 -0
  68. package/bin/github/batch-raw-types.d.mts +5 -0
  69. package/bin/github/batch.d.mts +5 -0
  70. package/bin/github/batch.mjs +10 -1
  71. package/bin/github/errors.d.mts +6 -0
  72. package/bin/github/errors.mjs +23 -5
  73. package/bin/github/fingerprint-fields.d.mts +1 -0
  74. package/bin/github/fingerprint-fields.mjs +1 -1
  75. package/bin/github/gql/batch-pr-page.gql +2 -0
  76. package/bin/github/gql/batch-pr.gql +5 -0
  77. package/bin/github/gql/commit-check-suites.gql +1 -0
  78. package/bin/github/gql/poll-stack-summary.gql +6 -0
  79. package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
  80. package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
  81. package/bin/github/gql/poll-summary-fragment.gql +16 -0
  82. package/bin/github/gql/pr-merge-policy.gql +1 -44
  83. package/bin/github/gql/ref-rules-query.gql +18 -0
  84. package/bin/github/gql/ref-rules.gql +46 -0
  85. package/bin/github/gql/upper-layer-conflict-target.gql +36 -0
  86. package/bin/github/graphql-internal-retry.d.mts +1 -1
  87. package/bin/github/graphql-internal-retry.mjs +4 -3
  88. package/bin/github/merge-queue-checks.mjs +7 -3
  89. package/bin/github/merge-target-rules.d.mts +20 -0
  90. package/bin/github/merge-target-rules.mjs +54 -0
  91. package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
  92. package/bin/github/poll-summary-annotation-probe.mjs +88 -0
  93. package/bin/github/poll-summary-fingerprint.mjs +1 -1
  94. package/bin/github/poll-summary-projector.d.mts +1 -1
  95. package/bin/github/poll-summary-projector.mjs +19 -12
  96. package/bin/github/poll-summary-queue-removal.mjs +7 -3
  97. package/bin/github/poll-summary-raw.d.mts +16 -0
  98. package/bin/github/poll-summary-readiness.mjs +1 -0
  99. package/bin/github/poll-summary-route.mjs +5 -0
  100. package/bin/github/poll-summary-unreported.d.mts +6 -0
  101. package/bin/github/poll-summary-unreported.mjs +51 -0
  102. package/bin/github/poll-summary.d.mts +1 -0
  103. package/bin/github/poll-summary.mjs +28 -3
  104. package/bin/github/queries.d.mts +12 -0
  105. package/bin/github/queries.mjs +14 -2
  106. package/bin/github/queue-removal-freshness.d.mts +20 -5
  107. package/bin/github/queue-removal-freshness.mjs +37 -10
  108. package/bin/github/stack-read.d.mts +4 -0
  109. package/bin/github/stack-read.mjs +13 -1
  110. package/bin/mcp/server.mjs +6 -1
  111. package/bin/state/ci-retrigger.d.mts +25 -0
  112. package/bin/state/ci-retrigger.mjs +48 -0
  113. package/bin/state/conflicting-head-seen.d.mts +13 -0
  114. package/bin/state/conflicting-head-seen.mjs +44 -0
  115. package/bin/types/escalate.d.mts +1 -1
  116. package/bin/types/github.d.mts +11 -0
  117. package/bin/types/iterate.d.mts +19 -3
  118. package/bin/types/poll-summary.d.mts +13 -0
  119. package/bin/types/report.d.mts +34 -1
  120. package/package.json +1 -1
  121. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  122. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  123. package/plugins/pr-shepherd/.mcp.json +1 -1
  124. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
  125. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  126. package/src/classify/types.mts +1 -1
@@ -0,0 +1,51 @@
1
+ import { graphql } from "../../github/client.mjs";
2
+ import { UPPER_LAYER_CONFLICT_TARGET_QUERY } from "../../github/queries.mjs";
3
+ import { pollRateLimitRetryAfterMs } from "../poll-quota.mjs";
4
+ /** Lowest-position open layer whose base is the stack trunk, if this page contains one. */
5
+ function selectBottomOpenLayer(nodes, trunk) {
6
+ const onTrunk = nodes.flatMap((node) => {
7
+ const pull = node?.pullRequest;
8
+ if (!node || !pull || pull.state !== "OPEN" || pull.baseRefName !== trunk)
9
+ return [];
10
+ return [{ position: node.position, number: pull.number }];
11
+ });
12
+ onTrunk.sort((a, b) => a.position - b.position);
13
+ return onTrunk[0]?.number;
14
+ }
15
+ export async function lookupUpperLayerTrunkConflict(input) {
16
+ let data;
17
+ try {
18
+ ({ data } = await graphql(UPPER_LAYER_CONFLICT_TARGET_QUERY, {
19
+ owner: input.owner,
20
+ name: input.name,
21
+ number: input.pr,
22
+ headRef: input.headRef,
23
+ }));
24
+ }
25
+ catch (err) {
26
+ if (pollRateLimitRetryAfterMs(err) !== null)
27
+ throw err;
28
+ ignore(input.pr, err instanceof Error ? err.message : String(err));
29
+ return undefined;
30
+ }
31
+ const pull = data.repository?.pullRequest;
32
+ const behindBy = pull?.baseRef?.compare?.behindBy;
33
+ if (!pull || behindBy == null) {
34
+ ignore(input.pr, pull ? "base comparison unavailable" : "pull request not found");
35
+ return undefined;
36
+ }
37
+ // behindBy > 0: the layer is still behind its parent. Keep the parent rebase.
38
+ if (behindBy !== 0)
39
+ return undefined;
40
+ const entries = pull.stack?.entries;
41
+ const bottomPr = entries ? selectBottomOpenLayer(entries.nodes, input.trunk) : undefined;
42
+ if (bottomPr === undefined) {
43
+ ignore(input.pr, entries?.pageInfo.hasNextPage
44
+ ? "stack entry page truncated before the trunk layer"
45
+ : "bottom open layer not found");
46
+ }
47
+ return { trunk: input.trunk, ...(bottomPr !== undefined && { bottomPr }) };
48
+ }
49
+ function ignore(pr, message) {
50
+ process.stderr.write(`pr-shepherd: upper-layer conflict target unavailable for PR #${pr} (ignored): ${message}\n`);
51
+ }
@@ -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,122 @@
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 buildUnreportedRequiredInstruction(input) {
16
+ const listed = input.names.map((name) => `\`${name}\``).join(", ");
17
+ const observed = input.trunkBehindBy !== undefined && input.trunkBehindBy > 0
18
+ ? `The stack trunk compare is behind by ${input.trunkBehindBy}.`
19
+ : input.behind
20
+ ? "This PR's derived merge status is BEHIND."
21
+ : "The stack trunk compare is not behind and this PR's derived merge status is not BEHIND.";
22
+ const update = input.stackRebase
23
+ ? `${input.stackRebase} Then push the rewritten stack with \`gh stack push\`.`
24
+ : `Rebase or otherwise update the PR branch from \`${input.baseBranch}\` according to repository conventions, then push.`;
25
+ return [
26
+ `Required status checks have no check run and no status context on this head, and no Actions workflow is running: ${listed}.`,
27
+ observed,
28
+ `If the stack trunk is behind or this PR is behind its base, update the branch so a pull_request synchronize event runs: ${update}`,
29
+ `Otherwise retrigger workflows once for this head with \`gh pr close ${input.pr} -R ${input.repo}\` then \`gh pr reopen ${input.pr} -R ${input.repo}\`.`,
30
+ "Do not close the PR again if those checks are still missing on the next poll.",
31
+ ].join(" ");
32
+ }
33
+ export async function planUnreportedRequired(input) {
34
+ const names = input.report.unreportedRequiredChecks ?? [];
35
+ const actionsRunning = input.report.actionsWorkflowInProgress === true || input.report.checks.inProgress.length > 0;
36
+ const failing = input.report.checks.failing.length > 0;
37
+ const conflicts = input.report.mergeStatus.status === "CONFLICTS";
38
+ const inQueue = input.report.mergeQueue?.inQueue === true;
39
+ if (names.length === 0 || actionsRunning || failing || conflicts || inQueue)
40
+ return {};
41
+ const behind = (input.report.trunkBehindBy ?? 0) > 0 || input.report.mergeStatus.status === "BEHIND";
42
+ const decision = decideUnreportedRequired({
43
+ behind,
44
+ alreadyRetriggered: sameCiRetrigger(await readCiRetrigger(input.stateKey), input.headSha, names),
45
+ otherAutonomousWork: input.otherAutonomousWork,
46
+ });
47
+ if (decision === "none")
48
+ return {};
49
+ if (decision === "escalate")
50
+ return { escalate: escalateUnreported(input.base, input.report, names) };
51
+ const rebase = stackRebase(input.report);
52
+ const instruction = buildUnreportedRequiredInstruction({
53
+ names,
54
+ repo: input.report.repo,
55
+ pr: input.report.pr,
56
+ baseBranch: input.report.baseBranch,
57
+ behind,
58
+ ...(input.report.trunkBehindBy !== undefined && { trunkBehindBy: input.report.trunkBehindBy }),
59
+ ...(rebase && { stackRebase: rebase }),
60
+ });
61
+ if (decision === "reopen") {
62
+ await writeCiRetrigger(input.stateKey, { headSha: input.headSha, contexts: names });
63
+ }
64
+ return { repairInstructions: [instruction] };
65
+ }
66
+ export function buildUnreportedFixResult(base, report, instructions) {
67
+ const prUrl = formatPrUrl(report.repo, report.pr);
68
+ return {
69
+ ...base,
70
+ action: "fix_code",
71
+ fix: {
72
+ threads: [],
73
+ resolutionOnlyThreads: [],
74
+ actionableComments: [],
75
+ reviewSummaryIds: [],
76
+ firstLookSummaries: [],
77
+ editedSummaries: [],
78
+ surfacedApprovals: [],
79
+ checks: [],
80
+ changesRequestedReviews: [],
81
+ resolveCommand: {
82
+ argv: buildPrShepherdCommand(["apply", "review", prUrl]).argv,
83
+ requiresHeadSha: false,
84
+ requiresDismissMessage: false,
85
+ hasMutations: false,
86
+ },
87
+ instructions: [...instructions, buildFixCompletionInstruction([])],
88
+ inProgressRunIds: [],
89
+ protectedRuns: [],
90
+ firstLookThreads: [],
91
+ firstLookComments: [],
92
+ },
93
+ cancelled: [],
94
+ };
95
+ }
96
+ function stackRebase(report) {
97
+ const stack = report.mergeStatus.mergeRequirements?.stack;
98
+ if (!stack)
99
+ return undefined;
100
+ const start = report.stackBottomPr !== undefined
101
+ ? { bottomPr: report.stackBottomPr }
102
+ : { trunk: stack.baseRefName };
103
+ return buildNativeStackRebaseInstruction(report.repo, stack.number, start);
104
+ }
105
+ function escalateUnreported(base, report, names) {
106
+ const detail = names.map((name) => `\`${name}\``).join(", ");
107
+ const escalateBase = {
108
+ triggers: ["required-checks-unreported"],
109
+ unresolvedThreads: [],
110
+ ambiguousComments: [],
111
+ changesRequestedReviews: [],
112
+ suggestion: buildEscalateSuggestion(["required-checks-unreported"], detail),
113
+ };
114
+ return {
115
+ ...base,
116
+ action: "escalate",
117
+ escalate: {
118
+ ...escalateBase,
119
+ humanMessage: buildEscalateHumanMessage(escalateBase, report.pr),
120
+ },
121
+ };
122
+ }
@@ -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;
@@ -0,0 +1,151 @@
1
+ export function uniqueReasons(reasons) {
2
+ const unique = [];
3
+ for (const reason of reasons) {
4
+ const cleaned = reason.trim();
5
+ if (cleaned && !unique.includes(cleaned))
6
+ unique.push(cleaned);
7
+ }
8
+ return unique;
9
+ }
10
+ export function reasonClause(reasons) {
11
+ const unique = uniqueReasons(reasons);
12
+ if (unique.length === 0)
13
+ return undefined;
14
+ const label = unique.length === 1 ? "rule" : "rules";
15
+ return `(${label}: ${unique.join("; ")})`;
16
+ }
17
+ function joinedReason(reasons) {
18
+ const unique = uniqueReasons(reasons);
19
+ return unique.length > 0 ? unique.join("; ") : undefined;
20
+ }
21
+ function plural(count, singular, pluralForm = `${singular}s`) {
22
+ return `${count} ${count === 1 ? singular : pluralForm}`;
23
+ }
24
+ export function ruleAutoResolveBody(counts) {
25
+ const parts = [];
26
+ if (counts.threads > 0)
27
+ parts.push(`auto-resolved ${plural(counts.threads, "thread")}`);
28
+ const minimized = [];
29
+ if (counts.comments > 0)
30
+ minimized.push(plural(counts.comments, "comment"));
31
+ if (counts.reviewSummaries > 0) {
32
+ minimized.push(plural(counts.reviewSummaries, "review summary", "review summaries"));
33
+ }
34
+ if (minimized.length > 0)
35
+ parts.push(`minimized ${minimized.join(" and ")}`);
36
+ return parts.length > 0 ? parts.join(", ") : undefined;
37
+ }
38
+ function withReasonClause(body, reasons) {
39
+ const clause = reasonClause(reasons);
40
+ return clause ? `${body} ${clause}` : body;
41
+ }
42
+ export function formatRuleAutoResolveJournalItem(input) {
43
+ const login = input.viewerLogin?.trim();
44
+ const actor = login ? `as @${login}` : "(token login unavailable)";
45
+ const clause = reasonClause(input.reasons);
46
+ const links = input.urls.map((url) => url.trim()).filter(Boolean);
47
+ const reasonSuffix = clause ? ` ${clause}` : "";
48
+ const linkSuffix = links.length > 0 ? `: ${links.join(", ")}` : "";
49
+ return `- ${input.body} ${actor}${reasonSuffix}${linkSuffix}`;
50
+ }
51
+ export function decorateAutoResolveError(error, reasons) {
52
+ const clause = reasonClause(reasons);
53
+ return clause ? `${error} ${clause}` : error;
54
+ }
55
+ export function reasonsForError(error, ids, ruleReasons) {
56
+ const id = ids.find((candidate) => error.startsWith(`${candidate}:`));
57
+ if (id)
58
+ return [...(ruleReasons.get(id) ?? [])];
59
+ if (!error.startsWith("rate limit:"))
60
+ return [];
61
+ return uniqueReasons(ids.flatMap((candidate) => [...(ruleReasons.get(candidate) ?? [])]));
62
+ }
63
+ function splitJoined(reason) {
64
+ if (!reason)
65
+ return [];
66
+ return reason.split("; ").filter(Boolean);
67
+ }
68
+ export function ruleAutoResolveFromReport(report) {
69
+ const threads = report.threads.autoResolved;
70
+ const minimized = report.comments.autoMinimized ?? [];
71
+ const errors = report.threads.autoResolveErrors;
72
+ if (threads.length === 0 && minimized.length === 0 && errors.length === 0)
73
+ return undefined;
74
+ const comments = minimized.filter((item) => item.kind === "pr-comment").length;
75
+ const reviewSummaries = minimized.filter((item) => item.kind === "review-summary").length;
76
+ const success = threads.length + minimized.length > 0;
77
+ const reasons = success
78
+ ? uniqueReasons([
79
+ ...threads.flatMap((thread) => splitJoined(thread.ruleReason)),
80
+ ...minimized.flatMap((item) => splitJoined(item.ruleReason)),
81
+ ])
82
+ : uniqueReasons(report.threads.autoResolveErrorReasons ?? []);
83
+ const body = ruleAutoResolveBody({ threads: threads.length, comments, reviewSummaries }) ??
84
+ `auto-resolve failed for ${plural(errors.length, "mutation")}`;
85
+ return {
86
+ summary: withReasonClause(body, reasons),
87
+ ...(threads.length > 0 && { threads }),
88
+ ...(minimized.length > 0 && { minimized }),
89
+ ...(errors.length > 0 && { errors }),
90
+ };
91
+ }
92
+ function formatRuleAutoResolveSection(event) {
93
+ if (!event)
94
+ return null;
95
+ const lines = ["## Classification auto-resolve", "", event.summary];
96
+ const links = [
97
+ ...(event.threads ?? []).map((thread) => thread.url.trim() || `\`${thread.id}\``),
98
+ ...(event.minimized ?? []).map((item) => item.url?.trim() || `\`${item.id}\``),
99
+ ];
100
+ if (links.length > 0)
101
+ lines.push("", ...links.map((link) => `- ${link}`));
102
+ if ((event.errors?.length ?? 0) > 0) {
103
+ lines.push("", ...(event.errors ?? []).map((error) => `- ${error}`));
104
+ }
105
+ return lines.join("\n");
106
+ }
107
+ export function projectRuleAutoResolve(event) {
108
+ return {
109
+ summary: event.summary,
110
+ ...((event.threads?.length ?? 0) > 0 && { threads: event.threads }),
111
+ ...((event.minimized?.length ?? 0) > 0 && { minimized: event.minimized }),
112
+ ...((event.errors?.length ?? 0) > 0 && { errors: event.errors }),
113
+ };
114
+ }
115
+ export function insertRuleAutoResolveSection(text, event) {
116
+ const section = formatRuleAutoResolveSection(event);
117
+ if (!section)
118
+ return text;
119
+ const index = text.lastIndexOf("## Instructions");
120
+ if (index === -1)
121
+ return `${text}\n\n${section}`;
122
+ return `${text.slice(0, index)}${section}\n\n${text.slice(index)}`;
123
+ }
124
+ export function stripReplayedRuleAutoResolve(report) {
125
+ const hasEvent = report.threads.autoResolved.length > 0 ||
126
+ report.threads.autoResolveErrors.length > 0 ||
127
+ (report.threads.autoResolveErrorReasons?.length ?? 0) > 0 ||
128
+ (report.comments.autoMinimized?.length ?? 0) > 0;
129
+ if (!hasEvent)
130
+ return report;
131
+ const { autoMinimized: _autoMinimized, ...comments } = report.comments;
132
+ const { autoResolveErrorReasons: _autoResolveErrorReasons, ...threads } = report.threads;
133
+ return {
134
+ ...report,
135
+ threads: { ...threads, autoResolved: [], autoResolveErrors: [] },
136
+ comments,
137
+ };
138
+ }
139
+ export function minimizedItem(kind, id, url, reasons) {
140
+ const ruleReason = joinedReason(reasons);
141
+ return {
142
+ id,
143
+ kind,
144
+ ...(url?.trim() ? { url: url.trim() } : {}),
145
+ ...(ruleReason ? { ruleReason } : {}),
146
+ };
147
+ }
148
+ export function resolvedThread(thread, reasons) {
149
+ const ruleReason = joinedReason(reasons);
150
+ return { ...thread, isResolved: true, ...(ruleReason ? { ruleReason } : {}) };
151
+ }
@@ -0,0 +1,21 @@
1
+ import type { BatchPartition } from "../classify/apply.mts";
2
+ import type { AutoMinimizedItem, AutoResolvedThread, BatchPrData } from "../types.mts";
3
+ export interface RuleAutoResolveApplied {
4
+ threadIds: string[];
5
+ commentIds: string[];
6
+ reviewSummaryIds: string[];
7
+ autoResolved: AutoResolvedThread[];
8
+ autoMinimized: AutoMinimizedItem[];
9
+ autoResolveErrors: string[];
10
+ errorReasons: string[];
11
+ }
12
+ export declare function applySuppressedRuleAutoResolve(input: {
13
+ enabled: boolean;
14
+ partition: BatchPartition;
15
+ batch: BatchPrData;
16
+ prNumber: number;
17
+ repo: {
18
+ owner: string;
19
+ name: string;
20
+ };
21
+ }): Promise<RuleAutoResolveApplied>;
@@ -0,0 +1,113 @@
1
+ import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
2
+ import { runJournal } from "./journal/index.mjs";
3
+ import { decorateAutoResolveError, formatRuleAutoResolveJournalItem, minimizedItem, reasonClause, reasonsForError, resolvedThread, ruleAutoResolveBody, uniqueReasons, } from "./rule-auto-resolve-format.mjs";
4
+ const EMPTY = {
5
+ threadIds: [],
6
+ commentIds: [],
7
+ reviewSummaryIds: [],
8
+ autoResolved: [],
9
+ autoMinimized: [],
10
+ autoResolveErrors: [],
11
+ errorReasons: [],
12
+ };
13
+ function idsIn(ids, confirmed) {
14
+ return ids.filter((id) => confirmed.has(id));
15
+ }
16
+ function decorateErrors(errors, ids, ruleReasons) {
17
+ return errors.map((error) => decorateAutoResolveError(error, reasonsForError(error, ids, ruleReasons)));
18
+ }
19
+ function successReasons(ids, ruleReasons) {
20
+ return uniqueReasons(ids.flatMap((id) => [...(ruleReasons.get(id) ?? [])]));
21
+ }
22
+ export async function applySuppressedRuleAutoResolve(input) {
23
+ if (!input.enabled) {
24
+ return {
25
+ ...EMPTY,
26
+ threadIds: input.partition.ruleAutoResolveThreadIds,
27
+ commentIds: input.partition.ruleAutoResolveCommentIds,
28
+ reviewSummaryIds: input.partition.ruleAutoResolveReviewSummaryIds,
29
+ };
30
+ }
31
+ const commentIds = input.partition.ruleAutoResolveCommentIds.filter((id) => input.partition.suppressedCommentIds.has(id));
32
+ const reviewSummaryIds = input.partition.ruleAutoResolveReviewSummaryIds.filter((id) => input.partition.suppressedReviewSummaryIds.has(id));
33
+ const threadIds = input.partition.ruleAutoResolveThreadIds.filter((id) => input.partition.suppressedThreadIds.has(id));
34
+ const minimizeIds = [...commentIds, ...reviewSummaryIds];
35
+ const [minimized, resolved] = await Promise.all([
36
+ minimizeIds.length > 0
37
+ ? autoMinimizeComments(minimizeIds)
38
+ : Promise.resolve({ minimized: [], errors: [] }),
39
+ threadIds.length > 0
40
+ ? autoResolveThreads(threadIds)
41
+ : Promise.resolve({ resolved: [], errors: [] }),
42
+ ]);
43
+ const minimizedSet = new Set(minimized.minimized);
44
+ const resolvedSet = new Set(resolved.resolved);
45
+ const resolvedIds = idsIn(threadIds, resolvedSet);
46
+ const minimizedCommentIds = idsIn(commentIds, minimizedSet);
47
+ const minimizedSummaryIds = idsIn(reviewSummaryIds, minimizedSet);
48
+ const threadsById = new Map(input.batch.reviewThreads.map((thread) => [thread.id, thread]));
49
+ const commentsById = new Map(input.batch.comments.map((comment) => [comment.id, comment]));
50
+ const summariesById = new Map(input.batch.reviewSummaries.map((review) => [review.id, review]));
51
+ const autoResolved = resolvedIds.flatMap((id) => {
52
+ const thread = threadsById.get(id);
53
+ return thread ? [resolvedThread(thread, input.partition.ruleReasons.get(id) ?? [])] : [];
54
+ });
55
+ const autoMinimized = [
56
+ ...minimizedCommentIds.map((id) => minimizedItem("pr-comment", id, commentsById.get(id)?.url, input.partition.ruleReasons.get(id) ?? [])),
57
+ ...minimizedSummaryIds.map((id) => minimizedItem("review-summary", id, summariesById.get(id)?.url, input.partition.ruleReasons.get(id) ?? [])),
58
+ ];
59
+ const autoResolveErrors = [
60
+ ...decorateErrors(minimized.errors, minimizeIds, input.partition.ruleReasons),
61
+ ...decorateErrors(resolved.errors, threadIds, input.partition.ruleReasons),
62
+ ];
63
+ const errorReasons = uniqueReasons([
64
+ ...minimized.errors.flatMap((error) => reasonsForError(error, minimizeIds, input.partition.ruleReasons)),
65
+ ...resolved.errors.flatMap((error) => reasonsForError(error, threadIds, input.partition.ruleReasons)),
66
+ ]);
67
+ const journalError = await journalSuccesses(input, autoResolved, autoMinimized);
68
+ if (journalError)
69
+ autoResolveErrors.push(journalError);
70
+ return {
71
+ threadIds: input.partition.ruleAutoResolveThreadIds.filter((id) => !resolvedSet.has(id)),
72
+ commentIds: input.partition.ruleAutoResolveCommentIds.filter((id) => !minimizedSet.has(id)),
73
+ reviewSummaryIds: input.partition.ruleAutoResolveReviewSummaryIds.filter((id) => !minimizedSet.has(id)),
74
+ autoResolved,
75
+ autoMinimized,
76
+ autoResolveErrors,
77
+ errorReasons,
78
+ };
79
+ }
80
+ async function journalSuccesses(input, autoResolved, autoMinimized) {
81
+ const body = ruleAutoResolveBody({
82
+ threads: autoResolved.length,
83
+ comments: autoMinimized.filter((item) => item.kind === "pr-comment").length,
84
+ reviewSummaries: autoMinimized.filter((item) => item.kind === "review-summary").length,
85
+ });
86
+ if (!body)
87
+ return undefined;
88
+ const ids = [...autoResolved.map((thread) => thread.id), ...autoMinimized.map((item) => item.id)];
89
+ const reasons = successReasons(ids, input.partition.ruleReasons);
90
+ const item = formatRuleAutoResolveJournalItem({
91
+ body,
92
+ reasons,
93
+ ...(input.batch.viewerLogin ? { viewerLogin: input.batch.viewerLogin } : {}),
94
+ urls: [
95
+ ...autoResolved.map((thread) => thread.url),
96
+ ...autoMinimized.map((item) => item.url ?? ""),
97
+ ],
98
+ });
99
+ try {
100
+ await runJournal({
101
+ prNumber: input.prNumber,
102
+ targetRepository: { owner: input.repo.owner, name: input.repo.name },
103
+ rawItem: item,
104
+ dryRun: false,
105
+ });
106
+ return undefined;
107
+ }
108
+ catch (err) {
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ const clause = reasonClause(reasons);
111
+ return clause ? `journal: ${message} ${clause}` : `journal: ${message}`;
112
+ }
113
+ }
@@ -7,11 +7,6 @@ export interface StackPlan {
7
7
  idle?: PollSummaryItem[];
8
8
  instructions: string[];
9
9
  }
10
- /**
11
- * Merge the ready prefix by its highest PR number. `gh stack merge <n>` tries a
12
- * stack number first, but stack numbers come from the repository's issue and
13
- * pull request sequence, so a PR number never names a stack.
14
- */
15
10
  export declare function planPrefixDrain(result: PollSummaryResult, mergeRequested: boolean): StackPlan | undefined;
16
11
  /**
17
12
  * A merge-requested, fully ready stack whose bottom open layer still targets