mergestorm 0.3.55 → 0.3.56

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.55 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.56 │
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 in v0.3.55: MERGESTORM_HOME moves login and stack state │
53
+ │ ▀ New in v0.3.56: Set automation per repository │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
package/dist/api.d.ts CHANGED
@@ -175,6 +175,19 @@ export type SettingsResponse = import("./automation-catalog.js").BearerSettingsV
175
175
  github_connected: boolean;
176
176
  stacks_ready?: boolean;
177
177
  };
178
+ export declare const REPO_SETTINGS_KEYS: readonly ["auto_patch_enabled", "auto_review_enabled", "auto_land_default"];
179
+ export type RepoSettingsKey = (typeof REPO_SETTINGS_KEYS)[number];
180
+ export type RepoSettings = {
181
+ owner: string;
182
+ repo: string;
183
+ } & Record<RepoSettingsKey, boolean | null>;
184
+ export type RepoSettingsPatch = Partial<Record<RepoSettingsKey, boolean | null>>;
185
+ export declare function listRepoSettings(cfg?: Config, opts?: {
186
+ timeoutMs?: number;
187
+ }): Promise<RepoSettings[]>;
188
+ export declare function patchRepoSettings(owner: string, repo: string, patch: RepoSettingsPatch, cfg?: Config, opts?: {
189
+ timeoutMs?: number;
190
+ }): Promise<RepoSettings>;
178
191
  /**
179
192
  * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
180
193
  * degrade (same contract as {@link getMe}), while 401 still surfaces.
package/dist/api.js CHANGED
@@ -172,6 +172,44 @@ export function applyPrReviewQuery(query, opts) {
172
172
  * The connected flags are read-only there and deliberately absent here.
173
173
  */
174
174
  export { SETTINGS_WRITABLE_KEYS };
175
+ export const REPO_SETTINGS_KEYS = ["auto_patch_enabled", "auto_review_enabled", "auto_land_default"];
176
+ function repoSettingsFailure(status, body, retryAfterSeconds, action) {
177
+ if (status === 429) {
178
+ return new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", {
179
+ retryAfterSeconds,
180
+ });
181
+ }
182
+ const message = body?.message;
183
+ if (typeof message === "string" && message.trim())
184
+ return new CommandError(message);
185
+ if (status === 404) {
186
+ return new CommandError("Repository settings are not available on this server yet. Deploy the API update or use the dashboard.");
187
+ }
188
+ return new CommandError(`Failed to ${action} repository settings (HTTP ${status}): ${JSON.stringify(body)}`);
189
+ }
190
+ export async function listRepoSettings(cfg, opts) {
191
+ const resolved = cfg ?? (await loadConfig());
192
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/settings/repos", {
193
+ timeoutMs: opts?.timeoutMs,
194
+ });
195
+ if (status !== 200)
196
+ throw repoSettingsFailure(status, body, retryAfterSeconds, "load");
197
+ const repos = body?.repos;
198
+ if (!Array.isArray(repos)) {
199
+ throw new CommandError(`Repository settings came back in an unexpected shape: ${JSON.stringify(body)}`);
200
+ }
201
+ return repos;
202
+ }
203
+ export async function patchRepoSettings(owner, repo, patch, cfg, opts) {
204
+ const resolved = cfg ?? (await loadConfig());
205
+ if (Object.keys(patch).length === 0) {
206
+ throw new CommandError("No repository settings to update.", 2, "usage");
207
+ }
208
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/settings/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "PATCH", json: patch, timeoutMs: opts?.timeoutMs });
209
+ if (status !== 200)
210
+ throw repoSettingsFailure(status, body, retryAfterSeconds, "update");
211
+ return body;
212
+ }
175
213
  /**
176
214
  * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
177
215
  * degrade (same contract as {@link getMe}), while 401 still surfaces.
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
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
- export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, 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 StackPublication, type StackPublicationRequest, type StackPublicationStatus, type ThreadDetail, type ThreadListItem, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, 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 RepoSettings, type RepoSettingsKey, type RepoSettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type StackPublication, type StackPublicationRequest, type StackPublicationStatus, 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, 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";
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
3
- export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, } 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, 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";
@@ -208,6 +208,10 @@ export const COMMAND_REGISTRY = [
208
208
  " [--vortex-skip-check none|neutral] [--vortex-findings failure|neutral|success]",
209
209
  " [--cyclone-fail-check failure|neutral]",
210
210
  " With flags: PATCH those settings and print the stored result",
211
+ "mergestorm settings --repos [--json] Repository-level Auto patch, Auto review and Auto land for every connected repository",
212
+ "mergestorm settings --repo <owner>/<repo> [--auto-patch on|off|inherit] [--auto-review on|off|inherit] [--auto-land on|off|inherit] [--json]",
213
+ " A repository value beats the account setting; a stack override (mergestorm stack set) beats the repository.",
214
+ " inherit clears the repository value so the account setting applies. --auto-land seeds new stacks only.",
211
215
  ],
212
216
  async run(args, ctx) {
213
217
  await cmdSettings(args, { mode: ctx.mode });
@@ -1,4 +1,4 @@
1
- import { type SettingsPatch, type SettingsResponse } from "../api.js";
1
+ import { type RepoSettings, type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } from "../api.js";
2
2
  /**
3
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.
@@ -10,13 +10,21 @@ type IgnoreBotOperation = {
10
10
  } | {
11
11
  action: "clear";
12
12
  };
13
+ export type RepoSettingsScope = {
14
+ owner: string;
15
+ repo: string;
16
+ patch: RepoSettingsPatch;
17
+ };
13
18
  export type SettingsArgs = {
14
19
  json: boolean;
15
20
  patch: SettingsPatch;
16
21
  ignoreBots?: IgnoreBotOperation[];
22
+ repo?: RepoSettingsScope;
23
+ listRepos?: boolean;
17
24
  };
25
+ export declare function formatRepoSettingsLines(repos: readonly RepoSettings[]): string[];
18
26
  /** Parse boolean and enum flags plus ordered ignore-list operations. */
