pr-shepherd 0.20.0 → 0.22.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 (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +11 -9
  3. package/bin/checks/triage.mjs +1 -0
  4. package/bin/checks/triage.test-support.mjs +1 -2
  5. package/bin/cli/default-poll.mjs +32 -0
  6. package/bin/cli/fix-formatter.mjs +36 -8
  7. package/bin/cli/help-command-pages.mjs +3 -4
  8. package/bin/cli/help-top-page.mjs +5 -5
  9. package/bin/cli/iterate-lean.mjs +4 -0
  10. package/bin/cli/iterate-lean.test-support.mjs +0 -2
  11. package/bin/cli/list-formatters.mjs +53 -4
  12. package/bin/cli/validate-default-args.mjs +32 -0
  13. package/bin/cli-parser.clean.test-support.mjs +0 -1
  14. package/bin/cli-parser.commit-suggestion.test-support.mjs +1 -2
  15. package/bin/cli-parser.iterate-fix.test-support.mjs +1 -2
  16. package/bin/cli-parser.iterate.test-support.mjs +1 -2
  17. package/bin/cli-parser.mjs +7 -7
  18. package/bin/cli-parser.test-support.mjs +1 -2
  19. package/bin/commands/check-annotations.mjs +40 -0
  20. package/bin/commands/check.mjs +11 -7
  21. package/bin/commands/check.test-support.mjs +10 -4
  22. package/bin/commands/clean.test-support.mjs +0 -1
  23. package/bin/commands/commit-suggestion.apply.test-support.mjs +1 -2
  24. package/bin/commands/commit-suggestion.test-support.mjs +1 -2
  25. package/bin/commands/iterate/escalate.mjs +16 -2
  26. package/bin/commands/iterate/fix-code.mjs +9 -2
  27. package/bin/commands/iterate/helpers.mjs +1 -0
  28. package/bin/commands/iterate/render.mjs +6 -0
  29. package/bin/commands/iterate/stall.mjs +43 -2
  30. package/bin/commands/iterate-stall.test-support.mjs +0 -1
  31. package/bin/commands/iterate-test-support.mjs +1 -1
  32. package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +8 -3
  33. package/bin/commands/poll.mjs +12 -3
  34. package/bin/commands/resolve.mjs +5 -4
  35. package/bin/commands/resolve.test-support.mjs +2 -2
  36. package/bin/commands/shepherd-journal.test-support.mjs +0 -2
  37. package/bin/comments/resolve.test-support.mjs +1 -2
  38. package/bin/config.json +1 -1
  39. package/bin/github/batch-parsers.mjs +30 -3
  40. package/bin/github/batch-parsers.test-support.mjs +1 -2
  41. package/bin/github/batch.mjs +2 -0
  42. package/bin/github/batch.test-support.mjs +1 -2
  43. package/bin/github/check-annotations.mjs +75 -0
  44. package/bin/github/client.test-support.mjs +55 -0
  45. package/bin/github/gql/batch-pr.gql +15 -2
  46. package/bin/github/gql/check-run-annotations.gql +32 -0
  47. package/bin/github/gql/review-thread-comments.gql +27 -0
  48. package/bin/github/http.test-support.mjs +1 -2
  49. package/bin/github/queries.mjs +4 -0
  50. package/bin/github/thread-comments.mjs +34 -0
  51. package/bin/reporters/agent.mjs +26 -0
  52. package/bin/state/seen-comments.test-support.mjs +19 -0
  53. package/bin/suggestions/patch.test-support.mjs +0 -2
  54. package/bin/threads/transcript.mjs +31 -0
  55. package/bin/types/agent-thread.mjs +1 -0
  56. package/bin/types/check-annotations.mjs +1 -0
  57. package/bin/types/review-thread.mjs +1 -0
  58. package/bin/types.mjs +3 -0
  59. package/package.json +1 -1
  60. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  61. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +9 -9
  62. package/bin/cli/default-iterate.mjs +0 -50
@@ -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.20.0",
4
+ "version": "0.22.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -22,10 +22,10 @@ Example Workflow:
22
22
 
23
23
  The CLI emits unified recheck instructions. Generated commands call `pr-shepherd` directly.
24
24
 
25
- At a high level, the skill invokes `pr-shepherd poll <PR>`, which provides actionable feedback directly to the agent:
25
+ At a high level, the skill invokes `pr-shepherd <PR>` (which polls by default), providing actionable feedback directly to the agent:
26
26
 
27
27
  ```text
28
- > pr-shepherd poll 123
28
+ > pr-shepherd 123
29
29
 
30
30
  # PR #123 [FIX_CODE]
31
31
 
@@ -34,7 +34,9 @@ At a high level, the skill invokes `pr-shepherd poll <PR>`, which provides actio
34
34
 
35
35
  ## Review threads
36
36
 
37
- ### `PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate/index.mts:42` (@alice)
37
+ ### `threadId=PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate/index.mts:42` (@alice)
38
+
39
+ #### `commentId=PRRC_kwDOSGizTs58XB1M` (@alice)
38
40
 
39
41
  > The variable name is misleading.
40
42
  >
@@ -61,7 +63,7 @@ _(schematic — actual steps depend on PR state)_
61
63
  4. Rebase and push: `git fetch origin && git rebase origin/main && git push --force-with-lease` — capture `HEAD_SHA=$(git rev-parse HEAD)`.
62
64
  5. Run the `resolve:` command above, substituting `"$HEAD_SHA"`.
63
65
  6. Add or update a `## Shepherd Journal` section in the PR description for any large decisions made, appending under the existing heading if it already exists.
64
- 7. CI needs time to run on the new push. Run `pr-shepherd poll 123` again to continue until Shepherd returns `[CANCEL]` or `[ESCALATE]`.
66
+ 7. CI needs time to run on the new push. Run `pr-shepherd 123` again to continue until Shepherd returns `[CANCEL]` or `[ESCALATE]`.
65
67
  ```
66
68
 
67
69
  On every iteration, a command is returned to instruct the agent exactly what to do. No guessing, no thinking, as few agentic turns as possible:
@@ -97,7 +99,7 @@ Some other workflow improvements:
97
99
 
98
100
  Recommendations:
99
101
 
100
- - Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling `pr-shepherd poll` until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
102
+ - Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling `pr-shepherd <PR>` until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures or CI that never starts).
101
103
  - Instruct your agents to write comments in a single review (comment, changes requested, or approved). This allows the review's comments/threads to be minimized or resolved together, keeping your pull request history clean. If you write inline comments outside of a review, each comment would still show up in the pull request history and take up space.
102
104
  - Avoid sticky comments as they will continue to be hidden. Instead, just make a new comment, especially on reviews. If you really want sticky comments, instruct your agent to unhide/unminimize them when updating them.
103
105
  - Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
@@ -114,7 +116,7 @@ Recommendations:
114
116
 
115
117
  ### Iterate a PR to completion
116
118
 
117
- Poll dispatcher — checks CI and review comments, fixes issues, and marks the PR ready for review when clean. Each non-terminal tick emits an `## Instructions` section; the skill follows it, then invokes `pr-shepherd poll` again until `[CANCEL]` or `[ESCALATE]`.
119
+ Poll dispatcher — checks CI and review comments, fixes issues, and marks the PR ready for review when clean. Each non-terminal tick emits an `## Instructions` section; the skill follows it, then invokes `pr-shepherd <PR>` again until `[CANCEL]` or `[ESCALATE]`.
118
120
 
119
121
  Claude Code (via `pr-shepherd` skill):
120
122
 
@@ -128,9 +130,9 @@ Codex or direct CLI:
128
130
 
129
131
  ```sh
130
132
  pr-shepherd 42
133
+ pr-shepherd 42 --interval 45s --timeout 4m
131
134
  pr-shepherd 42 --ready-delay 15m
132
- pr-shepherd poll 42
133
- pr-shepherd iterate 42 # legacy-compatible spelling
135
+ pr-shepherd iterate 42 # single tick
134
136
  ```
135
137
 
136
138
  ## Iterate decision loop
@@ -197,7 +199,7 @@ Iterate a PR from Codex:
197
199
  pr-shepherd iterate 42
198
200
  ```
199
201
 
200
- Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows.
202
+ Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures or CI that never starts). `pr-shepherd iterate 42` remains supported for existing workflows.
201
203
 
202
204
  ## Configuration
203
205
 
@@ -53,6 +53,7 @@ function workflowRunToCheckRun(run) {
53
53
  name: run.name?.trim() || `workflow run ${run.id}`,
54
54
  status: "COMPLETED",
55
55
  conclusion: "STARTUP_FAILURE",
56
+ source: "startup_failure",
56
57
  detailsUrl: run.html_url,
57
58
  event: run.event,
58
59
  runId: String(run.id),
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { vi, beforeEach } from "vitest";
3
2
  // ---------------------------------------------------------------------------
4
3
  // Stub fetch globally so http.mts uses our mock.
5
4
  // ---------------------------------------------------------------------------
@@ -0,0 +1,32 @@
1
+ import { parsePrNumber } from "./args.mjs";
2
+ import { validateDefaultArgs } from "./validate-default-args.mjs";
3
+ import { USAGE } from "./help.mjs";
4
+ const DEFAULT_POLL_FLAGS_WITH_VALUES = new Set([
5
+ "--format",
6
+ "--ready-delay",
7
+ "--stall-timeout",
8
+ "--interval",
9
+ "--timeout",
10
+ ]);
11
+ const DEFAULT_POLL_BOOLEAN_FLAGS = new Set([
12
+ "--verbose",
13
+ "--no-auto-mark-ready",
14
+ "--no-auto-cancel-actionable",
15
+ ]);
16
+ export function isDefaultPollInvocation(subcommand) {
17
+ if (subcommand === "--help" || subcommand === "-h")
18
+ return false;
19
+ return (subcommand === undefined || parsePrNumber(subcommand) !== null || isDefaultPollFlag(subcommand));
20
+ }
21
+ export function validateDefaultPollArgs(args) {
22
+ return validateDefaultArgs(args, DEFAULT_POLL_FLAGS_WITH_VALUES, DEFAULT_POLL_BOOLEAN_FLAGS, writeDefaultUsageError);
23
+ }
24
+ function isDefaultPollFlag(arg) {
25
+ const name = arg.split("=", 1)[0];
26
+ return DEFAULT_POLL_FLAGS_WITH_VALUES.has(name) || DEFAULT_POLL_BOOLEAN_FLAGS.has(arg);
27
+ }
28
+ function writeDefaultUsageError(arg) {
29
+ process.stderr.write(`Unknown subcommand: ${arg}\n`);
30
+ process.stderr.write(`${USAGE.top}\n`);
31
+ process.exitCode = 1;
32
+ }
@@ -1,7 +1,7 @@
1
1
  import { renderResolveCommand } from "../commands/iterate/render.mjs";
2
2
  import { joinSections } from "../util/markdown.mjs";
3
3
  import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
4
- import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, } from "./list-formatters.mjs";
4
+ import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, renderThreadConversation, blockquote, } from "./list-formatters.mjs";
5
5
  import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
6
6
  export function formatFixCodeResult(header, result, opts) {
7
7
  const readyDelaySuffix = opts?.readyDelaySuffix;
@@ -14,7 +14,7 @@ export function formatFixCodeResult(header, result, opts) {
14
14
  const heading = t.url ? `[threadId=${t.id}](${t.url})` : `\`threadId=${t.id}\``;
15
15
  const suggestionMarker = t.suggestion ? " [suggestion]" : "";
16
16
  sections.push(`### ${heading} — ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}`);
17
- sections.push(blockquote(t.body));
17
+ sections.push(renderThreadConversation(t));
18
18
  if (t.suggestion) {
19
19
  sections.push(renderSuggestionBlock(t.suggestion, ""));
20
20
  }
@@ -57,6 +57,21 @@ export function formatFixCodeResult(header, result, opts) {
57
57
  });
