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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +11 -9
- package/bin/checks/triage.mjs +1 -0
- package/bin/checks/triage.test-support.mjs +1 -2
- package/bin/cli/default-poll.mjs +32 -0
- package/bin/cli/fix-formatter.mjs +36 -8
- package/bin/cli/help-command-pages.mjs +3 -4
- package/bin/cli/help-top-page.mjs +5 -5
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/iterate-lean.test-support.mjs +0 -2
- package/bin/cli/list-formatters.mjs +53 -4
- package/bin/cli/validate-default-args.mjs +32 -0
- package/bin/cli-parser.clean.test-support.mjs +0 -1
- package/bin/cli-parser.commit-suggestion.test-support.mjs +1 -2
- package/bin/cli-parser.iterate-fix.test-support.mjs +1 -2
- package/bin/cli-parser.iterate.test-support.mjs +1 -2
- package/bin/cli-parser.mjs +7 -7
- package/bin/cli-parser.test-support.mjs +1 -2
- package/bin/commands/check-annotations.mjs +40 -0
- package/bin/commands/check.mjs +11 -7
- package/bin/commands/check.test-support.mjs +10 -4
- package/bin/commands/clean.test-support.mjs +0 -1
- package/bin/commands/commit-suggestion.apply.test-support.mjs +1 -2
- package/bin/commands/commit-suggestion.test-support.mjs +1 -2
- package/bin/commands/iterate/escalate.mjs +16 -2
- package/bin/commands/iterate/fix-code.mjs +9 -2
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/render.mjs +6 -0
- package/bin/commands/iterate/stall.mjs +43 -2
- package/bin/commands/iterate-stall.test-support.mjs +0 -1
- package/bin/commands/iterate-test-support.mjs +1 -1
- package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +8 -3
- package/bin/commands/poll.mjs +12 -3
- package/bin/commands/resolve.mjs +5 -4
- package/bin/commands/resolve.test-support.mjs +2 -2
- package/bin/commands/shepherd-journal.test-support.mjs +0 -2
- package/bin/comments/resolve.test-support.mjs +1 -2
- package/bin/config.json +1 -1
- package/bin/github/batch-parsers.mjs +30 -3
- package/bin/github/batch-parsers.test-support.mjs +1 -2
- package/bin/github/batch.mjs +2 -0
- package/bin/github/batch.test-support.mjs +1 -2
- package/bin/github/check-annotations.mjs +75 -0
- package/bin/github/client.test-support.mjs +55 -0
- package/bin/github/gql/batch-pr.gql +15 -2
- package/bin/github/gql/check-run-annotations.gql +32 -0
- package/bin/github/gql/review-thread-comments.gql +27 -0
- package/bin/github/http.test-support.mjs +1 -2
- package/bin/github/queries.mjs +4 -0
- package/bin/github/thread-comments.mjs +34 -0
- package/bin/reporters/agent.mjs +26 -0
- package/bin/state/seen-comments.test-support.mjs +19 -0
- package/bin/suggestions/patch.test-support.mjs +0 -2
- package/bin/threads/transcript.mjs +31 -0
- package/bin/types/agent-thread.mjs +1 -0
- package/bin/types/check-annotations.mjs +1 -0
- package/bin/types/review-thread.mjs +1 -0
- package/bin/types.mjs +3 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +9 -9
- 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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/bin/github/queries.mjs
CHANGED
|
@@ -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
|
+
}
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -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
|
|
23
|
+
2. **Run `pr-shepherd`:**
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
pr-shepherd
|
|
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
|
|
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.
|
|
31
|
+
Print the full output. Follow the `## Instructions` section exactly for the current action.
|
|
32
32
|
|
|
33
|
-
3. **Persistence:** Continuously call `pr-shepherd
|
|
34
|
-
- `[WAIT]`: call `pr-shepherd
|
|
35
|
-
- `[MARK_READY]`: call `pr-shepherd
|
|
36
|
-
- `[FIX_CODE]`: follow the output's `## Instructions`, then call `pr-shepherd
|
|
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
|
-
}
|