mergestorm 0.3.35 → 0.3.37
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 +1 -1
- package/dist/commands/stack.d.ts +2 -1
- package/dist/commands/stack.js +9 -4
- package/dist/stack-blockers.d.ts +59 -2
- package/dist/stack-blockers.js +187 -14
- package/dist/stack-dto.d.ts +10 -0
- package/dist/stack-watch.d.ts +2 -1
- package/dist/stack-watch.js +3 -3
- package/dist/ui/banner.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
|
|
|
45
45
|
|
|
46
46
|
```
|
|
47
47
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
48
|
-
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.
|
|
48
|
+
│ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.37 │
|
|
49
49
|
│ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
|
|
50
50
|
│ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
|
|
51
51
|
│ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type StackHeldBlocker, type StackRepairHint } from "../stack-blockers.js";
|
|
2
2
|
import { type StackWatchObligation } from "../stack-watch-obligation.js";
|
|
3
|
-
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, listStacks, openStackPull, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
3
|
+
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, listMergeQueueEntries, listStacks, openStackPull, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { createPrs, findOpenPrNumber } from "../gh.js";
|
|
6
6
|
import { parseGithubOriginRepo } from "../git.js";
|
|
@@ -147,6 +147,7 @@ export declare function cmdStackWatch(args: string[], deps?: {
|
|
|
147
147
|
export declare function cmdStackStatus(args: string[], deps?: {
|
|
148
148
|
loadConfig?: typeof loadConfig;
|
|
149
149
|
getEnrichedStack?: typeof getEnrichedStack;
|
|
150
|
+
listMergeQueueEntries?: typeof listMergeQueueEntries;
|
|
150
151
|
}): Promise<void>;
|
|
151
152
|
export type StackSetDeps = {
|
|
152
153
|
loadConfig?: typeof loadConfig;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
|
|
2
2
|
import { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
|
|
3
|
-
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, landNextStack, listStacks, openStackPull, restackStack, setStackPolicy, } from "../api.js";
|
|
3
|
+
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, landNextStack, listMergeQueueEntries, listStacks, openStackPull, restackStack, setStackPolicy, } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
|
|
6
6
|
import { createPrs, findOpenPrNumber, ghReady, githubApiFromEnv, } from "../gh.js";
|
|
@@ -910,7 +910,7 @@ export async function cmdStackWait(args, deps = {}) {
|
|
|
910
910
|
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
911
911
|
}, null, 2)
|
|
912
912
|
: [
|
|
913
|
-
`Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending ?? null, envelope.actAfter ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`,
|
|
913
|
+
`Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending ?? null, envelope.actAfter ?? null, envelope.queueWait ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`,
|
|
914
914
|
...stackRepairLine(envelope.repair),
|
|
915
915
|
...stackWatchTextLines(envelope.watch),
|
|
916
916
|
].join("\n"));
|
|
@@ -1018,7 +1018,10 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
1018
1018
|
console.log(watch.message);
|
|
1019
1019
|
throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
|
|
1020
1020
|
}
|
|
1021
|
-
const
|
|
1021
|
+
const entries = await (deps.listMergeQueueEntries ?? listMergeQueueEntries)(cfg, {
|
|
1022
|
+
stackId: stack.id,
|
|
1023
|
+
});
|
|
1024
|
+
const { attention, held, currentCandidate, repair, queueWait } = stackBlockers(stack, entries);
|
|
1022
1025
|
const watch = stackWatchObligation({
|
|
1023
1026
|
stackId: stack.id,
|
|
1024
1027
|
terminal: stackTerminalReason(stack),
|
|
@@ -1027,7 +1030,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
1027
1030
|
freshCursor: true,
|
|
1028
1031
|
});
|
|
1029
1032
|
if (asJson) {
|
|
1030
|
-
console.log(JSON.stringify({ ...stack, held, repair, watch }, null, 2));
|
|
1033
|
+
console.log(JSON.stringify({ ...stack, held, repair, queueWait, watch }, null, 2));
|
|
1031
1034
|
return;
|
|
1032
1035
|
}
|
|
1033
1036
|
const lines = formatStackHuman(stack, false);
|
|
@@ -1040,6 +1043,8 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
1040
1043
|
(layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
|
|
1041
1044
|
(layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
|
|
1042
1045
|
}
|
|
1046
|
+
if (queueWait && !attention)
|
|
1047
|
+
lines.push(` Queue: #${queueWait.prNumber} ${queueWait.text}`);
|
|
1043
1048
|
lines.push(...[...stackHeldLine(held), ...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
|
|
1044
1049
|
await present("Stack status", lines);
|
|
1045
1050
|
}
|
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ export type StackRepairHint = {
|
|
|
54
54
|
branch: string;
|
|
55
55
|
findings: string;
|
|
56
56
|
steps: string;
|
|
57
|
+
} | {
|
|
58
|
+
kind: "tempest_rerun";
|
|
59
|
+
prNumber: number;
|
|
60
|
+
headSha: string | null;
|
|
61
|
+
branch: string;
|
|
62
|
+
stoppedByPerson: boolean;
|
|
63
|
+
steps: string;
|
|
57
64
|
} | {
|
|
58
65
|
kind: "vortex_findings";
|
|
59
66
|
prNumber: number;
|
|
@@ -68,6 +75,14 @@ export type StackRepairHint = {
|
|
|
68
75
|
headSha: string | null;
|
|
69
76
|
branch: string;
|
|
70
77
|
steps: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "restack_failed";
|
|
80
|
+
prNumber: number;
|
|
81
|
+
headSha: string | null;
|
|
82
|
+
branch: string;
|
|
83
|
+
liveParent: string | null;
|
|
84
|
+
restackKind: string;
|
|
85
|
+
steps: string;
|
|
71
86
|
} | {
|
|
72
87
|
kind: "seam_review_stuck";
|
|
73
88
|
prNumber: number;
|
|
@@ -89,6 +104,21 @@ export type StackRepairHint = {
|
|
|
89
104
|
branch: string;
|
|
90
105
|
since: string;
|
|
91
106
|
steps: string;
|
|
107
|
+
} | {
|
|
108
|
+
kind: "queue_stalled";
|
|
109
|
+
prNumber: number;
|
|
110
|
+
headSha: string | null;
|
|
111
|
+
branch: string;
|
|
112
|
+
since: string | null;
|
|
113
|
+
waitReason: string | null;
|
|
114
|
+
steps: string;
|
|
115
|
+
} | {
|
|
116
|
+
kind: "vortex_skipped";
|
|
117
|
+
prNumber: number;
|
|
118
|
+
headSha: string | null;
|
|
119
|
+
branch: string;
|
|
120
|
+
skipReason: string;
|
|
121
|
+
steps: string;
|
|
92
122
|
} | {
|
|
93
123
|
kind: "promote_failed";
|
|
94
124
|
prNumber: number;
|
|
@@ -112,21 +142,46 @@ export declare function boundDirty(layer: StackLayerDto): boolean;
|
|
|
112
142
|
export declare function mergeabilityPending(layer: StackLayerDto): boolean;
|
|
113
143
|
export declare function sameHead(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
114
144
|
export declare function currentLayer(stack: StackDto): StackLayerDto | undefined;
|
|
145
|
+
export declare const RESTACK_FAILED = "Restack failed";
|
|
115
146
|
export declare const SEAM_REVIEW_NOT_RUNNING = "Seam review pending, no review running";
|
|
116
147
|
export declare const VORTEX_REVIEW_OUT_OF_QUOTA = "Review skipped, out of review quota";
|
|
117
148
|
export declare const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no review running";
|
|
118
149
|
export declare const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
|
|
119
150
|
export declare const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
|
|
120
151
|
export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a person";
|
|
152
|
+
export declare const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
|
|
153
|
+
export declare const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
|
|
154
|
+
export declare const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
|
|
121
155
|
export declare const AUTO_LAND_CI_PENDING_ATTENTION_MS: number;
|
|
122
156
|
export declare const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS: number;
|
|
123
157
|
export declare const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
|
|
124
158
|
export declare const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed \u2014 Auto land reads a failing check on GitHub at this head";
|
|
125
159
|
export declare function autoLandWaitBlocker(stack: StackDto, layer: StackLayerDto, nowMs: number): string | null;
|
|
160
|
+
export declare const MERGE_QUEUE_STALL_ATTENTION_MS: number;
|
|
161
|
+
export type StackQueueWait = {
|
|
162
|
+
prNumber: number;
|
|
163
|
+
headSha: string | null;
|
|
164
|
+
text: string;
|
|
165
|
+
since: string | null;
|
|
166
|
+
waitReason: string | null;
|
|
167
|
+
};
|
|
168
|
+
export type MergeQueueWait = {
|
|
169
|
+
text: string;
|
|
170
|
+
stalled: boolean;
|
|
171
|
+
since: string | null;
|
|
172
|
+
waitReason: string | null;
|
|
173
|
+
aheadStackId: string | null;
|
|
174
|
+
aheadOther: boolean;
|
|
175
|
+
};
|
|
176
|
+
export declare function mergeQueueWait(queue: readonly MergeQueueEntryDto[], nowMs: number): MergeQueueWait | null;
|
|
126
177
|
export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
|
|
127
178
|
export declare function conflictRepairSteps(branch: string, liveParent: string | null): string;
|
|
179
|
+
export declare function restackFailedRepairSteps(branch: string, liveParent: string | null, failure: {
|
|
180
|
+
kind: string;
|
|
181
|
+
detail?: string | null;
|
|
182
|
+
}): string;
|
|
128
183
|
export declare function agentsCannotClear(blocker: string): boolean;
|
|
129
|
-
export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined): StackRepairHint | null;
|
|
184
|
+
export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null): StackRepairHint | null;
|
|
130
185
|
/** Pair-gate attention plus live hard blocks on every other open layer. */
|
|
131
186
|
export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">, nowMs?: number): {
|
|
132
187
|
attention: null;
|
|
@@ -141,6 +196,7 @@ export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntry
|
|
|
141
196
|
agents: StackLayerAgents | null;
|
|
142
197
|
repair: StackRepairHint | null;
|
|
143
198
|
landGatePending: StackLandGatePending | null;
|
|
199
|
+
queueWait: StackQueueWait | null;
|
|
144
200
|
} | {
|
|
145
201
|
attention: StackBlocker | null;
|
|
146
202
|
held: null;
|
|
@@ -154,6 +210,7 @@ export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntry
|
|
|
154
210
|
agents: StackLayerAgents | null;
|
|
155
211
|
repair: StackRepairHint | null;
|
|
156
212
|
landGatePending: StackLandGatePending | null;
|
|
213
|
+
queueWait: StackQueueWait | null;
|
|
157
214
|
};
|
|
158
215
|
/** Compact contract text shared by status and wait summaries. */
|
|
159
|
-
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null, actAfter?: StackHeldBlocker["actAfter"] | null): string;
|
|
216
|
+
export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null, actAfter?: StackHeldBlocker["actAfter"] | null, queueWait?: Pick<StackQueueWait, "prNumber" | "text"> | null): string;
|
package/dist/stack-blockers.js
CHANGED
|
@@ -51,6 +51,7 @@ export function currentLayer(stack) {
|
|
|
51
51
|
return promote[0] ?? open.find((layer) => layer.prNumber === stack.unit?.landPrNumber) ??
|
|
52
52
|
(land && land.prNumber > 0 && land.state !== "merged" && land.state !== "closed" ? land : undefined);
|
|
53
53
|
}
|
|
54
|
+
export const RESTACK_FAILED = "Restack failed";
|
|
54
55
|
export const SEAM_REVIEW_NOT_RUNNING = "Seam review pending, no review running";
|
|
55
56
|
export const VORTEX_REVIEW_OUT_OF_QUOTA = "Review skipped, out of review quota";
|
|
56
57
|
export const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no review running";
|
|
@@ -71,7 +72,16 @@ function vortexChangesRequestedAtHead(layer) {
|
|
|
71
72
|
function cycloneHandoffAtHead(stack, layer) {
|
|
72
73
|
if (sameHead(layer.cycloneHandoff?.headSha, layer.headSha))
|
|
73
74
|
return layer.cycloneHandoff.reason;
|
|
74
|
-
return stack.autoPatchOverride === false ? "auto_patch_off" : null;
|
|
75
|
+
return stack.cyclonePatchOff ?? (stack.autoPatchOverride === false ? "auto_patch_off" : null);
|
|
76
|
+
}
|
|
77
|
+
const CYCLONE_HANDOFF_WORDS = {
|
|
78
|
+
auto_patch_off: "auto-patch is off for this stack",
|
|
79
|
+
stack_auto_patch_off: "auto-patch is off for this stack",
|
|
80
|
+
account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
|
|
81
|
+
cyclone_not_connected: "Cyclone is not connected to this repo",
|
|
82
|
+
};
|
|
83
|
+
function cycloneHandoffWords(reason) {
|
|
84
|
+
return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
|
|
75
85
|
}
|
|
76
86
|
function vortexFindingsLeftForPerson(stack, layer) {
|
|
77
87
|
return vortexChangesRequestedAtHead(layer) && !layerAgentsBusy(layer).cyclone &&
|
|
@@ -82,6 +92,40 @@ function quotaSkippedReview(layer) {
|
|
|
82
92
|
return review?.status === "skipped" && review.skip_reason === "quota_exceeded" &&
|
|
83
93
|
sameHead(review.head_sha, layer.headSha);
|
|
84
94
|
}
|
|
95
|
+
export const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
|
|
96
|
+
export const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
|
|
97
|
+
export const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
|
|
98
|
+
const VORTEX_SKIP_BLOCKERS = {
|
|
99
|
+
auto_review_off: VORTEX_AUTO_REVIEW_OFF,
|
|
100
|
+
billing_blocked: VORTEX_BILLING_SKIPPED,
|
|
101
|
+
billing_unavailable: VORTEX_BILLING_SKIPPED,
|
|
102
|
+
};
|
|
103
|
+
function vortexIdleSkipBlocker(layer) {
|
|
104
|
+
if (layerAgentsBusy(layer).vortex)
|
|
105
|
+
return null;
|
|
106
|
+
const review = layer.vortexReview;
|
|
107
|
+
if (review?.status === "skipped" && sameHead(review.head_sha, layer.headSha)) {
|
|
108
|
+
const reason = review.skip_reason?.trim() ?? "";
|
|
109
|
+
const blocker = VORTEX_SKIP_BLOCKERS[reason];
|
|
110
|
+
if (blocker)
|
|
111
|
+
return { blocker, skipReason: reason };
|
|
112
|
+
}
|
|
113
|
+
if (layer.vortexStatus === "incomplete" && sameHead(review?.head_sha, layer.headSha)) {
|
|
114
|
+
return { blocker: VORTEX_REVIEW_INCOMPLETE, skipReason: "incomplete" };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function vortexSkippedSteps(stack, layer, skipReason) {
|
|
119
|
+
const head = layer.headSha ?? "its live head";
|
|
120
|
+
const mention = `comment @mergestorm-vortex review on #${layer.prNumber}`;
|
|
121
|
+
if (skipReason === "auto_review_off") {
|
|
122
|
+
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
|
+
}
|
|
124
|
+
if (skipReason === "incomplete") {
|
|
125
|
+
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
|
+
}
|
|
127
|
+
return `Vortex skipped #${layer.prNumber} at ${head} because it could not confirm billing for this account, so Auto land waits for a review that will not come. Do not patch or push for this. Re-run the review: ${mention}. If it is skipped again, tell the human to check the plan and billing in the Mergestorm dashboard.`;
|
|
128
|
+
}
|
|
85
129
|
function seamGateBlocker(stack, layer) {
|
|
86
130
|
const members = stack.unit?.members ?? [];
|
|
87
131
|
if (layer.prNumber === stack.unit?.landPrNumber)
|
|
@@ -132,7 +176,7 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
132
176
|
if (layer.restackError.kind === "rebase_conflict")
|
|
133
177
|
return hardBlock("Conflict");
|
|
134
178
|
if (!restackRetryPending(layer.restackError, layer))
|
|
135
|
-
return hardBlock(
|
|
179
|
+
return hardBlock(RESTACK_FAILED);
|
|
136
180
|
}
|
|
137
181
|
if (layer.state === "conflict")
|
|
138
182
|
return hardBlock("Conflict");
|
|
@@ -152,13 +196,16 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
152
196
|
sameHead(run.sha, layer.headSha)))
|
|
153
197
|
return hardBlock("Cyclone failed");
|
|
154
198
|
const tempest = layer.agentRuns?.find((run) => run.agent === "tempest" &&
|
|
155
|
-
|
|
199
|
+
run.status === "findings" && sameHead(run.sha, layer.headSha));
|
|
156
200
|
if (tempest)
|
|
157
201
|
return hardBlock(`Tempest ${tempest.status}`);
|
|
158
202
|
if (layer.vortexStatus === "failed")
|
|
159
203
|
return hardBlock("Review failed");
|
|
160
204
|
if (quotaSkippedReview(layer))
|
|
161
205
|
return hardBlock(VORTEX_REVIEW_OUT_OF_QUOTA);
|
|
206
|
+
const vortexSkip = vortexIdleSkipBlocker(layer);
|
|
207
|
+
if (vortexSkip)
|
|
208
|
+
return hardBlock(vortexSkip.blocker);
|
|
162
209
|
const seam = seamGateBlocker(stack, layer);
|
|
163
210
|
if (seam)
|
|
164
211
|
return hardBlock(seam);
|
|
@@ -263,6 +310,55 @@ export function autoLandWaitBlocker(stack, layer, nowMs) {
|
|
|
263
310
|
return null;
|
|
264
311
|
}
|
|
265
312
|
}
|
|
313
|
+
export const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
|
|
314
|
+
const MERGE_QUEUE_STALLED_PREFIX = "Merge queue stuck: ";
|
|
315
|
+
const LIVE_QUEUE_STATES = ["queued", "running", "waiting"];
|
|
316
|
+
function minutesOf(ms) {
|
|
317
|
+
return ms == null ? 0 : Math.floor(ms / 60_000);
|
|
318
|
+
}
|
|
319
|
+
export function mergeQueueWait(queue, nowMs) {
|
|
320
|
+
const entry = queue.find((candidate) => LIVE_QUEUE_STATES.includes(candidate.state));
|
|
321
|
+
if (!entry)
|
|
322
|
+
return null;
|
|
323
|
+
const reason = entry.waitReason?.trim() || null;
|
|
324
|
+
if (entry.state === "queued") {
|
|
325
|
+
const queuedFor = minutesOf(waitedMs(entry.enqueuedAt, nowMs));
|
|
326
|
+
const ahead = entry.aheadInRepo;
|
|
327
|
+
if (!ahead) {
|
|
328
|
+
return {
|
|
329
|
+
text: `Queued ${queuedFor}m in the merge queue, not picked up yet${reason ? ` (last note: ${reason})` : ""}`,
|
|
330
|
+
stalled: false,
|
|
331
|
+
since: entry.enqueuedAt, waitReason: reason, aheadStackId: null, aheadOther: false,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const aheadMs = ahead.claimedAt ? waitedMs(ahead.claimedAt, nowMs) : null;
|
|
335
|
+
const aheadReason = ahead.waitReason?.trim() || null;
|
|
336
|
+
const who = ahead.stackId ? `stack ${ahead.stackId}` : "another user's stack";
|
|
337
|
+
const aheadFor = aheadMs == null ? "an unknown duration" : `${minutesOf(aheadMs)}m`;
|
|
338
|
+
return {
|
|
339
|
+
text: `Queued ${queuedFor}m behind ${who}, which has been ${ahead.state} in the merge queue for ${aheadFor}${aheadReason ? `: ${aheadReason}` : ""}`,
|
|
340
|
+
stalled: aheadMs != null && aheadMs >= MERGE_QUEUE_STALL_ATTENTION_MS,
|
|
341
|
+
since: ahead.claimedAt, waitReason: aheadReason, aheadStackId: ahead.stackId, aheadOther: !ahead.stackId,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const since = entry.claimedAt ?? null;
|
|
345
|
+
const liveMs = since === null ? null : waitedMs(since, nowMs);
|
|
346
|
+
const liveFor = liveMs == null ? "an unknown duration" : `${minutesOf(liveMs)}m`;
|
|
347
|
+
return {
|
|
348
|
+
text: `Merge queue ${entry.state === "waiting" ? "waiting" : "running"} for ${liveFor}: ${reason ?? "no reason recorded"}`,
|
|
349
|
+
stalled: liveMs != null && liveMs >= MERGE_QUEUE_STALL_ATTENTION_MS,
|
|
350
|
+
since, waitReason: reason, aheadStackId: null, aheadOther: false,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function queueStalledSteps(stack, layer, wait) {
|
|
354
|
+
if (wait.aheadStackId || wait.aheadOther) {
|
|
355
|
+
const ahead = wait.aheadStackId
|
|
356
|
+
? `Run mg stack status ${wait.aheadStackId} and fix what it names there; this stack moves once that entry lands or bounces.`
|
|
357
|
+
: "That entry belongs to another Mergestorm user, so you cannot fix it from here.";
|
|
358
|
+
return `#${layer.prNumber} is waiting in the merge queue behind another entry in ${stack.owner}/${stack.repo} that has made no progress since ${wait.since ?? "it was claimed"}. Nothing on ${layer.branch} needs a change. ${ahead} If it stays stuck, tell the human: the queue for this repo is held by that entry.`;
|
|
359
|
+
}
|
|
360
|
+
return `The merge queue has held #${layer.prNumber} at ${layer.headSha ?? "its head"} since ${wait.since ?? "it was claimed"} without landing or bouncing it${wait.waitReason ? `; its last note is: ${wait.waitReason}` : ", and it recorded no reason"}. If the note names a failing check or a conflict on ${layer.branch}, fix that with an ordinary push. If it waits on CI, list the head's checks (gh run list --branch ${layer.branch}) and re-run the one that never finished (gh run rerun <run-id>). If nothing moves after that, tell the human: they can take the stack out of the queue and enqueue it again (mg queue rm ${stack.id}, then mg queue add ${stack.id}). Do not retarget the PR base.`;
|
|
361
|
+
}
|
|
266
362
|
function isTerminalLayer(layer) {
|
|
267
363
|
return layer.state === "merged" || layer.state === "closed";
|
|
268
364
|
}
|
|
@@ -299,24 +395,48 @@ export function conflictRepairSteps(branch, liveParent) {
|
|
|
299
395
|
}
|
|
300
396
|
return `Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve the conflicts, run the tests, then an ordinary git push (no force). Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
|
|
301
397
|
}
|
|
398
|
+
export function restackFailedRepairSteps(branch, liveParent, failure) {
|
|
399
|
+
const why = `Mergestorm could not restack ${branch}${liveParent ? ` onto ${liveParent}` : ""} (${failure.kind}${failure.detail?.trim() ? `: ${failure.detail.trim()}` : ""}) and will not try this head again.`;
|
|
400
|
+
if (!liveParent) {
|
|
401
|
+
return `${why} The live parent of ${branch} is unknown (it is never an mg-park-* freeze). Do not merge or rebase anything; stop and tell the human.`;
|
|
402
|
+
}
|
|
403
|
+
return `${why} Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve any conflicts, run the tests, then an ordinary git push (no force). Mergestorm clears Restack failed once the pushed head contains the tip of ${liveParent}. If GitHub refuses the push (branch protection or permissions), stop and tell the human. Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
|
|
404
|
+
}
|
|
302
405
|
function landPrLiveParent(stack, layer) {
|
|
303
406
|
const base = stack.unit?.landTarget?.trim() || layer.parentBranch?.trim();
|
|
304
407
|
return base && !isMgParkBase(base) && base !== layer.branch ? base : null;
|
|
305
408
|
}
|
|
306
409
|
export function agentsCannotClear(blocker) {
|
|
307
|
-
return blocker === "Conflict" || blocker ===
|
|
410
|
+
return blocker === "Conflict" || blocker === RESTACK_FAILED || blocker === "Draft PR" ||
|
|
308
411
|
blocker === SEAM_REVIEW_NOT_RUNNING || blocker === VORTEX_REVIEW_OUT_OF_QUOTA ||
|
|
412
|
+
blocker === VORTEX_AUTO_REVIEW_OFF || blocker === VORTEX_BILLING_SKIPPED ||
|
|
413
|
+
blocker === VORTEX_REVIEW_INCOMPLETE ||
|
|
309
414
|
STUCK_SEAM_BLOCKERS.includes(blocker) ||
|
|
310
415
|
blocker.startsWith("Merge conflicts") || blocker.startsWith(UNIT_ABANDONED_BLOCKER_PREFIX);
|
|
311
416
|
}
|
|
312
|
-
|
|
417
|
+
const TEMPEST_RERUN_BLOCKER = /^(?:landing blocked:\s*)?tempest_(failed|stopped)\b/i;
|
|
418
|
+
function isTempestFindingsBlocker(attention, bounce) {
|
|
313
419
|
const blocker = attention.blocker.trim();
|
|
420
|
+
const reason = bounce?.bounceDetail?.message?.trim();
|
|
421
|
+
if (reason && TEMPEST_RERUN_BLOCKER.test(reason))
|
|
422
|
+
return false;
|
|
314
423
|
return attention.bounceKind === "tempest_findings" || /^Tempest findings\b/i.test(blocker) ||
|
|
315
424
|
/^(?:landing blocked:\s*)?tempest_findings\b/i.test(blocker);
|
|
316
425
|
}
|
|
317
|
-
export function stackRepair(stack, attention, layer, bounce) {
|
|
426
|
+
export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
318
427
|
if (!attention || !layer || attention.prNumber !== layer.prNumber)
|
|
319
428
|
return null;
|
|
429
|
+
if (queueWait && attention.blocker.startsWith(MERGE_QUEUE_STALLED_PREFIX)) {
|
|
430
|
+
return {
|
|
431
|
+
kind: "queue_stalled",
|
|
432
|
+
prNumber: layer.prNumber,
|
|
433
|
+
headSha: layer.headSha ?? null,
|
|
434
|
+
branch: layer.branch,
|
|
435
|
+
since: queueWait.since,
|
|
436
|
+
waitReason: queueWait.waitReason,
|
|
437
|
+
steps: queueStalledSteps(stack, layer, queueWait),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
320
440
|
const landPr = !!stack.unit?.branch && layer.prNumber === stack.unit.landPrNumber && layer.branch === stack.unit.branch;
|
|
321
441
|
const restackConflict = attention.blocker === "Conflict" || attention.bounceKind === "restack_conflict";
|
|
322
442
|
const mergeConflict = attention.blocker.startsWith("Merge conflicts");
|
|
@@ -332,6 +452,18 @@ export function stackRepair(stack, attention, layer, bounce) {
|
|
|
332
452
|
steps: conflictRepairSteps(layer.branch, liveParent),
|
|
333
453
|
};
|
|
334
454
|
}
|
|
455
|
+
if (attention.blocker === RESTACK_FAILED && layer.restackError) {
|
|
456
|
+
const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
|
|
457
|
+
return {
|
|
458
|
+
kind: "restack_failed",
|
|
459
|
+
prNumber: layer.prNumber,
|
|
460
|
+
headSha: layer.headSha ?? null,
|
|
461
|
+
branch: layer.branch,
|
|
462
|
+
liveParent,
|
|
463
|
+
restackKind: layer.restackError.kind,
|
|
464
|
+
steps: restackFailedRepairSteps(layer.branch, liveParent, layer.restackError),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
335
467
|
if (attention.blocker.startsWith(UNIT_ABANDONED_BLOCKER_PREFIX)) {
|
|
336
468
|
const openPrCount = stack.layers.filter((candidate) => candidate.prNumber > 0 && !isTerminalLayer(candidate)).length;
|
|
337
469
|
return {
|
|
@@ -368,7 +500,7 @@ export function stackRepair(stack, attention, layer, bounce) {
|
|
|
368
500
|
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.`,
|
|
369
501
|
};
|
|
370
502
|
}
|
|
371
|
-
if (landPr && isTempestFindingsBlocker(attention)) {
|
|
503
|
+
if (landPr && isTempestFindingsBlocker(attention, bounce)) {
|
|
372
504
|
const findings = `gh api repos/${stack.owner}/${stack.repo}/issues/${layer.prNumber}/comments`;
|
|
373
505
|
return {
|
|
374
506
|
kind: "tempest_findings",
|
|
@@ -379,6 +511,35 @@ export function stackRepair(stack, attention, layer, bounce) {
|
|
|
379
511
|
steps: `Tempest never patches, and Cyclone is not patching this land PR. Read the Tempest report on it (${findings}), verify each finding against ${layer.branch} at its live remote head, fix the real ones with the smallest patch on ${layer.branch}, run the tests, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push (no force). Do not retarget the PR base.`,
|
|
380
512
|
};
|
|
381
513
|
}
|
|
514
|
+
const rerunBlocker = attention.blocker.trim().match(TEMPEST_RERUN_BLOCKER) ??
|
|
515
|
+
bounce?.bounceDetail?.message?.trim().match(TEMPEST_RERUN_BLOCKER);
|
|
516
|
+
const rerun = landPr ? rerunBlocker : null;
|
|
517
|
+
if (rerun) {
|
|
518
|
+
const stoppedByPerson = rerun[1].toLowerCase() === "stopped";
|
|
519
|
+
const comment = `gh pr comment ${layer.prNumber} --repo ${stack.owner}/${stack.repo} --body`;
|
|
520
|
+
const head = layer.headSha ?? "its live head";
|
|
521
|
+
return {
|
|
522
|
+
kind: "tempest_rerun",
|
|
523
|
+
prNumber: layer.prNumber,
|
|
524
|
+
headSha: layer.headSha ?? null,
|
|
525
|
+
branch: layer.branch,
|
|
526
|
+
stoppedByPerson,
|
|
527
|
+
steps: stoppedByPerson
|
|
528
|
+
? `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.`
|
|
529
|
+
: `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.`,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const vortexSkip = vortexIdleSkipBlocker(layer);
|
|
533
|
+
if (vortexSkip && attention.blocker === vortexSkip.blocker) {
|
|
534
|
+
return {
|
|
535
|
+
kind: "vortex_skipped",
|
|
536
|
+
prNumber: layer.prNumber,
|
|
537
|
+
headSha: layer.headSha ?? null,
|
|
538
|
+
branch: layer.branch,
|
|
539
|
+
skipReason: vortexSkip.skipReason,
|
|
540
|
+
steps: vortexSkippedSteps(stack, layer, vortexSkip.skipReason),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
382
543
|
const wait = autoLandWaitOn(stack, layer);
|
|
383
544
|
if (wait && attention.blocker.startsWith(AUTO_LAND_CI_PENDING_PREFIX)) {
|
|
384
545
|
return {
|
|
@@ -422,7 +583,7 @@ export function stackRepair(stack, attention, layer, bounce) {
|
|
|
422
583
|
branch: layer.branch,
|
|
423
584
|
handoff,
|
|
424
585
|
findings,
|
|
425
|
-
steps: `Vortex requested changes on ${layer.branch} at ${layer.headSha ?? "its live head"} and Cyclone left them for a person${handoff ? ` (${handoff
|
|
586
|
+
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.`,
|
|
426
587
|
};
|
|
427
588
|
}
|
|
428
589
|
if (attention.blocker.startsWith("CI failed") || attention.bounceKind === "ci_failure") {
|
|
@@ -454,6 +615,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
454
615
|
let attentionLayer;
|
|
455
616
|
let bounce;
|
|
456
617
|
const issues = [];
|
|
618
|
+
const queueWait = stack.archivedAt ? null : mergeQueueWait(queue, nowMs);
|
|
457
619
|
for (const layer of open) {
|
|
458
620
|
const selected = layer.prNumber === candidate?.prNumber;
|
|
459
621
|
let result = layerAttention(stack, layer, selected ? queue : [], cursor);
|
|
@@ -473,6 +635,9 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
473
635
|
waitNamed = true;
|
|
474
636
|
}
|
|
475
637
|
}
|
|
638
|
+
if (!result.blocker && selected && queueWait?.stalled) {
|
|
639
|
+
result = { blocker: `${MERGE_QUEUE_STALLED_PREFIX}${queueWait.text}`, bounceKind: null };
|
|
640
|
+
}
|
|
476
641
|
if (!result.blocker) {
|
|
477
642
|
if (mergeabilityPending(layer) && (selected || !candidate || layer.position > candidate.position)) {
|
|
478
643
|
issues.push({ prNumber: layer.prNumber, headSha: layer.headSha ?? null,
|
|
@@ -495,6 +660,10 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
495
660
|
else if (!candidate || layer.position > candidate.position)
|
|
496
661
|
issues.push(issue);
|
|
497
662
|
}
|
|
663
|
+
const queued = queueWait && candidate && !queueWait.stalled
|
|
664
|
+
? { prNumber: candidate.prNumber, headSha: candidate.headSha ?? null, text: queueWait.text,
|
|
665
|
+
since: queueWait.since, waitReason: queueWait.waitReason }
|
|
666
|
+
: null;
|
|
498
667
|
const settleIssue = mergeabilitySettleIssue(stack, open, issues, attention);
|
|
499
668
|
if (settleIssue)
|
|
500
669
|
issues.push(settleIssue);
|
|
@@ -517,11 +686,14 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
517
686
|
if (busy.length && attention && agentsCannotClear(attention.blocker)) {
|
|
518
687
|
const held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
|
|
519
688
|
return { attention: null, held, issues: [attention, ...issues], currentCandidate, bounce: undefined, busy, agents,
|
|
520
|
-
repair: stackRepair(stack, attention, attentionLayer, undefined), landGatePending };
|
|
689
|
+
repair: stackRepair(stack, attention, attentionLayer, undefined, queueWait), landGatePending, queueWait: queued };
|
|
690
|
+
}
|
|
691
|
+
if (busy.length) {
|
|
692
|
+
return { attention: null, held: null, issues, currentCandidate, bounce: undefined, busy, agents, repair: null, landGatePending,
|
|
693
|
+
queueWait: queued };
|
|
521
694
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return { attention, held: null, issues, currentCandidate, bounce, busy, agents, repair: stackRepair(stack, attention, attentionLayer, bounce), landGatePending };
|
|
695
|
+
return { attention, held: null, issues, currentCandidate, bounce, busy, agents,
|
|
696
|
+
repair: stackRepair(stack, attention, attentionLayer, bounce, queueWait), landGatePending, queueWait: queued };
|
|
525
697
|
}
|
|
526
698
|
function mergeabilitySettleIssue(stack, open, issues, attention) {
|
|
527
699
|
const settle = stack.autoEnqueueSettle;
|
|
@@ -539,7 +711,7 @@ function mergeabilitySettleIssue(stack, open, issues, attention) {
|
|
|
539
711
|
blocker: `Merge state unknown${vs}; Auto land is waiting for a mergeability verdict`, bounceKind: null };
|
|
540
712
|
}
|
|
541
713
|
/** Compact contract text shared by status and wait summaries. */
|
|
542
|
-
export function stackBlockersSummary(attention, issues, busy = [], landGatePending = null, actAfter = null) {
|
|
714
|
+
export function stackBlockersSummary(attention, issues, busy = [], landGatePending = null, actAfter = null, queueWait = null) {
|
|
543
715
|
const agentWork = (entry) => entry.agent === "cyclone" ? "Cyclone patches" : "Vortex reviews";
|
|
544
716
|
const blocked = attention && actAfter && busy.length
|
|
545
717
|
? ` · blocked after the agents: #${attention.prNumber} ${attention.blocker} (plan the fix; act once ${busy.map(agentWork).join(" and ")} finish and the watch returns attention)`
|
|
@@ -552,5 +724,6 @@ export function stackBlockersSummary(attention, issues, busy = [], landGatePendi
|
|
|
552
724
|
const gate = landGatePending && !attention
|
|
553
725
|
? ` · land gate: #${landGatePending.prNumber} ${landGatePending.reason} (nothing to fix; wait)`
|
|
554
726
|
: "";
|
|
555
|
-
|
|
727
|
+
const queue = queueWait && !attention ? ` · queue: #${queueWait.prNumber} ${queueWait.text}` : "";
|
|
728
|
+
return blocked + working + gate + queue + (listed.length ? ` · issues: ${labels}${listed.length > 3 ? `; +${listed.length - 3} more` : ""}` : "");
|
|
556
729
|
}
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -307,6 +307,7 @@ export type StackUnitDto = {
|
|
|
307
307
|
landPr: StackLayerDto | null;
|
|
308
308
|
};
|
|
309
309
|
export type CycloneOwnerMatch = "same" | "different" | "none" | "lookup_failed";
|
|
310
|
+
export type CyclonePatchOffReason = "stack_auto_patch_off" | "account_auto_patch_off" | "cyclone_not_connected";
|
|
310
311
|
export type StackDto = {
|
|
311
312
|
id: string;
|
|
312
313
|
owner: string;
|
|
@@ -383,6 +384,7 @@ export type StackDto = {
|
|
|
383
384
|
* `lookup_failed`: the comparison could not be loaded. Fail closed.
|
|
384
385
|
*/
|
|
385
386
|
cycloneOwnerMatch?: CycloneOwnerMatch;
|
|
387
|
+
cyclonePatchOff?: CyclonePatchOffReason;
|
|
386
388
|
layers: StackLayerDto[];
|
|
387
389
|
/** Present when a review-unit row exists for this stack. */
|
|
388
390
|
unit?: StackUnitDto;
|
|
@@ -422,6 +424,12 @@ export type MergeQueueEntryBatchDto = {
|
|
|
422
424
|
state: MergeQueueBatchState;
|
|
423
425
|
landedCount: number;
|
|
424
426
|
};
|
|
427
|
+
export type MergeQueueAheadDto = {
|
|
428
|
+
stackId: string | null;
|
|
429
|
+
state: "running" | "waiting";
|
|
430
|
+
claimedAt: string | null;
|
|
431
|
+
waitReason: string | null;
|
|
432
|
+
};
|
|
425
433
|
export type MergeQueueEntryDto = {
|
|
426
434
|
id: string;
|
|
427
435
|
stackId: string;
|
|
@@ -445,4 +453,6 @@ export type MergeQueueEntryDto = {
|
|
|
445
453
|
cancelRequestedAt?: string | null;
|
|
446
454
|
batch?: MergeQueueEntryBatchDto | null;
|
|
447
455
|
finishedAt: string | null;
|
|
456
|
+
claimedAt?: string | null;
|
|
457
|
+
aheadInRepo?: MergeQueueAheadDto | null;
|
|
448
458
|
};
|
package/dist/stack-watch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLayerAgents, type StackLandGatePending, type StackRepairHint } from "./stack-blockers.js";
|
|
1
|
+
import { type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLayerAgents, type StackLandGatePending, type StackQueueWait, type StackRepairHint } from "./stack-blockers.js";
|
|
2
2
|
import { type StackWatchObligation } from "./stack-watch-obligation.js";
|
|
3
3
|
import { apiFetch } from "./api.js";
|
|
4
4
|
import type { Config } from "./config.js";
|
|
@@ -33,6 +33,7 @@ export type StackWatchEnvelope = {
|
|
|
33
33
|
agents: StackLayerAgents | null;
|
|
34
34
|
repair: StackRepairHint | null;
|
|
35
35
|
landGatePending: StackLandGatePending | null;
|
|
36
|
+
queueWait?: StackQueueWait | null;
|
|
36
37
|
watch: StackWatchObligation;
|
|
37
38
|
};
|
|
38
39
|
export type PollStackWatchOptions = {
|
package/dist/stack-watch.js
CHANGED
|
@@ -120,7 +120,7 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
120
120
|
if (snapshot && Array.isArray(snapshot.stacks) && !stack) {
|
|
121
121
|
terminal = "not_found";
|
|
122
122
|
throw new StackWatchError("Stack not found or not owned by the current user", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable", blocker: null, issues: [], busy: [], actAfter: null, waitingOn: [],
|
|
123
|
-
prNumber: null, headSha: null, repair: null, agents: null, currentCandidate: null, landGatePending: null }));
|
|
123
|
+
prNumber: null, headSha: null, repair: null, agents: null, currentCandidate: null, landGatePending: null, queueWait: null }));
|
|
124
124
|
}
|
|
125
125
|
if (!stack || !Array.isArray(stack.layers)) {
|
|
126
126
|
throw new StackWatchError("Stack snapshot missing or invalid", seal({ ...lastEnvelope, status: "failed", assessment: "unavailable" }));
|
|
@@ -140,13 +140,13 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
140
140
|
};
|
|
141
141
|
};
|
|
142
142
|
const evaluate = (stack, entries) => {
|
|
143
|
-
const { attention, held: named, issues, currentCandidate, bounce, busy, agents, repair, landGatePending } = stackBlockers(stack, entries, cursor, now());
|
|
143
|
+
const { attention, held: named, issues, currentCandidate, bounce, busy, agents, repair, landGatePending, queueWait } = stackBlockers(stack, entries, cursor, now());
|
|
144
144
|
if (bounce)
|
|
145
145
|
cursor = Object.freeze({ ...cursor, bounceId: bounce.id,
|
|
146
146
|
...(bounce.finishedAt !== undefined ? { afterFinishedAt: bounce.finishedAt } : {}) });
|
|
147
147
|
const held = busy[0];
|
|
148
148
|
const shown = attention ?? named;
|
|
149
|
-
lastEnvelope = { ...lastEnvelope, cursor, issues, currentCandidate, assessment: "available", busy, agents, repair, landGatePending,
|
|
149
|
+
lastEnvelope = { ...lastEnvelope, cursor, issues, currentCandidate, assessment: "available", busy, agents, repair, landGatePending, queueWait,
|
|
150
150
|
actAfter: named?.actAfter ?? null, waitingOn: named?.waitingOn ?? [],
|
|
151
151
|
blocker: shown?.blocker ?? null, bounceKind: shown?.bounceKind ?? null,
|
|
152
152
|
prNumber: shown?.prNumber ?? held?.prNumber ?? currentCandidate?.prNumber ?? null,
|
package/dist/ui/banner.js
CHANGED
|
@@ -141,6 +141,14 @@ const WHATS_NEW = {
|
|
|
141
141
|
"stack watch names a stuck seam review",
|
|
142
142
|
"stack watch names findings Cyclone left for you",
|
|
143
143
|
],
|
|
144
|
+
"0.3.36": [
|
|
145
|
+
"stack watch names what the merge queue waits on",
|
|
146
|
+
"stack watch names why Vortex skipped a head",
|
|
147
|
+
"stack watch says how to re-run Tempest",
|
|
148
|
+
],
|
|
149
|
+
"0.3.37": [
|
|
150
|
+
"turning auto-review on reviews skipped heads",
|
|
151
|
+
],
|
|
144
152
|
};
|
|
145
153
|
function densityFromVariant(variant) {
|
|
146
154
|
if (variant === "mini" || variant === "nano")
|
package/package.json
CHANGED