mergestorm 0.3.40 → 0.3.41

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,7 +45,7 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
45
45
 
46
46
  ```
47
47
  ╭──────────────────────────────────────────────────────────────────────────────╮
48
- │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.40 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.41 │
49
49
  │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
50
  │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
51
  │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
@@ -103,12 +103,12 @@ mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --
103
103
 
104
104
  ### Watching a stack in the background
105
105
 
106
- `mg stack watch <stack-id>` is for agents that cannot hold a long turn open. Run it as a background command, have your host notify you on output matching `MS-WATCH (ATTENTION|LANDED)`, and end the turn. While it waits it spends no model tokens and talks only to the Mergestorm API, not GitHub.
106
+ `mg stack watch <stack-id>` is for agents that cannot hold a long turn open. Run it as a background command only after confirming that your host can resume the task on completion or output matching `MS-WATCH (ATTENTION|LANDED)`. The process monitors the stack; it does not itself resume a Codex, Claude Code, or Cursor conversation. Without a notification mechanism, keep using bounded `stack_wait` calls or explicitly report that automatic follow-up is unavailable. While it waits it spends no model tokens and talks only to the Mergestorm API, not GitHub.
107
107
 
108
108
  - It calls `stack wait` in slices of at most 300s and carries the cursor (`enrolled_head_sha`, `after_finished_at`, `bounce_id`) from one slice to the next.
109
109
  - It prints nothing while the stack is `waiting` or `in_progress`, including while Cyclone or Vortex holds the blocked PR (`busy[]`, `actAfter: "agents_idle"`).
110
110
  - On attention it prints `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` and exits 3.
111
- - When the watch is done it prints `MS-WATCH LANDED stack=<id> reason=<reason>` and exits 0. Only `reason=landed` is a landed stack; `closed`, `archived` and `not_found` end the watch without a land.
111
+ - A confirmed landing prints `MS-WATCH LANDED stack=<id> reason=landed` and exits 0. `closed`, `archived` and `not_found` end the watch with `MS-WATCH ATTENTION stack=<id> reason=<reason>` and exit 3. A missing stack does not prove a landing; verify each PR before reporting it merged.
112
112
  - A failed read is retried once. If the retry fails too, it prints `MS-WATCH ATTENTION failed stack=<id> error="<message>"` and exits 3. A rate limit is waited out.
113
113
  - `--head <sha>` enrolls the SHA you just pushed. After fixing a blocker, restart the watcher with it.
114
114
  - `--ignore <text>` keeps it silent on the first attention whose blocker contains that text (case-insensitive), until the blocker, its PR or its head changes. Use it for a blocker a human has taken over. Repeat the flag for more than one.
@@ -137,7 +137,7 @@ Use `mergestorm dismiss` when a Vortex finding is wrong and you have checked it
137
137
 
138
138
  If the parent branch is already a layer of a **registered** (submitted) stack, `create` and `submit` refuse unless you pass **`--extend`**. That blocks accidentally gluing an unrelated PR onto an open unit. Growing a registered stack on purpose: `mg stack create --onto <tip> --extend` → commit → `mg stack submit --extend`.
139
139
 
140
- `stack submit` walks the **active** local stack: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; adopt then moves the bottom of a 2+ PR stack onto `mg-stack-<n>` — leave it there; skips heads that already have an open PR; body from the tip commit — Summary + Test plan, preserving Fixes/Closes/Resolves), registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key), then drops that local stack entry so the next create onto trunk is clean. Push is always your local `git` credentials. PRs open as you when `gh` is installed and `gh api user` succeeds; otherwise `stack submit` asks the Mergestorm API (`POST /api/v1/stacks/pulls`) to open them as the Mergestorm infrastructure App on your behalf, after checking your GitHub account has write access, and assigns you. That App is Mergestorm Surge, or Cyclone on accounts not yet moved to Surge; stacks, the merge queue and Auto land run through it, and Cyclone is the auto-patch App. Without either, submit stops before pushing and prints the server's message, which names the App to install and links its install page. `mg settings` prints a `Stacks, merge queue, Auto land` line when the server reports whether that App is ready.
140
+ `stack submit` walks the **active** local stack: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; adopt then moves the bottom of a 2+ PR stack onto `mg-stack-<n>` — leave it there; skips heads that already have an open PR; body from the tip commit — Summary + Test plan, preserving Fixes/Closes/Resolves), registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key), then drops that local stack entry so the next create onto trunk is clean. Push is always your local `git` credentials. PRs open as you when `gh` is installed and `gh api user` succeeds; otherwise `stack submit` asks the Mergestorm API (`POST /api/v1/stacks/pulls`) to open them as the Mergestorm Surge App on your behalf, after checking your GitHub account has write access, and assigns you. Stacks, the merge queue and Auto land run through Surge, and Cyclone is the auto-patch App. Without Surge, submit stops before pushing and prints the server's message, which names the App to install and links its install page. `mg settings` prints a `Stacks, merge queue, Auto land` line when the server reports whether that App is ready.
141
141
 
142
142
  Legacy repo-local `.mergestorm/stack.json` state migrates automatically on the next stack command. If local authoring state is stale or malformed, use `mg stack reset --force`; this clears only pre-submit CLI state and never deletes branches, PRs, or registered stacks.
143
143
 
package/dist/api.d.ts CHANGED
@@ -238,8 +238,8 @@ export declare function ensureUpperPark(stackId: string, cfg?: Config): Promise<
238
238
  freezeBranch: string;
239
239
  created: boolean;
240
240
  }>;
241
- export declare const CYCLONE_INSTALL_URL = "https://github.com/apps/mergestorm-cyclone/installations/new";
242
- export declare function cycloneNotInstalledMessage(owner: string, repo: string, installUrl?: string): string;
241
+ export declare const SURGE_INSTALL_URL = "https://github.com/apps/mergestorm-surge/installations/new";
242
+ export declare function surgeNotInstalledMessage(owner: string, repo: string, installUrl?: string): string;
243
243
  export declare function findStackPull(cfg: Config, input: {
244
244
  owner: string;
245
245
  repo: string;
package/dist/api.js CHANGED
@@ -478,15 +478,15 @@ export async function ensureUpperPark(stackId, cfg) {
478
478
  created: body.created === true,
479
479
  };
480
480
  }
481
- export const CYCLONE_INSTALL_URL = "https://github.com/apps/mergestorm-cyclone/installations/new";
482
- export function cycloneNotInstalledMessage(owner, repo, installUrl = CYCLONE_INSTALL_URL) {
483
- return (`Opening PRs needs either the GitHub CLI (gh auth login) or Cyclone installed on ` +
481
+ export const SURGE_INSTALL_URL = "https://github.com/apps/mergestorm-surge/installations/new";
482
+ export function surgeNotInstalledMessage(owner, repo, installUrl = SURGE_INSTALL_URL) {
483
+ return (`Opening PRs needs either the GitHub CLI (gh auth login) or Mergestorm Surge installed on ` +
484
484
  `${owner}/${repo} (${installUrl}).`);
485
485
  }
486
486
  function installUrlFrom(message) {
487
487
  if (typeof message !== "string")
488
- return CYCLONE_INSTALL_URL;
489
- return message.match(GITHUB_APP_INSTALL_URL)?.[0] ?? CYCLONE_INSTALL_URL;
488
+ return SURGE_INSTALL_URL;
489
+ return message.match(GITHUB_APP_INSTALL_URL)?.[0] ?? SURGE_INSTALL_URL;
490
490
  }
491
491
  function stackPullFailure(status, body, owner, repo, failLabel) {
492
492
  const rejection = (body && typeof body === "object" ? body : {});
@@ -496,7 +496,7 @@ function stackPullFailure(status, body, owner, repo, failLabel) {
496
496
  const text = `${String(rejection.error ?? "")} ${String(rejection.message ?? "")}`;
497
497
  if (rejection.error === "cyclone_not_installed" ||
498
498
  /not installed|installation[^\n]*not found/i.test(text)) {
499
- return new CommandError(cycloneNotInstalledMessage(owner, repo, installUrlFrom(rejection.message)), 1, "cyclone_not_installed");
499
+ return new CommandError(surgeNotInstalledMessage(owner, repo, installUrlFrom(rejection.message)), 1, "cyclone_not_installed");
500
500
  }
501
501
  if (status === 404) {
502
502
  return new CommandError("Stacks API cannot open PRs on this server yet. Install the GitHub CLI (gh auth login) and retry.");
@@ -37,7 +37,7 @@ export function stackWatchTextLines(watch) {
37
37
  const next = watch.next;
38
38
  return [
39
39
  ...(next ? [
40
- `Next: ${next.background} as a background command (notify on ${STACK_WATCH_NOTIFY_PATTERN}, then end your turn)`,
40
+ `Only with a confirmed host notification that resumes this task: run ${next.background} as a background command (notify on ${STACK_WATCH_NOTIFY_PATTERN}), then end your turn`,
41
41
  `Or poll: ${next.command} (MCP: stack_wait ${JSON.stringify(next.args)})`,
42
42
  ] : []),
43
43
  STACK_WATCH_NOT_DONE_SENTENCE,
@@ -58,7 +58,7 @@ const STACK_USAGE = `usage:
58
58
  mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--json]
