pr-shepherd 0.35.0 → 0.36.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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +32 -12
  3. package/bin/checks/triage.mjs +11 -3
  4. package/bin/cli/args.mjs +1 -0
  5. package/bin/cli/default-poll.mjs +1 -0
  6. package/bin/cli/help-command-pages.d.mts +1 -1
  7. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  8. package/bin/cli/help-iterate-poll-pages.mjs +10 -5
  9. package/bin/cli/help-top-page.d.mts +1 -1
  10. package/bin/cli/help-top-page.mjs +2 -1
  11. package/bin/cli/help.d.mts +2 -2
  12. package/bin/cli/iterate-formatter.mjs +11 -2
  13. package/bin/cli/iterate-lean.mjs +1 -0
  14. package/bin/cli/poll-handler.mjs +7 -0
  15. package/bin/commands/check.d.mts +1 -0
  16. package/bin/commands/check.mjs +26 -22
  17. package/bin/commands/commit-suggestion.mjs +11 -5
  18. package/bin/commands/iterate/fix-code.mjs +1 -1
  19. package/bin/commands/iterate/helpers.d.mts +1 -0
  20. package/bin/commands/iterate/helpers.mjs +15 -6
  21. package/bin/commands/iterate/index.mjs +3 -10
  22. package/bin/commands/poll.d.mts +2 -0
  23. package/bin/commands/poll.mjs +51 -17
  24. package/bin/commands/ready-delay.mjs +3 -13
  25. package/bin/config/load.mjs +67 -25
  26. package/bin/github/batch-page-helpers.d.mts +45 -0
  27. package/bin/github/batch-page-helpers.mjs +63 -0
  28. package/bin/github/batch-page.d.mts +14 -0
  29. package/bin/github/batch-page.mjs +62 -0
  30. package/bin/github/batch-parse-suites.d.mts +4 -0
  31. package/bin/github/batch-parse-suites.mjs +25 -0
  32. package/bin/github/batch-parsers-rules.d.mts +6 -0
  33. package/bin/github/batch-parsers-rules.mjs +122 -0
  34. package/bin/github/batch-parsers.mjs +6 -0
  35. package/bin/github/batch-raw-rules.d.mts +59 -0
  36. package/bin/github/batch-raw-rules.mjs +1 -0
  37. package/bin/github/batch-raw-types.d.mts +20 -11
  38. package/bin/github/batch.d.mts +2 -0
  39. package/bin/github/batch.mjs +14 -120
  40. package/bin/github/gql/batch-pr-page.gql +189 -0
  41. package/bin/github/gql/batch-pr.gql +74 -21
  42. package/bin/github/gql/commit-suggestion-thread.gql +40 -0
  43. package/bin/github/http.d.mts +2 -1
  44. package/bin/github/http.mjs +2 -1
  45. package/bin/github/pagination.d.mts +3 -2
  46. package/bin/github/pagination.mjs +3 -2
  47. package/bin/github/queries.d.mts +4 -0
  48. package/bin/github/queries.mjs +4 -0
  49. package/bin/github/rest-http.d.mts +6 -1
  50. package/bin/github/rest-http.mjs +25 -86
  51. package/bin/github/rest-text.d.mts +1 -0
  52. package/bin/github/rest-text.mjs +88 -0
  53. package/bin/github/suggestion-thread.d.mts +9 -0
  54. package/bin/github/suggestion-thread.mjs +45 -0
  55. package/bin/github/thread-comments.mjs +12 -8
  56. package/bin/merge-status/derive.mjs +2 -0
  57. package/bin/merge-status/requirements-format.d.mts +3 -0
  58. package/bin/merge-status/requirements-format.mjs +88 -0
  59. package/bin/merge-status/requirements.d.mts +2 -0
  60. package/bin/merge-status/requirements.mjs +51 -0
  61. package/bin/state/base.d.mts +9 -0
  62. package/bin/state/base.mjs +23 -0
  63. package/bin/state/bot-cr-seen.mjs +3 -13
  64. package/bin/state/fix-attempts.mjs +3 -13
  65. package/bin/state/iterate-stall.mjs +3 -13
  66. package/bin/state/seen-comments.mjs +6 -13
  67. package/bin/types/github.d.mts +4 -7
  68. package/bin/types/iterate.d.mts +6 -7
  69. package/bin/types/merge-requirements.d.mts +82 -0
  70. package/bin/types/merge-requirements.mjs +2 -0
  71. package/bin/types.d.mts +1 -0
  72. package/bin/types.mjs +1 -0
  73. package/bin/util/path-segment.d.mts +2 -0
  74. package/bin/util/path-segment.mjs +2 -0
  75. package/bin/util/pool.d.mts +2 -0
  76. package/bin/util/pool.mjs +18 -0
  77. package/package.json +1 -1
  78. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  79. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  80. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -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.35.0",
