mergestorm 0.3.57 → 0.3.59
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/commands/stack.js +2 -2
- package/dist/stack-blockers.d.ts +10 -2
- package/dist/stack-blockers.js +51 -6
- package/dist/stack-dto.d.ts +5 -3
- package/dist/stack-dto.js +2 -0
- package/dist/stack-watch.d.ts +2 -0
- package/dist/stack-watch.js +6 -2
- package/dist/ui/banner.js +6 -0
- package/dist/ui/logo.d.ts +0 -7
- package/dist/ui/logo.js +0 -7
- package/dist/ui/prompt.d.ts +0 -17
- package/dist/ui/prompt.js +0 -21
- package/package.json +1 -1
- package/skill/mergestorm-bounce-watch/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -45,12 +45,12 @@ 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.59 │
|
|
49
49
|
│ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
|
|
50
50
|
│ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
|
|
51
51
|
│ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
|
|
52
52
|
│ ▝▀▀▘ review a diff · usage for tabs · /help for all commands │
|
|
53
|
-
│ ▀ New in v0.3.
|
|
53
|
+
│ ▀ New in v0.3.59: One-shot stack wait rides out a slow read │
|
|
54
54
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
55
55
|
mergestorm
|
|
56
56
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
|
|
1
|
+
import { cycloneStatusWords, 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
3
|
import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackPublication, landNextStack, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
@@ -1283,7 +1283,7 @@ export async function cmdStackStatus(args, deps = {}) {
|
|
|
1283
1283
|
lines.push(` ${String(layer.position).padStart(2)} ${pr}${head} ${layer.state.padEnd(14)} ${layer.branch}` +
|
|
1284
1284
|
` CI: ${layer.ciStatus ?? "unknown"} review: ${layer.reviewStatus ?? "unknown"}` +
|
|
1285
1285
|
(layer.vortexStatus ? ` Vortex: ${layer.vortexStatus}` : "") +
|
|
1286
|
-
(layer.cycloneStatus ? ` Cyclone: ${layer
|
|
1286
|
+
(layer.cycloneStatus ? ` Cyclone: ${cycloneStatusWords(layer)}` : ""));
|
|
1287
1287
|
}
|
|
1288
1288
|
if (queueWait && !attention)
|
|
1289
1289
|
lines.push(` Queue: #${queueWait.prNumber} ${queueWait.text}`);
|
package/dist/stack-blockers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MergeQueueEntryDto, type StackAgentsBusy, type StackDto, type StackLayerDto } from "./stack-dto.js";
|
|
1
|
+
import { type AutoLandOffReason, type MergeQueueEntryDto, type StackAgentsBusy, type StackDto, type StackLayerDto } from "./stack-dto.js";
|
|
2
2
|
import type { StackWatchCursor } from "./stack-watch.js";
|
|
3
3
|
export type StackBlocker = {
|
|
4
4
|
prNumber: number;
|
|
@@ -6,7 +6,7 @@ export type StackBlocker = {
|
|
|
6
6
|
blocker: string;
|
|
7
7
|
bounceKind: string | null;
|
|
8
8
|
};
|
|
9
|
-
export type StackBlockerReason = "restack_conflict" | "restack_failed" | "draft_pr" | "merge_conflict" | "ci_failed" | "vortex_failed" | "vortex_out_of_quota" | "vortex_skipped" | "vortex_findings_need_person" | "seam_failed" | "seam_findings" | "seam_not_running" | "seam_review_stuck" | "seam_rereview_not_running" | "seam_verdict_stale" | "unit_abandoned" | "land_gate" | "bounce" | "auto_land_off" | "auto_land_ci_pending" | "auto_land_members_remaining" | "auto_land_vortex_wait" | "auto_land_promote_failed" | "auto_land_ci_unreadable" | "auto_land_waited" | "queue_stalled" | "land_pr_missing";
|
|
9
|
+
export type StackBlockerReason = "restack_conflict" | "restack_failed" | "draft_pr" | "merge_conflict" | "ci_failed" | "vortex_failed" | "vortex_out_of_quota" | "vortex_skipped" | "vortex_findings_need_person" | "seam_failed" | "seam_findings" | "seam_not_running" | "seam_review_stuck" | "seam_rereview_not_running" | "seam_verdict_stale" | "unit_abandoned" | "land_gate" | "bounce" | "auto_land_off" | "auto_land_switched_off" | "auto_land_ci_pending" | "auto_land_members_remaining" | "auto_land_vortex_wait" | "auto_land_promote_failed" | "auto_land_ci_unreadable" | "auto_land_waited" | "queue_stalled" | "land_pr_missing";
|
|
10
10
|
export type ClassifiedStackBlocker = StackBlocker & {
|
|
11
11
|
reason: StackBlockerReason;
|
|
12
12
|
};
|
|
@@ -89,6 +89,13 @@ export type StackRepairHint = {
|
|
|
89
89
|
branch: string;
|
|
90
90
|
bounceKind: string;
|
|
91
91
|
steps: string;
|
|
92
|
+
} | {
|
|
93
|
+
kind: "auto_land_switched_off";
|
|
94
|
+
prNumber: number;
|
|
95
|
+
headSha: string | null;
|
|
96
|
+
branch: string;
|
|
97
|
+
offReason: AutoLandOffReason;
|
|
98
|
+
steps: string;
|
|
92
99
|
} | {
|
|
93
100
|
kind: "ci_pending" | "vortex_not_green";
|
|
94
101
|
prNumber: number;
|
|
@@ -163,6 +170,7 @@ export declare const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no rev
|
|
|
163
170
|
export declare const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
|
|
164
171
|
export declare const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
|
|
165
172
|
export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a person";
|
|
173
|
+
export declare function cycloneStatusWords(layer: Pick<StackLayerDto, "cycloneStatus" | "cycloneHandoff" | "headSha">): string | null;
|
|
166
174
|
export declare function autoResolveActive(stack: StackDto, layer: StackLayerDto): boolean;
|
|
167
175
|
export declare function autoFixCiActive(stack: StackDto, layer: StackLayerDto): boolean;
|
|
168
176
|
export declare const CI_DID_NOT_FINISH_BLOCKER = "CI did not finish \u2014 left for a person";
|
package/dist/stack-blockers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deriveStackAgentsBusy, mergeQueueBounceLabel, restackRetryPending, } from "./stack-dto.js";
|
|
1
|
+
import { CYCLONE_CI_FIX_HELD_REASON, CYCLONE_FIX_HELD_REASON, deriveStackAgentsBusy, mergeQueueBounceLabel, restackRetryPending, } from "./stack-dto.js";
|
|
2
2
|
export function landGateIsPending(reason) {
|
|
3
3
|
const token = reason?.trim().replace(/^landing blocked:\s*/i, "").match(/^[a-z_]+/i)?.[0] ?? "";
|
|
4
4
|
return /_(?:pending|running)$/i.test(token);
|
|
@@ -79,6 +79,7 @@ function cycloneHandoffAtHead(stack, layer) {
|
|
|
79
79
|
}
|
|
80
80
|
const CYCLONE_HANDOFF_WORDS = {
|
|
81
81
|
failed: "Cyclone's patch failed, so it pushed nothing",
|
|
82
|
+
fix_held: "Cyclone wrote a fix and held it on purpose, so it pushed nothing",
|
|
82
83
|
auto_patch_off: "auto-patch is off for this stack",
|
|
83
84
|
stack_auto_patch_off: "auto-patch is off for this stack",
|
|
84
85
|
repo_auto_patch_off: "auto-patch is off for this repository and this stack does not turn it on",
|
|
@@ -107,14 +108,23 @@ const CYCLONE_HANDOFF_WORDS = {
|
|
|
107
108
|
ci_unverified: "Cyclone's static checks did not cover its fix, so it pushed nothing",
|
|
108
109
|
ci_unreadable: "Cyclone could not read the failing checks",
|
|
109
110
|
ci_needs_actions_read: "Cyclone's GitHub App cannot read the Actions logs on this repo",
|
|
111
|
+
ci_fix_held: "Cyclone wrote a CI fix and held it on purpose, so it pushed nothing",
|
|
110
112
|
ci_unresolved: "Cyclone's CI fix failed",
|
|
111
113
|
stack_auto_fix_ci_off: "auto-fix CI is off for this stack",
|
|
112
114
|
account_auto_fix_ci_off: "auto-fix CI is off for the account and this stack does not turn it on",
|
|
113
115
|
};
|
|
114
116
|
function cycloneHandoffWords(reason, detail) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
117
|
+
const words = CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
|
|
118
|
+
const held = reason === CYCLONE_FIX_HELD_REASON || reason === CYCLONE_CI_FIX_HELD_REASON;
|
|
119
|
+
return held && detail?.trim() ? `${words}: ${detail.trim()}` : words;
|
|
120
|
+
}
|
|
121
|
+
export function cycloneStatusWords(layer) {
|
|
122
|
+
if (!layer.cycloneStatus)
|
|
123
|
+
return null;
|
|
124
|
+
if (layer.cycloneStatus !== "held")
|
|
125
|
+
return layer.cycloneStatus;
|
|
126
|
+
const detail = cycloneHandoffDetailAtHead(layer)?.trim();
|
|
127
|
+
return detail ? `held its fix, not pushed (${detail})` : "held its fix, not pushed";
|
|
118
128
|
}
|
|
119
129
|
function cycloneHandoffDetailAtHead(layer) {
|
|
120
130
|
return sameHead(layer.cycloneHandoff?.headSha, layer.headSha) ? layer.cycloneHandoff.detail ?? null : null;
|
|
@@ -153,7 +163,7 @@ function ciHandoffNote(stack, layer) {
|
|
|
153
163
|
return null;
|
|
154
164
|
const handoff = layer.cycloneCiHandoff;
|
|
155
165
|
if (handoff && sameHead(handoff.headSha, layer.headSha)) {
|
|
156
|
-
return `Cyclone tried to fix this failing CI at ${layer.headSha ?? handoff.headSha} and left it for a person (${cycloneHandoffWords(handoff.reason)}), so it will not try this head again.`;
|
|
166
|
+
return `Cyclone tried to fix this failing CI at ${layer.headSha ?? handoff.headSha} and left it for a person (${cycloneHandoffWords(handoff.reason, handoff.detail)}), so it will not try this head again.`;
|
|
157
167
|
}
|
|
158
168
|
if (stack.cycloneCiOff) {
|
|
159
169
|
return `Cyclone will not fix this failing CI by itself: ${cycloneHandoffWords(stack.cycloneCiOff)}.`;
|
|
@@ -162,6 +172,7 @@ function ciHandoffNote(stack, layer) {
|
|
|
162
172
|
}
|
|
163
173
|
const CYCLONE_HELD_FINDINGS_REASONS = [
|
|
164
174
|
"failed",
|
|
175
|
+
CYCLONE_FIX_HELD_REASON,
|
|
165
176
|
"left_for_human",
|
|
166
177
|
"infra_failure",
|
|
167
178
|
"pr_state_unavailable",
|
|
@@ -359,6 +370,8 @@ function layerAttention(stack, layer, queue, cursor) {
|
|
|
359
370
|
return none;
|
|
360
371
|
if (kind === "ci_failure" && !bounce.bounceDetail?.batch && (headChecksAllGreen(layer) || headChecksRerunning(layer)))
|
|
361
372
|
return none;
|
|
373
|
+
if (kind === "ci_timeout" && headChecksAllGreen(layer))
|
|
374
|
+
return none;
|
|
362
375
|
const label = mergeQueueBounceLabel(bounce);
|
|
363
376
|
return { blocker: label, reason: "bounce", bounceKind: kind, bounce };
|
|
364
377
|
}
|
|
@@ -391,6 +404,17 @@ function disarmingBounce(stack, queue) {
|
|
|
391
404
|
return recorded;
|
|
392
405
|
return undefined;
|
|
393
406
|
}
|
|
407
|
+
const AUTO_LAND_SWITCHED_OFF_PREFIX = "Auto land switched off: ";
|
|
408
|
+
const AUTO_LAND_SWITCHED_OFF_BY = {
|
|
409
|
+
cancelled: "its merge queue entry was cancelled",
|
|
410
|
+
watcher: "Mergestorm found nothing it could land",
|
|
411
|
+
};
|
|
412
|
+
function switchedOffByAnotherActor(stack) {
|
|
413
|
+
if (stack.archivedAt || stack.autoEnqueueWhenReady !== false)
|
|
414
|
+
return undefined;
|
|
415
|
+
const reason = stack.autoLandOff?.reason;
|
|
416
|
+
return reason && AUTO_LAND_SWITCHED_OFF_BY[reason] ? reason : undefined;
|
|
417
|
+
}
|
|
394
418
|
export const AUTO_LAND_CI_PENDING_ATTENTION_MS = 45 * 60_000;
|
|
395
419
|
export const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS = 30 * 60_000;
|
|
396
420
|
export const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
|
|
@@ -725,13 +749,27 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
|
|
|
725
749
|
};
|
|
726
750
|
}
|
|
727
751
|
if (reason === "auto_land_off" && attention.bounceKind) {
|
|
752
|
+
const nothingToFix = attention.bounceKind === "ci_timeout" && headChecksAllGreen(layer);
|
|
728
753
|
return {
|
|
729
754
|
kind: "auto_land_off",
|
|
730
755
|
prNumber: layer.prNumber,
|
|
731
756
|
headSha: layer.headSha ?? null,
|
|
732
757
|
branch: layer.branch,
|
|
733
758
|
bounceKind: attention.bounceKind,
|
|
734
|
-
steps:
|
|
759
|
+
steps: nothingToFix
|
|
760
|
+
? `The ci_timeout bounce turned Auto land off, so nothing will enqueue this stack. Every check on ${layer.branch} is green at this head now, so nothing needs a change: do not patch or push for it. Turn Auto land back on (mg stack set ${stack.id} --auto-land on) or enqueue the stack.`
|
|
761
|
+
: `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.`,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const offReason = stack.autoLandOff?.reason;
|
|
765
|
+
if (reason === "auto_land_switched_off" && offReason) {
|
|
766
|
+
return {
|
|
767
|
+
kind: "auto_land_switched_off",
|
|
768
|
+
prNumber: layer.prNumber,
|
|
769
|
+
headSha: layer.headSha ?? null,
|
|
770
|
+
branch: layer.branch,
|
|
771
|
+
offReason,
|
|
772
|
+
steps: `Auto land is off for this stack (autoLandOff reason ${offReason}: ${AUTO_LAND_SWITCHED_OFF_BY[offReason] ?? offReason}), so nothing will enqueue it and it will not land by itself. No bounce did this and nothing in the layers needs a change: do not patch or push for it. A person or another agent session may have switched it off on purpose, so do not turn Auto land back on yourself. Tell the human that Auto land is off and why, and turn it back on (mg stack set ${stack.id} --auto-land on) only when they say so.`,
|
|
735
773
|
};
|
|
736
774
|
}
|
|
737
775
|
if (reason === "vortex_failed") {
|
|
@@ -920,6 +958,13 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
|
|
|
920
958
|
result = { blocker: `${AUTO_LAND_OFF_BLOCKER_PREFIX}${kind} bounce`, reason: "auto_land_off", bounceKind: kind, bounce: disarmed };
|
|
921
959
|
}
|
|
922
960
|
}
|
|
961
|
+
if (!result.blocker && selected) {
|
|
962
|
+
const offReason = switchedOffByAnotherActor(stack);
|
|
963
|
+
if (offReason) {
|
|
964
|
+
result = { blocker: `${AUTO_LAND_SWITCHED_OFF_PREFIX}${AUTO_LAND_SWITCHED_OFF_BY[offReason]}`,
|
|
965
|
+
reason: "auto_land_switched_off", bounceKind: null };
|
|
966
|
+
}
|
|
967
|
+
}
|
|
923
968
|
let waitNamed = false;
|
|
924
969
|
if (!result.blocker) {
|
|
925
970
|
const waiting = autoLandWaitNamed(stack, layer, nowMs);
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -64,7 +64,9 @@ export type StackVortexReviewView = {
|
|
|
64
64
|
/** Which table spoke: the authority row, a live job, a lease, or nothing. */
|
|
65
65
|
source: "pr_reviews" | "github_webhook_jobs" | "lease" | "none";
|
|
66
66
|
};
|
|
67
|
-
export type StackCycloneStatus = "patching" | "awaiting_fix" | "failed";
|
|
67
|
+
export type StackCycloneStatus = "patching" | "awaiting_fix" | "held" | "failed";
|
|
68
|
+
export declare const CYCLONE_FIX_HELD_REASON = "fix_held";
|
|
69
|
+
export declare const CYCLONE_CI_FIX_HELD_REASON = "ci_fix_held";
|
|
68
70
|
export type StackCycloneHandoff = {
|
|
69
71
|
headSha: string;
|
|
70
72
|
reason: string;
|
|
@@ -171,7 +173,7 @@ export type StackLayerConflictResolution = {
|
|
|
171
173
|
*/
|
|
172
174
|
dispatchId: string | null;
|
|
173
175
|
};
|
|
174
|
-
export type StackLayerCiFixStatus = "verified" | "unverified" | "no_changes" | "failed";
|
|
176
|
+
export type StackLayerCiFixStatus = "verified" | "unverified" | "no_changes" | "held" | "failed";
|
|
175
177
|
export type StackLayerCiFix = {
|
|
176
178
|
status: StackLayerCiFixStatus;
|
|
177
179
|
failingCheck: string | null;
|
|
@@ -439,7 +441,7 @@ export type MergeQueueBounceBatch = {
|
|
|
439
441
|
withPrNumbers: number[];
|
|
440
442
|
};
|
|
441
443
|
export type MergeQueueBatchState = "verifying" | "landing" | "landed" | "stopped" | "bounced" | "abandoned";
|
|
442
|
-
export type MergeQueueBatchCiCheckState = "passed" | "skipped" | "failed" | "running" | "queued";
|
|
444
|
+
export type MergeQueueBatchCiCheckState = "passed" | "skipped" | "cancelled" | "failed" | "running" | "queued";
|
|
443
445
|
export type MergeQueueBatchCiCheckDto = {
|
|
444
446
|
name: string;
|
|
445
447
|
state: MergeQueueBatchCiCheckState;
|
package/dist/stack-dto.js
CHANGED
|
@@ -23,6 +23,8 @@ export function restackRetryPending(error, layer) {
|
|
|
23
23
|
* dependency-free and byte-for-byte aligned with the canonical wire contract.
|
|
24
24
|
*/
|
|
25
25
|
export const AUTO_LAND_OFF_REASONS = ["user", "archive", "watcher", "cancelled", "landed", "bounced"];
|
|
26
|
+
export const CYCLONE_FIX_HELD_REASON = "fix_held";
|
|
27
|
+
export const CYCLONE_CI_FIX_HELD_REASON = "ci_fix_held";
|
|
26
28
|
export const STACK_AGENT_QUEUED_STALE_MS = 15 * 60 * 1000;
|
|
27
29
|
export const AUTO_RESOLVE_ENQUEUE_GRACE_MS = 2 * 60_000;
|
|
28
30
|
function agentShaMayBeHead(sha, head) {
|
package/dist/stack-watch.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { Config } from "./config.js";
|
|
|
5
5
|
export declare const STACK_WATCH_SCHEMA: "mergestorm.stack_watch/v1";
|
|
6
6
|
export declare const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45000;
|
|
7
7
|
export declare const STACK_WATCH_BUSY_RECHECK_S = 15;
|
|
8
|
+
export declare const STACK_WATCH_ONE_SHOT_READ_TIMEOUT_MS = 30000;
|
|
9
|
+
export declare const STACK_WATCH_DEADLINE_SNAPSHOT_TIMEOUT_MS = 5000;
|
|
8
10
|
/** Resume selectors are fixed for the entire wait, including transient retries. */
|
|
9
11
|
export type StackWatchCursor = {
|
|
10
12
|
readonly stackId: string;
|
package/dist/stack-watch.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { currentLayer, layerAgents, stackBlockers, } from "./stack-blockers.js";
|
|
2
2
|
import { stackTerminalReason, stackWatchObligation, } from "./stack-watch-obligation.js";
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
-
import { apiFetch } from "./api.js";
|
|
4
|
+
import { apiFetch, DEFAULT_API_TIMEOUT_MS } from "./api.js";
|
|
5
5
|
import { REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, transientRetryWaitMs, } from "./commands/review-client.js";
|
|
6
6
|
export const STACK_WATCH_SCHEMA = "mergestorm.stack_watch/v1";
|
|
7
7
|
export const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45_000;
|
|
8
8
|
export const STACK_WATCH_BUSY_RECHECK_S = 15;
|
|
9
|
+
export const STACK_WATCH_ONE_SHOT_READ_TIMEOUT_MS = DEFAULT_API_TIMEOUT_MS;
|
|
10
|
+
export const STACK_WATCH_DEADLINE_SNAPSHOT_TIMEOUT_MS = 5_000;
|
|
9
11
|
export class StackWatchTimeoutError extends Error {
|
|
10
12
|
lastEnvelope;
|
|
11
13
|
constructor(lastEnvelope) {
|
|
@@ -77,7 +79,9 @@ export async function pollStackWatch(cfg, stackId, opts = {}) {
|
|
|
77
79
|
response = await fetch(cfg, route, {
|
|
78
80
|
signal: opts.signal, timeoutMs: route.includes("&wait=")
|
|
79
81
|
? Number(new URL(route, "https://local").searchParams.get("wait")) * 1000 + 5_000
|
|
80
|
-
: finalSnapshot
|
|
82
|
+
: finalSnapshot
|
|
83
|
+
? timeoutMs === 0 ? STACK_WATCH_ONE_SHOT_READ_TIMEOUT_MS : STACK_WATCH_DEADLINE_SNAPSHOT_TIMEOUT_MS
|
|
84
|
+
: Math.max(1, Math.min(30_000, deadline - now())),
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
catch (err) {
|
package/dist/ui/banner.js
CHANGED
|
@@ -209,6 +209,12 @@ const WHATS_NEW = {
|
|
|
209
209
|
"0.3.57": [
|
|
210
210
|
"issue create --help shows the usage",
|
|
211
211
|
],
|
|
212
|
+
"0.3.58": [
|
|
213
|
+
"Watch reports Auto land switched off",
|
|
214
|
+
],
|
|
215
|
+
"0.3.59": [
|
|
216
|
+
"One-shot stack wait rides out a slow read",
|
|
217
|
+
],
|
|
212
218
|
};
|
|
213
219
|
function densityFromVariant(variant) {
|
|
214
220
|
if (variant === "mini" || variant === "nano")
|
package/dist/ui/logo.d.ts
CHANGED
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
* puts it. Same X/width as the SVG, shifted up half a cell. Painted fg 77.
|
|
6
6
|
*/
|
|
7
7
|
export declare const TORNADO_LOGO: string[];
|
|
8
|
-
/** Single idle frame. Kept for callers that index frames. */
|
|
9
|
-
export declare const TORNADO_FRAMES: string[][];
|
|
10
8
|
/** Every sprite glyph is one column. */
|
|
11
9
|
export declare const TORNADO_LOGO_WIDTH: number;
|
|
12
|
-
export declare const TORNADO_LOGO_COMPACT: string[];
|
|
13
|
-
/** @deprecated Use {@link TORNADO_LOGO}. */
|
|
14
|
-
export declare const STORM_MARK: string[];
|
|
15
|
-
/** @deprecated Use {@link TORNADO_LOGO_WIDTH}. */
|
|
16
|
-
export declare const STORM_MARK_WIDTH: number;
|
|
17
10
|
/** Paint one sprite row: each run of glyphs in brand green 77, spaces left bare. */
|
|
18
11
|
export declare function paintSpriteRow(row: string): string;
|
package/dist/ui/logo.js
CHANGED
|
@@ -13,15 +13,8 @@ export const TORNADO_LOGO = [
|
|
|
13
13
|
" ▝▀▀▘ ",
|
|
14
14
|
" ▀ ",
|
|
15
15
|
];
|
|
16
|
-
/** Single idle frame. Kept for callers that index frames. */
|
|
17
|
-
export const TORNADO_FRAMES = [TORNADO_LOGO];
|
|
18
16
|
/** Every sprite glyph is one column. */
|
|
19
17
|
export const TORNADO_LOGO_WIDTH = Math.max(...TORNADO_LOGO.map((line) => [...line].length));
|
|
20
|
-
export const TORNADO_LOGO_COMPACT = TORNADO_LOGO;
|
|
21
|
-
/** @deprecated Use {@link TORNADO_LOGO}. */
|
|
22
|
-
export const STORM_MARK = TORNADO_LOGO;
|
|
23
|
-
/** @deprecated Use {@link TORNADO_LOGO_WIDTH}. */
|
|
24
|
-
export const STORM_MARK_WIDTH = TORNADO_LOGO_WIDTH;
|
|
25
18
|
/** Paint one sprite row: each run of glyphs in brand green 77, spaces left bare. */
|
|
26
19
|
export function paintSpriteRow(row) {
|
|
27
20
|
return row.replace(/[^ ]+/g, (run) => ansi.brightGreen(run));
|
package/dist/ui/prompt.d.ts
CHANGED
|
@@ -35,35 +35,18 @@ export declare class PromptClosedError extends Error {
|
|
|
35
35
|
* prompt cells survive (`merg` + `mergestorm` -> `mergmmmmmergestorm`).
|
|
36
36
|
*/
|
|
37
37
|
export declare function promptRedrawPrefix(inputRow: number): string;
|
|
38
|
-
/**
|
|
39
|
-
* From the content row, move to the first row after the input chrome
|
|
40
|
-
* and erase the slash overlay. Boxed chrome is top + content + bottom
|
|
41
|
-
* (`down(2)`). Plain chrome is the content line (`down(1)`).
|
|
42
|
-
*/
|
|
43
|
-
export declare function promptClearOverlay(boxed: boolean): string;
|
|
44
38
|
/**
|
|
45
39
|
* Cursor home + ED 0: clears the whole screen from row 1 and leaves the
|
|
46
40
|
* cursor there. Used to redraw the welcome panel on resize, where the old
|
|
47
41
|
* frame's row count is unknowable after the terminal rewraps.
|
|
48
42
|
*/
|
|
49
43
|
export declare const HEADER_REDRAW_PREFIX = "\u001B[H\u001B[0J";
|
|
50
|
-
export declare const MOUSE_ENABLE = "\u001B[?1000h\u001B[?1006h";
|
|
51
|
-
export declare const MOUSE_DISABLE = "\u001B[?1000l\u001B[?1006l";
|
|
52
|
-
export type SpriteRect = {
|
|
53
|
-
col: number;
|
|
54
|
-
row: number;
|
|
55
|
-
width: number;
|
|
56
|
-
height: number;
|
|
57
|
-
};
|
|
58
|
-
/** SGR coordinates and the rectangle are both 1-based. */
|
|
59
|
-
export declare function hitTestSprite(col: number, row: number, rect: SpriteRect): boolean;
|
|
60
44
|
export declare const DROPDOWN_MAX_COMMAND_ROWS = 8;
|
|
61
45
|
export declare function formatSlashLabel(spec: CommandSpec): string;
|
|
62
46
|
export declare function specMatchesTerm(spec: CommandSpec, term: string): boolean;
|
|
63
47
|
export declare function slashToken(buffer: string): string | null;
|
|
64
48
|
export declare function matchSlashCommands(buffer: string, specs: CommandSpec[], dismissed?: boolean): CommandSpec[];
|
|
65
49
|
export declare function isExactSlashCommand(buffer: string, specs: CommandSpec[]): boolean;
|
|
66
|
-
export declare function dropdownMaxRows(terminalRows: number | undefined, inputRow: number): number;
|
|
67
50
|
export declare function dropdownViewport(count: number, index: number, maxRows: number): {
|
|
68
51
|
start: number;
|
|
69
52
|
moreAbove: number;
|
package/dist/ui/prompt.js
CHANGED
|
@@ -25,27 +25,12 @@ const clearDown = "\u001b[0J";
|
|
|
25
25
|
export function promptRedrawPrefix(inputRow) {
|
|
26
26
|
return `${up(inputRow)}\r${clearDown}`;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
29
|
-
* From the content row, move to the first row after the input chrome
|
|
30
|
-
* and erase the slash overlay. Boxed chrome is top + content + bottom
|
|
31
|
-
* (`down(2)`). Plain chrome is the content line (`down(1)`).
|
|
32
|
-
*/
|
|
33
|
-
export function promptClearOverlay(boxed) {
|
|
34
|
-
return `${down(boxed ? 2 : 1)}\r${clearDown}`;
|
|
35
|
-
}
|
|
36
28
|
/**
|
|
37
29
|
* Cursor home + ED 0: clears the whole screen from row 1 and leaves the
|
|
38
30
|
* cursor there. Used to redraw the welcome panel on resize, where the old
|
|
39
31
|
* frame's row count is unknowable after the terminal rewraps.
|
|
40
32
|
*/
|
|
41
33
|
export const HEADER_REDRAW_PREFIX = `\u001b[H${clearDown}`;
|
|
42
|
-
export const MOUSE_ENABLE = "\u001b[?1000h\u001b[?1006h";
|
|
43
|
-
export const MOUSE_DISABLE = "\u001b[?1000l\u001b[?1006l";
|
|
44
|
-
/** SGR coordinates and the rectangle are both 1-based. */
|
|
45
|
-
export function hitTestSprite(col, row, rect) {
|
|
46
|
-
return col >= rect.col && col < rect.col + rect.width &&
|
|
47
|
-
row >= rect.row && row < rect.row + rect.height;
|
|
48
|
-
}
|
|
49
34
|
export const DROPDOWN_MAX_COMMAND_ROWS = 8;
|
|
50
35
|
export function formatSlashLabel(spec) {
|
|
51
36
|
const extra = spec.aliases?.length
|
|
@@ -77,12 +62,6 @@ export function isExactSlashCommand(buffer, specs) {
|
|
|
77
62
|
return false;
|
|
78
63
|
return specs.some((spec) => spec.name === token || (spec.aliases ?? []).includes(token));
|
|
79
64
|
}
|
|
80
|
-
export function dropdownMaxRows(terminalRows, inputRow) {
|
|
81
|
-
const rows = typeof terminalRows === "number" && Number.isFinite(terminalRows) && terminalRows >= 1
|
|
82
|
-
? terminalRows
|
|
83
|
-
: 24;
|
|
84
|
-
return Math.max(1, Math.min(DROPDOWN_MAX_COMMAND_ROWS, rows - inputRow - 3));
|
|
85
|
-
}
|
|
86
65
|
export function dropdownViewport(count, index, maxRows) {
|
|
87
66
|
const max = Math.max(1, maxRows);
|
|
88
67
|
if (count <= max)
|
package/package.json
CHANGED
|
@@ -61,6 +61,8 @@ Re-arm only when all of these hold:
|
|
|
61
61
|
|
|
62
62
|
Do not re-arm when `autoLandOff` is null or missing or its `reason` is anything but `bounced` (`user` means the human turned Auto land off; `archive`, `watcher`, `cancelled`, and `landed` are not yours to undo), when the stack was never armed, when the stack is archived, or when any condition above fails; report the newest bounce and stop.
|
|
63
63
|
|
|
64
|
+
When `autoLandOff.reason` is `cancelled` or `watcher` and the current PR has no live blocker of its own, the watch returns `attention` with blocker `Auto land switched off: <why>` and repair kind `auto_land_switched_off`. A live blocker on that PR (failed CI, a conflict, Vortex findings) is reported first and handled by its own repair; the switched-off attention follows once it is cleared. For the switched-off attention itself, no bounce did it and the layers need no change: do not patch, push, or turn Auto land back on yourself. A person or another agent session may have switched it off on purpose. Tell the human that Auto land is off and why, and turn it back on only when they say so.
|
|
65
|
+
|
|
64
66
|
After a patch, re-arm as soon as your push is confirmed; do not wait for CI or reviews, since Auto land itself waits for green checks and idle agents. First recheck the remote head of the PR you pushed, or of the bounced PR for an infra-only cause (`git ls-remote origin refs/heads/<branch>`), and `stack_status`: if that head is not the SHA you pushed or confirmed green, stop and tell the human; if Auto land is already on, leave it. Then call `stack_set` with `stack_id` and `auto_land: true` (the CLI equivalent is `mg stack set <id> --auto-land on`), and confirm with `stack_status` that `autoEnqueueWhenReady` is `true`. Pass no other `stack_set` key. Keep the `after_finished_at` and `bounce_id` selectors for that bounce and keep watching until the stack lands.
|
|
65
67
|
|
|
66
68
|
The cap is one re-arm per distinct `bounce_id` and 3 per stack per watch. Every arm clears `autoLandOff`, so a disarm after your re-arm reads its own reason, never your old bounce. A human who takes the stack back while it is off (turns Auto land off, archives or unarchives it, or cancels its queue entry) replaces `bounced` with `user`, `archive`, or `cancelled`, so `bounced` also means nobody took it back since that bounce. A fourth disarming bounce after three re-arms means your diagnosis is wrong or the fault is outside your scope. At the cap, leave Auto land off and stop: tell the human the stack is not landed, Auto land is off, and list each bounce you re-armed for (id, kind, PR, head, `finishedAt`) with what you did for it, plus the newest bounce and the current head and check state.
|