mergestorm 0.3.38 → 0.3.40

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.38 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.40 │
49
49
  │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
50
  │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
51
  │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
@@ -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 Cyclone on your behalf, after checking your GitHub account has write access, and assigns you. Without either, submit stops before pushing and links the Cyclone install page.
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.
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
@@ -1,7 +1,8 @@
1
1
  import { type Config } from "./config.js";
2
2
  import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, type BearerSettingsKey as SettingsWritableKey } from "./automation-catalog.js";
3
+ import { CommandError } from "./errors.js";
3
4
  import type { MergeQueueEntryDto, StackDto } from "./stack-dto.js";
4
- export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackTempestStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
5
+ export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
5
6
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
6
7
  export declare const DEFAULT_API_TIMEOUT_MS = 30000;
7
8
  /** Message prefix of the CommandError thrown when a request exceeds its timeout. */
@@ -172,6 +173,7 @@ export type SettingsPatch = Partial<import("./automation-catalog.js").BearerSett
172
173
  export type SettingsResponse = import("./automation-catalog.js").BearerSettingsValues & {
173
174
  cyclone_connected: boolean;
174
175
  github_connected: boolean;
176
+ stacks_ready?: boolean;
175
177
  };
176
178
  /**
177
179
  * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
@@ -228,6 +230,9 @@ export type StackPolicyPatch = {
228
230
  export declare function stackPolicyBody(policy: StackPolicyPatch | undefined): StackPolicyPatch;
229
231
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
230
232
  export declare function adoptStack(owner: string, repo: string, prNumber: number, cfg?: Config, policy?: StackPolicyPatch): Promise<unknown>;
233
+ export declare const SURGE_REFUSAL_FALLBACK = "Mergestorm Surge is not installed or not ready on this repository. Install it from Agents.";
234
+ export declare function surgeRefusal(body: unknown): CommandError | null;
235
+ export declare const GITHUB_UNREADABLE_MESSAGE = "GitHub could not be read just now; try again.";
231
236
  /** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
232
237
  export declare function ensureUpperPark(stackId: string, cfg?: Config): Promise<{
233
238
  freezeBranch: string;
package/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { apiBase, loadConfig, resolveApiKey } from "./config.js";
2
2
  import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, } from "./automation-catalog.js";
3
- import { CommandError, REVIEW_EXIT, rateLimitedMessage, } from "./errors.js";
3
+ import { CommandError, REVIEW_EXIT, rateLimitedMessage, plainServerText, } from "./errors.js";
4
4
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
5
5
  export const DEFAULT_API_TIMEOUT_MS = 30_000;
6
6
  /** Message prefix of the CommandError thrown when a request exceeds its timeout. */
@@ -416,12 +416,53 @@ export async function adoptStack(owner, repo, prNumber, cfg, policy) {
416
416
  : code === "supabase_unconfigured"
417
417
  ? "Stacks are not configured on this server."
418
418
  : undefined;
419
+ const surge = surgeRefusal(body);
420
+ if (surge)
421
+ throw surge;
419
422
  throw new CommandError(message?.trim()
420
423
  ? message
421
424
  : fallbackMessage ?? `Failed to import stack (HTTP ${status}): ${JSON.stringify(body)}`, 1, code);
422
425
  }
423
426
  return body;
424
427
  }