59
59
  Loops stack wait with the cursor and stays silent while the stack is waiting or in progress.
60
60
  Prints one MS-WATCH ATTENTION line and exits 3 on attention or a repeated failed read; prints
61
- MS-WATCH LANDED and exits 0 once the watch is done. Run it as a background command.
61
+ MS-WATCH LANDED and exits 0 only for a confirmed landing. Other terminal reasons exit 3 with ATTENTION. Run it as a background command.
62
62
  --json writes exactly one JSON document to stdout and sends the MS-WATCH lines to stderr.
63
63
  mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
64
64
  mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
@@ -996,7 +996,9 @@ export async function cmdStackWatch(args, deps = {}) {
996
996
  ? `Stopped watching stack ${stackId} after --max; it is not landed.`
997
997
  : result.outcome === "aborted"
998
998
  ? `Stopped watching stack ${stackId}; it is not landed.`
999
- : `Stack ${stackId} needs attention; it is not landed.`;
999
+ : result.envelope?.watch.reason === "not_found"
1000
+ ? `Stack ${stackId} was not found; landing is unconfirmed. Verify each PR before reporting it landed.`
1001
+ : `Stack ${stackId} needs attention; it is not landed.`;
1000
1002
  throw new CommandError(reason, result.exitCode, result.outcome === "timeout" ? "review_timeout" : "stack_attention");
1001
1003
  }
