pr-shepherd 1.0.0 → 1.1.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/checks/classify.d.mts +3 -1
- package/bin/checks/classify.mjs +8 -1
- package/bin/cli/poll-summary-formatter.mjs +3 -0
- package/bin/cli/rule-auto-resolve-lines.d.mts +3 -0
- package/bin/cli/rule-auto-resolve-lines.mjs +12 -0
- package/bin/cli/stack-layer-view.d.mts +2 -1
- package/bin/cli/stack-overview.mjs +3 -0
- package/bin/cli/triage-formatter.mjs +5 -1
- package/bin/commands/check-activity.d.mts +8 -0
- package/bin/commands/check-activity.mjs +26 -0
- package/bin/commands/check-annotations.mjs +2 -1
- package/bin/commands/check-status.d.mts +6 -1
- package/bin/commands/check-status.mjs +9 -4
- package/bin/commands/check-wait-detectors.mjs +11 -0
- package/bin/commands/check.mjs +10 -3
- package/bin/commands/iterate/escalate.mjs +4 -2
- package/bin/commands/poll-summary-explicit-instructions.d.mts +1 -1
- package/bin/commands/poll-summary-explicit-instructions.mjs +9 -7
- package/bin/commands/poll-summary-instructions.d.mts +2 -2
- package/bin/commands/poll-summary-instructions.mjs +16 -15
- package/bin/commands/poll-summary-signature.d.mts +6 -0
- package/bin/commands/poll-summary-signature.mjs +19 -2
- package/bin/commands/poll-summary.mjs +22 -14
- package/bin/commands/ready-delay.d.mts +1 -0
- package/bin/commands/ready-delay.mjs +8 -3
- 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 +7 -0
- package/bin/commands/rule-auto-resolve-format.mjs +11 -4
- package/bin/commands/rule-auto-resolve.d.mts +14 -2
- package/bin/commands/stack-drain.d.mts +1 -1
- package/bin/commands/stack-drain.mjs +3 -3
- package/bin/commands/stack-work.d.mts +4 -1
- package/bin/commands/stack-work.mjs +10 -2
- package/bin/commands/tick.mjs +1 -0
- package/bin/comments/thread-visibility.d.mts +6 -0
- package/bin/comments/thread-visibility.mjs +30 -15
- package/bin/github/batch-parse-suites.d.mts +14 -0
- package/bin/github/batch-parse-suites.mjs +4 -1
- package/bin/github/batch-parser-helpers.mjs +5 -0
- package/bin/github/batch-raw-types.d.mts +1 -0
- package/bin/github/check-annotation-cache.d.mts +5 -0
- package/bin/github/check-annotation-cache.mjs +6 -3
- package/bin/github/fingerprint.mjs +4 -0
- package/bin/github/github-fetch.d.mts +6 -2
- package/bin/github/github-fetch.mjs +37 -2
- package/bin/github/github-timeouts.d.mts +8 -0
- package/bin/github/github-timeouts.mjs +8 -0
- package/bin/github/gql/batch-pr-page.gql +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/commit-check-contexts.gql +1 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +1 -0
- package/bin/github/http-utils.mjs +3 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +5 -0
- package/bin/github/poll-summary-annotation-probe.mjs +17 -3
- package/bin/github/poll-summary-annotations.d.mts +14 -0
- package/bin/github/poll-summary-annotations.mjs +75 -0
- package/bin/github/poll-summary-blocking-reviewer.d.mts +2 -0
- package/bin/github/poll-summary-blocking-reviewer.mjs +9 -0
- package/bin/github/poll-summary-checks.d.mts +1 -0
- package/bin/github/poll-summary-checks.mjs +21 -2
- package/bin/github/poll-summary-command.d.mts +7 -1
- package/bin/github/poll-summary-command.mjs +29 -0
- package/bin/github/poll-summary-fingerprint.d.mts +7 -0
- package/bin/github/poll-summary-fingerprint.mjs +7 -0
- package/bin/github/poll-summary-projector.d.mts +3 -1
- package/bin/github/poll-summary-projector.mjs +15 -25
- package/bin/github/poll-summary-queue-removal.d.mts +2 -0
- package/bin/github/poll-summary-queue-removal.mjs +11 -0
- package/bin/github/poll-summary-raw.d.mts +1 -0
- package/bin/github/poll-summary-readiness.mjs +3 -0
- package/bin/github/poll-summary-review-parity.d.mts +24 -0
- package/bin/github/poll-summary-review-parity.mjs +82 -0
- package/bin/github/poll-summary-review.d.mts +11 -2
- package/bin/github/poll-summary-review.mjs +87 -59
- package/bin/github/poll-summary-route.mjs +2 -0
- package/bin/github/poll-summary-row-error.d.mts +16 -0
- package/bin/github/poll-summary-row-error.mjs +74 -0
- package/bin/github/poll-summary-rule-auto-resolve.d.mts +32 -0
- package/bin/github/poll-summary-rule-auto-resolve.mjs +119 -0
- package/bin/github/poll-summary-rule-handoff.d.mts +7 -0
- package/bin/github/poll-summary-rule-handoff.mjs +20 -0
- package/bin/github/poll-summary-rule-items.d.mts +20 -0
- package/bin/github/poll-summary-rule-items.mjs +28 -0
- package/bin/github/poll-summary-unreported.d.mts +7 -0
- package/bin/github/poll-summary-unreported.mjs +16 -0
- package/bin/github/poll-summary.mjs +15 -7
- package/bin/github/rest-check-read.mjs +3 -1
- package/bin/github/rest-reader-core.mjs +13 -2
- package/bin/github/rest-summary-read.mjs +12 -1
- package/bin/github/rest-text.mjs +3 -2
- package/bin/github/stack-handoff-page.mjs +1 -0
- package/bin/reporters/agent.mjs +2 -0
- package/bin/types/github.d.mts +8 -0
- package/bin/types/poll-summary.d.mts +23 -0
- package/bin/types/report.d.mts +5 -1
- 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/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -0
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* to PR readiness.
|
|
8
8
|
* 2. Drop checks with `conclusion == SKIPPED` or `conclusion == NEUTRAL` from the
|
|
9
9
|
* pass/fail tally. Report them as "skipped" for transparency but don't block on them.
|
|
10
|
-
* 3.
|
|
10
|
+
* 3. A check that is not `COMPLETED` while its check suite is `COMPLETED` is stuck and
|
|
11
|
+
* classifies as `failing`.
|
|
12
|
+
* 4. Reclassify `CANCELLED` checks as "superseded" (non-blocking) when a newer
|
|
11
13
|
* run of the same workflow exists on the same commit and event, or when an
|
|
12
14
|
* exact matching check from a lower-ID run started and succeeded later.
|
|
13
15
|
* GitHub can start jobs out of workflow-run creation order.
|
package/bin/checks/classify.mjs
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* to PR readiness.
|
|
8
8
|
* 2. Drop checks with `conclusion == SKIPPED` or `conclusion == NEUTRAL` from the
|
|
9
9
|
* pass/fail tally. Report them as "skipped" for transparency but don't block on them.
|
|
10
|
-
* 3.
|
|
10
|
+
* 3. A check that is not `COMPLETED` while its check suite is `COMPLETED` is stuck and
|
|
11
|
+
* classifies as `failing`.
|
|
12
|
+
* 4. Reclassify `CANCELLED` checks as "superseded" (non-blocking) when a newer
|
|
11
13
|
* run of the same workflow exists on the same commit and event, or when an
|
|
12
14
|
* exact matching check from a lower-ID run started and succeeded later.
|
|
13
15
|
* GitHub can start jobs out of workflow-run creation order.
|
|
@@ -76,6 +78,11 @@ function classify(check, relevantEvents) {
|
|
|
76
78
|
const { status, conclusion } = check;
|
|
77
79
|
// Not yet finished.
|
|
78
80
|
if (status !== "COMPLETED") {
|
|
81
|
+
// The enclosing suite finished while this run did not: nothing will update it again, so
|
|
82
|
+
// waiting would only end in the stall timeout. Surface it as failing work (rerun the
|
|
83
|
+
// workflow) with the raw status and suite status shown.
|
|
84
|
+
if (check.suiteStatus === "COMPLETED")
|
|
85
|
+
return { ...check, category: "failing" };
|
|
79
86
|
return { ...check, category: "in_progress" };
|
|
80
87
|
}
|
|
81
88
|
// Skipped / neutral — report but don't block.
|
|
@@ -3,6 +3,7 @@ import { withPollSummaryInstructions } from "../commands/poll-summary-instructio
|
|
|
3
3
|
import { formatStackOverview, projectStackOverview } from "./stack-overview.mjs";
|
|
4
4
|
import { formatTransportEvidence } from "./transport-formatter.mjs";
|
|
5
5
|
import { formatNextCheckLines } from "./next-check-format.mjs";
|
|
6
|
+
import { formatRuleAutoResolveLines } from "./rule-auto-resolve-lines.mjs";
|
|
6
7
|
export function formatPollSummaryResult(result) {
|
|
7
8
|
if (result.selection.kind === "stack")
|
|
8
9
|
return formatStackOverview(projectStackOverview(result));
|
|
@@ -69,12 +70,14 @@ function formatItem(item) {
|
|
|
69
70
|
...(review
|
|
70
71
|
? [` - review: ${formatCounts(review, review.incomplete ? ", incomplete" : "")}`]
|
|
71
72
|
: []),
|
|
73
|
+
...formatRuleAutoResolveLines(item.ruleAutoResolve),
|
|
72
74
|
...(item.queueRemoval
|
|
73
75
|
? [
|
|
74
76
|
` - queue removal: reason \`${item.queueRemoval.reason ?? "UNKNOWN"}\` · at \`${item.queueRemoval.createdAtUnix}\`${item.queueRemoval.actor ? ` · actor \`@${item.queueRemoval.actor}\`` : ""}${item.queueRemoval.beforeCommitOid ? ` · commit \`${item.queueRemoval.beforeCommitOid}\`` : ""}${item.queueRemoval.beforeCommitParentOids?.length ? ` · parents \`${item.queueRemoval.beforeCommitParentOids.join(",")}\`` : ""}`,
|
|
75
77
|
]
|
|
76
78
|
: []),
|
|
77
79
|
` - reasons: ${item.reasons.map((reason) => `\`${reason}\``).join(", ")}`,
|
|
80
|
+
...(item.error ? [` - error: ${item.error.replace(/\s*[\r\n]+\s*/g, " ")}`] : []),
|
|
78
81
|
...(item.transport ? [` - transport \`${item.transport}\``] : []),
|
|
79
82
|
...(item.transportUnavailable ?? []).map(({ field, reason }) => ` - unavailable \`${field}\`: ${reason}`),
|
|
80
83
|
...(item.pollCommand
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PollSummaryRuleAutoResolve } from "../types.mts";
|
|
2
|
+
/** Row lines for an aggregate row or stack layer: the one-PR `## Classification auto-resolve` content. */
|
|
3
|
+
export declare function formatRuleAutoResolveLines(event: PollSummaryRuleAutoResolve | undefined): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Row lines for an aggregate row or stack layer: the one-PR `## Classification auto-resolve` content. */
|
|
2
|
+
export function formatRuleAutoResolveLines(event) {
|
|
3
|
+
if (!event)
|
|
4
|
+
return [];
|
|
5
|
+
const link = (kind, id, url) => ` - ${kind} \`${id}\`${url ? ` ${url}` : ""}`;
|
|
6
|
+
return [
|
|
7
|
+
` - classification auto-resolve: ${event.summary}`,
|
|
8
|
+
...(event.threads ?? []).map((thread) => link("thread", thread.id, thread.url)),
|
|
9
|
+
...(event.minimized ?? []).map((item) => link(item.kind, item.id, item.url)),
|
|
10
|
+
...(event.errors ?? []).map((error) => ` - ${error}`),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PollSummaryItem } from "../types.mts";
|
|
1
|
+
import type { PollSummaryItem, PollSummaryRuleAutoResolve } from "../types.mts";
|
|
2
2
|
/** One lean stack row shared by Markdown, JSON, and MCP. */
|
|
3
3
|
export interface StackLayerView {
|
|
4
4
|
transport?: "rest";
|
|
@@ -26,6 +26,7 @@ export interface StackLayerView {
|
|
|
26
26
|
actionable?: number;
|
|
27
27
|
checksIncomplete?: true;
|
|
28
28
|
reviewIncomplete?: true;
|
|
29
|
+
ruleAutoResolve?: PollSummaryRuleAutoResolve;
|
|
29
30
|
queueRemoval?: {
|
|
30
31
|
reason: string | null;
|
|
31
32
|
actor?: string;
|
|
@@ -3,6 +3,7 @@ import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
|
3
3
|
import { formatTransportEvidence } from "./transport-formatter.mjs";
|
|
4
4
|
import { formatNextCheckLines } from "./next-check-format.mjs";
|
|
5
5
|
import { formatPrUrl } from "../pr-reference.mjs";
|
|
6
|
+
import { formatRuleAutoResolveLines } from "./rule-auto-resolve-lines.mjs";
|
|
6
7
|
/** Lean stack payload shared by Markdown, JSON, and MCP. */
|
|
7
8
|
export function projectStackOverview(result) {
|
|
8
9
|
const selection = result.selection;
|
|
@@ -59,6 +60,7 @@ function projectLayer(item, stale) {
|
|
|
59
60
|
(item.review?.actionable ?? 0) > 0 && { actionable: item.review?.actionable }),
|
|
60
61
|
...(item.checks?.incomplete && { checksIncomplete: true }),
|
|
61
62
|
...(item.review?.incomplete && { reviewIncomplete: true }),
|
|
63
|
+
...(item.ruleAutoResolve && { ruleAutoResolve: item.ruleAutoResolve }),
|
|
62
64
|
...(removal && {
|
|
63
65
|
queueRemoval: {
|
|
64
66
|
reason: removal.reason,
|
|
@@ -156,6 +158,7 @@ function formatLayerLines(layer, index, overview) {
|
|
|
156
158
|
return [
|
|
157
159
|
`- ${layer.url === formatPrUrl(overview.repo, layer.pr) ? `PR #${layer.pr}: ${layer.title}` : `[PR #${layer.pr}: ${layer.title}](${layer.url})`}${facts.length > 0 ? ` — ${facts.join(" · ")}` : ""}`,
|
|
158
160
|
` - ${details.join(" · ")}`,
|
|
161
|
+
...formatRuleAutoResolveLines(layer.ruleAutoResolve),
|
|
159
162
|
...(layer.transport ? [` - transport \`${layer.transport}\``] : []),
|
|
160
163
|
...(layer.transportUnavailable ?? []).map(({ field, reason }) => ` - unavailable \`${field}\`: ${reason}`),
|
|
161
164
|
];
|
|
@@ -74,7 +74,11 @@ export function formatTriageResult(header, result, opts = {}) {
|
|
|
74
74
|
: ch.detailsUrl
|
|
75
75
|
? `external \`${ch.detailsUrl}\``
|
|
76
76
|
: "(no runId)";
|
|
77
|
-
const conclusionTag = ch.
|
|
77
|
+
const conclusionTag = ch.status !== undefined
|
|
78
|
+
? ` [status: ${ch.status}, suite: ${ch.suiteStatus}]`
|
|
79
|
+
: ch.conclusion !== null
|
|
80
|
+
? ` [conclusion: ${ch.conclusion}]`
|
|
81
|
+
: "";
|
|
78
82
|
const attemptTag = ch.runAttempt !== undefined ? ` [attempt: ${ch.runAttempt}]` : "";
|
|
79
83
|
const scopeTag = ch.scope
|
|
80
84
|
? ` [scope: ${ch.scope}${ch.commitOid ? `, commit: ${ch.commitOid}` : ""}]`
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BatchPartition } from "../classify/apply.mts";
|
|
2
|
+
import type { PrActivitySummary } from "../types.mts";
|
|
3
|
+
/**
|
|
4
|
+
* Drop rule-suppressed items from the activity digest, matching what `check` hides from the
|
|
5
|
+
* agent. IDs whose automatic minimize/resolve was denied stay visible: `check` surfaces those
|
|
6
|
+
* once so the caller can act. `reviewRoundCount` is raw review history and stays unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare function filterSuppressedActivity(activity: PrActivitySummary | undefined, partition: BatchPartition, denied: Iterable<string>): PrActivitySummary | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop rule-suppressed items from the activity digest, matching what `check` hides from the
|
|
3
|
+
* agent. IDs whose automatic minimize/resolve was denied stay visible: `check` surfaces those
|
|
4
|
+
* once so the caller can act. `reviewRoundCount` is raw review history and stays unchanged.
|
|
5
|
+
*/
|
|
6
|
+
export function filterSuppressedActivity(activity, partition, denied) {
|
|
7
|
+
if (!activity)
|
|
8
|
+
return activity;
|
|
9
|
+
const deniedIds = new Set(denied);
|
|
10
|
+
const hidden = (id, suppressed) => suppressed.has(id) && !deniedIds.has(id);
|
|
11
|
+
const reviewItemsSinceLatestCommit = activity.reviewItemsSinceLatestCommit.filter((item) => {
|
|
12
|
+
switch (item.kind) {
|
|
13
|
+
case "pr-comment":
|
|
14
|
+
return !hidden(item.id, partition.suppressedCommentIds);
|
|
15
|
+
case "review-thread-comment":
|
|
16
|
+
return !item.threadId || !hidden(item.threadId, partition.suppressedThreadIds);
|
|
17
|
+
case "review-summary":
|
|
18
|
+
return !hidden(item.id, partition.suppressedReviewSummaryIds);
|
|
19
|
+
case "changes-requested-review":
|
|
20
|
+
return !partition.suppressedChangesRequestedIds.has(item.id);
|
|
21
|
+
default:
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return { ...activity, reviewItemsSinceLatestCommit };
|
|
26
|
+
}
|
|
@@ -42,7 +42,8 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber,
|
|
|
42
42
|
...buckets.filtered,
|
|
43
43
|
...buckets.ignored,
|
|
44
44
|
].filter(shouldFetchCheckAnnotations);
|
|
45
|
-
const
|
|
45
|
+
const expectedCounts = new Map(candidates.flatMap((c) => c.id != null && c.annotationCount ? [[c.id, c.annotationCount]] : []));
|
|
46
|
+
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber, cacheOpts && { ...cacheOpts, expectedCounts });
|
|
46
47
|
const byId = new Map(annotated.flatMap((c) => (c.id != null ? [[c.id, c]] : [])));
|
|
47
48
|
const apply = (list) => list.map((c) => {
|
|
48
49
|
const next = (c.id == null ? undefined : byId.get(c.id)) ?? c;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { MergeStatusResult, ShepherdStatus } from "../types.mts";
|
|
2
2
|
import type { CiVerdict } from "../checks/classify.mts";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `awaitingChecks` is true when required checks have not reported, or when the PR has no relevant
|
|
5
|
+
* check yet while a relevant Actions workflow suite is still running: the suite exists before
|
|
6
|
+
* its check runs, so a CLEAN or DRAFT merge state is not yet proof that CI passed.
|
|
7
|
+
*/
|
|
8
|
+
export declare function computeStatus(verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, mergeStatus: MergeStatusResult, changesRequestedReviews: number, awaitingChecks?: boolean): ShepherdStatus;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `awaitingChecks` is true when required checks have not reported, or when the PR has no relevant
|
|
3
|
+
* check yet while a relevant Actions workflow suite is still running: the suite exists before
|
|
4
|
+
* its check runs, so a CLEAN or DRAFT merge state is not yet proof that CI passed.
|
|
5
|
+
*/
|
|
6
|
+
export function computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedReviews, awaitingChecks = false) {
|
|
2
7
|
// Merge conflicts are always terminal regardless of CI state.
|
|
3
8
|
if (mergeStatus.status === "CONFLICTS")
|
|
4
9
|
return "FAILING";
|
|
@@ -26,7 +31,7 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
26
31
|
changesRequestedReviews === 0 &&
|
|
27
32
|
(mergeStatus.status === "BLOCKED" || mergeStatus.status === "UNSTABLE") &&
|
|
28
33
|
!mergeStatus.blockingBotReviewInProgress &&
|
|
29
|
-
!
|
|
34
|
+
!awaitingChecks) {
|
|
30
35
|
return "READY";
|
|
31
36
|
}
|
|
32
37
|
if (mergeStatus.status === "BLOCKED" ||
|
|
@@ -40,9 +45,9 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
40
45
|
// DRAFT is treated the same as CLEAN for readiness — marking the PR ready resolves it.
|
|
41
46
|
if ((mergeStatus.status === "CLEAN" || mergeStatus.status === "DRAFT") &&
|
|
42
47
|
verdict.allPassed &&
|
|
43
|
-
!
|
|
48
|
+
!awaitingChecks)
|
|
44
49
|
return "READY";
|
|
45
|
-
if (
|
|
50
|
+
if (awaitingChecks && (mergeStatus.status === "CLEAN" || mergeStatus.status === "DRAFT"))
|
|
46
51
|
return "PENDING";
|
|
47
52
|
return "UNKNOWN";
|
|
48
53
|
}
|
|
@@ -30,6 +30,17 @@ function detectorsCover(stored) {
|
|
|
30
30
|
* Read the PR's REST change detectors: the pull, the head's check runs, check suites and
|
|
31
31
|
* statuses, the three feedback lists, and the base branch. Every read is conditional, so an
|
|
32
32
|
* unchanged resource answers a free 304.
|
|
33
|
+
*
|
|
34
|
+
* The feedback lists are deliberately not rule-aware (#635 gap 7). An edited bot comment, even
|
|
35
|
+
* a rule-suppressed or minimized one, answers 200 and wakes the tick. The wake costs one
|
|
36
|
+
* charged list read plus one `BatchPr` first page (a complete fingerprint needs no more),
|
|
37
|
+
* the same point a fingerprint hit spends. Once the digest is re-recorded, a later eligible
|
|
38
|
+
* tick can replay for free again, but only if every detector answers 304 with that digest and
|
|
39
|
+
* the stored snapshot is within `poll.reconcile-seconds`; otherwise it goes to `BatchPr`. Being
|
|
40
|
+
* rule-aware would need the loaded rules, a persisted set of hidden comment ids (REST has no
|
|
41
|
+
* `isMinimized`), and a content digest in place of the ETag. That is more code, and more risk
|
|
42
|
+
* of sleeping through a real change (a rule-suppressed comment edited so it no longer matches,
|
|
43
|
+
* an edited minimized human comment, which `check` surfaces), than the one point it saves.
|
|
33
44
|
*/
|
|
34
45
|
async function readDetectors(prNumber, repo, report) {
|
|
35
46
|
const prefix = restRepoPath(repo);
|
package/bin/commands/check.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers
|
|
|
33
33
|
import { isConfiguredBotAuthor, isHumanAuthor, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
34
34
|
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "./iterate/thread-mutation-routing.mjs";
|
|
35
35
|
import { loadClassifyRules } from "../classify/loader.mjs";
|
|
36
|
+
import { filterSuppressedActivity } from "./check-activity.mjs";
|
|
36
37
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
37
38
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
38
39
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
@@ -260,7 +261,9 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
260
261
|
canGenerateItemMutation(review.id, review.body, batchData.viewerAuthorization?.viewerCanAdminister, "dismiss") ||
|
|
261
262
|
visibleChangesRequestedIds.has(review.id));
|
|
262
263
|
}).length;
|
|
263
|
-
|
|
264
|
+
const awaitingChecks = unreported.hasUnreportedRequired ||
|
|
265
|
+
(!verdict.hasChecks && unreported.actionsWorkflowInProgress === true);
|
|
266
|
+
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount, awaitingChecks);
|
|
264
267
|
status = enforceTransportReadiness(status, batchData);
|
|
265
268
|
// Resolve any pending mergeability refresh (and the resulting MERGED/CLOSED short-circuit)
|
|
266
269
|
// before deciding what to persist below — deferWhileQueued must see the same final,
|
|
@@ -268,7 +271,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
268
271
|
// snapshot. A conflict newly discovered by this REST read is exactly the kind of check-driven
|
|
269
272
|
// signal that keeps a queued PR out of the deferral path.
|
|
270
273
|
if (status === "READY" && !didRefreshMergeability) {
|
|
271
|
-
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount,
|
|
274
|
+
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount, awaitingChecks);
|
|
272
275
|
batchData = refreshed.batchData;
|
|
273
276
|
mergeStatus = refreshed.mergeStatus;
|
|
274
277
|
status = refreshed.status;
|
|
@@ -449,7 +452,11 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
449
452
|
...(batchData.allowedMergeMethods && {
|
|
450
453
|
allowedMergeMethods: batchData.allowedMergeMethods,
|
|
451
454
|
}),
|
|
452
|
-
activity: batchData.activity,
|
|
455
|
+
activity: filterSuppressedActivity(batchData.activity, partition, [
|
|
456
|
+
...deniedRuleAutoResolveCommentIds,
|
|
457
|
+
...deniedRuleAutoResolveThreadIds,
|
|
458
|
+
...deniedRuleAutoResolveReviewSummaryIds,
|
|
459
|
+
]),
|
|
453
460
|
...(hasQueueState && {
|
|
454
461
|
mergeQueue: {
|
|
455
462
|
enabled: Boolean(batchData.isMergeQueueEnabled),
|
|
@@ -43,10 +43,12 @@ function renderEscalateAnnotation(annotation) {
|
|
|
43
43
|
function renderEscalateCheck(check) {
|
|
44
44
|
const workflowPrefix = check.workflowName ? `${check.workflowName} › ` : "";
|
|
45
45
|
const jobLabel = check.jobName ?? check.name;
|
|
46
|
-
const
|
|
46
|
+
const conclusionTag = check.status !== undefined
|
|
47
|
+
? `[status: ${check.status}, suite: ${check.suiteStatus}]`
|
|
48
|
+
: `[conclusion: ${check.conclusion ?? "UNKNOWN"}]`;
|
|
47
49
|
const attempt = check.runAttempt !== undefined ? ` [attempt: ${check.runAttempt}]` : "";
|
|
48
50
|
const lines = [
|
|
49
|
-
`- ${renderCheckTarget(check)} — \`${workflowPrefix}${jobLabel}\`
|
|
51
|
+
`- ${renderCheckTarget(check)} — \`${workflowPrefix}${jobLabel}\` ${conclusionTag}${attempt}${renderCheckScope(check)}`,
|
|
50
52
|
];
|
|
51
53
|
if (check.failedStep)
|
|
52
54
|
lines.push(` > failed step: ${check.failedStep}`);
|
|
@@ -3,4 +3,4 @@ import type { PollSummaryResult } from "../types.mts";
|
|
|
3
3
|
export declare function isHandedOffSelection(result: PollSummaryResult): boolean;
|
|
4
4
|
/** A final full-review probe can finish this selection without another aggregate read. */
|
|
5
5
|
export declare function onlyDraftVerificationSelection(result: PollSummaryResult): boolean;
|
|
6
|
-
export declare function explicitInstructions(result: PollSummaryResult): string[];
|
|
6
|
+
export declare function explicitInstructions(result: PollSummaryResult, rerunCommand?: string): string[];
|
|
@@ -12,7 +12,10 @@ export function onlyDraftVerificationSelection(result) {
|
|
|
12
12
|
item.action === "escalate" ||
|
|
13
13
|
isDraftReadinessVerification(item)));
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** Rerunning is mandatory while any selected row is non-terminal; a finished one-PR poll is not the end. */
|
|
16
|
+
const KEEP_WATCHING = "Do not end the turn while any selected row is non-terminal: a one-PR poll returning `CANCEL` or `ESCALATE` finishes only that row, and rows that are `CANCEL`, `ESCALATE`, or handed off do not count.";
|
|
17
|
+
export function explicitInstructions(result, rerunCommand) {
|
|
18
|
+
const again = rerunCommand ? `\`${rerunCommand}\`` : "this aggregate command";
|
|
16
19
|
if (result.reason === "all_terminal")
|
|
17
20
|
return [
|
|
18
21
|
result.prs.some((item) => item.reasons.includes("draft-auto-mark-ready-disabled"))
|
|
@@ -21,27 +24,26 @@ export function explicitInstructions(result) {
|
|
|
21
24
|
];
|
|
22
25
|
if (result.quotaWarning && result.reason !== "actionable") {
|
|
23
26
|
return [
|
|
24
|
-
buildQuotaAwareContinuation(result.quotaWarning,
|
|
27
|
+
buildQuotaAwareContinuation(result.quotaWarning, `1. This aggregate selection is non-terminal. Run ${again} again at the cadence below. ${KEEP_WATCHING} Before continuing,`),
|
|
25
28
|
];
|
|
26
29
|
}
|
|
27
30
|
if (result.reason === "waiting" || result.reason === "timeout") {
|
|
28
|
-
return [
|
|
31
|
+
return [`1. Run ${again} again when the caller is ready to recheck. ${KEEP_WATCHING}`];
|
|
29
32
|
}
|
|
30
33
|
const instructions = [
|
|
31
34
|
"1. Choose each non-WAIT, non-CANCEL row that can proceed independently and run or delegate its exact `pollCommand`.",
|
|
32
35
|
"2. Follow each selected one-PR poll's `## Instructions` until it returns `CANCEL` or `ESCALATE`.",
|
|
33
|
-
|
|
36
|
+
`3. Run ${again} again after selected work completes; one row's \`ESCALATE\` does not stop work on other rows. ${KEEP_WATCHING}`,
|
|
34
37
|
];
|
|
35
38
|
if (onlyDraftVerificationSelection(result)) {
|
|
36
|
-
instructions[2] =
|
|
37
|
-
"3. If every draft verification probe returns `[CANCEL]` with reason `auto-mark-ready-disabled`, stop this selection and report any `ESCALATE` rows; leave the drafts unchanged. If any probe returns `[ESCALATE]`, report that handoff and stop after the other probes finish. Otherwise follow the probes' instructions and rerun this aggregate selector when that work completes.";
|
|
39
|
+
instructions[2] = `3. If every draft verification probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, stop this selection and report any \`ESCALATE\` rows; leave the drafts unchanged. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow the probes' instructions and rerun ${again} when that work completes.`;
|
|
38
40
|
}
|
|
39
41
|
else if (isHandedOffSelection(result)) {
|
|
40
42
|
instructions[2] =
|
|
41
43
|
"3. Stop after reporting each `ESCALATE` handoff; every selected row is `CANCEL` or `ESCALATE`, so do not run this aggregate poll again.";
|
|
42
44
|
}
|
|
43
45
|
else if (result.quotaWarning) {
|
|
44
|
-
instructions[2] = buildQuotaAwareContinuation(result.quotaWarning,
|
|
46
|
+
instructions[2] = buildQuotaAwareContinuation(result.quotaWarning, `3. After selected work completes, run ${again} again at the cadence below; one row's \`ESCALATE\` does not stop work on other rows. ${KEEP_WATCHING}`);
|
|
45
47
|
}
|
|
46
48
|
return instructions;
|
|
47
49
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { PollSummaryItem, PollSummaryResult } from "../types.mts";
|
|
2
2
|
/** Keep aggregate JSON, Markdown, and MCP instructions on one projection. */
|
|
3
|
-
export declare function withPollSummaryInstructions(result: PollSummaryResult, mergeRequested: boolean): PollSummaryResult;
|
|
3
|
+
export declare function withPollSummaryInstructions(result: PollSummaryResult, mergeRequested: boolean, rerunCommand?: string): PollSummaryResult;
|
|
4
4
|
/** The projected summary, plus the idle layers when the stack plan can only wait on them. */
|
|
5
|
-
export declare function planPollSummary(result: PollSummaryResult, mergeRequested: boolean): {
|
|
5
|
+
export declare function planPollSummary(result: PollSummaryResult, mergeRequested: boolean, rerunCommand?: string): {
|
|
6
6
|
result: PollSummaryResult;
|
|
7
7
|
idle?: PollSummaryItem[];
|
|
8
8
|
};
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
|
|
3
3
|
import { explicitInstructions } from "./poll-summary-explicit-instructions.mjs";
|
|
4
4
|
import { appendAutonomousInstructions, appendHumanHandoffInstructions, findHumanHandoffs, idleWaitPlan, isStackLayerReady, planPrefixDrain, retargetWaitPlan, stackPosition, } from "./stack-drain.mjs";
|
|
5
|
-
import { appendStackRerunInstruction, onlyDraftReadinessVerification, splitStackWork, } from "./stack-work.mjs";
|
|
5
|
+
import { appendStackRerunInstruction, onlyDraftReadinessVerification, splitStackWork, stackRerunTarget, } from "./stack-work.mjs";
|
|
6
6
|
/** Keep aggregate JSON, Markdown, and MCP instructions on one projection. */
|
|
7
|
-
export function withPollSummaryInstructions(result, mergeRequested) {
|
|
8
|
-
return planPollSummary(result, mergeRequested).result;
|
|
7
|
+
export function withPollSummaryInstructions(result, mergeRequested, rerunCommand) {
|
|
8
|
+
return planPollSummary(result, mergeRequested, rerunCommand).result;
|
|
9
9
|
}
|
|
10
10
|
/** The projected summary, plus the idle layers when the stack plan can only wait on them. */
|
|
11
|
-
export function planPollSummary(result, mergeRequested) {
|
|
11
|
+
export function planPollSummary(result, mergeRequested, rerunCommand) {
|
|
12
12
|
if (result.selection.kind !== "stack") {
|
|
13
|
-
return { result: { ...result, instructions: explicitInstructions(result) } };
|
|
13
|
+
return { result: { ...result, instructions: explicitInstructions(result, rerunCommand) } };
|
|
14
14
|
}
|
|
15
15
|
const prs = [...result.prs].sort((left, right) => stackPosition(left) - stackPosition(right));
|
|
16
16
|
const staleChildren = new Set(result.stackAncestry?.map((gap) => gap.childPr) ?? []);
|
|
@@ -60,7 +60,7 @@ export function planPollSummary(result, mergeRequested) {
|
|
|
60
60
|
return layer;
|
|
61
61
|
}),
|
|
62
62
|
};
|
|
63
|
-
const planned = planStack(projected, mergeRequested);
|
|
63
|
+
const planned = planStack(projected, mergeRequested, rerunCommand);
|
|
64
64
|
const instructions = [...planned.instructions];
|
|
65
65
|
if (result.quotaWarning && planned.action === "shepherd") {
|
|
66
66
|
instructions.push(buildQuotaAwareContinuation(result.quotaWarning, `${instructions.length + 1}. After completing the stack action,`));
|
|
@@ -82,7 +82,8 @@ export function planPollSummary(result, mergeRequested) {
|
|
|
82
82
|
...(planned.idle && { idle: planned.idle }),
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
function planStack(result, mergeRequested) {
|
|
85
|
+
function planStack(result, mergeRequested, rerunCommand) {
|
|
86
|
+
const again = stackRerunTarget(rerunCommand);
|
|
86
87
|
const open = result.prs.filter((item) => item.state === "OPEN");
|
|
87
88
|
const gaps = result.stackAncestry ?? [];
|
|
88
89
|
const staleChildren = new Set(gaps.map((gap) => gap.childPr));
|
|
@@ -93,7 +94,7 @@ function planStack(result, mergeRequested) {
|
|
|
93
94
|
const agentWork = runnableCandidates.length > 0;
|
|
94
95
|
// Only owned work defers a human handoff: rerunning cannot advance another author's layer.
|
|
95
96
|
const ownedWork = runnableCandidates.some((item) => item.owned);
|
|
96
|
-
const drain = planPrefixDrain(result, mergeRequested);
|
|
97
|
+
const drain = planPrefixDrain(result, mergeRequested, rerunCommand);
|
|
97
98
|
if (drain)
|
|
98
99
|
return drain;
|
|
99
100
|
const handoffs = findHumanHandoffs(result);
|
|
@@ -108,9 +109,9 @@ function planStack(result, mergeRequested) {
|
|
|
108
109
|
if (ownedWork) {
|
|
109
110
|
const rerun = onlyDraftReadinessVerification(work.sessions, work.idle) &&
|
|
110
111
|
work.sessions.every((item) => item.owned)
|
|
111
|
-
?
|
|
112
|
-
:
|
|
113
|
-
appendStackRerunInstruction(instructions, runnableCandidates, rerun);
|
|
112
|
+
? `If every listed one-PR probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, report the human handoff above and stop this \`stack\` command. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow each probe's instructions and rerun ${again} when that work completes.`
|
|
113
|
+
: `After the listed one-PR sessions, rerun ${again}. Stop for the human handoff only when no autonomous shepherding remains.`;
|
|
114
|
+
appendStackRerunInstruction(instructions, runnableCandidates, rerun, false);
|
|
114
115
|
}
|
|
115
116
|
return { action: stop ? "escalate" : "shepherd", stackMergeable: false, instructions };
|
|
116
117
|
}
|
|
@@ -129,7 +130,7 @@ function planStack(result, mergeRequested) {
|
|
|
129
130
|
instructions.push(`${instructions.length + 1}. PR #${item.pr} needs a one-PR session, but Shepherd could not produce its command. ${ownedWork ? "After autonomous shepherding, ask" : "Stop and ask"} for direction.`);
|
|
130
131
|
}
|
|
131
132
|
if (ownedWork) {
|
|
132
|
-
appendStackRerunInstruction(instructions, runnableCandidates,
|
|
133
|
+
appendStackRerunInstruction(instructions, runnableCandidates, `After the listed one-PR sessions, rerun ${again}. Stop for the human handoff only when no autonomous shepherding remains.`, false);
|
|
133
134
|
}
|
|
134
135
|
return { action: ownedWork ? "shepherd" : "escalate", stackMergeable: false, instructions };
|
|
135
136
|
}
|
|
@@ -149,10 +150,10 @@ function planStack(result, mergeRequested) {
|
|
|
149
150
|
appendAutonomousInstructions(instructions, work.sessions);
|
|
150
151
|
if (onlyDraftReadinessVerification(work.sessions, work.idle) &&
|
|
151
152
|
work.sessions.every((item) => item.owned)) {
|
|
152
|
-
instructions.push(`${instructions.length + 1}. If every listed one-PR probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, stop shepherding this stack and leave those drafts unchanged. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow each probe's instructions and rerun
|
|
153
|
+
instructions.push(`${instructions.length + 1}. If every listed one-PR probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, stop shepherding this stack and leave those drafts unchanged. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow each probe's instructions and rerun ${again} when that work completes.`);
|
|
153
154
|
return { action: "shepherd", stackMergeable: false, instructions };
|
|
154
155
|
}
|
|
155
|
-
const action = appendStackRerunInstruction(instructions, work.sessions,
|
|
156
|
+
const action = appendStackRerunInstruction(instructions, work.sessions, `After the selected one-PR sessions, rerun ${again}.`);
|
|
156
157
|
return { action, stackMergeable: false, instructions };
|
|
157
158
|
}
|
|
158
159
|
if (open.some((item) => item.isInMergeQueue)) {
|
|
@@ -176,7 +177,7 @@ function planStack(result, mergeRequested) {
|
|
|
176
177
|
const instructions = [];
|
|
177
178
|
const sessions = open.filter((item) => item.action !== "cancel");
|
|
178
179
|
appendAutonomousInstructions(instructions, sessions);
|
|
179
|
-
const action = appendStackRerunInstruction(instructions, sessions,
|
|
180
|
+
const action = appendStackRerunInstruction(instructions, sessions, `After the selected one-PR sessions, rerun ${again}.`);
|
|
180
181
|
return { action, stackMergeable, instructions };
|
|
181
182
|
}
|
|
182
183
|
return retargetWaitPlan(open[0]);
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { PollSummaryResult } from "../types.mts";
|
|
2
|
+
/**
|
|
3
|
+
* Decision-relevant state only. Raw review totals and the ignored, skipped, filtered, and
|
|
4
|
+
* superseded check counts move when rules suppress a bot notice or a bot reposts a status
|
|
5
|
+
* check, which would reprint quiet polls and keep an idle stack from reaching `stall-timeout`.
|
|
6
|
+
* Post-rule `actionable` and the check transitions that drive routing stay in.
|
|
7
|
+
*/
|
|
2
8
|
export declare function summaryStatusSignature(result: PollSummaryResult): string;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision-relevant state only. Raw review totals and the ignored, skipped, filtered, and
|
|
3
|
+
* superseded check counts move when rules suppress a bot notice or a bot reposts a status
|
|
4
|
+
* check, which would reprint quiet polls and keep an idle stack from reaching `stall-timeout`.
|
|
5
|
+
* Post-rule `actionable` and the check transitions that drive routing stay in.
|
|
6
|
+
*/
|
|
1
7
|
export function summaryStatusSignature(result) {
|
|
2
8
|
return JSON.stringify({
|
|
3
9
|
nextAction: result.nextAction,
|
|
@@ -9,8 +15,19 @@ export function summaryStatusSignature(result) {
|
|
|
9
15
|
mergeable: item.mergeable,
|
|
10
16
|
mergeStateStatus: item.mergeStateStatus,
|
|
11
17
|
reviewDecision: item.reviewDecision,
|
|
12
|
-
checks: item.checks
|
|
13
|
-
|
|
18
|
+
checks: item.checks && {
|
|
19
|
+
passing: item.checks.passing,
|
|
20
|
+
failing: item.checks.failing,
|
|
21
|
+
inProgress: item.checks.inProgress,
|
|
22
|
+
incomplete: item.checks.incomplete,
|
|
23
|
+
annotated: item.checks.annotated,
|
|
24
|
+
unreportedRequired: item.checks.unreportedRequired,
|
|
25
|
+
actionsWorkflowInProgress: item.checks.actionsWorkflowInProgress,
|
|
26
|
+
},
|
|
27
|
+
review: item.review && {
|
|
28
|
+
actionable: item.review.actionable,
|
|
29
|
+
incomplete: item.review.incomplete,
|
|
30
|
+
},
|
|
14
31
|
})),
|
|
15
32
|
});
|
|
16
33
|
}
|