mergestorm 0.3.18 → 0.3.20

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.18 │
48
+ │ ▟██▛ mergestorm v0.3.20 │
49
49
  │ ▜▙ ● msk_live_… · maelstrom │
50
50
  │ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used · Resets Sep 5, 10:53am (UTC)│
51
51
  │ │
52
52
  │ review a diff · usage for tabs · /help for all commands │
53
- │ New in v0.3.18: pr --wait fences a stale same-head pass │
53
+ │ New in v0.3.20: stack watch pair-gate attention and upstack issues │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -86,7 +86,7 @@ mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
86
86
  mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
87
87
  mergestorm chain [slug] Branch review-chain timeline (default: current branch)
88
88
  mergestorm whoami [--json] Key prefix, plan, API base, config path
89
- mergestorm skill install --claude|--cursor Copy the mergestorm-review and mergestorm-pr-loop skills into this repo
89
+ mergestorm skill install --claude|--cursor|--agents Copy the Mergestorm skills into this repo
90
90
  mergestorm thread <slug> Jobs in a review thread
91
91
  mergestorm stack create [name] New local stack layer (optional `--onto` / `--trunk` / `--extend`)
92
92
  mergestorm stack submit Push layers, open PRs (`gh`), register via adopt (optional `--extend`)
@@ -94,6 +94,7 @@ mergestorm stack list [--json] List registered stacks
94
94
  mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
95
95
  mergestorm stack restack <stack-id> Restack descendants
96
96
  mergestorm stack land <stack-id> Land / promote (into review unit when present)
97
+ mergestorm stack wait <stack-id> [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait)
97
98
  mergestorm stack reset --force Clear local authoring state (not branches/PRs)