428
+ const INFRASTRUCTURE_REFUSAL_CODES = [
429
+ "cyclone_not_connected",
430
+ "cyclone_not_installed",
431
+ "cyclone_permission_missing",
432
+ ];
433
+ function refusalReason(body) {
434
+ const reason = (body && typeof body === "object" ? body.reason : undefined);
435
+ return typeof reason === "string" && plainServerText(reason).trim() ? plainServerText(reason).trim() : undefined;
436
+ }
437
+ export const SURGE_REFUSAL_FALLBACK = "Mergestorm Surge is not installed or not ready on this repository. Install it from Agents.";
438
+ export function surgeRefusal(body) {
439
+ if (!body || typeof body !== "object")
440
+ return null;
441
+ const refusal = body;
442
+ const reason = refusalReason(body);
443
+ if (!reason?.startsWith("surge_"))
444
+ return null;
445
+ const error = INFRASTRUCTURE_REFUSAL_CODES.find((code) => code === refusal.error);
446
+ if (!error)
447
+ return null;
448
+ const message = serverMessage(refusal.message) ?? SURGE_REFUSAL_FALLBACK;
449
+ const installUrl = githubAppInstallUrl(refusal.installUrl) ?? "";
450
+ return new CommandError(installUrl && !message.includes(installUrl) ? `${message} (${installUrl})` : message, 1, error, { reason });
451
+ }
452
+ export const GITHUB_UNREADABLE_MESSAGE = "GitHub could not be read just now; try again.";
453
+ function serverMessage(message) {
454
+ if (typeof message !== "string")
455
+ return null;
456
+ const plain = plainServerText(message).trim();
457
+ return plain ? plain : null;
458
+ }
459
+ const GITHUB_APP_INSTALL_URL = /https:\/\/github\.com\/apps\/[^\s)]+\/installations\/new/;
460
+ function githubAppInstallUrl(value) {
461
+ if (typeof value !== "string")
462
+ return null;
463
+ const match = GITHUB_APP_INSTALL_URL.exec(plainServerText(value).trim());
464
+ return match && match.index === 0 ? match[0] : null;
465
+ }
425
466
  /** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
426
467
  export async function ensureUpperPark(stackId, cfg) {
427
468
  const body = await stackMutation(stackId, "/ensure-upper-park", "POST", {}, "Failed to ensure upper-park freeze", cfg);
@@ -445,10 +486,13 @@ export function cycloneNotInstalledMessage(owner, repo, installUrl = CYCLONE_INS
445
486
  function installUrlFrom(message) {
446
487
  if (typeof message !== "string")
447
488
  return CYCLONE_INSTALL_URL;
448
- return message.match(/https:\/\/github\.com\/apps\/[^\s)]+\/installations\/new/)?.[0] ?? CYCLONE_INSTALL_URL;
489
+ return message.match(GITHUB_APP_INSTALL_URL)?.[0] ?? CYCLONE_INSTALL_URL;
449
490
  }
450
491
  function stackPullFailure(status, body, owner, repo, failLabel) {
451
492
  const rejection = (body && typeof body === "object" ? body : {});
493
+ const surge = surgeRefusal(body);
494
+ if (surge)
495
+ return surge;
452
496
  const text = `${String(rejection.error ?? "")} ${String(rejection.message ?? "")}`;
453
497
  if (rejection.error === "cyclone_not_installed" ||
454
498
  /not installed|installation[^\n]*not found/i.test(text)) {
@@ -500,6 +544,9 @@ async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg)
500
544
  throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
501
545
  }
502
546
  if (status !== 200) {
547
+ const surge = surgeRefusal(body);
548
+ if (surge)
549
+ throw surge;
503
550
  const rejection = body;
504
551
  if (method === "PATCH" && rejection?.error === "cyclone_not_connected") {
505
552
  throw new CommandError(typeof rejection.message === "string" ? rejection.message : rejection.error, 1, "cyclone_not_connected");
@@ -554,7 +601,7 @@ export async function listMergeQueueEntries(cfg, opts) {
554
601
  }
555
602
  return entries;
556
603
  }
557
- async function queueMutation(route, failLabel, cfg) {
604
+ async function queueMutation(route, failLabel, cfg, opts = {}) {
558
605
  const resolved = cfg ?? (await loadConfig());
559
606
  const { status, body } = await apiFetch(resolved, route, {
560
607
  method: "POST",
@@ -572,18 +619,27 @@ async function queueMutation(route, failLabel, cfg) {
572
619
  }
573
620
  if (status === 409) {
574
621
  const refusal = body;
575
- if (refusal?.error === "conflict_unchanged" && typeof refusal.message === "string") {
622
+ if ((refusal?.error === "conflict_unchanged" || refusal?.error === "ci_failed") &&
623
+ typeof refusal.message === "string") {
576
624
  throw new CommandError(refusal.message);
577
625
  }
578
626
  }
627
+ if (opts.githubUnreadableRefusal &&
628
+ status === 503 &&
629
+ body?.error === "github_unreadable") {
630
+ throw new CommandError(serverMessage(body.message) ?? GITHUB_UNREADABLE_MESSAGE, 1, "github_unreadable");
631
+ }
579
632
  if (status < 200 || status >= 300) {
633
+ const surge = surgeRefusal(body);
634
+ if (surge)
635
+ throw surge;
580
636
  throw new CommandError(`${failLabel} (HTTP ${status}): ${JSON.stringify(body)}`);
581
637
  }
582
638
  return body;
583
639
  }
584
640
  /** Enqueue a stack for verified landing. */
