mergestorm 0.3.15 → 0.3.16

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.15 │
48
+ │ ▟██▛ mergestorm v0.3.16 │
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.15: PR wait + settings + both skills in the npm tarball │
53
+ │ New in v0.3.16: verify findings, small diff, public dismiss on skip │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -99,7 +99,7 @@ mergestorm stack reset --force Clear local authoring state (not branches/PRs)
99
99
 
100
100
  `mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack create`. Happy path: **create → commit → submit → restack → land**. On review-unit stacks, `stack land` **promotes** the tip into the unit (same gates as the dashboard); otherwise it lands the bottom open PR. `stack adopt` is for importing a chain that already exists on GitHub (legacy / Graphite). Restack/land use the same login key (`/api/v1/stacks`). Unattended landing is `mg queue add`.
101
101
 
102
- `stack create` checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and records `{ branch, parentBranch }` in CLI-managed state under **`~/.mergestorm/stacks/<repo-id>/stack.json`**. The CLI updates this automatically; never edit it. Linked worktrees share state, while independent clones remain isolated. Creating onto trunk with a non-empty active stack starts a new one. `--trunk` only sets trunk metadata — it does **not** change the parent; use `--onto main` (etc.) for a fresh stack from trunk.
102
+ `stack create` checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and records `{ branch, parentBranch }` in CLI-managed state under **`~/.mergestorm/stacks/<repo-id>/stack.json`**. The CLI updates this automatically; never edit it. Each worktree has its own authoring state; independent clones stay isolated too. Creating onto trunk with a non-empty active stack starts a new one. `--trunk` only sets trunk metadata — it does **not** change the parent; use `--onto main` (etc.) for a fresh stack from trunk.
103
103
 
104
104
  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`.
105
105
 
@@ -119,7 +119,7 @@ Removes `~/.mergestorm/config.json` so the stored key is no longer used.
119
119
 
120
120
  ### `review [base] [head]`
121
121
 
122
- Collects `git diff base...head` (default: discovered trunk — `origin/HEAD`, `main`, or `master`), attaches the changed file contents (up to 40 files, skipping files > 400 KB), submits the job, and polls until it finishes (~8 min max — Core plus a Max fleet can exceed 3 minutes). The thread slug defaults to the current branch (`local/<branch>`); pass `--thread` to override so follow-ups chain on a chosen slug. The CLI does not scan `.mergestorm/context/`.
122
+ Collects `git diff base...head` (default: discovered trunk — `origin/HEAD`, `main`, or `master`), attaches the changed file contents (up to 40 files, skipping files > 400 KB), submits the job, and polls until it finishes (~8 min max — Core plus a Max fleet can exceed 3 minutes). The thread slug defaults to the current branch (`local/<branch>`); pass `--thread` to override so follow-ups chain on a chosen slug. The CLI still does not scan a repo folder for review context. GitHub Vortex will read house-rule files in a later PR (`AGENTS.md`, `CLAUDE.md`, alwaysApply Cursor rules). That ingest is not shipping yet.
123
123
 
124
124
  ```bash
125
125
  mergestorm review # discovered trunk (origin/HEAD, main, or master)...HEAD
package/dist/api.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Config } from "./config.js";
2
+ import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, type BearerSettingsKey as SettingsWritableKey } from "./automation-catalog.js";
2
3
  import type { MergeQueueEntryDto, StackDto } from "./stack-dto.js";
3
4
  export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackTempestStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