4
+ "version": "0.36.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -2,11 +2,28 @@
2
2
 
3
3
  Autonomous PR CI monitor and review-comment resolver for agentic coding tools, including Claude Code and Codex.
4
4
 
5
- The goal is to help an agent carry a planned change to a human-reviewable PR: passing CI, no unresolved Shepherd-visible work, and a useful PR description/journal.
5
+ ## Why
6
+
7
+ An agent finishing a PR should think about code, not reconstruct GitHub state or invent a next-step policy each tick. Without Shepherd it fans out across GitHub MCP, `gh`, and GraphQL, then guesses what to do with the result.
8
+
9
+ ## What it does
10
+
11
+ 1. **Gather all context for a PR** in one invocation: review threads, comments, replies, summaries, CI, mergeability, merge requirements, first-look / outdated / edited items, and author provenance.
12
+ 2. **Provide deterministic actions for the agent**: exactly one of `WAIT`, `MARK_READY`, `FIX_CODE`, `CANCEL`, or `ESCALATE`, plus numbered `## Instructions` and explicit `apply` / `build_suggestion_patch` mutations. The agent still decides whether a comment or CI failure needs a code change. Shepherd does not classify signal vs noise and does not mutate git.
13
+
14
+ Highlights:
15
+
16
+ - Batched GraphQL reads and writes (plus REST where GraphQL cannot) so one poll replaces a tool-call fan-out. MCP `iterate` is one tick and the client owns recurrence; `--debounce` is a poll-dispatcher settle window, not an MCP tool.
17
+ - CI summaries include failed checks, and the failed job/step plus a log excerpt when triage can fetch them. Job and log details are omitted for `STARTUP_FAILURE` and `CANCELLED`; agents may still inspect logs.
18
+ - Handles GitHub comment types (comments, threads, replies) and their states, including first-look, outdated, resolved, minimized, and edited.
19
+ - `apply` batches resolve / reply / minimize / dismiss. `build_suggestion_patch` emits a unified diff in output, not a patch file, and does not mutate git.
20
+ - `BEHIND` is mergeability information, not a rebase or a guarantee that the next push is at the default-branch tip. The agent can update the branch before pushing.
21
+
22
+ Full reference: [docs/README.md](docs/README.md). Feature matrix: [docs/features.md](docs/features.md).
6
23
 
7
24
  ## How It Works
8
25
 
9
- `pr-shepherd` moves deterministic PR orchestration into a local MCP server, with a CLI for shells and CI. Both interfaces fetch the same GitHub state, emit raw-enough context, and return a numbered plan for the calling agent to follow. The agent still decides whether a comment or CI failure requires a code change.
26
+ `pr-shepherd` moves deterministic PR orchestration into a local MCP server, with a CLI for shells and CI. Both interfaces fetch the same GitHub state, emit raw-enough context, and return a numbered plan for the calling agent to follow.
10
27
 
11
28
  The MCP server exposes three tools: `iterate`, `apply`, and `build_suggestion_patch`. `apply` accepts ordered review mutations, file-view mutations, and journal entries. The shipped skills are thin dispatchers for those tools.
12
29
 
@@ -25,8 +42,10 @@ Example shape:
25
42
 
26
43
  # PR #123 [FIX_CODE]
27
44
 
28
- **status** `UNRESOLVED_COMMENTS` · **merge** `BLOCKED` · **state** `OPEN` · **repo** `owner/repo`
45
+ **status** `UNRESOLVED_COMMENTS` · **merge** `CLEAN` · **state** `OPEN` · **repo** `owner/repo`
29
46
  **summary** 3 passing
47
+ Approvals: None [Not Required]
48
+ Conversations Resolved: No [Not Required]
30
49
 
31
50
  ## Review threads
32
51
 
@@ -39,20 +58,20 @@ Example shape:
39
58
  - `24697658766` — `CI › lint / typecheck / test (22.x)` [conclusion: FAILURE]
40
59
  > oxfmt
41
60
 
42
- ## Post-fix plan
61
+ ## Post-fix push
43
62
 
44
63
  - base: `main`
45
- - apply: reply to `PRRT_kwDOSGizTs58XB1L` after the relevant HEAD SHA is visible
64
+ - apply review: `pr-shepherd apply review 123 --reply-thread-ids PRRT_kwDOSGizTs58XB1L --message "$DISMISS_MESSAGE" --require-sha "$HEAD_SHA"`
46
65
 
47
66
  ## Instructions
48
67
 
