pr-shepherd 0.29.0 → 0.30.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.
@@ -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.29.0",
4
+ "version": "0.30.1",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -181,6 +181,7 @@ checks:
181
181
  - pull_request_target
182
182
  - merge_group
183
183
  actions:
184
+ autoMinimizeSuppressed: true
184
185
  autoMarkReady: false
185
186
  ```
186
187
 
@@ -208,7 +209,7 @@ const rule: ClassifyRule = (item) => {
208
209
  export default rule;
209
210
  ```
210
211
 
211
- `suppress: true` hides the item from agent output. `autoResolve: true` queues it for the minimize/resolve mutation. Both can apply together.
212
+ `suppress: true` hides the item from agent output. `autoResolve: true` queues it for the minimize/resolve mutation. When both apply together, Shepherd performs that mutation silently during `iterate` by default (`actions.autoMinimizeSuppressed: true`) so repetitive bot noise does not create a `fix_code` handoff.
212
213
 
213
214
  TypeScript rules are loaded by the runtime's native TypeScript support; keep them to erasable syntax such as type annotations and `import type`. Runtime TypeScript features that need transpilation, such as enums, namespaces, parameter properties, and decorators, are not supported. Use `.mts` for portable ESM rules across Node, Bun, and Deno.
214
215
 
@@ -1,5 +1,11 @@
1
- import { rest } from "../github/http.mjs";
1
+ /* eslint-disable max-lines */
2
+ import { rest, restText } from "../github/http.mjs";
2
3
  const STARTUP_FAILURE_STATUS = "startup_failure";
4
+ const LOG_EXCERPT_CONTEXT_LINES = 16;
5
+ const LOG_EXCERPT_TAIL_LINES = 28;
6
+ const LOG_EXCERPT_MAX_CHARS = 4_000;
7
+ const TRUNCATED_SUFFIX = "\n[truncated]";
8
+ const ANSI_SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
3
9
  export function triageFailingChecks(failingChecks, repo) {
4
10
  const jobsCache = new Map();
5
11
  return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
@@ -12,11 +18,13 @@ async function triageCheck(check, repo, jobsCache) {
12
18
  }
13
19
  const jobs = await fetchJobs(check.runId, repo, jobsCache);
14
20
  const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
21
+ const logExcerpt = jobInfo?.jobId ? await fetchJobLogExcerpt(jobInfo.jobId, repo) : undefined;
15
22
  return {
16
23
  ...check,
17
24
  ...(jobInfo?.workflowName !== undefined && { workflowName: jobInfo.workflowName }),
18
25
  ...(jobInfo?.jobName !== undefined && { jobName: jobInfo.jobName }),
19
26
  ...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
27
+ ...(logExcerpt !== undefined && { logExcerpt }),
20
28
  };
21
29
  }
22
30
  export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
@@ -107,8 +115,122 @@ function pickJobInfo(jobs, checkName) {
107
115
  s.conclusion !== "skipped" &&
108
116
  s.conclusion !== "neutral")?.name;
109
117
  return {
118
+ ...(job.id !== undefined && { jobId: job.id }),
110
119
  workflowName: job.workflow_name,
111
120
  jobName: job.name,
112
121
  failedStep,
113
122
  };
114
123
  }
