pr-shepherd 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +23 -93
  4. package/bin/cli/args.mjs +0 -1
  5. package/bin/cli/default-iterate.mjs +2 -7
  6. package/bin/cli/exit-codes.mjs +2 -4
  7. package/bin/cli/fix-formatter.mjs +8 -8
  8. package/bin/cli/handlers.mjs +3 -72
  9. package/bin/cli/iterate-formatter.mjs +8 -17
  10. package/bin/cli/iterate-instructions.mjs +13 -47
  11. package/bin/cli/iterate-lean.mjs +4 -11
  12. package/bin/cli/list-formatters.mjs +6 -3
  13. package/bin/cli-parser.iterate-fixtures.mjs +4 -7
  14. package/bin/cli-parser.mjs +4 -33
  15. package/bin/commands/check-status.mjs +2 -2
  16. package/bin/commands/check-terminal-report.mjs +35 -0
  17. package/bin/commands/check.mjs +12 -7
  18. package/bin/commands/commit-suggestion-instruction.mjs +23 -0
  19. package/bin/commands/iterate/check-instructions.mjs +1 -1
  20. package/bin/commands/iterate/classify.mjs +9 -4
  21. package/bin/commands/iterate/escalate.mjs +5 -6
  22. package/bin/commands/iterate/fix-code.mjs +3 -3
  23. package/bin/commands/iterate/helpers.mjs +0 -29
  24. package/bin/commands/iterate/index.mjs +18 -19
  25. package/bin/commands/iterate/render.mjs +5 -17
  26. package/bin/commands/resolve-instructions.mjs +2 -10
  27. package/bin/commands/resolve-mutate.mjs +16 -0
  28. package/bin/commands/resolve.mjs +6 -17
  29. package/bin/commands/shepherd-journal.mjs +1 -1
  30. package/bin/comments/minimize-policy.mjs +15 -0
  31. package/bin/comments/visible-comments.mjs +20 -0
  32. package/bin/config/load.mjs +10 -3
  33. package/bin/config.json +2 -3
  34. package/bin/github/batch-parsers.mjs +10 -0
  35. package/bin/github/gql/batch-pr.gql +6 -0
  36. package/bin/github/queries.mjs +0 -2
  37. package/bin/index.mjs +0 -2
  38. package/bin/merge-status/derive.mjs +6 -6
  39. package/bin/reporters/agent.mjs +10 -3
  40. package/bin/state/iterate-stall.mjs +9 -0
  41. package/package.json +5 -6
  42. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  43. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +31 -26
  44. package/bin/commands/iterate.mjs +0 -2
  45. package/bin/commands/monitor.mjs +0 -139
  46. package/bin/commands/status.mjs +0 -126
  47. package/bin/github/gql/multi-pr-status-paged.gql +0 -31
  48. package/bin/reporters/check-instructions.mjs +0 -69
  49. package/bin/reporters/json.mjs +0 -10
  50. package/bin/reporters/text.mjs +0 -156
  51. package/plugin/skills/check/SKILL.md +0 -37
  52. package/plugin/skills/monitor/SKILL.md +0 -40
  53. package/plugin/skills/resolve/SKILL.md +0 -38
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "pr-shepherd",
12
12
  "description": "Autonomous PR CI monitor and review-comment resolver",
13
- "source": "./plugin",
13
+ "source": "./",
14
14
  "category": "productivity",
15
15
  "homepage": "https://github.com/jonathanong/pr-shepherd"
16
16
  }
@@ -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.15.2",
4
+ "version": "0.16.1",
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": "./plugin/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:monitor`
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 adapts monitor instructions to the calling agent. Claude Code gets `/loop` bootstrap instructions. Codex is detected with `AGENT=codex` or the current Codex CLI signal `CODEX_CI=1`; it gets a reusable pr-shepherd command and explicit goal-friendly iterate instructions because Codex does not provide `/loop` scheduling in this workflow. Generated commands use `cli.runner` from `.pr-shepherdrc.yml`: `auto` (default), `npx`, `pnpm`, or `yarn`.
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, to start the monitor, the skill/command invokes a CLI that returns a prompt to be ingested by the agent _(schematic paraphrased for brevity; actual output is more detailed; examples use `npx` as the default spelling — a pnpm or yarn repo would emit `pnpm exec` or `yarn run` instead)_:
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. Stop this iteration.
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. In Claude Code, `/pr-shepherd:monitor` uses `/loop` and continues working when your rate limit window is reset. In Codex, keep an active goal cycling the reusable command with a fresh 1-4 minute sleep before each rerun until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
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
- ### Monitor a PR
145
-
146
- In Claude Code, creates a fixed-interval loop (default every 4 minutes, configurable via `watch.interval`) that checks CI and review comments, fixes issues, and marks the PR ready for review when clean. The loop runs as a cron job that fires on schedule — agents end each turn cleanly and the cron fires again automatically. The loop stops automatically when the PR is merged or closed (auto-expires after 7 days).
115
+ ### Iterate a PR to completion
147
116
 
148
- In Codex, run `pr-shepherd monitor <PR>` once through the repo package runner to emit the goal-friendly recurrence prompt, then follow that prompt. Its reusable follow-up command is emitted by the CLI.
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]`.
149
118
 
150
- Claude Code:
119
+ Claude Code (via `pr-shepherd` skill):
151
120
 
