pr-shepherd 0.54.0 → 0.55.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 +3 -3
- package/bin/checks/unreported-required.d.mts +29 -0
- package/bin/checks/unreported-required.mjs +52 -0
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +31 -31
- package/bin/classify/rule-action.d.mts +8 -0
- package/bin/classify/rule-action.mjs +29 -0
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-branch-segment.d.mts +3 -0
- package/bin/cli/iterate-branch-segment.mjs +13 -0
- package/bin/cli/iterate-formatter.mjs +34 -22
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +16 -0
- package/bin/cli/poll-summary-emitter.mjs +4 -1
- package/bin/cli/poll-summary-formatter.mjs +10 -3
- package/bin/cli/stack-overview.d.mts +48 -0
- package/bin/cli/stack-overview.mjs +138 -0
- package/bin/commands/check-fingerprint.mjs +2 -1
- package/bin/commands/check-status.d.mts +1 -1
- package/bin/commands/check-status.mjs +8 -3
- package/bin/commands/check-unreported.d.mts +26 -0
- package/bin/commands/check-unreported.mjs +73 -0
- package/bin/commands/check.mjs +45 -38
- package/bin/commands/iterate/base.mjs +9 -0
- package/bin/commands/iterate/conflicting-head-ci.d.mts +18 -0
- package/bin/commands/iterate/conflicting-head-ci.mjs +32 -0
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/fix-code.mjs +30 -1
- package/bin/commands/iterate/index.mjs +69 -3
- package/bin/commands/iterate/merge-state.mjs +12 -12
- package/bin/commands/iterate/merge.d.mts +12 -2
- package/bin/commands/iterate/merge.mjs +32 -2
- package/bin/commands/iterate/native-stack-rebase.d.mts +8 -2
- package/bin/commands/iterate/native-stack-rebase.mjs +14 -4
- package/bin/commands/iterate/stack-trunk-conflict.d.mts +13 -0
- package/bin/commands/iterate/stack-trunk-conflict.mjs +51 -0
- package/bin/commands/iterate/unreported-required.d.mts +18 -0
- package/bin/commands/iterate/unreported-required.mjs +122 -0
- package/bin/commands/poll-summary.mjs +1 -0
- package/bin/commands/poll.mjs +9 -2
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
- package/bin/commands/rule-auto-resolve-format.mjs +151 -0
- package/bin/commands/rule-auto-resolve.d.mts +21 -0
- package/bin/commands/rule-auto-resolve.mjs +113 -0
- package/bin/commands/stack-drain.d.mts +0 -5
- package/bin/commands/stack-drain.mjs +26 -16
- package/bin/commands/stack-merge-flag.d.mts +7 -0
- package/bin/commands/stack-merge-flag.mjs +6 -0
- package/bin/config/load.d.mts +3 -0
- package/bin/config/load.mjs +9 -2
- package/bin/config/merge-method.d.mts +27 -0
- package/bin/config/merge-method.mjs +56 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/batch-parse-suites.d.mts +5 -0
- package/bin/github/batch-parse-suites.mjs +20 -0
- package/bin/github/batch-parsers.d.mts +1 -1
- package/bin/github/batch-parsers.mjs +7 -0
- package/bin/github/batch-raw-types.d.mts +5 -0
- package/bin/github/batch.d.mts +5 -0
- package/bin/github/batch.mjs +10 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +23 -5
- package/bin/github/fingerprint-fields.d.mts +1 -0
- package/bin/github/fingerprint-fields.mjs +1 -1
- package/bin/github/gql/batch-pr-page.gql +2 -0
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/commit-check-suites.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +6 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
- package/bin/github/gql/poll-summary-fragment.gql +16 -0
- package/bin/github/gql/pr-merge-policy.gql +1 -44
- package/bin/github/gql/ref-rules-query.gql +18 -0
- package/bin/github/gql/ref-rules.gql +46 -0
- package/bin/github/gql/upper-layer-conflict-target.gql +36 -0
- package/bin/github/graphql-internal-retry.d.mts +1 -1
- package/bin/github/graphql-internal-retry.mjs +4 -3
- package/bin/github/merge-queue-checks.mjs +7 -3
- package/bin/github/merge-target-rules.d.mts +20 -0
- package/bin/github/merge-target-rules.mjs +54 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
- package/bin/github/poll-summary-annotation-probe.mjs +88 -0
- package/bin/github/poll-summary-fingerprint.mjs +1 -1
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +19 -12
- package/bin/github/poll-summary-queue-removal.mjs +7 -3
- package/bin/github/poll-summary-raw.d.mts +16 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-route.mjs +5 -0
- package/bin/github/poll-summary-unreported.d.mts +6 -0
- package/bin/github/poll-summary-unreported.mjs +51 -0
- package/bin/github/poll-summary.d.mts +1 -0
- package/bin/github/poll-summary.mjs +28 -3
- package/bin/github/queries.d.mts +12 -0
- package/bin/github/queries.mjs +14 -2
- package/bin/github/queue-removal-freshness.d.mts +20 -5
- package/bin/github/queue-removal-freshness.mjs +37 -10
- package/bin/github/stack-read.d.mts +4 -0
- package/bin/github/stack-read.mjs +13 -1
- package/bin/mcp/server.mjs +6 -1
- package/bin/state/ci-retrigger.d.mts +25 -0
- package/bin/state/ci-retrigger.mjs +48 -0
- package/bin/state/conflicting-head-seen.d.mts +13 -0
- package/bin/state/conflicting-head-seen.mjs +44 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +11 -0
- package/bin/types/iterate.d.mts +19 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +34 -1
- package/package.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/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
|
@@ -6,6 +6,8 @@ import { formatMergeRequirementLines } from "../merge-status/requirements-format
|
|
|
6
6
|
import { appendMergeQueueHeader, formatDeferredWorkLine, formatMergeAction, } from "./iterate-merge-formatter.mjs";
|
|
7
7
|
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
8
8
|
import { formatActivityLine } from "./iterate-activity-formatter.mjs";
|
|
9
|
+
import { branchStateSegment } from "./iterate-branch-segment.mjs";
|
|
10
|
+
import { insertRuleAutoResolveSection } from "../commands/rule-auto-resolve-format.mjs";
|
|
9
11
|
/**
|
|
10
12
|
* Format an IterateResult as human-readable Markdown.
|
|
11
13
|
*
|
|
@@ -23,21 +25,17 @@ import { formatActivityLine } from "./iterate-activity-formatter.mjs";
|
|
|
23
25
|
export function formatIterateResult(result, opts) {
|
|
24
26
|
const verbose = opts?.verbose ?? false;
|
|
25
27
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
28
|
+
const finish = (text) => insertRuleAutoResolveSection(text, result.ruleAutoResolve);
|
|
26
29
|
const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
|
|
27
30
|
const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
|
|
28
31
|
? ` · **reviewDecision** \`${result.reviewDecision}\``
|
|
29
32
|
: "";
|
|
30
33
|
const baseBranchSeg = verbose && result.baseBranch ? ` · **baseBranch** \`${result.baseBranch}\`` : "";
|
|
31
34
|
const baseLine = `**status** \`${result.status}\` · **merge** \`${result.mergeStateStatus}\`${reviewDecisionSeg} · **state** \`${result.state}\` · **repo** \`${result.repo}\`${baseBranchSeg}`;
|
|
35
|
+
const branchSeg = branchStateSegment(result);
|
|
32
36
|
let summaryLine;
|
|
33
37
|
if (verbose) {
|
|
34
|
-
|
|
35
|
-
if (result.mergeStatus === "BEHIND" && result.baseBranch) {
|
|
36
|
-
verboseBranch = ` · **branch** behind PR base \`${result.baseBranch}\``;
|
|
37
|
-
}
|
|
38
|
-
else if (result.mergeStatus === "CONFLICTS" && result.baseBranch) {
|
|
39
|
-
verboseBranch = ` · **branch** conflicts with PR base \`${result.baseBranch}\``;
|
|
40
|
-
}
|
|
38
|
+
const verboseBranch = branchSeg ? ` · ${branchSeg}` : "";
|
|
41
39
|
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}`;
|
|
42
40
|
}
|
|
43
41
|
else {
|
|
@@ -58,12 +56,8 @@ export function formatIterateResult(result, opts) {
|
|
|
58
56
|
segs.push(`**blockingBotReviewInProgress**`);
|
|
59
57
|
if (result.isDraft)
|
|
60
58
|
segs.push(`**isDraft**`);
|
|
61
|
-
if (
|
|
62
|
-
segs.push(
|
|
63
|
-
}
|
|
64
|
-
else if (result.mergeStatus === "CONFLICTS" && result.baseBranch) {
|
|
65
|
-
segs.push(`**branch** conflicts with PR base \`${result.baseBranch}\``);
|
|
66
|
-
}
|
|
59
|
+
if (branchSeg)
|
|
60
|
+
segs.push(branchSeg);
|
|
67
61
|
summaryLine = segs.join(" · ");
|
|
68
62
|
}
|
|
69
63
|
// Surface an explicit `--ready-delay` override (set only when the user passed the flag)
|
|
@@ -105,6 +99,7 @@ export function formatIterateResult(result, opts) {
|
|
|
105
99
|
const names = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
106
100
|
headerLines.push(`**superseded** ${names}`);
|
|
107
101
|
}
|
|
102
|
+
appendUnreportedLines(headerLines, result);
|
|
108
103
|
appendMergeQueueHeader(headerLines, result);
|
|
109
104
|
const activityLine = formatActivityLine(result);
|
|
110
105
|
if (activityLine)
|
|
@@ -115,22 +110,29 @@ export function formatIterateResult(result, opts) {
|
|
|
115
110
|
const verboseChecks = verbose ? formatRelevantChecks(result.checks) : null;
|
|
116
111
|
const telemetrySections = [quotaWarning, apiUsage, verboseChecks];
|
|
117
112
|
switch (result.action) {
|
|
113
|
+
case "ready":
|
|
114
|
+
return joinSections([
|
|
115
|
+
header,
|
|
116
|
+
...telemetrySections,
|
|
117
|
+
adaptIterateLog(result.log),
|
|
118
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
119
|
+
]);
|
|
118
120
|
case "wait": {
|
|
119
121
|
const waitLines = [header, ...telemetrySections, adaptIterateLog(result.log)];
|
|
120
122
|
if (result.deferredWork)
|
|
121
123
|
waitLines.push(formatDeferredWorkLine(result.deferredWork));
|
|
122
124
|
waitLines.push(`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`);
|
|
123
|
-
return joinSections(waitLines);
|
|
125
|
+
return finish(joinSections(waitLines));
|
|
124
126
|
}
|
|
125
127
|
case "mark_ready":
|
|
126
|
-
return joinSections([
|
|
128
|
+
return finish(joinSections([
|
|
127
129
|
header,
|
|
128
130
|
...telemetrySections,
|
|
129
131
|
adaptIterateLog(result.log),
|
|
130
132
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
131
|
-
]);
|
|
133
|
+
]));
|
|
132
134
|
case "merge":
|
|
133
|
-
return formatMergeAction(joinSections([header, ...telemetrySections]), result);
|
|
135
|
+
return finish(formatMergeAction(joinSections([header, ...telemetrySections]), result));
|
|
134
136
|
case "cancel": {
|
|
135
137
|
const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
|
|
136
138
|
if (result.mergeRequirements) {
|
|
@@ -147,26 +149,36 @@ export function formatIterateResult(result, opts) {
|
|
|
147
149
|
const supersededStr = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
148
150
|
cancelHeaderLines.push(`**superseded** ${supersededStr}`);
|
|
149
151
|
}
|
|
152
|
+
appendUnreportedLines(cancelHeaderLines, result);
|
|
150
153
|
appendMergeQueueHeader(cancelHeaderLines, result);
|
|
151
154
|
if (activityLine)
|
|
152
155
|
cancelHeaderLines.push(activityLine);
|
|
153
|
-
return joinSections([
|
|
156
|
+
return finish(joinSections([
|
|
154
157
|
cancelHeaderLines.join("\n"),
|
|
155
158
|
...(apiUsage ? [apiUsage] : []),
|
|
156
159
|
...(verboseChecks ? [verboseChecks] : []),
|
|
157
160
|
adaptIterateLog(result.log),
|
|
158
161
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
159
|
-
]);
|
|
162
|
+
]));
|
|
160
163
|
}
|
|
161
164
|
case "escalate":
|
|
162
|
-
return joinSections([
|
|
165
|
+
return finish(joinSections([
|
|
163
166
|
header,
|
|
164
167
|
...(apiUsage ? [apiUsage] : []),
|
|
165
168
|
...(verboseChecks ? [verboseChecks] : []),
|
|
166
169
|
result.escalate.humanMessage,
|
|
167
170
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
168
|
-
]);
|
|
171
|
+
]));
|
|
169
172
|
case "fix_code":
|
|
170
|
-
return formatFixCodeResult(joinSections([header, ...telemetrySections]), result, { verbose });
|
|
173
|
+
return finish(formatFixCodeResult(joinSections([header, ...telemetrySections]), result, { verbose }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function appendUnreportedLines(lines, result) {
|
|
177
|
+
if (result.unreportedRequiredChecks && result.unreportedRequiredChecks.length > 0) {
|
|
178
|
+
const names = result.unreportedRequiredChecks.map((name) => "`" + name + "`").join(", ");
|
|
179
|
+
lines.push(`**unreported required** ${names}`);
|
|
180
|
+
}
|
|
181
|
+
if (result.trunkBehindBy !== undefined && result.trunkBehindBy > 0) {
|
|
182
|
+
lines.push(`**trunk behind** \`${result.trunkBehindBy}\``);
|
|
171
183
|
}
|
|
172
184
|
}
|
|
@@ -6,6 +6,12 @@ import { AUTO_MARK_READY_DISABLED_HOLD } from "../commands/stack-work.mjs";
|
|
|
6
6
|
import { buildPrShepherdCommand } from "./runner.mjs";
|
|
7
7
|
export function buildSimpleIterateInstructions(result) {
|
|
8
8
|
switch (result.action) {
|
|
9
|
+
case "ready": {
|
|
10
|
+
const sentence = `PR #${result.pr} is ready. Ready-delay has ${result.remainingSeconds}s left. Run this same command again when the timer elapses. Do not start other work.`;
|
|
11
|
+
return [
|
|
12
|
+
result.quotaWarning ? buildQuotaAwareContinuation(result.quotaWarning, sentence) : sentence,
|
|
13
|
+
];
|
|
14
|
+
}
|
|
9
15
|
case "wait":
|
|
10
16
|
if (result.stackDraftHold)
|
|
11
17
|
return [buildStackDraftHoldInstruction(result, result.stackDraftHold)];
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { adaptIterateLog, buildSimpleIterateInstructions } from "./iterate-instructions.mjs";
|
|
2
|
+
import { projectRuleAutoResolve } from "../commands/rule-auto-resolve-format.mjs";
|
|
2
3
|
/**
|
|
3
4
|
* Project an IterateResult to a lean JSON shape for the default (non-verbose) output.
|
|
4
5
|
* Omits fields that are the trivial default (false, 0, empty) or state-gated fields
|
|
@@ -41,6 +42,7 @@ export function projectIterateLean(result, opts) {
|
|
|
41
42
|
remainingSeconds: result.remainingSeconds,
|
|
42
43
|
}),
|
|
43
44
|
...(result.baseBranch && { baseBranch: result.baseBranch }),
|
|
45
|
+
...(result.stackTrunkConflict && { stackTrunkConflict: result.stackTrunkConflict }),
|
|
44
46
|
...(result.branchProtection !== null && { branchProtection: result.branchProtection }),
|
|
45
47
|
...(result.mergeRequirements && { mergeRequirements: result.mergeRequirements }),
|
|
46
48
|
...(result.mergeQueue && { mergeQueue: result.mergeQueue }),
|
|
@@ -65,9 +67,23 @@ export function projectIterateLean(result, opts) {
|
|
|
65
67
|
...((result.supersededNames?.length ?? 0) > 0 && {
|
|
66
68
|
supersededNames: result.supersededNames,
|
|
67
69
|
}),
|
|
70
|
+
...((result.unreportedRequiredChecks?.length ?? 0) > 0 && {
|
|
71
|
+
unreportedRequiredChecks: result.unreportedRequiredChecks,
|
|
72
|
+
}),
|
|
73
|
+
...(result.trunkBehindBy !== undefined &&
|
|
74
|
+
result.trunkBehindBy > 0 && { trunkBehindBy: result.trunkBehindBy }),
|
|
68
75
|
...(result.quotaWarning && { quotaWarning: result.quotaWarning }),
|
|
76
|
+
...(result.ruleAutoResolve && {
|
|
77
|
+
ruleAutoResolve: projectRuleAutoResolve(result.ruleAutoResolve),
|
|
78
|
+
}),
|
|
69
79
|
};
|
|
70
80
|
switch (result.action) {
|
|
81
|
+
case "ready":
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
log: adaptIterateLog(result.log),
|
|
85
|
+
instructions: simpleInstructions(result),
|
|
86
|
+
};
|
|
71
87
|
case "wait":
|
|
72
88
|
return {
|
|
73
89
|
...base,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { EXIT } from "../exit-codes.mjs";
|
|
2
2
|
import { formatPollSummaryResult } from "./poll-summary-formatter.mjs";
|
|
3
|
+
import { projectStackOverview } from "./stack-overview.mjs";
|
|
3
4
|
export function emitPollSummaryResult(result, opts) {
|
|
4
|
-
process.stdout.write(opts.format === "json"
|
|
5
|
+
process.stdout.write(opts.format === "json"
|
|
6
|
+
? `${JSON.stringify(result.selection.kind === "stack" ? projectStackOverview(result) : result)}\n`
|
|
7
|
+
: `${formatPollSummaryResult(result)}\n`);
|
|
5
8
|
process.exitCode = pollSummaryExitCode(result);
|
|
6
9
|
}
|
|
7
10
|
function pollSummaryExitCode(result) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
2
2
|
import { withPollSummaryInstructions } from "../commands/poll-summary-instructions.mjs";
|
|
3
|
+
import { formatStackOverview, projectStackOverview } from "./stack-overview.mjs";
|
|
3
4
|
export function formatPollSummaryResult(result) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
if (result.selection.kind === "stack")
|
|
6
|
+
return formatStackOverview(projectStackOverview(result));
|
|
7
|
+
const selection = `PRs ${result.selection.requested.map((pr) => `#${pr}`).join(", ")}`;
|
|
7
8
|
const lines = [
|
|
8
9
|
`# Poll summary [${result.reason.toUpperCase()}]`,
|
|
9
10
|
"",
|
|
@@ -52,6 +53,12 @@ function formatItem(item) {
|
|
|
52
53
|
...(checks
|
|
53
54
|
? [` - checks: ${formatCounts(checks, checks.incomplete ? ", incomplete" : "")}`]
|
|
54
55
|
: []),
|
|
56
|
+
...(checks?.unreportedRequired?.length
|
|
57
|
+
? [
|
|
58
|
+
` - unreported required: ${checks.unreportedRequired.map((name) => `\`${name}\``).join(", ")}`,
|
|
59
|
+
]
|
|
60
|
+
: []),
|
|
61
|
+
...(checks?.actionsWorkflowInProgress ? [" - actions workflow in progress"] : []),
|
|
55
62
|
...(review
|
|
56
63
|
? [` - review: ${formatCounts(review, review.incomplete ? ", incomplete" : "")}`]
|
|
57
64
|
: []),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PollSummaryItem, PollSummaryResult, PollSummaryStackAncestry, StackNextAction } from "../types.mts";
|
|
2
|
+
import type { ApiUsage, GraphqlQuotaWarning } from "../types/api-usage.mts";
|
|
3
|
+
/** Agent-facing stack row. Planning keeps the richer PollSummaryItem. */
|
|
4
|
+
interface StackLayerView {
|
|
5
|
+
pr: number;
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
state: PollSummaryItem["state"];
|
|
9
|
+
shepherded: boolean;
|
|
10
|
+
mergeable: boolean;
|
|
11
|
+
blocker?: string;
|
|
12
|
+
author?: string;
|
|
13
|
+
owned?: true;
|
|
14
|
+
isDraft?: true;
|
|
15
|
+
isInMergeQueue?: true;
|
|
16
|
+
position?: number;
|
|
17
|
+
stackSize?: number;
|
|
18
|
+
baseRefName: string;
|
|
19
|
+
failing?: number;
|
|
20
|
+
inProgress?: number;
|
|
21
|
+
actionable?: number;
|
|
22
|
+
checksIncomplete?: true;
|
|
23
|
+
reviewIncomplete?: true;
|
|
24
|
+
queueRemoval?: {
|
|
25
|
+
reason: string | null;
|
|
26
|
+
actor?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface StackOverview {
|
|
30
|
+
mode: "summary";
|
|
31
|
+
repo: string;
|
|
32
|
+
selection: Extract<PollSummaryResult["selection"], {
|
|
33
|
+
kind: "stack";
|
|
34
|
+
}>;
|
|
35
|
+
reason: PollSummaryResult["reason"];
|
|
36
|
+
stackMergeable?: boolean;
|
|
37
|
+
nextAction?: StackNextAction;
|
|
38
|
+
prs: StackLayerView[];
|
|
39
|
+
stackAncestry?: PollSummaryStackAncestry[];
|
|
40
|
+
instructions?: string[];
|
|
41
|
+
apiUsage?: ApiUsage;
|
|
42
|
+
quotaWarning?: GraphqlQuotaWarning;
|
|
43
|
+
}
|
|
44
|
+
/** Lean stack payload shared by Markdown, JSON, and MCP. */
|
|
45
|
+
export declare function projectStackOverview(result: PollSummaryResult): StackOverview;
|
|
46
|
+
/** Markdown for a stack overview. */
|
|
47
|
+
export declare function formatStackOverview(overview: StackOverview): string;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { stackLayerBlockReason } from "../commands/stack-layer-readiness.mjs";
|
|
2
|
+
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
3
|
+
/** Lean stack payload shared by Markdown, JSON, and MCP. */
|
|
4
|
+
export function projectStackOverview(result) {
|
|
5
|
+
const selection = result.selection;
|
|
6
|
+
if (selection.kind !== "stack") {
|
|
7
|
+
throw new Error("projectStackOverview requires a stack selection");
|
|
8
|
+
}
|
|
9
|
+
const stale = new Set(result.stackAncestry?.map((gap) => gap.childPr) ?? []);
|
|
10
|
+
return {
|
|
11
|
+
mode: "summary",
|
|
12
|
+
repo: result.repo,
|
|
13
|
+
selection,
|
|
14
|
+
reason: result.reason,
|
|
15
|
+
...(result.stackMergeable !== undefined && { stackMergeable: result.stackMergeable }),
|
|
16
|
+
...(result.nextAction && { nextAction: result.nextAction }),
|
|
17
|
+
prs: result.prs.map((item) => projectLayer(item, stale.has(item.pr))),
|
|
18
|
+
...(result.stackAncestry?.length && { stackAncestry: result.stackAncestry }),
|
|
19
|
+
...(result.instructions?.length && { instructions: result.instructions }),
|
|
20
|
+
...(result.apiUsage && { apiUsage: result.apiUsage }),
|
|
21
|
+
...(result.quotaWarning && { quotaWarning: result.quotaWarning }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function projectLayer(item, stale) {
|
|
25
|
+
const blocker = layerBlocker(item, stale);
|
|
26
|
+
const removal = item.queueRemoval;
|
|
27
|
+
return {
|
|
28
|
+
pr: item.pr,
|
|
29
|
+
title: item.title,
|
|
30
|
+
url: item.url,
|
|
31
|
+
state: item.state,
|
|
32
|
+
shepherded: item.readyReceipt === true,
|
|
33
|
+
mergeable: blocker === undefined,
|
|
34
|
+
...(blocker && { blocker }),
|
|
35
|
+
...(item.authorLogin && { author: item.authorLogin }),
|
|
36
|
+
...(item.owned && { owned: true }),
|
|
37
|
+
...(item.isDraft && { isDraft: true }),
|
|
38
|
+
...(item.isInMergeQueue && { isInMergeQueue: true }),
|
|
39
|
+
...(item.stack && { position: item.stack.position, stackSize: item.stack.size }),
|
|
40
|
+
baseRefName: item.baseRefName,
|
|
41
|
+
...(blocker === "failing-checks" &&
|
|
42
|
+
(item.checks?.failing ?? 0) > 0 && { failing: item.checks?.failing }),
|
|
43
|
+
...(blocker === "checks-in-progress" &&
|
|
44
|
+
(item.checks?.inProgress ?? 0) > 0 && { inProgress: item.checks?.inProgress }),
|
|
45
|
+
...(blocker === "review-work" &&
|
|
46
|
+
(item.review?.actionable ?? 0) > 0 && { actionable: item.review?.actionable }),
|
|
47
|
+
...(item.checks?.incomplete && { checksIncomplete: true }),
|
|
48
|
+
...(item.review?.incomplete && { reviewIncomplete: true }),
|
|
49
|
+
...(removal && {
|
|
50
|
+
queueRemoval: {
|
|
51
|
+
reason: removal.reason,
|
|
52
|
+
...(removal.actor && { actor: removal.actor }),
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Mergeability blocker. A missing receipt is "not shepherded", not a mergeability reason. */
|
|
58
|
+
function layerBlocker(item, stale) {
|
|
59
|
+
if (item.action === "escalate") {
|
|
60
|
+
return item.reasons.find((reason) => reason !== "appears-ready") ?? "escalate";
|
|
61
|
+
}
|
|
62
|
+
if (stale)
|
|
63
|
+
return "stale-ancestry";
|
|
64
|
+
const reason = stackLayerBlockReason(item);
|
|
65
|
+
if (reason === undefined || reason === "no-ready-receipt")
|
|
66
|
+
return undefined;
|
|
67
|
+
return reason;
|
|
68
|
+
}
|
|
69
|
+
/** Skill stop tokens. Other stack actions stay untagged so the loop keeps running. */
|
|
70
|
+
function stackTerminalTag(nextAction) {
|
|
71
|
+
if (nextAction === "cancel")
|
|
72
|
+
return " [CANCEL]";
|
|
73
|
+
if (nextAction === "escalate")
|
|
74
|
+
return " [ESCALATE]";
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
/** Markdown for a stack overview. */
|
|
78
|
+
export function formatStackOverview(overview) {
|
|
79
|
+
const stack = overview.selection;
|
|
80
|
+
const lines = [
|
|
81
|
+
`# ${overview.repo} stack #${stack.stackNumber}${stackTerminalTag(overview.nextAction)} — ${overview.reason}`,
|
|
82
|
+
"",
|
|
83
|
+
`Stack: #${stack.stackNumber} · anchor PR #${stack.anchor} · ${stack.stackSize} layers · mode \`${overview.mode}\``,
|
|
84
|
+
...(overview.stackMergeable !== undefined
|
|
85
|
+
? [`stackMergeable: ${overview.stackMergeable}`]
|
|
86
|
+
: []),
|
|
87
|
+
...(overview.nextAction ? [`nextAction: ${overview.nextAction}`] : []),
|
|
88
|
+
"",
|
|
89
|
+
"## Layers",
|
|
90
|
+
"",
|
|
91
|
+
...overview.prs.flatMap(formatLayerLines),
|
|
92
|
+
];
|
|
93
|
+
if (overview.stackAncestry?.length) {
|
|
94
|
+
lines.push("", "## Stack ancestry", "");
|
|
95
|
+
for (const gap of overview.stackAncestry) {
|
|
96
|
+
lines.push(`- PR #${gap.childPr} base \`${gap.childBaseRefName}\` at \`${gap.childBaseRefOid}\` differs from parent PR #${gap.parentPr} head \`${gap.parentHeadRefName}\` at \`${gap.parentHeadRefOid}\`.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (overview.instructions?.length) {
|
|
100
|
+
lines.push("", "## Instructions", "", ...overview.instructions);
|
|
101
|
+
}
|
|
102
|
+
const quota = formatQuotaWarning(overview.quotaWarning);
|
|
103
|
+
const usage = formatApiUsage(overview.apiUsage);
|
|
104
|
+
if (quota)
|
|
105
|
+
lines.push("", quota);
|
|
106
|
+
if (usage)
|
|
107
|
+
lines.push("", usage);
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
function formatLayerLines(layer) {
|
|
111
|
+
const facts = [
|
|
112
|
+
layer.shepherded ? "shepherded" : "not shepherded",
|
|
113
|
+
layer.mergeable ? "mergeable" : `not mergeable (\`${layer.blocker ?? "unknown"}\`)`,
|
|
114
|
+
layer.author ? `owner \`@${layer.author}\`` : undefined,
|
|
115
|
+
layer.owned ? "owned" : undefined,
|
|
116
|
+
].filter((fact) => fact !== undefined);
|
|
117
|
+
const details = [
|
|
118
|
+
layer.state,
|
|
119
|
+
layer.isDraft ? "draft" : undefined,
|
|
120
|
+
layer.isInMergeQueue ? "in merge queue" : undefined,
|
|
121
|
+
layer.position !== undefined && layer.stackSize !== undefined
|
|
122
|
+
? `position ${layer.position}/${layer.stackSize}`
|
|
123
|
+
: undefined,
|
|
124
|
+
`base \`${layer.baseRefName}\``,
|
|
125
|
+
layer.failing ? `${layer.failing} failing` : undefined,
|
|
126
|
+
layer.inProgress ? `${layer.inProgress} in progress` : undefined,
|
|
127
|
+
layer.actionable ? `${layer.actionable} actionable` : undefined,
|
|
128
|
+
layer.checksIncomplete ? "checks incomplete" : undefined,
|
|
129
|
+
layer.reviewIncomplete ? "review incomplete" : undefined,
|
|
130
|
+
layer.queueRemoval
|
|
131
|
+
? `removed from merge queue (${layer.queueRemoval.reason ?? "unknown reason"}${layer.queueRemoval.actor ? ` by @${layer.queueRemoval.actor}` : ""})`
|
|
132
|
+
: undefined,
|
|
133
|
+
].filter((detail) => detail !== undefined);
|
|
134
|
+
return [
|
|
135
|
+
`- [PR #${layer.pr}: ${layer.title}](${layer.url}) — ${facts.join(" · ")}`,
|
|
136
|
+
` - ${details.join(" · ")}`,
|
|
137
|
+
];
|
|
138
|
+
}
|
|
@@ -2,6 +2,7 @@ import { getMergeableState } from "../github/client.mjs";
|
|
|
2
2
|
import { fetchPrFingerprint, fingerprintsEqual, } from "../github/fingerprint.mjs";
|
|
3
3
|
import { fingerprintInputDigest, loadPrFingerprint } from "../state/pr-fingerprint.mjs";
|
|
4
4
|
import { hasCheckDrivenActionableWork } from "./check-annotations.mjs";
|
|
5
|
+
import { stripReplayedRuleAutoResolve } from "./rule-auto-resolve-format.mjs";
|
|
5
6
|
function reportAllowsFingerprintSkip(report) {
|
|
6
7
|
if (report.status === "READY")
|
|
7
8
|
return false;
|
|
@@ -50,7 +51,7 @@ export async function tryReuseFingerprintReport(prNumber, repo, stateKey, config
|
|
|
50
51
|
if (!(await cachedReportSurvivesMergeabilityRefresh(prNumber, repo, cached.report, cached.fingerprint))) {
|
|
51
52
|
return null;
|
|
52
53
|
}
|
|
53
|
-
return { ...cached.report, fingerprintReused: true };
|
|
54
|
+
return { ...stripReplayedRuleAutoResolve(cached.report), fingerprintReused: true };
|
|
54
55
|
}
|
|
55
56
|
async function cachedReportSurvivesMergeabilityRefresh(prNumber, repo, report, fingerprint) {
|
|
56
57
|
const restDerived = fingerprint.mergeable !== report.mergeStatus.mergeable ||
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { MergeStatusResult, ShepherdStatus } from "../types.mts";
|
|
2
2
|
import type { CiVerdict } from "../checks/classify.mts";
|
|
3
|
-
export declare function computeStatus(verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, mergeStatus: MergeStatusResult, changesRequestedReviews: number): ShepherdStatus;
|
|
3
|
+
export declare function computeStatus(verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, mergeStatus: MergeStatusResult, changesRequestedReviews: number, hasUnreportedRequired?: boolean): ShepherdStatus;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedReviews) {
|
|
1
|
+
export function computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedReviews, hasUnreportedRequired = false) {
|
|
2
2
|
// Merge conflicts are always terminal regardless of CI state.
|
|
3
3
|
if (mergeStatus.status === "CONFLICTS")
|
|
4
4
|
return "FAILING";
|
|
@@ -25,7 +25,8 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
25
25
|
unresolvedComments === 0 &&
|
|
26
26
|
changesRequestedReviews === 0 &&
|
|
27
27
|
(mergeStatus.status === "BLOCKED" || mergeStatus.status === "UNSTABLE") &&
|
|
28
|
-
!mergeStatus.blockingBotReviewInProgress
|
|
28
|
+
!mergeStatus.blockingBotReviewInProgress &&
|
|
29
|
+
!hasUnreportedRequired) {
|
|
29
30
|
return "READY";
|
|
30
31
|
}
|
|
31
32
|
if (mergeStatus.status === "BLOCKED" ||
|
|
@@ -37,7 +38,11 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
37
38
|
if (changesRequestedReviews > 0 || unresolvedThreads > 0 || unresolvedComments > 0)
|
|
38
39
|
return "UNRESOLVED_COMMENTS";
|
|
39
40
|
// DRAFT is treated the same as CLEAN for readiness — marking the PR ready resolves it.
|
|
40
|
-
if ((mergeStatus.status === "CLEAN" || mergeStatus.status === "DRAFT") &&
|
|
41
|
+
if ((mergeStatus.status === "CLEAN" || mergeStatus.status === "DRAFT") &&
|
|
42
|
+
verdict.allPassed &&
|
|
43
|
+
!hasUnreportedRequired)
|
|
41
44
|
return "READY";
|
|
45
|
+
if (hasUnreportedRequired && (mergeStatus.status === "CLEAN" || mergeStatus.status === "DRAFT"))
|
|
46
|
+
return "PENDING";
|
|
42
47
|
return "UNKNOWN";
|
|
43
48
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type WorkflowSuiteSnapshot } from "../checks/unreported-required.mts";
|
|
2
|
+
import type { BatchPrData, CheckRun, ShepherdReport } from "../types.mts";
|
|
3
|
+
import type { RepoInfo } from "../github/client.mts";
|
|
4
|
+
export interface UnreportedRequiredFields {
|
|
5
|
+
unreportedRequiredChecks?: string[];
|
|
6
|
+
trunkBehindBy?: number;
|
|
7
|
+
actionsWorkflowInProgress?: true;
|
|
8
|
+
stackBottomPr?: number;
|
|
9
|
+
headRefName: string;
|
|
10
|
+
hasUnreportedRequired: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Load trunk rules for a stack and diff them against the head's check names. */
|
|
13
|
+
export declare function collectUnreportedRequired(input: {
|
|
14
|
+
batchData: BatchPrData;
|
|
15
|
+
checks: readonly CheckRun[];
|
|
16
|
+
suites: readonly WorkflowSuiteSnapshot[];
|
|
17
|
+
owner: string;
|
|
18
|
+
name: string;
|
|
19
|
+
pr: number;
|
|
20
|
+
relevantEvents: readonly string[];
|
|
21
|
+
}): Promise<UnreportedRequiredFields>;
|
|
22
|
+
/**
|
|
23
|
+
* A fingerprint hit can keep a stale trunk `behindBy`. Refresh the trunk compare
|
|
24
|
+
* and the required-context diff without refetching the whole batch.
|
|
25
|
+
*/
|
|
26
|
+
export declare function refreshCachedUnreported(report: ShepherdReport, repo: RepoInfo): Promise<ShepherdReport>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { actionsWorkflowInProgress, reportedCheckNames, unreportedRequiredContexts, } from "../checks/unreported-required.mjs";
|
|
2
|
+
import { loadMergeTargetStatus } from "../github/merge-target-rules.mjs";
|
|
3
|
+
/** Load trunk rules for a stack and diff them against the head's check names. */
|
|
4
|
+
export async function collectUnreportedRequired(input) {
|
|
5
|
+
const localContexts = input.batchData.branchRules?.requiredStatusCheckContexts ??
|
|
6
|
+
input.batchData.branchProtection?.requiredStatusCheckContexts ??
|
|
7
|
+
[];
|
|
8
|
+
const target = await loadMergeTargetStatus({
|
|
9
|
+
owner: input.owner,
|
|
10
|
+
name: input.name,
|
|
11
|
+
pr: input.pr,
|
|
12
|
+
baseRefName: input.batchData.baseRefName,
|
|
13
|
+
headRefName: input.batchData.headRefName,
|
|
14
|
+
localContexts,
|
|
15
|
+
stack: input.batchData.stack,
|
|
16
|
+
});
|
|
17
|
+
const unreported = unreportedRequiredContexts(target.contexts, reportedCheckNames(input.checks));
|
|
18
|
+
const actionsRunning = actionsWorkflowInProgress(input.suites, new Set(input.relevantEvents));
|
|
19
|
+
return {
|
|
20
|
+
...(unreported.length > 0 && { unreportedRequiredChecks: unreported }),
|
|
21
|
+
...(target.trunkBehindBy !== undefined &&
|
|
22
|
+
target.trunkBehindBy > 0 && { trunkBehindBy: target.trunkBehindBy }),
|
|
23
|
+
...(actionsRunning && { actionsWorkflowInProgress: true }),
|
|
24
|
+
...(target.stackBottomPr !== undefined && { stackBottomPr: target.stackBottomPr }),
|
|
25
|
+
headRefName: input.batchData.headRefName,
|
|
26
|
+
hasUnreportedRequired: unreported.length > 0,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A fingerprint hit can keep a stale trunk `behindBy`. Refresh the trunk compare
|
|
31
|
+
* and the required-context diff without refetching the whole batch.
|
|
32
|
+
*/
|
|
33
|
+
export async function refreshCachedUnreported(report, repo) {
|
|
34
|
+
const stack = report.mergeStatus?.mergeRequirements?.stack;
|
|
35
|
+
if (!stack || !report.headRefName)
|
|
36
|
+
return report;
|
|
37
|
+
const target = await loadMergeTargetStatus({
|
|
38
|
+
owner: repo.owner,
|
|
39
|
+
name: repo.name,
|
|
40
|
+
pr: report.pr,
|
|
41
|
+
baseRefName: report.baseBranch,
|
|
42
|
+
headRefName: report.headRefName,
|
|
43
|
+
localContexts: report.mergeStatus.mergeRequirements?.requiredStatusChecks?.contexts ?? [],
|
|
44
|
+
stack,
|
|
45
|
+
});
|
|
46
|
+
const unreported = unreportedRequiredContexts(target.contexts, reportedCheckNames(reportChecks(report)));
|
|
47
|
+
const next = { ...report };
|
|
48
|
+
if (unreported.length > 0)
|
|
49
|
+
next.unreportedRequiredChecks = unreported;
|
|
50
|
+
else
|
|
51
|
+
delete next.unreportedRequiredChecks;
|
|
52
|
+
if (target.trunkBehindBy !== undefined && target.trunkBehindBy > 0) {
|
|
53
|
+
next.trunkBehindBy = target.trunkBehindBy;
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
delete next.trunkBehindBy;
|
|
57
|
+
if (target.stackBottomPr !== undefined)
|
|
58
|
+
next.stackBottomPr = target.stackBottomPr;
|
|
59
|
+
if (unreported.length > 0 && next.status === "READY")
|
|
60
|
+
next.status = "PENDING";
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
function reportChecks(report) {
|
|
64
|
+
return [
|
|
65
|
+
...report.checks.passing,
|
|
66
|
+
...report.checks.failing,
|
|
67
|
+
...report.checks.inProgress,
|
|
68
|
+
...report.checks.skipped,
|
|
69
|
+
...report.checks.filtered,
|
|
70
|
+
...(report.checks.ignored ?? []),
|
|
71
|
+
...(report.checks.supersededNames ?? []).map((name) => ({ name })),
|
|
72
|
+
];
|
|
73
|
+
}
|