pr-shepherd 0.59.3 → 0.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/api.d.mts +1 -1
- package/bin/classify/standard/codecov-report.d.mts +1 -1
- package/bin/classify/standard/codecov-report.mjs +7 -2
- package/bin/classify/standard/coderabbit-notices.d.mts +4 -3
- package/bin/classify/standard/coderabbit-notices.mjs +29 -47
- package/bin/classify/standard/codex-notices.mjs +34 -7
- package/bin/classify/standard/dependabot-notices.mjs +5 -1
- package/bin/classify/standard/gemini-notices.d.mts +1 -1
- package/bin/classify/standard/gemini-notices.mjs +16 -5
- package/bin/classify/standard/kilo-no-issues.d.mts +1 -1
- package/bin/classify/standard/kilo-no-issues.mjs +37 -10
- package/bin/classify/standard/sourcery-notices.mjs +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 -25
- 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/helpers.mjs +9 -5
- package/bin/commands/iterate/index.mjs +44 -17
- package/bin/commands/iterate/render.mjs +1 -4
- 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/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 +6 -0
- 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/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/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { adaptIterateLog, buildSimpleIterateInstructions } from "./iterate-instructions.mjs";
|
|
2
2
|
import { projectRuleAutoResolve } from "../commands/rule-auto-resolve-format.mjs";
|
|
3
|
+
import { projectEditedComments } from "./edited-comments-format.mjs";
|
|
3
4
|
import { projectMergeRequirements } from "../merge-status/requirements-format.mjs";
|
|
4
5
|
import { branchSegmentShowsBase } from "./iterate-branch-segment.mjs";
|
|
5
6
|
import { isRedundantMergeQueue } from "./iterate-merge-formatter.mjs";
|
|
7
|
+
import { capList } from "../util/cap-list.mjs";
|
|
6
8
|
/**
|
|
7
9
|
* Project an IterateResult to a lean JSON shape for the default (non-verbose) output.
|
|
8
10
|
* Omits fields that are the trivial default (false, 0, empty) or state-gated fields
|
|
@@ -31,7 +33,7 @@ export function projectIterateLean(result, opts) {
|
|
|
31
33
|
}),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
|
-
const counts = Object.fromEntries(["passing", "skipped", "filtered", "inProgress", "superseded"]
|
|
36
|
+
const counts = Object.fromEntries(["passing", "skipped", "filtered", "inProgress", "queued", "superseded"]
|
|
35
37
|
.filter((k) => result.summary[k] > 0)
|
|
36
38
|
.map((k) => [k, result.summary[k]]));
|
|
37
39
|
const summary = Object.keys(counts).length > 0 ? counts : null;
|
|
@@ -82,15 +84,9 @@ export function projectIterateLean(result, opts) {
|
|
|
82
84
|
}),
|
|
83
85
|
},
|
|
84
86
|
}),
|
|
85
|
-
...(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
...((result.ignoredNames?.length ?? 0) > 0 && {
|
|
89
|
-
ignoredNames: result.ignoredNames,
|
|
90
|
-
}),
|
|
91
|
-
...((result.supersededNames?.length ?? 0) > 0 && {
|
|
92
|
-
supersededNames: result.supersededNames,
|
|
93
|
-
}),
|
|
87
|
+
...cappedList("inProgressChecks", result.inProgressChecks),
|
|
88
|
+
...cappedList("ignoredNames", result.ignoredNames),
|
|
89
|
+
...cappedList("supersededNames", result.supersededNames),
|
|
94
90
|
...((result.unreportedRequiredChecks?.length ?? 0) > 0 && {
|
|
95
91
|
unreportedRequiredChecks: result.unreportedRequiredChecks,
|
|
96
92
|
}),
|
|
@@ -104,6 +100,9 @@ export function projectIterateLean(result, opts) {
|
|
|
104
100
|
...(result.ruleAutoResolve && {
|
|
105
101
|
ruleAutoResolve: projectRuleAutoResolve(result.ruleAutoResolve),
|
|
106
102
|
}),
|
|
103
|
+
...(result.editedComments?.length && {
|
|
104
|
+
editedComments: projectEditedComments(result.editedComments),
|
|
105
|
+
}),
|
|
107
106
|
};
|
|
108
107
|
switch (result.action) {
|
|
109
108
|
case "ready":
|
|
@@ -144,7 +143,7 @@ export function projectIterateLean(result, opts) {
|
|
|
144
143
|
case "fix_code":
|
|
145
144
|
return {
|
|
146
145
|
...base,
|
|
147
|
-
|
|
146
|
+
// Passing checks stay summary counts and failures stay in `fix.checks`, as in lean text.
|
|
148
147
|
...(result.cancelled.length > 0 && { cancelled: result.cancelled }),
|
|
149
148
|
fix: {
|
|
150
149
|
...(result.fix.threads.length > 0 && { threads: result.fix.threads }),
|
|
@@ -190,6 +189,7 @@ export function projectIterateLean(result, opts) {
|
|
|
190
189
|
...(result.fix.resolveOnlyCommand && {
|
|
191
190
|
resolveOnlyCommand: result.fix.resolveOnlyCommand,
|
|
192
191
|
}),
|
|
192
|
+
...(result.fix.reviewItems && { reviewItems: result.fix.reviewItems }),
|
|
193
193
|
...(result.fix.instructions.length > 0 && {
|
|
194
194
|
instructions: result.fix.instructions,
|
|
195
195
|
}),
|
|
@@ -229,6 +229,7 @@ export function projectIterateLean(result, opts) {
|
|
|
229
229
|
result.escalate.thrashHistory.length > 0 && {
|
|
230
230
|
thrashHistory: result.escalate.thrashHistory,
|
|
231
231
|
}),
|
|
232
|
+
...(result.escalate.reviewItems && { reviewItems: result.escalate.reviewItems }),
|
|
232
233
|
...(result.escalate.mergeQueueRemoval && {
|
|
233
234
|
mergeQueueRemoval: result.escalate.mergeQueueRemoval,
|
|
234
235
|
}),
|
|
@@ -246,4 +247,11 @@ export function projectIterateLean(result, opts) {
|
|
|
246
247
|
};
|
|
247
248
|
}
|
|
248
249
|
}
|
|
250
|
+
/** `{ [key]: first 5, [key + "More"]: hidden count }`, matching the capped Markdown lists. */
|
|
251
|
+
function cappedList(key, items) {
|
|
252
|
+
if (!items || items.length === 0)
|
|
253
|
+
return {};
|
|
254
|
+
const { shown, hidden } = capList(items);
|
|
255
|
+
return { [key]: shown, ...(hidden > 0 && { [`${key}More`]: hidden }) };
|
|
256
|
+
}
|
|
249
257
|
export { projectIterateVerbose } from "./iterate-verbose.mjs";
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { adaptIterateLog, buildSimpleIterateInstructions } from "./iterate-instructions.mjs";
|
|
2
|
+
import { projectEditedComments } from "./edited-comments-format.mjs";
|
|
2
3
|
export function projectIterateVerbose(result, opts) {
|
|
3
4
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
4
5
|
const readyDelayOverride = readyDelaySuffix ? { readyDelayOverride: readyDelaySuffix } : {};
|
|
6
|
+
// Edited comments use the lean projection: their bookkeeping fields have no Markdown form.
|
|
7
|
+
const edited = result.editedComments && {
|
|
8
|
+
editedComments: projectEditedComments(result.editedComments),
|
|
9
|
+
};
|
|
5
10
|
if (result.action === "fix_code")
|
|
6
|
-
return { ...result, ...readyDelayOverride };
|
|
11
|
+
return { ...result, ...readyDelayOverride, ...edited };
|
|
7
12
|
const log = "log" in result && typeof result.log === "string" ? { log: adaptIterateLog(result.log) } : {};
|
|
8
13
|
// `cancel` has no instructions, so verbose JSON omits the field as verbose Markdown omits the section.
|
|
9
14
|
const instructions = result.action === "cancel" ? {} : { instructions: buildSimpleIterateInstructions(result) };
|
|
10
|
-
return { ...result, ...log, ...readyDelayOverride, ...instructions };
|
|
15
|
+
return { ...result, ...log, ...readyDelayOverride, ...instructions, ...edited };
|
|
11
16
|
}
|
|
@@ -49,9 +49,6 @@ export declare function renderCommentBullet(c: {
|
|
|
49
49
|
}, opts?: {
|
|
50
50
|
statusTag?: string;
|
|
51
51
|
}): string;
|
|
52
|
-
export declare function renderEditedCommentTag(c: {
|
|
53
|
-
edited?: boolean;
|
|
54
|
-
}): string | undefined;
|
|
55
52
|
export declare function renderReviewBullet(r: {
|
|
56
53
|
id: string;
|
|
57
54
|
author: string;
|
|
@@ -101,9 +101,6 @@ export function renderCommentBullet(c, opts = {}) {
|
|
|
101
101
|
const statusSuffix = opts.statusTag ? ` ${opts.statusTag}` : "";
|
|
102
102
|
return `- \`commentId=${c.id}\`${link} (${renderAuthor(c.author, c.authorType, c.authorAssociation)})${statusSuffix}: ${renderBodyPreview(c.body)}`;
|
|
103
103
|
}
|
|
104
|
-
export function renderEditedCommentTag(c) {
|
|
105
|
-
return c.edited ? "[edited since first look]" : undefined;
|
|
106
|
-
}
|
|
107
104
|
export function renderReviewBullet(r, opts = {}) {
|
|
108
105
|
const base = `- \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType, r.authorAssociation)})`;
|
|
109
106
|
const staleTag = r.staleReview
|
|
@@ -138,8 +135,7 @@ export function buildFirstLookBullets(firstLookThreads, resolutionOnlyIds, first
|
|
|
138
135
|
}));
|
|
139
136
|
}
|
|
140
137
|
for (const c of firstLookComments) {
|
|
141
|
-
|
|
142
|
-
bullets.push(renderCommentBullet(c, { statusTag: `[status: minimized${editedSuffix}]` }));
|
|
138
|
+
bullets.push(renderCommentBullet(c, { statusTag: "[status: minimized]" }));
|
|
143
139
|
}
|
|
144
140
|
return bullets;
|
|
145
141
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReviewItemRollup } from "../comments/review-output-budget.mts";
|
|
2
|
+
import type { ActionableComment, EditedComment, FirstLookComment, FirstLookThread, Review, ReviewThread } from "../types.mts";
|
|
3
|
+
/** Every check.mts list that renders a review item, generates a mutation for it, or marks it seen. */
|
|
4
|
+
export interface ReviewOutputBuckets {
|
|
5
|
+
changesRequested: {
|
|
6
|
+
visible: Review[];
|
|
7
|
+
toMarkSeen: Review[];
|
|
8
|
+
};
|
|
9
|
+
threads: {
|
|
10
|
+
activeThreads: ReviewThread[];
|
|
11
|
+
resolutionOnlyThreads: ReviewThread[];
|
|
12
|
+
firstLookThreads: FirstLookThread[];
|
|
13
|
+
toMarkSeen: ReviewThread[];
|
|
14
|
+
};
|
|
15
|
+
comments: {
|
|
16
|
+
actionable: ActionableComment[];
|
|
17
|
+
minimizeIds: string[];
|
|
18
|
+
toMarkSeen: ActionableComment[];
|
|
19
|
+
};
|
|
20
|
+
firstLookComments: FirstLookComment[];
|
|
21
|
+
firstLookSummaries: Review[];
|
|
22
|
+
editedSummaries: Review[];
|
|
23
|
+
editedComments: EditedComment[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Applies `iterate.reviewOutputMaxChars` to one tick's review buckets. Priority: changes-requested
|
|
27
|
+
* reviews, active threads and comments, first-look items, review summaries, edited comments.
|
|
28
|
+
* Approved reviews are opt-in minimize targets, not review work, and are never held.
|
|
29
|
+
* Held items leave every list — the report, the generated mutation IDs, and the seen-marker
|
|
30
|
+
* writes — so the next tick shows them. `rollup` is set only when something was held.
|
|
31
|
+
* Edited comments are held only alongside other held work: edits never block readiness, so a
|
|
32
|
+
* ready or terminal tick must not leave some of them unshown. `heldThreadReviewIds` names the
|
|
33
|
+
* parent reviews of held unresolved threads, which must not be minimized while those wait.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyReviewOutputBudget(buckets: ReviewOutputBuckets, maxChars: number): {
|
|
36
|
+
buckets: ReviewOutputBuckets;
|
|
37
|
+
rollup?: ReviewItemRollup;
|
|
38
|
+
heldThreadReviewIds?: string[];
|
|
39
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { selectReviewOutput, } from "../comments/review-output-budget.mjs";
|
|
2
|
+
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
3
|
+
const bodyItem = (item) => ({
|
|
4
|
+
id: item.id,
|
|
5
|
+
cost: item.body.length,
|
|
6
|
+
...(item.createdAtUnix !== undefined && { createdAtUnix: item.createdAtUnix }),
|
|
7
|
+
});
|
|
8
|
+
const threadItem = (thread) => ({
|
|
9
|
+
...bodyItem(thread),
|
|
10
|
+
cost: threadTranscriptBody(thread).length,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Applies `iterate.reviewOutputMaxChars` to one tick's review buckets. Priority: changes-requested
|
|
14
|
+
* reviews, active threads and comments, first-look items, review summaries, edited comments.
|
|
15
|
+
* Approved reviews are opt-in minimize targets, not review work, and are never held.
|
|
16
|
+
* Held items leave every list — the report, the generated mutation IDs, and the seen-marker
|
|
17
|
+
* writes — so the next tick shows them. `rollup` is set only when something was held.
|
|
18
|
+
* Edited comments are held only alongside other held work: edits never block readiness, so a
|
|
19
|
+
* ready or terminal tick must not leave some of them unshown. `heldThreadReviewIds` names the
|
|
20
|
+
* parent reviews of held unresolved threads, which must not be minimized while those wait.
|
|
21
|
+
*/
|
|
22
|
+
export function applyReviewOutputBudget(buckets, maxChars) {
|
|
23
|
+
const { changesRequested, threads, comments } = buckets;
|
|
24
|
+
const selection = selectReviewOutput([
|
|
25
|
+
changesRequested.visible.map(bodyItem),
|
|
26
|
+
[
|
|
27
|
+
...threads.activeThreads.map(threadItem),
|
|
28
|
+
...threads.resolutionOnlyThreads.map(threadItem),
|
|
29
|
+
...comments.actionable.map(bodyItem),
|
|
30
|
+
],
|
|
31
|
+
[...threads.firstLookThreads.map(threadItem), ...buckets.firstLookComments.map(bodyItem)],
|
|
32
|
+
[...buckets.firstLookSummaries.map(bodyItem), ...buckets.editedSummaries.map(bodyItem)],
|
|
33
|
+
buckets.editedComments.map(bodyItem),
|
|
34
|
+
], maxChars);
|
|
35
|
+
if (selection.rollup === undefined)
|
|
36
|
+
return { buckets };
|
|
37
|
+
const nonEditedIds = [
|
|
38
|
+
...changesRequested.visible,
|
|
39
|
+
...threads.activeThreads,
|
|
40
|
+
...threads.resolutionOnlyThreads,
|
|
41
|
+
...comments.actionable,
|
|
42
|
+
...threads.firstLookThreads,
|
|
43
|
+
...buckets.firstLookComments,
|
|
44
|
+
...buckets.firstLookSummaries,
|
|
45
|
+
...buckets.editedSummaries,
|
|
46
|
+
].map((item) => item.id);
|
|
47
|
+
if (nonEditedIds.every(selection.keep))
|
|
48
|
+
return { buckets };
|
|
49
|
+
const heldThreadReviewIds = [...threads.activeThreads, ...threads.resolutionOnlyThreads]
|
|
50
|
+
.filter((thread) => !selection.keep(thread.id))
|
|
51
|
+
.flatMap((thread) => (thread.reviewId !== undefined ? [thread.reviewId] : []));
|
|
52
|
+
const keep = (items) => items.filter((item) => selection.keep(item.id));
|
|
53
|
+
return {
|
|
54
|
+
buckets: {
|
|
55
|
+
changesRequested: {
|
|
56
|
+
visible: keep(changesRequested.visible),
|
|
57
|
+
toMarkSeen: keep(changesRequested.toMarkSeen),
|
|
58
|
+
},
|
|
59
|
+
threads: {
|
|
60
|
+
activeThreads: keep(threads.activeThreads),
|
|
61
|
+
resolutionOnlyThreads: keep(threads.resolutionOnlyThreads),
|
|
62
|
+
firstLookThreads: keep(threads.firstLookThreads),
|
|
63
|
+
toMarkSeen: keep(threads.toMarkSeen),
|
|
64
|
+
},
|
|
65
|
+
comments: {
|
|
66
|
+
actionable: keep(comments.actionable),
|
|
67
|
+
minimizeIds: comments.minimizeIds.filter(selection.keep),
|
|
68
|
+
toMarkSeen: keep(comments.toMarkSeen),
|
|
69
|
+
},
|
|
70
|
+
firstLookComments: keep(buckets.firstLookComments),
|
|
71
|
+
firstLookSummaries: keep(buckets.firstLookSummaries),
|
|
72
|
+
editedSummaries: keep(buckets.editedSummaries),
|
|
73
|
+
editedComments: keep(buckets.editedComments),
|
|
74
|
+
},
|
|
75
|
+
rollup: selection.rollup,
|
|
76
|
+
...(heldThreadReviewIds.length > 0 && { heldThreadReviewIds }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -10,7 +10,11 @@ type StateKey = {
|
|
|
10
10
|
/**
|
|
11
11
|
* Outcome of the GraphQL transport's REST change detectors. `reused` replays the stored report
|
|
12
12
|
* without any GraphQL request. Otherwise `digest` is what the detectors read before this tick's
|
|
13
|
-
* GraphQL snapshot, recorded beside the new report by `recordWaitDetectors`.
|
|
13
|
+
* GraphQL snapshot, recorded beside the new report by `recordWaitDetectors`. `changed` is set
|
|
14
|
+
* when this read differs from the digest stored beside the report, or no digest is stored yet
|
|
15
|
+
* (the seeding tick): the stored report may predate what the detectors saw, so the tick must
|
|
16
|
+
* not reuse it through the GraphQL fingerprint either. A same-digest read past
|
|
17
|
+
* `poll.reconcileSeconds` leaves `changed` unset.
|
|
14
18
|
*/
|
|
15
19
|
export type WaitDetectorResult = {
|
|
16
20
|
reused: ShepherdReport;
|
|
@@ -18,6 +22,7 @@ export type WaitDetectorResult = {
|
|
|
18
22
|
} | {
|
|
19
23
|
digest: string;
|
|
20
24
|
readAt: number;
|
|
25
|
+
changed?: true;
|
|
21
26
|
};
|
|
22
27
|
/**
|
|
23
28
|
* On the GraphQL transport, run the REST change detectors for an idle stored report. Returns
|
|
@@ -83,7 +83,8 @@ export async function readWaitDetectors(prNumber, repo, stateKey, config, now =
|
|
|
83
83
|
baseTipOid: observed.baseTipOid,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
const changed = previous?.digest !== state.digest;
|
|
87
|
+
return { digest: state.digest, readAt: now, ...(changed && { changed: true }) };
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
89
90
|
* After a GraphQL tick, store the digest its detectors read beside the report that tick built.
|
package/bin/commands/check.d.mts
CHANGED
|
@@ -7,6 +7,8 @@ type CheckOptions = GlobalOptions & {
|
|
|
7
7
|
persistSeen?: boolean;
|
|
8
8
|
fingerprintCache?: boolean;
|
|
9
9
|
merge?: boolean;
|
|
10
|
+
/** Per-tick review-item body budget (`iterate.reviewOutputMaxChars`). Omitted or 0: no cap. */
|
|
11
|
+
reviewOutputMaxChars?: number;
|
|
10
12
|
};
|
|
11
13
|
export declare function runCheck(opts: CheckOptions, context?: CheckExecutionContext): Promise<ShepherdReport>;
|
|
12
14
|
export {};
|
package/bin/commands/check.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
|
28
28
|
import { classifyThreadVisibility } from "../comments/thread-visibility.mjs";
|
|
29
29
|
import { classifyReviewsForDisplay, classifyChangesRequestedReviewsForDisplay, } from "../comments/review-visibility.mjs";
|
|
30
30
|
import { applySuppressedRuleAutoResolve } from "./rule-auto-resolve.mjs";
|
|
31
|
+
import { applyReviewOutputBudget } from "./check-review-budget.mjs";
|
|
31
32
|
import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers.mjs";
|
|
32
33
|
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
33
34
|
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./iterate/thread-mutation-routing.mjs";
|
|
@@ -70,7 +71,11 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
70
71
|
: undefined;
|
|
71
72
|
if (detectors && "reused" in detectors)
|
|
72
73
|
return refreshReused(detectors.reused, detectors.baseTipOid);
|
|
73
|
-
|
|
74
|
+
// A detector change, or a first seeding read, means the stored report may predate what the
|
|
75
|
+
// detectors saw; an unchanged first-page fingerprint cannot vouch for it (it omits
|
|
76
|
+
// per-check-run fields), so take a full snapshot instead.
|
|
77
|
+
const detectorsChanged = detectors !== undefined && "changed" in detectors;
|
|
78
|
+
const reuse = reuseFingerprint && !detectorsChanged
|
|
74
79
|
? await fingerprintReuser(prNumber, repo, stateKey, config)
|
|
75
80
|
: undefined;
|
|
76
81
|
const batchOptions = {
|
|
@@ -215,13 +220,17 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
215
220
|
threadHasAuthorizedMutation(thread, replyThreadIds, resolveThreadIds))
|
|
216
221
|
.map((thread) => thread.id));
|
|
217
222
|
const threadVisibility = classifyThreadVisibility(visibleThreadCandidates, seenMap, botUsernames, repeatableThreadIds, resolveOtherHumanThreads);
|
|
218
|
-
const firstLookComments = minimizedCommentCandidates
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
const firstLookComments = minimizedCommentCandidates
|
|
224
|
+
.filter((c) => classifyItem(c.id, c.body, seenMap) === "new")
|
|
225
|
+
.map((c) => ({ ...c, firstLookStatus: "minimized" }));
|
|
226
|
+
// Edited PR comments never block readiness. Iterate shows them on the next action that is not
|
|
227
|
+
// wait/ready and writes their markers then, so an edit is never marked seen while hidden.
|
|
228
|
+
const editedComments = [
|
|
229
|
+
...minimizedCommentCandidates
|
|
230
|
+
.filter((c) => classifyItem(c.id, c.body, seenMap) === "edited")
|
|
231
|
+
.map((c) => ({ ...c, edited: true })),
|
|
232
|
+
...visibleCommentClassification.edited,
|
|
233
|
+
];
|
|
225
234
|
const firstLookSummaries = [];
|
|
226
235
|
const editedSummaries = [];
|
|
227
236
|
const seenSummaries = [];
|
|
@@ -251,7 +260,6 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
251
260
|
canGenerateItemMutation(review.id, review.body, batchData.viewerAuthorization?.viewerCanAdminister, "dismiss") ||
|
|
252
261
|
visibleChangesRequestedIds.has(review.id));
|
|
253
262
|
}).length;
|
|
254
|
-
const approvedReviews = approvedReviewVisibility.visible;
|
|
255
263
|
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount, unreported.hasUnreportedRequired);
|
|
256
264
|
status = enforceTransportReadiness(status, batchData);
|
|
257
265
|
// Resolve any pending mergeability refresh (and the resulting MERGED/CLOSED short-circuit)
|
|
@@ -292,6 +300,16 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
292
300
|
filtered: merged.filtered,
|
|
293
301
|
ignored: merged.ignored,
|
|
294
302
|
}, mergeStatus.status);
|
|
303
|
+
// A deferred tick shows no review items, so it is never capped; its seen markers stay unwritten.
|
|
304
|
+
const { buckets: budgeted, rollup: reviewItems, heldThreadReviewIds, } = applyReviewOutputBudget({
|
|
305
|
+
changesRequested: changesRequestedReviewVisibility,
|
|
306
|
+
threads: threadVisibility,
|
|
307
|
+
comments: visibleCommentClassification,
|
|
308
|
+
firstLookComments,
|
|
309
|
+
firstLookSummaries,
|
|
310
|
+
editedSummaries,
|
|
311
|
+
editedComments,
|
|
312
|
+
}, deferWhileQueued ? 0 : (opts.reviewOutputMaxChars ?? 0));
|
|
295
313
|
if (opts.persistSeen !== false) {
|
|
296
314
|
const successfulAnnotations = [
|
|
297
315
|
...merged.passing,
|
|
@@ -304,11 +322,11 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
304
322
|
const deferredMarkSeen = deferWhileQueued
|
|
305
323
|
? []
|
|
306
324
|
: [
|
|
307
|
-
...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
308
|
-
...
|
|
309
|
-
...
|
|
310
|
-
...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
|
|
311
|
-
...
|
|
325
|
+
...budgeted.firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
326
|
+
...budgeted.threads.toMarkSeen.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
327
|
+
...budgeted.comments.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
328
|
+
...[...budgeted.firstLookSummaries, ...budgeted.editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
|
|
329
|
+
...budgeted.changesRequested.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
312
330
|
...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
313
331
|
];
|
|
314
332
|
await Promise.allSettled([
|
|
@@ -351,7 +369,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
351
369
|
partition: authorizedPartition,
|
|
352
370
|
batch: batchData,
|
|
353
371
|
});
|
|
354
|
-
const visibleMutationThreadIds = new Set([...
|
|
372
|
+
const visibleMutationThreadIds = new Set([...budgeted.threads.activeThreads, ...budgeted.threads.resolutionOnlyThreads].map((thread) => thread.id));
|
|
355
373
|
const ruleAutoResolveThreadIds = [
|
|
356
374
|
...authorizedRuleAutoResolveThreadIds,
|
|
357
375
|
...[...deniedRuleAutoResolveThreadIds].filter((id) => visibleMutationThreadIds.has(id)),
|
|
@@ -400,27 +418,30 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
400
418
|
}),
|
|
401
419
|
},
|
|
402
420
|
threads: {
|
|
403
|
-
actionable:
|
|
404
|
-
resolutionOnly:
|
|
421
|
+
actionable: budgeted.threads.activeThreads,
|
|
422
|
+
resolutionOnly: budgeted.threads.resolutionOnlyThreads,
|
|
405
423
|
autoResolved,
|
|
406
424
|
autoResolveErrors,
|
|
407
425
|
...(errorReasons.length > 0 && { autoResolveErrorReasons: errorReasons }),
|
|
408
|
-
firstLook:
|
|
426
|
+
firstLook: budgeted.threads.firstLookThreads,
|
|
409
427
|
...(ruleAutoResolveThreadIds.length > 0
|
|
410
428
|
? { ruleAutoResolveIds: ruleAutoResolveThreadIds }
|
|
411
429
|
: undefined),
|
|
412
430
|
},
|
|
413
431
|
comments: {
|
|
414
|
-
actionable:
|
|
415
|
-
minimizeIds: [...
|
|
416
|
-
firstLook: firstLookComments,
|
|
432
|
+
actionable: budgeted.comments.actionable,
|
|
433
|
+
minimizeIds: [...budgeted.comments.minimizeIds, ...ruleAutoResolveCommentIds],
|
|
434
|
+
firstLook: budgeted.firstLookComments,
|
|
435
|
+
...(budgeted.editedComments.length > 0 && { edited: budgeted.editedComments }),
|
|
417
436
|
...(autoMinimized.length > 0 && { autoMinimized }),
|
|
418
437
|
},
|
|
419
|
-
changesRequestedReviews,
|
|
438
|
+
changesRequestedReviews: budgeted.changesRequested.visible,
|
|
420
439
|
reviewSummaries: seenSummaries,
|
|
421
|
-
firstLookSummaries,
|
|
422
|
-
editedSummaries,
|
|
423
|
-
approvedReviews,
|
|
440
|
+
firstLookSummaries: budgeted.firstLookSummaries,
|
|
441
|
+
editedSummaries: budgeted.editedSummaries,
|
|
442
|
+
approvedReviews: approvedReviewVisibility.visible,
|
|
443
|
+
...(reviewItems && { reviewItems }),
|
|
444
|
+
...(heldThreadReviewIds && { heldThreadReviewIds }),
|
|
424
445
|
...(ruleAutoResolveReviewSummaryIds.length > 0
|
|
425
446
|
? { ruleAutoResolveReviewSummaryIds }
|
|
426
447
|
: undefined),
|
|
@@ -37,5 +37,10 @@ export declare function buildResolveCommandInstruction(resolveCommand: ResolveCo
|
|
|
37
37
|
export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
|
|
38
38
|
/** Update the PR branch after an external blocker merges or closes. Never rerun that job. */
|
|
39
39
|
export declare function buildReleasedBlockerInstruction(prNumber: number): string;
|
|
40
|
+
/** `iterate.reviewOutputMaxChars` held review items back; the rerun step that follows shows them. */
|
|
41
|
+
export declare function buildReviewSubsetInstruction(reviewItems: {
|
|
42
|
+
shown: number;
|
|
43
|
+
total: number;
|
|
44
|
+
}): string;
|
|
40
45
|
/** Recurrence only. Commit, push, rerun, and SHA steps are earlier instructions. */
|
|
41
46
|
export declare function buildFixCompletionInstruction(): string;
|
|
@@ -74,6 +74,10 @@ export function buildReleasedBlockerInstruction(prNumber) {
|
|
|
74
74
|
? "Update this PR branch from its base using the repository's branch-update procedure. Do not rerun the job; a rerun retests the old merge ref."
|
|
75
75
|
: `Update this PR branch from its base with \`gh pr update-branch ${prNumber} --rebase\`. Do not rerun the job; a rerun retests the old merge ref.`;
|
|
76
76
|
}
|
|
77
|
+
/** `iterate.reviewOutputMaxChars` held review items back; the rerun step that follows shows them. */
|
|
78
|
+
export function buildReviewSubsetInstruction(reviewItems) {
|
|
79
|
+
return `This is a subset: ${reviewItems.shown} of ${reviewItems.total} review items are shown, and only their IDs are in the commands above. After handling these items, rerun Shepherd for the next batch; do not fetch the held items yourself.`;
|
|
80
|
+
}
|
|
77
81
|
/** Recurrence only. Commit, push, rerun, and SHA steps are earlier instructions. */
|
|
78
82
|
export function buildFixCompletionInstruction() {
|
|
79
83
|
return FIX_CODE_CONTINUATION;
|
|
@@ -5,7 +5,7 @@ export declare function classifyReviewSummaries(summaries: {
|
|
|
5
5
|
firstLook: Review[];
|
|
6
6
|
seen: Review[];
|
|
7
7
|
edited: Review[];
|
|
8
|
-
}, approvals: Review[], minimizeApprovals: boolean, minimizeComments?: MinimizeCommentsPolicy | undefined, botUsernames?: NormalizedBotUsernames, unresolvedThreads?: ReviewThread
|
|
8
|
+
}, approvals: Review[], minimizeApprovals: boolean, minimizeComments?: MinimizeCommentsPolicy | undefined, botUsernames?: NormalizedBotUsernames, unresolvedThreads?: ReadonlyArray<Pick<ReviewThread, "reviewId">>, ruleAutoResolveIds?: string[]): {
|
|
9
9
|
minimizeIds: string[];
|
|
10
10
|
selfMinimizeIds: string[];
|
|
11
11
|
firstLookSummaries: Review[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { markSeen } from "../../state/seen-comments.mjs";
|
|
2
|
+
/** Records the edited comments a result shows, so each edit is shown once. Returns the result. */
|
|
3
|
+
export async function markEditedCommentsSeen(result) {
|
|
4
|
+
const [owner, repo] = result.repo.split("/");
|
|
5
|
+
if (!result.editedComments?.length || !owner || !repo)
|
|
6
|
+
return result;
|
|
7
|
+
const key = { owner, repo, pr: result.pr };
|
|
8
|
+
await Promise.allSettled(result.editedComments.map((c) => markSeen(key, c.id, c.body)));
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
@@ -9,7 +9,7 @@ import { buildFixInstructions } from "./render.mjs";
|
|
|
9
9
|
import { buildRemovedQueueRecovery, buildStackQueueRemovalAcknowledgment } from "./merge.mjs";
|
|
10
10
|
import { hasLogEvidence } from "./check-evidence.mjs";
|
|
11
11
|
import { currentEjectionCommit, } from "./queue-recovery-instructions.mjs";
|
|
12
|
-
import { buildReleasedBlockerInstruction } from "./check-instructions.mjs";
|
|
12
|
+
import { buildReleasedBlockerInstruction, buildReviewSubsetInstruction, } from "./check-instructions.mjs";
|
|
13
13
|
import { buildNativeStackLayerRebase } from "./native-stack-rebase.mjs";
|
|
14
14
|
import { lookupUpperLayerTrunkConflict } from "./stack-trunk-conflict.mjs";
|
|
15
15
|
import { lookupMergedBasePullRequests } from "./merged-base-pull-requests.mjs";
|
|
@@ -78,6 +78,14 @@ function pendingReviewCommands(resolveCommand, resolveOnlyCommand) {
|
|
|
78
78
|
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
79
79
|
}
|
|
80
80
|
export async function handleFixCode(ctx) {
|
|
81
|
+
const result = await handleFixCodeTick(ctx);
|
|
82
|
+
// Every escalation from a capped tick carries the rollup: the reader sees a subset.
|
|
83
|
+
const { reviewItems } = ctx.report;
|
|
84
|
+
if (result.action !== "escalate" || reviewItems === undefined)
|
|
85
|
+
return result;
|
|
86
|
+
return { ...result, escalate: { ...result.escalate, reviewItems } };
|
|
87
|
+
}
|
|
88
|
+
async function handleFixCodeTick(ctx) {
|
|
81
89
|
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, releasedCheckNames = EMPTY_RELEASED, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
|
|
82
90
|
const prReference = formatPrUrl(report.repo, prNumber);
|
|
83
91
|
const failingChecks = report.checks.failing;
|
|
@@ -221,6 +229,7 @@ export async function handleFixCode(ctx) {
|
|
|
221
229
|
editedSummaries.length > 0 ||
|
|
222
230
|
report.threads.firstLook.length > 0 ||
|
|
223
231
|
report.comments.firstLook.length > 0 ||
|
|
232
|
+
report.reviewItems !== undefined ||
|
|
224
233
|
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
225
234
|
failingAgentChecks.some((check) => releasedCheckNames.has(check.name) ||
|
|
226
235
|
belongsToActiveWorkflowRun(check) ||
|
|
@@ -348,6 +357,12 @@ export async function handleFixCode(ctx) {
|
|
|
348
357
|
if (completion !== undefined)
|
|
349
358
|
instructions.push(completion);
|
|
350
359
|
}
|
|
360
|
+
if (report.reviewItems) {
|
|
361
|
+
const completion = instructions.pop();
|
|
362
|
+
instructions.push(buildReviewSubsetInstruction(report.reviewItems));
|
|
363
|
+
if (completion !== undefined)
|
|
364
|
+
instructions.push(completion);
|
|
365
|
+
}
|
|
351
366
|
if (repairInstructions && repairInstructions.length > 0) {
|
|
352
367
|
instructions.unshift(...repairInstructions);
|
|
353
368
|
}
|
|
@@ -396,6 +411,7 @@ export async function handleFixCode(ctx) {
|
|
|
396
411
|
firstLookComments,
|
|
397
412
|
inProgressRunIds,
|
|
398
413
|
protectedRuns,
|
|
414
|
+
...(report.reviewItems && { reviewItems: report.reviewItems }),
|
|
399
415
|
},
|
|
400
416
|
cancelled,
|
|
401
417
|
};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { blockedReasonFromRequirements } from "../../merge-status/requirements-format.mjs";
|
|
2
|
+
import { capList, moreSuffix } from "../../util/cap-list.mjs";
|
|
3
|
+
/** GitHub statuses for a check that has not started running yet. */
|
|
4
|
+
const QUEUED_STATUSES = new Set(["QUEUED", "WAITING", "PENDING", "REQUESTED"]);
|
|
2
5
|
export function buildSummary(report) {
|
|
6
|
+
const queued = report.checks.inProgress.filter((c) => QUEUED_STATUSES.has(c.status)).length;
|
|
3
7
|
return {
|
|
4
8
|
passing: report.checks.passing.length,
|
|
5
9
|
skipped: report.checks.skipped.length,
|
|
6
10
|
filtered: report.checks.filtered.length,
|
|
7
|
-
inProgress: report.checks.inProgress.length,
|
|
11
|
+
inProgress: report.checks.inProgress.length - queued,
|
|
12
|
+
queued,
|
|
8
13
|
superseded: report.checks.supersededNames?.length ?? 0,
|
|
9
14
|
};
|
|
10
15
|
}
|
|
@@ -111,13 +116,12 @@ export function buildWaitLog(base) {
|
|
|
111
116
|
const counts = [
|
|
112
117
|
...(summary.passing > 0 ? [`${summary.passing} passing`] : []),
|
|
113
118
|
...(summary.inProgress > 0 ? [`${summary.inProgress} in-progress`] : []),
|
|
119
|
+
...(summary.queued > 0 ? [`${summary.queued} queued`] : []),
|
|
114
120
|
];
|
|
115
121
|
const parts = counts.length > 0 ? [counts.join(", ")] : [];
|
|
116
122
|
if ((base.inProgressChecks?.length ?? 0) > 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.map((c) => c.name)
|
|
120
|
-
.join(", ")}`);
|
|
123
|
+
const { shown, hidden } = capList(base.inProgressChecks);
|
|
124
|
+
parts.push(`active checks: ${shown.map((c) => c.name).join(", ")}${moreSuffix(hidden)}`);
|
|
121
125
|
}
|
|
122
126
|
switch (base.mergeStatus) {
|
|
123
127
|
case "BLOCKED": {
|