98
99
  ```
99
100
 
package/dist/api.d.ts CHANGED
@@ -159,9 +159,9 @@ export declare function applyPrReviewQuery(query: URLSearchParams, opts: PrRevie
159
159
  */
160
160
  export { SETTINGS_WRITABLE_KEYS };
161
161
  export type { SettingsWritableKey };
162
- export type SettingsPatch = Partial<Record<SettingsWritableKey, boolean>>;
162
+ export type SettingsPatch = Partial<import("./automation-catalog.js").BearerSettingsValues>;
163
163
  /** GET and PATCH `/api/v1/settings` both answer with this shape. */
164
- export type SettingsResponse = Record<SettingsWritableKey, boolean> & {
164
+ export type SettingsResponse = import("./automation-catalog.js").BearerSettingsValues & {
165
165
  cyclone_connected: boolean;
166
166
  github_connected: boolean;
167
167
  };
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, } 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. */
@@ -382,7 +382,43 @@ export async function adoptStack(owner, repo, prNumber, cfg, policy) {
382
382
  throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
383
383
  }
384
384
  if (status !== 200) {
385
- throw new CommandError(`Failed to import stack (HTTP ${status}): ${JSON.stringify(body)}`);
385
+ const error = body && typeof body === "object" && "error" in body && typeof body.error === "string"
386
+ ? body.error
387
+ : undefined;
388
+ const message = body && typeof body === "object" && "message" in body && typeof body.message === "string"
389
+ ? body.message
390
+ : undefined;
391
+ const code = error === "auth_invalid" ||
392
+ error === "api_timeout" ||
393
+ error === "not_a_repo" ||
394
+ error === "not_found" ||
395
+ error === "missing_api_key" ||
396
+ error === "registered_parent" ||
397
+ error === "usage" ||
398
+ error === "review_quota" ||
399
+ error === "review_failed" ||
400
+ error === "review_timeout" ||
401
+ error === "rate_limited" ||
402
+ error === "cyclone_not_installed" ||
403
+ error === "pr_merged" ||
404
+ error === "pr_closed" ||
405
+ error === "not_stacked" ||
406
+ error === "draft" ||
407
+ error === "reserved_branch" ||
408
+ error === "supabase_unconfigured" ||
409
+ error === "cyclone_not_connected" ||
410
+ error === "busy" ||
411
+ error === "adopt_failed"
412
+ ? error
413
+ : undefined;
414
+ const fallbackMessage = code === "not_stacked"
415
+ ? "This pull request is not part of a stack."
416
+ : code === "supabase_unconfigured"
417
+ ? "Stacks are not configured on this server."
418
+ : undefined;
419
+ throw new CommandError(message?.trim()
420
+ ? message
421
+ : fallbackMessage ?? `Failed to import stack (HTTP ${status}): ${JSON.stringify(body)}`, 1, code);
386
422
  }
387
423
  return body;
388
424
  }
@@ -414,6 +450,10 @@ async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg)
414
450
  throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
415
451
  }
416
452
  if (status !== 200) {
453
+ const rejection = body;
454
+ if (method === "PATCH" && rejection?.error === "cyclone_not_connected") {
455
+ throw new CommandError(typeof rejection.message === "string" ? rejection.message : rejection.error, 1, "cyclone_not_connected");
456
+ }
417
457
  throw new CommandError(`${failLabel} (HTTP ${status}): ${JSON.stringify(body)}`);
418
458
  }
419
459
  return body;
@@ -26,6 +26,18 @@ export declare const BEARER_SETTINGS: readonly [{
26
26
  readonly key: "cyclone_skip_ci_enabled";
27
27
  readonly flag: "--cyclone-skip-ci";
28
28
  readonly label: "Skip CI on Cyclone commits";
29
+ }, {
30
+ readonly key: "cyclone_patch_unverified_languages";
31
+ readonly flag: "--cyclone-patch-unverified";
32
+ readonly label: "Patch languages we cannot typecheck";
33
+ }, {
34
+ readonly key: "vortex_auto_overflow_enabled";
35
+ readonly flag: "--vortex-auto-overflow";
36
+ readonly label: "Vortex auto overflow";
37
+ }, {
38
+ readonly key: "vortex_skip_all_clear_comments";
39
+ readonly flag: "--vortex-skip-all-clear";
40
+ readonly label: "Vortex skip All-clear comments";
29
41
  }, {
30
42
  readonly key: "vortex_seam_specialist_enabled";
31
43
  readonly flag: "--vortex-seam";
@@ -34,8 +46,40 @@ export declare const BEARER_SETTINGS: readonly [{
34
46
  readonly key: "auto_land_default";
35
47
  readonly flag: "--auto-land";
36
48
  readonly label: "Auto land (new stacks)";
49
+ }, {
50
+ readonly key: "ignored_bot_logins";
51
+ readonly flag: "--ignore-bot";
52
+ readonly label: "Ignored bot logins";
53
+ readonly kind: "logins";
54
+ }, {
55
+ readonly key: "vortex_bot_skip_check";
56
+ readonly flag: "--vortex-skip-check";
57
+ readonly label: "Vortex bot skip check";
58
+ readonly kind: "enum";
59
+ readonly values: readonly ["none", "neutral"];
60
+ }, {
61
+ readonly key: "vortex_findings_check";
62
+ readonly flag: "--vortex-findings";
63
+ readonly label: "Vortex findings check";
64
+ readonly kind: "enum";
65
+ readonly values: readonly ["failure", "neutral", "success"];
66
+ }, {
67
+ readonly key: "cyclone_patch_failure_check";
68
+ readonly flag: "--cyclone-fail-check";
69
+ readonly label: "Cyclone patch failure check";
70
+ readonly kind: "enum";
71
+ readonly values: readonly ["failure", "neutral"];
37
72
  }];
38
73
  export type BearerSettingsKey = (typeof BEARER_SETTINGS)[number]["key"];
39
74
  export declare const BEARER_SETTINGS_KEYS: readonly BearerSettingsKey[];
40
75
  export declare const BEARER_SETTINGS_FLAGS: Record<string, BearerSettingsKey>;
41
76
  export declare const BEARER_SETTINGS_LABELS: Record<BearerSettingsKey, string>;
77
+ /** Omitted kind means boolean for the original toggle rows. */
78
+ export type BearerSettingsValues = {
79
+ [Row in (typeof BEARER_SETTINGS)[number] as Row["key"]]: Row extends {
80
+ kind: "logins";
81
+ } ? string[] : Row extends {
82
+ values: readonly (infer Value)[];
83
+ } ? Value : boolean;
84
+ };
85
+ export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_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" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
@@ -6,9 +6,19 @@ export const BEARER_SETTINGS = [
6
6
  { key: "review_unit_land_prs_enabled", flag: "--review-unit-land", label: "Review Unit land PRs" },
7
7
  { key: "cyclone_review_unit_land_prs_enabled", flag: "--cyclone-review-unit-land", label: "Cyclone Review Unit land PRs" },
8
8
  { key: "cyclone_skip_ci_enabled", flag: "--cyclone-skip-ci", label: "Skip CI on Cyclone commits" },
9
+ { key: "cyclone_patch_unverified_languages", flag: "--cyclone-patch-unverified", label: "Patch languages we cannot typecheck" },
10
+ { key: "vortex_auto_overflow_enabled", flag: "--vortex-auto-overflow", label: "Vortex auto overflow" },
11
+ { key: "vortex_skip_all_clear_comments", flag: "--vortex-skip-all-clear", label: "Vortex skip All-clear comments" },
9
12
  { key: "vortex_seam_specialist_enabled", flag: "--vortex-seam", label: "Vortex seam specialist" },
10
13
  { key: "auto_land_default", flag: "--auto-land", label: "Auto land (new stacks)" },
14
+ { key: "ignored_bot_logins", flag: "--ignore-bot", label: "Ignored bot logins", kind: "logins" },
15
+ { key: "vortex_bot_skip_check", flag: "--vortex-skip-check", label: "Vortex bot skip check", kind: "enum", values: ["none", "neutral"] },
16
+ { key: "vortex_findings_check", flag: "--vortex-findings", label: "Vortex findings check", kind: "enum", values: ["failure", "neutral", "success"] },
17
+ { key: "cyclone_patch_failure_check", flag: "--cyclone-fail-check", label: "Cyclone patch failure check", kind: "enum", values: ["failure", "neutral"] },
11
18
  ];
12
19
  export const BEARER_SETTINGS_KEYS = BEARER_SETTINGS.map((r) => r.key);
13
20
  export const BEARER_SETTINGS_FLAGS = Object.fromEntries(BEARER_SETTINGS.map((r) => [r.flag, r.key]));
14
21
  export const BEARER_SETTINGS_LABELS = Object.fromEntries(BEARER_SETTINGS.map((r) => [r.key, r.label]));
22
+ export const BEARER_BOOLEAN_SETTINGS_KEYS = BEARER_SETTINGS
23
+ .filter((row) => !("kind" in row))
24
+ .map((row) => row.key);
@@ -1,7 +1,10 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
2
- export { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, type BearerSettingsKey, } from "../automation-catalog.js";
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, 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 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
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";
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
+ export { BOUNCE_KIND_LABELS, mergeQueueBounceLabel } from "../stack-dto.js";
9
+ export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, StackWatchError, StackWatchTimeoutError, pollStackWatch, type PollStackWatchOptions, type StackWatchCursor, type StackWatchEnvelope, } from "../stack-watch.js";
10
+ export { stackBlockers, stackBlockersSummary, sameHead, type StackBlocker } from "../stack-blockers.js";
@@ -1,7 +1,10 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
- export { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
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, 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
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";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
7
7
  export { discoverTrunk } from "../git-stack.js";
8
+ export { BOUNCE_KIND_LABELS, mergeQueueBounceLabel } from "../stack-dto.js";
9
+ export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, StackWatchError, StackWatchTimeoutError, pollStackWatch, } from "../stack-watch.js";
10
+ export { stackBlockers, stackBlockersSummary, sameHead } from "../stack-blockers.js";
@@ -27,7 +27,7 @@ export declare function buildStatusTabLines(input: StatusTabInput): string[];
27
27
  /** In-place job detail for the Jobs tab (same envelope as `status <id>`). */
28
28
  export declare function formatJobDetailLines(row: ReviewJobRow): string[];
29
29
  /** Human labels for the Config tab and the static `settings` printout. */
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_review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "vortex_seam_specialist_enabled" | "auto_land_default", string>;
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_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" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check", string>;
31
31
  /** Config rows in a stable order: writable toggles, then connected flags. */
32
32
  export declare function buildConfigRows(settings: SettingsResponse): TabsConfigRow[];
33
33
  /**
@@ -1,5 +1,5 @@
1
- import { getMe, getReview, getSettings, listJobs, patchSettings, SETTINGS_WRITABLE_KEYS, } from "../api.js";
2
- import { BEARER_SETTINGS_LABELS } from "../automation-catalog.js";
1
+ import { getMe, getReview, getSettings, listJobs, patchSettings, } from "../api.js";
2
+ import { BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_LABELS } from "../automation-catalog.js";
3
3
  import { apiBase, keyDisplay, loadConfig, resolveApiKey } from "../config.js";
4
4
  import { CommandError } from "../errors.js";
5
5
  import { ansi } from "../ui/ansi.js";
@@ -124,7 +124,7 @@ export const SETTINGS_LABELS = BEARER_SETTINGS_LABELS;
124
124
  /** Config rows in a stable order: writable toggles, then connected flags. */
125
125
  export function buildConfigRows(settings) {
126
126
  return [
127
- ...SETTINGS_WRITABLE_KEYS.map((key) => ({
127
+ ...BEARER_BOOLEAN_SETTINGS_KEYS.map((key) => ({
128
128
  key,
129
129
  label: SETTINGS_LABELS[key],
130
130
  value: settings[key] === true,
@@ -58,7 +58,7 @@ export function buildHelpTabs() {
58
58
  "",
59
59
  rule("Single stack (from trunk)"),
60
60
  cmd("stack create --onto main", "fresh stack, not on an existing one"),
61
- cmd("stack list", "what's registered for this repo"),
61
+ cmd("stack list · wait <id>", "registered stacks · wait for attention"),
62
62
  cmd("queue [add <stack-id>]", "list entries · queue verified landing"),
63
63
  cmd("stack land <id>", "land now · skips queue verification"),
64
64
  "",
@@ -5,7 +5,7 @@ export type QueueCmdDeps = {
5
5
  canBrowse?: () => boolean;
6
6
  runLineTabsBrowser?: typeof runLineTabsBrowser;
7
7
  };
8
- export declare const QUEUE_USAGE = "usage:\n mergestorm queue [list] [--json]\n mergestorm queue add <stack-id> [--json]\n mergestorm queue rm <entry-id|stack-id> [--json]\n\n queue lists your live merge-queue entries. rm prefers an entry id; a live\n stack id is resolved to its queue entry before cancellation.";
8
+ export declare const QUEUE_USAGE = "usage:\n mergestorm queue [list] [--json]\n mergestorm queue add <stack-id> [--json]\n mergestorm queue rm <entry-id|stack-id> [--json]\n\n queue lists live and recent bounced merge-queue entries. rm prefers an entry id; a live\n stack id is resolved to its queue entry before cancellation.";
9
9
  /** Entry ids win; otherwise a live stack id resolves to its entry id. */
10
10
  export declare function resolveQueueEntryId(idOrStackId: string, entries: readonly MergeQueueEntryDto[]): string;
11
11
  export declare function buildQueueListLines(entries: readonly MergeQueueEntryDto[], fullStackId?: boolean): string[];
@@ -1,6 +1,7 @@
1
1
  import { cancelMergeQueueEntry, enqueueStack, listMergeQueueEntries, } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
+ import { mergeQueueBounceLabel } from "../stack-dto.js";
4
5
  import { ansi } from "../ui/ansi.js";
5
6
  import { runLineTabsBrowser } from "../ui/line-tabs.js";
6
7
  import { present } from "../ui/present.js";
@@ -31,7 +32,7 @@ export const QUEUE_USAGE = `usage:
31
32
  mergestorm queue add <stack-id> [--json]
