pr-shepherd 0.28.0 → 0.30.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 +5 -2
- package/bin/checks/triage.mjs +123 -1
- package/bin/classify/loader.mjs +0 -19
- package/bin/cli/fix-formatter.mjs +8 -0
- package/bin/commands/check.mjs +36 -8
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/index.mjs +1 -0
- package/bin/comments/resolve.mjs +18 -0
- package/bin/config.json +1 -0
- package/bin/github/check-annotations.mjs +9 -2
- package/bin/index.mjs +0 -0
- package/bin/reporters/agent.mjs +1 -0
- package/package.json +3 -4
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/bin/pr-shepherd +0 -2
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,13 +209,15 @@ 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.
|
|
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.
|
|
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.
|
|
212
215
|
|
|
213
216
|
Ready-to-use examples for common patterns are in [`examples/classification/`](examples/classification/).
|
|
214
217
|
|
|
215
218
|
## Requirements
|
|
216
219
|
|
|
217
|
-
- Node.js >= 22.
|
|
220
|
+
- Node.js >= 22.18.0, Bun, or Deno
|
|
218
221
|
- A GitHub token or authenticated `gh` CLI; private repositories require `repo` scope.
|
|
219
222
|
- `git`
|
|
220
223
|
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
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
|
+
}
|
package/bin/classify/loader.mjs
CHANGED
|
@@ -32,22 +32,6 @@ function collectRuleFiles(dir) {
|
|
|
32
32
|
.map((name) => join(dir, name))
|
|
33
33
|
.sort();
|
|
34
34
|
}
|
|
35
|
-
let tsxAttempted = false;
|
|
36
|
-
async function ensureTsxRegistered() {
|
|
37
|
-
if (tsxAttempted)
|
|
38
|
-
return;
|
|
39
|
-
tsxAttempted = true;
|
|
40
|
-
try {
|
|
41
|
-
const { register } = await import("tsx/esm/api");
|
|
42
|
-
register();
|
|
43
|
-
/* c8 ignore start */
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
-
process.stderr.write(`pr-shepherd: failed to register tsx — .ts/.mts classification rules will not load: ${msg}\n`);
|
|
48
|
-
}
|
|
49
|
-
/* c8 ignore stop */
|
|
50
|
-
}
|
|
51
35
|
const ruleCache = new Map();
|
|
52
36
|
export async function loadRules(files) {
|
|
53
37
|
if (files.length === 0)
|
|
@@ -56,9 +40,6 @@ export async function loadRules(files) {
|
|
|
56
40
|
const cached = ruleCache.get(cacheKey);
|
|
57
41
|
if (cached !== undefined)
|
|
58
42
|
return cached;
|
|
59
|
-
const hasTs = files.some((f) => f.endsWith(".ts") || f.endsWith(".mts"));
|
|
60
|
-
if (hasTs)
|
|
61
|
-
await ensureTsxRegistered();
|
|
62
43
|
const rules = [];
|
|
63
44
|
for (const file of files) {
|
|
64
45
|
try {
|
|
@@ -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 "?";
|
package/bin/commands/check.mjs
CHANGED
|
@@ -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
|
-
...(
|
|
136
|
-
? { ruleAutoResolveIds:
|
|
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
|
-
...(
|
|
153
|
-
? { 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`
|
|
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`");
|
|
@@ -53,6 +53,7 @@ export function buildRelevantChecks(report) {
|
|
|
53
53
|
...(c.jobName !== undefined && { jobName: c.jobName }),
|
|
54
54
|
...(c.failedStep !== undefined && { failedStep: c.failedStep }),
|
|
55
55
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
56
|
+
...(c.logExcerpt !== undefined && { logExcerpt: c.logExcerpt }),
|
|
56
57
|
...(c.annotations !== undefined && { annotations: c.annotations }),
|
|
57
58
|
},
|
|
58
59
|
];
|
|
@@ -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) {
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -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
|
@@ -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;
|
package/bin/index.mjs
CHANGED
|
File without changes
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"pr-shepherd": "bin/
|
|
9
|
+
"pr-shepherd": "bin/index.mjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/**",
|
|
@@ -20,11 +20,10 @@
|
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=22.
|
|
23
|
+
"node": ">=22.18.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"picomatch": "^4.0.4",
|
|
27
|
-
"tsx": "^4.22.4",
|
|
28
27
|
"yaml": "^2.7.0"
|
|
29
28
|
},
|
|
30
29
|
"exports": {
|
package/bin/pr-shepherd
DELETED