mergestorm 0.3.50 → 0.3.52

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.50 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.52 │
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.52: Sign-in errors name the agent login │
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");
@@ -63,11 +63,14 @@ function coreReviewDoneAtHead(layer) {
63
63
  return review?.status === "done" && sameHead(review.head_sha, layer.headSha);
64
64
  }
65
65
  export const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a person";
66
- function vortexChangesRequestedAtHead(layer) {
66
+ function vortexFindingsRestAtHead(layer) {
67
67
  const review = layer.vortexReview;
68
68
  if (review && !(review.status === "done" && sameHead(review.head_sha, layer.headSha)))
69
69
  return false;
70
- return layer.vortexStatus === "findings" && layer.reviewStatus === "changes_requested";
70
+ return layer.vortexStatus === "findings" || layer.vortexStatus === "throttled";
71
+ }
72
+ function vortexChangesRequestedAtHead(layer) {
73
+ return vortexFindingsRestAtHead(layer) && layer.reviewStatus === "changes_requested";
71
74
  }
72
75
  function cycloneHandoffAtHead(stack, layer) {
73
76
  if (sameHead(layer.cycloneHandoff?.headSha, layer.headSha))
@@ -75,6 +78,7 @@ function cycloneHandoffAtHead(stack, layer) {
75
78
  return stack.cyclonePatchOff ?? (stack.autoPatchOverride === false ? "auto_patch_off" : null);
76
79
  }
77
80
  const CYCLONE_HANDOFF_WORDS = {
81
+ failed: "Cyclone's patch failed, so it pushed nothing",
78
82
  auto_patch_off: "auto-patch is off for this stack",
79
83
  stack_auto_patch_off: "auto-patch is off for this stack",
80
84
  account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
@@ -105,9 +109,14 @@ const CYCLONE_HANDOFF_WORDS = {
105
109
  stack_auto_fix_ci_off: "auto-fix CI is off for this stack",
106
110
  account_auto_fix_ci_off: "auto-fix CI is off for the account and this stack does not turn it on",
107
111
  };
108
- 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()}`;
109
115
  return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
110
116
  }
117
+ function cycloneHandoffDetailAtHead(layer) {
118
+ return sameHead(layer.cycloneHandoff?.headSha, layer.headSha) ? layer.cycloneHandoff.detail ?? null : null;
119
+ }
111
120
  function isLandPrLayer(stack, layer) {
112
121
  return layer.prNumber === stack.unit?.landPrNumber || (!!stack.unit?.branch && layer.branch === stack.unit.branch);
113
122
  }
@@ -149,9 +158,24 @@ function ciHandoffNote(stack, layer) {
149
158
  }
150
159
  return null;
151
160
  }
161
+ const CYCLONE_HELD_FINDINGS_REASONS = [
162
+ "failed",
163
+ "left_for_human",
164
+ "infra_failure",
165
+ "pr_state_unavailable",
166
+ "vortex_patch_cap",
167
+ "patch_job_lost",
168
+ ];
169
+ function vortexImportantFindingsHeldAtHead(layer) {
170
+ return vortexFindingsRestAtHead(layer) && layer.reviewStatus === "reviewed" &&
171
+ sameHead(layer.cycloneHandoff?.headSha, layer.headSha) &&
172
+ CYCLONE_HELD_FINDINGS_REASONS.includes(layer.cycloneHandoff.reason);
173
+ }
152
174
  function vortexFindingsLeftForPerson(stack, layer) {
153
- return vortexChangesRequestedAtHead(layer) && !layerAgentsBusy(layer).cyclone &&
154
- cycloneHandoffAtHead(stack, layer) !== null;
175
+ if (layerAgentsBusy(layer).cyclone)
176
+ return false;
177
+ return vortexImportantFindingsHeldAtHead(layer) ||
178
+ (vortexChangesRequestedAtHead(layer) && cycloneHandoffAtHead(stack, layer) !== null);
155
179
  }
156
180
  function quotaSkippedReview(layer) {
157
181
  const review = layer.vortexReview;
@@ -776,7 +800,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
776
800
  branch: layer.branch,
777
801
  handoff,
778
802
  findings,
779
- steps: `Vortex 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.`,
803
+ 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.`,
780
804
  };
781
805
  }
782
806
  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;
package/dist/ui/banner.js CHANGED
@@ -188,6 +188,12 @@ const WHATS_NEW = {
188
188
  "0.3.50": [
189
189
  "Agent sign-in: login --start, --finish",
190
190
  ],
191
+ "0.3.51": [
192
+ "Watch names findings held on a throttled head",
193
+ ],
194
+ "0.3.52": [
195
+ "Sign-in errors name the agent login",
196
+ ],
191
197
  };
192
198
  function densityFromVariant(variant) {
193
199
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.50",
3
+ "version": "0.3.52",
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