49
- 1. Decide for each item under `## Review threads` and `## Failing checks` whether a code change is warranted. If code changes are needed, apply edits, commit, and push according to the repository's conventions.
68
+ 1. Decide for each item under `## Review threads` and `## Failing checks` whether a code change is warranted. If code changes are needed, apply edits, commit, push, then run the `apply review:` command.
50
69
  2. For each failing check under `## Failing checks`: fetch logs when needed and decide whether to rerun or fix.
51
- 3. Use `apply` with a specific reply/dismiss message and the relevant HEAD SHA.
70
+ 3. Run the `apply review:` command shown above, substituting `$HEAD_SHA` and `$DISMISS_MESSAGE`.
52
71
  4. Stop this iteration.
53
72
  ```
54
73
 
55
- See [docs/actions.md](docs/actions.md) for the complete output contract.
74
+ See [docs/actions.md](docs/actions.md) for the complete output contract. Iterate/poll PR outcomes use exit codes `0` and `10`–`14`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
56
75
 
57
76
  ## Workflow Assumptions
58
77
 
@@ -100,6 +119,7 @@ pr-shepherd 42 # poll until non-WAIT or timeout
100
119
  pr-shepherd 42 --interval 60s --timeout 270s
101
120
  pr-shepherd 42 --quiet-status # print only changed WAIT status snapshots
102
121
  pr-shepherd 42 --until-terminal # continue through WAIT/MARK_READY until work or terminal state
122
+ pr-shepherd 42 --debounce 5m # wait 5m after first FIX_CODE, then return one batched tick
103
123
  pr-shepherd 42 --ready-delay 15m
104
124
  pr-shepherd iterate 42 # single tick
105
125
  ```
@@ -179,7 +199,7 @@ Replace `<version>` with a published version. Full config-file examples, tool sc
179
199
 
180
200
  ## Configuration
181
201
 
182
- Create `.pr-shepherdrc.yml` in your project root or an ancestor directory.
202
+ Create `.pr-shepherdrc.yml` in your project root, an ancestor directory, or `$HOME`. Every file on the walk is deep-merged; closer directories override farther ones.
183
203
 
184
204
  ```yaml
185
205
  ignoreChecks:
@@ -231,9 +251,9 @@ TypeScript rules are loaded by the runtime's native TypeScript support; keep the
231
251
 
232
252
  Ready-to-use examples for common patterns are in [`examples/classification/`](examples/classification/).
233
253
 
234
- ## Compatibility aliases
254
+ ## CLI aliases
235
255
 
236
- The legacy CLI subcommands `poll`, `resolve`, `commit-suggestion`, and `mark-files-as-viewed` remain available for compatibility but are no longer advertised as the primary integration. Prefer canonical default polling/`iterate` in a shell and the MCP `iterate`, `apply`, and `build_suggestion_patch` tools in an agent client.
256
+ `poll`, `resolve`, `commit-suggestion`, `mark-files-as-viewed`, `journal`, `clean`, and `log-file` are CLI aliases. Prefer default polling/`iterate` in a shell and the MCP `iterate`, `apply`, and `build_suggestion_patch` tools in an agent client.
237
257
 
238
258
  ## Requirements
239
259
 
@@ -243,7 +263,7 @@ The legacy CLI subcommands `poll`, `resolve`, `commit-suggestion`, and `mark-fil
243
263
 
244
264
  ## Docs
245
265
 
246
- Full reference: [docs/README.md](docs/README.md).
266
+ Full reference, grouped by the two jobs (gather context / emit actions): [docs/README.md](docs/README.md).
247
267
 
248
268
  ## Harness Ecosystem
249
269
 
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable max-lines */
2
- import { rest, restText } from "../github/http.mjs";
2
+ import { restWithRateLimit, restText } from "../github/http.mjs";
3
3
  const STARTUP_FAILURE_STATUS = "startup_failure";
4
4
  const LOG_EXCERPT_CONTEXT_LINES = 16;
5
5
  const LOG_EXCERPT_TAIL_LINES = 28;
