pr-shepherd 0.10.3 → 0.12.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.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/README.md +51 -9
  3. package/bin/agent-runtime.mjs +7 -0
  4. package/bin/checks/triage.mjs +5 -46
  5. package/bin/cli/args.mjs +10 -2
  6. package/bin/cli/default-iterate.mjs +54 -0
  7. package/bin/cli/duration-flag.mjs +22 -0
  8. package/bin/cli/fix-formatter.mjs +25 -15
  9. package/bin/cli/formatters.mjs +26 -38
  10. package/bin/cli/handlers.mjs +26 -24
  11. package/bin/cli/iterate-formatter.mjs +14 -10
  12. package/bin/cli/iterate-instructions.mjs +75 -0
  13. package/bin/cli/iterate-lean.mjs +59 -6
  14. package/bin/cli/list-formatters.mjs +11 -0
  15. package/bin/cli-parser.iterate-fixtures.mjs +2 -0
  16. package/bin/cli-parser.mjs +15 -1
  17. package/bin/commands/check.mjs +6 -6
  18. package/bin/commands/commit-suggestion.mjs +34 -80
  19. package/bin/commands/iterate/classify.mjs +5 -4
  20. package/bin/commands/iterate/escalate.mjs +2 -1
  21. package/bin/commands/iterate/fix-code.mjs +16 -8
  22. package/bin/commands/iterate/helpers.mjs +34 -0
  23. package/bin/commands/iterate/index.mjs +2 -2
  24. package/bin/commands/iterate/render.mjs +25 -37
  25. package/bin/commands/iterate/stall.mjs +3 -1
  26. package/bin/commands/iterate.mjs +1 -1
  27. package/bin/commands/monitor.mjs +68 -17
  28. package/bin/commands/ready-delay.mjs +2 -1
  29. package/bin/commands/resolve-instructions.mjs +8 -4
  30. package/bin/commands/resolve.mjs +4 -4
  31. package/bin/config.json +1 -3
  32. package/bin/index.mjs +1 -0
  33. package/bin/reporters/agent.mjs +6 -2
  34. package/bin/reporters/check-instructions.mjs +11 -5
  35. package/bin/reporters/json.mjs +2 -2
  36. package/bin/reporters/text.mjs +24 -16
  37. package/package.json +3 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
4
- "version": "0.10.3",
3
+ "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
+ "version": "0.12.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pr-shepherd
2
2
 
3
- Autonomous PR CI monitor and review-comment resolver for Claude Code.
3
+ Autonomous PR CI monitor and review-comment resolver for agentic coding tools, including Claude Code and Codex.
4
4
  The goal is to have an agent take a plan to a human-reviewable PR autonomously.
5
5
 
6
6
  Example Workflow:
@@ -20,6 +20,8 @@ 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 `npx pr-shepherd <PR>` command and explicit goal-friendly iterate instructions because Codex does not provide `/loop` scheduling in this workflow.
24
+
23
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)_:
24
26
 
25
27
  ```
@@ -40,7 +42,7 @@ Loop args: `4m`
40
42
  after completing the actions below. The cron job handles the next fire.
41
43
 
42
44
  Run in a single Bash call:
43
- npx pr-shepherd iterate 123
45
+ npx pr-shepherd 123
44
46
 
45
47
  …(self-dedup guidance, error-handling instructions)…
46
48
 
@@ -50,10 +52,10 @@ Run in a single Bash call:
50
52
  2. Otherwise, invoke the /loop skill with Loop args and the full ## Loop prompt body.
51
53
  ```
52
54
 
53
- Each iteration calls `npx pr-shepherd iterate <PR>`, which provides actionable feedback directly to the agent:
55
+ Each iteration calls `npx pr-shepherd <PR>`, which provides actionable feedback directly to the agent:
54
56
 
