pr-shepherd 1.1.0 → 1.1.2
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/triage-formatter.mjs +5 -1
- package/bin/commands/check-annotations.d.mts +0 -3
- package/bin/commands/check-annotations.mjs +1 -10
- package/bin/commands/check-status.d.mts +6 -1
- package/bin/commands/check-status.mjs +9 -4
- package/bin/commands/check-unreported.mjs +1 -1
- package/bin/commands/check.mjs +6 -13
- package/bin/commands/iterate/conflicting-head-ci.d.mts +1 -1
- package/bin/commands/iterate/conflicting-head-ci.mjs +1 -1
- 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.mjs +15 -14
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -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/github/batch-parser-helpers.mjs +2 -0
- package/bin/github/batch-raw-types.d.mts +1 -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-annotations.d.mts +1 -1
- package/bin/github/poll-summary-annotations.mjs +2 -2
- package/bin/github/poll-summary-checks.mjs +1 -0
- package/bin/github/poll-summary-command.d.mts +7 -1
- package/bin/github/poll-summary-command.mjs +29 -0
- package/bin/github/poll-summary-projector.mjs +2 -1
- package/bin/github/poll-summary-raw.d.mts +1 -0
- package/bin/github/poll-summary-readiness.mjs +2 -0
- package/bin/github/poll-summary-route.mjs +2 -0
- package/bin/github/poll-summary-unreported.d.mts +7 -0
- package/bin/github/poll-summary-unreported.mjs +16 -0
- package/bin/github/rest-check-read.mjs +3 -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 +6 -0
- package/bin/types/report.d.mts +5 -3
- package/package.json +1 -1
- 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.
|
|
@@ -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}` : ""}]`
|
|
@@ -14,14 +14,12 @@ export declare function hasCheckDrivenActionableWork(checks: {
|
|
|
14
14
|
passing: ClassifiedCheck[];
|
|
15
15
|
skipped: ClassifiedCheck[];
|
|
16
16
|
filtered: ClassifiedCheck[];
|
|
17
|
-
ignored?: TriagedCheck[];
|
|
18
17
|
}, mergeStatusValue: string): boolean;
|
|
19
18
|
export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
20
19
|
passing: ClassifiedCheck[];
|
|
21
20
|
failing: TriagedCheck[];
|
|
22
21
|
skipped: ClassifiedCheck[];
|
|
23
22
|
filtered: ClassifiedCheck[];
|
|
24
|
-
ignored: ClassifiedCheck[];
|
|
25
23
|
}, seenMap: Map<string, {
|
|
26
24
|
seenAt: number;
|
|
27
25
|
}>, prNumber: number, cacheOpts?: AnnotationCacheOptions): Promise<{
|
|
@@ -29,6 +27,5 @@ export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
|
29
27
|
failing: TriagedCheck[];
|
|
30
28
|
skipped: ClassifiedCheck[];
|
|
31
29
|
filtered: ClassifiedCheck[];
|
|
32
|
-
ignored: ClassifiedCheck[];
|
|
33
30
|
}>;
|
|
34
31
|
export declare function annotationMarkerBody(a: CheckAnnotation): string;
|
|
@@ -12,7 +12,6 @@ export function checksWithActionableAnnotations(report) {
|
|
|
12
12
|
...report.checks.passing,
|
|
13
13
|
...report.checks.skipped,
|
|
14
14
|
...report.checks.filtered,
|
|
15
|
-
...(report.checks.ignored ?? []),
|
|
16
15
|
].filter(hasActionableAnnotation);
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
@@ -25,13 +24,7 @@ export function checksWithActionableAnnotations(report) {
|
|
|
25
24
|
*/
|
|
26
25
|
export function hasCheckDrivenActionableWork(checks, mergeStatusValue) {
|
|
27
26
|
return (checks.failing.length > 0 ||
|
|
28
|
-
[
|
|
29
|
-
...checks.failing,
|
|
30
|
-
...checks.passing,
|
|
31
|
-
...checks.skipped,
|
|
32
|
-
...checks.filtered,
|
|
33
|
-
...(checks.ignored ?? []),
|
|
34
|
-
].some(hasActionableAnnotation) ||
|
|
27
|
+
[...checks.failing, ...checks.passing, ...checks.skipped, ...checks.filtered].some(hasActionableAnnotation) ||
|
|
35
28
|
mergeStatusValue === "CONFLICTS");
|
|
36
29
|
}
|
|
37
30
|
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber, cacheOpts) {
|
|
@@ -40,7 +33,6 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber,
|
|
|
40
33
|
...buckets.passing,
|
|
41
34
|
...buckets.skipped,
|
|
42
35
|
...buckets.filtered,
|
|
43
|
-
...buckets.ignored,
|
|
44
36
|
].filter(shouldFetchCheckAnnotations);
|
|
45
37
|
const expectedCounts = new Map(candidates.flatMap((c) => c.id != null && c.annotationCount ? [[c.id, c.annotationCount]] : []));
|
|
46
38
|
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber, cacheOpts && { ...cacheOpts, expectedCounts });
|
|
@@ -57,7 +49,6 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber,
|
|
|
57
49
|
failing: apply(buckets.failing),
|
|
58
50
|
skipped: apply(buckets.skipped),
|
|
59
51
|
filtered: apply(buckets.filtered),
|
|
60
|
-
ignored: apply(buckets.ignored),
|
|
61
52
|
};
|
|
62
53
|
}
|
|
63
54
|
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber, cacheOpts) {
|
|
@@ -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
|
}
|
|
@@ -110,7 +110,7 @@ function reportChecks(report) {
|
|
|
110
110
|
...report.checks.inProgress,
|
|
111
111
|
...report.checks.skipped,
|
|
112
112
|
...report.checks.filtered,
|
|
113
|
-
...(report.checks.
|
|
113
|
+
...(report.checks.ignoredNames ?? []).map((name) => ({ name })),
|
|
114
114
|
...(report.checks.supersededNames ?? []).map((name) => ({ name })),
|
|
115
115
|
];
|
|
116
116
|
}
|
package/bin/commands/check.mjs
CHANGED
|
@@ -186,7 +186,6 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
186
186
|
const inProgress = classifiedChecks.filter((c) => c.category === "in_progress");
|
|
187
187
|
const skipped = classifiedChecks.filter((c) => c.category === "skipped");
|
|
188
188
|
const filtered = classifiedChecks.filter((c) => c.category === "filtered");
|
|
189
|
-
const ignored = classifiedChecks.filter((c) => c.category === "ignored");
|
|
190
189
|
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
191
190
|
? await triageFailingChecks(failing, repo, stateKey, triageBudget, classifiedChecks.filter((c) => c.category !== "failing"))
|
|
192
191
|
: failing;
|
|
@@ -199,8 +198,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
199
198
|
const ruleSet = await loadClassifyRules(getEffectiveCwd(), config.standardRules);
|
|
200
199
|
const classifyIndex = buildClassifyIndex(ruleSet, batchData);
|
|
201
200
|
const partition = partitionBatch(classifyIndex, batchData);
|
|
202
|
-
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered
|
|
203
|
-
const ignoredAnnotated = merged.ignored.filter((c) => (c.annotations?.length ?? 0) > 0);
|
|
201
|
+
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered }, seenMap, prNumber, { stateKey, headSha: batchData.headRefOid });
|
|
204
202
|
const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized && !partition.suppressedCommentIds.has(c.id));
|
|
205
203
|
const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => !canGenerateItemMutation(id, batchData.comments.find((comment) => comment.id === id)?.body ?? "", batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize, "minimize")));
|
|
206
204
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate["minimize-comments"], botUsernames, new Set(batchData.comments
|
|
@@ -261,7 +259,9 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
261
259
|
canGenerateItemMutation(review.id, review.body, batchData.viewerAuthorization?.viewerCanAdminister, "dismiss") ||
|
|
262
260
|
visibleChangesRequestedIds.has(review.id));
|
|
263
261
|
}).length;
|
|
264
|
-
|
|
262
|
+
const awaitingChecks = unreported.hasUnreportedRequired ||
|
|
263
|
+
(!verdict.hasChecks && unreported.actionsWorkflowInProgress === true);
|
|
264
|
+
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount, awaitingChecks);
|
|
265
265
|
status = enforceTransportReadiness(status, batchData);
|
|
266
266
|
// Resolve any pending mergeability refresh (and the resulting MERGED/CLOSED short-circuit)
|
|
267
267
|
// before deciding what to persist below — deferWhileQueued must see the same final,
|
|
@@ -269,7 +269,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
269
269
|
// snapshot. A conflict newly discovered by this REST read is exactly the kind of check-driven
|
|
270
270
|
// signal that keeps a queued PR out of the deferral path.
|
|
271
271
|
if (status === "READY" && !didRefreshMergeability) {
|
|
272
|
-
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount,
|
|
272
|
+
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, changesRequestedReviewCount, awaitingChecks);
|
|
273
273
|
batchData = refreshed.batchData;
|
|
274
274
|
mergeStatus = refreshed.mergeStatus;
|
|
275
275
|
status = refreshed.status;
|
|
@@ -299,7 +299,6 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
299
299
|
passing: merged.passing,
|
|
300
300
|
skipped: merged.skipped,
|
|
301
301
|
filtered: merged.filtered,
|
|
302
|
-
ignored: merged.ignored,
|
|
303
302
|
}, mergeStatus.status);
|
|
304
303
|
// A deferred tick shows no review items, so it is never capped; its seen markers stay unwritten.
|
|
305
304
|
const { buckets: budgeted, rollup: reviewItems, heldThreadReviewIds, } = applyReviewOutputBudget({
|
|
@@ -312,12 +311,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
312
311
|
editedComments,
|
|
313
312
|
}, deferWhileQueued ? 0 : (opts.reviewOutputMaxChars ?? 0));
|
|
314
313
|
if (opts.persistSeen !== false) {
|
|
315
|
-
const successfulAnnotations = [
|
|
316
|
-
...merged.passing,
|
|
317
|
-
...merged.skipped,
|
|
318
|
-
...merged.filtered,
|
|
319
|
-
...merged.ignored,
|
|
320
|
-
]
|
|
314
|
+
const successfulAnnotations = [...merged.passing, ...merged.skipped, ...merged.filtered]
|
|
321
315
|
.filter((check) => check.conclusion === "SUCCESS")
|
|
322
316
|
.flatMap((check) => check.annotations ?? []);
|
|
323
317
|
const deferredMarkSeen = deferWhileQueued
|
|
@@ -408,7 +402,6 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
408
402
|
inProgress: inProgress,
|
|
409
403
|
skipped: merged.skipped,
|
|
410
404
|
filtered: merged.filtered,
|
|
411
|
-
...(ignoredAnnotated.length > 0 && { ignored: ignoredAnnotated }),
|
|
412
405
|
filteredNames: verdict.filteredNames,
|
|
413
406
|
blockedByFilteredCheck,
|
|
414
407
|
...(verdict.ignoredNames.length > 0 && {
|
|
@@ -12,7 +12,7 @@ export declare function countReportedChecks(checks: {
|
|
|
12
12
|
inProgress: readonly unknown[];
|
|
13
13
|
skipped: readonly unknown[];
|
|
14
14
|
filtered: readonly unknown[];
|
|
15
|
-
|
|
15
|
+
ignoredNames?: readonly unknown[];
|
|
16
16
|
}): number;
|
|
17
17
|
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
18
18
|
export declare function insertConflictingHeadCiNote(instructions: string[], note: string | undefined): void;
|
|
@@ -18,7 +18,7 @@ export function countReportedChecks(checks) {
|
|
|
18
18
|
checks.inProgress.length +
|
|
19
19
|
checks.skipped.length +
|
|
20
20
|
checks.filtered.length +
|
|
21
|
-
(checks.
|
|
21
|
+
(checks.ignoredNames?.length ?? 0));
|
|
22
22
|
}
|
|
23
23
|
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
24
24
|
export function insertConflictingHeadCiNote(instructions, note) {
|
|
@@ -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]);
|
|
@@ -4,6 +4,7 @@ import { ShepherdError } from "../exit-codes.mjs";
|
|
|
4
4
|
import { getRepoInfo } from "../github/client.mjs";
|
|
5
5
|
import { getGithubTransport } from "../github/transport.mjs";
|
|
6
6
|
import { withApiTelemetryScope, summarizeApiTelemetry } from "../github/api-telemetry.mjs";
|
|
7
|
+
import { aggregateRerunCommand } from "../github/poll-summary-command.mjs";
|
|
7
8
|
import { fetchPollSummary } from "../github/poll-summary.mjs";
|
|
8
9
|
import { writeStackHandoffs } from "../github/stack-handoff.mjs";
|
|
9
10
|
import { sleep } from "../util/sleep.mjs";
|
|
@@ -19,7 +20,7 @@ export function runPollSummary(opts) {
|
|
|
19
20
|
return withApiTelemetryScope(async () => {
|
|
20
21
|
const tick = await readPollSummaryTick(opts);
|
|
21
22
|
await handOffRoutedLayers(tick);
|
|
22
|
-
return attachUsage(tick.result, opts
|
|
23
|
+
return attachUsage(tick.result, opts);
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
26
|
export function runAggregatePoll(opts) {
|
|
@@ -60,7 +61,7 @@ async function readPollSummaryTick(opts) {
|
|
|
60
61
|
prs: fetched.prs,
|
|
61
62
|
...(fetched.stackAncestry?.length && { stackAncestry: fetched.stackAncestry }),
|
|
62
63
|
...(fetched.allowedMergeMethods && { allowedMergeMethods: fetched.allowedMergeMethods }),
|
|
63
|
-
}, opts.merge === true);
|
|
64
|
+
}, opts.merge === true, aggregateRerunCommand(`${repo.owner}/${repo.name}`, fetched.selection, opts));
|
|
64
65
|
const result = await applyStackStallGuard(planned, repo, opts.stallTimeoutSeconds ?? loadConfig().iterate["stall-timeout-minutes"] * 60);
|
|
65
66
|
return { result, repo, ...(fetched.handoffSource && { handoffSource: fetched.handoffSource }) };
|
|
66
67
|
}
|
|
@@ -104,7 +105,7 @@ async function runAggregatePollCore(opts) {
|
|
|
104
105
|
stackMergeable: true,
|
|
105
106
|
instructions: ["1. Stop — every stack layer is merged."],
|
|
106
107
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
107
|
-
}, opts
|
|
108
|
+
}, opts);
|
|
108
109
|
}
|
|
109
110
|
return attachUsage({
|
|
110
111
|
...explicit,
|
|
@@ -116,7 +117,7 @@ async function runAggregatePollCore(opts) {
|
|
|
116
117
|
`1. Native stack #${last.selection.stackNumber} disappeared before every tracked layer merged. Stop and ask the stack owner to reconcile the remaining PRs; do not claim the stack is complete.`,
|
|
117
118
|
],
|
|
118
119
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
119
|
-
}, opts
|
|
120
|
+
}, opts);
|
|
120
121
|
}
|
|
121
122
|
const early = await rateLimitRetry.wait(error, {
|
|
122
123
|
untilTerminal: opts.untilTerminal === true,
|
|
@@ -127,7 +128,7 @@ async function runAggregatePollCore(opts) {
|
|
|
127
128
|
onAllTerminal: (pulls) => aggregateCancelFromPulls(last, opts.targetRepository, pulls),
|
|
128
129
|
});
|
|
129
130
|
if (early) {
|
|
130
|
-
return presentProbedCancel(pendingQuotaWarning ? { ...early, quotaWarning: pendingQuotaWarning } : early, opts
|
|
131
|
+
return presentProbedCancel(pendingQuotaWarning ? { ...early, quotaWarning: pendingQuotaWarning } : early, opts);
|
|
131
132
|
}
|
|
132
133
|
continue;
|
|
133
134
|
}
|
|
@@ -148,14 +149,14 @@ async function runAggregatePollCore(opts) {
|
|
|
148
149
|
...last,
|
|
149
150
|
reason: "all_terminal",
|
|
150
151
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
151
|
-
}, opts
|
|
152
|
+
}, opts);
|
|
152
153
|
}
|
|
153
154
|
if (immediate) {
|
|
154
155
|
return attachUsage({
|
|
155
156
|
...last,
|
|
156
157
|
reason: "actionable",
|
|
157
158
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
158
|
-
}, opts
|
|
159
|
+
}, opts);
|
|
159
160
|
}
|
|
160
161
|
if (hasFix) {
|
|
161
162
|
if (debounceMs === 0 || Date.now() >= (debounceUntil ??= Date.now() + debounceMs)) {
|
|
@@ -164,13 +165,13 @@ async function runAggregatePollCore(opts) {
|
|
|
164
165
|
...last,
|
|
165
166
|
reason: "actionable",
|
|
166
167
|
...(pendingQuotaWarning && { quotaWarning: pendingQuotaWarning }),
|
|
167
|
-
}, opts
|
|
168
|
+
}, opts);
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
else {
|
|
171
172
|
debounceUntil = null;
|
|
172
173
|
if (opts.untilTerminal && pendingQuotaWarning) {
|
|
173
|
-
return attachUsage({ ...last, quotaWarning: pendingQuotaWarning }, opts
|
|
174
|
+
return attachUsage({ ...last, quotaWarning: pendingQuotaWarning }, opts);
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
const elapsedMs = Date.now() - start;
|
|
@@ -180,7 +181,7 @@ async function runAggregatePollCore(opts) {
|
|
|
180
181
|
if (!opts.untilTerminal && debounceUntil === null) {
|
|
181
182
|
const remainingMs = timeoutMs - elapsedMs;
|
|
182
183
|
if (remainingMs <= 0 || remainingMs + TIMER_DRIFT_TOLERANCE_MS < sleepMs) {
|
|
183
|
-
return attachUsage({ ...last, reason: "timeout" }, opts
|
|
184
|
+
return attachUsage({ ...last, reason: "timeout" }, opts);
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
const statusSignature = summaryStatusSignature(last);
|
|
@@ -199,15 +200,15 @@ function writeAggregateRuleAutoResolve(result) {
|
|
|
199
200
|
if (item.ruleAutoResolve?.summary)
|
|
200
201
|
process.stderr.write(`#${item.pr} ${item.ruleAutoResolve.summary}\n`);
|
|
201
202
|
}
|
|
202
|
-
function presentProbedCancel(result,
|
|
203
|
+
function presentProbedCancel(result, opts) {
|
|
203
204
|
// The stack planner already cancels an all-merged stack and escalates a
|
|
204
205
|
// closed-unmerged layer. Do not rewrite that into a successful stop.
|
|
205
|
-
return attachUsage({ ...result, reason: "all_terminal" },
|
|
206
|
+
return attachUsage({ ...result, reason: "all_terminal" }, opts);
|
|
206
207
|
}
|
|
207
208
|
function isMissingStack(error) {
|
|
208
209
|
return (error instanceof ShepherdError && error.message.includes("not part of a native GitHub stack"));
|
|
209
210
|
}
|
|
210
|
-
function attachUsage(result,
|
|
211
|
+
function attachUsage(result, opts) {
|
|
211
212
|
const apiUsage = summarizeApiTelemetry();
|
|
212
213
|
const enriched = apiUsage ? { ...result, apiUsage } : result;
|
|
213
214
|
// A disappeared native stack is an aggregate-level terminal handoff. Keep
|
|
@@ -216,5 +217,5 @@ function attachUsage(result, mergeRequested) {
|
|
|
216
217
|
if (enriched.selection.kind === "stack" && enriched.nextAction === "escalate") {
|
|
217
218
|
return enriched;
|
|
218
219
|
}
|
|
219
|
-
return withPollSummaryInstructions(enriched,
|
|
220
|
+
return withPollSummaryInstructions(enriched, opts.merge === true, aggregateRerunCommand(enriched.repo, enriched.selection, opts));
|
|
220
221
|
}
|
|
@@ -10,6 +10,6 @@ export declare function refreshUnknownMergeability(prNumber: number, repo: RepoI
|
|
|
10
10
|
batchData: BatchPrData;
|
|
11
11
|
didRefresh: boolean;
|
|
12
12
|
}>;
|
|
13
|
-
export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number,
|
|
13
|
+
export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number, awaitingChecks?: boolean): Promise<ReadyMergeabilityRefresh>;
|
|
14
14
|
export declare function isBlockedByFilteredCheck(mergeStatus: MergeStatusResult, verdict: CiVerdict): boolean;
|
|
15
15
|
export {};
|
|
@@ -8,10 +8,10 @@ export async function refreshUnknownMergeability(prNumber, repo, batchData) {
|
|
|
8
8
|
}
|
|
9
9
|
return { batchData: await readMergeability(prNumber, repo, batchData), didRefresh: true };
|
|
10
10
|
}
|
|
11
|
-
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount,
|
|
11
|
+
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount, awaitingChecks = false) {
|
|
12
12
|
const refreshedBatchData = await readMergeability(prNumber, repo, batchData);
|
|
13
13
|
const mergeStatus = deriveMergeStatus(refreshedBatchData);
|
|
14
|
-
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount,
|
|
14
|
+
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount, awaitingChecks);
|
|
15
15
|
return { batchData: refreshedBatchData, mergeStatus, status };
|
|
16
16
|
}
|
|
17
17
|
export function isBlockedByFilteredCheck(mergeStatus, verdict) {
|
|
@@ -8,7 +8,7 @@ export interface StackPlan {
|
|
|
8
8
|
idle?: PollSummaryItem[];
|
|
9
9
|
instructions: string[];
|
|
10
10
|
}
|
|
11
|
-
export declare function planPrefixDrain(result: PollSummaryResult, mergeRequested: boolean): StackPlan | undefined;
|
|
11
|
+
export declare function planPrefixDrain(result: PollSummaryResult, mergeRequested: boolean, rerunCommand?: string): StackPlan | undefined;
|
|
12
12
|
/**
|
|
13
13
|
* A merge-requested, fully ready stack whose bottom open layer still targets
|
|
14
14
|
* the merged layer below it until GitHub retargets it onto the stack base.
|
|
@@ -2,7 +2,7 @@ import { findHumanHandoffs, appendHumanHandoffInstructions } from "./stack-hando
|
|
|
2
2
|
export { findHumanHandoffs, appendHumanHandoffInstructions } from "./stack-handoffs.mjs";
|
|
3
3
|
import { stackMergeFlag } from "./stack-merge-flag.mjs";
|
|
4
4
|
import { stackLayerBlockReason } from "./stack-layer-readiness.mjs";
|
|
5
|
-
import { splitStackWork } from "./stack-work.mjs";
|
|
5
|
+
import { splitStackWork, stackRerunTarget } from "./stack-work.mjs";
|
|
6
6
|
import { getGithubTransport } from "../github/transport.mjs";
|
|
7
7
|
import { buildMergeCommandPlan, renderMergeCommand } from "./iterate/merge.mjs";
|
|
8
8
|
function readyPrefixTop(result) {
|
|
@@ -28,7 +28,7 @@ function readyPrefixTop(result) {
|
|
|
28
28
|
}
|
|
29
29
|
return top;
|
|
30
30
|
}
|
|
31
|
-
export function planPrefixDrain(result, mergeRequested) {
|
|
31
|
+
export function planPrefixDrain(result, mergeRequested, rerunCommand) {
|
|
32
32
|
if (!mergeRequested)
|
|
33
33
|
return undefined;
|
|
34
34
|
const top = readyPrefixTop(result);
|
|
@@ -92,7 +92,7 @@ export function planPrefixDrain(result, mergeRequested) {
|
|
|
92
92
|
const handoffs = findHumanHandoffs(result);
|
|
93
93
|
if (handoffs)
|
|
94
94
|
appendHumanHandoffInstructions(instructions, handoffs, false);
|
|
95
|
-
instructions.push(`${instructions.length + 1}. After the merge attempt, rerun this same
|
|
95
|
+
instructions.push(`${instructions.length + 1}. After the merge attempt, rerun ${rerunCommand ? stackRerunTarget(rerunCommand) : "this same `stack --merge` command"}; GitHub retargets the next layer onto \`${top.stack.baseRefName}\`. Shepherd any layer that GitHub rejects or ejects.`);
|
|
96
96
|
return {
|
|
97
97
|
action: "merge",
|
|
98
98
|
stackMergeable: gaps.length === 0 && open.every(isStackLayerReady),
|