pr-shepherd 0.59.2 → 0.60.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 +3 -3
- package/bin/api.d.mts +1 -1
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/edited-comments-format.d.mts +14 -0
- package/bin/cli/edited-comments-format.mjs +39 -0
- package/bin/cli/fix-formatter.mjs +5 -2
- package/bin/cli/iterate-activity-formatter.d.mts +1 -1
- package/bin/cli/iterate-activity-formatter.mjs +4 -5
- package/bin/cli/iterate-check-names.d.mts +5 -0
- package/bin/cli/iterate-check-names.mjs +17 -0
- package/bin/cli/iterate-formatter.mjs +15 -19
- package/bin/cli/iterate-lean.mjs +19 -11
- package/bin/cli/iterate-verbose.mjs +7 -2
- package/bin/cli/list-formatters.d.mts +0 -3
- package/bin/cli/list-formatters.mjs +1 -5
- package/bin/commands/check-review-budget.d.mts +39 -0
- package/bin/commands/check-review-budget.mjs +78 -0
- package/bin/commands/check-wait-detectors.d.mts +6 -1
- package/bin/commands/check-wait-detectors.mjs +2 -1
- package/bin/commands/check.d.mts +2 -0
- package/bin/commands/check.mjs +46 -27
- package/bin/commands/iterate/check-instructions.d.mts +5 -0
- package/bin/commands/iterate/check-instructions.mjs +4 -0
- package/bin/commands/iterate/classify.d.mts +1 -1
- package/bin/commands/iterate/edited-comment-markers.d.mts +3 -0
- package/bin/commands/iterate/edited-comment-markers.mjs +10 -0
- package/bin/commands/iterate/fix-code.mjs +17 -1
- package/bin/commands/iterate/fix-loop-instruction.d.mts +2 -8
- package/bin/commands/iterate/fix-loop-instruction.mjs +8 -29
- package/bin/commands/iterate/helpers.mjs +9 -5
- package/bin/commands/iterate/index.mjs +44 -17
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +18 -17
- package/bin/commands/poll-rate-limit-cancel.mjs +1 -1
- package/bin/commands/poll-seen-options.d.mts +8 -0
- package/bin/commands/poll-seen-options.mjs +7 -0
- package/bin/commands/poll.mjs +7 -5
- package/bin/commands/rule-auto-resolve-format.d.mts +0 -12
- package/bin/commands/rule-auto-resolve-format.mjs +2 -11
- package/bin/commands/rule-auto-resolve.d.mts +0 -5
- package/bin/commands/rule-auto-resolve.mjs +1 -42
- package/bin/commands/shepherd-journal.d.mts +0 -9
- package/bin/commands/shepherd-journal.mjs +0 -15
- package/bin/comments/review-output-budget.d.mts +32 -0
- package/bin/comments/review-output-budget.mjs +55 -0
- package/bin/comments/visible-comments.d.mts +3 -1
- package/bin/comments/visible-comments.mjs +10 -5
- package/bin/config/load.d.mts +8 -2
- package/bin/config/load.mjs +7 -0
- package/bin/config.json +2 -1
- package/bin/github/poll-summary-fingerprint.mjs +5 -1
- package/bin/github/poll-summary-review.mjs +2 -1
- package/bin/mcp/server.mjs +1 -1
- package/bin/reporters/agent.d.mts +1 -3
- package/bin/reporters/agent.mjs +0 -1
- package/bin/types/escalate.d.mts +9 -0
- package/bin/types/iterate.d.mts +19 -1
- package/bin/types/report.d.mts +23 -4
- package/bin/util/cap-list.d.mts +7 -0
- package/bin/util/cap-list.mjs +13 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +1 -2
- package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +1 -1
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import type { AutoMinimizedItem, AutoResolvedThread, RuleAutoResolveReport, ShepherdReport } from "../types.mts";
|
|
2
2
|
export declare function uniqueReasons(reasons: readonly string[]): string[];
|
|
3
|
-
export declare function reasonClause(reasons: readonly string[]): string | undefined;
|
|
4
|
-
export declare function ruleAutoResolveBody(counts: {
|
|
5
|
-
threads: number;
|
|
6
|
-
comments: number;
|
|
7
|
-
reviewSummaries: number;
|
|
8
|
-
}): string | undefined;
|
|
9
|
-
export declare function formatRuleAutoResolveJournalItem(input: {
|
|
10
|
-
body: string;
|
|
11
|
-
reasons: readonly string[];
|
|
12
|
-
viewerLogin?: string;
|
|
13
|
-
urls: readonly string[];
|
|
14
|
-
}): string;
|
|
15
3
|
export declare function decorateAutoResolveError(error: string, reasons: readonly string[]): string;
|
|
16
4
|
export declare function reasonsForError(error: string, ids: readonly string[], ruleReasons: ReadonlyMap<string, readonly string[]>): string[];
|
|
17
5
|
export declare function ruleAutoResolveFromReport(report: ShepherdReport): RuleAutoResolveReport | undefined;
|
|
@@ -7,7 +7,7 @@ export function uniqueReasons(reasons) {
|
|
|
7
7
|
}
|
|
8
8
|
return unique;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
function reasonClause(reasons) {
|
|
11
11
|
const unique = uniqueReasons(reasons);
|
|
12
12
|
if (unique.length === 0)
|
|
13
13
|
return undefined;
|
|
@@ -21,7 +21,7 @@ function joinedReason(reasons) {
|
|
|
21
21
|
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
22
22
|
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
function ruleAutoResolveBody(counts) {
|
|
25
25
|
const parts = [];
|
|
26
26
|
if (counts.threads > 0)
|
|
27
27
|
parts.push(`auto-resolved ${plural(counts.threads, "thread")}`);
|
|
@@ -39,15 +39,6 @@ function withReasonClause(body, reasons) {
|
|
|
39
39
|
const clause = reasonClause(reasons);
|
|
40
40
|
return clause ? `${body} ${clause}` : body;
|
|
41
41
|
}
|
|
42
|
-
export function formatRuleAutoResolveJournalItem(input) {
|
|
43
|
-
const login = input.viewerLogin?.trim();
|
|
44
|
-
const actor = login ? `as @${login}` : "(token login unavailable)";
|
|
45
|
-
const clause = reasonClause(input.reasons);
|
|
46
|
-
const links = input.urls.map((url) => url.trim()).filter(Boolean);
|
|
47
|
-
const reasonSuffix = clause ? ` ${clause}` : "";
|
|
48
|
-
const linkSuffix = links.length > 0 ? `: ${links.join(", ")}` : "";
|
|
49
|
-
return `- ${input.body} ${actor}${reasonSuffix}${linkSuffix}`;
|
|
50
|
-
}
|
|
51
42
|
export function decorateAutoResolveError(error, reasons) {
|
|
52
43
|
const clause = reasonClause(reasons);
|
|
53
44
|
return clause ? `${error} ${clause}` : error;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
2
|
-
import { runJournal } from "./journal/index.mjs";
|
|
3
2
|
import { canGenerateGithubMutation } from "../github/mutation-policy.mjs";
|
|
4
|
-
import { decorateAutoResolveError,
|
|
3
|
+
import { decorateAutoResolveError, minimizedItem, reasonsForError, resolvedThread, uniqueReasons, } from "./rule-auto-resolve-format.mjs";
|
|
5
4
|
const EMPTY = {
|
|
6
5
|
threadIds: [],
|
|
7
6
|
commentIds: [],
|
|
@@ -17,9 +16,6 @@ function idsIn(ids, confirmed) {
|
|
|
17
16
|
function decorateErrors(errors, ids, ruleReasons) {
|
|
18
17
|
return errors.map((error) => decorateAutoResolveError(error, reasonsForError(error, ids, ruleReasons)));
|
|
19
18
|
}
|
|
20
|
-
function successReasons(ids, ruleReasons) {
|
|
21
|
-
return uniqueReasons(ids.flatMap((id) => [...(ruleReasons.get(id) ?? [])]));
|
|
22
|
-
}
|
|
23
19
|
export async function applySuppressedRuleAutoResolve(input) {
|
|
24
20
|
if (!input.enabled) {
|
|
25
21
|
return {
|
|
@@ -70,9 +66,6 @@ export async function applySuppressedRuleAutoResolve(input) {
|
|
|
70
66
|
...minimized.errors.flatMap((error) => reasonsForError(error, minimizeIds, input.partition.ruleReasons)),
|
|
71
67
|
...resolved.errors.flatMap((error) => reasonsForError(error, threadIds, input.partition.ruleReasons)),
|
|
72
68
|
]);
|
|
73
|
-
const journalError = await journalSuccesses(input, autoResolved, autoMinimized);
|
|
74
|
-
if (journalError)
|
|
75
|
-
autoResolveErrors.push(journalError);
|
|
76
69
|
return {
|
|
77
70
|
threadIds: input.partition.ruleAutoResolveThreadIds.filter((id) => !resolvedSet.has(id)),
|
|
78
71
|
commentIds: input.partition.ruleAutoResolveCommentIds.filter((id) => !minimizedSet.has(id)),
|
|
@@ -83,37 +76,3 @@ export async function applySuppressedRuleAutoResolve(input) {
|
|
|
83
76
|
errorReasons,
|
|
84
77
|
};
|
|
85
78
|
}
|
|
86
|
-
async function journalSuccesses(input, autoResolved, autoMinimized) {
|
|
87
|
-
const body = ruleAutoResolveBody({
|
|
88
|
-
threads: autoResolved.length,
|
|
89
|
-
comments: autoMinimized.filter((item) => item.kind === "pr-comment").length,
|
|
90
|
-
reviewSummaries: autoMinimized.filter((item) => item.kind === "review-summary").length,
|
|
91
|
-
});
|
|
92
|
-
if (!body)
|
|
93
|
-
return undefined;
|
|
94
|
-
const ids = [...autoResolved.map((thread) => thread.id), ...autoMinimized.map((item) => item.id)];
|
|
95
|
-
const reasons = successReasons(ids, input.partition.ruleReasons);
|
|
96
|
-
const item = formatRuleAutoResolveJournalItem({
|
|
97
|
-
body,
|
|
98
|
-
reasons,
|
|
99
|
-
...(input.batch.viewerLogin ? { viewerLogin: input.batch.viewerLogin } : {}),
|
|
100
|
-
urls: [
|
|
101
|
-
...autoResolved.map((thread) => thread.url),
|
|
102
|
-
...autoMinimized.map((item) => item.url ?? ""),
|
|
103
|
-
],
|
|
104
|
-
});
|
|
105
|
-
try {
|
|
106
|
-
await runJournal({
|
|
107
|
-
prNumber: input.prNumber,
|
|
108
|
-
targetRepository: { owner: input.repo.owner, name: input.repo.name },
|
|
109
|
-
rawItem: item,
|
|
110
|
-
dryRun: false,
|
|
111
|
-
});
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
116
|
-
const clause = reasonClause(reasons);
|
|
117
|
-
return clause ? `journal: ${message} ${clause}` : `journal: ${message}`;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -4,12 +4,3 @@ export declare const SHEPHERD_JOURNAL_DETAILS_OPEN = "<details>";
|
|
|
4
4
|
export declare const SHEPHERD_JOURNAL_DETAILS_SUMMARY = "<summary>Shepherd Journal</summary>";
|
|
5
5
|
export declare const SHEPHERD_JOURNAL_DETAILS_CLOSE = "</details>";
|
|
6
6
|
export declare const SHEPHERD_JOURNAL_APPEND_HINT = "If Shepherd Journal details already exist, append entries inside them instead of creating another container.";
|
|
7
|
-
export declare const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Read each body under `## Review summaries (first look)` and journal any warranted note before review mutations.";
|
|
8
|
-
/** The concrete `apply journal` command. The placeholder is single-quoted so a substituted decision stays literal in the shell. */
|
|
9
|
-
export declare function buildShepherdJournalCommand(prReference: string | number): string;
|
|
10
|
-
/**
|
|
11
|
-
* Build the Shepherd Journal instruction step. The reference-citation convention (link
|
|
12
|
-
* threads/comments by heading URL, cite reviews by ID) is a short clause, so it stays inline:
|
|
13
|
-
* a separate playbook would cost the agent an extra read turn that outweighs the clause.
|
|
14
|
-
*/
|
|
15
|
-
export declare function buildShepherdJournalInstruction(prReference: string | number): string;
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
2
1
|
export const SHEPHERD_JOURNAL_SECTION = "Shepherd Journal";
|
|
3
2
|
export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
|
|
4
3
|
export const SHEPHERD_JOURNAL_DETAILS_OPEN = "<details>";
|
|
5
4
|
export const SHEPHERD_JOURNAL_DETAILS_SUMMARY = "<summary>Shepherd Journal</summary>";
|
|
6
5
|
export const SHEPHERD_JOURNAL_DETAILS_CLOSE = "</details>";
|
|
7
6
|
export const SHEPHERD_JOURNAL_APPEND_HINT = "If Shepherd Journal details already exist, append entries inside them instead of creating another container.";
|
|
8
|
-
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = `Read each body under \`## Review summaries (first look)\` and journal any warranted note before review mutations.`;
|
|
9
|
-
/** The concrete `apply journal` command. The placeholder is single-quoted so a substituted decision stays literal in the shell. */
|
|
10
|
-
export function buildShepherdJournalCommand(prReference) {
|
|
11
|
-
return `${buildPrShepherdCommand(["apply", "journal", String(prReference)]).text} '- <decision>'`;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Build the Shepherd Journal instruction step. The reference-citation convention (link
|
|
15
|
-
* threads/comments by heading URL, cite reviews by ID) is a short clause, so it stays inline:
|
|
16
|
-
* a separate playbook would cost the agent an extra read turn that outweighs the clause.
|
|
17
|
-
*/
|
|
18
|
-
export function buildShepherdJournalInstruction(prReference) {
|
|
19
|
-
const command = buildShepherdJournalCommand(prReference);
|
|
20
|
-
return `Journal key decisions or rejections with URLs or IDs: \`${command}\``;
|
|
21
|
-
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tick output budget for review items (see `iterate.reviewOutputMaxChars`).
|
|
3
|
+
*
|
|
4
|
+
* Fetching is complete; only the rendered volume is capped. When the bodies of every surfaced
|
|
5
|
+
* review item fit the budget, nothing changes. Otherwise items are taken in a fixed priority
|
|
6
|
+
* order — group by group, oldest first within a group — until the next item would exceed the
|
|
7
|
+
* budget (at least one item is always shown). Every other budgeted item is held: the caller
|
|
8
|
+
* omits it from the report, from generated mutation IDs, and from seen-marker writes, so a
|
|
9
|
+
* later tick shows it.
|
|
10
|
+
*/
|
|
11
|
+
/** One review item's identity, age, and rendered-body cost in characters. */
|
|
12
|
+
export interface BudgetItem {
|
|
13
|
+
id: string;
|
|
14
|
+
createdAtUnix?: number;
|
|
15
|
+
cost: number;
|
|
16
|
+
}
|
|
17
|
+
/** Raw rollup emitted only when items were held: `shown` of `total` review items this tick. */
|
|
18
|
+
export interface ReviewItemRollup {
|
|
19
|
+
shown: number;
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ReviewOutputSelection {
|
|
23
|
+
/** False only for a budgeted item held back this tick. Unbudgeted IDs are always kept. */
|
|
24
|
+
keep: (id: string) => boolean;
|
|
25
|
+
rollup?: ReviewItemRollup;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Select which review items to show. `groups` is in priority order; an ID that appears in more
|
|
29
|
+
* than one group (e.g. a thread that is both resolution-only and first-look) is counted and
|
|
30
|
+
* charged once, in its first group. `maxChars <= 0` disables the cap.
|
|
31
|
+
*/
|
|
32
|
+
export declare function selectReviewOutput(groups: ReadonlyArray<ReadonlyArray<BudgetItem>>, maxChars: number): ReviewOutputSelection;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tick output budget for review items (see `iterate.reviewOutputMaxChars`).
|
|
3
|
+
*
|
|
4
|
+
* Fetching is complete; only the rendered volume is capped. When the bodies of every surfaced
|
|
5
|
+
* review item fit the budget, nothing changes. Otherwise items are taken in a fixed priority
|
|
6
|
+
* order — group by group, oldest first within a group — until the next item would exceed the
|
|
7
|
+
* budget (at least one item is always shown). Every other budgeted item is held: the caller
|
|
8
|
+
* omits it from the report, from generated mutation IDs, and from seen-marker writes, so a
|
|
9
|
+
* later tick shows it.
|
|
10
|
+
*/
|
|
11
|
+
const KEEP_ALL = { keep: () => true };
|
|
12
|
+
function oldestFirst(items) {
|
|
13
|
+
// Unknown ages sort last; Array.prototype.sort is stable, so fetch order breaks ties.
|
|
14
|
+
const age = (item) => item.createdAtUnix ?? Number.MAX_SAFE_INTEGER;
|
|
15
|
+
return [...items].sort((a, b) => age(a) - age(b));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Select which review items to show. `groups` is in priority order; an ID that appears in more
|
|
19
|
+
* than one group (e.g. a thread that is both resolution-only and first-look) is counted and
|
|
20
|
+
* charged once, in its first group. `maxChars <= 0` disables the cap.
|
|
21
|
+
*/
|
|
22
|
+
export function selectReviewOutput(groups, maxChars) {
|
|
23
|
+
if (maxChars <= 0)
|
|
24
|
+
return KEEP_ALL;
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const ordered = [];
|
|
27
|
+
for (const group of groups) {
|
|
28
|
+
const unique = group.filter((item) => {
|
|
29
|
+
if (seen.has(item.id))
|
|
30
|
+
return false;
|
|
31
|
+
seen.add(item.id);
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
ordered.push(...oldestFirst(unique));
|
|
35
|
+
}
|
|
36
|
+
const totalCost = ordered.reduce((sum, item) => sum + item.cost, 0);
|
|
37
|
+
if (totalCost <= maxChars)
|
|
38
|
+
return KEEP_ALL;
|
|
39
|
+
const shown = new Set();
|
|
40
|
+
let used = 0;
|
|
41
|
+
for (const item of ordered) {
|
|
42
|
+
if (shown.size > 0 && used + item.cost > maxChars)
|
|
43
|
+
break;
|
|
44
|
+
shown.add(item.id);
|
|
45
|
+
used += item.cost;
|
|
46
|
+
}
|
|
47
|
+
const held = new Set(ordered.filter((item) => !shown.has(item.id)).map((item) => item.id));
|
|
48
|
+
// The at-least-one rule can show every item despite the budget: nothing is held, no rollup.
|
|
49
|
+
if (held.size === 0)
|
|
50
|
+
return KEEP_ALL;
|
|
51
|
+
return {
|
|
52
|
+
keep: (id) => !held.has(id),
|
|
53
|
+
rollup: { shown: shown.size, total: ordered.length },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type SeenMarker } from "../state/seen-comments.mts";
|
|
2
2
|
import type { MinimizeCommentsPolicy } from "../config/load.mts";
|
|
3
|
-
import type { ActionableComment, PrComment } from "../types.mts";
|
|
3
|
+
import type { ActionableComment, EditedComment, PrComment } from "../types.mts";
|
|
4
4
|
import type { NormalizedBotUsernames } from "./authors.mts";
|
|
5
5
|
interface VisibleCommentClassification {
|
|
6
6
|
actionable: ActionableComment[];
|
|
7
7
|
minimizeIds: string[];
|
|
8
8
|
toMarkSeen: ActionableComment[];
|
|
9
|
+
/** Seen comments whose body changed. Not readiness work; iterate surfaces and marks them. */
|
|
10
|
+
edited: EditedComment[];
|
|
9
11
|
}
|
|
10
12
|
export declare function classifyVisibleComments(comments: PrComment[], seenMap: Map<string, SeenMarker>, minimizeComments: MinimizeCommentsPolicy | undefined, botUsernames?: NormalizedBotUsernames, deniedMinimizeIds?: ReadonlySet<string>): VisibleCommentClassification;
|
|
11
13
|
export {};
|
|
@@ -5,7 +5,14 @@ export function classifyVisibleComments(comments, seenMap, minimizeComments, bot
|
|
|
5
5
|
const actionable = [];
|
|
6
6
|
const minimizeIds = [];
|
|
7
7
|
const toMarkSeen = [];
|
|
8
|
+
const edited = [];
|
|
8
9
|
for (const c of comments.filter((comment) => !comment.isMinimized)) {
|
|
10
|
+
const cls = classifyItem(c.id, c.body, seenMap);
|
|
11
|
+
// An edit is never readiness work, even when the comment is eligible for minimization.
|
|
12
|
+
if (cls === "edited") {
|
|
13
|
+
edited.push({ ...c, edited: true });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
9
16
|
if (!deniedMinimizeIds.has(c.id) &&
|
|
10
17
|
canGenerateGithubMutation(c.viewerCanMinimize, "minimize") &&
|
|
11
18
|
shouldMinimizeAuthor(c.authorType, minimizeComments, c.author, botUsernames)) {
|
|
@@ -14,12 +21,10 @@ export function classifyVisibleComments(comments, seenMap, minimizeComments, bot
|
|
|
14
21
|
toMarkSeen.push(c); // prevents re-surfacing as first-look after GitHub marks it minimized
|
|
15
22
|
continue;
|
|
16
23
|
}
|
|
17
|
-
const cls = classifyItem(c.id, c.body, seenMap);
|
|
18
24
|
if (cls === "unchanged")
|
|
19
25
|
continue;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
toMarkSeen.push(comment);
|
|
26
|
+
actionable.push(c);
|
|
27
|
+
toMarkSeen.push(c);
|
|
23
28
|
}
|
|
24
|
-
return { actionable, minimizeIds, toMarkSeen };
|
|
29
|
+
return { actionable, minimizeIds, toMarkSeen, edited };
|
|
25
30
|
}
|
package/bin/config/load.d.mts
CHANGED
|
@@ -74,6 +74,12 @@ export interface PrShepherdConfig {
|
|
|
74
74
|
* (default) omits the hint entirely; the CLI never prescribes rebase/merge mechanics itself.
|
|
75
75
|
*/
|
|
76
76
|
behindBaseHint: string;
|
|
77
|
+
/**
|
|
78
|
+
* Per-tick budget for review-item bodies in `fix_code` output, in characters. Over budget,
|
|
79
|
+
* iterate shows a priority-ordered subset and holds the rest (unmarked) for later ticks.
|
|
80
|
+
* `0` disables the cap.
|
|
81
|
+
*/
|
|
82
|
+
reviewOutputMaxChars: number;
|
|
77
83
|
/**
|
|
78
84
|
* When to resolve other-human inline threads after Shepherd replies. Own and bot threads
|
|
79
85
|
* always reply-and-resolve. `none` (default) keeps other humans reply-only; `outdated`
|
|
@@ -81,8 +87,8 @@ export interface PrShepherdConfig {
|
|
|
81
87
|
*/
|
|
82
88
|
resolveOtherHumanThreads: ResolveOtherHumanThreads;
|
|
83
89
|
/**
|
|
84
|
-
* `inline` (default) prints the commit/push
|
|
85
|
-
* every tick; `playbook` folds
|
|
90
|
+
* `inline` (default) prints the commit/push step of `fix_code` instructions
|
|
91
|
+
* every tick; `playbook` folds it into one `Playbook: "Fix-code loop"` pointer. The
|
|
86
92
|
* default is inline because the token bench measured playbook as a net cost: it adds a read. The `--instructions` flag and MCP `instructions` input override per call.
|
|
87
93
|
*/
|
|
88
94
|
instructions: InstructionStyle;
|
package/bin/config/load.mjs
CHANGED
|
@@ -92,6 +92,11 @@ function parseInstructionStyle(value) {
|
|
|
92
92
|
return value;
|
|
93
93
|
throw new Error(`Invalid config: iterate.instructions must be "playbook" or "inline", got ${JSON.stringify(value)}`);
|
|
94
94
|
}
|
|
95
|
+
function parseReviewOutputMaxChars(value) {
|
|
96
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0)
|
|
97
|
+
return value;
|
|
98
|
+
throw new Error(`Invalid config: iterate.reviewOutputMaxChars must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
99
|
+
}
|
|
95
100
|
function parseCliCommand(value) {
|
|
96
101
|
if (!Array.isArray(value) ||
|
|
97
102
|
value.length === 0 ||
|
|
@@ -299,6 +304,7 @@ const KNOWN_NESTED_KEYS = {
|
|
|
299
304
|
"behindBaseHint",
|
|
300
305
|
"resolveOtherHumanThreads",
|
|
301
306
|
"instructions",
|
|
307
|
+
"reviewOutputMaxChars",
|
|
302
308
|
]),
|
|
303
309
|
poll: new Set([
|
|
304
310
|
"intervalSeconds",
|
|
@@ -404,6 +410,7 @@ function parseConfig(value, normalizeMergeArgs = true) {
|
|
|
404
410
|
config.iterate.minimizeComments = parseMinimizeCommentsPolicy(config.iterate.minimizeComments);
|
|
405
411
|
config.iterate.resolveOtherHumanThreads = parseResolveOtherHumanThreads(config.iterate.resolveOtherHumanThreads);
|
|
406
412
|
config.iterate.instructions = parseInstructionStyle(config.iterate.instructions);
|
|
413
|
+
config.iterate.reviewOutputMaxChars = parseReviewOutputMaxChars(config.iterate.reviewOutputMaxChars);
|
|
407
414
|
config.checks.ignoreLogLines = parseIgnoreLogLines(config.checks.ignoreLogLines);
|
|
408
415
|
config.standardRules = parseStandardRules(config.classify);
|
|
409
416
|
return config;
|
package/bin/config.json
CHANGED
|
@@ -29,7 +29,11 @@ export function fingerprintRawSummaryPr(summary) {
|
|
|
29
29
|
// Omitted bodies cannot participate in the hash. Bind truncated evidence
|
|
30
30
|
// to the PR revision instead, so updates require a fresh full review poll.
|
|
31
31
|
...(reviewTruncated && { reviewUpdatedAt: raw.updatedAt }),
|
|
32
|
-
comments
|
|
32
|
+
// A PR comment edit is not readiness evidence; its ID still binds new comments.
|
|
33
|
+
comments: {
|
|
34
|
+
...raw.comments,
|
|
35
|
+
nodes: raw.comments.nodes.map(({ body: _body, ...node }) => node),
|
|
36
|
+
},
|
|
33
37
|
reviews: hideBodies(raw.reviews),
|
|
34
38
|
reviewThreads: {
|
|
35
39
|
...raw.reviewThreads,
|
|
@@ -19,7 +19,8 @@ export async function summarizePollSummaryReview(raw, seen, viewerCanAdminister)
|
|
|
19
19
|
raw.reviewThreads.pageInfo.hasPreviousPage;
|
|
20
20
|
for (const comment of raw.comments.nodes) {
|
|
21
21
|
if (!isSuppressed(rules, "pr-comment", comment, comment.id, undefined, comment.body, comment.isMinimized) &&
|
|
22
|
-
|
|
22
|
+
// An edited PR comment is shown once by iterate but never blocks readiness.
|
|
23
|
+
classifyItem(comment.id, comment.body, seen) === "new")
|
|
23
24
|
actionable += 1;
|
|
24
25
|
}
|
|
25
26
|
const latestStateByAuthor = new Map((raw.latestReviews?.nodes ?? []).flatMap((review) => review.author ? [[review.author.login.toLowerCase(), review.state]] : []));
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -33,7 +33,7 @@ const iterateInputSchema = z
|
|
|
33
33
|
instructions: z
|
|
34
34
|
.enum(["playbook", "inline"])
|
|
35
35
|
.optional()
|
|
36
|
-
.describe("fix_code instruction style. inline (default) prints every step each tick; playbook folds commit
|
|
36
|
+
.describe("fix_code instruction style. inline (default) prints every step each tick; playbook folds commit and push into the Fix-code loop playbook."),
|
|
37
37
|
pollMode: z
|
|
38
38
|
.enum(["auto", "poll", "event"])
|
|
39
39
|
.optional()
|
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { ReviewThread, PrComment, TriagedCheck, ClassifiedCheck, AgentThread, AgentComment, AgentCheck, AgentStalledCheck } from "../types.mts";
|
|
12
12
|
export declare function toAgentThread(t: ReviewThread): AgentThread;
|
|
13
|
-
export declare function toAgentComment(c: PrComment
|
|
14
|
-
edited?: boolean;
|
|
15
|
-
}): AgentComment;
|
|
13
|
+
export declare function toAgentComment(c: PrComment): AgentComment;
|
|
16
14
|
export declare function toAgentCheck(c: TriagedCheck): AgentCheck;
|
|
17
15
|
export declare function toAgentStalledCheck(c: ClassifiedCheck, nowSeconds: number): AgentStalledCheck;
|
|
18
16
|
/**
|
package/bin/reporters/agent.mjs
CHANGED
package/bin/types/escalate.d.mts
CHANGED
|
@@ -31,6 +31,15 @@ export interface EscalateDetails {
|
|
|
31
31
|
threadId: string;
|
|
32
32
|
attempts: number;
|
|
33
33
|
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Set only when `iterate.reviewOutputMaxChars` held review items back on the tick that
|
|
36
|
+
* escalated: `shown` of `total` review items appear here. The held items stay unmarked, so the
|
|
37
|
+
* next run shows them.
|
|
38
|
+
*/
|
|
39
|
+
reviewItems?: {
|
|
40
|
+
shown: number;
|
|
41
|
+
total: number;
|
|
42
|
+
};
|
|
34
43
|
/** Review mutations generated for this tick, retained so an escalation cannot strand them. */
|
|
35
44
|
pendingReviewCommands?: {
|
|
36
45
|
resolveOnlyCommand?: ResolveCommand;
|
package/bin/types/iterate.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentThread, AgentComment, AgentCheck, GlobalOptions, RelevantCheck, ShepherdStatus, FirstLookThread, FirstLookComment, RuleAutoResolveReport } from "./report.mts";
|
|
1
|
+
import type { AgentThread, AgentComment, AgentCheck, GlobalOptions, RelevantCheck, ShepherdStatus, FirstLookThread, FirstLookComment, EditedComment, RuleAutoResolveReport } from "./report.mts";
|
|
2
2
|
import type { ActiveCheck, PrActivitySummary } from "./activity.mts";
|
|
3
3
|
import type { BranchProtection, MergeStateStatus, MergedBasePullRequest, Review, ReviewDecision, ReviewThread, ShepherdMergeStatus } from "./github.mts";
|
|
4
4
|
import type { MergeRequirements } from "./merge-requirements.mts";
|
|
@@ -12,7 +12,10 @@ export interface IterateResultSummary {
|
|
|
12
12
|
passing: number;
|
|
13
13
|
skipped: number;
|
|
14
14
|
filtered: number;
|
|
15
|
+
/** Non-completed checks GitHub reports as `IN_PROGRESS`. */
|
|
15
16
|
inProgress: number;
|
|
17
|
+
/** Non-completed checks GitHub reports as `QUEUED`, `WAITING`, `PENDING`, or `REQUESTED`. */
|
|
18
|
+
queued: number;
|
|
16
19
|
superseded: number;
|
|
17
20
|
}
|
|
18
21
|
export interface IterateResultBase {
|
|
@@ -70,6 +73,11 @@ export interface IterateResultBase {
|
|
|
70
73
|
nextCheck?: NextCheck;
|
|
71
74
|
/** Classification-rule resolve/minimize applied during this fetch. Omitted when nothing happened. */
|
|
72
75
|
ruleAutoResolve?: RuleAutoResolveReport;
|
|
76
|
+
/**
|
|
77
|
+
* PR comments edited since Shepherd last showed them. Edits never block readiness, so they are
|
|
78
|
+
* held during `wait`/`ready` and shown once on the next other action. Omitted when empty.
|
|
79
|
+
*/
|
|
80
|
+
editedComments?: EditedComment[];
|
|
73
81
|
}
|
|
74
82
|
interface IterateResultWait extends IterateResultBase {
|
|
75
83
|
action: "wait";
|
|
@@ -143,6 +151,11 @@ interface FixRebaseAndPush {
|
|
|
143
151
|
firstLookThreads: FirstLookThread[];
|
|
144
152
|
/** First-look comments — previously hidden, surfaced for acknowledgment only. */
|
|
145
153
|
firstLookComments: FirstLookComment[];
|
|
154
|
+
/** Set only when `iterate.reviewOutputMaxChars` held review items back: `shown` of `total`. */
|
|
155
|
+
reviewItems?: {
|
|
156
|
+
shown: number;
|
|
157
|
+
total: number;
|
|
158
|
+
};
|
|
146
159
|
}
|
|
147
160
|
export interface IterateResultFixCode extends IterateResultBase {
|
|
148
161
|
action: "fix_code";
|
|
@@ -176,6 +189,11 @@ export interface IterateCommandOptions extends GlobalOptions {
|
|
|
176
189
|
* only by `runPollCore`; excluded from the public `IterateInput` in api.mts.
|
|
177
190
|
*/
|
|
178
191
|
persistSeen?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Internal. `true` attaches edited comments without writing their markers; the poll loop
|
|
194
|
+
* writes them for the one result it returns. Set only by `runPollCore`.
|
|
195
|
+
*/
|
|
196
|
+
deferEditedCommentMarkers?: boolean;
|
|
179
197
|
/**
|
|
180
198
|
* Internal. `true` allows fingerprint reuse of a previous WAIT-shaped report.
|
|
181
199
|
* Poll sets this only for internal WAIT/MARK_READY continuation ticks — never the
|
package/bin/types/report.d.mts
CHANGED
|
@@ -26,10 +26,14 @@ export interface FirstLookThread extends ReviewThread {
|
|
|
26
26
|
}
|
|
27
27
|
export interface FirstLookComment extends PrComment {
|
|
28
28
|
firstLookStatus: "minimized";
|
|
29
|
-
edited?: boolean;
|
|
30
29
|
}
|
|
31
|
-
export type ActionableComment = PrComment
|
|
32
|
-
|
|
30
|
+
export type ActionableComment = PrComment;
|
|
31
|
+
/**
|
|
32
|
+
* A seen PR comment whose body changed since Shepherd last showed it. An edit is not readiness
|
|
33
|
+
* work: iterate shows it once on the next action that is not `wait` or `ready`.
|
|
34
|
+
*/
|
|
35
|
+
export type EditedComment = PrComment & {
|
|
36
|
+
edited: true;
|
|
33
37
|
};
|
|
34
38
|
export type ShepherdStatus = "MERGED" | "CLOSED" | "READY" | "FAILING" | "PENDING" | "IN_PROGRESS" | "UNRESOLVED_COMMENTS" | "UNKNOWN";
|
|
35
39
|
export interface ShepherdReport {
|
|
@@ -91,6 +95,8 @@ export interface ShepherdReport {
|
|
|
91
95
|
minimizeIds?: string[];
|
|
92
96
|
/** First-look items — minimized comments not yet seen by the agent. */
|
|
93
97
|
firstLook: FirstLookComment[];
|
|
98
|
+
/** Seen PR comments (minimized or not) whose body changed. Not marked seen by check. Omitted when empty. */
|
|
99
|
+
edited?: EditedComment[];
|
|
94
100
|
/** PR comments and review summaries minimized by a suppress+autoResolve rule this fetch. */
|
|
95
101
|
autoMinimized?: AutoMinimizedItem[];
|
|
96
102
|
};
|
|
@@ -103,6 +109,20 @@ export interface ShepherdReport {
|
|
|
103
109
|
editedSummaries: Review[];
|
|
104
110
|
/** APPROVED reviews not yet minimized — opt-in minimize target. */
|
|
105
111
|
approvedReviews: Review[];
|
|
112
|
+
/**
|
|
113
|
+
* Set only when `iterate.reviewOutputMaxChars` held review items back this tick: `shown` of
|
|
114
|
+
* `total` review items appear in this report. Held items are absent from every review list,
|
|
115
|
+
* from the generated mutation IDs, and from the seen markers written this tick.
|
|
116
|
+
*/
|
|
117
|
+
reviewItems?: {
|
|
118
|
+
shown: number;
|
|
119
|
+
total: number;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Internal, iterate-only: parent review IDs of unresolved threads held by the budget. Their
|
|
123
|
+
* summaries are not minimized this tick. Not rendered.
|
|
124
|
+
*/
|
|
125
|
+
heldThreadReviewIds?: string[];
|
|
106
126
|
/** COMMENTED review summary IDs matched by user rules with autoResolve:true — minimized without surfacing to agent. */
|
|
107
127
|
ruleAutoResolveReviewSummaryIds?: string[];
|
|
108
128
|
/** Branch protection rule for the PR's base branch. Null when no rule exists or the base ref is unavailable. */
|
|
@@ -167,7 +187,6 @@ export interface AgentComment {
|
|
|
167
187
|
authorAssociation?: import("./github.mts").CommentAuthorAssociation;
|
|
168
188
|
body: string;
|
|
169
189
|
url: string;
|
|
170
|
-
edited?: boolean;
|
|
171
190
|
}
|
|
172
191
|
/** Check shape emitted to the iterate agent under `fix_code` or check escalation. */
|
|
173
192
|
export interface AgentCheck {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** First `LEAN_LIST_CAP` items plus the count of items left out. */
|
|
2
|
+
export declare function capList<T>(items: readonly T[]): {
|
|
3
|
+
shown: T[];
|
|
4
|
+
hidden: number;
|
|
5
|
+
};
|
|
6
|
+
/** `", +N more"` suffix for a capped inline list; empty when nothing was left out. */
|
|
7
|
+
export declare function moreSuffix(hidden: number): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Lean list cap shared by Markdown lines, lean JSON, and wait logs. */
|
|
2
|
+
const LEAN_LIST_CAP = 5;
|
|
3
|
+
/** First `LEAN_LIST_CAP` items plus the count of items left out. */
|
|
4
|
+
export function capList(items) {
|
|
5
|
+
return {
|
|
6
|
+
shown: items.slice(0, LEAN_LIST_CAP),
|
|
7
|
+
hidden: Math.max(0, items.length - LEAN_LIST_CAP),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** `", +N more"` suffix for a capped inline list; empty when nothing was left out. */
|
|
11
|
+
export function moreSuffix(hidden) {
|
|
12
|
+
return hidden > 0 ? `, +${hidden} more` : "";
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"husky": "^9.1.7",
|
|
91
91
|
"knip": "^6.14.1",
|
|
92
92
|
"marked": "^18.0.11",
|
|
93
|
-
"oxfmt": "^0.
|
|
93
|
+
"oxfmt": "^0.72.0",
|
|
94
94
|
"oxlint": "^1.60.0",
|
|
95
95
|
"typescript": "^7.0.2",
|
|
96
96
|
"vitest": "^5.0.0"
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# Fix-code loop
|
|
2
2
|
|
|
3
|
-
Apply when a step says `Playbook: "Fix-code loop"`. It replaces the commit
|
|
3
|
+
Apply when a step says `Playbook: "Fix-code loop"`. It replaces the commit and push procedure that `--instructions=inline` prints on every `[FIX_CODE]` tick. Run the numbered steps in `## Instructions` in order; this is the one that names it.
|
|
4
4
|
|
|
5
5
|
- Review and fix first. Decide per item whether it needs a code change; the reply and resolve mutations are generated either way.
|
|
6
6
|
- Commit and push. If you changed any files (code, docs, or config), commit them and push to the PR head branch. If you did not, do not commit. The repository's pre-push hooks and `AGENTS.md` own lint, typecheck, and format, and its conventions own rebase style and push flags.
|
|
7
|
-
- Journal. Only when the step that names this playbook prints an `apply journal` command: for any substantial decision or rejection, run that command with a `- <decision>` item. Link threads and comments by heading URL and cite reviews by ID. Journal before the review mutations. When no `apply journal` command is printed, journaling is unavailable on this tick; do not build or run one.
|
|
8
7
|
- The `apply review` step stays inline because it needs `$DISMISS_MESSAGE` substitution. Push first: its `--require-sha HEAD` must read the pushed SHA.
|
|
9
8
|
- `[FIX_CODE]` is non-terminal. After the last step, iterate again immediately with the same options. Only `[ESCALATE]` hands work to a human, and `[CANCEL]` ends polling.
|
|
10
9
|
- Playbooks named by other steps (`Playbook: "<name>"`) ship with the package. Without the skill, print one with `pr-shepherd playbook "<name>"`, or the MCP `playbook` tool.
|