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 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.57 │
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.57: issue create --help shows the usage │
53
+ │ ▀ New in v0.3.59: One-shot stack wait rides out a slow read │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -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.cycloneStatus}` : ""));
1286
+ (layer.cycloneStatus ? ` Cyclone: ${cycloneStatusWords(layer)}` : ""));
1287
1287
  }
1288
1288
  if (queueWait && !attention)
1289
1289
  lines.push(` Queue: #${queueWait.prNumber} ${queueWait.text}`);
@@ -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";
@@ -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
- if (reason === "failed" && detail?.trim())
116
- return `Cyclone wrote a fix and held it, so it pushed nothing: ${detail.trim()}`;
117
- return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
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: `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.`,
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);
@@ -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) {
@@ -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;
@@ -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 ? 5_000 : Math.max(1, Math.min(30_000, deadline - now())),
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));
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.57",
3
+ "version": "0.3.59",
4
4
  "description": "Mergestorm CLI (mg) — local reviews & stacked PRs. MIT; build from source: https://github.com/marginsystems/mergestorm-cli",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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.