pr-shepherd 0.22.0 → 0.24.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 +87 -125
- package/bin/cli/args.mjs +2 -0
- package/bin/cli/fix-formatter.mjs +3 -1
- package/bin/cli/formatters.mjs +3 -45
- package/bin/cli/handlers.mjs +22 -1
- package/bin/cli/help-command-pages.mjs +27 -2
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/iterate-instructions.mjs +1 -1
- package/bin/cli/list-formatters.mjs +5 -2
- package/bin/cli/mark-files-as-viewed-flags.mjs +34 -0
- package/bin/cli/mark-files-as-viewed-formatter.mjs +52 -0
- package/bin/cli/mutate-formatter.mjs +50 -0
- package/bin/cli-parser.mjs +11 -2
- package/bin/commands/check.mjs +25 -56
- package/bin/commands/commit-suggestion-instruction.mjs +1 -1
- package/bin/commands/iterate/classify.mjs +32 -31
- package/bin/commands/iterate/fix-code.mjs +23 -19
- package/bin/commands/iterate/index.mjs +4 -1
- package/bin/commands/iterate/render.mjs +6 -3
- package/bin/commands/iterate/stall.mjs +1 -1
- package/bin/commands/mark-files-as-viewed.mjs +220 -0
- package/bin/commands/resolve-instructions.mjs +5 -2
- package/bin/commands/resolve-mutate.mjs +49 -4
- package/bin/commands/resolve.mjs +21 -78
- package/bin/commands/shepherd-journal.mjs +1 -1
- package/bin/comments/authors.mjs +32 -0
- package/bin/comments/minimize-policy.mjs +8 -4
- package/bin/comments/pending-ops.mjs +6 -0
- package/bin/comments/resolve.mjs +28 -11
- package/bin/comments/review-thread-markers.mjs +18 -0
- package/bin/comments/review-visibility.mjs +14 -0
- package/bin/comments/thread-visibility.mjs +60 -0
- package/bin/comments/visible-comments.mjs +2 -2
- package/bin/config/load.mjs +7 -0
- package/bin/config.json +14 -0
- package/bin/github/batch-parser-helpers.mjs +3 -4
- package/bin/github/batch-parsers.mjs +8 -6
- package/bin/github/client.mjs +1 -2
- package/bin/github/gql/batch-pr.gql +3 -0
- package/bin/reporters/agent.mjs +2 -0
- package/bin/state/seen-comments.mjs +40 -4
- package/bin/threads/transcript.mjs +6 -6
- package/package.json +8 -4
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +31 -0
- package/bin/checks/triage.test-support.mjs +0 -60
- package/bin/cli/iterate-lean.test-support.mjs +0 -3
- package/bin/cli-parser.clean.test-support.mjs +0 -44
- package/bin/cli-parser.commit-suggestion.test-support.mjs +0 -65
- package/bin/cli-parser.iterate-fix.test-support.mjs +0 -43
- package/bin/cli-parser.iterate-fixtures.mjs +0 -75
- package/bin/cli-parser.iterate.test-support.mjs +0 -44
- package/bin/cli-parser.test-support.mjs +0 -42
- package/bin/commands/check.test-support.mjs +0 -147
- package/bin/commands/clean.test-support.mjs +0 -47
- package/bin/commands/commit-suggestion.apply.test-support.mjs +0 -87
- package/bin/commands/commit-suggestion.test-support.mjs +0 -112
- package/bin/commands/iterate-stall.test-support.mjs +0 -24
- package/bin/commands/iterate-test-support.mjs +0 -150
- package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +0 -123
- package/bin/commands/poll.test-support.mjs +0 -77
- package/bin/commands/resolve.test-support.mjs +0 -114
- package/bin/commands/shepherd-journal.test-support.mjs +0 -7
- package/bin/comments/outdated.mjs +0 -15
- package/bin/comments/resolve.test-support.mjs +0 -39
- package/bin/github/batch-parsers.test-support.mjs +0 -66
- package/bin/github/batch.test-support.mjs +0 -66
- package/bin/github/client.test-support.mjs +0 -55
- package/bin/github/http.test-support.mjs +0 -51
- package/bin/state/seen-comments.test-support.mjs +0 -19
- package/bin/suggestions/patch.test-support.mjs +0 -2
package/README.md
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
# pr-shepherd
|
|
2
2
|
|
|
3
3
|
Autonomous PR CI monitor and review-comment resolver for agentic coding tools, including Claude Code and Codex.
|
|
4
|
-
The goal is to have an agent take a plan to a human-reviewable PR autonomously.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
The goal is to help an agent carry a planned change to a human-reviewable PR: passing CI, no unresolved Shepherd-visible work, and a useful PR description/journal.
|
|
7
6
|
|
|
8
|
-
|
|
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:pr-shepherd`
|
|
13
|
-
6. Agent makes a draft PR
|
|
14
|
-
7. PR has passing CI -> draft is marked Ready for Review
|
|
15
|
-
8. Review bots begin providing reviews
|
|
16
|
-
9. Agent automatically classifies and fixes review comments based on the Plan
|
|
17
|
-
10. Human reviews PR with a well-documented PR title and description, passing CI, and no open threads/comments/reviews
|
|
7
|
+
## How It Works
|
|
18
8
|
|
|
19
|
-
|
|
9
|
+
`pr-shepherd` moves deterministic PR orchestration into a CLI. The CLI fetches GitHub state, emits raw-enough context, and prints a numbered `## Instructions` section for the calling agent to follow. The agent still decides whether a comment or CI failure requires a code change.
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
The shipped skills invoke the default poll dispatcher (`pr-shepherd <PR>`, equivalent to `pr-shepherd poll <PR>`). Use `pr-shepherd iterate <PR>` for one single tick.
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
Each tick returns exactly one action:
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
- `WAIT` — no immediate action; poll can recheck until timeout.
|
|
16
|
+
- `MARK_READY` — the CLI already converted an eligible draft PR to ready for review.
|
|
17
|
+
- `FIX_CODE` — review items, failing CI, conflicts, or minimization work need agent action.
|
|
18
|
+
- `CANCEL` — terminal success for merged/closed PRs or elapsed ready-delay.
|
|
19
|
+
- `ESCALATE` — manual direction is needed.
|
|
20
|
+
|
|
21
|
+
Example shape:
|
|
26
22
|
|
|
27
23
|
```text
|
|
28
24
|
> pr-shepherd 123
|
|
@@ -36,205 +32,171 @@ At a high level, the skill invokes `pr-shepherd <PR>` (which polls by default),
|
|
|
36
32
|
|
|
37
33
|
### `threadId=PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate/index.mts:42` (@alice)
|
|
38
34
|
|
|
39
|
-
#### `commentId=PRRC_kwDOSGizTs58XB1M` (@alice)
|
|
40
|
-
|
|
41
35
|
> The variable name is misleading.
|
|
42
|
-
>
|
|
43
|
-
> Consider renaming `x` to `remainingSeconds` so readers don't have to
|
|
44
|
-
> trace back to the declaration to understand its meaning.
|
|
45
36
|
|
|
46
37
|
## Failing checks
|
|
47
38
|
|
|
48
|
-
- `24697658766` — `CI › lint / typecheck / test (22.x)`
|
|
39
|
+
- `24697658766` — `CI › lint / typecheck / test (22.x)` [conclusion: FAILURE]
|
|
49
40
|
> oxfmt
|
|
50
41
|
|
|
51
42
|
## Post-fix push
|
|
52
43
|
|
|
53
44
|
- base: `main`
|
|
54
|
-
- resolve: `pr-shepherd resolve 123 --
|
|
45
|
+
- resolve: `pr-shepherd resolve 123 --reply-thread-ids PRRT_kwDOSGizTs58XB1L --message "$DISMISS_MESSAGE" --require-sha "$HEAD_SHA"`
|
|
55
46
|
|
|
56
47
|
## Instructions
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
3. Commit changed files.
|
|
63
|
-
4. Rebase and push: `git fetch origin && git rebase origin/main && git push --force-with-lease` — capture `HEAD_SHA=$(git rev-parse HEAD)`.
|
|
64
|
-
5. Run the `resolve:` command above, substituting `"$HEAD_SHA"`.
|
|
65
|
-
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.
|
|
66
|
-
7. CI needs time to run on the new push. Run `pr-shepherd 123` again to continue until Shepherd returns `[CANCEL]` or `[ESCALATE]`.
|
|
49
|
+
1. Decide for each item under `## Review threads` and `## Failing checks` whether a code change is warranted. If code changes are needed, apply edits, commit, rebase/push according to the repository's conventions, then run the `resolve:` command.
|
|
50
|
+
2. For each failing check under `## Failing checks`: fetch logs when needed and decide whether to rerun or fix.
|
|
51
|
+
3. Run the `resolve:` command shown above with a specific `$DISMISS_MESSAGE` and the relevant `$HEAD_SHA`.
|
|
52
|
+
4. Stop this iteration.
|
|
67
53
|
```
|
|
68
54
|
|
|
69
|
-
|
|
55
|
+
See [docs/actions.md](docs/actions.md) for the complete output contract.
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
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"
|
|
73
|
-
```
|
|
57
|
+
## Workflow Assumptions
|
|
74
58
|
|
|
75
|
-
|
|
59
|
+
This system is opinionated and works best with PRs that use required status checks and conversation resolution.
|
|
76
60
|
|
|
77
|
-
|
|
61
|
+
- Human-authored threads are replied to, not resolved or minimized by Shepherd.
|
|
62
|
+
- Detected bots and configured `botUsernames` review threads are returned until resolved; bot/non-human threads, PR comments, and review summaries can be resolved or minimized when eligible. Review summaries are not minimized while known inline child threads from that review remain unresolved.
|
|
63
|
+
- Agents must not reply to their own latest thread reply; generated instructions call this out before `--reply-thread-ids` mutations.
|
|
64
|
+
- Every review thread/comment/review summary is surfaced at least once, even if already outdated, resolved, or minimized; edited items re-surface through seen markers.
|
|
65
|
+
- Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
|
|
66
|
+
- The CLI never performs git mutations. It emits instructions; the caller commits, rebases, pushes, and handles repository hooks.
|
|
67
|
+
- `commit-suggestion` turns one GitHub suggestion thread into a patch and commit instructions, but still does not edit the working tree or git history.
|
|
78
68
|
|
|
79
|
-
|
|
80
|
-
- There are required status checks
|
|
81
|
-
- All inline comments are resolved
|
|
82
|
-
- **ALL** comments/threads/reviews will be hidden by default except for PR approvals. The only option here is to hide PR approvals as well.
|
|
83
|
-
- The primary reason is to optimize tokens by avoiding re-fetching comments and re-adding them to the agent's context.
|
|
84
|
-
- This also ties hand-in-hand with requiring all inline comments to be resolved.
|
|
85
|
-
- We also want to avoid storing state as comments can be unresolved/minimized/hidden.
|
|
86
|
-
- `pr-shepherd` keeps the PR title and description up to date, including a journal of decisions with links to comments/threads/reviews (that would be hidden at this point).
|
|
87
|
-
- This may break your workflow if your PR titles and descriptions are restricted to a specific format.
|
|
88
|
-
- `pr-shepherd` does **NOT** reply to inline comments when resolving them. Doing so would require agentic loops and more tokens. Instead, it updates the PR title & description once per loop with only the relevant information.
|
|
89
|
-
- Branches are currently kept up-to-date with `git push --force-with-lease`. Please make a PR for making `merge <default branch>` an option.
|
|
90
|
-
- Branches are currently only rebased when 1) pushing a commit on a branch that is out of date or 2) there are merge conflicts. It does not continuously rebase the branch (use a merge queue for that).
|
|
91
|
-
- To optimize AI code reviewer tokens, create your pull requests initially as drafts and instruct your AI code reviewers to only code review PRs that are ready for review. `pr-shepherd` will automatically mark PRs as ready for review when all CI passes (can be disabled). If you have no intention of marking your PR as ready for review, then don't run `pr-shepherd`.
|
|
69
|
+
## Usage
|
|
92
70
|
|
|
93
|
-
|
|
71
|
+
### Iterate A PR
|
|
94
72
|
|
|
95
|
-
|
|
96
|
-
- `pr-shepherd` waits 10 minutes (configurable) until after all comments are hidden and CI passes before exiting. The primary reason is to wait for any lingering automated code reviews that do not provide status updates via the GitHub GraphQL API.
|
|
97
|
-
- The agent is instructed to cancel failed CI runs and, when a failure looks transient (e.g. network timeout, runner setup crash), re-run them via `gh run rerun <id> --failed`. The primary reason is to minimize CI costs.
|
|
98
|
-
- `pr-shepherd` supports "commit suggestions" by converting them into a diff, applying them, and then committing them with attribution. This avoids a file read & write. One commit is always made per suggestion to avoid any merge conflicts — in these cases, the agent will resolve the comment manually.
|
|
73
|
+
Claude Code:
|
|
99
74
|
|
|
100
|
-
|
|
75
|
+
```text
|
|
76
|
+
/goal /pr-shepherd:pr-shepherd # infer PR from current branch
|
|
77
|
+
/goal /pr-shepherd:pr-shepherd 42
|
|
78
|
+
```
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
- 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.
|
|
104
|
-
- 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.
|
|
105
|
-
- Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
|
|
80
|
+
Codex:
|
|
106
81
|
|
|
107
|
-
|
|
82
|
+
```text
|
|
83
|
+
/goal $pr-shepherd # infer PR from current branch
|
|
84
|
+
/goal $pr-shepherd 42
|
|
85
|
+
```
|
|
108
86
|
|
|
109
|
-
|
|
110
|
-
- **Reduced GitHub rate-limit exposure** — GraphQL requests are batched when possible
|
|
111
|
-
- **Minimal state** — `pr-shepherd` stores minimal state in `$PR_SHEPHERD_STATE_DIR` (default `$TMPDIR/pr-shepherd-state/`), not in the repository
|
|
112
|
-
- **Classifications and decisions still happen at the agent level** — `pr-shepherd`'s goal is to provide sufficient context to make informed decisions and provide clear actionable steps without writing unreliable code-level heuristics
|
|
113
|
-
- **Configurable** — `pr-shepherd` is configurable via `.pr-shepherdrc.yml`, which is only possible with a light prompt that simply invokes the CLI which returns the prompt.
|
|
87
|
+
Direct CLI:
|
|
114
88
|
|
|
115
|
-
|
|
89
|
+
```sh
|
|
90
|
+
pr-shepherd 42 # poll until non-WAIT or timeout
|
|
91
|
+
pr-shepherd 42 --interval 45s --timeout 4m
|
|
92
|
+
pr-shepherd 42 --ready-delay 15m
|
|
93
|
+
pr-shepherd iterate 42 # single tick
|
|
94
|
+
pr-shepherd poll 42 # explicit poll command
|
|
95
|
+
```
|
|
116
96
|
|
|
117
|
-
###
|
|
97
|
+
### Resolve Review Items
|
|
118
98
|
|
|
119
|
-
|
|
99
|
+
```sh
|
|
100
|
+
pr-shepherd resolve 42 --fetch
|
|
101
|
+
pr-shepherd resolve 42 --reply-thread-ids PRRT_abc --message "Renamed the variable for clarity." --require-sha "$(git rev-parse HEAD)"
|
|
102
|
+
```
|
|
120
103
|
|
|
121
|
-
|
|
104
|
+
### Apply One Suggestion Thread
|
|
122
105
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
/pr-shepherd:pr-shepherd 42
|
|
126
|
-
/pr-shepherd:pr-shepherd 42 --ready-delay 15m
|
|
106
|
+
```sh
|
|
107
|
+
pr-shepherd commit-suggestion 42 --thread-id PRRT_abc --message "rename value for clarity"
|
|
127
108
|
```
|
|
128
109
|
|
|
129
|
-
|
|
110
|
+
The output contains a diff and numbered instructions for applying, staging, committing, resolving, and pushing.
|
|
111
|
+
|
|
112
|
+
### Mark Files As Viewed
|
|
130
113
|
|
|
131
114
|
```sh
|
|
132
|
-
pr-shepherd 42
|
|
133
|
-
pr-shepherd 42
|
|
134
|
-
pr-shepherd 42 --ready-delay 15m
|
|
135
|
-
pr-shepherd iterate 42 # single tick
|
|
115
|
+
pr-shepherd mark-files-as-viewed 42 --tests
|
|
116
|
+
pr-shepherd mark-files-as-viewed 42 src/a.ts --match '^docs/'
|
|
136
117
|
```
|
|
137
118
|
|
|
138
|
-
|
|
119
|
+
The shipped `mark-files-as-viewed` skill maps a standalone `tests` argument to `--tests`.
|
|
139
120
|
|
|
140
|
-
|
|
121
|
+
### Clean Local State
|
|
141
122
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
`pr-shepherd` accumulates state under `$PR_SHEPHERD_STATE_DIR` (seen markers, fix-attempt counters, stall fingerprints, etc.). To reset it:
|
|
123
|
+
`pr-shepherd` stores seen markers, fix-attempt counters, stall fingerprints, ready-delay markers, and logs under `$PR_SHEPHERD_STATE_DIR` (default `$TMPDIR/pr-shepherd-state`).
|
|
145
124
|
|
|
146
125
|
```sh
|
|
147
|
-
pr-shepherd clean current
|
|
148
|
-
pr-shepherd clean repo
|
|
149
|
-
pr-shepherd clean all
|
|
126
|
+
pr-shepherd clean current
|
|
127
|
+
pr-shepherd clean repo
|
|
128
|
+
pr-shepherd clean all --dry-run
|
|
129
|
+
pr-shepherd log-file
|
|
150
130
|
```
|
|
151
131
|
|
|
152
|
-
Add `--dry-run` to preview what would be removed. See [docs/cli-usage.md](docs/cli-usage.md) for the full `clean` reference.
|
|
153
|
-
|
|
154
132
|
## Install
|
|
155
133
|
|
|
156
|
-
|
|
134
|
+
Skill and plugin install methods add the skill definitions only. Install the `pr-shepherd` CLI separately wherever the skill runs.
|
|
157
135
|
|
|
158
136
|
### Claude Code
|
|
159
137
|
|
|
160
|
-
Install as a Claude Code plugin:
|
|
161
|
-
|
|
162
138
|
```bash
|
|
163
139
|
claude /plugin marketplace add jonathanong/pr-shepherd
|
|
164
140
|
claude /plugin install pr-shepherd
|
|
165
141
|
```
|
|
166
142
|
|
|
167
|
-
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.
|
|
168
|
-
|
|
169
143
|
### Codex
|
|
170
144
|
|
|
171
|
-
Codex uses the repo-shipped Codex plugin rather than the Claude plugin or `/pr-shepherd:*` slash commands. The plugin provides one `pr-shepherd` skill for iterating a PR to completion.
|
|
172
|
-
|
|
173
|
-
Install the Codex plugin marketplace from GitHub:
|
|
174
|
-
|
|
175
145
|
```bash
|
|
176
146
|
codex plugin marketplace add jonathanong/pr-shepherd
|
|
177
147
|
```
|
|
178
148
|
|
|
179
|
-
Or pin a
|
|
149
|
+
Or pin a ref:
|
|
180
150
|
|
|
181
151
|
```bash
|
|
182
152
|
codex plugin marketplace add jonathanong/pr-shepherd --ref main
|
|
183
153
|
```
|
|
184
154
|
|
|
185
|
-
For local development
|
|
155
|
+
For local development:
|
|
186
156
|
|
|
187
157
|
```bash
|
|
188
158
|
git clone https://github.com/jonathanong/pr-shepherd ~/.codex/plugin-sources/pr-shepherd
|
|
189
159
|
codex plugin marketplace add ~/.codex/plugin-sources/pr-shepherd
|
|
190
160
|
```
|
|
191
161
|
|
|
192
|
-
After adding the marketplace,
|
|
193
|
-
|
|
194
|
-
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.
|
|
195
|
-
|
|
196
|
-
Iterate a PR from Codex:
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
pr-shepherd iterate 42
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
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 or CI that never starts). `pr-shepherd iterate 42` remains supported for existing workflows.
|
|
162
|
+
After adding the marketplace, install/enable the `pr-shepherd` plugin from Codex. The marketplace root must contain `.agents/plugins/marketplace.json` and `plugins/pr-shepherd/`.
|
|
203
163
|
|
|
204
164
|
## Configuration
|
|
205
165
|
|
|
206
|
-
Create
|
|
166
|
+
Create `.pr-shepherdrc.yml` in your project root or an ancestor directory.
|
|
207
167
|
|
|
208
168
|
```yaml
|
|
209
169
|
iterate:
|
|
210
|
-
fixAttemptsPerThread: 5
|
|
170
|
+
fixAttemptsPerThread: 5
|
|
171
|
+
stallTimeoutMinutes: 60
|
|
172
|
+
minimizeApprovals: false
|
|
211
173
|
minimizeComments: all # all | bots | users | none
|
|
212
174
|
checks:
|
|
213
175
|
ciTriggerEvents:
|
|
214
176
|
- pull_request
|
|
215
177
|
- pull_request_target
|
|
216
|
-
- merge_group
|
|
178
|
+
- merge_group
|
|
217
179
|
actions:
|
|
218
|
-
autoMarkReady: false
|
|
180
|
+
autoMarkReady: false
|
|
219
181
|
```
|
|
220
182
|
|
|
221
|
-
Environment variables:
|
|
183
|
+
Environment variables:
|
|
184
|
+
|
|
185
|
+
- `GH_TOKEN` / `GITHUB_TOKEN` / `GITHUB_PERSONAL_ACCESS_TOKEN` for auth; `gh auth token` is used as a fallback.
|
|
186
|
+
- `PR_SHEPHERD_STATE_DIR` to override state and log location.
|
|
187
|
+
- `PR_SHEPHERD_LOG_DISABLED=1` to disable per-worktree debug logging.
|
|
222
188
|
|
|
223
|
-
See [docs/configuration.md](docs/configuration.md) for full
|
|
189
|
+
See [docs/configuration.md](docs/configuration.md) for the full reference.
|
|
224
190
|
|
|
225
191
|
## Requirements
|
|
226
192
|
|
|
227
|
-
- Node.js
|
|
228
|
-
- A GitHub token
|
|
193
|
+
- Node.js >= 22.0.0
|
|
194
|
+
- A GitHub token or authenticated `gh` CLI; private repositories require `repo` scope.
|
|
229
195
|
- `git`
|
|
230
196
|
|
|
231
197
|
## Docs
|
|
232
198
|
|
|
233
|
-
Full reference: [docs/README.md](docs/README.md)
|
|
234
|
-
|
|
235
|
-
## Architecture
|
|
236
|
-
|
|
237
|
-
See [docs/architecture.md](docs/architecture.md) for the module map and dependency rules.
|
|
199
|
+
Full reference: [docs/README.md](docs/README.md).
|
|
238
200
|
|
|
239
201
|
## License
|
|
240
202
|
|
package/bin/cli/args.mjs
CHANGED
|
@@ -13,10 +13,12 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
13
13
|
"--description",
|
|
14
14
|
"--thread-id",
|
|
15
15
|
"--resolve-thread-ids",
|
|
16
|
+
"--reply-thread-ids",
|
|
16
17
|
"--minimize-comment-ids",
|
|
17
18
|
"--dismiss-review-ids",
|
|
18
19
|
"--interval",
|
|
19
20
|
"--timeout",
|
|
21
|
+
"--match",
|
|
20
22
|
]);
|
|
21
23
|
// Boolean flags that do NOT consume the next argument. Any --flag not in this
|
|
22
24
|
// set and not in FLAGS_WITH_VALUES is treated conservatively as value-taking
|
|
@@ -12,8 +12,10 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
12
12
|
const lineLabel = renderLineRange(t.startLine, t.line);
|
|
13
13
|
const loc = t.path ? `\`${t.path}:${lineLabel}\`` : "(no location)";
|
|
14
14
|
const heading = t.url ? `[threadId=${t.id}](${t.url})` : `\`threadId=${t.id}\``;
|
|
15
|
+
const reviewMarker = t.reviewId ? ` [reviewId=${t.reviewId}]` : "";
|
|
15
16
|
const suggestionMarker = t.suggestion ? " [suggestion]" : "";
|
|
16
|
-
|
|
17
|
+
const editedMarker = t.edited ? " [edited since first look]" : "";
|
|
18
|
+
sections.push(`### ${heading} — ${loc} (${renderAuthor(t.author, t.authorType)})${reviewMarker}${suggestionMarker}${editedMarker}`);
|
|
17
19
|
sections.push(renderThreadConversation(t));
|
|
18
20
|
if (t.suggestion) {
|
|
19
21
|
sections.push(renderSuggestionBlock(t.suggestion, ""));
|
package/bin/cli/formatters.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { formatIterateResult } from "./iterate-formatter.mjs";
|
|
2
2
|
export { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
|
|
3
3
|
export { formatCleanResult } from "./clean-formatter.mjs";
|
|
4
|
+
export { formatMarkFilesAsViewedResult } from "./mark-files-as-viewed-formatter.mjs";
|
|
5
|
+
export { formatMutateResult } from "./mutate-formatter.mjs";
|
|
4
6
|
import { safeFence } from "./fence.mjs";
|
|
5
7
|
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderThreadResolutionStatusTag, buildFirstLookBullets, renderEditedCommentTag, } from "./list-formatters.mjs";
|
|
6
8
|
import { joinSections } from "../util/markdown.mjs";
|
|
@@ -79,12 +81,7 @@ export function formatCommitSuggestionResult(result) {
|
|
|
79
81
|
lines.push(result.patch.trimEnd());
|
|
80
82
|
lines.push(fence);
|
|
81
83
|
}
|
|
82
|
-
lines.push("");
|
|
83
|
-
lines.push("## Suggested commit message");
|
|
84
|
-
lines.push("");
|
|
85
|
-
lines.push(result.commitMessage);
|
|
86
|
-
lines.push("");
|
|
87
|
-
lines.push(result.commitBody);
|
|
84
|
+
lines.push("", "## Suggested commit message", "", result.commitMessage, "", result.commitBody);
|
|
88
85
|
if (result.postActionInstructions.length > 0) {
|
|
89
86
|
lines.push("");
|
|
90
87
|
lines.push("## Instructions");
|
|
@@ -95,42 +92,3 @@ export function formatCommitSuggestionResult(result) {
|
|
|
95
92
|
}
|
|
96
93
|
return lines.join("\n");
|
|
97
94
|
}
|
|
98
|
-
export function formatMutateResult(result) {
|
|
99
|
-
const lines = [];
|
|
100
|
-
if (result.resolvedThreads.length)
|
|
101
|
-
lines.push(`Resolved threads (${result.resolvedThreads.length}): ${result.resolvedThreads.join(", ")}`);
|
|
102
|
-
if (result.minimizedComments.length)
|
|
103
|
-
lines.push(`Minimized comments (${result.minimizedComments.length}): ${result.minimizedComments.join(", ")}`);
|
|
104
|
-
if (result.dismissedReviews.length)
|
|
105
|
-
lines.push(`Dismissed reviews (${result.dismissedReviews.length}): ${result.dismissedReviews.join(", ")}`);
|
|
106
|
-
if (result.skippedDismissals?.length)
|
|
107
|
-
lines.push(`Skipped dismissals (${result.skippedDismissals.length}): ${result.skippedDismissals.join(", ")}`);
|
|
108
|
-
if (result.rateLimit) {
|
|
109
|
-
const details = [
|
|
110
|
-
result.rateLimit.retryAfterSeconds !== undefined
|
|
111
|
-
? `retry after ${result.rateLimit.retryAfterSeconds}s`
|
|
112
|
-
: null,
|
|
113
|
-
result.rateLimit.remaining !== undefined && result.rateLimit.limit !== undefined
|
|
114
|
-
? `remaining ${result.rateLimit.remaining}/${result.rateLimit.limit}`
|
|
115
|
-
: null,
|
|
116
|
-
result.rateLimit.resetAt !== undefined
|
|
117
|
-
? `reset at ${new Date(result.rateLimit.resetAt * 1000).toISOString()}`
|
|
118
|
-
: null,
|
|
119
|
-
]
|
|
120
|
-
.filter(Boolean)
|
|
121
|
-
.join(", ");
|
|
122
|
-
lines.push(`Stopped: GitHub rate limit hit — ${result.rateLimit.message}${details ? ` (${details})` : ""}`);
|
|
123
|
-
}
|
|
124
|
-
if (result.unresolvedThreads?.length)
|
|
125
|
-
lines.push(`Not resolved due to rate limit (${result.unresolvedThreads.length}): ${result.unresolvedThreads.join(", ")}`);
|
|
126
|
-
if (result.unminimizedComments?.length)
|
|
127
|
-
lines.push(`Not minimized due to rate limit (${result.unminimizedComments.length}): ${result.unminimizedComments.join(", ")}`);
|
|
128
|
-
if (result.undismissedReviews?.length)
|
|
129
|
-
lines.push(`Not dismissed due to rate limit (${result.undismissedReviews.length}): ${result.undismissedReviews.join(", ")}`);
|
|
130
|
-
const errors = result.rateLimit
|
|
131
|
-
? result.errors.filter((e) => !e.startsWith("rate limit:"))
|
|
132
|
-
: result.errors;
|
|
133
|
-
if (errors.length)
|
|
134
|
-
lines.push(`Errors:\n ${errors.join("\n ")}`);
|
|
135
|
-
return lines.join("\n");
|
|
136
|
-
}
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { runCommitSuggestion } from "../commands/commit-suggestion.mjs";
|
|
2
|
+
import { runMarkFilesAsViewed } from "../commands/mark-files-as-viewed.mjs";
|
|
2
3
|
import { runIterate } from "../commands/iterate/index.mjs";
|
|
3
4
|
import { runClean } from "../commands/clean.mjs";
|
|
4
5
|
import { loadConfig } from "../config/load.mjs";
|
|
5
6
|
import { parseCommonArgs, getFlag } from "./args.mjs";
|
|
6
7
|
import { USAGE } from "./help.mjs";
|
|
7
|
-
import { formatCommitSuggestionResult, formatCleanResult } from "./formatters.mjs";
|
|
8
|
+
import { formatCommitSuggestionResult, formatCleanResult, formatMarkFilesAsViewedResult, } from "./formatters.mjs";
|
|
8
9
|
import { parseIterateFlags } from "./iterate-flags.mjs";
|
|
9
10
|
import { emitIterateResult } from "./iterate-emitter.mjs";
|
|
11
|
+
import { parseMarkFilesAsViewedArgs } from "./mark-files-as-viewed-flags.mjs";
|
|
10
12
|
const CLEAN_VARIANTS = new Set(["pr", "branch", "current", "repo", "all"]);
|
|
11
13
|
export async function handleClean(args) {
|
|
12
14
|
const variant = args[0];
|
|
@@ -108,3 +110,22 @@ export async function handleIterate(args) {
|
|
|
108
110
|
readyDelaySuffix: flags.readyDelaySuffix ?? undefined,
|
|
109
111
|
});
|
|
110
112
|
}
|
|
113
|
+
export async function handleMarkFilesAsViewed(args) {
|
|
114
|
+
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
115
|
+
const parsed = parseMarkFilesAsViewedArgs(extra);
|
|
116
|
+
if (!parsed.ok) {
|
|
117
|
+
process.stderr.write(`pr-shepherd: mark-files-as-viewed: ${parsed.error}\n`);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const result = await runMarkFilesAsViewed({
|
|
122
|
+
...globalOpts,
|
|
123
|
+
prNumber,
|
|
124
|
+
files: parsed.files,
|
|
125
|
+
tests: parsed.tests,
|
|
126
|
+
matchPatterns: parsed.matchPatterns,
|
|
127
|
+
});
|
|
128
|
+
process.stdout.write(globalOpts.format === "json"
|
|
129
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
130
|
+
: `${formatMarkFilesAsViewedResult(result)}\n`);
|
|
131
|
+
}
|
|
@@ -5,6 +5,7 @@ Fetch actionable review items or apply GitHub review-state mutations after fixes
|
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
pr-shepherd resolve [PR] [--fetch] [--format text|json]
|
|
8
|
+
pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
|
|
8
9
|
pr-shepherd resolve [PR] --resolve-thread-ids A,B [--minimize-comment-ids X,Y]
|
|
9
10
|
[--dismiss-review-ids Q] [--message MSG]
|
|
10
11
|
[--require-sha SHA] [--format text|json]
|
|
@@ -13,14 +14,18 @@ Modes:
|
|
|
13
14
|
fetch Default when no mutation IDs are provided. Fetches review threads,
|
|
14
15
|
comments, first-look items, and instructions.
|
|
15
16
|
mutate Runs when any mutation ID flag is present. Resolves threads,
|
|
16
|
-
|
|
17
|
+
replies to human threads, resolves non-human/manual thread IDs,
|
|
18
|
+
minimizes non-human comments/review summaries, and dismisses non-human reviews.
|
|
17
19
|
|
|
18
20
|
Flags:
|
|
19
21
|
--fetch Force fetch mode.
|
|
20
22
|
--resolve-thread-ids <ids> Comma-separated review thread IDs to resolve.
|
|
23
|
+
Human-authored thread IDs are skipped; use --reply-thread-ids.
|
|
24
|
+
--reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
|
|
21
25
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
22
26
|
--dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
|
|
23
|
-
--message <text>
|
|
27
|
+
--message <text> Reply/dismiss message. Required with --reply-thread-ids
|
|
28
|
+
or --dismiss-review-ids.
|
|
24
29
|
--require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
|
|
25
30
|
--format text|json Output format. Default: text.
|
|
26
31
|
--help, -h Print this help and exit before GitHub I/O.
|
|
@@ -49,6 +54,26 @@ Preconditions:
|
|
|
49
54
|
Exit codes:
|
|
50
55
|
0 suggestion patch and instructions produced
|
|
51
56
|
1 validation, lookup, precondition, or suggestion parsing failure`,
|
|
57
|
+
"mark-files-as-viewed": `pr-shepherd mark-files-as-viewed
|
|
58
|
+
|
|
59
|
+
Mark changed files as viewed in the GitHub pull request diff.
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
pr-shepherd mark-files-as-viewed [PR] [files...] [--tests] [--match REGEX]
|
|
63
|
+
[--format text|json]
|
|
64
|
+
|
|
65
|
+
Selectors:
|
|
66
|
+
files... Exact changed-file paths from the PR diff.
|
|
67
|
+
--tests Select changed test files.
|
|
68
|
+
--match <regex> Select changed files whose paths match a case-insensitive JavaScript regex.
|
|
69
|
+
May be repeated.
|
|
70
|
+
|
|
71
|
+
Flags:
|
|
72
|
+
--format text|json Output format. Default: text.
|
|
73
|
+
--help, -h Print this help and exit before GitHub I/O.
|
|
74
|
+
|
|
75
|
+
PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
|
|
76
|
+
Exit code: 0 on success; 1 on validation or lookup failure.`,
|
|
52
77
|
iterate: `pr-shepherd iterate
|
|
53
78
|
|
|
54
79
|
Run one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.
|
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
pr-shepherd poll [PR] [poll-flags] [iterate-flags]
|
|
11
11
|
pr-shepherd resolve [PR] [resolve-flags]
|
|
12
12
|
pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [flags]
|
|
13
|
+
pr-shepherd mark-files-as-viewed [PR] [files...] [--tests] [--match REGEX]
|
|
13
14
|
pr-shepherd clean <pr|branch|current|repo|all> [value] [flags]
|
|
14
15
|
pr-shepherd log-file [--format text|json]
|
|
15
16
|
|
|
@@ -19,6 +20,7 @@ Commands:
|
|
|
19
20
|
poll Re-run iterate while the action is WAIT, then print the final tick.
|
|
20
21
|
resolve Fetch actionable review items, or resolve/minimize/dismiss IDs.
|
|
21
22
|
commit-suggestion Convert one GitHub suggestion thread into a patch and commit instructions.
|
|
23
|
+
mark-files-as-viewed Mark PR changed files as viewed in GitHub.
|
|
22
24
|
clean Remove pr-shepherd state files.
|
|
23
25
|
log-file Print the per-worktree debug log path.
|
|
24
26
|
|
|
@@ -32,7 +32,7 @@ export function adaptFixCodeInstructions(instructions, pr, readyDelaySuffix) {
|
|
|
32
32
|
export function adaptIterateLog(log) {
|
|
33
33
|
return log.replace(/\s+—\s+\d+s until auto-cancel/g, "");
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
function buildIterateCommand(pr, readyDelaySuffix) {
|
|
36
36
|
const suffix = readyDelaySuffix?.trim();
|
|
37
37
|
return buildPrShepherdCommand([String(pr), ...(suffix ? ["--ready-delay", suffix] : [])]).text;
|
|
38
38
|
}
|
|
@@ -9,7 +9,7 @@ export function renderBodyPreview(body) {
|
|
|
9
9
|
const firstLine = normalizedBody.split("\n")[0]?.trim() ?? "";
|
|
10
10
|
return firstLine.slice(0, BODY_PREVIEW_MAX);
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
function renderFirstLookStatusTag(t) {
|
|
13
13
|
const editedSuffix = t.edited ? ", edited" : "";
|
|
14
14
|
return t.autoResolved
|
|
15
15
|
? `[status: outdated, auto-resolved${editedSuffix}]`
|
|
@@ -27,8 +27,10 @@ export function renderThreadBullet(t, opts = {}) {
|
|
|
27
27
|
? `\`${t.path}:${renderLineRange(t.startLine ?? undefined, t.line ?? null)}\``
|
|
28
28
|
: "`(no location)`";
|
|
29
29
|
const suggestionMarker = t.suggestion ? " [suggestion]" : "";
|
|
30
|
+
const editedMarker = t.edited && !opts.suppressEditedMarker ? " [edited since first look]" : "";
|
|
31
|
+
const reviewMarker = t.reviewId ? ` [reviewId=${t.reviewId}]` : "";
|
|
30
32
|
const statusSuffix = opts.statusTag ? ` ${opts.statusTag}` : "";
|
|
31
|
-
const bulletLine = `- \`threadId=${t.id}\`${link} ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}${statusSuffix}`;
|
|
33
|
+
const bulletLine = `- \`threadId=${t.id}\`${link} ${loc} (${renderAuthor(t.author, t.authorType)})${reviewMarker}${suggestionMarker}${editedMarker}${statusSuffix}`;
|
|
32
34
|
if (!opts.noBody && (!t.comments || t.comments.length === 0)) {
|
|
33
35
|
const legacyLine = `${bulletLine}: ${renderBodyPreview(t.body)}`;
|
|
34
36
|
return t.suggestion && opts.renderSuggestion
|
|
@@ -111,6 +113,7 @@ export function buildFirstLookBullets(firstLookThreads, resolutionOnlyIds, first
|
|
|
111
113
|
bullets.push(renderThreadBullet(t, {
|
|
112
114
|
statusTag: renderFirstLookStatusTag(t),
|
|
113
115
|
noBody: resolutionOnlyIds.has(t.id),
|
|
116
|
+
suppressEditedMarker: true,
|
|
114
117
|
}));
|
|
115
118
|
}
|
|
116
119
|
for (const c of firstLookComments) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hasFlag } from "./args.mjs";
|
|
2
|
+
export function parseMarkFilesAsViewedArgs(args) {
|
|
3
|
+
const files = [];
|
|
4
|
+
const matchPatterns = [];
|
|
5
|
+
const tests = hasFlag(args, "--tests");
|
|
6
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
7
|
+
const arg = args[i];
|
|
8
|
+
if (arg === "--tests")
|
|
9
|
+
continue;
|
|
10
|
+
if (arg === "--match") {
|
|
11
|
+
const value = args[i + 1];
|
|
12
|
+
if (value === undefined || value.startsWith("--")) {
|
|
13
|
+
return { ok: false, error: "--match requires a regex value" };
|
|
14
|
+
}
|
|
15
|
+
matchPatterns.push(value);
|
|
16
|
+
i += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg.startsWith("--match=")) {
|
|
20
|
+
const value = arg.slice("--match=".length);
|
|
21
|
+
if (value === "")
|
|
22
|
+
return { ok: false, error: "--match requires a regex value" };
|
|
23
|
+
matchPatterns.push(value);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg.startsWith("--"))
|
|
27
|
+
return { ok: false, error: `unknown flag: "${arg}"` };
|
|
28
|
+
files.push(arg);
|
|
29
|
+
}
|
|
30
|
+
if (files.length === 0 && !tests && matchPatterns.length === 0) {
|
|
31
|
+
return { ok: false, error: "provide at least one file, --tests, or --match <regex>" };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, files, tests, matchPatterns };
|
|
34
|
+
}
|