32
33
  mergestorm queue rm <entry-id|stack-id> [--json]
33
34
 
34
- queue lists your live merge-queue entries. rm prefers an entry id; a live
35
+ queue lists live and recent bounced merge-queue entries. rm prefers an entry id; a live
35
36
  stack id is resolved to its queue entry before cancellation.`;
36
37
  function positionalArgs(args) {
37
38
  const unknown = args.filter((arg) => arg.startsWith("-") &&
@@ -66,17 +67,31 @@ export function buildQueueListLines(entries, fullStackId = false) {
66
67
  ];
67
68
  }
68
69
  return entries.map((entry) => {
69
- const reason = entry.waitReason ?? entry.bounceReason;
70
+ const reason = entry.bounceDetail
71
+ ? mergeQueueBounceLabel(entry)
72
+ : entry.waitReason ?? (entry.bounceReason ? mergeQueueBounceLabel(entry) : null);
73
+ const prNumber = entry.bounceDetail?.prNumber != null && entry.bounceDetail.prNumber > 0
74
+ ? entry.bounceDetail.prNumber
75
+ : entry.waitReason?.match(/#([1-9]\d*)\b/)?.[1];
76
+ const headSha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha;
77
+ const pr = prNumber != null
78
+ ? ` #${prNumber}${headSha ? `@${headSha.slice(0, 7)}` : ""}`
79
+ : "";
70
80
  const glyph = colorQueueGlyph(entry.state, queueStateGlyph(entry.state));
