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
@@ -0,0 +1,32 @@
1
+ query CheckRunAnnotations($id: ID!, $cursor: String) {
2
+ node(id: $id) {
3
+ __typename
4
+ ... on CheckRun {
5
+ annotations(first: 100, after: $cursor) {
6
+ pageInfo {
7
+ hasNextPage
8
+ endCursor
9
+ }
10
+ nodes {
11
+ fullDatabaseId
12
+ path
13
+ annotationLevel
14
+ title
15
+ message
16
+ rawDetails
17
+ blobUrl
18
+ location {
19
+ start {
20
+ line
21
+ column
22
+ }
23
+ end {
24
+ line
25
+ column
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,27 @@
1
+ query ReviewThreadComments($threadId: ID!, $commentsCursor: String) {
2
+ node(id: $threadId) {
3
+ __typename
4
+ ... on PullRequestReviewThread {
5
+ comments(first: 100, after: $commentsCursor) {
6
+ pageInfo {
7
+ hasNextPage
8
+ endCursor
9
+ }
10
+ nodes {
11
+ id
12
+ isMinimized
13
+ url
14
+ author {
15
+ __typename
16
+ login
17
+ }
18
+ body
19
+ path
20
+ line
21
+ startLine
22
+ createdAt
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
@@ -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 and child_process globally before any imports.
5
4
  // ---------------------------------------------------------------------------
@@ -11,6 +11,10 @@ import { join } from "node:path";
11
11
  const gql = (name) => readFileSync(join(import.meta.dirname, "gql", name), "utf8");
12
12
  /** The primary batch query that fetches CI + comments + merge status in one round-trip. */
13
13
  export const BATCH_PR_QUERY = gql("batch-pr.gql");
14
+ /** Fetches additional comments for a single review thread when its nested connection paginates. */
15
+ export const REVIEW_THREAD_COMMENTS_QUERY = gql("review-thread-comments.gql");
16
+ /** Fetch inline annotations for a single CheckRun by node ID. */
17
+ export const CHECK_RUN_ANNOTATIONS_QUERY = gql("check-run-annotations.gql");
14
18
  /** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
15
19
  export const GET_PR_HEAD_SHA_QUERY = gql("get-pr-head-sha.gql");
16
20
  /** Look up PR number by branch name (for getCurrentPrNumber). */
@@ -0,0 +1,34 @@
1
+ import { graphql } from "./client.mjs";
2
+ import { paginateForward } from "./pagination.mjs";
3
+ import { REVIEW_THREAD_COMMENTS_QUERY } from "./queries.mjs";
4
+ export async function hydrateThreadCommentPages(threads) {
5
+ const hydrated = [];
6
+ for (const thread of threads) {
7
+ hydrated.push(await hydrateThreadCommentPage(thread));
8
+ }
9
+ return hydrated;
10
+ }
11
+ async function hydrateThreadCommentPage(thread) {
12
+ const pageInfo = thread.comments.pageInfo;
13
+ if (!pageInfo?.hasNextPage || !pageInfo.endCursor)
14
+ return thread;
15
+ const extra = await paginateForward(async (cursor) => {
16
+ const res = await graphql(REVIEW_THREAD_COMMENTS_QUERY, {
17
+ threadId: thread.id,
18
+ ...(cursor ? { commentsCursor: cursor } : {}),
19
+ });
20
+ const node = res.data.node;
21
+ if (!node?.comments) {
22
+ const nodeType = node?.__typename ?? "null";
23
+ throw new Error(`Review thread ${thread.id} did not resolve to PullRequestReviewThread while paginating comments (node type: ${nodeType})`);
24
+ }
25
+ return node.comments;
26
+ }, pageInfo.endCursor);
27
+ return {
28
+ ...thread,
29
+ comments: {
30
+ pageInfo: { hasNextPage: false, endCursor: null },
31
+ nodes: [...thread.comments.nodes, ...extra],
32
+ },
33
+ };
34
+ }
@@ -22,6 +22,15 @@ export function toAgentThread(t) {
22
22
  ...(t.authorType !== undefined && { authorType: t.authorType }),
23
23
  body: t.body,
24
24
  url: t.url,
25
+ ...(t.comments !== undefined && {
26
+ comments: t.comments.map((c) => ({
27
+ id: c.id,
28
+ author: c.author,
29
+ ...(c.authorType !== undefined && { authorType: c.authorType }),
30
+ body: c.body,
31
+ url: c.url,
32
+ })),
33
+ }),
25
34
  ...(suggestion !== undefined && { suggestion }),
26
35
  };
27
36
  }
@@ -48,6 +57,23 @@ export function toAgentCheck(c) {
48
57
  ...(c.jobName !== undefined && { jobName: c.jobName }),
49
58
  ...(c.failedStep !== undefined && { failedStep: c.failedStep }),
50
59
  ...(c.summary !== undefined && { summary: c.summary }),
60
+ ...(c.annotations !== undefined && { annotations: c.annotations }),
61
+ };
62
+ }
63
+ export function toAgentStalledCheck(c, nowSeconds) {
64
+ const createdAtUnix = c.createdAtUnix ?? nowSeconds;
65
+ const activityAtUnix = c.updatedAtUnix ?? createdAtUnix;
66
+ return {
67
+ name: c.name,
68
+ status: c.status,
69
+ source: c.source ?? "check_run",
70
+ runId: c.runId,
71
+ detailsUrl: c.detailsUrl || null,
72
+ ...(c.createdAtUnix !== undefined && { createdAtUnix: c.createdAtUnix }),
73
+ ...(c.startedAtUnix !== undefined && { startedAtUnix: c.startedAtUnix }),
74
+ ...(c.updatedAtUnix !== undefined && { updatedAtUnix: c.updatedAtUnix }),
75
+ ageSeconds: Math.max(0, nowSeconds - activityAtUnix),
76
+ ...(c.summary !== undefined && { summary: c.summary }),
51
77
  };
52
78
  }
53
79
  /**
@@ -0,0 +1,19 @@
1
+ import { beforeEach, afterEach } from "vitest";
2
+ import { randomBytes, createHash } from "node:crypto";
3
+ import { rm } from "node:fs/promises";
4
+ export function idToFilename(id) {
5
+ return createHash("sha256").update(id, "utf8").digest("hex") + ".json";
6
+ }
7
+ export const testKey = { owner: "test-owner", repo: "test-repo", pr: 123 };
8
+ export const testId = "PRRT_kwDOTest123";
9
+ export let testStateDir;
10
+ export function registerHooks() {
11
+ beforeEach(() => {
12
+ testStateDir = `${process.env["TMPDIR"] ?? "/tmp"}/shepherd-seen-test-${randomBytes(4).toString("hex")}`;
13
+ process.env["PR_SHEPHERD_STATE_DIR"] = testStateDir;
14
+ });
15
+ afterEach(async () => {
16
+ delete process.env["PR_SHEPHERD_STATE_DIR"];
17
+ await rm(testStateDir, { recursive: true, force: true });
18
+ });
19
+ }
@@ -1,4 +1,2 @@
1
- // @ts-nocheck
2
- import { describe, it, expect } from "vitest";
3
1
  import { buildUnifiedDiff } from "./patch.mjs";
4
2
  export { buildUnifiedDiff };
@@ -0,0 +1,31 @@
1
+ export function threadComments(thread) {
2
+ if (thread.comments && thread.comments.length > 0) {
3
+ return thread.comments.map((c) => ({
4
+ id: c.id,
5
+ isMinimized: c.isMinimized ?? false,
6
+ author: c.author,
7
+ authorType: c.authorType ?? "Unknown",
8
+ body: c.body,
9
+ url: c.url,
10
+ createdAtUnix: c.createdAtUnix ?? 0,
11
+ }));
12
+ }
13
+ return [
14
+ {
15
+ id: "",
16
+ isMinimized: false,
17
+ author: thread.author,
18
+ authorType: thread.authorType ?? "Unknown",
19
+ body: thread.body,
20
+ url: thread.url ?? "",
21
+ createdAtUnix: thread.createdAtUnix ?? 0,
22
+ },
23
+ ];
24
+ }
25
+ export function threadTranscriptBody(thread) {
26
+ if (!thread.comments || thread.comments.length === 0)
27
+ return thread.body;
28
+ return threadComments(thread)
29
+ .map((c) => `${c.id}\n${c.body}`)
30
+ .join("\n\n--- thread comment ---\n\n");
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/bin/types.mjs CHANGED
@@ -1,4 +1,7 @@
1
1
  /** Shared type definitions for the shepherd CLI. */
2
2
  export * from "./types/github.mjs";
3
+ export * from "./types/review-thread.mjs";
4
+ export * from "./types/agent-thread.mjs";
5
+ export * from "./types/check-annotations.mjs";
3
6
  export * from "./types/report.mjs";
4
7
  export * from "./types/iterate.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Ong",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -20,24 +20,24 @@ Poll dispatcher for iterating a PR to completion.
20
20
  - Otherwise, infer: `gh pr view --json number --jq .number`
21
21
  - If no PR found, report an error and stop.
22
22
 
23
- 2. **Run `pr-shepherd poll`:**
23
+ 2. **Run `pr-shepherd`:**
24
24
 
25
25
  ```bash
26
- pr-shepherd poll <N>
26
+ pr-shepherd <N> --interval 45s --timeout 4m
27
27
  ```
28
28
 
29
- Do not pass `$ARGUMENTS` through as extra flags. If you need to inspect supported options, run `pr-shepherd poll --help`.
29
+ Do not pass `$ARGUMENTS` through as extra flags. If you need to inspect supported options, run `pr-shepherd --help`.
30
30
 
31
- Print the full output. Follow the `## Instructions` section exactly for the current action. When those instructions tell you to stop and recheck with `pr-shepherd <N>` after a delay, use `pr-shepherd poll <N>` as the next invocation instead; do not also run the one-shot command.
31
+ Print the full output. Follow the `## Instructions` section exactly for the current action.
32
32
 
33
- 3. **Persistence:** Continuously call `pr-shepherd poll <N>` until the CLI returns `[CANCEL]` or `[ESCALATE]`, unless the human directs you to stop. Every other action is non-terminal:
34
- - `[WAIT]`: call `pr-shepherd poll <N>` again.
35
- - `[MARK_READY]`: call `pr-shepherd poll <N>` again.
36
- - `[FIX_CODE]`: follow the output's `## Instructions`, then call `pr-shepherd poll <N>` again.
33
+ 3. **Persistence:** Continuously call `pr-shepherd <N> --interval 45s --timeout 4m` until the CLI returns `[CANCEL]` or `[ESCALATE]`, unless the human directs you to stop. Every other action is non-terminal:
34
+ - `[WAIT]`: call `pr-shepherd <N> --interval 45s --timeout 4m` again.
35
+ - `[MARK_READY]`: call `pr-shepherd <N> --interval 45s --timeout 4m` again.
36
+ - `[FIX_CODE]`: follow the output's `## Instructions`, then call `pr-shepherd <N> --interval 45s --timeout 4m` again.
37
37
 
38
38
  Treat a nonzero poll exit code as PR state only when the output contains a matching `# PR #N [ACTION]` heading. Exit code `1` can also mean a command or validation failure; if there is no `[FIX_CODE]` heading, surface the error and stop instead of looping.
39
39
 
40
40
  4. **Stop conditions (terminal states):**
41
41
  - Stop when the CLI emits `[CANCEL]` (ready-delay completed, or PR merged/closed).
42
- - Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures.
42
+ - Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures or CI that never starts.
43
43
  - **Do NOT merge the pull request** unless the human has explicitly requested or allowed it.
@@ -1,50 +0,0 @@
1
- import { parsePrNumber } from "./args.mjs";
2
- import { USAGE } from "./help.mjs";
3
- const DEFAULT_ITERATE_FLAGS_WITH_VALUES = new Set(["--format", "--ready-delay", "--stall-timeout"]);
4
- const DEFAULT_ITERATE_BOOLEAN_FLAGS = new Set([
5
- "--verbose",
6
- "--no-auto-mark-ready",
7
- "--no-auto-cancel-actionable",
8
- ]);
9
- export function isDefaultIterateInvocation(subcommand) {
10
- if (subcommand === "--help" || subcommand === "-h")
11
- return false;
12
- return (subcommand === undefined ||
13
- parsePrNumber(subcommand) !== null ||
14
- isDefaultIterateFlag(subcommand));
15
- }
16
- export function validateDefaultIterateArgs(args) {
17
- let sawPr = false;
18
- for (let i = 0; i < args.length; i += 1) {
19
- const arg = args[i];
20
- if (DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(arg)) {
21
- if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
22
- writeDefaultUsageError(arg);
23
- return false;
24
- }
25
- i += 1;
26
- continue;
27
- }
28
- const inlineFlag = arg.split("=", 1)[0];
29
- if (DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(inlineFlag))
30
- continue;
31
- if (DEFAULT_ITERATE_BOOLEAN_FLAGS.has(arg))
32
- continue;
33
- if (parsePrNumber(arg) !== null && !sawPr) {
34
- sawPr = true;
35
- continue;
36
- }
37
- writeDefaultUsageError(arg);
38
- return false;
39
- }
40
- return true;
41
- }
42
- function isDefaultIterateFlag(arg) {
43
- const name = arg.split("=", 1)[0];
44
- return DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(name) || DEFAULT_ITERATE_BOOLEAN_FLAGS.has(arg);
45
- }
46
- function writeDefaultUsageError(arg) {
47
- process.stderr.write(`Unknown subcommand: ${arg}\n`);
48
- process.stderr.write(`${USAGE.top}\n`);
49
- process.exitCode = 1;
50
- }