pr-shepherd 0.19.0 → 0.20.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.
@@ -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.19.0",
4
+ "version": "0.20.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -20,12 +20,12 @@ Example Workflow:
20
20
 
21
21
  `pr-shepherd` optimizes token management, rate limits, and agentic orchestration by moving **ALL** deterministic logic and prompts to code via a CLI tool, enshrining what would be a large skill or command prompt (of which the agent would inevitably make mistakes) into the code and returning a clear, actionable prompt.
22
22
 
23
- The CLI emits unified recheck instructions. Generated commands use `cli.runner` from `.pr-shepherdrc.yml`: `auto` (default), `npx`, `pnpm`, `yarn`, or `bun`.
23
+ The CLI emits unified recheck instructions. Generated commands call `pr-shepherd` directly.
24
24
 
25
- At a high level, the skill invokes `pr-shepherd <PR>` through the selected package runner, which provides actionable feedback directly to the agent:
25
+ At a high level, the skill invokes `pr-shepherd poll <PR>`, which provides actionable feedback directly to the agent:
26
26
 
27
- ```
28
- > npx pr-shepherd 123
27
+ ```text
28
+ > pr-shepherd poll 123
29
29
 
30
30
  # PR #123 [FIX_CODE]
31
31
 
@@ -44,12 +44,12 @@ At a high level, the skill invokes `pr-shepherd <PR>` through the selected packa
44
44
  ## Failing checks
45
45
 
46
46
  - `24697658766` — `CI › lint / typecheck / test (22.x)`
47
- > npx oxfmt
47
+ > oxfmt
48
48
 
49
49
  ## Post-fix push
50
50
 
51
51
  - base: `main`
52
- - resolve: `npx pr-shepherd resolve 123 --resolve-thread-ids PRRT_kwDOSGizTs58XB1L --minimize-comment-ids IC_kwDOSGizTs7_ajT8,IC_kwDOSGizTs7_ajT9 --dismiss-review-ids PRR_kwDOSGizTs58XB1R --message "$DISMISS_MESSAGE" --require-sha "$HEAD_SHA"`
52
+ - resolve: `pr-shepherd resolve 123 --resolve-thread-ids PRRT_kwDOSGizTs58XB1L --minimize-comment-ids IC_kwDOSGizTs7_ajT8,IC_kwDOSGizTs7_ajT9 --dismiss-review-ids PRR_kwDOSGizTs58XB1R --message "$DISMISS_MESSAGE" --require-sha "$HEAD_SHA"`
53
53
 
54
54
  ## Instructions
55
55
 
@@ -61,13 +61,13 @@ _(schematic — actual steps depend on PR state)_
61
61
  4. Rebase and push: `git fetch origin && git rebase origin/main && git push --force-with-lease` — capture `HEAD_SHA=$(git rev-parse HEAD)`.
62
62
  5. Run the `resolve:` command above, substituting `"$HEAD_SHA"`.
63
63
  6. Add or update a `## Shepherd Journal` section in the PR description for any large decisions made, appending under the existing heading if it already exists.
64
- 7. CI needs time to run on the new push. Schedule one session-only follow-up task to run `npx pr-shepherd 123` to recheck once after a fresh delay between 30 seconds and 4 minutes, then end this turn. Do not sleep or rerun inline.
64
+ 7. CI needs time to run on the new push. Run `pr-shepherd poll 123` again to continue until Shepherd returns `[CANCEL]` or `[ESCALATE]`.
65
65
  ```
66
66
 
67
67
  On every iteration, a command is returned to instruct the agent exactly what to do. No guessing, no thinking, as few agentic turns as possible:
68
68
 
69
- ```
70
- npx pr-shepherd resolve 123 --resolve-thread-ids PRRT_kwDOSGizTs58XB1L --minimize-comment-ids IC_kwDOSGizTs7_ajT8,IC_kwDOSGizTs7_ajT9 --dismiss-review-ids PRR_kwDOSGizTs58XB1R --message "$DISMISS_MESSAGE" --require-sha "$HEAD_SHA"
69
+ ```text
70
+ pr-shepherd resolve 123 --resolve-thread-ids PRRT_kwDOSGizTs58XB1L --minimize-comment-ids IC_kwDOSGizTs7_ajT8,IC_kwDOSGizTs7_ajT9 --dismiss-review-ids PRR_kwDOSGizTs58XB1R --message "$DISMISS_MESSAGE" --require-sha "$HEAD_SHA"
71
71
  ```
72
72
 
73
73
  ## Workflow
@@ -97,7 +97,7 @@ Some other workflow improvements:
97
97
 
98
98
  Recommendations:
99
99
 
100
- - Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling the reusable command until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
100
+ - Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling `pr-shepherd poll` until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
101
101
  - Instruct your agents to write comments in a single review (comment, changes requested, or approved). This allows the review's comments/threads to be minimized or resolved together, keeping your pull request history clean. If you write inline comments outside of a review, each comment would still show up in the pull request history and take up space.
102
102
  - Avoid sticky comments as they will continue to be hidden. Instead, just make a new comment, especially on reviews. If you really want sticky comments, instruct your agent to unhide/unminimize them when updating them.
103
103
  - Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
@@ -114,7 +114,7 @@ Recommendations:
114
114
 
115
115
  ### Iterate a PR to completion
116
116
 
117
- One-tick dispatcher — checks CI and review comments, fixes issues, and marks the PR ready for review when clean. Each non-terminal tick emits an `## Instructions` section telling the agent how to run the next tick; the loop continues until `[CANCEL]` or `[ESCALATE]`.
117
+ Poll dispatcher — checks CI and review comments, fixes issues, and marks the PR ready for review when clean. Each non-terminal tick emits an `## Instructions` section; the skill follows it, then invokes `pr-shepherd poll` again until `[CANCEL]` or `[ESCALATE]`.
118
118
 
119
119
  Claude Code (via `pr-shepherd` skill):
120
120
 
@@ -127,9 +127,10 @@ Claude Code (via `pr-shepherd` skill):
127
127
  Codex or direct CLI:
128
128
 
129
129
  ```sh
130
- npx pr-shepherd 42
131
- npx pr-shepherd 42 --ready-delay 15m
132
- npx pr-shepherd iterate 42 # legacy-compatible spelling
130
+ pr-shepherd 42
131
+ pr-shepherd 42 --ready-delay 15m
132
+ pr-shepherd poll 42
133
+ pr-shepherd iterate 42 # legacy-compatible spelling
133
134
  ```
134
135
 
135
136
  ## Iterate decision loop
@@ -141,25 +142,16 @@ On each tick: fetch PR state in one GraphQL batch → classify CI, comments, and
141
142
  `pr-shepherd` accumulates state under `$PR_SHEPHERD_STATE_DIR` (seen markers, fix-attempt counters, stall fingerprints, etc.). To reset it:
142
143
 
143
144
  ```sh
144
- npx pr-shepherd clean current # remove state for the current branch's PR
145
- npx pr-shepherd clean repo # remove all state for this repo
146
- npx pr-shepherd clean all # remove all pr-shepherd state
145
+ pr-shepherd clean current # remove state for the current branch's PR
146
+ pr-shepherd clean repo # remove all state for this repo
147
+ pr-shepherd clean all # remove all pr-shepherd state
147
148
  ```
148
149
 
149
150
  Add `--dry-run` to preview what would be removed. See [docs/cli-usage.md](docs/cli-usage.md) for the full `clean` reference.
150
151
 
151
152
  ## Install
152
153
 
