pr-shepherd 0.59.3 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/api.d.mts +1 -1
- package/bin/cli/edited-comments-format.d.mts +14 -0
- package/bin/cli/edited-comments-format.mjs +39 -0
- package/bin/cli/fix-formatter.mjs +5 -2
- package/bin/cli/iterate-activity-formatter.d.mts +1 -1
- package/bin/cli/iterate-activity-formatter.mjs +4 -5
- package/bin/cli/iterate-check-names.d.mts +5 -0
- package/bin/cli/iterate-check-names.mjs +17 -0
- package/bin/cli/iterate-formatter.mjs +15 -19
- package/bin/cli/iterate-lean.mjs +19 -11
- package/bin/cli/iterate-verbose.mjs +7 -2
- package/bin/cli/list-formatters.d.mts +0 -3
- package/bin/cli/list-formatters.mjs +1 -5
- package/bin/commands/check-review-budget.d.mts +39 -0
- package/bin/commands/check-review-budget.mjs +78 -0
- package/bin/commands/check-wait-detectors.d.mts +6 -1
- package/bin/commands/check-wait-detectors.mjs +2 -1
- package/bin/commands/check.d.mts +2 -0
- package/bin/commands/check.mjs +46 -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/api.d.mts
CHANGED
|
@@ -18,7 +18,7 @@ export interface CreatePrShepherdOptions {
|
|
|
18
18
|
}
|
|
19
19
|
/** A positive PR number, GitHub pull-request URL, or owner/repo#number reference. */
|
|
20
20
|
export type PrReference = number | string;
|
|
21
|
-
type IterateOptions = Omit<IterateCommandOptions, "format" | "prNumber" | "targetRepository" | "persistSeen" | "fingerprintCache" | "stallDeadlineSink" | "deferQuotaWarning" | "quotaWarningMinimumPollIntervalMinutes">;
|
|
21
|
+
type IterateOptions = Omit<IterateCommandOptions, "format" | "prNumber" | "targetRepository" | "persistSeen" | "deferEditedCommentMarkers" | "fingerprintCache" | "stallDeadlineSink" | "deferQuotaWarning" | "quotaWarningMinimumPollIntervalMinutes">;
|
|
22
22
|
export type SingleIterateInput = IterateOptions & {
|
|
23
23
|
pr?: PrReference;
|
|
24
24
|
prs?: never;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EditedComment } from "../types.mts";
|
|
2
|
+
/** Insert the edited-comments section before `## Instructions` (or append it). */
|
|
3
|
+
export declare function insertEditedCommentsSection(text: string, comments: readonly EditedComment[] | undefined, verbose: boolean): string;
|
|
4
|
+
/** JSON projection matching the text section: the agent comment plus its minimized status. */
|
|
5
|
+
export declare function projectEditedComments(comments: readonly EditedComment[]): {
|
|
6
|
+
id: string;
|
|
7
|
+
viewerCanMinimize?: boolean;
|
|
8
|
+
author: string;
|
|
9
|
+
authorType?: import("../types.mts").AuthorType;
|
|
10
|
+
authorAssociation?: import("../types.mts").CommentAuthorAssociation;
|
|
11
|
+
body: string;
|
|
12
|
+
url: string;
|
|
13
|
+
isMinimized?: boolean | undefined;
|
|
14
|
+
}[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { blockquote, renderAuthor } from "./list-formatters.mjs";
|
|
2
|
+
import { BODY_TRUNCATE_MAX_CHARS } from "./body-truncate.mjs";
|
|
3
|
+
import { toAgentComment } from "../reporters/agent.mjs";
|
|
4
|
+
const HEADING = "Edited comments";
|
|
5
|
+
function editedTag(c) {
|
|
6
|
+
return c.isMinimized ? "[status: minimized, edited]" : "[edited since first look]";
|
|
7
|
+
}
|
|
8
|
+
/** Markdown for `editedComments`; null when there are none. Edits are informational only. */
|
|
9
|
+
function formatEditedCommentsSection(comments, verbose) {
|
|
10
|
+
if (!comments?.length)
|
|
11
|
+
return null;
|
|
12
|
+
const cap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
|
|
13
|
+
const parts = [
|
|
14
|
+
`## ${HEADING} (${comments.length}) — informational; edits do not block readiness`,
|
|
15
|
+
];
|
|
16
|
+
for (const c of comments) {
|
|
17
|
+
const heading = c.url ? `[commentId=${c.id}](${c.url})` : `\`commentId=${c.id}\``;
|
|
18
|
+
parts.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)}) ${editedTag(c)}`);
|
|
19
|
+
parts.push(blockquote(c.body, cap, c.url));
|
|
20
|
+
}
|
|
21
|
+
return parts.join("\n\n");
|
|
22
|
+
}
|
|
23
|
+
/** Insert the edited-comments section before `## Instructions` (or append it). */
|
|
24
|
+
export function insertEditedCommentsSection(text, comments, verbose) {
|
|
25
|
+
const section = formatEditedCommentsSection(comments, verbose);
|
|
26
|
+
if (!section)
|
|
27
|
+
return text;
|
|
28
|
+
const index = text.lastIndexOf("## Instructions");
|
|
29
|
+
if (index === -1)
|
|
30
|
+
return `${text}\n\n${section}`;
|
|
31
|
+
return `${text.slice(0, index)}${section}\n\n${text.slice(index)}`;
|
|
32
|
+
}
|
|
33
|
+
/** JSON projection matching the text section: the agent comment plus its minimized status. */
|
|
34
|
+
export function projectEditedComments(comments) {
|
|
35
|
+
return comments.map((c) => ({
|
|
36
|
+
...toAgentComment(c),
|
|
37
|
+
...(c.isMinimized && { isMinimized: true }),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
@@ -13,6 +13,10 @@ export function formatFixCodeResult(header, result, opts = {}) {
|
|
|
13
13
|
const verbose = opts.verbose ?? false;
|
|
14
14
|
const topCap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
|
|
15
15
|
const sections = [header];
|
|
16
|
+
if (result.fix.reviewItems) {
|
|
17
|
+
const { shown, total } = result.fix.reviewItems;
|
|
18
|
+
sections.push(`**showing** ${shown} of ${total} review items`);
|
|
19
|
+
}
|
|
16
20
|
if (result.mergedBasePullRequests?.length) {
|
|
17
21
|
sections.push("## Merged PRs matching the current base");
|
|
18
22
|
sections.push(result.mergedBasePullRequests
|
|
@@ -50,9 +54,8 @@ export function formatFixCodeResult(header, result, opts = {}) {
|
|
|
50
54
|
sections.push("## Actionable comments");
|
|
51
55
|
for (const c of result.fix.actionableComments) {
|
|
52
56
|
const heading = c.url ? `[commentId=${c.id}](${c.url})` : `\`commentId=${c.id}\``;
|
|
53
|
-
const editedMarker = c.edited ? " [edited since first look]" : "";
|
|
54
57
|
const authorizationMarker = c.viewerCanMinimize === false ? " [viewer cannot minimize]" : "";
|
|
55
|
-
sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)})${authorizationMarker}
|
|
58
|
+
sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)})${authorizationMarker}`);
|
|
56
59
|
sections.push(blockquote(c.body, topCap, c.url));
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { IterateResult } from "../types.mts";
|
|
2
|
-
export declare function formatActivityLine(result: IterateResult): string | null;
|
|
2
|
+
export declare function formatActivityLine(result: IterateResult, verbose?: boolean): string | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { formatNameList } from "./iterate-check-names.mjs";
|
|
2
|
+
export function formatActivityLine(result, verbose = false) {
|
|
2
3
|
const activity = result.activity ?? {
|
|
3
4
|
commitCount: 0,
|
|
4
5
|
reviewRoundCount: 0,
|
|
@@ -17,10 +18,8 @@ export function formatActivityLine(result) {
|
|
|
17
18
|
parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
|
|
18
19
|
}
|
|
19
20
|
if (hasActiveChecks) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.map((check) => `\`${check.name}\``)
|
|
23
|
-
.join(", ")}`);
|
|
21
|
+
const names = result.inProgressChecks.map((check) => check.name);
|
|
22
|
+
parts.push(`active: ${formatNameList(names, verbose)}`);
|
|
24
23
|
}
|
|
25
24
|
return `**activity** ${parts.join(" · ")}`;
|
|
26
25
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IterateResult } from "../types.mts";
|
|
2
|
+
/** Backticked inline name list; lean output caps it and appends `+N more`. */
|
|
3
|
+
export declare function formatNameList(names: readonly string[], verbose: boolean): string;
|
|
4
|
+
/** `**ignored**` and `**superseded**` header lines, omitted when their lists are empty. */
|
|
5
|
+
export declare function formatSuppressedNameLines(result: IterateResult, verbose: boolean): string[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { capList, moreSuffix } from "../util/cap-list.mjs";
|
|
2
|
+
/** Backticked inline name list; lean output caps it and appends `+N more`. */
|
|
3
|
+
export function formatNameList(names, verbose) {
|
|
4
|
+
const { shown, hidden } = verbose ? { shown: names, hidden: 0 } : capList(names);
|
|
5
|
+
return shown.map((n) => "`" + n + "`").join(", ") + moreSuffix(hidden);
|
|
6
|
+
}
|
|
7
|
+
/** `**ignored**` and `**superseded**` header lines, omitted when their lists are empty. */
|
|
8
|
+
export function formatSuppressedNameLines(result, verbose) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
if (result.ignoredNames && result.ignoredNames.length > 0) {
|
|
11
|
+
lines.push(`**ignored** ${formatNameList(result.ignoredNames, verbose)}`);
|
|
12
|
+
}
|
|
13
|
+
if (result.supersededNames && result.supersededNames.length > 0) {
|
|
14
|
+
lines.push(`**superseded** ${formatNameList(result.supersededNames, verbose)}`);
|
|
15
|
+
}
|
|
16
|
+
return lines;
|
|
17
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { insertEditedCommentsSection } from "./edited-comments-format.mjs";
|
|
1
3
|
import { formatFixCodeResult } from "./fix-formatter.mjs";
|
|
2
4
|
import { formatRelevantChecks } from "./iterate-checks-formatter.mjs";
|
|
3
5
|
import { joinSections } from "../util/markdown.mjs";
|
|
@@ -6,6 +8,7 @@ import { formatMergeRequirementLines } from "../merge-status/requirements-format
|
|
|
6
8
|
import { appendMergeQueueHeader, formatDeferredWorkLine, formatMergeAction, } from "./iterate-merge-formatter.mjs";
|
|
7
9
|
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
8
10
|
import { formatActivityLine } from "./iterate-activity-formatter.mjs";
|
|
11
|
+
import { formatSuppressedNameLines } from "./iterate-check-names.mjs";
|
|
9
12
|
import { branchStateSegment } from "./iterate-branch-segment.mjs";
|
|
10
13
|
import { insertRuleAutoResolveSection } from "../commands/rule-auto-resolve-format.mjs";
|
|
11
14
|
import { formatTransportEvidence } from "./transport-formatter.mjs";
|
|
@@ -41,7 +44,7 @@ export function formatIterateResult(result, opts) {
|
|
|
41
44
|
instructions.includes(`--repo ${result.repo} `));
|
|
42
45
|
return generated ? full.replace(repoSeg, "") : full;
|
|
43
46
|
};
|
|
44
|
-
const finish = (text) => dropRepeatedRepo(insertRuleAutoResolveSection(text, result.ruleAutoResolve));
|
|
47
|
+
const finish = (text) => dropRepeatedRepo(insertEditedCommentsSection(insertRuleAutoResolveSection(text, result.ruleAutoResolve), result.editedComments, verbose));
|
|
45
48
|
const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
|
|
46
49
|
const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
|
|
47
50
|
? ` · **reviewDecision** \`${result.reviewDecision}\``
|
|
@@ -57,7 +60,7 @@ export function formatIterateResult(result, opts) {
|
|
|
57
60
|
let summaryLine;
|
|
58
61
|
if (verbose) {
|
|
59
62
|
const verboseBranch = branchSeg ? ` · ${branchSeg}` : "";
|
|
60
|
-
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress, ${result.summary.superseded} superseded · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}${readyDelaySuffix ? ` · **ready-delay** \`${readyDelaySuffix}\` (override)` : ""}`;
|
|
63
|
+
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress, ${result.summary.queued} queued, ${result.summary.superseded} superseded · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}${readyDelaySuffix ? ` · **ready-delay** \`${readyDelaySuffix}\` (override)` : ""}`;
|
|
61
64
|
}
|
|
62
65
|
else {
|
|
63
66
|
const counts = [];
|
|
@@ -69,6 +72,8 @@ export function formatIterateResult(result, opts) {
|
|
|
69
72
|
counts.push(`${result.summary.filtered} filtered`);
|
|
70
73
|
if (result.summary.inProgress > 0)
|
|
71
74
|
counts.push(`${result.summary.inProgress} inProgress`);
|
|
75
|
+
if (result.summary.queued > 0)
|
|
76
|
+
counts.push(`${result.summary.queued} queued`);
|
|
72
77
|
if (result.summary.superseded > 0)
|
|
73
78
|
counts.push(`${result.summary.superseded} superseded`);
|
|
74
79
|
const segs = counts.length > 0 ? [`**summary** ${counts.join(", ")}`] : [];
|
|
@@ -114,17 +119,10 @@ export function formatIterateResult(result, opts) {
|
|
|
114
119
|
else if (requiredLine) {
|
|
115
120
|
headerLines.push(requiredLine);
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
const names = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
|
|
119
|
-
headerLines.push(`**ignored** ${names}`);
|
|
120
|
-
}
|
|
121
|
-
if (result.supersededNames && result.supersededNames.length > 0) {
|
|
122
|
-
const names = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
123
|
-
headerLines.push(`**superseded** ${names}`);
|
|
124
|
-
}
|
|
122
|
+
headerLines.push(...formatSuppressedNameLines(result, verbose));
|
|
125
123
|
appendUnreportedLines(headerLines, result);
|
|
126
124
|
appendMergeQueueHeader(headerLines, result);
|
|
127
|
-
const activityLine = formatActivityLine(result);
|
|
125
|
+
const activityLine = formatActivityLine(result, verbose);
|
|
128
126
|
if (activityLine)
|
|
129
127
|
headerLines.push(activityLine);
|
|
130
128
|
headerLines.push(...formatNextCheckLines(result.nextCheck));
|
|
@@ -176,14 +174,7 @@ export function formatIterateResult(result, opts) {
|
|
|
176
174
|
else if (requiredLine) {
|
|
177
175
|
cancelHeaderLines.push(requiredLine);
|
|
178
176
|
}
|
|
179
|
-
|
|
180
|
-
const ignoredStr = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
|
|
181
|
-
cancelHeaderLines.push(`**ignored** ${ignoredStr}`);
|
|
182
|
-
}
|
|
183
|
-
if (result.supersededNames && result.supersededNames.length > 0) {
|
|
184
|
-
const supersededStr = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
185
|
-
cancelHeaderLines.push(`**superseded** ${supersededStr}`);
|
|
186
|
-
}
|
|
177
|
+
cancelHeaderLines.push(...formatSuppressedNameLines(result, verbose));
|
|
187
178
|
appendUnreportedLines(cancelHeaderLines, result);
|
|
188
179
|
appendMergeQueueHeader(cancelHeaderLines, result);
|
|
189
180
|
if (activityLine)
|
|
@@ -198,6 +189,11 @@ export function formatIterateResult(result, opts) {
|
|
|
198
189
|
case "escalate":
|
|
199
190
|
return finish(joinSections([
|
|
200
191
|
header,
|
|
192
|
+
...(result.escalate.reviewItems
|
|
193
|
+
? [
|
|
194
|
+
`**showing** ${result.escalate.reviewItems.shown} of ${result.escalate.reviewItems.total} review items`,
|
|
195
|
+
]
|
|
196
|
+
: []),
|
|
201
197
|
...(apiUsage ? [apiUsage] : []),
|
|
202
198
|
...(verboseChecks ? [verboseChecks] : []),
|
|
203
199
|
result.escalate.humanMessage,
|
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 {};
|