pr-shepherd 0.10.0 → 0.10.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/plugin.json +1 -1
- package/bin/cli/fix-formatter.mjs +12 -3
- package/bin/cli/iterate-lean.mjs +3 -0
- package/bin/cli-parser.iterate-fixtures.mjs +1 -0
- package/bin/commands/check.mjs +6 -3
- package/bin/commands/iterate/classify.mjs +5 -3
- package/bin/commands/iterate/fix-code.mjs +3 -2
- package/bin/commands/iterate/index.mjs +2 -1
- package/bin/commands/iterate/render.mjs +21 -19
- package/bin/reporters/text.mjs +2 -1
- package/package.json +5 -4
|
@@ -56,9 +56,18 @@ export function formatFixCodeResult(header, result) {
|
|
|
56
56
|
sections.push("## Changes-requested reviews");
|
|
57
57
|
sections.push(result.fix.changesRequestedReviews.map((r) => renderReviewBullet(r)).join("\n"));
|
|
58
58
|
}
|
|
59
|
-
if (result.fix.
|
|
60
|
-
sections.push("## Review summaries (
|
|
61
|
-
|
|
59
|
+
if (result.fix.firstLookSummaries.length > 0) {
|
|
60
|
+
sections.push("## Review summaries (first look — to be minimized)");
|
|
61
|
+
for (const r of result.fix.firstLookSummaries) {
|
|
62
|
+
sections.push(`### \`reviewId=${r.id}\` (@${r.author})`);
|
|
63
|
+
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const firstLookSummaryIds = new Set(result.fix.firstLookSummaries.map((r) => r.id));
|
|
67
|
+
const seenSummaryIds = result.fix.reviewSummaryIds.filter((id) => !firstLookSummaryIds.has(id));
|
|
68
|
+
if (seenSummaryIds.length > 0) {
|
|
69
|
+
sections.push("## Review IDs to minimize queue");
|
|
70
|
+
sections.push(seenSummaryIds.map((id) => `- \`${id}\``).join("\n"));
|
|
62
71
|
}
|
|
63
72
|
if (result.fix.surfacedApprovals.length > 0) {
|
|
64
73
|
sections.push("## Approvals (surfaced — not minimized)");
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -52,6 +52,9 @@ export function projectIterateLean(result) {
|
|
|
52
52
|
...(result.fix.reviewSummaryIds.length > 0 && {
|
|
53
53
|
reviewSummaryIds: result.fix.reviewSummaryIds,
|
|
54
54
|
}),
|
|
55
|
+
...(result.fix.firstLookSummaries.length > 0 && {
|
|
56
|
+
firstLookSummaries: result.fix.firstLookSummaries,
|
|
57
|
+
}),
|
|
55
58
|
...(result.fix.surfacedApprovals.length > 0 && {
|
|
56
59
|
surfacedApprovals: result.fix.surfacedApprovals,
|
|
57
60
|
}),
|
package/bin/commands/check.mjs
CHANGED
|
@@ -46,7 +46,6 @@ export async function runCheck(opts) {
|
|
|
46
46
|
mergeStateStatus: restState.mergeStateStatus ?? batchData.mergeStateStatus,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
// Classify checks.
|
|
50
49
|
const classifiedChecks = classifyChecks(batchData.checks);
|
|
51
50
|
const verdict = getCiVerdict(classifiedChecks);
|
|
52
51
|
const passing = classifiedChecks.filter((c) => c.category === "passed");
|
|
@@ -58,7 +57,6 @@ export async function runCheck(opts) {
|
|
|
58
57
|
? await triageFailingChecks(failing, repo, config.checks.logTailLines, config.checks.logTailChars)
|
|
59
58
|
: failing;
|
|
60
59
|
const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
|
|
61
|
-
// Resolve threads and comments.
|
|
62
60
|
const unresolvedThreads = batchData.reviewThreads.filter((t) => !t.isResolved && !t.isMinimized);
|
|
63
61
|
const visibleComments = batchData.comments.filter((c) => !c.isMinimized);
|
|
64
62
|
// Auto-resolve outdated threads.
|
|
@@ -96,10 +94,14 @@ export async function runCheck(opts) {
|
|
|
96
94
|
const firstLookComments = minimizedCommentCandidates
|
|
97
95
|
.filter((c) => !seenSet.has(c.id))
|
|
98
96
|
.map((c) => ({ ...c, firstLookStatus: "minimized" }));
|
|
97
|
+
// Split review summaries into first-look (unseen — surface body) vs seen (minimize silently).
|
|
98
|
+
const firstLookSummaries = batchData.reviewSummaries.filter((r) => !seenSet.has(r.id));
|
|
99
|
+
const seenSummaries = batchData.reviewSummaries.filter((r) => seenSet.has(r.id));
|
|
99
100
|
// Mark first-look items as seen (best-effort — markSeen never throws).
|
|
100
101
|
await Promise.allSettled([
|
|
101
102
|
...firstLookThreads.map((t) => markSeen(stateKey, t.id)),
|
|
102
103
|
...firstLookComments.map((c) => markSeen(stateKey, c.id)),
|
|
104
|
+
...firstLookSummaries.map((r) => markSeen(stateKey, r.id)),
|
|
103
105
|
]);
|
|
104
106
|
// Actionable: all active threads and all visible comments (no classification — LLM handles triage).
|
|
105
107
|
const actionableThreads = activeThreads;
|
|
@@ -140,7 +142,8 @@ export async function runCheck(opts) {
|
|
|
140
142
|
firstLook: firstLookComments,
|
|
141
143
|
},
|
|
142
144
|
changesRequestedReviews: batchData.changesRequestedReviews,
|
|
143
|
-
reviewSummaries:
|
|
145
|
+
reviewSummaries: seenSummaries,
|
|
146
|
+
firstLookSummaries,
|
|
144
147
|
approvedReviews: batchData.approvedReviews,
|
|
145
148
|
};
|
|
146
149
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export function classifyReviewSummaries(summaries, approvals, minimizeApprovals) {
|
|
2
|
-
|
|
2
|
+
// Both first-look and seen summaries go into the minimize mutation; first-look bodies are
|
|
3
|
+
// rendered in the output so the agent sees them before the minimize happens.
|
|
4
|
+
const minimizeIds = [...summaries.firstLook, ...summaries.seen].map((r) => r.id);
|
|
3
5
|
if (minimizeApprovals) {
|
|
4
6
|
for (const r of approvals)
|
|
5
7
|
minimizeIds.push(r.id);
|
|
6
|
-
return { minimizeIds, surfacedApprovals: [] };
|
|
8
|
+
return { minimizeIds, firstLookSummaries: summaries.firstLook, surfacedApprovals: [] };
|
|
7
9
|
}
|
|
8
|
-
return { minimizeIds, surfacedApprovals: approvals };
|
|
10
|
+
return { minimizeIds, firstLookSummaries: summaries.firstLook, surfacedApprovals: approvals };
|
|
9
11
|
}
|
|
10
12
|
export function buildResolveCommand(threads, allCommentIds, reviews, checks, prNumber) {
|
|
11
13
|
const argv = ["npx", "pr-shepherd", "resolve", String(prNumber)];
|
|
@@ -6,7 +6,7 @@ import { buildFixInstructions } from "./render.mjs";
|
|
|
6
6
|
import { applyStallGuard } from "./stall.mjs";
|
|
7
7
|
import { tryCancelRun } from "./helpers.mjs";
|
|
8
8
|
export async function handleFixCode(ctx) {
|
|
9
|
-
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, surfacedApprovals, } = ctx;
|
|
9
|
+
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, surfacedApprovals, } = ctx;
|
|
10
10
|
const failingChecks = report.checks.failing;
|
|
11
11
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
12
12
|
const isNewSha = stored?.headSha !== headSha;
|
|
@@ -73,7 +73,7 @@ export async function handleFixCode(ctx) {
|
|
|
73
73
|
}
|
|
74
74
|
const firstLookThreads = report.threads.firstLook;
|
|
75
75
|
const firstLookComments = report.comments.firstLook;
|
|
76
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments);
|
|
76
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries);
|
|
77
77
|
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, {
|
|
78
78
|
...base,
|
|
79
79
|
baseBranch: baseLookup.branch,
|
|
@@ -83,6 +83,7 @@ export async function handleFixCode(ctx) {
|
|
|
83
83
|
threads,
|
|
84
84
|
actionableComments,
|
|
85
85
|
reviewSummaryIds,
|
|
86
|
+
firstLookSummaries,
|
|
86
87
|
surfacedApprovals,
|
|
87
88
|
checks,
|
|
88
89
|
changesRequestedReviews,
|
|
@@ -91,7 +91,7 @@ export async function runIterate(opts) {
|
|
|
91
91
|
}
|
|
92
92
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
93
93
|
const stallKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
94
|
-
const { minimizeIds: reviewSummaryIds, surfacedApprovals } = classifyReviewSummaries(report.reviewSummaries, report.approvedReviews, config.iterate.minimizeApprovals);
|
|
94
|
+
const { minimizeIds: reviewSummaryIds, firstLookSummaries, surfacedApprovals, } = classifyReviewSummaries({ firstLook: report.firstLookSummaries, seen: report.reviewSummaries }, report.approvedReviews, config.iterate.minimizeApprovals);
|
|
95
95
|
const hasActionableWork = report.threads.actionable.length > 0 ||
|
|
96
96
|
report.comments.actionable.length > 0 ||
|
|
97
97
|
report.changesRequestedReviews.length > 0 ||
|
|
@@ -110,6 +110,7 @@ export async function runIterate(opts) {
|
|
|
110
110
|
repoOwner,
|
|
111
111
|
repoName,
|
|
112
112
|
reviewSummaryIds,
|
|
113
|
+
firstLookSummaries,
|
|
113
114
|
surfacedApprovals,
|
|
114
115
|
});
|
|
115
116
|
}
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render a
|
|
3
|
-
*
|
|
4
|
-
* the two known placeholders ($DISMISS_MESSAGE, $HEAD_SHA) and any whitespace-
|
|
5
|
-
* bearing arg in double quotes so multi-word values don't split across flags.
|
|
2
|
+
* Render a resolve command as a shell snippet for the narrow placeholder-based
|
|
3
|
+
* invocation used by iterate.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* This is not a general-purpose shell escaper. It only wraps
|
|
6
|
+
* `$DISMISS_MESSAGE` and whitespace-bearing `rc.argv` entries in double quotes
|
|
7
|
+
* so the surrounding command template can later substitute placeholder values.
|
|
8
|
+
*
|
|
9
|
+
* Callers must preserve that contract:
|
|
10
|
+
* - placeholder substitution must replace the entire quoted token (for example,
|
|
11
|
+
* replace `"$DISMISS_MESSAGE"` as a whole, not text inside the quotes);
|
|
12
|
+
* - `rc.argv` must not contain `"`, `$`, `` ` ``, or `\`, because this helper
|
|
13
|
+
* does not escape them and will throw if they are present;
|
|
14
|
+
* - `$HEAD_SHA` must not appear in `rc.argv`; when `requiresHeadSha` is set it
|
|
15
|
+
* is appended separately below as the already-quoted token `"$HEAD_SHA"`.
|
|
11
16
|
*/
|
|
12
17
|
export function renderResolveCommand(rc) {
|
|
13
|
-
// `$HEAD_SHA` is never in `rc.argv` — it is appended pre-quoted below when
|
|
14
|
-
// `requiresHeadSha`. Only `$DISMISS_MESSAGE` (or whitespace-bearing values)
|
|
15
|
-
// need quoting here.
|
|
16
18
|
const needsQuoting = (arg) => {
|
|
17
19
|
if (arg === "$DISMISS_MESSAGE")
|
|
18
20
|
return true;
|
|
19
|
-
// Assert no characters that would break the naive escaper are present in arg
|
|
20
21
|
if (/["$`\\]/.test(arg)) {
|
|
21
22
|
throw new Error(`Unexpected character in argv arg that needsQuoting can't handle: ${JSON.stringify(arg)}`);
|
|
22
23
|
}
|
|
@@ -28,7 +29,7 @@ export function renderResolveCommand(rc) {
|
|
|
28
29
|
}
|
|
29
30
|
return parts.join(" ");
|
|
30
31
|
}
|
|
31
|
-
export function buildFixInstructions(threads, actionableComments, checks, reviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = []) {
|
|
32
|
+
export function buildFixInstructions(threads, actionableComments, checks, reviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = []) {
|
|
32
33
|
const instructions = [];
|
|
33
34
|
const hasSuggestions = threads.some((t) => t.suggestion);
|
|
34
35
|
if (hasSuggestions) {
|
|
@@ -72,8 +73,13 @@ export function buildFixInstructions(threads, actionableComments, checks, review
|
|
|
72
73
|
instructions.push(`Rebase and push: \`git fetch origin && git rebase origin/${baseBranch} && git push --force-with-lease\`${captureHint}`);
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
77
|
+
if (firstLookTotal > 0) {
|
|
78
|
+
instructions.push(`Items in \`## First-look items\` are for acknowledgement only — do not pass their IDs to \`--resolve-thread-ids\`, \`--minimize-comment-ids\`, or \`--dismiss-review-ids\`. Acknowledge each one with a one-line classification (e.g. "outdated — addressed by commit abc1234", "resolved — already fixed", "minimized — noise").`);
|
|
79
|
+
}
|
|
80
|
+
if (firstLookSummaries.length > 0) {
|
|
81
|
+
instructions.push(`Review the bodies shown under \`## Review summaries (first look — to be minimized)\` — you are seeing these for the first time. Their IDs are already included in the \`resolve:\` command's \`--minimize-comment-ids\`; if any warrants a \`## Shepherd Journal\` entry, record it before running resolve.`);
|
|
82
|
+
}
|
|
77
83
|
if (resolveCommand.hasMutations) {
|
|
78
84
|
const substituteParts = [];
|
|
79
85
|
if (resolveCommand.requiresHeadSha) {
|
|
@@ -88,10 +94,6 @@ export function buildFixInstructions(threads, actionableComments, checks, review
|
|
|
88
94
|
if (needsPush && cancelledCount > 0) {
|
|
89
95
|
instructions.push(`Do not re-run \`gh run cancel\` on the IDs listed under \`## Cancelled runs\` — the CLI cancelled those runs before your push, and your push has already triggered new runs with different IDs.`);
|
|
90
96
|
}
|
|
91
|
-
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
92
|
-
if (firstLookTotal > 0) {
|
|
93
|
-
instructions.push(`Items in \`## First-look items\` are for acknowledgement only — do not pass their IDs to \`--resolve-thread-ids\`, \`--minimize-comment-ids\`, or \`--dismiss-review-ids\`. Acknowledge each one with a one-line classification (e.g. "outdated — addressed by commit abc1234", "resolved — already fixed", "minimized — noise").`);
|
|
94
|
-
}
|
|
95
97
|
if (resolveCommand.hasMutations) {
|
|
96
98
|
instructions.push(`For any large decisions or rejections you made this iteration, add or update a \`## Shepherd Journal\` section in the PR description (\`gh pr edit ${prNumber} --body …\`) summarizing each decision. For threads and comments, use the markdown link shown in its heading above; for reviews, reference the review ID.`);
|
|
97
99
|
}
|
package/bin/reporters/text.mjs
CHANGED
|
@@ -98,7 +98,8 @@ export function formatText(report) {
|
|
|
98
98
|
commentsSection = `## PR Comments\n\n${lines.join("\n")}`;
|
|
99
99
|
}
|
|
100
100
|
const changesRequestedSection = reviewListSection("CHANGES_REQUESTED Reviews", report.changesRequestedReviews);
|
|
101
|
-
const
|
|
101
|
+
const allSummaries = [...report.firstLookSummaries, ...report.reviewSummaries];
|
|
102
|
+
const reviewSummariesSection = reviewListSection("Review Summaries", allSummaries);
|
|
102
103
|
const approvedReviewsSection = reviewListSection("Approved Reviews", report.approvedReviews);
|
|
103
104
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
104
105
|
let firstLookSection = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -24,15 +24,16 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^25.6.0",
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.4",
|
|
28
|
+
"husky": "^9.1.7",
|
|
27
29
|
"oxfmt": "^0.46.0",
|
|
28
30
|
"oxlint": "^1.60.0",
|
|
29
31
|
"typescript": "^6.0.3",
|
|
30
|
-
"vitest": "^4.1.4"
|
|
31
|
-
"@vitest/coverage-v8": "^4.1.4"
|
|
32
|
+
"vitest": "^4.1.4"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "node scripts/build.mjs",
|
|
35
|
-
"prepare": "node scripts/install-
|
|
36
|
+
"prepare": "node scripts/install-husky.mjs && node scripts/install-plugin-symlink.mjs && npm run build",
|
|
36
37
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
38
39
|
"lint": "oxlint src/ plugin/skills/",
|