pr-shepherd 0.15.2 → 0.16.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +23 -89
- package/bin/cli/args.mjs +0 -1
- package/bin/cli/default-iterate.mjs +2 -7
- package/bin/cli/exit-codes.mjs +0 -4
- package/bin/cli/fix-formatter.mjs +8 -8
- package/bin/cli/handlers.mjs +3 -72
- package/bin/cli/iterate-formatter.mjs +8 -17
- package/bin/cli/iterate-instructions.mjs +13 -47
- package/bin/cli/iterate-lean.mjs +4 -11
- package/bin/cli/list-formatters.mjs +6 -3
- package/bin/cli-parser.iterate-fixtures.mjs +4 -7
- package/bin/cli-parser.mjs +4 -33
- package/bin/commands/check-status.mjs +2 -2
- package/bin/commands/check.mjs +7 -6
- package/bin/commands/commit-suggestion-instruction.mjs +23 -0
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/classify.mjs +9 -4
- package/bin/commands/iterate/escalate.mjs +5 -6
- package/bin/commands/iterate/fix-code.mjs +3 -3
- package/bin/commands/iterate/helpers.mjs +0 -29
- package/bin/commands/iterate/index.mjs +18 -19
- package/bin/commands/iterate/render.mjs +5 -17
- package/bin/commands/resolve-instructions.mjs +2 -10
- package/bin/commands/resolve-mutate.mjs +16 -0
- package/bin/commands/resolve.mjs +6 -17
- package/bin/commands/shepherd-journal.mjs +1 -1
- package/bin/comments/minimize-policy.mjs +15 -0
- package/bin/comments/visible-comments.mjs +20 -0
- package/bin/config/load.mjs +10 -3
- package/bin/config.json +2 -3
- package/bin/github/batch-parsers.mjs +10 -0
- package/bin/github/gql/batch-pr.gql +6 -0
- package/bin/github/queries.mjs +0 -2
- package/bin/index.mjs +0 -2
- package/bin/merge-status/derive.mjs +6 -6
- package/bin/reporters/agent.mjs +10 -3
- package/bin/state/iterate-stall.mjs +9 -0
- package/package.json +5 -6
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +31 -26
- package/bin/commands/iterate.mjs +0 -2
- package/bin/commands/monitor.mjs +0 -139
- package/bin/commands/status.mjs +0 -126
- package/bin/github/gql/multi-pr-status-paged.gql +0 -31
- package/bin/reporters/check-instructions.mjs +0 -69
- package/bin/reporters/json.mjs +0 -10
- package/bin/reporters/text.mjs +0 -156
- package/plugin/skills/check/SKILL.md +0 -37
- package/plugin/skills/monitor/SKILL.md +0 -40
- package/plugin/skills/resolve/SKILL.md +0 -38
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
3
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Ong",
|
|
7
7
|
"email": "jonathanrichardong@gmail.com"
|
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
"repository": "https://github.com/jonathanong/pr-shepherd",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": ["github", "pull-request", "ci", "code-review", "automation"],
|
|
13
|
-
"skills": "./
|
|
13
|
+
"skills": "./plugins/pr-shepherd/skills/"
|
|
14
14
|
}
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Example Workflow:
|
|
|
9
9
|
2. Create a plan
|
|
10
10
|
3. Accept the plan
|
|
11
11
|
4. Switch to Auto Mode
|
|
12
|
-
5. Prompt: `make a PR, then run /pr-shepherd:
|
|
12
|
+
5. Prompt: `make a PR, then run /pr-shepherd:pr-shepherd`
|
|
13
13
|
6. Agent makes a draft PR
|
|
14
14
|
7. PR has passing CI -> draft is marked Ready for Review
|
|
15
15
|
8. Review bots begin providing reviews
|
|
@@ -20,38 +20,9 @@ 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
|
|
23
|
+
The CLI emits unified sleep/timeout instructions for all runtimes: pick a fresh sleep/timeout between 30 seconds and 4 minutes and rerun the configured pr-shepherd command. Codex is detected with `AGENT=codex` or the current Codex CLI signal `CODEX_CI=1`. Generated commands use `cli.runner` from `.pr-shepherdrc.yml`: `auto` (default), `npx`, `pnpm`, or `yarn`.
|
|
24
24
|
|
|
25
|
-
At a high level,
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
/pr-shepherd:monitor
|
|
29
|
-
|
|
30
|
-
> npx pr-shepherd monitor 123
|
|
31
|
-
|
|
32
|
-
# PR #123 [MONITOR]
|
|
33
|
-
|
|
34
|
-
Loop tag: `#pr-shepherd-loop:pr=123:`
|
|
35
|
-
|
|
36
|
-
## Loop prompt
|
|
37
|
-
|
|
38
|
-
#pr-shepherd-loop:pr=123:
|
|
39
|
-
|
|
40
|
-
**IMPORTANT — dynamic recurrence rules:** Do not invoke `/loop` again from inside
|
|
41
|
-
this prompt. For nonterminal iterations, call `ScheduleWakeup` with `delaySeconds`
|
|
42
|
-
between 60 and 240 and this same prompt body.
|
|
43
|
-
|
|
44
|
-
Run in a single Bash call:
|
|
45
|
-
npx pr-shepherd 123
|
|
46
|
-
|
|
47
|
-
…(self-dedup guidance, error-handling instructions)…
|
|
48
|
-
|
|
49
|
-
## Instructions
|
|
50
|
-
|
|
51
|
-
1. Invoke the /loop skill with the full ## Loop prompt body and no fixed interval.
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Each iteration calls `pr-shepherd <PR>` through the selected package runner, which provides actionable feedback directly to the agent:
|
|
25
|
+
At a high level, the skill invokes `pr-shepherd <PR>` through the selected package runner, which provides actionable feedback directly to the agent:
|
|
55
26
|
|
|
56
27
|
```
|
|
57
28
|
> npx pr-shepherd 123
|
|
@@ -63,7 +34,7 @@ Each iteration calls `pr-shepherd <PR>` through the selected package runner, whi
|
|
|
63
34
|
|
|
64
35
|
## Review threads
|
|
65
36
|
|
|
66
|
-
### `PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate.mts:42` (@alice)
|
|
37
|
+
### `PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate/index.mts:42` (@alice)
|
|
67
38
|
|
|
68
39
|
> The variable name is misleading.
|
|
69
40
|
>
|
|
@@ -90,7 +61,7 @@ _(schematic — actual steps depend on PR state)_
|
|
|
90
61
|
4. Rebase and push: `git fetch origin && git rebase origin/main && git push --force-with-lease` — capture `HEAD_SHA=$(git rev-parse HEAD)`.
|
|
91
62
|
5. Run the `resolve:` command above, substituting `"$HEAD_SHA"`.
|
|
92
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.
|
|
93
|
-
7.
|
|
64
|
+
7. CI needs time to run on the new push. Pick a fresh sleep/timeout between 30 seconds and 4 minutes, wait that long, then rerun `npx pr-shepherd 123` to recheck.
|
|
94
65
|
```
|
|
95
66
|
|
|
96
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:
|
|
@@ -126,7 +97,7 @@ Some other workflow improvements:
|
|
|
126
97
|
|
|
127
98
|
Recommendations:
|
|
128
99
|
|
|
129
|
-
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs.
|
|
100
|
+
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling the reusable command with a fresh sleep/timeout between 30 seconds and 4 minutes before each rerun until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
|
|
130
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.
|
|
131
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.
|
|
132
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.
|
|
@@ -141,57 +112,29 @@ Recommendations:
|
|
|
141
112
|
|
|
142
113
|
## Usage
|
|
143
114
|
|
|
144
|
-
###
|
|
115
|
+
### Iterate a PR to completion
|
|
145
116
|
|
|
146
|
-
|
|
117
|
+
One-tick dispatcher — checks CI and review comments, fixes issues, and marks the PR ready for review when clean. Each non-terminal tick emits an `## Instructions` section telling the agent to pick a fresh 30s–4m delay and rerun; the loop continues until `[CANCEL]` or `[ESCALATE]`.
|
|
147
118
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Claude Code:
|
|
119
|
+
Claude Code (via `pr-shepherd` skill):
|
|
151
120
|
|
|
152
121
|
```
|
|
153
|
-
/pr-shepherd:
|
|
154
|
-
/pr-shepherd:
|
|
155
|
-
/pr-shepherd:
|
|
122
|
+
/pr-shepherd:pr-shepherd # infer PR from current branch
|
|
123
|
+
/pr-shepherd:pr-shepherd 42
|
|
124
|
+
/pr-shepherd:pr-shepherd 42 --ready-delay 15m
|
|
156
125
|
```
|
|
157
126
|
|
|
158
|
-
Codex:
|
|
127
|
+
Codex or direct CLI:
|
|
159
128
|
|
|
160
129
|
```sh
|
|
161
|
-
npx pr-shepherd
|
|
162
|
-
npx pr-shepherd monitor 42 # bootstrap PR #42, then follow its instructions
|
|
163
|
-
npx pr-shepherd 42 # subsequent explicit check/action tick
|
|
130
|
+
npx pr-shepherd 42
|
|
164
131
|
npx pr-shepherd 42 --ready-delay 15m
|
|
165
132
|
npx pr-shepherd iterate 42 # legacy-compatible spelling
|
|
166
133
|
```
|
|
167
134
|
|
|
168
|
-
### Check a PR
|
|
169
|
-
|
|
170
|
-
One-shot status snapshot — merge state, CI results, and unresolved comments.
|
|
171
|
-
Accepts multiple PR numbers.
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
/pr-shepherd:check # infer from branch
|
|
175
|
-
/pr-shepherd:check 42
|
|
176
|
-
/pr-shepherd:check 41 42 43
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Resolve review comments
|
|
180
|
-
|
|
181
|
-
Fetches all actionable threads and comments, triages them, applies fixes,
|
|
182
|
-
pushes, then resolves/minimizes/dismisses via `--require-sha` (waits until
|
|
183
|
-
GitHub has seen the push before resolving).
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
/pr-shepherd:resolve # infer from branch
|
|
187
|
-
/pr-shepherd:resolve 42
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
See [docs/skills.md](docs/skills.md) for full argument reference.
|
|
191
|
-
|
|
192
135
|
## Iterate decision loop
|
|
193
136
|
|
|
194
|
-
On each
|
|
137
|
+
On each tick: fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (`fix_code`, `mark_ready`, `cancel`, `escalate`, or `wait`). All runtimes pick a fresh sleep/timeout between 30 seconds and 4 minutes for each nonterminal recurrence. See [docs/iterate-flow.md](docs/iterate-flow.md) for the decision table and [docs/flow.md](docs/flow.md) for the end-to-end flow diagram.
|
|
195
138
|
|
|
196
139
|
## Install
|
|
197
140
|
|
|
@@ -222,11 +165,11 @@ Alternatively, install the Claude skills individually via `npx skills`:
|
|
|
222
165
|
npx skills add jonathanong/pr-shepherd
|
|
223
166
|
```
|
|
224
167
|
|
|
225
|
-
Installs the
|
|
168
|
+
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).
|
|
226
169
|
|
|
227
170
|
### Codex
|
|
228
171
|
|
|
229
|
-
Codex uses the repo-shipped Codex plugin rather than the Claude plugin or `/pr-shepherd:*` slash commands. The plugin provides one
|
|
172
|
+
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.
|
|
230
173
|
|
|
231
174
|
Install the Codex plugin marketplace from GitHub:
|
|
232
175
|
|
|
@@ -265,25 +208,15 @@ If your Codex environment does not already set `CODEX_CI=1`, set `AGENT=codex` s
|
|
|
265
208
|
export AGENT=codex
|
|
266
209
|
```
|
|
267
210
|
|
|
268
|
-
Then
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
<runner> pr-shepherd monitor 42
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
For example, a repo like `~/filaments` that declares `packageManager: "pnpm@..."` and has `pnpm-lock.yaml` should use `pnpm exec pr-shepherd monitor 42`. For npm repos, use `npx --no-install pr-shepherd monitor 42`.
|
|
275
|
-
|
|
276
|
-
Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. The monitor bootstrap runs one tick and prints the reusable follow-up command, usually:
|
|
211
|
+
Then iterate a PR from Codex with the target repository's package runner:
|
|
277
212
|
|
|
278
213
|
```bash
|
|
279
|
-
<runner> pr-shepherd 42
|
|
214
|
+
<runner> pr-shepherd iterate 42
|
|
280
215
|
```
|
|
281
216
|
|
|
282
|
-
For
|
|
283
|
-
|
|
284
|
-
### Without the plugin
|
|
217
|
+
For example, a repo like `~/filaments` that declares `packageManager: "pnpm@..."` and has `pnpm-lock.yaml` should use `pnpm exec pr-shepherd iterate 42`. For npm repos, use `npx pr-shepherd iterate 42`.
|
|
285
218
|
|
|
286
|
-
|
|
219
|
+
Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. The skill runs one tick and the instructions tell you to pick a fresh sleep/timeout between 30 seconds and 4 minutes before the next rerun. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows.
|
|
287
220
|
|
|
288
221
|
### As a global CLI
|
|
289
222
|
|
|
@@ -297,7 +230,8 @@ Create a `.pr-shepherdrc.yml` in your project root (or any parent directory) to
|
|
|
297
230
|
|
|
298
231
|
```yaml
|
|
299
232
|
iterate:
|
|
300
|
-
|
|
233
|
+
fixAttemptsPerThread: 5 # raise before escalating to manual review
|
|
234
|
+
minimizeComments: all # all | bots | users | none
|
|
301
235
|
checks:
|
|
302
236
|
ciTriggerEvents:
|
|
303
237
|
- pull_request
|
package/bin/cli/args.mjs
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { parsePrNumber } from "./args.mjs";
|
|
2
|
-
const DEFAULT_ITERATE_FLAGS_WITH_VALUES = new Set([
|
|
3
|
-
"--format",
|
|
4
|
-
"--ready-delay",
|
|
5
|
-
"--cooldown-seconds",
|
|
6
|
-
"--stall-timeout",
|
|
7
|
-
]);
|
|
2
|
+
const DEFAULT_ITERATE_FLAGS_WITH_VALUES = new Set(["--format", "--ready-delay", "--stall-timeout"]);
|
|
8
3
|
const DEFAULT_ITERATE_BOOLEAN_FLAGS = new Set([
|
|
9
4
|
"--verbose",
|
|
10
5
|
"--no-auto-mark-ready",
|
|
@@ -48,7 +43,7 @@ function isDefaultIterateFlag(arg) {
|
|
|
48
43
|
function writeDefaultUsageError(arg) {
|
|
49
44
|
process.stderr.write(`Unknown subcommand: ${arg}\n`);
|
|
50
45
|
process.stderr.write("Usage: pr-shepherd [PR] [options]\n" +
|
|
51
|
-
" pr-shepherd <
|
|
46
|
+
" pr-shepherd <resolve|commit-suggestion|iterate|log-file> [options]\n" +
|
|
52
47
|
" pr-shepherd --version | -v\n");
|
|
53
48
|
process.exitCode = 1;
|
|
54
49
|
}
|
package/bin/cli/exit-codes.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.mjs";
|
|
2
|
-
import { deriveVerdict } from "../commands/status.mjs";
|
|
3
2
|
export function parseDurationToMinutes(s, defaultMinutes) {
|
|
4
3
|
const m = /^(\d+)(m|min|minutes?|h|hours?)?$/.exec(s.trim());
|
|
5
4
|
if (!m)
|
|
@@ -34,6 +33,3 @@ export function iterateActionToExitCode(action) {
|
|
|
34
33
|
return 0;
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
|
-
export function deriveSimpleReady(s) {
|
|
38
|
-
return deriveVerdict(s) === "READY";
|
|
39
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { renderResolveCommand } from "../commands/iterate.mjs";
|
|
1
|
+
import { renderResolveCommand } from "../commands/iterate/render.mjs";
|
|
2
2
|
import { joinSections } from "../util/markdown.mjs";
|
|
3
3
|
import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
|
|
4
|
-
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, renderThreadResolutionStatusTag, } from "./list-formatters.mjs";
|
|
4
|
+
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, renderThreadResolutionStatusTag, renderAuthor, } from "./list-formatters.mjs";
|
|
5
5
|
import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
|
|
6
6
|
export function formatFixCodeResult(header, result, opts) {
|
|
7
7
|
const runtime = opts?.runtime ?? "claude";
|
|
@@ -15,7 +15,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
15
15
|
const loc = t.path ? `\`${t.path}:${lineLabel}\`` : "(no location)";
|
|
16
16
|
const heading = t.url ? `[threadId=${t.id}](${t.url})` : `\`threadId=${t.id}\``;
|
|
17
17
|
const suggestionMarker = t.suggestion ? " [suggestion]" : "";
|
|
18
|
-
sections.push(`### ${heading} — ${loc} (
|
|
18
|
+
sections.push(`### ${heading} — ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}`);
|
|
19
19
|
sections.push(blockquote(t.body));
|
|
20
20
|
if (t.suggestion) {
|
|
21
21
|
sections.push(renderSuggestionBlock(t.suggestion, ""));
|
|
@@ -32,7 +32,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
32
32
|
sections.push("## Actionable comments");
|
|
33
33
|
for (const c of result.fix.actionableComments) {
|
|
34
34
|
const heading = c.url ? `[commentId=${c.id}](${c.url})` : `\`commentId=${c.id}\``;
|
|
35
|
-
sections.push(`### ${heading} (
|
|
35
|
+
sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType)})`);
|
|
36
36
|
sections.push(blockquote(c.body));
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -63,16 +63,16 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
63
63
|
sections.push(result.fix.changesRequestedReviews.map((r) => renderReviewBullet(r)).join("\n"));
|
|
64
64
|
}
|
|
65
65
|
if (result.fix.firstLookSummaries.length > 0) {
|
|
66
|
-
sections.push("## Review summaries (first look
|
|
66
|
+
sections.push("## Review summaries (first look)");
|
|
67
67
|
for (const r of result.fix.firstLookSummaries) {
|
|
68
|
-
sections.push(`### \`reviewId=${r.id}\` (
|
|
68
|
+
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType)})`);
|
|
69
69
|
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
if (result.fix.editedSummaries.length > 0) {
|
|
73
73
|
sections.push("## Review summaries (edited since first look — already minimized; do not re-minimize)");
|
|
74
74
|
for (const r of result.fix.editedSummaries) {
|
|
75
|
-
sections.push(`### \`reviewId=${r.id}\` (
|
|
75
|
+
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType)})`);
|
|
76
76
|
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -85,7 +85,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
85
85
|
if (result.fix.surfacedApprovals.length > 0) {
|
|
86
86
|
sections.push("## Approvals (surfaced — not minimized)");
|
|
87
87
|
for (const r of result.fix.surfacedApprovals) {
|
|
88
|
-
sections.push(`### \`reviewId=${r.id}\` (
|
|
88
|
+
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType)})`);
|
|
89
89
|
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
90
90
|
}
|
|
91
91
|
}
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { runCommitSuggestion } from "../commands/commit-suggestion.mjs";
|
|
2
|
-
import { runIterate } from "../commands/iterate.mjs";
|
|
3
|
-
import { runMonitor, formatMonitorResult, formatMonitorJson } from "../commands/monitor.mjs";
|
|
4
|
-
import { runStatus, formatStatusTable } from "../commands/status.mjs";
|
|
5
|
-
import { getRepoInfo } from "../github/client.mjs";
|
|
2
|
+
import { runIterate } from "../commands/iterate/index.mjs";
|
|
6
3
|
import { loadConfig } from "../config/load.mjs";
|
|
7
4
|
import { detectAgentRuntime } from "../agent-runtime.mjs";
|
|
8
|
-
import { parseCommonArgs, getFlag, hasFlag
|
|
9
|
-
import { parseDurationToMinutes, iterateActionToExitCode
|
|
5
|
+
import { parseCommonArgs, getFlag, hasFlag } from "./args.mjs";
|
|
6
|
+
import { parseDurationToMinutes, iterateActionToExitCode } from "./exit-codes.mjs";
|
|
10
7
|
import { formatCommitSuggestionResult, formatIterateResult, projectIterateLean, projectIterateVerbose, } from "./formatters.mjs";
|
|
11
8
|
import { validateDurationFlag } from "./duration-flag.mjs";
|
|
12
9
|
export async function handleCommitSuggestion(args) {
|
|
@@ -44,10 +41,6 @@ export async function handleIterate(args) {
|
|
|
44
41
|
return;
|
|
45
42
|
const cfg = loadConfig();
|
|
46
43
|
const readyDelaySeconds = parseDurationToMinutes(readyDelaySuffix ?? "", cfg.watch.readyDelayMinutes) * 60;
|
|
47
|
-
const cooldownSecondsStr = getFlag(extra, "--cooldown-seconds");
|
|
48
|
-
const cooldownSeconds = cooldownSecondsStr
|
|
49
|
-
? parseIntStrict(cooldownSecondsStr, "--cooldown-seconds")
|
|
50
|
-
: cfg.iterate.cooldownSeconds;
|
|
51
44
|
const noAutoMarkReady = hasFlag(extra, "--no-auto-mark-ready");
|
|
52
45
|
const noAutoCancelActionable = hasFlag(extra, "--no-auto-cancel-actionable");
|
|
53
46
|
const stallTimeoutStr = getFlag(extra, "--stall-timeout");
|
|
@@ -58,7 +51,6 @@ export async function handleIterate(args) {
|
|
|
58
51
|
...globalOpts,
|
|
59
52
|
prNumber,
|
|
60
53
|
readyDelaySeconds,
|
|
61
|
-
cooldownSeconds,
|
|
62
54
|
stallTimeoutSeconds,
|
|
63
55
|
noAutoMarkReady,
|
|
64
56
|
noAutoCancelActionable,
|
|
@@ -80,64 +72,3 @@ export async function handleIterate(args) {
|
|
|
80
72
|
}
|
|
81
73
|
process.exitCode = iterateActionToExitCode(result.action);
|
|
82
74
|
}
|
|
83
|
-
export async function handleMonitor(args) {
|
|
84
|
-
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
85
|
-
const runtime = detectAgentRuntime();
|
|
86
|
-
const readyDelayStr = getFlag(extra, "--ready-delay");
|
|
87
|
-
const readyDelaySuffix = validateDurationFlag("pr-shepherd monitor", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
|
|
88
|
-
if (readyDelaySuffix === null)
|
|
89
|
-
return;
|
|
90
|
-
const remaining = [];
|
|
91
|
-
for (let i = 0; i < extra.length; i++) {
|
|
92
|
-
const a = extra[i];
|
|
93
|
-
if (a === "--ready-delay") {
|
|
94
|
-
i++;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (a.startsWith("--ready-delay="))
|
|
98
|
-
continue;
|
|
99
|
-
remaining.push(a);
|
|
100
|
-
}
|
|
101
|
-
const unknownFlags = remaining.filter((a) => a.startsWith("--"));
|
|
102
|
-
if (unknownFlags.length > 0) {
|
|
103
|
-
process.stderr.write(`pr-shepherd monitor: ignoring unknown flags: ${unknownFlags.join(" ")}\n`);
|
|
104
|
-
}
|
|
105
|
-
const unknownPositionals = remaining.filter((a) => !a.startsWith("--"));
|
|
106
|
-
if (unknownPositionals.length > 0) {
|
|
107
|
-
process.stderr.write(`pr-shepherd monitor: unexpected positional arguments ignored: ${unknownPositionals.join(" ")}\n`);
|
|
108
|
-
}
|
|
109
|
-
let result;
|
|
110
|
-
try {
|
|
111
|
-
result = await runMonitor({
|
|
112
|
-
...globalOpts,
|
|
113
|
-
prNumber,
|
|
114
|
-
readyDelaySuffix: readyDelaySuffix ?? undefined,
|
|
115
|
-
runtime,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
120
|
-
process.exitCode = 1;
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
process.stdout.write(globalOpts.format === "json"
|
|
124
|
-
? `${JSON.stringify(formatMonitorJson(result, { runtime }), null, 2)}\n`
|
|
125
|
-
: `${formatMonitorResult(result, { runtime })}\n`);
|
|
126
|
-
}
|
|
127
|
-
export async function handleStatus(args) {
|
|
128
|
-
const { global: globalOpts } = parseCommonArgs(args);
|
|
129
|
-
const prNumbers = parseStatusPrNumbers(args);
|
|
130
|
-
if (prNumbers.length === 0) {
|
|
131
|
-
process.stderr.write("Usage: pr-shepherd status PR1 [PR2 …]\n");
|
|
132
|
-
process.exitCode = 1;
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const repo = await getRepoInfo();
|
|
136
|
-
const summaries = await runStatus({ ...globalOpts, prNumbers });
|
|
137
|
-
const output = globalOpts.format === "json"
|
|
138
|
-
? JSON.stringify(summaries, null, 2)
|
|
139
|
-
: formatStatusTable(summaries, `${repo.owner}/${repo.name}`);
|
|
140
|
-
process.stdout.write(`${output}\n`);
|
|
141
|
-
const allReady = summaries.every((s) => deriveSimpleReady(s));
|
|
142
|
-
process.exitCode = allReady ? 0 : 1;
|
|
143
|
-
}
|
|
@@ -4,17 +4,16 @@ import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, }
|
|
|
4
4
|
/**
|
|
5
5
|
* Format an IterateResult as human-readable Markdown.
|
|
6
6
|
*
|
|
7
|
-
* Load-bearing conventions the
|
|
7
|
+
* Load-bearing conventions the iterate skill relies on:
|
|
8
8
|
* 1. The H1 heading on line 1 contains `[<ACTION>]` — the action tag identifies
|
|
9
9
|
* the output for logging and validation. Behavior is driven by `## Instructions`,
|
|
10
10
|
* not by dispatching on the tag.
|
|
11
|
-
* 2. `[FIX_CODE]`
|
|
12
|
-
*
|
|
13
|
-
* extracts the backticked content for execution.
|
|
11
|
+
* 2. `[FIX_CODE]` wraps the `resolve` command under `## Post-fix push` in
|
|
12
|
+
* backticks — the skill extracts the backticked content for execution.
|
|
14
13
|
* 3. Every action ends with a `## Instructions` section — numbered `1.`, `2.`, … —
|
|
15
|
-
* that tells the
|
|
14
|
+
* that tells the agent exactly what to do with this output. The section is
|
|
16
15
|
* unconditional: every action, every variant, always emits at least one step.
|
|
17
|
-
* The
|
|
16
|
+
* The skill simply follows those steps; it does not need its own dispatch table.
|
|
18
17
|
*/
|
|
19
18
|
export function formatIterateResult(result, opts) {
|
|
20
19
|
const verbose = opts?.verbose ?? false;
|
|
@@ -28,7 +27,7 @@ export function formatIterateResult(result, opts) {
|
|
|
28
27
|
const baseLine = `**status** \`${result.status}\` · **merge** \`${result.mergeStateStatus}\`${reviewDecisionSeg} · **state** \`${result.state}\` · **repo** \`${result.repo}\``;
|
|
29
28
|
let summaryLine;
|
|
30
29
|
if (verbose) {
|
|
31
|
-
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress · **remainingSeconds** ${result.remainingSeconds} · **
|
|
30
|
+
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}`;
|
|
32
31
|
}
|
|
33
32
|
else {
|
|
34
33
|
const counts = [`${result.summary.passing} passing`];
|
|
@@ -42,22 +41,14 @@ export function formatIterateResult(result, opts) {
|
|
|
42
41
|
if (result.status === "READY" && result.remainingSeconds > 0) {
|
|
43
42
|
segs.push(`**remainingSeconds** ${result.remainingSeconds}`);
|
|
44
43
|
}
|
|
45
|
-
if (result.
|
|
46
|
-
segs.push(`**
|
|
44
|
+
if (result.blockingBotReviewInProgress)
|
|
45
|
+
segs.push(`**blockingBotReviewInProgress**`);
|
|
47
46
|
if (result.isDraft)
|
|
48
47
|
segs.push(`**isDraft**`);
|
|
49
48
|
summaryLine = segs.join(" · ");
|
|
50
49
|
}
|
|
51
50
|
const header = [heading, "", baseLine, summaryLine].join("\n");
|
|
52
51
|
switch (result.action) {
|
|
53
|
-
case "cooldown":
|
|
54
|
-
// In default mode: suppress base/summary lines — cooldown carries UNKNOWN/empty
|
|
55
|
-
// placeholders that add no value. Emit only heading + log + Instructions.
|
|
56
|
-
return joinSections([
|
|
57
|
-
verbose ? header : heading,
|
|
58
|
-
adaptIterateLog(result.log, runtime),
|
|
59
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, runner))}`,
|
|
60
|
-
]);
|
|
61
52
|
case "wait":
|
|
62
53
|
return joinSections([
|
|
63
54
|
header,
|
|
@@ -1,67 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FIX_INSTRUCTION_STOP_AFTER_PUSH, FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK, } from "../commands/iterate/render.mjs";
|
|
2
2
|
import { buildPrShepherdCommand } from "./runner.mjs";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function capitalizeFirst(value) {
|
|
6
|
-
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
3
|
+
function buildRecheckInstruction(rerunCommand, tail) {
|
|
4
|
+
return `Pick a fresh sleep/timeout between 30 seconds and 4 minutes, wait that long, then rerun \`${rerunCommand}\` ${tail}`;
|
|
7
5
|
}
|
|
8
|
-
export function buildSimpleIterateInstructions(result,
|
|
6
|
+
export function buildSimpleIterateInstructions(result, _runtime, readyDelaySuffix, runner) {
|
|
9
7
|
const rerunCommand = buildCodexIterateCommand(result.pr, readyDelaySuffix, runner);
|
|
10
|
-
const retryDelay = buildCodexRetryDelay();
|
|
11
8
|
switch (result.action) {
|
|
12
|
-
case "cooldown":
|
|
13
|
-
return [
|
|
14
|
-
runtime === "codex"
|
|
15
|
-
? `Continue the active Codex goal — ${retryDelay}, then rerun \`${rerunCommand}\` after CI starts reporting.`
|
|
16
|
-
: `CI still needs time to start reporting. ${CLAUDE_DYNAMIC_WAKEUP}`,
|
|
17
|
-
];
|
|
18
9
|
case "wait":
|
|
19
|
-
return [
|
|
20
|
-
runtime === "codex"
|
|
21
|
-
? `Continue the active Codex goal — ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`
|
|
22
|
-
: CLAUDE_DYNAMIC_WAKEUP,
|
|
23
|
-
];
|
|
10
|
+
return [buildRecheckInstruction(rerunCommand, "to continue the active goal.")];
|
|
24
11
|
case "mark_ready":
|
|
25
12
|
return [
|
|
26
|
-
|
|
27
|
-
? `The CLI already marked the PR ready for review. Continue the active Codex goal until the ready-delay completes — ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`
|
|
28
|
-
: `The CLI already marked the PR ready for review. ${CLAUDE_DYNAMIC_WAKEUP}`,
|
|
13
|
+
`The CLI already marked the PR ready for review. ${buildRecheckInstruction(rerunCommand, "to recheck.")}`,
|
|
29
14
|
];
|
|
30
15
|
case "cancel":
|
|
31
|
-
return [
|
|
32
|
-
runtime === "codex"
|
|
33
|
-
? "Stop — no recurring Codex monitor is running to cancel."
|
|
34
|
-
: `Stop — do not schedule another dynamic wakeup. If this loop was started with a fixed-interval \`/loop\` schedule, call \`CronList\`, find the job whose prompt contains \`#pr-shepherd-loop:pr=${result.pr}:\`, and cancel it with \`CronDelete\`.`,
|
|
35
|
-
"Stop.",
|
|
36
|
-
];
|
|
16
|
+
return ["Stop — the active goal is complete."];
|
|
37
17
|
case "escalate":
|
|
38
|
-
return [
|
|
39
|
-
runtime === "codex"
|
|
40
|
-
? "Stop — no recurring Codex monitor is running to cancel."
|
|
41
|
-
: `Stop — do not schedule another dynamic wakeup. If this loop was started with a fixed-interval \`/loop\` schedule, call \`CronList\`, find the job whose prompt contains \`#pr-shepherd-loop:pr=${result.pr}:\`, and cancel it with \`CronDelete\`.`,
|
|
42
|
-
"Stop — the PR needs human direction before monitoring can resume.",
|
|
43
|
-
];
|
|
18
|
+
return ["Stop — the PR needs human direction before iterating can resume."];
|
|
44
19
|
}
|
|
45
20
|
}
|
|
46
|
-
export function adaptFixCodeInstructions(instructions, pr,
|
|
47
|
-
if (runtime !== "codex")
|
|
48
|
-
return instructions;
|
|
21
|
+
export function adaptFixCodeInstructions(instructions, pr, _runtime, readyDelaySuffix, runner) {
|
|
49
22
|
const rerunCommand = buildCodexIterateCommand(pr, readyDelaySuffix, runner);
|
|
50
|
-
const retryDelay = buildCodexRetryDelay();
|
|
51
23
|
return instructions.map((instruction) => {
|
|
52
24
|
if (instruction === FIX_INSTRUCTION_STOP_AFTER_PUSH) {
|
|
53
|
-
return `
|
|
25
|
+
return `CI needs time to run on the new push. ${buildRecheckInstruction(rerunCommand, "to recheck.")}`;
|
|
54
26
|
}
|
|
55
|
-
if (instruction === FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK
|
|
56
|
-
|
|
57
|
-
return `Continue the active Codex goal — ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`;
|
|
27
|
+
if (instruction === FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK) {
|
|
28
|
+
return buildRecheckInstruction(rerunCommand, "to recheck.");
|
|
58
29
|
}
|
|
59
30
|
return instruction;
|
|
60
31
|
});
|
|
61
32
|
}
|
|
62
|
-
export function adaptIterateLog(log,
|
|
63
|
-
if (runtime !== "codex")
|
|
64
|
-
return log;
|
|
33
|
+
export function adaptIterateLog(log, _runtime) {
|
|
65
34
|
return log.replace(/\s+—\s+\d+s until auto-cancel/g, "");
|
|
66
35
|
}
|
|
67
36
|
export function buildCodexIterateCommand(pr, readyDelaySuffix, runner) {
|
|
@@ -70,9 +39,6 @@ export function buildCodexIterateCommand(pr, readyDelaySuffix, runner) {
|
|
|
70
39
|
runner,
|
|
71
40
|
}).text;
|
|
72
41
|
}
|
|
73
|
-
export function buildCodexRetryDelay() {
|
|
74
|
-
return CODEX_DYNAMIC_WAIT;
|
|
75
|
-
}
|
|
76
42
|
export function numberInstructions(instructions) {
|
|
77
43
|
return instructions.map((instruction, i) => `${i + 1}. ${instruction}`).join("\n");
|
|
78
44
|
}
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export function projectIterateLean(result, opts) {
|
|
|
18
18
|
mergeStateStatus: result.mergeStateStatus,
|
|
19
19
|
...(result.mergeStatus === "BLOCKED" &&
|
|
20
20
|
result.reviewDecision !== null && { reviewDecision: result.reviewDecision }),
|
|
21
|
-
...(result.
|
|
21
|
+
...(result.blockingBotReviewInProgress && { blockingBotReviewInProgress: true }),
|
|
22
22
|
...(result.isDraft && { isDraft: true }),
|
|
23
23
|
summary: {
|
|
24
24
|
passing: result.summary.passing,
|
|
@@ -34,12 +34,6 @@ export function projectIterateLean(result, opts) {
|
|
|
34
34
|
...(result.baseBranch && { baseBranch: result.baseBranch }),
|
|
35
35
|
};
|
|
36
36
|
switch (result.action) {
|
|
37
|
-
case "cooldown":
|
|
38
|
-
return {
|
|
39
|
-
...base,
|
|
40
|
-
log: adaptIterateLog(result.log, runtime),
|
|
41
|
-
instructions: simpleInstructions(result),
|
|
42
|
-
};
|
|
43
37
|
case "wait":
|
|
44
38
|
return {
|
|
45
39
|
...base,
|
|
@@ -66,7 +60,6 @@ export function projectIterateLean(result, opts) {
|
|
|
66
60
|
...(result.checks.length > 0 && { checks: result.checks }),
|
|
67
61
|
...(result.cancelled.length > 0 && { cancelled: result.cancelled }),
|
|
68
62
|
fix: {
|
|
69
|
-
mode: result.fix.mode,
|
|
70
63
|
...(result.fix.threads.length > 0 && { threads: result.fix.threads }),
|
|
71
64
|
...(result.fix.resolutionOnlyThreads.length > 0 && {
|
|
72
65
|
resolutionOnlyThreads: result.fix.resolutionOnlyThreads,
|
|
@@ -119,9 +112,9 @@ export function projectIterateLean(result, opts) {
|
|
|
119
112
|
...(result.escalate.changesRequestedReviews.length > 0 && {
|
|
120
113
|
changesRequestedReviews: result.escalate.changesRequestedReviews,
|
|
121
114
|
}),
|
|
122
|
-
...(result.escalate.
|
|
123
|
-
result.escalate.
|
|
124
|
-
|
|
115
|
+
...(result.escalate.thrashHistory &&
|
|
116
|
+
result.escalate.thrashHistory.length > 0 && {
|
|
117
|
+
thrashHistory: result.escalate.thrashHistory,
|
|
125
118
|
}),
|
|
126
119
|
suggestion: result.escalate.suggestion,
|
|
127
120
|
humanMessage: result.escalate.humanMessage,
|