pr-shepherd 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +37 -302
- package/bin/checks/classify.mjs +5 -4
- package/bin/checks/triage.mjs +76 -62
- package/bin/cli/args.mjs +29 -61
- package/bin/cli/exit-codes.mjs +39 -0
- package/bin/cli/fix-formatter.mjs +76 -0
- package/bin/cli/formatters.mjs +108 -0
- package/bin/cli/handlers.mjs +138 -0
- package/bin/cli/iterate-formatter.mjs +78 -0
- package/bin/cli-parser.iterate-fixtures.mjs +65 -0
- package/bin/cli-parser.mjs +110 -0
- package/bin/commands/check-status.mjs +35 -0
- package/bin/commands/check.mjs +14 -61
- package/bin/commands/commit-suggestion.mjs +159 -0
- package/bin/commands/iterate/classify.mjs +77 -0
- package/bin/commands/iterate/escalate.mjs +124 -0
- package/bin/commands/iterate/fix-code.mjs +97 -0
- package/bin/commands/iterate/helpers.mjs +103 -0
- package/bin/commands/iterate/index.mjs +122 -0
- package/bin/commands/iterate/render.mjs +119 -0
- package/bin/commands/iterate/stall.mjs +65 -0
- package/bin/commands/iterate/steps.mjs +31 -0
- package/bin/commands/iterate.mjs +2 -628
- package/bin/commands/monitor.mjs +78 -0
- package/bin/commands/ready-delay.mjs +3 -4
- package/bin/commands/resolve-instructions.mjs +39 -0
- package/bin/commands/resolve.mjs +34 -3
- package/bin/commands/status.mjs +7 -0
- package/bin/comments/resolve.mjs +1 -1
- package/bin/config/load.mjs +17 -113
- package/bin/config.json +10 -22
- package/bin/github/batch-parsers.mjs +140 -0
- package/bin/github/batch-raw-types.mjs +2 -0
- package/bin/github/batch.mjs +34 -129
- package/bin/github/client.mjs +47 -9
- package/bin/github/gql/batch-pr.gql +20 -0
- package/bin/github/http.mjs +32 -30
- package/bin/index.mjs +15 -2
- package/bin/merge-status/derive.mjs +11 -11
- package/bin/reporters/agent.mjs +13 -4
- package/bin/reporters/check-instructions.mjs +65 -0
- package/bin/reporters/json.mjs +3 -2
- package/bin/reporters/text.mjs +108 -61
- package/bin/{cache → state}/fix-attempts.mjs +3 -3
- package/bin/state/iterate-stall.mjs +74 -0
- package/bin/suggestions/parse.mjs +119 -0
- package/bin/suggestions/patch.mjs +52 -0
- package/bin/types/github.mjs +2 -0
- package/bin/types/iterate.mjs +2 -0
- package/bin/types/report.mjs +2 -0
- package/bin/types.mjs +3 -1
- package/package.json +3 -3
- package/plugin/skills/check/SKILL.md +15 -48
- package/plugin/skills/monitor/SKILL.md +11 -64
- package/plugin/skills/resolve/SKILL.md +10 -76
- package/bin/cache/file-cache.mjs +0 -79
- package/bin/cli.mjs +0 -286
package/README.md
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
# pr-shepherd
|
|
2
2
|
|
|
3
3
|
Autonomous PR CI monitor and review-comment resolver for Claude Code.
|
|
4
|
+
The goal is to have an agent take a plan to a human-reviewable PR autonomously.
|
|
5
|
+
|
|
6
|
+
Example Workflow:
|
|
7
|
+
|
|
8
|
+
1. `/model opusplan`
|
|
9
|
+
2. Create a plan
|
|
10
|
+
3. Accept the plan
|
|
11
|
+
4. Switch to Auto Mode
|
|
12
|
+
5. Prompt: `make a PR, then run /pr-shepherd:monitor`
|
|
13
|
+
6. ...
|
|
14
|
+
7. Human reviews PR with passing CI, no open threads, and all comments minimized
|
|
4
15
|
|
|
5
16
|
## Why pr-shepherd
|
|
6
17
|
|
|
7
18
|
Concrete improvements to an agentic PR-review workflow:
|
|
8
19
|
|
|
9
20
|
- **Faster monitor loops** — one batched GraphQL query per tick (see [docs/graphql.md](docs/graphql.md)) instead of N REST round-trips
|
|
10
|
-
- **Lower context usage per iteration** — classification lives in
|
|
11
|
-
- **Deterministic output** — `--format=json` and `--format=text` surface equivalent information, so both scripts and agents see the same state
|
|
21
|
+
- **Lower context usage per iteration** — classification lives in the CLI; the agent receives one decision per tick and never sees raw GraphQL payloads or resolved threads
|
|
12
22
|
- **Prompt-cache friendly** — the 4-minute default tick is tuned to Claude's 5-minute prompt-cache TTL (tunable via `watch.interval`)
|
|
13
|
-
- **Reduced GitHub rate-limit exposure** — read
|
|
23
|
+
- **Reduced GitHub rate-limit exposure** — one batched GraphQL read per tick; loop-state files (fix-attempts, stall detection, ready-delay timer) are kept in `$TMPDIR/pr-shepherd-state/`
|
|
14
24
|
- **No MCP surface** — skills call the CLI via `npx`; no long-lived MCP server, no extra auth boundary, smaller reasoning surface
|
|
15
25
|
- **Skills over subagents** — skill prompts inject into the main conversation rather than spawning a subagent that reloads CLAUDE.md every turn
|
|
16
26
|
- **Safe to interrupt** — all state lives in the PR on GitHub; the cron loop self-terminates when the PR is merged, closed, or settles after ready-delay
|
|
@@ -20,130 +30,9 @@ Concrete improvements to an agentic PR-review workflow:
|
|
|
20
30
|
- **Reduced agent context** — logic lives in the CLI, not the prompt
|
|
21
31
|
- **Reduced GitHub rate-limit exhaustion** — primary PR state is fetched via a batched GraphQL query
|
|
22
32
|
- **Fewer tool calls** — comment resolutions are batched; resolved threads never reach the agent
|
|
23
|
-
- **No MCP** — smaller reasoning surface, much faster than the GitHub MCP
|
|
24
|
-
- **No vendor lock-in** — runs against `gh` + `git`; no hosted service required
|
|
25
33
|
- **Skills over subagents** — subagents reload all CLAUDE.md context on every turn; skills inject into the main conversation instead, keeping cost low
|
|
26
34
|
- **JSON/text parity** — `--format=json` and `--format=text` carry equivalent information; every field in one has a representation in the other
|
|
27
35
|
|
|
28
|
-
## Features
|
|
29
|
-
|
|
30
|
-
pr-shepherd is split into a CLI (deterministic, pure GitHub I/O) and three Claude Code skills that wrap the CLI with model-driven flow and mutation.
|
|
31
|
-
|
|
32
|
-
### What the CLI does
|
|
33
|
-
|
|
34
|
-
Deterministic commands that fetch, classify, and mutate PR state without invoking the model:
|
|
35
|
-
|
|
36
|
-
- **`check`** — one-shot PR snapshot (merge state, CI results, unresolved comments) via a single batched GraphQL query
|
|
37
|
-
- **`resolve`** — fetch/triage mode auto-resolves outdated threads and returns actionable items; mutate mode batch-resolves threads, minimizes comments, and dismisses reviews by ID, polling `--require-sha` so reviewers see the push before threads close
|
|
38
|
-
- **`iterate`** — classifies current PR state and emits exactly one of eight actions: `cooldown`, `wait`, `rerun_ci`, `mark_ready`, `rebase`, `fix_code`, `cancel`, `escalate` (see [docs/actions.md](docs/actions.md))
|
|
39
|
-
- **`status`** — multi-PR summary table, one lightweight GraphQL query per PR in parallel
|
|
40
|
-
|
|
41
|
-
Cross-cutting machinery: file cache with atomic writes ([docs/cache.md](docs/cache.md)), merge-status derivation ([docs/merge-status.md](docs/merge-status.md)), CI failure classification into `actionable` / `infrastructure` / `timeout` / `flaky` ([docs/checks.md](docs/checks.md)), outdated-thread detection ([docs/comments.md](docs/comments.md)), deprecation-warning-aware RC loader.
|
|
42
|
-
|
|
43
|
-
### What the skills do
|
|
44
|
-
|
|
45
|
-
Claude Code skills that wrap the CLI with model-driven triage, code edits, and flow control:
|
|
46
|
-
|
|
47
|
-
- **`/pr-shepherd:check`** — calls `check --format=json` and prints a human summary; never declares "ready to merge" unless every gate passes (merge status CLEAN, status READY, Copilot review not in progress)
|
|
48
|
-
- **`/pr-shepherd:monitor`** — creates a `/loop` cron job (4-minute default, 8-hour expiry, 50-turn cap), deduplicates via a `# pr-shepherd-loop:pr=<N>` tag in `CronList`, dispatches on the `[ACTION]` H1 tag each tick, runs rebase scripts and fix instructions in the main conversation
|
|
49
|
-
- **`/pr-shepherd:resolve`** — runs `resolve --fetch`, triages every returned item into Fixed / Actionable / Not-relevant / Outdated / Acknowledge (no silent drops — this is a project invariant), applies edits, cancels stale CI runs, pushes, then calls `resolve` in mutate mode with `--require-sha`
|
|
50
|
-
|
|
51
|
-
See [docs/skills.md](docs/skills.md) for full skill reference.
|
|
52
|
-
|
|
53
|
-
## Install
|
|
54
|
-
|
|
55
|
-
> **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:
|
|
56
|
-
>
|
|
57
|
-
> ```bash
|
|
58
|
-
> npm install --save-dev pr-shepherd
|
|
59
|
-
> ```
|
|
60
|
-
>
|
|
61
|
-
> 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`.
|
|
62
|
-
|
|
63
|
-
### As individual skills via `npx skills`
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npx skills add jonathanong/pr-shepherd
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
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).
|
|
70
|
-
|
|
71
|
-
### As a Claude Code plugin (recommended)
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
claude /plugin marketplace add jonathanong/pr-shepherd
|
|
75
|
-
claude /plugin install pr-shepherd
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
This repo ships two `marketplace.json` files that serve different 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.
|
|
79
|
-
|
|
80
|
-
See [Usage](#usage) below.
|
|
81
|
-
|
|
82
|
-
### Without the plugin — custom slash command
|
|
83
|
-
|
|
84
|
-
If you don't want the full plugin, create a project-local (or user-scope)
|
|
85
|
-
slash command that wraps the CLI directly. This still requires `pr-shepherd`
|
|
86
|
-
to be installed in the repository first (`npm install pr-shepherd`), so that
|
|
87
|
-
`npx pr-shepherd ...` runs without prompting to install the package.
|
|
88
|
-
|
|
89
|
-
1. **Create the command file:**
|
|
90
|
-
- Project-scope: `.claude/commands/pr-check.md`
|
|
91
|
-
- User-scope: `~/.claude/commands/pr-check.md`
|
|
92
|
-
|
|
93
|
-
2. **Paste this as the file contents:**
|
|
94
|
-
|
|
95
|
-
````markdown
|
|
96
|
-
---
|
|
97
|
-
description: "Check GitHub CI status and review comments for the current PR"
|
|
98
|
-
argument-hint: "[PR number or URL ...]"
|
|
99
|
-
allowed-tools: ["Bash", "Read", "Grep"]
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
# PR Status Check
|
|
103
|
-
|
|
104
|
-
## Arguments: $ARGUMENTS
|
|
105
|
-
|
|
106
|
-
## Resolve PR number(s)
|
|
107
|
-
|
|
108
|
-
1. If `$ARGUMENTS` contains PR numbers or GitHub PR URLs, extract the number(s).
|
|
109
|
-
2. Otherwise, infer: `gh pr list --head "$(git rev-parse --abbrev-ref HEAD)" --json number --jq '.[0].number'`
|
|
110
|
-
3. If no PR found, report an error and stop.
|
|
111
|
-
|
|
112
|
-
## Run the check
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
npx pr-shepherd check <PR_NUMBER> --format=json
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Parse the JSON and report:
|
|
119
|
-
|
|
120
|
-
- **Merge status** (`report.mergeStatus.status`): CLEAN | BEHIND | CONFLICTS | BLOCKED | UNSTABLE | DRAFT | UNKNOWN
|
|
121
|
-
- **CI check results** (`report.checks`): passing count, failing names, in-progress names
|
|
122
|
-
- **Unresolved review comments** (`report.threads.actionable` + `report.comments.actionable`): count + details
|
|
123
|
-
````
|
|
124
|
-
|
|
125
|
-
3. **Use it in Claude Code:**
|
|
126
|
-
|
|
127
|
-
```
|
|
128
|
-
/pr-check
|
|
129
|
-
/pr-check 42
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
For `monitor` and `resolve` custom commands, do **not** copy the
|
|
133
|
-
[`plugin/skills/`](plugin/skills/) files directly — those contain skill/plugin-specific
|
|
134
|
-
frontmatter that is not valid for `.claude/commands/` files. Instead, create
|
|
135
|
-
`.claude/commands/pr-monitor.md` and/or `.claude/commands/pr-resolve.md`
|
|
136
|
-
using the same command-file structure as the `pr-check` example above, with
|
|
137
|
-
the CLI invocation changed to `npx pr-shepherd iterate ...` or
|
|
138
|
-
`npx pr-shepherd resolve ...`. To drive the CLI without Claude at all, see
|
|
139
|
-
[docs/usage.md](docs/usage.md).
|
|
140
|
-
|
|
141
|
-
### As a global CLI
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
npm install -g pr-shepherd
|
|
145
|
-
```
|
|
146
|
-
|
|
147
36
|
## Usage
|
|
148
37
|
|
|
149
38
|
### Monitor a PR
|
|
@@ -185,174 +74,45 @@ See [docs/skills.md](docs/skills.md) for full argument reference.
|
|
|
185
74
|
|
|
186
75
|
## Workflow
|
|
187
76
|
|
|
188
|
-
On each tick (4-minute default, tunable via `watch.interval`): fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (fix code, rebase, rerun CI, mark ready, or wait). See [docs/flow.md](docs/flow.md) for the
|
|
189
|
-
|
|
190
|
-
## CLI
|
|
77
|
+
On each tick (4-minute default, tunable via `watch.interval`): fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (fix code, rebase, rerun CI, mark ready, or wait). See [docs/iterate-flow.md](docs/iterate-flow.md) for the decision table and [docs/flow.md](docs/flow.md) for the end-to-end flow diagram.
|
|
191
78
|
|
|
192
|
-
|
|
193
|
-
pr-shepherd check [PR] # read-only PR status snapshot
|
|
194
|
-
pr-shepherd resolve [PR] [--fetch | --resolve-thread-ids …]
|
|
195
|
-
pr-shepherd iterate [PR] [--cooldown-seconds N] [--ready-delay Nm] [--last-push-time N]
|
|
196
|
-
pr-shepherd status PR1 [PR2 …] # multi-PR table
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
Common flags (all subcommands):
|
|
200
|
-
|
|
201
|
-
| Flag | Default | Description |
|
|
202
|
-
| --------------------- | ------- | ----------------------------------------------------------------------------------- |
|
|
203
|
-
| `--format text\|json` | `text` | Output format |
|
|
204
|
-
| `--no-cache` | false | Bypass the 5-minute file cache |
|
|
205
|
-
| `--cache-ttl N` | `300` | Cache TTL in seconds; takes precedence over `PR_SHEPHERD_CACHE_TTL_SECONDS` env var |
|
|
206
|
-
|
|
207
|
-
### pr-shepherd check [PR]
|
|
208
|
-
|
|
209
|
-
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.
|
|
210
|
-
|
|
211
|
-
```sh
|
|
212
|
-
pr-shepherd check # infer PR from current branch
|
|
213
|
-
pr-shepherd check 42
|
|
214
|
-
pr-shepherd check 42 --format=json
|
|
215
|
-
pr-shepherd check 42 --no-cache
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Exit codes: `0` READY · `2` IN_PROGRESS · `3` UNRESOLVED_COMMENTS · `1` all other statuses
|
|
219
|
-
|
|
220
|
-
**Example output:**
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
PR #42 — owner/repo
|
|
224
|
-
Status: UNRESOLVED_COMMENTS
|
|
225
|
-
|
|
226
|
-
Merge Status: CLEAN
|
|
227
|
-
mergeStateStatus: CLEAN
|
|
228
|
-
mergeable: MERGEABLE
|
|
229
|
-
reviewDecision: APPROVED
|
|
230
|
-
isDraft: false
|
|
231
|
-
copilotReviewInProgress:false
|
|
232
|
-
|
|
233
|
-
CI Checks: 3/3 passed
|
|
234
|
-
|
|
235
|
-
Actionable Review Threads (1):
|
|
236
|
-
- threadId=RT_kwDOBxyz123 src/api.ts:47 (@reviewer)
|
|
237
|
-
Please add error handling here
|
|
238
|
-
|
|
239
|
-
Summary: 1 actionable item(s) remaining
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### pr-shepherd resolve [PR]
|
|
243
|
-
|
|
244
|
-
Two modes: **fetch** (default) auto-resolves outdated threads and returns actionable items; **mutate** resolves/minimizes/dismisses specific IDs after you push fixes.
|
|
245
|
-
|
|
246
|
-
**Fetch mode:**
|
|
247
|
-
|
|
248
|
-
```sh
|
|
249
|
-
pr-shepherd resolve # fetch + auto-resolve outdated threads
|
|
250
|
-
pr-shepherd resolve 42 --fetch --format=json
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
```
|
|
254
|
-
Actionable Review Threads (2):
|
|
255
|
-
- threadId=RT_kwDOabc src/api.ts:47 (@reviewer): Please add error handling here
|
|
256
|
-
- threadId=RT_kwDOdef src/utils.ts:12 (@bot): Consider using a const here
|
|
257
|
-
|
|
258
|
-
Summary: 2 actionable item(s)
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Mutate mode** (after pushing fixes):
|
|
262
|
-
|
|
263
|
-
```sh
|
|
264
|
-
pr-shepherd resolve 42 \
|
|
265
|
-
--resolve-thread-ids RT_kwDOabc,RT_kwDOdef \
|
|
266
|
-
--minimize-comment-ids IC_kwDOxyz \
|
|
267
|
-
--dismiss-review-ids PRR_kwDO123 \
|
|
268
|
-
--message "Switched query to parameterized form in src/db.ts" \
|
|
269
|
-
--require-sha $(git rev-parse HEAD)
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
```
|
|
273
|
-
Resolved threads (2): RT_kwDOabc, RT_kwDOdef
|
|
274
|
-
Minimized comments (1): IC_kwDOxyz
|
|
275
|
-
Dismissed reviews (1): PRR_kwDO123
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
`--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.
|
|
79
|
+
## Install
|
|
279
80
|
|
|
280
|
-
|
|
81
|
+
> **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:
|
|
82
|
+
>
|
|
83
|
+
> ```bash
|
|
84
|
+
> npm install --save-dev pr-shepherd
|
|
85
|
+
> ```
|
|
86
|
+
>
|
|
87
|
+
> 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`.
|
|
281
88
|
|
|
282
|
-
|
|
89
|
+
### As individual skills via `npx skills`
|
|
283
90
|
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
--ready-delay 10m \
|
|
287
|
-
--last-push-time "$(git log -1 --format=%ct HEAD)"
|
|
91
|
+
```bash
|
|
92
|
+
npx skills add jonathanong/pr-shepherd
|
|
288
93
|
```
|
|
289
94
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
| Flag | Default | Description |
|
|
293
|
-
| ----------------------------- | ------- | ------------------------------------------------- |
|
|
294
|
-
| `--ready-delay Nm` | `10m` | Settle window before the loop cancels after READY |
|
|
295
|
-
| `--cooldown-seconds N` | `30` | Wait after a push before reading CI |
|
|
296
|
-
| `--last-push-time N` | — | Unix timestamp hint embedded in the result |
|
|
297
|
-
| `--no-auto-rerun` | false | Return `wait` instead of rerunning transient CI |
|
|
298
|
-
| `--no-auto-mark-ready` | false | Skip converting draft → ready-for-review |
|
|
299
|
-
| `--no-auto-cancel-actionable` | false | Skip cancelling actionable failing runs |
|
|
300
|
-
|
|
301
|
-
**Markdown output** (default). The monitor SKILL reads the `[ACTION]` tag from the H1 heading to decide what to do. Every action emits an H1, a bolded base-fields line, a bolded summary line, then an action-specific body. Example for `[WAIT]`:
|
|
302
|
-
|
|
303
|
-
```markdown
|
|
304
|
-
# PR #42 [WAIT]
|
|
305
|
-
|
|
306
|
-
**status** `READY` · **merge** `CLEAN` · **state** `OPEN` · **repo** `owner/repo`
|
|
307
|
-
**summary** 3 passing, 0 skipped, 0 filtered, 0 inProgress · **remainingSeconds** 540 · **copilotReviewInProgress** false · **isDraft** false · **shouldCancel** false
|
|
95
|
+
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).
|
|
308
96
|
|
|
309
|
-
|
|
310
|
-
```
|
|
97
|
+
### As a Claude Code plugin (recommended)
|
|
311
98
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
**JSON output** (`--format=json`, compact single line):
|
|
317
|
-
|
|
318
|
-
```json
|
|
319
|
-
{
|
|
320
|
-
"pr": 42,
|
|
321
|
-
"repo": "owner/repo",
|
|
322
|
-
"status": "READY",
|
|
323
|
-
"state": "OPEN",
|
|
324
|
-
"mergeStateStatus": "CLEAN",
|
|
325
|
-
"copilotReviewInProgress": false,
|
|
326
|
-
"isDraft": false,
|
|
327
|
-
"shouldCancel": false,
|
|
328
|
-
"remainingSeconds": 540,
|
|
329
|
-
"summary": { "passing": 3, "skipped": 0, "filtered": 0, "inProgress": 0 },
|
|
330
|
-
"action": "wait"
|
|
331
|
-
}
|
|
99
|
+
```bash
|
|
100
|
+
claude /plugin marketplace add jonathanong/pr-shepherd
|
|
101
|
+
claude /plugin install pr-shepherd
|
|
332
102
|
```
|
|
333
103
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
### pr-shepherd status PR1 [PR2 …]
|
|
337
|
-
|
|
338
|
-
Multi-PR summary table. One lightweight GraphQL query per PR, run in parallel.
|
|
104
|
+
This repo ships two `marketplace.json` files that serve different 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.
|
|
339
105
|
|
|
340
|
-
|
|
341
|
-
pr-shepherd status 41 42 43
|
|
342
|
-
pr-shepherd status 100 --format=json
|
|
343
|
-
```
|
|
106
|
+
### Without the plugin
|
|
344
107
|
|
|
345
|
-
|
|
108
|
+
See [docs/custom-commands.md](docs/custom-commands.md) for a project-local slash command that wraps the CLI without the plugin.
|
|
346
109
|
|
|
347
|
-
|
|
110
|
+
### As a global CLI
|
|
348
111
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
PR #43 Fix edge case in parser BLOCKED SUCCESS (threads truncated — run shepherd check for full count)
|
|
112
|
+
```bash
|
|
113
|
+
npm install -g pr-shepherd
|
|
352
114
|
```
|
|
353
115
|
|
|
354
|
-
Exit code: `0` if every PR is READY, `1` otherwise.
|
|
355
|
-
|
|
356
116
|
## Configuration
|
|
357
117
|
|
|
358
118
|
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.
|
|
@@ -369,32 +129,7 @@ actions:
|
|
|
369
129
|
autoRebase: false # disable for repos that enforce merge commits
|
|
370
130
|
```
|
|
371
131
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
| Key | Default | Purpose |
|
|
375
|
-
| ------------------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------- |
|
|
376
|
-
| `cache.ttlSeconds` | `300` | File-cache TTL for read operations |
|
|
377
|
-
| `iterate.cooldownSeconds` | `30` | Wait after a push before reading CI |
|
|
378
|
-
| `iterate.fixAttemptsPerThread` | `3` | Max fix attempts per unresolved thread before `escalate` |
|
|
379
|
-
| `watch.interval` | `"4m"` | Monitor tick interval (tuned to Claude's 5-min prompt-cache TTL) |
|
|
380
|
-
| `watch.readyDelayMinutes` | `10` | Settle window after READY before the monitor loop cancels |
|
|
381
|
-
| `watch.expiresHours` | `8` | Max lifetime of a monitor cron job |
|
|
382
|
-
| `watch.maxTurns` | `50` | Max monitor ticks per session |
|
|
383
|
-
| `resolve.concurrency` | `4` | Parallel fanout for per-thread GraphQL fetches |
|
|
384
|
-
| `resolve.shaPoll.intervalMs` | `2000` | Poll interval when waiting for `--require-sha` to land on GitHub |
|
|
385
|
-
| `resolve.shaPoll.maxAttempts` | `10` | Max `--require-sha` polls before giving up |
|
|
386
|
-
| `resolve.fetchReviewSummaries` | `true` | Surface `COMMENTED` review summaries in `resolve --fetch` output |
|
|
387
|
-
| `checks.ciTriggerEvents` | `["pull_request", "pull_request_target"]` | Workflow `on:` events treated as PR CI (add `merge_group` for merge-queue repos) |
|
|
388
|
-
| `checks.timeoutPatterns` | see [`src/config.json`](src/config.json) | Log patterns that classify a failure as `timeout` |
|
|
389
|
-
| `checks.infraPatterns` | see [`src/config.json`](src/config.json) | Log patterns that classify a failure as `infrastructure` |
|
|
390
|
-
| `checks.logMaxLines` | `50` | Max log lines kept per failing check |
|
|
391
|
-
| `checks.logMaxChars` | `3000` | Max log characters kept per failing check |
|
|
392
|
-
| `mergeStatus.blockingReviewerLogins` | `["copilot"]` | Reviewer logins whose pending review blocks `mark_ready` |
|
|
393
|
-
| `actions.autoResolveOutdated` | `true` | Auto-resolve threads that point to code no longer in the PR diff |
|
|
394
|
-
| `actions.autoRebase` | `true` | Emit `rebase` for flaky failures when the branch is behind base |
|
|
395
|
-
| `actions.autoMarkReady` | `true` | Emit `mark_ready` when a draft PR's CI goes clean |
|
|
396
|
-
|
|
397
|
-
Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`), `PR_SHEPHERD_CACHE_DIR` (override cache base dir), `PR_SHEPHERD_CACHE_TTL_SECONDS` (override cache TTL; `--cache-ttl` takes precedence over this env var, which in turn takes precedence over the RC/config value).
|
|
132
|
+
Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`), `PR_SHEPHERD_STATE_DIR` (override loop-state base dir).
|
|
398
133
|
|
|
399
134
|
See [docs/configuration.md](docs/configuration.md) for full semantics and deprecated-key migration.
|
|
400
135
|
|
package/bin/checks/classify.mjs
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* pass/fail tally. Report them as "skipped" for transparency but don't block on them.
|
|
10
10
|
*/
|
|
11
11
|
import { loadConfig } from "../config/load.mjs";
|
|
12
|
-
const RELEVANT_EVENTS = new Set(loadConfig().checks.ciTriggerEvents);
|
|
13
12
|
/**
|
|
14
13
|
* Classify a list of raw check runs into shepherd categories.
|
|
15
14
|
*
|
|
@@ -17,11 +16,13 @@ const RELEVANT_EVENTS = new Set(loadConfig().checks.ciTriggerEvents);
|
|
|
17
16
|
* @returns Classified checks. "filtered" items were excluded from the tally.
|
|
18
17
|
*/
|
|
19
18
|
export function classifyChecks(checks) {
|
|
20
|
-
|
|
19
|
+
const relevantEvents = new Set(loadConfig().checks.ciTriggerEvents);
|
|
20
|
+
return checks.map((c) => classify(c, relevantEvents));
|
|
21
21
|
}
|
|
22
|
-
function classify(check) {
|
|
22
|
+
function classify(check, relevantEvents) {
|
|
23
23
|
// Filter: runs from non-PR events don't count toward PR readiness.
|
|
24
|
-
|
|
24
|
+
// event === null means a commit StatusContext — these are always relevant regardless of ciTriggerEvents.
|
|
25
|
+
if (check.event !== null && !relevantEvents.has(check.event)) {
|
|
25
26
|
return { ...check, category: "filtered" };
|
|
26
27
|
}
|
|
27
28
|
const { status, conclusion } = check;
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,92 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Triage failing check runs into
|
|
3
|
-
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
2
|
+
* Triage failing check runs into three categories based solely on GitHub's
|
|
3
|
+
* own conclusion field — no log-content classification:
|
|
4
|
+
* - timeout: conclusion is TIMED_OUT.
|
|
5
|
+
* - cancelled: conclusion is CANCELLED, STARTUP_FAILURE, or STALE.
|
|
6
|
+
* - actionable: everything else (FAILURE, ACTION_REQUIRED, …).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Exception: checks with runId === null are always classified as "actionable"
|
|
9
|
+
* regardless of conclusion, so they surface in fix_code where the monitor
|
|
10
|
+
* escalates to the user (no run to rerun/inspect).
|
|
11
|
+
*
|
|
12
|
+
* For all checks with a non-null runId, the jobs API is called once per runId
|
|
13
|
+
* (results are cached across checks that share a run) to fetch workflow name
|
|
14
|
+
* and, for actionable checks, the first failed step name. No log fetching is done.
|
|
15
|
+
*
|
|
16
|
+
* Note on infrastructure-killed FAILURE runs: GitHub reports these as
|
|
17
|
+
* conclusion === "FAILURE", so they classify as "actionable". The jobs API
|
|
18
|
+
* surfaces their failedStep (e.g. "Set up job") which gives the agent a
|
|
19
|
+
* GitHub-native signal to distinguish runner setup deaths from real test
|
|
20
|
+
* failures — without any log-pattern analysis at the CLI level.
|
|
10
21
|
*/
|
|
11
|
-
import { rest
|
|
12
|
-
import { loadConfig } from "../config/load.mjs";
|
|
13
|
-
const config = loadConfig();
|
|
14
|
-
const TIMEOUT_PATTERNS = config.checks.timeoutPatterns.map((p) => new RegExp(p, "i"));
|
|
15
|
-
const INFRA_PATTERNS = config.checks.infraPatterns.map((p) => new RegExp(p, "i"));
|
|
22
|
+
import { rest } from "../github/http.mjs";
|
|
16
23
|
// ---------------------------------------------------------------------------
|
|
17
24
|
// Public API
|
|
18
25
|
// ---------------------------------------------------------------------------
|
|
19
26
|
/**
|
|
20
|
-
*
|
|
27
|
+
* Triage each failing check: classify by GitHub conclusion and call the jobs
|
|
28
|
+
* API for each check with a non-null runId to fetch workflow name and (for
|
|
29
|
+
* actionable failures) the name of the first failed step.
|
|
21
30
|
*
|
|
22
|
-
*
|
|
31
|
+
* Jobs responses are cached by runId so checks that share a run (e.g. matrix
|
|
32
|
+
* builds or multiple required steps in one workflow) make only one API call.
|
|
23
33
|
*/
|
|
24
34
|
export function triageFailingChecks(failingChecks, repo) {
|
|
25
|
-
|
|
35
|
+
const jobsCache = new Map();
|
|
36
|
+
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
|
|
26
37
|
}
|
|
27
38
|
// ---------------------------------------------------------------------------
|
|
28
39
|
// Internal
|
|
29
40
|
// ---------------------------------------------------------------------------
|
|
30
|
-
async function triageCheck(check, repo) {
|
|
41
|
+
async function triageCheck(check, repo, jobsCache) {
|
|
42
|
+
const failureKind = check.runId === null ? "actionable" : classifyConclusion(check.conclusion);
|
|
31
43
|
if (check.runId === null) {
|
|
32
|
-
return { ...check, failureKind
|
|
44
|
+
return { ...check, failureKind };
|
|
33
45
|
}
|
|
34
|
-
const
|
|
35
|
-
const
|
|
46
|
+
const jobs = await fetchJobs(check.runId, repo, jobsCache);
|
|
47
|
+
const jobInfo = jobs ? pickJobInfo(jobs, check.name, failureKind) : undefined;
|
|
36
48
|
return {
|
|
37
49
|
...check,
|
|
38
50
|
failureKind,
|
|
39
|
-
|
|
51
|
+
workflowName: jobInfo?.workflowName,
|
|
52
|
+
...(failureKind === "actionable" && { failedStep: jobInfo?.failedStep }),
|
|
40
53
|
};
|
|
41
54
|
}
|
|
42
|
-
|
|
55
|
+
function classifyConclusion(c) {
|
|
56
|
+
if (c === "TIMED_OUT")
|
|
57
|
+
return "timeout";
|
|
58
|
+
if (c === "CANCELLED" || c === "STARTUP_FAILURE" || c === "STALE")
|
|
59
|
+
return "cancelled";
|
|
60
|
+
return "actionable";
|
|
61
|
+
}
|
|
62
|
+
function fetchJobs(runId, repo, cache) {
|
|
63
|
+
const cached = cache.get(runId);
|
|
64
|
+
if (cached)
|
|
65
|
+
return cached;
|
|
66
|
+
const promise = fetchJobsUncached(runId, repo);
|
|
67
|
+
cache.set(runId, promise);
|
|
68
|
+
return promise;
|
|
69
|
+
}
|
|
70
|
+
async function fetchJobsUncached(runId, repo) {
|
|
71
|
+
const { owner, name } = repo;
|
|
72
|
+
const perPage = 100;
|
|
73
|
+
const MAX_JOB_PAGES = 20; // 2000 jobs max
|
|
74
|
+
let pagesFetched = 0;
|
|
75
|
+
const allJobs = [];
|
|
43
76
|
try {
|
|
44
|
-
const { owner, name } = repo;
|
|
45
|
-
const perPage = 100;
|
|
46
|
-
const allJobs = [];
|
|
47
77
|
for (let page = 1;; page++) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (jobsData.jobs.length < perPage)
|
|
78
|
+
if (++pagesFetched > MAX_JOB_PAGES) {
|
|
79
|
+
process.stderr.write(`pr-shepherd: job pagination cap (${MAX_JOB_PAGES * 100} jobs) reached for run ${runId} — triage may be incomplete\n`);
|
|
51
80
|
break;
|
|
52
|
-
}
|
|
53
|
-
const failedJobs = allJobs.filter((j) => ["failure", "timed_out", "cancelled"].includes(j.conclusion ?? ""));
|
|
54
|
-
if (failedJobs.length === 0)
|
|
55
|
-
return "";
|
|
56
|
-
const logParts = await Promise.all(failedJobs.map(async (job) => {
|
|
57
|
-
try {
|
|
58
|
-
// Job-level endpoint (jobs/{id}/logs) redirects to plain text, unlike
|
|
59
|
-
// run-level (runs/{id}/logs) which returns a ZIP archive.
|
|
60
|
-
const logs = await restText(`/repos/${owner}/${name}/actions/jobs/${job.id}/logs`);
|
|
61
|
-
return logs.trim() ? `===== job: ${job.name} =====\n${logs}` : "";
|
|
62
81
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const ansiEscapes = /\u001B\[[0-9;]*m/g;
|
|
69
|
-
return combined
|
|
70
|
-
.replace(ansiEscapes, "")
|
|
71
|
-
.split("\n")
|
|
72
|
-
.slice(-config.checks.logMaxLines)
|
|
73
|
-
.join("\n");
|
|
82
|
+
const data = await rest("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`);
|
|
83
|
+
allJobs.push(...data.jobs);
|
|
84
|
+
if (data.jobs.length < perPage)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
88
|
catch {
|
|
76
|
-
return
|
|
89
|
+
return undefined;
|
|
77
90
|
}
|
|
91
|
+
return allJobs;
|
|
78
92
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
93
|
+
function pickJobInfo(jobs, checkName, failureKind) {
|
|
94
|
+
// Match by name. For matrix jobs sharing a check name, prefer a failing one.
|
|
95
|
+
// Fall back to prefix matching for matrix jobs whose workflow-API name includes
|
|
96
|
+
// a suffix like "(ubuntu)" while checkName is just the base name.
|
|
97
|
+
const exactMatches = jobs.filter((j) => j.name === checkName);
|
|
98
|
+
const matchedJobs = exactMatches.length > 0 ? exactMatches : jobs.filter((j) => j.name.startsWith(checkName));
|
|
99
|
+
const job = matchedJobs.find((j) => j.conclusion === "failure") ?? matchedJobs[0];
|
|
100
|
+
if (!job)
|
|
101
|
+
return undefined;
|
|
102
|
+
const failedStep = failureKind === "actionable"
|
|
103
|
+
? job.steps?.find((s) => s.conclusion === "failure")?.name
|
|
104
|
+
: undefined;
|
|
105
|
+
return { workflowName: job.workflow_name, failedStep };
|
|
92
106
|
}
|