pr-shepherd 0.45.0 → 0.46.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 +16 -5
- 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 +13 -0
- package/bin/cli/help-command-pages.mjs +13 -0
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +14 -1
- 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/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/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +16 -13
- package/bin/commands/poll.mjs +1 -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 -1
- package/bin/github/client.mjs +1 -1
- 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/pr-shepherd/SKILL.md +21 -15
|
@@ -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 requested mark-ready or merge/enqueue operation. Ask a repository maintainer to perform that state change; Shepherd will not recommend a command that would be denied.";
|
|
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) {
|
|
@@ -2,4 +2,4 @@ import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, Fir
|
|
|
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
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
|
|
5
|
+
isBehind?: boolean, viewerCanUpdate?: boolean): string[];
|
|
@@ -11,8 +11,10 @@ export function renderResolveCommand(rc) {
|
|
|
11
11
|
return renderShellCommand(parts);
|
|
12
12
|
}
|
|
13
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
|
|
14
|
+
isBehind = false, viewerCanUpdate = false) {
|
|
15
15
|
const instructions = [];
|
|
16
|
+
const locatedThreads = threads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
17
|
+
const unlocatedThreads = threads.filter((thread) => thread.path === null || thread.line === null);
|
|
16
18
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
17
19
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
18
20
|
const hasNonConflictHints = threads.length > 0 ||
|
|
@@ -23,8 +25,10 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
23
25
|
// Start with interpretation. The agent decides what raw feedback warrants a code change.
|
|
24
26
|
if (hasNonConflictHints) {
|
|
25
27
|
const actionableSections = [];
|
|
26
|
-
if (
|
|
28
|
+
if (locatedThreads.length > 0)
|
|
27
29
|
actionableSections.push("`## Review threads`");
|
|
30
|
+
if (unlocatedThreads.length > 0)
|
|
31
|
+
actionableSections.push("`## Unlocated review threads (logged once — no mutation)`");
|
|
28
32
|
if (actionableComments.length > 0)
|
|
29
33
|
actionableSections.push("`## Actionable comments`");
|
|
30
34
|
if (failingChecks.length > 0)
|
|
@@ -53,17 +57,20 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
53
57
|
if (editedTotal > 0) {
|
|
54
58
|
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
59
|
}
|
|
60
|
+
if (unlocatedThreads.length > 0) {
|
|
61
|
+
instructions.push("Acknowledge each item under `## Unlocated review threads (logged once — no mutation)`. Shepherd cannot route a code fix or review mutation without a path and line; the unchanged item will be skipped on later ticks.");
|
|
62
|
+
}
|
|
56
63
|
// GitHub exposes no exact viewer capability for workflow-run cancellation, so the
|
|
57
64
|
// informational run lists never produce a cancellation recommendation.
|
|
58
65
|
void inProgressRunIds;
|
|
59
66
|
void cancelledCount;
|
|
60
|
-
const hasSuggestions =
|
|
67
|
+
const hasSuggestions = locatedThreads.some((t) => t.suggestion);
|
|
61
68
|
if (hasSuggestions)
|
|
62
69
|
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
63
|
-
if (
|
|
70
|
+
if (locatedThreads.length > 0 || actionableComments.length > 0) {
|
|
64
71
|
// Actionable comments carry no file/line location (unlike threads), so "referenced above"
|
|
65
72
|
// is only accurate when threads are present.
|
|
66
|
-
const filesRef =
|
|
73
|
+
const filesRef = locatedThreads.length > 0 ? "each file referenced above" : "the relevant files";
|
|
67
74
|
instructions.push(`Apply every warranted review fix in ${filesRef}.`);
|
|
68
75
|
}
|
|
69
76
|
if (resolutionOnlyThreads.length > 0) {
|
|
@@ -81,14 +88,10 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
81
88
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
82
89
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
83
90
|
if (hasConflicts) {
|
|
84
|
-
instructions.push(
|
|
85
|
-
? `Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`
|
|
86
|
-
: `Commit any remaining conflict-resolution changes${mutationSuffix}.`);
|
|
91
|
+
instructions.push(`Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`);
|
|
87
92
|
}
|
|
88
93
|
else if (hasNonConflictHints) {
|
|
89
|
-
instructions.push(
|
|
90
|
-
? "If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue with the remaining steps."
|
|
91
|
-
: "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.");
|
|
94
|
+
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue with the remaining steps.");
|
|
92
95
|
}
|
|
93
96
|
if (viewerCanUpdate &&
|
|
94
97
|
(hasReviewMutations ||
|
|
@@ -100,7 +103,7 @@ isBehind = false, viewerCanUpdate = false, pushAuthorized = false) {
|
|
|
100
103
|
}
|
|
101
104
|
if (resolveOnlyCommand?.hasMutations)
|
|
102
105
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
103
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand
|
|
104
|
-
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha
|
|
106
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand));
|
|
107
|
+
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
105
108
|
return instructions;
|
|
106
109
|
}
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -99,10 +99,7 @@ async function runPollCore(opts) {
|
|
|
99
99
|
pendingQuotaWarning !== undefined &&
|
|
100
100
|
!(lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) &&
|
|
101
101
|
!(debounceUntil !== null && !pastDebounce)) {
|
|
102
|
-
|
|
103
|
-
(lastResult.action === "fix_code" &&
|
|
104
|
-
lastResult.fix.instructions.some((instruction) => /stop polling|human direction/i.test(instruction)));
|
|
105
|
-
if (terminalOrHandoff) {
|
|
102
|
+
if (["cancel", "escalate"].includes(lastResult.action)) {
|
|
106
103
|
const { quotaWarning: _quotaWarning, ...withoutQuotaWarning } = lastResult;
|
|
107
104
|
lastResult = withoutQuotaWarning;
|
|
108
105
|
}
|
|
@@ -24,5 +24,5 @@ export declare function classifyReviewsForDisplay(reviews: Review[], seenMap: Ma
|
|
|
24
24
|
*
|
|
25
25
|
* Human-authored CR reviews continue to flow through the standard seen-gate.
|
|
26
26
|
*/
|
|
27
|
-
export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames): ReviewVisibility;
|
|
27
|
+
export declare function classifyChangesRequestedReviewsForDisplay(reviews: Review[], seenMap: Map<string, SeenMarker>, botUsernames: NormalizedBotUsernames, repeatBotReviews?: boolean): ReviewVisibility;
|
|
28
28
|
export {};
|
|
@@ -31,7 +31,7 @@ export function classifyReviewsForDisplay(reviews, seenMap) {
|
|
|
31
31
|
*
|
|
32
32
|
* Human-authored CR reviews continue to flow through the standard seen-gate.
|
|
33
33
|
*/
|
|
34
|
-
export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botUsernames) {
|
|
34
|
+
export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botUsernames, repeatBotReviews = true) {
|
|
35
35
|
const visible = [];
|
|
36
36
|
const toMarkSeen = [];
|
|
37
37
|
for (const review of reviews) {
|
|
@@ -39,7 +39,8 @@ export function classifyChangesRequestedReviewsForDisplay(reviews, seenMap, botU
|
|
|
39
39
|
const cls = classifyItem(review.id, review.body, seenMap);
|
|
40
40
|
if (isBot) {
|
|
41
41
|
if (cls === "unchanged") {
|
|
42
|
-
|
|
42
|
+
if (repeatBotReviews)
|
|
43
|
+
visible.push({ ...review, staleBotCr: true });
|
|
43
44
|
}
|
|
44
45
|
else if (cls === "edited") {
|
|
45
46
|
visible.push({ ...review, edited: true });
|
|
@@ -7,5 +7,5 @@ interface ThreadVisibility {
|
|
|
7
7
|
firstLookThreads: FirstLookThread[];
|
|
8
8
|
toMarkSeen: ReviewThread[];
|
|
9
9
|
}
|
|
10
|
-
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames): ThreadVisibility;
|
|
10
|
+
export declare function classifyThreadVisibility(threads: ReviewThread[], seenMap: Map<string, SeenMarker>, botUsernames?: NormalizedBotUsernames, repeatableThreadIds?: ReadonlySet<string>): ThreadVisibility;
|
|
11
11
|
export {};
|