pr-shepherd 0.52.0 → 0.52.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/README.md +11 -8
- package/bin/cli/help-command-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +4 -3
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -2
- package/bin/cli/help.d.mts +2 -2
- package/bin/cli/iterate-instructions.mjs +41 -0
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/poll-summary-formatter.mjs +3 -1
- package/bin/cli/runner.d.mts +1 -0
- package/bin/cli/runner.mjs +3 -1
- package/bin/commands/check.mjs +13 -4
- package/bin/commands/clean.mjs +7 -13
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +11 -7
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/fix-code.mjs +6 -1
- package/bin/commands/iterate/helpers.d.mts +0 -1
- package/bin/commands/iterate/helpers.mjs +0 -15
- package/bin/commands/iterate/index.mjs +57 -27
- package/bin/commands/iterate/merge-state.mjs +6 -4
- package/bin/commands/iterate/native-stack-rebase.d.mts +34 -0
- package/bin/commands/iterate/native-stack-rebase.mjs +43 -0
- package/bin/commands/iterate/parent-first.d.mts +18 -2
- package/bin/commands/iterate/parent-first.mjs +45 -32
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +7 -12
- package/bin/commands/iterate/stale-ancestry.d.mts +1 -1
- package/bin/commands/iterate/stale-ancestry.mjs +10 -7
- package/bin/commands/iterate/stall.mjs +40 -3
- package/bin/commands/poll-progress.d.mts +5 -1
- package/bin/commands/poll-progress.mjs +7 -1
- package/bin/commands/poll-summary-instructions.d.mts +6 -1
- package/bin/commands/poll-summary-instructions.mjs +51 -87
- package/bin/commands/poll-summary.mjs +4 -2
- package/bin/commands/poll.mjs +3 -1
- package/bin/commands/ready-delay.d.mts +9 -4
- package/bin/commands/ready-delay.mjs +34 -20
- package/bin/commands/shepherd-journal.mjs +4 -1
- package/bin/commands/stack-drain.d.mts +35 -0
- package/bin/commands/stack-drain.mjs +129 -0
- package/bin/commands/stack-layer-readiness.d.mts +7 -0
- package/bin/commands/stack-layer-readiness.mjs +35 -0
- package/bin/commands/stack-stall.d.mts +14 -0
- package/bin/commands/stack-stall.mjs +69 -0
- package/bin/commands/stack-work.d.mts +32 -0
- package/bin/commands/stack-work.mjs +39 -0
- package/bin/config/load.d.mts +2 -0
- package/bin/config/load.mjs +10 -0
- package/bin/config.json +1 -0
- package/bin/github/batch-parsers.mjs +1 -1
- package/bin/github/batch-raw-rules.d.mts +0 -3
- package/bin/github/batch-raw-types.d.mts +2 -0
- package/bin/github/errors.d.mts +5 -0
- package/bin/github/errors.mjs +4 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +8 -2
- package/bin/github/gql/poll-stack-topology.gql +38 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +34 -0
- package/bin/github/gql/poll-summary-check-page.gql +23 -0
- package/bin/github/gql/poll-summary-fragment.gql +5 -62
- package/bin/github/gql/pr-merge-policy.gql +0 -3
- package/bin/github/merge-queue-checks.mjs +10 -1
- package/bin/github/poll-summary-check-hydration.d.mts +12 -0
- package/bin/github/poll-summary-check-hydration.mjs +55 -0
- package/bin/github/poll-summary-fingerprint.mjs +24 -4
- package/bin/github/poll-summary-projector.mjs +8 -7
- package/bin/github/poll-summary-queue-removal.mjs +10 -1
- package/bin/github/poll-summary-raw.d.mts +19 -31
- package/bin/github/poll-summary-route.mjs +8 -5
- package/bin/github/poll-summary.mjs +16 -73
- package/bin/github/queries.d.mts +7 -0
- package/bin/github/queries.mjs +9 -1
- package/bin/github/queue-removal-freshness.d.mts +16 -0
- package/bin/github/queue-removal-freshness.mjs +26 -0
- package/bin/github/stack-read.d.mts +34 -0
- package/bin/github/stack-read.mjs +92 -0
- package/bin/log/log-file.d.mts +1 -1
- package/bin/log/log-file.mjs +4 -17
- package/bin/state/base.d.mts +18 -1
- package/bin/state/base.mjs +65 -13
- package/bin/state/fix-attempts.d.mts +1 -1
- package/bin/state/fix-attempts.mjs +1 -1
- package/bin/state/graphql-quota-warnings.mjs +2 -6
- package/bin/state/iterate-stall.d.mts +7 -15
- package/bin/state/iterate-stall.mjs +6 -64
- package/bin/state/rest-cache.d.mts +1 -1
- package/bin/state/rest-cache.mjs +1 -1
- package/bin/state/stack-stall.d.mts +16 -0
- package/bin/state/stack-stall.mjs +12 -0
- package/bin/state/stall-state-store.d.mts +37 -0
- package/bin/state/stall-state-store.mjs +74 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +1 -1
- package/bin/types/iterate.d.mts +2 -1
- package/bin/types/merge-requirements.d.mts +19 -0
- package/bin/types/poll-summary.d.mts +5 -0
- package/bin/types/report.d.mts +1 -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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { runCheck } from "../check.mjs";
|
|
3
|
-
import { updateReadyDelay } from "../ready-delay.mjs";
|
|
3
|
+
import { clearReadyDelay, updateReadyDelay } from "../ready-delay.mjs";
|
|
4
4
|
import { getCurrentPrNumber } from "../../github/client.mjs";
|
|
5
5
|
import { loadConfig } from "../../config/load.mjs";
|
|
6
6
|
import { EXIT, ShepherdError } from "../../exit-codes.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { buildWaitLog, buildTerminalCancelResult, blockedCancelNote } from "./helpers.mjs";
|
|
8
8
|
import { classifyReviewSummaries } from "./classify.mjs";
|
|
9
9
|
import { applyStallGuard } from "./stall.mjs";
|
|
10
10
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
@@ -22,7 +22,7 @@ import { fingerprintRawSummaryPr } from "../../github/poll-summary-fingerprint.m
|
|
|
22
22
|
import { currentQueueRemovalEvent } from "../../github/poll-summary-queue-removal.mjs";
|
|
23
23
|
import { isCurrentSummaryReady } from "../../github/poll-summary-readiness.mjs";
|
|
24
24
|
import { clearReadyReceipt, isReadyReceiptCurrent, readReadyReceipt, writeReadyReceipt, } from "../../state/ready-receipts.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { findParentMarkReadyBlock, heldByLowerLayer, stackDraftHold } from "./parent-first.mjs";
|
|
26
26
|
import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
|
|
27
27
|
export function runIterate(opts) {
|
|
28
28
|
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
@@ -51,8 +51,10 @@ async function runIterateCore(opts) {
|
|
|
51
51
|
throw new ShepherdError(`Unexpected repo format: "${report.repo}" (expected "owner/name")`, EXIT.DATAERR);
|
|
52
52
|
}
|
|
53
53
|
const stallKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
54
|
+
const receiptKey = { owner: repoOwner, repo: repoName, pr: report.pr };
|
|
54
55
|
if (report.mergeStatus.state !== "OPEN") {
|
|
55
|
-
await
|
|
56
|
+
await clearReadyDelay(report.pr, repoOwner, repoName);
|
|
57
|
+
await clearReadyReceipt(receiptKey);
|
|
56
58
|
await clearStallState(stallKey);
|
|
57
59
|
return buildTerminalCancelResult(report);
|
|
58
60
|
}
|
|
@@ -74,19 +76,22 @@ async function runIterateCore(opts) {
|
|
|
74
76
|
if (unminimized.length > 0)
|
|
75
77
|
reviewSummaryIds = [...reviewSummaryIds, ...unminimized];
|
|
76
78
|
}
|
|
77
|
-
|
|
79
|
+
// Hidden PR comments surface once so the agent can acknowledge them, but
|
|
80
|
+
// they are not readiness evidence: bots keep editing hidden notices after a
|
|
81
|
+
// PR settles. They never restart the ready-delay or void a READY receipt.
|
|
82
|
+
const hasReadinessWork = report.threads.actionable.length > 0 ||
|
|
78
83
|
report.threads.resolutionOnly.length > 0 ||
|
|
79
84
|
report.threads.firstLook.length > 0 ||
|
|
80
85
|
(report.threads.ruleAutoResolveIds?.length ?? 0) > 0 ||
|
|
81
86
|
report.comments.actionable.length > 0 ||
|
|
82
87
|
(report.comments.minimizeIds?.length ?? 0) > 0 ||
|
|
83
|
-
report.comments.firstLook.length > 0 ||
|
|
84
88
|
report.changesRequestedReviews.length > 0 ||
|
|
85
89
|
hasCheckDrivenActionableWork(report.checks, report.mergeStatus.status) ||
|
|
86
90
|
reviewSummaryIds.length > 0 ||
|
|
87
91
|
firstLookSummaries.length > 0 ||
|
|
88
92
|
editedSummaries.length > 0 ||
|
|
89
93
|
(config.iterate.minimizeApprovals && surfacedApprovals.length > 0);
|
|
94
|
+
const hasActionableWork = hasReadinessWork || report.comments.firstLook.length > 0;
|
|
90
95
|
const activeMerge = Boolean(opts.merge && (report.mergeQueue?.inQueue || report.mergeQueue?.autoMergeRequest));
|
|
91
96
|
const staleAncestry = await findStaleNativeStackAncestry(report, {
|
|
92
97
|
owner: repoOwner,
|
|
@@ -94,15 +99,17 @@ async function runIterateCore(opts) {
|
|
|
94
99
|
});
|
|
95
100
|
const isCleanReadyState = report.status === "READY" &&
|
|
96
101
|
!report.mergeStatus.isDraft &&
|
|
97
|
-
!
|
|
102
|
+
!hasReadinessWork &&
|
|
98
103
|
!activeMerge &&
|
|
99
104
|
staleAncestry === null;
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
const receiptCurrent = await revalidateReadyReceipt(receiptKey, report, hasReadinessWork);
|
|
106
|
+
const headSha = report.headSha ?? "unknown";
|
|
107
|
+
// The elapsed marker survives a hidden-comment acknowledgement tick and is
|
|
108
|
+
// consumed only when this tick cancels or merges. A receipt that is still
|
|
109
|
+
// current already proves the delay elapsed for this exact head, base, and
|
|
110
|
+
// readiness evidence, so a rerun (e.g. with --merge) does not wait again.
|
|
111
|
+
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName, { headSha, alreadyElapsed: receiptCurrent });
|
|
104
112
|
const base = buildIterateBase(report, readyState);
|
|
105
|
-
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
106
113
|
// Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
|
|
107
114
|
// GitHub itself is already acting on — the queue will eject the PR for these regardless of
|
|
108
115
|
// what Shepherd does, so they always surface immediately. Only review threads/comments/
|
|
@@ -168,27 +175,28 @@ async function runIterateCore(opts) {
|
|
|
168
175
|
const canMarkReady = report.status === "READY" &&
|
|
169
176
|
report.mergeStatus.isDraft &&
|
|
170
177
|
!report.mergeStatus.blockingBotReviewInProgress;
|
|
171
|
-
const
|
|
172
|
-
? await
|
|
173
|
-
:
|
|
174
|
-
const
|
|
178
|
+
const parentBlock = canMarkReady
|
|
179
|
+
? await findParentMarkReadyBlock(report, { owner: repoOwner, name: repoName })
|
|
180
|
+
: undefined;
|
|
181
|
+
const autoMarkReady = !opts.noAutoMarkReady && config.actions.autoMarkReady;
|
|
182
|
+
const markReadyResult = await markReadyIfAuthorized(canMarkReady && !parentBlock && autoMarkReady, base, report);
|
|
175
183
|
if (markReadyResult)
|
|
176
184
|
return markReadyResult;
|
|
177
185
|
if (readyState.shouldCancel && !report.mergeStatus.isDraft) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (!receiptWritten) {
|
|
186
|
+
const receiptWritten = receiptCurrent || (await recordReadyReceipt(receiptKey, report));
|
|
187
|
+
// Aggregate --stack routing trusts only a layer's receipt, so an unwritten
|
|
188
|
+
// one keeps the elapsed marker and retries next tick. A one-PR receipt
|
|
189
|
+
// only lets a rerun skip the wait, so its failure never changes the action.
|
|
190
|
+
if (!receiptWritten && report.mergeStatus.mergeRequirements?.stack !== undefined) {
|
|
183
191
|
const receiptWait = {
|
|
184
192
|
...base,
|
|
185
193
|
action: "wait",
|
|
186
194
|
shouldCancel: false,
|
|
187
|
-
remainingSeconds: readyDelaySeconds,
|
|
188
195
|
log: `WAIT: PR #${base.pr} reached ready-delay but its stack readiness receipt could not be persisted`,
|
|
189
196
|
};
|
|
190
197
|
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, receiptWait, report, reviewSummaryIds);
|
|
191
198
|
}
|
|
199
|
+
await clearReadyDelay(report.pr, repoOwner, repoName);
|
|
192
200
|
await clearStallState(stallKey);
|
|
193
201
|
const mergeResult = buildReadyMergeOutcome(opts.merge, true, base, report);
|
|
194
202
|
if (mergeResult)
|
|
@@ -201,7 +209,20 @@ async function runIterateCore(opts) {
|
|
|
201
209
|
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
202
210
|
};
|
|
203
211
|
}
|
|
204
|
-
|
|
212
|
+
const hold = stackDraftHold(report, autoMarkReady, parentBlock);
|
|
213
|
+
const wait = {
|
|
214
|
+
...base,
|
|
215
|
+
action: "wait",
|
|
216
|
+
log: buildWaitLog(base),
|
|
217
|
+
...(hold && { stackDraftHold: hold }),
|
|
218
|
+
};
|
|
219
|
+
// The lower layer's own session owns progress here, so this draft cannot
|
|
220
|
+
// stall; its stall clock restarts once that layer releases it.
|
|
221
|
+
if (heldByLowerLayer(wait)) {
|
|
222
|
+
await clearStallState(stallKey);
|
|
223
|
+
return wait;
|
|
224
|
+
}
|
|
225
|
+
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, wait, report, reviewSummaryIds);
|
|
205
226
|
}
|
|
206
227
|
async function recordReadyReceipt(key, report) {
|
|
207
228
|
if (report.status !== "READY" ||
|
|
@@ -245,10 +266,11 @@ async function recordReadyReceipt(key, report) {
|
|
|
245
266
|
return false;
|
|
246
267
|
}
|
|
247
268
|
}
|
|
248
|
-
|
|
269
|
+
/** Clear a stale READY receipt; return whether a current receipt remains. */
|
|
270
|
+
async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
249
271
|
const receipt = await readReadyReceipt(key);
|
|
250
272
|
if (!receipt)
|
|
251
|
-
return;
|
|
273
|
+
return false;
|
|
252
274
|
const retainQueuedReceipt = report.mergeQueue?.inQueue === true &&
|
|
253
275
|
report.mergeStatus.state === "OPEN" &&
|
|
254
276
|
!report.mergeStatus.isDraft &&
|
|
@@ -257,9 +279,9 @@ async function invalidateStaleReadyReceipt(key, report, hasActionableWork) {
|
|
|
257
279
|
report.mergeStatus.isDraft ||
|
|
258
280
|
!report.headSha ||
|
|
259
281
|
!report.baseRefOid ||
|
|
260
|
-
(!retainQueuedReceipt && (report.status !== "READY" ||
|
|
282
|
+
(!retainQueuedReceipt && (report.status !== "READY" || hasReadinessWork))) {
|
|
261
283
|
await clearReadyReceipt(key);
|
|
262
|
-
return;
|
|
284
|
+
return false;
|
|
263
285
|
}
|
|
264
286
|
try {
|
|
265
287
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
@@ -268,7 +290,12 @@ async function invalidateStaleReadyReceipt(key, report, hasActionableWork) {
|
|
|
268
290
|
// views still place the PR in the queue.
|
|
269
291
|
const queuedBaseAdvanced = retainQueuedReceipt && raw.isInMergeQueue;
|
|
270
292
|
const fingerprint = fingerprintRawSummaryPr(queuedBaseAdvanced ? { ...raw, baseRefOid: receipt.baseRefOid } : raw);
|
|
293
|
+
// The fresh snapshot must name the commits this tick evaluated, or the
|
|
294
|
+
// receipt would vouch for a head or base the report never saw.
|
|
295
|
+
const sameCommits = raw.headRefOid === report.headSha &&
|
|
296
|
+
(queuedBaseAdvanced || raw.baseRefOid === report.baseRefOid);
|
|
271
297
|
if (fingerprint === null ||
|
|
298
|
+
!sameCommits ||
|
|
272
299
|
!isReadyReceiptCurrent(receipt, {
|
|
273
300
|
headRefOid: raw.headRefOid,
|
|
274
301
|
baseRefOid: queuedBaseAdvanced ? receipt.baseRefOid : raw.baseRefOid,
|
|
@@ -277,10 +304,13 @@ async function invalidateStaleReadyReceipt(key, report, hasActionableWork) {
|
|
|
277
304
|
isDraft: raw.isDraft,
|
|
278
305
|
})) {
|
|
279
306
|
await clearReadyReceipt(key);
|
|
307
|
+
return false;
|
|
280
308
|
}
|
|
309
|
+
return true;
|
|
281
310
|
}
|
|
282
311
|
catch {
|
|
283
312
|
// Fail closed: an unreadable current snapshot cannot validate old evidence.
|
|
284
313
|
await clearReadyReceipt(key);
|
|
314
|
+
return false;
|
|
285
315
|
}
|
|
286
316
|
}
|
|
@@ -2,9 +2,11 @@ import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
|
2
2
|
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
3
|
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
4
4
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
|
+
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
6
|
+
import { inlineCode } from "../../util/markdown.mjs";
|
|
5
7
|
/**
|
|
6
|
-
* A one-PR poll cannot prove
|
|
7
|
-
* aggregate stack selector, which
|
|
8
|
+
* A one-PR poll cannot prove the layers below it merged or that its PR number is a safe merge
|
|
9
|
+
* selector. Route it through the aggregate stack selector, which merges one bottom layer at a time.
|
|
8
10
|
*/
|
|
9
11
|
function buildStackedRouteResult(base, report, stack) {
|
|
10
12
|
const prUrl = formatPrUrl(report.repo, report.pr);
|
|
@@ -22,14 +24,14 @@ function buildStackedRouteResult(base, report, stack) {
|
|
|
22
24
|
checks: [],
|
|
23
25
|
changesRequestedReviews: [],
|
|
24
26
|
resolveCommand: {
|
|
25
|
-
argv: ["
|
|
27
|
+
argv: buildPrShepherdCommand(["apply", "review", prUrl]).argv,
|
|
26
28
|
requiresHeadSha: false,
|
|
27
29
|
requiresDismissMessage: false,
|
|
28
30
|
hasMutations: false,
|
|
29
31
|
},
|
|
30
32
|
instructions: [
|
|
31
33
|
`PR #${report.pr} is layer ${stack.position} of ${stack.size} in native stack #${stack.number}; do not run \`gh pr merge\` for this layer.`,
|
|
32
|
-
`Run
|
|
34
|
+
`Run ${inlineCode(buildPrShepherdCommand(["--stack", prUrl, "--until-terminal", "--merge"]).text)} to reconcile the stack and run each bottom-layer merge command it emits.`,
|
|
33
35
|
],
|
|
34
36
|
inProgressRunIds: [],
|
|
35
37
|
protectedRuns: [],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { StackStatus } from "../../types.mts";
|
|
2
|
+
/**
|
|
3
|
+
* Where a native-stack rebase starts: an upper layer rebases from its parent stack branch
|
|
4
|
+
* without touching trunk; the bottom layer rebases the whole stack onto trunk.
|
|
5
|
+
*/
|
|
6
|
+
export type NativeStackRebaseStart = {
|
|
7
|
+
parentBranch: string;
|
|
8
|
+
} | {
|
|
9
|
+
bottomPr: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* One gh-stack rebase step. A native stack layer must not be rebased or merged from its base
|
|
13
|
+
* branch alone: that rewrites one branch and strands every layer above it.
|
|
14
|
+
*
|
|
15
|
+
* `gh stack` rebases the stack it tracks locally and `gh stack push` publishes those local
|
|
16
|
+
* layers, so the step first imports the stack by its number (a bare number resolves as a
|
|
17
|
+
* stack number before a PR number) and checks each local layer against its PR head.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildNativeStackRebaseInstruction(repo: string, stackNumber: number, start: NativeStackRebaseStart): string;
|
|
20
|
+
/**
|
|
21
|
+
* The stack-aware branch update for a native stack layer (a conflict, or a behind branch whose
|
|
22
|
+
* workflow keeps failing); undefined outside a stack.
|
|
23
|
+
* A layer whose PR targets the stack's trunk (`stack.baseRefName`) is the bottom open layer —
|
|
24
|
+
* position 1, or a higher layer GitHub retargeted after every layer below it merged. Any other
|
|
25
|
+
* layer is an upper layer whose parent is its own PR base branch.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
28
|
+
number: number;
|
|
29
|
+
baseBranch: string;
|
|
30
|
+
}, stack: StackStatus | undefined): string | undefined;
|
|
31
|
+
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
32
|
+
export declare function buildConflictInstruction(stackRebase: string | undefined): string;
|
|
33
|
+
/** Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack. */
|
|
34
|
+
export declare function buildBranchPushInstruction(stackRebase: string | undefined, hasConflicts: boolean, mutationSuffix: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One gh-stack rebase step. A native stack layer must not be rebased or merged from its base
|
|
3
|
+
* branch alone: that rewrites one branch and strands every layer above it.
|
|
4
|
+
*
|
|
5
|
+
* `gh stack` rebases the stack it tracks locally and `gh stack push` publishes those local
|
|
6
|
+
* layers, so the step first imports the stack by its number (a bare number resolves as a
|
|
7
|
+
* stack number before a PR number) and checks each local layer against its PR head.
|
|
8
|
+
*/
|
|
9
|
+
export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
10
|
+
const [checkout, command] = "parentBranch" in start
|
|
11
|
+
? [
|
|
12
|
+
`check out the parent stack branch \`${start.parentBranch}\``,
|
|
13
|
+
"gh stack rebase --upstack --no-trunk",
|
|
14
|
+
]
|
|
15
|
+
: [`check out the head branch of PR #${start.bottomPr}`, "gh stack rebase"];
|
|
16
|
+
const prepare = `if \`gh stack\` does not track stack #${stackNumber} locally, import it with \`gh stack checkout ${stackNumber}\`, then confirm every layer's local branch is at its PR's head commit — a stale local layer would overwrite that PR's newer commits on push`;
|
|
17
|
+
return `From a clean checkout of \`${repo}\`, ${prepare}. Then ${checkout} and run \`${command}\`; if it stops on a conflict, resolve it and run \`gh stack rebase --continue\`.`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The stack-aware branch update for a native stack layer (a conflict, or a behind branch whose
|
|
21
|
+
* workflow keeps failing); undefined outside a stack.
|
|
22
|
+
* A layer whose PR targets the stack's trunk (`stack.baseRefName`) is the bottom open layer —
|
|
23
|
+
* position 1, or a higher layer GitHub retargeted after every layer below it merged. Any other
|
|
24
|
+
* layer is an upper layer whose parent is its own PR base branch.
|
|
25
|
+
*/
|
|
26
|
+
export function buildNativeStackLayerRebase(repo, pr, stack) {
|
|
27
|
+
if (!stack)
|
|
28
|
+
return undefined;
|
|
29
|
+
return buildNativeStackRebaseInstruction(repo, stack.number, pr.baseBranch === stack.baseRefName ? { bottomPr: pr.number } : { parentBranch: pr.baseBranch });
|
|
30
|
+
}
|
|
31
|
+
/** Point at the conflicts, and at the stack rebase when the PR is a native stack layer. */
|
|
32
|
+
export function buildConflictInstruction(stackRebase) {
|
|
33
|
+
const pointer = "The branch has merge conflicts (see `**branch**` above).";
|
|
34
|
+
return stackRebase ? `${pointer} ${stackRebase}` : `${pointer} Resolve them before committing.`;
|
|
35
|
+
}
|
|
36
|
+
/** Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack. */
|
|
37
|
+
export function buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix) {
|
|
38
|
+
if (stackRebase)
|
|
39
|
+
return `Commit any remaining changes on the PR head branch and push the rewritten stack with \`gh stack push\`${mutationSuffix}.`;
|
|
40
|
+
return hasConflicts
|
|
41
|
+
? `Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`
|
|
42
|
+
: "Push the updated PR head branch before iterating immediately.";
|
|
43
|
+
}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import type { RepoInfo } from "../../github/client.mts";
|
|
2
|
-
import type { ShepherdReport } from "../../types.mts";
|
|
2
|
+
import type { IterateResult, ShepherdReport, StackDraftHold, StackLowerLayerBlock } from "../../types.mts";
|
|
3
|
+
/**
|
|
4
|
+
* What keeps a draft child from being marked ready: the lowest blocking lower layer,
|
|
5
|
+
* or a stack read that could not attribute the block to one.
|
|
6
|
+
*/
|
|
7
|
+
export type ParentMarkReadyBlock = StackLowerLayerBlock | "unverifiable";
|
|
3
8
|
/**
|
|
4
9
|
* Draft children may only be converted after their immediate parent has
|
|
5
10
|
* independently completed a one-PR ready-delay and the stack boundary is
|
|
6
11
|
* still linear. A failed or incomplete stack read blocks this mutation but
|
|
7
12
|
* does not block ordinary review/CI work in the caller.
|
|
8
13
|
*/
|
|
9
|
-
export declare function
|
|
14
|
+
export declare function findParentMarkReadyBlock(report: ShepherdReport, repo: RepoInfo): Promise<ParentMarkReadyBlock | undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* A native stack draft this one-PR session cannot promote: a lower layer blocks it, or
|
|
17
|
+
* automatic mark-ready is off. Undefined when the session can still advance the PR by
|
|
18
|
+
* iterating.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stackDraftHold(report: ShepherdReport, autoMarkReady: boolean, parentBlock: ParentMarkReadyBlock | undefined): StackDraftHold | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The lower layer a held draft waits on. That layer's own session owns this draft's
|
|
23
|
+
* progress, so the draft neither stalls nor keeps polling while it waits.
|
|
24
|
+
*/
|
|
25
|
+
export declare function heldByLowerLayer(result: IterateResult): StackLowerLayerBlock | undefined;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { fetchPollSummary } from "../../github/poll-summary.mjs";
|
|
2
|
+
import { stackLayerBlockReason } from "../stack-layer-readiness.mjs";
|
|
2
3
|
/**
|
|
3
4
|
* Draft children may only be converted after their immediate parent has
|
|
4
5
|
* independently completed a one-PR ready-delay and the stack boundary is
|
|
5
6
|
* still linear. A failed or incomplete stack read blocks this mutation but
|
|
6
7
|
* does not block ordinary review/CI work in the caller.
|
|
7
8
|
*/
|
|
8
|
-
export async function
|
|
9
|
+
export async function findParentMarkReadyBlock(report, repo) {
|
|
9
10
|
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
10
11
|
if (!stack || stack.position === 1)
|
|
11
|
-
return
|
|
12
|
+
return undefined;
|
|
12
13
|
if (stack.position < 1)
|
|
13
|
-
return
|
|
14
|
+
return "unverifiable";
|
|
14
15
|
try {
|
|
15
16
|
const summary = await fetchPollSummary({ stackPrNumber: report.pr }, repo);
|
|
16
17
|
const child = summary.prs.find((item) => item.pr === report.pr);
|
|
@@ -19,39 +20,51 @@ export async function parentBlocksMarkReady(report, repo) {
|
|
|
19
20
|
.sort((left, right) => (left.stack?.position ?? Number.MAX_SAFE_INTEGER) -
|
|
20
21
|
(right.stack?.position ?? Number.MAX_SAFE_INTEGER));
|
|
21
22
|
if (!child || child.state !== "OPEN" || lowerLayers.length !== stack.position - 1)
|
|
22
|
-
return
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// A merged parent is already satisfied; GitHub may have retargeted the
|
|
31
|
-
// child to the trunk as part of the merge.
|
|
32
|
-
if (parent.state === "MERGED")
|
|
23
|
+
return "unverifiable";
|
|
24
|
+
// A stale boundary means that layer is no longer based on the parent it was
|
|
25
|
+
// reviewed against. Gaps above this child do not affect its promotion boundary.
|
|
26
|
+
const staleChildren = new Set(summary.stackAncestry?.map((gap) => gap.childPr));
|
|
27
|
+
for (const layer of lowerLayers) {
|
|
28
|
+
// A merged layer is already satisfied; GitHub may have retargeted the
|
|
29
|
+
// layer above it to the trunk as part of the merge.
|
|
30
|
+
if (layer.state === "MERGED")
|
|
33
31
|
continue;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return true;
|
|
38
|
-
if (["DIRTY", "BEHIND", "UNKNOWN"].includes(parent.mergeStateStatus))
|
|
39
|
-
return true;
|
|
40
|
-
// A receipt only establishes readiness after a merge-queue removal once
|
|
41
|
-
// the one-PR session has observed and acknowledged that exact removal.
|
|
42
|
-
// The aggregate projection preserves an unacknowledged removal here, so
|
|
43
|
-
// do not let its otherwise-current receipt promote a child draft.
|
|
44
|
-
if (parent.queueRemoval)
|
|
45
|
-
return true;
|
|
46
|
-
// A parent that looks ready but has not completed its own one-PR receipt
|
|
47
|
-
// is not sufficient evidence for a child draft transition.
|
|
48
|
-
if (parent.readyReceipt !== true)
|
|
49
|
-
return true;
|
|
32
|
+
const reason = staleChildren.has(layer.pr) ? "stale-ancestry" : stackLayerBlockReason(layer);
|
|
33
|
+
if (reason)
|
|
34
|
+
return { pr: layer.pr, reason };
|
|
50
35
|
}
|
|
51
|
-
|
|
36
|
+
// This layer's own stale boundary belongs to its own session's repair; the
|
|
37
|
+
// earlier stale-ancestry read missed it, so this snapshot is unsettled.
|
|
38
|
+
return staleChildren.has(report.pr) ? "unverifiable" : undefined;
|
|
52
39
|
}
|
|
53
40
|
catch {
|
|
54
41
|
// Never convert a child draft based on an unverifiable parent.
|
|
55
|
-
return
|
|
42
|
+
return "unverifiable";
|
|
56
43
|
}
|
|
57
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A native stack draft this one-PR session cannot promote: a lower layer blocks it, or
|
|
47
|
+
* automatic mark-ready is off. Undefined when the session can still advance the PR by
|
|
48
|
+
* iterating.
|
|
49
|
+
*/
|
|
50
|
+
export function stackDraftHold(report, autoMarkReady, parentBlock) {
|
|
51
|
+
if (!report.mergeStatus.mergeRequirements?.stack || !report.mergeStatus.isDraft)
|
|
52
|
+
return undefined;
|
|
53
|
+
// Any lower-layer block outranks the session flag: even with automatic
|
|
54
|
+
// mark-ready enabled, this draft cannot advance until that layer does, and a
|
|
55
|
+
// lower-layer read that failed must not hide behind the flag.
|
|
56
|
+
if (parentBlock === "unverifiable")
|
|
57
|
+
return { kind: "lower-layer-not-ready" };
|
|
58
|
+
if (parentBlock)
|
|
59
|
+
return { kind: "lower-layer-not-ready", lowerLayer: parentBlock };
|
|
60
|
+
return autoMarkReady ? undefined : { kind: "auto-mark-ready-disabled" };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The lower layer a held draft waits on. That layer's own session owns this draft's
|
|
64
|
+
* progress, so the draft neither stalls nor keeps polling while it waits.
|
|
65
|
+
*/
|
|
66
|
+
export function heldByLowerLayer(result) {
|
|
67
|
+
return result.action === "wait" && result.stackDraftHold?.kind === "lower-layer-not-ready"
|
|
68
|
+
? result.stackDraftHold.lowerLayer
|
|
69
|
+
: undefined;
|
|
70
|
+
}
|
|
@@ -2,4 +2,4 @@ import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, Fir
|
|
|
2
2
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
3
3
|
export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
4
4
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, _cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], _inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
5
|
-
isBehind?: boolean, viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean): string[];
|
|
5
|
+
isBehind?: boolean, viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean, stackRebase?: string): string[];
|
|
@@ -4,6 +4,7 @@ import { SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE, buildShepherdJournalInstruction,
|
|
|
4
4
|
import { isFailingAgentCheck } from "../../checks/conclusions.mjs";
|
|
5
5
|
import { buildCommitSuggestionInstruction } from "../commit-suggestion-instruction.mjs";
|
|
6
6
|
import { partitionFixThreads, reviewSectionRefs } from "./fix-instruction-threads.mjs";
|
|
7
|
+
import { buildBranchPushInstruction, buildConflictInstruction } from "./native-stack-rebase.mjs";
|
|
7
8
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
8
9
|
export function renderResolveCommand(rc) {
|
|
9
10
|
const parts = [...rc.argv];
|
|
@@ -12,14 +13,11 @@ export function renderResolveCommand(rc) {
|
|
|
12
13
|
return renderShellCommand(parts);
|
|
13
14
|
}
|
|
14
15
|
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, _cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], _inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
15
|
-
isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
16
|
+
isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false, stackRebase) {
|
|
16
17
|
const instructions = [];
|
|
17
18
|
const { locatedThreads, unlocatedMutatedThreads, unlocatedThreads } = partitionFixThreads(threads, resolveCommand, resolveOnlyCommand);
|
|
18
19
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
19
|
-
const repeatedWorkflowBranchRecoveryInstructions = buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasExhaustedWorkflowRerun, {
|
|
20
|
-
isBehind,
|
|
21
|
-
hasConflicts,
|
|
22
|
-
});
|
|
20
|
+
const repeatedWorkflowBranchRecoveryInstructions = buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasExhaustedWorkflowRerun, { isBehind, hasConflicts }, stackRebase);
|
|
23
21
|
const hasRepeatedWorkflowBranchRecovery = repeatedWorkflowBranchRecoveryInstructions.length > 0;
|
|
24
22
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
25
23
|
const hasNonConflictHints = threads.length > 0 ||
|
|
@@ -41,7 +39,7 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
41
39
|
instructions.push(`Review each item ${sectionRef} and decide whether it needs a code change.`);
|
|
42
40
|
}
|
|
43
41
|
if (hasConflicts && !hasRepeatedWorkflowBranchRecovery) {
|
|
44
|
-
instructions.push(
|
|
42
|
+
instructions.push(buildConflictInstruction(stackRebase));
|
|
45
43
|
}
|
|
46
44
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
47
45
|
if (firstLookTotal > 0) {
|
|
@@ -82,11 +80,8 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
82
80
|
instructions.push(...buildBehindBaseHintInstruction(baseBranch, behindBaseHint, isBehind));
|
|
83
81
|
const hasReviewMutations = resolveCommand.hasMutations || resolveOnlyCommand?.hasMutations === true;
|
|
84
82
|
const mutationSuffix = hasReviewMutations ? " before review mutations" : "";
|
|
85
|
-
if (hasConflicts) {
|
|
86
|
-
instructions.push(
|
|
87
|
-
}
|
|
88
|
-
else if (hasRepeatedWorkflowBranchRecovery) {
|
|
89
|
-
instructions.push("Push the updated PR head branch before iterating immediately.");
|
|
83
|
+
if (hasConflicts || hasRepeatedWorkflowBranchRecovery) {
|
|
84
|
+
instructions.push(buildBranchPushInstruction(stackRebase, hasConflicts, mutationSuffix));
|
|
90
85
|
}
|
|
91
86
|
else if (hasNonConflictHints) {
|
|
92
87
|
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate immediately with the same options. If you did not change code, do not commit and continue with the remaining steps.");
|
|
@@ -101,6 +96,6 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
101
96
|
}
|
|
102
97
|
if (resolveOnlyCommand?.hasMutations)
|
|
103
98
|
instructions.push("Run the `resolve-only:` command shown above.");
|
|
104
|
-
instructions.push(...buildResolveCommandInstruction(resolveCommand), buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha));
|
|
99
|
+
instructions.push(...buildResolveCommandInstruction(resolveCommand), buildFixCompletionInstruction(failingChecks, hasConflicts, resolveCommand.requiresHeadSha, stackRebase !== undefined));
|
|
105
100
|
return instructions;
|
|
106
101
|
}
|
|
@@ -4,7 +4,7 @@ import type { ShepherdReport } from "../../types/report.mts";
|
|
|
4
4
|
/**
|
|
5
5
|
* A verified stale boundary for the PR being shepherded.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
7
|
+
* The stack topology read is authoritative for this check: it compares the
|
|
8
8
|
* child's recorded base OID with the current head OID of its immediate open
|
|
9
9
|
* parent. GitHub can report both PRs CLEAN while this boundary is stale.
|
|
10
10
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readStackTopology, stackAncestryGaps } from "../../github/stack-read.mjs";
|
|
2
|
+
import { buildNativeStackRebaseInstruction } from "./native-stack-rebase.mjs";
|
|
2
3
|
/**
|
|
3
4
|
* Find a stale immediate parent boundary for one PR.
|
|
4
5
|
*
|
|
@@ -12,13 +13,13 @@ export async function findStaleNativeStackAncestry(report, repo) {
|
|
|
12
13
|
if (!stack || stack.position <= 1)
|
|
13
14
|
return null;
|
|
14
15
|
try {
|
|
15
|
-
const
|
|
16
|
-
const ancestry =
|
|
16
|
+
const topology = await readStackTopology(report.pr, repo);
|
|
17
|
+
const ancestry = stackAncestryGaps(topology.ordered).find((gap) => gap.childPr === report.pr);
|
|
17
18
|
if (!ancestry)
|
|
18
19
|
return null;
|
|
19
20
|
return {
|
|
20
21
|
...ancestry,
|
|
21
|
-
instructions: buildStaleNativeStackAncestryInstructions(repo, ancestry),
|
|
22
|
+
instructions: buildStaleNativeStackAncestryInstructions(repo, stack.number, ancestry),
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
catch {
|
|
@@ -28,10 +29,12 @@ export async function findStaleNativeStackAncestry(report, repo) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
/** Build the one-PR repair guidance after a stale boundary was verified. */
|
|
31
|
-
function buildStaleNativeStackAncestryInstructions(repo, ancestry) {
|
|
32
|
+
function buildStaleNativeStackAncestryInstructions(repo, stackNumber, ancestry) {
|
|
32
33
|
return [
|
|
33
34
|
`PR #${ancestry.childPr} records base \`${ancestry.childBaseRefName}\` at \`${ancestry.childBaseRefOid}\`, but its open parent PR #${ancestry.parentPr} currently ends at \`${ancestry.parentHeadRefName}\` \`${ancestry.parentHeadRefOid}\`.`,
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
buildNativeStackRebaseInstruction(`${repo.owner}/${repo.name}`, stackNumber, {
|
|
36
|
+
parentBranch: ancestry.parentHeadRefName,
|
|
37
|
+
}),
|
|
38
|
+
"Push the rewritten stack with `gh stack push`.",
|
|
36
39
|
];
|
|
37
40
|
}
|
|
@@ -90,12 +90,27 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
const fingerprint = computeStallFingerprint(prospectiveResult.action, headSha, base, report, reviewSummaryIds);
|
|
93
|
-
const
|
|
93
|
+
const read = await readStallState(stallKey);
|
|
94
|
+
if (!read.ok) {
|
|
95
|
+
if (stallTimeoutSeconds <= 0)
|
|
96
|
+
return prospectiveResult;
|
|
97
|
+
return stallStateUnavailable(base, prReference, prospectiveResult, read.reason);
|
|
98
|
+
}
|
|
99
|
+
const stored = read.state;
|
|
100
|
+
const persistTimer = async () => {
|
|
101
|
+
const wrote = await writeStallState(stallKey, { fingerprint, firstSeenAt: nowSeconds });
|
|
102
|
+
if (!wrote.ok && stallTimeoutSeconds > 0) {
|
|
103
|
+
return stallStateUnavailable(base, prReference, prospectiveResult, wrote.reason);
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
};
|
|
94
107
|
if (stored && stored.fingerprint === fingerprint) {
|
|
95
108
|
const ageSeconds = nowSeconds - stored.firstSeenAt;
|
|
96
109
|
if (ageSeconds < 0) {
|
|
97
110
|
// Clock skew: stored timestamp is in the future. Reset to avoid perpetually negative age.
|
|
98
|
-
|
|
111
|
+
const failed = await persistTimer();
|
|
112
|
+
if (failed)
|
|
113
|
+
return failed;
|
|
99
114
|
}
|
|
100
115
|
else if (stallTimeoutSeconds <= 0) {
|
|
101
116
|
// Stall detection disabled: refresh so re-enabling starts a fresh timer.
|
|
@@ -126,9 +141,31 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
126
141
|
return prospectiveResult;
|
|
127
142
|
}
|
|
128
143
|
// Fingerprint changed or no prior state — reset the stall timer.
|
|
129
|
-
|
|
144
|
+
const failed = await persistTimer();
|
|
145
|
+
if (failed)
|
|
146
|
+
return failed;
|
|
130
147
|
return prospectiveResult;
|
|
131
148
|
}
|
|
149
|
+
function stallStateUnavailable(base, prReference, prospectiveResult, reason) {
|
|
150
|
+
const pending = pendingReviewCommandsFromResult(prospectiveResult);
|
|
151
|
+
const escalateBase = {
|
|
152
|
+
triggers: ["stall-state-unavailable"],
|
|
153
|
+
unresolvedThreads: [],
|
|
154
|
+
ambiguousComments: [],
|
|
155
|
+
changesRequestedReviews: [],
|
|
156
|
+
...surfacedSummariesFromResult(prospectiveResult),
|
|
157
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
158
|
+
suggestion: buildEscalateSuggestion(["stall-state-unavailable"], reason),
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
action: "escalate",
|
|
163
|
+
escalate: {
|
|
164
|
+
...escalateBase,
|
|
165
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prReference),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
132
169
|
function findCiStartStalledChecks(checks, nowSeconds, opts) {
|
|
133
170
|
if (opts.stallTimeoutSeconds <= 0 || opts.action !== "wait")
|
|
134
171
|
return [];
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { IterateResult } from "../types.mts";
|
|
2
|
+
type WaitResult = Extract<IterateResult, {
|
|
3
|
+
action: "wait";
|
|
4
|
+
}>;
|
|
2
5
|
export declare function writeWaitProgress(opts: {
|
|
3
6
|
tick: number;
|
|
4
7
|
elapsedMs: number;
|
|
5
8
|
sleepMs: number;
|
|
6
|
-
result:
|
|
9
|
+
result: WaitResult;
|
|
7
10
|
quietStatus: boolean;
|
|
8
11
|
verbose: boolean;
|
|
9
12
|
lastWaitSignature: string | null;
|
|
10
13
|
}): string | null;
|
|
11
14
|
export declare function writeDebounceProgress(tick: number, elapsedMs: number, remainingMs: number): void;
|
|
15
|
+
export {};
|