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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -56
- package/bin/cli/fix-formatter.mjs +3 -3
- package/bin/cli/formatters.mjs +5 -4
- package/bin/cli/handlers.mjs +0 -1
- package/bin/cli/help-command-pages.mjs +154 -0
- package/bin/cli/help-top-page.mjs +59 -0
- package/bin/cli/help.mjs +4 -30
- package/bin/cli/iterate-emitter.mjs +0 -1
- package/bin/cli/iterate-formatter.mjs +4 -6
- package/bin/cli/iterate-instructions.mjs +6 -8
- package/bin/cli/iterate-lean.mjs +4 -6
- package/bin/cli/list-formatters.mjs +3 -0
- package/bin/cli/poll-handler.mjs +0 -1
- package/bin/cli/runner.mjs +2 -103
- package/bin/cli-parser.commit-suggestion.test-support.mjs +1 -1
- package/bin/cli-parser.iterate-fixtures.mjs +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +2 -2
- package/bin/commands/commit-suggestion.mjs +6 -2
- package/bin/commands/commit-suggestion.test-support.mjs +1 -1
- package/bin/commands/iterate/classify.mjs +2 -2
- package/bin/commands/iterate/fix-code.mjs +3 -3
- package/bin/commands/iterate/index.mjs +0 -1
- package/bin/commands/iterate/render.mjs +4 -3
- package/bin/commands/resolve-instructions.mjs +6 -7
- package/bin/commands/resolve.mjs +1 -1
- package/bin/comments/visible-comments.mjs +3 -2
- package/bin/config/load.mjs +0 -7
- package/bin/config.json +0 -3
- package/bin/reporters/agent.mjs +1 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +11 -27
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
|
|
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
|
|
25
|
+
At a high level, the skill invokes `pr-shepherd poll <PR>`, which provides actionable feedback directly to the agent:
|
|
26
26
|
|
|
27
|
-
```
|
|
28
|
-
>
|
|
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
|
-
>
|
|
47
|
+
> oxfmt
|
|
48
48
|
|
|
49
49
|
## Post-fix push
|
|
50
50
|
|
|
51
51
|
- base: `main`
|
|
52
|
-
- resolve: `
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
112
|
+
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix)));
|
|
113
113
|
return joinSections(sections);
|
|
114
114
|
}
|
|
115
115
|
function blockquote(body) {
|
package/bin/cli/formatters.mjs
CHANGED
|
@@ -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
|
|
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})`);
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -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:
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7
|
-
const rerunCommand = buildIterateCommand(result.pr, readyDelaySuffix
|
|
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
|
|
24
|
-
const rerunCommand = buildIterateCommand(pr, readyDelaySuffix
|
|
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
|
|
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");
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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}`;
|
package/bin/cli/poll-handler.mjs
CHANGED
package/bin/cli/runner.mjs
CHANGED
|
@@ -1,111 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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: `
|
|
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: ["
|
|
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
|
|
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
|
-
]
|
|
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([
|
|
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({
|
|
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
|
|
41
|
-
const argv = buildPrShepherdCommand(["resolve", String(prNumber)]
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
@@ -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 = []
|
|
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
|
|
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)
|
|
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
|
|
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 =
|
|
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(`
|
|
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
|
|
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".`);
|
package/bin/commands/resolve.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
17
|
-
|
|
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
|
}
|
package/bin/config/load.mjs
CHANGED
|
@@ -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
package/bin/reporters/agent.mjs
CHANGED
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ allowed-tools: ["Bash", "Read", "Grep", "Edit", "Write", "Glob", "Skill"]
|
|
|
8
8
|
|
|
9
9
|
# pr-shepherd
|
|
10
10
|
|
|
11
|
-
|
|
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. **
|
|
23
|
+
2. **Run `pr-shepherd poll`:**
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
|
|
26
|
+
pr-shepherd poll <N>
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Do not pass `$ARGUMENTS` through as extra flags. If you need to inspect supported options, run `pr-shepherd poll --help`.
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|