58
58
  sections.push(bullets.join("\n\n"));
59
59
  }
60
+ const checksWithAnnotations = result.fix.checks.filter((ch) => (ch.annotations?.length ?? 0) > 0);
61
+ if (checksWithAnnotations.length > 0) {
62
+ sections.push("## Check annotations");
63
+ for (const ch of checksWithAnnotations) {
64
+ const workflowPrefix = ch.workflowName ? `${ch.workflowName} › ` : "";
65
+ const jobLabel = ch.jobName ? ch.jobName : ch.name;
66
+ const locator = ch.runId
67
+ ? `\`${ch.runId}\``
68
+ : ch.detailsUrl
69
+ ? `external \`${ch.detailsUrl}\``
70
+ : "(no runId)";
71
+ sections.push(`### ${locator} — \`${workflowPrefix}${jobLabel}\``);
72
+ sections.push(ch.annotations.map(renderCheckAnnotation).join("\n\n"));
73
+ }
74
+ }
60
75
  if (result.fix.changesRequestedReviews.length > 0) {
61
76
  sections.push("## Changes-requested reviews");
62
77
  sections.push(result.fix.changesRequestedReviews.map((r) => renderReviewBullet(r)).join("\n"));
@@ -112,10 +127,23 @@ export function formatFixCodeResult(header, result, opts) {
112
127
  sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix)));
