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.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +9 -4
  3. package/bin/cli/default-poll.mjs +2 -1
  4. package/bin/cli/duration-flag.mjs +5 -4
  5. package/bin/cli/{exit-codes.mjs → duration.mjs} +0 -26
  6. package/bin/cli/handlers.mjs +9 -8
  7. package/bin/cli/help-command-pages.mjs +17 -72
  8. package/bin/cli/help-iterate-poll-pages.mjs +72 -0
  9. package/bin/cli/help-top-page.mjs +8 -5
  10. package/bin/cli/iterate-emitter.mjs +2 -2
  11. package/bin/cli/iterate-flags.mjs +1 -1
  12. package/bin/cli/journal-handler.mjs +40 -7
  13. package/bin/cli/poll-handler.mjs +1 -1
  14. package/bin/cli/resolve-validators.mjs +3 -2
  15. package/bin/cli-parser.mjs +5 -4
  16. package/bin/commands/check.mjs +2 -1
  17. package/bin/commands/commit-suggestion.mjs +18 -17
  18. package/bin/commands/iterate/check-instructions.mjs +39 -5
  19. package/bin/commands/iterate/fix-code.mjs +4 -1
  20. package/bin/commands/iterate/index.mjs +5 -3
  21. package/bin/commands/iterate/render.mjs +9 -23
  22. package/bin/commands/mark-files-as-viewed.mjs +8 -8
  23. package/bin/commands/resolve-mutate.mjs +2 -1
  24. package/bin/comments/resolve.mjs +3 -1
  25. package/bin/config.json +2 -1
  26. package/bin/exit-codes.mjs +74 -0
  27. package/bin/github/batch-response.mjs +21 -0
  28. package/bin/github/batch.mjs +12 -22
  29. package/bin/github/errors.mjs +28 -2
  30. package/bin/github/graphql-http.mjs +45 -13
  31. package/bin/github/graphql-response.mjs +47 -0
  32. package/bin/github/http-auth.mjs +2 -1
  33. package/bin/github/rest-http.mjs +19 -5
  34. package/bin/index.mjs +2 -1
  35. package/package.json +3 -3
  36. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  37. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
- "version": "0.32.5",
4
+ "version": "0.34.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
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; private repositories require `repo` scope.
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
- ## License
231
+ ## Harness Ecosystem
232
232
 
233
- [MIT](LICENSE)
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
@@ -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 = 1;
34
+ process.exitCode = EXIT.USAGE;
34
35
  }
@@ -1,11 +1,12 @@
1
- import { parseSecondsDurationParts } from "./exit-codes.mjs";
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 = 1;
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 = 1;
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 = 1;
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
- }
@@ -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 = 1;
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 = 1;
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 = 1;
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 = 1;
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 = 1;
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 = 1;
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 = 1;
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 = 1;
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; 1 on validation, lookup, or mutation failure.`,
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 suggestion patch and instructions produced
53
- 1 validation, lookup, precondition, or suggestion parsing failure`,
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; 1 on validation or lookup failure.`,
74
- iterate: `pr-shepherd 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; 1 on validation or cleanup failure.`,
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); 1 on validation, lookup, or mutation failure.`,
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 for iterate and poll:
58
- 0 WAIT or MARK_READY
59
- 1 FIX_CODE, or a command/validation error
60
- 2 CANCEL
61
- 3 ESCALATE
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 { iterateActionToExitCode } from "./exit-codes.mjs";
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 = iterateActionToExitCode(result.action);
17
+ process.exitCode = iterateResultToExitCode(result);
18
18
  }
@@ -1,5 +1,5 @@
1
1
  import { getFlag, hasFlag } from "./args.mjs";
2
- import { parseDurationToSeconds } from "./exit-codes.mjs";
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 = 1;
15
+ process.exitCode = EXIT.USAGE;
12
16
  return;
13
17
  }
14
18
  const { prNumber, extra } = parseJournalArgs(args);
15
- const rawItem = extra[0];
16
- if (!rawItem) {
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 = 1;
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 = 1;
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" && i + 1 < args.length && !args[i + 1].startsWith("--")) {
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
  }
@@ -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 "./exit-codes.mjs";
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 = 1;
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 = 1;
23
+ process.exitCode = EXIT.DATAERR;
23
24
  return false;
24
25
  }
@@ -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 = 1;
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 = 1;
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 = 1;
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 = 1;
133
+ process.exitCode = EXIT.USAGE;
133
134
  return;
134
135
  }
135
136
  const result = await runResolveMutate({
@@ -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 Error("No open PR found for current branch. Pass a PR number explicitly.");
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;