152
121
  ```
153
- /pr-shepherd:monitor # infer PR from current branch
154
- /pr-shepherd:monitor 42
155
- /pr-shepherd:monitor 42 --ready-delay 15m
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 monitor # bootstrap from current branch, then follow its instructions
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 dynamic tick: fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (`fix_code`, `mark_ready`, `cancel`, `escalate`, `wait`, or `cooldown`). Claude and Codex both choose a fresh wait between 1 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.
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 three skills (`check`, `monitor`, `resolve`) into your agent's skill directory (`.claude/skills/` for project scope, `~/.claude/skills/` with `-g` for global scope). Powered by [skills.sh](https://skills.sh).
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 umbrella `pr-shepherd` skill for check, resolve, monitor, and iterate workflows.
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 start a PR monitor from Codex with the target repository's package runner:
211
+ Then iterate a PR from Codex with the target repository's package runner:
269
212
 
270
213
  ```bash
271
- <runner> pr-shepherd monitor 42
214
+ <runner> pr-shepherd iterate 42
272
215
  ```
273
216
 
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`.
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`.
275
218
 
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:
277
-
278
- ```bash
279
- <runner> pr-shepherd 42
280
- ```
281
-
282
- For an active Codex goal, rerun that command after picking a fresh 1-4 minute sleep until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows. There is no background `/loop` scheduler in Codex.
283
-
284
- ### Without the plugin
285
-
286
- See [docs/custom-commands.md](docs/custom-commands.md) for a project-local slash command that wraps the CLI without the plugin.
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
- cooldownSeconds: 60 # wait longer after a push before reading CI
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
@@ -325,10 +259,6 @@ Full reference: [docs/README.md](docs/README.md) — CLI usage, skills, configur
325
259
 
326
260
  See [docs/architecture.md](docs/architecture.md) for the module map and dependency rules.
327
261
 
328
- ## Forking
329
-
330
- See [docs/forking.md](docs/forking.md) if you want to customize pr-shepherd for your own use or team.
331
-
332
262
  ## License
333
263
 
334
264
  [MIT](LICENSE)
package/bin/cli/args.mjs CHANGED
@@ -7,7 +7,6 @@ import { parseArgs } from "node:util";
7
7
  const FLAGS_WITH_VALUES = new Set([
8
8
  "--format",
9
9
  "--ready-delay",
10
- "--cooldown-seconds",
11
10
  "--stall-timeout",
12
11
  "--require-sha",
13
12
  "--message",
@@ -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 <check|resolve|commit-suggestion|iterate|monitor|status|log-file> [options]\n" +
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
  }
@@ -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)
@@ -12,6 +11,8 @@ export function parseDurationToMinutes(s, defaultMinutes) {
12
11
  }
13
12
  export function statusToExitCode(status) {
14
13
  switch (status) {
14
+ case "MERGED":
15
+ case "CLOSED":
15
16
  case "READY":
16
17
  return 0;
17
18
  case "IN_PROGRESS":
@@ -34,6 +35,3 @@ export function iterateActionToExitCode(action) {
34
35
  return 0;
35
36
  }
36
37
  }
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} (@${t.author})${suggestionMarker}`);
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} (@${c.author})`);
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 — to be minimized)");
66
+ sections.push("## Review summaries (first look)");
67
67
  for (const r of result.fix.firstLookSummaries) {
68
- sections.push(`### \`reviewId=${r.id}\` (@${r.author})`);
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}\` (@${r.author})`);
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}\` (@${r.author})`);
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
  }
@@ -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, parseStatusPrNumbers, parseIntStrict, } from "./args.mjs";
9
- import { parseDurationToMinutes, iterateActionToExitCode, deriveSimpleReady, } from "./exit-codes.mjs";
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 monitor SKILL relies on:
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]` uses the `rebase-and-push` variant: the `resolve` bullet under
12
- * `## Post-fix push` wraps the resolve command in backticks the SKILL
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 monitor exactly what to do with this output. The section is
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 SKILL simply follows those steps; it does not need its own dispatch table.
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} · **copilotReviewInProgress** ${result.copilotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}`;
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.copilotReviewInProgress)
46
- segs.push(`**copilotReviewInProgress**`);
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 { FIX_INSTRUCTION_END_ITERATION, FIX_INSTRUCTION_STOP_AFTER_PUSH, FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK, } from "../commands/iterate/render.mjs";
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
- const CLAUDE_DYNAMIC_WAKEUP = "Schedule the next dynamic wakeup with `ScheduleWakeup` using `delaySeconds` between 60 and 240 and the same loop prompt (skip if running under a fixed-interval cron loop — the next scheduled fire handles it), then end this iteration.";
4
- const CODEX_DYNAMIC_WAIT = "pick a fresh sleep/timeout between 1 and 4 minutes, wait that long";
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, runtime, readyDelaySuffix, runner) {
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
- runtime === "codex"
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, runtime, readyDelaySuffix, runner) {
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 `Continue the active Codex goal — CI needs time to run on the new push. ${capitalizeFirst(retryDelay)}, then rerun \`${rerunCommand}\` to recheck.`;
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
- instruction === FIX_INSTRUCTION_END_ITERATION) {
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, runtime) {
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
  }
@@ -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.copilotReviewInProgress && { copilotReviewInProgress: true }),
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.attemptHistory &&
123
- result.escalate.attemptHistory.length > 0 && {
124
- attemptHistory: result.escalate.attemptHistory,
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,