mergestorm 0.3.51 → 0.3.53

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.51 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.53 │
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: mg dismiss records an audited finding dismissal │
53
+ │ ▀ New in v0.3.53: Busy queue no longer reads as stuck │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
package/dist/api.js CHANGED
@@ -81,7 +81,7 @@ function rethrowFetchAbort(err, timeoutMs) {
81
81
  export async function apiFetch(cfg, route, init = {}) {
82
82
  const key = resolveApiKey(cfg);
83
83
  if (!key) {
84
- throw new CommandError("No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.", 1, "missing_api_key");
84
+ throw new CommandError("No API key. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`) or set MERGESTORM_API_KEY.", 1, "missing_api_key");
85
85
  }
86
86
  const { json, timeoutMs = DEFAULT_API_TIMEOUT_MS, signal: userSignal, ...rest } = init;
87
87
  const headers = {
@@ -114,7 +114,7 @@ export async function apiFetch(cfg, route, init = {}) {
114
114
  // keep text
115
115
  }
116
116
  if (res.status === 401) {
117
- throw new CommandError("API key invalid or revoked. Run `mergestorm login`.", 1, "auth_invalid");
117
+ throw new CommandError("API key invalid or revoked. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`).", 1, "auth_invalid");
118
118
  }
119
119
  const retryAfterSeconds = parseRetryAfterSeconds({
120
120
  header: res.headers.get("retry-after"),
@@ -183,7 +183,7 @@ export async function openTabsBrowser(initial) {
183
183
  const cfg = await loadConfig();
184
184
  const key = resolveApiKey(cfg);
185
185
  if (!key) {
186
- throw new CommandError("Not logged in. Run `mergestorm login`.");
186
+ throw new CommandError("Not logged in. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`).");
187
187
  }
188
188
  let me = null;
189
189
  let keyInvalid = false;
@@ -220,6 +220,9 @@ export async function cmdLogin(args) {
220
220
  console.log(` ${ansi.brightGreen(pending.openUrl)}`);
221
221
  console.log(`\n Code: ${ansi.bold(pending.userCode)}\n`);
222
222
  openBrowser(pending.openUrl);
223
+ if (!process.stdout.isTTY) {
224
+ console.log("No terminal here. If this command may be cut off before the approval, use `mergestorm login --start`, then `mergestorm login --finish`.");
225
+ }
223
226
  console.log("Waiting for approval…");
224
227
  await pollDeviceLogin(pending, "mergestorm login", "Run `mergestorm login` again.");
225
228
  }
@@ -9,7 +9,7 @@ export async function cmdWhoami(args, opts = {}) {
9
9
  const cfg = await loadConfig();
10
10
  const key = resolveApiKey(cfg);
11
11
  if (!key) {
12
- throw new CommandError("Not logged in. Run `mergestorm login`.");
12
+ throw new CommandError("Not logged in. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`).");
13
13
  }
14
14
  if (!asJson && canBrowse()) {
15
15
  await openTabsBrowser("status");
@@ -199,6 +199,7 @@ export type MergeQueueWait = {
199
199
  waitReason: string | null;
200
200
  aheadStackId: string | null;
201
201
  aheadOther: boolean;
202
+ idleSince?: string | null;
202
203
  };
203
204
  export declare function mergeQueueWait(queue: readonly MergeQueueEntryDto[], nowMs: number): MergeQueueWait | null;
204
205
  export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
@@ -78,6 +78,7 @@ function cycloneHandoffAtHead(stack, layer) {
78
78
  return stack.cyclonePatchOff ?? (stack.autoPatchOverride === false ? "auto_patch_off" : null);
79
79
  }
80
80
  const CYCLONE_HANDOFF_WORDS = {
81
+ failed: "Cyclone's patch failed, so it pushed nothing",
81
82
  auto_patch_off: "auto-patch is off for this stack",
82
83
  stack_auto_patch_off: "auto-patch is off for this stack",
83
84
  account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
@@ -101,16 +102,21 @@ const CYCLONE_HANDOFF_WORDS = {
101
102
  ci_red_on_base: "the same check also fails on the base branch",
102
103
  ci_rerun_exhausted: "Cyclone already had Surge re-run this check once at this head",
103
104
  ci_rerun_unavailable: "the failure looked flaky and Surge could not re-run it",
104
- ci_unverified: "Cyclone could not confirm a fix locally, so it pushed nothing",
105
+ ci_unverified: "Cyclone's static checks did not cover its fix, so it pushed nothing",
105
106
  ci_unreadable: "Cyclone could not read the failing checks",
106
107
  ci_needs_actions_read: "Cyclone's GitHub App cannot read the Actions logs on this repo",
107
108
  ci_unresolved: "Cyclone's CI fix failed",
108
109
  stack_auto_fix_ci_off: "auto-fix CI is off for this stack",
109
110
  account_auto_fix_ci_off: "auto-fix CI is off for the account and this stack does not turn it on",
110
111
  };
111
- function cycloneHandoffWords(reason) {
112
+ function cycloneHandoffWords(reason, detail) {
113
+ if (reason === "failed" && detail?.trim())
114
+ return `Cyclone wrote a fix and held it, so it pushed nothing: ${detail.trim()}`;
112
115
  return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
113
116
  }
117
+ function cycloneHandoffDetailAtHead(layer) {
118
+ return sameHead(layer.cycloneHandoff?.headSha, layer.headSha) ? layer.cycloneHandoff.detail ?? null : null;
119
+ }
114
120
  function isLandPrLayer(stack, layer) {
115
121
  return layer.prNumber === stack.unit?.landPrNumber || (!!stack.unit?.branch && layer.branch === stack.unit.branch);
116
122
  }
@@ -514,11 +520,22 @@ export function mergeQueueWait(queue, nowMs) {
514
520
  const queuedFor = minutesOf(queuedMs);
515
521
  const ahead = entry.aheadInRepo;
516
522
  if (!ahead) {
523
+ const progressAt = entry.repoQueue?.progressAt ?? null;
524
+ const progressMs = progressAt ? waitedMs(progressAt, nowMs) : null;
525
+ const idleMs = queuedMs != null && progressMs != null ? Math.min(queuedMs, progressMs) : queuedMs;
526
+ const queuedAhead = entry.repoQueue?.queuedAhead ?? 0;
527
+ const turn = queuedAhead > 0
528
+ ? `behind ${queuedAhead} older queued ${queuedAhead === 1 ? "entry" : "entries"}`
529
+ : "not picked up yet";
530
+ const moved = progressMs != null && queuedMs != null && progressMs < queuedMs
531
+ ? `; the queue last moved ${minutesOf(progressMs)}m ago`
532
+ : "";
517
533
  return {
518
- text: `Queued ${queuedFor}m in the merge queue, not picked up yet${reason ? ` (last note: ${reason})` : ""}`,
519
- stalled: queuedMs != null && queuedMs >= MERGE_QUEUE_STALL_ATTENTION_MS,
534
+ text: `Queued ${queuedFor}m in the merge queue, ${turn}${moved}${reason ? ` (last note: ${reason})` : ""}`,
535
+ stalled: idleMs != null && idleMs >= MERGE_QUEUE_STALL_ATTENTION_MS,
520
536
  notPickedUp: true,
521
537
  since: entry.enqueuedAt, waitReason: reason, aheadStackId: null, aheadOther: false,
538
+ idleSince: moved ? progressAt : null,
522
539
  };
523
540
  }
524
541
  const aheadMs = ahead.claimedAt ? waitedMs(ahead.claimedAt, nowMs) : null;
@@ -548,7 +565,7 @@ function queueStalledSteps(stack, layer, wait) {
548
565
  return `#${layer.prNumber} is waiting in the merge queue behind another entry in ${stack.owner}/${stack.repo} that has made no progress since ${wait.since ?? "it was claimed"}. Nothing on ${layer.branch} needs a change. ${ahead} If it stays stuck, tell the human: the queue for this repo is held by that entry.`;
549
566
  }
550
567
  if (wait.notPickedUp) {
551
- return `#${layer.prNumber} has been queued in the merge queue for ${stack.owner}/${stack.repo} since ${wait.since ?? "it was enqueued"} and nothing is working on it or on an entry ahead of it${wait.waitReason ? `; its last note is: ${wait.waitReason}` : ""}. Nothing on ${layer.branch} needs a change: do not patch or push for this. If the note says the base branch keeps moving, the queue is waiting for a quiet moment on it and continues by itself once pushes to that branch pause. Otherwise tell the human: the merge queue is not picking this entry up; they can take the stack out of the queue and enqueue it again (mg queue rm ${stack.id}, then mg queue add ${stack.id}). Do not retarget the PR base.`;
568
+ return `#${layer.prNumber} has been queued in the merge queue for ${stack.owner}/${stack.repo} since ${wait.since ?? "it was enqueued"} and nothing is working on it or on an entry ahead of it${wait.idleSince ? `: the queue last claimed or finished an entry at ${wait.idleSince}` : ""}${wait.waitReason ? `; its last note is: ${wait.waitReason}` : ""}. Nothing on ${layer.branch} needs a change: do not patch or push for this. If the note says the base branch keeps moving, the queue is waiting for a quiet moment on it and continues by itself once pushes to that branch pause. Otherwise tell the human: the merge queue is not picking this entry up; they can take the stack out of the queue and enqueue it again (mg queue rm ${stack.id}, then mg queue add ${stack.id}). Do not retarget the PR base.`;
552
569
  }
553
570
  return `The merge queue has held #${layer.prNumber} at ${layer.headSha ?? "its head"} since ${wait.since ?? "it was claimed"} without landing or bouncing it${wait.waitReason ? `; its last note is: ${wait.waitReason}` : ", and it recorded no reason"}. If the note names a failing check or a conflict on ${layer.branch}, fix that with an ordinary push. If it waits on CI, list the head's checks (gh run list --branch ${layer.branch}) and re-run the one that never finished (gh run rerun <run-id>). If nothing moves after that, tell the human: they can take the stack out of the queue and enqueue it again (mg queue rm ${stack.id}, then mg queue add ${stack.id}). Do not retarget the PR base.`;
554
571
  }
@@ -794,7 +811,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
794
811
  branch: layer.branch,
795
812
  handoff,
796
813
  findings,
797
- steps: `Vortex ${vortexImportantFindingsHeldAtHead(layer) ? "left Important findings" : "requested changes"} on ${layer.branch} at ${layer.headSha ?? "its live head"} and Cyclone left them for a person${handoff ? ` (${cycloneHandoffWords(handoff)})` : ""}, so nothing will patch them and Auto land waits. Read the newest Vortex review and its inline comments (${findings}), verify each finding against ${layer.branch} at its live remote head, fix the real ones with the smallest patch, run the tests, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push (no force). If a finding is wrong, tell the human; mg dismiss ${stack.owner}/${stack.repo}#${layer.prNumber} --head <sha> --review <id> dismisses that review. Do not retarget the PR base.`,
814
+ steps: `Vortex ${vortexImportantFindingsHeldAtHead(layer) ? "left Important findings" : "requested changes"} on ${layer.branch} at ${layer.headSha ?? "its live head"} and Cyclone left them for a person${handoff ? ` (${cycloneHandoffWords(handoff, cycloneHandoffDetailAtHead(layer))})` : ""}, so nothing will patch them and Auto land waits. Read the newest Vortex review and its inline comments (${findings}), verify each finding against ${layer.branch} at its live remote head, fix the real ones with the smallest patch, run the tests, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push (no force). If a finding is wrong, tell the human; mg dismiss ${stack.owner}/${stack.repo}#${layer.prNumber} --head <sha> --review <id> dismisses that review. Do not retarget the PR base.`,
798
815
  };
799
816
  }
800
817
  if (reason === "ci_failed" || attention.bounceKind === "ci_failure") {
@@ -69,6 +69,7 @@ export type StackCycloneHandoff = {
69
69
  headSha: string;
70
70
  reason: string;
71
71
  at: string | null;
72
+ detail?: string | null;
72
73
  };
73
74
  export type StackAgentName = "vortex" | "cyclone";
74
75
  /**
@@ -108,6 +109,7 @@ export type StackAgentRun = {
108
109
  reviewed: number;
109
110
  total: number;
110
111
  } | null;
112
+ detail?: string | null;
111
113
  };
112
114
  export type StackAgentsBusy = {
113
115
  vortex: boolean;
@@ -455,6 +457,10 @@ export type MergeQueueAheadDto = {
455
457
  claimedAt: string | null;
456
458
  waitReason: string | null;
457
459
  };
460
+ export type MergeQueueRepoQueueDto = {
461
+ queuedAhead: number;
462
+ progressAt: string | null;
463
+ };
458
464
  export type MergeQueueEntryDto = {
459
465
  id: string;
460
466
  stackId: string;
@@ -480,4 +486,5 @@ export type MergeQueueEntryDto = {
480
486
  finishedAt: string | null;
481
487
  claimedAt?: string | null;
482
488
  aheadInRepo?: MergeQueueAheadDto | null;
489
+ repoQueue?: MergeQueueRepoQueueDto | null;
483
490
  };
package/dist/ui/banner.js CHANGED
@@ -191,6 +191,12 @@ const WHATS_NEW = {
191
191
  "0.3.51": [
192
192
  "Watch names findings held on a throttled head",
193
193
  ],
194
+ "0.3.52": [
195
+ "Sign-in errors name the agent login",
196
+ ],
197
+ "0.3.53": [
198
+ "Busy queue no longer reads as stuck",
199
+ ],
194
200
  };
195
201
  function densityFromVariant(variant) {
196
202
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.51",
3
+ "version": "0.3.53",
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",
@@ -22,7 +22,7 @@ mg skill install --claude --cursor --agents
22
22
  1. There is an already-open GitHub PR on a Vortex-monitored repo. No PR, wrong skill.
23
23
  2. Auto-patch (Cyclone) must be off for this PR so you are the only patcher. Do not take anyone's word for it: the settings API and the PR's stack are the source of truth, and step 1 of the loop verifies them before anything else runs. Cyclone is off for the PR when its adopted stack has `autoPatchOverride` `false`, or when that override is unset and the account `auto_patch_enabled` is `false`.
24
24
  3. The Bearer key and the Cyclone GitHub App install must be the same Mergestorm account. `stack_list` / `stack_status` expose `keyUserId`, `cycloneInstallUserId`, and `cycloneOwnerMatch`. Cyclone apply always uses the install account, not the key. If those identities differ, your `settings_get` mute reading is about the wrong person.
25
- 4. `whoami` works on the `mergestorm` MCP server (or `mg whoami --json` exits 0). Otherwise stop and ask for install/login.
25
+ 4. `whoami` works on the `mergestorm` MCP server (or `mg whoami --json` exits 0). Otherwise stop and ask for install, or sign in yourself: run `mg login --start`, send the person the link it prints, and after they approve it run `mg login --finish`.
26
26
 
27
27
  ## Loop (in order)
28
28