19
- export declare function parseSettingsArgs(args: string[]): SettingsArgs;
27
+ export declare function parseSettingsArgs(rawArgs: string[]): SettingsArgs;
20
28
  /** Static printout includes the Config toggles plus enum and login settings. */
21
29
  export declare function formatSettingsLines(settings: SettingsResponse): string[];
22
30
  export declare function cmdSettings(args: string[], opts?: {
@@ -1,4 +1,4 @@
1
- import { getSettings, patchSettings, } from "../api.js";
1
+ import { getSettings, listRepoSettings, patchRepoSettings, patchSettings, REPO_SETTINGS_KEYS, } from "../api.js";
2
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";
@@ -10,6 +10,107 @@ import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
10
10
  * read-only on the API and deliberately have no flag here.
11
11
  */
12
12
  export const SETTINGS_FLAGS = BEARER_SETTINGS_FLAGS;
13
+ const REPO_FLAG_LABELS = {
14
+ auto_patch_enabled: "Auto patch",
15
+ auto_review_enabled: "Auto review",
16
+ auto_land_default: "Auto land (new stacks)",
17
+ };
18
+ function parseRepoSlug(raw) {
19
+ const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw ?? "");
20
+ if (!match)
21
+ throw new CommandError("--repo takes <owner>/<repo>.", 2, "usage");
22
+ return { owner: match[1], repo: match[2] };
23
+ }
24
+ function repoArg(args) {
25
+ const rest = [];
26
+ let slug;
27
+ let found = false;
28
+ let listRepos = false;
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ if (arg === "--repos")
32
+ listRepos = true;
33
+ else if (arg === "--repo") {
34
+ found = true;
35
+ slug = args[++i];
36
+ }
37
+ else if (arg.startsWith("--repo=")) {
38
+ found = true;
39
+ slug = arg.slice("--repo=".length);
40
+ }
41
+ else
42
+ rest.push(arg);
43
+ }
44
+ if (found && slug === undefined)
45
+ slug = "";
46
+ return { slug, rest, listRepos };
47
+ }
48
+ function parseRepoSettingsArgs(slug, args) {
49
+ const scope = { ...parseRepoSlug(slug), patch: {} };
50
+ let json = false;
51
+ for (let i = 0; i < args.length; i++) {
52
+ const arg = args[i];
53
+ if (arg === "--json") {
54
+ json = true;
55
+ continue;
56
+ }
57
+ const eq = arg.indexOf("=");
58
+ const flag = eq >= 0 ? arg.slice(0, eq) : arg;
59
+ const key = SETTINGS_FLAGS[flag];
60
+ if (!key) {
61
+ throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
62
+ }
63
+ if (!REPO_SETTINGS_KEYS.includes(key)) {
64
+ throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review and --auto-land can be set.`, 2, "usage");
65
+ }
66
+ const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
67
+ if (value !== "on" && value !== "off" && value !== "inherit") {
68
+ throw new CommandError(`${flag} takes on, off or inherit with --repo.`, 2, "usage");
69
+ }
70
+ scope.patch[key] = value === "inherit" ? null : value === "on";
71
+ }
72
+ return { json, patch: {}, repo: scope };
73
+ }
74
+ function repoSettingWords(value) {
75
+ return value === null ? "inherit (follows the account)" : value ? "on" : "off";
76
+ }
77
+ export function formatRepoSettingsLines(repos) {
78
+ if (repos.length === 0)
79
+ return [" No repositories are connected to this account."];
80
+ const width = Math.max(...Object.values(REPO_FLAG_LABELS).map((label) => label.length));
81
+ return repos.flatMap((entry) => [
82
+ ` ${entry.owner}/${entry.repo}`,
83
+ ...REPO_SETTINGS_KEYS.map((key) => ` ${REPO_FLAG_LABELS[key].padEnd(width + 2)}${repoSettingWords(entry[key])}`),
84
+ ]);
85
+ }
86
+ async function cmdRepoSettings(parsed, opts) {
87
+ const cfg = await loadConfig();
88
+ let repos;
89
+ const scope = parsed.repo;
90
+ if (scope && Object.keys(scope.patch).length > 0) {
91
+ repos = [await patchRepoSettings(scope.owner, scope.repo, scope.patch, cfg)];
92
+ }
93
+ else {
94
+ repos = await listRepoSettings(cfg);
95
+ if (scope) {
96
+ const wanted = `${scope.owner}/${scope.repo}`.toLowerCase();
97
+ repos = repos.filter((entry) => `${entry.owner}/${entry.repo}`.toLowerCase() === wanted);
98
+ if (repos.length === 0) {
99
+ throw new CommandError(`${scope.owner}/${scope.repo} is not connected to this account.`);
100
+ }
101
+ }
102
+ }
103
+ if (parsed.json) {
104
+ const text = JSON.stringify(scope ? repos[0] : { repos }, null, 2);
105
+ if (opts.mode === "shell") {
106
+ await present("Repository settings", text.split("\n"));
107
+ return;
108
+ }
109
+ console.log(text);
110
+ return;
111
+ }
112
+ await present("Repository settings", formatRepoSettingsLines(repos));
113
+ }
13
114
  function canonicalLogin(raw) {
14
115
  const login = raw.trim().toLowerCase().replace(/\[bot\]$/, "");
15
116
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(login)) {
@@ -18,7 +119,19 @@ function canonicalLogin(raw) {
18
119
  return login;
19
120
  }
20
121
  /** Parse boolean and enum flags plus ordered ignore-list operations. */
21
- export function parseSettingsArgs(args) {
122
+ export function parseSettingsArgs(rawArgs) {
123
+ const { slug, rest: args, listRepos } = repoArg(rawArgs);
124
+ if (slug !== undefined) {
125
+ if (listRepos)
126
+ throw new CommandError("--repos and --repo cannot be combined.", 2, "usage");
127
+ return parseRepoSettingsArgs(slug, args);
128
+ }
129
+ if (listRepos) {
130
+ const other = args.find((arg) => arg !== "--json");
131
+ if (other)
132
+ throw new CommandError(`--repos lists repository settings and takes only --json, not ${other}.`, 2, "usage");
133
+ return { json: args.includes("--json"), patch: {}, listRepos: true };
134
+ }
22
135
  let json = false;
23
136
  const ignoreBots = [];
24
137
  const patch = {};
@@ -97,7 +210,12 @@ export function formatSettingsLines(settings) {
97
210
  });
98
211
  }
99
212
  export async function cmdSettings(args, opts = {}) {
100
- const { json, patch, ignoreBots } = parseSettingsArgs(args);
213
+ const parsed = parseSettingsArgs(args);
214
+ if (parsed.repo || parsed.listRepos) {
215
+ await cmdRepoSettings(parsed, opts);
216
+ return;
217
+ }
218
+ const { json, patch, ignoreBots } = parsed;
101
219
  const hasPatch = Object.keys(patch).length > 0 || Boolean(ignoreBots?.length);
102
220
  // Bare `settings` on a TTY: the tabbed browser, opened on Config.
103
221
  if (!hasPatch && !json && canBrowse()) {
@@ -81,6 +81,8 @@ const CYCLONE_HANDOFF_WORDS = {
81
81
  failed: "Cyclone's patch failed, so it pushed nothing",
82
82
  auto_patch_off: "auto-patch is off for this stack",
83
83
  stack_auto_patch_off: "auto-patch is off for this stack",
84
+ repo_auto_patch_off: "auto-patch is off for this repository and this stack does not turn it on",
85
+ repo_setting_unavailable: "the repository's auto-patch setting could not be read, so Cyclone will not patch until it can",
84
86
  account_auto_patch_off: "auto-patch is off for the account and this stack does not turn it on",
85
87
  cyclone_not_connected: "Cyclone is not connected to this repo",
86
88
  infra_failure: "Cyclone could not start the patch: GitHub sign-in, comment load or checkout kept failing",
@@ -220,7 +222,22 @@ function vortexSkippedSteps(stack, layer, skipReason) {
220
222
  const head = layer.headSha ?? "its live head";
221
223
  const mention = `comment @mergestorm-vortex review on #${layer.prNumber}`;
222
224
  if (skipReason === "auto_review_off") {
223
- return `Vortex auto-review is off for ${stack.owner}/${stack.repo} or for this stack, so it skipped #${layer.prNumber} at ${head} and Auto land waits for a review that will not come. Do not patch or push for this. Tell the human: turning auto-review on (mg stack set ${stack.id} --auto-review on, or the account setting in the dashboard) queues a review of this head by itself. To review only this head and leave auto-review off, ${mention}.`;
225
+ const slug = `${stack.owner}/${stack.repo}`;
226
+ const where = stack.vortexReviewOff === "stack_auto_review_off"
227
+ ? "for this stack"
228
+ : stack.vortexReviewOff === "repo_auto_review_off"
229
+ ? `for the repository ${slug}`
230
+ : stack.vortexReviewOff === "account_auto_review_off"
231
+ ? "for the account"
232
+ : `for ${slug} or for this stack`;
233
+ const lead = `Vortex auto-review is off ${where}, so it skipped #${layer.prNumber} at ${head} and Auto land waits for a review that will not come. Do not patch or push for this.`;
234
+ if (stack.vortexReviewOff === "repo_auto_review_off") {
235
+ return `${lead} Tell the human: turning auto-review on for this stack only (mg stack set ${stack.id} --auto-review on) queues a review of this head by itself. Turning it on for the repository (mg settings --repo ${slug} --auto-review on) applies from the next push and queues nothing, so this head still needs a mention. To review only this head and leave auto-review off, ${mention}.`;
236
+ }
237
+ const sinceTurnedOn = stack.vortexReviewOff
238
+ ? ""
239
+ : ` If auto-review has been turned back on since this head was pushed, the skip is from before that and nothing re-queues it: ${mention}.`;
240
+ return `${lead} Tell the human: turning auto-review on (mg stack set ${stack.id} --auto-review on, or the account setting in the dashboard) queues a review of this head by itself. To review only this head and leave auto-review off, ${mention}.${sinceTurnedOn}`;
224
241
  }
225
242
  if (skipReason === "ignored_author") {
226
243
  return `Vortex skipped #${layer.prNumber} at ${head} because the PR's author is on the account's ignored authors list, so Auto land waits for a review that will not come. Do not patch or push for this. To review this head once, ${mention}. To have this author's PRs reviewed by themselves, tell the human to remove the author from the ignored authors list in the Mergestorm dashboard settings.`;
@@ -313,7 +313,8 @@ export type StackUnitDto = {
313
313
  landPr: StackLayerDto | null;
314
314
  };
315
315
  export type CycloneOwnerMatch = "same" | "different" | "none" | "lookup_failed";
316
- export type CyclonePatchOffReason = "stack_auto_patch_off" | "account_auto_patch_off" | "cyclone_not_connected";
316
+ export type CyclonePatchOffReason = "stack_auto_patch_off" | "repo_auto_patch_off" | "repo_setting_unavailable" | "account_auto_patch_off" | "cyclone_not_connected";
317
+ export type VortexReviewOffReason = "stack_auto_review_off" | "repo_auto_review_off" | "account_auto_review_off";
317
318
  export type CycloneConflictOffReason = "stack_auto_resolve_off" | "account_auto_resolve_off" | "cyclone_not_connected";
318
319
  export type CycloneCiOffReason = "stack_auto_fix_ci_off" | "account_auto_fix_ci_off" | "cyclone_not_connected";
319
320
  export type StackDto = {
@@ -364,12 +365,14 @@ export type StackDto = {
364
365
  } | null;
365
366
  /**
366
367
  * Per-stack Vortex auto-review override. `null` (or missing) follows the
367
- * account `auto_review_enabled` flag; a boolean wins in both directions.
368
+ * repository setting and then the account `auto_review_enabled` flag; a
369
+ * boolean wins in both directions.
368
370
  */
369
371
  autoReviewOverride?: boolean | null;
370
372
  /**
371
373
  * Per-stack Cyclone auto-patch override. `null` (or missing) follows the
372
- * account `auto_patch_enabled` flag; a boolean wins in both directions.
374
+ * repository setting and then the account `auto_patch_enabled` flag; a
375
+ * boolean wins in both directions.
373
376
  */
374
377
  autoPatchOverride?: boolean | null;
375
378
  autoResolveConflictsOverride?: boolean | null;
@@ -395,6 +398,11 @@ export type StackDto = {
395
398
  */
396
399
  cycloneOwnerMatch?: CycloneOwnerMatch;
397
400
  cyclonePatchOff?: CyclonePatchOffReason;
401
+ /**
402
+ * Which level turned Auto review off for this stack: its own override, the
403
+ * repository setting, or the account. Absent while Auto review is on.
404
+ */
405
+ vortexReviewOff?: VortexReviewOffReason;
398
406
  cycloneConflictOff?: CycloneConflictOffReason;
399
407
  cycloneCiOff?: CycloneCiOffReason;
400
408
  infrastructureInstallUserId?: string | null;
package/dist/ui/banner.js CHANGED
@@ -203,6 +203,9 @@ const WHATS_NEW = {
203
203
  "0.3.55": [
204
204
  "MERGESTORM_HOME moves login and stack state",
205
205
  ],
206
+ "0.3.56": [
207
+ "Set automation per repository",
208
+ ],
206
209
  };
207
210
  function densityFromVariant(variant) {
208
211
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.55",
3
+ "version": "0.3.56",
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",