153
- > **Note:** Skill and plugin install methods add the skill definitions only — they do not install the `pr-shepherd` CLI. The skills invoke `pr-shepherd` through the repo package runner, so you also need the CLI available. If you're using `pr-shepherd` as development tooling for your repo, install it as a dev dependency so the selected runner resolves it without prompting:
154
- >
155
- > ```bash
156
- > pnpm add -D pr-shepherd # pnpm repos
157
- > yarn add -D pr-shepherd # yarn repos
158
- > bun add -d pr-shepherd # bun repos
159
- > npm install --save-dev pr-shepherd
160
- > ```
161
- >
162
- > A plain `npm install pr-shepherd` adds it to regular dependencies instead; use that only if you specifically want it under `dependencies`. Or install globally: `npm install -g pr-shepherd`.
154
+ > **Note:** Skill and plugin install methods add the skill definitions only — they do not install the `pr-shepherd` CLI. Ensure `pr-shepherd` is available on `PATH` wherever the skill runs.
163
155
 
164
156
  ### Claude Code
165
157
 
@@ -170,15 +162,7 @@ claude /plugin marketplace add jonathanong/pr-shepherd
170
162
  claude /plugin install pr-shepherd
171
163
  ```
172
164
 
173
- This repo ships two `marketplace.json` files that serve different Claude install flows: the root `marketplace.json` resolves the plugin from the npm registry (used by the `claude /plugin marketplace add` command above); `.claude-plugin/marketplace.json` is the owner-level registry manifest that resolves the plugin from the local plugin directory (used when Claude Code installs from a local or git-based source). Both files are needed to support these two install paths.
174
-
175
- Alternatively, install the Claude skills individually via `npx skills`:
176
-
177
- ```bash
178
- npx skills add jonathanong/pr-shepherd
179
- ```
180
-
181
- Installs the `pr-shepherd` skill into your agent's skill directory (`.claude/skills/` for project scope, `~/.claude/skills/` with `-g` for global scope). Powered by [skills.sh](https://skills.sh).
165
+ This repo ships two `marketplace.json` files that serve different Claude install flows: the root `marketplace.json` resolves the published plugin (used by the `claude /plugin marketplace add` command above); `.claude-plugin/marketplace.json` is the owner-level registry manifest that resolves the plugin from the local plugin directory (used when Claude Code installs from a local or git-based source). Both files are needed to support these two install paths.
182
166
 
183
167
  ### Codex
184
168
 
@@ -205,33 +189,16 @@ codex plugin marketplace add ~/.codex/plugin-sources/pr-shepherd
205
189
 
206
190
  After adding the marketplace, open the Codex plugin directory, choose the `jonathanong` marketplace, and install/enable `pr-shepherd`. The marketplace root must contain `.agents/plugins/marketplace.json` and `plugins/pr-shepherd/`.
207
191
 
208
- Install the CLI where Codex will run it:
192
+ Install the CLI where Codex will run it so `pr-shepherd` is available on `PATH`. The plugin only installs the skill; it does not install the CLI into target repositories.
209
193
 
210
- ```bash
211
- pnpm add -D pr-shepherd # pnpm repos
212
- yarn add -D pr-shepherd # yarn repos
213
- bun add -d pr-shepherd # bun repos
214
- npm install --save-dev pr-shepherd
215
- ```
216
-
217
- The plugin only installs the skill; it does not install the CLI into target repositories. To install the CLI globally instead, use `npm install -g pr-shepherd`.
218
-
219
- Iterate a PR from Codex with the target repository's package runner:
194
+ Iterate a PR from Codex:
220
195
 
221
196
  ```bash
222
- <runner> pr-shepherd iterate 42
197
+ pr-shepherd iterate 42
223
198
  ```
224
199
 
225
- For example, a repo like `~/filaments` that declares `packageManager: "pnpm@..."` and has `pnpm-lock.yaml` should use `pnpm exec pr-shepherd iterate 42`. For Bun repos (with `bun.lock` or `bun.lockb`), use `bunx pr-shepherd iterate 42`. For npm repos, use `npx pr-shepherd iterate 42`.
226
-
227
200
  Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows.
228
201
 
229
- ### As a global CLI
230
-
231
- ```bash
232
- npm install -g pr-shepherd
233
- ```
234
-
235
202
  ## Configuration
236
203
 
237
204
  Create a `.pr-shepherdrc.yml` in your project root (or any parent directory) to override defaults. The loader walks up from `cwd` to `$HOME` (if `$HOME` is on that path) or the filesystem root; the first match wins.
@@ -5,7 +5,6 @@ import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag
5
5
  import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
6
6
  export function formatFixCodeResult(header, result, opts) {
7
7
  const readyDelaySuffix = opts?.readyDelaySuffix;
8
- const runner = opts?.runner;
9
8
  const sections = [header];
10
9
  if (result.fix.threads.length > 0) {
11
10
  sections.push("## Review threads");
@@ -31,7 +30,8 @@ export function formatFixCodeResult(header, result, opts) {
31
30
  sections.push("## Actionable comments");
32
31
  for (const c of result.fix.actionableComments) {
33
32
  const heading = c.url ? `[commentId=${c.id}](${c.url})` : `\`commentId=${c.id}\``;
34
- sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType)})`);
33
+ const editedMarker = c.edited ? " [edited since first look]" : "";
34
+ sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType)})${editedMarker}`);
35
35
  sections.push(blockquote(c.body));
36
36
  }
37
37
  }
@@ -109,7 +109,7 @@ export function formatFixCodeResult(header, result, opts) {
109
109
  }
110
110
  sections.push(postFixLines.join("\n"));
111
111
  sections.push("## Instructions");
112
- sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix, runner)));
112
+ sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix)));
113
113
  return joinSections(sections);
114
114
  }
115
115
  function blockquote(body) {
@@ -2,7 +2,7 @@ export { formatIterateResult } from "./iterate-formatter.mjs";
2
2
  export { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
3
3
  export { formatCleanResult } from "./clean-formatter.mjs";
4
4
  import { safeFence } from "./fence.mjs";
5
- import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderThreadResolutionStatusTag, buildFirstLookBullets, } from "./list-formatters.mjs";
5
+ import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderThreadResolutionStatusTag, buildFirstLookBullets, renderEditedCommentTag, } from "./list-formatters.mjs";
6
6
  import { joinSections } from "../util/markdown.mjs";
