pr-shepherd 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +170 -8
- package/bin/commands/check.mjs +1 -1
- package/bin/commands/resolve.mjs +1 -1
- package/bin/github/batch.mjs +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/package.json +1 -1
- package/plugin/skills/monitor/SKILL.md +18 -7
- package/plugin/skills/resolve/SKILL.md +5 -3
package/README.md
CHANGED
|
@@ -21,6 +21,22 @@ Autonomous PR CI monitor and review-comment resolver for Claude Code.
|
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
|
+
> **Note:** Skill and plugin install methods add the skill definitions only — they do not install the `pr-shepherd` CLI. The skills invoke `npx pr-shepherd`, 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 `npx` resolves it without prompting:
|
|
25
|
+
>
|
|
26
|
+
> ```bash
|
|
27
|
+
> npm install --save-dev pr-shepherd
|
|
28
|
+
> ```
|
|
29
|
+
>
|
|
30
|
+
> 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`.
|
|
31
|
+
|
|
32
|
+
### As individual skills via `npx skills`
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx skills add jonathanong/pr-shepherd
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Installs the three skills (`check`, `monitor`, `resolve`) 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).
|
|
39
|
+
|
|
24
40
|
### As a Claude Code plugin (recommended)
|
|
25
41
|
|
|
26
42
|
```bash
|
|
@@ -149,14 +165,160 @@ pr-shepherd iterate [PR] [--cooldown-seconds N] [--ready-delay Nm] [--last-push-
|
|
|
149
165
|
pr-shepherd status PR1 [PR2 …] # multi-PR table
|
|
150
166
|
```
|
|
151
167
|
|
|
152
|
-
Common flags:
|
|
168
|
+
Common flags (all subcommands):
|
|
169
|
+
|
|
170
|
+
| Flag | Default | Description |
|
|
171
|
+
| --------------------- | ------- | ------------------------------------------------------------------------------ |
|
|
172
|
+
| `--format text\|json` | `text` | Output format |
|
|
173
|
+
| `--no-cache` | false | Bypass the 5-minute file cache |
|
|
174
|
+
| `--cache-ttl N` | `300` | Cache TTL in seconds; `PR_SHEPHERD_CACHE_TTL_SECONDS` env var takes precedence |
|
|
175
|
+
|
|
176
|
+
### pr-shepherd check [PR]
|
|
177
|
+
|
|
178
|
+
Read-only PR status snapshot. Fetches CI results, merge state, and review comments in one GraphQL batch. PR number is inferred from the current branch when omitted.
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
pr-shepherd check # infer PR from current branch
|
|
182
|
+
pr-shepherd check 42
|
|
183
|
+
pr-shepherd check 42 --format=json
|
|
184
|
+
pr-shepherd check 42 --no-cache
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Exit codes: `0` READY · `2` IN_PROGRESS · `3` UNRESOLVED_COMMENTS · `1` all other statuses
|
|
188
|
+
|
|
189
|
+
**Example output:**
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
PR #42 — owner/repo
|
|
193
|
+
Status: UNRESOLVED_COMMENTS
|
|
194
|
+
|
|
195
|
+
Merge Status: CLEAN
|
|
196
|
+
mergeStateStatus: CLEAN
|
|
197
|
+
mergeable: MERGEABLE
|
|
198
|
+
reviewDecision: APPROVED
|
|
199
|
+
isDraft: false
|
|
200
|
+
copilotReviewInProgress:false
|
|
201
|
+
|
|
202
|
+
CI Checks: 3/3 passed
|
|
203
|
+
|
|
204
|
+
Actionable Review Threads (1):
|
|
205
|
+
- threadId=RT_kwDOBxyz123 src/api.ts:47 (@reviewer)
|
|
206
|
+
Please add error handling here
|
|
207
|
+
|
|
208
|
+
Summary: 1 actionable item(s) remaining
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### pr-shepherd resolve [PR]
|
|
212
|
+
|
|
213
|
+
Two modes: **fetch** (default) auto-resolves outdated threads and returns actionable items; **mutate** resolves/minimizes/dismisses specific IDs after you push fixes.
|
|
214
|
+
|
|
215
|
+
**Fetch mode:**
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
pr-shepherd resolve # fetch + auto-resolve outdated threads
|
|
219
|
+
pr-shepherd resolve 42 --fetch --format=json
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Actionable Review Threads (2):
|
|
224
|
+
- threadId=RT_kwDOabc src/api.ts:47 (@reviewer): Please add error handling here
|
|
225
|
+
- threadId=RT_kwDOdef src/utils.ts:12 (@bot): Consider using a const here
|
|
226
|
+
|
|
227
|
+
Summary: 2 actionable item(s)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Mutate mode** (after pushing fixes):
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
pr-shepherd resolve 42 \
|
|
234
|
+
--resolve-thread-ids RT_kwDOabc,RT_kwDOdef \
|
|
235
|
+
--minimize-comment-ids IC_kwDOxyz \
|
|
236
|
+
--dismiss-review-ids PRR_kwDO123 \
|
|
237
|
+
--message "Switched query to parameterized form in src/db.ts" \
|
|
238
|
+
--require-sha $(git rev-parse HEAD)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
Resolved threads (2): RT_kwDOabc, RT_kwDOdef
|
|
243
|
+
Minimized comments (1): IC_kwDOxyz
|
|
244
|
+
Dismissed reviews (1): PRR_kwDO123
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`--require-sha` polls GitHub until the PR head matches the SHA before mutating — ensures reviewers see the fix before threads are closed. Exit code: always `0`. `--message` is required only when `--dismiss-review-ids` is set, and should describe the specific fix — it is shown to the reviewer on GitHub.
|
|
248
|
+
|
|
249
|
+
### pr-shepherd iterate [PR]
|
|
250
|
+
|
|
251
|
+
One monitor tick: classifies current PR state and emits a single action. Used by the cron loop; the monitor skill calls this every 4 minutes and acts on the result. See [docs/iterate-flow.md](docs/iterate-flow.md) for the full decision tree.
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
pr-shepherd iterate 42 --no-cache --format=json \
|
|
255
|
+
--ready-delay 10m \
|
|
256
|
+
--last-push-time "$(git log -1 --format=%ct HEAD)"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Flags:
|
|
260
|
+
|
|
261
|
+
| Flag | Default | Description |
|
|
262
|
+
| ----------------------------- | ------- | ------------------------------------------------- |
|
|
263
|
+
| `--ready-delay Nm` | `10m` | Settle window before the loop cancels after READY |
|
|
264
|
+
| `--cooldown-seconds N` | `30` | Wait after a push before reading CI |
|
|
265
|
+
| `--last-push-time N` | — | Unix timestamp hint embedded in the result |
|
|
266
|
+
| `--no-auto-rerun` | false | Return `wait` instead of rerunning transient CI |
|
|
267
|
+
| `--no-auto-mark-ready` | false | Skip converting draft → ready-for-review |
|
|
268
|
+
| `--no-auto-cancel-actionable` | false | Skip cancelling actionable failing runs |
|
|
269
|
+
|
|
270
|
+
**Text output** (one line per action):
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
PR #42 [COOLDOWN] status=UNKNOWN merge=UNKNOWN (cooldown: CI still starting)
|
|
274
|
+
PR #42 [WAIT] status=READY merge=CLEAN (540s until cancel)
|
|
275
|
+
PR #42 [RERUN_CI] status=FAILING merge=UNSTABLE reran=12345,67890
|
|
276
|
+
PR #42 [FIX_CODE] status=UNRESOLVED_COMMENTS merge=BLOCKED threads=2 comments=0 checks=1 cancelled=1
|
|
277
|
+
PR #42 [REBASE] status=FAILING merge=BEHIND (branch is behind main)
|
|
278
|
+
PR #42 [MARK_READY] status=READY merge=CLEAN markedReady=true
|
|
279
|
+
PR #42 [CANCEL] status=READY merge=CLEAN (ready-delay elapsed)
|
|
280
|
+
PR #42 [ESCALATE] status=UNRESOLVED_COMMENTS merge=BLOCKED triggers=fix-thrash — Same thread(s) attempted multiple times without resolution — fix manually then rerun /pr-shepherd:monitor
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**JSON output** (`--format=json`, compact single line):
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"pr": 42,
|
|
288
|
+
"repo": "owner/repo",
|
|
289
|
+
"status": "READY",
|
|
290
|
+
"state": "OPEN",
|
|
291
|
+
"mergeStateStatus": "CLEAN",
|
|
292
|
+
"copilotReviewInProgress": false,
|
|
293
|
+
"isDraft": false,
|
|
294
|
+
"shouldCancel": false,
|
|
295
|
+
"remainingSeconds": 540,
|
|
296
|
+
"summary": { "passing": 3, "skipped": 0, "filtered": 0, "inProgress": 0 },
|
|
297
|
+
"action": "wait"
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Exit codes: `0` wait/cooldown/rerun_ci/mark_ready · `1` fix_code/rebase · `2` cancel · `3` escalate
|
|
302
|
+
|
|
303
|
+
### pr-shepherd status PR1 [PR2 …]
|
|
304
|
+
|
|
305
|
+
Multi-PR summary table. One lightweight GraphQL query per PR, run in parallel.
|
|
306
|
+
|
|
307
|
+
```sh
|
|
308
|
+
pr-shepherd status 41 42 43
|
|
309
|
+
pr-shepherd status 100 --format=json
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
# owner/repo — PR status (3)
|
|
315
|
+
|
|
316
|
+
PR #41 Add new feature for user authentication READY SUCCESS
|
|
317
|
+
PR #42 Refactor internal module IN PROGRESS PENDING
|
|
318
|
+
PR #43 Fix edge case in parser BLOCKED SUCCESS (threads truncated — run shepherd check for full count)
|
|
319
|
+
```
|
|
153
320
|
|
|
154
|
-
|
|
155
|
-
| --------------------- | ------- | ------------------------------- |
|
|
156
|
-
| `--format text\|json` | `text` | Output format |
|
|
157
|
-
| `--no-cache` | false | Bypass the 5-minute file cache |
|
|
158
|
-
| `--cache-ttl N` | 300 | Cache TTL in seconds |
|
|
159
|
-
| `--ready-delay Nm` | `10m` | Settle window before loop exits |
|
|
321
|
+
Exit code: `0` if every PR is READY, `1` otherwise.
|
|
160
322
|
|
|
161
323
|
## Configuration
|
|
162
324
|
|
|
@@ -178,7 +340,7 @@ See [docs/configuration.md](docs/configuration.md) for all options.
|
|
|
178
340
|
|
|
179
341
|
## Requirements
|
|
180
342
|
|
|
181
|
-
- Node.js ≥
|
|
343
|
+
- Node.js ≥ 22.0.0
|
|
182
344
|
- `gh` CLI authenticated (`gh auth login`); `repo` scope is required for private repositories (public repositories may not need it)
|
|
183
345
|
- `git`
|
|
184
346
|
|
package/bin/commands/check.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export async function runCheck(opts) {
|
|
|
62
62
|
// Triage failures (fetch logs) — skipped when caller will short-circuit before needing failureKind.
|
|
63
63
|
const triaged = failing.length > 0 && !opts.skipTriage ? await triageFailingChecks(failing) : failing;
|
|
64
64
|
// Resolve threads and comments.
|
|
65
|
-
const unresolvedThreads = batchData.reviewThreads.filter((t) => !t.isResolved);
|
|
65
|
+
const unresolvedThreads = batchData.reviewThreads.filter((t) => !t.isResolved && !t.isMinimized);
|
|
66
66
|
const visibleComments = batchData.comments.filter((c) => !c.isMinimized);
|
|
67
67
|
// Auto-resolve outdated threads.
|
|
68
68
|
const outdated = getOutdatedThreads(unresolvedThreads);
|
package/bin/commands/resolve.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export async function runResolveFetch(opts) {
|
|
|
28
28
|
}
|
|
29
29
|
// Always bypass cache for resolve — we need fresh data before mutating.
|
|
30
30
|
const { data } = await fetchPrBatch(prNumber, repo);
|
|
31
|
-
const unresolvedThreads = data.reviewThreads.filter((t) => !t.isResolved);
|
|
31
|
+
const unresolvedThreads = data.reviewThreads.filter((t) => !t.isResolved && !t.isMinimized);
|
|
32
32
|
const visibleComments = data.comments.filter((c) => !c.isMinimized);
|
|
33
33
|
// Auto-resolve outdated.
|
|
34
34
|
const outdated = getOutdatedThreads(unresolvedThreads);
|
package/bin/github/batch.mjs
CHANGED
|
@@ -117,6 +117,7 @@ function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes, rawChe
|
|
|
117
117
|
id: t.id,
|
|
118
118
|
isResolved: t.isResolved,
|
|
119
119
|
isOutdated: t.isOutdated,
|
|
120
|
+
isMinimized: comment?.isMinimized ?? false,
|
|
120
121
|
path: comment?.path ?? null,
|
|
121
122
|
line: comment?.line ?? null,
|
|
122
123
|
author: comment?.author?.login ?? "unknown",
|
package/package.json
CHANGED
|
@@ -39,7 +39,7 @@ Invoke `/loop <INTERVAL> --max-turns 50 --expires 8h` via the Skill tool. Use th
|
|
|
39
39
|
|
|
40
40
|
````
|
|
41
41
|
# pr-shepherd-loop:pr=<PR_NUMBER>
|
|
42
|
-
Run the following in a single Bash invocation
|
|
42
|
+
Run the following in a single Bash invocation:
|
|
43
43
|
npx pr-shepherd iterate <PR_NUMBER> --ready-delay <READY_DELAY> --no-cache --last-push-time "$(git log -1 --format=%ct HEAD)" --format=json
|
|
44
44
|
|
|
45
45
|
Exit codes 0, 1, 2, and 3 are all valid signals — always try to parse stdout as JSON first. If the command exits non-zero and stdout is not parseable JSON (e.g. a crash), log the first line of stderr and continue (do not cancel the loop).
|
|
@@ -51,7 +51,7 @@ Parse the `action` field and act:
|
|
|
51
51
|
- `rerun_ci` → log: `RERAN <N> CI checks: <reran joined by space>`
|
|
52
52
|
- `mark_ready` → log: `MARKED READY: PR <pr>`
|
|
53
53
|
- `cancel` → invoke `/loop cancel` and stop
|
|
54
|
-
- `rebase` → run
|
|
54
|
+
- `rebase` → run:
|
|
55
55
|
```bash
|
|
56
56
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
57
57
|
echo "SKIP rebase: dirty worktree (uncommitted changes present)"
|
|
@@ -76,15 +76,26 @@ Parse the `action` field and act:
|
|
|
76
76
|
After fixing manually, rerun /pr-shepherd:monitor <PR> to resume.
|
|
77
77
|
|
|
78
78
|
- `fix_code` → do the following, then stop this iteration (CI needs time):
|
|
79
|
-
|
|
79
|
+
0. **Triage `fix.comments`** into two buckets before taking any action:
|
|
80
|
+
- **Noise** (`NOISE_COMMENT_IDS`): bot-authored comments with no actionable code feedback — e.g. quota/rate-limit warnings ("you have reached your daily quota", "please wait up to N hours"), "resuming" notices, bare acknowledgements, or any comment whose body contains no file path, line number, or concrete code suggestion. Collect their `id`s.
|
|
81
|
+
- **Actionable**: everything else. When in doubt, treat as actionable.
|
|
82
|
+
All items in `fix.threads` are always actionable (they carry a file path and line by construction).
|
|
83
|
+
1. For each item in `fix.threads` and each **actionable** `fix.comments`: read the referenced file/line and apply the fix (Edit/Write tools).
|
|
80
84
|
2. For each item in `fix.checks`:
|
|
81
|
-
- If `runId` is non-null: fetch the failure log with `gh run view <runId> --log-failed
|
|
85
|
+
- If `runId` is non-null: fetch the failure log with `gh run view <runId> --log-failed`, scan the output to identify the failure (e.g. grep for `FAIL` for test failures, `error:` for type/compile errors, lint rule names for lint failures), then read the relevant file and apply the fix (Edit/Write tools).
|
|
82
86
|
- If `runId` is null: the failed check is an external status check that cannot be inspected via run logs. Escalate — tell the user to open `detailsUrl` in the PR checks UI, inspect the failure manually, and rerun `/pr-shepherd:monitor <PR_NUMBER>` after addressing it. Do not attempt to fix these automatically.
|
|
83
87
|
3. For each item in `fix.changesRequestedReviews`: read the review body and apply the requested changes.
|
|
84
88
|
4. If files were changed, `git add <files> && git commit -m "<appropriate commit message>"`
|
|
85
|
-
5. `git fetch origin && git rebase origin/<BASE_BRANCH> && git push --force-with-lease`
|
|
86
|
-
6.
|
|
87
|
-
7.
|
|
89
|
+
5. If files were changed: `git fetch origin && git rebase origin/<BASE_BRANCH> && git push --force-with-lease`, then `HEAD_SHA=$(git rev-parse HEAD)`.
|
|
90
|
+
6. If **only noise** was found (no files changed, no threads/checks/reviews to act on): skip commit/push and omit `--require-sha` in the next step.
|
|
91
|
+
7. Resolve the items on GitHub. Build the command from the non-empty ID lists only — always start with:
|
|
92
|
+
`npx pr-shepherd resolve <PR_NUMBER>`
|
|
93
|
+
Then append:
|
|
94
|
+
- `--resolve-thread-ids <IDs>` only if `fix.threads` was non-empty.
|
|
95
|
+
- `--minimize-comment-ids <IDs>` if any comments exist (use `NOISE_COMMENT_IDS` plus IDs of any other comments to minimize).
|
|
96
|
+
- `--dismiss-review-ids <IDs> --message "<specific description of what you changed>"` only if `fix.changesRequestedReviews` was non-empty. The message is shown to the reviewer on GitHub — write one sentence describing the actual fix (e.g. `"Switched to parameterized query in src/db.ts"`). Never use generic text like `"address review comments"`.
|
|
97
|
+
- `--require-sha "$HEAD_SHA"` only if a push occurred (omit when only noise was handled).
|
|
98
|
+
Omit any flag whose ID list is empty.
|
|
88
99
|
|
|
89
100
|
````
|
|
90
101
|
|
|
@@ -62,17 +62,19 @@ Resolve unresolved review threads and minimize PR comments on the current PR —
|
|
|
62
62
|
- `git fetch origin && git rebase origin/$BASE_BRANCH && git push --force-with-lease`
|
|
63
63
|
- Cancel stale CI runs: `gh run list --branch "$BRANCH" --status in_progress --json databaseId --jq '.[].databaseId' | xargs -I{} gh run cancel {}`
|
|
64
64
|
|
|
65
|
-
6. **Resolve all verified items** — **only after the push
|
|
65
|
+
6. **Resolve all verified items** — **only after the push, and only if at least one of the three ID lists is non-empty.** If all lists are empty, skip this step entirely (running resolve with no mutation IDs enters fetch mode as a side effect). Build the command from the non-empty ID lists; omit any flag whose list is empty:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
npx pr-shepherd resolve <N> \
|
|
69
69
|
--resolve-thread-ids <comma-separated-IDs> \
|
|
70
70
|
--minimize-comment-ids <comma-separated-IDs> \
|
|
71
71
|
--dismiss-review-ids <comma-separated-IDs> \
|
|
72
|
-
--message "
|
|
72
|
+
--message "<specific description of the fix that addressed this review>" \
|
|
73
73
|
--require-sha $(git rev-parse HEAD)
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
`--message` belongs **only** with `--dismiss-review-ids`. Omit it entirely when not dismissing a review. When you are dismissing, write one sentence describing the actual fix — the text is sent to GitHub as the dismissal reason and is shown to the reviewer. Generic text like `"Addressed in <SHA>"` or `"address review comments"` is not acceptable.
|
|
77
|
+
|
|
76
78
|
The `--require-sha` flag ensures pr-shepherd verifies GitHub has the new commit before resolving.
|
|
77
79
|
|
|
78
80
|
7. **Report results** from the CLI output.
|
|
@@ -82,4 +84,4 @@ Resolve unresolved review threads and minimize PR comments on the current PR —
|
|
|
82
84
|
- NEVER resolve threads before pushing fixes (use `--require-sha`).
|
|
83
85
|
- NEVER blindly resolve items — always read and verify first.
|
|
84
86
|
- Resolve from ALL authors — bots, AI reviewers, and humans alike.
|
|
85
|
-
- `--message` is required when using `--dismiss-review-ids
|
|
87
|
+
- `--message` is required when using `--dismiss-review-ids`, and must NOT be passed otherwise. The CLI throws if it is missing during dismissal. The message must describe the specific change that addressed the review (e.g. `"Added null check in handler.ts:42"`); generic boilerplate like `"address review comments"` or `"Addressed in <SHA>"` is reviewer-hostile and forbidden.
|