mergestorm 0.3.38 → 0.3.40
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/README.md +2 -2
- package/dist/api.d.ts +6 -1
- package/dist/api.js +61 -5
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/commands/browse.d.ts +5 -0
- package/dist/commands/browse.js +18 -1
- package/dist/commands/review-client.d.ts +8 -1
- package/dist/commands/review-client.js +18 -3
- package/dist/commands/review.d.ts +2 -1
- package/dist/commands/review.js +1 -0
- package/dist/commands/settings.js +2 -3
- package/dist/commands/stack.js +1 -2
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +9 -0
- package/dist/stack-blockers.d.ts +26 -16
- package/dist/stack-blockers.js +197 -117
- package/dist/stack-dto.d.ts +7 -8
- package/dist/stack-dto.js +0 -2
- package/dist/ui/banner.js +7 -1
- package/dist/ui/tabs.d.ts +5 -0
- package/dist/ui/tabs.js +5 -3
- package/package.json +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +4 -3
- package/skill/mergestorm-pr-loop/SKILL.md +2 -2
package/dist/stack-blockers.js
CHANGED
|
@@ -57,7 +57,7 @@ export const VORTEX_REVIEW_OUT_OF_QUOTA = "Review skipped, out of review quota";
|
|
|
57
57
|
export const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no review running";
|
|
58
58
|
export const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
|
|
59
59
|
export const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
|
|
60
|
-
const
|
|
60
|
+
const STUCK_SEAM_REASONS = ["seam_review_stuck", "seam_rereview_not_running", "seam_verdict_stale"];
|
|
61
61
|
function coreReviewDoneAtHead(layer) {
|
|
62
62
|
const review = layer.vortexReview;
|
|
63
63
|
return review?.status === "done" && sameHead(review.head_sha, layer.headSha);
|
|
@@ -79,6 +79,13 @@ const CYCLONE_HANDOFF_WORDS = {
|
|
|
79
79
|
stack_auto_patch_off: "auto-patch is off for this stack",
|
|
80
80
|
account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
|
|
81
81
|
cyclone_not_connected: "Cyclone is not connected to this repo",
|
|
82
|
+
infra_failure: "Cyclone could not start the patch: GitHub sign-in, comment load or checkout kept failing",
|
|
83
|
+
no_changes: "Cyclone found nothing to change for these findings",
|
|
84
|
+
credits_exhausted: "the account is out of standard credits this month",
|
|
85
|
+
no_billing: "the account has no billing set up",
|
|
86
|
+
unsupported_language: "the findings are in a language Cyclone does not patch",
|
|
87
|
+
policy_owner_changed: "the stack's owner changed while the patch was queued",
|
|
88
|
+
patch_job_lost: "Cyclone's patch job was lost before it finished",
|
|
82
89
|
};
|
|
83
90
|
function cycloneHandoffWords(reason) {
|
|
84
91
|
return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
|
|
@@ -95,11 +102,21 @@ function quotaSkippedReview(layer) {
|
|
|
95
102
|
export const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
|
|
96
103
|
export const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
|
|
97
104
|
export const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
|
|
105
|
+
export const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
|
|
106
|
+
export const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
|
|
107
|
+
export const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
|
|
108
|
+
export const VORTEX_REVIEW_FAILED = "Review failed";
|
|
98
109
|
const VORTEX_SKIP_BLOCKERS = {
|
|
99
110
|
auto_review_off: VORTEX_AUTO_REVIEW_OFF,
|
|
100
111
|
billing_blocked: VORTEX_BILLING_SKIPPED,
|
|
101
112
|
billing_unavailable: VORTEX_BILLING_SKIPPED,
|
|
113
|
+
ignored_author: VORTEX_IGNORED_AUTHOR,
|
|
114
|
+
repo_disabled: VORTEX_REPO_DISABLED,
|
|
102
115
|
};
|
|
116
|
+
export function vortexRetryScheduled(layer) {
|
|
117
|
+
const review = layer.vortexReview;
|
|
118
|
+
return review?.skip_reason?.trim() === "retry_scheduled" && sameHead(review.head_sha, layer.headSha);
|
|
119
|
+
}
|
|
103
120
|
function vortexIdleSkipBlocker(layer) {
|
|
104
121
|
if (layerAgentsBusy(layer).vortex)
|
|
105
122
|
return null;
|
|
@@ -121,6 +138,12 @@ function vortexSkippedSteps(stack, layer, skipReason) {
|
|
|
121
138
|
if (skipReason === "auto_review_off") {
|
|
122
139
|
return `Vortex auto-review is off for ${stack.owner}/${stack.repo} or for this stack, so it skipped #${layer.prNumber} at ${head} and Auto land waits for a review that will not come. Do not patch or push for this. Tell the human: turning auto-review on (mg stack set ${stack.id} --auto-review on, or the account setting in the dashboard) queues a review of this head by itself. To review only this head and leave auto-review off, ${mention}.`;
|
|
123
140
|
}
|
|
141
|
+
if (skipReason === "ignored_author") {
|
|
142
|
+
return `Vortex skipped #${layer.prNumber} at ${head} because the PR's author is on the account's ignored authors list, so Auto land waits for a review that will not come. Do not patch or push for this. To review this head once, ${mention}. To have this author's PRs reviewed by themselves, tell the human to remove the author from the ignored authors list in the Mergestorm dashboard settings.`;
|
|
143
|
+
}
|
|
144
|
+
if (skipReason === "repo_disabled") {
|
|
145
|
+
return `Vortex skipped #${layer.prNumber} at ${head} because reviews are turned off for ${stack.owner}/${stack.repo}, so Auto land waits for a review that will not come. Do not patch or push for this, and a mention will not run a review while the repository is off. Tell the human to turn the repository on in the Mergestorm dashboard; after that a push or a comment @mergestorm-vortex review on #${layer.prNumber} runs the review.`;
|
|
146
|
+
}
|
|
124
147
|
if (skipReason === "incomplete") {
|
|
125
148
|
return `Vortex reviewed only some of the files in #${layer.prNumber} at ${head} and left the rest unreviewed, so Auto land waits for full coverage. Do not patch or push for this. To review the remaining files, ${mention} (the Continue button on the stack is unrelated), or tell the human to do it; turning on automatic overflow reviews in the dashboard settings covers this for later heads.`;
|
|
126
149
|
}
|
|
@@ -136,24 +159,26 @@ function seamGateBlocker(stack, layer) {
|
|
|
136
159
|
if (!member || member.promotedHeadSha?.trim())
|
|
137
160
|
return null;
|
|
138
161
|
const seamState = member.seamState?.trim().toLowerCase();
|
|
162
|
+
const stale = { blocker: SEAM_VERDICT_STALE, reason: "seam_verdict_stale" };
|
|
139
163
|
if (seamState === "failed")
|
|
140
|
-
return "Seam review failed";
|
|
141
|
-
if (seamState === "findings" && sameHead(member.seamReviewedSha, layer.headSha))
|
|
142
|
-
return "Seam findings";
|
|
164
|
+
return { blocker: "Seam review failed", reason: "seam_failed" };
|
|
165
|
+
if (seamState === "findings" && sameHead(member.seamReviewedSha, layer.headSha)) {
|
|
166
|
+
return { blocker: "Seam findings", reason: "seam_findings" };
|
|
167
|
+
}
|
|
143
168
|
if (layerAgentsBusy(layer).vortex)
|
|
144
169
|
return null;
|
|
145
170
|
if (seamState === "pending")
|
|
146
|
-
return SEAM_REVIEW_NOT_RUNNING;
|
|
171
|
+
return { blocker: SEAM_REVIEW_NOT_RUNNING, reason: "seam_not_running" };
|
|
147
172
|
if (seamState === "reviewing")
|
|
148
|
-
return SEAM_REVIEW_STUCK;
|
|
173
|
+
return { blocker: SEAM_REVIEW_STUCK, reason: "seam_review_stuck" };
|
|
149
174
|
if (seamState === "pending_rereview")
|
|
150
|
-
return SEAM_REREVIEW_NOT_RUNNING;
|
|
175
|
+
return { blocker: SEAM_REREVIEW_NOT_RUNNING, reason: "seam_rereview_not_running" };
|
|
151
176
|
if (!layer.headSha?.trim() || sameHead(member.seamReviewedSha, layer.headSha))
|
|
152
177
|
return null;
|
|
153
178
|
if (seamState === "findings")
|
|
154
|
-
return
|
|
179
|
+
return stale;
|
|
155
180
|
if (seamState === "approved" && !coreReviewDoneAtHead(layer))
|
|
156
|
-
return
|
|
181
|
+
return stale;
|
|
157
182
|
return null;
|
|
158
183
|
}
|
|
159
184
|
function headChecksAllGreen(layer) {
|
|
@@ -163,64 +188,58 @@ function headChecksAllGreen(layer) {
|
|
|
163
188
|
}
|
|
164
189
|
/** Shared live-fact precedence; bounce history is only passed for the candidate. */
|
|
165
190
|
function layerAttention(stack, layer, queue, cursor) {
|
|
166
|
-
const none = { blocker: null, bounceKind: null };
|
|
191
|
+
const none = { blocker: null, reason: null, bounceKind: null };
|
|
167
192
|
if (stack.archivedAt)
|
|
168
193
|
return none;
|
|
169
194
|
const bounce = queue.filter((entry) => entry.state === "bounced")
|
|
170
195
|
.sort((a, b) => (Date.parse(b.finishedAt ?? "") || 0) - (Date.parse(a.finishedAt ?? "") || 0))[0];
|
|
171
196
|
const kind = bounce?.bounceDetail?.kind;
|
|
172
197
|
const recoverable = kind === "head_moved" || kind === "must_consolidate";
|
|
173
|
-
const hardBlock = (blocker) => ({ blocker, bounceKind: null });
|
|
198
|
+
const hardBlock = (blocker, reason) => ({ blocker, reason, bounceKind: null });
|
|
174
199
|
if (layer) {
|
|
175
200
|
if (layer.restackError && layer.state !== "restacking") {
|
|
176
201
|
if (layer.restackError.kind === "rebase_conflict")
|
|
177
|
-
return hardBlock("Conflict");
|
|
202
|
+
return hardBlock("Conflict", "restack_conflict");
|
|
178
203
|
if (!restackRetryPending(layer.restackError, layer))
|
|
179
|
-
return hardBlock(RESTACK_FAILED);
|
|
204
|
+
return hardBlock(RESTACK_FAILED, "restack_failed");
|
|
180
205
|
}
|
|
181
206
|
if (layer.state === "conflict")
|
|
182
|
-
return hardBlock("Conflict");
|
|
207
|
+
return hardBlock("Conflict", "restack_conflict");
|
|
183
208
|
if (layer.draft)
|
|
184
209
|
return {
|
|
185
|
-
blocker: "Draft PR", bounceKind: "pr_draft",
|
|
210
|
+
blocker: "Draft PR", reason: "draft_pr", bounceKind: "pr_draft",
|
|
186
211
|
...(kind === "pr_draft" && sameHead(bounce.bounceDetail?.headSha ?? bounce.verifyHeadSha, layer.headSha) &&
|
|
187
212
|
(bounce.bounceDetail?.prNumber == null || bounce.bounceDetail.prNumber === layer.prNumber) ? { bounce } : {}),
|
|
188
213
|
};
|
|
189
|
-
if (boundDirty(layer))
|
|
190
|
-
return hardBlock(`Merge conflicts${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}
|
|
214
|
+
if (boundDirty(layer)) {
|
|
215
|
+
return hardBlock(`Merge conflicts${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}`, "merge_conflict");
|
|
216
|
+
}
|
|
191
217
|
if (layer.ciStatus === "failure" || (layer.checks?.failure ?? 0) > 0) {
|
|
192
218
|
const name = layer.checks?.failingName?.trim();
|
|
193
|
-
return hardBlock(`CI failed${name ? ` — ${name}` : ""}
|
|
219
|
+
return hardBlock(`CI failed${name ? ` — ${name}` : ""}`, "ci_failed");
|
|
220
|
+
}
|
|
221
|
+
if (layer.vortexStatus === "failed" && !vortexRetryScheduled(layer) &&
|
|
222
|
+
(!layer.vortexReview?.head_sha || sameHead(layer.vortexReview.head_sha, layer.headSha))) {
|
|
223
|
+
return hardBlock(VORTEX_REVIEW_FAILED, "vortex_failed");
|
|
194
224
|
}
|
|
195
|
-
if (layer.agentRuns?.some((run) => run.agent === "cyclone" && run.status === "failed" &&
|
|
196
|
-
sameHead(run.sha, layer.headSha)))
|
|
197
|
-
return hardBlock("Cyclone failed");
|
|
198
|
-
const tempest = layer.agentRuns?.find((run) => run.agent === "tempest" &&
|
|
199
|
-
run.status === "findings" && sameHead(run.sha, layer.headSha));
|
|
200
|
-
if (tempest)
|
|
201
|
-
return hardBlock(`Tempest ${tempest.status}`);
|
|
202
|
-
if (layer.vortexStatus === "failed")
|
|
203
|
-
return hardBlock("Review failed");
|
|
204
225
|
if (quotaSkippedReview(layer))
|
|
205
|
-
return hardBlock(VORTEX_REVIEW_OUT_OF_QUOTA);
|
|
226
|
+
return hardBlock(VORTEX_REVIEW_OUT_OF_QUOTA, "vortex_out_of_quota");
|
|
206
227
|
const vortexSkip = vortexIdleSkipBlocker(layer);
|
|
207
228
|
if (vortexSkip)
|
|
208
|
-
return hardBlock(vortexSkip.blocker);
|
|
229
|
+
return hardBlock(vortexSkip.blocker, "vortex_skipped");
|
|
209
230
|
const seam = seamGateBlocker(stack, layer);
|
|
210
231
|
if (seam)
|
|
211
|
-
return hardBlock(seam);
|
|
232
|
+
return hardBlock(seam.blocker, seam.reason);
|
|
212
233
|
if (unitAbandonedUnder(stack, layer)) {
|
|
213
|
-
return hardBlock(`${UNIT_ABANDONED_BLOCKER_PREFIX}${layer.parentBranch?.trim() || stack.trunkBranch}
|
|
234
|
+
return hardBlock(`${UNIT_ABANDONED_BLOCKER_PREFIX}${layer.parentBranch?.trim() || stack.trunkBranch}`, "unit_abandoned");
|
|
214
235
|
}
|
|
215
236
|
if (stack.unit?.landPrNumber === layer.prNumber) {
|
|
216
237
|
const landing = stack.unit.landingBlockReason?.trim();
|
|
217
238
|
if (landing && !landGateIsPending(landing))
|
|
218
|
-
return hardBlock(landing);
|
|
219
|
-
if (stack.unit.tempestLandStatus?.toLowerCase() === "failed")
|
|
220
|
-
return hardBlock("Tempest failed");
|
|
239
|
+
return hardBlock(landing, "land_gate");
|
|
221
240
|
}
|
|
222
241
|
if (vortexFindingsLeftForPerson(stack, layer))
|
|
223
|
-
return hardBlock(VORTEX_FINDINGS_NEED_PERSON);
|
|
242
|
+
return hardBlock(VORTEX_FINDINGS_NEED_PERSON, "vortex_findings_need_person");
|
|
224
243
|
}
|
|
225
244
|
if (!bounce || !kind || recoverable || !layer || queue.some((entry) => ["queued", "running", "waiting"].includes(entry.state)))
|
|
226
245
|
return none;
|
|
@@ -233,7 +252,8 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
233
252
|
return none;
|
|
234
253
|
if (kind === "ci_failure" && !bounce.bounceDetail?.batch && headChecksAllGreen(layer))
|
|
235
254
|
return none;
|
|
236
|
-
|
|
255
|
+
const label = mergeQueueBounceLabel(bounce);
|
|
256
|
+
return { blocker: label, reason: "bounce", bounceKind: kind, bounce };
|
|
237
257
|
}
|
|
238
258
|
const UNIT_ABANDONED_BLOCKER_PREFIX = "Review unit abandoned: based on ";
|
|
239
259
|
function unitAbandonedUnder(stack, layer) {
|
|
@@ -275,6 +295,7 @@ export const AUTO_LAND_WAIT_ATTENTION_MS = 45 * 60_000;
|
|
|
275
295
|
export const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS = 10 * 60_000;
|
|
276
296
|
const AUTO_LAND_CI_UNREADABLE_PREFIX = "Mergestorm cannot read ";
|
|
277
297
|
const AUTO_LAND_WAITED_PREFIX = "Auto land has waited ";
|
|
298
|
+
const AUTO_LAND_MEMBERS_REMAINING_PREFIX = "The land PR cannot be queued: ";
|
|
278
299
|
const CI_UNREADABLE_WHAT = {
|
|
279
300
|
check_runs_unreadable: "the check runs",
|
|
280
301
|
commit_statuses_unreadable: "the commit statuses",
|
|
@@ -287,13 +308,7 @@ const CI_UNREADABLE_PERMISSION = {
|
|
|
287
308
|
};
|
|
288
309
|
const AUTO_LAND_WAIT_WORDS = {
|
|
289
310
|
mergeability_unknown: "GitHub has not said whether this PR can merge into its base",
|
|
290
|
-
|
|
291
|
-
tempest_running: "Tempest's review of this head has not finished",
|
|
292
|
-
tempest_trigger_failed: "Tempest's review of this head could not be started",
|
|
293
|
-
tempest_stopped: "Tempest's review of this head was stopped",
|
|
294
|
-
tempest_failed: "Tempest's review of this head broke without a result",
|
|
295
|
-
tempest_findings: "Tempest reported findings on this head",
|
|
296
|
-
cyclone_failed: "Cyclone's patch run failed on this head",
|
|
311
|
+
members_remaining: "a PR that is not a layer of this stack is still open on the stack branch",
|
|
297
312
|
stack_dirty: "this layer is not clean (it has a conflict or needs a restack)",
|
|
298
313
|
must_consolidate: "this PR's base is not the stack's land target, and the stack has no review unit to promote it into",
|
|
299
314
|
missing_head: "GitHub returned no head commit for this PR",
|
|
@@ -348,6 +363,9 @@ function autoLandWaitOn(stack, layer) {
|
|
|
348
363
|
return autoLandWaitLayer(stack)?.prNumber === layer.prNumber ? armedAutoLandWait(stack) : null;
|
|
349
364
|
}
|
|
350
365
|
export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
366
|
+
return autoLandWaitNamed(stack, layer, nowMs)?.blocker ?? null;
|
|
367
|
+
}
|
|
368
|
+
function autoLandWaitNamed(stack, layer, nowMs) {
|
|
351
369
|
const wait = autoLandWaitOn(stack, layer);
|
|
352
370
|
if (!wait)
|
|
353
371
|
return null;
|
|
@@ -357,18 +375,32 @@ export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
|
357
375
|
const minutes = Math.floor(waited / 60_000);
|
|
358
376
|
switch (wait.reason) {
|
|
359
377
|
case "ci_failure":
|
|
360
|
-
return AUTO_LAND_CI_FAILED_BLOCKER;
|
|
378
|
+
return { blocker: AUTO_LAND_CI_FAILED_BLOCKER, reason: "ci_failed" };
|
|
361
379
|
case "ci_pending":
|
|
362
380
|
return waited >= AUTO_LAND_CI_PENDING_ATTENTION_MS
|
|
363
|
-
?
|
|
381
|
+
? {
|
|
382
|
+
blocker: `${AUTO_LAND_CI_PENDING_PREFIX}${minutes}m — a check on this head has not finished, so Auto land will not queue it`,
|
|
383
|
+
reason: "auto_land_ci_pending",
|
|
384
|
+
}
|
|
364
385
|
: null;
|
|
386
|
+
case "members_remaining":
|
|
387
|
+
return {
|
|
388
|
+
blocker: `${AUTO_LAND_MEMBERS_REMAINING_PREFIX}${wait.detail ?? AUTO_LAND_WAIT_WORDS.members_remaining}`,
|
|
389
|
+
reason: "auto_land_members_remaining",
|
|
390
|
+
};
|
|
365
391
|
case "vortex_not_green":
|
|
366
|
-
return waited >= AUTO_LAND_VORTEX_WAIT_ATTENTION_MS
|
|
367
|
-
?
|
|
392
|
+
return waited >= AUTO_LAND_VORTEX_WAIT_ATTENTION_MS && !vortexRetryScheduled(layer)
|
|
393
|
+
? {
|
|
394
|
+
blocker: `${AUTO_LAND_VORTEX_WAIT_PREFIX}${minutes}m — Auto land is waiting for an approving Vortex review at this head`,
|
|
395
|
+
reason: "auto_land_vortex_wait",
|
|
396
|
+
}
|
|
368
397
|
: null;
|
|
369
398
|
case "promote_merge_failed":
|
|
370
399
|
return (wait.attempts ?? 0) >= AUTO_LAND_PROMOTE_FAILED_ATTEMPTS
|
|
371
|
-
?
|
|
400
|
+
? {
|
|
401
|
+
blocker: `${AUTO_LAND_PROMOTE_FAILED_PREFIX}${wait.attempts} times (merge_failed) — Auto land stopped retrying this head${wait.detail ? `: ${wait.detail}` : ""}`,
|
|
402
|
+
reason: "auto_land_promote_failed",
|
|
403
|
+
}
|
|
372
404
|
: null;
|
|
373
405
|
default:
|
|
374
406
|
break;
|
|
@@ -376,11 +408,14 @@ export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
|
376
408
|
const unreadable = CI_UNREADABLE_WHAT[wait.reason];
|
|
377
409
|
if (unreadable) {
|
|
378
410
|
return ciUnreadableForGood(wait) || waited >= AUTO_LAND_CI_UNREADABLE_ATTENTION_MS
|
|
379
|
-
?
|
|
411
|
+
? {
|
|
412
|
+
blocker: `${AUTO_LAND_CI_UNREADABLE_PREFIX}${unreadable} on this head${wait.detail ? ` (${wait.detail})` : ""} — Auto land cannot tell whether CI passed`,
|
|
413
|
+
reason: "auto_land_ci_unreadable",
|
|
414
|
+
}
|
|
380
415
|
: null;
|
|
381
416
|
}
|
|
382
417
|
return waited >= AUTO_LAND_WAIT_ATTENTION_MS
|
|
383
|
-
? `${AUTO_LAND_WAITED_PREFIX}${minutes}m: ${autoLandWaitWords(wait.reason)}
|
|
418
|
+
? { blocker: `${AUTO_LAND_WAITED_PREFIX}${minutes}m: ${autoLandWaitWords(wait.reason)}`, reason: "auto_land_waited" }
|
|
384
419
|
: null;
|
|
385
420
|
}
|
|
386
421
|
export const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
|
|
@@ -484,27 +519,25 @@ function landPrLiveParent(stack, layer) {
|
|
|
484
519
|
const base = stack.unit?.landTarget?.trim() || layer.parentBranch?.trim();
|
|
485
520
|
return base && !isMgParkBase(base) && base !== layer.branch ? base : null;
|
|
486
521
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return false;
|
|
501
|
-
return attention.bounceKind === "tempest_findings" || /^Tempest findings\b/i.test(blocker) ||
|
|
502
|
-
/^(?:landing blocked:\s*)?tempest_findings\b/i.test(blocker);
|
|
522
|
+
const AGENTS_CANNOT_CLEAR = [
|
|
523
|
+
"restack_conflict",
|
|
524
|
+
"restack_failed",
|
|
525
|
+
"draft_pr",
|
|
526
|
+
"merge_conflict",
|
|
527
|
+
"seam_not_running",
|
|
528
|
+
...STUCK_SEAM_REASONS,
|
|
529
|
+
"vortex_out_of_quota",
|
|
530
|
+
"vortex_skipped",
|
|
531
|
+
"unit_abandoned",
|
|
532
|
+
];
|
|
533
|
+
export function agentsCannotClear(reason) {
|
|
534
|
+
return AGENTS_CANNOT_CLEAR.includes(reason);
|
|
503
535
|
}
|
|
504
536
|
export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
505
537
|
if (!attention || !layer || attention.prNumber !== layer.prNumber)
|
|
506
538
|
return null;
|
|
507
|
-
|
|
539
|
+
const reason = attention.reason;
|
|
540
|
+
if (queueWait && reason === "queue_stalled") {
|
|
508
541
|
return {
|
|
509
542
|
kind: "queue_stalled",
|
|
510
543
|
prNumber: layer.prNumber,
|
|
@@ -516,8 +549,8 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
516
549
|
};
|
|
517
550
|
}
|
|
518
551
|
const landPr = !!stack.unit?.branch && layer.prNumber === stack.unit.landPrNumber && layer.branch === stack.unit.branch;
|
|
519
|
-
const restackConflict =
|
|
520
|
-
const mergeConflict =
|
|
552
|
+
const restackConflict = reason === "restack_conflict" || attention.bounceKind === "restack_conflict";
|
|
553
|
+
const mergeConflict = reason === "merge_conflict";
|
|
521
554
|
if (restackConflict || mergeConflict) {
|
|
522
555
|
const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
|
|
523
556
|
return {
|
|
@@ -530,7 +563,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
530
563
|
steps: conflictRepairSteps(layer.branch, liveParent),
|
|
531
564
|
};
|
|
532
565
|
}
|
|
533
|
-
if (
|
|
566
|
+
if (reason === "restack_failed" && layer.restackError) {
|
|
534
567
|
const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
|
|
535
568
|
return {
|
|
536
569
|
kind: "restack_failed",
|
|
@@ -542,7 +575,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
542
575
|
steps: restackFailedRepairSteps(layer.branch, liveParent, layer.restackError),
|
|
543
576
|
};
|
|
544
577
|
}
|
|
545
|
-
if (
|
|
578
|
+
if (reason === "unit_abandoned") {
|
|
546
579
|
const openPrCount = stack.layers.filter((candidate) => candidate.prNumber > 0 && !isTerminalLayer(candidate)).length;
|
|
547
580
|
return {
|
|
548
581
|
kind: "unit_abandoned",
|
|
@@ -552,11 +585,11 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
552
585
|
steps: `The stack's review unit was abandoned when #${layer.prNumber}'s base on GitHub was changed off its mg-stack trunk, so Auto land cannot promote or land this stack. Ask the human whether to re-root it. ${openPrCount < 2 ? `With only one open PR, re-adopting #${layer.prNumber} will not create a review unit; close and resubmit it to create a fresh stack.` : `If yes, tell the human that mg stack adopt ${stack.owner}/${stack.repo}#${layer.prNumber} re-roots it on a new review unit.`} Do not retarget the PR base yourself.`,
|
|
553
586
|
};
|
|
554
587
|
}
|
|
555
|
-
if (
|
|
588
|
+
if (STUCK_SEAM_REASONS.includes(reason)) {
|
|
556
589
|
const member = stack.unit?.members?.find((entry) => entry.prNumber === layer.prNumber);
|
|
557
590
|
const seamState = member?.seamState?.trim().toLowerCase() || "unknown";
|
|
558
591
|
const reviewed = member?.seamReviewedSha?.trim();
|
|
559
|
-
const what =
|
|
592
|
+
const what = reason === "seam_verdict_stale"
|
|
560
593
|
? `The seam (integration) review verdict on #${layer.prNumber} (${seamState}) was made at ${reviewed ? reviewed.slice(0, 7) : "an older head"}, not the live head ${layer.headSha ?? "(unknown)"}, and no seam review is queued or running to replace it`
|
|
561
594
|
: `The seam (integration) review of #${layer.prNumber} is ${seamState} but no seam review is queued or running at ${layer.headSha ?? "the live head"}`;
|
|
562
595
|
return {
|
|
@@ -568,7 +601,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
568
601
|
steps: `${what}, so Auto land cannot promote it. Do not patch or push for this. Tell the human to click Continue on the stack in the Mergestorm dashboard, which re-queues the seam review at the live head of ${layer.branch}; a new commit pushed to ${layer.branch} also queues one. Then keep watching.`,
|
|
569
602
|
};
|
|
570
603
|
}
|
|
571
|
-
if (
|
|
604
|
+
if (reason === "auto_land_off" && attention.bounceKind) {
|
|
572
605
|
return {
|
|
573
606
|
kind: "auto_land_off",
|
|
574
607
|
prNumber: layer.prNumber,
|
|
@@ -578,48 +611,39 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
578
611
|
steps: `The ${attention.bounceKind} bounce turned Auto land off, so nothing will enqueue this stack. Fix the cause of the bounce on ${layer.branch}, then turn Auto land back on (mg stack set ${stack.id} --auto-land on) or enqueue the stack.`,
|
|
579
612
|
};
|
|
580
613
|
}
|
|
581
|
-
if (
|
|
582
|
-
const findings = `gh api repos/${stack.owner}/${stack.repo}/issues/${layer.prNumber}/comments`;
|
|
614
|
+
if (reason === "vortex_failed") {
|
|
583
615
|
return {
|
|
584
|
-
kind: "
|
|
616
|
+
kind: "vortex_failed",
|
|
585
617
|
prNumber: layer.prNumber,
|
|
586
618
|
headSha: layer.headSha ?? null,
|
|
587
619
|
branch: layer.branch,
|
|
588
|
-
|
|
589
|
-
steps: `
|
|
620
|
+
detail: null,
|
|
621
|
+
steps: `Vortex's review of #${layer.prNumber} at ${layer.headSha ?? "its head"} failed and nothing will run it again by itself, so Auto land waits. There are no findings to fix. Run the review again: comment @mergestorm-vortex review on #${layer.prNumber} (gh pr comment ${layer.prNumber} --repo ${stack.owner}/${stack.repo} --body "@mergestorm-vortex review"); a new commit pushed to ${layer.branch} also queues one. If it fails again, tell the human.`,
|
|
590
622
|
};
|
|
591
623
|
}
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
const rerun = landPr ? rerunBlocker : null;
|
|
595
|
-
if (rerun) {
|
|
596
|
-
const stoppedByPerson = rerun[1].toLowerCase() === "stopped";
|
|
597
|
-
const comment = `gh pr comment ${layer.prNumber} --repo ${stack.owner}/${stack.repo} --body`;
|
|
598
|
-
const head = layer.headSha ?? "its live head";
|
|
624
|
+
const vortexSkip = vortexIdleSkipBlocker(layer);
|
|
625
|
+
if (vortexSkip && reason === "vortex_skipped") {
|
|
599
626
|
return {
|
|
600
|
-
kind: "
|
|
627
|
+
kind: "vortex_skipped",
|
|
601
628
|
prNumber: layer.prNumber,
|
|
602
629
|
headSha: layer.headSha ?? null,
|
|
603
630
|
branch: layer.branch,
|
|
604
|
-
|
|
605
|
-
steps:
|
|
606
|
-
? `Tempest's review of land PR #${layer.prNumber} at ${head} was stopped, so Auto land will not land it. There are no findings to fix: do not patch or push. Ask the human whether to run Tempest again. If they say yes, run ${comment} "@mergestorm-tempest review --force" and keep watching.`
|
|
607
|
-
: `Tempest's review of land PR #${layer.prNumber} at ${head} broke before it produced a result, and Mergestorm already re-ran it the most times it will on its own. There are no findings to fix: do not patch or push. Run it once more with ${comment} "@mergestorm-tempest review" and keep watching. If it breaks again, tell the human.`,
|
|
631
|
+
skipReason: vortexSkip.skipReason,
|
|
632
|
+
steps: vortexSkippedSteps(stack, layer, vortexSkip.skipReason),
|
|
608
633
|
};
|
|
609
634
|
}
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
635
|
+
const wait = autoLandWaitOn(stack, layer);
|
|
636
|
+
if (wait?.reason === "members_remaining" && reason === "auto_land_members_remaining") {
|
|
612
637
|
return {
|
|
613
|
-
kind: "
|
|
638
|
+
kind: "members_remaining",
|
|
614
639
|
prNumber: layer.prNumber,
|
|
615
640
|
headSha: layer.headSha ?? null,
|
|
616
641
|
branch: layer.branch,
|
|
617
|
-
|
|
618
|
-
steps:
|
|
642
|
+
detail: wait.detail,
|
|
643
|
+
steps: `Auto land will not send land PR #${layer.prNumber} to the merge queue while another PR is open on ${layer.branch} that is not a layer of this stack${wait.detail ? ` (${wait.detail})` : ""}. Nothing on ${layer.branch} needs a change: do not patch or push for this. Close that PR, retarget it to another base, or adopt it into the stack (mg stack adopt ${stack.owner}/${stack.repo}#<that PR>); a draft PR or a PR from a fork cannot be adopted, so close or retarget it. If the PR is not yours, tell the human. Auto land sends the land PR to the queue by itself once that PR is gone from ${layer.branch}.`,
|
|
619
644
|
};
|
|
620
645
|
}
|
|
621
|
-
|
|
622
|
-
if (wait && attention.blocker.startsWith(AUTO_LAND_CI_PENDING_PREFIX)) {
|
|
646
|
+
if (wait && reason === "auto_land_ci_pending") {
|
|
623
647
|
return {
|
|
624
648
|
kind: "ci_pending",
|
|
625
649
|
prNumber: layer.prNumber,
|
|
@@ -629,7 +653,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
629
653
|
steps: `A check on ${layer.branch} at ${layer.headSha ?? "its head"} has been pending since ${wait.since}, and Auto land will not queue an unfinished head. List the head's checks (gh api repos/${stack.owner}/${stack.repo}/commits/${layer.headSha ?? "<sha>"}/check-runs, and gh run list --branch ${layer.branch}) and find the one that never finished. Re-run it (gh run rerun <run-id>) if it is stuck or was never picked up. If it is a required check that no workflow reports, or it fails again, tell the human. Push a fix only when the check is red for a reason in the code. Do not retarget the PR base.`,
|
|
630
654
|
};
|
|
631
655
|
}
|
|
632
|
-
if (wait &&
|
|
656
|
+
if (wait && reason === "auto_land_vortex_wait") {
|
|
633
657
|
return {
|
|
634
658
|
kind: "vortex_not_green",
|
|
635
659
|
prNumber: layer.prNumber,
|
|
@@ -639,7 +663,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
639
663
|
steps: `Auto land has waited since ${wait.since} for an approving Vortex review of ${layer.branch} at ${layer.headSha ?? "its head"}, and no Vortex run is working on it. Read the PR's latest Vortex review (gh api repos/${stack.owner}/${stack.repo}/pulls/${layer.prNumber}/reviews). If it has findings, verify and fix the real ones on ${layer.branch} with an ordinary push, or record a wrong one with mg dismiss as mergestorm-pr-loop describes. If no review exists at this head, or it failed or was skipped, tell the human: nothing re-reviews this head until they re-run the review or push a new head.`,
|
|
640
664
|
};
|
|
641
665
|
}
|
|
642
|
-
if (wait &&
|
|
666
|
+
if (wait && reason === "auto_land_ci_unreadable") {
|
|
643
667
|
const what = CI_UNREADABLE_WHAT[wait.reason] ?? "the checks";
|
|
644
668
|
const permission = CI_UNREADABLE_PERMISSION[wait.reason] ?? "Checks: read and Commit statuses: read";
|
|
645
669
|
return {
|
|
@@ -652,7 +676,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
652
676
|
steps: `GitHub has not let Mergestorm read ${what} for #${layer.prNumber} at ${layer.headSha ?? "its head"} since ${wait.since}${wait.detail ? ` (${wait.detail})` : ""}, so Auto land cannot tell whether CI passed and will not move this PR. Nothing on ${layer.branch} needs a change: do not patch or push for this. Tell the human: the Mergestorm GitHub App needs the permission ${permission} on ${stack.owner}/${stack.repo}. They open the app's installation on GitHub (Settings, then GitHub Apps or Installed GitHub Apps), accept the pending permission request, and check that ${stack.repo} is in the app's repository access. Auto land continues by itself once the read works. If GitHub itself is having an outage, the wait clears when it ends.`,
|
|
653
677
|
};
|
|
654
678
|
}
|
|
655
|
-
if (wait &&
|
|
679
|
+
if (wait && reason === "auto_land_waited") {
|
|
656
680
|
return {
|
|
657
681
|
kind: "auto_land_waiting",
|
|
658
682
|
prNumber: layer.prNumber,
|
|
@@ -663,7 +687,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
663
687
|
steps: `Auto land has been waiting on this stack since ${wait.since} because ${autoLandWaitWords(wait.reason)} (reason ${wait.reason}${wait.detail ? `: ${wait.detail}` : ""}), and nothing has changed that in the meantime. Run mg stack status ${stack.id} and read what it shows for #${layer.prNumber}. If it names something on ${layer.branch} that a commit fixes, fix it there with an ordinary push. Otherwise do not patch or push: tell the human what Auto land is waiting for and since when. Do not retarget the PR base.`,
|
|
664
688
|
};
|
|
665
689
|
}
|
|
666
|
-
if (wait &&
|
|
690
|
+
if (wait && reason === "auto_land_promote_failed") {
|
|
667
691
|
const attempts = wait.attempts ?? AUTO_LAND_PROMOTE_FAILED_ATTEMPTS;
|
|
668
692
|
return {
|
|
669
693
|
kind: "promote_failed",
|
|
@@ -675,7 +699,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
675
699
|
steps: `Auto land tried to promote #${layer.prNumber} at ${layer.headSha ?? "its head"} ${attempts} times and GitHub refused the merge each time${wait.detail ? ` (${wait.detail})` : ""}, so it stopped retrying this head. Read why: gh api repos/${stack.owner}/${stack.repo}/pulls/${layer.prNumber} --jq '{mergeable, mergeable_state, base: .base.ref}', and the branch protection on that base. If the cause is in ${layer.branch}, fix it and push with an ordinary push: a new head is retried. If it is outside the PR (branch protection, permissions, a required review), tell the human, and that once it is fixed they can retry the promote by turning Auto land off and on (mg stack set ${stack.id} --auto-land off, then --auto-land on). Do not toggle Auto land yourself, and do not retarget the PR base.`,
|
|
676
700
|
};
|
|
677
701
|
}
|
|
678
|
-
if (
|
|
702
|
+
if (reason === "vortex_findings_need_person") {
|
|
679
703
|
const findings = `gh api repos/${stack.owner}/${stack.repo}/pulls/${layer.prNumber}/reviews`;
|
|
680
704
|
const handoff = cycloneHandoffAtHead(stack, layer);
|
|
681
705
|
return {
|
|
@@ -688,7 +712,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
688
712
|
steps: `Vortex requested changes on ${layer.branch} at ${layer.headSha ?? "its live head"} and Cyclone left them for a person${handoff ? ` (${cycloneHandoffWords(handoff)})` : ""}, so nothing will patch them and Auto land waits. Read the newest Vortex review and its inline comments (${findings}), verify each finding against ${layer.branch} at its live remote head, fix the real ones with the smallest patch, run the tests, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push (no force). If a finding is wrong, tell the human; mg dismiss ${stack.owner}/${stack.repo}#${layer.prNumber} --head <sha> --review <id> dismisses that review. Do not retarget the PR base.`,
|
|
689
713
|
};
|
|
690
714
|
}
|
|
691
|
-
if (
|
|
715
|
+
if (reason === "ci_failed" || attention.bounceKind === "ci_failure") {
|
|
692
716
|
const failingCheck = bounce?.bounceDetail?.failingCheck?.trim() || layer.checks?.failingName?.trim() || null;
|
|
693
717
|
return {
|
|
694
718
|
kind: "ci_failure",
|
|
@@ -701,6 +725,44 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
701
725
|
}
|
|
702
726
|
return null;
|
|
703
727
|
}
|
|
728
|
+
export const LAND_PR_MISSING_PREFIX = "No open land PR for ";
|
|
729
|
+
function landPrMissing(stack, candidate) {
|
|
730
|
+
const unit = stack.unit;
|
|
731
|
+
if (!unit || candidate || stack.archivedAt || unit.state === "landed" || unit.state === "abandoned")
|
|
732
|
+
return null;
|
|
733
|
+
if (stack.layers.some((layer) => !isTerminalLayer(layer)))
|
|
734
|
+
return null;
|
|
735
|
+
const landing = unit.landingBlockReason?.trim();
|
|
736
|
+
if (!landing || landGateIsPending(landing) || unit.landPr?.state === "merged")
|
|
737
|
+
return null;
|
|
738
|
+
const closed = unit.landPr && unit.landPr.prNumber > 0 ? unit.landPr : null;
|
|
739
|
+
const prNumber = unit.landPrNumber ?? closed?.prNumber ??
|
|
740
|
+
stack.layers.filter((layer) => layer.prNumber > 0).sort((a, b) => b.position - a.position)[0]?.prNumber;
|
|
741
|
+
if (prNumber == null || prNumber <= 0)
|
|
742
|
+
return null;
|
|
743
|
+
const headSha = closed?.headSha ?? null;
|
|
744
|
+
const target = unit.landTarget?.trim() || stack.landTarget;
|
|
745
|
+
const detail = landing.replace(/^landing blocked:\s*/i, "").replace(/^land_pr_unavailable:\s*/i, "").trim() || null;
|
|
746
|
+
const landPrRef = unit.landPrNumber ?? closed?.prNumber ?? null;
|
|
747
|
+
const restore = landPrRef != null
|
|
748
|
+
? `restore it from the closed land PR's head (gh pr view ${landPrRef} --repo ${stack.owner}/${stack.repo} --json headRefOid,state, then git push origin <that sha>:refs/heads/${unit.branch}) and reopen #${landPrRef}`
|
|
749
|
+
: `restore it from the commit it pointed at (git push origin <sha>:refs/heads/${unit.branch})`;
|
|
750
|
+
return {
|
|
751
|
+
reason: "land_pr_missing",
|
|
752
|
+
attention: {
|
|
753
|
+
prNumber, headSha, bounceKind: null,
|
|
754
|
+
blocker: `${LAND_PR_MISSING_PREFIX}${unit.branch} — every layer is merged into it, but it cannot land in ${target} without one`,
|
|
755
|
+
},
|
|
756
|
+
repair: {
|
|
757
|
+
kind: "land_pr_unavailable",
|
|
758
|
+
prNumber,
|
|
759
|
+
headSha,
|
|
760
|
+
branch: unit.branch,
|
|
761
|
+
detail,
|
|
762
|
+
steps: `Every layer of this stack is merged into ${unit.branch}, but there is no open land PR from ${unit.branch} into ${target} and Mergestorm cannot open one${detail ? ` (${detail})` : ""}, so the stack cannot land. Nothing in the layers needs a change: do not patch or push for this. Check whether the stack branch still exists: git ls-remote origin refs/heads/${unit.branch}. If it is gone (deleting it makes GitHub close the land PR), tell the human: ${restore}, or close the stack if the work is no longer wanted. If the branch exists, tell the human that Mergestorm cannot open the land PR and give them the error above. Do not open a PR into ${target} yourself.`,
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
}
|
|
704
766
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
705
767
|
export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now()) {
|
|
706
768
|
const candidate = currentLayer(stack);
|
|
@@ -714,9 +776,11 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
714
776
|
(boundDirty(layer) || layer.state === "conflict" ||
|
|
715
777
|
(layer.state !== "restacking" && layer.restackError?.kind === "rebase_conflict"))).sort((a, b) => a.position - b.position)[0];
|
|
716
778
|
let attention = null;
|
|
779
|
+
let attentionReason = null;
|
|
717
780
|
let attentionLayer;
|
|
718
781
|
let bounce;
|
|
719
782
|
const issues = [];
|
|
783
|
+
const mergeStateUnknownPrs = new Set();
|
|
720
784
|
const queueWait = stack.archivedAt ? null : mergeQueueWait(queue, nowMs);
|
|
721
785
|
for (const layer of open) {
|
|
722
786
|
const selected = layer.prNumber === candidate?.prNumber;
|
|
@@ -726,22 +790,26 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
726
790
|
disarmed = disarmingBounce(stack, queue);
|
|
727
791
|
if (disarmed) {
|
|
728
792
|
const kind = disarmed.bounceDetail.kind;
|
|
729
|
-
result = { blocker: `${AUTO_LAND_OFF_BLOCKER_PREFIX}${kind} bounce`, bounceKind: kind, bounce: disarmed };
|
|
793
|
+
result = { blocker: `${AUTO_LAND_OFF_BLOCKER_PREFIX}${kind} bounce`, reason: "auto_land_off", bounceKind: kind, bounce: disarmed };
|
|
730
794
|
}
|
|
731
795
|
}
|
|
732
796
|
let waitNamed = false;
|
|
733
797
|
if (!result.blocker) {
|
|
734
|
-
const waiting =
|
|
798
|
+
const waiting = autoLandWaitNamed(stack, layer, nowMs);
|
|
735
799
|
if (waiting) {
|
|
736
|
-
result = {
|
|
800
|
+
result = { ...waiting, bounceKind: null };
|
|
737
801
|
waitNamed = true;
|
|
738
802
|
}
|
|
739
803
|
}
|
|
740
804
|
if (!result.blocker && selected && queueWait?.stalled) {
|
|
741
|
-
result = { blocker: `${MERGE_QUEUE_STALLED_PREFIX}${queueWait.text}`, bounceKind: null };
|
|
805
|
+
result = { blocker: `${MERGE_QUEUE_STALLED_PREFIX}${queueWait.text}`, reason: "queue_stalled", bounceKind: null };
|
|
742
806
|
}
|
|
743
807
|
if (!result.blocker) {
|
|
808
|
+
if (vortexRetryScheduled(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
809
|
+
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null, blocker: VORTEX_RETRY_SCHEDULED, bounceKind: null });
|
|
810
|
+
}
|
|
744
811
|
if (mergeabilityPending(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
812
|
+
mergeStateUnknownPrs.add(layer.prNumber);
|
|
745
813
|
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
746
814
|
blocker: `Merge state unknown${layer.parentBranch ? ` vs ${layer.parentBranch}` : ""}`, bounceKind: null });
|
|
747
815
|
}
|
|
@@ -750,9 +818,10 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
750
818
|
const issue = { prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
751
819
|
blocker: result.blocker, bounceKind: result.bounceKind };
|
|
752
820
|
if (selected ||
|
|
753
|
-
result.
|
|
821
|
+
result.reason === "unit_abandoned" ||
|
|
754
822
|
(!attention && (layer === child || waitNamed))) {
|
|
755
823
|
attention = issue;
|
|
824
|
+
attentionReason = result.reason;
|
|
756
825
|
attentionLayer = layer;
|
|
757
826
|
bounce = result.bounce;
|
|
758
827
|
if (disarmed)
|
|
@@ -764,9 +833,10 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
764
833
|
}
|
|
765
834
|
if (!attention) {
|
|
766
835
|
const unlisted = autoLandWaitLayer(stack);
|
|
767
|
-
const waiting = unlisted && !open.includes(unlisted) ?
|
|
836
|
+
const waiting = unlisted && !open.includes(unlisted) ? autoLandWaitNamed(stack, unlisted, nowMs) : null;
|
|
768
837
|
if (unlisted && waiting) {
|
|
769
|
-
attention = { prNumber: unlisted.prNumber, headSha: unlisted.headSha ?? null, blocker: waiting, bounceKind: null };
|
|
838
|
+
attention = { prNumber: unlisted.prNumber, headSha: unlisted.headSha ?? null, blocker: waiting.blocker, bounceKind: null };
|
|
839
|
+
attentionReason = waiting.reason;
|
|
770
840
|
attentionLayer = unlisted;
|
|
771
841
|
}
|
|
772
842
|
}
|
|
@@ -774,9 +844,18 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
774
844
|
? { prNumber: candidate.prNumber, headSha: candidate.headSha ?? null, text: queueWait.text,
|
|
775
845
|
since: queueWait.since, waitReason: queueWait.waitReason }
|
|
776
846
|
: null;
|
|
777
|
-
const settleIssue = mergeabilitySettleIssue(stack, open,
|
|
847
|
+
const settleIssue = mergeabilitySettleIssue(stack, open, mergeStateUnknownPrs, attention);
|
|
778
848
|
if (settleIssue)
|
|
779
849
|
issues.push(settleIssue);
|
|
850
|
+
const landMissing = landPrMissing(stack, candidate);
|
|
851
|
+
if (landMissing) {
|
|
852
|
+
attention = landMissing.attention;
|
|
853
|
+
attentionReason = landMissing.reason;
|
|
854
|
+
attentionLayer = undefined;
|
|
855
|
+
}
|
|
856
|
+
const classified = attention && attentionReason
|
|
857
|
+
? { ...attention, reason: attentionReason }
|
|
858
|
+
: null;
|
|
780
859
|
const focus = attentionLayer ?? candidate;
|
|
781
860
|
const agents = focus ? layerAgents(focus) : null;
|
|
782
861
|
const busy = [];
|
|
@@ -793,25 +872,26 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
793
872
|
const landGatePending = landLayer && landing && landGateIsPending(landing)
|
|
794
873
|
? { prNumber: landLayer.prNumber, headSha: landLayer.headSha ?? null, reason: landing }
|
|
795
874
|
: null;
|
|
796
|
-
if (busy.length && attention && agentsCannotClear(
|
|
875
|
+
if (busy.length && attention && classified && agentsCannotClear(classified.reason)) {
|
|
797
876
|
const held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
|
|
798
877
|
return { attention: null, held, issues: [attention, ...issues], currentCandidate, bounce: undefined, busy, agents,
|
|
799
|
-
repair: stackRepair(stack,
|
|
878
|
+
repair: stackRepair(stack, classified, attentionLayer, undefined, queueWait), landGatePending, queueWait: queued };
|
|
800
879
|
}
|
|
801
880
|
if (busy.length) {
|
|
802
881
|
return { attention: null, held: null, issues, currentCandidate, bounce: undefined, busy, agents, repair: null, landGatePending,
|
|
803
882
|
queueWait: queued };
|
|
804
883
|
}
|
|
805
884
|
return { attention, held: null, issues, currentCandidate, bounce, busy, agents,
|
|
806
|
-
repair: stackRepair(stack,
|
|
885
|
+
repair: landMissing?.repair ?? stackRepair(stack, classified, attentionLayer, bounce, queueWait), landGatePending,
|
|
886
|
+
queueWait: queued };
|
|
807
887
|
}
|
|
808
|
-
function mergeabilitySettleIssue(stack, open,
|
|
888
|
+
function mergeabilitySettleIssue(stack, open, mergeStateUnknownPrs, attention) {
|
|
809
889
|
const settle = stack.autoEnqueueSettle;
|
|
810
890
|
if (stack.archivedAt || settle?.action !== "mergeability")
|
|
811
891
|
return null;
|
|
812
892
|
if (attention?.prNumber === settle.prNumber)
|
|
813
893
|
return null;
|
|
814
|
-
if (
|
|
894
|
+
if (mergeStateUnknownPrs.has(settle.prNumber))
|
|
815
895
|
return null;
|
|
816
896
|
const layer = open.find((candidate) => candidate.prNumber === settle.prNumber);
|
|
817
897
|
if (layer && !sameHead(layer.headSha, settle.headSha))
|