pr-shepherd 0.45.0 → 0.46.1
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 +20 -9
- package/bin/api.d.mts +2 -2
- package/bin/checks/conclusions.d.mts +0 -16
- package/bin/checks/conclusions.mjs +0 -27
- package/bin/cli/fix-formatter.mjs +10 -4
- package/bin/cli/help-command-pages.d.mts +16 -5
- package/bin/cli/help-command-pages.mjs +16 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -1
- package/bin/cli/help.d.mts +17 -6
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-checks-formatter.d.mts +2 -0
- package/bin/cli/iterate-checks-formatter.mjs +55 -0
- package/bin/cli/iterate-formatter.mjs +7 -2
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/journal-extract-handler.d.mts +2 -0
- package/bin/cli/journal-extract-handler.mjs +48 -0
- package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -6
- package/bin/cli/safe-body-file.d.mts +14 -0
- package/bin/cli/safe-body-file.mjs +42 -0
- package/bin/cli-parser.mjs +7 -0
- package/bin/commands/check-status.mjs +4 -4
- package/bin/commands/check.mjs +25 -5
- package/bin/commands/iterate/api-usage.mjs +1 -8
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +8 -32
- package/bin/commands/iterate/classify.mjs +2 -1
- package/bin/commands/iterate/escalate.mjs +63 -9
- package/bin/commands/iterate/fix-code.mjs +83 -80
- package/bin/commands/iterate/index.mjs +2 -2
- package/bin/commands/iterate/merge-state.mjs +8 -37
- package/bin/commands/iterate/merge.mjs +1 -0
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +16 -13
- package/bin/commands/journal/index.d.mts +1 -0
- package/bin/commands/journal/index.mjs +1 -10
- package/bin/commands/mark-files-as-viewed.d.mts +2 -1
- package/bin/commands/mark-files-as-viewed.mjs +116 -4
- package/bin/commands/poll.mjs +1 -4
- package/bin/commands/resolve-mutate.mjs +17 -40
- package/bin/comments/resolve.d.mts +4 -0
- package/bin/comments/resolve.mjs +10 -4
- package/bin/comments/review-visibility.d.mts +1 -1
- package/bin/comments/review-visibility.mjs +3 -2
- package/bin/comments/thread-visibility.d.mts +1 -1
- package/bin/comments/thread-visibility.mjs +9 -3
- package/bin/github/client.d.mts +1 -2
- package/bin/github/client.mjs +1 -4
- package/bin/github/gql/get-pr-body.gql +0 -1
- package/bin/mcp/server.mjs +2 -2
- package/bin/pr-reference.d.mts +1 -1
- package/bin/pr-reference.mjs +1 -1
- package/bin/types/escalate.d.mts +5 -3
- package/bin/types/report.d.mts +1 -1
- package/package.json +2 -2
- 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 +2 -2
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +21 -15
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
4
|
+
const DEFAULT_DEPENDENCIES = {
|
|
5
|
+
platform: process.platform,
|
|
6
|
+
noFollow: constants.O_NOFOLLOW,
|
|
7
|
+
nonBlock: constants.O_NONBLOCK,
|
|
8
|
+
open,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Reads a regular file after rejecting unsafe final path entries without exposing its contents.
|
|
12
|
+
* Callers must trust parent directories: O_NOFOLLOW only protects the final path entry.
|
|
13
|
+
*/
|
|
14
|
+
export async function readSafeBodyFile(filePath, dependencies = DEFAULT_DEPENDENCIES) {
|
|
15
|
+
const flags = safeOpenFlags(dependencies);
|
|
16
|
+
if (flags === null)
|
|
17
|
+
throw noInput();
|
|
18
|
+
let handle;
|
|
19
|
+
try {
|
|
20
|
+
handle = await dependencies.open(filePath, flags);
|
|
21
|
+
if (!(await handle.stat()).isFile())
|
|
22
|
+
throw new Error("not a regular file");
|
|
23
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw noInput();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await handle?.close().catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function safeOpenFlags(dependencies) {
|
|
33
|
+
if (dependencies.platform === "win32" ||
|
|
34
|
+
dependencies.noFollow === undefined ||
|
|
35
|
+
dependencies.nonBlock === undefined) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return constants.O_RDONLY | dependencies.noFollow | dependencies.nonBlock;
|
|
39
|
+
}
|
|
40
|
+
function noInput() {
|
|
41
|
+
return new ShepherdError("body file could not be read safely", EXIT.NOINPUT);
|
|
42
|
+
}
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { USAGE, helpKeyForArgs, maybePrintHelp } from "./cli/help.mjs";
|
|
|
10
10
|
import { formatMutateResult } from "./cli/formatters.mjs";
|
|
11
11
|
import { handleClean, handleCommitSuggestion, handleSuggestionPatches, handleIterate, handleMarkFilesAsViewed, } from "./cli/handlers.mjs";
|
|
12
12
|
import { handleJournal } from "./cli/journal-handler.mjs";
|
|
13
|
+
import { handleJournalExtract } from "./cli/journal-extract-handler.mjs";
|
|
13
14
|
import { handlePoll } from "./cli/poll-handler.mjs";
|
|
14
15
|
import { warnPrrcThreadIds, validateRequireSha, rejectPrrcMinimizeIds, } from "./cli/resolve-validators.mjs";
|
|
15
16
|
import { setupLog } from "./log/setup.mjs";
|
|
@@ -32,6 +33,12 @@ export async function main(argv) {
|
|
|
32
33
|
await handleLogFile(args.slice(1));
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
36
|
+
// Extraction deliberately precedes legacy warnings and logging: it is a local,
|
|
37
|
+
// GitHub/config/log-free read path for automation that already has a PR body.
|
|
38
|
+
if (subcommand === "journal" && args[1] === "extract") {
|
|
39
|
+
await handleJournalExtract(args.slice(2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
35
42
|
// The public command groups keep admin diagnostics out of the normal
|
|
36
43
|
// command namespace. Keep this before setupLog because log-file reads the
|
|
37
44
|
// log path and must not initialize/tee the log it is reporting.
|
|
@@ -9,15 +9,15 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
9
9
|
return "FAILING";
|
|
10
10
|
if (verdict.anyInProgress)
|
|
11
11
|
return "IN_PROGRESS";
|
|
12
|
-
// BLOCKED with no remaining shepherd work —
|
|
12
|
+
// BLOCKED with no remaining shepherd work — enter ready-delay regardless of why GitHub
|
|
13
13
|
// is BLOCKED (review pending, insufficient approvals, branch-protection rule, etc.).
|
|
14
14
|
// Requires hasChecks so that a PR with zero relevant checks (CI never started, or all
|
|
15
15
|
// filtered/skipped) doesn't prematurely trigger READY before any check has reported.
|
|
16
16
|
// Exception: UNSTABLE with ignored checks — UNSTABLE means only non-required checks are
|
|
17
|
-
// pending/failing, and if those are all ignored the
|
|
17
|
+
// pending/failing, and if those are all ignored the ready state is safe even with no other checks.
|
|
18
18
|
// BLOCKED is excluded from the ignoredNames extension: BLOCKED can mean required checks haven't
|
|
19
|
-
// started, and
|
|
20
|
-
// blockingBotReviewInProgress is still excluded — a bot review is
|
|
19
|
+
// started, and entering READY prematurely there risks a broken merge attempt.
|
|
20
|
+
// blockingBotReviewInProgress is still excluded — a bot review is Shepherd work, not a READY state.
|
|
21
21
|
const hasRelevantPassingChecks = verdict.hasChecks || (mergeStatus.status === "UNSTABLE" && verdict.ignoredNames.length > 0);
|
|
22
22
|
if (verdict.allPassed &&
|
|
23
23
|
hasRelevantPassingChecks &&
|
package/bin/commands/check.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
|
|
|
16
16
|
import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, } from "../comments/review-visibility.mjs";
|
|
17
17
|
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
18
18
|
import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
|
|
19
|
-
import { normalizeBotUsernames } from "../comments/authors.mjs";
|
|
19
|
+
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
20
|
+
import { buildThreadMutationRouting } from "./iterate/thread-mutation-routing.mjs";
|
|
20
21
|
import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
21
22
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
22
23
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
@@ -80,7 +81,17 @@ export async function runCheck(opts) {
|
|
|
80
81
|
const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize !== true));
|
|
81
82
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate.minimizeComments, botUsernames);
|
|
82
83
|
const deniedRuleAutoResolveThreadIds = new Set(partition.ruleAutoResolveThreadIds.filter((id) => batchData.reviewThreads.find((thread) => thread.id === id)?.viewerCanResolve !== true));
|
|
83
|
-
const
|
|
84
|
+
const visibleThreadCandidates = batchData.reviewThreads.filter((t) => !partition.suppressedThreadIds.has(t.id) || deniedRuleAutoResolveThreadIds.has(t.id));
|
|
85
|
+
const threadMutationRouting = buildThreadMutationRouting(visibleThreadCandidates, botUsernames, partition.ruleAutoResolveThreadIds);
|
|
86
|
+
const replyThreadIds = new Set(threadMutationRouting.replyThreadIds);
|
|
87
|
+
const resolveThreadIds = new Set(threadMutationRouting.resolveThreadIds);
|
|
88
|
+
const repeatableThreadIds = new Set(visibleThreadCandidates
|
|
89
|
+
.filter((thread) => thread.path !== null &&
|
|
90
|
+
thread.line !== null &&
|
|
91
|
+
(!replyThreadIds.has(thread.id) || thread.viewerCanReply === true) &&
|
|
92
|
+
(!resolveThreadIds.has(thread.id) || thread.viewerCanResolve === true))
|
|
93
|
+
.map((thread) => thread.id));
|
|
94
|
+
const threadVisibility = classifyThreadVisibility(visibleThreadCandidates, seenMap, botUsernames, repeatableThreadIds);
|
|
84
95
|
const firstLookComments = minimizedCommentCandidates.flatMap((c) => {
|
|
85
96
|
const cls = classifyItem(c.id, c.body, seenMap);
|
|
86
97
|
if (cls === "unchanged")
|
|
@@ -103,7 +114,7 @@ export async function runCheck(opts) {
|
|
|
103
114
|
else
|
|
104
115
|
seenSummaries.push(r);
|
|
105
116
|
}
|
|
106
|
-
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames);
|
|
117
|
+
const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames, batchData.viewerAuthorization?.viewerCanAdminister === true);
|
|
107
118
|
const approvedReviewVisibility = classifyReviewsForDisplay(batchData.approvedReviews, seenMap);
|
|
108
119
|
if (opts.persistSeen !== false) {
|
|
109
120
|
const successfulAnnotations = [
|
|
@@ -145,12 +156,21 @@ export async function runCheck(opts) {
|
|
|
145
156
|
ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize === true),
|
|
146
157
|
};
|
|
147
158
|
const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(authorizedPartition, opts.autoMinimizeSuppressed);
|
|
159
|
+
const visibleMutationThreadIds = new Set([...threadVisibility.activeThreads, ...threadVisibility.resolutionOnlyThreads].map((thread) => thread.id));
|
|
148
160
|
const ruleAutoResolveThreadIds = [
|
|
149
161
|
...authorizedRuleAutoResolveThreadIds,
|
|
150
|
-
...deniedRuleAutoResolveThreadIds,
|
|
162
|
+
...[...deniedRuleAutoResolveThreadIds].filter((id) => visibleMutationThreadIds.has(id)),
|
|
151
163
|
];
|
|
152
164
|
const changesRequestedReviews = changesRequestedReviewVisibility.visible;
|
|
153
|
-
const
|
|
165
|
+
const visibleChangesRequestedIds = new Set(changesRequestedReviews.map((review) => review.id));
|
|
166
|
+
const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((review) => {
|
|
167
|
+
if (partition.suppressedChangesRequestedIds.has(review.id))
|
|
168
|
+
return false;
|
|
169
|
+
const isBot = !isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames);
|
|
170
|
+
return (!isBot ||
|
|
171
|
+
batchData.viewerAuthorization?.viewerCanAdminister === true ||
|
|
172
|
+
visibleChangesRequestedIds.has(review.id));
|
|
173
|
+
}).length;
|
|
154
174
|
const approvedReviews = approvedReviewVisibility.visible;
|
|
155
175
|
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount);
|
|
156
176
|
if (status === "READY" && !didRefreshMergeability) {
|
|
@@ -3,11 +3,7 @@ import { summarizeApiTelemetry } from "../../github/api-telemetry.mjs";
|
|
|
3
3
|
import { evaluateWorktreeGraphqlQuotaWarning } from "../../state/graphql-quota-warnings.mjs";
|
|
4
4
|
import { buildQuotaAwareContinuation } from "../../quota-warning.mjs";
|
|
5
5
|
function shouldWarn(result) {
|
|
6
|
-
|
|
7
|
-
return true;
|
|
8
|
-
if (result.action !== "fix_code")
|
|
9
|
-
return false;
|
|
10
|
-
return !result.fix.instructions.some((instruction) => /stop polling|human direction/i.test(instruction));
|
|
6
|
+
return ["wait", "mark_ready", "merge", "fix_code"].includes(result.action);
|
|
11
7
|
}
|
|
12
8
|
export async function attachApiUsage(result, persistWarning, preservePersistedWarning = false) {
|
|
13
9
|
const apiUsage = summarizeApiTelemetry();
|
|
@@ -34,9 +30,6 @@ export async function attachApiUsage(result, persistWarning, preservePersistedWa
|
|
|
34
30
|
if (/\[FIX_CODE\].*non-terminal/i.test(completion)) {
|
|
35
31
|
instructions[instructions.length - 1] = buildQuotaAwareContinuation(quotaWarning, "`[FIX_CODE]` is non-terminal. After completing these steps,");
|
|
36
32
|
}
|
|
37
|
-
else if (/\[FIX_CODE\].*is conditional:.*if you did not change code,/i.test(completion)) {
|
|
38
|
-
instructions[instructions.length - 1] = completion.replace(/if you did not change code,(.*?)\s+and iterate again with the same options\./i, (_, before) => `if you did not change code,${before.trimEnd()}. ${buildQuotaAwareContinuation(quotaWarning, "").trimStart()}`);
|
|
39
|
-
}
|
|
40
33
|
}
|
|
41
34
|
return { ...withWarning, fix: { ...withWarning.fix, instructions } };
|
|
42
35
|
}
|
|
@@ -27,7 +27,7 @@ export declare function buildBehindBaseHintInstruction(baseBranch: string, hint:
|
|
|
27
27
|
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
28
28
|
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
29
29
|
*/
|
|
30
|
-
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand
|
|
30
|
+
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
31
31
|
/** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
|
|
32
32
|
export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
|
|
33
|
-
export declare function buildFixCompletionInstruction(checks: AgentCheck[],
|
|
33
|
+
export declare function buildFixCompletionInstruction(checks: AgentCheck[], hasConflicts?: boolean, hasShaGatedReviewMutations?: boolean): string;
|
|
@@ -36,7 +36,7 @@ export function buildBehindBaseHintInstruction(baseBranch, hint, isBehind) {
|
|
|
36
36
|
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
37
37
|
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
38
38
|
*/
|
|
39
|
-
export function buildResolveCommandInstruction(resolveCommand
|
|
39
|
+
export function buildResolveCommandInstruction(resolveCommand) {
|
|
40
40
|
if (!resolveCommand.hasMutations)
|
|
41
41
|
return [];
|
|
42
42
|
const instructions = [];
|
|
@@ -44,9 +44,7 @@ export function buildResolveCommandInstruction(resolveCommand, pushAuthorized =
|
|
|
44
44
|
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked viewer-authored human thread is emitted resolve-only, not for another reply.");
|
|
45
45
|
}
|
|
46
46
|
if (resolveCommand.requiresHeadSha) {
|
|
47
|
-
instructions.push(
|
|
48
|
-
? "If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA."
|
|
49
|
-
: "If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, do not run this command until an authorized push updates the remote PR head; then replace `$HEAD_SHA` with that pushed commit SHA.");
|
|
47
|
+
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.");
|
|
50
48
|
}
|
|
51
49
|
if (resolveCommand.requiresDismissMessage) {
|
|
52
50
|
instructions.push("Replace `$DISMISS_MESSAGE` with one sentence describing what changed.");
|
|
@@ -69,39 +67,17 @@ export function buildFailingCheckInstructions(checks) {
|
|
|
69
67
|
instructions.push('A `[rerun authorized]` check includes a `rerun:` command. See "CI failure triage" in the pr-shepherd skill for which conclusions warrant a rerun versus a code fix.');
|
|
70
68
|
}
|
|
71
69
|
if (hasBare) {
|
|
72
|
-
instructions.push("For each `(no runId)` failure,
|
|
70
|
+
instructions.push("For each `(no runId)` failure, preserve the displayed metadata; Shepherd will escalate when no other autonomous work remains.");
|
|
73
71
|
}
|
|
74
72
|
return instructions;
|
|
75
73
|
}
|
|
76
|
-
export function buildFixCompletionInstruction(checks,
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
return "`[FIX_CODE]` is non-terminal: resolve the conflicts, commit, push to the PR head branch, then iterate again with the same options.";
|
|
80
|
-
}
|
|
81
|
-
return "`[FIX_CODE]` requires a human handoff for an authorized push after conflict resolution. Shepherd cannot verify the Git credential's push authorization. Stop polling after committing, and resume only after the remote PR head changes.";
|
|
82
|
-
}
|
|
83
|
-
const hasUninspectableFailure = checks.some((check) => !check.runId && !check.detailsUrl);
|
|
84
|
-
if (hasUninspectableFailure) {
|
|
85
|
-
return "`[FIX_CODE]` requires a human handoff for an uninspectable failing check. Stop polling after escalating, and resume only after human direction.";
|
|
86
|
-
}
|
|
87
|
-
// Checks that would otherwise need a workflow-run mutation (rerun) to move forward.
|
|
88
|
-
// `rerunCommand` is only populated when the viewer's repository role grants Actions
|
|
89
|
-
// rerun capability (see canRerunWorkflows) — unauthorized checks stay a terminal handoff.
|
|
90
|
-
const ciHandoffChecks = checks.filter((check) => check.conclusion === "CANCELLED" ||
|
|
91
|
-
check.conclusion === "STARTUP_FAILURE" ||
|
|
92
|
-
(check.runId === null && Boolean(check.detailsUrl)) ||
|
|
93
|
-
(check.runId !== null && !check.logExcerpt?.trim()));
|
|
94
|
-
const hasUnauthorizedCiHandoff = ciHandoffChecks.some((check) => !check.rerunCommand);
|
|
95
|
-
if (hasUnauthorizedCiHandoff) {
|
|
96
|
-
return "`[FIX_CODE]` requires a human handoff for a failing check with no authorized follow-up action. Stop polling after escalating, and resume only after human direction.";
|
|
97
|
-
}
|
|
74
|
+
export function buildFixCompletionInstruction(checks, hasConflicts = false, hasShaGatedReviewMutations = false) {
|
|
75
|
+
if (hasConflicts)
|
|
76
|
+
return "`[FIX_CODE]` is non-terminal: resolve the conflicts, commit, push to the PR head branch, then iterate again with the same options.";
|
|
98
77
|
if (hasShaGatedReviewMutations) {
|
|
99
|
-
if
|
|
100
|
-
return "`[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run the review mutations using the pushed commit SHA and iterate again with the same options; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
|
|
101
|
-
}
|
|
102
|
-
return "`[FIX_CODE]` is conditional: if you changed code, stop after committing and resume only after an authorized push changes the remote PR head; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
|
|
78
|
+
return "`[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run the review mutations using the pushed commit SHA and iterate again with the same options; if you did not change code, complete the authorized review mutations and iterate again with the same options.";
|
|
103
79
|
}
|
|
104
|
-
if (
|
|
80
|
+
if (checks.some((check) => check.rerunCommand)) {
|
|
105
81
|
return "`[FIX_CODE]` is non-terminal. Run any warranted reruns for `[rerun authorized]` checks (or apply code fixes for real failures), then iterate again with the same options to continue.";
|
|
106
82
|
}
|
|
107
83
|
return "`[FIX_CODE]` is non-terminal. After completing these steps, iterate again with the same options to continue.";
|
|
@@ -67,7 +67,8 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
67
67
|
const canResolve = new Set(authorizationThreads
|
|
68
68
|
.filter((thread) => thread.viewerCanResolve === true)
|
|
69
69
|
.map((thread) => thread.id));
|
|
70
|
-
const
|
|
70
|
+
const pairedResolveIds = new Set(routed.pairedResolveThreadIds);
|
|
71
|
+
const replyThreadIds = routed.replyThreadIds.filter((id) => canReply.has(id) && (!pairedResolveIds.has(id) || canResolve.has(id)));
|
|
71
72
|
// Viewer-authored human resolves stay paired with an authorized reply. Marker-ended
|
|
72
73
|
// viewer-authored retries and bot/non-human resolves need only resolve authorization.
|
|
73
74
|
const pairedResolveThreadIds = routed.pairedResolveThreadIds.filter((id) => canReply.has(id) && canResolve.has(id));
|
|
@@ -2,6 +2,61 @@ import { loadConfig } from "../../config/load.mjs";
|
|
|
2
2
|
function renderEscalateAuthor(item) {
|
|
3
3
|
return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
|
|
4
4
|
}
|
|
5
|
+
function renderCheckTarget(check) {
|
|
6
|
+
const parts = [];
|
|
7
|
+
if (check.runId)
|
|
8
|
+
parts.push(`run \`${check.runId}\``);
|
|
9
|
+
if (check.detailsUrl)
|
|
10
|
+
parts.push(`URL \`${check.detailsUrl}\``);
|
|
11
|
+
return parts.length > 0 ? parts.join(", ") : "no run ID or URL";
|
|
12
|
+
}
|
|
13
|
+
function renderCheckScope(check) {
|
|
14
|
+
if (!check.scope)
|
|
15
|
+
return "";
|
|
16
|
+
const commit = check.commitOid ? ` at \`${check.commitOid}\`` : "";
|
|
17
|
+
return `, scope \`${check.scope}\`${commit}`;
|
|
18
|
+
}
|
|
19
|
+
function renderBlockquoteLines(value, indent) {
|
|
20
|
+
return value.split("\n").map((line) => `${indent}> ${line}`);
|
|
21
|
+
}
|
|
22
|
+
function renderEscalateAnnotation(annotation) {
|
|
23
|
+
const start = annotation.startLine ?? annotation.endLine ?? "?";
|
|
24
|
+
const end = annotation.endLine ?? annotation.startLine ?? "?";
|
|
25
|
+
const range = start === end ? String(start) : `${start}-${end}`;
|
|
26
|
+
const columns = annotation.startColumn == null && annotation.endColumn == null
|
|
27
|
+
? ""
|
|
28
|
+
: `, columns ${annotation.startColumn ?? "?"}-${annotation.endColumn ?? "?"}`;
|
|
29
|
+
const title = annotation.title ? ` — ${annotation.title}` : "";
|
|
30
|
+
const link = annotation.blobUrl ? ` [source](${annotation.blobUrl})` : "";
|
|
31
|
+
const lines = [
|
|
32
|
+
` - annotation \`${annotation.id}\`${link} — \`${annotation.path}:${range}\` [${annotation.level}${columns}]${title}`,
|
|
33
|
+
...renderBlockquoteLines(annotation.message, " "),
|
|
34
|
+
];
|
|
35
|
+
if (annotation.rawDetails) {
|
|
36
|
+
lines.push(...renderBlockquoteLines(annotation.rawDetails, " "));
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
function renderEscalateCheck(check) {
|
|
41
|
+
const workflowPrefix = check.workflowName ? `${check.workflowName} › ` : "";
|
|
42
|
+
const jobLabel = check.jobName ?? check.name;
|
|
43
|
+
const conclusion = check.conclusion ?? "UNKNOWN";
|
|
44
|
+
const lines = [
|
|
45
|
+
`- ${renderCheckTarget(check)} — \`${workflowPrefix}${jobLabel}\` [conclusion: ${conclusion}]${renderCheckScope(check)}`,
|
|
46
|
+
];
|
|
47
|
+
if (check.failedStep)
|
|
48
|
+
lines.push(` > failed step: ${check.failedStep}`);
|
|
49
|
+
if (check.summary)
|
|
50
|
+
lines.push(` > ${check.summary}`);
|
|
51
|
+
if (check.logExcerpt)
|
|
52
|
+
lines.push(...renderBlockquoteLines(check.logExcerpt, " "));
|
|
53
|
+
if (check.rerunCommand)
|
|
54
|
+
lines.push(` rerun: \`${check.rerunCommand}\``);
|
|
55
|
+
for (const annotation of check.annotations ?? []) {
|
|
56
|
+
lines.push(...renderEscalateAnnotation(annotation));
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
5
60
|
export function checkEscalateTriggers(actionableThreads, threadAttempts) {
|
|
6
61
|
const triggers = [];
|
|
7
62
|
const maxAttempts = loadConfig().iterate.fixAttemptsPerThread;
|
|
@@ -10,11 +65,6 @@ export function checkEscalateTriggers(actionableThreads, threadAttempts) {
|
|
|
10
65
|
if (thrashThreads.length > 0) {
|
|
11
66
|
triggers.push("fix-thrash");
|
|
12
67
|
}
|
|
13
|
-
// Trigger 2: actionable thread has no file/line — cannot locate code to edit.
|
|
14
|
-
const unlocatable = actionableThreads.filter((t) => t.path === null || t.line === null);
|
|
15
|
-
if (unlocatable.length > 0) {
|
|
16
|
-
triggers.push("thread-missing-location");
|
|
17
|
-
}
|
|
18
68
|
return {
|
|
19
69
|
triggers,
|
|
20
70
|
thrashHistory: thrashThreads.length > 0
|
|
@@ -69,6 +119,7 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
69
119
|
const hasItems = escalate.unresolvedThreads.length > 0 ||
|
|
70
120
|
escalate.changesRequestedReviews.length > 0 ||
|
|
71
121
|
escalate.ambiguousComments.length > 0 ||
|
|
122
|
+
(escalate.checks?.length ?? 0) > 0 ||
|
|
72
123
|
(escalate.stalledChecks?.length ?? 0) > 0;
|
|
73
124
|
if (hasItems) {
|
|
74
125
|
lines.push("");
|
|
@@ -86,6 +137,9 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
86
137
|
}
|
|
87
138
|
if ((escalate.stalledChecks?.length ?? 0) > 0)
|
|
88
139
|
lines.push("");
|
|
140
|
+
for (const check of escalate.checks ?? []) {
|
|
141
|
+
lines.push(...renderEscalateCheck(check), "");
|
|
142
|
+
}
|
|
89
143
|
for (const t of escalate.unresolvedThreads) {
|
|
90
144
|
const loc = t.path ? `\`${t.path}:${t.line ?? "?"}\`` : "(no location)";
|
|
91
145
|
lines.push(`- thread \`${t.id}\` — ${loc} (${renderEscalateAuthor(t)}):`);
|
|
@@ -144,8 +198,11 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
144
198
|
return lines.join("\n");
|
|
145
199
|
}
|
|
146
200
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
201
|
+
if (triggers.includes("check-follow-up-unavailable")) {
|
|
202
|
+
return "One or more failing checks have no autonomous follow-up available. Use the displayed conclusion, run or URL, and included evidence to handle them manually.";
|
|
203
|
+
}
|
|
147
204
|
if (triggers.includes("authorization-required")) {
|
|
148
|
-
return "GitHub did not confirm that the current viewer may perform
|
|
205
|
+
return "GitHub did not confirm that the current viewer may perform the automatic mark-ready operation. Ask a repository maintainer to mark the pull request ready before resuming Shepherd.";
|
|
149
206
|
}
|
|
150
207
|
if (triggers.includes("merge-queue-removed")) {
|
|
151
208
|
const reason = detail ? ` GitHub reason: ${detail}.` : "";
|
|
@@ -162,9 +219,6 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
162
219
|
if (triggers.includes("fix-thrash")) {
|
|
163
220
|
return "Same thread(s) reached the automated attempt limit — treat this as a manual handoff. Apply the fix by hand.";
|
|
164
221
|
}
|
|
165
|
-
if (triggers.includes("thread-missing-location")) {
|
|
166
|
-
return "Review thread has no file/line reference — automated location routing failed and manual handling is required.";
|
|
167
|
-
}
|
|
168
222
|
if (triggers.includes("bot-cr-not-dismissed")) {
|
|
169
223
|
const ids = detail ? ` (review IDs: ${detail})` : "";
|
|
170
224
|
return `Bot CHANGES_REQUESTED review(s) remained undismissed past the stall window${ids}. The agent likely dropped \`--dismiss-review-ids\` from a prior apply command. Dismiss the review(s) manually (or re-run \`pr-shepherd apply review\` with the IDs) to unblock the PR.`;
|
|
@@ -11,9 +11,23 @@ import { applyStallGuard } from "./stall.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
|
-
import { canRerunWorkflows
|
|
14
|
+
import { canRerunWorkflows } from "../../checks/conclusions.mjs";
|
|
15
15
|
import { loadConfig } from "../../config/load.mjs";
|
|
16
16
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
17
|
+
function checkRequiresHumanFollowUp(check) {
|
|
18
|
+
if (check.rerunCommand)
|
|
19
|
+
return false;
|
|
20
|
+
if (check.conclusion === "ACTION_REQUIRED" ||
|
|
21
|
+
check.conclusion === "CANCELLED" ||
|
|
22
|
+
check.conclusion === "STARTUP_FAILURE")
|
|
23
|
+
return true;
|
|
24
|
+
// An external check's direct URL is actionable evidence: the agent can inspect the
|
|
25
|
+
// provider and/or reproduce the reported failure locally. Only a truly bare check
|
|
26
|
+
// has no autonomous investigation path.
|
|
27
|
+
if (check.runId === null)
|
|
28
|
+
return !check.detailsUrl?.trim();
|
|
29
|
+
return !check.logExcerpt?.trim();
|
|
30
|
+
}
|
|
17
31
|
function nextFixAttempts(stored, headSha, threads) {
|
|
18
32
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
19
33
|
const threadBodyHashes = stored?.threadBodyHashes
|
|
@@ -41,66 +55,17 @@ export async function handleFixCode(ctx) {
|
|
|
41
55
|
]);
|
|
42
56
|
const replyIdSet = new Set(routedThreadMutations.replyThreadIds);
|
|
43
57
|
const resolveIdSet = new Set(routedThreadMutations.resolveThreadIds);
|
|
44
|
-
const unauthorizedReplies = allThreads.filter((thread) =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const unauthorizedResolves = allThreads.filter((thread) => report.viewerAuthorization !== undefined &&
|
|
48
|
-
resolveIdSet.has(thread.id) &&
|
|
49
|
-
thread.viewerCanResolve !== true);
|
|
50
|
-
const unauthorizedDismissals = report.changesRequestedReviews.filter((review) => report.viewerAuthorization !== undefined &&
|
|
51
|
-
(!isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames)) &&
|
|
58
|
+
const unauthorizedReplies = allThreads.filter((thread) => replyIdSet.has(thread.id) && thread.viewerCanReply !== true);
|
|
59
|
+
const unauthorizedResolves = allThreads.filter((thread) => resolveIdSet.has(thread.id) && thread.viewerCanResolve !== true);
|
|
60
|
+
const unauthorizedDismissals = report.changesRequestedReviews.filter((review) => (!isHumanAuthor(review) || isConfiguredBotAuthor(review, botUsernames)) &&
|
|
52
61
|
report.viewerAuthorization?.viewerCanAdminister !== true);
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
? [
|
|
56
|
-
{
|
|
57
|
-
action: "reply-thread",
|
|
58
|
-
targetIds: unauthorizedReplies.map((thread) => thread.id),
|
|
59
|
-
reason: "denied-or-unverifiable",
|
|
60
|
-
},
|
|
61
|
-
]
|
|
62
|
-
: []),
|
|
63
|
-
...(unauthorizedResolves.length > 0
|
|
64
|
-
? [
|
|
65
|
-
{
|
|
66
|
-
action: "resolve-thread",
|
|
67
|
-
targetIds: unauthorizedResolves.map((thread) => thread.id),
|
|
68
|
-
reason: "denied-or-unverifiable",
|
|
69
|
-
},
|
|
70
|
-
]
|
|
71
|
-
: []),
|
|
72
|
-
...(unauthorizedDismissals.length > 0
|
|
73
|
-
? [
|
|
74
|
-
{
|
|
75
|
-
action: "dismiss-review",
|
|
76
|
-
targetIds: unauthorizedDismissals.map((review) => review.id),
|
|
77
|
-
reason: "denied-or-unverifiable",
|
|
78
|
-
},
|
|
79
|
-
]
|
|
80
|
-
: []),
|
|
81
|
-
];
|
|
82
|
-
if (authorization.length > 0) {
|
|
83
|
-
const authorizationEscalateBase = {
|
|
84
|
-
triggers: ["authorization-required"],
|
|
85
|
-
unresolvedThreads: allThreads.map(toAgentThread),
|
|
86
|
-
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
87
|
-
changesRequestedReviews: report.changesRequestedReviews,
|
|
88
|
-
authorization,
|
|
89
|
-
suggestion: buildEscalateSuggestion(["authorization-required"]),
|
|
90
|
-
};
|
|
91
|
-
return {
|
|
92
|
-
...base,
|
|
93
|
-
action: "escalate",
|
|
94
|
-
escalate: {
|
|
95
|
-
...authorizationEscalateBase,
|
|
96
|
-
humanMessage: buildEscalateHumanMessage(authorizationEscalateBase, prReference),
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
62
|
+
const skippedThreadIds = new Set([...unauthorizedReplies, ...unauthorizedResolves].map((thread) => thread.id));
|
|
63
|
+
const retryableActionableThreads = report.threads.actionable.filter((thread) => !skippedThreadIds.has(thread.id) && thread.path !== null && thread.line !== null);
|
|
100
64
|
const protectedRuns = [];
|
|
101
65
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
102
|
-
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha,
|
|
103
|
-
const botCrReviews = report.changesRequestedReviews.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames))
|
|
66
|
+
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, headSha, retryableActionableThreads);
|
|
67
|
+
const botCrReviews = report.changesRequestedReviews.filter((r) => (!isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames)) &&
|
|
68
|
+
report.viewerAuthorization?.viewerCanAdminister === true);
|
|
104
69
|
const botCrStateKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
105
70
|
const previousBotCrState = await readBotCrSeenState(botCrStateKey);
|
|
106
71
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
@@ -121,11 +86,13 @@ export async function handleFixCode(ctx) {
|
|
|
121
86
|
action: "escalate",
|
|
122
87
|
escalate: {
|
|
123
88
|
...escalateBase,
|
|
124
|
-
humanMessage: buildEscalateHumanMessage(escalateBase, prReference
|
|
89
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference, {
|
|
90
|
+
merge: opts.merge,
|
|
91
|
+
}),
|
|
125
92
|
},
|
|
126
93
|
};
|
|
127
94
|
}
|
|
128
|
-
const escalateTriggers = checkEscalateTriggers(
|
|
95
|
+
const escalateTriggers = checkEscalateTriggers(retryableActionableThreads, threadAttempts);
|
|
129
96
|
if (escalateTriggers.triggers.length > 0) {
|
|
130
97
|
const escalateBase = {
|
|
131
98
|
triggers: escalateTriggers.triggers,
|
|
@@ -140,7 +107,9 @@ export async function handleFixCode(ctx) {
|
|
|
140
107
|
action: "escalate",
|
|
141
108
|
escalate: {
|
|
142
109
|
...escalateBase,
|
|
143
|
-
humanMessage: buildEscalateHumanMessage(escalateBase, prReference
|
|
110
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference, {
|
|
111
|
+
merge: opts.merge,
|
|
112
|
+
}),
|
|
144
113
|
},
|
|
145
114
|
};
|
|
146
115
|
}
|
|
@@ -183,40 +152,72 @@ export async function handleFixCode(ctx) {
|
|
|
183
152
|
...toAgentChecks(annotatedExtra).map((c) => ({ ...c, annotationOnly: true })),
|
|
184
153
|
];
|
|
185
154
|
const { changesRequestedReviews } = report;
|
|
155
|
+
const actionableChangesRequestedReviews = changesRequestedReviews.filter((review) => review.staleReview !== true ||
|
|
156
|
+
!isHumanAuthor(review) ||
|
|
157
|
+
isConfiguredBotAuthor(review, botUsernames));
|
|
158
|
+
const skippedDismissalIds = new Set(unauthorizedDismissals.map((review) => review.id));
|
|
159
|
+
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !skippedDismissalIds.has(review.id));
|
|
160
|
+
const resolutionOnlyThreadsForWork = resolutionOnlyThreads.filter((thread) => !skippedThreadIds.has(thread.id) && thread.path !== null && thread.line !== null);
|
|
186
161
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
187
162
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
188
163
|
const { behindBaseHint } = loadConfig().iterate;
|
|
189
|
-
// Whether GitHub reports the viewer can push to the PR head branch (own-repo write
|
|
190
|
-
// access, or a fork with viewerCanEditFiles/head-repo write for a fork PR). Drives
|
|
191
|
-
// whether the fix instructions push autonomously or hand off for an authorized push —
|
|
192
|
-
// see canPushToHead.
|
|
193
|
-
const pushAuthorized = canPushToHead(report.viewerAuthorization);
|
|
194
164
|
// Only surface in-progress runs when a push is plausible — resolution-only and
|
|
195
165
|
// summary-only iterations have no path to a push, so listing runs would prompt
|
|
196
166
|
// unnecessary cancellation.
|
|
197
167
|
const inProgressRunIds = [];
|
|
198
168
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
199
169
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
170
|
+
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
171
|
+
const manualFollowUpChecks = failingAgentChecks.filter((check) => !belongsToActiveWorkflowRun(check) && checkRequiresHumanFollowUp(check));
|
|
172
|
+
const hasAutonomousWork = hasConflicts ||
|
|
173
|
+
threads.length > 0 ||
|
|
174
|
+
resolutionOnlyThreads.length > 0 ||
|
|
175
|
+
actionableComments.length > 0 ||
|
|
176
|
+
commentMinimizeIds.length > 0 ||
|
|
177
|
+
actionableChangesRequestedReviews.length > 0 ||
|
|
178
|
+
reviewSummaryIds.length > 0 ||
|
|
179
|
+
firstLookSummaries.length > 0 ||
|
|
180
|
+
editedSummaries.length > 0 ||
|
|
181
|
+
report.threads.firstLook.length > 0 ||
|
|
182
|
+
report.comments.firstLook.length > 0 ||
|
|
183
|
+
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
184
|
+
failingAgentChecks.some((check) => belongsToActiveWorkflowRun(check) || !checkRequiresHumanFollowUp(check));
|
|
185
|
+
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
186
|
+
const checkEscalateBase = {
|
|
187
|
+
triggers: ["check-follow-up-unavailable"],
|
|
188
|
+
unresolvedThreads: [],
|
|
189
|
+
ambiguousComments: [],
|
|
190
|
+
changesRequestedReviews,
|
|
191
|
+
checks: manualFollowUpChecks,
|
|
192
|
+
suggestion: buildEscalateSuggestion(["check-follow-up-unavailable"]),
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
action: "escalate",
|
|
197
|
+
escalate: {
|
|
198
|
+
...checkEscalateBase,
|
|
199
|
+
humanMessage: buildEscalateHumanMessage(checkEscalateBase, prReference, {
|
|
200
|
+
merge: opts.merge,
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Push access to the PR head branch is a usage precondition. Build review mutations for
|
|
206
|
+
// conflict ticks normally so the caller can push and complete the same fix_code cycle.
|
|
207
|
+
const retryableActionableIds = new Set(retryableActionableThreads.map((thread) => thread.id));
|
|
208
|
+
const retryableAgentThreads = threads.filter((thread) => retryableActionableIds.has(thread.id));
|
|
209
|
+
const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(retryableAgentThreads, resolutionOnlyThreadsForWork, allCommentIds, changesRequestedReviewsForWork, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads);
|
|
209
210
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
210
211
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
211
212
|
// prediction that the agent *will* push. Intentionally broader than `pushLikely` above:
|
|
212
213
|
// resolution-only threads also need a known base in case the agent does push.
|
|
213
|
-
const pushIsPlausible =
|
|
214
|
+
const pushIsPlausible = retryableActionableThreads.length > 0 ||
|
|
214
215
|
failingAgentChecks.length > 0 ||
|
|
215
216
|
annotatedExtra.length > 0 ||
|
|
216
217
|
hasConflicts ||
|
|
217
|
-
|
|
218
|
+
changesRequestedReviewsForWork.length > 0 ||
|
|
218
219
|
actionableComments.length > 0 ||
|
|
219
|
-
|
|
220
|
+
resolutionOnlyThreadsForWork.length > 0;
|
|
220
221
|
if (baseLookup.isFallback && pushIsPlausible) {
|
|
221
222
|
const fallbackEscalateBase = {
|
|
222
223
|
triggers: ["base-branch-unknown"],
|
|
@@ -230,13 +231,15 @@ export async function handleFixCode(ctx) {
|
|
|
230
231
|
action: "escalate",
|
|
231
232
|
escalate: {
|
|
232
233
|
...fallbackEscalateBase,
|
|
233
|
-
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase, prReference
|
|
234
|
+
humanMessage: buildEscalateHumanMessage(fallbackEscalateBase, prReference, {
|
|
235
|
+
merge: opts.merge,
|
|
236
|
+
}),
|
|
234
237
|
},
|
|
235
238
|
};
|
|
236
239
|
}
|
|
237
240
|
const firstLookThreads = report.threads.firstLook;
|
|
238
241
|
const firstLookComments = report.comments.firstLook;
|
|
239
|
-
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
|
|
242
|
+
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);
|
|
240
243
|
const prospectiveResult = {
|
|
241
244
|
...base,
|
|
242
245
|
baseBranch: baseLookup.branch,
|
|
@@ -81,8 +81,8 @@ async function runIterateCore(opts) {
|
|
|
81
81
|
editedSummaries.length > 0 ||
|
|
82
82
|
(config.iterate.minimizeApprovals && surfacedApprovals.length > 0);
|
|
83
83
|
const activeMerge = Boolean(opts.merge && (report.mergeQueue?.inQueue || report.mergeQueue?.autoMergeRequest));
|
|
84
|
-
const
|
|
85
|
-
const readyState = await updateReadyDelay(report.pr,
|
|
84
|
+
const isCleanReadyState = report.status === "READY" && !hasActionableWork && !activeMerge;
|
|
85
|
+
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName);
|
|
86
86
|
const base = buildIterateBase(report, readyState);
|
|
87
87
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
88
88
|
if (hasActionableWork) {
|