pr-shepherd 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -18
- package/bin/api.d.mts +3 -2
- package/bin/api.mjs +13 -18
- package/bin/checks/classify.d.mts +3 -1
- package/bin/checks/classify.mjs +5 -2
- package/bin/cli/args.mjs +14 -9
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/fix-formatter.mjs +20 -8
- package/bin/cli/handlers.mjs +1 -0
- package/bin/cli/help-command-pages.d.mts +9 -6
- package/bin/cli/help-command-pages.mjs +7 -4
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +9 -4
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +10 -7
- package/bin/cli/iterate-flags.d.mts +1 -0
- package/bin/cli/iterate-flags.mjs +2 -0
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +9 -6
- package/bin/cli/iterate-instructions.mjs +15 -0
- package/bin/cli/iterate-lean.d.mts +1 -2
- package/bin/cli/iterate-lean.mjs +16 -17
- package/bin/cli/iterate-merge-formatter.d.mts +3 -0
- package/bin/cli/iterate-merge-formatter.mjs +52 -0
- package/bin/cli/iterate-verbose.d.mts +6 -0
- package/bin/cli/iterate-verbose.mjs +14 -0
- package/bin/cli/journal-formatter.mjs +3 -0
- package/bin/cli/journal-handler.mjs +11 -7
- package/bin/cli/list-formatters.d.mts +3 -1
- package/bin/cli/list-formatters.mjs +9 -7
- package/bin/cli/mark-files-as-viewed-formatter.mjs +7 -1
- package/bin/cli/mutate-formatter.mjs +4 -0
- package/bin/cli/poll-handler.mjs +1 -0
- package/bin/cli/runner.mjs +4 -3
- package/bin/commands/check-terminal-report.mjs +1 -0
- package/bin/commands/check.mjs +64 -7
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +2 -2
- package/bin/commands/commit-suggestion.mjs +3 -1
- package/bin/commands/iterate/base.d.mts +5 -0
- package/bin/commands/iterate/base.mjs +25 -0
- package/bin/commands/iterate/check-instructions.d.mts +5 -19
- package/bin/commands/iterate/check-instructions.mjs +24 -25
- package/bin/commands/iterate/classify.d.mts +2 -2
- package/bin/commands/iterate/classify.mjs +43 -25
- package/bin/commands/iterate/escalate.d.mts +3 -1
- package/bin/commands/iterate/escalate.mjs +29 -2
- package/bin/commands/iterate/fix-code.mjs +81 -26
- package/bin/commands/iterate/helpers.d.mts +0 -2
- package/bin/commands/iterate/helpers.mjs +6 -17
- package/bin/commands/iterate/index.mjs +31 -43
- package/bin/commands/iterate/mark-ready.d.mts +2 -0
- package/bin/commands/iterate/mark-ready.mjs +38 -0
- package/bin/commands/iterate/merge-state.d.mts +15 -0
- package/bin/commands/iterate/merge-state.mjs +82 -0
- package/bin/commands/iterate/merge.d.mts +13 -0
- package/bin/commands/iterate/merge.mjs +45 -0
- package/bin/commands/iterate/render.d.mts +2 -2
- package/bin/commands/iterate/render.mjs +19 -20
- package/bin/commands/iterate/stall.mjs +4 -2
- package/bin/commands/iterate/thread-mutation-routing.d.mts +9 -0
- package/bin/commands/iterate/thread-mutation-routing.mjs +29 -0
- package/bin/commands/journal/index.d.mts +5 -0
- package/bin/commands/journal/index.mjs +11 -2
- package/bin/commands/mark-files-as-viewed.d.mts +2 -1
- package/bin/commands/mark-files-as-viewed.mjs +5 -92
- package/bin/commands/poll.mjs +3 -1
- package/bin/commands/resolve-mutate.mjs +51 -9
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/commands/suggestion-patch-item.d.mts +1 -1
- package/bin/commands/suggestion-patch-item.mjs +3 -11
- package/bin/commands/suggestion-patches.mjs +3 -3
- package/bin/comments/authors.d.mts +6 -0
- package/bin/comments/authors.mjs +5 -0
- package/bin/comments/marker.d.mts +6 -1
- package/bin/comments/marker.mjs +8 -1
- package/bin/comments/resolve.d.mts +4 -0
- package/bin/comments/thread-visibility.mjs +10 -10
- package/bin/comments/visible-comments.mjs +2 -1
- package/bin/config/load.d.mts +5 -1
- package/bin/config/load.mjs +34 -0
- package/bin/config/merge-command-args.d.mts +2 -0
- package/bin/config/merge-command-args.mjs +44 -0
- package/bin/config.json +3 -0
- package/bin/exit-codes.d.mts +2 -0
- package/bin/exit-codes.mjs +4 -0
- package/bin/github/batch-parse-checks.d.mts +3 -0
- package/bin/github/batch-parse-checks.mjs +29 -0
- package/bin/github/batch-parsers-rules.d.mts +3 -1
- package/bin/github/batch-parsers-rules.mjs +33 -0
- package/bin/github/batch-parsers.d.mts +2 -2
- package/bin/github/batch-parsers.mjs +40 -27
- package/bin/github/batch-raw-rules.d.mts +48 -0
- package/bin/github/batch-raw-types.d.mts +12 -0
- package/bin/github/batch-raw-types.mjs +0 -1
- package/bin/github/batch.mjs +3 -1
- package/bin/github/client.d.mts +1 -0
- package/bin/github/client.mjs +8 -1
- package/bin/github/gql/batch-pr-page.gql +6 -0
- package/bin/github/gql/batch-pr.gql +105 -0
- package/bin/github/gql/commit-check-contexts.gql +58 -0
- package/bin/github/gql/get-pr-body.gql +1 -0
- package/bin/github/gql/review-thread-comments.gql +1 -0
- package/bin/github/gql/suggestion-threads.gql +1 -0
- package/bin/github/merge-queue-checks.d.mts +4 -0
- package/bin/github/merge-queue-checks.mjs +48 -0
- package/bin/github/queries.d.mts +2 -0
- package/bin/github/queries.mjs +2 -0
- package/bin/github/suggestion-thread.mjs +1 -0
- package/bin/mcp/server.mjs +13 -6
- package/bin/pr-reference.d.mts +13 -0
- package/bin/pr-reference.mjs +23 -1
- package/bin/reporters/agent.mjs +11 -3
- package/bin/threads/transcript.d.mts +2 -0
- package/bin/threads/transcript.mjs +2 -0
- package/bin/types/activity.d.mts +2 -0
- package/bin/types/agent-thread.d.mts +1 -0
- package/bin/types/escalate.d.mts +35 -0
- package/bin/types/escalate.mjs +1 -0
- package/bin/types/github.d.mts +27 -0
- package/bin/types/github.mjs +0 -3
- package/bin/types/iterate.d.mts +23 -39
- package/bin/types/merge-action.d.mts +12 -0
- package/bin/types/merge-action.mjs +1 -0
- package/bin/types/merge-queue.d.mts +13 -0
- package/bin/types/merge-queue.mjs +1 -0
- package/bin/types/merge-requirements.d.mts +17 -0
- package/bin/types/report.d.mts +22 -1
- package/bin/types/review-thread.d.mts +1 -0
- package/bin/types/suggestion-patch.d.mts +1 -1
- package/bin/types.d.mts +3 -0
- package/bin/types.mjs +3 -0
- package/bin/util/markdown.d.mts +2 -0
- package/bin/util/markdown.mjs +7 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +3 -3
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +17 -15
- package/bin/commands/iterate/reruns.d.mts +0 -20
- package/bin/commands/iterate/reruns.mjs +0 -105
|
@@ -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
|
-
}
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -112,7 +112,9 @@ export async function runPoll(opts) {
|
|
|
112
112
|
await sleep(intervalMs);
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
115
|
-
if (untilTerminal
|
|
115
|
+
if ((untilTerminal || iterateOpts.merge) &&
|
|
116
|
+
lastResult.action === "mark_ready" &&
|
|
117
|
+
!pastDebounce) {
|
|
116
118
|
debounceUntil = null;
|
|
117
119
|
await sleep(intervalMs);
|
|
118
120
|
continue;
|
|
@@ -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
|
@@ -43,10 +43,14 @@ interface PrShepherdConfig {
|
|
|
43
43
|
mergeStatus: {
|
|
44
44
|
blockingReviewerLogins: string[];
|
|
45
45
|
};
|
|
46
|
+
merge?: {
|
|
47
|
+
/** Options added to ordinary `gh pr merge` commands. Queue commands never use these. */
|
|
48
|
+
commandArgs: string[];
|
|
49
|
+
};
|
|
46
50
|
actions: {
|
|
47
51
|
autoMinimizeSuppressed: boolean;
|
|
48
52
|
autoMarkReady: boolean;
|
|
49
|
-
/**
|
|
53
|
+
/** Legacy-named patterns that keep matching Actions checks visible despite ignoreChecks. */
|
|
50
54
|
neverCancelRuns: string[];
|
|
51
55
|
/** @deprecated Accepted for compatibility, ignored by the loader. */
|
|
52
56
|
autoResolveOutdated?: boolean;
|
package/bin/config/load.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
import builtins from "../config.json" with { type: "json" };
|
|
7
7
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
8
|
+
import { findMergeStrategies } from "./merge-command-args.mjs";
|
|
8
9
|
const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
|
|
9
10
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
10
11
|
/**
|
|
@@ -85,6 +86,35 @@ function parseNeverCancelRuns(value) {
|
|
|
85
86
|
}
|
|
86
87
|
return value;
|
|
87
88
|
}
|
|
89
|
+
const SHEPHERD_OWNED_MERGE_FLAGS = [
|
|
90
|
+
"--repo",
|
|
91
|
+
"-R",
|
|
92
|
+
"--auto",
|
|
93
|
+
"--disable-auto",
|
|
94
|
+
"--match-head-commit",
|
|
95
|
+
"--admin",
|
|
96
|
+
"--body-file",
|
|
97
|
+
"-F",
|
|
98
|
+
"--help",
|
|
99
|
+
"-h",
|
|
100
|
+
];
|
|
101
|
+
function parseMergeCommandArgs(value) {
|
|
102
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
103
|
+
throw new Error("Invalid config: merge.commandArgs must be an array of strings");
|
|
104
|
+
}
|
|
105
|
+
for (const arg of value) {
|
|
106
|
+
if (SHEPHERD_OWNED_MERGE_FLAGS.some((flag) => arg === flag ||
|
|
107
|
+
arg.startsWith(`${flag}=`) ||
|
|
108
|
+
(flag.startsWith("-") && !flag.startsWith("--") && arg.startsWith(flag)))) {
|
|
109
|
+
throw new Error(`Invalid config: merge.commandArgs cannot include Shepherd-owned ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const strategies = findMergeStrategies(value);
|
|
113
|
+
if (strategies.length > 1) {
|
|
114
|
+
throw new Error(`Invalid config: merge.commandArgs includes multiple merge strategies: ${strategies.join(", ")}`);
|
|
115
|
+
}
|
|
116
|
+
return strategies.length === 0 ? [...value, "--merge"] : [...value];
|
|
117
|
+
}
|
|
88
118
|
const KNOWN_CONFIG_KEYS = new Set([
|
|
89
119
|
"classify",
|
|
90
120
|
"botUsernames",
|
|
@@ -94,6 +124,7 @@ const KNOWN_CONFIG_KEYS = new Set([
|
|
|
94
124
|
"resolve",
|
|
95
125
|
"checks",
|
|
96
126
|
"mergeStatus",
|
|
127
|
+
"merge",
|
|
97
128
|
"actions",
|
|
98
129
|
]);
|
|
99
130
|
const KNOWN_NESTED_KEYS = {
|
|
@@ -108,6 +139,7 @@ const KNOWN_NESTED_KEYS = {
|
|
|
108
139
|
resolve: new Set(["shaPoll"]),
|
|
109
140
|
checks: new Set(["ciTriggerEvents"]),
|
|
110
141
|
mergeStatus: new Set(["blockingReviewerLogins"]),
|
|
142
|
+
merge: new Set(["commandArgs"]),
|
|
111
143
|
actions: new Set([
|
|
112
144
|
"autoMinimizeSuppressed",
|
|
113
145
|
"autoMarkReady",
|
|
@@ -194,6 +226,8 @@ export function loadConfig() {
|
|
|
194
226
|
config.botUsernames = parseBotUsernames(config.botUsernames);
|
|
195
227
|
config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
|
|
196
228
|
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
229
|
+
if (config.merge)
|
|
230
|
+
config.merge.commandArgs = parseMergeCommandArgs(config.merge.commandArgs);
|
|
197
231
|
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
198
232
|
configCache.set(cwd, config);
|
|
199
233
|
return config;
|