pr-shepherd 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -12
- 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 +22 -0
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/poll-summary-formatter.mjs +4 -3
- 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/api-usage.mjs +2 -2
- 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 +48 -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 +6 -7
- package/bin/commands/iterate/parent-first.mjs +10 -54
- 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-quota.mjs +2 -2
- package/bin/commands/poll-summary-instructions.d.mts +6 -1
- package/bin/commands/poll-summary-instructions.mjs +71 -122
- package/bin/commands/poll-summary.mjs +4 -2
- 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 +138 -0
- package/bin/commands/stack-layer-readiness.d.mts +6 -0
- package/bin/commands/stack-layer-readiness.mjs +34 -0
- package/bin/commands/stack-stall.d.mts +14 -0
- package/bin/commands/stack-stall.mjs +68 -0
- package/bin/commands/stack-work.d.mts +32 -0
- package/bin/commands/stack-work.mjs +38 -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/api-telemetry-aggregate.d.mts +1 -0
- package/bin/github/api-telemetry-aggregate.mjs +8 -2
- package/bin/github/api-telemetry.d.mts +4 -0
- package/bin/github/api-telemetry.mjs +12 -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/graphql-http.mjs +6 -0
- package/bin/github/http-auth.d.mts +3 -0
- package/bin/github/http-auth.mjs +6 -0
- package/bin/github/http-intermediate.d.mts +1 -0
- package/bin/github/http-intermediate.mjs +3 -0
- 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-policy.d.mts +7 -1
- package/bin/state/graphql-quota-policy.mjs +41 -14
- package/bin/state/graphql-quota-warnings.mjs +4 -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/api-usage.d.mts +2 -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 +9 -0
- package/bin/types/poll-summary.d.mts +5 -2
- 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 { 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,25 @@ async function runIterateCore(opts) {
|
|
|
168
175
|
const canMarkReady = report.status === "READY" &&
|
|
169
176
|
report.mergeStatus.isDraft &&
|
|
170
177
|
!report.mergeStatus.blockingBotReviewInProgress;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
: false;
|
|
174
|
-
const markReadyResult = await markReadyIfAuthorized(canMarkReady && !blockedByParent && !opts.noAutoMarkReady && config.actions.autoMarkReady, base, report);
|
|
178
|
+
const autoMarkReady = !opts.noAutoMarkReady && config.actions.autoMarkReady;
|
|
179
|
+
const markReadyResult = await markReadyIfAuthorized(canMarkReady && autoMarkReady, base, report);
|
|
175
180
|
if (markReadyResult)
|
|
176
181
|
return markReadyResult;
|
|
177
182
|
if (readyState.shouldCancel && !report.mergeStatus.isDraft) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (!receiptWritten) {
|
|
183
|
+
const receiptWritten = receiptCurrent || (await recordReadyReceipt(receiptKey, report));
|
|
184
|
+
// Aggregate --stack routing trusts only a layer's receipt, so an unwritten
|
|
185
|
+
// one keeps the elapsed marker and retries next tick. A one-PR receipt
|
|
186
|
+
// only lets a rerun skip the wait, so its failure never changes the action.
|
|
187
|
+
if (!receiptWritten && report.mergeStatus.mergeRequirements?.stack !== undefined) {
|
|
183
188
|
const receiptWait = {
|
|
184
189
|
...base,
|
|
185
190
|
action: "wait",
|
|
186
191
|
shouldCancel: false,
|
|
187
|
-
remainingSeconds: readyDelaySeconds,
|
|
188
192
|
log: `WAIT: PR #${base.pr} reached ready-delay but its stack readiness receipt could not be persisted`,
|
|
189
193
|
};
|
|
190
194
|
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, receiptWait, report, reviewSummaryIds);
|
|
191
195
|
}
|
|
196
|
+
await clearReadyDelay(report.pr, repoOwner, repoName);
|
|
192
197
|
await clearStallState(stallKey);
|
|
193
198
|
const mergeResult = buildReadyMergeOutcome(opts.merge, true, base, report);
|
|
194
199
|
if (mergeResult)
|
|
@@ -201,7 +206,14 @@ async function runIterateCore(opts) {
|
|
|
201
206
|
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
202
207
|
};
|
|
203
208
|
}
|
|
204
|
-
|
|
209
|
+
const hold = stackDraftHold(report, autoMarkReady);
|
|
210
|
+
const wait = {
|
|
211
|
+
...base,
|
|
212
|
+
action: "wait",
|
|
213
|
+
log: buildWaitLog(base),
|
|
214
|
+
...(hold && { stackDraftHold: hold }),
|
|
215
|
+
};
|
|
216
|
+
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, wait, report, reviewSummaryIds);
|
|
205
217
|
}
|
|
206
218
|
async function recordReadyReceipt(key, report) {
|
|
207
219
|
if (report.status !== "READY" ||
|
|
@@ -245,10 +257,11 @@ async function recordReadyReceipt(key, report) {
|
|
|
245
257
|
return false;
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
|
-
|
|
260
|
+
/** Clear a stale READY receipt; return whether a current receipt remains. */
|
|
261
|
+
async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
249
262
|
const receipt = await readReadyReceipt(key);
|
|
250
263
|
if (!receipt)
|
|
251
|
-
return;
|
|
264
|
+
return false;
|
|
252
265
|
const retainQueuedReceipt = report.mergeQueue?.inQueue === true &&
|
|
253
266
|
report.mergeStatus.state === "OPEN" &&
|
|
254
267
|
!report.mergeStatus.isDraft &&
|
|
@@ -257,9 +270,9 @@ async function invalidateStaleReadyReceipt(key, report, hasActionableWork) {
|
|
|
257
270
|
report.mergeStatus.isDraft ||
|
|
258
271
|
!report.headSha ||
|
|
259
272
|
!report.baseRefOid ||
|
|
260
|
-
(!retainQueuedReceipt && (report.status !== "READY" ||
|
|
273
|
+
(!retainQueuedReceipt && (report.status !== "READY" || hasReadinessWork))) {
|
|
261
274
|
await clearReadyReceipt(key);
|
|
262
|
-
return;
|
|
275
|
+
return false;
|
|
263
276
|
}
|
|
264
277
|
try {
|
|
265
278
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
@@ -268,7 +281,12 @@ async function invalidateStaleReadyReceipt(key, report, hasActionableWork) {
|
|
|
268
281
|
// views still place the PR in the queue.
|
|
269
282
|
const queuedBaseAdvanced = retainQueuedReceipt && raw.isInMergeQueue;
|
|
270
283
|
const fingerprint = fingerprintRawSummaryPr(queuedBaseAdvanced ? { ...raw, baseRefOid: receipt.baseRefOid } : raw);
|
|
284
|
+
// The fresh snapshot must name the commits this tick evaluated, or the
|
|
285
|
+
// receipt would vouch for a head or base the report never saw.
|
|
286
|
+
const sameCommits = raw.headRefOid === report.headSha &&
|
|
287
|
+
(queuedBaseAdvanced || raw.baseRefOid === report.baseRefOid);
|
|
271
288
|
if (fingerprint === null ||
|
|
289
|
+
!sameCommits ||
|
|
272
290
|
!isReadyReceiptCurrent(receipt, {
|
|
273
291
|
headRefOid: raw.headRefOid,
|
|
274
292
|
baseRefOid: queuedBaseAdvanced ? receipt.baseRefOid : raw.baseRefOid,
|
|
@@ -277,10 +295,13 @@ async function invalidateStaleReadyReceipt(key, report, hasActionableWork) {
|
|
|
277
295
|
isDraft: raw.isDraft,
|
|
278
296
|
})) {
|
|
279
297
|
await clearReadyReceipt(key);
|
|
298
|
+
return false;
|
|
280
299
|
}
|
|
300
|
+
return true;
|
|
281
301
|
}
|
|
282
302
|
catch {
|
|
283
303
|
// Fail closed: an unreadable current snapshot cannot validate old evidence.
|
|
284
304
|
await clearReadyReceipt(key);
|
|
305
|
+
return false;
|
|
285
306
|
}
|
|
286
307
|
}
|
|
@@ -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,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { ShepherdReport } from "../../types.mts";
|
|
1
|
+
import type { ShepherdReport, StackDraftHold } from "../../types.mts";
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* A native stack draft this one-PR session cannot promote because it is ready
|
|
4
|
+
* and automatic mark-ready is off. A draft that is not ready yet keeps the
|
|
5
|
+
* ordinary wait. A clean draft is marked ready on its own, without waiting
|
|
6
|
+
* for lower layers.
|
|
8
7
|
*/
|
|
9
|
-
export declare function
|
|
8
|
+
export declare function stackDraftHold(report: ShepherdReport, autoMarkReady: boolean): StackDraftHold | undefined;
|
|
@@ -1,57 +1,13 @@
|
|
|
1
|
-
import { fetchPollSummary } from "../../github/poll-summary.mjs";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* A native stack draft this one-PR session cannot promote because it is ready
|
|
3
|
+
* and automatic mark-ready is off. A draft that is not ready yet keeps the
|
|
4
|
+
* ordinary wait. A clean draft is marked ready on its own, without waiting
|
|
5
|
+
* for lower layers.
|
|
7
6
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const summary = await fetchPollSummary({ stackPrNumber: report.pr }, repo);
|
|
16
|
-
const child = summary.prs.find((item) => item.pr === report.pr);
|
|
17
|
-
const lowerLayers = summary.prs
|
|
18
|
-
.filter((item) => (item.stack?.position ?? Number.MAX_SAFE_INTEGER) < stack.position)
|
|
19
|
-
.sort((left, right) => (left.stack?.position ?? Number.MAX_SAFE_INTEGER) -
|
|
20
|
-
(right.stack?.position ?? Number.MAX_SAFE_INTEGER));
|
|
21
|
-
if (!child || child.state !== "OPEN" || lowerLayers.length !== stack.position - 1)
|
|
22
|
-
return true;
|
|
23
|
-
// Any stale boundary up through the child means at least one lower layer
|
|
24
|
-
// is no longer the base it was reviewed against. Ignore gaps above this
|
|
25
|
-
// child because they do not affect its immediate promotion boundary.
|
|
26
|
-
const checkedLayers = new Set([report.pr, ...lowerLayers.map((item) => item.pr)]);
|
|
27
|
-
if (summary.stackAncestry?.some((gap) => checkedLayers.has(gap.childPr)))
|
|
28
|
-
return true;
|
|
29
|
-
for (const parent of lowerLayers) {
|
|
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")
|
|
33
|
-
continue;
|
|
34
|
-
if (parent.state !== "OPEN")
|
|
35
|
-
return true;
|
|
36
|
-
if (parent.isDraft || parent.mergeable === "CONFLICTING")
|
|
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;
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Never convert a child draft based on an unverifiable parent.
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
7
|
+
export function stackDraftHold(report, autoMarkReady) {
|
|
8
|
+
if (!report.mergeStatus.mergeRequirements?.stack || !report.mergeStatus.isDraft)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (report.status !== "READY" || report.mergeStatus.blockingBotReviewInProgress)
|
|
11
|
+
return undefined;
|
|
12
|
+
return autoMarkReady ? undefined : { kind: "auto-mark-ready-disabled" };
|
|
57
13
|
}
|
|
@@ -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 {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
function writeTickProgress(tick, elapsedSeconds, detail) {
|
|
2
2
|
process.stderr.write(`[poll tick ${tick} / +${elapsedSeconds}s] WAIT — ${detail}\n`);
|
|
3
3
|
}
|
|
4
|
+
function waitReason(result) {
|
|
5
|
+
return result.log.replace(/^WAIT: /, "");
|
|
6
|
+
}
|
|
4
7
|
function waitSignature(result) {
|
|
5
8
|
const activity = result.activity ?? {
|
|
6
9
|
commitCount: 0,
|
|
@@ -36,7 +39,10 @@ export function writeWaitProgress(opts) {
|
|
|
36
39
|
const elapsedSeconds = Math.round(opts.elapsedMs / 1000);
|
|
37
40
|
const sleepSeconds = Math.round(opts.sleepMs / 1000);
|
|
38
41
|
if (!opts.quietStatus) {
|
|
39
|
-
|
|
42
|
+
const reason = waitReason(opts.result);
|
|
43
|
+
writeTickProgress(opts.tick, elapsedSeconds, opts.verbose
|
|
44
|
+
? `${reason} — sleeping ${sleepSeconds}s`
|
|
45
|
+
: `${reason}; next tick in ${sleepSeconds}s`);
|
|
40
46
|
return opts.lastWaitSignature;
|
|
41
47
|
}
|
|
42
48
|
const signature = waitSignature(opts.result);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { evaluateWorktreeGraphqlQuotaWarning } from "../state/graphql-quota-warnings.mjs";
|
|
2
|
-
import { summarizeApiTelemetry } from "../github/api-telemetry.mjs";
|
|
2
|
+
import { summarizeApiTelemetry, withGraphqlCredentialFingerprint, } from "../github/api-telemetry.mjs";
|
|
3
3
|
import { GitHubRequestError } from "../github/errors.mjs";
|
|
4
4
|
import { isRateLimitMessage } from "../comments/rate-limit.mjs";
|
|
5
5
|
const GRAPHQL_RETRY_AFTER_DEFAULT_MS = 60_000;
|
|
@@ -44,5 +44,5 @@ export async function aggregateQuotaWarning(result, bands, intervalSeconds) {
|
|
|
44
44
|
return evaluateWorktreeGraphqlQuotaWarning({ owner, repo }, bands.map((band) => ({
|
|
45
45
|
...band,
|
|
46
46
|
pollIntervalMinutes: Math.max(band.pollIntervalMinutes, intervalSeconds / 60),
|
|
47
|
-
})), usage, true);
|
|
47
|
+
})), withGraphqlCredentialFingerprint(usage), true);
|
|
48
48
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import type { PollSummaryResult } from "../types.mts";
|
|
1
|
+
import type { PollSummaryItem, PollSummaryResult } from "../types.mts";
|
|
2
2
|
/** Keep aggregate JSON, Markdown, and MCP instructions on one projection. */
|
|
3
3
|
export declare function withPollSummaryInstructions(result: PollSummaryResult, mergeRequested: boolean): PollSummaryResult;
|
|
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): {
|
|
6
|
+
result: PollSummaryResult;
|
|
7
|
+
idle?: PollSummaryItem[];
|
|
8
|
+
};
|