mergestorm 0.3.64 → 0.3.67

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.
Files changed (76) hide show
  1. package/README.md +28 -9
  2. package/dist/api.d.ts +77 -11
  3. package/dist/api.js +228 -6
  4. package/dist/automation-catalog.d.ts +9 -1
  5. package/dist/automation-catalog.js +2 -0
  6. package/dist/cli.js +21 -19
  7. package/dist/client/index.d.ts +3 -2
  8. package/dist/client/index.js +3 -2
  9. package/dist/commands/account.d.ts +3 -0
  10. package/dist/commands/account.js +75 -0
  11. package/dist/commands/browse.d.ts +1 -9
  12. package/dist/commands/browse.js +10 -8
  13. package/dist/commands/dismiss.d.ts +0 -2
  14. package/dist/commands/dismiss.js +2 -2
  15. package/dist/commands/help.js +3 -2
  16. package/dist/commands/issue.d.ts +24 -6
  17. package/dist/commands/issue.js +157 -31
  18. package/dist/commands/login.js +46 -14
  19. package/dist/commands/logout.js +6 -7
  20. package/dist/commands/memory.d.ts +2 -3
  21. package/dist/commands/memory.js +8 -6
  22. package/dist/commands/registry.d.ts +1 -0
  23. package/dist/commands/registry.js +35 -11
  24. package/dist/commands/review-client.d.ts +0 -3
  25. package/dist/commands/review-client.js +3 -3
  26. package/dist/commands/review.d.ts +1 -1
  27. package/dist/commands/review.js +1 -1
  28. package/dist/commands/settings.d.ts +3 -5
  29. package/dist/commands/settings.js +5 -3
  30. package/dist/commands/stack.d.ts +19 -5
  31. package/dist/commands/stack.js +88 -4
  32. package/dist/commands/whoami.js +14 -10
  33. package/dist/config.d.ts +55 -2
  34. package/dist/config.js +264 -13
  35. package/dist/errors.d.ts +1 -1
  36. package/dist/gh.d.ts +0 -4
  37. package/dist/gh.js +4 -4
  38. package/dist/git.d.ts +1 -1
  39. package/dist/git.js +2 -2
  40. package/dist/memory-api.d.ts +1 -0
  41. package/dist/memory-api.js +2 -2
  42. package/dist/node-version.d.ts +0 -2
  43. package/dist/node-version.js +1 -1
  44. package/dist/pr-body.d.ts +4 -0
  45. package/dist/pr-body.js +15 -0
  46. package/dist/repo-identity.d.ts +2 -1
  47. package/dist/repo-identity.js +8 -4
  48. package/dist/server-publish.d.ts +1 -24
  49. package/dist/server-publish.js +9 -9
  50. package/dist/stack-blockers.d.ts +12 -38
  51. package/dist/stack-blockers.js +57 -33
  52. package/dist/stack-dto.d.ts +3 -0
  53. package/dist/stack-dto.js +15 -2
  54. package/dist/stack-meta.d.ts +2 -2
  55. package/dist/stack-meta.js +1 -1
  56. package/dist/stack-watch-loop.d.ts +2 -13
  57. package/dist/stack-watch-loop.js +12 -12
  58. package/dist/submit-pr-base.d.ts +0 -1
  59. package/dist/submit-pr-base.js +1 -1
  60. package/dist/ui/ansi.d.ts +0 -10
  61. package/dist/ui/ansi.js +2 -2
  62. package/dist/ui/banner.d.ts +3 -1
  63. package/dist/ui/banner.js +13 -3
  64. package/dist/ui/box.d.ts +0 -2
  65. package/dist/ui/box.js +1 -1
  66. package/dist/ui/prompt.d.ts +0 -11
  67. package/dist/ui/prompt.js +7 -7
  68. package/dist/ui/tabs.d.ts +2 -2
  69. package/dist/ui/tabs.js +1 -1
  70. package/dist/ui/width.d.ts +0 -9
  71. package/dist/ui/width.js +3 -3
  72. package/dist/web-api.d.ts +17 -0
  73. package/dist/web-api.js +30 -0
  74. package/package.json +1 -1
  75. package/skill/SKILL.md +4 -0
  76. package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
@@ -7,11 +7,12 @@ export type BannerState = {
7
7
  me: MeResponse | null;
8
8
  key: string | undefined;
9
9
  keyInvalid: boolean;
10
+ account?: string | null;
10
11
  };
11
12
  /** Full welcome, mid pane, or Claude-style short chrome that still shows the mark. */
12
13
  export type BannerDensity = "full" | "compact" | "mini" | "nano";
13
14
  /** `true` = compact. Strings pick the short-pane densities. */
14
- export type BannerVariant = boolean | "mini" | "nano";
15
+ type BannerVariant = boolean | "mini" | "nano";
15
16
  /** Handle returned by {@link printBannerHeader}; `rows` reflows for a resize. */