585
641
  export async function enqueueStack(stackId, cfg) {
586
- return queueMutation(`/api/v1/stacks/${encodeURIComponent(stackId)}/enqueue`, "Failed to add stack to merge queue", cfg);
642
+ return queueMutation(`/api/v1/stacks/${encodeURIComponent(stackId)}/enqueue`, "Failed to add stack to merge queue", cfg, { githubUnreadableRefusal: true });
587
643
  }
588
644
  /** Cancel a live merge-queue entry by entry id. */
589
645
  export async function cancelMergeQueueEntry(entryId, cfg) {
@@ -2,7 +2,7 @@ export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig,
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, type BearerSettingsKey, } from "../automation-catalog.js";
3
3
  export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type ThreadDetail, type ThreadListItem, } from "../api.js";
4
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
5
- export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
5
+ export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
7
7
  export { discoverTrunk } from "../git-stack.js";
8
8
  export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel, type StackAgentsBusy } from "../stack-dto.js";
@@ -2,7 +2,7 @@ export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig,
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
3
3
  export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.js";
4
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
5
- export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
5
+ export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
7
7
  export { discoverTrunk } from "../git-stack.js";
8
8
  export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel } from "../stack-dto.js";
@@ -28,6 +28,11 @@ export declare function buildStatusTabLines(input: StatusTabInput): string[];
28
28
  export declare function formatJobDetailLines(row: ReviewJobRow): string[];
29
29
  /** Human labels for the Config tab and the static `settings` printout. */
30
30
  export declare const SETTINGS_LABELS: Record<"auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check", string>;
31
+ export declare const STACKS_READY_LABEL = "Stacks, merge queue, Auto land";
32
+ export declare const STACKS_READY_WORDS: {
33
+ readonly on: "ready";
34
+ readonly off: "not ready (install Mergestorm Surge)";
35
+ };
31
36
  /** Config rows in a stable order: writable toggles, then connected flags. */
32
37
  export declare function buildConfigRows(settings: SettingsResponse): TabsConfigRow[];
33
38
  /**
@@ -121,8 +121,14 @@ export function formatJobDetailLines(row) {
121
121
  }
122
122
  /** Human labels for the Config tab and the static `settings` printout. */
123
123
  export const SETTINGS_LABELS = BEARER_SETTINGS_LABELS;
124
+ export const STACKS_READY_LABEL = "Stacks, merge queue, Auto land";
125
+ export const STACKS_READY_WORDS = {
126
+ on: "ready",
127
+ off: "not ready (install Mergestorm Surge)",
128
+ };
124
129
  /** Config rows in a stable order: writable toggles, then connected flags. */
