pr-shepherd 0.42.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 +24 -18
- package/bin/api.d.mts +3 -2
- package/bin/api.mjs +13 -18
- package/bin/checks/classify.d.mts +3 -1
- package/bin/checks/classify.mjs +5 -2
- package/bin/cli/args.mjs +14 -9
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/fix-formatter.mjs +20 -8
- package/bin/cli/handlers.mjs +1 -0
- 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 +9 -4
- 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-flags.d.mts +1 -0
- package/bin/cli/iterate-flags.mjs +2 -0
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +9 -6
- package/bin/cli/iterate-instructions.mjs +15 -0
- package/bin/cli/iterate-lean.d.mts +1 -2
- package/bin/cli/iterate-lean.mjs +16 -17
- package/bin/cli/iterate-merge-formatter.d.mts +3 -0
- package/bin/cli/iterate-merge-formatter.mjs +52 -0
- package/bin/cli/iterate-verbose.d.mts +6 -0
- package/bin/cli/iterate-verbose.mjs +14 -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/cli/poll-handler.mjs +1 -0
- package/bin/cli/runner.mjs +4 -3
- package/bin/commands/check-terminal-report.mjs +1 -0
- package/bin/commands/check.mjs +64 -7
- 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/base.d.mts +5 -0
- package/bin/commands/iterate/base.mjs +25 -0
- 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 +3 -1
- package/bin/commands/iterate/escalate.mjs +29 -2
- package/bin/commands/iterate/fix-code.mjs +81 -26
- package/bin/commands/iterate/helpers.d.mts +0 -2
- package/bin/commands/iterate/helpers.mjs +6 -17
- package/bin/commands/iterate/index.mjs +31 -43
- 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.d.mts +15 -0
- package/bin/commands/iterate/merge-state.mjs +82 -0
- package/bin/commands/iterate/merge.d.mts +13 -0
- package/bin/commands/iterate/merge.mjs +45 -0
- 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/poll.mjs +3 -1
- 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 +5 -1
- package/bin/config/load.mjs +34 -0
- package/bin/config/merge-command-args.d.mts +2 -0
- package/bin/config/merge-command-args.mjs +44 -0
- package/bin/config.json +3 -0
- package/bin/exit-codes.d.mts +2 -0
- package/bin/exit-codes.mjs +4 -0
- package/bin/github/batch-parse-checks.d.mts +3 -0
- package/bin/github/batch-parse-checks.mjs +29 -0
- package/bin/github/batch-parsers-rules.d.mts +3 -1
- package/bin/github/batch-parsers-rules.mjs +33 -0
- package/bin/github/batch-parsers.d.mts +2 -2
- package/bin/github/batch-parsers.mjs +40 -27
- package/bin/github/batch-raw-rules.d.mts +48 -0
- package/bin/github/batch-raw-types.d.mts +12 -0
- package/bin/github/batch-raw-types.mjs +0 -1
- package/bin/github/batch.mjs +3 -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 +105 -0
- package/bin/github/gql/commit-check-contexts.gql +58 -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/merge-queue-checks.d.mts +4 -0
- package/bin/github/merge-queue-checks.mjs +48 -0
- package/bin/github/queries.d.mts +2 -0
- package/bin/github/queries.mjs +2 -0
- package/bin/github/suggestion-thread.mjs +1 -0
- package/bin/mcp/server.mjs +13 -6
- package/bin/pr-reference.d.mts +13 -0
- package/bin/pr-reference.mjs +23 -1
- package/bin/reporters/agent.mjs +11 -3
- package/bin/threads/transcript.d.mts +2 -0
- package/bin/threads/transcript.mjs +2 -0
- package/bin/types/activity.d.mts +2 -0
- package/bin/types/agent-thread.d.mts +1 -0
- package/bin/types/escalate.d.mts +35 -0
- package/bin/types/escalate.mjs +1 -0
- package/bin/types/github.d.mts +27 -0
- package/bin/types/github.mjs +0 -3
- package/bin/types/iterate.d.mts +23 -39
- package/bin/types/merge-action.d.mts +12 -0
- package/bin/types/merge-action.mjs +1 -0
- package/bin/types/merge-queue.d.mts +13 -0
- package/bin/types/merge-queue.mjs +1 -0
- package/bin/types/merge-requirements.d.mts +17 -0
- package/bin/types/report.d.mts +22 -1
- package/bin/types/review-thread.d.mts +1 -0
- package/bin/types/suggestion-patch.d.mts +1 -1
- package/bin/types.d.mts +3 -0
- package/bin/types.mjs +3 -0
- package/bin/util/markdown.d.mts +2 -0
- package/bin/util/markdown.mjs +7 -0
- 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 +17 -15
- package/bin/commands/iterate/reruns.d.mts +0 -20
- package/bin/commands/iterate/reruns.mjs +0 -105
|
@@ -5,13 +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 { formatPrUrl } from "../../pr-reference.mjs";
|
|
15
16
|
function nextFixAttempts(stored, headSha, threads) {
|
|
16
17
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
17
18
|
const threadBodyHashes = stored?.threadBodyHashes
|
|
@@ -29,9 +30,73 @@ function nextFixAttempts(stored, headSha, threads) {
|
|
|
29
30
|
}
|
|
30
31
|
export async function handleFixCode(ctx) {
|
|
31
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);
|
|
32
34
|
const failingChecks = report.checks.failing;
|
|
33
35
|
const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
|
|
34
|
-
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 = [];
|
|
35
100
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
36
101
|
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha, report.threads.actionable);
|
|
37
102
|
const botCrReviews = report.changesRequestedReviews.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames));
|
|
@@ -55,7 +120,7 @@ export async function handleFixCode(ctx) {
|
|
|
55
120
|
action: "escalate",
|
|
56
121
|
escalate: {
|
|
57
122
|
...escalateBase,
|
|
58
|
-
humanMessage: buildEscalateHumanMessage(escalateBase,
|
|
123
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
59
124
|
},
|
|
60
125
|
};
|
|
61
126
|
}
|
|
@@ -74,18 +139,14 @@ export async function handleFixCode(ctx) {
|
|
|
74
139
|
action: "escalate",
|
|
75
140
|
escalate: {
|
|
76
141
|
...escalateBase,
|
|
77
|
-
humanMessage: buildEscalateHumanMessage(escalateBase,
|
|
142
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
78
143
|
},
|
|
79
144
|
};
|
|
80
145
|
}
|
|
81
146
|
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const results = await Promise.all(runIds.map((id) => tryCancelRun(id, repoOwner, repoName)));
|
|
86
|
-
cancelled = results.filter((id) => id !== null);
|
|
87
|
-
}
|
|
88
|
-
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 = [];
|
|
89
150
|
const baseLookup = validateBaseBranch(report.baseBranch);
|
|
90
151
|
const threads = report.threads.actionable.map(toAgentThread);
|
|
91
152
|
const resolutionOnlyThreads = report.threads.resolutionOnly;
|
|
@@ -102,21 +163,15 @@ export async function handleFixCode(ctx) {
|
|
|
102
163
|
// Only surface in-progress runs when a push is plausible — resolution-only and
|
|
103
164
|
// summary-only iterations have no path to a push, so listing runs would prompt
|
|
104
165
|
// unnecessary cancellation.
|
|
105
|
-
const
|
|
106
|
-
failingAgentChecks.length > 0 ||
|
|
107
|
-
annotatedExtra.length > 0 ||
|
|
108
|
-
hasConflicts ||
|
|
109
|
-
changesRequestedReviews.length > 0 ||
|
|
110
|
-
actionableComments.length > 0;
|
|
111
|
-
const inProgressRunIds = pushLikely
|
|
112
|
-
? buildInProgressRunIds(report, cancelledSet, {
|
|
113
|
-
suppressProtectedFreshReruns: false,
|
|
114
|
-
protectedRunIds,
|
|
115
|
-
})
|
|
116
|
-
: [];
|
|
166
|
+
const inProgressRunIds = [];
|
|
117
167
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
118
168
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
119
|
-
|
|
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);
|
|
120
175
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
121
176
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
122
177
|
// prediction that the agent *will* push. Intentionally broader than `pushLikely` above:
|
|
@@ -141,13 +196,13 @@ export async function handleFixCode(ctx) {
|
|
|
141
196
|
action: "escalate",
|
|
142
197
|
escalate: {
|
|
143
198
|
...fallbackEscalateBase,
|
|
144
|
-
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase,
|
|
199
|
+
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase, prReference),
|
|
145
200
|
},
|
|
146
201
|
};
|
|
147
202
|
}
|
|
148
203
|
const firstLookThreads = report.threads.firstLook;
|
|
149
204
|
const firstLookComments = report.comments.firstLook;
|
|
150
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts,
|
|
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);
|
|
151
206
|
const prospectiveResult = {
|
|
152
207
|
...base,
|
|
153
208
|
baseBranch: baseLookup.branch,
|
|
@@ -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,
|
|
@@ -65,6 +63,8 @@ export function buildRelevantChecks(report) {
|
|
|
65
63
|
runId: c.runId,
|
|
66
64
|
detailsUrl: c.detailsUrl || null,
|
|
67
65
|
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
66
|
+
...(c.scope !== undefined && { scope: c.scope }),
|
|
67
|
+
...(c.commitOid !== undefined && { commitOid: c.commitOid }),
|
|
68
68
|
summary: c.summary,
|
|
69
69
|
},
|
|
70
70
|
];
|
|
@@ -85,6 +85,8 @@ export function buildRelevantChecks(report) {
|
|
|
85
85
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
86
86
|
...(c.logExcerpt !== undefined && { logExcerpt: c.logExcerpt }),
|
|
87
87
|
...(c.annotations !== undefined && { annotations: c.annotations }),
|
|
88
|
+
...(c.scope !== undefined && { scope: c.scope }),
|
|
89
|
+
...(c.commitOid !== undefined && { commitOid: c.commitOid }),
|
|
88
90
|
},
|
|
89
91
|
];
|
|
90
92
|
});
|
|
@@ -98,23 +100,10 @@ export function buildActiveChecks(report) {
|
|
|
98
100
|
detailsUrl: c.detailsUrl || null,
|
|
99
101
|
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
100
102
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
103
|
+
...(c.scope !== undefined && { scope: c.scope }),
|
|
104
|
+
...(c.commitOid !== undefined && { commitOid: c.commitOid }),
|
|
101
105
|
}));
|
|
102
106
|
}
|
|
103
|
-
// Best-effort: cancelling a completed run is a no-op, not an error.
|
|
104
|
-
export async function tryCancelRun(runId, owner, repo) {
|
|
105
|
-
try {
|
|
106
|
-
await rest("POST", `/repos/${owner}/${repo}/actions/runs/${runId}/cancel`);
|
|
107
|
-
return runId;
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
111
|
-
// GitHub returns 409 when the run reached a terminal state — expected, not worth logging.
|
|
112
|
-
if (/409|already completed|cannot cancel a workflow run that is completed/i.test(msg))
|
|
113
|
-
return null;
|
|
114
|
-
process.stderr.write(`pr-shepherd: cancel run ${runId} failed (ignored): ${msg}\n`);
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
107
|
export async function getCurrentHeadSha() {
|
|
119
108
|
try {
|
|
120
109
|
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], {
|
|
@@ -1,11 +1,9 @@
|
|
|
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
|
-
import { getCurrentHeadSha,
|
|
6
|
+
import { getCurrentHeadSha, buildWaitLog, buildTerminalCancelResult, blockedCancelNote, } from "./helpers.mjs";
|
|
9
7
|
import { classifyReviewSummaries } from "./classify.mjs";
|
|
10
8
|
import { applyStallGuard } from "./stall.mjs";
|
|
11
9
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
@@ -13,6 +11,9 @@ import { handleFixCode } from "./fix-code.mjs";
|
|
|
13
11
|
import { normalizeBotUsernames } from "../../comments/authors.mjs";
|
|
14
12
|
import { autoMinimizeComments } from "../../comments/resolve.mjs";
|
|
15
13
|
import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
14
|
+
import { buildReadyMergeResult, handleActiveMergeState } from "./merge-state.mjs";
|
|
15
|
+
import { buildIterateBase } from "./base.mjs";
|
|
16
|
+
import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
16
17
|
export async function runIterate(opts) {
|
|
17
18
|
const config = loadConfig();
|
|
18
19
|
const botUsernames = normalizeBotUsernames(config.botUsernames);
|
|
@@ -74,40 +75,12 @@ export async function runIterate(opts) {
|
|
|
74
75
|
report.mergeStatus.status === "CONFLICTS" ||
|
|
75
76
|
reviewSummaryIds.length > 0 ||
|
|
76
77
|
firstLookSummaries.length > 0 ||
|
|
77
|
-
editedSummaries.length > 0
|
|
78
|
-
|
|
78
|
+
editedSummaries.length > 0 ||
|
|
79
|
+
(config.iterate.minimizeApprovals && surfacedApprovals.length > 0);
|
|
80
|
+
const activeMerge = Boolean(opts.merge && (report.mergeQueue?.inQueue || report.mergeQueue?.autoMergeRequest));
|
|
81
|
+
const isCleanReadyHandoff = report.status === "READY" && !hasActionableWork && !activeMerge;
|
|
79
82
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyHandoff, readyDelaySeconds, repoOwner, repoName);
|
|
80
|
-
const base =
|
|
81
|
-
pr: report.pr,
|
|
82
|
-
repo: report.repo,
|
|
83
|
-
status: report.status,
|
|
84
|
-
state: report.mergeStatus.state,
|
|
85
|
-
mergeStateStatus: report.mergeStatus.mergeStateStatus,
|
|
86
|
-
mergeStatus: report.mergeStatus.status,
|
|
87
|
-
reviewDecision: report.mergeStatus.reviewDecision,
|
|
88
|
-
blockingBotReviewInProgress: report.mergeStatus.blockingBotReviewInProgress,
|
|
89
|
-
isDraft: report.mergeStatus.isDraft,
|
|
90
|
-
shouldCancel: readyState.shouldCancel,
|
|
91
|
-
remainingSeconds: readyState.remainingSeconds,
|
|
92
|
-
summary: buildSummary(report),
|
|
93
|
-
baseBranch: report.baseBranch,
|
|
94
|
-
branchProtection: report.branchProtection,
|
|
95
|
-
mergeRequirements: report.mergeStatus.mergeRequirements,
|
|
96
|
-
checks: buildRelevantChecks(report),
|
|
97
|
-
inProgressChecks: buildActiveChecks(report),
|
|
98
|
-
...buildSuppressedCheckFields(report),
|
|
99
|
-
activity: report.activity,
|
|
100
|
-
};
|
|
101
|
-
if (readyState.shouldCancel) {
|
|
102
|
-
await clearStallState(stallKey);
|
|
103
|
-
const cancelNote = blockedCancelNote(base);
|
|
104
|
-
return {
|
|
105
|
-
...base,
|
|
106
|
-
action: "cancel",
|
|
107
|
-
reason: "ready-delay-elapsed",
|
|
108
|
-
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
83
|
+
const base = buildIterateBase(report, readyState);
|
|
111
84
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
112
85
|
if (hasActionableWork) {
|
|
113
86
|
return handleFixCode({
|
|
@@ -128,17 +101,32 @@ export async function runIterate(opts) {
|
|
|
128
101
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
129
102
|
});
|
|
130
103
|
}
|
|
104
|
+
const mergeStateResult = await handleActiveMergeState({
|
|
105
|
+
enabled: opts.merge,
|
|
106
|
+
active: activeMerge,
|
|
107
|
+
base,
|
|
108
|
+
report,
|
|
109
|
+
stallKey,
|
|
110
|
+
});
|
|
111
|
+
if (mergeStateResult)
|
|
112
|
+
return mergeStateResult;
|
|
131
113
|
const canMarkReady = report.status === "READY" &&
|
|
132
114
|
report.mergeStatus.isDraft &&
|
|
133
|
-
!report.mergeStatus.blockingBotReviewInProgress
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
115
|
+
!report.mergeStatus.blockingBotReviewInProgress;
|
|
116
|
+
const markReadyResult = await markReadyIfAuthorized(canMarkReady && !opts.noAutoMarkReady && config.actions.autoMarkReady, base, report);
|
|
117
|
+
if (markReadyResult)
|
|
118
|
+
return markReadyResult;
|
|
119
|
+
if (readyState.shouldCancel) {
|
|
120
|
+
await clearStallState(stallKey);
|
|
121
|
+
const mergeResult = buildReadyMergeResult(opts.merge, true, base, report);
|
|
122
|
+
if (mergeResult)
|
|
123
|
+
return mergeResult;
|
|
124
|
+
const cancelNote = blockedCancelNote(base);
|
|
137
125
|
return {
|
|
138
126
|
...base,
|
|
139
|
-
action: "
|
|
140
|
-
|
|
141
|
-
log: `
|
|
127
|
+
action: "cancel",
|
|
128
|
+
reason: "ready-delay-elapsed",
|
|
129
|
+
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
142
130
|
};
|
|
143
131
|
}
|
|
144
132
|
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, { ...base, action: "wait", log: buildWaitLog(base) }, report, reviewSummaryIds);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IterateResult, IterateResultBase, ShepherdReport } from "../../types.mts";
|
|
2
|
+
type StallKey = {
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
pr: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function buildReadyMergeResult(enabled: boolean | undefined, readyElapsed: boolean, base: IterateResultBase, report: ShepherdReport): IterateResult | null;
|
|
8
|
+
export declare function handleActiveMergeState(input: {
|
|
9
|
+
enabled: boolean | undefined;
|
|
10
|
+
active: boolean;
|
|
11
|
+
base: IterateResultBase;
|
|
12
|
+
report: ShepherdReport;
|
|
13
|
+
stallKey: StallKey;
|
|
14
|
+
}): Promise<IterateResult | null>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
2
|
+
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
|
+
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
4
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
|
+
export function buildReadyMergeResult(enabled, readyElapsed, base, report) {
|
|
6
|
+
if (!enabled || !readyElapsed || report.mergeStatus.isDraft)
|
|
7
|
+
return null;
|
|
8
|
+
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
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
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
action: "escalate",
|
|
40
|
+
escalate: {
|
|
41
|
+
...authorizationEscalateBase,
|
|
42
|
+
humanMessage: buildEscalateHumanMessage(authorizationEscalateBase, report.pr, {
|
|
43
|
+
merge: true,
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function handleActiveMergeState(input) {
|
|
49
|
+
const { enabled, active, base, report, stallKey } = input;
|
|
50
|
+
if (enabled && active) {
|
|
51
|
+
await clearStallState(stallKey);
|
|
52
|
+
return {
|
|
53
|
+
...base,
|
|
54
|
+
action: "wait",
|
|
55
|
+
log: report.mergeQueue?.inQueue
|
|
56
|
+
? `WAIT: PR #${report.pr} is in the merge queue`
|
|
57
|
+
: `WAIT: PR #${report.pr} has auto-merge enabled`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const removal = report.mergeQueue?.latestRemoval;
|
|
61
|
+
if (!enabled || !removal || report.mergeQueue?.headUpdatedAfterRemoval)
|
|
62
|
+
return null;
|
|
63
|
+
await clearStallState(stallKey);
|
|
64
|
+
const escalateBase = {
|
|
65
|
+
triggers: ["merge-queue-removed"],
|
|
66
|
+
unresolvedThreads: [],
|
|
67
|
+
ambiguousComments: [],
|
|
68
|
+
changesRequestedReviews: [],
|
|
69
|
+
mergeQueueRemoval: removal,
|
|
70
|
+
suggestion: buildEscalateSuggestion(["merge-queue-removed"], removal.reason ?? "GitHub did not provide a reason"),
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
...base,
|
|
74
|
+
action: "escalate",
|
|
75
|
+
escalate: {
|
|
76
|
+
...escalateBase,
|
|
77
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
|
|
78
|
+
merge: true,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MergeCommandPlan } from "../../types.mts";
|
|
2
|
+
interface MergePlanInput {
|
|
3
|
+
pr: number;
|
|
4
|
+
repo: string;
|
|
5
|
+
nodeId: string;
|
|
6
|
+
headSha: string;
|
|
7
|
+
queue: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildMergeCommandPlan(input: MergePlanInput): MergeCommandPlan;
|
|
10
|
+
export declare function renderMergeCommand(command: {
|
|
11
|
+
argv: string[];
|
|
12
|
+
}): string;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadConfig } from "../../config/load.mjs";
|
|
2
|
+
import { findMergeStrategies } from "../../config/merge-command-args.mjs";
|
|
3
|
+
import { renderShellCommand } from "../../cli/runner.mjs";
|
|
4
|
+
const ENQUEUE_MUTATION = "mutation EnqueuePullRequest($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }";
|
|
5
|
+
export function buildMergeCommandPlan(input) {
|
|
6
|
+
const base = [
|
|
7
|
+
"gh",
|
|
8
|
+
"pr",
|
|
9
|
+
"merge",
|
|
10
|
+
String(input.pr),
|
|
11
|
+
"--repo",
|
|
12
|
+
input.repo,
|
|
13
|
+
"--match-head-commit",
|
|
14
|
+
input.headSha,
|
|
15
|
+
];
|
|
16
|
+
if (input.queue) {
|
|
17
|
+
return {
|
|
18
|
+
mode: "queue",
|
|
19
|
+
command: { argv: base },
|
|
20
|
+
queueApiFallbackCommand: {
|
|
21
|
+
argv: [
|
|
22
|
+
"gh",
|
|
23
|
+
"api",
|
|
24
|
+
"graphql",
|
|
25
|
+
"-f",
|
|
26
|
+
`query=${ENQUEUE_MUTATION}`,
|
|
27
|
+
"-f",
|
|
28
|
+
`pullRequestId=${input.nodeId}`,
|
|
29
|
+
"-f",
|
|
30
|
+
`expectedHeadOid=${input.headSha}`,
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const configuredArgs = loadConfig().merge?.commandArgs ?? [];
|
|
36
|
+
const hasStrategy = findMergeStrategies(configuredArgs).length > 0;
|
|
37
|
+
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs, "--merge"];
|
|
38
|
+
return {
|
|
39
|
+
mode: "auto",
|
|
40
|
+
command: { argv: [...base, "--auto", ...commandArgs] },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function renderMergeCommand(command) {
|
|
44
|
+
return renderShellCommand(command.argv);
|
|
45
|
+
}
|
|
@@ -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[];
|
|
@@ -10,8 +10,8 @@ export function renderResolveCommand(rc) {
|
|
|
10
10
|
parts.push("--require-sha", "$HEAD_SHA");
|
|
11
11
|
return renderShellCommand(parts);
|
|
12
12
|
}
|
|
13
|
-
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts,
|
|
14
|
-
isBehind = false) {
|
|
13
|
+
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
|
+
isBehind = false, viewerCanUpdate = false) {
|
|
15
15
|
const instructions = [];
|
|
16
16
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
17
17
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
@@ -38,13 +38,13 @@ isBehind = false) {
|
|
|
38
38
|
instructions.push(`Review each item ${sectionRef} and decide whether it needs a code change.`);
|
|
39
39
|
}
|
|
40
40
|
if (hasConflicts) {
|
|
41
|
-
instructions.push("The branch has merge conflicts (see `**branch**` above). Resolve them before committing
|
|
41
|
+
instructions.push("The branch has merge conflicts (see `**branch**` above). Resolve them before committing.");
|
|
42
42
|
}
|
|
43
43
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
44
44
|
if (firstLookTotal > 0) {
|
|
45
45
|
instructions.push("Review every item under `## First-look items` before acting.");
|
|
46
46
|
}
|
|
47
|
-
if (firstLookSummaries.length > 0)
|
|
47
|
+
if (firstLookSummaries.length > 0 && viewerCanUpdate)
|
|
48
48
|
instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
|
|
49
49
|
const editedTotal = editedSummaries.length +
|
|
50
50
|
actionableComments.filter((c) => c.edited).length +
|
|
@@ -53,15 +53,13 @@ isBehind = false) {
|
|
|
53
53
|
if (editedTotal > 0) {
|
|
54
54
|
instructions.push("Read every item marked `[edited since first look]`, including edited summaries and edited first-look bullets, before deciding whether to resolve a matching thread.");
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
instructions.push("Do not cancel the IDs under `## Cancelled runs` again. The CLI already cancelled them.");
|
|
61
|
-
}
|
|
56
|
+
// GitHub exposes no exact viewer capability for workflow-run cancellation, so the
|
|
57
|
+
// informational run lists never produce a cancellation recommendation.
|
|
58
|
+
void inProgressRunIds;
|
|
59
|
+
void cancelledCount;
|
|
62
60
|
const hasSuggestions = threads.some((t) => t.suggestion);
|
|
63
61
|
if (hasSuggestions)
|
|
64
|
-
instructions.push(buildCommitSuggestionInstruction(
|
|
62
|
+
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
65
63
|
if (threads.length > 0 || actionableComments.length > 0) {
|
|
66
64
|
// Actionable comments carry no file/line location (unlike threads), so "referenced above"
|
|
67
65
|
// is only accurate when threads are present.
|
|
@@ -83,21 +81,22 @@ isBehind = false) {
|
|
|
83
81
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
84
82
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
85
83
|
if (hasConflicts) {
|
|
86
|
-
instructions.push(`Commit any remaining
|
|
84
|
+
instructions.push(`Commit any remaining conflict-resolution changes${mutationSuffix}.`);
|
|
87
85
|
}
|
|
88
86
|
else if (hasNonConflictHints) {
|
|
89
|
-
instructions.push(
|
|
87
|
+
instructions.push("If you changed code, commit any remaining changes, then stop and hand off for a push whose authorization is established outside Shepherd; do not run the remaining review mutations or iterate until the remote PR head changes. Shepherd cannot verify the Git credential's push authorization. If you did not change code, do not commit and continue with the remaining steps.");
|
|
90
88
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
if (viewerCanUpdate &&
|
|
90
|
+
(hasReviewMutations ||
|
|
91
|
+
hasNonConflictHints ||
|
|
92
|
+
firstLookTotal > 0 ||
|
|
93
|
+
firstLookSummaries.length > 0 ||
|
|
94
|
+
editedTotal > 0)) {
|
|
95
|
+
instructions.push(buildShepherdJournalInstruction(prReference));
|
|
97
96
|
}
|
|
98
97
|
if (resolveOnlyCommand?.hasMutations)
|
|
99
98
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
100
99
|
instructions.push(...buildResolveCommandInstruction(resolveCommand));
|
|
101
|
-
instructions.push(buildFixCompletionInstruction(failingChecks));
|
|
100
|
+
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
102
101
|
return instructions;
|
|
103
102
|
}
|