16
17
  export type BannerHandle = {
17
18
  rows(columns?: number, variant?: BannerVariant, frame?: number): string[];
@@ -35,3 +36,4 @@ export declare function printBannerHeader(opts?: {
35
36
  paint?: boolean;
36
37
  }): Promise<BannerHandle>;
37
38
  export declare function shellPrompt(_loggedIn: boolean): string;
39
+ export {};
package/dist/ui/banner.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getMe } from "../api.js";
2
2
  import { CommandError } from "../errors.js";
3
- import { keyDisplay, loadConfig, resolveApiKey } from "../config.js";
3
+ import { keyDisplay, loadConfig, resolveApiKey, settleAccountName } from "../config.js";
4
4
  import { cliVersion } from "../version.js";
5
5
  import { ansi } from "./ansi.js";
6
6
  import { frameWidth, padVisible, preferBoxedUi, roundedBox, sideBySide, terminalColumns, visibleWidth, } from "./box.js";
@@ -230,6 +230,15 @@ const WHATS_NEW = {
230
230
  "0.3.64": [
231
231
  "Memory commands and Overdrive merge batches",
232
232
  ],
233
+ "0.3.65": [
234
+ "Up to 3 accounts: `account use`, `--account <name>`",
235
+ ],
236
+ "0.3.66": [
237
+ "`issue wait`; submit needs an approved issue",
238
+ ],
239
+ "0.3.67": [
240
+ "Issue review: reply once, then one re-check",
241
+ ],
233
242
  };
234
243
  function densityFromVariant(variant) {
235
244
  if (variant === "mini" || variant === "nano")
@@ -247,7 +256,7 @@ function statusLine(state) {
247
256
  if (state.keyInvalid) {
248
257
  return `${ansi.dim("○")} API key invalid or revoked · type ${ansi.brightGreen("login")} to sign in again`;
249
258
  }
250
- const display = state.me?.key.prefix ?? keyDisplay(state.key) ?? "msk_live_…";
259
+ const display = state.account ?? state.me?.key.prefix ?? keyDisplay(state.key) ?? "msk_live_…";
251
260
  if (!state.me) {
252
261
  return `${ansi.brightGreen("●")} ${ansi.bold(display)} ${ansi.dim("· account details unavailable")}`;
253
262
  }
@@ -373,7 +382,8 @@ export async function printBannerHeader(opts) {
373
382
  }
374
383
  }
375
384
  }