125
130
  export function buildConfigRows(settings) {
131
+ const stacksReadyKnown = typeof settings.stacks_ready === "boolean";
126
132
  return [
127
133
  ...BEARER_BOOLEAN_SETTINGS_KEYS.map((key) => ({
128
134
  key,
@@ -130,9 +136,20 @@ export function buildConfigRows(settings) {
130
136
  value: settings[key] === true,
131
137
  writable: true,
132
138
  })),
139
+ ...(stacksReadyKnown
140
+ ? [
141
+ {
142
+ key: "stacks_ready",
143
+ label: STACKS_READY_LABEL,
144
+ value: settings.stacks_ready === true,
145
+ writable: false,
146
+ readOnlyWords: STACKS_READY_WORDS,
147
+ },
148
+ ]
149
+ : []),
133
150
  {
134
151
  key: "cyclone_connected",
135
- label: "Cyclone",
152
+ label: stacksReadyKnown ? "Cyclone (auto patch)" : "Cyclone",
136
153
  value: settings.cyclone_connected === true,
137
154
  writable: false,
138
155
  },
@@ -84,12 +84,19 @@ export declare class ReviewPollTimeoutError extends Error {
84
84
  readonly lastRow: ReviewJobRow;
85
85
  constructor(jobId: string, lastRow: ReviewJobRow);
86
86
  }
87
+ export type PrReviewWaitTarget = {
88
+ afterSha?: string;
89
+ pass?: number;
90
+ afterPass?: number;
91
+ };
92
+ export declare function noPrReviewYetMessage(owner: string, repo: string, prNumber: number, target?: PrReviewWaitTarget): string;
87
93
  export declare class PrReviewPollTimeoutError extends Error {
88
94
  readonly owner: string;
89
95
  readonly repo: string;
90
96
  readonly prNumber: number;
91
97
  readonly lastEnvelope: PrVortexReview | null;
92
- constructor(owner: string, repo: string, prNumber: number, lastEnvelope: PrVortexReview | null);
98
+ readonly reviewSeen: boolean;
99
+ constructor(owner: string, repo: string, prNumber: number, lastEnvelope: PrVortexReview | null, target?: PrReviewWaitTarget, reviewSeen?: boolean);
93
100
  }
94
101
  export type PollReviewOptions = {
95
102
  timeoutMs?: number;
@@ -249,18 +249,31 @@ export class ReviewPollTimeoutError extends Error {
249
249
  this.lastRow = lastRow;
250
250
  }
251
251
  }
252
+ export function noPrReviewYetMessage(owner, repo, prNumber, target = {}) {
253
+ const sha = target.afterSha?.trim();
254
+ const pass = target.pass !== undefined
255
+ ? `Vortex pass ${target.pass}`
256
+ : target.afterPass !== undefined ? `Vortex pass after pass ${target.afterPass}` : "Vortex review";
257
+ return `No ${pass} exists for ${owner}/${repo}#${prNumber}${sha ? ` at ${sha.slice(0, 12)}` : ""} yet. ` +
258
+ "If the push was in the last few minutes, wait once more. If it is still missing after that, Vortex is not reviewing this head " +
259
+ "(auto-review may be off for the repo or the stack, or the push did not reach Mergestorm): " +
260
+ `comment "@mergestorm-vortex review" on the PR to run it, or review the branch locally with mg review; a new push also queues a review.`;
261
+ }
252
262
  export class PrReviewPollTimeoutError extends Error {
253
263
  owner;
254
264
  repo;
255
265
  prNumber;
256
266
  lastEnvelope;
257
- constructor(owner, repo, prNumber, lastEnvelope) {
258
- super(`Timed out waiting for PR review ${owner}/${repo}#${prNumber}`);
267
+ reviewSeen;
268
+ constructor(owner, repo, prNumber, lastEnvelope, target = {}, reviewSeen = lastEnvelope !== null) {
269
+ super(`Timed out waiting for PR review ${owner}/${repo}#${prNumber}` +
270
+ (reviewSeen ? "" : `. ${noPrReviewYetMessage(owner, repo, prNumber, target)}`));
259
271
  this.name = "PrReviewPollTimeoutError";
260
272
  this.owner = owner;
261
273
  this.repo = repo;
262
274
  this.prNumber = prNumber;
263
275
  this.lastEnvelope = lastEnvelope;
276
+ this.reviewSeen = reviewSeen;
264
277
  }
265
278
  }
266
279
  function shaMatches(actual, expected) {
@@ -286,6 +299,7 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
286
299
  applyPrReviewQuery(query, opts);
287
300
  let transientFailures = 0;
288
301
  let lastEnvelope = null;
302
+ let apiTimeoutSeen = false;
289
303
  const sleepBeforeRetry = async (retryAfterSeconds) => {
290
304
  const waitMs = Math.min(transientRetryWaitMs({
291
305
  retryAfterSeconds,
@@ -315,6 +329,7 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
315
329
  if (isCommandErrorCode(err, "api_timeout")) {
316
330
  if (opts.propagateTimeout)
317
331
  throw err;
332
+ apiTimeoutSeen = true;
318
333
  break;
319
334
  }
320
335
  if (isTransientReviewPollError(err)) {
@@ -364,7 +379,7 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
364
379
  return lastEnvelope;
365
380
  opts.onTick?.("progress");
366
381
  }
367
- throw new PrReviewPollTimeoutError(owner, repo, prNumber, lastEnvelope);
382
+ throw new PrReviewPollTimeoutError(owner, repo, prNumber, lastEnvelope, opts, lastEnvelope !== null || apiTimeoutSeen);
368
383
  }
369
384
  export async function pollReview(cfg, jobId, opts = {}) {
370
385
  const timeoutMs = opts.timeoutMs ?? REVIEW_POLL_DEFAULT_TIMEOUT_MS;
@@ -1,4 +1,4 @@
1
- import { REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus } from "./review-client.js";
1
+ import { REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, type PollReviewOptions } from "./review-client.js";
2
2
  export { REVIEW_POLL_MAX_TRANSIENT_RETRIES, isTransientReviewPollError, isTransientReviewPollStatus, };
3
3
  /** Same public specialist allowlist as the API contract and migration 117. */
4
4
  export declare const VORTEX_ROUTER_MODES: readonly ["off", "standard", "max", "manual"];
@@ -43,6 +43,7 @@ export type ReviewOptions = {
43
43
  interactive?: boolean;
44
44
  /** Test seam; production uses the two-second poll interval. */
45
45
  pollIntervalMs?: number;
46
+ pollClock?: Pick<PollReviewOptions, "sleep" | "now" | "random">;
46
47
  };
47
48
  export declare function reviewStatusHint(jobId: string): string;
48
49
  /** Append recovery hint once a job id exists (idempotent). */
@@ -357,6 +357,7 @@ export async function cmdReview(args, opts = {}) {
357
357
  row = await pollReview(cfg, jobId, {
358
358
  timeoutMs: parsed.timeoutMs,
359
359
  intervalMs: opts.pollIntervalMs,
360
+ ...opts.pollClock,
360
361
  signal: opts.signal,
361
362
  propagateTimeout: interactive,
362
363
  onTick(kind) {
@@ -3,6 +3,7 @@ import { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS } from "../automation-catalog.js
3
3
  import { loadConfig } from "../config.js";
4
4
  import { CommandError } from "../errors.js";
5
5
  import { present } from "../ui/present.js";
6
+ import { readOnlyRowWords } from "../ui/tabs.js";
6
7
  import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
7
8
  /**
8
9
  * CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
@@ -91,9 +92,7 @@ export function formatSettingsLines(settings) {
91
92
  ? row.value
92
93
  ? "on"
93
94
  : "off"
94
- : row.value
95
- ? "connected"
96
- : "not connected";
95
+ : readOnlyRowWords(row);
97
96
  return ` ${row.label.padEnd(labelWidth + 2)}${value}`;
98
97
  });
99
98
  }
@@ -1040,8 +1040,7 @@ export async function cmdStackStatus(args, deps = {}) {
1040
1040
  lines.push(` ${String(layer.position).padStart(2)} ${pr}${head} ${layer.state.padEnd(14)} ${layer.branch}` +
1041
1041
  ` CI: ${layer.ciStatus ?? "unknown"} review: ${layer.reviewStatus ?? "unknown"}` +
1042
1042
  (layer.vortexStatus ? ` Vortex: ${layer.vortexStatus}` : "") +
1043
- (layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
1044
- (layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
1043
+ (layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : ""));
1045
1044
  }
1046
1045
  if (queueWait && !attention)
1047
1046
  lines.push(` Queue: #${queueWait.prNumber} ${queueWait.text}`);
package/dist/errors.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Stable machine codes for CommandError control flow.
3
3
  * UX copy in `message` may change freely; match on `code`, never on message text.
4
4
  */
5
- export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "busy" | "adopt_failed" | "stack_attention";
5
+ export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "cyclone_permission_missing" | "busy" | "adopt_failed" | "github_unreadable" | "stack_attention";
6
6
  /** Stable one-shot exit codes for machine callers of `mergestorm review`. */
7
7
  export declare const REVIEW_EXIT: {
8
8
  readonly success: 0;
@@ -15,14 +15,17 @@ export declare const REVIEW_EXIT: {
15
15
  };
16
16
  export type CommandErrorExtras = {
17
17
  retryAfterSeconds?: number;
18
+ reason?: string;
18
19
  };
19
20
  /** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
20
21
  export declare class CommandError extends Error {
21
22
  readonly exitCode: number;
22
23
  readonly code?: CommandErrorCode;
23
24
  readonly retryAfterSeconds?: number;
25
+ readonly reason?: string;
24
26
  constructor(message: string, exitCode?: number, code?: CommandErrorCode, extras?: CommandErrorExtras);
25
27
  }
28
+ export declare function plainServerText(text: string): string;
26
29
  export declare const RATE_LIMITED_HINT = "Rate limited. Wait for a running review or mergestorm jobs.";
27
30
  export declare function rateLimitedMessage(retryAfterSeconds?: number): string;
28
31
  export declare function isCommandErrorCode(err: unknown, code: CommandErrorCode): err is CommandError;
package/dist/errors.js CHANGED
@@ -13,6 +13,7 @@ export class CommandError extends Error {
13
13
  exitCode;
14
14
  code;
15
15
  retryAfterSeconds;
16
+ reason;
16
17
  constructor(message, exitCode = 1, code, extras) {
17
18
  super(message);
18
19
  this.name = "CommandError";
@@ -21,8 +22,16 @@ export class CommandError extends Error {
21
22
  if (extras?.retryAfterSeconds !== undefined) {
22
23
  this.retryAfterSeconds = extras.retryAfterSeconds;
23
24
  }
25
+ if (extras?.reason !== undefined) {
26
+ this.reason = extras.reason;
27
+ }
24
28
  }
25
29
  }
30
+ const ANSI_SEQUENCE = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b\[[0-?]*[ -/]*[@-~]/g;
31
+ const CONTROL_CHARACTER = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
32
+ export function plainServerText(text) {
33
+ return text.replace(ANSI_SEQUENCE, "").replace(CONTROL_CHARACTER, "");
34
+ }
26
35
  export const RATE_LIMITED_HINT = "Rate limited. Wait for a running review or mergestorm jobs.";
27
36
  export function rateLimitedMessage(retryAfterSeconds) {
28
37
  if (retryAfterSeconds == null || !Number.isFinite(retryAfterSeconds)) {
@@ -6,6 +6,10 @@ export type StackBlocker = {
6
6
  blocker: string;
7
7
  bounceKind: string | null;
8
8
  };
9
+ export type StackBlockerReason = "restack_conflict" | "restack_failed" | "draft_pr" | "merge_conflict" | "ci_failed" | "vortex_failed" | "vortex_out_of_quota" | "vortex_skipped" | "vortex_findings_need_person" | "seam_failed" | "seam_findings" | "seam_not_running" | "seam_review_stuck" | "seam_rereview_not_running" | "seam_verdict_stale" | "unit_abandoned" | "land_gate" | "bounce" | "auto_land_off" | "auto_land_ci_pending" | "auto_land_members_remaining" | "auto_land_vortex_wait" | "auto_land_promote_failed" | "auto_land_ci_unreadable" | "auto_land_waited" | "queue_stalled" | "land_pr_missing";
10
+ export type ClassifiedStackBlocker = StackBlocker & {
11
+ reason: StackBlockerReason;
12
+ };
9
13
  export type StackAgentBusy = {
10
14
  prNumber: number;
11
15
  headSha: string | null;
@@ -47,20 +51,6 @@ export type StackRepairHint = {
47
51
  branch: string;
48
52
  failingCheck: string | null;
49
53
  steps: string;
50
- } | {
51
- kind: "tempest_findings";
52
- prNumber: number;
53
- headSha: string | null;
54
- branch: string;
55
- findings: string;
56
- steps: string;
57
- } | {
58
- kind: "tempest_rerun";
59
- prNumber: number;
60
- headSha: string | null;
61
- branch: string;
62
- stoppedByPerson: boolean;
63
- steps: string;
64
54
  } | {
65
55
  kind: "vortex_findings";
66
56
  prNumber: number;
@@ -120,6 +110,20 @@ export type StackRepairHint = {
120
110
  since: string | null;
121
111
  waitReason: string | null;
122
112
  steps: string;
113
+ } | {
114
+ kind: "members_remaining" | "vortex_failed";
115
+ prNumber: number;
116
+ headSha: string | null;
117
+ branch: string;
118
+ detail: string | null;
119
+ steps: string;
120
+ } | {
121
+ kind: "land_pr_unavailable";
122
+ prNumber: number;
123
+ headSha: string | null;
124
+ branch: string;
125
+ detail: string | null;
126
+ steps: string;
123
127
  } | {
124
128
  kind: "vortex_skipped";
125
129
  prNumber: number;
@@ -160,6 +164,11 @@ export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a perso
160
164
  export declare const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
161
165
  export declare const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
162
166
  export declare const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
167
+ export declare const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
168
+ export declare const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
169
+ export declare const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
170
+ export declare const VORTEX_REVIEW_FAILED = "Review failed";
171
+ export declare function vortexRetryScheduled(layer: StackLayerDto): boolean;
163
172
  export declare const AUTO_LAND_CI_PENDING_ATTENTION_MS: number;
164
173
  export declare const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS: number;
165
174
  export declare const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
@@ -192,8 +201,9 @@ export declare function restackFailedRepairSteps(branch: string, liveParent: str
192
201
  kind: string;
193
202
  detail?: string | null;
194
203
  }): string;
195
- export declare function agentsCannotClear(blocker: string): boolean;
196
- export declare function stackRepair(stack: StackDto, attention: StackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null): StackRepairHint | null;
204
+ export declare function agentsCannotClear(reason: StackBlockerReason): boolean;
205
+ export declare function stackRepair(stack: StackDto, attention: ClassifiedStackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null): StackRepairHint | null;
206
+ export declare const LAND_PR_MISSING_PREFIX = "No open land PR for ";
197
207
  /** Pair-gate attention plus live hard blocks on every other open layer. */
198
208
  export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">, nowMs?: number): {
199
209
  attention: null;