55
57
  ```
56
- > npx pr-shepherd iterate 123
58
+ > npx pr-shepherd 123
57
59
 
58
60
  # PR #123 [FIX_CODE]
59
61
 
@@ -125,7 +127,7 @@ Some other workflow improvements:
125
127
 
126
128
  Recommendations:
127
129
 
128
- - Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. As it uses `/loop`, it will continue working when your rate limit window is reset. The loop cancels automatically when the PR is merged, closed, or after the ready-delay elapses.
130
+ - 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 `npx pr-shepherd <PR>` command every `watch.interval` (default 4m) until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
129
131
  - 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.
130
132
  - 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.
131
133
  - Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
@@ -142,9 +144,11 @@ Recommendations:
142
144
 
143
145
  ### Monitor a PR
144
146
 
145
- Creates a cron loop that fires every 4 minutes, checks CI and review
146
- comments, fixes issues, and marks the PR ready for review when clean. The
147
- loop cancels automatically when the PR is merged or closed.
147
+ In Claude Code, creates a cron loop that fires every 4 minutes, checks CI and review comments, fixes issues, and marks the PR ready for review when clean. The loop cancels automatically when the PR is merged or closed.
148
+
149
+ In Codex, run `npx pr-shepherd monitor <PR>` once to emit the goal-friendly recurrence prompt, then follow that prompt. Its reusable follow-up command is `npx pr-shepherd <PR>`.
150
+
151
+ Claude Code:
148
152
 
149
153
  ```
150
154
  /pr-shepherd:monitor # infer PR from current branch
@@ -153,6 +157,16 @@ loop cancels automatically when the PR is merged or closed.
153
157
  /pr-shepherd:monitor 42 --ready-delay 15m
154
158
  ```
155
159
 
160
+ Codex:
161
+
162
+ ```sh
163
+ npx pr-shepherd monitor # bootstrap from current branch, then follow its instructions
164
+ npx pr-shepherd monitor 42 # bootstrap PR #42, then follow its instructions
165
+ npx pr-shepherd 42 # subsequent explicit check/action tick
166
+ npx pr-shepherd 42 --ready-delay 15m
167
+ npx pr-shepherd iterate 42 # legacy-compatible spelling
168
+ ```
169
+
156
170
  ### Check a PR
157
171
 
158
172
  One-shot status snapshot — merge state, CI results, and unresolved comments.
@@ -208,6 +222,34 @@ claude /plugin install pr-shepherd
208
222
 
209
223
  This repo ships two `marketplace.json` files that serve different install flows: the root `marketplace.json` resolves the plugin from the npm registry (used by the `claude /plugin marketplace add` command above); `.claude-plugin/marketplace.json` is the owner-level registry manifest that resolves the plugin from the local plugin directory (used when Claude Code installs from a local or git-based source). Both files are needed to support these two install paths.
210
224
 
225
+ ### For Codex
226
+
227
+ Codex does not use the Claude plugin or `/pr-shepherd:*` slash commands. Install the CLI where Codex will run it, then call `npx pr-shepherd` directly:
228
+
229
+ ```bash
230
+ npm install --save-dev pr-shepherd
231
+ ```
232
+
233
+ If your Codex environment does not already set `CODEX_CI=1`, set `AGENT=codex` so `pr-shepherd` emits Codex-compatible instructions instead of Claude `/loop` instructions:
234
+
235
+ ```bash
236
+ export AGENT=codex
237
+ ```
238
+
239
+ Then start a PR monitor from Codex:
240
+
241
+ ```bash
242
+ npx pr-shepherd monitor 42
243
+ ```
244
+
245
+ Follow the output's `## Instructions`. The monitor bootstrap runs one tick and prints the reusable follow-up command, usually:
246
+
247
+ ```bash
248
+ npx pr-shepherd 42
249
+ ```
250
+
251
+ For an active Codex goal, rerun that command every `watch.interval` (default 4m) until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `npx pr-shepherd iterate 42` remains supported for existing workflows. There is no background `/loop` scheduler in Codex.
252
+
211
253
  ### Without the plugin
212
254
 
213
255
  See [docs/custom-commands.md](docs/custom-commands.md) for a project-local slash command that wraps the CLI without the plugin.
@@ -234,7 +276,7 @@ actions:
234
276
  autoMarkReady: false # disable to stay draft until you manually promote