71
81
  const id = fullStackId ? entry.stackId : entry.stackId.slice(0, 8);
72
- const core = ` ${String(entry.position).padStart(2)} ` +
73
- `${glyph} ${entry.state.padEnd(8)} ${entry.owner}/${entry.repo} ${id}` +
74
- (reason ? ` ${reason}` : "");
82
+ const coreWithoutReason = ` ${String(entry.position).padStart(2)} ` +
83
+ `${glyph} ${entry.state.padEnd(8)} ${entry.owner}/${entry.repo} ${id}`;
84
+ const coreWithoutPr = coreWithoutReason + (reason ? ` ${reason}` : "");
85
+ const core = `${coreWithoutPr}${pr}`;
75
86
  const who = entry.enqueuedBy === "human" || entry.enqueuedBy === "agent"
76
87
  ? ` by ${entry.enqueuedBy}`
77
88
  : "";
78
89
  const withWho = `${core}${who}`;
79
- return visibleWidth(withWho) <= 75 ? withWho : core;
90
+ if (visibleWidth(withWho) <= 80)
91
+ return withWho;
92
+ if (visibleWidth(core) <= 80)
93
+ return core;
94
+ return pr ? core : coreWithoutPr;
80
95
  });
81
96
  }
82
97
  async function listQueue(asJson, deps = {}) {
@@ -166,7 +166,12 @@ export const COMMAND_REGISTRY = [
166
166
  " [--auto-review on|off] [--auto-patch on|off] [--vortex-thinking on|off]",
167
167
  " [--repo-overview on|off] [--review-unit-land on|off]",
168
168
  " [--cyclone-review-unit-land on|off] [--cyclone-skip-ci on|off]",
169
- " [--vortex-seam on|off] [--auto-land on|off] Auto land default for new stacks",
169
+ " [--cyclone-patch-unverified on|off] Patch languages we cannot typecheck",
170
+ " [--vortex-seam on|off] [--vortex-skip-all-clear on|off]",
171
+ " [--auto-land on|off] Auto land default for new stacks",
172
+ " [--ignore-bot add <login>|remove <login>|clear]",
173
+ " [--vortex-skip-check none|neutral] [--vortex-findings failure|neutral|success]",
174
+ " [--cyclone-fail-check failure|neutral]",
170
175
  " With flags: PATCH those settings and print the stored result",
171
176
  ],
172
177
  async run(args, ctx) {
@@ -175,9 +180,9 @@ export const COMMAND_REGISTRY = [
175
180
  },
176
181
  {
177
182
  name: "skill",
178
- summary: "Install mergestorm-review and mergestorm-pr-loop for Claude or Cursor",
183
+ summary: "Install Mergestorm skills for Claude, Cursor, or agents",
179
184
  usage: [
180
- "mergestorm skill install --claude|--cursor Copy the mergestorm-review and mergestorm-pr-loop skills into this repo",
185
+ "mergestorm skill install --claude|--cursor|--agents Copy the Mergestorm skills into this repo",
181
186
  " [--json]",
182
187
  ],
183
188
  async run(args) {
@@ -201,7 +206,7 @@ export const COMMAND_REGISTRY = [
201
206
  name: "queue",
202
207
  summary: "Merge queue: list, add a stack, or remove an entry",
203
208
  usage: [
204
- "mergestorm queue [list] [--json] List live merge-queue entries",
209
+ "mergestorm queue [list] [--json] List live and recent bounced merge-queue entries",
205
210
  "mergestorm queue add <stack-id> [--json] Queue a stack for verified landing",
206
211
  "mergestorm queue rm <entry-id|stack-id> [--json] Remove a live queue entry",
207
212
  ],
@@ -217,13 +222,15 @@ export const COMMAND_REGISTRY = [
217
222
  "mergestorm stack submit Push/open active stack ([--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off])",
218
223
  "mergestorm stack reset --force Clear this repo's pre-submit authoring state",
219
224
  "mergestorm stack list [--json] List registered stacks",
225
+ "mergestorm stack status <stack-id> [--json] Enriched stack status",
226
+ "mergestorm stack wait <stack-id> [--json] [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait)",
220
227
  "mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json] Per-stack policy",
221
228
  "mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] Import an open PR chain",
222
229
  "mergestorm stack restack <stack-id> Restack stack descendants",
223
230
  "mergestorm stack land <stack-id> Land bottom PR (or promote into unit)",
224
231
  ],
225
- async run(args) {
226
- await cmdStack(args);
232
+ async run(args, ctx) {
233
+ await cmdStack(args, { signal: ctx.signal });
227
234
  },
228
235
  },
229
236
  {
@@ -271,7 +271,6 @@ function shaMatches(actual, expected) {
271
271
  /** Poll the DB-only PR review endpoint until the requested pass is resting. */
272
272
  export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {}) {
273
273
  const timeoutMs = opts.timeoutMs ?? REVIEW_POLL_DEFAULT_TIMEOUT_MS;
274
- const intervalMs = opts.intervalMs ?? REVIEW_POLL_INTERVAL_MS;
275
274
  const now = opts.now ?? Date.now;
276
275
  const wait = opts.sleep ?? sleep;
277
276
  const startedAt = now();
@@ -296,17 +295,13 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
296
295
  if (waitMs > 0)
297
296
  await wait(waitMs, opts.signal);
298
297
  };
299
- const sleepBeforePoll = async () => {
300
- const waitMs = Math.min(stretchedPollIntervalMs(now() - startedAt, intervalMs), Math.max(0, deadline - now()));
301
- if (waitMs > 0)
302
- await wait(waitMs, opts.signal);
303
- };
304
298
  while (now() < deadline) {
299
+ query.set("wait", String(Math.min(45, Math.max(1, Math.ceil((deadline - now()) / 1000)))));
305
300
  let poll;
306
301
  try {
307
302
  poll = await apiFetch(cfg, `/api/v1/stacks/pr-review?${query.toString()}`, {
308
303
  signal: opts.signal,
309
- timeoutMs: Math.max(1, Math.min(30_000, deadline - now())),
304
+ timeoutMs: Number(query.get("wait")) * 1000 + 5_000,
310
305
  });
311
306
  }
312
307
  catch (err) {
@@ -331,7 +326,6 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
331
326
  if (poll.status === 404) {
332
327
  transientFailures = 0;
333
328
  opts.onTick?.("progress");
334
- await sleepBeforePoll();
335
329
  continue;
336
330
  }
337
331
  if (poll.status !== 200) {
@@ -358,7 +352,6 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
358
352
  (!isPositiveInteger(envelope.pass) || (opts.pass !== undefined && envelope.pass !== opts.pass) ||
359
353
  (opts.afterPass !== undefined && envelope.pass <= opts.afterPass))) {
360
354
  opts.onTick?.("progress");
361
- await sleepBeforePoll();
362
355
  continue;
363
356
  }
364
357
  lastEnvelope = envelope;
@@ -370,7 +363,6 @@ export async function pollPrVortexReview(cfg, owner, repo, prNumber, opts = {})
370
363
  if (resting && matches && !stale)
371
364
  return lastEnvelope;
372
365
  opts.onTick?.("progress");
373
- await sleepBeforePoll();
374
366
  }
375
367
  throw new PrReviewPollTimeoutError(owner, repo, prNumber, lastEnvelope);
376
368
  }
@@ -1,17 +1,25 @@
1
1
  import { type SettingsPatch, type SettingsResponse } from "../api.js";
2
2
  /**
3
- * CLI flag → Bearer settings key. on|off only. The connected flags are
3
+ * CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
4
4
  * read-only on the API and deliberately have no flag here.
5
5
  */
6
- export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_review_unit_land_prs_enabled" | "cyclone_skip_ci_enabled" | "vortex_seam_specialist_enabled" | "auto_land_default">;
6
+ export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "review_unit_land_prs_enabled" | "cyclone_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" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
7
+ type IgnoreBotOperation = {
8
+ action: "add" | "remove";
9
+ login: string;
10
+ } | {
11
+ action: "clear";
12
+ };
7
13
  export type SettingsArgs = {
8
14
  json: boolean;
9
15
  patch: SettingsPatch;
16
+ ignoreBots?: IgnoreBotOperation[];
10
17
  };
11
- /** Parse `settings` argv. Accepts `--flag on|off` and `--flag=on|off`. */
18
+ /** Parse boolean and enum flags plus ordered ignore-list operations. */
12
19
  export declare function parseSettingsArgs(args: string[]): SettingsArgs;
13
- /** Static printout of the GET/PATCH response shape (rows match the Config tab). */
20
+ /** Static printout includes the Config toggles plus enum and login settings. */
14
21
  export declare function formatSettingsLines(settings: SettingsResponse): string[];
15
22
  export declare function cmdSettings(args: string[], opts?: {
16
23
  mode?: "oneshot" | "shell";
17
24
  }): Promise<void>;
25
+ export {};
@@ -1,17 +1,25 @@
1
1
  import { getSettings, patchSettings, } from "../api.js";
2
- import { BEARER_SETTINGS_FLAGS } from "../automation-catalog.js";
2
+ 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
6
  import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
7
7
  /**
8
- * CLI flag → Bearer settings key. on|off only. The connected flags are
8
+ * CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
9
9
  * read-only on the API and deliberately have no flag here.
10
10
  */
11
11
  export const SETTINGS_FLAGS = BEARER_SETTINGS_FLAGS;
12
- /** Parse `settings` argv. Accepts `--flag on|off` and `--flag=on|off`. */
12
+ function canonicalLogin(raw) {
13
+ const login = raw.trim().toLowerCase().replace(/\[bot\]$/, "");
14
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(login)) {
15
+ throw new CommandError(`Invalid bot login: ${raw}.`, 2, "usage");
16
+ }
17
+ return login;
18
+ }
19
+ /** Parse boolean and enum flags plus ordered ignore-list operations. */
13
20
  export function parseSettingsArgs(args) {
14
21
  let json = false;
22
+ const ignoreBots = [];
15
23
  const patch = {};
16
24
  for (let i = 0; i < args.length; i++) {
17
25
  const arg = args[i];
@@ -26,37 +34,91 @@ export function parseSettingsArgs(args) {
26
34
  throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
27
35
  }
28
36
  const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
37
+ const row = BEARER_SETTINGS.find((row) => row.key === key);
38
+ if ("kind" in row && row.kind === "logins") {
39
+ if (value === "clear")
40
+ ignoreBots.push({ action: "clear" });
41
+ else if ((value === "add" || value === "remove") && args[i + 1] && !args[i + 1].startsWith("--")) {
42
+ ignoreBots.push({ action: value, login: canonicalLogin(args[++i]) });
43
+ }
44
+ else {
45
+ throw new CommandError(`${flag} takes add <login>, remove <login>, or clear.`, 2, "usage");
46
+ }
47
+ continue;
48
+ }
49
+ if ("kind" in row && row.kind === "enum") {
50
+ if (value === undefined || !row.values.includes(value)) {
51
+ throw new CommandError(`${flag} takes ${row.values.join(" or ")}.`, 2, "usage");
52
+ }
53
+ Object.assign(patch, { [key]: value });
54
+ continue;
55
+ }
29
56
  if (value !== "on" && value !== "off") {
30
57
  throw new CommandError(`${flag} takes on or off.`, 2, "usage");
31
58
  }
32
- patch[key] = value === "on";
59
+ Object.assign(patch, { [key]: value === "on" });
33
60
  }
34
- return { json, patch };
61
+ return { json, patch, ...(ignoreBots.length ? { ignoreBots } : {}) };
35
62
  }
36
- /** Static printout of the GET/PATCH response shape (rows match the Config tab). */
63
+ /** Static printout includes the Config toggles plus enum and login settings. */
37
64
  export function formatSettingsLines(settings) {
38
- const rows = buildConfigRows(settings);
65
+ const rows = [
66
+ ...buildConfigRows(settings),
67
+ ...BEARER_SETTINGS.filter((row) => "kind" in row).map((row) => ({
68
+ ...row, writable: true, value: settings[row.key],
69
+ })),
70
+ ];
39
71
  const labelWidth = Math.max(...rows.map((r) => r.label.length));
40
72
  return rows.map((row) => {
41
- const value = row.writable
42
- ? row.value
43
- ? "on"
44
- : "off"
45
- : row.value
46
- ? "connected"
47
- : "not connected";
73
+ const value = "kind" in row && row.value === undefined
74
+ ? "(unavailable)"
75
+ : Array.isArray(row.value)
76
+ ? row.value.join(", ") || "(empty)"
77
+ : typeof row.value === "string"
78
+ ? row.value
79
+ : row.writable
80
+ ? row.value
81
+ ? "on"
82
+ : "off"
83
+ : row.value
84
+ ? "connected"
85
+ : "not connected";
48
86
  return ` ${row.label.padEnd(labelWidth + 2)}${value}`;
49
87
  });
50
88
  }
51
89
  export async function cmdSettings(args, opts = {}) {
52
- const { json, patch } = parseSettingsArgs(args);
53
- const hasPatch = Object.keys(patch).length > 0;
90
+ const { json, patch, ignoreBots } = parseSettingsArgs(args);
91
+ const hasPatch = Object.keys(patch).length > 0 || Boolean(ignoreBots?.length);
54
92
  // Bare `settings` on a TTY: the tabbed browser, opened on Config.
55
93
  if (!hasPatch && !json && canBrowse()) {
56
94
  await openTabsBrowser("config");
57
95
  return;
58
96
  }
59
97
  const cfg = await loadConfig();
98
+ if (ignoreBots?.length) {
99
+ // A leading clear needs no read. Otherwise fetch before writing the full list.
100
+ const current = ignoreBots[0].action === "clear" ? null : await getSettings(cfg);
101
+ if (ignoreBots[0].action !== "clear" && !current) {
102
+ throw new CommandError("Settings are not available; cannot update ignored bot logins.");
103
+ }
104
+ let logins = [...new Set((current?.ignored_bot_logins ?? []).flatMap((raw) => {
105
+ try {
106
+ return [canonicalLogin(raw)];
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ }))];
112
+ for (const op of ignoreBots) {
113
+ if (op.action === "clear")
114
+ logins = [];
115
+ else if (op.action === "remove")
116
+ logins = logins.filter((login) => login !== op.login);
117
+ else if (!logins.includes(op.login))
118
+ logins.push(op.login);
119
+ }
120
+ patch.ignored_bot_logins = logins;
121
+ }
60
122
  let settings;
61
123
  if (hasPatch) {
62
124
  settings = await patchSettings(patch, cfg);
@@ -1,9 +1,10 @@
1
1
  export declare const SKILL_NAME = "mergestorm-review";
2
2
  export declare const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
3
- export declare const SKILL_NAMES: readonly ["mergestorm-review", "mergestorm-pr-loop"];
3
+ export declare const BOUNCE_WATCH_SKILL_NAME = "mergestorm-bounce-watch";
4
+ export declare const SKILL_NAMES: readonly ["mergestorm-review", "mergestorm-pr-loop", "mergestorm-bounce-watch"];
4
5
  export type SkillName = (typeof SKILL_NAMES)[number];
5
- export declare const SKILL_USAGE = "usage:\n mergestorm skill install --claude|--cursor [--json]\n\n Copies mergestorm-review and mergestorm-pr-loop into this repo (git toplevel, else cwd):\n --claude .claude/skills/<name>/SKILL.md\n --cursor .cursor/skills/<name>/SKILL.md\n Pass both flags to write both trees. Overwrites existing copies.";
6
- export type SkillTarget = "claude" | "cursor";
6
+ export declare const SKILL_USAGE: string;
7
+ export type SkillTarget = "claude" | "cursor" | "agents";
7
8
  export type SkillInstallRequest = {
8
9
  help: boolean;
9
10
  json: boolean;