124
+ async function fetchJobLogExcerpt(jobId, repo) {
125
+ const { owner, name } = repo;
126
+ try {
127
+ return buildLogExcerpt(await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`));
128
+ }
129
+ catch {
130
+ return undefined;
131
+ }
132
+ }
133
+ function buildLogExcerpt(raw) {
134
+ const lines = raw
135
+ .split(/\r?\n/)
136
+ .map(cleanLogLine)
137
+ .filter((line) => line.trim() !== "");
138
+ if (lines.length === 0)
139
+ return undefined;
140
+ const aggregateExcerpt = buildAggregateJobResultsExcerpt(lines);
141
+ if (aggregateExcerpt !== undefined)
142
+ return aggregateExcerpt;
143
+ const errorIndex = findLogExcerptAnchor(lines);
144
+ if (errorIndex === -1)
145
+ return truncateLogExcerpt(lines.slice(-LOG_EXCERPT_TAIL_LINES).join("\n"));
146
+ const start = Math.max(0, errorIndex - LOG_EXCERPT_CONTEXT_LINES);
147
+ const excerpt = lines.slice(start, Math.min(lines.length, errorIndex + LOG_EXCERPT_CONTEXT_LINES + 1));
148
+ return truncateAnchoredExcerpt(excerpt, errorIndex - start);
149
+ }
150
+ function findLogExcerptAnchor(lines) {
151
+ const explicitError = lines.findIndex((line) => line.includes("##[error]"));
152
+ if (explicitError !== -1)
153
+ return explicitError;
154
+ return lines.findIndex((line) => /\b(error|failed|cancelled)\b/i.test(line));
155
+ }
156
+ function buildAggregateJobResultsExcerpt(lines) {
157
+ const jobResults = extractJobResults(lines);
158
+ if (jobResults === undefined)
159
+ return undefined;
160
+ const failed = Object.entries(jobResults)
161
+ .map(([name, value]) => ({ name, result: extractJobResult(value) }))
162
+ .filter((entry) => entry.result !== undefined && !["success", "skipped"].includes(entry.result));
163
+ if (failed.length === 0)
164
+ return undefined;
165
+ const output = [
166
+ ...lines.filter((line) => /required jobs failed|exit code \d+/i.test(line)),
167
+ "Job results (non-success):",
168
+ ...failed.map((entry) => `${entry.name}: ${entry.result}`),
169
+ ];
170
+ return truncateLogExcerpt(output.join("\n"));
171
+ }
172
+ function extractJobResults(lines) {
173
+ const startIndex = lines.findIndex((line) => line.includes("Job results:"));
174
+ if (startIndex === -1)
175
+ return undefined;
176
+ const block = collectJsonBlock(lines, startIndex);
177
+ if (block === undefined)
178
+ return undefined;
179
+ try {
180
+ const parsed = JSON.parse(block);
181
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
182
+ ? parsed
183
+ : undefined;
184
+ }
185
+ catch {
186
+ return undefined;
187
+ }
188
+ }
189
+ function collectJsonBlock(lines, startIndex) {
190
+ const startLine = lines[startIndex] ?? "";
191
+ const objectStart = startLine.indexOf("{");
192
+ if (objectStart === -1)
193
+ return undefined;
194
+ const collected = [startLine.slice(objectStart)];
195
+ let depth = braceDepth(collected[0]);
196
+ for (let i = startIndex + 1; i < lines.length && depth > 0; i++) {
197
+ const line = lines[i] ?? "";
198
+ collected.push(line);
199
+ depth += braceDepth(line);
200
+ }
201
+ return depth === 0 ? collected.join("\n") : undefined;
202
+ }
203
+ function braceDepth(line) {
204
+ return [...line].reduce((depth, ch) => {
205
+ if (ch === "{")
206
+ return depth + 1;
207
+ if (ch === "}")
208
+ return depth - 1;
209
+ return depth;
210
+ }, 0);
211
+ }
212
+ function extractJobResult(value) {
213
+ if (value === null || typeof value !== "object" || Array.isArray(value))
214
+ return undefined;
215
+ const result = value.result;
216
+ return typeof result === "string" ? result : undefined;
217
+ }
218
+ function truncateLogExcerpt(text) {
219
+ if (text.length <= LOG_EXCERPT_MAX_CHARS)
220
+ return text;
221
+ return `${text.slice(0, LOG_EXCERPT_MAX_CHARS - TRUNCATED_SUFFIX.length).trimEnd()}${TRUNCATED_SUFFIX}`;
222
+ }
223
+ function truncateAnchoredExcerpt(lines, anchorIndex) {
224
+ const text = lines.join("\n");
225
+ if (text.length <= LOG_EXCERPT_MAX_CHARS)
226
+ return text;
227
+ return truncateLogExcerpt(`${TRUNCATED_SUFFIX.trim()}\n${lines.slice(anchorIndex).join("\n")}`);
228
+ }
229
+ function cleanLogLine(line) {
230
+ return line
231
+ .replace(/^\uFEFF/, "")
232
+ .replace(ANSI_SGR_RE, "")
233
+ .replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s*/, "")
234
+ .replace(/##\[(?:group|endgroup)\]/g, "")
235
+ .trimEnd();
236
+ }
@@ -53,6 +53,8 @@ export function formatFixCodeResult(header, result) {
53
53
  lines.push(` > ${ch.failedStep}`);
54
54
  if (ch.summary)
55
55
  lines.push(` > ${ch.summary}`);
56
+ if (ch.logExcerpt)
57
+ lines.push(indentBlockquote(ch.logExcerpt, " "));
56
58
  }
57
59
  return lines.join("\n");
58
60
  });
@@ -141,6 +143,12 @@ function renderCheckAnnotation(a) {
141
143
  lines.push(blockquote(a.rawDetails));
142
144
  return lines.join("\n");
143
145
  }
146
+ function indentBlockquote(body, indent) {
147
+ return blockquote(body)
148
+ .split("\n")
149
+ .map((line) => `${indent}${line}`)
150
+ .join("\n");
151
+ }
144
152
  function renderAnnotationRange(a) {
145
153
  if (a.startLine === null && a.endLine === null)
146
154
  return "?";
@@ -14,6 +14,7 @@ import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs"
14
14
  import { threadTranscriptBody } from "../threads/transcript.mjs";
15
15
  import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
16
16
  import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, } from "../comments/review-visibility.mjs";
17
+ import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
17
18
  import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
18
19
  import { normalizeBotUsernames } from "../comments/authors.mjs";
19
20
  import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
@@ -98,6 +99,7 @@ export async function runCheck(opts) {
98
99
  .map((r) => markSeen(stateKey, r.id, r.body)),
99
100
  ]);
100
101
  await markReviewInlineThreadMarkers(stateKey, batchData.reviewThreads);
102
+ const { threadIds: ruleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, } = await remainingRuleAutoResolveIds(partition, opts.autoMinimizeSuppressed);
101
103
  const changesRequestedReviews = changesRequestedReviewVisibility.visible;
102
104
  const changesRequestedReviewCount = batchData.changesRequestedReviews.filter((r) => !partition.suppressedChangesRequestedIds.has(r.id)).length;
103
105
  const approvedReviews = approvedReviewVisibility.visible;
@@ -132,16 +134,13 @@ export async function runCheck(opts) {
132
134
  autoResolved: [],
133
135
  autoResolveErrors: [],
134
136
  firstLook: threadVisibility.firstLookThreads,
135
- ...(partition.ruleAutoResolveThreadIds.length > 0
136
- ? { ruleAutoResolveIds: partition.ruleAutoResolveThreadIds }
137
+ ...(ruleAutoResolveThreadIds.length > 0
138
+ ? { ruleAutoResolveIds: ruleAutoResolveThreadIds }
137
139
  : undefined),
138
140
  },
139
141
  comments: {
140
142
  actionable: visibleCommentClassification.actionable,
141
- minimizeIds: [
142
- ...visibleCommentClassification.minimizeIds,
143
- ...partition.ruleAutoResolveCommentIds,
144
- ],
143
+ minimizeIds: [...visibleCommentClassification.minimizeIds, ...ruleAutoResolveCommentIds],
145
144
  firstLook: firstLookComments,
146
145
  },
147
146
  changesRequestedReviews,
@@ -149,10 +148,39 @@ export async function runCheck(opts) {
149
148
  firstLookSummaries,
150
149
  editedSummaries,
151
150
  approvedReviews,
152
- ...(partition.ruleAutoResolveReviewSummaryIds.length > 0
153
- ? { ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds }
151
+ ...(ruleAutoResolveReviewSummaryIds.length > 0
152
+ ? { ruleAutoResolveReviewSummaryIds }
154
153
  : undefined),
155
154
  branchProtection: batchData.branchProtection,
156
155
  activity: batchData.activity,
157
156
  };
158
157
  }
158
+ async function remainingRuleAutoResolveIds(partition, autoMinimizeSuppressed = false) {
159
+ const consumedIds = autoMinimizeSuppressed
160
+ ? await selfApplySuppressedRuleAutoResolve(partition)
161
+ : { minimized: new Set(), resolvedThreads: new Set() };
162
+ return {
163
+ threadIds: partition.ruleAutoResolveThreadIds.filter((id) => !consumedIds.resolvedThreads.has(id)),
164
+ commentIds: partition.ruleAutoResolveCommentIds.filter((id) => !consumedIds.minimized.has(id)),
165
+ reviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => !consumedIds.minimized.has(id)),
166
+ };
167
+ }
168
+ async function selfApplySuppressedRuleAutoResolve(partition) {
169
+ const minimizeIds = [
170
+ ...partition.ruleAutoResolveCommentIds.filter((id) => partition.suppressedCommentIds.has(id)),
171
+ ...partition.ruleAutoResolveReviewSummaryIds.filter((id) => partition.suppressedReviewSummaryIds.has(id)),
172
+ ];
173
+ const threadIds = partition.ruleAutoResolveThreadIds.filter((id) => partition.suppressedThreadIds.has(id));
174
+ const [minimized, resolved] = await Promise.all([
175
+ minimizeIds.length > 0
176
+ ? autoMinimizeComments(minimizeIds)
177
+ : Promise.resolve({ minimized: [], errors: [] }),
178
+ threadIds.length > 0
179
+ ? autoResolveThreads(threadIds)
180
+ : Promise.resolve({ resolved: [], errors: [] }),
181
+ ]);
182
+ return {
183
+ minimized: new Set(minimized.minimized),
184
+ resolvedThreads: new Set(resolved.resolved),
185
+ };
186
+ }
@@ -18,7 +18,7 @@ export function buildFailingCheckInstructions(checks) {
18
18
  const hasBare = checks.some((c) => !c.runId && !c.detailsUrl);
19
19
  const parts = [];
20
20
  if (hasRunId) {
21
- parts.push("fetch the log with `gh run view <runId> --log-failed` and decide: rerun with `gh run rerun <runId> --failed` for transient infrastructure failures (network timeout, OOM kill, runner crash), or apply a code fix for real test/build failures");
21
+ parts.push("read any included log excerpt first; fetch the full log with `gh run view <runId> --log-failed` when the excerpt is insufficient; decide whether to rerun with `gh run rerun <runId> --failed` for transient infrastructure failures (network timeout, OOM kill, runner crash), or apply a code fix for real test/build failures; if GitHub omits workflow-evaluation details from API/log output, open the run URL in the GitHub UI");
22
22
  }
23
23
  if (hasCancelled) {
24
24
  parts.push("for `[conclusion: CANCELLED]` entries: rerun with `gh run rerun <runId>` if the cancellation looks unintended (not superseded by a newer push or concurrency-group eviction); otherwise treat as resolved — do NOT confuse with IDs under `## Cancelled runs`");
@@ -7,7 +7,7 @@ import { checkEscalateTriggers, validateBaseBranch, buildEscalateSuggestion, bui
7
7
  import { buildResolveCommand } from "./classify.mjs";
8
8
  import { buildFixInstructions } from "./render.mjs";
9
9
  import { applyStallGuard } from "./stall.mjs";
10
- import { tryCancelRun, buildInProgressRunIds } from "./helpers.mjs";
10
+ import { tryCancelRun, buildAutoCancelRunIds, buildInProgressRunIds } from "./helpers.mjs";
11
11
  import { annotationMarkerBody } from "../check-annotations.mjs";
12
12
  import { threadTranscriptBody } from "../../threads/transcript.mjs";
13
13
  import { isHumanAuthor, isConfiguredBotAuthor } from "../../comments/authors.mjs";
@@ -78,10 +78,8 @@ export async function handleFixCode(ctx) {
78
78
  await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
79
79
  let cancelled = [];
80
80
  if (!opts.noAutoCancelActionable) {
81
- const uniqueRunIds = [
82
- ...new Set(failingChecks.map((c) => c.runId).filter((id) => id !== null)),
83
- ];
84
- const results = await Promise.all(uniqueRunIds.map((id) => tryCancelRun(id, repoOwner, repoName)));
81
+ const runIds = buildAutoCancelRunIds(report);
82
+ const results = await Promise.all(runIds.map((id) => tryCancelRun(id, repoOwner, repoName)));
85
83
  cancelled = results.filter((id) => id !== null);
86
84
  }
87
85
  const cancelledSet = new Set(cancelled);
@@ -100,7 +98,11 @@ export async function handleFixCode(ctx) {
100
98
  hasConflicts ||
101
99
  changesRequestedReviews.length > 0 ||
102
100
  actionableComments.length > 0;
103
- const inProgressRunIds = pushLikely ? buildInProgressRunIds(report, cancelledSet) : [];
101
+ const inProgressRunIds = pushLikely
102
+ ? buildInProgressRunIds(report, cancelledSet, {
103
+ suppressProtectedFreshReruns: false,
104
+ })
105
+ : [];
104
106
  const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
105
107
  const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
106
108
  const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber, botUsernames, ruleAutoResolveThreadIds);
@@ -2,13 +2,7 @@ import { execFile as execFileCb } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { rest } from "../../github/http.mjs";
4
4
  const execFile = promisify(execFileCb);
5
- export function buildInProgressRunIds(report, cancelledSet) {
6
- return [
7
- ...new Set(report.checks.inProgress
8
- .map((c) => c.runId)
9
- .filter((id) => id !== null && !cancelledSet.has(id))),
10
- ];
11
- }
5
+ export { buildAutoCancelRunIds, buildInProgressRunIds } from "./reruns.mjs";
12
6
  export function buildSummary(report) {
13
7
  return {
14
8
  passing: report.checks.passing.length,
@@ -17,12 +11,7 @@ export function buildSummary(report) {
17
11
  inProgress: report.checks.inProgress.length,
18
12
  };
19
13
  }
20
- /**
21
- * Build the full list of CI checks relevant to PR readiness: triggered by a PR
22
- * event (or StatusContext with null event), completed, and not skipped/neutral.
23
- * Includes both passing and failing. Failing entries carry workflowName, jobName,
24
- * failedStep, and summary.
25
- */
14
+ /** Build completed, non-skipped checks relevant to PR readiness. */
26
15
  export function buildRelevantChecks(report) {
27
16
  const excluded = new Set([null, "SKIPPED", "NEUTRAL"]);
28
17
  const passing = report.checks.passing.flatMap((c) => {
@@ -53,6 +42,7 @@ export function buildRelevantChecks(report) {
53
42
  ...(c.jobName !== undefined && { jobName: c.jobName }),
54
43
  ...(c.failedStep !== undefined && { failedStep: c.failedStep }),
55
44
  ...(c.summary !== undefined && { summary: c.summary }),
45
+ ...(c.logExcerpt !== undefined && { logExcerpt: c.logExcerpt }),
56
46
  ...(c.annotations !== undefined && { annotations: c.annotations }),
57
47
  },
58
48
  ];
@@ -23,6 +23,7 @@ export async function runIterate(opts) {
23
23
  const report = await runCheck({
24
24
  ...optsWithPr,
25
25
  autoResolve: config.actions.autoResolveOutdated,
26
+ autoMinimizeSuppressed: config.actions.autoMinimizeSuppressed,
26
27
  });
27
28
  const [repoOwner, repoName] = report.repo.split("/");
28
29
  if (!repoOwner || !repoName) {
@@ -0,0 +1,44 @@
1
+ function matchesRerunCheck(failure, check) {
2
+ if (failure.runId !== null && check.runId !== null) {
3
+ return failure.runId === check.runId && failure.name === check.name;
4
+ }
5
+ return failure.runId === null && check.runId === null && failure.name === check.name;
6
+ }
7
+ function isProtectedByFreshRerun(failure, check) {
8
+ const attemptStartedAt = check.startedAtUnix ?? check.updatedAtUnix ?? check.createdAtUnix;
9
+ return (attemptStartedAt !== undefined &&
10
+ matchesRerunCheck(failure, check) &&
11
+ failure.completedAtUnix !== undefined &&
12
+ attemptStartedAt >= failure.completedAtUnix &&
13
+ (failure.startedAtUnix === undefined || failure.startedAtUnix < attemptStartedAt));
14
+ }
15
+ function hasProtectedFreshRerun(failure, checks) {
16
+ return checks.some((check) => isProtectedByFreshRerun(failure, check));
17
+ }
18
+ function isProtectedFreshRerun(check, failures) {
19
+ const matchingFailures = failures.filter((failure) => matchesRerunCheck(failure, check));
20
+ return (matchingFailures.length > 0 &&
21
+ matchingFailures.every((failure) => isProtectedByFreshRerun(failure, check)));
22
+ }
23
+ function protectedFreshRerunIds(report) {
24
+ return new Set(report.checks.inProgress
25
+ .filter((check) => isProtectedFreshRerun(check, report.checks.failing))
26
+ .map((check) => check.runId)
27
+ .filter((id) => id !== null));
28
+ }
29
+ export function buildAutoCancelRunIds(report) {
30
+ return [
31
+ ...new Set(report.checks.failing
32
+ .filter((check) => !hasProtectedFreshRerun(check, report.checks.inProgress))
33
+ .map((check) => check.runId)
34
+ .filter((id) => id !== null)),
35
+ ];
36
+ }
37
+ export function buildInProgressRunIds(report, cancelledSet, opts = {}) {
38
+ const protectedRunIds = opts.suppressProtectedFreshReruns === false ? new Set() : protectedFreshRerunIds(report);
39
+ return [
40
+ ...new Set(report.checks.inProgress
41
+ .map((check) => check.runId)
42
+ .filter((id) => id !== null && !cancelledSet.has(id) && !protectedRunIds.has(id))),
43
+ ];
44
+ }
@@ -57,6 +57,11 @@ export async function applyResolveOptions(pr, repo, opts) {
57
57
  return result;
58
58
  }
59
59
  export async function autoResolveOutdated(threadIds) {
60
+ return autoResolveThreads(threadIds);
61
+ }
62
+ export async function autoResolveThreads(threadIds) {
63
+ if (threadIds.length === 0)
64
+ return { resolved: [], errors: [] };
60
65
  const result = {
61
66
  repliedThreads: [],
62
67
  resolvedThreads: [],
@@ -67,6 +72,19 @@ export async function autoResolveOutdated(threadIds) {
67
72
  await bulkApply([], threadIds, [], [], "", result);
68
73
  return { resolved: result.resolvedThreads, errors: result.errors };
69
74
  }
75
+ export async function autoMinimizeComments(minimizeIds) {
76
+ if (minimizeIds.length === 0)
77
+ return { minimized: [], errors: [] };
78
+ const result = {
79
+ repliedThreads: [],
80
+ resolvedThreads: [],
81
+ minimizedComments: [],
82
+ dismissedReviews: [],
83
+ errors: [],
84
+ };
85
+ await bulkApply([], [], minimizeIds, [], "", result);
86
+ return { minimized: result.minimizedComments, errors: result.errors };
87
+ }
70
88
  // Keep mutation batches small so rate-limit stops leave a precise pending list.
71
89
  const BULK_CHUNK_SIZE = 10;
72
90
  function buildBulkMutation(replyIds, resolveIds, minimizeIds, dismissIds, dismissMessage) {
package/bin/config.json CHANGED
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "actions": {
39
39
  "autoResolveOutdated": true,
40
+ "autoMinimizeSuppressed": true,
40
41
  "autoMarkReady": true,
41
42
  "commitSuggestions": true
42
43
  }
@@ -84,14 +84,11 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
84
84
  const event = node.checkSuite?.workflowRun?.event ?? null;
85
85
  const runId = extractRunId(node.detailsUrl);
86
86
  const summary = extractCheckRunSummary(node.title, node.summary);
87
- const rawCreatedAt = node.checkSuite
88
- ? (node.checkSuite.workflowRun?.createdAt ?? node.checkSuite.createdAt)
89
- : undefined;
90
- const rawUpdatedAt = node.checkSuite
91
- ? (node.checkSuite.workflowRun?.updatedAt ?? node.checkSuite.updatedAt)
92
- : undefined;
87
+ const rawCreatedAt = node.checkSuite?.workflowRun?.createdAt ?? node.checkSuite?.createdAt;
88
+ const rawUpdatedAt = node.checkSuite?.workflowRun?.updatedAt ?? node.checkSuite?.updatedAt;
93
89
  const createdAtUnix = rawCreatedAt ? parseCreatedAt(rawCreatedAt) : undefined;
94
90
  const startedAtUnix = node.startedAt ? parseCreatedAt(node.startedAt) : undefined;
91
+ const completedAtUnix = node.completedAt ? parseCreatedAt(node.completedAt) : undefined;
95
92
  const updatedAtUnix = rawUpdatedAt ? parseCreatedAt(rawUpdatedAt) : undefined;
96
93
  return [
97
94
  {
@@ -105,6 +102,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
105
102
  runId,
106
103
  ...(createdAtUnix !== undefined && { createdAtUnix }),
107
104
  ...(startedAtUnix !== undefined && { startedAtUnix }),
105
+ ...(completedAtUnix !== undefined && { completedAtUnix }),
108
106
  ...(updatedAtUnix !== undefined && { updatedAtUnix }),
109
107
  ...(summary !== undefined && { summary }),
110
108
  },
@@ -3,6 +3,8 @@ import { graphql } from "./client.mjs";
3
3
  import { CHECK_RUN_ANNOTATIONS_QUERY } from "./queries.mjs";
4
4
  const ANNOTATIONS_PER_PAGE = 100;
5
5
  const MAX_ANNOTATION_PAGES = 10;
6
+ const ANNOTATION_TEXT_MAX_CHARS = 4_000;
7
+ const TRUNCATED_SUFFIX = "\n[truncated]";
6
8
  export async function fetchCheckRunAnnotations(checkRunId) {
7
9
  let cursor = null;
8
10
  const nodes = [];
@@ -49,11 +51,16 @@ function toCheckAnnotation(checkRunId, raw) {
49
51
  }),
50
52
  level: raw.annotationLevel,
51
53
  ...(title !== undefined && { title }),
52
- message: raw.message,
53
- ...(rawDetails !== undefined && { rawDetails }),
54
+ message: truncateAnnotationText(raw.message),
55
+ ...(rawDetails !== undefined && { rawDetails: truncateAnnotationText(rawDetails) }),
54
56
  ...(blobUrl !== undefined && { blobUrl }),
55
57
  };
56
58
  }
59
+ function truncateAnnotationText(text) {
60
+ if (text.length <= ANNOTATION_TEXT_MAX_CHARS)
61
+ return text;
62
+ return `${text.slice(0, ANNOTATION_TEXT_MAX_CHARS - TRUNCATED_SUFFIX.length).trimEnd()}${TRUNCATED_SUFFIX}`;
63
+ }
57
64
  function fallbackId(checkRunId, raw) {
58
65
  const start = raw.location?.start;
59
66
  const end = raw.location?.end;
@@ -192,6 +192,7 @@ query BatchPr(
192
192
  status
193
193
  conclusion
194
194
  detailsUrl
195
+ completedAt
195
196
  startedAt
196
197
  title
197
198
  summary
@@ -59,6 +59,7 @@ export function toAgentCheck(c) {
59
59
  ...(c.jobName !== undefined && { jobName: c.jobName }),
60
60
  ...(c.failedStep !== undefined && { failedStep: c.failedStep }),
61
61
  ...(c.summary !== undefined && { summary: c.summary }),
62
+ ...(c.logExcerpt !== undefined && { logExcerpt: c.logExcerpt }),
62
63
  ...(c.annotations !== undefined && { annotations: c.annotations }),
63
64
  };
64
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.29.0",
3
+ "version": "0.30.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",