pr-shepherd 0.15.2 → 0.16.1
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +23 -93
- package/bin/cli/args.mjs +0 -1
- package/bin/cli/default-iterate.mjs +2 -7
- package/bin/cli/exit-codes.mjs +2 -4
- package/bin/cli/fix-formatter.mjs +8 -8
- package/bin/cli/handlers.mjs +3 -72
- package/bin/cli/iterate-formatter.mjs +8 -17
- package/bin/cli/iterate-instructions.mjs +13 -47
- package/bin/cli/iterate-lean.mjs +4 -11
- package/bin/cli/list-formatters.mjs +6 -3
- package/bin/cli-parser.iterate-fixtures.mjs +4 -7
- package/bin/cli-parser.mjs +4 -33
- package/bin/commands/check-status.mjs +2 -2
- package/bin/commands/check-terminal-report.mjs +35 -0
- package/bin/commands/check.mjs +12 -7
- package/bin/commands/commit-suggestion-instruction.mjs +23 -0
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/classify.mjs +9 -4
- package/bin/commands/iterate/escalate.mjs +5 -6
- package/bin/commands/iterate/fix-code.mjs +3 -3
- package/bin/commands/iterate/helpers.mjs +0 -29
- package/bin/commands/iterate/index.mjs +18 -19
- package/bin/commands/iterate/render.mjs +5 -17
- package/bin/commands/resolve-instructions.mjs +2 -10
- package/bin/commands/resolve-mutate.mjs +16 -0
- package/bin/commands/resolve.mjs +6 -17
- package/bin/commands/shepherd-journal.mjs +1 -1
- package/bin/comments/minimize-policy.mjs +15 -0
- package/bin/comments/visible-comments.mjs +20 -0
- package/bin/config/load.mjs +10 -3
- package/bin/config.json +2 -3
- package/bin/github/batch-parsers.mjs +10 -0
- package/bin/github/gql/batch-pr.gql +6 -0
- package/bin/github/queries.mjs +0 -2
- package/bin/index.mjs +0 -2
- package/bin/merge-status/derive.mjs +6 -6
- package/bin/reporters/agent.mjs +10 -3
- package/bin/state/iterate-stall.mjs +9 -0
- package/package.json +5 -6
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +31 -26
- package/bin/commands/iterate.mjs +0 -2
- package/bin/commands/monitor.mjs +0 -139
- package/bin/commands/status.mjs +0 -126
- package/bin/github/gql/multi-pr-status-paged.gql +0 -31
- package/bin/reporters/check-instructions.mjs +0 -69
- package/bin/reporters/json.mjs +0 -10
- package/bin/reporters/text.mjs +0 -156
- package/plugin/skills/check/SKILL.md +0 -37
- package/plugin/skills/monitor/SKILL.md +0 -40
- package/plugin/skills/resolve/SKILL.md +0 -38
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
2
2
|
import { SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS, buildShepherdJournalInstruction, } from "./shepherd-journal.mjs";
|
|
3
|
+
import { buildCommitSuggestionInstruction } from "./commit-suggestion-instruction.mjs";
|
|
3
4
|
/**
|
|
4
5
|
* Build the numbered triage/fix/resolve instruction steps for the agent to follow.
|
|
5
6
|
* Steps are conditionally emitted based on what the fetch returned (mirrors
|
|
@@ -32,16 +33,7 @@ export function buildFetchInstructions(prNumber, result, runner) {
|
|
|
32
33
|
instructions.push(`First-look bullets tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
|
|
33
34
|
}
|
|
34
35
|
if (hasSuggestions) {
|
|
35
|
-
|
|
36
|
-
"commit-suggestion",
|
|
37
|
-
String(prNumber),
|
|
38
|
-
"--thread-id",
|
|
39
|
-
"<id>",
|
|
40
|
-
"--message",
|
|
41
|
-
"<one-sentence headline>",
|
|
42
|
-
"--format=json",
|
|
43
|
-
], { runner }).text;
|
|
44
|
-
instructions.push(`For each Actionable thread marked \`[suggestion]\` in \`## Actionable Review Threads\` above: run \`${commitSuggestionCommand}\` to retrieve the patch and suggested commit. The CLI does not mutate the working tree — apply the patch yourself (run \`git apply\` with the diff shown, or edit the file directly using the line range), then stage the listed file and run the suggested \`git commit\` from the \`## Instructions\` section. Include the thread ID in \`--resolve-thread-ids\` in the resolve command below (the thread is not auto-resolved). If the patch fails to apply (drift since the suggestion was written), fall through to the manual fix step. Do not retry the same \`commit-suggestion\` invocation.`);
|
|
36
|
+
instructions.push(buildCommitSuggestionInstruction(prNumber, "## Actionable Review Threads", true, runner));
|
|
45
37
|
}
|
|
46
38
|
if (hasCodeItems) {
|
|
47
39
|
instructions.push(`Read and edit each file referenced under \`## Actionable Review Threads\`, \`## Actionable PR Comments\`, and \`## Pending CHANGES_REQUESTED reviews\` above. Reclassify each fixed item as Fixed. If an item is too complex to address, leave it as Actionable for the final report.`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
2
|
+
import { applyResolveOptions } from "../comments/resolve.mjs";
|
|
3
|
+
export async function runResolveMutate(opts) {
|
|
4
|
+
const repo = await getRepoInfo();
|
|
5
|
+
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
6
|
+
if (prNumber === null) {
|
|
7
|
+
throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
|
|
8
|
+
}
|
|
9
|
+
return applyResolveOptions(prNumber, repo, {
|
|
10
|
+
resolveThreadIds: opts.resolveThreadIds,
|
|
11
|
+
minimizeCommentIds: opts.minimizeCommentIds,
|
|
12
|
+
dismissReviewIds: opts.dismissReviewIds,
|
|
13
|
+
dismissMessage: opts.dismissMessage,
|
|
14
|
+
requireSha: opts.requireSha,
|
|
15
|
+
});
|
|
16
|
+
}
|
package/bin/commands/resolve.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
2
2
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
3
3
|
import { getOutdatedThreads } from "../comments/outdated.mjs";
|
|
4
|
-
import { autoResolveOutdated
|
|
4
|
+
import { autoResolveOutdated } from "../comments/resolve.mjs";
|
|
5
5
|
import { loadConfig } from "../config/load.mjs";
|
|
6
|
+
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
6
7
|
import { extractSuggestion } from "../suggestions/extract.mjs";
|
|
7
8
|
import { buildFetchInstructions } from "./resolve-instructions.mjs";
|
|
8
9
|
import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
|
|
10
|
+
export { runResolveMutate } from "./resolve-mutate.mjs";
|
|
9
11
|
export async function runResolveFetch(opts) {
|
|
10
12
|
const repo = await getRepoInfo();
|
|
11
13
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
@@ -15,7 +17,6 @@ export async function runResolveFetch(opts) {
|
|
|
15
17
|
const { data } = await fetchPrBatch(prNumber, repo);
|
|
16
18
|
const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
|
|
17
19
|
const unresolvedThreads = data.reviewThreads.filter((t) => !t.isResolved);
|
|
18
|
-
const visibleComments = data.comments.filter((c) => !c.isMinimized);
|
|
19
20
|
const outdatedCandidates = data.reviewThreads.filter((t) => t.isOutdated);
|
|
20
21
|
const resolvedCandidates = data.reviewThreads.filter((t) => t.isResolved && !t.isOutdated);
|
|
21
22
|
const minimizedThreadCandidates = data.reviewThreads.filter((t) => t.isMinimized && !t.isResolved && !t.isOutdated);
|
|
@@ -70,6 +71,7 @@ export async function runResolveFetch(opts) {
|
|
|
70
71
|
const activeThreads = unresolvedThreads.filter((t) => !t.isOutdated && !t.isMinimized);
|
|
71
72
|
const resolutionOnlyThreads = unresolvedThreads.filter((t) => !autoResolvedIds.has(t.id) && (t.isOutdated || t.isMinimized));
|
|
72
73
|
const cfg = loadConfig();
|
|
74
|
+
const visibleCommentClassification = classifyVisibleComments(data.comments, seenMap, cfg.iterate?.minimizeComments);
|
|
73
75
|
const actionableThreads = activeThreads.map(({ isResolved: _r, isOutdated: _o, ...rest }) => {
|
|
74
76
|
const thread = rest;
|
|
75
77
|
const suggestion = extractSuggestion(rest);
|
|
@@ -114,13 +116,14 @@ export async function runResolveFetch(opts) {
|
|
|
114
116
|
await Promise.allSettled([
|
|
115
117
|
...firstLookThreads.map((t) => markSeen(stateKey, t.id, t.body)),
|
|
116
118
|
...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
119
|
+
...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
117
120
|
]);
|
|
118
121
|
const result = {
|
|
119
122
|
prNumber,
|
|
120
123
|
actionableThreads,
|
|
121
124
|
resolutionOnlyThreads,
|
|
122
125
|
firstLookThreads,
|
|
123
|
-
actionableComments:
|
|
126
|
+
actionableComments: visibleCommentClassification.actionable,
|
|
124
127
|
firstLookComments,
|
|
125
128
|
changesRequestedReviews: data.changesRequestedReviews,
|
|
126
129
|
reviewSummaries: cfg.resolve.fetchReviewSummaries ? data.reviewSummaries : [],
|
|
@@ -128,17 +131,3 @@ export async function runResolveFetch(opts) {
|
|
|
128
131
|
};
|
|
129
132
|
return { ...result, instructions: buildFetchInstructions(prNumber, result, cfg.cli?.runner) };
|
|
130
133
|
}
|
|
131
|
-
export async function runResolveMutate(opts) {
|
|
132
|
-
const repo = await getRepoInfo();
|
|
133
|
-
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
134
|
-
if (prNumber === null) {
|
|
135
|
-
throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
|
|
136
|
-
}
|
|
137
|
-
return applyResolveOptions(prNumber, repo, {
|
|
138
|
-
resolveThreadIds: opts.resolveThreadIds,
|
|
139
|
-
minimizeCommentIds: opts.minimizeCommentIds,
|
|
140
|
-
dismissReviewIds: opts.dismissReviewIds,
|
|
141
|
-
dismissMessage: opts.dismissMessage,
|
|
142
|
-
requireSha: opts.requireSha,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const SHEPHERD_JOURNAL_SECTION = "## Shepherd Journal";
|
|
2
2
|
export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
|
|
3
3
|
export const SHEPHERD_JOURNAL_APPEND_HINT = "If this section already exists, append your entries under it instead of creating a duplicate heading.";
|
|
4
|
-
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look
|
|
4
|
+
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time. Any IDs eligible for minimization are already included in the resolve command's `--minimize-comment-ids`; if any warrants a Shepherd Journal note, append it before running resolve.";
|
|
5
5
|
export function buildShepherdJournalInstruction(prNumber, itemReferenceGuidance) {
|
|
6
6
|
return [
|
|
7
7
|
`For any large decisions or rejections you made this iteration, add or update a \`${SHEPHERD_JOURNAL_SECTION}\` section in the PR description (\`gh pr edit ${prNumber} --body …\`) summarizing each decision.`,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function shouldMinimizeAuthor(authorType, policy) {
|
|
2
|
+
switch (policy) {
|
|
3
|
+
case undefined:
|
|
4
|
+
case "all":
|
|
5
|
+
return true;
|
|
6
|
+
case "bots":
|
|
7
|
+
return authorType === "Bot";
|
|
8
|
+
case "users":
|
|
9
|
+
return authorType === "User";
|
|
10
|
+
case "none":
|
|
11
|
+
return false;
|
|
12
|
+
default:
|
|
13
|
+
throw new Error(`Invalid minimizeComments policy: ${String(policy)}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { shouldMinimizeAuthor } from "./minimize-policy.mjs";
|
|
2
|
+
import { classifyItem } from "../state/seen-comments.mjs";
|
|
3
|
+
export function classifyVisibleComments(comments, seenMap, minimizeComments) {
|
|
4
|
+
const actionable = [];
|
|
5
|
+
const minimizeIds = [];
|
|
6
|
+
const toMarkSeen = [];
|
|
7
|
+
for (const c of comments.filter((comment) => !comment.isMinimized)) {
|
|
8
|
+
if (shouldMinimizeAuthor(c.authorType, minimizeComments)) {
|
|
9
|
+
actionable.push(c);
|
|
10
|
+
minimizeIds.push(c.id);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const cls = classifyItem(c.id, c.body, seenMap);
|
|
14
|
+
if (cls === "unchanged")
|
|
15
|
+
continue;
|
|
16
|
+
actionable.push(c);
|
|
17
|
+
toMarkSeen.push(c);
|
|
18
|
+
}
|
|
19
|
+
return { actionable, minimizeIds, toMarkSeen };
|
|
20
|
+
}
|
package/bin/config/load.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import builtins from "../config.json" with { type: "json" };
|
|
6
6
|
import { parseCliRunner } from "../cli/runner.mjs";
|
|
7
|
+
const MINIMIZE_COMMENTS_POLICIES = ["all", "bots", "users", "none"];
|
|
7
8
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
8
9
|
function findRcFile(startDir) {
|
|
9
10
|
const home = homedir();
|
|
@@ -37,6 +38,14 @@ function deepMerge(base, override) {
|
|
|
37
38
|
}
|
|
38
39
|
return result;
|
|
39
40
|
}
|
|
41
|
+
function isMinimizeCommentsPolicy(value) {
|
|
42
|
+
return MINIMIZE_COMMENTS_POLICIES.some((policy) => policy === value);
|
|
43
|
+
}
|
|
44
|
+
function parseMinimizeCommentsPolicy(value) {
|
|
45
|
+
if (isMinimizeCommentsPolicy(value))
|
|
46
|
+
return value;
|
|
47
|
+
throw new Error(`Invalid config: iterate.minimizeComments must be one of "all", "bots", "users", or "none", got ${JSON.stringify(value)}`);
|
|
48
|
+
}
|
|
40
49
|
const defaults = builtins;
|
|
41
50
|
const configCache = new Map();
|
|
42
51
|
export function loadConfig() {
|
|
@@ -58,9 +67,7 @@ export function loadConfig() {
|
|
|
58
67
|
throw new Error(`Invalid config: cli must be a plain object, got ${JSON.stringify(config.cli)}`);
|
|
59
68
|
}
|
|
60
69
|
config.cli.runner = parseCliRunner(config.cli.runner);
|
|
61
|
-
|
|
62
|
-
throw new Error(`Invalid config: watch.interval must be a duration string like "4m" or "1h", got ${JSON.stringify(config.watch?.interval)}`);
|
|
63
|
-
}
|
|
70
|
+
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
64
71
|
configCache.set(cwd, config);
|
|
65
72
|
return config;
|
|
66
73
|
}
|
package/bin/config.json
CHANGED
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
"runner": "auto"
|
|
4
4
|
},
|
|
5
5
|
"iterate": {
|
|
6
|
-
"cooldownSeconds": 30,
|
|
7
6
|
"fixAttemptsPerThread": 3,
|
|
8
7
|
"stallTimeoutMinutes": 30,
|
|
9
|
-
"minimizeApprovals": false
|
|
8
|
+
"minimizeApprovals": false,
|
|
9
|
+
"minimizeComments": "all"
|
|
10
10
|
},
|
|
11
11
|
"watch": {
|
|
12
|
-
"interval": "4m",
|
|
13
12
|
"readyDelayMinutes": 10
|
|
14
13
|
},
|
|
15
14
|
"resolve": {
|
|
@@ -18,6 +18,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
18
18
|
line: comment?.line ?? null,
|
|
19
19
|
startLine: comment?.startLine ?? null,
|
|
20
20
|
author: comment?.author?.login ?? "unknown",
|
|
21
|
+
authorType: mapAuthorType(comment?.author?.__typename),
|
|
21
22
|
body: comment?.body ?? "",
|
|
22
23
|
url: comment?.url ?? "",
|
|
23
24
|
createdAtUnix: comment ? parseCreatedAt(comment.createdAt) : 0,
|
|
@@ -27,6 +28,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
27
28
|
id: c.id,
|
|
28
29
|
isMinimized: c.isMinimized,
|
|
29
30
|
author: c.author?.login ?? "unknown",
|
|
31
|
+
authorType: mapAuthorType(c.author?.__typename),
|
|
30
32
|
body: c.body,
|
|
31
33
|
url: c.url,
|
|
32
34
|
createdAtUnix: parseCreatedAt(c.createdAt),
|
|
@@ -34,6 +36,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
34
36
|
const changesRequestedReviews = rawReviewNodes.map((r) => ({
|
|
35
37
|
id: r.id,
|
|
36
38
|
author: r.author?.login ?? "unknown",
|
|
39
|
+
authorType: mapAuthorType(r.author?.__typename),
|
|
37
40
|
body: r.body,
|
|
38
41
|
}));
|
|
39
42
|
const reviewSummaries = rawReviewSummaryNodes
|
|
@@ -41,6 +44,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
41
44
|
.map((r) => ({
|
|
42
45
|
id: r.id,
|
|
43
46
|
author: r.author?.login ?? "unknown",
|
|
47
|
+
authorType: mapAuthorType(r.author?.__typename),
|
|
44
48
|
body: r.body,
|
|
45
49
|
}));
|
|
46
50
|
// APPROVED reviews often have empty bodies (clicking "Approve" without a comment), so
|
|
@@ -51,6 +55,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
51
55
|
.map((r) => ({
|
|
52
56
|
id: r.id,
|
|
53
57
|
author: r.author?.login ?? "unknown",
|
|
58
|
+
authorType: mapAuthorType(r.author?.__typename),
|
|
54
59
|
body: r.body,
|
|
55
60
|
}));
|
|
56
61
|
const checks = rawCheckNodes.flatMap((node) => {
|
|
@@ -109,6 +114,11 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
109
114
|
checks,
|
|
110
115
|
};
|
|
111
116
|
}
|
|
117
|
+
function mapAuthorType(typeName) {
|
|
118
|
+
if (typeName === "User" || typeName === "Bot")
|
|
119
|
+
return typeName;
|
|
120
|
+
return "Unknown";
|
|
121
|
+
}
|
|
112
122
|
function parseCreatedAt(iso) {
|
|
113
123
|
const ms = new Date(iso).getTime();
|
|
114
124
|
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
|
@@ -43,6 +43,7 @@ query BatchPr(
|
|
|
43
43
|
latestReviews(last: 100) {
|
|
44
44
|
nodes {
|
|
45
45
|
author {
|
|
46
|
+
__typename
|
|
46
47
|
login
|
|
47
48
|
}
|
|
48
49
|
state
|
|
@@ -64,6 +65,7 @@ query BatchPr(
|
|
|
64
65
|
isMinimized
|
|
65
66
|
url
|
|
66
67
|
author {
|
|
68
|
+
__typename
|
|
67
69
|
login
|
|
68
70
|
}
|
|
69
71
|
body
|
|
@@ -85,6 +87,7 @@ query BatchPr(
|
|
|
85
87
|
isMinimized
|
|
86
88
|
url
|
|
87
89
|
author {
|
|
90
|
+
__typename
|
|
88
91
|
login
|
|
89
92
|
}
|
|
90
93
|
body
|
|
@@ -103,6 +106,7 @@ query BatchPr(
|
|
|
103
106
|
nodes {
|
|
104
107
|
id
|
|
105
108
|
author {
|
|
109
|
+
__typename
|
|
106
110
|
login
|
|
107
111
|
}
|
|
108
112
|
body
|
|
@@ -117,6 +121,7 @@ query BatchPr(
|
|
|
117
121
|
id
|
|
118
122
|
isMinimized
|
|
119
123
|
author {
|
|
124
|
+
__typename
|
|
120
125
|
login
|
|
121
126
|
}
|
|
122
127
|
body
|
|
@@ -131,6 +136,7 @@ query BatchPr(
|
|
|
131
136
|
id
|
|
132
137
|
isMinimized
|
|
133
138
|
author {
|
|
139
|
+
__typename
|
|
134
140
|
login
|
|
135
141
|
}
|
|
136
142
|
body
|
package/bin/github/queries.mjs
CHANGED
|
@@ -13,8 +13,6 @@ const gql = (name) => readFileSync(join(import.meta.dirname, "gql", name), "utf8
|
|
|
13
13
|
export const BATCH_PR_QUERY = gql("batch-pr.gql");
|
|
14
14
|
/** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
|
|
15
15
|
export const GET_PR_HEAD_SHA_QUERY = gql("get-pr-head-sha.gql");
|
|
16
|
-
/** Paginated follow-up for `shepherd status` — used when reviewThreads is truncated (totalCount > 100). */
|
|
17
|
-
export const MULTI_PR_STATUS_QUERY_WITH_CURSOR = gql("multi-pr-status-paged.gql");
|
|
18
16
|
/** Look up PR number by branch name (for getCurrentPrNumber). */
|
|
19
17
|
export const PR_NUMBER_BY_BRANCH_QUERY = gql("pr-number-by-branch.gql");
|
|
20
18
|
/** Convert a draft PR to ready for review. */
|
package/bin/index.mjs
CHANGED
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* pr-shepherd [PR]
|
|
7
|
-
* pr-shepherd check [PR]
|
|
8
7
|
* pr-shepherd resolve [PR]
|
|
9
8
|
* pr-shepherd iterate [PR]
|
|
10
|
-
* pr-shepherd status PR1 [PR2 …]
|
|
11
9
|
*/
|
|
12
10
|
import { main } from "./cli-parser.mjs";
|
|
13
11
|
function formatCause(cause, seen = new Set(), depth = 0) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Interpretation order for `status` — first match wins:
|
|
8
8
|
* 1. mergeable == CONFLICTING → CONFLICTS (hard conflict even for drafts)
|
|
9
|
-
* 2.
|
|
9
|
+
* 2. blockingBotReviewInProgress → BLOCKED
|
|
10
10
|
* 3. mergeStateStatus DIRTY → CONFLICTS (GitHub merge conflicts, even for drafts)
|
|
11
11
|
* 4. isDraft → DRAFT
|
|
12
12
|
* 5. mergeStateStatus BEHIND → BEHIND
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { loadConfig } from "../config/load.mjs";
|
|
19
19
|
export function deriveMergeStatus(pr) {
|
|
20
|
-
const
|
|
20
|
+
const blockingBotReviewInProgress = detectBlockingBotReview(pr);
|
|
21
21
|
let status;
|
|
22
22
|
if (pr.mergeable === "CONFLICTING") {
|
|
23
23
|
status = "CONFLICTS";
|
|
24
24
|
}
|
|
25
|
-
else if (
|
|
25
|
+
else if (blockingBotReviewInProgress) {
|
|
26
26
|
status = "BLOCKED";
|
|
27
27
|
}
|
|
28
28
|
else if (pr.mergeStateStatus === "DIRTY") {
|
|
@@ -53,14 +53,14 @@ export function deriveMergeStatus(pr) {
|
|
|
53
53
|
isDraft: pr.isDraft,
|
|
54
54
|
mergeable: pr.mergeable,
|
|
55
55
|
reviewDecision: pr.reviewDecision,
|
|
56
|
-
|
|
56
|
+
blockingBotReviewInProgress,
|
|
57
57
|
mergeStateStatus: pr.mergeStateStatus,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
|
-
//
|
|
61
|
+
// Blocking bot review detection
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
|
-
function
|
|
63
|
+
function detectBlockingBotReview(pr) {
|
|
64
64
|
// A blocking bot review is "in progress" when:
|
|
65
65
|
// 1. Any reviewRequest has a login starting with one of the configured prefixes, OR
|
|
66
66
|
// 2. Any latestReview has such a login AND state == "PENDING"
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These strip fields that are always-false by the time items reach iterate
|
|
5
5
|
* (isResolved, isOutdated, isMinimized, createdAtUnix) and check metadata the
|
|
6
|
-
*
|
|
6
|
+
* agent/iterate prompt never reads (event, status, category).
|
|
7
7
|
* conclusion is preserved on AgentCheck so the formatter can branch on run-level conclusions.
|
|
8
8
|
* detailsUrl is preserved in AgentCheck as a fallback for external status checks.
|
|
9
|
-
* The original domain types are preserved
|
|
9
|
+
* The original domain types are preserved as internal snapshot types.
|
|
10
10
|
*/
|
|
11
11
|
import { extractSuggestion } from "../suggestions/extract.mjs";
|
|
12
12
|
export function toAgentThread(t) {
|
|
@@ -19,13 +19,20 @@ export function toAgentThread(t) {
|
|
|
19
19
|
t.startLine !== null &&
|
|
20
20
|
t.startLine !== t.line && { startLine: t.startLine }),
|
|
21
21
|
author: t.author,
|
|
22
|
+
...(t.authorType !== undefined && { authorType: t.authorType }),
|
|
22
23
|
body: t.body,
|
|
23
24
|
url: t.url,
|
|
24
25
|
...(suggestion !== undefined && { suggestion }),
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
export function toAgentComment(c) {
|
|
28
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
id: c.id,
|
|
31
|
+
author: c.author,
|
|
32
|
+
...(c.authorType !== undefined && { authorType: c.authorType }),
|
|
33
|
+
body: c.body,
|
|
34
|
+
url: c.url,
|
|
35
|
+
};
|
|
29
36
|
}
|
|
30
37
|
export function toAgentCheck(c) {
|
|
31
38
|
if (c.conclusion === "SKIPPED" || c.conclusion === "NEUTRAL") {
|
|
@@ -32,6 +32,15 @@ export async function readStallState(key) {
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/** Clear stall state so the next invocation starts a fresh timer (fire-and-forget — never throws). */
|
|
36
|
+
export async function clearStallState(key) {
|
|
37
|
+
try {
|
|
38
|
+
await unlink(resolvePath(key));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Best-effort — file may not exist.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
35
44
|
/** Write stall state (fire-and-forget — never throws). */
|
|
36
45
|
export async function writeStallState(key, state) {
|
|
37
46
|
let tmp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/**",
|
|
13
|
-
"plugin/**",
|
|
14
13
|
"plugins/**",
|
|
15
14
|
"plugins/**/.codex-plugin/**",
|
|
16
15
|
".claude-plugin/**",
|
|
@@ -36,12 +35,12 @@
|
|
|
36
35
|
},
|
|
37
36
|
"scripts": {
|
|
38
37
|
"build": "node scripts/build.mjs",
|
|
39
|
-
"prepare": "node scripts/install-husky.mjs &&
|
|
38
|
+
"prepare": "node scripts/install-husky.mjs && npm run build",
|
|
40
39
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
41
40
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"lint": "oxlint src/
|
|
43
|
-
"format": "oxfmt src/
|
|
44
|
-
"format:check": "oxfmt --check src/
|
|
41
|
+
"lint": "oxlint src/ plugins/ .agents/plugins/",
|
|
42
|
+
"format": "oxfmt src/ plugins/ .agents/plugins/ docs/ README.md",
|
|
43
|
+
"format:check": "oxfmt --check src/ plugins/ .agents/plugins/ docs/ README.md",
|
|
45
44
|
"test": "vitest run",
|
|
46
45
|
"test:coverage": "vitest run --coverage",
|
|
47
46
|
"test:watch": "vitest"
|
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pr-shepherd
|
|
3
|
-
description: '
|
|
3
|
+
description: 'Iterate a GitHub pull request to completion with pr-shepherd. Use for requests like "use pr-shepherd", "iterate PR #123", or "run pr-shepherd until this PR is ready".'
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: "[PR number or URL]"
|
|
6
|
+
allowed-tools: ["Bash", "Read", "Grep", "Edit", "Write", "Glob", "Skill"]
|
|
4
7
|
---
|
|
5
8
|
|
|
6
9
|
# pr-shepherd
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
One-tick dispatcher for iterating a PR to completion.
|
|
9
12
|
|
|
10
|
-
##
|
|
13
|
+
## Arguments: $ARGUMENTS
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
- If the user provides a PR number, use it.
|
|
14
|
-
- If the user provides a GitHub PR URL, extract the PR number.
|
|
15
|
-
- If no PR is provided, infer it from the current branch with:
|
|
16
|
-
`gh pr view --json number --jq .number`
|
|
17
|
-
- If no PR is found, report that and stop.
|
|
15
|
+
## Steps
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
- `
|
|
17
|
+
1. **Resolve PR number:**
|
|
18
|
+
- If `$ARGUMENTS` contains a PR number, use it.
|
|
19
|
+
- If `$ARGUMENTS` contains a GitHub PR URL, extract the number.
|
|
20
|
+
- Otherwise, infer: `gh pr view --json number --jq .number`
|
|
21
|
+
- If no PR found, report an error and stop.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
- Prefer `package.json` `packageManager`: `pnpm@...` -> `pnpm exec`, `yarn@...` -> `yarn run`, `npm@...` -> `npx --no-install`.
|
|
24
|
-
- If `packageManager` is absent, use lockfiles: `pnpm-lock.yaml` -> `pnpm exec`, `yarn.lock` -> `yarn run`, `package-lock.json` or no signal -> `npx --no-install`.
|
|
25
|
-
- Example: in `~/filaments`, use `pnpm exec pr-shepherd ...` because the root package declares `packageManager: "pnpm@..."` and has `pnpm-lock.yaml`.
|
|
23
|
+
2. **Short-circuit if merged or closed:**
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- In other repositories, run through the selected package runner so Codex does not install packages implicitly. If the package is missing, tell the user to install `pr-shepherd` with the matching dev-dependency command: `pnpm add -D pr-shepherd`, `yarn add -D pr-shepherd`, or `npm install --save-dev pr-shepherd`.
|
|
25
|
+
```bash
|
|
26
|
+
gh pr view <N> --json state --jq '.state'
|
|
27
|
+
```
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
- `<runner> pr-shepherd PR_NUMBER`
|
|
29
|
+
If `MERGED` or `CLOSED`, output: `PR #N is already merged/closed. Nothing to do.` and stop.
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
3. **Select the package runner** from the target repository root:
|
|
32
|
+
- Prefer `package.json` `packageManager`: `pnpm@...` → `pnpm exec`, `yarn@...` → `yarn run`, `npm@...` → `npx`.
|
|
33
|
+
- If `packageManager` is absent, use lockfiles: `pnpm-lock.yaml` → `pnpm exec`, `yarn.lock` → `yarn run`, `package-lock.json` or no signal → `npx`.
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
4. **Run one iterate tick:**
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
If the package is missing in the target repository, tell the user to install pr-shepherd with the matching dev-dependency command: `pnpm add -D pr-shepherd`, `yarn add -D pr-shepherd`, or `npm install --save-dev pr-shepherd`.
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
```bash
|
|
40
|
+
<runner> pr-shepherd <N>
|
|
41
|
+
```
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Print the full output. Follow the `## Instructions` section exactly.
|
|
44
|
+
|
|
45
|
+
5. **Stop conditions:**
|
|
46
|
+
- Stop when the CLI emits `[CANCEL]` (ready-delay completed, or PR merged/closed).
|
|
47
|
+
- Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures.
|
|
48
|
+
- All other actions (`[WAIT]`, `[MARK_READY]`, `[FIX_CODE]`) are non-terminal: follow the `## Instructions` to sleep/wait and rerun.
|
package/bin/commands/iterate.mjs
DELETED