pr-shepherd 0.48.0 → 0.50.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 +14 -2
- package/bin/api.d.mts +19 -3
- package/bin/api.mjs +57 -8
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +1 -1
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/help-command-pages.d.mts +9 -9
- package/bin/cli/help-command-pages.mjs +8 -8
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +10 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +5 -3
- package/bin/cli/help.d.mts +10 -10
- package/bin/cli/iterate-instructions.mjs +9 -2
- package/bin/cli/iterate-lean.mjs +11 -0
- package/bin/cli/poll-handler.mjs +25 -4
- package/bin/cli/poll-summary-emitter.d.mts +4 -0
- package/bin/cli/poll-summary-emitter.mjs +22 -0
- package/bin/cli/poll-summary-formatter.d.mts +2 -0
- package/bin/cli/poll-summary-formatter.mjs +96 -0
- package/bin/cli/poll-targets.d.mts +15 -0
- package/bin/cli/poll-targets.mjs +114 -0
- package/bin/cli/validate-default-args.mjs +1 -4
- package/bin/cli-parser.mjs +3 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -8
- package/bin/commands/iterate/check-instructions.mjs +2 -11
- package/bin/commands/iterate/escalate.mjs +30 -2
- package/bin/commands/iterate/fix-code.mjs +62 -20
- package/bin/commands/iterate/render.mjs +1 -1
- package/bin/commands/iterate/stall.mjs +30 -0
- package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
- package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
- package/bin/commands/poll-summary.d.mts +10 -0
- package/bin/commands/poll-summary.mjs +163 -0
- package/bin/commands/ready-delay.d.mts +3 -1
- package/bin/commands/ready-delay.mjs +3 -2
- package/bin/commands/resolve-mutate.mjs +22 -68
- package/bin/comments/resolve.d.mts +5 -0
- package/bin/comments/resolve.mjs +2 -11
- package/bin/github/gql/poll-stack-summary.gql +33 -0
- package/bin/github/gql/poll-summary-fragment.gql +198 -0
- package/bin/github/poll-summary-checks.d.mts +3 -0
- package/bin/github/poll-summary-checks.mjs +61 -0
- package/bin/github/poll-summary-projector.d.mts +4 -0
- package/bin/github/poll-summary-projector.mjs +81 -0
- package/bin/github/poll-summary-raw.d.mts +134 -0
- package/bin/github/poll-summary-raw.mjs +1 -0
- package/bin/github/poll-summary-review.d.mts +4 -0
- package/bin/github/poll-summary-review.mjs +85 -0
- package/bin/github/poll-summary-route.d.mts +4 -0
- package/bin/github/poll-summary-route.mjs +47 -0
- package/bin/github/poll-summary.d.mts +7 -0
- package/bin/github/poll-summary.mjs +110 -0
- package/bin/github/queries.d.mts +4 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/mcp/server.mjs +39 -6
- package/bin/pr-reference.d.mts +2 -0
- package/bin/pr-reference.mjs +4 -0
- package/bin/state/fix-attempts.d.mts +3 -4
- package/bin/state/fix-attempts.mjs +2 -3
- package/bin/types/escalate.d.mts +10 -0
- package/bin/types/poll-summary.d.mts +82 -0
- package/bin/types/poll-summary.mjs +1 -0
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -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/pr-shepherd/SKILL.md +5 -10
package/bin/cli/poll-handler.mjs
CHANGED
|
@@ -6,8 +6,21 @@ import { validateSecondsDurationFlag } from "./duration-flag.mjs";
|
|
|
6
6
|
import { parseIterateFlags } from "./iterate-flags.mjs";
|
|
7
7
|
import { emitIterateResult } from "./iterate-emitter.mjs";
|
|
8
8
|
import { EXIT } from "../exit-codes.mjs";
|
|
9
|
+
import { runAggregatePoll } from "../commands/poll-summary.mjs";
|
|
10
|
+
import { emitPollSummaryResult } from "./poll-summary-emitter.mjs";
|
|
11
|
+
import { parsePollTargets, resolvePollTargets } from "./poll-targets.mjs";
|
|
9
12
|
export async function handlePoll(args) {
|
|
10
|
-
const
|
|
13
|
+
const parsedTargets = parsePollTargets(args);
|
|
14
|
+
if (!parsedTargets)
|
|
15
|
+
return;
|
|
16
|
+
const needsResolution = parsedTargets.stack !== undefined || parsedTargets.refs.length > 1;
|
|
17
|
+
const resolvedTargets = needsResolution
|
|
18
|
+
? await resolvePollTargets(parsedTargets)
|
|
19
|
+
: { prNumbers: [] };
|
|
20
|
+
const isAggregate = resolvedTargets.stackPrNumber !== undefined || resolvedTargets.prNumbers.length > 1;
|
|
21
|
+
const { prNumber, global: commonGlobalOpts, extra: commonExtra } = parseCommonArgs(args);
|
|
22
|
+
const globalOpts = isAggregate ? parsedTargets.global : commonGlobalOpts;
|
|
23
|
+
const extra = isAggregate ? parsedTargets.extra : commonExtra;
|
|
11
24
|
const cfg = loadConfig();
|
|
12
25
|
const flags = parseIterateFlags(extra, cfg);
|
|
13
26
|
if (flags.readyDelaySuffix === null || flags.stallTimeoutSuffix === null)
|
|
@@ -37,9 +50,8 @@ export async function handlePoll(args) {
|
|
|
37
50
|
return;
|
|
38
51
|
}
|
|
39
52
|
const quietStatus = quietStatusFlag || (!noQuietStatusFlag && cfg.poll.quietStatus);
|
|
40
|
-
const
|
|
53
|
+
const shared = {
|
|
41
54
|
...globalOpts,
|
|
42
|
-
prNumber,
|
|
43
55
|
readyDelaySeconds: flags.readyDelaySeconds,
|
|
44
56
|
stallTimeoutSeconds: flags.stallTimeoutSeconds,
|
|
45
57
|
noAutoMarkReady: flags.noAutoMarkReady,
|
|
@@ -50,7 +62,16 @@ export async function handlePoll(args) {
|
|
|
50
62
|
debounceSeconds,
|
|
51
63
|
quietStatus,
|
|
52
64
|
untilTerminal: hasFlag(extra, "--until-terminal"),
|
|
53
|
-
}
|
|
65
|
+
};
|
|
66
|
+
if (isAggregate) {
|
|
67
|
+
const result = await runAggregatePoll({
|
|
68
|
+
...shared,
|
|
69
|
+
...resolvedTargets,
|
|
70
|
+
});
|
|
71
|
+
emitPollSummaryResult(result, { format: globalOpts.format });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = await runPoll({ ...shared, prNumber });
|
|
54
75
|
emitIterateResult(result, {
|
|
55
76
|
format: globalOpts.format,
|
|
56
77
|
verbose: globalOpts.verbose ?? false,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
2
|
+
import { formatPollSummaryResult } from "./poll-summary-formatter.mjs";
|
|
3
|
+
export function emitPollSummaryResult(result, opts) {
|
|
4
|
+
process.stdout.write(opts.format === "json" ? `${JSON.stringify(result)}\n` : `${formatPollSummaryResult(result)}\n`);
|
|
5
|
+
process.exitCode = pollSummaryExitCode(result);
|
|
6
|
+
}
|
|
7
|
+
function pollSummaryExitCode(result) {
|
|
8
|
+
const actions = new Set(result.prs.map((item) => item.action));
|
|
9
|
+
if (actions.has("escalate"))
|
|
10
|
+
return EXIT.ESCALATE;
|
|
11
|
+
if (actions.has("fix_code"))
|
|
12
|
+
return EXIT.FIX_CODE;
|
|
13
|
+
if (actions.has("merge"))
|
|
14
|
+
return EXIT.MERGE;
|
|
15
|
+
if (actions.has("mark_ready"))
|
|
16
|
+
return EXIT.MARK_READY;
|
|
17
|
+
if (actions.has("wait"))
|
|
18
|
+
return EXIT.WAIT;
|
|
19
|
+
if (result.prs.some((item) => item.reasons.includes("closed")))
|
|
20
|
+
return EXIT.CLOSED;
|
|
21
|
+
return EXIT.OK;
|
|
22
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
2
|
+
import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
|
|
3
|
+
export function formatPollSummaryResult(result) {
|
|
4
|
+
const selection = result.selection.kind === "stack"
|
|
5
|
+
? `stack #${result.selection.stackNumber} anchored at PR #${result.selection.anchor} (${result.selection.stackSize} PRs)`
|
|
6
|
+
: `PRs ${result.selection.requested.map((pr) => `#${pr}`).join(", ")}`;
|
|
7
|
+
const lines = [
|
|
8
|
+
`# Poll summary [${result.reason.toUpperCase()}]`,
|
|
9
|
+
"",
|
|
10
|
+
`**repo** \`${result.repo}\` · **selection** ${selection} · **mode** \`${result.mode}\``,
|
|
11
|
+
"",
|
|
12
|
+
"## Pull requests",
|
|
13
|
+
"",
|
|
14
|
+
...result.prs.map(formatItem),
|
|
15
|
+
];
|
|
16
|
+
const apiUsage = result.apiUsage ? formatApiUsage(result.apiUsage) : null;
|
|
17
|
+
const quotaWarning = formatQuotaWarning(result.quotaWarning);
|
|
18
|
+
if (quotaWarning)
|
|
19
|
+
lines.push("", quotaWarning);
|
|
20
|
+
if (apiUsage)
|
|
21
|
+
lines.push("", apiUsage);
|
|
22
|
+
lines.push("", "## Instructions", "", ...formatInstructions(result));
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
25
|
+
function formatInstructions(result) {
|
|
26
|
+
if (result.reason === "all_terminal")
|
|
27
|
+
return ["1. Stop — every selected PR is terminal."];
|
|
28
|
+
if (result.quotaWarning && result.reason !== "actionable") {
|
|
29
|
+
return [
|
|
30
|
+
buildQuotaAwareContinuation(result.quotaWarning, "1. This aggregate selection is non-terminal. Before continuing,"),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (result.reason === "waiting" || result.reason === "timeout") {
|
|
34
|
+
return ["1. Run this aggregate selector again when the caller is ready to recheck."];
|
|
35
|
+
}
|
|
36
|
+
const instructions = [
|
|
37
|
+
"1. Choose each non-WAIT, non-CANCEL row that can proceed independently and run or delegate its exact `pollCommand`.",
|
|
38
|
+
"2. Follow each selected one-PR poll's `## Instructions` until it returns `CANCEL` or `ESCALATE`.",
|
|
39
|
+
"3. Run this aggregate poll again after selected work completes; one row's `ESCALATE` does not stop work on other rows.",
|
|
40
|
+
];
|
|
41
|
+
if (result.quotaWarning) {
|
|
42
|
+
instructions[2] = buildQuotaAwareContinuation(result.quotaWarning, "3. After selected work completes,");
|
|
43
|
+
}
|
|
44
|
+
return instructions;
|
|
45
|
+
}
|
|
46
|
+
function formatItem(item) {
|
|
47
|
+
const flags = [item.isDraft ? "draft" : null, item.isInMergeQueue ? "queued" : null]
|
|
48
|
+
.filter((value) => value !== null)
|
|
49
|
+
.join(", ");
|
|
50
|
+
const stack = item.stack
|
|
51
|
+
? ` · stack \`${item.stack.number}\` position \`${item.stack.position}/${item.stack.size}\` base \`${item.stack.baseRefName}\``
|
|
52
|
+
: "";
|
|
53
|
+
const reviewDecision = item.reviewDecision ? ` · reviewDecision \`${item.reviewDecision}\`` : "";
|
|
54
|
+
const stateFlags = flags ? ` · flags \`${flags}\`` : "";
|
|
55
|
+
const blockingReviewer = item.blockingReviewerInProgress
|
|
56
|
+
? " · blocking reviewer `in progress`"
|
|
57
|
+
: "";
|
|
58
|
+
const readyDelay = item.remainingSeconds !== undefined ? ` · ready delay \`${item.remainingSeconds}s\`` : "";
|
|
59
|
+
const checks = item.checks;
|
|
60
|
+
const review = item.review;
|
|
61
|
+
return [
|
|
62
|
+
`- [PR #${item.pr}: ${escapeMarkdownText(item.title)}](${item.url}) [${item.action.toUpperCase()}]`,
|
|
63
|
+
` - state \`${item.state}\` · mergeable \`${item.mergeable}\` · merge \`${item.mergeStateStatus}\`${reviewDecision}${stateFlags}${blockingReviewer}${readyDelay}${stack}`,
|
|
64
|
+
` - head \`${item.headRefName}\` at \`${item.headRefOid}\` · base \`${item.baseRefName}\``,
|
|
65
|
+
...(checks
|
|
66
|
+
? [` - checks: ${formatCounts(checks, checks.incomplete ? ", incomplete" : "")}`]
|
|
67
|
+
: []),
|
|
68
|
+
...(review
|
|
69
|
+
? [` - review: ${formatCounts(review, review.incomplete ? ", incomplete" : "")}`]
|
|
70
|
+
: []),
|
|
71
|
+
` - reasons: ${item.reasons.map((reason) => `\`${reason}\``).join(", ")}`,
|
|
72
|
+
...(item.pollCommand ? [` - pollCommand: \`${item.pollCommand}\``] : []),
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
function formatCounts(counts, suffix) {
|
|
76
|
+
const plural = { inProgress: "in progress" };
|
|
77
|
+
const singular = {
|
|
78
|
+
comments: "comment",
|
|
79
|
+
inProgress: "in progress",
|
|
80
|
+
reviews: "review",
|
|
81
|
+
threads: "thread",
|
|
82
|
+
};
|
|
83
|
+
const rendered = Object.entries(counts)
|
|
84
|
+
.filter(([, count]) => typeof count === "number")
|
|
85
|
+
.map(([name, count]) => `${count} ${count === 1 ? (singular[name] ?? name) : (plural[name] ?? name)}`)
|
|
86
|
+
.join(", ");
|
|
87
|
+
return rendered ? `${rendered}${suffix}` : suffix.replace(/^, /, "");
|
|
88
|
+
}
|
|
89
|
+
function escapeMarkdownText(value) {
|
|
90
|
+
return value
|
|
91
|
+
.replace(/&/g, "&")
|
|
92
|
+
.replace(/</g, "<")
|
|
93
|
+
.replace(/>/g, ">")
|
|
94
|
+
.replace(/[\r\n]+/g, " ")
|
|
95
|
+
.replace(/([\\[\]])/g, "\\$1");
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RepoInfo } from "../github/client.mts";
|
|
2
|
+
import { type ParsedPrReference } from "../pr-reference.mts";
|
|
3
|
+
import type { GlobalOptions } from "../types.mts";
|
|
4
|
+
export interface ParsedPollTargets {
|
|
5
|
+
refs: ParsedPrReference[];
|
|
6
|
+
stack: ParsedPrReference | undefined;
|
|
7
|
+
global: GlobalOptions;
|
|
8
|
+
extra: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function parsePollTargets(args: string[]): ParsedPollTargets | null;
|
|
11
|
+
export declare function resolvePollTargets(parsed: ParsedPollTargets): Promise<{
|
|
12
|
+
prNumbers: number[];
|
|
13
|
+
stackPrNumber?: number;
|
|
14
|
+
targetRepository?: RepoInfo;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
3
|
+
import { getRepoInfo } from "../github/client.mjs";
|
|
4
|
+
import { parseCliPrReference, normalizeRepositoryIdentity, resolveParsedPrTarget, } from "../pr-reference.mjs";
|
|
5
|
+
const VALUE_FLAGS = new Set([
|
|
6
|
+
"--format",
|
|
7
|
+
"--ready-delay",
|
|
8
|
+
"--stall-timeout",
|
|
9
|
+
"--interval",
|
|
10
|
+
"--timeout",
|
|
11
|
+
"--debounce",
|
|
12
|
+
"--stack",
|
|
13
|
+
]);
|
|
14
|
+
export function parsePollTargets(args) {
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args,
|
|
17
|
+
strict: false,
|
|
18
|
+
allowPositionals: true,
|
|
19
|
+
options: { format: { type: "string" }, verbose: { type: "boolean" } },
|
|
20
|
+
});
|
|
21
|
+
const consumed = new Set();
|
|
22
|
+
const refs = [];
|
|
23
|
+
let stack;
|
|
24
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
+
const arg = args[index];
|
|
26
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
27
|
+
if (arg === "--format" || arg === "--stack") {
|
|
28
|
+
consumed.add(index);
|
|
29
|
+
if (index + 1 < args.length)
|
|
30
|
+
consumed.add(index + 1);
|
|
31
|
+
}
|
|
32
|
+
if (arg === "--stack") {
|
|
33
|
+
const parsed = parseCliPrReference(args[index + 1] ?? "");
|
|
34
|
+
if (!parsed)
|
|
35
|
+
return usage(`invalid --stack PR reference: ${args[index + 1] ?? "(missing)"}`);
|
|
36
|
+
if (stack)
|
|
37
|
+
return usage("--stack may only be specified once");
|
|
38
|
+
stack = parsed;
|
|
39
|
+
}
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const equals = arg.indexOf("=");
|
|
44
|
+
const name = equals < 0 ? arg : arg.slice(0, equals);
|
|
45
|
+
if (VALUE_FLAGS.has(name)) {
|
|
46
|
+
if (name === "--format" || name === "--stack")
|
|
47
|
+
consumed.add(index);
|
|
48
|
+
if (name === "--stack") {
|
|
49
|
+
const parsed = parseCliPrReference(arg.slice(equals + 1));
|
|
50
|
+
if (!parsed)
|
|
51
|
+
return usage(`invalid --stack PR reference: ${arg.slice(equals + 1)}`);
|
|
52
|
+
if (stack)
|
|
53
|
+
return usage("--stack may only be specified once");
|
|
54
|
+
stack = parsed;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === "--verbose") {
|
|
59
|
+
consumed.add(index);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg.startsWith("--"))
|
|
63
|
+
continue;
|
|
64
|
+
const parsed = parseCliPrReference(arg);
|
|
65
|
+
if (!parsed)
|
|
66
|
+
continue;
|
|
67
|
+
refs.push(parsed);
|
|
68
|
+
consumed.add(index);
|
|
69
|
+
}
|
|
70
|
+
if (stack && refs.length > 0)
|
|
71
|
+
return usage("--stack cannot be combined with explicit PRs");
|
|
72
|
+
const format = (values.format ?? "text");
|
|
73
|
+
return {
|
|
74
|
+
refs,
|
|
75
|
+
stack,
|
|
76
|
+
global: { format, verbose: values.verbose === true },
|
|
77
|
+
extra: args.filter((_, index) => !consumed.has(index)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function resolvePollTargets(parsed) {
|
|
81
|
+
const all = parsed.stack ? [parsed.stack] : parsed.refs;
|
|
82
|
+
if (all.length === 0)
|
|
83
|
+
return { prNumbers: [] };
|
|
84
|
+
const checkoutRepo = all.some((ref) => ref.repository === undefined)
|
|
85
|
+
? await getRepoInfo()
|
|
86
|
+
: undefined;
|
|
87
|
+
let selectedRepo;
|
|
88
|
+
const prNumbers = [];
|
|
89
|
+
for (const ref of all) {
|
|
90
|
+
const target = resolveParsedPrTarget(ref);
|
|
91
|
+
if (target.prNumber === undefined)
|
|
92
|
+
return usageThrow("PR number is required");
|
|
93
|
+
const repo = target.targetRepository ?? checkoutRepo;
|
|
94
|
+
if (selectedRepo &&
|
|
95
|
+
normalizeRepositoryIdentity(`${selectedRepo.owner}/${selectedRepo.name}`) !==
|
|
96
|
+
normalizeRepositoryIdentity(`${repo.owner}/${repo.name}`)) {
|
|
97
|
+
return usageThrow("aggregate poll only supports PRs from one repository");
|
|
98
|
+
}
|
|
99
|
+
selectedRepo = repo;
|
|
100
|
+
if (!prNumbers.includes(target.prNumber))
|
|
101
|
+
prNumbers.push(target.prNumber);
|
|
102
|
+
}
|
|
103
|
+
return parsed.stack
|
|
104
|
+
? { prNumbers: [], stackPrNumber: prNumbers[0], targetRepository: selectedRepo }
|
|
105
|
+
: { prNumbers, targetRepository: selectedRepo };
|
|
106
|
+
}
|
|
107
|
+
function usage(message) {
|
|
108
|
+
process.stderr.write(`pr-shepherd: ${message}\n`);
|
|
109
|
+
process.exitCode = EXIT.USAGE;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function usageThrow(message) {
|
|
113
|
+
throw new ShepherdError(message, EXIT.USAGE);
|
|
114
|
+
}
|
|
@@ -5,7 +5,6 @@ import { parsePrNumber } from "./args.mjs";
|
|
|
5
5
|
* offending arg so callers can print their own usage message.
|
|
6
6
|
*/
|
|
7
7
|
export function validateDefaultArgs(args, flagsWithValues, booleanFlags, onError) {
|
|
8
|
-
let sawPr = false;
|
|
9
8
|
for (let i = 0; i < args.length; i += 1) {
|
|
10
9
|
const arg = args[i];
|
|
11
10
|
if (flagsWithValues.has(arg)) {
|
|
@@ -21,10 +20,8 @@ export function validateDefaultArgs(args, flagsWithValues, booleanFlags, onError
|
|
|
21
20
|
continue;
|
|
22
21
|
if (booleanFlags.has(arg))
|
|
23
22
|
continue;
|
|
24
|
-
if (parsePrNumber(arg) !== null
|
|
25
|
-
sawPr = true;
|
|
23
|
+
if (parsePrNumber(arg) !== null)
|
|
26
24
|
continue;
|
|
27
|
-
}
|
|
28
25
|
onError(arg);
|
|
29
26
|
return false;
|
|
30
27
|
}
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -213,4 +213,7 @@ async function handleResolve(args, command = "apply review") {
|
|
|
213
213
|
process.stdout.write(globalOpts.format === "json"
|
|
214
214
|
? `${JSON.stringify(result, null, 2)}\n`
|
|
215
215
|
: `${formatMutateResult(result)}\n`);
|
|
216
|
+
if (result.errors.length > 0) {
|
|
217
|
+
process.exitCode = result.rateLimit ? EXIT.TEMPFAIL : EXIT.UNAVAILABLE;
|
|
218
|
+
}
|
|
216
219
|
}
|
|
@@ -26,15 +26,9 @@ export declare function buildRepeatedWorkflowBranchRecoveryInstructions(baseBran
|
|
|
26
26
|
*
|
|
27
27
|
* - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
|
|
28
28
|
* empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
|
|
29
|
-
* Marker-based self-reply routing is already reflected in the generated IDs. The instruction
|
|
30
|
-
* below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
|
|
31
|
-
* not mistaken for an automated reply merely because the GitHub login matches.
|
|
32
|
-
*
|
|
33
29
|
* Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
|
|
34
|
-
* dismiss-ID retention
|
|
35
|
-
*
|
|
36
|
-
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
37
|
-
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
30
|
+
* dismiss-ID retention. The pointer below is load-bearing: without it, nothing in CLI output
|
|
31
|
+
* tells the agent that playbook exists.
|
|
38
32
|
*/
|
|
39
33
|
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
40
34
|
/** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
|
|
@@ -43,23 +43,14 @@ export function buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasE
|
|
|
43
43
|
*
|
|
44
44
|
* - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
|
|
45
45
|
* empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
|
|
46
|
-
* Marker-based self-reply routing is already reflected in the generated IDs. The instruction
|
|
47
|
-
* below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
|
|
48
|
-
* not mistaken for an automated reply merely because the GitHub login matches.
|
|
49
|
-
*
|
|
50
46
|
* Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
|
|
51
|
-
* dismiss-ID retention
|
|
52
|
-
*
|
|
53
|
-
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
54
|
-
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
47
|
+
* dismiss-ID retention. The pointer below is load-bearing: without it, nothing in CLI output
|
|
48
|
+
* tells the agent that playbook exists.
|
|
55
49
|
*/
|
|
56
50
|
export function buildResolveCommandInstruction(resolveCommand) {
|
|
57
51
|
if (!resolveCommand.hasMutations)
|
|
58
52
|
return [];
|
|
59
53
|
const instructions = [];
|
|
60
|
-
if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
|
|
61
|
-
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked thread that is still being resolved is emitted resolve-only, not for another reply.");
|
|
62
|
-
}
|
|
63
54
|
if (resolveCommand.requiresHeadSha) {
|
|
64
55
|
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.");
|
|
65
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
2
|
import { inlineCode } from "../../util/markdown.mjs";
|
|
3
|
+
import { renderResolveCommand } from "./render.mjs";
|
|
3
4
|
function renderEscalateAuthor(item) {
|
|
4
5
|
return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
|
|
5
6
|
}
|
|
@@ -121,6 +122,8 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
121
122
|
const hasItems = escalate.unresolvedThreads.length > 0 ||
|
|
122
123
|
escalate.changesRequestedReviews.length > 0 ||
|
|
123
124
|
escalate.ambiguousComments.length > 0 ||
|
|
125
|
+
(escalate.firstLookSummaries?.length ?? 0) > 0 ||
|
|
126
|
+
(escalate.editedSummaries?.length ?? 0) > 0 ||
|
|
124
127
|
(escalate.checks?.length ?? 0) > 0 ||
|
|
125
128
|
(escalate.stalledChecks?.length ?? 0) > 0;
|
|
126
129
|
if (hasItems) {
|
|
@@ -157,6 +160,20 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
157
160
|
lines.push(` > ${bodyLine}`);
|
|
158
161
|
lines.push("");
|
|
159
162
|
}
|
|
163
|
+
for (const [heading, summaries] of [
|
|
164
|
+
["Review summaries (first look)", escalate.firstLookSummaries ?? []],
|
|
165
|
+
["Review summaries (edited since first look)", escalate.editedSummaries ?? []],
|
|
166
|
+
]) {
|
|
167
|
+
if (summaries.length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
lines.push(`### ${heading}`, "");
|
|
170
|
+
for (const summary of summaries) {
|
|
171
|
+
lines.push(`- review \`${summary.id}\` (${renderEscalateAuthor(summary)}):`, "");
|
|
172
|
+
for (const bodyLine of summary.body.split("\n"))
|
|
173
|
+
lines.push(` > ${bodyLine}`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
160
177
|
for (const c of escalate.ambiguousComments) {
|
|
161
178
|
lines.push(`- comment \`${c.id}\` (${renderEscalateAuthor(c)}):`);
|
|
162
179
|
lines.push("");
|
|
@@ -194,7 +211,17 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
194
211
|
lines.push("## Fix attempts");
|
|
195
212
|
lines.push("");
|
|
196
213
|
for (const a of escalate.thrashHistory) {
|
|
197
|
-
lines.push(`- thread \`${a.threadId}\`
|
|
214
|
+
lines.push(`- thread \`${a.threadId}\` pending commands returned ${a.attempts} times`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const pending = escalate.pendingReviewCommands;
|
|
218
|
+
if (pending?.resolveOnlyCommand?.hasMutations || pending?.resolveCommand?.hasMutations) {
|
|
219
|
+
lines.push("", "## Pending review commands", "");
|
|
220
|
+
if (pending.resolveOnlyCommand?.hasMutations) {
|
|
221
|
+
lines.push(`- resolve-only: \`${renderResolveCommand(pending.resolveOnlyCommand)}\``);
|
|
222
|
+
}
|
|
223
|
+
if (pending.resolveCommand?.hasMutations) {
|
|
224
|
+
lines.push(`- apply review: \`${renderResolveCommand(pending.resolveCommand)}\``);
|
|
198
225
|
}
|
|
199
226
|
}
|
|
200
227
|
lines.push("");
|
|
@@ -227,7 +254,8 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
227
254
|
return `Could not determine the PR's base branch${reason} — automated rebases are paused because branch safety is unclear. Run the rebase manually against the PR's real target branch.`;
|
|
228
255
|
}
|
|
229
256
|
if (triggers.includes("fix-thrash")) {
|
|
230
|
-
|
|
257
|
+
const attempts = loadConfig().iterate.fixAttemptsPerThread;
|
|
258
|
+
return `The same thread(s) remain unresolved after their pending review commands were returned for ${attempts} FIX_CODE ticks. Automated iteration is paused for a manual decision.`;
|
|
231
259
|
}
|
|
232
260
|
if (triggers.includes("bot-cr-not-dismissed")) {
|
|
233
261
|
const ids = detail ? ` (review IDs: ${detail})` : "";
|
|
@@ -33,7 +33,7 @@ function checkRequiresHumanFollowUp(check) {
|
|
|
33
33
|
return !check.detailsUrl?.trim();
|
|
34
34
|
return !check.logExcerpt?.trim();
|
|
35
35
|
}
|
|
36
|
-
function nextFixAttempts(stored,
|
|
36
|
+
function nextFixAttempts(stored, threads, countAttempt) {
|
|
37
37
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
38
38
|
const threadBodyHashes = stored?.threadBodyHashes
|
|
39
39
|
? { ...stored.threadBodyHashes }
|
|
@@ -41,13 +41,32 @@ function nextFixAttempts(stored, headSha, threads) {
|
|
|
41
41
|
for (const t of threads) {
|
|
42
42
|
const bodyHash = hashBody(threadTranscriptBody(t));
|
|
43
43
|
const previousHash = threadBodyHashes[t.id];
|
|
44
|
-
if (
|
|
44
|
+
if (!countAttempt)
|
|
45
45
|
continue;
|
|
46
46
|
threadAttempts[t.id] = previousHash === bodyHash ? (threadAttempts[t.id] ?? 0) + 1 : 1;
|
|
47
47
|
threadBodyHashes[t.id] = bodyHash;
|
|
48
48
|
}
|
|
49
49
|
return { threadAttempts, threadBodyHashes };
|
|
50
50
|
}
|
|
51
|
+
function previousFixAttempts(stored, threads) {
|
|
52
|
+
if (!stored?.threadBodyHashes)
|
|
53
|
+
return {};
|
|
54
|
+
const attempts = {};
|
|
55
|
+
for (const thread of threads) {
|
|
56
|
+
const bodyHash = hashBody(threadTranscriptBody(thread));
|
|
57
|
+
if (stored.threadBodyHashes[thread.id] === bodyHash) {
|
|
58
|
+
attempts[thread.id] = stored.threadAttempts[thread.id] ?? 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return attempts;
|
|
62
|
+
}
|
|
63
|
+
function pendingReviewCommands(resolveCommand, resolveOnlyCommand) {
|
|
64
|
+
const pending = {
|
|
65
|
+
...(resolveOnlyCommand?.hasMutations && { resolveOnlyCommand }),
|
|
66
|
+
...(resolveCommand.hasMutations && { resolveCommand }),
|
|
67
|
+
};
|
|
68
|
+
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
69
|
+
}
|
|
51
70
|
export async function handleFixCode(ctx) {
|
|
52
71
|
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, } = ctx;
|
|
53
72
|
const prReference = formatPrUrl(report.repo, prNumber);
|
|
@@ -70,7 +89,22 @@ export async function handleFixCode(ctx) {
|
|
|
70
89
|
const retryableActionableThreads = mutationActionableThreads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
71
90
|
const protectedRuns = [];
|
|
72
91
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
73
|
-
const
|
|
92
|
+
const countFixCodeAttempt = opts.persistSeen !== false;
|
|
93
|
+
const priorThreadAttempts = previousFixAttempts(stored, retryableActionableThreads);
|
|
94
|
+
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, retryableActionableThreads, countFixCodeAttempt);
|
|
95
|
+
const resolutionOnlyThreadsForWork = report.threads.resolutionOnly.filter((thread) => !skippedThreadIds.has(thread.id) &&
|
|
96
|
+
((thread.path !== null && thread.line !== null) ||
|
|
97
|
+
threadHasAuthorizedMutation(thread, replyIdSet, resolveIdSet)));
|
|
98
|
+
const actionableChangesRequestedReviews = report.changesRequestedReviews.filter((review) => review.staleReview !== true ||
|
|
99
|
+
!isHumanAuthor(review) ||
|
|
100
|
+
isConfiguredBotAuthor(review, botUsernames));
|
|
101
|
+
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !unauthorizedDismissals.some((candidate) => candidate.id === review.id));
|
|
102
|
+
const buildReviewCommands = (checks) => buildResolveCommand(report.threads.actionable
|
|
103
|
+
.filter((thread) => mutationActionableThreads.some((candidate) => candidate.id === thread.id))
|
|
104
|
+
.map(toAgentThread), resolutionOnlyThreadsForWork, [
|
|
105
|
+
...(report.comments.minimizeIds ?? report.comments.actionable.map((comment) => comment.id)),
|
|
106
|
+
...reviewSummaryIds,
|
|
107
|
+
], changesRequestedReviewsForWork, checks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads, resolveOtherHumanThreads);
|
|
74
108
|
const botCrReviews = report.changesRequestedReviews.filter((r) => (!isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames)) &&
|
|
75
109
|
report.viewerAuthorization?.viewerCanAdminister === true);
|
|
76
110
|
const botCrStateKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
@@ -79,13 +113,16 @@ export async function handleFixCode(ctx) {
|
|
|
79
113
|
const { next: nextBotCrState, staleIds: staleBotCrIds } = updateBotCrSeenState(previousBotCrState, botCrReviews, nowSeconds, stallTimeoutSeconds);
|
|
80
114
|
await writeBotCrSeenState(botCrStateKey, nextBotCrState);
|
|
81
115
|
if (staleBotCrIds.length > 0) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
116
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(toAgentChecks(failingChecks));
|
|
117
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
84
118
|
const escalateBase = {
|
|
85
119
|
triggers: ["bot-cr-not-dismissed"],
|
|
86
120
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
87
121
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
88
|
-
changesRequestedReviews:
|
|
122
|
+
changesRequestedReviews: report.changesRequestedReviews,
|
|
123
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
124
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
125
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
89
126
|
suggestion: buildEscalateSuggestion(["bot-cr-not-dismissed"], staleBotCrIds.join(", ")),
|
|
90
127
|
};
|
|
91
128
|
return {
|
|
@@ -99,14 +136,21 @@ export async function handleFixCode(ctx) {
|
|
|
99
136
|
},
|
|
100
137
|
};
|
|
101
138
|
}
|
|
102
|
-
const escalateTriggers =
|
|
139
|
+
const escalateTriggers = countFixCodeAttempt
|
|
140
|
+
? checkEscalateTriggers(retryableActionableThreads, priorThreadAttempts)
|
|
141
|
+
: { triggers: [], thrashHistory: undefined };
|
|
103
142
|
if (escalateTriggers.triggers.length > 0) {
|
|
143
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(toAgentChecks(failingChecks));
|
|
144
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
104
145
|
const escalateBase = {
|
|
105
146
|
triggers: escalateTriggers.triggers,
|
|
106
147
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
107
148
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
108
149
|
changesRequestedReviews: report.changesRequestedReviews,
|
|
150
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
151
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
109
152
|
thrashHistory: escalateTriggers.thrashHistory,
|
|
153
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
110
154
|
suggestion: buildEscalateSuggestion(escalateTriggers.triggers),
|
|
111
155
|
};
|
|
112
156
|
return {
|
|
@@ -120,7 +164,6 @@ export async function handleFixCode(ctx) {
|
|
|
120
164
|
},
|
|
121
165
|
};
|
|
122
166
|
}
|
|
123
|
-
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
124
167
|
// GitHub does not expose a per-run viewer capability for cancellation, so Shepherd never
|
|
125
168
|
// issues or recommends a cancellation regardless of repository role. A rerun is different:
|
|
126
169
|
// GitHub's Actions rerun API requires actions:write, which rides with WRITE+ repo access, so
|
|
@@ -163,14 +206,6 @@ export async function handleFixCode(ctx) {
|
|
|
163
206
|
...toAgentChecks(annotatedExtra).map((c) => ({ ...c, annotationOnly: true })),
|
|
164
207
|
];
|
|
165
208
|
const { changesRequestedReviews } = report;
|
|
166
|
-
const actionableChangesRequestedReviews = changesRequestedReviews.filter((review) => review.staleReview !== true ||
|
|
167
|
-
!isHumanAuthor(review) ||
|
|
168
|
-
isConfiguredBotAuthor(review, botUsernames));
|
|
169
|
-
const skippedDismissalIds = new Set(unauthorizedDismissals.map((review) => review.id));
|
|
170
|
-
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !skippedDismissalIds.has(review.id));
|
|
171
|
-
const resolutionOnlyThreadsForWork = resolutionOnlyThreads.filter((thread) => !skippedThreadIds.has(thread.id) &&
|
|
172
|
-
((thread.path !== null && thread.line !== null) ||
|
|
173
|
-
threadHasAuthorizedMutation(thread, replyIdSet, resolveIdSet)));
|
|
174
209
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
175
210
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
176
211
|
const { behindBaseHint } = loadConfig().iterate;
|
|
@@ -179,7 +214,6 @@ export async function handleFixCode(ctx) {
|
|
|
179
214
|
// unnecessary cancellation.
|
|
180
215
|
const inProgressRunIds = [];
|
|
181
216
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
182
|
-
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
183
217
|
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
184
218
|
const manualFollowUpChecks = failingAgentChecks.filter((check) => !belongsToActiveWorkflowRun(check) && checkRequiresHumanFollowUp(check));
|
|
185
219
|
const exhaustedAttempts = manualFollowUpChecks.filter((check) => check.runAttempt !== undefined && check.runAttempt > 1);
|
|
@@ -199,6 +233,8 @@ export async function handleFixCode(ctx) {
|
|
|
199
233
|
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
200
234
|
failingAgentChecks.some((check) => belongsToActiveWorkflowRun(check) || !checkRequiresHumanFollowUp(check));
|
|
201
235
|
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
236
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(failingAgentChecks);
|
|
237
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
202
238
|
const checkSuggestion = exhaustedAttempts.length > 0
|
|
203
239
|
? `GitHub reports a later workflow attempt (${exhaustedAttempts
|
|
204
240
|
.map((check) => `${check.runId ?? check.name}: attempt ${check.runAttempt}`)
|
|
@@ -209,7 +245,10 @@ export async function handleFixCode(ctx) {
|
|
|
209
245
|
unresolvedThreads: [],
|
|
210
246
|
ambiguousComments: [],
|
|
211
247
|
changesRequestedReviews,
|
|
248
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
249
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
212
250
|
checks: manualFollowUpChecks,
|
|
251
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
213
252
|
suggestion: checkSuggestion,
|
|
214
253
|
};
|
|
215
254
|
return {
|
|
@@ -225,9 +264,7 @@ export async function handleFixCode(ctx) {
|
|
|
225
264
|
}
|
|
226
265
|
// Push access to the PR head branch is a usage precondition. Build review mutations for
|
|
227
266
|
// conflict ticks normally so the caller can push and complete the same fix_code cycle.
|
|
228
|
-
const
|
|
229
|
-
const mutationAgentThreads = threads.filter((thread) => mutationActionableIds.has(thread.id));
|
|
230
|
-
const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(mutationAgentThreads, resolutionOnlyThreadsForWork, allCommentIds, changesRequestedReviewsForWork, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads, resolveOtherHumanThreads);
|
|
267
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(failingAgentChecks);
|
|
231
268
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
232
269
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
233
270
|
// prediction that the agent *will* push. Located resolution-only threads retain that guard;
|
|
@@ -241,11 +278,15 @@ export async function handleFixCode(ctx) {
|
|
|
241
278
|
actionableComments.length > 0 ||
|
|
242
279
|
locatedResolutionOnlyThreadsForWork.length > 0;
|
|
243
280
|
if (baseLookup.isFallback && pushIsPlausible) {
|
|
281
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
244
282
|
const fallbackEscalateBase = {
|
|
245
283
|
triggers: ["base-branch-unknown"],
|
|
246
284
|
unresolvedThreads: [...threads, ...resolutionOnlyThreads.map(toAgentThread)],
|
|
247
285
|
ambiguousComments: actionableComments,
|
|
248
286
|
changesRequestedReviews,
|
|
287
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
288
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
289
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
249
290
|
suggestion: buildEscalateSuggestion(["base-branch-unknown"], baseLookup.failureReason),
|
|
250
291
|
};
|
|
251
292
|
return {
|
|
@@ -288,6 +329,7 @@ export async function handleFixCode(ctx) {
|
|
|
288
329
|
};
|
|
289
330
|
const result = await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds);
|
|
290
331
|
if (result.action === "fix_code" && opts.persistSeen !== false) {
|
|
332
|
+
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
291
333
|
await Promise.allSettled(result.fix.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
|
|
292
334
|
}
|
|
293
335
|
return result;
|