235
277
  ```
236
278
 
237
- Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`), `PR_SHEPHERD_STATE_DIR` (override loop-state and log base dir), `PR_SHEPHERD_LOG_DISABLED=1` (disable the per-worktree debug log).
279
+ Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`), `PR_SHEPHERD_STATE_DIR` (override loop-state and log base dir), `PR_SHEPHERD_LOG_DISABLED=1` (disable the per-worktree debug log), `AGENT=codex` or `CODEX_CI=1` (emit Codex-compatible monitor instructions).
238
280
 
239
281
  See [docs/configuration.md](docs/configuration.md) for full semantics and deprecated-key migration.
240
282
 
@@ -0,0 +1,7 @@
1
+ export function detectAgentRuntime(env = process.env) {
2
+ if (env.AGENT?.trim().toLowerCase() === "codex")
3
+ return "codex";
4
+ if (env.CODEX_CI === "1")
5
+ return "codex";
6
+ return "claude";
7
+ }
@@ -1,23 +1,19 @@
1
- import { rest, restText } from "../github/http.mjs";
2
- export function triageFailingChecks(failingChecks, repo, logTailLines, logTailChars = 200) {
1
+ import { rest } from "../github/http.mjs";
2
+ export function triageFailingChecks(failingChecks, repo) {
3
3
  const jobsCache = new Map();
4
- return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache, logTailLines, logTailChars)));
4
+ return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
5
5
  }
6
- async function triageCheck(check, repo, jobsCache, logTailLines, logTailChars) {
7
- if (check.runId === null) {
6
+ async function triageCheck(check, repo, jobsCache) {
7
+ if (check.runId === null || check.conclusion === "CANCELLED") {
8
8
  return { ...check };
9
9
  }
10
10
  const jobs = await fetchJobs(check.runId, repo, jobsCache);
11
11
  const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
12
- const logTail = jobInfo?.jobId !== undefined && logTailLines > 0
13
- ? await fetchLogTail(jobInfo.jobId, repo, logTailLines, logTailChars, jobInfo.failedStep)
14
- : undefined;
15
12
  return {
16
13
  ...check,
17
14
  ...(jobInfo?.workflowName !== undefined && { workflowName: jobInfo.workflowName }),
18
15
  ...(jobInfo?.jobName !== undefined && { jobName: jobInfo.jobName }),
19
16
  ...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
20
- ...(logTail !== undefined && { logTail }),
21
17
  };
22
18
  }
23
19
  function fetchJobs(runId, repo, cache) {
@@ -67,42 +63,5 @@ function pickJobInfo(jobs, checkName) {
67
63
  workflowName: job.workflow_name,
68
64
  jobName: job.name,
69
65
  failedStep,
70
- jobId: job.id,
71
66
  };
72
67
  }
73
- async function fetchLogTail(jobId, repo, logTailLines, logTailChars, failedStepName) {
74
- const { owner, name } = repo;
75
- try {
76
- const text = await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`);
77
- const allLines = text.split("\n");
78
- const stepLines = failedStepName ? extractStepLines(allLines, failedStepName) : null;
79
- const lines = stepLines ?? allLines;
80
- const tail = lines.length <= logTailLines ? lines.join("\n") : lines.slice(-logTailLines).join("\n");
81
- return tail.length <= logTailChars ? tail : tail.slice(-logTailChars);
82
- }
83
- catch {
84
- return undefined;
85
- }
86
- }
87
- // Extract the lines inside the ##[group]..##[endgroup] section for the named step.
88
- // Returns null when no matching group is found so the caller falls back to the full log.
89
- function extractStepLines(lines, stepName) {
90
- const lowerStep = stepName.toLowerCase();
91
- let inStep = false;
92
- const result = [];
93
- for (const line of lines) {
94
- const content = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+/, "");
95
- if (!inStep) {
96
- if (content.startsWith("##[group]") && content.slice(9).toLowerCase().includes(lowerStep)) {
97
- inStep = true;
98
- }
99
- }
100
- else if (content.startsWith("##[endgroup]")) {
101
- inStep = false;
102
- }
103
- else {
104
- result.push(line);
105
- }
106
- }
107
- return result.length > 0 ? result : null;
108
- }
package/bin/cli/args.mjs CHANGED
@@ -94,8 +94,8 @@ export function parseCommonArgs(args) {
94
94
  }