113
128
  return joinSections(sections);
114
129
  }
115
- function blockquote(body) {
116
- return body
117
- .replace(/\r\n/g, "\n")
118
- .split("\n")
119
- .map((line) => (line === "" ? ">" : `> ${line}`))
120
- .join("\n");
130
+ function renderCheckAnnotation(a) {
131
+ const loc = `${a.path}:${renderAnnotationRange(a)}`;
132
+ const link = a.blobUrl ? ` [↗](${a.blobUrl})` : "";
133
+ const title = a.title ? ` — ${a.title}` : "";
134
+ const lines = [`- \`${a.id}\`${link} \`${loc}\` [${a.level}]${title}`];
135
+ if (a.message.trim() !== "")
136
+ lines.push(blockquote(a.message));
137
+ if (a.rawDetails !== undefined && a.rawDetails.trim() !== "")
138
+ lines.push(blockquote(a.rawDetails));
139
+ return lines.join("\n");
140
+ }
141
+ function renderAnnotationRange(a) {
142
+ if (a.startLine === null && a.endLine === null)
143
+ return "?";
144
+ const start = a.startLine ?? a.endLine;
145
+ const end = a.endLine ?? a.startLine;
146
+ if (start === end)
147
+ return String(start);
148
+ return `${start}-${end}`;
121
149
  }
@@ -51,11 +51,10 @@ Exit codes:
51
51
  1 validation, lookup, precondition, or suggestion parsing failure`,
52
52
  iterate: `pr-shepherd iterate
53
53
 
54
- Run one iterate tick for a pull request. This is an alias for 'pr-shepherd [PR]'.
54
+ Run one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.
55
55
  The output contains one action and an action-specific ## Instructions section.
56
56
 
57
57
  Usage:
58
- pr-shepherd [PR] [iterate-flags]
59
58
  pr-shepherd iterate [PR] [iterate-flags]
60
59
 
61
60
  Iterate flags:
@@ -98,11 +97,11 @@ Forwarded iterate flags:
98
97
  --no-auto-mark-ready Do not convert draft PRs to ready for review.
99
98
  --no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
100
99
  --format text|json Output Markdown text or JSON. Default: text.
101
- --verbose Include verbose iterate fields and always print poll progress.
100
+ --verbose Include verbose iterate fields and detailed per-tick lines.
102
101
  --help, -h Print this help and exit before GitHub, git, config, or log I/O.
103
102
 
104
103
  Durations accept seconds, minutes, or hours: 30s, 2m, 1h, or bare seconds.
105
- WAIT progress is written to stderr when stderr is a TTY or --verbose is set.
104
+ Each WAIT tick writes a single dot to stderr; --verbose emits the detailed per-tick line.
106
105
 
107
106
  Exit codes:
108
107
  0 WAIT timeout or MARK_READY
@@ -5,8 +5,8 @@ Autonomous PR CI monitor and review-comment resolver for agentic coding tools.
5
5
  Usage:
6
6
  pr-shepherd --version | -v
7
7
  pr-shepherd --help | -h
8
- pr-shepherd [PR] [flags]
9
- pr-shepherd iterate [PR] [flags]
8
+ pr-shepherd [PR] [poll-flags] [iterate-flags]
9
+ pr-shepherd iterate [PR] [iterate-flags]
10
10
  pr-shepherd poll [PR] [poll-flags] [iterate-flags]
11
11
  pr-shepherd resolve [PR] [resolve-flags]
12
12
  pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [flags]
@@ -14,8 +14,8 @@ Usage:
14
14
  pr-shepherd log-file [--format text|json]
15
15
 
16
16
  Commands:
17
- [PR] Run one iterate tick. This is the default command.
18
- iterate Alias for the default one-tick iterate command.
17
+ [PR] Poll until non-WAIT or timeout. This is the default command.
18
+ iterate Run one iterate tick (single-tick alias).
19
19
  poll Re-run iterate while the action is WAIT, then print the final tick.
20
20
  resolve Fetch actionable review items, or resolve/minimize/dismiss IDs.
21
21
  commit-suggestion Convert one GitHub suggestion thread into a patch and commit instructions.
@@ -28,7 +28,7 @@ PR argument:
28
28
 
29
29
  Common flags:
30
30
  --format text|json Output Markdown text or JSON. Default: text.
31
- --verbose Include verbose iterate fields and poll progress.
31
+ --verbose Include verbose iterate fields and detailed poll-tick lines.
32
32
  --help, -h Print help and exit before any GitHub, git, config, or log I/O.
33
33
 
34
34
  Iterate flags:
@@ -111,6 +111,10 @@ export function projectIterateLean(result, opts) {
111
111
  ...(result.escalate.changesRequestedReviews.length > 0 && {
112
112
  changesRequestedReviews: result.escalate.changesRequestedReviews,
113
113
  }),
114
+ ...(result.escalate.stalledChecks &&
115
+ result.escalate.stalledChecks.length > 0 && {
116
+ stalledChecks: result.escalate.stalledChecks,
117
+ }),
114
118
  ...(result.escalate.thrashHistory &&
115
119
  result.escalate.thrashHistory.length > 0 && {
116
120
  thrashHistory: result.escalate.thrashHistory,
@@ -1,5 +1,3 @@
1
- // @ts-nocheck
2
- import { describe, it, expect } from "vitest";
3
1
  import { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
4
2
  import { makeIterateResult } from "../cli-parser.iterate-fixtures.mjs";
5
3
  export { makeIterateResult, projectIterateLean, projectIterateVerbose };
@@ -1,4 +1,5 @@
1
1
  import { renderLineRange, renderSuggestionBlock } from "./suggestion-renderer.mjs";
2
+ import { threadComments } from "../threads/transcript.mjs";
2
3
  const BODY_PREVIEW_MAX = 100;
3
4
  export function renderAuthor(author, authorType) {
4
5
  return authorType ? `@${author} · ${authorType}` : `@${author}`;
@@ -27,12 +28,60 @@ export function renderThreadBullet(t, opts = {}) {
27
28
  : "`(no location)`";
28
29
  const suggestionMarker = t.suggestion ? " [suggestion]" : "";
29
30
  const statusSuffix = opts.statusTag ? ` ${opts.statusTag}` : "";
30
- const bodySuffix = opts.noBody ? "" : `: ${renderBodyPreview(t.body)}`;
31
- const bulletLine = `- \`threadId=${t.id}\`${link} ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}${statusSuffix}${bodySuffix}`;
31
+ const bulletLine = `- \`threadId=${t.id}\`${link} ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}${statusSuffix}`;
32
+ if (!opts.noBody && (!t.comments || t.comments.length === 0)) {
33
+ const legacyLine = `${bulletLine}: ${renderBodyPreview(t.body)}`;
34
+ return t.suggestion && opts.renderSuggestion
35
+ ? `${legacyLine}\n${renderSuggestionBlock(t.suggestion)}`
36
+ : legacyLine;
37
+ }
38
+ const parts = [bulletLine];
39
+ if (!opts.noBody) {
40
+ parts.push(renderThreadCommentBullets(t));
41
+ }
32
42
  if (t.suggestion && opts.renderSuggestion) {
33
- return `${bulletLine}\n${renderSuggestionBlock(t.suggestion)}`;
43
+ parts.push(renderSuggestionBlock(t.suggestion));
34
44
  }
35
- return bulletLine;
45
+ return parts.join("\n");
46
+ }
47
+ export function renderThreadConversation(t) {
48
+ if (!t.comments || t.comments.length === 0)
49
+ return blockquote(t.body);
50
+ return threadComments(t)
51
+ .map((c) => {
52
+ const heading = c.id
53
+ ? c.url
54
+ ? `#### [commentId=${c.id}](${c.url}) (${renderAuthor(c.author, c.authorType)})`
55
+ : `#### \`commentId=${c.id}\` (${renderAuthor(c.author, c.authorType)})`
56
+ : `#### (${renderAuthor(c.author, c.authorType)})`;
57
+ return `${heading}\n\n${blockquote(c.body)}`;
58
+ })
59
+ .join("\n\n");
60
+ }
61
+ export function blockquote(body) {
62
+ return body
63
+ .replace(/\r\n/g, "\n")
64
+ .split("\n")
65
+ .map((line) => (line === "" ? ">" : `> ${line}`))
66
+ .join("\n");
67
+ }
68
+ function renderThreadCommentBullets(t) {
69
+ return threadComments(t)
70
+ .map((c) => {
71
+ const link = c.url ? ` [↗](${c.url})` : "";
72
+ const id = c.id ? `\`commentId=${c.id}\`` : "comment";
73
+ return [
74
+ ` - ${id}${link} (${renderAuthor(c.author, c.authorType)})`,
75
+ indentBlockquote(c.body, " "),
76
+ ].join("\n");
77
+ })
78
+ .join("\n");
79
+ }
80
+ function indentBlockquote(body, indent) {
81
+ return blockquote(body)
82
+ .split("\n")
83
+ .map((line) => `${indent}${line}`)
84
+ .join("\n");
36
85
  }
37
86
  export function renderCommentBullet(c, opts = {}) {
38
87
  const link = c.url ? ` [↗](${c.url})` : "";
@@ -0,0 +1,32 @@
1
+ import { parsePrNumber } from "./args.mjs";
2
+ /**
3
+ * Validate args for default-dispatch paths (e.g. `pr-shepherd [PR] [flags]`).
4
+ * Returns false when an unexpected token is found; calls onError with the
5
+ * offending arg so callers can print their own usage message.
6
+ */
7
+ export function validateDefaultArgs(args, flagsWithValues, booleanFlags, onError) {
8
+ let sawPr = false;
9
+ for (let i = 0; i < args.length; i += 1) {
10
+ const arg = args[i];
11
+ if (flagsWithValues.has(arg)) {
12
+ if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
13
+ onError(arg);
14
+ return false;
15
+ }
16
+ i += 1;
17
+ continue;
18
+ }
19
+ const inlineFlag = arg.split("=", 1)[0];
20
+ if (flagsWithValues.has(inlineFlag))
21
+ continue;
22
+ if (booleanFlags.has(arg))
23
+ continue;
24
+ if (parsePrNumber(arg) !== null && !sawPr) {
25
+ sawPr = true;
26
+ continue;
27
+ }
28
+ onError(arg);
29
+ return false;
30
+ }
31
+ return true;
32
+ }
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { vi, beforeEach, afterEach } from "vitest";
3
2
  vi.mock("./commands/clean.mts", () => ({
4
3
  runClean: vi.fn(),
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
1
+ import { vi, beforeEach, afterEach } from "vitest";
3
2
  vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
4
3
  vi.mock("./commands/resolve.mts", () => ({
5
4
  runResolveFetch: vi.fn(),
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
1
+ import { vi, beforeEach, afterEach } from "vitest";
3
2
  vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
4
3
  vi.mock("./commands/resolve.mts", () => ({
5
4
  runResolveFetch: vi.fn(),
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
1
+ import { vi, beforeEach, afterEach } from "vitest";
3
2
  vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
4
3
  vi.mock("./commands/resolve.mts", () => ({
5
4
  runResolveFetch: vi.fn(),
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Usage:
5
5
  * pr-shepherd --version
6
- * pr-shepherd [PR] [--format text|json] [--ready-delay Nm]
6
+ * pr-shepherd [PR] [--interval 45s] [--timeout 4m] [--format text|json] [--ready-delay Nm]
7
7
  * [--stall-timeout <duration>] [--no-auto-mark-ready]
8
8
  * [--no-auto-cancel-actionable]
9
9
  * pr-shepherd resolve [PR] [--fetch] [--resolve-thread-ids A,B] [--minimize-comment-ids X,Y]
@@ -22,7 +22,7 @@ import { readFileSync } from "node:fs";
22
22
  import { runResolveFetch, runResolveMutate } from "./commands/resolve.mjs";
23
23
  import { runLogFile } from "./commands/log-file.mjs";
24
24
  import { parseCommonArgs, getFlag, hasFlag, parseList } from "./cli/args.mjs";
25
- import { isDefaultIterateInvocation, validateDefaultIterateArgs } from "./cli/default-iterate.mjs";
25
+ import { isDefaultPollInvocation, validateDefaultPollArgs } from "./cli/default-poll.mjs";
26
26
  import { USAGE, maybePrintHelp } from "./cli/help.mjs";
27
27
  import { formatFetchResult, formatMutateResult } from "./cli/formatters.mjs";
28
28
  import { handleClean, handleCommitSuggestion, handleIterate } from "./cli/handlers.mjs";
@@ -49,8 +49,8 @@ export async function main(argv) {
49
49
  }
50
50
  // Short-circuit all --help/-h before any I/O or logging.
51
51
  if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
52
- if (isDefaultIterateInvocation(subcommand)) {
53
- process.stdout.write(`${USAGE.iterate}\n`);
52
+ if (isDefaultPollInvocation(subcommand)) {
53
+ process.stdout.write(`${USAGE.poll}\n`);
54
54
  }
55
55
  else {
56
56
  const key = subcommand != null && subcommand in USAGE
@@ -62,10 +62,10 @@ export async function main(argv) {
62
62
  }
63
63
  // Initialize the per-worktree log and install a stdout tee.
64
64
  await setupLog(argv);
65
- if (isDefaultIterateInvocation(subcommand)) {
66
- if (!validateDefaultIterateArgs(args))
65
+ if (isDefaultPollInvocation(subcommand)) {
66
+ if (!validateDefaultPollArgs(args))
67
67
  return;
68
- await handleIterate(args);
68
+ await handlePoll(args);
69
69
  return;
70
70
  }
71
71
  switch (subcommand) {
@@ -1,6 +1,5 @@
1
- // @ts-nocheck
2
1
  import { readFileSync } from "node:fs";
3
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { vi, beforeEach, afterEach } from "vitest";
4
3
  vi.mock("./commands/resolve.mts", () => ({
5
4
  runResolveFetch: vi.fn(),
6
5
  runResolveMutate: vi.fn(),
@@ -0,0 +1,40 @@
1
+ import { fetchCheckRunAnnotations } from "../github/check-annotations.mjs";
2
+ export async function attachUnseenCheckAnnotations(checks, seenMap, prNumber) {
3
+ const checksWithAnnotations = [];
4
+ for (const check of checks) {
5
+ // eslint-disable-next-line no-await-in-loop
6
+ checksWithAnnotations.push(await attachForCheck(check, seenMap, prNumber));
7
+ }
8
+ return checksWithAnnotations;
9
+ }
10
+ async function attachForCheck(check, seenMap, prNumber) {
11
+ if (check.id == null)
12
+ return check;
13
+ let annotations;
14
+ try {
15
+ annotations = await fetchCheckRunAnnotations(check.id);
16
+ }
17
+ catch (err) {
18
+ const msg = err instanceof Error ? err.message : String(err);
19
+ process.stderr.write(`pr-shepherd: annotation fetch failed for PR #${prNumber} check "${check.name}" (ignored): ${msg}\n`);
20
+ return check;
21
+ }
22
+ const unseen = annotations.filter((a) => !seenMap.has(a.id));
23
+ if (unseen.length === 0)
24
+ return check;
25
+ return { ...check, annotations: unseen };
26
+ }
27
+ export function annotationMarkerBody(a) {
28
+ return JSON.stringify({
29
+ path: a.path,
30
+ startLine: a.startLine,
31
+ endLine: a.endLine,
32
+ startColumn: a.startColumn,
33
+ endColumn: a.endColumn,
34
+ level: a.level,
35
+ title: a.title,
36
+ message: a.message,
37
+ rawDetails: a.rawDetails,
38
+ blobUrl: a.blobUrl,
39
+ });
40
+ }
@@ -9,9 +9,11 @@ import { deriveMergeStatus } from "../merge-status/derive.mjs";
9
9
  import { loadConfig } from "../config/load.mjs";
10
10
  import { classifyVisibleComments } from "../comments/visible-comments.mjs";
11
11
  import { computeStatus } from "./check-status.mjs";
12
+ import { attachUnseenCheckAnnotations } from "./check-annotations.mjs";
12
13
  import { buildTerminalReport } from "./check-terminal-report.mjs";
13
14
  import { isBlockedByFilteredCheck, refreshReadyMergeability, refreshUnknownMergeability, } from "./ready-mergeability.mjs";
14
15
  import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
16
+ import { threadTranscriptBody } from "../threads/transcript.mjs";
15
17
  export async function runCheck(opts) {
16
18
  const repo = await getRepoInfo();
17
19
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
@@ -38,15 +40,18 @@ export async function runCheck(opts) {
38
40
  const inProgress = classifiedChecks.filter((c) => c.category === "in_progress");
39
41
  const skipped = classifiedChecks.filter((c) => c.category === "skipped");
40
42
  const filtered = classifiedChecks.filter((c) => c.category === "filtered");
41
- const triaged = failing.length > 0 && !opts.skipTriage ? await triageFailingChecks(failing, repo) : failing;
43
+ const triagedBase = failing.length > 0 && !opts.skipTriage ? await triageFailingChecks(failing, repo) : failing;
42
44
  const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
45
+ const seenMap = await loadSeenMap(stateKey);
46
+ const triaged = await attachUnseenCheckAnnotations(triagedBase, seenMap, prNumber);
43
47
  const unresolvedThreads = batchData.reviewThreads.filter((t) => !t.isResolved);
44
48
  const outdated = getOutdatedThreads(unresolvedThreads);
45
49
  let autoResolved = [];
46
50
  let autoResolveErrors = [];
47
51
  if (opts.autoResolve && outdated.length > 0) {
48
52
  const { resolved: resolvedIds, errors } = await autoResolveOutdated(outdated.map((t) => t.id));
49
- autoResolved = outdated.filter((t) => resolvedIds.includes(t.id));
53
+ const resolvedIdsSet = new Set(resolvedIds); // ⚡ Bolt optimization: O(1) lookup
54
+ autoResolved = outdated.filter((t) => resolvedIdsSet.has(t.id));
50
55
  autoResolveErrors = errors;
51
56
  }
52
57
  const activeThreads = unresolvedThreads.filter((t) => !t.isOutdated && !t.isMinimized);
@@ -54,12 +59,11 @@ export async function runCheck(opts) {
54
59
  const resolvedCandidates = batchData.reviewThreads.filter((t) => t.isResolved && !t.isOutdated);
55
60
  const minimizedThreadCandidates = batchData.reviewThreads.filter((t) => t.isMinimized && !t.isResolved && !t.isOutdated);
56
61
  const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized);
57
- const seenMap = await loadSeenMap(stateKey);
58
62
  const visibleCommentClassification = classifyVisibleComments(batchData.comments, seenMap, config.iterate.minimizeComments);
59
63
  const autoResolvedIds = new Set(autoResolved.map((t) => t.id));
60
64
  const firstLookThreads = [
61
65
  ...outdatedCandidates.flatMap((t) => {
62
- const cls = classifyItem(t.id, t.body, seenMap);
66
+ const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
63
67
  if (cls === "unchanged")
64
68
  return [];
65
69
  const base = {
@@ -70,14 +74,14 @@ export async function runCheck(opts) {
70
74
  return cls === "edited" ? [{ ...base, edited: true }] : [base];
71
75
  }),
72
76
  ...resolvedCandidates.flatMap((t) => {
73
- const cls = classifyItem(t.id, t.body, seenMap);
77
+ const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
74
78
  if (cls === "unchanged")
75
79
  return [];
76
80
  const base = { ...t, firstLookStatus: "resolved" };
77
81
  return cls === "edited" ? [{ ...base, edited: true }] : [base];
78
82
  }),
79
83
  ...minimizedThreadCandidates.flatMap((t) => {
80
- const cls = classifyItem(t.id, t.body, seenMap);
84
+ const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
81
85
  if (cls === "unchanged")
82
86
  return [];
83
87
  const base = { ...t, firstLookStatus: "minimized" };
@@ -104,7 +108,7 @@ export async function runCheck(opts) {
104
108
  seenSummaries.push(r);
105
109
  }
106
110
  await Promise.allSettled([
107
- ...firstLookThreads.map((t) => markSeen(stateKey, t.id, t.body)),
111
+ ...firstLookThreads.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
108
112
  ...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
109
113
  ...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
110
114
  ...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { vi, beforeEach } from "vitest";
3
2
  vi.mock("../github/batch.mts", () => ({ fetchPrBatch: vi.fn() }));
4
3
  vi.mock("../github/client.mts", () => ({
5
4
  getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
@@ -10,6 +9,9 @@ vi.mock("../checks/triage.mts", () => ({
10
9
  triageFailingChecks: vi.fn((checks) => Promise.resolve(checks)),
11
10
  fetchStartupFailureChecks: vi.fn().mockResolvedValue([]),
12
11
  }));
12
+ vi.mock("../github/check-annotations.mts", () => ({
13
+ fetchCheckRunAnnotations: vi.fn().mockResolvedValue([]),
14
+ }));
13
15
  vi.mock("../comments/resolve.mts", () => ({
14
16
  autoResolveOutdated: vi.fn().mockResolvedValue({ resolved: [], errors: [] }),
15
17
  }));
@@ -27,6 +29,7 @@ import { runCheck } from "./check.mjs";
27
29
  import { fetchPrBatch } from "../github/batch.mjs";
28
30
  import { getCurrentPrNumber, getMergeableState } from "../github/client.mjs";
29
31
  import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
32
+ import { fetchCheckRunAnnotations } from "../github/check-annotations.mjs";
30
33
  import { loadSeenMap, markSeen, hashBody } from "../state/seen-comments.mjs";
31
34
  import { autoResolveOutdated } from "../comments/resolve.mjs";
32
35
  const mockFetchPrBatch = vi.mocked(fetchPrBatch);
@@ -34,6 +37,7 @@ const mockGetCurrentPrNumber = vi.mocked(getCurrentPrNumber);
34
37
  const mockGetMergeableState = vi.mocked(getMergeableState);
35
38
  const mockTriageFailingChecks = vi.mocked(triageFailingChecks);
36
39
  const mockFetchStartupFailureChecks = vi.mocked(fetchStartupFailureChecks);
40
+ const mockFetchCheckRunAnnotations = vi.mocked(fetchCheckRunAnnotations);
37
41
  const mockLoadSeenMap = vi.mocked(loadSeenMap);
38
42
  const mockMarkSeen = vi.mocked(markSeen);
39
43
  const mockAutoResolveOutdated = vi.mocked(autoResolveOutdated);
@@ -42,7 +46,7 @@ function defaultConfig() {
42
46
  return {
43
47
  iterate: {
44
48
  fixAttemptsPerThread: 3,
45
- stallTimeoutMinutes: 30,
49
+ stallTimeoutMinutes: 60,
46
50
  minimizeApprovals: false,
47
51
  minimizeComments: "all",
48
52
  },
@@ -64,6 +68,7 @@ function defaultConfig() {
64
68
  }
65
69
  function makeCheck(overrides = {}) {
66
70
  return {
71
+ id: null,
67
72
  name: "tests",
68
73
  status: "COMPLETED",
69
74
  conclusion: "SUCCESS",
@@ -136,6 +141,7 @@ export function registerHooks() {
136
141
  mockFetchPrBatch.mockResolvedValue({ data: makeBatchData() });
137
142
  mockGetMergeableState.mockResolvedValue({ mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" });
138
143
  mockFetchStartupFailureChecks.mockResolvedValue([]);
144
+ mockFetchCheckRunAnnotations.mockResolvedValue([]);
139
145
  });
140
146
  }
141
- export { BASE_OPTS, autoResolveOutdated, defaultConfig, fetchPrBatch, fetchStartupFailureChecks, getCurrentPrNumber, getMergeableState, hashBody, loadSeenMap, makeBatchData, makeCheck, makeComment, makeThread, markSeen, mockAutoResolveOutdated, mockFetchPrBatch, mockFetchStartupFailureChecks, mockGetCurrentPrNumber, mockGetMergeableState, mockLoadConfig, mockLoadSeenMap, mockMarkSeen, mockTriageFailingChecks, runCheck, triageFailingChecks, };
147
+ export { BASE_OPTS, autoResolveOutdated, defaultConfig, fetchPrBatch, fetchStartupFailureChecks, fetchCheckRunAnnotations, getCurrentPrNumber, getMergeableState, hashBody, loadSeenMap, makeBatchData, makeCheck, makeComment, makeThread, markSeen, mockAutoResolveOutdated, mockFetchPrBatch, mockFetchStartupFailureChecks, mockFetchCheckRunAnnotations, mockGetCurrentPrNumber, mockGetMergeableState, mockLoadConfig, mockLoadSeenMap, mockMarkSeen, mockTriageFailingChecks, runCheck, triageFailingChecks, };
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { vi, beforeEach, afterEach } from "vitest";
3
2
  import { join } from "node:path";
4
3
  import { mkdtemp, realpath, rm, mkdir, writeFile, stat } from "node:fs/promises";