pr-shepherd 0.43.0 → 0.44.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 +18 -17
- package/bin/api.d.mts +3 -2
- package/bin/api.mjs +13 -18
- package/bin/cli/args.mjs +13 -9
- package/bin/cli/fix-formatter.mjs +8 -6
- package/bin/cli/help-command-pages.d.mts +9 -6
- package/bin/cli/help-command-pages.mjs +7 -4
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +2 -2
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +10 -7
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +5 -5
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/journal-formatter.mjs +3 -0
- package/bin/cli/journal-handler.mjs +11 -7
- package/bin/cli/list-formatters.d.mts +3 -1
- package/bin/cli/list-formatters.mjs +9 -7
- package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -1
- package/bin/cli/mutate-formatter.mjs +4 -0
- package/bin/commands/check.mjs +22 -6
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +2 -2
- package/bin/commands/commit-suggestion.mjs +3 -1
- package/bin/commands/iterate/check-instructions.d.mts +5 -19
- package/bin/commands/iterate/check-instructions.mjs +24 -25
- package/bin/commands/iterate/classify.d.mts +2 -2
- package/bin/commands/iterate/classify.mjs +43 -25
- package/bin/commands/iterate/escalate.d.mts +1 -1
- package/bin/commands/iterate/escalate.mjs +11 -0
- package/bin/commands/iterate/fix-code.mjs +81 -42
- package/bin/commands/iterate/helpers.d.mts +0 -2
- package/bin/commands/iterate/helpers.mjs +0 -15
- package/bin/commands/iterate/index.mjs +6 -12
- package/bin/commands/iterate/mark-ready.d.mts +2 -0
- package/bin/commands/iterate/mark-ready.mjs +38 -0
- package/bin/commands/iterate/merge-state.mjs +38 -9
- package/bin/commands/iterate/merge.mjs +0 -1
- package/bin/commands/iterate/render.d.mts +2 -2
- package/bin/commands/iterate/render.mjs +19 -20
- package/bin/commands/iterate/stall.mjs +4 -2
- package/bin/commands/iterate/thread-mutation-routing.d.mts +9 -0
- package/bin/commands/iterate/thread-mutation-routing.mjs +29 -0
- package/bin/commands/journal/index.d.mts +5 -0
- package/bin/commands/journal/index.mjs +11 -2
- package/bin/commands/mark-files-as-viewed.d.mts +2 -1
- package/bin/commands/mark-files-as-viewed.mjs +5 -92
- package/bin/commands/resolve-mutate.mjs +51 -9
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/commands/suggestion-patch-item.d.mts +1 -1
- package/bin/commands/suggestion-patch-item.mjs +3 -11
- package/bin/commands/suggestion-patches.mjs +3 -3
- package/bin/comments/authors.d.mts +6 -0
- package/bin/comments/authors.mjs +5 -0
- package/bin/comments/marker.d.mts +6 -1
- package/bin/comments/marker.mjs +8 -1
- package/bin/comments/resolve.d.mts +4 -0
- package/bin/comments/thread-visibility.mjs +10 -10
- package/bin/comments/visible-comments.mjs +2 -1
- package/bin/config/load.d.mts +1 -1
- package/bin/github/batch-parsers.d.mts +2 -2
- package/bin/github/batch-parsers.mjs +17 -1
- package/bin/github/batch-raw-types.d.mts +12 -0
- package/bin/github/batch.mjs +1 -1
- package/bin/github/client.d.mts +1 -0
- package/bin/github/client.mjs +8 -1
- package/bin/github/gql/batch-pr-page.gql +6 -0
- package/bin/github/gql/batch-pr.gql +13 -0
- package/bin/github/gql/get-pr-body.gql +1 -0
- package/bin/github/gql/review-thread-comments.gql +1 -0
- package/bin/github/gql/suggestion-threads.gql +1 -0
- package/bin/github/suggestion-thread.mjs +1 -0
- package/bin/mcp/server.mjs +12 -6
- package/bin/pr-reference.d.mts +13 -0
- package/bin/pr-reference.mjs +23 -1
- package/bin/reporters/agent.mjs +5 -0
- package/bin/threads/transcript.d.mts +2 -0
- package/bin/threads/transcript.mjs +2 -0
- package/bin/types/agent-thread.d.mts +1 -0
- package/bin/types/escalate.d.mts +6 -1
- package/bin/types/github.d.mts +19 -0
- package/bin/types/iterate.d.mts +9 -8
- package/bin/types/report.d.mts +14 -1
- package/bin/types/review-thread.d.mts +1 -0
- package/bin/types/suggestion-patch.d.mts +1 -1
- package/package.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/skills/mark-files-as-viewed/SKILL.md +3 -3
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +15 -13
- package/bin/commands/iterate/reruns.d.mts +0 -20
- package/bin/commands/iterate/reruns.mjs +0 -105
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
2
2
|
import { shouldMinimizeAuthor } from "../../comments/minimize-policy.mjs";
|
|
3
3
|
import { isConfiguredBotAuthor, isHumanAuthor, } from "../../comments/authors.mjs";
|
|
4
|
+
import { buildThreadMutationRouting } from "./thread-mutation-routing.mjs";
|
|
4
5
|
function dedupeIds(ids) {
|
|
5
6
|
const seen = new Set();
|
|
6
7
|
const out = [];
|
|
@@ -14,7 +15,8 @@ function dedupeIds(ids) {
|
|
|
14
15
|
}
|
|
15
16
|
export function classifyReviewSummaries(summaries, approvals, minimizeApprovals, minimizeComments = "all", botUsernames = new Set(), unresolvedThreads = [], ruleAutoResolveIds = []) {
|
|
16
17
|
const blockedReviewIds = new Set(unresolvedThreads.flatMap((t) => (t.reviewId !== undefined ? [t.reviewId] : [])));
|
|
17
|
-
const eligible = (r) =>
|
|
18
|
+
const eligible = (r) => r.viewerCanMinimize === true &&
|
|
19
|
+
shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames) &&
|
|
18
20
|
!blockedReviewIds.has(r.id);
|
|
19
21
|
// First-look summaries still need one tick to surface their body to the agent,
|
|
20
22
|
// so their minimize IDs ride in the agent-facing apply command. Seen summaries
|
|
@@ -34,7 +36,8 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
|
|
|
34
36
|
if (minimizeApprovals) {
|
|
35
37
|
const surfacedApprovals = [];
|
|
36
38
|
for (const r of approvals) {
|
|
37
|
-
if (
|
|
39
|
+
if (r.viewerCanMinimize === true &&
|
|
40
|
+
shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames))
|
|
38
41
|
minimizeIds.push(r.id);
|
|
39
42
|
else
|
|
40
43
|
surfacedApprovals.push(r);
|
|
@@ -55,35 +58,45 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
|
|
|
55
58
|
surfacedApprovals: approvals,
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
|
-
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks,
|
|
61
|
+
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prReference, botUsernames = new Set(), ruleAutoResolveThreadIds = [], viewerAuthorization, authorizationThreads = []) {
|
|
59
62
|
const allThreads = [...threads, ...resolutionOnlyThreads];
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
const routed = buildThreadMutationRouting(allThreads, botUsernames, ruleAutoResolveThreadIds);
|
|
64
|
+
const canReply = new Set(authorizationThreads
|
|
65
|
+
.filter((thread) => thread.viewerCanReply === true)
|
|
66
|
+
.map((thread) => thread.id));
|
|
67
|
+
const canResolve = new Set(authorizationThreads
|
|
68
|
+
.filter((thread) => thread.viewerCanResolve === true)
|
|
69
|
+
.map((thread) => thread.id));
|
|
70
|
+
const replyThreadIds = routed.replyThreadIds.filter((id) => canReply.has(id));
|
|
71
|
+
// Viewer-authored human resolves stay paired with an authorized reply. Marker-ended
|
|
72
|
+
// viewer-authored retries and bot/non-human resolves need only resolve authorization.
|
|
73
|
+
const pairedResolveThreadIds = routed.pairedResolveThreadIds.filter((id) => canReply.has(id) && canResolve.has(id));
|
|
74
|
+
const standaloneResolveThreadIds = routed.standaloneResolveThreadIds.filter((id) => canResolve.has(id));
|
|
75
|
+
const resolveThreadIds = dedupeIds([...pairedResolveThreadIds, ...standaloneResolveThreadIds]);
|
|
70
76
|
// Bot/non-human CHANGES_REQUESTED reviews are auto-dismissed after the agent pushes a fix.
|
|
71
77
|
// Human reviews are left for the reviewer to re-review or dismiss themselves.
|
|
72
|
-
const dismissReviewIds = dedupeIds(
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
const dismissReviewIds = dedupeIds(viewerAuthorization?.viewerCanAdminister === true
|
|
79
|
+
? reviews
|
|
80
|
+
.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames))
|
|
81
|
+
.map((r) => r.id)
|
|
82
|
+
: []);
|
|
75
83
|
const hasReply = replyThreadIds.length > 0;
|
|
76
84
|
const hasDismiss = dismissReviewIds.length > 0;
|
|
77
85
|
// Mutations that require --message: replies (to human threads) and dismissals (of bot CR reviews).
|
|
78
86
|
const hasMessageMutations = hasReply || hasDismiss;
|
|
79
87
|
const hasResolveOrMinimize = resolveThreadIds.length > 0 || allCommentIds.length > 0;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
88
|
+
const hasStandaloneResolveOrMinimize = standaloneResolveThreadIds.length > 0 || allCommentIds.length > 0;
|
|
89
|
+
if (hasMessageMutations && hasStandaloneResolveOrMinimize) {
|
|
90
|
+
// Split: message-bearing mutations plus their paired viewer-authored resolves ride in
|
|
91
|
+
// resolveArgv; standalone resolve/minimize mutations go in resolveOnlyArgv so they can
|
|
92
|
+
// run without SHA or message.
|
|
93
|
+
const resolveArgv = buildPrShepherdCommand(["apply", "review", String(prReference)]).argv;
|
|
84
94
|
if (replyThreadIds.length > 0) {
|
|
85
95
|
resolveArgv.push("--reply-thread-ids", replyThreadIds.join(","));
|
|
86
96
|
}
|
|
97
|
+
if (pairedResolveThreadIds.length > 0) {
|
|
98
|
+
resolveArgv.push("--resolve-thread-ids", pairedResolveThreadIds.join(","));
|
|
99
|
+
}
|
|
87
100
|
resolveArgv.push("--message", "$DISMISS_MESSAGE");
|
|
88
101
|
if (hasDismiss) {
|
|
89
102
|
resolveArgv.push("--dismiss-review-ids", dismissReviewIds.join(","));
|
|
@@ -96,12 +109,15 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
96
109
|
requiresHeadSha,
|
|
97
110
|
requiresDismissMessage: true,
|
|
98
111
|
...(replyThreadIds.length > 0 ? { replyThreadIds } : undefined),
|
|
112
|
+
...(pairedResolveThreadIds.length > 0
|
|
113
|
+
? { resolveThreadIds: pairedResolveThreadIds }
|
|
114
|
+
: undefined),
|
|
99
115
|
...(hasDismiss ? { dismissReviewIds } : undefined),
|
|
100
116
|
hasMutations: true,
|
|
101
117
|
};
|
|
102
|
-
const resolveOnlyArgv = buildPrShepherdCommand(["apply", "review", String(
|
|
103
|
-
if (
|
|
104
|
-
resolveOnlyArgv.push("--resolve-thread-ids",
|
|
118
|
+
const resolveOnlyArgv = buildPrShepherdCommand(["apply", "review", String(prReference)]).argv;
|
|
119
|
+
if (standaloneResolveThreadIds.length > 0) {
|
|
120
|
+
resolveOnlyArgv.push("--resolve-thread-ids", standaloneResolveThreadIds.join(","));
|
|
105
121
|
}
|
|
106
122
|
if (allCommentIds.length > 0) {
|
|
107
123
|
resolveOnlyArgv.push("--minimize-comment-ids", allCommentIds.join(","));
|
|
@@ -110,13 +126,15 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
110
126
|
argv: resolveOnlyArgv,
|
|
111
127
|
requiresHeadSha: false,
|
|
112
128
|
requiresDismissMessage: false,
|
|
113
|
-
...(
|
|
129
|
+
...(standaloneResolveThreadIds.length > 0
|
|
130
|
+
? { resolveThreadIds: standaloneResolveThreadIds }
|
|
131
|
+
: undefined),
|
|
114
132
|
hasMutations: true,
|
|
115
133
|
};
|
|
116
134
|
return { resolveCommand, resolveOnlyCommand };
|
|
117
135
|
}
|
|
118
136
|
// Single command: all mutations combined (or only one category present).
|
|
119
|
-
const argv = buildPrShepherdCommand(["apply", "review", String(
|
|
137
|
+
const argv = buildPrShepherdCommand(["apply", "review", String(prReference)]).argv;
|
|
120
138
|
if (replyThreadIds.length > 0) {
|
|
121
139
|
argv.push("--reply-thread-ids", replyThreadIds.join(","));
|
|
122
140
|
argv.push("--message", "$DISMISS_MESSAGE");
|
|
@@ -26,7 +26,7 @@ interface BaseBranchLookup {
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function formatDurationApprox(seconds: number): string;
|
|
28
28
|
export declare function validateBaseBranch(raw: string): BaseBranchLookup;
|
|
29
|
-
export declare function buildEscalateHumanMessage(escalate: Omit<EscalateDetails, "humanMessage">, pr: number, opts?: {
|
|
29
|
+
export declare function buildEscalateHumanMessage(escalate: Omit<EscalateDetails, "humanMessage">, pr: string | number, opts?: {
|
|
30
30
|
merge?: boolean;
|
|
31
31
|
}): string;
|
|
32
32
|
export declare function buildEscalateSuggestion(triggers: EscalateTrigger[], detail?: string): string;
|
|
@@ -121,6 +121,14 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
121
121
|
if (removal.beforeCommitOid)
|
|
122
122
|
lines.push(`- queue commit: \`${removal.beforeCommitOid}\``);
|
|
123
123
|
}
|
|
124
|
+
if (escalate.authorization && escalate.authorization.length > 0) {
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push("## Authorization");
|
|
127
|
+
lines.push("");
|
|
128
|
+
for (const item of escalate.authorization) {
|
|
129
|
+
lines.push(`- ${item.action}: ${item.targetIds.map((id) => `\`${id}\``).join(", ")} — GitHub denied the action or did not expose a confirming capability`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
124
132
|
if (escalate.thrashHistory && escalate.thrashHistory.length > 0) {
|
|
125
133
|
lines.push("");
|
|
126
134
|
lines.push("## Fix attempts");
|
|
@@ -136,6 +144,9 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
136
144
|
return lines.join("\n");
|
|
137
145
|
}
|
|
138
146
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
147
|
+
if (triggers.includes("authorization-required")) {
|
|
148
|
+
return "GitHub did not confirm that the current viewer may perform one or more required actions. Ask a repository maintainer to handle the listed items; Shepherd will not recommend commands that would be denied.";
|
|
149
|
+
}
|
|
139
150
|
if (triggers.includes("merge-queue-removed")) {
|
|
140
151
|
const reason = detail ? ` GitHub reason: ${detail}.` : "";
|
|
141
152
|
return `The PR left the merge queue without an actionable check failure.${reason} Confirm the removal was not intentional before adding it again.`;
|
|
@@ -5,14 +5,14 @@ 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 } from "./thread-mutation-routing.mjs";
|
|
8
9
|
import { buildFixInstructions } from "./render.mjs";
|
|
9
10
|
import { applyStallGuard } from "./stall.mjs";
|
|
10
|
-
import { tryCancelRun, buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./helpers.mjs";
|
|
11
11
|
import { annotationMarkerBody, checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
12
12
|
import { threadTranscriptBody } from "../../threads/transcript.mjs";
|
|
13
13
|
import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs";
|
|
14
14
|
import { loadConfig } from "../../config/load.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
16
16
|
function nextFixAttempts(stored, headSha, threads) {
|
|
17
17
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
18
18
|
const threadBodyHashes = stored?.threadBodyHashes
|
|
@@ -30,9 +30,73 @@ function nextFixAttempts(stored, headSha, threads) {
|
|
|
30
30
|
}
|
|
31
31
|
export async function handleFixCode(ctx) {
|
|
32
32
|
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, } = ctx;
|
|
33
|
+
const prReference = formatPrUrl(report.repo, prNumber);
|
|
33
34
|
const failingChecks = report.checks.failing;
|
|
34
35
|
const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
|
|
35
|
-
const
|
|
36
|
+
const allThreads = [...report.threads.actionable, ...report.threads.resolutionOnly];
|
|
37
|
+
const ruleAutoResolveIds = new Set(ruleAutoResolveThreadIds ?? []);
|
|
38
|
+
const routedThreadMutations = buildThreadMutationRouting(allThreads, botUsernames, [
|
|
39
|
+
...ruleAutoResolveIds,
|
|
40
|
+
]);
|
|
41
|
+
const replyIdSet = new Set(routedThreadMutations.replyThreadIds);
|
|
42
|
+
const resolveIdSet = new Set(routedThreadMutations.resolveThreadIds);
|
|
43
|
+
const unauthorizedReplies = allThreads.filter((thread) => report.viewerAuthorization !== undefined &&
|
|
44
|
+
replyIdSet.has(thread.id) &&
|
|
45
|
+
thread.viewerCanReply !== true);
|
|
46
|
+
const unauthorizedResolves = allThreads.filter((thread) => report.viewerAuthorization !== undefined &&
|
|
47
|
+
resolveIdSet.has(thread.id) &&
|
|
48
|
+
thread.viewerCanResolve !== true);
|
|
49
|
+
const unauthorizedDismissals = report.changesRequestedReviews.filter((review) => report.viewerAuthorization !== undefined &&
|
|
50
|
+
(!isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames)) &&
|
|
51
|
+
report.viewerAuthorization?.viewerCanAdminister !== true);
|
|
52
|
+
const authorization = [
|
|
53
|
+
...(unauthorizedReplies.length > 0
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
action: "reply-thread",
|
|
57
|
+
targetIds: unauthorizedReplies.map((thread) => thread.id),
|
|
58
|
+
reason: "denied-or-unverifiable",
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: []),
|
|
62
|
+
...(unauthorizedResolves.length > 0
|
|
63
|
+
? [
|
|
64
|
+
{
|
|
65
|
+
action: "resolve-thread",
|
|
66
|
+
targetIds: unauthorizedResolves.map((thread) => thread.id),
|
|
67
|
+
reason: "denied-or-unverifiable",
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
...(unauthorizedDismissals.length > 0
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
action: "dismiss-review",
|
|
75
|
+
targetIds: unauthorizedDismissals.map((review) => review.id),
|
|
76
|
+
reason: "denied-or-unverifiable",
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
80
|
+
];
|
|
81
|
+
if (authorization.length > 0) {
|
|
82
|
+
const authorizationEscalateBase = {
|
|
83
|
+
triggers: ["authorization-required"],
|
|
84
|
+
unresolvedThreads: allThreads.map(toAgentThread),
|
|
85
|
+
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
86
|
+
changesRequestedReviews: report.changesRequestedReviews,
|
|
87
|
+
authorization,
|
|
88
|
+
suggestion: buildEscalateSuggestion(["authorization-required"]),
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
...base,
|
|
92
|
+
action: "escalate",
|
|
93
|
+
escalate: {
|
|
94
|
+
...authorizationEscalateBase,
|
|
95
|
+
humanMessage: buildEscalateHumanMessage(authorizationEscalateBase, prReference),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const protectedRuns = [];
|
|
36
100
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
37
101
|
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha, report.threads.actionable);
|
|
38
102
|
const botCrReviews = report.changesRequestedReviews.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames));
|
|
@@ -56,7 +120,7 @@ export async function handleFixCode(ctx) {
|
|
|
56
120
|
action: "escalate",
|
|
57
121
|
escalate: {
|
|
58
122
|
...escalateBase,
|
|
59
|
-
humanMessage: buildEscalateHumanMessage(escalateBase,
|
|
123
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
60
124
|
},
|
|
61
125
|
};
|
|
62
126
|
}
|
|
@@ -75,18 +139,14 @@ export async function handleFixCode(ctx) {
|
|
|
75
139
|
action: "escalate",
|
|
76
140
|
escalate: {
|
|
77
141
|
...escalateBase,
|
|
78
|
-
humanMessage: buildEscalateHumanMessage(escalateBase,
|
|
142
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
79
143
|
},
|
|
80
144
|
};
|
|
81
145
|
}
|
|
82
146
|
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const results = await Promise.all(runIds.map((id) => tryCancelRun(id, repoOwner, repoName)));
|
|
87
|
-
cancelled = results.filter((id) => id !== null);
|
|
88
|
-
}
|
|
89
|
-
const cancelledSet = new Set(cancelled);
|
|
147
|
+
// GitHub does not expose a per-run viewer capability for cancellation. Fail closed:
|
|
148
|
+
// do not issue or recommend an Actions mutation based only on repository role.
|
|
149
|
+
const cancelled = [];
|
|
90
150
|
const baseLookup = validateBaseBranch(report.baseBranch);
|
|
91
151
|
const threads = report.threads.actionable.map(toAgentThread);
|
|
92
152
|
const resolutionOnlyThreads = report.threads.resolutionOnly;
|
|
@@ -103,21 +163,15 @@ export async function handleFixCode(ctx) {
|
|
|
103
163
|
// Only surface in-progress runs when a push is plausible — resolution-only and
|
|
104
164
|
// summary-only iterations have no path to a push, so listing runs would prompt
|
|
105
165
|
// unnecessary cancellation.
|
|
106
|
-
const
|
|
107
|
-
failingAgentChecks.length > 0 ||
|
|
108
|
-
annotatedExtra.length > 0 ||
|
|
109
|
-
hasConflicts ||
|
|
110
|
-
changesRequestedReviews.length > 0 ||
|
|
111
|
-
actionableComments.length > 0;
|
|
112
|
-
const inProgressRunIds = pushLikely
|
|
113
|
-
? buildInProgressRunIds(report, cancelledSet, {
|
|
114
|
-
suppressProtectedFreshReruns: false,
|
|
115
|
-
protectedRunIds,
|
|
116
|
-
})
|
|
117
|
-
: [];
|
|
166
|
+
const inProgressRunIds = [];
|
|
118
167
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
119
168
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
120
|
-
|
|
169
|
+
// Conflict resolution necessarily changes the branch head, but Shepherd cannot verify
|
|
170
|
+
// authorization for the local Git credential that would publish it. Defer review mutations
|
|
171
|
+
// until an authorized push updates the PR and a fresh iteration can rebuild SHA-safe commands.
|
|
172
|
+
const { resolveCommand, resolveOnlyCommand } = hasConflicts
|
|
173
|
+
? buildResolveCommand([], [], [], [], [], prReference, botUsernames, [], undefined, [])
|
|
174
|
+
: buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads);
|
|
121
175
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
122
176
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
123
177
|
// prediction that the agent *will* push. Intentionally broader than `pushLikely` above:
|
|
@@ -142,27 +196,13 @@ export async function handleFixCode(ctx) {
|
|
|
142
196
|
action: "escalate",
|
|
143
197
|
escalate: {
|
|
144
198
|
...fallbackEscalateBase,
|
|
145
|
-
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase,
|
|
199
|
+
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase, prReference),
|
|
146
200
|
},
|
|
147
201
|
};
|
|
148
202
|
}
|
|
149
203
|
const firstLookThreads = report.threads.firstLook;
|
|
150
204
|
const firstLookComments = report.comments.firstLook;
|
|
151
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts,
|
|
152
|
-
const hasMergeGroupFailure = failingChecks.some((check) => check.scope === "merge_group");
|
|
153
|
-
const needsRequeue = Boolean(hasMergeGroupFailure || report.mergeQueue?.inQueue || report.mergeQueue?.latestRemoval);
|
|
154
|
-
const requeue = opts.merge && needsRequeue
|
|
155
|
-
? buildMergeCommandPlan({
|
|
156
|
-
pr: report.pr,
|
|
157
|
-
repo: report.repo,
|
|
158
|
-
nodeId: report.nodeId,
|
|
159
|
-
headSha: "$HEAD_SHA",
|
|
160
|
-
queue: true,
|
|
161
|
-
})
|
|
162
|
-
: undefined;
|
|
163
|
-
if (requeue) {
|
|
164
|
-
instructions.splice(Math.max(0, instructions.length - 1), 0, "After all fixes and review mutations, replace `$HEAD_SHA` in the requeue commands with the full pushed PR-head SHA (or `$(git rev-parse HEAD)`). If GitHub no longer reports the PR in the merge queue, run the `requeue:` command shown above; if the gh CLI reports that auto-merge is disabled, run `requeue API fallback:` instead.");
|
|
165
|
-
}
|
|
205
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true);
|
|
166
206
|
const prospectiveResult = {
|
|
167
207
|
...base,
|
|
168
208
|
baseBranch: baseLookup.branch,
|
|
@@ -184,7 +224,6 @@ export async function handleFixCode(ctx) {
|
|
|
184
224
|
firstLookComments,
|
|
185
225
|
inProgressRunIds,
|
|
186
226
|
protectedRuns,
|
|
187
|
-
...(requeue && { requeue }),
|
|
188
227
|
},
|
|
189
228
|
cancelled,
|
|
190
229
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ActiveCheck, IterateResult, IterateResultBase, IterateResultSummary, RelevantCheck, ShepherdReport } from "../../types.mts";
|
|
2
|
-
export { buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./reruns.mts";
|
|
3
2
|
export declare function buildSummary(report: ShepherdReport): IterateResultSummary;
|
|
4
3
|
/** Non-blocking check-name lists (ignored/superseded), omitted from the result when empty. */
|
|
5
4
|
export declare function buildSuppressedCheckFields(report: ShepherdReport): Pick<IterateResultBase, "ignoredNames" | "supersededNames">;
|
|
@@ -8,7 +7,6 @@ export declare function buildTerminalCancelResult(report: ShepherdReport): Itera
|
|
|
8
7
|
/** Build completed, non-skipped checks relevant to PR readiness. */
|
|
9
8
|
export declare function buildRelevantChecks(report: ShepherdReport): RelevantCheck[];
|
|
10
9
|
export declare function buildActiveChecks(report: ShepherdReport): ActiveCheck[];
|
|
11
|
-
export declare function tryCancelRun(runId: string, owner: string, repo: string): Promise<string | null>;
|
|
12
10
|
export declare function getCurrentHeadSha(): Promise<string | null>;
|
|
13
11
|
export declare function buildWaitLog(base: IterateResultBase): string;
|
|
14
12
|
export declare function blockedCancelNote(base: IterateResultBase): string;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { execFile as execFileCb } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { rest } from "../../github/http.mjs";
|
|
4
3
|
import { getExecutionCwd } from "../../execution-context.mjs";
|
|
5
4
|
import { blockedReasonFromRequirements } from "../../merge-status/requirements-format.mjs";
|
|
6
5
|
const execFile = promisify(execFileCb);
|
|
7
|
-
export { buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./reruns.mjs";
|
|
8
6
|
export function buildSummary(report) {
|
|
9
7
|
return {
|
|
10
8
|
passing: report.checks.passing.length,
|
|
@@ -106,19 +104,6 @@ export function buildActiveChecks(report) {
|
|
|
106
104
|
...(c.commitOid !== undefined && { commitOid: c.commitOid }),
|
|
107
105
|
}));
|
|
108
106
|
}
|
|
109
|
-
export async function tryCancelRun(runId, owner, repo) {
|
|
110
|
-
try {
|
|
111
|
-
await rest("POST", `/repos/${owner}/${repo}/actions/runs/${runId}/cancel`);
|
|
112
|
-
return runId;
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
-
if (/409|already completed|cannot cancel a workflow run that is completed/i.test(msg))
|
|
117
|
-
return null;
|
|
118
|
-
process.stderr.write(`pr-shepherd: cancel run ${runId} failed (ignored): ${msg}\n`);
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
107
|
export async function getCurrentHeadSha() {
|
|
123
108
|
try {
|
|
124
109
|
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { runCheck } from "../check.mjs";
|
|
2
2
|
import { updateReadyDelay } from "../ready-delay.mjs";
|
|
3
3
|
import { getCurrentPrNumber } from "../../github/client.mjs";
|
|
4
|
-
import { graphql } from "../../github/http.mjs";
|
|
5
|
-
import { MARK_PR_READY_MUTATION } from "../../github/queries.mjs";
|
|
6
4
|
import { loadConfig } from "../../config/load.mjs";
|
|
7
5
|
import { EXIT, ShepherdError } from "../../exit-codes.mjs";
|
|
8
6
|
import { getCurrentHeadSha, buildWaitLog, buildTerminalCancelResult, blockedCancelNote, } from "./helpers.mjs";
|
|
@@ -15,6 +13,7 @@ import { autoMinimizeComments } from "../../comments/resolve.mjs";
|
|
|
15
13
|
import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
16
14
|
import { buildReadyMergeResult, handleActiveMergeState } from "./merge-state.mjs";
|
|
17
15
|
import { buildIterateBase } from "./base.mjs";
|
|
16
|
+
import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
18
17
|
export async function runIterate(opts) {
|
|
19
18
|
const config = loadConfig();
|
|
20
19
|
const botUsernames = normalizeBotUsernames(config.botUsernames);
|
|
@@ -76,7 +75,8 @@ export async function runIterate(opts) {
|
|
|
76
75
|
report.mergeStatus.status === "CONFLICTS" ||
|
|
77
76
|
reviewSummaryIds.length > 0 ||
|
|
78
77
|
firstLookSummaries.length > 0 ||
|
|
79
|
-
editedSummaries.length > 0
|
|
78
|
+
editedSummaries.length > 0 ||
|
|
79
|
+
(config.iterate.minimizeApprovals && surfacedApprovals.length > 0);
|
|
80
80
|
const activeMerge = Boolean(opts.merge && (report.mergeQueue?.inQueue || report.mergeQueue?.autoMergeRequest));
|
|
81
81
|
const isCleanReadyHandoff = report.status === "READY" && !hasActionableWork && !activeMerge;
|
|
82
82
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyHandoff, readyDelaySeconds, repoOwner, repoName);
|
|
@@ -113,15 +113,9 @@ export async function runIterate(opts) {
|
|
|
113
113
|
const canMarkReady = report.status === "READY" &&
|
|
114
114
|
report.mergeStatus.isDraft &&
|
|
115
115
|
!report.mergeStatus.blockingBotReviewInProgress;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
...base,
|
|
120
|
-
action: "mark_ready",
|
|
121
|
-
markedReady: true,
|
|
122
|
-
log: `MARKED READY: PR #${report.pr} converted from draft to ready for review`,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
116
|
+
const markReadyResult = await markReadyIfAuthorized(canMarkReady && !opts.noAutoMarkReady && config.actions.autoMarkReady, base, report);
|
|
117
|
+
if (markReadyResult)
|
|
118
|
+
return markReadyResult;
|
|
125
119
|
if (readyState.shouldCancel) {
|
|
126
120
|
await clearStallState(stallKey);
|
|
127
121
|
const mergeResult = buildReadyMergeResult(opts.merge, true, base, report);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { graphql } from "../../github/http.mjs";
|
|
2
|
+
import { MARK_PR_READY_MUTATION } from "../../github/queries.mjs";
|
|
3
|
+
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
4
|
+
export async function markReadyIfAuthorized(enabled, base, report) {
|
|
5
|
+
if (!enabled)
|
|
6
|
+
return null;
|
|
7
|
+
if (report.viewerAuthorization?.viewerCanUpdate === true) {
|
|
8
|
+
await graphql(MARK_PR_READY_MUTATION, { pullRequestId: report.nodeId });
|
|
9
|
+
return {
|
|
10
|
+
...base,
|
|
11
|
+
action: "mark_ready",
|
|
12
|
+
markedReady: true,
|
|
13
|
+
log: `MARKED READY: PR #${report.pr} converted from draft to ready for review`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const escalation = {
|
|
17
|
+
triggers: ["authorization-required"],
|
|
18
|
+
unresolvedThreads: [],
|
|
19
|
+
ambiguousComments: [],
|
|
20
|
+
changesRequestedReviews: [],
|
|
21
|
+
authorization: [
|
|
22
|
+
{
|
|
23
|
+
action: "mark-ready",
|
|
24
|
+
targetIds: [report.nodeId],
|
|
25
|
+
reason: "denied-or-unverifiable",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
suggestion: buildEscalateSuggestion(["authorization-required"]),
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
action: "escalate",
|
|
33
|
+
escalate: {
|
|
34
|
+
...escalation,
|
|
35
|
+
humanMessage: buildEscalateHumanMessage(escalation, report.pr),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
2
2
|
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
3
|
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
4
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
4
5
|
export function buildReadyMergeResult(enabled, readyElapsed, base, report) {
|
|
5
6
|
if (!enabled || !readyElapsed || report.mergeStatus.isDraft)
|
|
6
7
|
return null;
|
|
7
8
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
8
9
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
10
|
+
if (!queue && report.viewerAuthorization?.viewerCanEnableAutoMerge === true) {
|
|
11
|
+
return {
|
|
12
|
+
...base,
|
|
13
|
+
action: "merge",
|
|
14
|
+
merge: buildMergeCommandPlan({
|
|
15
|
+
pr: report.pr,
|
|
16
|
+
repo: report.repo,
|
|
17
|
+
nodeId: report.nodeId,
|
|
18
|
+
headSha: report.headSha ?? "unknown",
|
|
19
|
+
queue: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const authorizationEscalateBase = {
|
|
24
|
+
triggers: ["authorization-required"],
|
|
25
|
+
unresolvedThreads: [],
|
|
26
|
+
ambiguousComments: [],
|
|
27
|
+
changesRequestedReviews: [],
|
|
28
|
+
authorization: [
|
|
29
|
+
{
|
|
30
|
+
action: "merge-or-enqueue",
|
|
31
|
+
targetIds: [report.nodeId],
|
|
32
|
+
reason: "denied-or-unverifiable",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
suggestion: buildEscalateSuggestion(["authorization-required"]),
|
|
36
|
+
};
|
|
9
37
|
return {
|
|
10
38
|
...base,
|
|
11
|
-
action: "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}),
|
|
39
|
+
action: "escalate",
|
|
40
|
+
escalate: {
|
|
41
|
+
...authorizationEscalateBase,
|
|
42
|
+
humanMessage: buildEscalateHumanMessage(authorizationEscalateBase, report.pr, {
|
|
43
|
+
merge: true,
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
19
46
|
};
|
|
20
47
|
}
|
|
21
48
|
export async function handleActiveMergeState(input) {
|
|
@@ -47,7 +74,9 @@ export async function handleActiveMergeState(input) {
|
|
|
47
74
|
action: "escalate",
|
|
48
75
|
escalate: {
|
|
49
76
|
...escalateBase,
|
|
50
|
-
humanMessage: buildEscalateHumanMessage(escalateBase, report.pr, {
|
|
77
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
|
|
78
|
+
merge: true,
|
|
79
|
+
}),
|
|
51
80
|
},
|
|
52
81
|
};
|
|
53
82
|
}
|
|
@@ -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,
|
|
5
|
-
isBehind?: boolean): string[];
|
|
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
|
+
isBehind?: boolean, viewerCanUpdate?: boolean): string[];
|