4
5
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
@@ -132,8 +133,8 @@ export type PrVortexReview = {
132
133
  * Bearer-writable settings keys, mirroring the `/api/v1/settings` allowlist.
133
134
  * The connected flags are read-only there and deliberately absent here.
134
135
  */
135
- export declare const SETTINGS_WRITABLE_KEYS: readonly ["auto_review_enabled", "auto_patch_enabled", "vortex_show_thinking_traces", "repo_overview_enabled", "review_unit_land_prs_enabled", "cyclone_review_unit_land_prs_enabled", "vortex_seam_specialist_enabled"];
136
- export type SettingsWritableKey = (typeof SETTINGS_WRITABLE_KEYS)[number];
136
+ export { SETTINGS_WRITABLE_KEYS };
137
+ export type { SettingsWritableKey };
137
138
  export type SettingsPatch = Partial<Record<SettingsWritableKey, boolean>>;
138
139
  /** GET and PATCH `/api/v1/settings` both answer with this shape. */
139
140
  export type SettingsResponse = Record<SettingsWritableKey, boolean> & {
@@ -180,13 +181,32 @@ export declare function listStacks(cfg?: Config, opts?: {
180
181
  export declare function getEnrichedStack(stackId: string, cfg?: Config, opts?: {
181
182
  timeoutMs?: number;
182
183
  }): Promise<StackDto | null>;
184
+ /**
185
+ * Per-stack automation policy. Auto land is a boolean; the review / patch
186
+ * overrides are tri-state (`null` clears back to the account flag). Absent
187
+ * keys are left untouched.
188
+ */
189
+ export type StackPolicyPatch = {
190
+ autoEnqueueWhenReady?: boolean;
191
+ autoReviewOverride?: boolean | null;
192
+ autoPatchOverride?: boolean | null;
193
+ };
194
+ /** Drop undefined keys so the wire body only carries what was requested. */
195
+ export declare function stackPolicyBody(policy: StackPolicyPatch | undefined): StackPolicyPatch;
183
196
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
184
- export declare function adoptStack(owner: string, repo: string, prNumber: number, cfg?: Config): Promise<unknown>;
197
+ export declare function adoptStack(owner: string, repo: string, prNumber: number, cfg?: Config, policy?: StackPolicyPatch): Promise<unknown>;
185
198
  /** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
186
199
  export declare function ensureUpperPark(stackId: string, cfg?: Config): Promise<{
187
200
  freezeBranch: string;
188
201
  created: boolean;
189
202
  }>;
203
+ /** Arm or disarm Auto land on one owned stack. */
204
+ export declare function setStackAutoLand(stackId: string, enabled: boolean, cfg?: Config): Promise<unknown>;
205
+ /**
206
+ * Patch any subset of one owned stack's automation policy
207
+ * (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
208
+ */
209
+ export declare function setStackPolicy(stackId: string, policy: StackPolicyPatch, cfg?: Config): Promise<unknown>;
190
210
  /** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
191
211
  export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
192
212
  /** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
package/dist/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { apiBase, loadConfig, resolveApiKey } from "./config.js";
2
+ import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, } from "./automation-catalog.js";
2
3
  import { CommandError, REVIEW_EXIT, rateLimitedMessage } from "./errors.js";
3
4
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
4
5
  export const DEFAULT_API_TIMEOUT_MS = 30_000;
@@ -158,15 +159,7 @@ export async function devicePost(base, route, json, opts = {}) {
158
159
  * Bearer-writable settings keys, mirroring the `/api/v1/settings` allowlist.
159
160
  * The connected flags are read-only there and deliberately absent here.
160
161
  */
161
- export const SETTINGS_WRITABLE_KEYS = [
162
- "auto_review_enabled",
163
- "auto_patch_enabled",
164
- "vortex_show_thinking_traces",
165
- "repo_overview_enabled",
166
- "review_unit_land_prs_enabled",
167
- "cyclone_review_unit_land_prs_enabled",
168
- "vortex_seam_specialist_enabled",
169
- ];
162
+ export { SETTINGS_WRITABLE_KEYS };
170
163
  /**
171
164
  * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
172
165
  * degrade (same contract as {@link getMe}), while 401 still surfaces.
@@ -343,12 +336,31 @@ export async function getEnrichedStack(stackId, cfg, opts) {
343
336
  }
344
337
  return stacks.find((stack) => stack?.id === id) ?? null;
345
338
  }
339
+ /** Drop undefined keys so the wire body only carries what was requested. */
340
+ export function stackPolicyBody(policy) {
341
+ const body = {};
342
+ if (typeof policy?.autoEnqueueWhenReady === "boolean") {
343
+ body.autoEnqueueWhenReady = policy.autoEnqueueWhenReady;
344
+ }
345
+ if (policy?.autoReviewOverride !== undefined) {
346
+ body.autoReviewOverride = policy.autoReviewOverride;
347
+ }
348
+ if (policy?.autoPatchOverride !== undefined) {
349
+ body.autoPatchOverride = policy.autoPatchOverride;
350
+ }
351
+ return body;
352
+ }
346
353
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
347
- export async function adoptStack(owner, repo, prNumber, cfg) {
354
+ export async function adoptStack(owner, repo, prNumber, cfg, policy) {
348
355
  const resolved = cfg ?? (await loadConfig());
349
356
  const { status, body } = await apiFetch(resolved, "/api/v1/stacks/adopt", {
350
357
  method: "POST",
351
- json: { owner, repo, prNumber },
358
+ json: {
359
+ owner,
360
+ repo,
361
+ prNumber,
362
+ ...stackPolicyBody(policy),
363
+ },
352
364
  });
353
365
  if (status === 404) {
354
366
  throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
@@ -390,6 +402,21 @@ async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg)
390
402
  }
391
403
  return body;
392
404
  }
405
+ /** Arm or disarm Auto land on one owned stack. */
406
+ export async function setStackAutoLand(stackId, enabled, cfg) {
407
+ return setStackPolicy(stackId, { autoEnqueueWhenReady: enabled }, cfg);
408
+ }
409
+ /**
410
+ * Patch any subset of one owned stack's automation policy
411
+ * (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
412
+ */
413
+ export async function setStackPolicy(stackId, policy, cfg) {
414
+ const json = stackPolicyBody(policy);
415
+ if (Object.keys(json).length === 0) {
416
+ throw new CommandError("setStackPolicy needs at least one policy key");
417
+ }
418
+ return stackMutation(stackId, "", "PATCH", json, "Failed to set stack policy", cfg);
419
+ }
393
420
  /** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
394
421
  export async function restackStack(stackId, cfg) {
395
422
  return stackMutation(stackId, "/restack", "POST", {}, "Failed to restack", cfg);
@@ -0,0 +1,37 @@
1
+ export declare const BEARER_SETTINGS: readonly [{
2
+ readonly key: "auto_review_enabled";
3
+ readonly flag: "--auto-review";
4
+ readonly label: "Auto review";
5
+ }, {
6
+ readonly key: "auto_patch_enabled";
7
+ readonly flag: "--auto-patch";
8
+ readonly label: "Auto patch";
9
+ }, {
10
+ readonly key: "vortex_show_thinking_traces";
11
+ readonly flag: "--vortex-thinking";
12
+ readonly label: "Vortex thinking traces";
13
+ }, {
14
+ readonly key: "repo_overview_enabled";
15
+ readonly flag: "--repo-overview";
16
+ readonly label: "Repo overview";
17
+ }, {
18
+ readonly key: "review_unit_land_prs_enabled";
19
+ readonly flag: "--review-unit-land";
20
+ readonly label: "Review Unit land PRs";
21
+ }, {
22
+ readonly key: "cyclone_review_unit_land_prs_enabled";
23
+ readonly flag: "--cyclone-review-unit-land";
24
+ readonly label: "Cyclone Review Unit land PRs";
25
+ }, {
26
+ readonly key: "vortex_seam_specialist_enabled";
27
+ readonly flag: "--vortex-seam";
28
+ readonly label: "Vortex seam specialist";
29
+ }, {
30
+ readonly key: "auto_land_default";
31
+ readonly flag: "--auto-land";
32
+ readonly label: "Auto land (new stacks)";
33
+ }];
34
+ export type BearerSettingsKey = (typeof BEARER_SETTINGS)[number]["key"];
35
+ export declare const BEARER_SETTINGS_KEYS: readonly BearerSettingsKey[];
36
+ export declare const BEARER_SETTINGS_FLAGS: Record<string, BearerSettingsKey>;
37
+ export declare const BEARER_SETTINGS_LABELS: Record<BearerSettingsKey, string>;
@@ -0,0 +1,13 @@
1
+ export const BEARER_SETTINGS = [
2
+ { key: "auto_review_enabled", flag: "--auto-review", label: "Auto review" },
3
+ { key: "auto_patch_enabled", flag: "--auto-patch", label: "Auto patch" },
4
+ { key: "vortex_show_thinking_traces", flag: "--vortex-thinking", label: "Vortex thinking traces" },
5
+ { key: "repo_overview_enabled", flag: "--repo-overview", label: "Repo overview" },
6
+ { key: "review_unit_land_prs_enabled", flag: "--review-unit-land", label: "Review Unit land PRs" },
7
+ { key: "cyclone_review_unit_land_prs_enabled", flag: "--cyclone-review-unit-land", label: "Cyclone Review Unit land PRs" },
8
+ { key: "vortex_seam_specialist_enabled", flag: "--vortex-seam", label: "Vortex seam specialist" },
9
+ { key: "auto_land_default", flag: "--auto-land", label: "Auto land (new stacks)" },
10
+ ];
11
+ export const BEARER_SETTINGS_KEYS = BEARER_SETTINGS.map((r) => r.key);
12
+ export const BEARER_SETTINGS_FLAGS = Object.fromEntries(BEARER_SETTINGS.map((r) => [r.flag, r.key]));
13
+ export const BEARER_SETTINGS_LABELS = Object.fromEntries(BEARER_SETTINGS.map((r) => [r.key, r.label]));
@@ -1,5 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
2
- 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, 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 ThreadDetail, type ThreadListItem, } from "../api.js";
2
+ export { BEARER_SETTINGS, 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, 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";
3
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
4
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, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
5
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
@@ -1,5 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
- 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, } from "../api.js";
2
+ export { BEARER_SETTINGS, 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, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.js";
3
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
4
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, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
5
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
@@ -1,4 +1,4 @@
1
- import { type JobListItem, type SettingsResponse, type SettingsWritableKey } from "../api.js";
1
+ import { type JobListItem, type SettingsResponse } from "../api.js";
2
2
  import { type ReviewJobRow } from "../ui/job-envelope.js";
3
3
  import { type TabId, type TabsConfigRow } from "../ui/tabs.js";
4
4
  import { type UsagePanelJob } from "../ui/usage.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<SettingsWritableKey, 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" | "vortex_seam_specialist_enabled" | "auto_land_default", 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,4 +1,5 @@
1
1
  import { getMe, getReview, getSettings, listJobs, patchSettings, SETTINGS_WRITABLE_KEYS, } from "../api.js";
2
+ import { BEARER_SETTINGS_LABELS } from "../automation-catalog.js";
2
3
  import { apiBase, keyDisplay, loadConfig, resolveApiKey } from "../config.js";
3
4
  import { CommandError } from "../errors.js";
4
5
  import { ansi } from "../ui/ansi.js";
@@ -117,15 +118,7 @@ export function formatJobDetailLines(row) {
117
118
  return lines;
118
119
  }
119
120
  /** Human labels for the Config tab and the static `settings` printout. */
120
- export const SETTINGS_LABELS = {
121
- auto_review_enabled: "Auto review",
122
- auto_patch_enabled: "Auto patch",
123
- vortex_show_thinking_traces: "Vortex thinking traces",
124
- repo_overview_enabled: "Repo overview",
125
- review_unit_land_prs_enabled: "Review Unit land PRs",
126
- cyclone_review_unit_land_prs_enabled: "Cyclone Review Unit land PRs",
127
- vortex_seam_specialist_enabled: "Vortex seam specialist",
128
- };
121
+ export const SETTINGS_LABELS = BEARER_SETTINGS_LABELS;
129
122
  /** Config rows in a stable order: writable toggles, then connected flags. */
130
123
  export function buildConfigRows(settings) {
131
124
  return [
@@ -164,6 +164,7 @@ export const COMMAND_REGISTRY = [
164
164
  " [--auto-review on|off] [--auto-patch on|off] [--vortex-thinking on|off]",
165
165
  " [--repo-overview on|off] [--review-unit-land on|off]",
166
166
  " [--cyclone-review-unit-land on|off] [--vortex-seam on|off]",
167
+ " [--auto-land on|off] Auto land default for stacks opened after this",
167
168
  " With flags: PATCH those settings and print the stored result",
168
169
  ],
169
170
  async run(args, ctx) {
@@ -210,11 +211,12 @@ export const COMMAND_REGISTRY = [
210
211
  name: "stack",
211
212
  summary: "Stacks: /stack how-to, list yours, create → submit",
212
213
  usage: [
213
- "mergestorm stack create [name] New local stack layer ([--onto] [--trunk] [--extend])",
214
- "mergestorm stack submit Push active local stack, open PRs ([--extend])",
214
+ "mergestorm stack create [name] New local layer ([--onto] [--trunk] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off])",
215
+ "mergestorm stack submit Push/open active stack ([--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off])",
215
216
  "mergestorm stack reset --force Clear this repo's pre-submit authoring state",
216
217
  "mergestorm stack list [--json] List registered stacks",
217
- "mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain",
218
+ "mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json] Per-stack policy",
219
+ "mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] Import an open PR chain",
218
220
  "mergestorm stack restack <stack-id> Restack stack descendants",
219
221
  "mergestorm stack land <stack-id> Land bottom PR (or promote into unit)",
220
222
  ],
@@ -1,9 +1,9 @@
1
- import { type SettingsPatch, type SettingsResponse, type SettingsWritableKey } from "../api.js";
1
+ import { type SettingsPatch, type SettingsResponse } from "../api.js";
2
2
  /**
3
3
  * CLI flag → Bearer settings key. on|off only. 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, SettingsWritableKey>;
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" | "vortex_seam_specialist_enabled" | "auto_land_default">;
7
7
  export type SettingsArgs = {
8
8
  json: boolean;
9
9
  patch: SettingsPatch;
@@ -1,4 +1,5 @@
1
1
  import { getSettings, patchSettings, } from "../api.js";
2
+ import { BEARER_SETTINGS_FLAGS } from "../automation-catalog.js";
2
3
  import { loadConfig } from "../config.js";
3
4
  import { CommandError } from "../errors.js";
4
5
  import { present } from "../ui/present.js";
@@ -7,15 +8,7 @@ import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
7
8
  * CLI flag → Bearer settings key. on|off only. The connected flags are
8
9
  * read-only on the API and deliberately have no flag here.
9
10
  */
10
- export const SETTINGS_FLAGS = {
11
- "--auto-review": "auto_review_enabled",
12
- "--auto-patch": "auto_patch_enabled",
13
- "--vortex-thinking": "vortex_show_thinking_traces",
14
- "--repo-overview": "repo_overview_enabled",
15
- "--review-unit-land": "review_unit_land_prs_enabled",
16
- "--cyclone-review-unit-land": "cyclone_review_unit_land_prs_enabled",
17
- "--vortex-seam": "vortex_seam_specialist_enabled",
18
- };
11
+ export const SETTINGS_FLAGS = BEARER_SETTINGS_FLAGS;
19
12
  /** Parse `settings` argv. Accepts `--flag on|off` and `--flag=on|off`. */
20
13
  export function parseSettingsArgs(args) {
21
14
  let json = false;
@@ -1,9 +1,10 @@
1
- import { adoptStack, ensureUpperPark, listStacks, type StackDto } from "../api.js";
1
+ import { adoptStack, ensureUpperPark, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { createPr, findOpenPrNumber } from "../gh.js";
4
4
  import { parseGithubOriginRepo } from "../git.js";
5
- import { branchExists, commitsAheadOf, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject } from "../git-stack.js";
5
+ import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
6
6
  import { buildPrBodyFromCommit } from "../pr-body.js";
7
+ import { gitTopLevel } from "../repo-identity.js";
7
8
  import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
8
9
  /** A registered stack layer that would become the parent of a new layer. */
9
10
  export type RegisteredParentHit = {
@@ -30,27 +31,72 @@ export declare function parseAdoptTarget(args: string[]): {
30
31
  };
31
32
  /** Empty + populated bodies for the TTY `stack list` panel. */
32
33
  export declare function buildStackListLines(stacks: StackDto[]): string[];
34
+ /** Short per-stack policy suffixes: Auto land when on, overrides when pinned. */
35
+ export declare function stackPolicyLabels(s: Pick<StackDto, "autoEnqueueWhenReady" | "autoReviewOverride" | "autoPatchOverride">): string[];
36
+ /** Per-open policy flags shared by create, submit, and adopt (`on|off` only). */
37
+ export type StackOpenPolicyFlags = {
38
+ autoLand?: boolean;
39
+ autoReview?: boolean;
40
+ autoPatch?: boolean;
41
+ };
42
+ /** Wire policy for adopt from the parsed `on|off` flags; absent keys stay off the body. */
43
+ export declare function openPolicyPatch(flags: StackOpenPolicyFlags): StackPolicyPatch | undefined;
33
44
  /** Parse `stack create` argv into options. Exported for tests. */
34
45
  export declare function parseStackCreateArgs(args: string[]): {
35
46
  name?: string;
36
47
  onto?: string;
37
48
  trunk?: string;
38
49
  extend: boolean;
50
+ autoLand?: boolean;
51
+ autoReview?: boolean;
52
+ autoPatch?: boolean;
39
53
  asJson: boolean;
40
54
  };
41
55
  /** Parse `stack submit` argv. Exported for tests. */
42
56
  export declare function parseStackSubmitArgs(args: string[]): {
43
57
  extend: boolean;
58
+ autoLand?: boolean;
59
+ autoReview?: boolean;
60
+ autoPatch?: boolean;
61
+ asJson: boolean;
62
+ };
63
+ export declare function parseStackAdoptArgs(args: string[]): {
64
+ owner: string;
65
+ repo: string;
66
+ prNumber: number;
67
+ autoLand?: boolean;
68
+ autoReview?: boolean;
69
+ autoPatch?: boolean;
70
+ asJson: boolean;
71
+ };
72
+ export type StackSetArgs = {
73
+ stackId: string;
74
+ /** At least one of these is present. */
75
+ autoLand?: boolean;
76
+ autoReview?: boolean | null;
77
+ autoPatch?: boolean | null;
44
78
  asJson: boolean;
45
79
  };
80
+ export declare function parseStackSetArgs(args: string[]): StackSetArgs;
46
81
  export declare function parseStackResetArgs(args: string[]): void;
47
82
  /** Injectable seams for unit tests. Production callers omit deps. */
48
83
  export type StackCreateDeps = {
49
84
  cwd?: string;
85
+ gitTopLevel?: typeof gitTopLevel;
86
+ worktreeDirty?: typeof worktreeDirty;
87
+ currentBranch?: typeof currentBranch;
88
+ branchExists?: typeof branchExists;
89
+ createBranchFromHead?: typeof createBranchFromHead;
90
+ deleteBranch?: typeof deleteBranch;
91
+ discoverTrunk?: typeof discoverTrunk;
92
+ defaultLayerBranchName?: typeof defaultLayerBranchName;
93
+ loadStackMeta?: typeof loadStackMeta;
94
+ saveStackMeta?: typeof saveStackMeta;
50
95
  listStacks?: typeof listStacks;
51
96
  loadConfig?: typeof loadConfig;
52
97
  parseGithubOriginRepo?: typeof parseGithubOriginRepo;
53
98
  };
99
+ export declare function cmdStackCreate(args: string[], deps?: StackCreateDeps): Promise<void>;
54
100
  /** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
55
101
  export type StackSubmitDeps = {
56
102
  cwd?: string;
@@ -74,4 +120,11 @@ export type StackSubmitDeps = {
74
120
  ensureUpperPark?: typeof ensureUpperPark;
75
121
  };
76
122
  export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
123
+ export type StackSetDeps = {
124
+ loadConfig?: typeof loadConfig;
125
+ setStackPolicy?: typeof setStackPolicy;
126
+ };
127
+ /** Human summary lines for a `stack set` write, one per flag. */
128
+ export declare function stackSetSummary(parsed: StackSetArgs): string[];
129
+ export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
77
130
  export declare function cmdStack(args: string[]): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, } from "../api.js";
1
+ import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
@@ -6,7 +6,7 @@ import { parseGithubOriginRepo } from "../git.js";
6
6
  import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
7
7
  import { buildPrBodyFromCommit } from "../pr-body.js";
8
8
  import { gitTopLevel } from "../repo-identity.js";
9
- import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, } from "../stack-meta.js";
9
+ import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
10
10
  import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
11
11
  import { ansi } from "../ui/ansi.js";
12
12
  import { runLineTabsBrowser } from "../ui/line-tabs.js";
@@ -14,14 +14,18 @@ import { present } from "../ui/present.js";
14
14
  import { canBrowse } from "./browse.js";
15
15
  import { openHelpBrowser } from "./help.js";
16
16
  const STACK_USAGE = `usage:
17
- mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
18
- mergestorm stack submit [--extend] [--json]
17
+ mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
18
+ mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
19
19
  mergestorm stack reset --force
20
20
  mergestorm stack list [--json]
21
- mergestorm stack adopt <owner/repo>#<pr>
21
+ mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
22
+ mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
22
23
  mergestorm stack restack <stack-id> [--json]
23
24
  mergestorm stack land <stack-id> [--json]
24
25
 
26
+ --auto-review / --auto-patch pin Vortex auto-review / Cyclone auto-patch for
27
+ one stack in either direction. Absent, or \`default\` on stack set, follows
28
+ the account setting.
25
29
  stack submit opens PRs with a body generated from each layer tip commit.
26
30
  Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
27
31
  review-unit park freeze (mg-park-*) so adopt does not retarget after open.
@@ -126,9 +130,25 @@ export function buildStackListLines(stacks) {
126
130
  lines.push(ansi.dim(" create · submit · land <id> · q to close"));
127
131
  return lines;
128
132
  }
133
+ /** Short per-stack policy suffixes: Auto land when on, overrides when pinned. */
134
+ export function stackPolicyLabels(s) {
135
+ const labels = [];
136
+ if (s.autoEnqueueWhenReady)
137
+ labels.push("auto-land on");
138
+ if (typeof s.autoReviewOverride === "boolean") {
139
+ labels.push(`auto-review ${s.autoReviewOverride ? "on" : "off"}`);
140
+ }
141
+ if (typeof s.autoPatchOverride === "boolean") {
142
+ labels.push(`auto-patch ${s.autoPatchOverride ? "on" : "off"}`);
143
+ }
144
+ return labels;
145
+ }
129
146
  function formatStackHuman(s) {
130
147
  const lines = [];
131
- lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}`);
148
+ lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
149
+ stackPolicyLabels(s)
150
+ .map((label) => ` ${label}`)
151
+ .join(""));
132
152
  lines.push(` ${ansi.dim(s.id)}`);
133
153
  if (s.layers.length === 0) {
134
154
  lines.push(ansi.dim(" (no layers)"));
@@ -141,12 +161,45 @@ function formatStackHuman(s) {
141
161
  }
142
162
  return lines;
143
163
  }
164
+ const OPEN_POLICY_FLAGS = [
165
+ { flag: "--auto-land", key: "autoLand" },
166
+ { flag: "--auto-review", key: "autoReview" },
167
+ { flag: "--auto-patch", key: "autoPatch" },
168
+ ];
169
+ /**
170
+ * Consume one `--auto-* on|off` flag at `args[i]` (space or `=` form). Returns
171
+ * the next index to scan, or null when `args[i]` is not a policy flag.
172
+ */
173
+ function takeOpenPolicyFlag(args, i, into, verb) {
174
+ const a = args[i];
175
+ for (const { flag, key } of OPEN_POLICY_FLAGS) {
176
+ if (a === flag) {
177
+ into[key] = parseOnOff(args[i + 1], flag, verb);
178
+ return i + 2;
179
+ }
180
+ if (a.startsWith(`${flag}=`)) {
181
+ into[key] = parseOnOff(a.slice(flag.length + 1), flag, verb);
182
+ return i + 1;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ /** Wire policy for adopt from the parsed `on|off` flags; absent keys stay off the body. */
188
+ export function openPolicyPatch(flags) {
189
+ const patch = {
190
+ ...(typeof flags.autoLand === "boolean" ? { autoEnqueueWhenReady: flags.autoLand } : {}),
191
+ ...(typeof flags.autoReview === "boolean" ? { autoReviewOverride: flags.autoReview } : {}),
192
+ ...(typeof flags.autoPatch === "boolean" ? { autoPatchOverride: flags.autoPatch } : {}),
193
+ };
194
+ return Object.keys(patch).length > 0 ? patch : undefined;
195
+ }
144
196
  /** Parse `stack create` argv into options. Exported for tests. */
145
197
  export function parseStackCreateArgs(args) {
146
198
  let name;
147
199
  let onto;
148
200
  let trunk;
149
201
  let extend = false;
202
+ const policy = {};
150
203
  let asJson = false;
151
204
  for (let i = 0; i < args.length; i++) {
152
205
  const a = args[i];
@@ -158,6 +211,11 @@ export function parseStackCreateArgs(args) {
158
211
  extend = true;
159
212
  continue;
160
213
  }
214
+ const next = takeOpenPolicyFlag(args, i, policy, "stack create");
215
+ if (next !== null) {
216
+ i = next - 1;
217
+ continue;
218
+ }
161
219
  if (a === "--onto") {
162
220
  const v = args[++i];
163
221
  if (!v) {
@@ -190,13 +248,24 @@ export function parseStackCreateArgs(args) {
190
248
  }
191
249
  name = a;
192
250
  }
193
- return { name, onto, trunk, extend, asJson };
251
+ return {
252
+ name,
253
+ onto,
254
+ trunk,
255
+ extend,
256
+ autoLand: policy.autoLand,
257
+ autoReview: policy.autoReview,
258
+ autoPatch: policy.autoPatch,
259
+ asJson,
260
+ };
194
261
  }
195
262
  /** Parse `stack submit` argv. Exported for tests. */
196
263
  export function parseStackSubmitArgs(args) {
197
264
  let extend = false;
265
+ const policy = {};
198
266
  let asJson = false;
199
- for (const a of args) {
267
+ for (let i = 0; i < args.length; i++) {
268
+ const a = args[i];
200
269
  if (a === "--json") {
201
270
  asJson = true;
202
271
  continue;
@@ -205,43 +274,159 @@ export function parseStackSubmitArgs(args) {
205
274
  extend = true;
206
275
  continue;
207
276
  }
208
- throw new CommandError("usage: mergestorm stack submit [--extend] [--json]");
277
+ const next = takeOpenPolicyFlag(args, i, policy, "stack submit");
278
+ if (next !== null) {
279
+ i = next - 1;
280
+ continue;
281
+ }
282
+ throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]");
283
+ }
284
+ return {
285
+ extend,
286
+ autoLand: policy.autoLand,
287
+ autoReview: policy.autoReview,
288
+ autoPatch: policy.autoPatch,
289
+ asJson,
290
+ };
291
+ }
292
+ function parseOnOff(raw, flag, verb) {
293
+ if (raw === "on")
294
+ return true;
295
+ if (raw === "off")
296
+ return false;
297
+ throw new CommandError(`usage: mergestorm ${verb} [${flag} on|off]`);
298
+ }
299
+ /** `stack set` overrides are tri-state: `default` writes null (follow the account flag). */
300
+ function parseOnOffDefault(raw, flag, verb) {
301
+ if (raw === "on")
302
+ return true;
303
+ if (raw === "off")
304
+ return false;
305
+ if (raw === "default")
306
+ return null;
307
+ throw new CommandError(`usage: mergestorm ${verb} [${flag} on|off|default]`);
308
+ }
309
+ export function parseStackAdoptArgs(args) {
310
+ const targetArgs = [];
311
+ const policy = {};
312
+ let asJson = false;
313
+ for (let i = 0; i < args.length; i++) {
314
+ const a = args[i];
315
+ if (a === "--json") {
316
+ asJson = true;
317
+ continue;
318
+ }
319
+ const next = takeOpenPolicyFlag(args, i, policy, "stack adopt <owner/repo>#<pr>");
320
+ if (next !== null) {
321
+ i = next - 1;
322
+ continue;
323
+ }
324
+ if (a.startsWith("-")) {
325
+ throw new CommandError(`${STACK_USAGE}\nunknown flag: ${a}`);
326
+ }
327
+ targetArgs.push(a);
328
+ }
329
+ const target = parseAdoptTarget(targetArgs);
330
+ return {
331
+ ...target,
332
+ autoLand: policy.autoLand,
333
+ autoReview: policy.autoReview,
334
+ autoPatch: policy.autoPatch,
335
+ asJson,
336
+ };
337
+ }
338
+ export function parseStackSetArgs(args) {
339
+ const usage = "usage: mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]";
340
+ const verb = "stack set <stack-id>";
341
+ let stackId;
342
+ let autoLand;
343
+ let autoReview;
344
+ let autoPatch;
345
+ let asJson = false;
346
+ for (let i = 0; i < args.length; i++) {
347
+ const arg = args[i];
348
+ if (arg === "--json") {
349
+ asJson = true;
350
+ }
351
+ else if (arg === "--auto-land") {
352
+ autoLand = parseOnOff(args[++i], "--auto-land", verb);
353
+ }
354
+ else if (arg.startsWith("--auto-land=")) {
355
+ autoLand = parseOnOff(arg.slice("--auto-land=".length), "--auto-land", verb);
356
+ }
357
+ else if (arg === "--auto-review") {
358
+ autoReview = parseOnOffDefault(args[++i], "--auto-review", verb);
359
+ }
360
+ else if (arg.startsWith("--auto-review=")) {
361
+ autoReview = parseOnOffDefault(arg.slice("--auto-review=".length), "--auto-review", verb);
362
+ }
363
+ else if (arg === "--auto-patch") {
364
+ autoPatch = parseOnOffDefault(args[++i], "--auto-patch", verb);
365
+ }
366
+ else if (arg.startsWith("--auto-patch=")) {
367
+ autoPatch = parseOnOffDefault(arg.slice("--auto-patch=".length), "--auto-patch", verb);
368
+ }
369
+ else if (arg.startsWith("-") || stackId !== undefined) {
370
+ throw new CommandError(usage);
371
+ }
372
+ else {
373
+ stackId = arg;
374
+ }
209
375
  }
210
- return { extend, asJson };
376
+ if (autoLand === undefined && autoReview === undefined && autoPatch === undefined) {
377
+ throw new CommandError(usage);
378
+ }
379
+ return {
380
+ stackId: requireStackId(stackId, usage),
381
+ ...(autoLand !== undefined ? { autoLand } : {}),
382
+ ...(autoReview !== undefined ? { autoReview } : {}),
383
+ ...(autoPatch !== undefined ? { autoPatch } : {}),
384
+ asJson,
385
+ };
211
386
  }
212
387
  export function parseStackResetArgs(args) {
213
388
  if (args.length !== 1 || args[0] !== "--force") {
214
389
  throw new CommandError("usage: mergestorm stack reset --force\nClears only CLI-managed pre-submit state; branches, PRs, and registered stacks are unchanged.");
215
390
  }
216
391
  }
217
- async function cmdStackCreate(args, deps = {}) {
218
- const { name, onto, trunk: trunkFlag, extend, asJson } = parseStackCreateArgs(args);
392
+ export async function cmdStackCreate(args, deps = {}) {
393
+ const { name, onto, trunk: trunkFlag, extend, autoLand, autoReview, autoPatch, asJson, } = parseStackCreateArgs(args);
219
394
  const cwd = deps.cwd ?? process.cwd();
395
+ const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
396
+ const worktreeDirtyFn = deps.worktreeDirty ?? worktreeDirty;
397
+ const currentBranchFn = deps.currentBranch ?? currentBranch;
398
+ const branchExistsFn = deps.branchExists ?? branchExists;
399
+ const createBranchFromHeadFn = deps.createBranchFromHead ?? createBranchFromHead;
400
+ const deleteBranchFn = deps.deleteBranch ?? deleteBranch;
401
+ const discoverTrunkFn = deps.discoverTrunk ?? discoverTrunk;
402
+ const defaultLayerBranchNameFn = deps.defaultLayerBranchName ?? defaultLayerBranchName;
403
+ const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
404
+ const saveStackMetaFn = deps.saveStackMeta ?? saveStackMeta;
220
405
  const listStacksFn = deps.listStacks ?? listStacks;
221
406
  const loadConfigFn = deps.loadConfig ?? loadConfig;
222
407
  const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
223
408
  try {
224
- gitTopLevel(cwd);
409
+ gitTopLevelFn(cwd);
225
410
  }
226
411
  catch {
227
412
  throw new CommandError("Not in a git repository. Run `mg stack create` from inside the repository whose stack you want to author.");
228
413
  }
229
- if (worktreeDirty(cwd)) {
414
+ if (worktreeDirtyFn(cwd)) {
230
415
  throw new CommandError("Working tree is dirty. Commit or stash changes before `stack create`.");
231
416
  }
232
- const parentBranch = onto?.trim() || currentBranch(cwd);
417
+ const parentBranch = onto?.trim() || currentBranchFn(cwd);
233
418
  if (!parentBranch) {
234
419
  throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) before `stack create`.");
235
420
  }
236
- if (!branchExists(parentBranch, cwd)) {
421
+ if (!branchExistsFn(parentBranch, cwd)) {
237
422
  throw new CommandError(`Parent branch not found: ${parentBranch}`);
238
423
  }
239
- const branch = defaultLayerBranchName(name);
240
- if (branchExists(branch, cwd)) {
424
+ const branch = defaultLayerBranchNameFn(name);
425
+ if (branchExistsFn(branch, cwd)) {
241
426
  throw new CommandError(`Branch already exists: ${branch}`);
242
427
  }
243
- let meta = await loadStackMeta(cwd);
244
- const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunk(cwd);
428
+ let meta = await loadStackMetaFn(cwd);
429
+ const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunkFn(cwd);
245
430
  if (!meta) {
246
431
  meta = emptyStackMeta(trunk);
247
432
  }
@@ -281,23 +466,28 @@ async function cmdStackCreate(args, deps = {}) {
281
466
  }
282
467
  try {
283
468
  meta = selectStackForCreate(meta, parentBranch);
469
+ meta = setActivePolicy(meta, {
470
+ autoEnqueueWhenReady: autoLand,
471
+ autoReviewOverride: autoReview,
472
+ autoPatchOverride: autoPatch,
473
+ });
284
474
  }
285
475
  catch (err) {
286
476
  throw new CommandError(err instanceof Error ? err.message : String(err));
287
477
  }
288
478
  try {
289
- createBranchFromHead(branch, cwd);
479
+ createBranchFromHeadFn(branch, cwd);
290
480
  }
291
481
  catch (err) {
292
482
  throw new CommandError(err instanceof Error ? err.message : String(err));
293
483
  }
294
484
  try {
295
485
  meta = appendLayer(meta, { branch, parentBranch });
296
- await saveStackMeta(meta, cwd);
486
+ await saveStackMetaFn(meta, cwd);
297
487
  }
298
488
  catch (err) {
299
489
  try {
300
- deleteBranch(branch, cwd);
490
+ deleteBranchFn(branch, cwd);
301
491
  }
302
492
  catch { /* rollback best-effort */ }
303
493
  throw new CommandError(err instanceof Error ? err.message : String(err));
@@ -328,7 +518,7 @@ async function cmdStackCreate(args, deps = {}) {
328
518
  await present("Stack", created);
329
519
  }
330
520
  export async function cmdStackSubmit(args, deps = {}) {
331
- const { extend, asJson } = parseStackSubmitArgs(args);
521
+ const { extend, autoLand, autoReview, autoPatch, asJson } = parseStackSubmitArgs(args);
332
522
  const cwd = deps.cwd ?? process.cwd();
333
523
  const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
334
524
  const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
@@ -399,6 +589,21 @@ export async function cmdStackSubmit(args, deps = {}) {
399
589
  owner,
400
590
  repo,
401
591
  });
592
+ // Explicit submit flags win; otherwise a fresh (unregistered) stack carries
593
+ // the choices remembered at `stack create`. Joining a registered stack
594
+ // never replays stored create-time flags onto it.
595
+ const stored = meta.stacks[meta.active];
596
+ const freshStack = plans.every((plan) => plan.stackId === null);
597
+ const pick = (flag, remembered) => typeof flag === "boolean"
598
+ ? flag
599
+ : freshStack && typeof remembered === "boolean"
600
+ ? remembered
601
+ : undefined;
602
+ const policy = openPolicyPatch({
603
+ autoLand: pick(autoLand, stored?.autoEnqueueWhenReady),
604
+ autoReview: pick(autoReview, stored?.autoReviewOverride),
605
+ autoPatch: pick(autoPatch, stored?.autoPatchOverride),
606
+ });
402
607
  for (const layer of layers) {
403
608
  if (!branchExistsFn(layer.branch, cwd)) {
404
609
  throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
@@ -475,7 +680,7 @@ export async function cmdStackSubmit(args, deps = {}) {
475
680
  let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
476
681
  const registerPr = async (prNumber) => {
477
682
  console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
478
- const body = await adoptStackFn(owner, repo, prNumber, cfg);
683
+ const body = await adoptStackFn(owner, repo, prNumber, cfg, policy);
479
684
  if (typeof body !== "object" || body === null) {
480
685
  throw new CommandError("Unexpected response shape from adopt API");
481
686
  }
@@ -576,15 +781,39 @@ async function cmdStackList(args) {
576
781
  listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
577
782
  await present("Stacks", listed);
578
783
  }
579
- async function cmdStackAdopt(args) {
580
- const asJson = args.includes("--json");
581
- const targetArgs = args.filter((a) => a !== "--json");
582
- if (targetArgs.length === 0) {
583
- throw new CommandError(`usage: mergestorm stack adopt <owner/repo>#<pr>`);
784
+ /** Human summary lines for a `stack set` write, one per flag. */
785
+ export function stackSetSummary(parsed) {
786
+ const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
787
+ const lines = [];
788
+ if (parsed.autoLand !== undefined) {
789
+ lines.push(`Auto land ${parsed.autoLand ? "on" : "off"}`);
584
790
  }
585
- const { owner, repo, prNumber } = parseAdoptTarget(targetArgs);
791
+ if (parsed.autoReview !== undefined) {
792
+ lines.push(`Auto-review ${tri(parsed.autoReview)}`);
793
+ }
794
+ if (parsed.autoPatch !== undefined) {
795
+ lines.push(`Auto-patch ${tri(parsed.autoPatch)}`);
796
+ }
797
+ return lines;
798
+ }
799
+ export async function cmdStackSet(args, deps = {}) {
800
+ const parsed = parseStackSetArgs(args);
801
+ const cfg = await (deps.loadConfig ?? loadConfig)();
802
+ const body = await (deps.setStackPolicy ?? setStackPolicy)(parsed.stackId, {
803
+ ...(parsed.autoLand !== undefined ? { autoEnqueueWhenReady: parsed.autoLand } : {}),
804
+ ...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
805
+ ...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
806
+ }, cfg);
807
+ if (parsed.asJson) {
808
+ console.log(JSON.stringify(body, null, 2));
809
+ return;
810
+ }
811
+ await present("Stack", stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)));
812
+ }
813
+ async function cmdStackAdopt(args) {
814
+ const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
586
815
  const cfg = await loadConfig();
587
- const body = await adoptStack(owner, repo, prNumber, cfg);
816
+ const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
588
817
  if (asJson) {
589
818
  console.log(JSON.stringify(body, null, 2));
590
819
  return;
@@ -670,6 +899,8 @@ export async function cmdStack(args) {
670
899
  return cmdStackReset(rest);
671
900
  if (sub === "list" || sub === "ls")
672
901
  return cmdStackList(rest);
902
+ if (sub === "set")
903
+ return cmdStackSet(rest);
673
904
  if (sub === "adopt")
674
905
  return cmdStackAdopt(rest);
675
906
  if (sub === "restack")
@@ -2,7 +2,7 @@ export declare const LEGACY_STACK_META_REL: string;
2
2
  export declare function gitTopLevel(cwd?: string): string;
3
3
  export declare function gitCommonDir(cwd?: string): string;
4
4
  /**
5
- * A physical clone and all its linked worktrees share one authoring state.
5
+ * Each worktree has its own authoring state.
6
6
  * Independent clones remain isolated even when they point at the same remote.
7
7
  */
8
8
  export declare function stableRepoId(cwd?: string): string;
@@ -41,11 +41,22 @@ export function gitCommonDir(cwd = process.cwd()) {
41
41
  }
42
42
  }
43
43
  /**
44
- * A physical clone and all its linked worktrees share one authoring state.
44
+ * Each worktree has its own authoring state.
45
45
  * Independent clones remain isolated even when they point at the same remote.
46
46
  */
47
47
  export function stableRepoId(cwd = process.cwd()) {
48
- return createHash("sha256").update(gitCommonDir(cwd)).digest("hex").slice(0, 20);
48
+ let raw;
49
+ try {
50
+ raw = git(["rev-parse", "--git-dir"], cwd).trim();
51
+ }
52
+ catch (err) {
53
+ rethrowIfNotARepo(err);
54
+ }
55
+ const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
56
+ return createHash("sha256")
57
+ .update(realpathSync(absolute))
58
+ .digest("hex")
59
+ .slice(0, 20);
49
60
  }
50
61
  export function cliStacksRoot() {
51
62
  return path.join(mergestormHome(), "stacks");
@@ -224,6 +224,16 @@ export type StackDto = {
224
224
  * Missing on older payloads is off.
225
225
  */
226
226
  autoEnqueueWhenReady?: boolean;
227
+ /**
228
+ * Per-stack Vortex auto-review override. `null` (or missing) follows the
229
+ * account `auto_review_enabled` flag; a boolean wins in both directions.
230
+ */
231
+ autoReviewOverride?: boolean | null;
232
+ /**
233
+ * Per-stack Cyclone auto-patch override. `null` (or missing) follows the
234
+ * account `auto_patch_enabled` flag; a boolean wins in both directions.
235
+ */
236
+ autoPatchOverride?: boolean | null;
227
237
  layers: StackLayerDto[];
228
238
  /** Present when a review-unit row exists for this stack. */
229
239
  unit?: StackUnitDto;
@@ -5,6 +5,10 @@ export type StackLayerMeta = {
5
5
  };
6
6
  export type LocalStack = {
7
7
  layers: StackLayerMeta[];
8
+ autoEnqueueWhenReady?: boolean;
9
+ /** Per-open override carried from `stack create` to `stack submit`; absent = account flag. */
10
+ autoReviewOverride?: boolean;
11
+ autoPatchOverride?: boolean;
8
12
  };
9
13
  export type StackMeta = {
10
14
  version: 2;
@@ -20,6 +24,14 @@ export declare function loadStackMeta(cwd?: string, stacksRoot?: string): Promis
20
24
  export declare function emptyStackMeta(trunk: string): StackMeta;
21
25
  export declare function activeStack(meta: StackMeta): LocalStack;
22
26
  export declare function activeLayers(meta: StackMeta): StackLayerMeta[];
27
+ export declare function setActiveAutoEnqueue(meta: StackMeta, enabled: boolean | undefined): StackMeta;
28
+ /** Per-open automation flags remembered on the active local stack until submit. */
29
+ export type LocalStackPolicy = {
30
+ autoEnqueueWhenReady?: boolean;
31
+ autoReviewOverride?: boolean;
32
+ autoPatchOverride?: boolean;
33
+ };
34
+ export declare function setActivePolicy(meta: StackMeta, policy: LocalStackPolicy): StackMeta;
23
35
  export declare function saveStackMeta(meta: StackMeta, cwd?: string, stacksRoot?: string): Promise<void>;
24
36
  /** Explicit recovery. Does not touch branches, PRs, or registered server stacks. */
25
37
  export declare function resetStackMeta(cwd?: string, stacksRoot?: string): Promise<void>;
@@ -8,6 +8,11 @@ import path from "node:path";
8
8
  import { CommandError, isCommandErrorCode } from "./errors.js";
9
9
  import { git } from "./git.js";
10
10
  import { cliStacksRoot, gitTopLevel, LEGACY_STACK_META_REL, legacyStackMetaPath, repoStateDir, stackMetaPath as resolveStackMetaPath, } from "./repo-identity.js";
11
+ const LOCAL_STACK_FLAGS = [
12
+ "autoEnqueueWhenReady",
13
+ "autoReviewOverride",
14
+ "autoPatchOverride",
15
+ ];
11
16
  /** Exported only for legacy migration/tests. New state never uses this path. */
12
17
  export const STACK_META_REL = LEGACY_STACK_META_REL;
13
18
  export function stackMetaPath(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
@@ -35,10 +40,18 @@ function normalizeMeta(raw) {
35
40
  if (!stack ||
36
41
  typeof stack !== "object" ||
37
42
  !Array.isArray(stack.layers) ||
38
- !stack.layers.every(isLayer)) {
43
+ !stack.layers.every(isLayer) ||
44
+ LOCAL_STACK_FLAGS.some((flag) => stack[flag] !== undefined &&
45
+ typeof stack[flag] !== "boolean")) {
39
46
  throw new Error("Stack authoring state has a malformed stack entry; run `mg stack reset --force` to clear it.");
40
47
  }
41
- stacks.push({ layers: stack.layers });
48
+ const entry = { layers: stack.layers };
49
+ for (const flag of LOCAL_STACK_FLAGS) {
50
+ const value = stack[flag];
51
+ if (typeof value === "boolean")
52
+ entry[flag] = value;
53
+ }
54
+ stacks.push(entry);
42
55
  }
43
56
  const active = obj.active;
44
57
  if (typeof active !== "number" ||
@@ -165,6 +178,22 @@ export function activeStack(meta) {
165
178
  export function activeLayers(meta) {
166
179
  return activeStack(meta).layers;
167
180
  }
181
+ export function setActiveAutoEnqueue(meta, enabled) {
182
+ return setActiveStackFlag(meta, "autoEnqueueWhenReady", enabled);
183
+ }
184
+ export function setActivePolicy(meta, policy) {
185
+ let next = meta;
186
+ for (const flag of LOCAL_STACK_FLAGS) {
187
+ next = setActiveStackFlag(next, flag, policy[flag]);
188
+ }
189
+ return next;
190
+ }
191
+ function setActiveStackFlag(meta, flag, value) {
192
+ if (value === undefined)
193
+ return meta;
194
+ const stacks = meta.stacks.map((stack, index) => index === meta.active ? { ...stack, [flag]: value } : stack);
195
+ return { ...meta, stacks };
196
+ }
168
197
  export async function saveStackMeta(meta, cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
169
198
  const file = stackMetaPath(cwd, stacksRoot);
170
199
  const dir = path.dirname(file);
@@ -229,7 +258,7 @@ export function appendLayer(meta, layer) {
229
258
  }
230
259
  }
231
260
  const stacks = meta.stacks.map((stack, index) => index === meta.active
232
- ? { layers: [...stack.layers, layer] }
261
+ ? { ...stack, layers: [...stack.layers, layer] }
233
262
  : stack);
234
263
  return { ...meta, stacks };
235
264
  }
package/dist/ui/banner.js CHANGED
@@ -81,6 +81,9 @@ const WHATS_NEW = {
81
81
  "mg pr --wait --after-sha, mg settings, mergestorm-pr-loop skill on npm",
82
82
  "mg skill install copies review + pr-loop from the packed tarball",
83
83
  ],
84
+ "0.3.16": [
85
+ "PR loop: verify each finding, prefer a small diff, post mergestorm-loop: dismiss on skip",
86
+ ],
84
87
  };
85
88
  function densityFromVariant(variant) {
86
89
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
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",
@@ -18,23 +18,29 @@ mg skill install --claude --cursor
18
18
  ## Preconditions (in order)
19
19
 
20
20
  1. There is an already-open GitHub PR on a Vortex-monitored repo. No PR, wrong skill.
21
- 2. Auto-patch (Cyclone) must be off so you are the only patcher. Do not take anyone's word for it: the settings API is the source of truth, and step 1 of the loop verifies it before anything else runs.
21
+ 2. Auto-patch (Cyclone) must be off for this PR so you are the only patcher. Do not take anyone's word for it: the settings API and the PR's stack are the source of truth, and step 1 of the loop verifies them before anything else runs. Cyclone is off for the PR when its adopted stack has `autoPatchOverride` `false`, or when that override is unset and the account `auto_patch_enabled` is `false`.
22
22
  3. `whoami` works on the `mergestorm` MCP server (or `mg whoami --json` exits 0). Otherwise stop and ask for install/login.
23
23
 
24
24
  ## Loop (in order)
25
25
 
26
26
  1. Call `settings_get` and read `auto_patch_enabled`.
27
27
  - If `settings_get` errors or its result has no `auto_patch_enabled`: refuse the loop. Do not wait, patch, or push; tell the human that Cyclone auto-patch could not be verified.
28
- - If it is `false`: continue. You are the only patcher.
29
- - If it is `true` and the human asked you to turn Cyclone off: call `settings_set` with `auto_patch_enabled: false`, then call `settings_get` again and confirm it now reads `false`.
30
- - If it is `true` otherwise: refuse the loop. Do not wait, patch, or push; tell the human Cyclone auto-patch is still on and where to turn it off.
31
- - Never set `auto_patch_enabled` to `true`.
32
- 2. Call `review_wait_pr` with the PR's owner, repo, and pr_number (`review_get_pr` works for a first look at the resting state).
28
+ - If it is `false`: find the PR's stack with `stack_list`. If its `autoPatchOverride` is `true`, refuse unless the human asked you to turn Cyclone off for this PR; if they did, use the adopted-stack and `stack_set { auto_patch: false }` path below. Otherwise continue. You are the only patcher.
29
+ - If it is `true`: find the PR's stack with `stack_list` (the stack whose `layers` contain this PR's `prNumber` for this owner/repo) and read its `autoPatchOverride`.
30
+ - If that stack's `autoPatchOverride` is `false`: continue. Cyclone is pinned off for this stack.
31
+ - If the PR has no stack, or the override is not `false`, and the human asked you to turn Cyclone off for this PR: the PR must be an adopted stack layer first (if it is not, stop and tell the human to adopt it, for example `mg stack adopt <owner/repo>#<n> --auto-patch off`). Then call `stack_set` with `{ stack_id, auto_patch: false }` on that stack, call `stack_status` with that `stack_id`, and confirm `autoPatchOverride` now reads `false`. Leave the account `auto_patch_enabled` unchanged; other PRs keep their own policy.
32
+ - Otherwise: refuse the loop. Do not wait, patch, or push; tell the human Cyclone auto-patch is still on for this PR and that `stack_set` with `auto_patch: false` on its stack turns it off for this PR only.
33
+ - Never set `auto_patch_enabled` with `settings_set` from this skill, in either direction, and never set `auto_patch: true` on a stack.
34
+ 2. Call `review_wait_pr` with the PR's owner, repo, pr_number, and `timeout_s` 45 (`review_get_pr` works for a first look at the resting state). A Vortex pass often takes several minutes. If status is `in_progress`, call `review_wait_pr` again with the same args. Do not pass a 300s timeout; hosts drop long MCP calls.
33
35
  3. Confirm the envelope's `head_sha` is the commit you have checked out as the PR head (compare its leading 7+ hex characters with `git rev-parse HEAD`). If it is not, someone else pushed after this pass and its findings are not yours to patch: do not edit. Fetch and check out the PR head, then go to step 7 with `after_sha` set to that head.
34
- 4. Read `findings.inline` and `findings.offDiff` (or `off_diff`) sorted by severity. Fix or explain each.
36
+ 4. Read `findings.inline` and `findings.offDiff` (or `off_diff`) sorted by severity. For each finding:
37
+ - Verify it against the current checkout. Do not treat the finding text as proven.
38
+ - Prefer the smallest correct patch. Do not refactor around a finding.
39
+ - Patch concrete bugs. A chat-only explanation is not a dismiss.
40
+ - If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line: `mergestorm-loop: dismiss`. Then one line per skipped finding (path, severity, why). Do not use the `cyclone-outcome:` prefix.
35
41
  5. Patch on the PR head branch only.
36
42
  6. Push that head branch with an ordinary push. If the push is rejected because the remote branch moved, another patcher already pushed for this pass. Do not rebase, do not force, do not retry the push: stop and tell the human that a second patcher is active on this PR.
37
- 7. Call `review_wait_pr` again with `after_sha` set to the SHA you just pushed (7+ hex). An envelope whose `head_sha` is an older commit is a previous pass, not yours; keep waiting.
43
+ 7. Call `review_wait_pr` again with `after_sha` set to the SHA you just pushed (7+ hex) and `timeout_s` 45. An envelope whose `head_sha` is an older commit is a previous pass, not yours; keep waiting.
38
44
  8. Repeat from step 3 until a stop rule fires.
39
45
 
40
46
  ## Stop rules
@@ -52,12 +58,14 @@ Map these to `mergestorm.pr_review/v1` envelope fields; do not invent statuses.
52
58
 
53
59
  - submit a local review job, or call the job-id tools `review_wait` / `review_get` here. Local jobs are `mergestorm-review`'s world: they cost credits and do not fire Vortex on the PR.
54
60
  - land, merge, enqueue, or touch stack authoring or the merge queue
55
- - turn `auto_patch_enabled` on, from this skill, for any reason
61
+ - change the account `auto_patch_enabled` from this skill, for any reason; Cyclone is muted per stack with `stack_set`, never per account
62
+ - set `auto_patch: true` on any stack
56
63
  - push any branch other than the PR head
57
64
  - force-push, or rebase and retry a push that was rejected because the remote moved
58
65
  - patch a pass whose `head_sha` is not the commit you have checked out
59
66
  - wrap the CLI in a shell escape hatch
60
67
  - claim a finding is fixed without a diff that touches its path
68
+ - claim a finding is dismissed without posting a `mergestorm-loop: dismiss` comment on the PR
61
69
 
62
70
  ## Fallback (no MCP)
63
71
 
@@ -67,7 +75,7 @@ Verify auto-patch with the CLI instead of `settings_get`:
67
75
  mg settings --json
68
76
  ```
69
77
 
70
- Read `auto_patch_enabled` from stdout. If the command errors or the result has no `auto_patch_enabled`, refuse the loop and tell the human that Cyclone auto-patch could not be verified. If it is `true` and the human asked you to turn Cyclone off, run `mg settings --auto-patch off` and re-read; otherwise refuse the loop, same rule as step 1.
78
+ Read `auto_patch_enabled` from stdout. If the command errors or the result has no `auto_patch_enabled`, refuse the loop and tell the human that Cyclone auto-patch could not be verified. Run `mg stack list --json` and find the stack whose layers contain this PR. If its `autoPatchOverride` is `false`, continue. If it is unset and the account flag is `false`, continue. If it is `true`, or it is unset while the account flag is `true`, refuse unless the human asked you to turn Cyclone off for this PR; only then run `mg stack set <stack-id> --auto-patch off` (adopt first with `mg stack adopt <owner/repo>#<n> --auto-patch off` when the PR has no stack) and re-read with `mg stack list --json`. Do not run `mg settings --auto-patch off`: that changes the account, not this PR.
71
79
 
72
80
  ```bash
73
81
  mg pr <owner/repo>#<n> --json --wait --after-sha <sha> --timeout <s>