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.
Files changed (70) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/bin/api.d.mts +1 -1
  4. package/bin/classify/types.d.mts +1 -1
  5. package/bin/cli/edited-comments-format.d.mts +14 -0
  6. package/bin/cli/edited-comments-format.mjs +39 -0
  7. package/bin/cli/fix-formatter.mjs +5 -2
  8. package/bin/cli/iterate-activity-formatter.d.mts +1 -1
  9. package/bin/cli/iterate-activity-formatter.mjs +4 -5
  10. package/bin/cli/iterate-check-names.d.mts +5 -0
  11. package/bin/cli/iterate-check-names.mjs +17 -0
  12. package/bin/cli/iterate-formatter.mjs +15 -19
  13. package/bin/cli/iterate-lean.mjs +19 -11
  14. package/bin/cli/iterate-verbose.mjs +7 -2
  15. package/bin/cli/list-formatters.d.mts +0 -3
  16. package/bin/cli/list-formatters.mjs +1 -5
  17. package/bin/commands/check-review-budget.d.mts +39 -0
  18. package/bin/commands/check-review-budget.mjs +78 -0
  19. package/bin/commands/check-wait-detectors.d.mts +6 -1
  20. package/bin/commands/check-wait-detectors.mjs +2 -1
  21. package/bin/commands/check.d.mts +2 -0
  22. package/bin/commands/check.mjs +46 -27
  23. package/bin/commands/iterate/check-instructions.d.mts +5 -0
  24. package/bin/commands/iterate/check-instructions.mjs +4 -0
  25. package/bin/commands/iterate/classify.d.mts +1 -1
  26. package/bin/commands/iterate/edited-comment-markers.d.mts +3 -0
  27. package/bin/commands/iterate/edited-comment-markers.mjs +10 -0
  28. package/bin/commands/iterate/fix-code.mjs +17 -1
  29. package/bin/commands/iterate/fix-loop-instruction.d.mts +2 -8
  30. package/bin/commands/iterate/fix-loop-instruction.mjs +8 -29
  31. package/bin/commands/iterate/helpers.mjs +9 -5
  32. package/bin/commands/iterate/index.mjs +44 -17
  33. package/bin/commands/iterate/render.d.mts +1 -1
  34. package/bin/commands/iterate/render.mjs +18 -17
  35. package/bin/commands/poll-rate-limit-cancel.mjs +1 -1
  36. package/bin/commands/poll-seen-options.d.mts +8 -0
  37. package/bin/commands/poll-seen-options.mjs +7 -0
  38. package/bin/commands/poll.mjs +7 -5
  39. package/bin/commands/rule-auto-resolve-format.d.mts +0 -12
  40. package/bin/commands/rule-auto-resolve-format.mjs +2 -11
  41. package/bin/commands/rule-auto-resolve.d.mts +0 -5
  42. package/bin/commands/rule-auto-resolve.mjs +1 -42
  43. package/bin/commands/shepherd-journal.d.mts +0 -9
  44. package/bin/commands/shepherd-journal.mjs +0 -15
  45. package/bin/comments/review-output-budget.d.mts +32 -0
  46. package/bin/comments/review-output-budget.mjs +55 -0
  47. package/bin/comments/visible-comments.d.mts +3 -1
  48. package/bin/comments/visible-comments.mjs +10 -5
  49. package/bin/config/load.d.mts +8 -2
  50. package/bin/config/load.mjs +7 -0
  51. package/bin/config.json +2 -1
  52. package/bin/github/poll-summary-fingerprint.mjs +5 -1
  53. package/bin/github/poll-summary-review.mjs +2 -1
  54. package/bin/mcp/server.mjs +1 -1
  55. package/bin/reporters/agent.d.mts +1 -3
  56. package/bin/reporters/agent.mjs +0 -1
  57. package/bin/types/escalate.d.mts +9 -0
  58. package/bin/types/iterate.d.mts +19 -1
  59. package/bin/types/report.d.mts +23 -4
  60. package/bin/util/cap-list.d.mts +7 -0
  61. package/bin/util/cap-list.mjs +13 -0
  62. package/package.json +2 -2
  63. package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
  64. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  65. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  66. package/plugins/pr-shepherd/.mcp.json +1 -1
  67. package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +1 -2
  68. package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +1 -1
  69. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  70. 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
- export function reasonClause(reasons) {
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
- export function ruleAutoResolveBody(counts) {
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;
@@ -13,9 +13,4 @@ export declare function applySuppressedRuleAutoResolve(input: {
13
13
  enabled: boolean;
14
14
  partition: BatchPartition;
15
15
  batch: BatchPrData;
16
- prNumber: number;
17
- repo: {
18
- owner: string;
19
- name: string;
20
- };
21
16
  }): Promise<RuleAutoResolveApplied>;
@@ -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, formatRuleAutoResolveJournalItem, minimizedItem, reasonClause, reasonsForError, resolvedThread, ruleAutoResolveBody, uniqueReasons, } from "./rule-auto-resolve-format.mjs";
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
- const comment = cls === "edited" ? { ...c, edited: true } : c;
21
- actionable.push(comment);
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
  }
@@ -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 and journal steps of `fix_code` instructions
85
- * every tick; `playbook` folds them into one `Playbook: "Fix-code loop"` pointer. The
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;
@@ -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
@@ -23,7 +23,8 @@
23
23
  "minimizeComments": "all",
24
24
  "behindBaseHint": "",
25
25
  "resolveOtherHumanThreads": "none",
26
- "instructions": "inline"
26
+ "instructions": "inline",
27
+ "reviewOutputMaxChars": 50000
27
28
  },
28
29
  "poll": {
29
30
  "intervalSeconds": 60,
@@ -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: hideBodies(raw.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
- classifyItem(comment.id, comment.body, seen) !== "unchanged")
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]] : []));
@@ -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, push and journal into the Fix-code loop playbook."),
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
  /**
@@ -51,7 +51,6 @@ export function toAgentComment(c) {
51
51
  ...(c.authorAssociation !== undefined && { authorAssociation: c.authorAssociation }),
52
52
  body: c.body,
53
53
  url: c.url,
54
- ...(c.edited === true && { edited: true }),
55
54
  };
56
55
  }
57
56
  export function toAgentCheck(c) {
@@ -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;
@@ -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
@@ -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
- edited?: boolean;
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.59.2",
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.71.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,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
- "version": "0.59.2",
4
+ "version": "0.60.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.59.2",
3
+ "version": "0.60.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pr-shepherd": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "pr-shepherd@0.59.2", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.60.0", "pr-shepherd-mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pr-shepherd": {
3
3
  "command": "npx",
4
- "args": ["--yes", "--package", "pr-shepherd@0.59.2", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.60.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }
@@ -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, push, and journal 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.
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.