pr-shepherd 0.60.0 → 1.0.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 +7 -5
- package/bin/api.d.mts +35 -24
- package/bin/api.mjs +68 -48
- package/bin/checks/classify.d.mts +1 -1
- package/bin/checks/classify.mjs +4 -4
- package/bin/checks/log-excerpt.mjs +1 -1
- package/bin/classify/standard/codecov-report.d.mts +1 -1
- package/bin/classify/standard/codecov-report.mjs +7 -2
- package/bin/classify/standard/coderabbit-notices.d.mts +4 -3
- package/bin/classify/standard/coderabbit-notices.mjs +29 -47
- package/bin/classify/standard/codex-notices.mjs +34 -7
- package/bin/classify/standard/dependabot-notices.mjs +5 -1
- package/bin/classify/standard/gemini-notices.d.mts +1 -1
- package/bin/classify/standard/gemini-notices.mjs +16 -5
- package/bin/classify/standard/kilo-no-issues.d.mts +1 -1
- package/bin/classify/standard/kilo-no-issues.mjs +37 -10
- package/bin/classify/standard/sourcery-notices.mjs +1 -1
- package/bin/cli/arg-flags.mjs +0 -3
- package/bin/cli/clean-handler.d.mts +1 -1
- package/bin/cli/clean-handler.mjs +5 -15
- package/bin/cli/default-poll.d.mts +2 -0
- package/bin/cli/default-poll.mjs +9 -6
- package/bin/cli/formatters.d.mts +2 -3
- package/bin/cli/formatters.mjs +0 -26
- package/bin/cli/handlers.d.mts +3 -3
- package/bin/cli/handlers.mjs +45 -35
- package/bin/cli/help-command-pages.d.mts +13 -158
- package/bin/cli/help-command-pages.mjs +12 -157
- package/bin/cli/help-iterate-poll-pages.d.mts +4 -3
- package/bin/cli/help-iterate-poll-pages.mjs +76 -36
- package/bin/cli/help-log-file-page.d.mts +1 -1
- package/bin/cli/help-log-file-page.mjs +3 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +19 -20
- package/bin/cli/help.d.mts +14 -159
- package/bin/cli/iterate-checks-formatter.mjs +1 -1
- package/bin/cli/iterate-flags.d.mts +2 -4
- package/bin/cli/iterate-flags.mjs +4 -20
- package/bin/cli/iterate-formatter.d.mts +1 -1
- package/bin/cli/iterate-formatter.mjs +6 -6
- package/bin/cli/iterate-instructions.d.mts +1 -1
- package/bin/cli/iterate-instructions.mjs +5 -6
- package/bin/cli/iterate-lean.mjs +32 -40
- package/bin/cli/iterate-verbose.mjs +1 -1
- package/bin/cli/journal-formatter.mjs +0 -3
- package/bin/cli/journal-handler.d.mts +1 -1
- package/bin/cli/journal-handler.mjs +2 -1
- package/bin/cli/mutate-formatter.mjs +0 -9
- package/bin/cli/next-check-format.d.mts +0 -4
- package/bin/cli/next-check-format.mjs +0 -4
- package/bin/cli/poll-handler.d.mts +3 -0
- package/bin/cli/poll-handler.mjs +88 -47
- package/bin/cli/poll-summary-emitter.mjs +2 -2
- package/bin/cli/poll-summary-formatter.mjs +2 -2
- package/bin/cli/poll-targets.d.mts +2 -2
- package/bin/cli/poll-targets.mjs +15 -35
- package/bin/cli/removed-flags.d.mts +3 -0
- package/bin/cli/removed-flags.mjs +24 -0
- package/bin/cli/runner.d.mts +1 -1
- package/bin/cli/runner.mjs +2 -2
- package/bin/cli/stack-overview.d.mts +0 -1
- package/bin/cli/stack-overview.mjs +1 -2
- package/bin/cli/{fix-formatter-extra.d.mts → triage-formatter-extra.d.mts} +1 -2
- package/bin/cli/{fix-formatter-extra.mjs → triage-formatter-extra.mjs} +0 -6
- package/bin/cli/triage-formatter.d.mts +4 -0
- package/bin/cli/{fix-formatter.mjs → triage-formatter.mjs} +31 -42
- package/bin/cli/unknown-flags.d.mts +6 -0
- package/bin/cli/unknown-flags.mjs +29 -0
- package/bin/cli-parser.mjs +41 -62
- package/bin/commands/check-annotations.d.mts +1 -1
- package/bin/commands/check-annotations.mjs +1 -1
- package/bin/commands/check-review-budget.d.mts +1 -1
- package/bin/commands/check-review-budget.mjs +1 -1
- package/bin/commands/check-wait-detectors.d.mts +1 -1
- package/bin/commands/check-wait-detectors.mjs +1 -1
- package/bin/commands/check.d.mts +1 -1
- package/bin/commands/check.mjs +7 -7
- package/bin/commands/commit-suggestion-instruction.d.mts +2 -2
- package/bin/commands/commit-suggestion-instruction.mjs +3 -3
- package/bin/commands/iterate/api-usage.mjs +8 -8
- package/bin/commands/iterate/check-instructions.d.mts +4 -4
- package/bin/commands/iterate/check-instructions.mjs +8 -8
- package/bin/commands/iterate/classify.mjs +2 -2
- package/bin/commands/iterate/escalate.mjs +6 -6
- package/bin/commands/iterate/index.mjs +20 -23
- package/bin/commands/iterate/merge-plan.mjs +3 -3
- package/bin/commands/iterate/merge-state.mjs +4 -7
- package/bin/commands/iterate/native-stack-rebase.mjs +1 -1
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +2 -2
- package/bin/commands/iterate/stall.mjs +11 -9
- package/bin/commands/iterate/{fix-loop-instruction.mjs → triage-loop-instruction.mjs} +7 -3
- package/bin/commands/iterate/{fix-code.d.mts → triage.d.mts} +2 -2
- package/bin/commands/iterate/{fix-code.mjs → triage.mjs} +14 -23
- package/bin/commands/iterate/unreported-required.mjs +2 -5
- package/bin/commands/journal/index.d.mts +1 -3
- package/bin/commands/journal/index.mjs +1 -1
- package/bin/commands/mark-files-as-viewed.d.mts +1 -3
- package/bin/commands/mark-files-as-viewed.mjs +1 -1
- package/bin/commands/next-check.d.mts +2 -2
- package/bin/commands/next-check.mjs +4 -2
- package/bin/commands/poll-progress.mjs +1 -1
- package/bin/commands/poll-summary-explicit-instructions.mjs +1 -1
- package/bin/commands/poll-summary-instructions.mjs +7 -7
- package/bin/commands/poll-summary.mjs +4 -4
- package/bin/commands/poll.d.mts +1 -1
- package/bin/commands/poll.mjs +4 -4
- package/bin/commands/resolve-mutate.d.mts +1 -1
- package/bin/commands/resolve-mutate.mjs +1 -1
- package/bin/commands/rest-stack-merge-readiness.mjs +1 -1
- package/bin/commands/single-tick.d.mts +5 -0
- package/bin/commands/single-tick.mjs +7 -0
- package/bin/commands/stack-drain.mjs +1 -1
- package/bin/commands/stack-merge-flag.mjs +1 -1
- package/bin/commands/stack-stall.d.mts +2 -2
- package/bin/commands/stack-stall.mjs +2 -2
- package/bin/commands/stack-work.d.mts +2 -2
- package/bin/commands/stack-work.mjs +2 -2
- package/bin/commands/suggestion-patch-item.d.mts +0 -1
- package/bin/commands/suggestion-patch-item.mjs +0 -12
- package/bin/commands/suggestion-patches.d.mts +2 -2
- package/bin/commands/suggestion-patches.mjs +1 -1
- package/bin/commands/tick-instructions.d.mts +9 -0
- package/bin/commands/tick-instructions.mjs +23 -0
- package/bin/commands/tick.d.mts +15 -0
- package/bin/commands/{event-mode.mjs → tick.mjs} +39 -55
- package/bin/comments/minimize-policy.mjs +1 -3
- package/bin/comments/resolve.d.mts +0 -23
- package/bin/comments/resolve.mjs +0 -4
- package/bin/comments/review-output-budget.d.mts +1 -1
- package/bin/comments/review-output-budget.mjs +1 -1
- package/bin/comments/sha-poll.mjs +3 -1
- package/bin/config/default-bot-usernames.d.mts +2 -0
- package/bin/config/default-bot-usernames.mjs +15 -0
- package/bin/config/load.d.mts +4 -138
- package/bin/config/load.mjs +88 -443
- package/bin/config/merge-command-args.mjs +2 -2
- package/bin/config/merge-method.d.mts +1 -2
- package/bin/config/merge-method.mjs +1 -8
- package/bin/config/renames.d.mts +8 -0
- package/bin/config/renames.mjs +87 -0
- package/bin/config/resolve.d.mts +31 -0
- package/bin/config/resolve.mjs +89 -0
- package/bin/config/schema.d.mts +161 -0
- package/bin/config/schema.mjs +168 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +3 -3
- package/bin/github/poll-summary-check-blockers.mjs +1 -1
- package/bin/github/poll-summary-command.mjs +8 -6
- package/bin/github/poll-summary-projector.mjs +2 -2
- package/bin/github/poll-summary-review.mjs +1 -1
- package/bin/github/poll-summary-route.mjs +9 -9
- package/bin/github/poll-summary-unreported.mjs +1 -1
- package/bin/github/poll-summary.mjs +1 -1
- package/bin/github/queries.d.mts +1 -1
- package/bin/github/queries.mjs +1 -1
- package/bin/mcp/server.mjs +48 -54
- package/bin/merge-status/derive.mjs +1 -1
- package/bin/state/durable-state.d.mts +1 -3
- package/bin/state/durable-state.mjs +2 -8
- package/bin/state/fix-attempts.d.mts +2 -2
- package/bin/state/fix-attempts.mjs +1 -1
- package/bin/state/merge-request.mjs +1 -1
- package/bin/state/pr-fingerprint.mjs +4 -11
- package/bin/state/seen-comments.d.mts +4 -5
- package/bin/state/seen-comments.mjs +8 -14
- package/bin/state/stack-stall.d.mts +1 -1
- package/bin/state/stack-stall.mjs +1 -1
- package/bin/state/stall-state-store.d.mts +1 -1
- package/bin/state/stall-state-store.mjs +1 -1
- package/bin/types/escalate.d.mts +2 -2
- package/bin/types/iterate.d.mts +12 -26
- package/bin/types/merge-queue.d.mts +2 -2
- package/bin/types/next-check.d.mts +1 -1
- package/bin/types/poll-summary.d.mts +3 -6
- package/bin/types/report.d.mts +2 -2
- package/bin/types/suggestion-patch.d.mts +1 -8
- package/package.json +2 -1
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -6
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/{cloud-event-loop.md → event-loop.md} +5 -5
- package/plugins/pr-shepherd/skills/pr-shepherd/references/mcp-fallback.md +3 -3
- package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/suggestion-patches.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/{fix-code-loop.md → triage-loop.md} +3 -3
- package/plugins/pr-shepherd/skills/reduce-pr-noise/SKILL.md +1 -1
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/settings.md +11 -11
- package/pr-shepherd.schema.json +361 -0
- package/bin/cli/event-state-scope.d.mts +0 -19
- package/bin/cli/event-state-scope.mjs +0 -60
- package/bin/cli/fix-formatter.d.mts +0 -4
- package/bin/commands/commit-suggestion.d.mts +0 -8
- package/bin/commands/commit-suggestion.mjs +0 -26
- package/bin/commands/event-instructions.d.mts +0 -11
- package/bin/commands/event-instructions.mjs +0 -27
- package/bin/commands/event-mode.d.mts +0 -14
- package/bin/commands/poll-mode.d.mts +0 -8
- package/bin/commands/poll-mode.mjs +0 -14
- package/bin/config.json +0 -68
- package/bin/types/protected-run.d.mts +0 -6
- package/bin/types/protected-run.mjs +0 -1
- /package/bin/commands/iterate/{fix-loop-instruction.d.mts → triage-loop-instruction.d.mts} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-v1 camelCase config keys and their v1 replacements. Used only to word the error for an
|
|
3
|
+
* unknown key: an old key never takes effect. `[]` stands for any array index.
|
|
4
|
+
*/
|
|
5
|
+
const RENAMED_KEYS = {
|
|
6
|
+
cliCommand: "cli-command",
|
|
7
|
+
botUsernames: "bot-usernames",
|
|
8
|
+
ignoreChecks: "checks.ignore",
|
|
9
|
+
"actions.neverCancelRuns": "checks.never-ignore",
|
|
10
|
+
"checks.ciTriggerEvents": "checks.ci-trigger-events",
|
|
11
|
+
"checks.ignoreLogLines": "checks.ignore-log-lines",
|
|
12
|
+
"iterate.fixAttemptsPerThread": "iterate.triage-attempts-per-thread",
|
|
13
|
+
"iterate.stallTimeoutMinutes": "iterate.stall-timeout-minutes",
|
|
14
|
+
"watch.readyDelayMinutes": "iterate.ready-delay-minutes",
|
|
15
|
+
"iterate.minimizeApprovals": "iterate.minimize-approvals",
|
|
16
|
+
"iterate.minimizeComments": "iterate.minimize-comments",
|
|
17
|
+
"iterate.behindBaseHint": "iterate.behind-base-hint",
|
|
18
|
+
"iterate.reviewOutputMaxChars": "iterate.review-output-max-chars",
|
|
19
|
+
"iterate.resolveOtherHumanThreads": "iterate.resolve-other-human-threads",
|
|
20
|
+
"poll.intervalSeconds": "poll.interval-seconds",
|
|
21
|
+
"poll.stackIntervalFactor": "poll.stack-interval-factor",
|
|
22
|
+
"poll.timeoutSeconds": "poll.timeout-seconds",
|
|
23
|
+
"poll.debounceSeconds": "poll.debounce-seconds",
|
|
24
|
+
"poll.quietStatus": "poll.quiet-status",
|
|
25
|
+
"poll.reconcileSeconds": "poll.reconcile-seconds",
|
|
26
|
+
"watch.graphqlQuotaWarnings": "poll.quota-warnings (item keys: remaining-percent, interval-minutes, interval-factor)",
|
|
27
|
+
"poll.quota-warnings[].remainingPercent": "poll.quota-warnings[].remaining-percent",
|
|
28
|
+
"poll.quota-warnings[].pollIntervalMinutes": "poll.quota-warnings[].interval-minutes",
|
|
29
|
+
"poll.quota-warnings[].pollIntervalFactor": "poll.quota-warnings[].interval-factor",
|
|
30
|
+
"resolve.shaPoll.intervalMs": "apply.sha-poll-interval-seconds (seconds, not milliseconds)",
|
|
31
|
+
"resolve.shaPoll.maxAttempts": "apply.sha-poll-max-attempts",
|
|
32
|
+
"mergeStatus.blockingReviewerLogins": "mark-ready.blocking-reviewer-logins",
|
|
33
|
+
"actions.autoMarkReady": "mark-ready.auto",
|
|
34
|
+
"merge.commandArgs": "merge.command-args",
|
|
35
|
+
"actions.autoMinimizeSuppressed": "actions.auto-minimize-suppressed",
|
|
36
|
+
"actions.workWhileQueued": "actions.work-while-queued",
|
|
37
|
+
};
|
|
38
|
+
/** Pre-v1 keys with no replacement key, and what to do instead. */
|
|
39
|
+
const REMOVED_KEYS = {
|
|
40
|
+
"poll.mode": "use `pr-shepherd iterate` for single ticks",
|
|
41
|
+
};
|
|
42
|
+
export function formatPath(path) {
|
|
43
|
+
return path.reduce((text, segment) => {
|
|
44
|
+
if (typeof segment === "number")
|
|
45
|
+
return `${text}[${segment}]`;
|
|
46
|
+
return text === "" ? segment : `${text}.${segment}`;
|
|
47
|
+
}, "");
|
|
48
|
+
}
|
|
49
|
+
function renameLookupKey(path) {
|
|
50
|
+
return path.reduce((text, segment) => {
|
|
51
|
+
if (typeof segment === "number")
|
|
52
|
+
return `${text}[]`;
|
|
53
|
+
return text === "" ? segment : `${text}.${segment}`;
|
|
54
|
+
}, "");
|
|
55
|
+
}
|
|
56
|
+
function isPlainObject(value) {
|
|
57
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Describe one unrecognized key at `path`. Old keys get a rename hint; an unknown section is
|
|
61
|
+
* searched for renamed keys inside it before it is reported as unknown as a whole.
|
|
62
|
+
*/
|
|
63
|
+
export function describeUnknownKey(path, value) {
|
|
64
|
+
const hints = describeNestedKey(path, value);
|
|
65
|
+
return hints.length > 0 ? hints : [`unknown key \`${formatPath(path)}\``];
|
|
66
|
+
}
|
|
67
|
+
function describeNestedKey(path, value) {
|
|
68
|
+
const lookupKey = renameLookupKey(path);
|
|
69
|
+
const removed = REMOVED_KEYS[lookupKey];
|
|
70
|
+
if (removed !== undefined)
|
|
71
|
+
return [`\`${formatPath(path)}\` was removed: ${removed}`];
|
|
72
|
+
const renamed = RENAMED_KEYS[lookupKey];
|
|
73
|
+
if (renamed !== undefined) {
|
|
74
|
+
const [to, note] = renamed.split(" (");
|
|
75
|
+
return [`\`${formatPath(path)}\` → \`${to}\`${note === undefined ? "" : ` (${note}`}`];
|
|
76
|
+
}
|
|
77
|
+
if (!isPlainObject(value))
|
|
78
|
+
return [];
|
|
79
|
+
const children = Object.entries(value).map(([key, child]) => {
|
|
80
|
+
const childPath = [...path, key];
|
|
81
|
+
return { childPath, hints: describeNestedKey(childPath, child) };
|
|
82
|
+
});
|
|
83
|
+
// A section with no old keys is reported whole; otherwise every other child is reported too.
|
|
84
|
+
if (children.every(({ hints }) => hints.length === 0))
|
|
85
|
+
return [];
|
|
86
|
+
return children.flatMap(({ childPath, hints }) => hints.length > 0 ? hints : [`unknown key \`${formatPath(childPath)}\``]);
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { ShepherdError } from "../exit-codes.mts";
|
|
3
|
+
import { type ConfigFile } from "./schema.mts";
|
|
4
|
+
export type MinimizeCommentsPolicy = ConfigFile["iterate"]["minimize-comments"];
|
|
5
|
+
export type ResolveOtherHumanThreads = ConfigFile["iterate"]["resolve-other-human-threads"];
|
|
6
|
+
export declare const INSTRUCTION_STYLES: readonly ["playbook", "inline"];
|
|
7
|
+
/**
|
|
8
|
+
* How `triage` `## Instructions` render invariant procedure text. `inline` (default)
|
|
9
|
+
* prints every step each tick; `playbook` folds them into one `Playbook: "Triage loop".` pointer.
|
|
10
|
+
*/
|
|
11
|
+
export type InstructionStyle = (typeof INSTRUCTION_STYLES)[number];
|
|
12
|
+
export declare function isInstructionStyle(value: unknown): value is InstructionStyle;
|
|
13
|
+
/** One `poll.quota-warnings` band resolved against `poll.interval-seconds`. */
|
|
14
|
+
export interface GraphqlQuotaWarningBand {
|
|
15
|
+
remainingPercent: number;
|
|
16
|
+
pollIntervalMinutes: number;
|
|
17
|
+
}
|
|
18
|
+
/** The validated v1 config: file keys as written, plus values derived from them. */
|
|
19
|
+
export type PrShepherdConfig = ConfigFile & {
|
|
20
|
+
/** Bundled classification rules enabled by `classify.standard`, resolved and sorted by name. */
|
|
21
|
+
standardRules: string[];
|
|
22
|
+
/** `poll.quota-warnings` resolved to absolute intervals, highest `remaining-percent` first. */
|
|
23
|
+
quotaWarningBands: GraphqlQuotaWarningBand[];
|
|
24
|
+
};
|
|
25
|
+
export declare function issueLines(issue: z.core.$ZodIssue, raw: unknown): string[];
|
|
26
|
+
export declare function configError(source: string, lines: string[]): ShepherdError;
|
|
27
|
+
/**
|
|
28
|
+
* Validate a merged raw config, apply defaults, and derive the resolved values. `source` names
|
|
29
|
+
* the files the raw object came from; it appears in errors.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveConfig(raw: Record<string, unknown>, source?: string): PrShepherdConfig;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { STANDARD_RULE_NAMES } from "../classify/standard/index.mjs";
|
|
2
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
3
|
+
import { findMergeStrategies } from "./merge-command-args.mjs";
|
|
4
|
+
import { describeUnknownKey, formatPath } from "./renames.mjs";
|
|
5
|
+
import { configSchema } from "./schema.mjs";
|
|
6
|
+
export const INSTRUCTION_STYLES = ["playbook", "inline"];
|
|
7
|
+
export function isInstructionStyle(value) {
|
|
8
|
+
return INSTRUCTION_STYLES.some((style) => style === value);
|
|
9
|
+
}
|
|
10
|
+
function valueAt(root, path) {
|
|
11
|
+
let current = root;
|
|
12
|
+
for (const segment of path) {
|
|
13
|
+
if (current === null || typeof current !== "object")
|
|
14
|
+
return undefined;
|
|
15
|
+
current = current[segment];
|
|
16
|
+
}
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
export function issueLines(issue, raw) {
|
|
20
|
+
const path = issue.path.filter((segment) => typeof segment !== "symbol");
|
|
21
|
+
if (issue.code === "unrecognized_keys") {
|
|
22
|
+
return issue.keys.flatMap((key) => describeUnknownKey([...path, key], valueAt(raw, [...path, key])));
|
|
23
|
+
}
|
|
24
|
+
return [`\`${formatPath(path)}\`: ${issue.message}`];
|
|
25
|
+
}
|
|
26
|
+
export function configError(source, lines) {
|
|
27
|
+
return new ShepherdError(`invalid config in ${source}:\n${lines.map((line) => ` - ${line}`).join("\n")}`, EXIT.CONFIG);
|
|
28
|
+
}
|
|
29
|
+
/** Longest `setTimeout` delay, in seconds. A bigger aggregate sleep would clamp to ~24.8 days. */
|
|
30
|
+
const MAX_POLL_INTERVAL_SECONDS = (2 ** 31 - 1) / 1000;
|
|
31
|
+
function resolveQuotaWarnings(config) {
|
|
32
|
+
const intervalSeconds = config.poll["interval-seconds"];
|
|
33
|
+
const errors = [];
|
|
34
|
+
const bands = config.poll["quota-warnings"].map((band, index) => {
|
|
35
|
+
const factor = band["interval-factor"];
|
|
36
|
+
const factorMinutes = factor === undefined ? 0 : (intervalSeconds * factor) / 60;
|
|
37
|
+
const pollIntervalMinutes = Math.max(band["interval-minutes"] ?? 0, factorMinutes);
|
|
38
|
+
if (!Number.isFinite(pollIntervalMinutes)) {
|
|
39
|
+
errors.push(`\`poll.quota-warnings[${index}]\`: resolves to a non-finite poll interval`);
|
|
40
|
+
}
|
|
41
|
+
return { remainingPercent: band["remaining-percent"], pollIntervalMinutes };
|
|
42
|
+
});
|
|
43
|
+
bands.sort((left, right) => right.remainingPercent - left.remainingPercent);
|
|
44
|
+
const decreasing = bands.some((band, index) => index > 0 && band.pollIntervalMinutes < (bands[index - 1]?.pollIntervalMinutes ?? 0));
|
|
45
|
+
if (decreasing) {
|
|
46
|
+
errors.push("`poll.quota-warnings`: the resolved interval must not decrease as remaining-percent decreases");
|
|
47
|
+
}
|
|
48
|
+
return { bands, errors };
|
|
49
|
+
}
|
|
50
|
+
/** Checks that span keys which may come from different files, so run after merging. */
|
|
51
|
+
function crossKeyErrors(config) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
const aggregate = config.poll["interval-seconds"] * config.poll["stack-interval-factor"];
|
|
54
|
+
if (!(aggregate <= MAX_POLL_INTERVAL_SECONDS)) {
|
|
55
|
+
errors.push(`\`poll.interval-seconds\` × \`poll.stack-interval-factor\` must be at most ${MAX_POLL_INTERVAL_SECONDS} seconds, got ${aggregate}`);
|
|
56
|
+
}
|
|
57
|
+
const method = config.merge.method;
|
|
58
|
+
const fromArgs = findMergeStrategies(config.merge["command-args"])[0];
|
|
59
|
+
if (method !== undefined && fromArgs !== undefined && fromArgs !== method) {
|
|
60
|
+
errors.push(`\`merge.method\` ${method} conflicts with the \`merge.command-args\` strategy ${fromArgs}`);
|
|
61
|
+
}
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
64
|
+
function resolveStandardRules(config) {
|
|
65
|
+
const flags = config.classify.standard;
|
|
66
|
+
const all = flags["all"] ?? false;
|
|
67
|
+
return STANDARD_RULE_NAMES.filter((name) => flags[name] ?? all).sort();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate a merged raw config, apply defaults, and derive the resolved values. `source` names
|
|
71
|
+
* the files the raw object came from; it appears in errors.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveConfig(raw, source = "built-in defaults") {
|
|
74
|
+
const result = configSchema.safeParse(raw);
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
throw configError(source, result.error.issues.flatMap((issue) => issueLines(issue, raw)));
|
|
77
|
+
}
|
|
78
|
+
const config = result.data;
|
|
79
|
+
const quota = resolveQuotaWarnings(config);
|
|
80
|
+
const errors = [...crossKeyErrors(config), ...quota.errors];
|
|
81
|
+
if (errors.length > 0)
|
|
82
|
+
throw configError(source, errors);
|
|
83
|
+
const resolved = {
|
|
84
|
+
...config,
|
|
85
|
+
standardRules: resolveStandardRules(config),
|
|
86
|
+
quotaWarningBands: quota.bands,
|
|
87
|
+
};
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The config format version this release reads. It tracks the package's semver major. */
|
|
3
|
+
export declare const CONFIG_VERSION = 1;
|
|
4
|
+
export declare const configSchema: z.ZodObject<{
|
|
5
|
+
version: z.ZodOptional<z.ZodLiteral<1>>;
|
|
6
|
+
github: z.ZodPrefault<z.ZodObject<{
|
|
7
|
+
transport: z.ZodDefault<z.ZodEnum<{
|
|
8
|
+
auto: "auto";
|
|
9
|
+
graphql: "graphql";
|
|
10
|
+
rest: "rest";
|
|
11
|
+
}>>;
|
|
12
|
+
}, z.core.$strict>>;
|
|
13
|
+
"cli-command": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
14
|
+
"bot-usernames": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
15
|
+
checks: z.ZodPrefault<z.ZodObject<{
|
|
16
|
+
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
17
|
+
"never-ignore": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
18
|
+
"ci-trigger-events": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
19
|
+
"ignore-log-lines": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
|
+
}, z.core.$strict>>;
|
|
21
|
+
iterate: z.ZodPrefault<z.ZodObject<{
|
|
22
|
+
"triage-attempts-per-thread": z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
"stall-timeout-minutes": z.ZodDefault<z.ZodNumber>;
|
|
24
|
+
"ready-delay-minutes": z.ZodDefault<z.ZodNumber>;
|
|
25
|
+
"minimize-approvals": z.ZodDefault<z.ZodBoolean>;
|
|
26
|
+
"minimize-comments": z.ZodDefault<z.ZodEnum<{
|
|
27
|
+
all: "all";
|
|
28
|
+
bots: "bots";
|
|
29
|
+
none: "none";
|
|
30
|
+
}>>;
|
|
31
|
+
"behind-base-hint": z.ZodDefault<z.ZodString>;
|
|
32
|
+
"review-output-max-chars": z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
"resolve-other-human-threads": z.ZodDefault<z.ZodEnum<{
|
|
34
|
+
always: "always";
|
|
35
|
+
none: "none";
|
|
36
|
+
outdated: "outdated";
|
|
37
|
+
}>>;
|
|
38
|
+
instructions: z.ZodDefault<z.ZodEnum<{
|
|
39
|
+
inline: "inline";
|
|
40
|
+
playbook: "playbook";
|
|
41
|
+
}>>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
poll: z.ZodPrefault<z.ZodObject<{
|
|
44
|
+
"interval-seconds": z.ZodDefault<z.ZodNumber>;
|
|
45
|
+
"stack-interval-factor": z.ZodDefault<z.ZodNumber>;
|
|
46
|
+
"timeout-seconds": z.ZodDefault<z.ZodNumber>;
|
|
47
|
+
"debounce-seconds": z.ZodDefault<z.ZodNumber>;
|
|
48
|
+
"quiet-status": z.ZodDefault<z.ZodBoolean>;
|
|
49
|
+
"reconcile-seconds": z.ZodDefault<z.ZodNumber>;
|
|
50
|
+
"quota-warnings": z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
51
|
+
"remaining-percent": z.ZodNumber;
|
|
52
|
+
"interval-minutes": z.ZodOptional<z.ZodNumber>;
|
|
53
|
+
"interval-factor": z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
}, z.core.$strict>>>;
|
|
55
|
+
}, z.core.$strict>>;
|
|
56
|
+
apply: z.ZodPrefault<z.ZodObject<{
|
|
57
|
+
"sha-poll-interval-seconds": z.ZodDefault<z.ZodNumber>;
|
|
58
|
+
"sha-poll-max-attempts": z.ZodDefault<z.ZodNumber>;
|
|
59
|
+
}, z.core.$strict>>;
|
|
60
|
+
"mark-ready": z.ZodPrefault<z.ZodObject<{
|
|
61
|
+
auto: z.ZodDefault<z.ZodBoolean>;
|
|
62
|
+
"blocking-reviewer-logins": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
63
|
+
}, z.core.$strict>>;
|
|
64
|
+
merge: z.ZodPrefault<z.ZodObject<{
|
|
65
|
+
method: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
merge: "merge";
|
|
67
|
+
rebase: "rebase";
|
|
68
|
+
squash: "squash";
|
|
69
|
+
}>>;
|
|
70
|
+
"command-args": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
71
|
+
}, z.core.$strict>>;
|
|
72
|
+
actions: z.ZodPrefault<z.ZodObject<{
|
|
73
|
+
"auto-minimize-suppressed": z.ZodDefault<z.ZodBoolean>;
|
|
74
|
+
"work-while-queued": z.ZodDefault<z.ZodBoolean>;
|
|
75
|
+
}, z.core.$strict>>;
|
|
76
|
+
classify: z.ZodPrefault<z.ZodObject<{
|
|
77
|
+
standard: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
78
|
+
[x: string]: string;
|
|
79
|
+
}> & z.core.$partial, z.ZodBoolean>>;
|
|
80
|
+
}, z.core.$strict>>;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
/** One non-empty config file on disk, which must declare `version`. */
|
|
83
|
+
export declare const configFileSchema: z.ZodObject<{
|
|
84
|
+
github: z.ZodPrefault<z.ZodObject<{
|
|
85
|
+
transport: z.ZodDefault<z.ZodEnum<{
|
|
86
|
+
auto: "auto";
|
|
87
|
+
graphql: "graphql";
|
|
88
|
+
rest: "rest";
|
|
89
|
+
}>>;
|
|
90
|
+
}, z.core.$strict>>;
|
|
91
|
+
"cli-command": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
92
|
+
"bot-usernames": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
93
|
+
checks: z.ZodPrefault<z.ZodObject<{
|
|
94
|
+
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
95
|
+
"never-ignore": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
96
|
+
"ci-trigger-events": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
97
|
+
"ignore-log-lines": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
98
|
+
}, z.core.$strict>>;
|
|
99
|
+
iterate: z.ZodPrefault<z.ZodObject<{
|
|
100
|
+
"triage-attempts-per-thread": z.ZodDefault<z.ZodNumber>;
|
|
101
|
+
"stall-timeout-minutes": z.ZodDefault<z.ZodNumber>;
|
|
102
|
+
"ready-delay-minutes": z.ZodDefault<z.ZodNumber>;
|
|
103
|
+
"minimize-approvals": z.ZodDefault<z.ZodBoolean>;
|
|
104
|
+
"minimize-comments": z.ZodDefault<z.ZodEnum<{
|
|
105
|
+
all: "all";
|
|
106
|
+
bots: "bots";
|
|
107
|
+
none: "none";
|
|
108
|
+
}>>;
|
|
109
|
+
"behind-base-hint": z.ZodDefault<z.ZodString>;
|
|
110
|
+
"review-output-max-chars": z.ZodDefault<z.ZodNumber>;
|
|
111
|
+
"resolve-other-human-threads": z.ZodDefault<z.ZodEnum<{
|
|
112
|
+
always: "always";
|
|
113
|
+
none: "none";
|
|
114
|
+
outdated: "outdated";
|
|
115
|
+
}>>;
|
|
116
|
+
instructions: z.ZodDefault<z.ZodEnum<{
|
|
117
|
+
inline: "inline";
|
|
118
|
+
playbook: "playbook";
|
|
119
|
+
}>>;
|
|
120
|
+
}, z.core.$strict>>;
|
|
121
|
+
poll: z.ZodPrefault<z.ZodObject<{
|
|
122
|
+
"interval-seconds": z.ZodDefault<z.ZodNumber>;
|
|
123
|
+
"stack-interval-factor": z.ZodDefault<z.ZodNumber>;
|
|
124
|
+
"timeout-seconds": z.ZodDefault<z.ZodNumber>;
|
|
125
|
+
"debounce-seconds": z.ZodDefault<z.ZodNumber>;
|
|
126
|
+
"quiet-status": z.ZodDefault<z.ZodBoolean>;
|
|
127
|
+
"reconcile-seconds": z.ZodDefault<z.ZodNumber>;
|
|
128
|
+
"quota-warnings": z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
129
|
+
"remaining-percent": z.ZodNumber;
|
|
130
|
+
"interval-minutes": z.ZodOptional<z.ZodNumber>;
|
|
131
|
+
"interval-factor": z.ZodOptional<z.ZodNumber>;
|
|
132
|
+
}, z.core.$strict>>>;
|
|
133
|
+
}, z.core.$strict>>;
|
|
134
|
+
apply: z.ZodPrefault<z.ZodObject<{
|
|
135
|
+
"sha-poll-interval-seconds": z.ZodDefault<z.ZodNumber>;
|
|
136
|
+
"sha-poll-max-attempts": z.ZodDefault<z.ZodNumber>;
|
|
137
|
+
}, z.core.$strict>>;
|
|
138
|
+
"mark-ready": z.ZodPrefault<z.ZodObject<{
|
|
139
|
+
auto: z.ZodDefault<z.ZodBoolean>;
|
|
140
|
+
"blocking-reviewer-logins": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
141
|
+
}, z.core.$strict>>;
|
|
142
|
+
merge: z.ZodPrefault<z.ZodObject<{
|
|
143
|
+
method: z.ZodOptional<z.ZodEnum<{
|
|
144
|
+
merge: "merge";
|
|
145
|
+
rebase: "rebase";
|
|
146
|
+
squash: "squash";
|
|
147
|
+
}>>;
|
|
148
|
+
"command-args": z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
149
|
+
}, z.core.$strict>>;
|
|
150
|
+
actions: z.ZodPrefault<z.ZodObject<{
|
|
151
|
+
"auto-minimize-suppressed": z.ZodDefault<z.ZodBoolean>;
|
|
152
|
+
"work-while-queued": z.ZodDefault<z.ZodBoolean>;
|
|
153
|
+
}, z.core.$strict>>;
|
|
154
|
+
classify: z.ZodPrefault<z.ZodObject<{
|
|
155
|
+
standard: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
156
|
+
[x: string]: string;
|
|
157
|
+
}> & z.core.$partial, z.ZodBoolean>>;
|
|
158
|
+
}, z.core.$strict>>;
|
|
159
|
+
version: z.ZodLiteral<1>;
|
|
160
|
+
}, z.core.$strict>;
|
|
161
|
+
export type ConfigFile = z.output<typeof configSchema>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { STANDARD_RULE_NAMES } from "../classify/standard/index.mjs";
|
|
3
|
+
import { DEFAULT_BOT_USERNAMES } from "./default-bot-usernames.mjs";
|
|
4
|
+
import { findMergeStrategies } from "./merge-command-args.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* The strict v1 `.pr-shepherdrc.yml` schema. Keys are kebab-case, durations carry their unit in
|
|
7
|
+
* the key name, and unknown keys or invalid values are hard errors. The defaults declared here
|
|
8
|
+
* are the built-in defaults; `pr-shepherd.schema.json` is generated from `configFileSchema`.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_QUOTA_WARNINGS = [
|
|
11
|
+
{ "remaining-percent": 30, "interval-factor": 2 },
|
|
12
|
+
{ "remaining-percent": 20, "interval-factor": 5 },
|
|
13
|
+
{ "remaining-percent": 10, "interval-factor": 10 },
|
|
14
|
+
];
|
|
15
|
+
const SHEPHERD_OWNED_MERGE_FLAGS = [
|
|
16
|
+
"--repo",
|
|
17
|
+
"-R",
|
|
18
|
+
"--auto",
|
|
19
|
+
"--disable-auto",
|
|
20
|
+
"--match-head-commit",
|
|
21
|
+
"--admin",
|
|
22
|
+
"--body-file",
|
|
23
|
+
"-F",
|
|
24
|
+
"--help",
|
|
25
|
+
"-h",
|
|
26
|
+
];
|
|
27
|
+
const strings = () => z.array(z.string());
|
|
28
|
+
const positive = () => z.number().positive();
|
|
29
|
+
const nonNegative = () => z.number().nonnegative();
|
|
30
|
+
const positiveInt = () => z.number().int().positive();
|
|
31
|
+
const regexPattern = z
|
|
32
|
+
.string()
|
|
33
|
+
.superRefine((pattern, ctx) => {
|
|
34
|
+
try {
|
|
35
|
+
new RegExp(pattern);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
ctx.addIssue({ code: "custom", message: `invalid regular expression: ${pattern}` });
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
// `z.toJSONSchema` drops the refine, so let editors check the pattern.
|
|
42
|
+
.meta({ format: "regex" });
|
|
43
|
+
const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
// The same Shepherd-owned flag test as the refine below, stated for editors: the exact flag or
|
|
45
|
+
// `--flag=value`, and any argument that starts with a short flag.
|
|
46
|
+
const SHEPHERD_OWNED_MERGE_FLAG_PATTERN = `^(?:${SHEPHERD_OWNED_MERGE_FLAGS.map((flag) => flag.startsWith("--") ? `${escapeRegex(flag)}(?:=|$)` : escapeRegex(flag)).join("|")})`;
|
|
47
|
+
const mergeCommandArgs = z
|
|
48
|
+
.array(z.string().meta({ not: { pattern: SHEPHERD_OWNED_MERGE_FLAG_PATTERN } }))
|
|
49
|
+
.superRefine((args, ctx) => {
|
|
50
|
+
for (const arg of args) {
|
|
51
|
+
const owned = SHEPHERD_OWNED_MERGE_FLAGS.some((flag) => arg === flag ||
|
|
52
|
+
arg.startsWith(`${flag}=`) ||
|
|
53
|
+
(flag.startsWith("-") && !flag.startsWith("--") && arg.startsWith(flag)));
|
|
54
|
+
if (owned)
|
|
55
|
+
ctx.addIssue({ code: "custom", message: `cannot include Shepherd-owned ${arg}` });
|
|
56
|
+
}
|
|
57
|
+
let strategies;
|
|
58
|
+
try {
|
|
59
|
+
strategies = findMergeStrategies(args);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
ctx.addIssue({ code: "custom", message: err instanceof Error ? err.message : String(err) });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (strategies.length > 1) {
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: "custom",
|
|
68
|
+
message: `includes multiple merge strategies: ${strategies.join(", ")}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const quotaWarningBand = z
|
|
73
|
+
.strictObject({
|
|
74
|
+
"remaining-percent": z.number().int().min(1).max(100),
|
|
75
|
+
"interval-minutes": positive().optional(),
|
|
76
|
+
"interval-factor": z.number().min(1).optional(),
|
|
77
|
+
})
|
|
78
|
+
.refine((band) => band["interval-minutes"] !== undefined || band["interval-factor"] !== undefined, { message: "must define interval-minutes, interval-factor, or both" })
|
|
79
|
+
// `z.toJSONSchema` drops the refine, so state the same requirement for editors.
|
|
80
|
+
.meta({ anyOf: [{ required: ["interval-minutes"] }, { required: ["interval-factor"] }] });
|
|
81
|
+
const quotaWarnings = z.array(quotaWarningBand).superRefine((bands, ctx) => {
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
for (const band of bands) {
|
|
84
|
+
const percent = band["remaining-percent"];
|
|
85
|
+
if (seen.has(percent)) {
|
|
86
|
+
ctx.addIssue({ code: "custom", message: `duplicate remaining-percent ${percent}` });
|
|
87
|
+
}
|
|
88
|
+
seen.add(percent);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const standardRuleName = z.enum(["all", ...STANDARD_RULE_NAMES]);
|
|
92
|
+
/** The config format version this release reads. It tracks the package's semver major. */
|
|
93
|
+
export const CONFIG_VERSION = 1;
|
|
94
|
+
// The merged config: `version` is optional because the built-in defaults come from no file.
|
|
95
|
+
export const configSchema = z.strictObject({
|
|
96
|
+
version: z.literal(CONFIG_VERSION).optional(),
|
|
97
|
+
github: z
|
|
98
|
+
.strictObject({ transport: z.enum(["auto", "graphql", "rest"]).default("auto") })
|
|
99
|
+
.prefault({}),
|
|
100
|
+
"cli-command": z
|
|
101
|
+
.array(z.string().regex(/\S/, "must be a non-empty string"))
|
|
102
|
+
.min(1)
|
|
103
|
+
.default(() => ["pr-shepherd"]),
|
|
104
|
+
"bot-usernames": strings().default(() => [...DEFAULT_BOT_USERNAMES]),
|
|
105
|
+
checks: z
|
|
106
|
+
.strictObject({
|
|
107
|
+
ignore: strings().default(() => []),
|
|
108
|
+
"never-ignore": strings().default(() => []),
|
|
109
|
+
"ci-trigger-events": strings().default(() => ["pull_request", "pull_request_target"]),
|
|
110
|
+
"ignore-log-lines": z.array(regexPattern).default(() => []),
|
|
111
|
+
})
|
|
112
|
+
.prefault({}),
|
|
113
|
+
iterate: z
|
|
114
|
+
.strictObject({
|
|
115
|
+
"triage-attempts-per-thread": positiveInt().default(3),
|
|
116
|
+
"stall-timeout-minutes": nonNegative().default(60),
|
|
117
|
+
"ready-delay-minutes": nonNegative().default(0),
|
|
118
|
+
"minimize-approvals": z.boolean().default(false),
|
|
119
|
+
"minimize-comments": z.enum(["all", "bots", "none"]).default("bots"),
|
|
120
|
+
"behind-base-hint": z.string().default(""),
|
|
121
|
+
"review-output-max-chars": z.number().int().nonnegative().default(50000),
|
|
122
|
+
"resolve-other-human-threads": z.enum(["none", "outdated", "always"]).default("none"),
|
|
123
|
+
instructions: z.enum(["inline", "playbook"]).default("inline"),
|
|
124
|
+
})
|
|
125
|
+
.prefault({}),
|
|
126
|
+
poll: z
|
|
127
|
+
.strictObject({
|
|
128
|
+
"interval-seconds": positive().default(60),
|
|
129
|
+
"stack-interval-factor": z.number().min(1).default(2),
|
|
130
|
+
"timeout-seconds": positive().default(270),
|
|
131
|
+
"debounce-seconds": nonNegative().default(60),
|
|
132
|
+
"quiet-status": z.boolean().default(false),
|
|
133
|
+
"reconcile-seconds": nonNegative().default(900),
|
|
134
|
+
"quota-warnings": quotaWarnings.default(() => structuredClone(DEFAULT_QUOTA_WARNINGS)),
|
|
135
|
+
})
|
|
136
|
+
.prefault({}),
|
|
137
|
+
apply: z
|
|
138
|
+
.strictObject({
|
|
139
|
+
"sha-poll-interval-seconds": positive().default(2),
|
|
140
|
+
"sha-poll-max-attempts": positiveInt().default(10),
|
|
141
|
+
})
|
|
142
|
+
.prefault({}),
|
|
143
|
+
"mark-ready": z
|
|
144
|
+
.strictObject({
|
|
145
|
+
auto: z.boolean().default(true),
|
|
146
|
+
"blocking-reviewer-logins": strings().default(() => []),
|
|
147
|
+
})
|
|
148
|
+
.prefault({}),
|
|
149
|
+
merge: z
|
|
150
|
+
.strictObject({
|
|
151
|
+
method: z.enum(["merge", "squash", "rebase"]).optional(),
|
|
152
|
+
"command-args": mergeCommandArgs.default(() => []),
|
|
153
|
+
})
|
|
154
|
+
.prefault({}),
|
|
155
|
+
actions: z
|
|
156
|
+
.strictObject({
|
|
157
|
+
"auto-minimize-suppressed": z.boolean().default(true),
|
|
158
|
+
"work-while-queued": z.boolean().default(false),
|
|
159
|
+
})
|
|
160
|
+
.prefault({}),
|
|
161
|
+
classify: z
|
|
162
|
+
.strictObject({
|
|
163
|
+
standard: z.partialRecord(standardRuleName, z.boolean()).default(() => ({})),
|
|
164
|
+
})
|
|
165
|
+
.prefault({}),
|
|
166
|
+
});
|
|
167
|
+
/** One non-empty config file on disk, which must declare `version`. */
|
|
168
|
+
export const configFileSchema = configSchema.extend({ version: z.literal(CONFIG_VERSION) });
|
package/bin/exit-codes.d.mts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const EXIT: Readonly<{
|
|
|
19
19
|
/** Draft PR converted to ready for review. */
|
|
20
20
|
readonly MARK_READY: 11;
|
|
21
21
|
/** Agent work required. */
|
|
22
|
-
readonly
|
|
22
|
+
readonly TRIAGE: 12;
|
|
23
23
|
/** Human attention required. */
|
|
24
24
|
readonly ESCALATE: 13;
|
|
25
25
|
/** `cancel` + `closed` — PR closed without merging. */
|
package/bin/exit-codes.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const EXIT = Object.freeze({
|
|
|
17
17
|
/** Draft PR converted to ready for review. */
|
|
18
18
|
MARK_READY: 11,
|
|
19
19
|
/** Agent work required. */
|
|
20
|
-
|
|
20
|
+
TRIAGE: 12,
|
|
21
21
|
/** Human attention required. */
|
|
22
22
|
ESCALATE: 13,
|
|
23
23
|
/** `cancel` + `closed` — PR closed without merging. */
|
|
@@ -67,8 +67,8 @@ export function iterateResultToExitCode(result) {
|
|
|
67
67
|
return EXIT.WAIT;
|
|
68
68
|
case "mark_ready":
|
|
69
69
|
return EXIT.MARK_READY;
|
|
70
|
-
case "
|
|
71
|
-
return EXIT.
|
|
70
|
+
case "triage":
|
|
71
|
+
return EXIT.TRIAGE;
|
|
72
72
|
case "escalate":
|
|
73
73
|
return EXIT.ESCALATE;
|
|
74
74
|
case "merge":
|
|
@@ -7,7 +7,7 @@ import { failingSummaryCheckNames } from "./poll-summary-checks.mjs";
|
|
|
7
7
|
* does not start a one-PR session for a wait it would otherwise shepherd.
|
|
8
8
|
*/
|
|
9
9
|
export async function applyOpenCheckBlockers(raw, repo, routed, checks, review, stack, requiredContexts) {
|
|
10
|
-
if (routed.action !== "
|
|
10
|
+
if (routed.action !== "triage" || !routed.reasons.includes("failing-checks"))
|
|
11
11
|
return routed;
|
|
12
12
|
if ((review.actionable ?? 0) > 0 || checks.incomplete)
|
|
13
13
|
return routed;
|
|
@@ -2,11 +2,15 @@ import { loadConfig } from "../config/load.mjs";
|
|
|
2
2
|
import { formatPrUrl } from "../pr-reference.mjs";
|
|
3
3
|
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
4
4
|
export function pollCommandFields(repo, pr, isDraft, opts) {
|
|
5
|
-
const autoMarkReadyDisabled = opts.noAutoMarkReady || loadConfig().
|
|
5
|
+
const autoMarkReadyDisabled = opts.noAutoMarkReady || loadConfig()["mark-ready"].auto === false;
|
|
6
6
|
const boundedDraft = isDraft && autoMarkReadyDisabled;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const url = formatPrUrl(repo, pr);
|
|
8
|
+
// A single tick routes each PR to one `iterate` tick rather than a sleeping poll.
|
|
9
|
+
const args = opts.tick
|
|
10
|
+
? ["iterate", url, ...(boundedDraft ? ["--no-auto-mark-ready"] : [])]
|
|
11
|
+
: boundedDraft
|
|
12
|
+
? [url, "--timeout", "1s", "--debounce", "0s", "--no-auto-mark-ready"]
|
|
13
|
+
: [url, "--until-terminal"];
|
|
10
14
|
if (opts.merge && opts.stackPrNumber === undefined)
|
|
11
15
|
args.push("--merge");
|
|
12
16
|
if (opts.readyDelaySeconds !== undefined)
|
|
@@ -15,8 +19,6 @@ export function pollCommandFields(repo, pr, isDraft, opts) {
|
|
|
15
19
|
args.push("--stall-timeout", `${opts.stallTimeoutSeconds}s`);
|
|
16
20
|
if (opts.instructions)
|
|
17
21
|
args.push("--instructions", opts.instructions);
|
|
18
|
-
if (opts.pollMode === "event")
|
|
19
|
-
args.push("--poll-mode", "event");
|
|
20
22
|
if (opts.noAutoMarkReady && !boundedDraft)
|
|
21
23
|
args.push("--no-auto-mark-ready");
|
|
22
24
|
const pollCommand = buildPrShepherdCommand(args).text;
|
|
@@ -36,7 +36,7 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
36
36
|
}
|
|
37
37
|
const appearsReady = reasons.includes("appears-ready");
|
|
38
38
|
if (opts.stackPrNumber === undefined) {
|
|
39
|
-
const readyDelaySeconds = opts.readyDelaySeconds ??
|
|
39
|
+
const readyDelaySeconds = opts.readyDelaySeconds ?? loadConfig().iterate["ready-delay-minutes"] * 60;
|
|
40
40
|
const readyState = await updateReadyDelay(raw.number, appearsReady, readyDelaySeconds, repo.owner, repo.name, { headSha: raw.headRefOid });
|
|
41
41
|
if (appearsReady && !readyState.shouldCancel) {
|
|
42
42
|
action = "wait";
|
|
@@ -134,7 +134,7 @@ function projectQueueRemoval(removal) {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
function detectBlockingReviewer(raw) {
|
|
137
|
-
const prefixes =
|
|
137
|
+
const prefixes = loadConfig()["mark-ready"]["blocking-reviewer-logins"].map((login) => login.toLowerCase());
|
|
138
138
|
// A team reviewer has a name, not a login, so it never matches a login prefix.
|
|
139
139
|
const matches = (author) => typeof author?.login === "string" &&
|
|
140
140
|
prefixes.some((prefix) => author.login.toLowerCase().startsWith(prefix));
|
|
@@ -11,7 +11,7 @@ import { threadEndedByShepherd } from "../comments/marker.mjs";
|
|
|
11
11
|
const THREAD_COMMENT_SEPARATOR = "\n\n--- thread comment ---\n\n";
|
|
12
12
|
export async function summarizePollSummaryReview(raw, seen, viewerCanAdminister) {
|
|
13
13
|
const config = loadConfig();
|
|
14
|
-
const bots = normalizeBotUsernames(config
|
|
14
|
+
const bots = normalizeBotUsernames(config["bot-usernames"]);
|
|
15
15
|
const rules = await loadClassifyRules(getEffectiveCwd(), config.standardRules);
|
|
16
16
|
let actionable = 0;
|
|
17
17
|
let incomplete = raw.comments.pageInfo.hasPreviousPage ||
|