@@ -43,10 +43,14 @@ async function fetchStartupFailureChecksUncached(repo, headSha, prNumber) {
43
43
  const MAX_RUN_PAGES = 10;
44
44
  const checks = [];
45
45
  for (let page = 1; page <= MAX_RUN_PAGES; page++) {
46
- const data = await rest("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`);
46
+ const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`);
47
47
  checks.push(...data.workflow_runs
48
48
  .filter((run) => runBelongsToPr(run, prNumber, headSha))
49
49
  .map(workflowRunToCheckRun));
50
+ if (rateLimit?.remaining === 0) {
51
+ process.stderr.write(`pr-shepherd: REST rate limit remaining is 0 while listing startup-failure runs for ${headSha} — detection may be incomplete\n`);
52
+ break;
53
+ }
50
54
  if (data.workflow_runs.length < perPage)
51
55
  break;
52
56
  if (page === MAX_RUN_PAGES) {
@@ -91,8 +95,12 @@ async function fetchJobsUncached(runId, repo) {
91
95
  process.stderr.write(`pr-shepherd: job pagination cap (${MAX_JOB_PAGES * 100} jobs) reached for run ${runId} — triage may be incomplete\n`);
92
96
  break;
93
97
  }
94
- const data = await rest("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`);
98
+ const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`);
95
99
  allJobs.push(...data.jobs);
100
+ if (rateLimit?.remaining === 0) {
101
+ process.stderr.write(`pr-shepherd: REST rate limit remaining is 0 while listing jobs for run ${runId} — triage may be incomplete\n`);
102
+ break;
103
+ }
96
104
  if (data.jobs.length < perPage)
97
105
  break;
98
106
  }
package/bin/cli/args.mjs CHANGED
@@ -18,6 +18,7 @@ const FLAGS_WITH_VALUES = new Set([
18
18
  "--dismiss-review-ids",
19
19
  "--interval",
20
20
  "--timeout",
21
+ "--debounce",
21
22
  "--match",
22
23
  ]);
23
24
  // Boolean flags that do NOT consume the next argument. Any --flag not in this
@@ -8,6 +8,7 @@ const DEFAULT_POLL_FLAGS_WITH_VALUES = new Set([
8
8
  "--stall-timeout",
9
9
  "--interval",
10
10
  "--timeout",
11
+ "--debounce",
11
12
  ]);
12
13
  const DEFAULT_POLL_BOOLEAN_FLAGS = new Set([
13
14
  "--verbose",
@@ -177,7 +177,7 @@ Flags:
177
177
  PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
178
178
  Exit code: 0 on success; nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
179
179
  readonly iterate: "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate code action; run another iterate tick later.\n MARK_READY Draft PR was marked ready for review.\n FIX_CODE Apply fixes, commit, push, and run the printed apply command.\n CANCEL Terminal state: merged/closed or ready-delay elapsed.\n ESCALATE Terminal state requiring human direction.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
180
- readonly poll: "pr-shepherd poll\n\nRun iterate repeatedly while the action is WAIT. Print only the final tick to stdout.\nPoll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. With --until-terminal, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR] [poll-flags] [iterate-flags]\n\nPoll flags:\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
180
+ readonly poll: "pr-shepherd poll\n\nRun iterate repeatedly for WAIT ticks and during the FIX_CODE debounce window. Print only the\nfinal tick to stdout.\nPoll exits as soon as iterate returns MARK_READY, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. FIX_CODE starts a --debounce settle window (default 1m): poll keeps\niterating at --interval, then runs one more tick after the window and returns that result.\nWith --until-terminal, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR] [poll-flags] [iterate-flags]\n\nPoll flags:\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.\nFIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
181
181
  readonly clean: `pr-shepherd clean
182
182
 
183
183
  Remove pr-shepherd state files from PR_SHEPHERD_STATE_DIR.
@@ -1,4 +1,4 @@
1
1
  export declare const ITERATE_USAGE = "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate code action; run another iterate tick later.\n MARK_READY Draft PR was marked ready for review.\n FIX_CODE Apply fixes, commit, push, and run the printed apply command.\n CANCEL Terminal state: merged/closed or ready-delay elapsed.\n ESCALATE Terminal state requiring human direction.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
2
- export declare const POLL_USAGE = "pr-shepherd poll\n\nRun iterate repeatedly while the action is WAIT. Print only the final tick to stdout.\nPoll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. With --until-terminal, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR] [poll-flags] [iterate-flags]\n\nPoll flags:\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
2
+ export declare const POLL_USAGE = "pr-shepherd poll\n\nRun iterate repeatedly for WAIT ticks and during the FIX_CODE debounce window. Print only the\nfinal tick to stdout.\nPoll exits as soon as iterate returns MARK_READY, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. FIX_CODE starts a --debounce settle window (default 1m): poll keeps\niterating at --interval, then runs one more tick after the window and returns that result.\nWith --until-terminal, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR] [poll-flags] [iterate-flags]\n\nPoll flags:\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.\nFIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
3
3
  /** Public help page for the default PR polling invocation. */
4
4
  export declare const DEFAULT_USAGE: string;
@@ -34,16 +34,20 @@ Exit codes:
34
34
  A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).`;
35
35
  export const POLL_USAGE = `pr-shepherd poll
36
36
 
37
- Run iterate repeatedly while the action is WAIT. Print only the final tick to stdout.
38
- Poll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout
39
- returns the last WAIT result. With --until-terminal, poll also continues through MARK_READY.
37
+ Run iterate repeatedly for WAIT ticks and during the FIX_CODE debounce window. Print only the
38
+ final tick to stdout.
39
+ Poll exits as soon as iterate returns MARK_READY, CANCEL, or ESCALATE, or when timeout
40
+ returns the last WAIT result. FIX_CODE starts a --debounce settle window (default 1m): poll keeps
41
+ iterating at --interval, then runs one more tick after the window and returns that result.
42
+ With --until-terminal, poll also continues through MARK_READY.
40
43
 
41
44
  Usage:
42
45
  pr-shepherd poll [PR] [poll-flags] [iterate-flags]
43
46
 
44
47
  Poll flags:
45
48
  --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.
46
- --timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.
49
+ --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: 4.5m.
50
+ --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.
47
51
  --quiet-status During WAIT polling, print only changed status snapshots.
48
52
  --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.
49
53
 
@@ -57,9 +61,10 @@ Forwarded iterate flags:
57
61
  --help, -h Print this help and exit before GitHub, git, config, or log I/O.
58
62
 
59
63
  Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds
60
- for --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with
64
+ for --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with
61
65
  an explicit unit (4.5m).
62
66
  Each WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.
67
+ FIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.
63
68
  With --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.
64
69
 
65
70
  Exit codes: same as iterate (the final tick's action/reason decides the code).
@@ -1 +1 @@
1
- export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patch [PR] --thread-id ID --message MSG [flags]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark changed files as viewed in GitHub.\n apply journal Append a list item to the ## Shepherd Journal section of a PR body.\n build-suggestion-patch\n Convert one GitHub suggestion thread into a patch and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap. Bare number = seconds. Default: 4.5m.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
1
+ export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patch [PR] --thread-id ID --message MSG [flags]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark changed files as viewed in GitHub.\n apply journal Append a list item to the ## Shepherd Journal section of a PR body.\n build-suggestion-patch\n Convert one GitHub suggestion thread into a patch and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
@@ -42,7 +42,8 @@ Iterate flags:
42
42
 
43
43
  Polling flags:
44
44
  --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.
45
- --timeout <duration> Poll wall-clock cap. Bare number = seconds. Default: 4.5m.
45
+ --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.
46
+ --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.
46
47
  --quiet-status During WAIT polling, print only changed status snapshots.
47
48
  --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.
48
49
 
@@ -177,7 +177,7 @@ Flags:
177
177
  PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
178
178
  Exit code: 0 on success; nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
179
179
  readonly iterate: "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate code action; run another iterate tick later.\n MARK_READY Draft PR was marked ready for review.\n FIX_CODE Apply fixes, commit, push, and run the printed apply command.\n CANCEL Terminal state: merged/closed or ready-delay elapsed.\n ESCALATE Terminal state requiring human direction.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
180
- readonly poll: "pr-shepherd poll\n\nRun iterate repeatedly while the action is WAIT. Print only the final tick to stdout.\nPoll exits as soon as iterate returns MARK_READY, FIX_CODE, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. With --until-terminal, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR] [poll-flags] [iterate-flags]\n\nPoll flags:\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
180
+ readonly poll: "pr-shepherd poll\n\nRun iterate repeatedly for WAIT ticks and during the FIX_CODE debounce window. Print only the\nfinal tick to stdout.\nPoll exits as soon as iterate returns MARK_READY, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. FIX_CODE starts a --debounce settle window (default 1m): poll keeps\niterating at --interval, then runs one more tick after the window and returns that result.\nWith --until-terminal, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR] [poll-flags] [iterate-flags]\n\nPoll flags:\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.\nFIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
181
181
  readonly clean: `pr-shepherd clean
182
182
 
183
183
  Remove pr-shepherd state files from PR_SHEPHERD_STATE_DIR.
@@ -228,7 +228,7 @@ Flags:
228
228
 
229
229
  Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
230
230
  readonly "log-file": "pr-shepherd log-file\n\nPrint the per-worktree append-only debug log path for the current repository.\nThe log is created by the first non-help pr-shepherd command that initializes logging.\n\nUsage:\n pr-shepherd log-file [--format text|json]\n\nFlags:\n --format text|json Print a raw path or {\"path\": \"...\"} JSON. Default: text.\n --help, -h Print this help and exit before logging setup.\n\nEnvironment:\n PR_SHEPHERD_LOG_DISABLED=1 disables logging.\n PR_SHEPHERD_STATE_DIR overrides the base state directory.\n\nExit code: 0 on success; 1 if repository identity cannot be resolved.";
231
- readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patch [PR] --thread-id ID --message MSG [flags]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark changed files as viewed in GitHub.\n apply journal Append a list item to the ## Shepherd Journal section of a PR body.\n build-suggestion-patch\n Convert one GitHub suggestion thread into a patch and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap. Bare number = seconds. Default: 4.5m.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
231
+ readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patch [PR] --thread-id ID --message MSG [flags]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark changed files as viewed in GitHub.\n apply journal Append a list item to the ## Shepherd Journal section of a PR body.\n build-suggestion-patch\n Convert one GitHub suggestion thread into a patch and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
232
232
  };
233
233
  /** Resolve help keys for nested public commands before any command I/O. */
234
234
  export declare function helpKeyForArgs(args: string[]): keyof typeof USAGE;
@@ -1,6 +1,7 @@
1
1
  import { formatFixCodeResult } from "./fix-formatter.mjs";
2
2
  import { joinSections } from "../util/markdown.mjs";
3
3
  import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, } from "./iterate-instructions.mjs";
4
+ import { formatMergeRequirementLines } from "../merge-status/requirements-format.mjs";
4
5
  function formatActivityLine(result) {
5
6
  const activity = result.activity ?? {
6
7
  commitCount: 0,
@@ -112,8 +113,12 @@ export function formatIterateResult(result, opts) {
112
113
  }
113
114
  const requiredLine = requiredParts.length > 0 ? `**required** ${requiredParts.join(", ")}` : null;
114
115
  const headerLines = [heading, "", baseLine, summaryLine];
115
- if (requiredLine)
116
+ if (result.mergeRequirements) {
117
+ headerLines.push(...formatMergeRequirementLines(result.mergeRequirements));
118
+ }
119
+ else if (requiredLine) {
116
120
  headerLines.push(requiredLine);
121
+ }
117
122
  if (result.ignoredNames && result.ignoredNames.length > 0) {
118
123
  const names = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
119
124
  headerLines.push(`**ignored** ${names}`);
@@ -141,8 +146,12 @@ export function formatIterateResult(result, opts) {
141
146
  ]);
142
147
  case "cancel": {
143
148
  const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
144
- if (requiredLine)
149
+ if (result.mergeRequirements) {
150
+ cancelHeaderLines.push(...formatMergeRequirementLines(result.mergeRequirements));
151
+ }
152
+ else if (requiredLine) {
145
153
  cancelHeaderLines.push(requiredLine);
154
+ }
146
155
  if (result.ignoredNames && result.ignoredNames.length > 0) {
147
156
  const ignoredStr = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
148
157
  cancelHeaderLines.push(`**ignored** ${ignoredStr}`);
@@ -41,6 +41,7 @@ export function projectIterateLean(result, opts) {
41
41
  }),
42
42
  ...(result.baseBranch && { baseBranch: result.baseBranch }),
43
43
  ...(result.branchProtection !== null && { branchProtection: result.branchProtection }),
44
+ ...(result.mergeRequirements && { mergeRequirements: result.mergeRequirements }),
44
45
  ...(hasActivity && {
45
46
  activity: {
46
47
  commitCount: activity.commitCount,
@@ -7,6 +7,7 @@ import { parseIterateFlags } from "./iterate-flags.mjs";
7
7
  import { emitIterateResult } from "./iterate-emitter.mjs";
8
8
  const DEFAULT_POLL_INTERVAL_SECONDS = 60;
9
9
  const DEFAULT_POLL_TIMEOUT_SECONDS = 270;
10
+ const DEFAULT_POLL_DEBOUNCE_SECONDS = 60;
10
11
  export async function handlePoll(args) {
11
12
  const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
12
13
  const cfg = loadConfig();
@@ -23,6 +24,11 @@ export async function handlePoll(args) {
23
24
  if (timeoutSuffix === null)
24
25
  return;
25
26
  const timeoutSeconds = parseDurationToSeconds(timeoutSuffix ?? "", DEFAULT_POLL_TIMEOUT_SECONDS);
27
+ const debounceStr = getFlag(extra, "--debounce");
28
+ const debounceSuffix = validateSecondsDurationFlag("pr-shepherd", "--debounce", debounceStr, hasFlag(extra, "--debounce"), { allowZero: true });
29
+ if (debounceSuffix === null)
30
+ return;
31
+ const debounceSeconds = parseDurationToSeconds(debounceSuffix ?? "", DEFAULT_POLL_DEBOUNCE_SECONDS, { allowZero: true });
26
32
  const result = await runPoll({
27
33
  ...globalOpts,
28
34
  prNumber,
@@ -32,6 +38,7 @@ export async function handlePoll(args) {
32
38
  noAutoCancelActionable: flags.noAutoCancelActionable,
33
39
  intervalSeconds,
34
40
  timeoutSeconds,
41
+ debounceSeconds,
35
42
  quietStatus: hasFlag(extra, "--quiet-status"),
36
43
  untilTerminal: hasFlag(extra, "--until-terminal"),
37
44
  });
@@ -3,4 +3,5 @@ export declare function runCheck(opts: GlobalOptions & {
3
3
  autoResolve?: boolean;
4
4
  autoMinimizeSuppressed?: boolean;
5
5
  skipTriage?: boolean;
6
+ persistSeen?: boolean;
6
7
  }): Promise<ShepherdReport>;
@@ -38,7 +38,9 @@ export async function runCheck(opts) {
38
38
  if (mergeStatus.state === "MERGED" || mergeStatus.state === "CLOSED") {
39
39
  return buildTerminalReport(prNumber, repo, batchData, mergeStatus, mergeStatus.state);
40
40
  }
41
- const startupFailureChecks = await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
41
+ const startupFailureChecks = result.checkSuitesComplete
42
+ ? []
43
+ : await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
42
44
  const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
43
45
  const classifiedChecks = classifyChecks(allChecks);
44
46
  const verdict = getCiVerdict(classifiedChecks);
@@ -80,27 +82,29 @@ export async function runCheck(opts) {
80
82
  }
81
83
  const changesRequestedReviewVisibility = classifyChangesRequestedReviewsForDisplay(batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)), seenMap, botUsernames);
82
84
  const approvedReviewVisibility = classifyReviewsForDisplay(batchData.approvedReviews, seenMap);
83
- await Promise.allSettled([
84
- ...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
85
- ...threadVisibility.toMarkSeen.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
86
- ...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
87
- ...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
88
- ...changesRequestedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
89
- ...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
90
- ...batchData.comments
91
- .filter((c) => partition.suppressedCommentIds.has(c.id))
92
- .map((c) => markSeen(stateKey, c.id, c.body)),
93
- ...batchData.reviewThreads
94
- .filter((t) => partition.suppressedThreadIds.has(t.id))
95
- .map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
96
- ...batchData.reviewSummaries
97
- .filter((r) => partition.suppressedReviewSummaryIds.has(r.id))
98
- .map((r) => markSeen(stateKey, r.id, r.body)),
99
- ...batchData.changesRequestedReviews
100
- .filter((r) => partition.suppressedChangesRequestedIds.has(r.id))
101
- .map((r) => markSeen(stateKey, r.id, r.body)),
102
- ]);
103
- await markReviewInlineThreadMarkers(stateKey, batchData.reviewThreads);
85
+ if (opts.persistSeen !== false) {
86
+ await Promise.allSettled([
87
+ ...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
88
+ ...threadVisibility.toMarkSeen.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
89
+ ...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
90
+ ...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
91
+ ...changesRequestedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
92
+ ...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
93
+ ...batchData.comments
94
+ .filter((c) => partition.suppressedCommentIds.has(c.id))
95
+ .map((c) => markSeen(stateKey, c.id, c.body)),
96
+ ...batchData.reviewThreads
97
+ .filter((t) => partition.suppressedThreadIds.has(t.id))
98
+ .map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
99
+ ...batchData.reviewSummaries
100
+ .filter((r) => partition.suppressedReviewSummaryIds.has(r.id))
101
+ .map((r) => markSeen(stateKey, r.id, r.body)),
102
+ ...batchData.changesRequestedReviews
103
+ .filter((r) => partition.suppressedChangesRequestedIds.has(r.id))
104
+ .map((r) => markSeen(stateKey, r.id, r.body)),
105
+ ]);
106
+ await markReviewInlineThreadMarkers(stateKey, batchData.reviewThreads);
107
+ }
104
108
  const { threadIds: ruleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(partition, opts.autoMinimizeSuppressed);
105
109
  const changesRequestedReviews = changesRequestedReviewVisibility.visible;
106
110
  const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)).length;
@@ -1,9 +1,9 @@
1
1
  import { execFile as execFileCb } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
- import { resolve } from "node:path";
3
+ import { relative, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { getRepoInfo, getCurrentPrNumber, getCurrentBranch } from "../github/client.mjs";
6
- import { fetchPrBatch } from "../github/batch.mjs";
6
+ import { fetchSuggestionThread } from "../github/suggestion-thread.mjs";
7
7
  import { parseSuggestion, isCommittableSuggestion } from "../suggestions/parse.mjs";
8
8
  import { buildUnifiedDiff } from "../suggestions/patch.mjs";
9
9
  import { EXIT, ShepherdError } from "../exit-codes.mjs";
@@ -28,7 +28,7 @@ export async function runCommitSuggestion(opts) {
28
28
  cwd: getExecutionCwd(),
29
29
  });
30
30
  const localHeadSha = localHeadOut.trim();
31
- const { data } = await fetchPrBatch(prNumber, repo);
31
+ const data = await fetchSuggestionThread(prNumber, repo, opts.threadId);
32
32
  if (!data.headRepoWithOwner) {
33
33
  throw new ShepherdError(`PR #${prNumber} head repository is unavailable (fork may have been deleted).`, EXIT.UNAVAILABLE);
34
34
  }
@@ -40,7 +40,7 @@ export async function runCommitSuggestion(opts) {
40
40
  throw new ShepherdError(`Local HEAD ${localHeadSha} does not match PR head ${data.headRefOid}. ` +
41
41
  `Pull/rebase "${data.headRefName}" to the latest PR head and try again.`, EXIT.UNAVAILABLE);
42
42
  }
43
- const thread = data.reviewThreads.find((t) => t.id === opts.threadId);
43
+ const thread = data.thread;
44
44
  if (!thread) {
45
45
  throw new ShepherdError(`Thread ${opts.threadId} not found on PR #${prNumber}.`, EXIT.UNAVAILABLE);
46
46
  }
@@ -73,7 +73,13 @@ export async function runCommitSuggestion(opts) {
73
73
  const startLine = thread.startLine ?? thread.line;
74
74
  const endLine = thread.line;
75
75
  const filePath = thread.path;
76
- const originalContent = await readFile(resolve(getEffectiveCwd(), filePath), "utf8");
76
+ const cwd = getEffectiveCwd();
77
+ const resolvedPath = resolve(cwd, filePath);
78
+ const rel = relative(cwd, resolvedPath);
79
+ if (rel.startsWith("..") || rel === "") {
80
+ throw new ShepherdError(`Thread ${opts.threadId} path escapes the working tree.`, EXIT.UNAVAILABLE);
81
+ }
82
+ const originalContent = await readFile(resolvedPath, "utf8");
77
83
  const patch = buildUnifiedDiff({
78
84
  path: filePath,
79
85
  originalContent,
@@ -166,7 +166,7 @@ export async function handleFixCode(ctx) {
166
166
  cancelled,
167
167
  };
168
168
  const result = await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds);
169
- if (result.action === "fix_code") {
169
+ if (result.action === "fix_code" && opts.persistSeen !== false) {
170
170
  await Promise.allSettled(result.fix.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
171
171
  }
172
172
  return result;
@@ -11,3 +11,4 @@ export declare function buildActiveChecks(report: ShepherdReport): ActiveCheck[]
11
11
  export declare function tryCancelRun(runId: string, owner: string, repo: string): Promise<string | null>;
12
12
  export declare function getCurrentHeadSha(): Promise<string | null>;
13
13
  export declare function buildWaitLog(base: IterateResultBase): string;
14
+ export declare function blockedCancelNote(base: IterateResultBase): string;
@@ -2,6 +2,7 @@ import { execFile as execFileCb } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { rest } from "../../github/http.mjs";
4
4
  import { getExecutionCwd } from "../../execution-context.mjs";
5
+ import { blockedReasonFromRequirements } from "../../merge-status/requirements-format.mjs";
5
6
  const execFile = promisify(execFileCb);
6
7
  export { buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./reruns.mjs";
7
8
  export function buildSummary(report) {
@@ -40,6 +41,7 @@ export function buildTerminalCancelResult(report) {
40
41
  summary: buildSummary(report),
41
42
  baseBranch: report.baseBranch,
42
43
  branchProtection: report.branchProtection,
44
+ mergeRequirements: report.mergeStatus.mergeRequirements,
43
45
  checks: buildRelevantChecks(report),
44
46
  inProgressChecks: buildActiveChecks(report),
45
47
  ...buildSuppressedCheckFields(report),
@@ -134,14 +136,15 @@ export function buildWaitLog(base) {
134
136
  .join(", ")}`);
135
137
  }
136
138
  switch (base.mergeStatus) {
137
- case "BLOCKED":
138
- if (base.reviewDecision === "REVIEW_REQUIRED")
139
- parts.push("awaiting human review");
140
- else if (base.reviewDecision === "APPROVED")
141
- parts.push("awaiting additional approvals");
142
- else
139
+ case "BLOCKED": {
140
+ const fromRules = blockedReasonFromRequirements(base.mergeRequirements);
141
+ if (fromRules)
142
+ parts.push(fromRules);
143
+ else if (!base.blockingBotReviewInProgress) {
143
144
  parts.push("awaiting human review or branch protection");
145
+ }
144
146
  break;
147
+ }
145
148
  case "BEHIND":
146
149
  parts.push("branch is behind base");
147
150
  break;
@@ -157,3 +160,9 @@ export function buildWaitLog(base) {
157
160
  }
158
161
  return parts.join(" — ");
159
162
  }
163
+ export function blockedCancelNote(base) {
164
+ if (base.mergeStatus !== "BLOCKED")
165
+ return "has been ready for review";
166
+ const fromRules = blockedReasonFromRequirements(base.mergeRequirements);
167
+ return fromRules ? `is ${fromRules}` : "is awaiting human review or branch protection resolution";
168
+ }