pr-shepherd 0.32.5 → 0.34.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 +9 -4
- package/bin/cli/default-poll.mjs +2 -1
- package/bin/cli/duration-flag.mjs +5 -4
- package/bin/cli/{exit-codes.mjs → duration.mjs} +0 -26
- package/bin/cli/handlers.mjs +9 -8
- package/bin/cli/help-command-pages.mjs +17 -72
- package/bin/cli/help-iterate-poll-pages.mjs +72 -0
- package/bin/cli/help-top-page.mjs +8 -5
- package/bin/cli/iterate-emitter.mjs +2 -2
- package/bin/cli/iterate-flags.mjs +1 -1
- package/bin/cli/journal-handler.mjs +40 -7
- package/bin/cli/poll-handler.mjs +1 -1
- package/bin/cli/resolve-validators.mjs +3 -2
- package/bin/cli-parser.mjs +5 -4
- package/bin/commands/check.mjs +2 -1
- package/bin/commands/commit-suggestion.mjs +18 -17
- package/bin/commands/iterate/check-instructions.mjs +39 -5
- package/bin/commands/iterate/fix-code.mjs +4 -1
- package/bin/commands/iterate/index.mjs +5 -3
- package/bin/commands/iterate/render.mjs +9 -23
- package/bin/commands/mark-files-as-viewed.mjs +8 -8
- package/bin/commands/resolve-mutate.mjs +2 -1
- package/bin/comments/resolve.mjs +3 -1
- package/bin/config.json +2 -1
- package/bin/exit-codes.mjs +74 -0
- package/bin/github/batch-response.mjs +21 -0
- package/bin/github/batch.mjs +12 -22
- package/bin/github/errors.mjs +28 -2
- package/bin/github/graphql-http.mjs +45 -13
- package/bin/github/graphql-response.mjs +47 -0
- package/bin/github/http-auth.mjs +2 -1
- package/bin/github/rest-http.mjs +19 -5
- package/bin/index.mjs +2 -1
- package/package.json +3 -3
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ actions:
|
|
|
190
190
|
|
|
191
191
|
Environment variables:
|
|
192
192
|
|
|
193
|
-
- `GH_TOKEN` / `GITHUB_TOKEN` / `GITHUB_PERSONAL_ACCESS_TOKEN` for auth; `gh auth token` is used as a fallback.
|
|
193
|
+
- `GH_TOKEN` / `GITHUB_TOKEN` / `GITHUB_PERSONAL_ACCESS_TOKEN` for auth; `gh auth token` is used as a fallback. See [GitHub authentication and token access](docs/authentication.md) for required PAT permissions.
|
|
194
194
|
- `PR_SHEPHERD_STATE_DIR` to override state and log location.
|
|
195
195
|
- `PR_SHEPHERD_LOG_DISABLED=1` to disable per-worktree debug logging.
|
|
196
196
|
|
|
@@ -221,13 +221,18 @@ Ready-to-use examples for common patterns are in [`examples/classification/`](ex
|
|
|
221
221
|
## Requirements
|
|
222
222
|
|
|
223
223
|
- Node.js >= 22.18.0, Bun, or Deno
|
|
224
|
-
- A GitHub token or authenticated `gh` CLI
|
|
224
|
+
- A GitHub token or authenticated `gh` CLI with the [required repository access](docs/authentication.md). A classic PAT needs the `repo` scope for complete operation.
|
|
225
225
|
- `git`
|
|
226
226
|
|
|
227
227
|
## Docs
|
|
228
228
|
|
|
229
229
|
Full reference: [docs/README.md](docs/README.md).
|
|
230
230
|
|
|
231
|
-
##
|
|
231
|
+
## Harness Ecosystem
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
This is part of the following harness ecosystem:
|
|
234
|
+
|
|
235
|
+
- [auto-harness](https://github.com/jonathanong/auto-harness) - non-interactive agent CLI orchestration across sandboxes
|
|
236
|
+
- [agent-blackboard](https://github.com/jonathanong/agent-blackboard) - session-scoped telemetry for autonomous agents
|
|
237
|
+
- [pr-shepherd](https://github.com/jonathanong/pr-shepherd) - autonomous pull request shepherd
|
|
238
|
+
- [no-mistakes](https://github.com/jonathanong/no-mistakes) - deterministic AST-based codebase intelligence, test selection, and linting for agents
|
package/bin/cli/default-poll.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
1
2
|
import { parsePrNumber } from "./args.mjs";
|
|
2
3
|
import { validateDefaultArgs } from "./validate-default-args.mjs";
|
|
3
4
|
import { USAGE } from "./help.mjs";
|
|
@@ -30,5 +31,5 @@ function isDefaultPollFlag(arg) {
|
|
|
30
31
|
function writeDefaultUsageError(arg) {
|
|
31
32
|
process.stderr.write(`Unknown subcommand: ${arg}\n`);
|
|
32
33
|
process.stderr.write(`${USAGE.top}\n`);
|
|
33
|
-
process.exitCode =
|
|
34
|
+
process.exitCode = EXIT.USAGE;
|
|
34
35
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
2
|
+
import { parseSecondsDurationParts } from "./duration.mjs";
|
|
2
3
|
export function validateSecondsDurationFlag(command, flag, value, presentAsSeparateArg, opts = {}) {
|
|
3
4
|
const bareUnit = opts.defaultUnit === "m" ? "minutes" : "seconds";
|
|
4
5
|
const example = opts.defaultUnit === "m" ? "15m" : "30s";
|
|
5
6
|
if (value === null) {
|
|
6
7
|
if (presentAsSeparateArg) {
|
|
7
8
|
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} ${example})\n`);
|
|
8
|
-
process.exitCode =
|
|
9
|
+
process.exitCode = EXIT.USAGE;
|
|
9
10
|
return null;
|
|
10
11
|
}
|
|
11
12
|
return undefined;
|
|
@@ -13,12 +14,12 @@ export function validateSecondsDurationFlag(command, flag, value, presentAsSepar
|
|
|
13
14
|
const trimmed = value.trim();
|
|
14
15
|
if (trimmed.startsWith("--")) {
|
|
15
16
|
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} ${example})\n`);
|
|
16
|
-
process.exitCode =
|
|
17
|
+
process.exitCode = EXIT.USAGE;
|
|
17
18
|
return null;
|
|
18
19
|
}
|
|
19
20
|
if (!parseSecondsDurationParts(trimmed, opts)) {
|
|
20
21
|
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 30s, 4.5m, 1h, or a bare number (${bareUnit}).\n`);
|
|
21
|
-
process.exitCode =
|
|
22
|
+
process.exitCode = EXIT.USAGE;
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
25
|
return trimmed;
|
|
@@ -40,29 +40,3 @@ export function parseDurationToSeconds(s, defaultSeconds, opts = {}) {
|
|
|
40
40
|
return parsed.value * 60;
|
|
41
41
|
return parsed.value;
|
|
42
42
|
}
|
|
43
|
-
export function statusToExitCode(status) {
|
|
44
|
-
switch (status) {
|
|
45
|
-
case "MERGED":
|
|
46
|
-
case "CLOSED":
|
|
47
|
-
case "READY":
|
|
48
|
-
return 0;
|
|
49
|
-
case "IN_PROGRESS":
|
|
50
|
-
return 2;
|
|
51
|
-
case "UNRESOLVED_COMMENTS":
|
|
52
|
-
return 3;
|
|
53
|
-
default:
|
|
54
|
-
return 1;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
export function iterateActionToExitCode(action) {
|
|
58
|
-
switch (action) {
|
|
59
|
-
case "fix_code":
|
|
60
|
-
return 1;
|
|
61
|
-
case "cancel":
|
|
62
|
-
return 2;
|
|
63
|
-
case "escalate":
|
|
64
|
-
return 3;
|
|
65
|
-
default:
|
|
66
|
-
return 0;
|
|
67
|
-
}
|
|
68
|
-
}
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { runMarkFilesAsViewed } from "../commands/mark-files-as-viewed.mjs";
|
|
|
3
3
|
import { runIterate } from "../commands/iterate/index.mjs";
|
|
4
4
|
import { runClean } from "../commands/clean.mjs";
|
|
5
5
|
import { loadConfig } from "../config/load.mjs";
|
|
6
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
6
7
|
import { parseCommonArgs, getFlag } from "./args.mjs";
|
|
7
8
|
import { USAGE } from "./help.mjs";
|
|
8
9
|
import { formatCommitSuggestionResult, formatCleanResult, formatMarkFilesAsViewedResult, } from "./formatters.mjs";
|
|
@@ -14,7 +15,7 @@ export async function handleClean(args) {
|
|
|
14
15
|
const variant = args[0];
|
|
15
16
|
if (!variant || !CLEAN_VARIANTS.has(variant)) {
|
|
16
17
|
process.stderr.write(`${USAGE.clean}\n`);
|
|
17
|
-
process.exitCode =
|
|
18
|
+
process.exitCode = EXIT.USAGE;
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
const rest = args.slice(1);
|
|
@@ -24,7 +25,7 @@ export async function handleClean(args) {
|
|
|
24
25
|
if (a === "--dry-run" || a === "--format" || a.startsWith("--format="))
|
|
25
26
|
continue;
|
|
26
27
|
process.stderr.write(`pr-shepherd: clean: unknown flag: "${a}"\n`);
|
|
27
|
-
process.exitCode =
|
|
28
|
+
process.exitCode = EXIT.USAGE;
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
31
|
const fmtIdx = rest.indexOf("--format");
|
|
@@ -38,7 +39,7 @@ export async function handleClean(args) {
|
|
|
38
39
|
}
|
|
39
40
|
if (formatValue !== undefined && formatValue !== "text" && formatValue !== "json") {
|
|
40
41
|
process.stderr.write(`pr-shepherd: clean: invalid --format value: "${formatValue}". Expected "text" or "json".\n`);
|
|
41
|
-
process.exitCode =
|
|
42
|
+
process.exitCode = EXIT.USAGE;
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
const jsonOut = formatValue === "json";
|
|
@@ -52,14 +53,14 @@ export async function handleClean(args) {
|
|
|
52
53
|
const positionals = rest.filter((a, i) => !flagConsumedIndices.has(i) && !a.startsWith("--"));
|
|
53
54
|
if (positionals.length > 1) {
|
|
54
55
|
process.stderr.write(`pr-shepherd: clean: too many positional arguments (expected at most 1, got ${positionals.length})\n`);
|
|
55
|
-
process.exitCode =
|
|
56
|
+
process.exitCode = EXIT.USAGE;
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
const value = positionals[0];
|
|
59
60
|
const result = await runClean({ variant: variant, value, dryRun });
|
|
60
61
|
if (!result.ok) {
|
|
61
62
|
process.stderr.write(`pr-shepherd: clean: ${result.error}\n`);
|
|
62
|
-
process.exitCode =
|
|
63
|
+
process.exitCode = EXIT.SOFTWARE;
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
process.stdout.write(jsonOut ? `${JSON.stringify(result, null, 2)}\n` : `${formatCleanResult(result)}\n`);
|
|
@@ -69,13 +70,13 @@ export async function handleCommitSuggestion(args) {
|
|
|
69
70
|
const threadId = getFlag(extra, "--thread-id");
|
|
70
71
|
if (!threadId) {
|
|
71
72
|
process.stderr.write(`${USAGE["commit-suggestion"]}\n`);
|
|
72
|
-
process.exitCode =
|
|
73
|
+
process.exitCode = EXIT.USAGE;
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
const message = getFlag(extra, "--message") ?? undefined;
|
|
76
77
|
if (!message || message.trim() === "") {
|
|
77
78
|
process.stderr.write("--message is required and must be non-empty\n");
|
|
78
|
-
process.exitCode =
|
|
79
|
+
process.exitCode = EXIT.USAGE;
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
const description = getFlag(extra, "--description") ?? undefined;
|
|
@@ -115,7 +116,7 @@ export async function handleMarkFilesAsViewed(args) {
|
|
|
115
116
|
const parsed = parseMarkFilesAsViewedArgs(extra);
|
|
116
117
|
if (!parsed.ok) {
|
|
117
118
|
process.stderr.write(`pr-shepherd: mark-files-as-viewed: ${parsed.error}\n`);
|
|
118
|
-
process.exitCode =
|
|
119
|
+
process.exitCode = EXIT.USAGE;
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
122
|
const result = await runMarkFilesAsViewed({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LOG_FILE_USAGE } from "./help-log-file-page.mjs";
|
|
2
|
+
import { ITERATE_USAGE, POLL_USAGE } from "./help-iterate-poll-pages.mjs";
|
|
2
3
|
export const COMMAND_USAGE = {
|
|
3
4
|
resolve: `pr-shepherd resolve
|
|
4
5
|
|
|
@@ -28,7 +29,7 @@ At least one non-empty action flag is required:
|
|
|
28
29
|
--reply-thread-ids, --resolve-thread-ids, --minimize-comment-ids, or --dismiss-review-ids.
|
|
29
30
|
|
|
30
31
|
PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
|
|
31
|
-
Exit code: 0 on success;
|
|
32
|
+
Exit code: 0 on success; nonzero on failure (sysexits.h — see docs/exit-codes.md).`,
|
|
32
33
|
"commit-suggestion": `pr-shepherd commit-suggestion
|
|
33
34
|
|
|
34
35
|
Build a patch and commit instructions for one GitHub review thread containing a suggestion block.
|
|
@@ -49,8 +50,10 @@ Preconditions:
|
|
|
49
50
|
The current branch must match the PR head ref, and local HEAD must match the PR head SHA.
|
|
50
51
|
|
|
51
52
|
Exit codes:
|
|
52
|
-
0
|
|
53
|
-
|
|
53
|
+
0 suggestion patch and instructions produced
|
|
54
|
+
64 usage error (missing/invalid flag)
|
|
55
|
+
69 precondition unmet (thread ineligible, branch/SHA mismatch, no open PR)
|
|
56
|
+
See docs/exit-codes.md for the full sysexits.h table.`,
|
|
54
57
|
"mark-files-as-viewed": `pr-shepherd mark-files-as-viewed
|
|
55
58
|
|
|
56
59
|
Mark changed files as viewed in the GitHub pull request diff.
|
|
@@ -70,73 +73,9 @@ Flags:
|
|
|
70
73
|
--help, -h Print this help and exit before GitHub I/O.
|
|
71
74
|
|
|
72
75
|
PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
|
|
73
|
-
Exit code: 0 on success;
|
|
74
|
-
iterate:
|
|
75
|
-
|
|
76
|
-
Run one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.
|
|
77
|
-
The output contains one action and an action-specific ## Instructions section.
|
|
78
|
-
|
|
79
|
-
Usage:
|
|
80
|
-
pr-shepherd iterate [PR] [iterate-flags]
|
|
81
|
-
|
|
82
|
-
Iterate flags:
|
|
83
|
-
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
84
|
-
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
85
|
-
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
86
|
-
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
87
|
-
--format text|json Output Markdown text or JSON. Default: text.
|
|
88
|
-
--verbose Include verbose iterate fields.
|
|
89
|
-
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
90
|
-
|
|
91
|
-
Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).
|
|
92
|
-
|
|
93
|
-
Actions:
|
|
94
|
-
WAIT No immediate code action; recheck later or use pr-shepherd poll.
|
|
95
|
-
MARK_READY Draft PR was marked ready for review.
|
|
96
|
-
FIX_CODE Apply fixes, commit, push, and run the printed resolve command.
|
|
97
|
-
CANCEL Terminal state: merged/closed or ready-delay elapsed.
|
|
98
|
-
ESCALATE Terminal state requiring human direction.
|
|
99
|
-
|
|
100
|
-
Exit codes:
|
|
101
|
-
0 WAIT or MARK_READY
|
|
102
|
-
1 FIX_CODE, or a command/validation error
|
|
103
|
-
2 CANCEL
|
|
104
|
-
3 ESCALATE`,
|
|
105
|
-
poll: `pr-shepherd poll
|
|
106
|
-
|
|
107
|
-
Run iterate repeatedly while the action is WAIT. Print only the final tick to stdout.
|
|
108
|
-
Poll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout
|
|
109
|
-
returns the last WAIT result. With --until-terminal, poll also continues through MARK_READY.
|
|
110
|
-
|
|
111
|
-
Usage:
|
|
112
|
-
pr-shepherd poll [PR] [poll-flags] [iterate-flags]
|
|
113
|
-
|
|
114
|
-
Poll flags:
|
|
115
|
-
--interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.
|
|
116
|
-
--timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.
|
|
117
|
-
--quiet-status During WAIT polling, print only changed status snapshots.
|
|
118
|
-
--until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.
|
|
119
|
-
|
|
120
|
-
Forwarded iterate flags:
|
|
121
|
-
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
122
|
-
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
123
|
-
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
124
|
-
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
125
|
-
--format text|json Output Markdown text or JSON. Default: text.
|
|
126
|
-
--verbose Include verbose iterate fields and detailed per-tick lines.
|
|
127
|
-
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
128
|
-
|
|
129
|
-
Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds
|
|
130
|
-
for --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with
|
|
131
|
-
an explicit unit (4.5m).
|
|
132
|
-
Each WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.
|
|
133
|
-
With --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.
|
|
134
|
-
|
|
135
|
-
Exit codes:
|
|
136
|
-
0 WAIT timeout or MARK_READY
|
|
137
|
-
1 FIX_CODE, or a command/validation error
|
|
138
|
-
2 CANCEL
|
|
139
|
-
3 ESCALATE`,
|
|
76
|
+
Exit code: 0 on success; nonzero on failure (sysexits.h — see docs/exit-codes.md).`,
|
|
77
|
+
iterate: ITERATE_USAGE,
|
|
78
|
+
poll: POLL_USAGE,
|
|
140
79
|
clean: `pr-shepherd clean
|
|
141
80
|
|
|
142
81
|
Remove pr-shepherd state files from PR_SHEPHERD_STATE_DIR.
|
|
@@ -161,7 +100,7 @@ Flags:
|
|
|
161
100
|
--format text|json Output format. Default: text.
|
|
162
101
|
--help, -h Print this help and exit before any cleanup.
|
|
163
102
|
|
|
164
|
-
Exit code: 0 on success;
|
|
103
|
+
Exit code: 0 on success (including a no-op --dry-run on a nonexistent target); nonzero on failure (sysexits.h — see docs/exit-codes.md).`,
|
|
165
104
|
journal: `pr-shepherd journal
|
|
166
105
|
|
|
167
106
|
Append a list item to the ## Shepherd Journal section of a PR body.
|
|
@@ -169,16 +108,22 @@ Creates the section at the end if absent. Idempotent — duplicate items are ski
|
|
|
169
108
|
|
|
170
109
|
Usage:
|
|
171
110
|
pr-shepherd journal [PR] <item> [--dry-run] [--format text|json]
|
|
111
|
+
pr-shepherd journal [PR] --file <path> [--dry-run] [--format text|json]
|
|
112
|
+
pr-shepherd journal [PR] --file - [--dry-run] [--format text|json]
|
|
172
113
|
|
|
173
114
|
PR PR number or GitHub pull request URL. Defaults to current branch PR.
|
|
174
115
|
item Markdown list item: must start with "- " followed by non-whitespace text.
|
|
175
116
|
Example: '- Rejected suggestion: kept existing pattern for consistency.'
|
|
117
|
+
Provide it as a positional argument, or via --file to avoid shell-escaping
|
|
118
|
+
backticks and multi-line Markdown. Exactly one of the two is required.
|
|
176
119
|
|
|
177
120
|
Flags:
|
|
121
|
+
--file <path> Read the entry from a file instead of a positional argument.
|
|
122
|
+
Pass --file - to read from stdin.
|
|
178
123
|
--dry-run Preview the new PR body without writing it to GitHub.
|
|
179
124
|
--format text|json Output format. Default: text.
|
|
180
125
|
--help, -h Print this help and exit before any GitHub I/O.
|
|
181
126
|
|
|
182
|
-
Exit code: 0 on success (including no-change no-op);
|
|
127
|
+
Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`,
|
|
183
128
|
"log-file": LOG_FILE_USAGE,
|
|
184
129
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const ITERATE_USAGE = `pr-shepherd iterate
|
|
2
|
+
|
|
3
|
+
Run one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.
|
|
4
|
+
The output contains one action and an action-specific ## Instructions section.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
pr-shepherd iterate [PR] [iterate-flags]
|
|
8
|
+
|
|
9
|
+
Iterate flags:
|
|
10
|
+
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
11
|
+
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
12
|
+
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
13
|
+
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
14
|
+
--format text|json Output Markdown text or JSON. Default: text.
|
|
15
|
+
--verbose Include verbose iterate fields.
|
|
16
|
+
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
17
|
+
|
|
18
|
+
Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).
|
|
19
|
+
|
|
20
|
+
Actions:
|
|
21
|
+
WAIT No immediate code action; recheck later or use pr-shepherd poll.
|
|
22
|
+
MARK_READY Draft PR was marked ready for review.
|
|
23
|
+
FIX_CODE Apply fixes, commit, push, and run the printed resolve command.
|
|
24
|
+
CANCEL Terminal state: merged/closed or ready-delay elapsed.
|
|
25
|
+
ESCALATE Terminal state requiring human direction.
|
|
26
|
+
|
|
27
|
+
Exit codes:
|
|
28
|
+
0 CANCEL (merged or ready-delay elapsed)
|
|
29
|
+
10 WAIT
|
|
30
|
+
11 MARK_READY
|
|
31
|
+
12 FIX_CODE
|
|
32
|
+
13 ESCALATE
|
|
33
|
+
14 CANCEL (closed without merging)
|
|
34
|
+
A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).`;
|
|
35
|
+
export const POLL_USAGE = `pr-shepherd poll
|
|
36
|
+
|
|
37
|
+
Run iterate repeatedly while the action is WAIT. Print only the final tick to stdout.
|
|
38
|
+
Poll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout
|
|
39
|
+
returns the last WAIT result. With --until-terminal, poll also continues through MARK_READY.
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
pr-shepherd poll [PR] [poll-flags] [iterate-flags]
|
|
43
|
+
|
|
44
|
+
Poll flags:
|
|
45
|
+
--interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.
|
|
46
|
+
--timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.
|
|
47
|
+
--quiet-status During WAIT polling, print only changed status snapshots.
|
|
48
|
+
--until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.
|
|
49
|
+
|
|
50
|
+
Forwarded iterate flags:
|
|
51
|
+
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
52
|
+
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
53
|
+
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
54
|
+
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
55
|
+
--format text|json Output Markdown text or JSON. Default: text.
|
|
56
|
+
--verbose Include verbose iterate fields and detailed per-tick lines.
|
|
57
|
+
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
58
|
+
|
|
59
|
+
Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds
|
|
60
|
+
for --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with
|
|
61
|
+
an explicit unit (4.5m).
|
|
62
|
+
Each WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.
|
|
63
|
+
With --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.
|
|
64
|
+
|
|
65
|
+
Exit codes: same as iterate (the final tick's action/reason decides the code).
|
|
66
|
+
0 CANCEL (merged or ready-delay elapsed)
|
|
67
|
+
10 WAIT (including a WAIT returned by --timeout)
|
|
68
|
+
11 MARK_READY
|
|
69
|
+
12 FIX_CODE
|
|
70
|
+
13 ESCALATE
|
|
71
|
+
14 CANCEL (closed without merging)
|
|
72
|
+
A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).`;
|
|
@@ -54,11 +54,14 @@ Clean variants:
|
|
|
54
54
|
repo Remove all state for the current repository.
|
|
55
55
|
all Remove all pr-shepherd state.
|
|
56
56
|
|
|
57
|
-
Exit codes
|
|
58
|
-
0
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
Exit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).
|
|
58
|
+
0 CANCEL (merged or ready-delay elapsed)
|
|
59
|
+
10 WAIT
|
|
60
|
+
11 MARK_READY
|
|
61
|
+
12 FIX_CODE
|
|
62
|
+
13 ESCALATE
|
|
63
|
+
14 CANCEL (closed without merging)
|
|
64
|
+
See docs/exit-codes.md for the full sysexits.h error-code table.
|
|
62
65
|
|
|
63
66
|
Duration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).
|
|
64
67
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { iterateResultToExitCode } from "../exit-codes.mjs";
|
|
2
2
|
import { formatIterateResult, projectIterateLean, projectIterateVerbose } from "./formatters.mjs";
|
|
3
3
|
export function emitIterateResult(result, opts) {
|
|
4
4
|
const projectionOpts = {
|
|
@@ -14,5 +14,5 @@ export function emitIterateResult(result, opts) {
|
|
|
14
14
|
const text = formatIterateResult(result, { verbose: opts.verbose, ...projectionOpts });
|
|
15
15
|
process.stdout.write(`${text}\n`);
|
|
16
16
|
}
|
|
17
|
-
process.exitCode =
|
|
17
|
+
process.exitCode = iterateResultToExitCode(result);
|
|
18
18
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getFlag, hasFlag } from "./args.mjs";
|
|
2
|
-
import { parseDurationToSeconds } from "./
|
|
2
|
+
import { parseDurationToSeconds } from "./duration.mjs";
|
|
3
3
|
import { validateSecondsDurationFlag } from "./duration-flag.mjs";
|
|
4
4
|
// --ready-delay and --stall-timeout are minute-family flags: a bare number means minutes, and 0 is a
|
|
5
5
|
// valid value (it disables the ready-delay settle window / stall-timeout escalation, respectively).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { EXIT, errorToExitCode } from "../exit-codes.mjs";
|
|
1
3
|
import { runJournal } from "../commands/journal/index.mjs";
|
|
2
|
-
import { parsePrNumber } from "./args.mjs";
|
|
4
|
+
import { getFlag, parsePrNumber } from "./args.mjs";
|
|
3
5
|
import { USAGE } from "./help.mjs";
|
|
4
6
|
export async function handleJournal(args) {
|
|
5
7
|
for (const a of args) {
|
|
@@ -7,15 +9,31 @@ export async function handleJournal(args) {
|
|
|
7
9
|
continue;
|
|
8
10
|
if (a === "--dry-run" || a === "--format" || a.startsWith("--format="))
|
|
9
11
|
continue;
|
|
12
|
+
if (a === "--file" || a.startsWith("--file="))
|
|
13
|
+
continue;
|
|
10
14
|
process.stderr.write(`pr-shepherd: journal: unknown flag: "${a}"\n`);
|
|
11
|
-
process.exitCode =
|
|
15
|
+
process.exitCode = EXIT.USAGE;
|
|
12
16
|
return;
|
|
13
17
|
}
|
|
14
18
|
const { prNumber, extra } = parseJournalArgs(args);
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
19
|
+
const filePath = getFlag(args, "--file");
|
|
20
|
+
if (filePath !== null && extra[0]) {
|
|
21
|
+
process.stderr.write(`pr-shepherd: journal: provide the entry as a positional argument or via --file, not both\n`);
|
|
22
|
+
process.exitCode = EXIT.USAGE;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let rawItem;
|
|
26
|
+
try {
|
|
27
|
+
rawItem = filePath !== null ? await readItemSource(filePath) : extra[0];
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
process.stderr.write(`pr-shepherd: journal: ${String(e)}\n`);
|
|
31
|
+
process.exitCode = EXIT.NOINPUT;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (rawItem === undefined) {
|
|
17
35
|
process.stderr.write(`${USAGE.journal}\n`);
|
|
18
|
-
process.exitCode =
|
|
36
|
+
process.exitCode = EXIT.USAGE;
|
|
19
37
|
return;
|
|
20
38
|
}
|
|
21
39
|
const dryRun = args.includes("--dry-run");
|
|
@@ -32,14 +50,29 @@ export async function handleJournal(args) {
|
|
|
32
50
|
}
|
|
33
51
|
catch (e) {
|
|
34
52
|
process.stderr.write(`pr-shepherd: journal: ${String(e)}\n`);
|
|
35
|
-
process.exitCode =
|
|
53
|
+
process.exitCode = errorToExitCode(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Reads the journal entry from a file, or from stdin when `filePath` is `-`. */
|
|
57
|
+
async function readItemSource(filePath) {
|
|
58
|
+
if (filePath === "-")
|
|
59
|
+
return readStdin();
|
|
60
|
+
return readFile(filePath, "utf8");
|
|
61
|
+
}
|
|
62
|
+
async function readStdin() {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for await (const chunk of process.stdin) {
|
|
65
|
+
chunks.push(chunk);
|
|
36
66
|
}
|
|
67
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
37
68
|
}
|
|
38
69
|
function parseJournalArgs(args) {
|
|
39
70
|
const flagConsumedIndices = new Set();
|
|
40
71
|
for (let i = 0; i < args.length; i++) {
|
|
41
72
|
const a = args[i];
|
|
42
|
-
if (a === "--format"
|
|
73
|
+
if ((a === "--format" || a === "--file") &&
|
|
74
|
+
i + 1 < args.length &&
|
|
75
|
+
!args[i + 1].startsWith("--")) {
|
|
43
76
|
flagConsumedIndices.add(i);
|
|
44
77
|
flagConsumedIndices.add(i + 1);
|
|
45
78
|
}
|
package/bin/cli/poll-handler.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runPoll } from "../commands/poll.mjs";
|
|
2
2
|
import { loadConfig } from "../config/load.mjs";
|
|
3
3
|
import { parseCommonArgs, getFlag, hasFlag } from "./args.mjs";
|
|
4
|
-
import { parseDurationToSeconds } from "./
|
|
4
|
+
import { parseDurationToSeconds } from "./duration.mjs";
|
|
5
5
|
import { validateSecondsDurationFlag } from "./duration-flag.mjs";
|
|
6
6
|
import { parseIterateFlags } from "./iterate-flags.mjs";
|
|
7
7
|
import { emitIterateResult } from "./iterate-emitter.mjs";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { EXIT } from "../exit-codes.mjs";
|
|
1
2
|
export function rejectPrrcMinimizeIds(ids) {
|
|
2
3
|
const prrcIds = ids.filter((id) => id.startsWith("PRRC_"));
|
|
3
4
|
if (prrcIds.length > 0) {
|
|
4
5
|
process.stderr.write(`pr-shepherd: resolve: --minimize-comment-ids contains thread comment IDs (PRRC_*): ${prrcIds.join(", ")}. Thread comments cannot be minimized individually — resolve the parent thread using --resolve-thread-ids with the PRRT_* thread ID instead.\n`);
|
|
5
|
-
process.exitCode =
|
|
6
|
+
process.exitCode = EXIT.DATAERR;
|
|
6
7
|
}
|
|
7
8
|
return prrcIds;
|
|
8
9
|
}
|
|
@@ -19,6 +20,6 @@ export function validateRequireSha(sha) {
|
|
|
19
20
|
if (/^[0-9a-f]{40}$/.test(sha))
|
|
20
21
|
return true;
|
|
21
22
|
process.stderr.write(`pr-shepherd: resolve: --require-sha must be a full 40-character lowercase hex SHA, got "${sha}". Short SHAs will never match GitHub's headRefOid. Use $(git rev-parse HEAD) to get the full SHA.\n`);
|
|
22
|
-
process.exitCode =
|
|
23
|
+
process.exitCode = EXIT.DATAERR;
|
|
23
24
|
return false;
|
|
24
25
|
}
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** CLI argument parsing and subcommand dispatch for pr-shepherd. See --help for usage. */
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { EXIT, errorToExitCode } from "./exit-codes.mjs";
|
|
3
4
|
import { runResolveMutate } from "./commands/resolve.mjs";
|
|
4
5
|
import { runLogFile } from "./commands/log-file.mjs";
|
|
5
6
|
import { parseCommonArgs, getFlag, hasFlag, parseList } from "./cli/args.mjs";
|
|
@@ -76,7 +77,7 @@ export async function main(argv) {
|
|
|
76
77
|
default:
|
|
77
78
|
process.stderr.write(`Unknown subcommand: ${subcommand ?? "(none)"}\n`);
|
|
78
79
|
process.stderr.write(`${USAGE.top}\n`);
|
|
79
|
-
process.exitCode =
|
|
80
|
+
process.exitCode = EXIT.USAGE;
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -102,7 +103,7 @@ async function handleLogFile(args) {
|
|
|
102
103
|
}
|
|
103
104
|
catch (e) {
|
|
104
105
|
process.stderr.write(`pr-shepherd: log-file: ${String(e)}\n`);
|
|
105
|
-
process.exitCode =
|
|
106
|
+
process.exitCode = errorToExitCode(e);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
async function handleResolve(args) {
|
|
@@ -120,7 +121,7 @@ async function handleResolve(args) {
|
|
|
120
121
|
return;
|
|
121
122
|
if (hasFlag(extra, "--fetch")) {
|
|
122
123
|
process.stderr.write("pr-shepherd: resolve: --fetch has been removed; run pr-shepherd iterate or poll to fetch the next action.\n");
|
|
123
|
-
process.exitCode =
|
|
124
|
+
process.exitCode = EXIT.USAGE;
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
const hasAction = resolveThreadIds.length > 0 ||
|
|
@@ -129,7 +130,7 @@ async function handleResolve(args) {
|
|
|
129
130
|
dismissReviewIds.length > 0;
|
|
130
131
|
if (!hasAction) {
|
|
131
132
|
process.stderr.write("pr-shepherd: resolve: an action flag is required (--reply-thread-ids, --resolve-thread-ids, --minimize-comment-ids, or --dismiss-review-ids).\n");
|
|
132
|
-
process.exitCode =
|
|
133
|
+
process.exitCode = EXIT.USAGE;
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
136
|
const result = await runResolveMutate({
|
package/bin/commands/check.mjs
CHANGED
|
@@ -19,11 +19,12 @@ import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers
|
|
|
19
19
|
import { normalizeBotUsernames } from "../comments/authors.mjs";
|
|
20
20
|
import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
21
21
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
22
|
+
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
22
23
|
export async function runCheck(opts) {
|
|
23
24
|
const repo = await getRepoInfo();
|
|
24
25
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
25
26
|
if (prNumber === null) {
|
|
26
|
-
throw new
|
|
27
|
+
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
27
28
|
}
|
|
28
29
|
const config = loadConfig();
|
|
29
30
|
const paginateApprovedReviews = config.iterate.minimizeApprovals;
|