pr-shepherd 1.1.1 → 1.2.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -17
- package/bin/classify/standard/coderabbit-notices.mjs +17 -1
- package/bin/classify/standard/codex-notices.mjs +5 -3
- package/bin/cli/iterate-instructions.mjs +3 -1
- package/bin/cli/iterate-lean.mjs +8 -1
- package/bin/cli/triage-formatter.mjs +5 -1
- package/bin/commands/check-annotations.d.mts +8 -3
- package/bin/commands/check-annotations.mjs +11 -10
- package/bin/commands/check-unreported.mjs +1 -1
- package/bin/commands/check.mjs +17 -22
- package/bin/commands/iterate/check-blocker-gate.d.mts +2 -0
- package/bin/commands/iterate/check-blocker-gate.mjs +3 -1
- package/bin/commands/iterate/check-instructions.d.mts +7 -1
- package/bin/commands/iterate/check-instructions.mjs +10 -3
- package/bin/commands/iterate/conflicting-head-ci.d.mts +1 -1
- package/bin/commands/iterate/conflicting-head-ci.mjs +1 -1
- package/bin/commands/iterate/index.mjs +17 -7
- package/bin/commands/iterate/merge-plan.d.mts +2 -0
- package/bin/commands/iterate/merge-plan.mjs +5 -0
- package/bin/commands/iterate/merge-state.d.mts +10 -1
- package/bin/commands/iterate/merge-state.mjs +1 -1
- package/bin/commands/iterate/native-stack-rebase.d.mts +13 -2
- package/bin/commands/iterate/native-stack-rebase.mjs +23 -3
- package/bin/commands/iterate/queued-conflict-deferral.d.mts +15 -0
- package/bin/commands/iterate/queued-conflict-deferral.mjs +20 -0
- package/bin/commands/iterate/render.d.mts +2 -1
- package/bin/commands/iterate/render.mjs +18 -12
- package/bin/commands/iterate/rerun-command.d.mts +7 -0
- package/bin/commands/iterate/rerun-command.mjs +22 -0
- package/bin/commands/iterate/run.mjs +2 -1
- package/bin/commands/iterate/triage.d.mts +5 -1
- package/bin/commands/iterate/triage.mjs +32 -11
- package/bin/commands/poll-summary-explicit-instructions.mjs +3 -0
- package/bin/commands/poll-summary-instructions.mjs +1 -1
- package/bin/comments/resolve.d.mts +2 -0
- package/bin/comments/resolve.mjs +7 -0
- package/bin/comments/rest-review-mutations.d.mts +1 -0
- package/bin/comments/rest-review-mutations.mjs +3 -1
- package/bin/github/poll-summary-annotations.d.mts +1 -1
- package/bin/github/poll-summary-annotations.mjs +2 -2
- package/bin/types/iterate.d.mts +2 -0
- package/bin/types/report.d.mts +2 -2
- package/package.json +2 -2
- package/plugins/pr-shepherd/.claude-plugin/icon.png +0 -0
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- 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/package-lock.json +1278 -0
- package/plugins/pr-shepherd/package.json +9 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +5 -4
- package/plugins/pr-shepherd/skills/pr-shepherd/references/branch-update.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/event-loop.md +1 -1
|
@@ -35,8 +35,19 @@ export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
|
35
35
|
trunk: string;
|
|
36
36
|
bottomPr?: number;
|
|
37
37
|
}, stackBottomPr?: number): string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Point at the conflicts, and at the stack rebase when the PR is a native stack layer. A queued
|
|
40
|
+
* conflicting entry cannot form a merge group, so GitHub removes it once the queue reaches it.
|
|
41
|
+
* Pulling it out early (dequeue, then resolve) or waiting for that removal is the caller's
|
|
42
|
+
* call, so a queued PR gets both paths without a recommendation. `dequeueCommand` is the
|
|
43
|
+
* rendered dequeue mutation; it is present only while the PR is a queue entry.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildConflictInstruction(stackRebase: string | undefined, dequeueCommand?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* The two paths for a conflicting PR that is still a queue entry. `resolve` is the branch
|
|
48
|
+
* update route; on a native stack, dequeueing a layer can also take queued layers above it out.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildQueuedConflictChoice(dequeueCommand: string, resolve: string, stack: boolean): string;
|
|
40
51
|
/**
|
|
41
52
|
* Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack.
|
|
42
53
|
* After a merge-queue ejection update, push only when the update or a fix changed the head.
|
|
@@ -54,10 +54,30 @@ export function buildNativeStackLayerRebase(repo, pr, stack, trunkConflict, stac
|
|
|
54
54
|
: { parentBranch: pr.baseBranch };
|
|
55
55
|
return buildNativeStackRebaseInstruction(repo, stack.number, start);
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Point at the conflicts, and at the stack rebase when the PR is a native stack layer. A queued
|
|
59
|
+
* conflicting entry cannot form a merge group, so GitHub removes it once the queue reaches it.
|
|
60
|
+
* Pulling it out early (dequeue, then resolve) or waiting for that removal is the caller's
|
|
61
|
+
* call, so a queued PR gets both paths without a recommendation. `dequeueCommand` is the
|
|
62
|
+
* rendered dequeue mutation; it is present only while the PR is a queue entry.
|
|
63
|
+
*/
|
|
64
|
+
export function buildConflictInstruction(stackRebase, dequeueCommand) {
|
|
59
65
|
const pointer = "The branch has merge conflicts (see `**branch**` above).";
|
|
60
|
-
|
|
66
|
+
const resolve = stackRebase ?? "Resolve them before committing.";
|
|
67
|
+
if (!dequeueCommand)
|
|
68
|
+
return `${pointer} ${resolve}`;
|
|
69
|
+
return `${pointer} ${buildQueuedConflictChoice(dequeueCommand, resolve, stackRebase !== undefined)}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The two paths for a conflicting PR that is still a queue entry. `resolve` is the branch
|
|
73
|
+
* update route; on a native stack, dequeueing a layer can also take queued layers above it out.
|
|
74
|
+
*/
|
|
75
|
+
export function buildQueuedConflictChoice(dequeueCommand, resolve, stack) {
|
|
76
|
+
const leaves = stack
|
|
77
|
+
? "this PR leaves the queue, and so may any queued layers above it in this stack; other entries stay queued"
|
|
78
|
+
: "only this PR leaves the queue and the other entries stay queued";
|
|
79
|
+
const requeue = stack ? "enqueue it and any removed layers again" : "enqueue it again";
|
|
80
|
+
return `The PR is still in the merge queue, but GitHub cannot merge it until the conflicts are resolved. Choose one path: (a) pull it from the queue and resolve now — first run \`${dequeueCommand}\` (\`gh pr\` cannot remove a queue entry); ${leaves}; then: ${resolve} Once the PR is ready again, ${requeue} (with \`--merge\`, Shepherd prints the enqueue command). Or (b) wait — skip the dequeue and every step marked \`If you chose (a):\`, and rerun Shepherd after the polling interval instead of immediately, until GitHub removes the entry itself; this step repeats until then, and an unchanged conflict escalates through \`stall-timeout\`. On either path, review items stay deferred until the PR leaves the queue.`;
|
|
61
81
|
}
|
|
62
82
|
/**
|
|
63
83
|
* Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IterateDeferredWork, Review, ShepherdReport } from "../../types.mts";
|
|
2
|
+
/** Review work for one triage tick; see `isQueuedConflict` in check-annotations.mts. */
|
|
3
|
+
interface ReviewWork {
|
|
4
|
+
report: ShepherdReport;
|
|
5
|
+
reviewSummaryIds: string[];
|
|
6
|
+
firstLookSummaries: Review[];
|
|
7
|
+
editedSummaries: Review[];
|
|
8
|
+
surfacedApprovals: Review[];
|
|
9
|
+
ruleAutoResolveThreadIds?: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Strip review work from a queued-conflict tick and count what was held back. */
|
|
12
|
+
export declare function deferQueuedConflictReviewWork(work: ReviewWork, minimizeApprovals: boolean): Omit<ReviewWork, "ruleAutoResolveThreadIds"> & {
|
|
13
|
+
deferredWork?: IterateDeferredWork;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { buildDeferredWork } from "./merge-state.mjs";
|
|
2
|
+
/** Strip review work from a queued-conflict tick and count what was held back. */
|
|
3
|
+
export function deferQueuedConflictReviewWork(work, minimizeApprovals) {
|
|
4
|
+
const deferredWork = buildDeferredWork({ ...work, minimizeApprovals });
|
|
5
|
+
const { reviewItems: _held, ...report } = work.report;
|
|
6
|
+
const { ruleAutoResolveIds: _rule, ...threads } = report.threads;
|
|
7
|
+
return {
|
|
8
|
+
report: {
|
|
9
|
+
...report,
|
|
10
|
+
threads: { ...threads, actionable: [], resolutionOnly: [], firstLook: [] },
|
|
11
|
+
comments: { ...report.comments, actionable: [], minimizeIds: [], firstLook: [] },
|
|
12
|
+
changesRequestedReviews: [],
|
|
13
|
+
},
|
|
14
|
+
reviewSummaryIds: [],
|
|
15
|
+
firstLookSummaries: [],
|
|
16
|
+
editedSummaries: [],
|
|
17
|
+
surfacedApprovals: [],
|
|
18
|
+
...(deferredWork && { deferredWork }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -10,4 +10,5 @@ export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
|
10
10
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behind-base-hint — see buildBehindBaseHintInstruction
|
|
11
11
|
isBehind?: boolean, _viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean, stackRebase?: string, // native stack layers rebase with gh-stack, not branch by branch
|
|
12
12
|
queueEjection?: QueueEjectionRecovery, // current merge-queue removal with failing queue checks
|
|
13
|
-
instructionStyle?: InstructionStyle
|
|
13
|
+
instructionStyle?: InstructionStyle, // see iterate.instructions
|
|
14
|
+
dequeueCommand?: string): string[];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { buildPrShepherdCommand, renderShellCommand } from "../../cli/runner.mjs";
|
|
2
|
-
import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintInstruction, buildRepeatedWorkflowBranchRecoveryInstructions, buildResolveCommandInstruction, buildFixCompletionInstruction, } from "./check-instructions.mjs";
|
|
2
|
+
import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintInstruction, buildRepeatedWorkflowBranchRecoveryInstructions, buildResolveCommandInstruction, buildFixCompletionInstruction, QUEUED_CONFLICT_CONTINUATION, } from "./check-instructions.mjs";
|
|
3
3
|
import { isFailingAgentCheck } from "../../checks/conclusions.mjs";
|
|
4
4
|
import { buildCommitSuggestionInstruction } from "../commit-suggestion-instruction.mjs";
|
|
5
5
|
import { partitionFixThreads } from "./fix-instruction-threads.mjs";
|
|
6
|
-
import { buildBranchPushInstruction, buildConflictInstruction } from "./native-stack-rebase.mjs";
|
|
6
|
+
import { buildBranchPushInstruction, buildConflictInstruction, buildQueuedConflictChoice, } from "./native-stack-rebase.mjs";
|
|
7
7
|
import { queueEjectionSteps } from "./queue-recovery-instructions.mjs";
|
|
8
8
|
import { buildPushSteps } from "./triage-loop-instruction.mjs";
|
|
9
9
|
/**
|
|
@@ -19,7 +19,8 @@ const REQUIRE_HEAD_SHA = "--require-sha HEAD";
|
|
|
19
19
|
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behind-base-hint — see buildBehindBaseHintInstruction
|
|
20
20
|
isBehind = false, _viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, stackRebase, // native stack layers rebase with gh-stack, not branch by branch
|
|
21
21
|
queueEjection, // current merge-queue removal with failing queue checks
|
|
22
|
-
instructionStyle = "inline"
|
|
22
|
+
instructionStyle = "inline", // see iterate.instructions
|
|
23
|
+
dequeueCommand) {
|
|
23
24
|
const instructions = [];
|
|
24
25
|
const { locatedThreads, unlocatedThreads } = partitionFixThreads(threads, resolveCommand, resolveOnlyCommand);
|
|
25
26
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
@@ -32,18 +33,23 @@ instructionStyle = "inline") {
|
|
|
32
33
|
changesRequestedReviews.length > 0 ||
|
|
33
34
|
actionableComments.length > 0;
|
|
34
35
|
// Start with interpretation. The agent decides what raw feedback warrants a code change.
|
|
36
|
+
// A queued conflict offers a wait path that publishes nothing, so fixes, branch updates,
|
|
37
|
+
// pushes, and review mutations (which must follow a push) become path (a).
|
|
38
|
+
const pathA = (step) => (dequeueCommand ? `If you chose (a): ${step}` : step);
|
|
35
39
|
if (hasNonConflictHints)
|
|
36
|
-
instructions.push("Fix each warranted item.");
|
|
40
|
+
instructions.push(pathA("Fix each warranted item."));
|
|
37
41
|
// A conflicting native stack layer follows the printed gh-stack route, so it omits the hint.
|
|
38
42
|
const branchUpdateHint = buildBehindBaseHintInstruction(baseBranch, behindBaseHint, {
|
|
39
43
|
isBehind,
|
|
40
44
|
hasConflicts: hasConflicts && !stackRebase,
|
|
41
|
-
});
|
|
45
|
+
}).map(pathA);
|
|
42
46
|
// The conflict hint belongs with the conflict step; otherwise it precedes the push step.
|
|
43
47
|
const hintWithConflictStep = hasConflicts && !hasRepeatedWorkflowBranchRecovery;
|
|
44
48
|
const branchRecovery = hasConflicts || hasRepeatedWorkflowBranchRecovery;
|
|
45
49
|
if (hintWithConflictStep)
|
|
46
|
-
instructions.push(buildConflictInstruction(stackRebase), ...branchUpdateHint);
|
|
50
|
+
instructions.push(buildConflictInstruction(stackRebase, dequeueCommand), ...branchUpdateHint);
|
|
51
|
+
else if (dequeueCommand && hasRepeatedWorkflowBranchRecovery)
|
|
52
|
+
instructions.push(`The branch has merge conflicts (see \`**branch**\` above). ${buildQueuedConflictChoice(dequeueCommand, "Follow the steps marked `If you chose (a):` below.", stackRebase !== undefined)}`);
|
|
47
53
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
48
54
|
if (firstLookTotal > 0)
|
|
49
55
|
instructions.push("Review every item under `## First-look items` before acting.");
|
|
@@ -56,12 +62,12 @@ instructionStyle = "inline") {
|
|
|
56
62
|
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.");
|
|
57
63
|
const hasSuggestions = locatedThreads.some((t) => t.suggestion);
|
|
58
64
|
if (hasSuggestions)
|
|
59
|
-
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
65
|
+
instructions.push(pathA(buildCommitSuggestionInstruction(prReference, "## Review threads")));
|
|
60
66
|
if (resolutionOnlyThreads.length > 0)
|
|
61
67
|
instructions.push("Review the threads under `## Review threads to resolve` before running the generated mutations.");
|
|
62
|
-
instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions, ...queueEjectionSteps(queueEjection, stackRebase, branchRecovery));
|
|
68
|
+
instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions.map(pathA), ...queueEjectionSteps(queueEjection, stackRebase, branchRecovery));
|
|
63
69
|
if (hasAnnotations)
|
|
64
|
-
instructions.push("Inspect every referenced range under `## Check annotations` and apply any warranted change.");
|
|
70
|
+
instructions.push(pathA("Inspect every referenced range under `## Check annotations` and apply any warranted change."));
|
|
65
71
|
// The first step already covers applying changes-requested bodies; only stale CRs add a step.
|
|
66
72
|
const staleClause = buildCrStaleClause(changesRequestedReviews);
|
|
67
73
|
if (staleClause)
|
|
@@ -83,11 +89,11 @@ instructionStyle = "inline") {
|
|
|
83
89
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
84
90
|
const genericPush = !(branchRecovery || queueEjection) && hasNonConflictHints;
|
|
85
91
|
if (branchRecovery || queueEjection) {
|
|
86
|
-
instructions.push(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix, !branchRecovery));
|
|
92
|
+
instructions.push(pathA(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix, !branchRecovery)));
|
|
87
93
|
}
|
|
88
94
|
instructions.push(...buildPushSteps(instructionStyle, genericPush));
|
|
89
95
|
if (resolveOnlyCommand?.hasMutations)
|
|
90
|
-
instructions.push(`Run: \`${renderResolveCommand(resolveOnlyCommand)}\``);
|
|
91
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand, renderResolveCommand(resolveCommand)), buildFixCompletionInstruction());
|
|
96
|
+
instructions.push(pathA(`Run: \`${renderResolveCommand(resolveOnlyCommand)}\``));
|
|
97
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand, renderResolveCommand(resolveCommand)).map(pathA), dequeueCommand ? QUEUED_CONFLICT_CONTINUATION : buildFixCompletionInstruction());
|
|
92
98
|
return instructions;
|
|
93
99
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IterateResult } from "../../types.mts";
|
|
2
|
+
/**
|
|
3
|
+
* When the caller passed a qualified PR, name it in the plain `[TRIAGE]` recurrence step, so the
|
|
4
|
+
* caller reruns the same target instead of shortening it to a bare number that resolves in the
|
|
5
|
+
* local checkout. A bare target already reruns correctly. The caller owns the mode and flags.
|
|
6
|
+
*/
|
|
7
|
+
export declare function withTriageRerunTarget(result: IterateResult, qualified: boolean): IterateResult;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
2
|
+
import { TRIAGE_CONTINUATION } from "./check-instructions.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* When the caller passed a qualified PR, name it in the plain `[TRIAGE]` recurrence step, so the
|
|
5
|
+
* caller reruns the same target instead of shortening it to a bare number that resolves in the
|
|
6
|
+
* local checkout. A bare target already reruns correctly. The caller owns the mode and flags.
|
|
7
|
+
*/
|
|
8
|
+
export function withTriageRerunTarget(result, qualified) {
|
|
9
|
+
if (!qualified || result.action !== "triage")
|
|
10
|
+
return result;
|
|
11
|
+
const instructions = result.triage.instructions;
|
|
12
|
+
if (instructions.at(-1) !== TRIAGE_CONTINUATION)
|
|
13
|
+
return result;
|
|
14
|
+
const target = formatPrUrl(result.repo, result.pr);
|
|
15
|
+
return {
|
|
16
|
+
...result,
|
|
17
|
+
triage: {
|
|
18
|
+
...result.triage,
|
|
19
|
+
instructions: [...instructions.slice(0, -1), `Rerun Shepherd now on \`${target}\`.`],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { withApiTelemetryScope } from "../../github/api-telemetry.mjs";
|
|
2
2
|
import { attachApiUsage } from "./api-usage.mjs";
|
|
3
|
+
import { withTriageRerunTarget } from "./rerun-command.mjs";
|
|
3
4
|
export function withIterateApiUsage(opts, runCore) {
|
|
4
5
|
return withApiTelemetryScope(async () => {
|
|
5
6
|
const result = await runCore();
|
|
6
|
-
return attachApiUsage(result, opts.deferQuotaWarning !== true, false, opts.quotaWarningMinimumPollIntervalMinutes);
|
|
7
|
+
return attachApiUsage(withTriageRerunTarget(result, opts.targetRepository !== undefined), opts.deferQuotaWarning !== true, false, opts.quotaWarningMinimumPollIntervalMinutes);
|
|
7
8
|
});
|
|
8
9
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IterateCommandOptions, IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
|
|
1
|
+
import type { IterateCommandOptions, IterateDeferredWork, IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
|
|
2
2
|
import type { NormalizedBotUsernames } from "../../comments/authors.mts";
|
|
3
3
|
interface HandleTriageContext {
|
|
4
4
|
base: IterateResultBase;
|
|
@@ -21,9 +21,13 @@ interface HandleTriageContext {
|
|
|
21
21
|
botUsernames: NormalizedBotUsernames;
|
|
22
22
|
/** Failing checks whose external blocker has merged or closed. */
|
|
23
23
|
releasedCheckNames?: ReadonlySet<string>;
|
|
24
|
+
/** Released check name → the blocker that merged or closed, shown on the check. */
|
|
25
|
+
releasedCheckBlockers?: ReadonlyMap<string, string>;
|
|
24
26
|
ruleAutoResolveThreadIds?: string[];
|
|
25
27
|
/** Verified stack-repair guidance, when ancestry is stale. */
|
|
26
28
|
repairInstructions?: string[];
|
|
29
|
+
/** Review work a queued conflict holds back until the PR leaves the queue. */
|
|
30
|
+
deferredWork?: IterateDeferredWork;
|
|
27
31
|
}
|
|
28
32
|
export declare function handleTriage(ctx: HandleTriageContext): Promise<IterateResult>;
|
|
29
33
|
export {};
|
|
@@ -11,12 +11,14 @@ import { hasLogEvidence } from "./check-evidence.mjs";
|
|
|
11
11
|
import { currentEjectionCommit, } from "./queue-recovery-instructions.mjs";
|
|
12
12
|
import { buildReleasedBlockerInstruction, buildReviewSubsetInstruction, } from "./check-instructions.mjs";
|
|
13
13
|
import { buildNativeStackLayerRebase } from "./native-stack-rebase.mjs";
|
|
14
|
+
import { buildDequeueCommand } from "./merge-plan.mjs";
|
|
15
|
+
import { renderShellCommand } from "../../cli/runner.mjs";
|
|
14
16
|
import { lookupUpperLayerTrunkConflict } from "./stack-trunk-conflict.mjs";
|
|
15
17
|
import { lookupMergedBasePullRequests } from "./merged-base-pull-requests.mjs";
|
|
16
18
|
import { conflictingHeadCiNote, countReportedChecks, insertConflictingHeadCiNote, } from "./conflicting-head-ci.mjs";
|
|
17
19
|
import { conflictingHeadFirstSeenUnix } from "../../state/conflicting-head-seen.mjs";
|
|
18
20
|
import { applyStallGuard } from "./stall.mjs";
|
|
19
|
-
import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
21
|
+
import { annotationMarkerBody, checksWithActionableAnnotations, isQueuedConflict, } from "../check-annotations.mjs";
|
|
20
22
|
import { threadTranscriptBody } from "../../threads/transcript.mjs";
|
|
21
23
|
import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs";
|
|
22
24
|
import { canRerunWorkflows } from "../../checks/conclusions.mjs";
|
|
@@ -86,7 +88,7 @@ export async function handleTriage(ctx) {
|
|
|
86
88
|
return { ...result, escalate: { ...result.escalate, reviewItems } };
|
|
87
89
|
}
|
|
88
90
|
async function handleTriageTick(ctx) {
|
|
89
|
-
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, releasedCheckNames = EMPTY_RELEASED, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
|
|
91
|
+
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, releasedCheckNames = EMPTY_RELEASED, releasedCheckBlockers, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
|
|
90
92
|
const prReference = formatPrUrl(report.repo, prNumber);
|
|
91
93
|
const failingChecks = report.checks.failing;
|
|
92
94
|
const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
|
|
@@ -178,8 +180,10 @@ async function handleTriageTick(ctx) {
|
|
|
178
180
|
: []));
|
|
179
181
|
const initialAttemptRunIds = new Set(failingChecks.flatMap((c) => (c.runId !== null && c.runAttempt === 1 ? [c.runId] : [])));
|
|
180
182
|
const failingAgentChecks = toAgentChecks(failingChecks).map((c) => {
|
|
181
|
-
if (releasedCheckNames.has(c.name))
|
|
182
|
-
|
|
183
|
+
if (releasedCheckNames.has(c.name)) {
|
|
184
|
+
const releasedBlocker = releasedCheckBlockers?.get(c.name);
|
|
185
|
+
return releasedBlocker ? { ...c, releasedBlocker } : c;
|
|
186
|
+
}
|
|
183
187
|
return rerunAuthorized &&
|
|
184
188
|
// Rerunning queue CI cannot restore a removed entry and overwrites its failure evidence.
|
|
185
189
|
c.scope !== "merge_group" &&
|
|
@@ -201,6 +205,7 @@ async function handleTriageTick(ctx) {
|
|
|
201
205
|
];
|
|
202
206
|
const { changesRequestedReviews } = report;
|
|
203
207
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
208
|
+
const queuedConflict = isQueuedConflict(report.mergeQueue?.inQueue, report.mergeStatus.status);
|
|
204
209
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
205
210
|
const { "behind-base-hint": behindBaseHint, instructions: configuredInstructions } = loadConfig().iterate;
|
|
206
211
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
@@ -340,14 +345,26 @@ async function handleTriageTick(ctx) {
|
|
|
340
345
|
const stackRebase = hasConflicts || (isBehind && exhaustedAttempts.length > 0) || queueEjection
|
|
341
346
|
? buildNativeStackLayerRebase(report.repo, { number: prNumber, baseBranch: baseLookup.branch }, stack, trunkConflict, report.stackBottomPr)
|
|
342
347
|
: undefined;
|
|
343
|
-
const
|
|
348
|
+
const dequeueCommand = queuedConflict
|
|
349
|
+
? renderShellCommand(buildDequeueCommand(report.nodeId))
|
|
350
|
+
: undefined;
|
|
351
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks.filter((check) => !releasedCheckNames.has(check.name)), changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, canGenerateGithubMutation(report.viewerAuthorization?.viewerCanUpdate, "ready"), exhaustedAttempts.length > 0, stackRebase, queueEjection, opts.instructions ?? configuredInstructions, dequeueCommand);
|
|
344
352
|
if (recovery && "unavailable" in recovery)
|
|
345
353
|
instructions.unshift(recovery.unavailable);
|
|
346
354
|
if (acknowledgmentRecovery && "unavailable" in acknowledgmentRecovery)
|
|
347
355
|
instructions.unshift(acknowledgmentRecovery.unavailable);
|
|
356
|
+
// A queued conflict offers a wait path, so every later branch-mutating step is path (a), and
|
|
357
|
+
// leading repair steps follow the dequeue-or-wait choice instead of preceding it.
|
|
358
|
+
const pathA = (step) => (dequeueCommand ? `If you chose (a): ${step}` : step);
|
|
359
|
+
const prependBranchSteps = (steps) => {
|
|
360
|
+
if (!dequeueCommand)
|
|
361
|
+
return void instructions.unshift(...steps);
|
|
362
|
+
const choice = instructions.findIndex((step) => step.includes(dequeueCommand));
|
|
363
|
+
instructions.splice(choice + 1, 0, ...steps.map(pathA));
|
|
364
|
+
};
|
|
348
365
|
if (failingAgentChecks.some((check) => releasedCheckNames.has(check.name))) {
|
|
349
366
|
const completion = instructions.pop();
|
|
350
|
-
instructions.push(buildReleasedBlockerInstruction(prNumber));
|
|
367
|
+
instructions.push(pathA(buildReleasedBlockerInstruction(prNumber)));
|
|
351
368
|
if (completion !== undefined)
|
|
352
369
|
instructions.push(completion);
|
|
353
370
|
}
|
|
@@ -357,15 +374,16 @@ async function handleTriageTick(ctx) {
|
|
|
357
374
|
if (completion !== undefined)
|
|
358
375
|
instructions.push(completion);
|
|
359
376
|
}
|
|
360
|
-
if (repairInstructions && repairInstructions.length > 0)
|
|
361
|
-
|
|
362
|
-
}
|
|
377
|
+
if (repairInstructions && repairInstructions.length > 0)
|
|
378
|
+
prependBranchSteps(repairInstructions);
|
|
363
379
|
if (mergedBasePullRequests.length > 0) {
|
|
364
380
|
// `gh pr edit` goes through GraphQL; REST callers retarget with the pulls PATCH endpoint.
|
|
365
381
|
const retarget = report.transport === "rest"
|
|
366
382
|
? `gh api --method PATCH repos/${report.repo}/pulls/${prNumber} -f base=<verified-parent-base>`
|
|
367
383
|
: `gh pr edit ${prReference} --base <verified-parent-base>`;
|
|
368
|
-
|
|
384
|
+
prependBranchSteps([
|
|
385
|
+
`Inspect every PR under \`## Merged PRs matching the current base\`. If this PR is the remaining layer intended for a merged parent's base branch, run \`${retarget}\` after replacing \`<verified-parent-base>\` with that parent's shell-quoted base branch, then rerun Shepherd immediately and follow its fresh instructions instead of the remaining steps here. Otherwise keep the current base and follow the remaining conflict-resolution steps.`,
|
|
386
|
+
]);
|
|
369
387
|
}
|
|
370
388
|
const checkRunCount = countReportedChecks(report.checks);
|
|
371
389
|
const suitesEmpty = report.headCheckSuitesEmpty === true;
|
|
@@ -404,12 +422,15 @@ async function handleTriageTick(ctx) {
|
|
|
404
422
|
firstLookThreads,
|
|
405
423
|
firstLookComments,
|
|
406
424
|
...(report.reviewItems && { reviewItems: report.reviewItems }),
|
|
425
|
+
...(ctx.deferredWork && { deferredWork: ctx.deferredWork }),
|
|
407
426
|
},
|
|
408
427
|
};
|
|
409
428
|
const result = await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds);
|
|
410
429
|
if (result.action === "triage" && opts.persistSeen !== false) {
|
|
411
430
|
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
412
|
-
|
|
431
|
+
// A queued conflict's annotation fixes are path (a); waiting keeps them unseen.
|
|
432
|
+
if (!queuedConflict)
|
|
433
|
+
await Promise.allSettled(result.triage.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
|
|
413
434
|
}
|
|
414
435
|
return result;
|
|
415
436
|
}
|
|
@@ -35,6 +35,9 @@ export function explicitInstructions(result, rerunCommand) {
|
|
|
35
35
|
"2. Follow each selected one-PR poll's `## Instructions` until it returns `CANCEL` or `ESCALATE`.",
|
|
36
36
|
`3. Run ${again} again after selected work completes; one row's \`ESCALATE\` does not stop work on other rows. ${KEEP_WATCHING}`,
|
|
37
37
|
];
|
|
38
|
+
if (result.prs.some((item) => item.action === "escalate") && !isHandedOffSelection(result)) {
|
|
39
|
+
instructions[0] = `${instructions[0]} Report each \`ESCALATE\` row's reasons to the user now and leave that PR alone; keep working on the other rows.`;
|
|
40
|
+
}
|
|
38
41
|
if (onlyDraftVerificationSelection(result)) {
|
|
39
42
|
instructions[2] = `3. If every draft verification probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, stop this selection and report any \`ESCALATE\` rows; leave the drafts unchanged. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow the probes' instructions and rerun ${again} when that work completes.`;
|
|
40
43
|
}
|
|
@@ -140,7 +140,7 @@ function planStack(result, mergeRequested, rerunCommand) {
|
|
|
140
140
|
action: "cancel",
|
|
141
141
|
stackMergeable: false,
|
|
142
142
|
instructions: [
|
|
143
|
-
`1. Stop shepherding this stack. Automatic mark-ready is disabled and ${work.held.map((item) => `PR #${item.pr}`).join(", ")} ${work.held.length === 1 ? "is" : "are"} clean but still in draft.`,
|
|
143
|
+
`1. Stop shepherding this stack. Automatic mark-ready is disabled and ${work.held.map((item) => `PR #${item.pr}`).join(", ")} ${work.held.length === 1 ? "is" : "are"} clean but still in draft. Leave ${work.held.length === 1 ? "it" : "them"} in draft and end the turn; do not mark ${work.held.length === 1 ? "it" : "them"} ready or suggest doing so, because that is the human's decision.`,
|
|
144
144
|
],
|
|
145
145
|
};
|
|
146
146
|
}
|
|
@@ -11,6 +11,8 @@ export interface ResolveResult {
|
|
|
11
11
|
rateLimit?: ResolveRateLimitStop;
|
|
12
12
|
sessionRefusal?: string;
|
|
13
13
|
instructions?: string[];
|
|
14
|
+
/** IDs GitHub denied (403/404); Shepherd omits them from later commands until edited. */
|
|
15
|
+
deniedIds?: string[];
|
|
14
16
|
unrepliedThreads?: string[];
|
|
15
17
|
unresolvedThreads?: string[];
|
|
16
18
|
unminimizedComments?: string[];
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -86,6 +86,11 @@ export async function applyResolveOptions(pr, repo, opts, preflight) {
|
|
|
86
86
|
"Restore GitHub access for this session using the proxy instructions above.",
|
|
87
87
|
"Retry only the pending IDs listed above.",
|
|
88
88
|
];
|
|
89
|
+
else if (result.deniedIds?.length)
|
|
90
|
+
result.instructions = [
|
|
91
|
+
`GitHub denied ${result.deniedIds.map((id) => `\`${id}\``).join(", ")}. Shepherd omits ${result.deniedIds.length === 1 ? "it" : "them"} from later commands until edited, so do not retry, escalate, or route around ${result.deniedIds.length === 1 ? "it" : "them"}.`,
|
|
92
|
+
"Rerun Shepherd with the same options and continue from its output.",
|
|
93
|
+
];
|
|
89
94
|
return result;
|
|
90
95
|
}
|
|
91
96
|
export async function autoResolveThreads(threadIds) {
|
|
@@ -192,6 +197,8 @@ async function bulkApplyChunk(resolveIds, replyIds, minimizeIds, dismissIds, dis
|
|
|
192
197
|
}), { mutation: true });
|
|
193
198
|
if ("restStopped" in resp)
|
|
194
199
|
restStopped = resp.restStopped === true;
|
|
200
|
+
if ("deniedIds" in resp && Array.isArray(resp.deniedIds) && resp.deniedIds.length)
|
|
201
|
+
(result.deniedIds ??= []).push(...resp.deniedIds);
|
|
195
202
|
if ("sessionRefusal" in resp && typeof resp.sessionRefusal === "string")
|
|
196
203
|
result.sessionRefusal = resp.sessionRefusal;
|
|
197
204
|
data = resp.data;
|
|
@@ -108,8 +108,10 @@ export async function applyRestReviewChunk(input) {
|
|
|
108
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
109
109
|
result.errors.push({ message, path: [op.alias] });
|
|
110
110
|
const stop = rateLimitFromError(error, message);
|
|
111
|
-
if (!stop && error instanceof GitHubRequestError && [403, 404].includes(error.status))
|
|
111
|
+
if (!stop && error instanceof GitHubRequestError && [403, 404].includes(error.status)) {
|
|
112
112
|
await rememberDenied(input, op.id, op.kind);
|
|
113
|
+
(result.deniedIds ??= []).push(op.id);
|
|
114
|
+
}
|
|
113
115
|
if (error instanceof GitHubRequestError) {
|
|
114
116
|
result.rateLimit = error.rateLimit;
|
|
115
117
|
result.retryAfterSeconds = error.retryAfterSeconds;
|
|
@@ -4,7 +4,7 @@ import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
|
4
4
|
type Routed = Pick<PollSummaryItem, "action" | "reasons">;
|
|
5
5
|
/**
|
|
6
6
|
* The one-PR check treats unseen annotations on any non-SUCCESS check (failing, skipped,
|
|
7
|
-
* filtered
|
|
7
|
+
* filtered or neutral) as actionable work; `checks.ignore` checks never gate, annotations included. The compact summary only has annotation
|
|
8
8
|
* totals, and only for a row that otherwise claims readiness, so it loads them then and reads
|
|
9
9
|
* the annotations of just the non-SUCCESS checks that have any. A row with unseen annotations,
|
|
10
10
|
* or whose annotations cannot be read, goes to its one-PR `pollCommand` instead of
|
|
@@ -11,7 +11,7 @@ const READY_CLAIMS = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
/**
|
|
13
13
|
* The one-PR check treats unseen annotations on any non-SUCCESS check (failing, skipped,
|
|
14
|
-
* filtered
|
|
14
|
+
* filtered or neutral) as actionable work; `checks.ignore` checks never gate, annotations included. The compact summary only has annotation
|
|
15
15
|
* totals, and only for a row that otherwise claims readiness, so it loads them then and reads
|
|
16
16
|
* the annotations of just the non-SUCCESS checks that have any. A row with unseen annotations,
|
|
17
17
|
* or whose annotations cannot be read, goes to its one-PR `pollCommand` instead of
|
|
@@ -44,7 +44,7 @@ function nonSuccessCheckTotals(raw, required, totalsKnown) {
|
|
|
44
44
|
for (const check of classifiedSummaryChecks(raw, required)) {
|
|
45
45
|
if (check.scope || check.id == null || !head.has(check.id))
|
|
46
46
|
continue;
|
|
47
|
-
if (
|
|
47
|
+
if (["in_progress", "superseded", "ignored"].includes(check.category))
|
|
48
48
|
continue;
|
|
49
49
|
if (check.status !== "COMPLETED" || check.conclusion === "SUCCESS")
|
|
50
50
|
continue;
|
package/bin/types/iterate.d.mts
CHANGED
|
@@ -149,6 +149,8 @@ interface TriageDetails {
|
|
|
149
149
|
shown: number;
|
|
150
150
|
total: number;
|
|
151
151
|
};
|
|
152
|
+
/** Review work a queued conflicting PR holds back until it leaves the queue. Omitted when empty. */
|
|
153
|
+
deferredWork?: import("./merge-queue.mts").IterateDeferredWork;
|
|
152
154
|
}
|
|
153
155
|
export interface IterateResultTriage extends IterateResultBase {
|
|
154
156
|
action: "triage";
|
package/bin/types/report.d.mts
CHANGED
|
@@ -69,8 +69,6 @@ export interface ShepherdReport {
|
|
|
69
69
|
skipped: ClassifiedCheck[];
|
|
70
70
|
/** Checks filtered out because they were triggered by a non-PR event (push, schedule, etc.). */
|
|
71
71
|
filtered: ClassifiedCheck[];
|
|
72
|
-
/** Ignored checks with unseen annotations; omitted when empty. */
|
|
73
|
-
ignored?: ClassifiedCheck[];
|
|
74
72
|
filteredNames: string[];
|
|
75
73
|
blockedByFilteredCheck: boolean;
|
|
76
74
|
ignoredNames?: string[];
|
|
@@ -215,6 +213,8 @@ export interface AgentCheck {
|
|
|
215
213
|
rerunCommand?: string;
|
|
216
214
|
/** Workflow-run attempt number, surfaced only after the initial attempt. */
|
|
217
215
|
runAttempt?: number;
|
|
216
|
+
/** `owner/name#number` of a recorded blocker that has merged or closed. */
|
|
217
|
+
releasedBlocker?: string;
|
|
218
218
|
annotations?: CheckAnnotation[];
|
|
219
219
|
annotationOnly?: true;
|
|
220
220
|
scope?: "merge_group";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"husky": "^9.1.7",
|
|
92
92
|
"knip": "^6.14.1",
|
|
93
93
|
"marked": "^18.0.11",
|
|
94
|
-
"oxfmt": "^0.
|
|
94
|
+
"oxfmt": "^0.75.0",
|
|
95
95
|
"oxlint": "^1.60.0",
|
|
96
96
|
"typescript": "^7.0.2",
|
|
97
97
|
"vitest": "^5.0.0"
|
|
Binary file
|