pr-shepherd 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -17
- package/bin/api.d.mts +3 -2
- package/bin/api.mjs +13 -18
- package/bin/cli/args.mjs +13 -9
- package/bin/cli/fix-formatter.mjs +8 -6
- package/bin/cli/help-command-pages.d.mts +9 -6
- package/bin/cli/help-command-pages.mjs +7 -4
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +2 -2
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +10 -7
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +5 -5
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/journal-formatter.mjs +3 -0
- package/bin/cli/journal-handler.mjs +11 -7
- package/bin/cli/list-formatters.d.mts +3 -1
- package/bin/cli/list-formatters.mjs +9 -7
- package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -1
- package/bin/cli/mutate-formatter.mjs +4 -0
- package/bin/commands/check.mjs +22 -6
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +2 -2
- package/bin/commands/commit-suggestion.mjs +3 -1
- package/bin/commands/iterate/check-instructions.d.mts +5 -19
- package/bin/commands/iterate/check-instructions.mjs +24 -25
- package/bin/commands/iterate/classify.d.mts +2 -2
- package/bin/commands/iterate/classify.mjs +43 -25
- package/bin/commands/iterate/escalate.d.mts +1 -1
- package/bin/commands/iterate/escalate.mjs +11 -0
- package/bin/commands/iterate/fix-code.mjs +81 -42
- package/bin/commands/iterate/helpers.d.mts +0 -2
- package/bin/commands/iterate/helpers.mjs +0 -15
- package/bin/commands/iterate/index.mjs +6 -12
- package/bin/commands/iterate/mark-ready.d.mts +2 -0
- package/bin/commands/iterate/mark-ready.mjs +38 -0
- package/bin/commands/iterate/merge-state.mjs +38 -9
- package/bin/commands/iterate/merge.mjs +0 -1
- package/bin/commands/iterate/render.d.mts +2 -2
- package/bin/commands/iterate/render.mjs +19 -20
- package/bin/commands/iterate/stall.mjs +4 -2
- package/bin/commands/iterate/thread-mutation-routing.d.mts +9 -0
- package/bin/commands/iterate/thread-mutation-routing.mjs +29 -0
- package/bin/commands/journal/index.d.mts +5 -0
- package/bin/commands/journal/index.mjs +11 -2
- package/bin/commands/mark-files-as-viewed.d.mts +2 -1
- package/bin/commands/mark-files-as-viewed.mjs +5 -92
- package/bin/commands/resolve-mutate.mjs +51 -9
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/commands/suggestion-patch-item.d.mts +1 -1
- package/bin/commands/suggestion-patch-item.mjs +3 -11
- package/bin/commands/suggestion-patches.mjs +3 -3
- package/bin/comments/authors.d.mts +6 -0
- package/bin/comments/authors.mjs +5 -0
- package/bin/comments/marker.d.mts +6 -1
- package/bin/comments/marker.mjs +8 -1
- package/bin/comments/resolve.d.mts +4 -0
- package/bin/comments/thread-visibility.mjs +10 -10
- package/bin/comments/visible-comments.mjs +2 -1
- package/bin/config/load.d.mts +1 -1
- package/bin/github/batch-parsers.d.mts +2 -2
- package/bin/github/batch-parsers.mjs +17 -1
- package/bin/github/batch-raw-types.d.mts +12 -0
- package/bin/github/batch.mjs +1 -1
- package/bin/github/client.d.mts +1 -0
- package/bin/github/client.mjs +8 -1
- package/bin/github/gql/batch-pr-page.gql +6 -0
- package/bin/github/gql/batch-pr.gql +13 -0
- package/bin/github/gql/get-pr-body.gql +1 -0
- package/bin/github/gql/review-thread-comments.gql +1 -0
- package/bin/github/gql/suggestion-threads.gql +1 -0
- package/bin/github/suggestion-thread.mjs +1 -0
- package/bin/mcp/server.mjs +12 -6
- package/bin/pr-reference.d.mts +13 -0
- package/bin/pr-reference.mjs +23 -1
- package/bin/reporters/agent.mjs +5 -0
- package/bin/threads/transcript.d.mts +2 -0
- package/bin/threads/transcript.mjs +2 -0
- package/bin/types/agent-thread.d.mts +1 -0
- package/bin/types/escalate.d.mts +6 -1
- package/bin/types/github.d.mts +19 -0
- package/bin/types/iterate.d.mts +9 -8
- package/bin/types/report.d.mts +14 -1
- package/bin/types/review-thread.d.mts +1 -0
- package/bin/types/suggestion-patch.d.mts +1 -1
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +3 -3
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +15 -13
- package/bin/commands/iterate/reruns.d.mts +0 -20
- package/bin/commands/iterate/reruns.mjs +0 -105
|
@@ -10,8 +10,8 @@ export function renderResolveCommand(rc) {
|
|
|
10
10
|
parts.push("--require-sha", "$HEAD_SHA");
|
|
11
11
|
return renderShellCommand(parts);
|
|
12
12
|
}
|
|
13
|
-
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts,
|
|
14
|
-
isBehind = false) {
|
|
13
|
+
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
14
|
+
isBehind = false, viewerCanUpdate = false) {
|
|
15
15
|
const instructions = [];
|
|
16
16
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
17
17
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
@@ -38,13 +38,13 @@ isBehind = false) {
|
|
|
38
38
|
instructions.push(`Review each item ${sectionRef} and decide whether it needs a code change.`);
|
|
39
39
|
}
|
|
40
40
|
if (hasConflicts) {
|
|
41
|
-
instructions.push("The branch has merge conflicts (see `**branch**` above). Resolve them before committing
|
|
41
|
+
instructions.push("The branch has merge conflicts (see `**branch**` above). Resolve them before committing.");
|
|
42
42
|
}
|
|
43
43
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
44
44
|
if (firstLookTotal > 0) {
|
|
45
45
|
instructions.push("Review every item under `## First-look items` before acting.");
|
|
46
46
|
}
|
|
47
|
-
if (firstLookSummaries.length > 0)
|
|
47
|
+
if (firstLookSummaries.length > 0 && viewerCanUpdate)
|
|
48
48
|
instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
|
|
49
49
|
const editedTotal = editedSummaries.length +
|
|
50
50
|
actionableComments.filter((c) => c.edited).length +
|
|
@@ -53,15 +53,13 @@ isBehind = false) {
|
|
|
53
53
|
if (editedTotal > 0) {
|
|
54
54
|
instructions.push("Read every item marked `[edited since first look]`, including edited summaries and edited first-look bullets, before deciding whether to resolve a matching thread.");
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
instructions.push("Do not cancel the IDs under `## Cancelled runs` again. The CLI already cancelled them.");
|
|
61
|
-
}
|
|
56
|
+
// GitHub exposes no exact viewer capability for workflow-run cancellation, so the
|
|
57
|
+
// informational run lists never produce a cancellation recommendation.
|
|
58
|
+
void inProgressRunIds;
|
|
59
|
+
void cancelledCount;
|
|
62
60
|
const hasSuggestions = threads.some((t) => t.suggestion);
|
|
63
61
|
if (hasSuggestions)
|
|
64
|
-
instructions.push(buildCommitSuggestionInstruction(
|
|
62
|
+
instructions.push(buildCommitSuggestionInstruction(prReference, "## Review threads"));
|
|
65
63
|
if (threads.length > 0 || actionableComments.length > 0) {
|
|
66
64
|
// Actionable comments carry no file/line location (unlike threads), so "referenced above"
|
|
67
65
|
// is only accurate when threads are present.
|
|
@@ -83,21 +81,22 @@ isBehind = false) {
|
|
|
83
81
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
84
82
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
85
83
|
if (hasConflicts) {
|
|
86
|
-
instructions.push(`Commit any remaining
|
|
84
|
+
instructions.push(`Commit any remaining conflict-resolution changes${mutationSuffix}.`);
|
|
87
85
|
}
|
|
88
86
|
else if (hasNonConflictHints) {
|
|
89
|
-
instructions.push(
|
|
87
|
+
instructions.push("If you changed code, commit any remaining changes, then stop and hand off for a push whose authorization is established outside Shepherd; do not run the remaining review mutations or iterate until the remote PR head changes. Shepherd cannot verify the Git credential's push authorization. If you did not change code, do not commit and continue with the remaining steps.");
|
|
90
88
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
if (viewerCanUpdate &&
|
|
90
|
+
(hasReviewMutations ||
|
|
91
|
+
hasNonConflictHints ||
|
|
92
|
+
firstLookTotal > 0 ||
|
|
93
|
+
firstLookSummaries.length > 0 ||
|
|
94
|
+
editedTotal > 0)) {
|
|
95
|
+
instructions.push(buildShepherdJournalInstruction(prReference));
|
|
97
96
|
}
|
|
98
97
|
if (resolveOnlyCommand?.hasMutations)
|
|
99
98
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
100
99
|
instructions.push(...buildResolveCommandInstruction(resolveCommand));
|
|
101
|
-
instructions.push(buildFixCompletionInstruction(failingChecks));
|
|
100
|
+
instructions.push(buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
102
101
|
return instructions;
|
|
103
102
|
}
|
|
@@ -2,6 +2,7 @@ import { readStallState, writeStallState } from "../../state/iterate-stall.mjs";
|
|
|
2
2
|
import { toAgentThread, toAgentComment, toAgentStalledCheck } from "../../reporters/agent.mjs";
|
|
3
3
|
import { buildEscalateSuggestion, buildEscalateHumanMessage, formatDurationApprox, } from "./escalate.mjs";
|
|
4
4
|
import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
5
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
6
|
function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds) {
|
|
6
7
|
const checks = [
|
|
7
8
|
...report.checks.failing.map((f) => `failing:${f.name}:${f.conclusion}`),
|
|
@@ -36,6 +37,7 @@ function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds
|
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds) {
|
|
40
|
+
const prReference = formatPrUrl(report.repo, prNumber);
|
|
39
41
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
40
42
|
const stalledChecks = findCiStartStalledChecks(report.checks.inProgress, nowSeconds, {
|
|
41
43
|
stallTimeoutSeconds,
|
|
@@ -56,7 +58,7 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
56
58
|
action: "escalate",
|
|
57
59
|
escalate: {
|
|
58
60
|
...escalateBase,
|
|
59
|
-
humanMessage: buildEscalateHumanMessage(escalateBase,
|
|
61
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
60
62
|
},
|
|
61
63
|
};
|
|
62
64
|
}
|
|
@@ -86,7 +88,7 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
86
88
|
action: "escalate",
|
|
87
89
|
escalate: {
|
|
88
90
|
...escalateBase,
|
|
89
|
-
humanMessage: buildEscalateHumanMessage(escalateBase,
|
|
91
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
90
92
|
},
|
|
91
93
|
};
|
|
92
94
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type NormalizedBotUsernames } from "../../comments/authors.mts";
|
|
2
|
+
import type { AgentThread, ReviewThread } from "../../types.mts";
|
|
3
|
+
export interface ThreadMutationRouting {
|
|
4
|
+
replyThreadIds: string[];
|
|
5
|
+
pairedResolveThreadIds: string[];
|
|
6
|
+
standaloneResolveThreadIds: string[];
|
|
7
|
+
resolveThreadIds: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function buildThreadMutationRouting(threads: Array<AgentThread | ReviewThread>, botUsernames: NormalizedBotUsernames, ruleAutoResolveThreadIds: string[]): ThreadMutationRouting;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "../../comments/authors.mjs";
|
|
2
|
+
import { threadEndedByShepherd } from "../../comments/marker.mjs";
|
|
3
|
+
function dedupeIds(ids) {
|
|
4
|
+
return [...new Set(ids)];
|
|
5
|
+
}
|
|
6
|
+
export function buildThreadMutationRouting(threads, botUsernames, ruleAutoResolveThreadIds) {
|
|
7
|
+
const isOrdinaryHuman = (thread) => isHumanAuthor(thread) && !isConfiguredBotAuthor(thread, botUsernames);
|
|
8
|
+
const replyThreadIds = dedupeIds(threads
|
|
9
|
+
.filter((thread) => isOrdinaryHuman(thread) && !threadEndedByShepherd(thread))
|
|
10
|
+
.map((thread) => thread.id));
|
|
11
|
+
const pairedResolveThreadIds = dedupeIds(threads
|
|
12
|
+
.filter((thread) => isViewerAuthoredHuman(thread, botUsernames) && !threadEndedByShepherd(thread))
|
|
13
|
+
.map((thread) => thread.id));
|
|
14
|
+
const pairedResolveIdSet = new Set(pairedResolveThreadIds);
|
|
15
|
+
// Rule-matched threads bypass author routing; resolve-mutate retains the human-author guard.
|
|
16
|
+
const standaloneResolveThreadIds = dedupeIds([
|
|
17
|
+
...threads
|
|
18
|
+
.filter((thread) => !isOrdinaryHuman(thread) ||
|
|
19
|
+
(isViewerAuthoredHuman(thread, botUsernames) && threadEndedByShepherd(thread)))
|
|
20
|
+
.map((thread) => thread.id),
|
|
21
|
+
...ruleAutoResolveThreadIds,
|
|
22
|
+
]).filter((id) => !pairedResolveIdSet.has(id));
|
|
23
|
+
return {
|
|
24
|
+
replyThreadIds,
|
|
25
|
+
pairedResolveThreadIds,
|
|
26
|
+
standaloneResolveThreadIds,
|
|
27
|
+
resolveThreadIds: dedupeIds([...pairedResolveThreadIds, ...standaloneResolveThreadIds]),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export interface RunJournalOptions {
|
|
2
2
|
prNumber: number | undefined;
|
|
3
|
+
targetRepository?: {
|
|
4
|
+
owner: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
3
7
|
rawItem: string;
|
|
4
8
|
dryRun: boolean;
|
|
5
9
|
}
|
|
@@ -9,6 +13,7 @@ export interface JournalResult {
|
|
|
9
13
|
sectionExisted: boolean;
|
|
10
14
|
dryRun: boolean;
|
|
11
15
|
previewBody?: string;
|
|
16
|
+
authorizationSkipped?: "denied-or-unverifiable";
|
|
12
17
|
}
|
|
13
18
|
/** @deprecated Hidden implementation for standalone `journal`; use `apply journal`. */
|
|
14
19
|
export declare function runJournal(opts: RunJournalOptions): Promise<JournalResult>;
|
|
@@ -7,13 +7,22 @@ export async function runJournal(opts) {
|
|
|
7
7
|
throw new Error(validation.error);
|
|
8
8
|
}
|
|
9
9
|
const { item } = validation;
|
|
10
|
-
const { owner, name } = await getRepoInfo();
|
|
10
|
+
const { owner, name } = opts.targetRepository ?? (await getRepoInfo());
|
|
11
11
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
12
12
|
if (!prNumber) {
|
|
13
13
|
throw new Error("PR number is required: no PR number provided and none found for current branch");
|
|
14
14
|
}
|
|
15
|
-
const { nodeId, body } = await getPullRequestBody(prNumber, owner, name);
|
|
15
|
+
const { nodeId, body, viewerCanUpdate } = await getPullRequestBody(prNumber, owner, name);
|
|
16
16
|
const { body: newBody, mutated, sectionExisted } = appendJournalItem(body, item);
|
|
17
|
+
if (mutated && !opts.dryRun && viewerCanUpdate !== true) {
|
|
18
|
+
return {
|
|
19
|
+
prNumber,
|
|
20
|
+
mutated: false,
|
|
21
|
+
sectionExisted,
|
|
22
|
+
dryRun: false,
|
|
23
|
+
authorizationSkipped: "denied-or-unverifiable",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
17
26
|
if (mutated && !opts.dryRun) {
|
|
18
27
|
await updatePullRequestBody(nodeId, newBody);
|
|
19
28
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ResolveRateLimitStop } from "../comments/rate-limit.mts";
|
|
2
2
|
import type { GlobalOptions } from "../types.mts";
|
|
3
3
|
export interface MarkFilesAsViewedOptions extends GlobalOptions {
|
|
4
4
|
prNumber?: number;
|
|
@@ -21,6 +21,7 @@ export interface MarkFilesAsViewedResult {
|
|
|
21
21
|
errors: string[];
|
|
22
22
|
rateLimit?: ResolveRateLimitStop;
|
|
23
23
|
unmarkedPaths?: string[];
|
|
24
|
+
authorizationSkipped?: "unverifiable";
|
|
24
25
|
}
|
|
25
26
|
/** @deprecated Hidden implementation for `mark-files-as-viewed`; use `apply files`. */
|
|
26
27
|
export declare function runMarkFilesAsViewed(opts: MarkFilesAsViewedOptions): Promise<MarkFilesAsViewedResult>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
|
-
import { graphql,
|
|
2
|
+
import { graphql, getCurrentPrNumber, getRepoInfo } from "../github/client.mjs";
|
|
3
3
|
import { paginateForward } from "../github/pagination.mjs";
|
|
4
|
-
import { isRateLimitMessage, rateLimitFromError, rateLimitFromGraphQlResult, } from "../comments/rate-limit.mjs";
|
|
5
4
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
6
5
|
const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr: Int!, $filesCursor: String) {
|
|
7
6
|
repository(owner: $owner, name: $repo) {
|
|
@@ -22,10 +21,9 @@ const FILES_QUERY = `query PullRequestFiles($owner: String!, $repo: String!, $pr
|
|
|
22
21
|
}
|
|
23
22
|
}`;
|
|
24
23
|
const TEST_FILE_RE = /(^|\/)(tests?|__tests__|spec)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|_tests?\.rs$|(^|\/)tests?\.rs$/i;
|
|
25
|
-
const BULK_CHUNK_SIZE = 10;
|
|
26
24
|
/** @deprecated Hidden implementation for `mark-files-as-viewed`; use `apply files`. */
|
|
27
25
|
export async function runMarkFilesAsViewed(opts) {
|
|
28
|
-
const repo = await getRepoInfo();
|
|
26
|
+
const repo = opts.targetRepository ?? (await getRepoInfo());
|
|
29
27
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
30
28
|
if (!prNumber) {
|
|
31
29
|
throw new ShepherdError("No PR number provided and no current branch PR found", EXIT.UNAVAILABLE);
|
|
@@ -52,8 +50,10 @@ export async function runMarkFilesAsViewed(opts) {
|
|
|
52
50
|
missingPaths: selected.missingPaths,
|
|
53
51
|
unmatchedSelectors: selected.unmatchedSelectors,
|
|
54
52
|
errors: [],
|
|
53
|
+
...(selected.pathsToMark.length > 0 && { authorizationSkipped: "unverifiable" }),
|
|
55
54
|
};
|
|
56
|
-
|
|
55
|
+
// GitHub exposes no exact viewer capability for markFileAsViewed. Repository role and
|
|
56
|
+
// viewerCanEditFiles describe different operations, so this command fails closed.
|
|
57
57
|
return result;
|
|
58
58
|
}
|
|
59
59
|
async function fetchPullRequestFiles(pr, repo) {
|
|
@@ -132,90 +132,3 @@ function selectChangedFiles(changedFiles, opts) {
|
|
|
132
132
|
const pathsToMark = matchedPaths.filter((path) => !alreadyViewedSet.has(path));
|
|
133
133
|
return { matchedPaths, alreadyViewedPaths, missingPaths, unmatchedSelectors, pathsToMark };
|
|
134
134
|
}
|
|
135
|
-
function buildBulkMutation(paths) {
|
|
136
|
-
const ops = paths.map((path, i) => ` m${i}: markFileAsViewed(input: { pullRequestId: $pullRequestId, path: ${JSON.stringify(path)} }) { pullRequest { id } }`);
|
|
137
|
-
return `mutation BulkMarkFilesAsViewed($pullRequestId: ID!) {\n${ops.join("\n")}\n}`;
|
|
138
|
-
}
|
|
139
|
-
async function bulkMarkFilesAsViewed(pullRequestId, paths, result) {
|
|
140
|
-
for (let i = 0; i < paths.length; i += BULK_CHUNK_SIZE) {
|
|
141
|
-
const chunk = paths.slice(i, i + BULK_CHUNK_SIZE);
|
|
142
|
-
// eslint-disable-next-line no-await-in-loop
|
|
143
|
-
const stopped = await bulkMarkFilesAsViewedChunk(pullRequestId, chunk, result, i + BULK_CHUNK_SIZE < paths.length);
|
|
144
|
-
if (stopped) {
|
|
145
|
-
const markedSet = new Set(result.markedPaths);
|
|
146
|
-
result.unmarkedPaths = paths.slice(i).filter((path) => !markedSet.has(path));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
async function bulkMarkFilesAsViewedChunk(pullRequestId, paths, result, hasPendingAfter) {
|
|
152
|
-
if (paths.length === 0)
|
|
153
|
-
return false;
|
|
154
|
-
let data = {};
|
|
155
|
-
let graphQlErrors = [];
|
|
156
|
-
let suppressCurrentChunkErrors = false;
|
|
157
|
-
let rateLimitStop;
|
|
158
|
-
try {
|
|
159
|
-
const resp = await graphqlWithRateLimit(buildBulkMutation(paths), { pullRequestId }, { allowPartialData: true });
|
|
160
|
-
data = resp.data;
|
|
161
|
-
graphQlErrors = (resp.errors ?? []);
|
|
162
|
-
const messages = graphQlErrors.map((e) => e.message);
|
|
163
|
-
suppressCurrentChunkErrors = messages.some(isRateLimitMessage);
|
|
164
|
-
rateLimitStop = rateLimitFromGraphQlResult(messages, {
|
|
165
|
-
rateLimit: resp.rateLimit,
|
|
166
|
-
retryAfterSeconds: resp.retryAfterSeconds,
|
|
167
|
-
stopOnZeroRemaining: hasPendingAfter,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
172
|
-
const stop = rateLimitFromError(err, msg);
|
|
173
|
-
if (stop) {
|
|
174
|
-
result.errors.push(`rate limit: ${stop.message}`);
|
|
175
|
-
result.rateLimit = stop;
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
for (const path of paths)
|
|
179
|
-
result.errors.push(`${path}: ${msg}`);
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
const errorMessagesByAlias = mapAliasErrors(graphQlErrors);
|
|
183
|
-
for (let i = 0; i < paths.length; i += 1) {
|
|
184
|
-
const alias = `m${i}`;
|
|
185
|
-
const m = data[alias];
|
|
186
|
-
if (m?.pullRequest?.id === pullRequestId) {
|
|
187
|
-
result.markedPaths.push(paths[i]);
|
|
188
|
-
}
|
|
189
|
-
else if (!suppressCurrentChunkErrors) {
|
|
190
|
-
result.errors.push(`${paths[i]}: ${errorMessagesByAlias.get(alias) ?? "mark returned null"}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (rateLimitStop) {
|
|
194
|
-
result.errors.push(`rate limit: ${rateLimitStop.message}`);
|
|
195
|
-
result.rateLimit = rateLimitStop;
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
function mapAliasErrors(errors) {
|
|
201
|
-
const out = new Map();
|
|
202
|
-
for (const error of errors) {
|
|
203
|
-
if (!Array.isArray(error.path))
|
|
204
|
-
continue;
|
|
205
|
-
const alias = error.path.find((part) => typeof part === "string" && isMarkAlias(part));
|
|
206
|
-
if (typeof alias === "string")
|
|
207
|
-
out.set(alias, error.message);
|
|
208
|
-
}
|
|
209
|
-
return out;
|
|
210
|
-
}
|
|
211
|
-
function isMarkAlias(value) {
|
|
212
|
-
if (!value.startsWith("m"))
|
|
213
|
-
return false;
|
|
214
|
-
if (value.length === 1)
|
|
215
|
-
return false;
|
|
216
|
-
for (const char of value.slice(1)) {
|
|
217
|
-
if (char < "0" || char > "9")
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
@@ -2,14 +2,14 @@ import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
|
2
2
|
import { applyResolveOptions } from "../comments/resolve.mjs";
|
|
3
3
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
4
4
|
import { loadConfig } from "../config/load.mjs";
|
|
5
|
-
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
5
|
+
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
6
6
|
import { markReplySeen } from "../state/seen-comments.mjs";
|
|
7
7
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
8
|
-
import { addPrShepherdMarker } from "../comments/marker.mjs";
|
|
8
|
+
import { addPrShepherdMarker, threadEndedByShepherd } from "../comments/marker.mjs";
|
|
9
9
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
10
10
|
/** @deprecated Hidden implementation for `resolve`; use `apply review`. */
|
|
11
11
|
export async function runResolveMutate(opts) {
|
|
12
|
-
const repo = await getRepoInfo();
|
|
12
|
+
const repo = opts.targetRepository ?? (await getRepoInfo());
|
|
13
13
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
14
14
|
if (prNumber === null) {
|
|
15
15
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
@@ -27,21 +27,55 @@ export async function runResolveMutate(opts) {
|
|
|
27
27
|
const humanReviewIds = new Set([...data.reviewSummaries, ...data.approvedReviews, ...data.changesRequestedReviews]
|
|
28
28
|
.filter((r) => isHumanAuthor(r) && !isConfiguredBotAuthor(r, botUsernames))
|
|
29
29
|
.map((r) => r.id));
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const replyAuthorizedIds = new Set(data.reviewThreads
|
|
31
|
+
.filter((thread) => thread.viewerCanReply === true)
|
|
32
|
+
.map((thread) => thread.id));
|
|
33
|
+
const resolveAuthorizedIds = new Set(data.reviewThreads
|
|
34
|
+
.filter((thread) => thread.viewerCanResolve === true)
|
|
35
|
+
.map((thread) => thread.id));
|
|
36
|
+
const minimizeAuthorizedIds = new Set([
|
|
37
|
+
...data.comments
|
|
38
|
+
.filter((comment) => comment.viewerCanMinimize === true)
|
|
39
|
+
.map((comment) => comment.id),
|
|
40
|
+
...data.reviewSummaries
|
|
41
|
+
.filter((review) => review.viewerCanMinimize === true)
|
|
42
|
+
.map((review) => review.id),
|
|
43
|
+
...data.approvedReviews
|
|
44
|
+
.filter((review) => review.viewerCanMinimize === true)
|
|
45
|
+
.map((review) => review.id),
|
|
46
|
+
]);
|
|
47
|
+
const requestedReplyIds = new Set((opts.replyThreadIds ?? []).filter((id) => replyAuthorizedIds.has(id)));
|
|
48
|
+
const allowedViewerHumanResolveIds = new Set(data.reviewThreads
|
|
49
|
+
.filter((thread) => isViewerAuthoredHuman(thread, botUsernames) &&
|
|
50
|
+
(requestedReplyIds.has(thread.id) || threadEndedByShepherd(thread)))
|
|
51
|
+
.map((thread) => thread.id));
|
|
52
|
+
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) => (!humanThreadIds.has(id) || allowedViewerHumanResolveIds.has(id)) &&
|
|
53
|
+
resolveAuthorizedIds.has(id));
|
|
54
|
+
const skippedHumanResolves = (opts.resolveThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !allowedViewerHumanResolveIds.has(id));
|
|
55
|
+
const skippedUnauthorizedResolves = (opts.resolveThreadIds ?? []).filter((id) => (!humanThreadIds.has(id) || allowedViewerHumanResolveIds.has(id)) &&
|
|
56
|
+
!resolveAuthorizedIds.has(id));
|
|
57
|
+
const replyThreadIds = opts.replyThreadIds?.filter((id) => humanThreadIds.has(id) && replyAuthorizedIds.has(id));
|
|
33
58
|
const skippedNonHumanReplies = (opts.replyThreadIds ?? []).filter((id) => !humanThreadIds.has(id));
|
|
34
|
-
const
|
|
59
|
+
const skippedUnauthorizedReplies = (opts.replyThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !replyAuthorizedIds.has(id));
|
|
60
|
+
const minimizeCommentIds = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id) && minimizeAuthorizedIds.has(id));
|
|
35
61
|
const skippedHumanMinimizes = (opts.minimizeCommentIds ?? []).filter((id) => humanCommentIds.has(id) || humanReviewIds.has(id));
|
|
36
|
-
const
|
|
62
|
+
const skippedUnauthorizedMinimizes = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id) && !minimizeAuthorizedIds.has(id));
|
|
63
|
+
const dismissReviewIds = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) &&
|
|
64
|
+
data.changesRequestedReviews.some((review) => review.id === id) &&
|
|
65
|
+
data.viewerAuthorization?.viewerCanAdminister === true);
|
|
37
66
|
const skippedHumanDismissals = (opts.dismissReviewIds ?? []).filter((id) => humanReviewIds.has(id));
|
|
67
|
+
const skippedUnauthorizedDismissals = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && !dismissReviewIds.includes(id));
|
|
68
|
+
const hasAuthorizedMutation = resolveThreadIds.length > 0 ||
|
|
69
|
+
(replyThreadIds?.length ?? 0) > 0 ||
|
|
70
|
+
minimizeCommentIds.length > 0 ||
|
|
71
|
+
dismissReviewIds.length > 0;
|
|
38
72
|
const result = await applyResolveOptions(prNumber, repo, {
|
|
39
73
|
resolveThreadIds,
|
|
40
74
|
replyThreadIds,
|
|
41
75
|
minimizeCommentIds,
|
|
42
76
|
dismissReviewIds,
|
|
43
77
|
dismissMessage: opts.dismissMessage,
|
|
44
|
-
requireSha: opts.requireSha,
|
|
78
|
+
requireSha: hasAuthorizedMutation ? opts.requireSha : undefined,
|
|
45
79
|
});
|
|
46
80
|
if (skippedHumanResolves.length > 0)
|
|
47
81
|
result.skippedHumanResolves = skippedHumanResolves;
|
|
@@ -51,6 +85,14 @@ export async function runResolveMutate(opts) {
|
|
|
51
85
|
result.skippedHumanDismissals = skippedHumanDismissals;
|
|
52
86
|
if (skippedNonHumanReplies.length > 0)
|
|
53
87
|
result.skippedNonHumanReplies = skippedNonHumanReplies;
|
|
88
|
+
if (skippedUnauthorizedReplies.length > 0)
|
|
89
|
+
result.skippedUnauthorizedReplies = skippedUnauthorizedReplies;
|
|
90
|
+
if (skippedUnauthorizedResolves.length > 0)
|
|
91
|
+
result.skippedUnauthorizedResolves = skippedUnauthorizedResolves;
|
|
92
|
+
if (skippedUnauthorizedMinimizes.length > 0)
|
|
93
|
+
result.skippedUnauthorizedMinimizes = skippedUnauthorizedMinimizes;
|
|
94
|
+
if (skippedUnauthorizedDismissals.length > 0)
|
|
95
|
+
result.skippedUnauthorizedDismissals = skippedUnauthorizedDismissals;
|
|
54
96
|
if (opts.dismissMessage) {
|
|
55
97
|
const markedMessage = addPrShepherdMarker(opts.dismissMessage);
|
|
56
98
|
await Promise.all(result.repliedThreads.map((id) => {
|
|
@@ -11,4 +11,4 @@ export declare const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review each body un
|
|
|
11
11
|
* invocation, so it lives in the pr-shepherd skill's "Shepherd Journal" playbook instead
|
|
12
12
|
* of being re-emitted every tick (see CLAUDE.md "Keep skills and loop prompts minimal").
|
|
13
13
|
*/
|
|
14
|
-
export declare function buildShepherdJournalInstruction(
|
|
14
|
+
export declare function buildShepherdJournalInstruction(prReference: string | number): string;
|
|
@@ -11,6 +11,6 @@ export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review each body under `##
|
|
|
11
11
|
* invocation, so it lives in the pr-shepherd skill's "Shepherd Journal" playbook instead
|
|
12
12
|
* of being re-emitted every tick (see CLAUDE.md "Keep skills and loop prompts minimal").
|
|
13
13
|
*/
|
|
14
|
-
export function buildShepherdJournalInstruction(
|
|
15
|
-
return `For any substantial decision or rejection, append \`- <decision>\` to Shepherd Journal with \`pr-shepherd apply journal ${
|
|
14
|
+
export function buildShepherdJournalInstruction(prReference) {
|
|
15
|
+
return `For any substantial decision or rejection, append \`- <decision>\` to Shepherd Journal with \`pr-shepherd apply journal ${prReference} '- <decision>'\`. See "Shepherd Journal" in the pr-shepherd skill for citation conventions.`;
|
|
16
16
|
}
|
|
@@ -10,6 +10,6 @@ export declare function buildSuggestionPatchItem({ thread, request, originalCont
|
|
|
10
10
|
request: SuggestionPatchRequest;
|
|
11
11
|
originalContent: string;
|
|
12
12
|
}): SuggestionPatchResult;
|
|
13
|
-
export declare function buildSingularInstructions(patch: SuggestionPatchResult,
|
|
13
|
+
export declare function buildSingularInstructions(patch: SuggestionPatchResult, prReference: string | number): string[];
|
|
14
14
|
export declare function buildCommitCommand(patch: SuggestionPatchResult): string;
|
|
15
15
|
export declare function quotePath(path: string): string;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
2
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
3
3
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
4
|
-
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
5
4
|
import { parseSuggestion, isCommittableSuggestion } from "../suggestions/parse.mjs";
|
|
6
5
|
import { buildUnifiedDiff } from "../suggestions/patch.mjs";
|
|
7
6
|
import { getUnsafeSuggestionRangeReason } from "../suggestions/range.mjs";
|
|
@@ -62,23 +61,16 @@ export function buildSuggestionPatchItem({ thread, request, originalContent, })
|
|
|
62
61
|
filesToStage: [thread.path],
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
|
-
export function buildSingularInstructions(patch,
|
|
64
|
+
export function buildSingularInstructions(patch, prReference) {
|
|
66
65
|
const range = patch.startLine === patch.endLine
|
|
67
66
|
? `line ${patch.startLine}`
|
|
68
67
|
: `lines ${patch.startLine}–${patch.endLine}`;
|
|
69
|
-
const resolveCommand = buildPrShepherdCommand([
|
|
70
|
-
"apply",
|
|
71
|
-
"review",
|
|
72
|
-
String(prNumber),
|
|
73
|
-
"--resolve-thread-ids",
|
|
74
|
-
patch.threadId,
|
|
75
|
-
]).text;
|
|
76
68
|
return [
|
|
77
69
|
`Apply the patch to \`${patch.path}\`: run \`git apply\` with the diff shown above, or edit the file directly using the line range (${range}).`,
|
|
78
70
|
`Stage the file: \`git add -- ${quotePath(patch.path)}\``,
|
|
79
71
|
`Commit: \`${buildCommitCommand(patch)}\``,
|
|
80
|
-
`
|
|
81
|
-
|
|
72
|
+
`Use the originating iterate output for any authorization-checked GitHub review action for thread \`${patch.threadId}\` on ${String(prReference)}.`,
|
|
73
|
+
"Shepherd cannot verify authorization for the Git credential that would push this branch, so this output does not recommend a push.",
|
|
82
74
|
];
|
|
83
75
|
}
|
|
84
76
|
export function buildCommitCommand(patch) {
|
|
@@ -5,7 +5,7 @@ import { buildCommitCommand, buildSuggestionPatchItem, quotePath, validateSugges
|
|
|
5
5
|
import { checkPatchesApply, getLocalHeadSha, getPathsStatus, isAncestor, readPrHeadFile, } from "./suggestion-patch-git.mjs";
|
|
6
6
|
export async function runSuggestionPatches(opts) {
|
|
7
7
|
validateRequests(opts.suggestions);
|
|
8
|
-
const repo = await getRepoInfo();
|
|
8
|
+
const repo = opts.targetRepository ?? (await getRepoInfo());
|
|
9
9
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
10
10
|
if (prNumber === null) {
|
|
11
11
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
@@ -96,8 +96,8 @@ function buildBatchInstructions(patches) {
|
|
|
96
96
|
]);
|
|
97
97
|
return [
|
|
98
98
|
...patchInstructions,
|
|
99
|
-
"
|
|
100
|
-
`
|
|
99
|
+
"Shepherd cannot verify authorization for the Git credential that would push this branch, so this output does not recommend a push.",
|
|
100
|
+
`Continue only with authorization-checked \`apply review\` instructions from the originating iterate output for thread IDs: ${patches.map((patch) => patch.threadId).join(", ")}.`,
|
|
101
101
|
];
|
|
102
102
|
}
|
|
103
103
|
function errorMessage(error) {
|
|
@@ -12,3 +12,9 @@ export declare function isConfiguredBotAuthor(author: {
|
|
|
12
12
|
login?: string;
|
|
13
13
|
authorType?: AuthorType;
|
|
14
14
|
}, botUsernames?: NormalizedBotUsernames): boolean;
|
|
15
|
+
export declare function isViewerAuthoredHuman(author: {
|
|
16
|
+
author?: string;
|
|
17
|
+
login?: string;
|
|
18
|
+
authorType?: AuthorType;
|
|
19
|
+
viewerDidAuthor?: true;
|
|
20
|
+
}, botUsernames?: NormalizedBotUsernames): boolean;
|
package/bin/comments/authors.mjs
CHANGED
|
@@ -30,3 +30,8 @@ export function isConfiguredBotAuthor(author, botUsernames = new Set()) {
|
|
|
30
30
|
return false;
|
|
31
31
|
return botUsernames.has(normalized);
|
|
32
32
|
}
|
|
33
|
+
export function isViewerAuthoredHuman(author, botUsernames = new Set()) {
|
|
34
|
+
return (author.viewerDidAuthor === true &&
|
|
35
|
+
isHumanAuthor(author) &&
|
|
36
|
+
!isConfiguredBotAuthor(author, botUsernames));
|
|
37
|
+
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export declare function hasPrShepherdMarker(body: string): boolean;
|
|
2
1
|
export declare function addPrShepherdMarker(body: string): string;
|
|
2
|
+
export declare function threadEndedByShepherd(thread: {
|
|
3
|
+
body: string;
|
|
4
|
+
comments?: readonly {
|
|
5
|
+
body: string;
|
|
6
|
+
}[];
|
|
7
|
+
}): boolean;
|
package/bin/comments/marker.mjs
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
const PR_SHEPHERD_MARKER = "<!-- pr-shepherd -->";
|
|
2
|
-
|
|
2
|
+
function hasPrShepherdMarker(body) {
|
|
3
3
|
return body.startsWith(PR_SHEPHERD_MARKER);
|
|
4
4
|
}
|
|
5
5
|
export function addPrShepherdMarker(body) {
|
|
6
6
|
return `${PR_SHEPHERD_MARKER}\n${body}`;
|
|
7
7
|
}
|
|
8
|
+
export function threadEndedByShepherd(thread) {
|
|
9
|
+
const comments = thread.comments;
|
|
10
|
+
if (comments && comments.length > 0) {
|
|
11
|
+
return hasPrShepherdMarker(comments[comments.length - 1].body);
|
|
12
|
+
}
|
|
13
|
+
return hasPrShepherdMarker(thread.body);
|
|
14
|
+
}
|
|
@@ -12,6 +12,10 @@ export interface ResolveResult {
|
|
|
12
12
|
skippedHumanMinimizes?: string[];
|
|
13
13
|
skippedHumanDismissals?: string[];
|
|
14
14
|
skippedNonHumanReplies?: string[];
|
|
15
|
+
skippedUnauthorizedReplies?: string[];
|
|
16
|
+
skippedUnauthorizedResolves?: string[];
|
|
17
|
+
skippedUnauthorizedMinimizes?: string[];
|
|
18
|
+
skippedUnauthorizedDismissals?: string[];
|
|
15
19
|
rateLimit?: ResolveRateLimitStop;
|
|
16
20
|
unrepliedThreads?: string[];
|
|
17
21
|
unresolvedThreads?: string[];
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { classifyItem } from "../state/seen-comments.mjs";
|
|
2
2
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
3
|
-
import { isConfiguredBotAuthor } from "./authors.mjs";
|
|
4
|
-
import {
|
|
5
|
-
function threadEndedByShepherd(thread) {
|
|
6
|
-
const comments = thread.comments;
|
|
7
|
-
if (comments && comments.length > 0) {
|
|
8
|
-
return hasPrShepherdMarker(comments[comments.length - 1].body);
|
|
9
|
-
}
|
|
10
|
-
return hasPrShepherdMarker(thread.body);
|
|
11
|
-
}
|
|
3
|
+
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "./authors.mjs";
|
|
4
|
+
import { threadEndedByShepherd } from "./marker.mjs";
|
|
12
5
|
function withEdited(thread, edited) {
|
|
13
6
|
return edited ? { ...thread, edited: true } : thread;
|
|
14
7
|
}
|
|
@@ -39,7 +32,14 @@ export function classifyThreadVisibility(threads, seenMap, botUsernames = new Se
|
|
|
39
32
|
return visible ? [visible] : [];
|
|
40
33
|
});
|
|
41
34
|
const resolutionOnlyThreads = unresolvedThreads
|
|
42
|
-
.filter((t) =>
|
|
35
|
+
.filter((t) => {
|
|
36
|
+
const endedByShepherd = threadEndedByShepherd(t);
|
|
37
|
+
const ordinaryHuman = isHumanAuthor(t) && !isConfiguredBotAuthor(t, botUsernames);
|
|
38
|
+
if (endedByShepherd && ordinaryHuman) {
|
|
39
|
+
return isViewerAuthoredHuman(t, botUsernames);
|
|
40
|
+
}
|
|
41
|
+
return t.isOutdated || t.isMinimized;
|
|
42
|
+
})
|
|
43
43
|
.map((t) => classifyVisibleThread(t, seenMap) ?? t);
|
|
44
44
|
const firstLookThreads = [
|
|
45
45
|
...threads.flatMap((t) => {
|
|
@@ -5,7 +5,8 @@ export function classifyVisibleComments(comments, seenMap, minimizeComments, bot
|
|
|
5
5
|
const minimizeIds = [];
|
|
6
6
|
const toMarkSeen = [];
|
|
7
7
|
for (const c of comments.filter((comment) => !comment.isMinimized)) {
|
|
8
|
-
if (
|
|
8
|
+
if (c.viewerCanMinimize === true &&
|
|
9
|
+
shouldMinimizeAuthor(c.authorType, minimizeComments, c.author, botUsernames)) {
|
|
9
10
|
actionable.push(c);
|
|
10
11
|
minimizeIds.push(c.id);
|
|
11
12
|
toMarkSeen.push(c); // prevents re-surfacing as first-look after GitHub marks it minimized
|
package/bin/config/load.d.mts
CHANGED
|
@@ -50,7 +50,7 @@ interface PrShepherdConfig {
|
|
|
50
50
|
actions: {
|
|
51
51
|
autoMinimizeSuppressed: boolean;
|
|
52
52
|
autoMarkReady: boolean;
|
|
53
|
-
/**
|
|
53
|
+
/** Legacy-named patterns that keep matching Actions checks visible despite ignoreChecks. */
|
|
54
54
|
neverCancelRuns: string[];
|
|
55
55
|
/** @deprecated Accepted for compatibility, ignored by the loader. */
|
|
56
56
|
autoResolveOutdated?: boolean;
|