pr-shepherd 0.48.0 → 0.50.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 (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -2
  3. package/bin/api.d.mts +19 -3
  4. package/bin/api.mjs +57 -8
  5. package/bin/classify/apply.d.mts +2 -0
  6. package/bin/classify/apply.mjs +1 -1
  7. package/bin/cli/default-poll.mjs +1 -0
  8. package/bin/cli/help-command-pages.d.mts +9 -9
  9. package/bin/cli/help-command-pages.mjs +8 -8
  10. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  11. package/bin/cli/help-iterate-poll-pages.mjs +10 -5
  12. package/bin/cli/help-top-page.d.mts +1 -1
  13. package/bin/cli/help-top-page.mjs +5 -3
  14. package/bin/cli/help.d.mts +10 -10
  15. package/bin/cli/iterate-instructions.mjs +9 -2
  16. package/bin/cli/iterate-lean.mjs +11 -0
  17. package/bin/cli/poll-handler.mjs +25 -4
  18. package/bin/cli/poll-summary-emitter.d.mts +4 -0
  19. package/bin/cli/poll-summary-emitter.mjs +22 -0
  20. package/bin/cli/poll-summary-formatter.d.mts +2 -0
  21. package/bin/cli/poll-summary-formatter.mjs +96 -0
  22. package/bin/cli/poll-targets.d.mts +15 -0
  23. package/bin/cli/poll-targets.mjs +114 -0
  24. package/bin/cli/validate-default-args.mjs +1 -4
  25. package/bin/cli-parser.mjs +3 -0
  26. package/bin/commands/iterate/check-instructions.d.mts +2 -8
  27. package/bin/commands/iterate/check-instructions.mjs +2 -11
  28. package/bin/commands/iterate/escalate.mjs +30 -2
  29. package/bin/commands/iterate/fix-code.mjs +62 -20
  30. package/bin/commands/iterate/render.mjs +1 -1
  31. package/bin/commands/iterate/stall.mjs +30 -0
  32. package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
  33. package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
  34. package/bin/commands/poll-summary.d.mts +10 -0
  35. package/bin/commands/poll-summary.mjs +163 -0
  36. package/bin/commands/ready-delay.d.mts +3 -1
  37. package/bin/commands/ready-delay.mjs +3 -2
  38. package/bin/commands/resolve-mutate.mjs +22 -68
  39. package/bin/comments/resolve.d.mts +5 -0
  40. package/bin/comments/resolve.mjs +2 -11
  41. package/bin/github/gql/poll-stack-summary.gql +33 -0
  42. package/bin/github/gql/poll-summary-fragment.gql +198 -0
  43. package/bin/github/poll-summary-checks.d.mts +3 -0
  44. package/bin/github/poll-summary-checks.mjs +61 -0
  45. package/bin/github/poll-summary-projector.d.mts +4 -0
  46. package/bin/github/poll-summary-projector.mjs +81 -0
  47. package/bin/github/poll-summary-raw.d.mts +134 -0
  48. package/bin/github/poll-summary-raw.mjs +1 -0
  49. package/bin/github/poll-summary-review.d.mts +4 -0
  50. package/bin/github/poll-summary-review.mjs +85 -0
  51. package/bin/github/poll-summary-route.d.mts +4 -0
  52. package/bin/github/poll-summary-route.mjs +47 -0
  53. package/bin/github/poll-summary.d.mts +7 -0
  54. package/bin/github/poll-summary.mjs +110 -0
  55. package/bin/github/queries.d.mts +4 -0
  56. package/bin/github/queries.mjs +4 -0
  57. package/bin/mcp/server.mjs +39 -6
  58. package/bin/pr-reference.d.mts +2 -0
  59. package/bin/pr-reference.mjs +4 -0
  60. package/bin/state/fix-attempts.d.mts +3 -4
  61. package/bin/state/fix-attempts.mjs +2 -3
  62. package/bin/types/escalate.d.mts +10 -0
  63. package/bin/types/poll-summary.d.mts +82 -0
  64. package/bin/types/poll-summary.mjs +1 -0
  65. package/bin/types.d.mts +1 -0
  66. package/bin/types.mjs +1 -0
  67. package/package.json +1 -1
  68. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  69. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  70. package/plugins/pr-shepherd/.mcp.json +1 -1
  71. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +5 -10
@@ -0,0 +1,198 @@
1
+ fragment PollSummaryPr on PullRequest {
2
+ number
3
+ title
4
+ url
5
+ state
6
+ isDraft
7
+ viewerCanUpdate
8
+ headRefName
9
+ headRefOid
10
+ baseRefName
11
+ mergeable
12
+ mergeStateStatus
13
+ reviewDecision
14
+ reviewRequests(last: 50) {
15
+ nodes {
16
+ requestedReviewer {
17
+ __typename
18
+ ... on User {
19
+ login
20
+ }
21
+ ... on Bot {
22
+ login
23
+ }
24
+ ... on Mannequin {
25
+ login
26
+ }
27
+ }
28
+ }
29
+ }
30
+ latestReviews(last: 50) {
31
+ nodes {
32
+ state
33
+ author {
34
+ __typename
35
+ login
36
+ }
37
+ }
38
+ }
39
+ isInMergeQueue
40
+ mergeQueueEntry {
41
+ headCommit {
42
+ statusCheckRollup {
43
+ contexts(last: 100) {
44
+ totalCount
45
+ pageInfo {
46
+ hasPreviousPage
47
+ }
48
+ nodes {
49
+ __typename
50
+ ... on CheckRun {
51
+ id
52
+ name
53
+ status
54
+ conclusion
55
+ detailsUrl
56
+ checkSuite {
57
+ workflowRun {
58
+ databaseId
59
+ event
60
+ workflow {
61
+ databaseId
62
+ name
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ... on StatusContext {
68
+ context
69
+ state
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ stack {
77
+ number
78
+ size
79
+ baseRefName
80
+ }
81
+ stackEntry {
82
+ position
83
+ }
84
+ comments(last: 100) {
85
+ totalCount
86
+ pageInfo {
87
+ hasPreviousPage
88
+ }
89
+ nodes {
90
+ id
91
+ body
92
+ isMinimized
93
+ url
94
+ authorAssociation
95
+ author {
96
+ __typename
97
+ login
98
+ }
99
+ }
100
+ }
101
+ reviews(last: 100) {
102
+ totalCount
103
+ pageInfo {
104
+ hasPreviousPage
105
+ }
106
+ nodes {
107
+ id
108
+ body
109
+ state
110
+ isMinimized
111
+ authorAssociation
112
+ author {
113
+ __typename
114
+ login
115
+ }
116
+ }
117
+ }
118
+ reviewThreads(last: 20) {
119
+ totalCount
120
+ pageInfo {
121
+ hasPreviousPage
122
+ }
123
+ nodes {
124
+ id
125
+ isResolved
126
+ isOutdated
127
+ path
128
+ rootComments: comments(first: 1) {
129
+ nodes {
130
+ id
131
+ body
132
+ url
133
+ viewerDidAuthor
134
+ authorAssociation
135
+ author {
136
+ __typename
137
+ login
138
+ }
139
+ }
140
+ }
141
+ comments(last: 5) {
142
+ totalCount
143
+ pageInfo {
144
+ hasPreviousPage
145
+ }
146
+ nodes {
147
+ id
148
+ body
149
+ url
150
+ authorAssociation
151
+ viewerDidAuthor
152
+ author {
153
+ __typename
154
+ login
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ commits(last: 1) {
161
+ nodes {
162
+ commit {
163
+ statusCheckRollup {
164
+ contexts(last: 100) {
165
+ totalCount
166
+ pageInfo {
167
+ hasPreviousPage
168
+ }
169
+ nodes {
170
+ __typename
171
+ ... on CheckRun {
172
+ id
173
+ name
174
+ status
175
+ conclusion
176
+ detailsUrl
177
+ checkSuite {
178
+ workflowRun {
179
+ databaseId
180
+ event
181
+ workflow {
182
+ databaseId
183
+ name
184
+ }
185
+ }
186
+ }
187
+ }
188
+ ... on StatusContext {
189
+ context
190
+ state
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,3 @@
1
+ import type { PollSummaryChecks } from "../types.mts";
2
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
3
+ export declare function summarizePollSummaryChecks(raw: RawSummaryPr): PollSummaryChecks;
@@ -0,0 +1,61 @@
1
+ import { classifyChecks } from "../checks/classify.mjs";
2
+ export function summarizePollSummaryChecks(raw) {
3
+ const rollups = [
4
+ raw.commits.nodes[0]?.commit.statusCheckRollup,
5
+ raw.mergeQueueEntry?.headCommit?.statusCheckRollup,
6
+ ].filter((rollup) => rollup !== null && rollup !== undefined);
7
+ const checks = rollups.flatMap((rollup, rollupIndex) => rollup.contexts.nodes.map((context) => {
8
+ if (context.__typename === "StatusContext") {
9
+ return {
10
+ name: context.context,
11
+ status: context.state === "PENDING" || context.state === "EXPECTED"
12
+ ? "IN_PROGRESS"
13
+ : "COMPLETED",
14
+ conclusion: context.state === "SUCCESS"
15
+ ? "SUCCESS"
16
+ : context.state === "FAILURE" || context.state === "ERROR"
17
+ ? "FAILURE"
18
+ : null,
19
+ source: "status_context",
20
+ detailsUrl: "",
21
+ event: null,
22
+ runId: null,
23
+ ...(rollupIndex === 1 && { scope: "merge_group" }),
24
+ };
25
+ }
26
+ const run = context.checkSuite?.workflowRun;
27
+ return {
28
+ id: context.id,
29
+ name: context.name,
30
+ status: context.status,
31
+ conclusion: context.conclusion,
32
+ source: "check_run",
33
+ detailsUrl: context.detailsUrl ?? "",
34
+ event: run?.event ?? null,
35
+ runId: run?.databaseId != null ? String(run.databaseId) : null,
36
+ ...(run?.workflow?.name && { workflowName: run.workflow.name }),
37
+ ...(run?.workflow?.databaseId != null && {
38
+ workflowId: String(run.workflow.databaseId),
39
+ }),
40
+ ...(rollupIndex === 1 && { scope: "merge_group" }),
41
+ };
42
+ }));
43
+ const counts = {};
44
+ for (const check of classifyChecks(checks, { additionalRelevantEvents: ["merge_group"] })) {
45
+ const key = {
46
+ passed: "passing",
47
+ failing: "failing",
48
+ in_progress: "inProgress",
49
+ skipped: "skipped",
50
+ filtered: "filtered",
51
+ ignored: "ignored",
52
+ superseded: "superseded",
53
+ }[check.category];
54
+ counts[key] = (counts[key] ?? 0) + 1;
55
+ }
56
+ const summary = counts;
57
+ if (rollups.some((rollup) => rollup.contexts.pageInfo.hasPreviousPage)) {
58
+ summary.incomplete = true;
59
+ }
60
+ return summary;
61
+ }
@@ -0,0 +1,4 @@
1
+ import type { PollSummaryCommandOptions, PollSummaryItem } from "../types.mts";
2
+ import type { RepoInfo } from "./client.mts";
3
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
4
+ export declare function summarizePollSummaryPr(raw: RawSummaryPr, repo: RepoInfo, opts: PollSummaryCommandOptions, viewerCanAdminister?: boolean): Promise<PollSummaryItem>;
@@ -0,0 +1,81 @@
1
+ import { loadConfig } from "../config/load.mjs";
2
+ import { updateReadyDelay } from "../commands/ready-delay.mjs";
3
+ import { formatPrUrl } from "../pr-reference.mjs";
4
+ import { buildPrShepherdCommand } from "../cli/runner.mjs";
5
+ import { loadSeenMap } from "../state/seen-comments.mjs";
6
+ import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
7
+ import { summarizePollSummaryReview } from "./poll-summary-review.mjs";
8
+ import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
9
+ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = false) {
10
+ const repoName = `${repo.owner}/${repo.name}`;
11
+ const seen = await loadSeenMap({ owner: repo.owner, repo: repo.name, pr: raw.number });
12
+ const checks = summarizePollSummaryChecks(raw);
13
+ const review = await summarizePollSummaryReview(raw, seen, viewerCanAdminister);
14
+ const blockingReviewerInProgress = detectBlockingReviewer(raw);
15
+ let { action, reasons } = routePollSummary(raw, checks, review, opts);
16
+ let remainingSeconds;
17
+ if (raw.isDraft && blockingReviewerInProgress && action === "mark_ready") {
18
+ action = "wait";
19
+ reasons = ["blocking-reviewer-in-progress"];
20
+ }
21
+ const appearsReady = reasons.includes("appears-ready");
22
+ const readyDelaySeconds = opts.readyDelaySeconds ?? (loadConfig().watch?.readyDelayMinutes ?? 10) * 60;
23
+ const readyState = await updateReadyDelay(raw.number, appearsReady, readyDelaySeconds, repo.owner, repo.name, { retainElapsed: true });
24
+ if (appearsReady && !readyState.shouldCancel) {
25
+ action = "wait";
26
+ reasons = ["ready-delay"];
27
+ remainingSeconds = readyState.remainingSeconds;
28
+ }
29
+ const stack = raw.stack
30
+ ? {
31
+ number: raw.stack.number,
32
+ size: raw.stack.size,
33
+ position: raw.stackEntry?.position ?? 0,
34
+ baseRefName: raw.stack.baseRefName,
35
+ }
36
+ : undefined;
37
+ return {
38
+ pr: raw.number,
39
+ repo: repoName,
40
+ title: raw.title,
41
+ url: raw.url || formatPrUrl(repoName, raw.number),
42
+ action,
43
+ reasons,
44
+ state: normalizePollSummaryState(raw.state),
45
+ mergeable: raw.mergeable,
46
+ mergeStateStatus: raw.mergeStateStatus,
47
+ ...(raw.reviewDecision && { reviewDecision: raw.reviewDecision }),
48
+ headRefName: raw.headRefName,
49
+ headRefOid: raw.headRefOid,
50
+ baseRefName: raw.baseRefName,
51
+ ...(raw.isDraft && { isDraft: true }),
52
+ ...(raw.isInMergeQueue && { isInMergeQueue: true }),
53
+ ...(blockingReviewerInProgress && { blockingReviewerInProgress: true }),
54
+ ...(remainingSeconds !== undefined && { remainingSeconds }),
55
+ ...(Object.keys(checks).length > 0 && { checks }),
56
+ ...(Object.keys(review).length > 0 && { review }),
57
+ ...(stack && { stack }),
58
+ ...(!["wait", "cancel"].includes(action) && {
59
+ pollCommand: buildPollCommand(repoName, raw.number, opts),
60
+ }),
61
+ };
62
+ }
63
+ function detectBlockingReviewer(raw) {
64
+ const prefixes = (loadConfig().mergeStatus?.blockingReviewerLogins ?? []).map((login) => login.toLowerCase());
65
+ const matches = (author) => author !== null && prefixes.some((prefix) => author.login.toLowerCase().startsWith(prefix));
66
+ return ((raw.reviewRequests?.nodes ?? []).some((request) => matches(request.requestedReviewer)) ||
67
+ (raw.latestReviews?.nodes ?? []).some((review) => review.state === "PENDING" && matches(review.author)));
68
+ }
69
+ function buildPollCommand(repo, pr, opts) {
70
+ const args = [formatPrUrl(repo, pr), "--until-terminal"];
71
+ if (opts.merge)
72
+ args.push("--merge");
73
+ if (opts.readyDelaySeconds !== undefined)
74
+ args.push("--ready-delay", `${opts.readyDelaySeconds}s`);
75
+ if (opts.stallTimeoutSeconds !== undefined) {
76
+ args.push("--stall-timeout", `${opts.stallTimeoutSeconds}s`);
77
+ }
78
+ if (opts.noAutoMarkReady)
79
+ args.push("--no-auto-mark-ready");
80
+ return buildPrShepherdCommand(args).text;
81
+ }
@@ -0,0 +1,134 @@
1
+ export interface RawAuthor {
2
+ __typename?: string;
3
+ login: string;
4
+ }
5
+ interface RawSummaryComment {
6
+ id: string;
7
+ body: string;
8
+ isMinimized: boolean;
9
+ viewerDidAuthor?: boolean;
10
+ authorAssociation?: string;
11
+ url?: string;
12
+ author: RawAuthor | null;
13
+ }
14
+ interface SummaryConnection<T> {
15
+ totalCount: number;
16
+ pageInfo: {
17
+ hasPreviousPage: boolean;
18
+ };
19
+ nodes: T[];
20
+ }
21
+ type RawCheckContext = {
22
+ __typename: "CheckRun";
23
+ id?: string;
24
+ name: string;
25
+ status: string;
26
+ conclusion: string | null;
27
+ detailsUrl?: string;
28
+ checkSuite: {
29
+ workflowRun: {
30
+ databaseId?: string | number;
31
+ event: string;
32
+ workflow?: {
33
+ name: string;
34
+ databaseId: string | number;
35
+ } | null;
36
+ } | null;
37
+ } | null;
38
+ } | {
39
+ __typename: "StatusContext";
40
+ context: string;
41
+ state: string;
42
+ };
43
+ interface RawCheckRollup {
44
+ contexts: SummaryConnection<RawCheckContext>;
45
+ }
46
+ export interface RawSummaryPr {
47
+ number: number;
48
+ title: string;
49
+ url: string;
50
+ state: string;
51
+ isDraft: boolean;
52
+ viewerCanUpdate: boolean;
53
+ headRefName: string;
54
+ headRefOid: string;
55
+ baseRefName: string;
56
+ mergeable: string;
57
+ mergeStateStatus: string;
58
+ reviewDecision: string | null;
59
+ reviewRequests?: {
60
+ nodes: Array<{
61
+ requestedReviewer: RawAuthor | null;
62
+ }>;
63
+ };
64
+ latestReviews?: {
65
+ nodes: Array<{
66
+ state: string;
67
+ author: RawAuthor | null;
68
+ }>;
69
+ };
70
+ isInMergeQueue: boolean;
71
+ mergeQueueEntry: {
72
+ headCommit: {
73
+ statusCheckRollup: RawCheckRollup | null;
74
+ } | null;
75
+ } | null;
76
+ stack: {
77
+ number: number;
78
+ size: number;
79
+ baseRefName: string;
80
+ } | null;
81
+ stackEntry: {
82
+ position: number;
83
+ } | null;
84
+ comments: SummaryConnection<RawSummaryComment>;
85
+ reviews: SummaryConnection<RawSummaryComment & {
86
+ state: string;
87
+ }>;
88
+ reviewThreads: SummaryConnection<{
89
+ id: string;
90
+ isResolved: boolean;
91
+ isOutdated: boolean;
92
+ path: string | null;
93
+ rootComments?: {
94
+ nodes: RawSummaryComment[];
95
+ };
96
+ comments: SummaryConnection<RawSummaryComment>;
97
+ }>;
98
+ commits: {
99
+ nodes: Array<{
100
+ commit: {
101
+ statusCheckRollup: RawCheckRollup | null;
102
+ };
103
+ }>;
104
+ };
105
+ }
106
+ export interface RawExplicitResponse {
107
+ repository: ({
108
+ viewerCanAdminister: boolean;
109
+ } & Record<string, RawSummaryPr | boolean | null>) | null;
110
+ }
111
+ export interface RawStackResponse {
112
+ repository: {
113
+ viewerCanAdminister: boolean;
114
+ pullRequest: {
115
+ stack: {
116
+ id: string;
117
+ number: number;
118
+ size: number;
119
+ baseRefName: string;
120
+ entries: {
121
+ pageInfo: {
122
+ hasNextPage: boolean;
123
+ endCursor: string | null;
124
+ };
125
+ nodes: Array<{
126
+ position: number;
127
+ pullRequest: RawSummaryPr | null;
128
+ }>;
129
+ };
130
+ } | null;
131
+ } | null;
132
+ } | null;
133
+ }
134
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { type SeenMarker } from "../state/seen-comments.mts";
2
+ import type { PollSummaryReview } from "../types.mts";
3
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
4
+ export declare function summarizePollSummaryReview(raw: RawSummaryPr, seen: Map<string, SeenMarker>, viewerCanAdminister: boolean): Promise<PollSummaryReview>;
@@ -0,0 +1,85 @@
1
+ import { normalizeBotUsernames } from "../comments/authors.mjs";
2
+ import { applyRules } from "../classify/apply.mjs";
3
+ import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
4
+ import { loadConfig } from "../config/load.mjs";
5
+ import { getEffectiveCwd } from "../execution-context.mjs";
6
+ import { classifyItem } from "../state/seen-comments.mjs";
7
+ const THREAD_COMMENT_SEPARATOR = "\n\n--- thread comment ---\n\n";
8
+ export async function summarizePollSummaryReview(raw, seen, viewerCanAdminister) {
9
+ const config = loadConfig();
10
+ const bots = normalizeBotUsernames(config.botUsernames);
11
+ const rules = await loadRules(discoverRuleFiles(getEffectiveCwd()));
12
+ let actionable = 0;
13
+ let incomplete = raw.comments.pageInfo.hasPreviousPage ||
14
+ raw.reviews.pageInfo.hasPreviousPage ||
15
+ raw.reviewThreads.pageInfo.hasPreviousPage;
16
+ for (const comment of raw.comments.nodes) {
17
+ if (!comment.isMinimized &&
18
+ !isSuppressed(rules, "pr-comment", comment) &&
19
+ classifyItem(comment.id, comment.body, seen) !== "unchanged")
20
+ actionable += 1;
21
+ }
22
+ const latestStateByAuthor = new Map((raw.latestReviews?.nodes ?? []).flatMap((review) => review.author ? [[review.author.login.toLowerCase(), review.state]] : []));
23
+ for (const review of raw.reviews.nodes) {
24
+ if (review.state === "COMMENTED") {
25
+ if (review.body.trim() !== "" &&
26
+ !review.isMinimized &&
27
+ !isSuppressed(rules, "review-summary", review) &&
28
+ classifyItem(review.id, review.body, seen) !== "unchanged")
29
+ actionable += 1;
30
+ continue;
31
+ }
32
+ if (review.state !== "CHANGES_REQUESTED")
33
+ continue;
34
+ const login = review.author?.login.toLowerCase();
35
+ if (login && latestStateByAuthor.get(login) === "APPROVED")
36
+ continue;
37
+ if (isSuppressed(rules, "changes-requested", review))
38
+ continue;
39
+ const isBot = review.author?.__typename === "Bot" || bots.has(review.author?.login.toLowerCase() ?? "");
40
+ if ((isBot && viewerCanAdminister) ||
41
+ classifyItem(review.id, review.body, seen) !== "unchanged")
42
+ actionable += 1;
43
+ }
44
+ for (const thread of raw.reviewThreads.nodes) {
45
+ if (thread.comments.pageInfo.hasPreviousPage)
46
+ incomplete = true;
47
+ const root = thread.rootComments?.nodes[0] ?? thread.comments.nodes[0];
48
+ const transcript = thread.comments.nodes
49
+ .map((comment) => comment.body)
50
+ .join(THREAD_COMMENT_SEPARATOR);
51
+ if (isSuppressed(rules, "review-thread", root, thread.id, thread.path, transcript))
52
+ continue;
53
+ const repeatable = root?.viewerDidAuthor === true ||
54
+ root?.author?.__typename === "Bot" ||
55
+ bots.has(root?.author?.login.toLowerCase() ?? "");
56
+ const unseen = classifyItem(thread.id, transcript, seen) !== "unchanged";
57
+ if ((!thread.isResolved && (repeatable || unseen)) ||
58
+ ((thread.isResolved || thread.isOutdated) && unseen))
59
+ actionable += 1;
60
+ }
61
+ return {
62
+ ...(raw.comments.totalCount > 0 && { comments: raw.comments.totalCount }),
63
+ ...(raw.reviews.totalCount > 0 && { reviews: raw.reviews.totalCount }),
64
+ ...(raw.reviewThreads.totalCount > 0 && { threads: raw.reviewThreads.totalCount }),
65
+ ...(actionable > 0 && { actionable }),
66
+ ...(incomplete && { incomplete: true }),
67
+ };
68
+ }
69
+ function isSuppressed(rules, kind, raw, id = raw?.id ?? "unknown", path, body = raw?.body ?? "") {
70
+ const item = {
71
+ kind,
72
+ id,
73
+ author: raw?.author?.login ?? "unknown",
74
+ authorType: raw?.author?.__typename === "Bot"
75
+ ? "Bot"
76
+ : raw?.author?.__typename === "User"
77
+ ? "User"
78
+ : "Unknown",
79
+ ...(raw?.authorAssociation && { authorAssociation: raw.authorAssociation }),
80
+ body,
81
+ ...(raw?.url && { url: raw.url }),
82
+ ...(kind === "review-thread" && { path }),
83
+ };
84
+ return applyRules(rules, item).suppress === true;
85
+ }
@@ -0,0 +1,4 @@
1
+ import type { PollSummaryChecks, PollSummaryCommandOptions, PollSummaryItem, PollSummaryReview } from "../types.mts";
2
+ import type { RawSummaryPr } from "./poll-summary-raw.mts";
3
+ export declare function routePollSummary(raw: RawSummaryPr, checks: PollSummaryChecks, review: PollSummaryReview, opts: PollSummaryCommandOptions): Pick<PollSummaryItem, "action" | "reasons">;
4
+ export declare function normalizePollSummaryState(state: string): PollSummaryItem["state"];
@@ -0,0 +1,47 @@
1
+ import { loadConfig } from "../config/load.mjs";
2
+ export function routePollSummary(raw, checks, review, opts) {
3
+ const actions = loadConfig().actions;
4
+ const state = normalizePollSummaryState(raw.state);
5
+ if (state === "MERGED" || state === "CLOSED") {
6
+ return { action: "cancel", reasons: [state.toLowerCase()] };
7
+ }
8
+ if (raw.mergeable === "CONFLICTING" || raw.mergeStateStatus === "DIRTY") {
9
+ return { action: "fix_code", reasons: ["merge-conflicts"] };
10
+ }
11
+ if ((checks.failing ?? 0) > 0)
12
+ return { action: "fix_code", reasons: ["failing-checks"] };
13
+ if ((review.actionable ?? 0) > 0) {
14
+ if (opts.merge && raw.isInMergeQueue && actions.workWhileQueued !== true) {
15
+ return { action: "wait", reasons: ["review-work-deferred-while-queued"] };
16
+ }
17
+ return { action: "fix_code", reasons: ["review-work"] };
18
+ }
19
+ if ((checks.inProgress ?? 0) > 0 ||
20
+ raw.mergeable === "UNKNOWN" ||
21
+ raw.mergeStateStatus === "UNKNOWN" ||
22
+ raw.mergeStateStatus === "BEHIND" ||
23
+ raw.mergeStateStatus === "BLOCKED" ||
24
+ raw.mergeStateStatus === "HAS_HOOKS") {
25
+ return { action: "wait", reasons: ["pending-or-unknown"] };
26
+ }
27
+ if (raw.isDraft) {
28
+ if (opts.noAutoMarkReady || actions.autoMarkReady === false) {
29
+ return { action: "wait", reasons: ["draft-auto-mark-ready-disabled"] };
30
+ }
31
+ return raw.viewerCanUpdate
32
+ ? { action: "mark_ready", reasons: ["draft-appears-ready"] }
33
+ : { action: "escalate", reasons: ["mark-ready-authorization-required"] };
34
+ }
35
+ if (opts.merge && raw.isInMergeQueue) {
36
+ return { action: "wait", reasons: ["already-in-merge-queue"] };
37
+ }
38
+ if (opts.merge && raw.stack) {
39
+ return { action: "fix_code", reasons: ["authoritative-poll-required"] };
40
+ }
41
+ if (opts.merge && !raw.stack)
42
+ return { action: "merge", reasons: ["appears-ready"] };
43
+ return { action: "cancel", reasons: ["appears-ready"] };
44
+ }
45
+ export function normalizePollSummaryState(state) {
46
+ return state === "OPEN" || state === "CLOSED" || state === "MERGED" ? state : "UNKNOWN";
47
+ }
@@ -0,0 +1,7 @@
1
+ import type { PollSummaryCommandOptions, PollSummaryItem, PollSummarySelection } from "../types.mts";
2
+ import { type RepoInfo } from "./client.mts";
3
+ export interface FetchedPollSummary {
4
+ selection: PollSummarySelection;
5
+ prs: PollSummaryItem[];
6
+ }
7
+ export declare function fetchPollSummary(opts: PollSummaryCommandOptions, repo: RepoInfo): Promise<FetchedPollSummary>;