pr-shepherd 0.34.0 → 0.36.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 +3 -2
- package/.grok-plugin/marketplace.json +17 -0
- package/README.md +80 -43
- package/bin/api.d.mts +80 -0
- package/bin/api.mjs +236 -0
- package/bin/checks/classify.d.mts +41 -0
- package/bin/checks/startup-failures.d.mts +2 -0
- package/bin/checks/superseded.d.mts +21 -0
- package/bin/checks/triage.d.mts +4 -0
- package/bin/checks/triage.mjs +11 -3
- package/bin/classify/apply.d.mts +18 -0
- package/bin/classify/apply.mjs +4 -0
- package/bin/classify/loader.d.mts +10 -0
- package/bin/classify/types.d.mts +35 -0
- package/bin/cli/args.d.mts +18 -0
- package/bin/cli/args.mjs +1 -0
- package/bin/cli/clean-formatter.d.mts +2 -0
- package/bin/cli/default-poll.d.mts +2 -0
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/duration-flag.d.mts +2 -0
- package/bin/cli/duration.d.mts +13 -0
- package/bin/cli/fence.d.mts +1 -0
- package/bin/cli/fix-formatter-extra.d.mts +3 -0
- package/bin/cli/fix-formatter.d.mts +2 -0
- package/bin/cli/fix-formatter.mjs +6 -6
- package/bin/cli/formatters.d.mts +7 -0
- package/bin/cli/handlers.d.mts +4 -0
- package/bin/cli/handlers.mjs +11 -10
- package/bin/cli/help-command-pages.d.mts +231 -0
- package/bin/cli/help-command-pages.mjs +106 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +4 -0
- package/bin/cli/help-iterate-poll-pages.mjs +14 -7
- package/bin/cli/help-log-file-page.d.mts +1 -0
- package/bin/cli/help-top-page.d.mts +1 -0
- package/bin/cli/help-top-page.mjs +16 -16
- package/bin/cli/help.d.mts +236 -0
- package/bin/cli/help.mjs +17 -0
- package/bin/cli/iterate-emitter.d.mts +8 -0
- package/bin/cli/iterate-flags.d.mts +11 -0
- package/bin/cli/iterate-formatter.d.mts +19 -0
- package/bin/cli/iterate-formatter.mjs +11 -2
- package/bin/cli/iterate-instructions.d.mts +6 -0
- package/bin/cli/iterate-lean.d.mts +12 -0
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/journal-formatter.d.mts +2 -0
- package/bin/cli/journal-formatter.mjs +15 -0
- package/bin/cli/journal-handler.d.mts +1 -0
- package/bin/cli/journal-handler.mjs +7 -20
- package/bin/cli/list-formatters.d.mts +76 -0
- package/bin/cli/list-formatters.mjs +9 -9
- package/bin/cli/mark-files-as-viewed-flags.d.mts +11 -0
- package/bin/cli/mark-files-as-viewed-formatter.d.mts +2 -0
- package/bin/cli/mutate-formatter.d.mts +2 -0
- package/bin/cli/poll-handler.d.mts +1 -0
- package/bin/cli/poll-handler.mjs +9 -2
- package/bin/cli/resolve-validators.d.mts +3 -0
- package/bin/cli/resolve-validators.mjs +3 -3
- package/bin/cli/runner.d.mts +7 -0
- package/bin/cli/suggestion-renderer.d.mts +3 -0
- package/bin/cli/validate-default-args.d.mts +6 -0
- package/bin/cli-parser.d.mts +2 -0
- package/bin/cli-parser.mjs +72 -16
- package/bin/commands/check-annotations.d.mts +5 -0
- package/bin/commands/check-status.d.mts +3 -0
- package/bin/commands/check-terminal-report.d.mts +5 -0
- package/bin/commands/check.d.mts +7 -0
- package/bin/commands/check.mjs +28 -23
- package/bin/commands/clean.d.mts +21 -0
- package/bin/commands/commit-suggestion-instruction.d.mts +8 -0
- package/bin/commands/commit-suggestion-instruction.mjs +3 -3
- package/bin/commands/commit-suggestion.d.mts +8 -0
- package/bin/commands/commit-suggestion.mjs +20 -13
- package/bin/commands/iterate/check-instructions.d.mts +16 -0
- package/bin/commands/iterate/check-instructions.mjs +3 -3
- package/bin/commands/iterate/classify.d.mts +18 -0
- package/bin/commands/iterate/classify.mjs +4 -4
- package/bin/commands/iterate/escalate.d.mts +31 -0
- package/bin/commands/iterate/escalate.mjs +7 -4
- package/bin/commands/iterate/fix-code.d.mts +25 -0
- package/bin/commands/iterate/fix-code.mjs +1 -1
- package/bin/commands/iterate/helpers.d.mts +14 -0
- package/bin/commands/iterate/helpers.mjs +19 -7
- package/bin/commands/iterate/index.d.mts +2 -0
- package/bin/commands/iterate/index.mjs +8 -12
- package/bin/commands/iterate/render.d.mts +5 -0
- package/bin/commands/iterate/render.mjs +8 -6
- package/bin/commands/iterate/reruns.d.mts +20 -0
- package/bin/commands/iterate/stall.d.mts +6 -0
- package/bin/commands/journal/index.d.mts +14 -0
- package/bin/commands/journal/index.mjs +1 -0
- package/bin/commands/journal/transform.d.mts +22 -0
- package/bin/commands/log-file.d.mts +5 -0
- package/bin/commands/mark-files-as-viewed.d.mts +26 -0
- package/bin/commands/mark-files-as-viewed.mjs +1 -0
- package/bin/commands/poll.d.mts +12 -0
- package/bin/commands/poll.mjs +52 -17
- package/bin/commands/ready-delay.d.mts +29 -0
- package/bin/commands/ready-delay.mjs +3 -13
- package/bin/commands/ready-mergeability.d.mts +15 -0
- package/bin/commands/resolve-mutate.d.mts +4 -0
- package/bin/commands/resolve-mutate.mjs +1 -0
- package/bin/commands/resolve.d.mts +4 -0
- package/bin/commands/shepherd-journal.d.mts +7 -0
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/comments/authors.d.mts +14 -0
- package/bin/comments/marker.d.mts +2 -0
- package/bin/comments/minimize-policy.d.mts +4 -0
- package/bin/comments/pending-ops.d.mts +15 -0
- package/bin/comments/rate-limit.d.mts +18 -0
- package/bin/comments/resolve.d.mts +34 -0
- package/bin/comments/resolve.mjs +1 -0
- package/bin/comments/review-thread-markers.d.mts +8 -0
- package/bin/comments/review-visibility.d.mts +28 -0
- package/bin/comments/sha-poll.d.mts +2 -0
- package/bin/comments/thread-visibility.d.mts +11 -0
- package/bin/comments/visible-comments.d.mts +11 -0
- package/bin/config/load.d.mts +60 -0
- package/bin/config/load.mjs +129 -16
- package/bin/config.json +0 -2
- package/bin/execution-context.d.mts +9 -0
- package/bin/execution-context.mjs +19 -0
- package/bin/exit-codes.d.mts +51 -0
- package/bin/github/activity.d.mts +3 -0
- package/bin/github/activity.mjs +7 -0
- package/bin/github/batch-page-helpers.d.mts +45 -0
- package/bin/github/batch-page-helpers.mjs +63 -0
- package/bin/github/batch-page.d.mts +14 -0
- package/bin/github/batch-page.mjs +62 -0
- package/bin/github/batch-parse-suites.d.mts +4 -0
- package/bin/github/batch-parse-suites.mjs +25 -0
- package/bin/github/batch-parser-helpers.d.mts +22 -0
- package/bin/github/batch-parsers-rules.d.mts +6 -0
- package/bin/github/batch-parsers-rules.mjs +122 -0
- package/bin/github/batch-parsers.d.mts +3 -0
- package/bin/github/batch-parsers.mjs +12 -0
- package/bin/github/batch-raw-rules.d.mts +59 -0
- package/bin/github/batch-raw-rules.mjs +1 -0
- package/bin/github/batch-raw-types.d.mts +216 -0
- package/bin/github/batch-response.d.mts +4 -0
- package/bin/github/batch.d.mts +24 -0
- package/bin/github/batch.mjs +14 -120
- package/bin/github/branch-protection.d.mts +3 -0
- package/bin/github/check-annotations.d.mts +2 -0
- package/bin/github/client.d.mts +46 -0
- package/bin/github/client.mjs +7 -2
- package/bin/github/errors.d.mts +25 -0
- package/bin/github/gql/batch-pr-page.gql +189 -0
- package/bin/github/gql/batch-pr.gql +79 -21
- package/bin/github/gql/commit-suggestion-thread.gql +40 -0
- package/bin/github/gql/review-thread-comments.gql +1 -0
- package/bin/github/graphql-http.d.mts +19 -0
- package/bin/github/graphql-response.d.mts +7 -0
- package/bin/github/http-auth.d.mts +4 -0
- package/bin/github/http-request.d.mts +7 -0
- package/bin/github/http-utils.d.mts +11 -0
- package/bin/github/http.d.mts +6 -0
- package/bin/github/http.mjs +2 -1
- package/bin/github/pagination.d.mts +46 -0
- package/bin/github/pagination.mjs +3 -2
- package/bin/github/queries.d.mts +28 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/github/rest-http.d.mts +7 -0
- package/bin/github/rest-http.mjs +25 -86
- package/bin/github/rest-text.d.mts +1 -0
- package/bin/github/rest-text.mjs +88 -0
- package/bin/github/suggestion-thread.d.mts +9 -0
- package/bin/github/suggestion-thread.mjs +45 -0
- package/bin/github/thread-comments.d.mts +2 -0
- package/bin/github/thread-comments.mjs +12 -8
- package/bin/index.d.mts +10 -0
- package/bin/index.mjs +1 -1
- package/bin/log/log-file.d.mts +28 -0
- package/bin/log/session.d.mts +31 -0
- package/bin/log/setup.d.mts +6 -0
- package/bin/mcp/index.d.mts +5 -0
- package/bin/mcp/index.mjs +8 -0
- package/bin/mcp/server.d.mts +8 -0
- package/bin/mcp/server.mjs +157 -0
- package/bin/mcp-stdio.d.mts +2 -0
- package/bin/mcp-stdio.mjs +7 -0
- package/bin/merge-status/derive.d.mts +19 -0
- package/bin/merge-status/derive.mjs +2 -0
- package/bin/merge-status/requirements-format.d.mts +3 -0
- package/bin/merge-status/requirements-format.mjs +88 -0
- package/bin/merge-status/requirements.d.mts +2 -0
- package/bin/merge-status/requirements.mjs +51 -0
- package/bin/reporters/agent.d.mts +23 -0
- package/bin/reporters/agent.mjs +3 -0
- package/bin/state/base.d.mts +10 -0
- package/bin/state/base.mjs +23 -0
- package/bin/state/bot-cr-seen.d.mts +51 -0
- package/bin/state/bot-cr-seen.mjs +4 -14
- package/bin/state/fix-attempts.d.mts +27 -0
- package/bin/state/fix-attempts.mjs +3 -13
- package/bin/state/iterate-stall.d.mts +27 -0
- package/bin/state/iterate-stall.mjs +3 -13
- package/bin/state/seen-comments.d.mts +62 -0
- package/bin/state/seen-comments.mjs +6 -13
- package/bin/suggestions/extract.d.mts +8 -0
- package/bin/suggestions/parse.d.mts +48 -0
- package/bin/suggestions/patch.d.mts +14 -0
- package/bin/threads/transcript.d.mts +14 -0
- package/bin/threads/transcript.mjs +4 -0
- package/bin/types/activity.d.mts +30 -0
- package/bin/types/agent-thread.d.mts +9 -0
- package/bin/types/check-annotations.d.mts +14 -0
- package/bin/types/check-classification.d.mts +19 -0
- package/bin/types/github.d.mts +136 -0
- package/bin/types/iterate.d.mts +156 -0
- package/bin/types/merge-requirements.d.mts +82 -0
- package/bin/types/merge-requirements.mjs +2 -0
- package/bin/types/protected-run.d.mts +6 -0
- package/bin/types/report.d.mts +176 -0
- package/bin/types/review-thread.d.mts +12 -0
- package/bin/types.d.mts +10 -0
- package/bin/types.mjs +1 -0
- package/bin/util/markdown.d.mts +1 -0
- package/bin/util/path-segment.d.mts +4 -0
- package/bin/util/path-segment.mjs +2 -0
- package/bin/util/pool.d.mts +2 -0
- package/bin/util/pool.mjs +18 -0
- package/bin/util/sleep.d.mts +1 -0
- package/bin/util/worktree.d.mts +9 -0
- package/bin/util/worktree.mjs +4 -1
- package/package.json +51 -37
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +3 -2
- package/plugins/pr-shepherd/.codex.mcp.json +8 -0
- package/plugins/pr-shepherd/.mcp.json +6 -0
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +5 -19
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -15
- package/src/classify/types.mts +12 -0
package/bin/config/load.mjs
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { join,
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { parse } from "yaml";
|
|
5
6
|
import builtins from "../config.json" with { type: "json" };
|
|
7
|
+
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
6
8
|
const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
|
|
7
9
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Collect `.pr-shepherdrc.yml` files from `startDir` toward `$HOME` (closest first).
|
|
12
|
+
* `$HOME/.pr-shepherdrc.yml` is always included when it exists, even if cwd is
|
|
13
|
+
* outside the home directory. The walk never includes the filesystem root, so a
|
|
14
|
+
* `/.pr-shepherdrc.yml` cannot override the user-level file.
|
|
15
|
+
*/
|
|
16
|
+
function collectRcFiles(startDir) {
|
|
17
|
+
const home = resolve(homedir());
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const files = [];
|
|
20
|
+
const add = (dir) => {
|
|
21
|
+
const candidate = resolve(join(dir, RC_FILENAME));
|
|
22
|
+
if (seen.has(candidate))
|
|
23
|
+
return;
|
|
13
24
|
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) {
|
|
14
|
-
|
|
25
|
+
seen.add(candidate);
|
|
26
|
+
files.push(candidate);
|
|
15
27
|
}
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
};
|
|
29
|
+
let current = resolve(startDir);
|
|
30
|
+
while (current !== dirname(current)) {
|
|
31
|
+
add(current);
|
|
32
|
+
if (current === home)
|
|
33
|
+
break;
|
|
18
34
|
current = dirname(current);
|
|
19
35
|
}
|
|
36
|
+
add(home);
|
|
37
|
+
return files;
|
|
20
38
|
}
|
|
21
39
|
function deepMerge(base, override) {
|
|
22
40
|
const result = { ...base };
|
|
@@ -41,6 +59,10 @@ function isMinimizeCommentsPolicy(value) {
|
|
|
41
59
|
return MINIMIZE_COMMENTS_POLICIES.some((policy) => policy === value);
|
|
42
60
|
}
|
|
43
61
|
function parseMinimizeCommentsPolicy(value) {
|
|
62
|
+
if (value === "users") {
|
|
63
|
+
process.stderr.write('pr-shepherd: config: iterate.minimizeComments: "users" is deprecated and is now treated as "none".\n');
|
|
64
|
+
return "none";
|
|
65
|
+
}
|
|
44
66
|
if (isMinimizeCommentsPolicy(value))
|
|
45
67
|
return value;
|
|
46
68
|
throw new Error(`Invalid config: iterate.minimizeComments must be one of "all", "bots", "users", or "none", got ${JSON.stringify(value)}`);
|
|
@@ -63,21 +85,112 @@ function parseNeverCancelRuns(value) {
|
|
|
63
85
|
}
|
|
64
86
|
return value;
|
|
65
87
|
}
|
|
88
|
+
const KNOWN_CONFIG_KEYS = new Set([
|
|
89
|
+
"classify",
|
|
90
|
+
"botUsernames",
|
|
91
|
+
"ignoreChecks",
|
|
92
|
+
"iterate",
|
|
93
|
+
"watch",
|
|
94
|
+
"resolve",
|
|
95
|
+
"checks",
|
|
96
|
+
"mergeStatus",
|
|
97
|
+
"actions",
|
|
98
|
+
]);
|
|
99
|
+
const KNOWN_NESTED_KEYS = {
|
|
100
|
+
iterate: new Set([
|
|
101
|
+
"fixAttemptsPerThread",
|
|
102
|
+
"stallTimeoutMinutes",
|
|
103
|
+
"minimizeApprovals",
|
|
104
|
+
"minimizeComments",
|
|
105
|
+
"behindBaseHint",
|
|
106
|
+
]),
|
|
107
|
+
watch: new Set(["readyDelayMinutes"]),
|
|
108
|
+
resolve: new Set(["shaPoll"]),
|
|
109
|
+
checks: new Set(["ciTriggerEvents"]),
|
|
110
|
+
mergeStatus: new Set(["blockingReviewerLogins"]),
|
|
111
|
+
actions: new Set([
|
|
112
|
+
"autoMinimizeSuppressed",
|
|
113
|
+
"autoMarkReady",
|
|
114
|
+
"neverCancelRuns",
|
|
115
|
+
"autoResolveOutdated",
|
|
116
|
+
"commitSuggestions",
|
|
117
|
+
]),
|
|
118
|
+
};
|
|
119
|
+
function warnUnknownConfigKeys(config) {
|
|
120
|
+
for (const key of Object.keys(config)) {
|
|
121
|
+
if (!KNOWN_CONFIG_KEYS.has(key)) {
|
|
122
|
+
process.stderr.write(`pr-shepherd: config: unknown key "${key}" ignored.\n`);
|
|
123
|
+
delete config[key];
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const value = config[key];
|
|
127
|
+
const nested = KNOWN_NESTED_KEYS[key];
|
|
128
|
+
if (nested === undefined ||
|
|
129
|
+
value === null ||
|
|
130
|
+
typeof value !== "object" ||
|
|
131
|
+
Array.isArray(value)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
for (const child of Object.keys(value)) {
|
|
135
|
+
if (!nested.has(child)) {
|
|
136
|
+
process.stderr.write(`pr-shepherd: config: unknown key "${key}.${child}" ignored.\n`);
|
|
137
|
+
delete value[child];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
66
142
|
const defaults = builtins;
|
|
67
143
|
const configCache = new Map();
|
|
144
|
+
function stripDeprecatedActionKeys(parsed) {
|
|
145
|
+
const rawActions = parsed.actions;
|
|
146
|
+
if (rawActions === null || typeof rawActions !== "object" || Array.isArray(rawActions))
|
|
147
|
+
return;
|
|
148
|
+
const actions = rawActions;
|
|
149
|
+
for (const key of ["autoResolveOutdated", "commitSuggestions"]) {
|
|
150
|
+
if (key in actions) {
|
|
151
|
+
process.stderr.write(`pr-shepherd: config: actions.${key} is deprecated and ignored.\n`);
|
|
152
|
+
delete actions[key];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function readRcFile(rcPath) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = readFileSync(rcPath, "utf8");
|
|
159
|
+
const parsed = (parse(raw) ?? {});
|
|
160
|
+
stripDeprecatedActionKeys(parsed);
|
|
161
|
+
warnUnknownConfigKeys(parsed);
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
process.stderr.write(`pr-shepherd: failed to parse ${rcPath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
68
169
|
export function loadConfig() {
|
|
69
|
-
const cwd =
|
|
170
|
+
const cwd = getEffectiveCwd();
|
|
70
171
|
if (configCache.has(cwd))
|
|
71
172
|
return configCache.get(cwd);
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
173
|
+
const rcPaths = collectRcFiles(cwd);
|
|
174
|
+
if (rcPaths.length === 0) {
|
|
74
175
|
configCache.set(cwd, defaults);
|
|
75
176
|
return defaults;
|
|
76
177
|
}
|
|
77
178
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
179
|
+
// Farthest file first so closer directories override, ESLint-style.
|
|
180
|
+
let overlay = {};
|
|
181
|
+
let loaded = false;
|
|
182
|
+
for (const rcPath of [...rcPaths].reverse()) {
|
|
183
|
+
const parsed = readRcFile(rcPath);
|
|
184
|
+
if (parsed === null)
|
|
185
|
+
continue;
|
|
186
|
+
overlay = deepMerge(overlay, parsed);
|
|
187
|
+
loaded = true;
|
|
188
|
+
}
|
|
189
|
+
if (!loaded) {
|
|
190
|
+
configCache.set(cwd, defaults);
|
|
191
|
+
return defaults;
|
|
192
|
+
}
|
|
193
|
+
const config = deepMerge(structuredClone(defaults), overlay);
|
|
81
194
|
config.botUsernames = parseBotUsernames(config.botUsernames);
|
|
82
195
|
config.ignoreChecks = parseIgnoreChecks(config.ignoreChecks);
|
|
83
196
|
config.actions.neverCancelRuns = parseNeverCancelRuns(config.actions.neverCancelRuns);
|
|
@@ -86,7 +199,7 @@ export function loadConfig() {
|
|
|
86
199
|
return config;
|
|
87
200
|
}
|
|
88
201
|
catch (err) {
|
|
89
|
-
process.stderr.write(`pr-shepherd: failed to parse ${
|
|
202
|
+
process.stderr.write(`pr-shepherd: failed to parse ${rcPaths[0]}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
90
203
|
const fallback = { ...defaults };
|
|
91
204
|
configCache.set(cwd, fallback);
|
|
92
205
|
return fallback;
|
package/bin/config.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs work with an optional working directory without changing Node's global
|
|
3
|
+
* process state. This keeps independent API/MCP requests safe to run together.
|
|
4
|
+
*/
|
|
5
|
+
export declare function runWithExecutionCwd<T>(cwd: string | undefined, work: () => T): T;
|
|
6
|
+
/** The caller-scoped working directory, when one was supplied. */
|
|
7
|
+
export declare function getExecutionCwd(): string | undefined;
|
|
8
|
+
/** The caller-scoped working directory, falling back to the CLI process cwd. */
|
|
9
|
+
export declare function getEffectiveCwd(): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const executionContext = new AsyncLocalStorage();
|
|
3
|
+
/**
|
|
4
|
+
* Runs work with an optional working directory without changing Node's global
|
|
5
|
+
* process state. This keeps independent API/MCP requests safe to run together.
|
|
6
|
+
*/
|
|
7
|
+
export function runWithExecutionCwd(cwd, work) {
|
|
8
|
+
if (cwd === undefined)
|
|
9
|
+
return work();
|
|
10
|
+
return executionContext.run({ cwd }, work);
|
|
11
|
+
}
|
|
12
|
+
/** The caller-scoped working directory, when one was supplied. */
|
|
13
|
+
export function getExecutionCwd() {
|
|
14
|
+
return executionContext.getStore()?.cwd;
|
|
15
|
+
}
|
|
16
|
+
/** The caller-scoped working directory, falling back to the CLI process cwd. */
|
|
17
|
+
export function getEffectiveCwd() {
|
|
18
|
+
return getExecutionCwd() ?? process.cwd();
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process exit codes for the pr-shepherd CLI.
|
|
3
|
+
*
|
|
4
|
+
* Three bands:
|
|
5
|
+
* 0 done — shepherd finished and the PR is in a good terminal state
|
|
6
|
+
* 10-19 shepherd RAN successfully; the code reports PR state
|
|
7
|
+
* 64-78 shepherd FAILED (BSD `sysexits.h` codes)
|
|
8
|
+
*
|
|
9
|
+
* Caller rule: `$? >= 64` means shepherd itself failed. `0` or `10-19` means it
|
|
10
|
+
* ran to completion and is reporting PR state. See docs/exit-codes.md.
|
|
11
|
+
*/
|
|
12
|
+
import type { IterateResult } from "./types.mts";
|
|
13
|
+
export declare const EXIT: Readonly<{
|
|
14
|
+
/** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
|
|
15
|
+
readonly OK: 0;
|
|
16
|
+
/** Nothing to do yet; CI still in progress. */
|
|
17
|
+
readonly WAIT: 10;
|
|
18
|
+
/** Draft PR converted to ready for review. */
|
|
19
|
+
readonly MARK_READY: 11;
|
|
20
|
+
/** Agent work required. */
|
|
21
|
+
readonly FIX_CODE: 12;
|
|
22
|
+
/** Human attention required. */
|
|
23
|
+
readonly ESCALATE: 13;
|
|
24
|
+
/** `cancel` + `closed` — PR closed without merging. */
|
|
25
|
+
readonly CLOSED: 14;
|
|
26
|
+
/** Bad/unknown flag, unknown subcommand, missing required arg, invalid duration. */
|
|
27
|
+
readonly USAGE: 64;
|
|
28
|
+
/** Malformed caller data: bad `--require-sha`, `PRRC_*` IDs, bad repo string. */
|
|
29
|
+
readonly DATAERR: 65;
|
|
30
|
+
/** Input file/stdin could not be read. */
|
|
31
|
+
readonly NOINPUT: 66;
|
|
32
|
+
/** Precondition unmet: no open PR for branch, thread not eligible, unclassified 4xx. */
|
|
33
|
+
readonly UNAVAILABLE: 69;
|
|
34
|
+
/** Unexpected/unclassified internal error — the fallback. */
|
|
35
|
+
readonly SOFTWARE: 70;
|
|
36
|
+
/** Retryable GitHub failure: 429, 5xx, rate limit exhausted, `Retry-After` present. */
|
|
37
|
+
readonly TEMPFAIL: 75;
|
|
38
|
+
/** GitHub 401/403 — missing token or insufficient PAT scopes. */
|
|
39
|
+
readonly NOPERM: 77;
|
|
40
|
+
/** `.pr-shepherdrc.yml` validation failure. */
|
|
41
|
+
readonly CONFIG: 78;
|
|
42
|
+
}>;
|
|
43
|
+
/** An error that carries its own exit code, so the top-level handler doesn't have to guess. */
|
|
44
|
+
export declare class ShepherdError extends Error {
|
|
45
|
+
readonly exitCode: number;
|
|
46
|
+
constructor(message: string, exitCode: number, opts?: {
|
|
47
|
+
cause?: unknown;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export declare function iterateResultToExitCode(result: IterateResult): number;
|
|
51
|
+
export declare function errorToExitCode(err: unknown): number;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PrActivitySummary, PrComment, Review, ReviewThread } from "../types.mts";
|
|
2
|
+
import type { RawPr } from "./batch-raw-types.mts";
|
|
3
|
+
export declare function buildPrActivitySummary(raw: RawPr, comments: PrComment[], reviewThreads: ReviewThread[], reviewSummaries: Review[], changesRequestedReviews: Review[], approvedReviews: Review[]): PrActivitySummary;
|
package/bin/github/activity.mjs
CHANGED
|
@@ -8,6 +8,7 @@ function reviewActivityItems(reviews, latestCommitCommittedAtUnix, kind) {
|
|
|
8
8
|
id: r.id,
|
|
9
9
|
author: r.author,
|
|
10
10
|
authorType: r.authorType,
|
|
11
|
+
...(r.authorAssociation !== undefined && { authorAssociation: r.authorAssociation }),
|
|
11
12
|
body: r.body,
|
|
12
13
|
createdAtUnix: r.createdAtUnix ?? 0,
|
|
13
14
|
}));
|
|
@@ -26,6 +27,9 @@ export function buildPrActivitySummary(raw, comments, reviewThreads, reviewSumma
|
|
|
26
27
|
id: c.id,
|
|
27
28
|
author: c.author,
|
|
28
29
|
authorType: c.authorType,
|
|
30
|
+
...(c.authorAssociation !== undefined && {
|
|
31
|
+
authorAssociation: c.authorAssociation,
|
|
32
|
+
}),
|
|
29
33
|
body: c.body,
|
|
30
34
|
url: c.url,
|
|
31
35
|
createdAtUnix: c.createdAtUnix,
|
|
@@ -37,6 +41,9 @@ export function buildPrActivitySummary(raw, comments, reviewThreads, reviewSumma
|
|
|
37
41
|
id: c.id,
|
|
38
42
|
author: c.author,
|
|
39
43
|
authorType: c.authorType,
|
|
44
|
+
...(c.authorAssociation !== undefined && {
|
|
45
|
+
authorAssociation: c.authorAssociation,
|
|
46
|
+
}),
|
|
40
47
|
body: c.body,
|
|
41
48
|
url: c.url,
|
|
42
49
|
createdAtUnix: c.createdAtUnix,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RawContextNode, RawComment, RawPr, RawReview, RawReviewSummary, RawThread } from "./batch-raw-types.mts";
|
|
2
|
+
export interface RawPageResponse {
|
|
3
|
+
repository: {
|
|
4
|
+
pullRequest: {
|
|
5
|
+
reviewThreads?: RawPr["reviewThreads"];
|
|
6
|
+
comments?: RawPr["comments"];
|
|
7
|
+
changesRequestedReviews?: RawPr["changesRequestedReviews"];
|
|
8
|
+
reviewSummaries?: RawPr["reviewSummaries"];
|
|
9
|
+
approvedReviews?: RawPr["approvedReviews"];
|
|
10
|
+
commits?: RawPr["commits"];
|
|
11
|
+
} | null;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function backwardCursor(conn: {
|
|
15
|
+
pageInfo: {
|
|
16
|
+
hasPreviousPage: boolean;
|
|
17
|
+
startCursor: string | null;
|
|
18
|
+
};
|
|
19
|
+
} | undefined): string | undefined;
|
|
20
|
+
export declare function forwardCursor(conn: {
|
|
21
|
+
pageInfo: {
|
|
22
|
+
hasNextPage: boolean;
|
|
23
|
+
endCursor: string | null;
|
|
24
|
+
};
|
|
25
|
+
} | undefined): string | undefined;
|
|
26
|
+
export interface PageCursors {
|
|
27
|
+
threads?: string;
|
|
28
|
+
comments?: string;
|
|
29
|
+
changesRequested?: string;
|
|
30
|
+
reviewSummaries?: string;
|
|
31
|
+
approvedReviews?: string;
|
|
32
|
+
checks?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface PageAccumulator {
|
|
35
|
+
threads: RawThread[];
|
|
36
|
+
comments: RawComment[];
|
|
37
|
+
changesRequested: RawReview[];
|
|
38
|
+
reviewSummaries: RawReviewSummary[];
|
|
39
|
+
approvedReviews: RawReviewSummary[];
|
|
40
|
+
checks: RawContextNode[];
|
|
41
|
+
}
|
|
42
|
+
export declare function applyIncludedPage(dest: PageAccumulator, pr2: NonNullable<RawPageResponse["repository"]["pullRequest"]>, cursors: PageCursors, firstOid: string | undefined, pageCount: number, pr: number): {
|
|
43
|
+
cursors: PageCursors;
|
|
44
|
+
pageCount: number;
|
|
45
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { requireContextNodes } from "./batch-response.mjs";
|
|
2
|
+
export function backwardCursor(conn) {
|
|
3
|
+
if (conn?.pageInfo.hasPreviousPage === true && conn.pageInfo.startCursor) {
|
|
4
|
+
return conn.pageInfo.startCursor;
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
export function forwardCursor(conn) {
|
|
9
|
+
if (conn?.pageInfo.hasNextPage === true && conn.pageInfo.endCursor) {
|
|
10
|
+
return conn.pageInfo.endCursor;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function prependConnection(dest, conn, pr) {
|
|
15
|
+
if (!conn)
|
|
16
|
+
throw new Error(`PR #${pr} not found`);
|
|
17
|
+
dest.unshift(...conn.nodes);
|
|
18
|
+
}
|
|
19
|
+
function takeCheckPage(pr2, firstOid, pageCount) {
|
|
20
|
+
if (!pr2.commits?.nodes[0]?.commit.statusCheckRollup) {
|
|
21
|
+
throw new Error(`Check-context pagination interrupted: statusCheckRollup disappeared on page ${pageCount + 1} (possible force-push race). Retry after the push stabilizes.`);
|
|
22
|
+
}
|
|
23
|
+
const currentOid = pr2.commits.nodes[0]?.commit.oid;
|
|
24
|
+
if (firstOid !== undefined && currentOid !== undefined && currentOid !== firstOid) {
|
|
25
|
+
throw new Error(`Check-context pagination interrupted: head commit changed from ${firstOid} to ${currentOid} between pages (force-push race). Retry.`);
|
|
26
|
+
}
|
|
27
|
+
const ctxs = pr2.commits.nodes[0]?.commit.statusCheckRollup?.contexts;
|
|
28
|
+
if (!ctxs) {
|
|
29
|
+
return { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
|
|
30
|
+
}
|
|
31
|
+
return { ...ctxs, nodes: requireContextNodes(ctxs.nodes) };
|
|
32
|
+
}
|
|
33
|
+
export function applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr) {
|
|
34
|
+
const next = {};
|
|
35
|
+
let nextPageCount = pageCount;
|
|
36
|
+
if (cursors.threads !== undefined) {
|
|
37
|
+
prependConnection(dest.threads, pr2.reviewThreads, pr);
|
|
38
|
+
next.threads = backwardCursor(pr2.reviewThreads);
|
|
39
|
+
}
|
|
40
|
+
if (cursors.comments !== undefined) {
|
|
41
|
+
prependConnection(dest.comments, pr2.comments, pr);
|
|
42
|
+
next.comments = backwardCursor(pr2.comments);
|
|
43
|
+
}
|
|
44
|
+
if (cursors.changesRequested !== undefined) {
|
|
45
|
+
prependConnection(dest.changesRequested, pr2.changesRequestedReviews, pr);
|
|
46
|
+
next.changesRequested = backwardCursor(pr2.changesRequestedReviews);
|
|
47
|
+
}
|
|
48
|
+
if (cursors.reviewSummaries !== undefined) {
|
|
49
|
+
prependConnection(dest.reviewSummaries, pr2.reviewSummaries, pr);
|
|
50
|
+
next.reviewSummaries = backwardCursor(pr2.reviewSummaries);
|
|
51
|
+
}
|
|
52
|
+
if (cursors.approvedReviews !== undefined) {
|
|
53
|
+
prependConnection(dest.approvedReviews, pr2.approvedReviews, pr);
|
|
54
|
+
next.approvedReviews = backwardCursor(pr2.approvedReviews);
|
|
55
|
+
}
|
|
56
|
+
if (cursors.checks !== undefined) {
|
|
57
|
+
nextPageCount++;
|
|
58
|
+
const extra = takeCheckPage(pr2, firstOid, nextPageCount);
|
|
59
|
+
dest.checks.push(...extra.nodes);
|
|
60
|
+
next.checks = forwardCursor(extra);
|
|
61
|
+
}
|
|
62
|
+
return { cursors: next, pageCount: nextPageCount };
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RateLimitInfo, type RepoInfo } from "./client.mts";
|
|
2
|
+
import { type PageAccumulator } from "./batch-page-helpers.mts";
|
|
3
|
+
import type { RawPr } from "./batch-raw-types.mts";
|
|
4
|
+
export interface PaginatedBatchNodes extends PageAccumulator {
|
|
5
|
+
rateLimit?: RateLimitInfo;
|
|
6
|
+
}
|
|
7
|
+
export interface PaginateBatchOptions {
|
|
8
|
+
paginateApprovedReviews?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fetch remaining connection pages with a slim `@include` query. Independent
|
|
12
|
+
* connections that still have cursors are combined into one request per round.
|
|
13
|
+
*/
|
|
14
|
+
export declare function paginateBatchConnections(pr: number, repo: RepoInfo, raw: RawPr, opts: PaginateBatchOptions, initialRateLimit?: RateLimitInfo): Promise<PaginatedBatchNodes>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { graphqlWithRateLimit } from "./client.mjs";
|
|
2
|
+
import { GitHubRequestError } from "./errors.mjs";
|
|
3
|
+
import { BATCH_PR_PAGE_QUERY } from "./queries.mjs";
|
|
4
|
+
import { requireContextNodes } from "./batch-response.mjs";
|
|
5
|
+
import { applyIncludedPage, backwardCursor, forwardCursor, } from "./batch-page-helpers.mjs";
|
|
6
|
+
/**
|
|
7
|
+
* Fetch remaining connection pages with a slim `@include` query. Independent
|
|
8
|
+
* connections that still have cursors are combined into one request per round.
|
|
9
|
+
*/
|
|
10
|
+
export async function paginateBatchConnections(pr, repo, raw, opts, initialRateLimit) {
|
|
11
|
+
const dest = {
|
|
12
|
+
threads: [...raw.reviewThreads.nodes],
|
|
13
|
+
comments: [...raw.comments.nodes],
|
|
14
|
+
changesRequested: [...raw.changesRequestedReviews.nodes],
|
|
15
|
+
reviewSummaries: [...raw.reviewSummaries.nodes],
|
|
16
|
+
approvedReviews: [...raw.approvedReviews.nodes],
|
|
17
|
+
checks: [
|
|
18
|
+
...requireContextNodes(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? []),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
const firstOid = raw.commits.nodes[0]?.commit.oid;
|
|
22
|
+
let cursors = {
|
|
23
|
+
threads: backwardCursor(raw.reviewThreads),
|
|
24
|
+
comments: backwardCursor(raw.comments),
|
|
25
|
+
changesRequested: backwardCursor(raw.changesRequestedReviews),
|
|
26
|
+
reviewSummaries: backwardCursor(raw.reviewSummaries),
|
|
27
|
+
approvedReviews: opts.paginateApprovedReviews === true ? backwardCursor(raw.approvedReviews) : undefined,
|
|
28
|
+
checks: forwardCursor(raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts),
|
|
29
|
+
};
|
|
30
|
+
let rateLimit = initialRateLimit;
|
|
31
|
+
let pageCount = 0;
|
|
32
|
+
while (Object.values(cursors).some((cursor) => cursor !== undefined)) {
|
|
33
|
+
if (rateLimit?.remaining === 0) {
|
|
34
|
+
throw new GitHubRequestError("GitHub GraphQL rate limit remaining is 0; pagination incomplete", { status: 403, rateLimit });
|
|
35
|
+
}
|
|
36
|
+
const res = await graphqlWithRateLimit(BATCH_PR_PAGE_QUERY, {
|
|
37
|
+
owner: repo.owner,
|
|
38
|
+
repo: repo.name,
|
|
39
|
+
pr,
|
|
40
|
+
includeThreads: cursors.threads !== undefined,
|
|
41
|
+
threadsCursor: cursors.threads ?? null,
|
|
42
|
+
includeComments: cursors.comments !== undefined,
|
|
43
|
+
commentsCursor: cursors.comments ?? null,
|
|
44
|
+
includeChangesRequested: cursors.changesRequested !== undefined,
|
|
45
|
+
changesRequestedCursor: cursors.changesRequested ?? null,
|
|
46
|
+
includeReviewSummaries: cursors.reviewSummaries !== undefined,
|
|
47
|
+
reviewSummariesCursor: cursors.reviewSummaries ?? null,
|
|
48
|
+
includeApprovedReviews: cursors.approvedReviews !== undefined,
|
|
49
|
+
approvedReviewsCursor: cursors.approvedReviews ?? null,
|
|
50
|
+
includeChecks: cursors.checks !== undefined,
|
|
51
|
+
checksCursor: cursors.checks ?? null,
|
|
52
|
+
});
|
|
53
|
+
rateLimit = res.rateLimit ?? rateLimit;
|
|
54
|
+
const pr2 = res.data.repository.pullRequest;
|
|
55
|
+
if (!pr2)
|
|
56
|
+
throw new Error(`PR #${pr} not found`);
|
|
57
|
+
const applied = applyIncludedPage(dest, pr2, cursors, firstOid, pageCount, pr);
|
|
58
|
+
cursors = applied.cursors;
|
|
59
|
+
pageCount = applied.pageCount;
|
|
60
|
+
}
|
|
61
|
+
return { ...dest, rateLimit };
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function parseCheckSuitesComplete(raw) {
|
|
2
|
+
return raw.commits.nodes[0]?.commit.checkSuites?.pageInfo.hasNextPage === false;
|
|
3
|
+
}
|
|
4
|
+
export function parseSuiteStartupFailures(raw) {
|
|
5
|
+
const suites = raw.commits.nodes[0]?.commit.checkSuites;
|
|
6
|
+
if (!suites)
|
|
7
|
+
return [];
|
|
8
|
+
return suites.nodes.flatMap((node) => {
|
|
9
|
+
if (node.conclusion !== "STARTUP_FAILURE" || node.workflowRun == null)
|
|
10
|
+
return [];
|
|
11
|
+
const runId = node.workflowRun.databaseId != null ? String(node.workflowRun.databaseId) : null;
|
|
12
|
+
const name = node.workflowRun.workflow?.name?.trim() || (runId ? `workflow run ${runId}` : "workflow run");
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
name,
|
|
16
|
+
status: "COMPLETED",
|
|
17
|
+
conclusion: "STARTUP_FAILURE",
|
|
18
|
+
source: "startup_failure",
|
|
19
|
+
detailsUrl: node.workflowRun.url ?? "",
|
|
20
|
+
event: node.workflowRun.event,
|
|
21
|
+
runId,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuthorType, CheckConclusion, CheckRun, CheckStatus, Review, ReviewThread } from "../types.mts";
|
|
2
|
+
import type { RawContextNode } from "./batch-raw-types.mts";
|
|
3
|
+
export declare function mapAuthorType(typeName: string | undefined | null, login?: string | undefined | null): AuthorType;
|
|
4
|
+
export declare function parseCreatedAt(iso: string): number;
|
|
5
|
+
/** Map a GraphQL CheckRun context node to a CheckRun. */
|
|
6
|
+
export declare function mapCheckRunNode(node: Extract<RawContextNode, {
|
|
7
|
+
__typename: "CheckRun";
|
|
8
|
+
}>): CheckRun;
|
|
9
|
+
export declare function latestApprovedLogins(latest: Array<{
|
|
10
|
+
login: string;
|
|
11
|
+
state: string;
|
|
12
|
+
}>): Set<string>;
|
|
13
|
+
/**
|
|
14
|
+
* A CR review is stale when its commit.oid differs from the PR head AND every
|
|
15
|
+
* associated review thread is resolved or outdated. Reviews with no associated
|
|
16
|
+
* threads are treated conservatively (not marked stale).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isReviewStale(review: Review, headRefOid: string, reviewThreads: ReviewThread[]): boolean;
|
|
19
|
+
export declare function mapStatusContextState(state: string): {
|
|
20
|
+
status: CheckStatus;
|
|
21
|
+
conclusion: CheckConclusion;
|
|
22
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BranchRules, MergeQueueEntryStatus, StackStatus } from "../types.mts";
|
|
2
|
+
import type { RawBaseRef, RawPrMergeFields } from "./batch-raw-rules.mts";
|
|
3
|
+
export declare const EMPTY_BRANCH_RULES: BranchRules;
|
|
4
|
+
export declare function parseBranchRules(baseRef: RawBaseRef | null | undefined): BranchRules;
|
|
5
|
+
export declare function parseMergeQueueEntry(raw: RawPrMergeFields["mergeQueueEntry"]): MergeQueueEntryStatus | null;
|
|
6
|
+
export declare function parseStack(raw: RawPrMergeFields): StackStatus | null;
|