mergestorm 0.3.56 → 0.3.58

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.56 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.58 │
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.56: Set automation per repository │
53
+ │ ▀ New in v0.3.58: Watch reports Auto land switched off │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -9,6 +9,7 @@ export const ISSUE_USAGE = [
9
9
  " Opens a GitHub issue through Mergestorm as the Surge App. Needs no gh and no GitHub login.",
10
10
  " Uses existing labels only. Running it again with the same title returns the open issue it already made when that issue is among the newest 100 open issues; an older one, or two calls at once, can still create a duplicate.",
11
11
  ].join("\n");
12
+ const VALUE_FLAGS = new Set(["--title", "--body", "--body-file", "--label", "--repo"]);
12
13
  function usage(message) {
13
14
  return new CommandError(message ? `${message}\n${ISSUE_USAGE}` : ISSUE_USAGE, REVIEW_EXIT.usage, "usage");
14
15
  }
@@ -65,7 +66,18 @@ export function parseIssueCreateArgs(args, readBody = (path) => readFileSync(pat
65
66
  }
66
67
  export async function cmdIssue(args, deps = {}) {
67
68
  const sub = args[0]?.toLowerCase();
68
- if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
69
+ const asksForHelp = (arg) => arg === "-h" || arg === "--help" || arg === "help";
70
+ const createAsksForHelp = (rest) => {
71
+ for (let i = 0; i < rest.length; i++) {
72
+ const arg = rest[i];
73
+ if (asksForHelp(arg))
74
+ return true;
75
+ if (VALUE_FLAGS.has(arg))
76
+ i++;
77
+ }
78
+ return false;
79
+ };
80
+ if (!sub || asksForHelp(sub) || (sub === "create" && createAsksForHelp(args.slice(1)))) {
69
81
  console.log(ISSUE_USAGE);
70
82
  return;
71
83
  }
@@ -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_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",
@@ -112,9 +113,16 @@ const CYCLONE_HANDOFF_WORDS = {
112
113
  account_auto_fix_ci_off: "auto-fix CI is off for the account and this stack does not turn it on",
113
114
  };
114
115
  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("_", " ");
116
+ const words = CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
117
+ return reason === CYCLONE_FIX_HELD_REASON && detail?.trim() ? `${words}: ${detail.trim()}` : words;
118
+ }
119
+ export function cycloneStatusWords(layer) {
120
+ if (!layer.cycloneStatus)
121
+ return null;
122
+ if (layer.cycloneStatus !== "held")
123
+ return layer.cycloneStatus;
124
+ const detail = cycloneHandoffDetailAtHead(layer)?.trim();
125
+ return detail ? `held its fix, not pushed (${detail})` : "held its fix, not pushed";
118
126
  }
119
127
  function cycloneHandoffDetailAtHead(layer) {
120
128
  return sameHead(layer.cycloneHandoff?.headSha, layer.headSha) ? layer.cycloneHandoff.detail ?? null : null;
@@ -162,6 +170,7 @@ function ciHandoffNote(stack, layer) {
162
170
  }
163
171
  const CYCLONE_HELD_FINDINGS_REASONS = [
164
172
  "failed",
173
+ CYCLONE_FIX_HELD_REASON,
165
174
  "left_for_human",
166
175
  "infra_failure",
167
176
  "pr_state_unavailable",
@@ -359,6 +368,8 @@ function layerAttention(stack, layer, queue, cursor) {
359
368
  return none;
360
369
  if (kind === "ci_failure" && !bounce.bounceDetail?.batch && (headChecksAllGreen(layer) || headChecksRerunning(layer)))
361
370
  return none;
371
+ if (kind === "ci_timeout" && headChecksAllGreen(layer))
372
+ return none;
362
373
  const label = mergeQueueBounceLabel(bounce);
363
374
  return { blocker: label, reason: "bounce", bounceKind: kind, bounce };
364
375
  }
@@ -391,6 +402,17 @@ function disarmingBounce(stack, queue) {
391
402
  return recorded;
392
403
  return undefined;
393
404
  }
405
+ const AUTO_LAND_SWITCHED_OFF_PREFIX = "Auto land switched off: ";
406
+ const AUTO_LAND_SWITCHED_OFF_BY = {
407
+ cancelled: "its merge queue entry was cancelled",
408
+ watcher: "Mergestorm found nothing it could land",
409
+ };
410
+ function switchedOffByAnotherActor(stack) {
411
+ if (stack.archivedAt || stack.autoEnqueueWhenReady !== false)
412
+ return undefined;
413
+ const reason = stack.autoLandOff?.reason;
414
+ return reason && AUTO_LAND_SWITCHED_OFF_BY[reason] ? reason : undefined;
415
+ }
394
416
  export const AUTO_LAND_CI_PENDING_ATTENTION_MS = 45 * 60_000;
395
417
  export const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS = 30 * 60_000;
396
418
  export const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
@@ -725,13 +747,27 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
725
747
  };
726
748
  }
727
749
  if (reason === "auto_land_off" && attention.bounceKind) {
750
+ const nothingToFix = attention.bounceKind === "ci_timeout" && headChecksAllGreen(layer);
728
751
  return {
729
752
  kind: "auto_land_off",
730
753
  prNumber: layer.prNumber,
731
754
  headSha: layer.headSha ?? null,
732
755
  branch: layer.branch,
733
756
  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.`,
757
+ steps: nothingToFix
758
+ ? `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.`
759
+ : `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.`,
760
+ };
761
+ }
762
+ const offReason = stack.autoLandOff?.reason;
763
+ if (reason === "auto_land_switched_off" && offReason) {
764
+ return {
765
+ kind: "auto_land_switched_off",
766
+ prNumber: layer.prNumber,
767
+ headSha: layer.headSha ?? null,
768
+ branch: layer.branch,
769
+ offReason,
770
+ 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
771
  };
736
772
  }
737
773
  if (reason === "vortex_failed") {
@@ -920,6 +956,13 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
920
956
  result = { blocker: `${AUTO_LAND_OFF_BLOCKER_PREFIX}${kind} bounce`, reason: "auto_land_off", bounceKind: kind, bounce: disarmed };
921
957
  }
922
958
  }
959
+ if (!result.blocker && selected) {
960
+ const offReason = switchedOffByAnotherActor(stack);
961
+ if (offReason) {
962
+ result = { blocker: `${AUTO_LAND_SWITCHED_OFF_PREFIX}${AUTO_LAND_SWITCHED_OFF_BY[offReason]}`,
963
+ reason: "auto_land_switched_off", bounceKind: null };
964
+ }
965
+ }
923
966
  let waitNamed = false;
924
967
  if (!result.blocker) {
925
968
  const waiting = autoLandWaitNamed(stack, layer, nowMs);
@@ -64,7 +64,8 @@ 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";
68
69
  export type StackCycloneHandoff = {
69
70
  headSha: string;
70
71
  reason: string;
package/dist/stack-dto.js CHANGED
@@ -23,6 +23,7 @@ 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";
26
27
  export const STACK_AGENT_QUEUED_STALE_MS = 15 * 60 * 1000;
27
28
  export const AUTO_RESOLVE_ENQUEUE_GRACE_MS = 2 * 60_000;
28
29
  function agentShaMayBeHead(sha, head) {
package/dist/ui/banner.js CHANGED
@@ -206,6 +206,12 @@ const WHATS_NEW = {
206
206
  "0.3.56": [
207
207
  "Set automation per repository",
208
208
  ],
209
+ "0.3.57": [
210
+ "issue create --help shows the usage",
211
+ ],
212
+ "0.3.58": [
213
+ "Watch reports Auto land switched off",
214
+ ],
209
215
  };
210
216
  function densityFromVariant(variant) {
211
217
  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.56",
3
+ "version": "0.3.58",
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.