95
95
  }
96
96
  }
97
- const prIndex = args.findIndex((a, index) => !skipForPrDetect.has(index) && !a.startsWith("--") && /^\d+$/.test(a));
98
- const prNumber = prIndex !== -1 ? parseInt(args[prIndex], 10) : undefined;
97
+ const prIndex = args.findIndex((a, index) => !skipForPrDetect.has(index) && !a.startsWith("--") && parsePrNumber(a) !== null);
98
+ const prNumber = prIndex !== -1 ? parsePrNumber(args[prIndex]) : undefined;
99
99
  // Remove consumed global-flag indices (and the PR number itself) from extra.
100
100
  if (prIndex !== -1) {
101
101
  consumedIndices.add(prIndex);
@@ -107,6 +107,14 @@ export function parseCommonArgs(args) {
107
107
  extra,
108
108
  };
109
109
  }
110
+ export function parsePrNumber(value) {
111
+ if (/^\d+$/.test(value))
112
+ return parseInt(value, 10);
113
+ const match = value.match(/^https?:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)(?:[/?#].*)?$/);
114
+ if (match)
115
+ return parseInt(match[1], 10);
116
+ return null;
117
+ }
110
118
  /** Get the value of a flag like `--flag value` or `--flag=value`. */
111
119
  export function getFlag(args, name) {
112
120
  for (let i = 0; i < args.length; i++) {
@@ -0,0 +1,54 @@
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
+ ]);
8
+ const DEFAULT_ITERATE_BOOLEAN_FLAGS = new Set([
9
+ "--verbose",
10
+ "--no-auto-mark-ready",
11
+ "--no-auto-cancel-actionable",
12
+ ]);
13
+ export function isDefaultIterateInvocation(subcommand) {
14
+ return (subcommand === undefined ||
15
+ parsePrNumber(subcommand) !== null ||
16
+ isDefaultIterateFlag(subcommand));
17
+ }
18
+ export function validateDefaultIterateArgs(args) {
19
+ let sawPr = false;
20
+ for (let i = 0; i < args.length; i += 1) {
21
+ const arg = args[i];
22
+ if (DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(arg)) {
23
+ if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
24
+ writeDefaultUsageError(arg);
25
+ return false;
26
+ }
27
+ i += 1;
28
+ continue;
29
+ }
30
+ const inlineFlag = arg.split("=", 1)[0];
31
+ if (DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(inlineFlag))
32
+ continue;
33
+ if (DEFAULT_ITERATE_BOOLEAN_FLAGS.has(arg))
34
+ continue;
35
+ if (parsePrNumber(arg) !== null && !sawPr) {
36
+ sawPr = true;
37
+ continue;
38
+ }
39
+ writeDefaultUsageError(arg);
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ function isDefaultIterateFlag(arg) {
45
+ const name = arg.split("=", 1)[0];
46
+ return DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(name) || DEFAULT_ITERATE_BOOLEAN_FLAGS.has(arg);
47
+ }
48
+ function writeDefaultUsageError(arg) {
49
+ process.stderr.write(`Unknown subcommand: ${arg}\n`);
50
+ process.stderr.write("Usage: pr-shepherd [PR] [options]\n" +
51
+ " pr-shepherd <check|resolve|commit-suggestion|iterate|monitor|status|log-file> [options]\n" +
52
+ " pr-shepherd --version | -v\n");
53
+ process.exitCode = 1;
54
+ }
@@ -0,0 +1,22 @@
1
+ export function validateDurationFlag(command, flag, value, presentAsSeparateArg) {
2
+ if (value === null) {
3
+ if (presentAsSeparateArg) {
4
+ process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 15m)\n`);
5
+ process.exitCode = 1;
6
+ return null;
7
+ }
8
+ return undefined;
9
+ }
10
+ const trimmed = value.trim();
11
+ if (trimmed.startsWith("--")) {
12
+ process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 15m)\n`);
13
+ process.exitCode = 1;
14
+ return null;
15
+ }
16
+ if (!/^\d+(?:m|min|minutes?|h|hours?)?$/.test(trimmed)) {
17
+ process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 5m, 2h, 10m, or 1h.\n`);
18
+ process.exitCode = 1;
19
+ return null;
20
+ }
21
+ return trimmed;
22
+ }
@@ -1,9 +1,12 @@
1
1
  import { renderResolveCommand } from "../commands/iterate.mjs";
2
- import { safeFence } from "./fence.mjs";
3
2
  import { joinSections } from "../util/markdown.mjs";
4
3
  import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
5
- import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, } from "./list-formatters.mjs";
6
- export function formatFixCodeResult(header, result) {
4
+ import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, renderThreadResolutionStatusTag, } from "./list-formatters.mjs";
5
+ import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
6
+ export function formatFixCodeResult(header, result, opts) {
7
+ const runtime = opts?.runtime ?? "claude";
8
+ const readyDelaySuffix = opts?.readyDelaySuffix;
9
+ const retryInterval = opts?.retryInterval;
7
10
  const sections = [header];
8
11
  if (result.fix.threads.length > 0) {
9
12
  sections.push("## Review threads");
@@ -19,6 +22,12 @@ export function formatFixCodeResult(header, result) {
19
22
  }
20
23
  }
21
24
  }
25
+ if (result.fix.resolutionOnlyThreads.length > 0) {
26
+ sections.push("## Review threads to resolve");
27
+ sections.push(result.fix.resolutionOnlyThreads
28
+ .map((t) => renderThreadBullet(t, { statusTag: renderThreadResolutionStatusTag(t) }))
29
+ .join("\n"));
30
+ }
22
31
  if (result.fix.actionableComments.length > 0) {
23
32
  sections.push("## Actionable comments");
24
33
  for (const c of result.fix.actionableComments) {
@@ -37,16 +46,13 @@ export function formatFixCodeResult(header, result) {
37
46
  : ch.detailsUrl
38
47
  ? `external \`${ch.detailsUrl}\``
39
48
  : "(no runId)";
40
- const lines = [`- ${locator} \`${workflowPrefix}${jobLabel}\``];
41
- if (ch.failedStep)
42
- lines.push(` > ${ch.failedStep}`);
43
- if (ch.summary)
44
- lines.push(` > ${ch.summary}`);
45
- if (ch.logTail) {
46
- const fence = safeFence(ch.logTail);
47
- lines.push(` ${fence}`);
48
- lines.push(ch.logTail.replace(/^/gm, " ").trimEnd());
49
- lines.push(` ${fence}`);
49
+ const conclusionTag = ch.conclusion !== null ? ` [conclusion: ${ch.conclusion}]` : "";
50
+ const lines = [`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}`];
51
+ if (ch.conclusion !== "CANCELLED") {
52
+ if (ch.failedStep)
53
+ lines.push(` > ${ch.failedStep}`);
54
+ if (ch.summary)
55
+ lines.push(` > ${ch.summary}`);
50
56
  }
51
57
  return lines.join("\n");
52
58
  });
@@ -85,7 +91,7 @@ export function formatFixCodeResult(header, result) {
85
91
  }
86
92
  const firstLookTotal = result.fix.firstLookThreads.length + result.fix.firstLookComments.length;
87
93
  if (firstLookTotal > 0) {
88
- sections.push(`## First-look items (${firstLookTotal}) — already closed on GitHub; acknowledge only`);
94
+ sections.push(`## First-look items (${firstLookTotal}) — acknowledge status before acting`);
89
95
  const bullets = [];
90
96
  for (const t of result.fix.firstLookThreads) {
91
97
  bullets.push(renderThreadBullet(t, { statusTag: renderFirstLookStatusTag(t) }));
@@ -96,6 +102,10 @@ export function formatFixCodeResult(header, result) {
96
102
  }
97
103
  sections.push(bullets.join("\n"));
98
104
  }
105
+ if (result.fix.inProgressRunIds.length > 0) {
106
+ sections.push("## In-progress runs");
107
+ sections.push(result.fix.inProgressRunIds.map((id) => `- \`${id}\``).join("\n"));
108
+ }
99
109
  if (result.cancelled.length > 0) {
100
110
  sections.push("## Cancelled runs");
101
111
  sections.push(result.cancelled.map((id) => `- \`${id}\``).join("\n"));
@@ -107,7 +117,7 @@ export function formatFixCodeResult(header, result) {
107
117
  }
108
118
  sections.push(postFixLines.join("\n"));
109
119
  sections.push("## Instructions");
110
- sections.push(result.fix.instructions.map((inst, i) => `${i + 1}. ${inst}`).join("\n"));
120
+ sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval)));
111
121
  return joinSections(sections);
112
122
  }
113
123
  function blockquote(body) {
@@ -1,10 +1,11 @@
1
1
  export { formatIterateResult } from "./iterate-formatter.mjs";
2
- export { projectIterateLean } from "./iterate-lean.mjs";
2
+ export { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
3
3
  import { safeFence } from "./fence.mjs";
4
- import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, } from "./list-formatters.mjs";
4
+ import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, renderThreadResolutionStatusTag, } from "./list-formatters.mjs";
5
5
  import { joinSections } from "../util/markdown.mjs";
6
6
  export function formatFetchResult(result) {
7
7
  const activeTotal = result.actionableThreads.length +
8
+ result.resolutionOnlyThreads.length +
8
9
  result.actionableComments.length +
9
10
  result.changesRequestedReviews.length +
10
11
  result.reviewSummaries.length;
@@ -25,6 +26,12 @@ export function formatFetchResult(result) {
25
26
  .map((t) => renderThreadBullet(t, { renderSuggestion: true }))
26
27
  .join("\n\n"));
27
28
  }
29
+ if (result.resolutionOnlyThreads.length > 0) {
30
+ sections.push(`## Review threads to resolve (${result.resolutionOnlyThreads.length})`);
31
+ sections.push(result.resolutionOnlyThreads
32
+ .map((t) => renderThreadBullet(t, { statusTag: renderThreadResolutionStatusTag(t) }))
33
+ .join("\n"));
34
+ }
28
35
  if (result.actionableComments.length > 0) {
29
36
  sections.push(`## Actionable PR Comments (${result.actionableComments.length})`);
30
37
  sections.push(result.actionableComments.map((c) => renderCommentBullet(c)).join("\n"));
@@ -38,7 +45,7 @@ export function formatFetchResult(result) {
38
45
  sections.push(result.reviewSummaries.map((r) => renderReviewBullet(r, { includeBody: true })).join("\n"));
39
46
  }
40
47
  if (firstLookTotal > 0) {
41
- sections.push(`## First-look items (${firstLookTotal}) — already closed on GitHub; acknowledge only`);
48
+ sections.push(`## First-look items (${firstLookTotal}) — acknowledge status before acting`);
42
49
  const bullets = [];
43
50
  for (const t of result.firstLookThreads) {
44
51
  bullets.push(renderThreadBullet(t, { statusTag: renderFirstLookStatusTag(t) }));
@@ -67,48 +74,29 @@ export function formatCommitSuggestionResult(result) {
67
74
  const range = result.startLine === result.endLine
68
75
  ? `line ${result.startLine}`
69
76
  : `lines ${result.startLine}–${result.endLine}`;
70
- function pushPatch(patch) {
71
- const fence = safeFence(patch);
77
+ lines.push(`Suggestion from @${result.author} for PR #${result.pr} — thread ${result.threadId}:`);
78
+ lines.push(` repo: ${result.repo}`);
79
+ lines.push(` ${result.path} (${range})`);
80
+ if (result.patch) {
81
+ const fence = safeFence(result.patch);
72
82
  lines.push("");
73
83
  lines.push(`${fence}diff`);
74
- lines.push(patch.trimEnd());
84
+ lines.push(result.patch.trimEnd());
75
85
  lines.push(fence);
76
86
  }
77
- if (result.dryRun) {
78
- if (result.valid) {
79
- lines.push(`Dry-run: would apply suggestion from @${result.author}:`);
80
- lines.push(` ${result.path} (${range})`);
81
- }
82
- else {
83
- lines.push(`Dry-run: suggestion cannot apply cleanly:`);
84
- lines.push(`- path: ${result.path} (${range})`);
85
- lines.push(`- author: @${result.author}`);
86
- lines.push(`- reason: ${result.reason ?? "unknown"}`);
87
- }
88
- if (result.patch)
89
- pushPatch(result.patch);
90
- }
91
- else if (result.applied) {
92
- lines.push(`Applied suggestion from @${result.author}:`);
93
- lines.push(` ${result.path} (${range})`);
94
- if (result.commitSha)
95
- lines.push(`Commit: ${result.commitSha}`);
96
- if (result.patch)
97
- pushPatch(result.patch);
98
- }
99
- else {
100
- lines.push(`Failed to apply suggestion ${result.threadId}:`);
101
- lines.push(`- path: ${result.path} (${range})`);
102
- lines.push(`- author: @${result.author}`);
103
- lines.push(`- reason: ${result.reason ?? "unknown"}`);
104
- if (result.patch)
105
- pushPatch(result.patch);
106
- }
107
- if (result.postActionInstruction) {
87
+ lines.push("");
88
+ lines.push("## Suggested commit message");
89
+ lines.push("");
90
+ lines.push(result.commitMessage);
91
+ lines.push("");
92
+ lines.push(result.commitBody);
93
+ if (result.postActionInstructions.length > 0) {
108
94
  lines.push("");
109
95
  lines.push("## Instructions");
110
96
  lines.push("");
111
- lines.push(`1. ${result.postActionInstruction}`);
97
+ result.postActionInstructions.forEach((inst, i) => {
98
+ lines.push(`${i + 1}. ${inst}`);
99
+ });
112
100
  }
113
101
  return lines.join("\n");
114
102
  }
@@ -1,24 +1,24 @@
1
1
  import { runCommitSuggestion } from "../commands/commit-suggestion.mjs";
2
2
  import { runIterate } from "../commands/iterate.mjs";
3
- import { runMonitor, formatMonitorResult } from "../commands/monitor.mjs";
3
+ import { runMonitor, formatMonitorResult, formatMonitorJson } from "../commands/monitor.mjs";
4
4
  import { runStatus, formatStatusTable } from "../commands/status.mjs";
5
5
  import { getRepoInfo } from "../github/client.mjs";
6
6
  import { loadConfig } from "../config/load.mjs";
7
+ import { detectAgentRuntime } from "../agent-runtime.mjs";
7
8
  import { parseCommonArgs, getFlag, hasFlag, parseStatusPrNumbers, parseIntStrict, } from "./args.mjs";
8
9
  import { parseDurationToMinutes, iterateActionToExitCode, deriveSimpleReady, } from "./exit-codes.mjs";
9
- import { formatCommitSuggestionResult, formatIterateResult, projectIterateLean, } from "./formatters.mjs";
10
+ import { formatCommitSuggestionResult, formatIterateResult, projectIterateLean, projectIterateVerbose, } from "./formatters.mjs";
11
+ import { validateDurationFlag } from "./duration-flag.mjs";
10
12
  export async function handleCommitSuggestion(args) {
11
13
  const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
12
14
  const threadId = getFlag(extra, "--thread-id");
13
15
  if (!threadId) {
14
- process.stderr.write("Usage: pr-shepherd commit-suggestion [PR] --thread-id ID [--message MSG] [--description DESC] [--dry-run]\n" +
15
- " (--message is required unless --dry-run is set)\n");
16
+ process.stderr.write("Usage: pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [--description DESC]\n");
16
17
  process.exitCode = 1;
17
18
  return;
18
19
  }
19
- const dryRun = hasFlag(extra, "--dry-run");
20
20
  const message = getFlag(extra, "--message") ?? undefined;
21
- if (!dryRun && (!message || message.trim() === "")) {
21
+ if (!message || message.trim() === "") {
22
22
  process.stderr.write("--message is required and must be non-empty\n");
23
23
  process.exitCode = 1;
24
24
  return;
@@ -30,23 +30,20 @@ export async function handleCommitSuggestion(args) {
30
30
  threadId,
31
31
  message,
32
32
  description,
33
- dryRun,
34
33
  });
35
34
  process.stdout.write(globalOpts.format === "json"
36
35
  ? `${JSON.stringify(result, null, 2)}\n`
37
36
  : `${formatCommitSuggestionResult(result)}\n`);
38
- if (result.dryRun) {
39
- process.exitCode = result.valid ? 0 : 1;
40
- }
41
- else {
42
- process.exitCode = result.applied ? 0 : 1;
43
- }
44
37
  }
45
38
  export async function handleIterate(args) {
46
39
  const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
40
+ const runtime = detectAgentRuntime();
47
41
  const readyDelayStr = getFlag(extra, "--ready-delay");
42
+ const readyDelaySuffix = validateDurationFlag("pr-shepherd", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
43
+ if (readyDelaySuffix === null)
44
+ return;
48
45
  const cfg = loadConfig();
49
- const readyDelaySeconds = parseDurationToMinutes(readyDelayStr ?? "", cfg.watch.readyDelayMinutes) * 60;
46
+ const readyDelaySeconds = parseDurationToMinutes(readyDelaySuffix ?? "", cfg.watch.readyDelayMinutes) * 60;
50
47
  const cooldownSecondsStr = getFlag(extra, "--cooldown-seconds");
51
48
  const cooldownSeconds = cooldownSecondsStr
52
49
  ? parseIntStrict(cooldownSecondsStr, "--cooldown-seconds")
@@ -66,24 +63,28 @@ export async function handleIterate(args) {
66
63
  noAutoMarkReady,
67
64
  noAutoCancelActionable,
68
65
  });
66
+ const projectionOpts = { runtime, readyDelaySuffix, retryInterval: cfg.watch.interval };
69
67
  if (globalOpts.format === "json") {
70
- const output = globalOpts.verbose ? result : projectIterateLean(result);
68
+ const output = globalOpts.verbose
69
+ ? projectIterateVerbose(result, projectionOpts)
70
+ : projectIterateLean(result, projectionOpts);
71
71
  process.stdout.write(`${JSON.stringify(output)}\n`);
72
72
  }
73
73
  else {
74
- process.stdout.write(`${formatIterateResult(result, { verbose: globalOpts.verbose })}\n`);
74
+ process.stdout.write(`${formatIterateResult(result, {
75
+ verbose: globalOpts.verbose,
76
+ ...projectionOpts,
77
+ })}\n`);
75
78
  }
76
79
  process.exitCode = iterateActionToExitCode(result.action);
77
80
  }
78
81
  export async function handleMonitor(args) {
79
82
  const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
83
+ const runtime = detectAgentRuntime();
80
84
  const readyDelayStr = getFlag(extra, "--ready-delay");
81
- if (hasFlag(extra, "--ready-delay") &&
82
- (readyDelayStr === null || readyDelayStr.startsWith("--"))) {
83
- process.stderr.write("pr-shepherd monitor: --ready-delay requires a value (e.g. --ready-delay 15m)\n");
84
- process.exitCode = 1;
85
+ const readyDelaySuffix = validateDurationFlag("pr-shepherd monitor", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
86
+ if (readyDelaySuffix === null)
85
87
  return;
86
- }
87
88
  const remaining = [];
88
89
  for (let i = 0; i < extra.length; i++) {
89
90
  const a = extra[i];
@@ -108,7 +109,8 @@ export async function handleMonitor(args) {
108
109
  result = await runMonitor({
109
110
  ...globalOpts,
110
111
  prNumber,
111
- readyDelaySuffix: readyDelayStr ?? undefined,
112
+ readyDelaySuffix: readyDelaySuffix ?? undefined,
113
+ runtime,
112
114
  });
113
115
  }
114
116
  catch (err) {
@@ -117,8 +119,8 @@ export async function handleMonitor(args) {
117
119
  return;
118
120
  }
119
121
  process.stdout.write(globalOpts.format === "json"
120
- ? `${JSON.stringify(result, null, 2)}\n`
121
- : `${formatMonitorResult(result)}\n`);
122
+ ? `${JSON.stringify(formatMonitorJson(result, { runtime }), null, 2)}\n`
123
+ : `${formatMonitorResult(result, { runtime })}\n`);
122
124
  }
123
125
  export async function handleStatus(args) {
124
126
  const { global: globalOpts } = parseCommonArgs(args);