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
|
@@ -2,9 +2,34 @@ import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
|
2
2
|
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
3
|
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
4
4
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
|
-
|
|
5
|
+
/** A stacked PR's merge is human-only: `gh pr merge` targets the PR's own base, which for a
|
|
6
|
+
* mid-stack layer is an unmerged parent branch, and auto-merge is unsupported on stacks. */
|
|
7
|
+
function buildStackedEscalateResult(base, report, stack) {
|
|
8
|
+
const escalateBase = {
|
|
9
|
+
triggers: ["stacked-pr"],
|
|
10
|
+
unresolvedThreads: [],
|
|
11
|
+
ambiguousComments: [],
|
|
12
|
+
changesRequestedReviews: [],
|
|
13
|
+
stack,
|
|
14
|
+
suggestion: buildEscalateSuggestion(["stacked-pr"], String(report.pr)),
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
...base,
|
|
18
|
+
action: "escalate",
|
|
19
|
+
escalate: {
|
|
20
|
+
...escalateBase,
|
|
21
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
|
|
22
|
+
merge: true,
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function buildReadyMergeOutcome(enabled, readyElapsed, base, report) {
|
|
6
28
|
if (!enabled || !readyElapsed || report.mergeStatus.isDraft)
|
|
7
29
|
return null;
|
|
30
|
+
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
31
|
+
if (stack)
|
|
32
|
+
return buildStackedEscalateResult(base, report, stack);
|
|
8
33
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
9
34
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
10
35
|
return {
|
|
@@ -19,14 +44,55 @@ export function buildReadyMergeResult(enabled, readyElapsed, base, report) {
|
|
|
19
44
|
}),
|
|
20
45
|
};
|
|
21
46
|
}
|
|
47
|
+
/** Raw counts of non-CI actionable work held back for one queued-PR wait tick. Omitted (all zero) when empty. */
|
|
48
|
+
function buildDeferredWork(input) {
|
|
49
|
+
const { report, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, minimizeApprovals, } = input;
|
|
50
|
+
// These buckets are not disjoint (e.g. an unresolved outdated thread is both
|
|
51
|
+
// `resolutionOnly` and `firstLook`; an eligible-to-minimize comment/summary is both
|
|
52
|
+
// `actionable`/`firstLook` and queued in `minimizeIds`/`reviewSummaryIds`) — dedupe by ID.
|
|
53
|
+
const threadIds = new Set([
|
|
54
|
+
...report.threads.actionable.map((t) => t.id),
|
|
55
|
+
...report.threads.resolutionOnly.map((t) => t.id),
|
|
56
|
+
...report.threads.firstLook.map((t) => t.id),
|
|
57
|
+
...(report.threads.ruleAutoResolveIds ?? []),
|
|
58
|
+
]);
|
|
59
|
+
const commentIds = new Set([
|
|
60
|
+
...report.comments.actionable.map((c) => c.id),
|
|
61
|
+
...(report.comments.minimizeIds ?? []),
|
|
62
|
+
...report.comments.firstLook.map((c) => c.id),
|
|
63
|
+
]);
|
|
64
|
+
const reviewSummaryIdSet = new Set([
|
|
65
|
+
...reviewSummaryIds,
|
|
66
|
+
...firstLookSummaries.map((r) => r.id),
|
|
67
|
+
...editedSummaries.map((r) => r.id),
|
|
68
|
+
...(minimizeApprovals ? surfacedApprovals.map((r) => r.id) : []),
|
|
69
|
+
]);
|
|
70
|
+
const deferredWork = {
|
|
71
|
+
threads: threadIds.size,
|
|
72
|
+
comments: commentIds.size,
|
|
73
|
+
changesRequestedReviews: report.changesRequestedReviews.length,
|
|
74
|
+
reviewSummaries: reviewSummaryIdSet.size,
|
|
75
|
+
};
|
|
76
|
+
const total = deferredWork.threads +
|
|
77
|
+
deferredWork.comments +
|
|
78
|
+
deferredWork.changesRequestedReviews +
|
|
79
|
+
deferredWork.reviewSummaries;
|
|
80
|
+
return total > 0 ? deferredWork : undefined;
|
|
81
|
+
}
|
|
22
82
|
export async function handleActiveMergeState(input) {
|
|
23
83
|
const { enabled, active, base, report, stallKey } = input;
|
|
84
|
+
const inQueue = report.mergeQueue?.inQueue === true;
|
|
24
85
|
if (enabled && active) {
|
|
25
86
|
await clearStallState(stallKey);
|
|
87
|
+
// Only the queued case ever holds back non-CI actionable work (see the `deferWhileQueued`
|
|
88
|
+
// gate in index.mts, which requires `inQueue === true`); an ordinary active auto-merge
|
|
89
|
+
// request with no queue membership never defers anything, so it never carries counts here.
|
|
90
|
+
const deferredWork = inQueue ? buildDeferredWork(input) : undefined;
|
|
26
91
|
return {
|
|
27
92
|
...base,
|
|
28
93
|
action: "wait",
|
|
29
|
-
|
|
94
|
+
...(deferredWork && { deferredWork }),
|
|
95
|
+
log: inQueue
|
|
30
96
|
? `WAIT: PR #${report.pr} is in the merge queue`
|
|
31
97
|
: `WAIT: PR #${report.pr} has auto-merge enabled`,
|
|
32
98
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, FirstLookThread, FirstLookComment, ReviewThread } from "../../types.mts";
|
|
2
2
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
3
3
|
export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
4
|
-
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number,
|
|
4
|
+
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, _cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], _inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
5
5
|
isBehind?: boolean, viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean): string[];
|
|
@@ -3,6 +3,7 @@ import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintI
|
|
|
3
3
|
import { SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE, buildShepherdJournalInstruction, } from "../shepherd-journal.mjs";
|
|
4
4
|
import { isFailingAgentCheck } from "../../checks/conclusions.mjs";
|
|
5
5
|
import { buildCommitSuggestionInstruction } from "../commit-suggestion-instruction.mjs";
|
|
6
|
+
import { partitionFixThreads, reviewSectionRefs } from "./fix-instruction-threads.mjs";
|
|
6
7
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
7
8
|
export function renderResolveCommand(rc) {
|
|
8
9
|
const parts = [...rc.argv];
|
|
@@ -10,11 +11,10 @@ export function renderResolveCommand(rc) {
|
|
|
10
11
|
parts.push("--require-sha", "$HEAD_SHA");
|
|
11
12
|
return renderShellCommand(parts);
|
|
12
13
|
}
|
|
13
|
-
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference,
|
|
14
|
+
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, _cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], _inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
14
15
|
isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
15
16
|
const instructions = [];
|
|
16
|
-
const locatedThreads
|
|
17
|
-
const unlocatedThreads = threads.filter((thread) => thread.path === null || thread.line === null);
|
|
17
|
+
const { locatedThreads, unlocatedMutatedThreads, unlocatedThreads } = partitionFixThreads(threads, resolveCommand, resolveOnlyCommand);
|
|
18
18
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
19
19
|
const repeatedWorkflowBranchRecoveryInstructions = buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasExhaustedWorkflowRerun, {
|
|
20
20
|
isBehind,
|
|
@@ -29,20 +29,14 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
29
29
|
actionableComments.length > 0;
|
|
30
30
|
// Start with interpretation. The agent decides what raw feedback warrants a code change.
|
|
31
31
|
if (hasNonConflictHints) {
|
|
32
|
-
const actionableSections =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
actionableSections.push("`## Failing checks`");
|
|
41
|
-
if (hasAnnotations) {
|
|
42
|
-
actionableSections.push("`## Check annotations`");
|
|
43
|
-
}
|
|
44
|
-
if (changesRequestedReviews.length > 0)
|
|
45
|
-
actionableSections.push("`## Changes-requested reviews`");
|
|
32
|
+
const actionableSections = reviewSectionRefs({
|
|
33
|
+
hasReviewThreads: locatedThreads.length > 0 || unlocatedMutatedThreads.length > 0,
|
|
34
|
+
hasUnlocatedSkipThreads: unlocatedThreads.length > 0,
|
|
35
|
+
hasActionableComments: actionableComments.length > 0,
|
|
36
|
+
hasFailingChecks: failingChecks.length > 0,
|
|
37
|
+
hasAnnotations,
|
|
38
|
+
hasChangesRequested: changesRequestedReviews.length > 0,
|
|
39
|
+
});
|
|
46
40
|
const sectionRef = actionableSections.length > 0 ? `under ${actionableSections.join(", ")}` : "above";
|
|
47
41
|
instructions.push(`Review each item ${sectionRef} and decide whether it needs a code change.`);
|
|
48
42
|
}
|
|
@@ -65,10 +59,6 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
65
59
|
if (unlocatedThreads.length > 0) {
|
|
66
60
|
instructions.push("Acknowledge each item under `## Unlocated review threads (logged once — no mutation)`. Shepherd cannot route a code fix or review mutation without a path and line; the unchanged item will be skipped on later ticks.");
|
|
67
61
|
}
|
|
68
|
-
// GitHub exposes no exact viewer capability for workflow-run cancellation, so the
|
|
69
|
-
// informational run lists never produce a cancellation recommendation.
|
|
70
|
-
void inProgressRunIds;
|
|
71
|
-
void cancelledCount;
|
|
72
62
|
const hasSuggestions = locatedThreads.some((t) => t.suggestion);
|
|
73
63
|
if (hasSuggestions)
|
|
74
64
|
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
@@ -111,7 +101,6 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
111
101
|
}
|
|
112
102
|
if (resolveOnlyCommand?.hasMutations)
|
|
113
103
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
114
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand));
|
|
115
|
-
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
104
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand), buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
116
105
|
return instructions;
|
|
117
106
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { type NormalizedBotUsernames } from "../../comments/authors.mts";
|
|
2
|
+
import { shouldResolveOtherHumanThread } from "../../comments/thread-resolve-policy.mts";
|
|
3
|
+
import type { ResolveOtherHumanThreads } from "../../config/load.mts";
|
|
2
4
|
import type { AgentThread, ReviewThread } from "../../types.mts";
|
|
5
|
+
export { shouldResolveOtherHumanThread };
|
|
6
|
+
export type RoutableThread = AgentThread | ReviewThread;
|
|
3
7
|
export interface ThreadMutationRouting {
|
|
4
8
|
replyThreadIds: string[];
|
|
5
9
|
pairedResolveThreadIds: string[];
|
|
6
10
|
standaloneResolveThreadIds: string[];
|
|
7
11
|
resolveThreadIds: string[];
|
|
8
12
|
}
|
|
9
|
-
export declare function
|
|
10
|
-
|
|
13
|
+
export declare function threadHasAuthorizedMutation(thread: {
|
|
14
|
+
id: string;
|
|
15
|
+
viewerCanReply?: boolean;
|
|
16
|
+
viewerCanResolve?: boolean;
|
|
17
|
+
}, replyThreadIds: ReadonlySet<string>, resolveThreadIds: ReadonlySet<string>): boolean;
|
|
18
|
+
export declare function buildThreadMutationRouting(threads: RoutableThread[], botUsernames: NormalizedBotUsernames, ruleAutoResolveThreadIds: string[], policy?: ResolveOtherHumanThreads): ThreadMutationRouting;
|
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "../../comments/authors.mjs";
|
|
2
2
|
import { threadEndedByShepherd } from "../../comments/marker.mjs";
|
|
3
|
+
import { shouldResolveOtherHumanThread } from "../../comments/thread-resolve-policy.mjs";
|
|
4
|
+
export { shouldResolveOtherHumanThread };
|
|
3
5
|
function dedupeIds(ids) {
|
|
4
6
|
return [...new Set(ids)];
|
|
5
7
|
}
|
|
6
|
-
|
|
7
|
-
return (!thread
|
|
8
|
-
thread.isOutdated &&
|
|
9
|
-
(thread.path === null || thread.line === null) &&
|
|
10
|
-
isConfiguredBotAuthor(thread, botUsernames) &&
|
|
11
|
-
thread.viewerCanResolve === true);
|
|
8
|
+
function isOrdinaryHuman(thread, botUsernames) {
|
|
9
|
+
return isHumanAuthor(thread) && !isConfiguredBotAuthor(thread, botUsernames);
|
|
12
10
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
function shouldPairResolve(thread, botUsernames, policy = "none") {
|
|
12
|
+
if (!isOrdinaryHuman(thread, botUsernames))
|
|
13
|
+
return true;
|
|
14
|
+
if (isViewerAuthoredHuman(thread, botUsernames))
|
|
15
|
+
return true;
|
|
16
|
+
return shouldResolveOtherHumanThread(thread, policy);
|
|
17
|
+
}
|
|
18
|
+
export function threadHasAuthorizedMutation(thread, replyThreadIds, resolveThreadIds) {
|
|
19
|
+
const inReply = replyThreadIds.has(thread.id);
|
|
20
|
+
const inResolve = resolveThreadIds.has(thread.id);
|
|
21
|
+
if (!inReply && !inResolve)
|
|
22
|
+
return false;
|
|
23
|
+
if (inReply && thread.viewerCanReply !== true)
|
|
24
|
+
return false;
|
|
25
|
+
if (inResolve && thread.viewerCanResolve !== true)
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
export function buildThreadMutationRouting(threads, botUsernames, ruleAutoResolveThreadIds, policy = "none") {
|
|
30
|
+
const replyThreadIds = dedupeIds(threads.filter((thread) => !threadEndedByShepherd(thread)).map((thread) => thread.id));
|
|
18
31
|
const pairedResolveThreadIds = dedupeIds(threads
|
|
19
|
-
.filter((thread) =>
|
|
32
|
+
.filter((thread) => shouldPairResolve(thread, botUsernames, policy) && !threadEndedByShepherd(thread))
|
|
20
33
|
.map((thread) => thread.id));
|
|
21
34
|
const pairedResolveIdSet = new Set(pairedResolveThreadIds);
|
|
22
35
|
// Rule-matched threads bypass author routing; resolve-mutate retains the human-author guard.
|
|
23
36
|
const standaloneResolveThreadIds = dedupeIds([
|
|
24
37
|
...threads
|
|
25
|
-
.filter((thread) =>
|
|
26
|
-
(isViewerAuthoredHuman(thread, botUsernames) && threadEndedByShepherd(thread)))
|
|
38
|
+
.filter((thread) => shouldPairResolve(thread, botUsernames, policy) && threadEndedByShepherd(thread))
|
|
27
39
|
.map((thread) => thread.id),
|
|
28
40
|
...ruleAutoResolveThreadIds,
|
|
29
41
|
]).filter((id) => !pairedResolveIdSet.has(id));
|
|
@@ -3,6 +3,7 @@ import { applyResolveOptions } from "../comments/resolve.mjs";
|
|
|
3
3
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
4
4
|
import { loadConfig } from "../config/load.mjs";
|
|
5
5
|
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
6
|
+
import { shouldResolveOtherHumanThread } from "./iterate/thread-mutation-routing.mjs";
|
|
6
7
|
import { markReplySeen } from "../state/seen-comments.mjs";
|
|
7
8
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
8
9
|
import { addPrShepherdMarker, threadEndedByShepherd } from "../comments/marker.mjs";
|
|
@@ -31,14 +32,24 @@ export async function runResolveMutate(opts) {
|
|
|
31
32
|
// print. Once a caller explicitly runs apply, GitHub's mutation response is
|
|
32
33
|
// authoritative and this path must not second-guess that intent.
|
|
33
34
|
const requestedReplyIds = new Set(opts.replyThreadIds ?? []);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const policy = config.iterate?.resolveOtherHumanThreads ?? "none";
|
|
36
|
+
const allowedHumanResolveIds = new Set(data.reviewThreads
|
|
37
|
+
.filter((thread) => {
|
|
38
|
+
if (!humanThreadIds.has(thread.id))
|
|
39
|
+
return false;
|
|
40
|
+
const paired = requestedReplyIds.has(thread.id) || threadEndedByShepherd(thread);
|
|
41
|
+
if (!paired)
|
|
42
|
+
return false;
|
|
43
|
+
if (isViewerAuthoredHuman(thread, botUsernames))
|
|
44
|
+
return true;
|
|
45
|
+
return shouldResolveOtherHumanThread(thread, policy);
|
|
46
|
+
})
|
|
37
47
|
.map((thread) => thread.id));
|
|
38
|
-
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) => !humanThreadIds.has(id) ||
|
|
39
|
-
const skippedHumanResolves = (opts.resolveThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !
|
|
40
|
-
const
|
|
41
|
-
const
|
|
48
|
+
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) => !humanThreadIds.has(id) || allowedHumanResolveIds.has(id));
|
|
49
|
+
const skippedHumanResolves = (opts.resolveThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !allowedHumanResolveIds.has(id));
|
|
50
|
+
const knownThreadIds = new Set(data.reviewThreads.map((thread) => thread.id));
|
|
51
|
+
const replyThreadIds = opts.replyThreadIds?.filter((id) => knownThreadIds.has(id));
|
|
52
|
+
const skippedNonHumanReplies = (opts.replyThreadIds ?? []).filter((id) => !knownThreadIds.has(id));
|
|
42
53
|
const minimizeCommentIds = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id));
|
|
43
54
|
const skippedHumanMinimizes = (opts.minimizeCommentIds ?? []).filter((id) => humanCommentIds.has(id) || humanReviewIds.has(id));
|
|
44
55
|
const dismissReviewIds = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && data.changesRequestedReviews.some((review) => review.id === id));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ResolveOtherHumanThreads } from "../config/load.mts";
|
|
2
|
+
import type { AgentThread, ReviewThread } from "../types.mts";
|
|
3
|
+
type OutdatableThread = AgentThread | ReviewThread;
|
|
4
|
+
/** Other-human threads are resolved only when the iterate enum allows it. */
|
|
5
|
+
export declare function shouldResolveOtherHumanThread(thread: OutdatableThread, policy: ResolveOtherHumanThreads): boolean;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function threadIsOutdated(thread) {
|
|
2
|
+
return "isOutdated" in thread && thread.isOutdated === true;
|
|
3
|
+
}
|
|
4
|
+
/** Other-human threads are resolved only when the iterate enum allows it. */
|
|
5
|
+
export function shouldResolveOtherHumanThread(thread, policy) {
|
|
6
|
+
if (policy === "always")
|
|
7
|
+
return true;
|
|
8
|
+
return policy === "outdated" && threadIsOutdated(thread);
|
|
9
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SeenMarker } from "../state/seen-comments.mts";
|
|
2
2
|
import { type NormalizedBotUsernames } from "./authors.mts";
|
|
3
|
+
import type { ResolveOtherHumanThreads } from "../config/load.mts";
|
|
3
4
|
import type { FirstLookThread, ReviewThread } from "../types.mts";
|
|
4
5
|
interface ThreadVisibility {
|
|
5
6
|
activeThreads: ReviewThread[];
|
|
@@ -7,5 +8,5 @@ interface ThreadVisibility {
|
|
|
7
8
|
firstLookThreads: FirstLookThread[];
|
|
8
9
|
toMarkSeen: ReviewThread[];
|
|
9
10
|
}
|
|
10
|
-
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames, repeatableThreadIds?: ReadonlySet<string
|
|
11
|
+
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames, repeatableThreadIds?: ReadonlySet<string>, resolveOtherHumanThreads?: ResolveOtherHumanThreads): ThreadVisibility;
|
|
11
12
|
export {};
|
|
@@ -2,6 +2,7 @@ import { classifyItem } from "../state/seen-comments.mjs";
|
|
|
2
2
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
3
3
|
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "./authors.mjs";
|
|
4
4
|
import { threadEndedByShepherd } from "./marker.mjs";
|
|
5
|
+
import { shouldResolveOtherHumanThread } from "./thread-resolve-policy.mjs";
|
|
5
6
|
function withEdited(thread, edited) {
|
|
6
7
|
return edited ? { ...thread, edited: true } : thread;
|
|
7
8
|
}
|
|
@@ -19,15 +20,19 @@ function classifyFirstLookThread(thread, seenMap, firstLookStatus) {
|
|
|
19
20
|
return null;
|
|
20
21
|
return { ...visible, firstLookStatus };
|
|
21
22
|
}
|
|
22
|
-
export function classifyThreadVisibility(threads, seenMap, botUsernames = new Set(), repeatableThreadIds) {
|
|
23
|
+
export function classifyThreadVisibility(threads, seenMap, botUsernames = new Set(), repeatableThreadIds, resolveOtherHumanThreads = "none") {
|
|
23
24
|
const shouldRepeat = (thread) => repeatableThreadIds?.has(thread.id) ?? true;
|
|
25
|
+
const isOrdinaryHuman = (thread) => isHumanAuthor(thread) && !isConfiguredBotAuthor(thread, botUsernames);
|
|
24
26
|
const unresolvedThreads = threads.filter((t) => !t.isResolved);
|
|
25
27
|
const activeThreads = unresolvedThreads
|
|
26
28
|
.filter((t) => !t.isOutdated && !t.isMinimized)
|
|
27
29
|
.flatMap((t) => {
|
|
28
30
|
if (threadEndedByShepherd(t))
|
|
29
31
|
return [];
|
|
30
|
-
|
|
32
|
+
const repeatAuthor = isConfiguredBotAuthor(t, botUsernames) ||
|
|
33
|
+
isViewerAuthoredHuman(t, botUsernames) ||
|
|
34
|
+
(isOrdinaryHuman(t) && resolveOtherHumanThreads === "always");
|
|
35
|
+
if (repeatAuthor && shouldRepeat(t))
|
|
31
36
|
return [t];
|
|
32
37
|
const visible = classifyVisibleThread(t, seenMap);
|
|
33
38
|
return visible ? [visible] : [];
|
|
@@ -35,9 +40,11 @@ export function classifyThreadVisibility(threads, seenMap, botUsernames = new Se
|
|
|
35
40
|
const resolutionOnlyThreads = unresolvedThreads
|
|
36
41
|
.filter((t) => {
|
|
37
42
|
const endedByShepherd = threadEndedByShepherd(t);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
if (endedByShepherd) {
|
|
44
|
+
if (!isOrdinaryHuman(t))
|
|
45
|
+
return true;
|
|
46
|
+
return (isViewerAuthoredHuman(t, botUsernames) ||
|
|
47
|
+
shouldResolveOtherHumanThread(t, resolveOtherHumanThreads));
|
|
41
48
|
}
|
|
42
49
|
return t.isOutdated || t.isMinimized;
|
|
43
50
|
})
|
package/bin/config/load.d.mts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
declare const MINIMIZE_COMMENTS_POLICIES: readonly ["all", "bots", "users", "none"];
|
|
2
2
|
export type MinimizeCommentsPolicy = (typeof MINIMIZE_COMMENTS_POLICIES)[number];
|
|
3
|
+
declare const RESOLVE_OTHER_HUMAN_THREADS: readonly ["none", "outdated", "always"];
|
|
4
|
+
export type ResolveOtherHumanThreads = (typeof RESOLVE_OTHER_HUMAN_THREADS)[number];
|
|
3
5
|
export interface GraphqlQuotaWarningBand {
|
|
4
6
|
remainingPercent: number;
|
|
5
7
|
pollIntervalMinutes: number;
|
|
@@ -31,6 +33,12 @@ export interface PrShepherdConfig {
|
|
|
31
33
|
* (default) omits the hint entirely; the CLI never prescribes rebase/merge mechanics itself.
|
|
32
34
|
*/
|
|
33
35
|
behindBaseHint: string;
|
|
36
|
+
/**
|
|
37
|
+
* When to resolve other-human inline threads after Shepherd replies. Own and bot threads
|
|
38
|
+
* always reply-and-resolve. `none` (default) keeps other humans reply-only; `outdated`
|
|
39
|
+
* also resolves when GitHub reports `isOutdated`; `always` pairs reply-and-resolve.
|
|
40
|
+
*/
|
|
41
|
+
resolveOtherHumanThreads: ResolveOtherHumanThreads;
|
|
34
42
|
};
|
|
35
43
|
watch: {
|
|
36
44
|
readyDelayMinutes: number;
|
|
@@ -44,6 +52,8 @@ export interface PrShepherdConfig {
|
|
|
44
52
|
};
|
|
45
53
|
checks: {
|
|
46
54
|
ciTriggerEvents: string[];
|
|
55
|
+
/** Regex patterns matched against each raw log line; matching lines are dropped from log excerpts. Empty by default — no lines are stripped unless configured. */
|
|
56
|
+
ignoreLogLines: string[];
|
|
47
57
|
};
|
|
48
58
|
mergeStatus: {
|
|
49
59
|
blockingReviewerLogins: string[];
|
|
@@ -57,6 +67,14 @@ export interface PrShepherdConfig {
|
|
|
57
67
|
autoMarkReady: boolean;
|
|
58
68
|
/** Legacy-named patterns that keep matching Actions checks visible despite ignoreChecks. */
|
|
59
69
|
neverCancelRuns: string[];
|
|
70
|
+
/**
|
|
71
|
+
* When `false` (default), `iterate --merge` defers non-CI actionable work
|
|
72
|
+
* (review threads, comments, changes-requested reviews, review summaries)
|
|
73
|
+
* while the PR sits in the merge queue, since a Shepherd-initiated push
|
|
74
|
+
* would eject it. When `true`, restores pre-existing behavior: actionable
|
|
75
|
+
* work is handled immediately regardless of queue membership.
|
|
76
|
+
*/
|
|
77
|
+
workWhileQueued: boolean;
|
|
60
78
|
/** @deprecated Accepted for compatibility, ignored by the loader. */
|
|
61
79
|
autoResolveOutdated?: boolean;
|
|
62
80
|
/** @deprecated Accepted for compatibility, ignored by the loader. */
|
package/bin/config/load.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import builtins from "../config.json" with { type: "json" };
|
|
|
7
7
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
8
8
|
import { findMergeStrategies } from "./merge-command-args.mjs";
|
|
9
9
|
const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
|
|
10
|
+
const RESOLVE_OTHER_HUMAN_THREADS = ["none", "outdated", "always"];
|
|
10
11
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
11
12
|
/**
|
|
12
13
|
* Collect `.pr-shepherdrc.yml` files from `startDir` toward `$HOME` (closest first).
|
|
@@ -68,6 +69,13 @@ function parseMinimizeCommentsPolicy(value) {
|
|
|
68
69
|
return value;
|
|
69
70
|
throw new Error(`Invalid config: iterate.minimizeComments must be one of "all", "bots", "users", or "none", got ${JSON.stringify(value)}`);
|
|
70
71
|
}
|
|
72
|
+
function parseResolveOtherHumanThreads(value) {
|
|
73
|
+
if (typeof value === "string" &&
|
|
74
|
+
RESOLVE_OTHER_HUMAN_THREADS.includes(value)) {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Invalid config: iterate.resolveOtherHumanThreads must be one of "none", "outdated", or "always", got ${JSON.stringify(value)}`);
|
|
78
|
+
}
|
|
71
79
|
function parseBotUsernames(value) {
|
|
72
80
|
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
73
81
|
throw new Error(`Invalid config: botUsernames must be an array of strings`);
|
|
@@ -86,6 +94,20 @@ function parseNeverCancelRuns(value) {
|
|
|
86
94
|
}
|
|
87
95
|
return value;
|
|
88
96
|
}
|
|
97
|
+
function parseIgnoreLogLines(value) {
|
|
98
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
99
|
+
throw new Error(`Invalid config: checks.ignoreLogLines must be an array of strings`);
|
|
100
|
+
}
|
|
101
|
+
for (const pattern of value) {
|
|
102
|
+
try {
|
|
103
|
+
new RegExp(pattern);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error(`Invalid config: checks.ignoreLogLines contains an invalid regular expression: ${pattern}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
89
111
|
const SHEPHERD_OWNED_MERGE_FLAGS = [
|
|
90
112
|
"--repo",
|
|
91
113
|
"-R",
|
|
@@ -165,16 +187,18 @@ const KNOWN_NESTED_KEYS = {
|
|
|
165
187
|
"minimizeApprovals",
|
|
166
188
|
"minimizeComments",
|
|
167
189
|
"behindBaseHint",
|
|
190
|
+
"resolveOtherHumanThreads",
|
|
168
191
|
]),
|
|
169
192
|
watch: new Set(["readyDelayMinutes", "graphqlQuotaWarnings"]),
|
|
170
193
|
resolve: new Set(["shaPoll"]),
|
|
171
|
-
checks: new Set(["ciTriggerEvents"]),
|
|
194
|
+
checks: new Set(["ciTriggerEvents", "ignoreLogLines"]),
|
|
172
195
|
mergeStatus: new Set(["blockingReviewerLogins"]),
|
|
173
196
|
merge: new Set(["commandArgs"]),
|
|
174
197
|
actions: new Set([
|
|
175
198
|
"autoMinimizeSuppressed",
|
|
176
199
|
"autoMarkReady",
|
|
177
200
|
"neverCancelRuns",
|
|
201
|
+
"workWhileQueued",
|
|
178
202
|
"autoResolveOutdated",
|
|
179
203
|
"commitSuggestions",
|
|
180
204
|
]),
|
|
@@ -261,6 +285,8 @@ export function loadConfig() {
|
|
|
261
285
|
config.merge.commandArgs = parseMergeCommandArgs(config.merge.commandArgs);
|
|
262
286
|
config.watch.graphqlQuotaWarnings = parseGraphqlQuotaWarnings(config.watch.graphqlQuotaWarnings);
|
|
263
287
|
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
288
|
+
config.iterate.resolveOtherHumanThreads = parseResolveOtherHumanThreads(config.iterate.resolveOtherHumanThreads);
|
|
289
|
+
config.checks.ignoreLogLines = parseIgnoreLogLines(config.checks.ignoreLogLines);
|
|
264
290
|
configCache.set(cwd, config);
|
|
265
291
|
return config;
|
|
266
292
|
}
|
package/bin/config.json
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"stallTimeoutMinutes": 60,
|
|
20
20
|
"minimizeApprovals": false,
|
|
21
21
|
"minimizeComments": "all",
|
|
22
|
-
"behindBaseHint": ""
|
|
22
|
+
"behindBaseHint": "",
|
|
23
|
+
"resolveOtherHumanThreads": "none"
|
|
23
24
|
},
|
|
24
25
|
"watch": {
|
|
25
26
|
"readyDelayMinutes": 10,
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
}
|
|
37
38
|
},
|
|
38
39
|
"checks": {
|
|
39
|
-
"ciTriggerEvents": ["pull_request", "pull_request_target"]
|
|
40
|
+
"ciTriggerEvents": ["pull_request", "pull_request_target"],
|
|
41
|
+
"ignoreLogLines": []
|
|
40
42
|
},
|
|
41
43
|
"mergeStatus": {
|
|
42
44
|
"blockingReviewerLogins": ["copilot"]
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
"actions": {
|
|
48
50
|
"autoMinimizeSuppressed": true,
|
|
49
51
|
"autoMarkReady": true,
|
|
50
|
-
"neverCancelRuns": []
|
|
52
|
+
"neverCancelRuns": [],
|
|
53
|
+
"workWhileQueued": false
|
|
51
54
|
}
|
|
52
55
|
}
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { createPrShepherd, PartialApplyError, PrShepherdValidationError, } from "../api.mjs";
|
|
6
6
|
import { isRepositoryQualifiedPrReference } from "../pr-reference.mjs";
|
|
7
7
|
import { formatJournalResult } from "../cli/journal-formatter.mjs";
|
|
8
|
-
import { formatCommitSuggestionResult, formatSuggestionPatchesResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, } from "../cli/formatters.mjs";
|
|
8
|
+
import { formatCommitSuggestionResult, formatSuggestionPatchesResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, projectIterateLean, } from "../cli/formatters.mjs";
|
|
9
9
|
import { formatCliError, serializeGitHubRequestErrorDetails } from "../cli/error-format.mjs";
|
|
10
10
|
import { errorToExitCode, EXIT } from "../exit-codes.mjs";
|
|
11
11
|
const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/repo#number reference";
|
|
@@ -88,7 +88,13 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
88
88
|
idempotentHint: false,
|
|
89
89
|
openWorldHint: true,
|
|
90
90
|
},
|
|
91
|
-
}, async (input) =>
|
|
91
|
+
}, async (input) => {
|
|
92
|
+
// One options object feeds both channels so JSON and Markdown cannot drift.
|
|
93
|
+
const opts = {
|
|
94
|
+
readyDelaySuffix: input.readyDelaySeconds === undefined ? undefined : `${input.readyDelaySeconds}s`,
|
|
95
|
+
};
|
|
96
|
+
return runTool(() => shepherd.iterate(requireRepositoryQualifiedPr(input)), (result) => formatIterateResult(result, opts), (result) => projectIterateLean(result, opts));
|
|
97
|
+
});
|
|
92
98
|
server.registerTool("apply", {
|
|
93
99
|
description: "Apply ordered review, journal, and file-view operations after prevalidation; explicit requests rely on GitHub's mutation response.",
|
|
94
100
|
inputSchema: applyInputSchema,
|
|
@@ -131,16 +137,22 @@ function readPackageVersion() {
|
|
|
131
137
|
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
132
138
|
return packageJson.version;
|
|
133
139
|
}
|
|
134
|
-
function toolResult(
|
|
140
|
+
function toolResult(structured, text) {
|
|
135
141
|
return {
|
|
136
142
|
content: [{ type: "text", text }],
|
|
137
|
-
structuredContent:
|
|
143
|
+
structuredContent: structured,
|
|
138
144
|
};
|
|
139
145
|
}
|
|
140
|
-
|
|
146
|
+
/**
|
|
147
|
+
* `project` mirrors the CLI's `--format=json` treatment of the same result. Tools
|
|
148
|
+
* whose CLI JSON is the raw result object (apply, build_suggestion_patch(es) — see
|
|
149
|
+
* handlers.mts and cli-parser.mts, which JSON.stringify the result directly) omit
|
|
150
|
+
* `project` and return the result unchanged, matching their own CLI JSON output.
|
|
151
|
+
*/
|
|
152
|
+
async function runTool(work, format, project) {
|
|
141
153
|
try {
|
|
142
154
|
const result = await work();
|
|
143
|
-
return toolResult(result, format(result));
|
|
155
|
+
return toolResult(project ? project(result) : result, format(result));
|
|
144
156
|
}
|
|
145
157
|
catch (error) {
|
|
146
158
|
return toolError(error);
|
package/bin/types/escalate.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentCheck, AgentComment, AgentThread } from "./report.mts";
|
|
2
2
|
import type { CheckStatus, Review } from "./github.mts";
|
|
3
|
-
import type { MergeQueueRemovalStatus } from "./merge-requirements.mts";
|
|
4
|
-
export type EscalateTrigger = "fix-thrash" | "base-branch-unknown" | "stall-timeout" | "check-follow-up-unavailable" | "authorization-required" | "bot-cr-not-dismissed" | "merge-queue-removed";
|
|
3
|
+
import type { MergeQueueRemovalStatus, StackStatus } from "./merge-requirements.mts";
|
|
4
|
+
export type EscalateTrigger = "fix-thrash" | "base-branch-unknown" | "stall-timeout" | "check-follow-up-unavailable" | "authorization-required" | "bot-cr-not-dismissed" | "merge-queue-removed" | "stacked-pr";
|
|
5
5
|
export interface AgentStalledCheck {
|
|
6
6
|
name: string;
|
|
7
7
|
status: CheckStatus;
|
|
@@ -29,6 +29,7 @@ export interface EscalateDetails {
|
|
|
29
29
|
suggestion: string;
|
|
30
30
|
humanMessage: string;
|
|
31
31
|
mergeQueueRemoval?: MergeQueueRemovalStatus;
|
|
32
|
+
stack?: StackStatus;
|
|
32
33
|
authorization?: Array<{
|
|
33
34
|
action: "mark-ready" | "merge-or-enqueue";
|
|
34
35
|
targetIds: string[];
|
package/bin/types/iterate.d.mts
CHANGED
|
@@ -48,6 +48,7 @@ export interface IterateResultBase {
|
|
|
48
48
|
interface IterateResultWait extends IterateResultBase {
|
|
49
49
|
action: "wait";
|
|
50
50
|
log: string;
|
|
51
|
+
deferredWork?: import("./merge-queue.mts").IterateDeferredWork;
|
|
51
52
|
}
|
|
52
53
|
export type CancelReason = "merged" | "closed" | "ready-delay-elapsed";
|
|
53
54
|
interface IterateResultCancel extends IterateResultBase {
|
|
@@ -135,9 +136,18 @@ export interface IterateCommandOptions extends GlobalOptions {
|
|
|
135
136
|
stallTimeoutSeconds?: number;
|
|
136
137
|
/** Legacy per-invocation no-op retained for API compatibility. */
|
|
137
138
|
neverCancelRuns?: string[];
|
|
139
|
+
/**
|
|
140
|
+
* Internal. `false` skips seen-marker writes (poll's discarded debounce ticks). Set
|
|
141
|
+
* only by `runPollCore`; excluded from the public `IterateInput` in api.mts.
|
|
142
|
+
*/
|
|
138
143
|
persistSeen?: boolean;
|
|
139
144
|
/** Shepherd through readiness and emit the exact merge/queue command when ready. */
|
|
140
145
|
merge?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Internal. Defers attaching a quota warning until an until-terminal poll actually
|
|
148
|
+
* breaks. Set only by `runPollCore`; excluded from the public `IterateInput` in
|
|
149
|
+
* api.mts.
|
|
150
|
+
*/
|
|
141
151
|
deferQuotaWarning?: boolean;
|
|
142
152
|
}
|
|
143
153
|
export {};
|
|
@@ -11,3 +11,19 @@ export interface MergeQueueReport {
|
|
|
11
11
|
/** The current PR head is not a parent of the removed synthetic queue commit. */
|
|
12
12
|
headUpdatedAfterRemoval?: true;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Raw counts of actionable work held back while the PR sits in the merge queue
|
|
16
|
+
* (`actions.workWhileQueued` is `false`, the default) — a Shepherd-initiated push
|
|
17
|
+
* or mutation right now would eject the PR. Omitted entirely once every count is
|
|
18
|
+
* zero. Not emitted for checks/annotations/conflicts: those always surface via
|
|
19
|
+
* `fix_code` immediately regardless of queue membership.
|
|
20
|
+
*/
|
|
21
|
+
export interface IterateDeferredWork {
|
|
22
|
+
/** Unique review threads across actionable, resolution-only, first-look, and rule-auto-resolve. */
|
|
23
|
+
threads: number;
|
|
24
|
+
/** Unique PR comments across actionable, minimize-queued, and first-look. */
|
|
25
|
+
comments: number;
|
|
26
|
+
changesRequestedReviews: number;
|
|
27
|
+
/** Unique review summaries across the minimize queue, first-look, edited, and (if opted in) surfaced approvals. */
|
|
28
|
+
reviewSummaries: number;
|
|
29
|
+
}
|