7
7
  export function formatFetchResult(result) {
8
8
  const activeTotal = result.actionableThreads.length +
@@ -18,8 +18,7 @@ export function formatFetchResult(result) {
18
18
  if (firstLookTotal > 0)
19
19
  headingParts.push(`${firstLookTotal} first-look`);
20
20
  const headingSuffix = headingParts.length > 0 ? headingParts.join(", ") : "0 actionable";
21
- const sections = [];
22
- sections.push(`# PR #${result.prNumber} — Resolve fetch (${headingSuffix})`);
21
+ const sections = [`# PR #${result.prNumber} — Resolve fetch (${headingSuffix})`];
23
22
  if (result.actionableThreads.length > 0) {
24
23
  sections.push(`## Actionable Review Threads (${result.actionableThreads.length})` +
25
24
  (result.commitSuggestionsEnabled ? " [commit-suggestions: enabled]" : ""));
@@ -35,7 +34,9 @@ export function formatFetchResult(result) {
35
34
  }
36
35
  if (result.actionableComments.length > 0) {
37
36
  sections.push(`## Actionable PR Comments (${result.actionableComments.length})`);
38
- sections.push(result.actionableComments.map((c) => renderCommentBullet(c)).join("\n"));
37
+ sections.push(result.actionableComments
38
+ .map((c) => renderCommentBullet(c, { statusTag: renderEditedCommentTag(c) }))
39
+ .join("\n"));
39
40
  }
40
41
  if (result.changesRequestedReviews.length > 0) {
41
42
  sections.push(`## Pending CHANGES_REQUESTED reviews (${result.changesRequestedReviews.length})`);
@@ -106,6 +106,5 @@ export async function handleIterate(args) {
106
106
  format: globalOpts.format,
107
107
  verbose: globalOpts.verbose ?? false,
108
108
  readyDelaySuffix: flags.readyDelaySuffix ?? undefined,
109
- runner: cfg.cli?.runner,
110
109
  });
111
110
  }
@@ -0,0 +1,154 @@
1
+ export const COMMAND_USAGE = {
2
+ resolve: `pr-shepherd resolve
3
+
4
+ Fetch actionable review items or apply GitHub review-state mutations after fixes.
5
+
6
+ Usage:
7
+ pr-shepherd resolve [PR] [--fetch] [--format text|json]
8
+ pr-shepherd resolve [PR] --resolve-thread-ids A,B [--minimize-comment-ids X,Y]
9
+ [--dismiss-review-ids Q] [--message MSG]
10
+ [--require-sha SHA] [--format text|json]
11
+
12
+ Modes:
13
+ fetch Default when no mutation IDs are provided. Fetches review threads,
14
+ comments, first-look items, and instructions.
15
+ mutate Runs when any mutation ID flag is present. Resolves threads,
16
+ minimizes comments/review summaries, and dismisses reviews.
17
+
18
+ Flags:
19
+ --fetch Force fetch mode.
20
+ --resolve-thread-ids <ids> Comma-separated review thread IDs to resolve.
21
+ --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
22
+ --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
23
+ --message <text> Dismiss message. Required with --dismiss-review-ids.
24
+ --require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
25
+ --format text|json Output format. Default: text.
26
+ --help, -h Print this help and exit before GitHub I/O.
27
+
28
+ PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
29
+ Exit code: 0 on success; 1 on validation, lookup, or mutation failure.`,
30
+ "commit-suggestion": `pr-shepherd commit-suggestion
31
+
32
+ Build a patch and commit instructions for one GitHub review thread containing a suggestion block.
33
+ The command does not edit files or mutate git history.
34
+
35
+ Usage:
36
+ pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG
37
+ [--description DESC] [--format text|json]
38
+
39
+ Flags:
40
+ --thread-id <id> Review thread ID containing exactly one suggestion to apply. Required.
41
+ --message <text> Suggested commit subject. Required and must be non-empty.
42
+ --description <text> Optional longer commit body.
43
+ --format text|json Output format. Default: text.
44
+ --help, -h Print this help and exit before GitHub, git, config, or log I/O.
45
+
46
+ Preconditions:
47
+ The current branch must match the PR head ref, and local HEAD must match the PR head SHA.
48
+
49
+ Exit codes:
50
+ 0 suggestion patch and instructions produced
51
+ 1 validation, lookup, precondition, or suggestion parsing failure`,
52
+ iterate: `pr-shepherd iterate
53
+
54
+ Run one iterate tick for a pull request. This is an alias for 'pr-shepherd [PR]'.
55
+ The output contains one action and an action-specific ## Instructions section.
56
+
57
+ Usage:
58
+ pr-shepherd [PR] [iterate-flags]
59
+ pr-shepherd iterate [PR] [iterate-flags]
60
+
61
+ Iterate flags:
62
+ --ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
63
+ --stall-timeout <duration> Escalate repeated unchanged failures after this duration.
64
+ --no-auto-mark-ready Do not convert draft PRs to ready for review.
65
+ --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
66
+ --format text|json Output Markdown text or JSON. Default: text.
67
+ --verbose Include verbose iterate fields.
68
+ --help, -h Print this help and exit before GitHub, git, config, or log I/O.
69
+
70
+ Actions:
71
+ WAIT No immediate code action; recheck later or use pr-shepherd poll.
72
+ MARK_READY Draft PR was marked ready for review.
73
+ FIX_CODE Apply fixes, commit, push, and run the printed resolve command.
74
+ CANCEL Terminal state: merged/closed or ready-delay elapsed.
75
+ ESCALATE Terminal state requiring human direction.
76
+
77
+ Exit codes:
78
+ 0 WAIT or MARK_READY
79
+ 1 FIX_CODE, or a command/validation error
80
+ 2 CANCEL
81
+ 3 ESCALATE`,
82
+ poll: `pr-shepherd poll
83
+
84
+ Run iterate repeatedly while the action is WAIT. Print only the final tick to stdout.
85
+ Poll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout
86
+ returns the last WAIT result.
87
+
88
+ Usage:
89
+ pr-shepherd poll [PR] [poll-flags] [iterate-flags]
90
+
91
+ Poll flags:
92
+ --interval <duration> Sleep between WAIT ticks. Default: 30s.
93
+ --timeout <duration> Maximum wall-clock wait. Default: 5m.
94
+
95
+ Forwarded iterate flags:
96
+ --ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
97
+ --stall-timeout <duration> Escalate repeated unchanged failures after this duration.
98
+ --no-auto-mark-ready Do not convert draft PRs to ready for review.
99
+ --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
100
+ --format text|json Output Markdown text or JSON. Default: text.
101
+ --verbose Include verbose iterate fields and always print poll progress.
102
+ --help, -h Print this help and exit before GitHub, git, config, or log I/O.
103
+
104
+ Durations accept seconds, minutes, or hours: 30s, 2m, 1h, or bare seconds.
105
+ WAIT progress is written to stderr when stderr is a TTY or --verbose is set.
106
+
107
+ Exit codes:
108
+ 0 WAIT timeout or MARK_READY
109
+ 1 FIX_CODE, or a command/validation error
110
+ 2 CANCEL
111
+ 3 ESCALATE`,
112
+ clean: `pr-shepherd clean
113
+
114
+ Remove pr-shepherd state files from PR_SHEPHERD_STATE_DIR.
115
+
116
+ Usage:
117
+ pr-shepherd clean pr [number] [--dry-run] [--format text|json]
118
+ pr-shepherd clean branch [name] [--dry-run] [--format text|json]
119
+ pr-shepherd clean current [--dry-run] [--format text|json]
120
+ pr-shepherd clean repo [--dry-run] [--format text|json]
121
+ pr-shepherd clean all [--dry-run] [--format text|json]
122
+
123
+ Variants:
124
+ pr [number] Remove state for one PR. Defaults to current branch PR.
125
+ branch [name] Resolve a branch to its open PR, then remove that PR's state.
126
+ Defaults to current branch.
127
+ current Alias for branch against the current branch.
128
+ repo Remove all state for the current repository, including worktree logs.
129
+ all Remove all pr-shepherd state.
130
+
131
+ Flags:
132
+ --dry-run Preview paths without removing them.
133
+ --format text|json Output format. Default: text.
134
+ --help, -h Print this help and exit before any cleanup.
135
+
136
+ Exit code: 0 on success; 1 on validation or cleanup failure.`,
137
+ "log-file": `pr-shepherd log-file
138
+
139
+ Print the per-worktree append-only debug log path for the current repository.
140
+ The log is created by the first non-help pr-shepherd command that initializes logging.
141
+
142
+ Usage:
143
+ pr-shepherd log-file [--format text|json]
144
+
145
+ Flags:
146
+ --format text|json Print a raw path or {"path": "..."} JSON. Default: text.
147
+ --help, -h Print this help and exit before logging setup.
148
+
149
+ Environment:
150
+ PR_SHEPHERD_LOG_DISABLED=1 disables logging.
151
+ PR_SHEPHERD_STATE_DIR overrides the base state directory.
152
+
153
+ Exit code: 0 on success; 1 if repository identity cannot be resolved.`,
154
+ };
@@ -0,0 +1,59 @@
1
+ export const TOP_USAGE = `pr-shepherd
2
+
3
+ Autonomous PR CI monitor and review-comment resolver for agentic coding tools.
4
+
5
+ Usage:
6
+ pr-shepherd --version | -v
7
+ pr-shepherd --help | -h
8
+ pr-shepherd [PR] [flags]
9
+ pr-shepherd iterate [PR] [flags]
10
+ pr-shepherd poll [PR] [poll-flags] [iterate-flags]
11
+ pr-shepherd resolve [PR] [resolve-flags]
12
+ pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [flags]
13
+ pr-shepherd clean <pr|branch|current|repo|all> [value] [flags]
14
+ pr-shepherd log-file [--format text|json]
15
+
16
+ Commands:
17
+ [PR] Run one iterate tick. This is the default command.
18
+ iterate Alias for the default one-tick iterate command.
19
+ poll Re-run iterate while the action is WAIT, then print the final tick.
20
+ resolve Fetch actionable review items, or resolve/minimize/dismiss IDs.
21
+ commit-suggestion Convert one GitHub suggestion thread into a patch and commit instructions.
22
+ clean Remove pr-shepherd state files.
23
+ log-file Print the per-worktree debug log path.
24
+
25
+ PR argument:
26
+ PR may be a number such as 42 or a GitHub pull request URL.
27
+ When omitted, pr-shepherd infers the current branch's pull request.
28
+
29
+ Common flags:
30
+ --format text|json Output Markdown text or JSON. Default: text.
31
+ --verbose Include verbose iterate fields and poll progress.
32
+ --help, -h Print help and exit before any GitHub, git, config, or log I/O.
33
+
34
+ Iterate flags:
35
+ --ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
36
+ --stall-timeout <duration> Escalate repeated unchanged failures after this duration.
37
+ --no-auto-mark-ready Do not convert draft PRs to ready for review.
38
+ --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
39
+
40
+ Poll flags:
41
+ --interval <duration> Sleep between WAIT ticks. Default: 30s.
42
+ --timeout <duration> Maximum poll wall-clock wait. Default: 5m.
43
+
44
+ Clean variants:
45
+ pr [number] Remove state for one PR. Defaults to current branch PR.
46
+ branch [name] Remove state for a branch's PR. Defaults to current branch.
47
+ current Alias for branch against the current branch.
48
+ repo Remove all state for the current repository.
49
+ all Remove all pr-shepherd state.
50
+
51
+ Exit codes for iterate and poll:
52
+ 0 WAIT or MARK_READY
53
+ 1 FIX_CODE, or a command/validation error
54
+ 2 CANCEL
55
+ 3 ESCALATE
56
+
57
+ Durations accept seconds, minutes, or hours: 30s, 2m, 1h, or bare seconds.
58
+
59
+ Run 'pr-shepherd <command> --help' for command-specific details.`;
package/bin/cli/help.mjs CHANGED
@@ -1,35 +1,9 @@
1
1
  import { hasFlag } from "./args.mjs";
2
+ import { COMMAND_USAGE } from "./help-command-pages.mjs";
3
+ import { TOP_USAGE } from "./help-top-page.mjs";
2
4
  export const USAGE = {
3
- top: "Usage:\n" +
4
- " pr-shepherd --version | -v\n" +
5
- " pr-shepherd [PR] [--format text|json] [--ready-delay Nm]\n" +
6
- " [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
7
- " [--no-auto-cancel-actionable]\n" +
8
- " pr-shepherd resolve [PR] [--fetch] [--resolve-thread-ids A,B] [--minimize-comment-ids X,Y]\n" +
9
- " [--dismiss-review-ids Q] [--message MSG] [--require-sha SHA]\n" +
10
- " pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [--description DESC]\n" +
11
- " [--format text|json]\n" +
12
- " pr-shepherd iterate [PR] [--format text|json] [--ready-delay Nm]\n" +
13
- " [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
14
- " [--no-auto-cancel-actionable]\n" +
15
- " pr-shepherd poll [PR] [--interval 30s] [--timeout 5m] [--format text|json] [--ready-delay Nm]\n" +
16
- " [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
17
- " [--no-auto-cancel-actionable]\n" +
18
- " pr-shepherd clean <pr|branch|current|repo|all> [value] [--dry-run] [--format text|json]\n" +
19
- " pr-shepherd log-file [--format text|json]",
20
- resolve: "Usage: pr-shepherd resolve [PR] [--fetch] [--resolve-thread-ids A,B]\n" +
21
- " [--minimize-comment-ids X,Y] [--dismiss-review-ids Q]\n" +
22
- " [--message MSG] [--require-sha SHA]",
23
- "commit-suggestion": "Usage: pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG\n" +
24
- " [--description DESC] [--format text|json]",
25
- iterate: "Usage: pr-shepherd iterate [PR] [--format text|json] [--ready-delay Nm]\n" +
26
- " [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
27
- " [--no-auto-cancel-actionable]",
28
- poll: "Usage: pr-shepherd poll [PR] [--interval 30s] [--timeout 5m] [--format text|json]\n" +
29
- " [--ready-delay Nm] [--stall-timeout <duration>]\n" +
30
- " [--no-auto-mark-ready] [--no-auto-cancel-actionable]",
31
- clean: "Usage: pr-shepherd clean <pr|branch|current|repo|all> [value] [--dry-run] [--format text|json]",
32
- "log-file": "Usage: pr-shepherd log-file [--format text|json]",
5
+ top: TOP_USAGE,
6
+ ...COMMAND_USAGE,
33
7
  };
34
8
  /** Prints usage for `key` to stdout and returns true if `--help` or `-h` is in args. */
35
9
  export function maybePrintHelp(args, key) {
@@ -3,7 +3,6 @@ import { formatIterateResult, projectIterateLean, projectIterateVerbose } from "
3
3
  export function emitIterateResult(result, opts) {
4
4
  const projectionOpts = {
5
5
  readyDelaySuffix: opts.readyDelaySuffix,
6
- runner: opts.runner,
7
6
  };
8
7
  if (opts.format === "json") {
9
8
  const output = opts.verbose
@@ -18,7 +18,6 @@ import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, }
18
18
  export function formatIterateResult(result, opts) {
19
19
  const verbose = opts?.verbose ?? false;
20
20
  const readyDelaySuffix = opts?.readyDelaySuffix;
21
- const runner = opts?.runner;
22
21
  const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
23
22
  const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
24
23
  ? ` · **reviewDecision** \`${result.reviewDecision}\``
@@ -87,13 +86,13 @@ export function formatIterateResult(result, opts) {
87
86
  return joinSections([
88
87
  header,
89
88
  adaptIterateLog(result.log),
90
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
89
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
91
90
  ]);
92
91
  case "mark_ready":
93
92
  return joinSections([
94
93
  header,
95
94
  adaptIterateLog(result.log),
96
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
95
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
97
96
  ]);
98
97
  case "cancel": {
99
98
  const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
@@ -102,19 +101,18 @@ export function formatIterateResult(result, opts) {
102
101
  return joinSections([
103
102
  cancelHeaderLines.join("\n"),
104
103
  adaptIterateLog(result.log),
105
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
104
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
106
105
  ]);
107
106
  }
108
107
  case "escalate":
109
108
  return joinSections([
110
109
  header,
111
110
  result.escalate.humanMessage,
112
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
111
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
113
112
  ]);
114
113
  case "fix_code":
115
114
  return formatFixCodeResult(header, result, {
116
115
  readyDelaySuffix,
117
- runner,
118
116
  });
119
117
  }
120
118
  }
@@ -3,8 +3,8 @@ import { buildPrShepherdCommand } from "./runner.mjs";
3
3
  function buildRecheckInstruction(rerunCommand, purpose) {
4
4
  return `Recheck: rerun \`${rerunCommand}\` to ${purpose} once after a fresh 30s–4m delay.`;
5
5
  }
6
- export function buildSimpleIterateInstructions(result, readyDelaySuffix, runner) {
7
- const rerunCommand = buildIterateCommand(result.pr, readyDelaySuffix, runner);
6
+ export function buildSimpleIterateInstructions(result, readyDelaySuffix) {
7
+ const rerunCommand = buildIterateCommand(result.pr, readyDelaySuffix);
8
8
  switch (result.action) {
9
9
  case "wait":
10
10
  return [buildRecheckInstruction(rerunCommand, "continue the active goal")];
@@ -20,8 +20,8 @@ export function buildSimpleIterateInstructions(result, readyDelaySuffix, runner)
20
20
  ];
21
21
  }
22
22
  }
23
- export function adaptFixCodeInstructions(instructions, pr, readyDelaySuffix, runner) {
24
- const rerunCommand = buildIterateCommand(pr, readyDelaySuffix, runner);
23
+ export function adaptFixCodeInstructions(instructions, pr, readyDelaySuffix) {
24
+ const rerunCommand = buildIterateCommand(pr, readyDelaySuffix);
25
25
  return instructions.map((instruction) => {
26
26
  if (instruction === FIX_INSTRUCTION_STOP) {
27
27
  return `${instruction} ${buildRecheckInstruction(rerunCommand, "recheck")}`;
@@ -32,11 +32,9 @@ export function adaptFixCodeInstructions(instructions, pr, readyDelaySuffix, run
32
32
  export function adaptIterateLog(log) {
33
33
  return log.replace(/\s+—\s+\d+s until auto-cancel/g, "");
34
34
  }
35
- export function buildIterateCommand(pr, readyDelaySuffix, runner) {
35
+ export function buildIterateCommand(pr, readyDelaySuffix) {
36
36
  const suffix = readyDelaySuffix?.trim();
37
- return buildPrShepherdCommand([String(pr), ...(suffix ? ["--ready-delay", suffix] : [])], {
38
- runner,
39
- }).text;
37
+ return buildPrShepherdCommand([String(pr), ...(suffix ? ["--ready-delay", suffix] : [])]).text;
40
38
  }
41
39
  export function numberInstructions(instructions) {
42
40
  return instructions.map((instruction, i) => `${i + 1}. ${instruction}`).join("\n");
@@ -6,8 +6,7 @@ import { adaptIterateLog, adaptFixCodeInstructions, buildSimpleIterateInstructio
6
6
  */
7
7
  export function projectIterateLean(result, opts) {
8
8
  const readyDelaySuffix = opts?.readyDelaySuffix;
9
- const runner = opts?.runner;
10
- const simpleInstructions = (r) => buildSimpleIterateInstructions(r, readyDelaySuffix, runner);
9
+ const simpleInstructions = (r) => buildSimpleIterateInstructions(r, readyDelaySuffix);
11
10
  const base = {
12
11
  action: result.action,
13
12
  pr: result.pr,
@@ -94,7 +93,7 @@ export function projectIterateLean(result, opts) {
94
93
  }),
95
94
  resolveCommand: result.fix.resolveCommand,
96
95
  ...(result.fix.instructions.length > 0 && {
97
- instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix, runner),
96
+ instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
98
97
  }),
99
98
  },
100
99
  };
@@ -125,13 +124,12 @@ export function projectIterateLean(result, opts) {
125
124
  }
126
125
  export function projectIterateVerbose(result, opts) {
127
126
  const readyDelaySuffix = opts?.readyDelaySuffix;
128
- const runner = opts?.runner;
129
127
  if (result.action === "fix_code") {
130
128
  return {
131
129
  ...result,
132
130
  fix: {
133
131
  ...result.fix,
134
- instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix, runner),
132
+ instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
135
133
  },
136
134
  };
137
135
  }
@@ -139,6 +137,6 @@ export function projectIterateVerbose(result, opts) {
139
137
  return {
140
138
  ...result,
141
139
  ...log,
142
- instructions: buildSimpleIterateInstructions(result, readyDelaySuffix, runner),
140
+ instructions: buildSimpleIterateInstructions(result, readyDelaySuffix),
143
141
  };
144
142
  }
@@ -39,6 +39,9 @@ export function renderCommentBullet(c, opts = {}) {
39
39
  const statusSuffix = opts.statusTag ? ` ${opts.statusTag}` : "";
40
40
  return `- \`commentId=${c.id}\`${link} (${renderAuthor(c.author, c.authorType)})${statusSuffix}: ${renderBodyPreview(c.body)}`;
41
41
  }
42
+ export function renderEditedCommentTag(c) {
43
+ return c.edited ? "[edited since first look]" : undefined;
44
+ }
42
45
  export function renderReviewBullet(r, opts = {}) {
43
46
  const bodySuffix = opts.includeBody && r.body != null && r.body !== "" ? `: ${renderBodyPreview(r.body)}` : "";
44
47
  return `- \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType)})${bodySuffix}`;
@@ -37,6 +37,5 @@ export async function handlePoll(args) {
37
37
  format: globalOpts.format,
38
38
  verbose: globalOpts.verbose ?? false,
39
39
  readyDelaySuffix: flags.readyDelaySuffix ?? undefined,
40
- runner: cfg.cli?.runner,
41
40
  });
42
41
  }
@@ -1,111 +1,10 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { homedir } from "node:os";
4
- export function buildPrShepherdCommand(args, opts) {
5
- const runner = resolveCliRunner(opts.runner, opts.cwd);
6
- const argv = baseArgvForRunner(runner).concat(args);
1
+ export function buildPrShepherdCommand(args) {
2
+ const argv = ["pr-shepherd", ...args];
7
3
  return { argv, text: renderShellCommand(argv) };
8
4
  }
9
- export function resolveCliRunner(runner, cwd = process.cwd()) {
10
- const configured = parseCliRunner(runner);
11
- return configured === "auto" ? detectPackageRunner(cwd) : configured;
12
- }
13
- const VALID_RUNNERS = ["auto", "npx", "pnpm", "yarn", "bun"];
14
- const VALID_RUNNERS_LIST = VALID_RUNNERS.map((v) => `"${v}"`).join(", ");
15
- export function parseCliRunner(runner) {
16
- if (runner === undefined)
17
- return "auto";
18
- if (typeof runner !== "string") {
19
- throw new Error(`Invalid config: cli.runner must be one of ${VALID_RUNNERS_LIST}, got ${JSON.stringify(runner)}`);
20
- }
21
- const value = runner.trim();
22
- if (VALID_RUNNERS.includes(value))
23
- return value;
24
- throw new Error(`Invalid config: cli.runner must be one of ${VALID_RUNNERS_LIST}, got ${JSON.stringify(runner)}`);
25
- }
26
5
  export function renderShellCommand(argv) {
27
6
  return argv.map(renderShellArg).join(" ");
28
7
  }
29
- function baseArgvForRunner(runner) {
30
- switch (runner) {
31
- case "npx":
32
- return ["npx", "pr-shepherd"];
33
- case "pnpm":
34
- return ["pnpm", "exec", "pr-shepherd"];
35
- case "yarn":
36
- return ["yarn", "run", "pr-shepherd"];
37
- case "bun":
38
- return ["bunx", "pr-shepherd"];
39
- }
40
- }
41
- const runnerCache = new Map();
42
- export function __resetRunnerCache() {
43
- runnerCache.clear();
44
- }
45
- function detectPackageRunner(startDir) {
46
- const cached = runnerCache.get(startDir);
47
- if (cached)
48
- return cached;
49
- const home = homedir();
50
- const repoRoot = findRepoRoot(startDir);
51
- let current = startDir;
52
- while (true) {
53
- const atBoundary = current === repoRoot || current === home || current === dirname(current);
54
- // Read signals for this directory unless it is home without being the repo root.
55
- // This lets dotfiles repos (repoRoot === home) detect their own package manager
56
- // while still preventing home's lockfiles from influencing unrelated projects.
57
- if (!atBoundary || current === repoRoot) {
58
- const packageManager = readPackageManager(current);
59
- if (packageManager?.startsWith("pnpm@"))
60
- return cacheRunner(startDir, "pnpm");
61
- if (packageManager?.startsWith("yarn@"))
62
- return cacheRunner(startDir, "yarn");
63
- if (packageManager?.startsWith("npm@"))
64
- return cacheRunner(startDir, "npx");
65
- if (packageManager?.startsWith("bun@"))
66
- return cacheRunner(startDir, "bun");
67
- if (isFile(join(current, "pnpm-lock.yaml")))
68
- return cacheRunner(startDir, "pnpm");
69
- if (isFile(join(current, "yarn.lock")))
70
- return cacheRunner(startDir, "yarn");
71
- if (isFile(join(current, "bun.lock")))
72
- return cacheRunner(startDir, "bun");
73
- if (isFile(join(current, "bun.lockb")))
74
- return cacheRunner(startDir, "bun");
75
- if (isFile(join(current, "package-lock.json")))
76
- return cacheRunner(startDir, "npx");
77
- }
78
- if (atBoundary)
79
- return cacheRunner(startDir, "npx");
80
- current = dirname(current);
81
- }
82
- }
83
- function cacheRunner(startDir, runner) {
84
- runnerCache.set(startDir, runner);
85
- return runner;
86
- }
87
- function readPackageManager(packageDir) {
88
- try {
89
- const parsed = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
90
- return typeof parsed.packageManager === "string" ? parsed.packageManager.trim() : null;
91
- }
92
- catch {
93
- return null;
94
- }
95
- }
96
- function findRepoRoot(startDir) {
97
- let current = startDir;
98
- while (true) {
99
- if (statSync(join(current, ".git"), { throwIfNoEntry: false }))
100
- return current;
101
- if (current === dirname(current))
102
- return null;
103
- current = dirname(current);
104
- }
105
- }
106
- function isFile(path) {
107
- return statSync(path, { throwIfNoEntry: false })?.isFile() === true;
108
- }
109
8
  function renderShellArg(arg) {
110
9
  if (/^\$[A-Z_][A-Z0-9_]*$/.test(arg))
111
10
  return `"${arg}"`;
@@ -43,7 +43,7 @@ const SUGGESTION_RESULT = {
43
43
  "Apply the patch to `a.ts`: run `git apply` with the diff shown above.",
44
44
  "Stage the file: `git add -- a.ts`",
45
45
  'Commit: `git commit -m "apply fix" -m "Co-authored-by: alice <alice@users.noreply.github.com>"`',
46
- "Resolve the thread on GitHub: `npx pr-shepherd resolve 42 --resolve-thread-ids t1`",
46
+ "Resolve the thread on GitHub: `pr-shepherd resolve 42 --resolve-thread-ids t1`",
47
47
  "Push when ready: `git push` (or `git push --force-with-lease` after rebasing).",
48
48
  ],
49
49
  };
@@ -35,7 +35,7 @@ export function makeIterateResult(action = "wait") {
35
35
  checks: [],
36
36
  changesRequestedReviews: [],
37
37
  resolveCommand: {
38
- argv: ["npx", "pr-shepherd", "resolve", "42"],
38
+ argv: ["pr-shepherd", "resolve", "42"],
39
39
  requiresHeadSha: true,
40
40
  requiresDismissMessage: false,
41
41
  hasMutations: false,
@@ -6,7 +6,7 @@ import { buildPrShepherdCommand } from "../cli/runner.mjs";
6
6
  * (e.g. `"## Review threads"` for iterate, `"## Actionable Review Threads"` for resolve).
7
7
  * @param includeDriftHint - Whether to add the trailing note about drift on failed apply.
8
8
  */
9
- export function buildCommitSuggestionInstruction(prNumber, sectionName, includeDriftHint, runner) {
9
+ export function buildCommitSuggestionInstruction(prNumber, sectionName, includeDriftHint) {
10
10
  const command = buildPrShepherdCommand([
11
11
  "commit-suggestion",
12
12
  String(prNumber),
@@ -15,7 +15,7 @@ export function buildCommitSuggestionInstruction(prNumber, sectionName, includeD
15
15
  "--message",
16
16
  "<one-sentence headline>",
17
17
  "--format=json",
18
- ], { runner }).text;
18
+ ]).text;
19
19
  const driftHint = includeDriftHint
20
20
  ? " If the patch fails to apply (drift since the suggestion was written), fall through to the manual fix step."
21
21
  : " If the patch fails to apply, fall through to the manual-edit step.";
@@ -5,7 +5,6 @@ import { getRepoInfo, getCurrentPrNumber, getCurrentBranch } from "../github/cli
5
5
  import { fetchPrBatch } from "../github/batch.mjs";
6
6
  import { parseSuggestion, isCommittableSuggestion } from "../suggestions/parse.mjs";
7
7
  import { buildUnifiedDiff } from "../suggestions/patch.mjs";
8
- import { loadConfig } from "../config/load.mjs";
9
8
  import { buildPrShepherdCommand } from "../cli/runner.mjs";
10
9
  const execFile = promisify(execFileCb);
11
10
  export async function runCommitSuggestion(opts) {
@@ -93,7 +92,12 @@ export async function runCommitSuggestion(opts) {
93
92
  `-m ${sq(commitMessageArg)}`,
94
93
  ...commitBodyArg.split("\n\n").map((p) => `-m ${sq(p)}`),
95
94
  ].join(" ");
96
- const resolveCommand = buildPrShepherdCommand(["resolve", String(prNumber), "--resolve-thread-ids", opts.threadId], { runner: loadConfig().cli?.runner }).text;
95
+ const resolveCommand = buildPrShepherdCommand([
96
+ "resolve",
97
+ String(prNumber),
98
+ "--resolve-thread-ids",
99
+ opts.threadId,
100
+ ]).text;
97
101
  const postActionInstructions = [
98
102
  `Apply the patch to \`${filePath}\`: run \`git apply\` with the diff shown above, or edit the file directly using the line range (${range}).`,
99
103
  `Stage the file: \`git add -- ${quotedPath}\``,
@@ -107,7 +107,7 @@ function setupHappyPath() {
107
107
  // ---------------------------------------------------------------------------
108
108
  export function registerHooks() {
109
109
  beforeEach(() => {
110
- mockLoadConfig.mockReturnValue({ cli: { runner: "auto" } });
110
+ mockLoadConfig.mockReturnValue({});
111
111
  });
112
112
  }
113
113
  export { FILE_CONTENT, GLOBAL_OPTS, fetchPrBatch, getCurrentBranch, getCurrentPrNumber, makeBatch, makeGitSuccess, makeThread, mockExecFile, mockFetchBatch, mockGetCurrentBranch, mockGetCurrentPrNumber, mockLoadConfig, mockReadFile, readFile, runCommitSuggestion, setupHappyPath, };
@@ -37,8 +37,8 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
37
37
  surfacedApprovals: approvals,
38
38
  };
39
39
  }
40
- export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber, runner) {
41
- const argv = buildPrShepherdCommand(["resolve", String(prNumber)], { runner }).argv;
40
+ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber) {
41
+ const argv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
42
42
  const resolveThreadIds = dedupeIds(threads.map((t) => t.id));
43
43
  const threadIds = dedupeIds([...resolveThreadIds, ...resolutionOnlyThreads.map((t) => t.id)]);
44
44
  if (threadIds.length > 0) {
@@ -7,7 +7,7 @@ import { buildFixInstructions } from "./render.mjs";
7
7
  import { applyStallGuard } from "./stall.mjs";
8
8
  import { tryCancelRun, buildInProgressRunIds } from "./helpers.mjs";
9
9
  export async function handleFixCode(ctx) {
10
- const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, cliRunner, } = ctx;
10
+ const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = ctx;
11
11
  const failingChecks = report.checks.failing;
12
12
  const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
13
13
  const isNewSha = stored?.headSha !== headSha;
@@ -64,7 +64,7 @@ export async function handleFixCode(ctx) {
64
64
  const inProgressRunIds = pushLikely ? buildInProgressRunIds(report, cancelledSet) : [];
65
65
  const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
66
66
  const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
67
- const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber, cliRunner);
67
+ const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber);
68
68
  const overlappingReviewIds = resolveCommand.droppedDismissReviewIds ?? [];
69
69
  if (overlappingReviewIds.length > 0) {
70
70
  process.stderr.write(`pr-shepherd: resolve command overlap: ${overlappingReviewIds.length} ` +
@@ -100,7 +100,7 @@ export async function handleFixCode(ctx) {
100
100
  }
101
101
  const firstLookThreads = report.threads.firstLook;
102
102
  const firstLookComments = report.comments.firstLook;
103
- const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, cliRunner);
103
+ const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads);
104
104
  return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, {
105
105
  ...base,
106
106
  baseBranch: baseLookup.branch,
@@ -122,7 +122,6 @@ export async function runIterate(opts) {
122
122
  firstLookSummaries,
123
123
  editedSummaries,
124
124
  surfacedApprovals,
125
- cliRunner: config.cli?.runner,
126
125
  });
127
126
  }
128
127
  const canMarkReady = report.status === "READY" &&
@@ -15,7 +15,7 @@ export function renderResolveCommand(rc) {
15
15
  }
16
16
  return renderShellCommand(parts);
17
17
  }
18
- export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], runner) {
18
+ export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = []) {
19
19
  const instructions = [];
20
20
  const hasNonConflictHints = threads.length > 0 ||
21
21
  checks.length > 0 ||
@@ -53,7 +53,7 @@ export function buildFixInstructions(threads, actionableComments, checks, change
53
53
  }
54
54
  const hasSuggestions = threads.some((t) => t.suggestion);
55
55
  if (hasSuggestions) {
56
- instructions.push(buildCommitSuggestionInstruction(prNumber, "## Review threads", false, runner));
56
+ instructions.push(buildCommitSuggestionInstruction(prNumber, "## Review threads", false));
57
57
  }
58
58
  if (threads.length > 0 || actionableComments.length > 0) {
59
59
  const suggestionFallback = hasSuggestions
@@ -93,10 +93,11 @@ export function buildFixInstructions(threads, actionableComments, checks, change
93
93
  instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
94
94
  }
95
95
  const editedTotal = editedSummaries.length +
96
+ actionableComments.filter((c) => c.edited).length +
96
97
  firstLookThreads.filter((t) => t.edited).length +
97
98
  firstLookComments.filter((c) => c.edited).length;
98
99
  if (editedTotal > 0) {
99
- instructions.push(`Items under \`## Review summaries (edited since first look)\` and any first-look bullet tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
100
+ instructions.push(`Items marked \`[edited since first look]\`, items under \`## Review summaries (edited since first look)\`, and any first-look bullet tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
100
101
  }
101
102
  if (resolveCommand.hasMutations) {
102
103
  instructions.push(buildShepherdJournalInstruction(prNumber, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEM_HEADINGS));
@@ -6,7 +6,7 @@ import { buildCommitSuggestionInstruction } from "./commit-suggestion-instructio
6
6
  * Steps are conditionally emitted based on what the fetch returned (mirrors
7
7
  * `buildFixInstructions` in `commands/iterate/render.mts`).
8
8
  */
9
- export function buildFetchInstructions(prNumber, result, runner) {
9
+ export function buildFetchInstructions(prNumber, result) {
10
10
  const { actionableThreads, resolutionOnlyThreads, firstLookThreads, actionableComments, firstLookComments, changesRequestedReviews, reviewSummaries, commitSuggestionsEnabled, } = result;
11
11
  const firstLookTotal = firstLookThreads.length + firstLookComments.length;
12
12
  const total = actionableThreads.length +
@@ -27,13 +27,14 @@ export function buildFetchInstructions(prNumber, result, runner) {
27
27
  if (firstLookTotal > 0) {
28
28
  instructions.push(`Items in \`## First-look items\` are shown so you can acknowledge their current status before acting. If a first-look thread also appears under \`## Review threads to resolve\`, include its ID in \`--resolve-thread-ids\`; otherwise do not pass first-look-only IDs to mutation flags.`);
29
29
  }
30
- const editedTotal = firstLookThreads.filter((t) => t.edited).length +
30
+ const editedTotal = actionableComments.filter((c) => c.edited).length +
31
+ firstLookThreads.filter((t) => t.edited).length +
31
32
  firstLookComments.filter((c) => c.edited).length;
32
33
  if (editedTotal > 0) {
33
- instructions.push(`First-look bullets tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
34
+ instructions.push(`Actionable comments marked \`[edited since first look]\` and first-look bullets tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
34
35
  }
35
36
  if (hasSuggestions) {
36
- instructions.push(buildCommitSuggestionInstruction(prNumber, "## Actionable Review Threads", true, runner));
37
+ instructions.push(buildCommitSuggestionInstruction(prNumber, "## Actionable Review Threads", true));
37
38
  }
38
39
  if (hasCodeItems) {
39
40
  instructions.push(`Read and edit each file referenced under \`## Actionable Review Threads\`, \`## Actionable PR Comments\`, and \`## Pending CHANGES_REQUESTED reviews\` above. Reclassify each fixed item as Fixed. If an item is too complex to address, leave it as Actionable for the final report.`);
@@ -50,9 +51,7 @@ export function buildFetchInstructions(prNumber, result, runner) {
50
51
  : reviewSummaries.length > 0
51
52
  ? ` Review-summary IDs (\`PRR_…\` from \`## Review summaries\`) go into \`--minimize-comment-ids\`.`
52
53
  : "";
53
- const resolveCommand = `${buildPrShepherdCommand(["resolve", String(prNumber)], {
54
- runner,
55
- }).text} [--resolve-thread-ids <ids>] [--minimize-comment-ids <ids>] [--dismiss-review-ids <ids> --message "<reason>"]`;
54
+ const resolveCommand = `${buildPrShepherdCommand(["resolve", String(prNumber)]).text} [--resolve-thread-ids <ids>] [--minimize-comment-ids <ids>] [--dismiss-review-ids <ids> --message "<reason>"]`;
56
55
  instructions.push(`Run \`${resolveCommand}\` with only the non-empty flag subsets. Skip the command entirely if all three ID lists are empty.${requireShaHint}${dismissNote}`);
57
56
  instructions.push(buildShepherdJournalInstruction(prNumber, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS));
58
57
  instructions.push(`Report: echo the CLI's mutation output, then one line per Acknowledged item: \`Acknowledged <id> (@<author>): <reason>\`. If any fetched item was neither resolved nor acknowledged, stop and escalate: "<N> item(s) fetched but not acted on or acknowledged — need human direction before closing".`);
@@ -129,5 +129,5 @@ export async function runResolveFetch(opts) {
129
129
  reviewSummaries: cfg.resolve.fetchReviewSummaries ? data.reviewSummaries : [],
130
130
  commitSuggestionsEnabled: cfg.actions.commitSuggestions,
131
131
  };
132
- return { ...result, instructions: buildFetchInstructions(prNumber, result, cfg.cli?.runner) };
132
+ return { ...result, instructions: buildFetchInstructions(prNumber, result) };
133
133
  }
@@ -13,8 +13,9 @@ export function classifyVisibleComments(comments, seenMap, minimizeComments) {
13
13
  const cls = classifyItem(c.id, c.body, seenMap);
14
14
  if (cls === "unchanged")
15
15
  continue;
16
- actionable.push(c);
17
- toMarkSeen.push(c);
16
+ const comment = cls === "edited" ? { ...c, edited: true } : c;
17
+ actionable.push(comment);
18
+ toMarkSeen.push(comment);
18
19
  }
19
20
  return { actionable, minimizeIds, toMarkSeen };
20
21
  }
@@ -3,7 +3,6 @@ import { join, dirname } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { parse } from "yaml";
5
5
  import builtins from "../config.json" with { type: "json" };
6
- import { parseCliRunner } from "../cli/runner.mjs";
7
6
  const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
8
7
  const RC_FILENAME = ".pr-shepherdrc.yml";
9
8
  function findRcFile(startDir) {
@@ -61,12 +60,6 @@ export function loadConfig() {
61
60
  const raw = readFileSync(rcPath, "utf8");
62
61
  const parsed = (parse(raw) ?? {});
63
62
  const config = deepMerge(defaults, parsed);
64
- // Validate cli and watch at load time so misconfigurations are caught once
65
- // with the rc file path in context, rather than during instruction rendering.
66
- if (config.cli === null || Array.isArray(config.cli) || typeof config.cli !== "object") {
67
- throw new Error(`Invalid config: cli must be a plain object, got ${JSON.stringify(config.cli)}`);
68
- }
69
- config.cli.runner = parseCliRunner(config.cli.runner);
70
63
  config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
71
64
  configCache.set(cwd, config);
72
65
  return config;
package/bin/config.json CHANGED
@@ -1,7 +1,4 @@
1
1
  {
2
- "cli": {
3
- "runner": "auto"
4
- },
5
2
  "iterate": {
6
3
  "fixAttemptsPerThread": 3,
7
4
  "stallTimeoutMinutes": 30,
@@ -32,6 +32,7 @@ export function toAgentComment(c) {
32
32
  ...(c.authorType !== undefined && { authorType: c.authorType }),
33
33
  body: c.body,
34
34
  url: c.url,
35
+ ...(c.edited === true && { edited: true }),
35
36
  };
36
37
  }
37
38
  export function toAgentCheck(c) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Ong",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -8,7 +8,7 @@ allowed-tools: ["Bash", "Read", "Grep", "Edit", "Write", "Glob", "Skill"]
8
8
 
9
9
  # pr-shepherd
10
10
 
11
- One-tick dispatcher for iterating a PR to completion.
11
+ Poll dispatcher for iterating a PR to completion.
12
12
 
13
13
  ## Arguments: $ARGUMENTS
14
14
 
@@ -20,40 +20,24 @@ One-tick dispatcher for iterating a PR to completion.
20
20
  - Otherwise, infer: `gh pr view --json number --jq .number`
21
21
  - If no PR found, report an error and stop.
22
22
 
23
- 2. **Short-circuit if merged or closed:**
23
+ 2. **Run `pr-shepherd poll`:**
24
24
 
25
25
  ```bash
26
- gh pr view <N> --json state --jq '.state'
26
+ pr-shepherd poll <N>
27
27
  ```
28
28
 
29
- If `MERGED` or `CLOSED`, output: `PR #N is already merged/closed. Nothing to do.` and stop.
29
+ Do not pass `$ARGUMENTS` through as extra flags. If you need to inspect supported options, run `pr-shepherd poll --help`.
30
30
 
31
- 3. **Select the package runner** from the target repository root:
32
- - Prefer `package.json` `packageManager`: `pnpm@...` → `pnpm exec`, `yarn@...` → `yarn run`, `bun@...` → `bunx`, `npm@...` → `npx`.
33
- - If `packageManager` is absent, use lockfiles: `pnpm-lock.yaml` → `pnpm exec`, `yarn.lock` → `yarn run`, `bun.lock` / `bun.lockb` → `bunx`, `package-lock.json` or no signal → `npx`.
31
+ Print the full output. Follow the `## Instructions` section exactly for the current action. When those instructions tell you to stop and recheck with `pr-shepherd <N>` after a delay, use `pr-shepherd poll <N>` as the next invocation instead; do not also run the one-shot command.
34
32
 
35
- 4. **Run one iterate tick:**
33
+ 3. **Persistence:** Continuously call `pr-shepherd poll <N>` until the CLI returns `[CANCEL]` or `[ESCALATE]`, unless the human directs you to stop. Every other action is non-terminal:
34
+ - `[WAIT]`: call `pr-shepherd poll <N>` again.
35
+ - `[MARK_READY]`: call `pr-shepherd poll <N>` again.
36
+ - `[FIX_CODE]`: follow the output's `## Instructions`, then call `pr-shepherd poll <N>` again.
36
37
 
37
- If the package is missing in the target repository, tell the user to install pr-shepherd with the matching dev-dependency command: `pnpm add -D pr-shepherd`, `yarn add -D pr-shepherd`, `bun add -d pr-shepherd`, or `npm install --save-dev pr-shepherd`.
38
+ Treat a nonzero poll exit code as PR state only when the output contains a matching `# PR #N [ACTION]` heading. Exit code `1` can also mean a command or validation failure; if there is no `[FIX_CODE]` heading, surface the error and stop instead of looping.
38
39
 
39
- ```bash
40
- <runner> pr-shepherd <N>
41
- ```
42
-
43
- Print the full output. Follow the `## Instructions` section exactly.
44
-
45
- 5. **Persistence:** Do not stop polling/iterating unless directed by the human or until the CLI returns a terminal state (`[CANCEL]` or `[ESCALATE]`). Every other action — `[WAIT]`, `[MARK_READY]`, `[FIX_CODE]` — is non-terminal and must be followed by another tick via one of the strategies below.
46
-
47
- 6. **Stop conditions (terminal states):**
40
+ 4. **Stop conditions (terminal states):**
48
41
  - Stop when the CLI emits `[CANCEL]` (ready-delay completed, or PR merged/closed).
49
42
  - Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures.
50
43
  - **Do NOT merge the pull request** unless the human has explicitly requested or allowed it.
51
-
52
- 7. **Non-terminal actions** (`[WAIT]`, `[MARK_READY]`, `[FIX_CODE]`) — follow the `## Instructions` in the output. Pick one iteration strategy:
53
- - **Blocking poll** — rerun as `<runner> pr-shepherd poll <N> [--interval <duration>] [--timeout <duration>]` (defaults: interval 30s, timeout 5m). Holds the agent turn until the action is non-WAIT or the timeout fires. Simplest when the agent cannot reliably schedule its own follow-up.
54
- - **Scheduled wakeup + one tick** — schedule a single session-only follow-up task to rerun `<runner> pr-shepherd <N>` after a fresh 30s–4m delay, then end the turn.
55
- - **Inline sleep + rerun** — sleep inline for a fresh 30s–4m delay, then rerun.
56
-
57
- **Never write a custom polling loop** (shell `while`/`until` loops, script files that loop over pr-shepherd output, etc.). Custom loops poll only for terminal states and silently skip `[FIX_CODE]` handling — actionable review threads, failing checks, and resolve commands get missed. Use `pr-shepherd poll` for WAIT-state waiting; it exits on any non-WAIT action so the caller handles `[FIX_CODE]` and other actionable outputs normally.
58
-
59
- Do not combine strategies (e.g., poll AND schedule a wakeup). Remember step 5 — every non-terminal output requires a follow-up tick.