376
- const state = { version: cliVersion(), me, key, keyInvalid };
385
+ const account = me ? await settleAccountName(cfg, me) : cfg.account;
386
+ const state = { version: cliVersion(), me, key, keyInvalid, account: account ?? null };
377
387
  if (opts?.paint !== false) {
378
388
  for (const line of buildBannerRows(state)) {
379
389
  console.log(line);
package/dist/ui/box.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import { padVisible, sliceVisible, visibleWidth } from "./width.js";
2
2
  export { padVisible, sliceVisible, visibleWidth };
3
- /** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
4
- export declare const MIN_BOXED_COLUMNS = 48;
5
3
  /**
6
4
  * Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
7
5
  * Pass `columns` to measure a stream other than process.stdout (or a
package/dist/ui/box.js CHANGED
@@ -5,7 +5,7 @@ import { padVisible, sliceVisible, visibleWidth } from "./width.js";
5
5
  // importers (banner, prompt) keep working.
6
6
  export { padVisible, sliceVisible, visibleWidth };
7
7
  /** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
8
- export const MIN_BOXED_COLUMNS = 48;
8
+ const MIN_BOXED_COLUMNS = 48;
9
9
  /**
10
10
  * Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
11
11
  * Pass `columns` to measure a stream other than process.stdout (or a
@@ -41,10 +41,7 @@ export declare function promptRedrawPrefix(inputRow: number): string;
41
41
  * frame's row count is unknowable after the terminal rewraps.
42
42
  */
43
43
  export declare const HEADER_REDRAW_PREFIX = "\u001B[H\u001B[0J";
44
- export declare const DROPDOWN_MAX_COMMAND_ROWS = 8;
45
44
  export declare function formatSlashLabel(spec: CommandSpec): string;
46
- export declare function specMatchesTerm(spec: CommandSpec, term: string): boolean;
47
- export declare function slashToken(buffer: string): string | null;
48
45
  export declare function matchSlashCommands(buffer: string, specs: CommandSpec[], dismissed?: boolean): CommandSpec[];
49
46
  export declare function isExactSlashCommand(buffer: string, specs: CommandSpec[]): boolean;
50
47
  export declare function dropdownViewport(count: number, index: number, maxRows: number): {
@@ -64,8 +61,6 @@ export declare function promptChromeRows(boxed: boolean, hasPrompt: boolean, has
64
61
  * passes 0 — do not pad a blank gap between header and prompt.
65
62
  */
66
63
  export declare function overlayReserve(terminalRows: number | undefined, headerRows: number, boxed: boolean, hasPrompt: boolean): number;
67
- /** Pad so the slash list sits just above the input bar (Claude-style). */
68
- export declare function padOverlayRows(rows: OverlayRow[], reserve: number): OverlayRow[];
69
64
  export interface HomeFrameFit {
70
65
  header: string[];
71
66
  reserve: number;
@@ -91,12 +86,6 @@ export declare function fitHomeFrame(params: {
91
86
  dockBottom?: boolean;
92
87
  }): HomeFrameFit;
93
88
  export declare function dropdownOverlayRows(dropdown: CommandSpec[], index: number, maxRows: number): OverlayRow[];
94
- /** Input prefix glyph (U+203A), kept as an escape so editors can't mangle it. */
95
- export declare const INPUT_PREFIX_GLYPH = "\u203A";
96
- /** Cells the `> ` prefix occupies before the buffer. */
97
- export declare const INPUT_PREFIX_CELLS = 2;
98
- /** Padding inside the boxed input frame. */
99
- export declare const INPUT_BOX_PADDING = 1;
100
89
  export interface InputWindow {
101
90
  /** First buffer cell shown. */
102
91
  start: number;
package/dist/ui/prompt.js CHANGED
@@ -31,19 +31,19 @@ export function promptRedrawPrefix(inputRow) {
31
31
  * frame's row count is unknowable after the terminal rewraps.
32
32
  */
33
33
  export const HEADER_REDRAW_PREFIX = `\u001b[H${clearDown}`;
34
- export const DROPDOWN_MAX_COMMAND_ROWS = 8;
34
+ const DROPDOWN_MAX_COMMAND_ROWS = 8;
35
35
  export function formatSlashLabel(spec) {
36
36
  const extra = spec.aliases?.length
37
37
  ? ` \u00b7 ${spec.aliases.join(" \u00b7 ")}`
38
38
  : "";
39
39
  return `/${spec.name}${extra}`;
40
40
  }
41
- export function specMatchesTerm(spec, term) {
41
+ function specMatchesTerm(spec, term) {
42
42
  if (spec.name.toLowerCase().startsWith(term))
43
43
  return true;
44
44
  return (spec.aliases ?? []).some((alias) => alias.toLowerCase().startsWith(term));
45
45
  }
46
- export function slashToken(buffer) {
46
+ function slashToken(buffer) {
47
47
  if (!buffer.startsWith("/"))
48
48
  return null;
49
49
  return buffer.slice(1).split(/\s/, 1)[0].toLowerCase();
@@ -97,7 +97,7 @@ export function overlayReserve(terminalRows, headerRows, boxed, hasPrompt) {
97
97
  return Math.max(0, Math.min(DROPDOWN_MAX_COMMAND_ROWS, available));
98
98
  }
99
99
  /** Pad so the slash list sits just above the input bar (Claude-style). */
100
- export function padOverlayRows(rows, reserve) {
100
+ function padOverlayRows(rows, reserve) {
101
101
  const max = Math.max(0, reserve);
102
102
  if (max === 0)
103
103
  return [];
@@ -194,11 +194,11 @@ export function dropdownOverlayRows(dropdown, index, maxRows) {
194
194
  return rows.slice(0, max);
195
195
  }
196
196
  /** Input prefix glyph (U+203A), kept as an escape so editors can't mangle it. */
197
- export const INPUT_PREFIX_GLYPH = "\u203a";
197
+ const INPUT_PREFIX_GLYPH = "\u203a";
198
198
  /** Cells the `> ` prefix occupies before the buffer. */
199
- export const INPUT_PREFIX_CELLS = 2;
199
+ const INPUT_PREFIX_CELLS = 2;
200
200
  /** Padding inside the boxed input frame. */
201
- export const INPUT_BOX_PADDING = 1;
201
+ const INPUT_BOX_PADDING = 1;
202
202
  /**
203
203
  * Horizontal scroll for a one-row editor: pick the `cells`-wide window of
204
204
  * `buffer` that keeps the cursor on screen, moving the previous window only
package/dist/ui/tabs.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { type UsagePanelInput } from "./usage.js";
2
2
  export type TabId = "status" | "usage" | "jobs" | "config";
3
- export declare const TAB_ORDER: readonly TabId[];
4
3
  export type TabsJob = {
5
4
  id: string;
6
5
  cells: {
@@ -44,7 +43,7 @@ export type TabsData = {
44
43
  configNote?: string;
45
44
  color?: boolean;
46
45
  };
47
- export type TabsDetail = {
46
+ type TabsDetail = {
48
47
  jobId: string;
49
48
  rows: string[];
50
49
  };
@@ -128,3 +127,4 @@ export type TabsBrowserOptions = {
128
127
  * TTY already).
129
128
  */
130
129
  export declare function runTabsBrowser(opts: TabsBrowserOptions): Promise<void>;
130
+ export {};
package/dist/ui/tabs.js CHANGED
@@ -5,7 +5,7 @@ import { frameWidth, preferBoxedUi, roundedBox, terminalColumns, } from "./box.j
5
5
  import { padVisible, visibleWidth } from "./width.js";
6
6
  import { formatUsagePanel } from "./usage.js";
7
7
  import { dropdownViewport } from "./prompt.js";
8
- export const TAB_ORDER = ["status", "usage", "jobs", "config"];
8
+ const TAB_ORDER = ["status", "usage", "jobs", "config"];
9
9
  const TAB_LABELS = {
10
10
  status: "Status",
11
11
  usage: "Usage",
@@ -4,15 +4,6 @@
4
4
  * and ZWJ are zero, an astral emoji is one grapheme. Getting this wrong is
5
5
  * why the right border of the prompt box drifted on non-Latin input.
6
6
  */
7
- export declare function stripSgr(str: string): string;
8
- /** Cells for one code point on its own (no cluster context). */
9
- export declare function codePointWidth(cp: number): number;
10
- /**
11
- * Cells one grapheme cluster occupies. A cluster is as wide as its widest
12
- * base (a ZWJ family emoji is 2, not 6); VS16 promotes a text-style symbol
13
- * to emoji presentation (2 cells); a cluster of nothing but marks is 0.
14
- */
15
- export declare function graphemeWidth(cluster: string): number;
16
7
  /** Terminal cells a string occupies, ignoring SGR colour codes. */
17
8
  export declare function visibleWidth(str: string): number;
18
9
  /**
package/dist/ui/width.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  const SGR_PATTERN = /\u001b\[[0-9;]*m/g;
8
8
  const SGR_SPLIT = /(\u001b\[[0-9;]*m)/;
9
- export function stripSgr(str) {
9
+ function stripSgr(str) {
10
10
  return str.replace(SGR_PATTERN, "");
11
11
  }
12
12
  /**
@@ -128,7 +128,7 @@ function isWide(cp) {
128
128
  return false;
129
129
  }
130
130
  /** Cells for one code point on its own (no cluster context). */
131
- export function codePointWidth(cp) {
131
+ function codePointWidth(cp) {
132
132
  if (cp === 0xfe0f)
133
133
  return 0; // VS16 handled at cluster level
134
134
  const ch = String.fromCodePoint(cp);
@@ -172,7 +172,7 @@ function* graphemes(text) {
172
172
  * base (a ZWJ family emoji is 2, not 6); VS16 promotes a text-style symbol
173
173
  * to emoji presentation (2 cells); a cluster of nothing but marks is 0.
174
174
  */
175
- export function graphemeWidth(cluster) {
175
+ function graphemeWidth(cluster) {
176
176
  let width = 0;
177
177
  let vs16 = false;
178
178
  for (const ch of cluster) {
@@ -0,0 +1,17 @@
1
+ import { type Config } from "./config.js";
2
+ export declare const WEB_SEARCH_KINDS: readonly ["web", "news"];
3
+ export declare const WEB_SEARCH_RECENCIES: readonly ["day", "week", "month", "year"];
4
+ export declare const WEB_SEARCH_PAGE_MAX = 5;
5
+ export type WebSearchKind = (typeof WEB_SEARCH_KINDS)[number];
6
+ export type WebSearchRecency = (typeof WEB_SEARCH_RECENCIES)[number];
7
+ export type WebSearchInput = {
8
+ query: string;
9
+ kind?: WebSearchKind;
10
+ recency?: WebSearchRecency;
11
+ page?: number;
12
+ };
13
+ export type WebText = {
14
+ text: string;
15
+ };
16
+ export declare function searchWeb(input: WebSearchInput, cfg?: Config): Promise<WebText>;
17
+ export declare function readWebPage(url: string, cfg?: Config): Promise<WebText>;
@@ -0,0 +1,30 @@
1
+ import { apiFetch } from "./api.js";
2
+ import { loadConfig } from "./config.js";
3
+ import { CommandError, REVIEW_EXIT, rateLimitedMessage } from "./errors.js";
4
+ export const WEB_SEARCH_KINDS = ["web", "news"];
5
+ export const WEB_SEARCH_RECENCIES = ["day", "week", "month", "year"];
6
+ export const WEB_SEARCH_PAGE_MAX = 5;
7
+ const KNOWN_CODES = new Set(["invalid_input", "payload_too_large", "web_unavailable"]);
8
+ function webFailure(status, body, retryAfterSeconds, action) {
9
+ const row = (body && typeof body === "object" ? body : {});
10
+ if (status === 429 || row.error === "rate_limited") {
11
+ return new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", retryAfterSeconds !== undefined ? { retryAfterSeconds } : undefined);
12
+ }
13
+ const message = typeof row.message === "string" && row.message ? row.message : null;
14
+ const code = typeof row.error === "string" && KNOWN_CODES.has(row.error) ? row.error : undefined;
15
+ if (status === 404 && !code)
16
+ return new CommandError("This Mergestorm server does not search the web yet.", 1, "not_found");
17
+ return new CommandError(message ?? `${action} (HTTP ${status})`, 1, code);
18
+ }
19
+ async function call(cfg, route, json, action) {
20
+ const { status, body, retryAfterSeconds } = await apiFetch(cfg ?? (await loadConfig()), route, { method: "POST", json });
21
+ if (status !== 200)
22
+ throw webFailure(status, body, retryAfterSeconds, action);
23
+ return body;
24
+ }
25
+ export function searchWeb(input, cfg) {
26
+ return call(cfg, "/api/v1/web/search", { ...input }, "Could not search the web");
27
+ }
28
+ export function readWebPage(url, cfg) {
29
+ return call(cfg, "/api/v1/web/page", { url }, "Could not read the page");
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.64",
3
+ "version": "0.3.67",
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",
package/skill/SKILL.md CHANGED
@@ -13,6 +13,10 @@ Before opening a PR; after a large refactor; when the user says review, audit, s
13
13
 
14
14
  Not for merging, landing, or pushing. For a PR that is already open on GitHub, use the `mergestorm-pr-loop` skill instead.
15
15
 
16
+ ## Before the PR: the issue review
17
+
18
+ Vortex reviews the issue against the repository before any code. Wait for its verdict with `mg issue wait N`, which reads the verdict from Vortex itself; never judge it from comment text. Ready or Ready with notes approves the issue; notes do not block, and the PR review checks them against the code. If the verdict is Needs answers, reply once in a comment on the issue: for each blocking point, accept the suggested fix or say why not, and update the issue to match. Vortex then re-checks only those points, once, and approves the issue; a point still open goes to the PR review. Wait again after your reply. Start the PR only once the issue is approved: `mg stack submit` refuses each PR whose linked issue (`Fixes #N`) is not, so every layer of a stack needs its own approved issue. On a repository with issue reviews off the wait returns at once. If the wait times out, tell the person. A person can approve early with a comment whose first line is `mergestorm-issue: approve`; never post that command yourself.
19
+
16
20
  ## Install (Claude Code / Cursor / Agents)
17
21
 
18
22
  ```bash
@@ -47,7 +47,7 @@ A stack whose layers are all merged into `mg-stack-N` but has no open land PR re
47
47
 
48
48
  1. Call `stack_wait` with the named `stack_id` and `timeout_s: 45`. On initial enrollment, omit the cursor selectors. Save the returned `cursor`: map `cursor.enrolledHeadSha` to `enrolled_head_sha`, `cursor.afterFinishedAt` to `after_finished_at`, and `cursor.bounceId` to `bounce_id` (omit only absent fields; preserve explicit nulls). Keep `bounce_id` alongside `after_finished_at`; never pass one without the other when the returned cursor has both. The shipped tool accepts these selectors, not a nested cursor argument. `watch.next.args` already holds them in that shape, with `timeout_s: 45`; passing it unchanged is the same call.
49
49
  2. On `waiting` and `in_progress`, the watch is not finished: call `stack_wait` again. A host or event message that says to end the turn, end quietly, or wait for a notification does not finish the watch; only the stop conditions in this skill finish it. Ending your turn while `mg stack watch` runs in the background is waiting, not finishing. Keep the same cursor across retries. An unread deadline times out as `failed`; stop and report. Timeout `waiting` or `in_progress` after a snapshot is not failure — call `stack_wait` again in 45s slices with the same cursor after inspecting `issues[]`. Inspect `issues[]` on `in_progress` too: a live queue does not erase upstack hard blocks. `waiting` with issues is not idle. If `assessment` is `unavailable`, do not infer that the stack is issue-free; refresh `stack_status`, whose `attention`, `issues`, and `currentCandidate` come from the same blocker rules as `stack_wait`. Do not replace the enrolled head with a newer snapshot head or advance the timestamp just because a wait timed out. On a surfaced `rate_limited` error, honor the retry delay and retain the cursor; on `failed` or an unrecoverable tool error, call `stack_wait` once more with the same cursor; if it fails again, stop and report the reason and that the stack is not landed. `queue_status` returning `queue_entry_not_found` is not a failure: an empty queue for the stack is a normal wait.
50
- 3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending` or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, or the land PR's base such as `main` when the blocked PR is the land PR, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. A stack whose `autoLandOff.reason` is `bounced` (a `merge_failed`, `gh_error`, or `ci_timeout` bounce turned Auto land off and nobody took the stack back since) returns `attention` with the blocker `Auto land off after <kind> bounce`, the bounce in `issues[]`, and a `repair` of kind `auto_land_off`; handle it as "Re-arm after a bounce" describes. A stack whose review unit was abandoned because a human moved its bottom PR off `mg-stack-N` returns `attention` with the blocker `Review unit abandoned: based on <base>` and a `repair` of kind `unit_abandoned`: do not patch or retarget anything. Ask the human whether to re-root it: if at least two PRs remain open, tell them that `mg stack adopt owner/repo#<bottom PR>` re-roots it on a new review unit; with only one open PR, re-adopting will not create a review unit, so tell them to close and resubmit it to create a fresh stack, then stop. Auto land records why it is waiting in the stack's `autoLandWait` (`reason`, `prNumber`, `headSha`, `since`), and the watch returns `attention` on that PR when the wait cannot clear on its own: a failing check Auto land reads live on GitHub while the ledger may still show CI green (blocker `CI failed — Auto land reads a failing check on GitHub at this head`, `repair` kind `ci_failure`); a check pending for 45 minutes (`repair` kind `ci_pending`: find the check that never finished and re-run it, or tell the human when no workflow reports it); no approving Vortex review for 30 minutes with no Vortex run working (`repair` kind `vortex_not_green`: fix or dismiss the findings, or tell the human when no review exists at the head); and a promote GitHub refused 3 times with `merge_failed`, after which Auto land stops retrying that head (`repair` kind `promote_failed`: fix the cause on the PR and push a new head, or tell the human when the cause is outside the PR; never toggle Auto land yourself for it). The watch also returns `attention` when GitHub refuses to let Mergestorm read the head's check runs or commit statuses (blocker `Mergestorm cannot read ... on this head`, `repair` kind `ci_unreadable`: do not patch or push; tell the human to grant the Mergestorm GitHub App the permission the repair names), and when any other wait reason has held for 45 minutes, a land gate left pending or running on the land PR included (blocker `Auto land has waited <N>m: <reason in plain words>`, `repair` kind `auto_land_waiting`: read `stack_status`, fix what a commit on the named branch can fix, otherwise tell the human what Auto land waits for and since when). A merge queue entry that has sat queued for 45 minutes with nothing working on it or ahead of it returns `attention` with the blocker `Merge queue stuck: ...` and a `repair` whose kind is queue_stalled: do not patch or push; tell the human. A Vortex review skipped because the PR author is on the ignored authors list, or because reviews are turned off for the repository, returns `attention` at once with a `repair` of kind `vortex_skipped`; a Vortex retry that is scheduled (`Vortex hit a temporary error on this head and will retry by itself`) is a wait listed in `issues[]`, never `attention`. `Review failed` carries a `repair` of kind `vortex_failed`: comment `@mergestorm-vortex review` on the PR or push a new commit, and tell the human if it fails again. A PR open on `mg-stack-N` that is not a layer of the stack returns `attention` at once with the blocker `The land PR cannot be queued: ...` and a `repair` of kind `members_remaining`: do not patch or push; close, retarget or adopt that PR (a draft or fork PR cannot be adopted). A layer whose live head has a Vortex verdict that requests changes after Cyclone left those findings for a person (a hold directive, the Vortex patch cap, auto-patch off, no billing, a batch it gave up on, or a PR state it could not read) returns `attention` with the blocker `Vortex findings need a person` and a `repair` of kind `vortex_findings` naming the Cyclone reason (`handoff`) and where the review is (`findings`): verify each finding against the checkout, fix the real ones with the smallest patch and an ordinary push, and tell the human about the ones you judge wrong. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy; the only policy change you make is the re-arm in "Re-arm after a bounce". A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A blocker no Vortex or Cyclone run can clear is not hidden by the hold: a merge conflict vs the live parent, a restack `Conflict` or `Restack failed` (a parked freeze included), a Draft PR, `Seam review pending, no review running`, or `Review skipped, out of review quota`. For those the envelope stays `in_progress` but names the blocker in the top-level `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"`, `waitingOn` listing the busy agents, and `repair` when there is one. Plan the fix from it, but do not patch, merge, or push while `actAfter` is set: keep calling `stack_wait` with the same cursor, and act when a snapshot after the agents finish returns `attention`; a Cyclone push can change or clear the blocker, so re-read it then. Blockers the agents can change (a red CI failure, a failed review, seam findings, a bounce) keep the plain hold: `blocker` is null, `actAfter` is null, and only `busy[]` names them. If a hold looks stuck, refresh `stack_status` and tell the human. Do not turn Cyclone off or Auto land off, and do not turn Auto land on to end the wait; turn it on only as "Re-arm after a bounce" allows. If the remote head moves while you wait, stop and tell the human.
50
+ 3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending` or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, or the land PR's base such as `main` when the blocked PR is the land PR, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. A stack whose `autoLandOff.reason` is `bounced` (a `merge_failed`, `gh_error`, or `ci_timeout` bounce turned Auto land off and nobody took the stack back since) returns `attention` with the blocker `Auto land off after <kind> bounce`, the bounce in `issues[]`, and a `repair` of kind `auto_land_off`; handle it as "Re-arm after a bounce" describes. A stack whose review unit was abandoned because a human moved its bottom PR off `mg-stack-N` returns `attention` with the blocker `Review unit abandoned: based on <base>` and a `repair` of kind `unit_abandoned`: do not patch or retarget anything. Ask the human whether to re-root it: if at least two PRs remain open, tell them that `mg stack adopt owner/repo#<bottom PR>` re-roots it on a new review unit; with only one open PR, re-adopting will not create a review unit, so tell them to close and resubmit it to create a fresh stack, then stop. Auto land records why it is waiting in the stack's `autoLandWait` (`reason`, `prNumber`, `headSha`, `since`), and the watch returns `attention` on that PR when the wait cannot clear on its own: a failing check Auto land reads live on GitHub while the ledger may still show CI green (blocker `CI failed — Auto land reads a failing check on GitHub at this head`, `repair` kind `ci_failure`); a check pending for 45 minutes (`repair` kind `ci_pending`: find the check that never finished and re-run it, or tell the human when no workflow reports it); no approving Vortex review for 30 minutes with no Vortex run working (`repair` kind `vortex_not_green`: fix or dismiss the findings, or tell the human when no review exists at the head); and a promote GitHub refused 3 times with `merge_failed`, after which Auto land stops retrying that head (`repair` kind `promote_failed`: fix the cause on the PR and push a new head, or tell the human when the cause is outside the PR; never toggle Auto land yourself for it). The watch also returns `attention` when GitHub refuses to let Mergestorm read the head's check runs or commit statuses (blocker `Mergestorm cannot read ... on this head`, `repair` kind `ci_unreadable`: do not patch or push; tell the human to grant the Mergestorm GitHub App the permission the repair names), and when any other wait reason has held for 45 minutes, a land gate left pending or running on the land PR included (blocker `Auto land has waited <N>m: <reason in plain words>`, `repair` kind `auto_land_waiting`: read `stack_status`, fix what a commit on the named branch can fix, otherwise tell the human what Auto land waits for and since when). A merge queue entry that has sat queued for 45 minutes with nothing working on it or ahead of it returns `attention` with the blocker `Merge queue stuck: ...` and a `repair` whose kind is queue_stalled: do not patch or push; tell the human. A Vortex review skipped because the PR author is on the ignored authors list, or because reviews are turned off for the repository, returns `attention` at once with a `repair` of kind `vortex_skipped`; a Vortex retry that is scheduled (`Vortex hit a temporary error on this head and will retry by itself`) is a wait listed in `issues[]`, never `attention`. `Review failed` carries a `repair` of kind `vortex_failed`: comment `@mergestorm-vortex review` on the PR or push a new commit, and tell the human if it fails again. A PR open on `mg-stack-N` that is not a layer of the stack returns `attention` at once with the blocker `The land PR cannot be queued: ...` and a `repair` of kind `members_remaining`: do not patch or push; close, retarget or adopt that PR (a draft or fork PR cannot be adopted). A layer whose live head has a Vortex verdict that requests changes after Cyclone left those findings for a person (a hold directive, the Vortex patch cap, auto-patch off, no billing, a batch it gave up on, or a PR state it could not read) returns `attention` with the blocker `Vortex findings need a person` and a `repair` of kind `vortex_findings` naming the Cyclone reason (`handoff`) and where the review is (`findings`): verify each finding against the checkout, fix the real ones with the smallest patch and an ordinary push, and tell the human about the ones you judge wrong. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy; the only policy change you make is the re-arm in "Re-arm after a bounce". A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A Cyclone run still patching after 60 minutes no longer holds: the watch returns `attention` with the blocker `Cyclone stalled: patching <sha> for <N>m with no push or result` and a `repair` of kind `cyclone_stalled`; patch the open Vortex findings yourself with the smallest fix and an ordinary push, and re-read `stack_status` instead of pushing if the branch moves first. A blocker no Vortex or Cyclone run can clear is not hidden by the hold: a merge conflict vs the live parent, a restack `Conflict` or `Restack failed` (a parked freeze included), a Draft PR, `Seam review pending, no review running`, or `Review skipped, out of review quota`. For those the envelope stays `in_progress` but names the blocker in the top-level `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"`, `waitingOn` listing the busy agents, and `repair` when there is one. Plan the fix from it, but do not patch, merge, or push while `actAfter` is set: keep calling `stack_wait` with the same cursor, and act when a snapshot after the agents finish returns `attention`; a Cyclone push can change or clear the blocker, so re-read it then. Blockers the agents can change (a red CI failure, a failed review, seam findings, a bounce) keep the plain hold: `blocker` is null, `actAfter` is null, and only `busy[]` names them. If a hold looks stuck, refresh `stack_status` and tell the human. Do not turn Cyclone off or Auto land off, and do not turn Auto land on to end the wait; turn it on only as "Re-arm after a bounce" allows. If the remote head moves while you wait, stop and tell the human.
51
51
  4. Use a clean worktree on that named PR's head branch and record the remote starting SHA. Fix the current blocker with the smallest patch, and run the relevant checks. For conflicts, the live parent is the sibling PR head or the unit branch (`mg-stack-N`, Mergestorm's owned trunk, not an alias for main) — not the DTO `parentBranch` when that field starts with `mg-park-*`. If `parentBranch` is `mg-park-*`, never merge or rebase onto it; if the real live parent is unknown, stop. The land PR is the one PR whose head is `mg-stack-N`: its live parent is its base (`main`), and merging that base into `mg-stack-N` with an ordinary push is its conflict repair, the same as for any other layer. Cyclone patches Vortex findings on a land PR when auto-patch is on for that stack; every other land PR blocker, a conflict with main or failing CI included, is yours: when `busy[]` shows no Cyclone run on it, fix it with the smallest patch on `mg-stack-N` and push with an ordinary push, never a force push; while Cyclone is patching findings on it the envelope stays `in_progress` with `busy[]`, so wait. For a conflict, prefer merging that live parent into the PR branch, resolving locally, then an ordinary push. Do not click Dashboard Continue yourself: Continue is a human action, and you relay it to the human only when step 5 or a `repair` names it. When the live parent is a sibling, never rebase onto trunk or `mg-stack-N` instead. Parked-freeze DIRTY alone needs no conflict patch. Leave GitHub bases as Mergestorm set them: the bottom of a 2+ PR stack is based on `mg-stack-N`, layer 2 on layer 1, PR3+ on `mg-park-*`.
52
52
  5. CI-only fixes may run while Cyclone auto-patch is still on for the stack. Auto-patch may stay on, and you still wait until the current Vortex run and the current Cyclone run on that PR have finished before the follow-up commit. When the gate is clean, inspect `issues[]` and fix a live upstack CI failure one PR at a time, refreshing the gate before the next PR. Do not use higher-layer bounce history as a patch target. Do not fight Cyclone: if the remote head moved, stop and tell the human. Do not take over Cyclone's review-finding work while it is active; if the blocker requires that or a policy/queue action rather than a concrete patch, stop and explain what needs human attention. On a `Seam findings` blocker, read that PR's seam finding, the Vortex review "Integration review — IU <n> seam" (`gh api repos/OWNER/REPO/pulls/N/reviews` and `gh api repos/OWNER/REPO/pulls/N/comments`), and patch its concrete findings on that PR's head; a "Decision required" seam finding needs a human, so tell them and do not guess. A seam finding you verified is wrong is recorded with `mg dismiss` (the audited dismissal in `mergestorm-pr-loop`) at that PR's live head and that seam review's id; a `mergestorm-loop: dismiss` comment alone does not clear the seam gate. On `Seam review failed`, do not patch; tell the human, unless a new push already has a seam re-review held in `busy[]`. On `Seam review pending, no review running`, do not patch; tell the human: the seam review was skipped (usually because the monthly review limit is reached) and nothing re-runs it until they raise the limit and click Continue on the stack, or push a new head. On `Seam review stuck in reviewing, no review running`, `Seam re-review pending, no review running`, or `Seam verdict is for an older head, no review running` (repair kind `seam_review_stuck`), do not patch or push; tell the human: no seam review is queued or running for that PR's live head, and clicking Continue on the stack re-queues it (a new pushed head also does). On `Review skipped, out of review quota`, do not patch; tell the human: the review credit limit for the period is reached and nothing re-reviews that head until they raise the limit and push a new head or re-run the review.
53
53
  6. Recheck Auto land (off is expected only after a disarming bounce you are handling) and the remote PR head (`git ls-remote origin refs/heads/<branch>`) immediately before pushing. Also call `queue_status` with the `stack_id`: while an entry of the stack is `queued`, `running`, or `waiting`, do not push. The queue is verifying the diff that was reviewed when it was queued; a push that changes that diff bounces the entry `head_moved` and stops the batch it is in, which re-runs CI for every PR in that batch. Do not cancel the entry. Keep the fix on a local branch and keep watching: when the entry bounces, push the fix to this PR; when the stack lands, open the fix as a follow-up PR (`mg stack create <name> --onto main`, then `mg stack submit`) and tell the human its number. Commit the patch and push the PR head with an ordinary push. A rebase onto that same live parent followed by force-with-lease is allowed only if the remote is still exactly the SHA you started from and Cyclone did not move it; use an explicit lease bound to that starting SHA. This exception never permits rebasing onto a parked freeze or substituting trunk / the unit for a sibling parent. If the remote head moved or the push is rejected because it moved, stop; do not rebase and retry.