1002
1004
  export async function cmdStackStatus(args, deps = {}) {
@@ -1146,7 +1148,7 @@ async function cmdStackLand(args) {
1146
1148
  await present("Stack", [
1147
1149
  ansi.brightGreen(` Promoted` +
1148
1150
  (pr != null ? ` #${pr}` : "") +
1149
- ` into U${uNumber}` +
1151
+ ` into IU ${uNumber}` +
1150
1152
  (branch ? ` (${branch})` : "") +
1151
1153
  ` on ${id}`),
1152
1154
  ]);
@@ -48,11 +48,14 @@ export async function runStackWatch(cfg, stackId, opts = {}) {
48
48
  let failures = 0;
49
49
  const ignoredKeys = new Map();
50
50
  let reportedKey = null;
51
- const landed = (envelope) => {
52
- emit(stackWatchLandedLine(envelope), envelope, "landed");
51
+ const finished = (envelope) => {
52
+ const landed = envelope.watch.reason === "landed";
53
+ const outcome = landed ? "landed" : "attention";
54
+ emit(landed ? stackWatchLandedLine(envelope)
55
+ : `${STACK_WATCH_MARKER} ATTENTION stack=${envelope.stackId} reason=${envelope.watch.reason}`, envelope, outcome);
53
56
  if (!opts.json)
54
57
  write(envelope.watch.message);
55
- return { outcome: "landed", exitCode: STACK_WATCH_EXIT.landed, envelope };
58
+ return { outcome, exitCode: landed ? STACK_WATCH_EXIT.landed : STACK_WATCH_EXIT.attention, envelope };
56
59
  };
57
60
  const failed = async (message, envelope) => {
58
61
  failures += 1;
@@ -82,7 +85,7 @@ export async function runStackWatch(cfg, stackId, opts = {}) {
82
85
  continue;
83
86
  }
84
87
  if (err instanceof StackWatchError && err.lastEnvelope.watch.done)
85
- return landed(err.lastEnvelope);
88
+ return finished(err.lastEnvelope);
86
89
  if (err instanceof StackWatchError && err.lastEnvelope.status === "rate_limited") {
87
90
  await pause(err.retryAfterSeconds !== undefined ? Math.max(1_000, err.retryAfterSeconds * 1000) : STACK_WATCH_RATE_LIMIT_WAIT_MS);
88
91
  continue;
@@ -95,7 +98,7 @@ export async function runStackWatch(cfg, stackId, opts = {}) {
95
98
  failures = 0;
96
99
  cursor = envelope.cursor;
97
100
  if (envelope.watch.done)
98
- return landed(envelope);
101
+ return finished(envelope);
99
102
  if (envelope.status === "attention") {
100
103
  const key = stackWatchAttentionKey(envelope);
101
104
  const blocker = (envelope.blocker ?? "").toLowerCase();
@@ -7,7 +7,7 @@ const DONE_MESSAGES = {
7
7
  landed: "This stack is landed. The watch is done; stop calling stack_wait for it.",
8
8
  closed: "Every PR in this stack is merged or closed and nothing is left to land, but not every PR landed. The watch is done; tell the human which PRs were closed without landing.",
9
9
  archived: "This stack is archived: the human took it back. The watch is done; stop calling stack_wait for it and do not push to its PRs.",
10
- not_found: "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, so a stack you were watching has most likely landed; a mistyped id or another account's stack reads the same. The watch is done; confirm each PR is merged (gh pr view <n> --json state) before reporting it landed.",
10
+ not_found: "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, which can mean merged or closed; a mistyped id or another account's stack reads the same. Landing is unconfirmed. The watch is done; confirm each PR is merged (gh pr view <n> --json state) before reporting it landed.",
11
11
  };
12
12
  export function stackTerminalReason(stack) {
13
13
  if (!stack)
@@ -56,7 +56,7 @@ export function stackWatchObligation(input) {
56
56
  const background = stackWatchBackgroundCommand(input.stackId);
57
57
  const parts = [
58
58
  STACK_WATCH_NOT_DONE_SENTENCE,
59
- `If you cannot hold a long turn open, run \`${background}\` as a background command instead, notify on output matching ${STACK_WATCH_NOTIFY_PATTERN}, and end your turn: it keeps calling stack_wait for you, stays silent while the stack waits, and wakes you on attention or when the watch is done.`,
59
+ `If your host has a confirmed notification that resumes this task, run \`${background}\` as a background command, notify on output matching ${STACK_WATCH_NOTIFY_PATTERN}, and end your turn. The command monitors the stack; it cannot wake an agent by itself. Without a confirmed notification, keep using stack_wait with the returned cursor, or report that automatic follow-up is unavailable before ending the turn.`,
60
60
  `Watch stack ${input.stackId} as a whole until it lands or the human takes it back. A clean push, a submit, or one merged layer does not finish it: merging a lower layer can break a layer above it.`,
61
61
  ];
62
62
  if (reason === "rate_limited") {
package/dist/ui/banner.js CHANGED
@@ -158,6 +158,9 @@ const WHATS_NEW = {
158
158
  "refusals name Surge when it is the App to install",
159
159
  "mg settings says whether stacks are ready",
160
160
  ],
161
+ "0.3.41": [
162
+ "the watch never reports a missing stack as landed",
163
+ ],
161
164
  };
162
165
  function densityFromVariant(variant) {
163
166
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.40",
3
+ "version": "0.3.41",
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",
@@ -13,15 +13,15 @@ A stack watch is done only when the stack is landed, or when the human takes it
13
13
 
14
14
  ## Waiting in the background
15
15
 
16
- If your host cannot hold a long turn open (Cursor, Claude Code, Codex, and other turn-based agents), do not poll `stack_wait` inside your turn and do not subscribe to GitHub events. Hand the waiting to the CLI watcher:
16
+ The CLI watcher monitors the stack but cannot wake an agent by itself. Use the background handoff only when the host has a confirmed notification that resumes this task. Without one, keep using `stack_wait` with `watch.next.args`, or tell the human that automatic follow-up is unavailable before ending the turn. Do not claim the task will resume just because a shell process is running. Do not subscribe to GitHub events. With a confirmed notification:
17
17
 
18
18
  1. Run `mg stack watch <stack-id>` as a background command (add `--head <sha>` right after you pushed that SHA) with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs the same stack wait against the Mergestorm API in slices of at most 300s, carries the cursor (`enrolled_head_sha`, `after_finished_at`, `bounce_id`) from one slice to the next, and prints nothing while the stack is `waiting` or `in_progress`, including `busy[]` holds and `actAfter: "agents_idle"`. It spends no model tokens while it waits. Ending your turn while it runs is not ending the task. Every `watch.next` carries this command as `watch.next.background`.
19
19
  2. It prints one line and exits for one of three reasons:
20
20
  - `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` (exit 3): the watch returned `attention`.
21
21
  - `MS-WATCH ATTENTION failed stack=<id> error="<message>"` (exit 3): a read failed and its one retry failed too.
22
- - `MS-WATCH LANDED stack=<id> reason=<reason>` (exit 0): `watch.done` is `true`. Only `reason=landed` is a landed stack; for `closed`, `archived`, or `not_found`, follow "Not done until landed".
22
+ - `MS-WATCH LANDED stack=<id> reason=landed` (exit 0): the stack is confirmed landed. `closed`, `archived`, and `not_found` instead emit `MS-WATCH ATTENTION stack=<id> reason=<reason>` (exit 3); follow "Not done until landed".
23
23
  With `--max <minutes>` it can also stop with `MS-WATCH TIMEOUT` (exit 5). The stack is not landed then; start the watcher again. `--json` writes exactly one JSON document to stdout: the final `mergestorm.stack_watch/v1` envelope, or `{ stackId, outcome }` when there is no envelope (a repeated failed read, timeout, or abort). MS-WATCH marker lines go to stderr; do not merge the streams before parsing stdout.
24
- 3. When it wakes you on `MS-WATCH ATTENTION pr=`, call `stack_status` once for that stack, then fix the blocker as steps 3 to 7 of "Watch and patch" say, push, and start the watcher again with `--head` set to the SHA you pushed. On `MS-WATCH ATTENTION failed`, the watcher already retried once: tell the human the stack is not landed and is no longer being watched.
24
+ 3. When the host resumes you on `MS-WATCH ATTENTION pr=`, call `stack_status` once for that stack, then fix the blocker as steps 3 to 7 of "Watch and patch" say, push, and start the watcher again with `--head` set to the SHA you pushed. On `MS-WATCH ATTENTION failed`, the watcher already retried once: tell the human the stack is not landed and is no longer being watched.
25
25
  4. When the human has taken one blocker over and asked you to keep watching the rest, pass `--ignore <blocker-substring>`. The watcher stays silent on the first attention whose blocker contains that text until the blocker, its PR, or its head changes; any other attention still wakes you.
26
26
  5. `--until landed` keeps the watcher running after an attention: it prints each new attention line once and exits only when the watch is done or a read fails twice. Use it only when your host streams output lines to you while the command runs.
27
27
 
@@ -39,7 +39,7 @@ A stack whose layers are all merged into `mg-stack-N` but has no open land PR re
39
39
  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.
40
40
  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.
41
41
  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 a land PR when auto-patch is on for that stack, otherwise you do: when `busy[]` shows no Cyclone run on it, fix its blocker with the smallest patch on `mg-stack-N` and push with an ordinary push, never a force push; while Cyclone is patching 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. Never use Dashboard Continue. 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-*`.
42
- 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 — U<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.
42
+ 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.
43
43
  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. 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.
44
44
  7. After your successful push, leave a clean worktree and keep watching the named stack. Start a new watch cycle with `enrolled_head_sha` set to the SHA you pushed (in the background: `mg stack watch <stack-id> --head <pushed-sha>`). If you handled a bounce, retain the returned `cursor.afterFinishedAt` as `after_finished_at` and `cursor.bounceId` as `bounce_id` so that attempt is not handled again. Keep these selectors unchanged across subsequent retries. Continue watching; do not enqueue anything. If the bounce you handled turned Auto land off, turn it back on as "Re-arm after a bounce" allows; if that section does not allow it, report and stop.
45
45