pr-shepherd 0.46.5 → 0.46.7
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -3
- package/bin/api.d.mts +1 -1
- package/bin/checks/triage.mjs +12 -1
- package/bin/cli/body-truncate.d.mts +12 -0
- package/bin/cli/body-truncate.mjs +123 -0
- package/bin/cli/fix-formatter-extra.d.mts +2 -1
- package/bin/cli/fix-formatter-extra.mjs +18 -3
- package/bin/cli/fix-formatter.d.mts +3 -1
- package/bin/cli/fix-formatter.mjs +42 -16
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +1 -1
- package/bin/cli/iterate-checks-formatter.mjs +8 -6
- package/bin/cli/iterate-formatter.mjs +10 -9
- package/bin/cli/iterate-lean.mjs +5 -0
- package/bin/cli/iterate-merge-formatter.d.mts +3 -1
- package/bin/cli/iterate-merge-formatter.mjs +15 -0
- package/bin/cli/list-formatters.d.mts +4 -3
- package/bin/cli/list-formatters.mjs +22 -14
- package/bin/commands/check-annotations.d.mts +15 -0
- package/bin/commands/check-annotations.mjs +23 -1
- package/bin/commands/check.d.mts +1 -0
- package/bin/commands/check.mjs +73 -40
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/classify.d.mts +2 -2
- package/bin/commands/iterate/classify.mjs +2 -2
- package/bin/commands/iterate/escalate.mjs +9 -0
- package/bin/commands/iterate/fix-code.mjs +11 -9
- package/bin/commands/iterate/fix-instruction-threads.d.mts +14 -0
- package/bin/commands/iterate/fix-instruction-threads.mjs +32 -0
- package/bin/commands/iterate/index.mjs +20 -7
- package/bin/commands/iterate/merge-state.d.mts +7 -2
- package/bin/commands/iterate/merge-state.mjs +68 -2
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +12 -23
- package/bin/commands/iterate/thread-mutation-routing.d.mts +10 -2
- package/bin/commands/iterate/thread-mutation-routing.mjs +26 -14
- package/bin/commands/resolve-mutate.mjs +18 -7
- package/bin/comments/thread-resolve-policy.d.mts +6 -0
- package/bin/comments/thread-resolve-policy.mjs +9 -0
- package/bin/comments/thread-visibility.d.mts +2 -1
- package/bin/comments/thread-visibility.mjs +12 -5
- package/bin/config/load.d.mts +18 -0
- package/bin/config/load.mjs +27 -1
- package/bin/config.json +6 -3
- package/bin/mcp/server.mjs +18 -6
- package/bin/types/escalate.d.mts +3 -2
- package/bin/types/iterate.d.mts +10 -0
- package/bin/types/merge-queue.d.mts +16 -0
- package/package.json +9 -6
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +17 -4
|
@@ -35,9 +35,10 @@ export declare function renderThreadBullet(t: ThreadBulletInput, opts?: {
|
|
|
35
35
|
renderSuggestion?: boolean;
|
|
36
36
|
noBody?: boolean;
|
|
37
37
|
suppressEditedMarker?: boolean;
|
|
38
|
+
verbose?: boolean;
|
|
38
39
|
}): string;
|
|
39
|
-
export declare function renderThreadConversation(t: ThreadBulletInput): string;
|
|
40
|
-
export declare function blockquote(body: string): string;
|
|
40
|
+
export declare function renderThreadConversation(t: ThreadBulletInput, verbose?: boolean): string;
|
|
41
|
+
export declare function blockquote(body: string, maxChars?: number, url?: string): string;
|
|
41
42
|
export declare function renderCommentBullet(c: {
|
|
42
43
|
id: string;
|
|
43
44
|
url?: string;
|
|
@@ -74,5 +75,5 @@ export declare function renderReviewListSection(heading: string, items: {
|
|
|
74
75
|
* Threads that also appear in resolutionOnlyIds have their body suppressed
|
|
75
76
|
* (already shown in `## Review threads to resolve`).
|
|
76
77
|
*/
|
|
77
|
-
export declare function buildFirstLookBullets(firstLookThreads: FirstLookThread[], resolutionOnlyIds: Set<string>, firstLookComments: FirstLookComment[]): string[];
|
|
78
|
+
export declare function buildFirstLookBullets(firstLookThreads: FirstLookThread[], resolutionOnlyIds: Set<string>, firstLookComments: FirstLookComment[], verbose?: boolean): string[];
|
|
78
79
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { renderLineRange, renderSuggestionBlock } from "./suggestion-renderer.mjs";
|
|
2
2
|
import { threadComments } from "../threads/transcript.mjs";
|
|
3
|
+
import { truncateBody, BODY_TRUNCATE_MAX_CHARS, NESTED_BODY_TRUNCATE_MAX_CHARS, } from "./body-truncate.mjs";
|
|
3
4
|
const BODY_PREVIEW_MAX = 100;
|
|
4
5
|
export function renderAuthor(author, authorType, authorAssociation, viewerDidAuthor) {
|
|
5
6
|
return [`@${author}`, authorType, authorAssociation, viewerDidAuthor ? "viewer-authored" : null]
|
|
@@ -41,48 +42,54 @@ export function renderThreadBullet(t, opts = {}) {
|
|
|
41
42
|
}
|
|
42
43
|
const parts = [bulletLine];
|
|
43
44
|
if (!opts.noBody) {
|
|
44
|
-
parts.push(renderThreadCommentBullets(t));
|
|
45
|
+
parts.push(renderThreadCommentBullets(t, opts.verbose));
|
|
45
46
|
}
|
|
46
47
|
if (t.suggestion && opts.renderSuggestion) {
|
|
47
48
|
parts.push(renderSuggestionBlock(t.suggestion));
|
|
48
49
|
}
|
|
49
50
|
return parts.join("\n");
|
|
50
51
|
}
|
|
51
|
-
export function renderThreadConversation(t) {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
export function renderThreadConversation(t, verbose = false) {
|
|
53
|
+
const topCap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
|
|
54
|
+
if (!t.comments || t.comments.length === 0) {
|
|
55
|
+
return blockquote(t.body, topCap, t.url);
|
|
56
|
+
}
|
|
57
|
+
const nestedCap = verbose ? undefined : NESTED_BODY_TRUNCATE_MAX_CHARS;
|
|
54
58
|
return threadComments(t)
|
|
55
|
-
.map((c) => {
|
|
59
|
+
.map((c, i) => {
|
|
56
60
|
const heading = c.id
|
|
57
61
|
? c.url
|
|
58
62
|
? `#### [commentId=${c.id}](${c.url}) (${renderAuthor(c.author, c.authorType, c.authorAssociation, c.viewerDidAuthor)})`
|
|
59
63
|
: `#### \`commentId=${c.id}\` (${renderAuthor(c.author, c.authorType, c.authorAssociation, c.viewerDidAuthor)})`
|
|
60
64
|
: `#### (${renderAuthor(c.author, c.authorType, c.authorAssociation, c.viewerDidAuthor)})`;
|
|
61
|
-
return `${heading}\n\n${blockquote(c.body)}`;
|
|
65
|
+
return `${heading}\n\n${blockquote(c.body, i === 0 ? topCap : nestedCap, c.url)}`;
|
|
62
66
|
})
|
|
63
67
|
.join("\n\n");
|
|
64
68
|
}
|
|
65
|
-
export function blockquote(body) {
|
|
66
|
-
|
|
69
|
+
export function blockquote(body, maxChars, url) {
|
|
70
|
+
const text = maxChars === undefined ? body : truncateBody(body, maxChars, url);
|
|
71
|
+
return text
|
|
67
72
|
.replace(/\r\n/g, "\n")
|
|
68
73
|
.split("\n")
|
|
69
74
|
.map((line) => (line === "" ? ">" : `> ${line}`))
|
|
70
75
|
.join("\n");
|
|
71
76
|
}
|
|
72
|
-
function renderThreadCommentBullets(t) {
|
|
77
|
+
function renderThreadCommentBullets(t, verbose = false) {
|
|
78
|
+
const topCap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
|
|
79
|
+
const nestedCap = verbose ? undefined : NESTED_BODY_TRUNCATE_MAX_CHARS;
|
|
73
80
|
return threadComments(t)
|
|
74
|
-
.map((c) => {
|
|
81
|
+
.map((c, i) => {
|
|
75
82
|
const link = c.url ? ` [↗](${c.url})` : "";
|
|
76
83
|
const id = c.id ? `\`commentId=${c.id}\`` : "comment";
|
|
77
84
|
return [
|
|
78
85
|
` - ${id}${link} (${renderAuthor(c.author, c.authorType, c.authorAssociation, c.viewerDidAuthor)})`,
|
|
79
|
-
indentBlockquote(c.body, " "),
|
|
86
|
+
indentBlockquote(c.body, " ", i === 0 ? topCap : nestedCap, c.url),
|
|
80
87
|
].join("\n");
|
|
81
88
|
})
|
|
82
89
|
.join("\n");
|
|
83
90
|
}
|
|
84
|
-
function indentBlockquote(body, indent) {
|
|
85
|
-
return blockquote(body)
|
|
91
|
+
function indentBlockquote(body, indent, maxChars, url) {
|
|
92
|
+
return blockquote(body, maxChars, url)
|
|
86
93
|
.split("\n")
|
|
87
94
|
.map((line) => `${indent}${line}`)
|
|
88
95
|
.join("\n");
|
|
@@ -118,13 +125,14 @@ export function renderReviewListSection(heading, items) {
|
|
|
118
125
|
* Threads that also appear in resolutionOnlyIds have their body suppressed
|
|
119
126
|
* (already shown in `## Review threads to resolve`).
|
|
120
127
|
*/
|
|
121
|
-
export function buildFirstLookBullets(firstLookThreads, resolutionOnlyIds, firstLookComments) {
|
|
128
|
+
export function buildFirstLookBullets(firstLookThreads, resolutionOnlyIds, firstLookComments, verbose = false) {
|
|
122
129
|
const bullets = [];
|
|
123
130
|
for (const t of firstLookThreads) {
|
|
124
131
|
bullets.push(renderThreadBullet(t, {
|
|
125
132
|
statusTag: renderFirstLookStatusTag(t),
|
|
126
133
|
noBody: resolutionOnlyIds.has(t.id),
|
|
127
134
|
suppressEditedMarker: true,
|
|
135
|
+
verbose,
|
|
128
136
|
}));
|
|
129
137
|
}
|
|
130
138
|
for (const c of firstLookComments) {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { type AnnotationCacheOptions } from "../github/check-annotations.mts";
|
|
2
2
|
import type { CheckAnnotation, ClassifiedCheck, ShepherdReport, TriagedCheck } from "../types.mts";
|
|
3
3
|
export declare function checksWithActionableAnnotations(report: ShepherdReport): TriagedCheck[];
|
|
4
|
+
/**
|
|
5
|
+
* True when GitHub itself is already acting on this PR regardless of Shepherd (a failing
|
|
6
|
+
* check, an unseen check-run annotation, or a hard merge conflict) — the categories that
|
|
7
|
+
* `commands/iterate/index.mts` never defers while a PR is queued. Shared with `check.mts` so
|
|
8
|
+
* the seen-marker suppression gate there can't drift from the actual iterate dispatch
|
|
9
|
+
* decision (a queued PR with a failing check still renders review items via `fix_code`; their
|
|
10
|
+
* seen markers must not be suppressed just because the PR happens to be queued).
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasCheckDrivenActionableWork(checks: {
|
|
13
|
+
failing: TriagedCheck[];
|
|
14
|
+
passing: ClassifiedCheck[];
|
|
15
|
+
skipped: ClassifiedCheck[];
|
|
16
|
+
filtered: ClassifiedCheck[];
|
|
17
|
+
ignored?: TriagedCheck[];
|
|
18
|
+
}, mergeStatusValue: string): boolean;
|
|
4
19
|
export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
5
20
|
passing: ClassifiedCheck[];
|
|
6
21
|
failing: TriagedCheck[];
|
|
@@ -2,6 +2,9 @@ import { fetchCheckRunAnnotations, } from "../github/check-annotations.mjs";
|
|
|
2
2
|
function shouldFetchCheckAnnotations(check) {
|
|
3
3
|
return check.id != null && check.status === "COMPLETED" && check.hasAnnotations === true;
|
|
4
4
|
}
|
|
5
|
+
function hasActionableAnnotation(check) {
|
|
6
|
+
return check.conclusion !== "SUCCESS" && (check.annotations?.length ?? 0) > 0;
|
|
7
|
+
}
|
|
5
8
|
export function checksWithActionableAnnotations(report) {
|
|
6
9
|
return [
|
|
7
10
|
...report.checks.failing,
|
|
@@ -9,7 +12,26 @@ export function checksWithActionableAnnotations(report) {
|
|
|
9
12
|
...report.checks.skipped,
|
|
10
13
|
...report.checks.filtered,
|
|
11
14
|
...(report.checks.ignored ?? []),
|
|
12
|
-
].filter(
|
|
15
|
+
].filter(hasActionableAnnotation);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when GitHub itself is already acting on this PR regardless of Shepherd (a failing
|
|
19
|
+
* check, an unseen check-run annotation, or a hard merge conflict) — the categories that
|
|
20
|
+
* `commands/iterate/index.mts` never defers while a PR is queued. Shared with `check.mts` so
|
|
21
|
+
* the seen-marker suppression gate there can't drift from the actual iterate dispatch
|
|
22
|
+
* decision (a queued PR with a failing check still renders review items via `fix_code`; their
|
|
23
|
+
* seen markers must not be suppressed just because the PR happens to be queued).
|
|
24
|
+
*/
|
|
25
|
+
export function hasCheckDrivenActionableWork(checks, mergeStatusValue) {
|
|
26
|
+
return (checks.failing.length > 0 ||
|
|
27
|
+
[
|
|
28
|
+
...checks.failing,
|
|
29
|
+
...checks.passing,
|
|
30
|
+
...checks.skipped,
|
|
31
|
+
...checks.filtered,
|
|
32
|
+
...(checks.ignored ?? []),
|
|
33
|
+
].some(hasActionableAnnotation) ||
|
|
34
|
+
mergeStatusValue === "CONFLICTS");
|
|
13
35
|
}
|
|
14
36
|
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber, cacheOpts) {
|
|
15
37
|
const candidates = [
|
package/bin/commands/check.d.mts
CHANGED
package/bin/commands/check.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { deriveMergeStatus } from "../merge-status/derive.mjs";
|
|
|
7
7
|
import { loadConfig } from "../config/load.mjs";
|
|
8
8
|
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
9
9
|
import { computeStatus } from "./check-status.mjs";
|
|
10
|
-
import { annotationMarkerBody, attachAndMergeCheckAnnotations } from "./check-annotations.mjs";
|
|
10
|
+
import { annotationMarkerBody, attachAndMergeCheckAnnotations, hasCheckDrivenActionableWork, } from "./check-annotations.mjs";
|
|
11
11
|
import { buildTerminalReport } from "./check-terminal-report.mjs";
|
|
12
12
|
import { isBlockedByFilteredCheck, refreshReadyMergeability, refreshUnknownMergeability, } from "./ready-mergeability.mjs";
|
|
13
13
|
import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
|
|
@@ -17,7 +17,7 @@ import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, }
|
|
|
17
17
|
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
18
18
|
import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
|
|
19
19
|
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
20
|
-
import { buildThreadMutationRouting,
|
|
20
|
+
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./iterate/thread-mutation-routing.mjs";
|
|
21
21
|
import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
22
22
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
23
23
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
@@ -47,9 +47,16 @@ export async function runCheck(opts) {
|
|
|
47
47
|
const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
|
|
48
48
|
const classifiedPrChecks = classifyChecks(allChecks);
|
|
49
49
|
const latestRemoval = batchData.latestMergeQueueRemoval;
|
|
50
|
+
// `timelineItems(last: 1, ...)` returns the single most recent removal regardless of age, so
|
|
51
|
+
// a PR removed from the queue once, long ago, and never re-added keeps returning that same
|
|
52
|
+
// historical event forever. When GitHub omits the removed queue commit for that old event
|
|
53
|
+
// (e.g. after the synthetic commit is garbage collected), freshness is unverifiable — treat
|
|
54
|
+
// it as stale/updated rather than as still current, so Shepherd doesn't escalate
|
|
55
|
+
// `merge-queue-removed` permanently on data it can no longer check. The raw removal fields
|
|
56
|
+
// still render in the merge-queue header regardless of this flag.
|
|
50
57
|
const headUpdatedAfterRemoval = Boolean(latestRemoval &&
|
|
51
|
-
latestRemoval.beforeCommitParentOids
|
|
52
|
-
|
|
58
|
+
(!latestRemoval.beforeCommitParentOids ||
|
|
59
|
+
!latestRemoval.beforeCommitParentOids.includes(batchData.headRefOid)));
|
|
53
60
|
const queueRawChecks = batchData.isInMergeQueue
|
|
54
61
|
? (batchData.mergeQueueChecks ?? [])
|
|
55
62
|
: latestRemoval && !headUpdatedAfterRemoval
|
|
@@ -83,17 +90,14 @@ export async function runCheck(opts) {
|
|
|
83
90
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate.minimizeComments, botUsernames);
|
|
84
91
|
const deniedRuleAutoResolveThreadIds = new Set(partition.ruleAutoResolveThreadIds.filter((id) => batchData.reviewThreads.find((thread) => thread.id === id)?.viewerCanResolve !== true));
|
|
85
92
|
const visibleThreadCandidates = batchData.reviewThreads.filter((t) => !partition.suppressedThreadIds.has(t.id) || deniedRuleAutoResolveThreadIds.has(t.id));
|
|
86
|
-
const
|
|
93
|
+
const resolveOtherHumanThreads = config.iterate.resolveOtherHumanThreads ?? "none";
|
|
94
|
+
const threadMutationRouting = buildThreadMutationRouting(visibleThreadCandidates, botUsernames, partition.ruleAutoResolveThreadIds, resolveOtherHumanThreads);
|
|
87
95
|
const replyThreadIds = new Set(threadMutationRouting.replyThreadIds);
|
|
88
96
|
const resolveThreadIds = new Set(threadMutationRouting.resolveThreadIds);
|
|
89
97
|
const repeatableThreadIds = new Set(visibleThreadCandidates
|
|
90
|
-
.filter((thread) => (thread
|
|
91
|
-
thread.line !== null &&
|
|
92
|
-
(!replyThreadIds.has(thread.id) || thread.viewerCanReply === true) &&
|
|
93
|
-
(!resolveThreadIds.has(thread.id) || thread.viewerCanResolve === true)) ||
|
|
94
|
-
canResolveOutdatedBotWithoutLocation(thread, botUsernames))
|
|
98
|
+
.filter((thread) => threadHasAuthorizedMutation(thread, replyThreadIds, resolveThreadIds))
|
|
95
99
|
.map((thread) => thread.id));
|
|
96
|
-
const threadVisibility = classifyThreadVisibility(visibleThreadCandidates, seenMap, botUsernames, repeatableThreadIds);
|
|
100
|
+
const threadVisibility = classifyThreadVisibility(visibleThreadCandidates, seenMap, botUsernames, repeatableThreadIds, resolveOtherHumanThreads);
|
|
97
101
|
const firstLookComments = minimizedCommentCandidates.flatMap((c) => {
|
|
98
102
|
const cls = classifyItem(c.id, c.body, seenMap);
|
|
99
103
|
if (cls === "unchanged")
|
|
@@ -118,6 +122,51 @@ export async function runCheck(opts) {
|
|
|
118
122
|
}
|
|
119
123
|
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames, batchData.viewerAuthorization?.viewerCanAdminister === true);
|
|
120
124
|
const approvedReviewVisibility = classifyReviewsForDisplay(batchData.approvedReviews, seenMap);
|
|
125
|
+
const changesRequestedReviews = changesRequestedReviewVisibility.visible;
|
|
126
|
+
const visibleChangesRequestedIds = new Set(changesRequestedReviews.map((review) => review.id));
|
|
127
|
+
const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((review) => {
|
|
128
|
+
if (partition.suppressedChangesRequestedIds.has(review.id))
|
|
129
|
+
return false;
|
|
130
|
+
const isBot = !isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames);
|
|
131
|
+
return (!isBot ||
|
|
132
|
+
batchData.viewerAuthorization?.viewerCanAdminister === true ||
|
|
133
|
+
visibleChangesRequestedIds.has(review.id));
|
|
134
|
+
}).length;
|
|
135
|
+
const approvedReviews = approvedReviewVisibility.visible;
|
|
136
|
+
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount);
|
|
137
|
+
// Resolve any pending mergeability refresh (and the resulting MERGED/CLOSED short-circuit)
|
|
138
|
+
// before deciding what to persist below — deferWhileQueued must see the same final,
|
|
139
|
+
// possibly-refreshed mergeStatus that commands/iterate/index.mts acts on, not the pre-refresh
|
|
140
|
+
// snapshot. A conflict newly discovered by this REST read is exactly the kind of check-driven
|
|
141
|
+
// signal that keeps a queued PR out of the deferral path.
|
|
142
|
+
if (status === "READY" && !didRefreshMergeability) {
|
|
143
|
+
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount);
|
|
144
|
+
batchData = refreshed.batchData;
|
|
145
|
+
mergeStatus = refreshed.mergeStatus;
|
|
146
|
+
status = refreshed.status;
|
|
147
|
+
if (mergeStatus.state === "MERGED" || mergeStatus.state === "CLOSED") {
|
|
148
|
+
return buildTerminalReport(prNumber, repo, batchData, mergeStatus, mergeStatus.state);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Mirrors the deferral gate in commands/iterate/index.mts exactly (via the shared
|
|
152
|
+
// hasCheckDrivenActionableWork helper, so the two can't drift): when this tick's non-CI
|
|
153
|
+
// actionable work (threads/comments/review summaries/changes-requested) will be held back
|
|
154
|
+
// because the PR is queued, none of it was actually shown to the agent this tick —
|
|
155
|
+
// persisting seen markers for it now would make it silently vanish from every later tick
|
|
156
|
+
// once it's no longer "new" or "edited", even after the PR leaves the queue. A queued PR
|
|
157
|
+
// that also has check-driven work (failing checks, actionable annotations, conflicts) is
|
|
158
|
+
// NOT deferred — index.mts still renders these items via fix_code — so this must stay false
|
|
159
|
+
// in that case too, or their seen markers would be suppressed while actually being shown.
|
|
160
|
+
const deferWhileQueued = opts.merge === true &&
|
|
161
|
+
batchData.isInMergeQueue === true &&
|
|
162
|
+
config.actions.workWhileQueued !== true &&
|
|
163
|
+
!hasCheckDrivenActionableWork({
|
|
164
|
+
failing: merged.failing,
|
|
165
|
+
passing: merged.passing,
|
|
166
|
+
skipped: merged.skipped,
|
|
167
|
+
filtered: merged.filtered,
|
|
168
|
+
ignored: merged.ignored,
|
|
169
|
+
}, mergeStatus.status);
|
|
121
170
|
if (opts.persistSeen !== false) {
|
|
122
171
|
const successfulAnnotations = [
|
|
123
172
|
...merged.passing,
|
|
@@ -127,19 +176,24 @@ export async function runCheck(opts) {
|
|
|
127
176
|
]
|
|
128
177
|
.filter((check) => check.conclusion === "SUCCESS")
|
|
129
178
|
.flatMap((check) => check.annotations ?? []);
|
|
179
|
+
const deferredMarkSeen = deferWhileQueued
|
|
180
|
+
? []
|
|
181
|
+
: [
|
|
182
|
+
...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
183
|
+
...threadVisibility.toMarkSeen.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
184
|
+
...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
185
|
+
...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
|
|
186
|
+
...changesRequestedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
187
|
+
...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
188
|
+
];
|
|
130
189
|
await Promise.allSettled([
|
|
131
190
|
...successfulAnnotations.map((a) => markSeen(stateKey, a.id, annotationMarkerBody(a))),
|
|
132
|
-
...
|
|
133
|
-
...threadVisibility.toMarkSeen.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
134
|
-
...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
135
|
-
...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
|
|
136
|
-
...changesRequestedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
137
|
-
...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
191
|
+
...deferredMarkSeen,
|
|
138
192
|
...batchData.comments
|
|
139
|
-
.filter((c) => partition.suppressedCommentIds.has(c.id))
|
|
193
|
+
.filter((c) => partition.suppressedCommentIds.has(c.id) && !deniedRuleAutoResolveCommentIds.has(c.id))
|
|
140
194
|
.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
141
195
|
...batchData.reviewThreads
|
|
142
|
-
.filter((t) => partition.suppressedThreadIds.has(t.id))
|
|
196
|
+
.filter((t) => partition.suppressedThreadIds.has(t.id) && !deniedRuleAutoResolveThreadIds.has(t.id))
|
|
143
197
|
.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
144
198
|
...batchData.reviewSummaries
|
|
145
199
|
.filter((r) => partition.suppressedReviewSummaryIds.has(r.id) &&
|
|
@@ -163,27 +217,6 @@ export async function runCheck(opts) {
|
|
|
163
217
|
...authorizedRuleAutoResolveThreadIds,
|
|
164
218
|
...[...deniedRuleAutoResolveThreadIds].filter((id) => visibleMutationThreadIds.has(id)),
|
|
165
219
|
];
|
|
166
|
-
const changesRequestedReviews = changesRequestedReviewVisibility.visible;
|
|
167
|
-
const visibleChangesRequestedIds = new Set(changesRequestedReviews.map((review) => review.id));
|
|
168
|
-
const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((review) => {
|
|
169
|
-
if (partition.suppressedChangesRequestedIds.has(review.id))
|
|
170
|
-
return false;
|
|
171
|
-
const isBot = !isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames);
|
|
172
|
-
return (!isBot ||
|
|
173
|
-
batchData.viewerAuthorization?.viewerCanAdminister === true ||
|
|
174
|
-
visibleChangesRequestedIds.has(review.id));
|
|
175
|
-
}).length;
|
|
176
|
-
const approvedReviews = approvedReviewVisibility.visible;
|
|
177
|
-
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount);
|
|
178
|
-
if (status === "READY" && !didRefreshMergeability) {
|
|
179
|
-
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount);
|
|
180
|
-
batchData = refreshed.batchData;
|
|
181
|
-
mergeStatus = refreshed.mergeStatus;
|
|
182
|
-
status = refreshed.status;
|
|
183
|
-
if (mergeStatus.state === "MERGED" || mergeStatus.state === "CLOSED") {
|
|
184
|
-
return buildTerminalReport(prNumber, repo, batchData, mergeStatus, mergeStatus.state);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
220
|
const blockedByFilteredCheck = isBlockedByFilteredCheck(mergeStatus, verdict);
|
|
188
221
|
const queueCommit = batchData.isInMergeQueue
|
|
189
222
|
? batchData.mergeQueueEntry?.headCommitOid
|
|
@@ -58,7 +58,7 @@ export function buildResolveCommandInstruction(resolveCommand) {
|
|
|
58
58
|
return [];
|
|
59
59
|
const instructions = [];
|
|
60
60
|
if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
|
|
61
|
-
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked
|
|
61
|
+
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked thread that is still being resolved is emitted resolve-only, not for another reply.");
|
|
62
62
|
}
|
|
63
63
|
if (resolveCommand.requiresHeadSha) {
|
|
64
64
|
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentThread, Review, ResolveCommand, AgentCheck, ReviewThread, ViewerAuthorization } from "../../types.mts";
|
|
2
2
|
import { type NormalizedBotUsernames } from "../../comments/authors.mts";
|
|
3
|
-
import type { MinimizeCommentsPolicy } from "../../config/load.mts";
|
|
3
|
+
import type { MinimizeCommentsPolicy, ResolveOtherHumanThreads } from "../../config/load.mts";
|
|
4
4
|
export declare function classifyReviewSummaries(summaries: {
|
|
5
5
|
firstLook: Review[];
|
|
6
6
|
seen: Review[];
|
|
@@ -12,7 +12,7 @@ export declare function classifyReviewSummaries(summaries: {
|
|
|
12
12
|
editedSummaries: Review[];
|
|
13
13
|
surfacedApprovals: Review[];
|
|
14
14
|
};
|
|
15
|
-
export declare function buildResolveCommand(threads: AgentThread[], resolutionOnlyThreads: ReviewThread[], allCommentIds: string[], reviews: Review[], checks: AgentCheck[], prReference: string | number, botUsernames?: NormalizedBotUsernames, ruleAutoResolveThreadIds?: string[], viewerAuthorization?: ViewerAuthorization, authorizationThreads?: ReviewThread[]): {
|
|
15
|
+
export declare function buildResolveCommand(threads: AgentThread[], resolutionOnlyThreads: ReviewThread[], allCommentIds: string[], reviews: Review[], checks: AgentCheck[], prReference: string | number, botUsernames?: NormalizedBotUsernames, ruleAutoResolveThreadIds?: string[], viewerAuthorization?: ViewerAuthorization, authorizationThreads?: ReviewThread[], resolveOtherHumanThreads?: ResolveOtherHumanThreads): {
|
|
16
16
|
resolveCommand: ResolveCommand;
|
|
17
17
|
resolveOnlyCommand?: ResolveCommand;
|
|
18
18
|
};
|
|
@@ -58,9 +58,9 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
|
|
|
58
58
|
surfacedApprovals: approvals,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prReference, botUsernames = new Set(), ruleAutoResolveThreadIds = [], viewerAuthorization, authorizationThreads = []) {
|
|
61
|
+
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prReference, botUsernames = new Set(), ruleAutoResolveThreadIds = [], viewerAuthorization, authorizationThreads = [], resolveOtherHumanThreads = "none") {
|
|
62
62
|
const allThreads = [...threads, ...resolutionOnlyThreads];
|
|
63
|
-
const routed = buildThreadMutationRouting(allThreads, botUsernames, ruleAutoResolveThreadIds);
|
|
63
|
+
const routed = buildThreadMutationRouting(allThreads, botUsernames, ruleAutoResolveThreadIds, resolveOtherHumanThreads);
|
|
64
64
|
const canReply = new Set(authorizationThreads
|
|
65
65
|
.filter((thread) => thread.viewerCanReply === true)
|
|
66
66
|
.map((thread) => thread.id));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
|
+
import { inlineCode } from "../../util/markdown.mjs";
|
|
2
3
|
function renderEscalateAuthor(item) {
|
|
3
4
|
return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
|
|
4
5
|
}
|
|
@@ -176,6 +177,10 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
176
177
|
if (removal.beforeCommitOid)
|
|
177
178
|
lines.push(`- queue commit: \`${removal.beforeCommitOid}\``);
|
|
178
179
|
}
|
|
180
|
+
if (escalate.stack) {
|
|
181
|
+
const s = escalate.stack;
|
|
182
|
+
lines.push("", "## GitHub stack", "", `- layer: \`${s.position}\` of \`${s.size}\` in stack \`${s.number}\``, `- stack base: ${inlineCode(s.baseRefName)}`);
|
|
183
|
+
}
|
|
179
184
|
if (escalate.authorization && escalate.authorization.length > 0) {
|
|
180
185
|
lines.push("");
|
|
181
186
|
lines.push("## Authorization");
|
|
@@ -199,6 +204,10 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
199
204
|
return lines.join("\n");
|
|
200
205
|
}
|
|
201
206
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
207
|
+
if (triggers.includes("stacked-pr")) {
|
|
208
|
+
const selector = detail ?? "<pr>";
|
|
209
|
+
return `This PR belongs to a GitHub stack, so Shepherd will not emit a merge command. \`gh pr merge\` targets the PR's own base branch — for a mid-stack layer that is the unmerged parent branch, not the stack's base — and auto-merge is unsupported on stacked PRs. Merge from the GitHub stack UI, or run \`gh stack merge --squash ${selector}\` (requires the \`github/gh-stack\` extension — run \`gh extension install github/gh-stack\` first if it's not installed), which lands this PR and every unmerged layer below it.`;
|
|
210
|
+
}
|
|
202
211
|
if (triggers.includes("check-follow-up-unavailable")) {
|
|
203
212
|
return "One or more failing checks have no autonomous follow-up available. Use the displayed conclusion, run or URL, and included evidence to handle them manually.";
|
|
204
213
|
}
|
|
@@ -5,7 +5,7 @@ import { toAgentThread, toAgentComment, toAgentChecks } from "../../reporters/ag
|
|
|
5
5
|
import { hashBody, markSeen } from "../../state/seen-comments.mjs";
|
|
6
6
|
import { checkEscalateTriggers, validateBaseBranch, buildEscalateSuggestion, buildEscalateHumanMessage, } from "./escalate.mjs";
|
|
7
7
|
import { buildResolveCommand } from "./classify.mjs";
|
|
8
|
-
import { buildThreadMutationRouting,
|
|
8
|
+
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./thread-mutation-routing.mjs";
|
|
9
9
|
import { buildFixInstructions } from "./render.mjs";
|
|
10
10
|
import { applyStallGuard } from "./stall.mjs";
|
|
11
11
|
import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
@@ -55,9 +55,8 @@ export async function handleFixCode(ctx) {
|
|
|
55
55
|
const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
|
|
56
56
|
const allThreads = [...report.threads.actionable, ...report.threads.resolutionOnly];
|
|
57
57
|
const ruleAutoResolveIds = new Set(ruleAutoResolveThreadIds ?? []);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
]);
|
|
58
|
+
const resolveOtherHumanThreads = loadConfig().iterate.resolveOtherHumanThreads ?? "none";
|
|
59
|
+
const routedThreadMutations = buildThreadMutationRouting(allThreads, botUsernames, [...ruleAutoResolveIds], resolveOtherHumanThreads);
|
|
61
60
|
const replyIdSet = new Set(routedThreadMutations.replyThreadIds);
|
|
62
61
|
const resolveIdSet = new Set(routedThreadMutations.resolveThreadIds);
|
|
63
62
|
const unauthorizedReplies = allThreads.filter((thread) => replyIdSet.has(thread.id) && thread.viewerCanReply !== true);
|
|
@@ -65,7 +64,10 @@ export async function handleFixCode(ctx) {
|
|
|
65
64
|
const unauthorizedDismissals = report.changesRequestedReviews.filter((review) => (!isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames)) &&
|
|
66
65
|
report.viewerAuthorization?.viewerCanAdminister !== true);
|
|
67
66
|
const skippedThreadIds = new Set([...unauthorizedReplies, ...unauthorizedResolves].map((thread) => thread.id));
|
|
68
|
-
const
|
|
67
|
+
const mutationActionableThreads = report.threads.actionable.filter((thread) => !skippedThreadIds.has(thread.id) &&
|
|
68
|
+
((thread.path !== null && thread.line !== null) ||
|
|
69
|
+
threadHasAuthorizedMutation(thread, replyIdSet, resolveIdSet)));
|
|
70
|
+
const retryableActionableThreads = mutationActionableThreads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
69
71
|
const protectedRuns = [];
|
|
70
72
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
71
73
|
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha, retryableActionableThreads);
|
|
@@ -168,7 +170,7 @@ export async function handleFixCode(ctx) {
|
|
|
168
170
|
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !skippedDismissalIds.has(review.id));
|
|
169
171
|
const resolutionOnlyThreadsForWork = resolutionOnlyThreads.filter((thread) => !skippedThreadIds.has(thread.id) &&
|
|
170
172
|
((thread.path !== null && thread.line !== null) ||
|
|
171
|
-
|
|
173
|
+
threadHasAuthorizedMutation(thread, replyIdSet, resolveIdSet)));
|
|
172
174
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
173
175
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
174
176
|
const { behindBaseHint } = loadConfig().iterate;
|
|
@@ -223,9 +225,9 @@ export async function handleFixCode(ctx) {
|
|
|
223
225
|
}
|
|
224
226
|
// Push access to the PR head branch is a usage precondition. Build review mutations for
|
|
225
227
|
// conflict ticks normally so the caller can push and complete the same fix_code cycle.
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(
|
|
228
|
+
const mutationActionableIds = new Set(mutationActionableThreads.map((thread) => thread.id));
|
|
229
|
+
const mutationAgentThreads = threads.filter((thread) => mutationActionableIds.has(thread.id));
|
|
230
|
+
const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(mutationAgentThreads, resolutionOnlyThreadsForWork, allCommentIds, changesRequestedReviewsForWork, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads, resolveOtherHumanThreads);
|
|
229
231
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
230
232
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
231
233
|
// prediction that the agent *will* push. Located resolution-only threads retain that guard;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentThread, ResolveCommand } from "../../types.mts";
|
|
2
|
+
export declare function partitionFixThreads(threads: AgentThread[], resolveCommand: ResolveCommand, resolveOnlyCommand?: ResolveCommand): {
|
|
3
|
+
locatedThreads: AgentThread[];
|
|
4
|
+
unlocatedMutatedThreads: AgentThread[];
|
|
5
|
+
unlocatedThreads: AgentThread[];
|
|
6
|
+
};
|
|
7
|
+
export declare function reviewSectionRefs(input: {
|
|
8
|
+
hasReviewThreads: boolean;
|
|
9
|
+
hasUnlocatedSkipThreads: boolean;
|
|
10
|
+
hasActionableComments: boolean;
|
|
11
|
+
hasFailingChecks: boolean;
|
|
12
|
+
hasAnnotations: boolean;
|
|
13
|
+
hasChangesRequested: boolean;
|
|
14
|
+
}): string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function mutatedThreadIdSet(resolveCommand, resolveOnlyCommand) {
|
|
2
|
+
return new Set([
|
|
3
|
+
...(resolveCommand.replyThreadIds ?? []),
|
|
4
|
+
...(resolveCommand.resolveThreadIds ?? []),
|
|
5
|
+
...(resolveOnlyCommand?.replyThreadIds ?? []),
|
|
6
|
+
...(resolveOnlyCommand?.resolveThreadIds ?? []),
|
|
7
|
+
]);
|
|
8
|
+
}
|
|
9
|
+
export function partitionFixThreads(threads, resolveCommand, resolveOnlyCommand) {
|
|
10
|
+
const mutatedThreadIds = mutatedThreadIdSet(resolveCommand, resolveOnlyCommand);
|
|
11
|
+
return {
|
|
12
|
+
locatedThreads: threads.filter((thread) => thread.path !== null && thread.line !== null),
|
|
13
|
+
unlocatedMutatedThreads: threads.filter((thread) => (thread.path === null || thread.line === null) && mutatedThreadIds.has(thread.id)),
|
|
14
|
+
unlocatedThreads: threads.filter((thread) => (thread.path === null || thread.line === null) && !mutatedThreadIds.has(thread.id)),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function reviewSectionRefs(input) {
|
|
18
|
+
const sections = [];
|
|
19
|
+
if (input.hasReviewThreads)
|
|
20
|
+
sections.push("`## Review threads`");
|
|
21
|
+
if (input.hasUnlocatedSkipThreads)
|
|
22
|
+
sections.push("`## Unlocated review threads (logged once — no mutation)`");
|
|
23
|
+
if (input.hasActionableComments)
|
|
24
|
+
sections.push("`## Actionable comments`");
|
|
25
|
+
if (input.hasFailingChecks)
|
|
26
|
+
sections.push("`## Failing checks`");
|
|
27
|
+
if (input.hasAnnotations)
|
|
28
|
+
sections.push("`## Check annotations`");
|
|
29
|
+
if (input.hasChangesRequested)
|
|
30
|
+
sections.push("`## Changes-requested reviews`");
|
|
31
|
+
return sections;
|
|
32
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import { runCheck } from "../check.mjs";
|
|
2
3
|
import { updateReadyDelay } from "../ready-delay.mjs";
|
|
3
4
|
import { getCurrentPrNumber } from "../../github/client.mjs";
|
|
@@ -10,8 +11,8 @@ import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
|
10
11
|
import { handleFixCode } from "./fix-code.mjs";
|
|
11
12
|
import { normalizeBotUsernames } from "../../comments/authors.mjs";
|
|
12
13
|
import { autoMinimizeComments } from "../../comments/resolve.mjs";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { hasCheckDrivenActionableWork } from "../check-annotations.mjs";
|
|
15
|
+
import { buildReadyMergeOutcome, handleActiveMergeState } from "./merge-state.mjs";
|
|
15
16
|
import { buildIterateBase } from "./base.mjs";
|
|
16
17
|
import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
17
18
|
import { withIterateApiUsage } from "./run.mjs";
|
|
@@ -73,9 +74,7 @@ async function runIterateCore(opts) {
|
|
|
73
74
|
(report.comments.minimizeIds?.length ?? 0) > 0 ||
|
|
74
75
|
report.comments.firstLook.length > 0 ||
|
|
75
76
|
report.changesRequestedReviews.length > 0 ||
|
|
76
|
-
report.checks.
|
|
77
|
-
checksWithActionableAnnotations(report).length > 0 ||
|
|
78
|
-
report.mergeStatus.status === "CONFLICTS" ||
|
|
77
|
+
hasCheckDrivenActionableWork(report.checks, report.mergeStatus.status) ||
|
|
79
78
|
reviewSummaryIds.length > 0 ||
|
|
80
79
|
firstLookSummaries.length > 0 ||
|
|
81
80
|
editedSummaries.length > 0 ||
|
|
@@ -85,7 +84,16 @@ async function runIterateCore(opts) {
|
|
|
85
84
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName);
|
|
86
85
|
const base = buildIterateBase(report, readyState);
|
|
87
86
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
88
|
-
|
|
87
|
+
// Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
|
|
88
|
+
// GitHub itself is already acting on — the queue will eject the PR for these regardless of
|
|
89
|
+
// what Shepherd does, so they always surface immediately. Only review threads/comments/
|
|
90
|
+
// changes-requested reviews/review summaries — the categories that would otherwise cause a
|
|
91
|
+
// Shepherd-initiated push while the PR sits safely in the queue — are eligible for deferral.
|
|
92
|
+
const checkDrivenActionableWork = hasCheckDrivenActionableWork(report.checks, report.mergeStatus.status);
|
|
93
|
+
const deferWhileQueued = opts.merge === true &&
|
|
94
|
+
report.mergeQueue?.inQueue === true &&
|
|
95
|
+
config.actions.workWhileQueued !== true;
|
|
96
|
+
if (hasActionableWork && !(deferWhileQueued && !checkDrivenActionableWork)) {
|
|
89
97
|
return handleFixCode({
|
|
90
98
|
base,
|
|
91
99
|
report,
|
|
@@ -110,6 +118,11 @@ async function runIterateCore(opts) {
|
|
|
110
118
|
base,
|
|
111
119
|
report,
|
|
112
120
|
stallKey,
|
|
121
|
+
reviewSummaryIds,
|
|
122
|
+
firstLookSummaries,
|
|
123
|
+
editedSummaries,
|
|
124
|
+
surfacedApprovals,
|
|
125
|
+
minimizeApprovals: config.iterate.minimizeApprovals,
|
|
113
126
|
});
|
|
114
127
|
if (mergeStateResult)
|
|
115
128
|
return mergeStateResult;
|
|
@@ -121,7 +134,7 @@ async function runIterateCore(opts) {
|
|
|
121
134
|
return markReadyResult;
|
|
122
135
|
if (readyState.shouldCancel) {
|
|
123
136
|
await clearStallState(stallKey);
|
|
124
|
-
const mergeResult =
|
|
137
|
+
const mergeResult = buildReadyMergeOutcome(opts.merge, true, base, report);
|
|
125
138
|
if (mergeResult)
|
|
126
139
|
return mergeResult;
|
|
127
140
|
const cancelNote = blockedCancelNote(base);
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import type { IterateResult, IterateResultBase, ShepherdReport } from "../../types.mts";
|
|
1
|
+
import type { IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
|
|
2
2
|
type StallKey = {
|
|
3
3
|
owner: string;
|
|
4
4
|
repo: string;
|
|
5
5
|
pr: number;
|
|
6
6
|
};
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function buildReadyMergeOutcome(enabled: boolean | undefined, readyElapsed: boolean, base: IterateResultBase, report: ShepherdReport): IterateResult | null;
|
|
8
8
|
export declare function handleActiveMergeState(input: {
|
|
9
9
|
enabled: boolean | undefined;
|
|
10
10
|
active: boolean;
|
|
11
11
|
base: IterateResultBase;
|
|
12
12
|
report: ShepherdReport;
|
|
13
13
|
stallKey: StallKey;
|
|
14
|
+
reviewSummaryIds: string[];
|
|
15
|
+
firstLookSummaries: Review[];
|
|
16
|
+
editedSummaries: Review[];
|
|
17
|
+
surfacedApprovals: Review[];
|
|
18
|
+
minimizeApprovals: boolean;
|
|
14
19
|
}): Promise<IterateResult | null>;
|
|
15
20
|
export {};
|