mergestorm 0.3.43 → 0.3.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
45
45
 
46
46
  ```
47
47
  ╭──────────────────────────────────────────────────────────────────────────────╮
48
- │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.43 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.45 │
49
49
  │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
50
  │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
51
  │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
package/dist/api.d.ts CHANGED
@@ -225,6 +225,8 @@ export type StackPolicyPatch = {
225
225
  autoEnqueueWhenReady?: boolean;
226
226
  autoReviewOverride?: boolean | null;
227
227
  autoPatchOverride?: boolean | null;
228
+ autoResolveConflictsOverride?: boolean | null;
229
+ autoFixCiOverride?: boolean | null;
228
230
  };
229
231
  /** Drop undefined keys so the wire body only carries what was requested. */
230
232
  export declare function stackPolicyBody(policy: StackPolicyPatch | undefined): StackPolicyPatch;
package/dist/api.js CHANGED
@@ -361,6 +361,12 @@ export function stackPolicyBody(policy) {
361
361
  if (policy?.autoPatchOverride !== undefined) {
362
362
  body.autoPatchOverride = policy.autoPatchOverride;
363
363
  }
364
+ if (policy?.autoResolveConflictsOverride !== undefined) {
365
+ body.autoResolveConflictsOverride = policy.autoResolveConflictsOverride;
366
+ }
367
+ if (policy?.autoFixCiOverride !== undefined) {
368
+ body.autoFixCiOverride = policy.autoFixCiOverride;
369
+ }
364
370
  return body;
365
371
  }
366
372
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
@@ -6,6 +6,14 @@ export declare const BEARER_SETTINGS: readonly [{
6
6
  readonly key: "auto_patch_enabled";
7
7
  readonly flag: "--auto-patch";
8
8
  readonly label: "Auto patch";
9
+ }, {
10
+ readonly key: "auto_resolve_conflicts_enabled";
11
+ readonly flag: "--auto-resolve-conflicts";
12
+ readonly label: "Auto-resolve merge conflicts";
13
+ }, {
14
+ readonly key: "auto_fix_ci_enabled";
15
+ readonly flag: "--auto-fix-ci";
16
+ readonly label: "Auto-fix failing CI";
9
17
  }, {
10
18
  readonly key: "vortex_show_thinking_traces";
11
19
  readonly flag: "--vortex-thinking";
@@ -94,4 +102,4 @@ export type BearerSettingsValues = {
94
102
  values: readonly (infer Value)[];
95
103
  } ? Value : boolean;
96
104
  };
97
- export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
105
+ export declare const BEARER_BOOLEAN_SETTINGS_KEYS: ("auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check")[];
@@ -1,6 +1,8 @@
1
1
  export const BEARER_SETTINGS = [
2
2
  { key: "auto_review_enabled", flag: "--auto-review", label: "Auto review" },
3
3
  { key: "auto_patch_enabled", flag: "--auto-patch", label: "Auto patch" },
4
+ { key: "auto_resolve_conflicts_enabled", flag: "--auto-resolve-conflicts", label: "Auto-resolve merge conflicts" },
5
+ { key: "auto_fix_ci_enabled", flag: "--auto-fix-ci", label: "Auto-fix failing CI" },
4
6
  { key: "vortex_show_thinking_traces", flag: "--vortex-thinking", label: "Vortex thinking traces" },
5
7
  { key: "repo_overview_enabled", flag: "--repo-overview", label: "Repo overview" },
6
8
  { key: "cyclone_skip_ci_enabled", flag: "--cyclone-skip-ci", label: "Skip CI on Cyclone commits" },
@@ -7,5 +7,5 @@ export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFal
7
7
  export { discoverTrunk } from "../git-stack.js";
8
8
  export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel, type StackAgentsBusy } from "../stack-dto.js";
9
9
  export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, type PollStackWatchOptions, type StackWatchCursor, type StackWatchEnvelope, } from "../stack-watch.js";
10
- export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLandGatePending, type StackLayerAgents, type StackRepairHint, } from "../stack-blockers.js";
10
+ export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, autoFixCiActive, autoResolveActive, type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLandGatePending, type StackLayerAgents, type StackRepairHint, } from "../stack-blockers.js";
11
11
  export { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackWatchBackgroundCommand, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, type StackWatchDoneReason, type StackWatchNext, type StackWatchNextArgs, type StackWatchObligation, type StackWatchObligationInput, type StackWatchReason, } from "../stack-watch-obligation.js";
@@ -7,5 +7,5 @@ export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-enve
7
7
  export { discoverTrunk } from "../git-stack.js";
8
8
  export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel } from "../stack-dto.js";
9
9
  export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, } from "../stack-watch.js";
10
- export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, } from "../stack-blockers.js";
10
+ export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, autoFixCiActive, autoResolveActive, } from "../stack-blockers.js";
11
11
  export { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackWatchBackgroundCommand, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.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" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check", string>;
30
+ export declare const SETTINGS_LABELS: Record<"auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check", string>;
31
31
  export declare const STACKS_READY_LABEL = "Stacks, merge queue, Auto land";
32
32
  export declare const STACKS_READY_WORDS: {
33
33
  readonly on: "ready";
@@ -180,7 +180,7 @@ export const COMMAND_REGISTRY = [
180
180
  summary: "Automation toggles (Config tab on a TTY)",
181
181
  usage: [
182
182
  "mergestorm settings [--json] Read the automation toggles (Config tab on a TTY)",
183
- " [--auto-review on|off] [--auto-patch on|off] [--vortex-thinking on|off]",
183
+ " [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--vortex-thinking on|off]",
184
184
  " [--repo-overview on|off]",
185
185
  " [--cyclone-skip-ci on|off]",
186
186
  " [--cyclone-patch-unverified on|off] Patch languages we cannot typecheck",
@@ -238,15 +238,15 @@ export const COMMAND_REGISTRY = [
238
238
  name: "stack",
239
239
  summary: "Stacks: /stack how-to, list yours, create → submit",
240
240
  usage: [
241
- "mergestorm stack create [name] New local layer ([--onto] [--trunk] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off])",
242
- "mergestorm stack submit Push/open active stack ([--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off])",
241
+ "mergestorm stack create [name] New local layer ([--onto] [--trunk] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off])",
242
+ "mergestorm stack submit Push/open active stack ([--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off])",
243
243
  "mergestorm stack reset --force Clear this repo's pre-submit authoring state",
244
244
  "mergestorm stack list [--json] List registered stacks",
245
245
  "mergestorm stack status <stack-id> [--json] Enriched stack status",
246
246
  "mergestorm stack wait <stack-id> [--json] [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait). --json: one document on stdout; timeout notice on stderr, exit 5",
247
247
  "mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--json] Background watcher: silent while waiting; MS-WATCH ATTENTION exits 3, MS-WATCH LANDED exits 0",
248
- "mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json] Per-stack policy",
249
- "mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] Import an open PR chain",
248
+ "mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--auto-resolve-conflicts on|off|default] [--auto-fix-ci on|off|default] [--json] Per-stack policy",
249
+ "mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] Import an open PR chain",
250
250
  "mergestorm stack restack <stack-id> Restack stack descendants",
251
251
  "mergestorm stack land <stack-id> Land bottom PR (or promote into unit)",
252
252
  ],
@@ -3,7 +3,7 @@ import { type SettingsPatch, type SettingsResponse } from "../api.js";
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.
5
5
  */
6
- export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
6
+ export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
7
7
  type IgnoreBotOperation = {
8
8
  action: "add" | "remove";
9
9
  login: string;
@@ -42,12 +42,14 @@ export declare function parseAdoptTarget(args: string[]): {
42
42
  /** Empty + populated bodies for the TTY `stack list` panel. */
43
43
  export declare function buildStackListLines(stacks: StackDto[]): string[];
44
44
  /** Short per-stack policy suffixes: Auto land when on, overrides when pinned. */
45
- export declare function stackPolicyLabels(s: Pick<StackDto, "autoEnqueueWhenReady" | "autoReviewOverride" | "autoPatchOverride">): string[];
45
+ export declare function stackPolicyLabels(s: Pick<StackDto, "autoEnqueueWhenReady" | "autoReviewOverride" | "autoPatchOverride" | "autoResolveConflictsOverride" | "autoFixCiOverride">): string[];
46
46
  /** Per-open policy flags shared by create, submit, and adopt (`on|off` only). */
47
47
  export type StackOpenPolicyFlags = {
48
48
  autoLand?: boolean;
49
49
  autoReview?: boolean;
50
50
  autoPatch?: boolean;
51
+ autoResolveConflicts?: boolean;
52
+ autoFixCi?: boolean;
51
53
  };
52
54
  /** Wire policy for adopt from the parsed `on|off` flags; absent keys stay off the body. */
53
55
  export declare function openPolicyPatch(flags: StackOpenPolicyFlags): StackPolicyPatch | undefined;
@@ -60,6 +62,8 @@ export declare function parseStackCreateArgs(args: string[]): {
60
62
  autoLand?: boolean;
61
63
  autoReview?: boolean;
62
64
  autoPatch?: boolean;
65
+ autoResolveConflicts?: boolean;
66
+ autoFixCi?: boolean;
63
67
  asJson: boolean;
64
68
  };
65
69
  /** Parse `stack submit` argv. Exported for tests. */
@@ -68,6 +72,8 @@ export declare function parseStackSubmitArgs(args: string[]): {
68
72
  autoLand?: boolean;
69
73
  autoReview?: boolean;
70
74
  autoPatch?: boolean;
75
+ autoResolveConflicts?: boolean;
76
+ autoFixCi?: boolean;
71
77
  rest: boolean;
72
78
  asJson: boolean;
73
79
  };
@@ -78,6 +84,8 @@ export declare function parseStackAdoptArgs(args: string[]): {
78
84
  autoLand?: boolean;
79
85
  autoReview?: boolean;
80
86
  autoPatch?: boolean;
87
+ autoResolveConflicts?: boolean;
88
+ autoFixCi?: boolean;
81
89
  asJson: boolean;
82
90
  };
83
91
  export type StackSetArgs = {
@@ -86,6 +94,8 @@ export type StackSetArgs = {
86
94
  autoLand?: boolean;
87
95
  autoReview?: boolean | null;
88
96
  autoPatch?: boolean | null;
97
+ autoResolveConflicts?: boolean | null;
98
+ autoFixCi?: boolean | null;
89
99
  asJson: boolean;
90
100
  };
91
101
  export declare function parseStackSetArgs(args: string[]): StackSetArgs;
@@ -27,7 +27,8 @@ export function stackRepairLine(repair) {
27
27
  if (!repair)
28
28
  return [];
29
29
  const files = "files" in repair && repair.files.length ? ` (files: ${repair.files.join(", ")})` : "";
30
- return [`Repair #${repair.prNumber} ${repair.kind}${files}: ${repair.steps}`];
30
+ const note = "note" in repair && repair.note ? ` ${repair.note}` : "";
31
+ return [`Repair #${repair.prNumber} ${repair.kind}${files}: ${repair.steps}${note}`];
31
32
  }
32
33
  export function stackWatchTextLines(watch) {
33
34
  if (!watch)
@@ -47,8 +48,8 @@ export function openStackWatch(stackId, opts = {}) {
47
48
  return stackWatchObligation({ stackId, terminal: null, unread: opts.unread, freshCursor: true });
48
49
  }
49
50
  const STACK_USAGE = `usage:
50
- mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
51
- mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]
51
+ mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--json]
52
+ mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--rest] [--json]
52
53
  mergestorm stack reset --force
53
54
  mergestorm stack list [--json]
54
55
  mergestorm stack status <stack-id> [--json]
@@ -60,12 +61,13 @@ const STACK_USAGE = `usage:
60
61
  Prints one MS-WATCH ATTENTION line and exits 3 on attention or a repeated failed read; prints
61
62
  MS-WATCH LANDED and exits 0 only for a confirmed landing. Other terminal reasons exit 3 with ATTENTION. Run it as a background command.
62
63
  --json writes exactly one JSON document to stdout and sends the MS-WATCH lines to stderr.
63
- mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
64
- mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
64
+ mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--auto-resolve-conflicts on|off|default] [--auto-fix-ci on|off|default] [--json]
65
+ mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--json]
65
66
  mergestorm stack restack <stack-id> [--json]
66
67
  mergestorm stack land <stack-id> [--json]
67
68
 
68
- --auto-review / --auto-patch pin Vortex auto-review / Cyclone auto-patch for
69
+ --auto-review / --auto-patch / --auto-resolve-conflicts / --auto-fix-ci pin
70
+ Vortex auto-review and the Cyclone automations for
69
71
  one stack in either direction. Absent, or \`default\` on stack set, follows
70
72
  the account setting.
71
73
  stack submit opens PRs with a body generated from each layer tip commit.
@@ -188,6 +190,12 @@ export function stackPolicyLabels(s) {
188
190
  if (typeof s.autoPatchOverride === "boolean") {
189
191
  labels.push(`auto-patch ${s.autoPatchOverride ? "on" : "off"}`);
190
192
  }
193
+ if (typeof s.autoResolveConflictsOverride === "boolean") {
194
+ labels.push(`auto-resolve-conflicts ${s.autoResolveConflictsOverride ? "on" : "off"}`);
195
+ }
196
+ if (typeof s.autoFixCiOverride === "boolean") {
197
+ labels.push(`auto-fix-ci ${s.autoFixCiOverride ? "on" : "off"}`);
198
+ }
191
199
  return labels;
192
200
  }
193
201
  function formatStackHuman(s, includeLayers = true) {
@@ -214,6 +222,8 @@ const OPEN_POLICY_FLAGS = [
214
222
  { flag: "--auto-land", key: "autoLand" },
215
223
  { flag: "--auto-review", key: "autoReview" },
216
224
  { flag: "--auto-patch", key: "autoPatch" },
225
+ { flag: "--auto-resolve-conflicts", key: "autoResolveConflicts" },
226
+ { flag: "--auto-fix-ci", key: "autoFixCi" },
217
227
  ];
218
228
  /**
219
229
  * Consume one `--auto-* on|off` flag at `args[i]` (space or `=` form). Returns
@@ -239,6 +249,10 @@ export function openPolicyPatch(flags) {
239
249
  ...(typeof flags.autoLand === "boolean" ? { autoEnqueueWhenReady: flags.autoLand } : {}),
240
250
  ...(typeof flags.autoReview === "boolean" ? { autoReviewOverride: flags.autoReview } : {}),
241
251
  ...(typeof flags.autoPatch === "boolean" ? { autoPatchOverride: flags.autoPatch } : {}),
252
+ ...(typeof flags.autoResolveConflicts === "boolean"
253
+ ? { autoResolveConflictsOverride: flags.autoResolveConflicts }
254
+ : {}),
255
+ ...(typeof flags.autoFixCi === "boolean" ? { autoFixCiOverride: flags.autoFixCi } : {}),
242
256
  };
243
257
  return Object.keys(patch).length > 0 ? patch : undefined;
244
258
  }
@@ -305,6 +319,8 @@ export function parseStackCreateArgs(args) {
305
319
  autoLand: policy.autoLand,
306
320
  autoReview: policy.autoReview,
307
321
  autoPatch: policy.autoPatch,
322
+ autoResolveConflicts: policy.autoResolveConflicts,
323
+ autoFixCi: policy.autoFixCi,
308
324
  asJson,
309
325
  };
310
326
  }
@@ -333,13 +349,15 @@ export function parseStackSubmitArgs(args) {
333
349
  i = next - 1;
334
350
  continue;
335
351
  }
336
- throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]");
352
+ throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--rest] [--json]");
337
353
  }
338
354
  return {
339
355
  extend,
340
356
  autoLand: policy.autoLand,
341
357
  autoReview: policy.autoReview,
342
358
  autoPatch: policy.autoPatch,
359
+ autoResolveConflicts: policy.autoResolveConflicts,
360
+ autoFixCi: policy.autoFixCi,
343
361
  rest,
344
362
  asJson,
345
363
  };
@@ -387,16 +405,20 @@ export function parseStackAdoptArgs(args) {
387
405
  autoLand: policy.autoLand,
388
406
  autoReview: policy.autoReview,
389
407
  autoPatch: policy.autoPatch,
408
+ autoResolveConflicts: policy.autoResolveConflicts,
409
+ autoFixCi: policy.autoFixCi,
390
410
  asJson,
391
411
  };
392
412
  }
393
413
  export function parseStackSetArgs(args) {
394
- const usage = "usage: mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]";
414
+ const usage = "usage: mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--auto-resolve-conflicts on|off|default] [--auto-fix-ci on|off|default] [--json]";
395
415
  const verb = "stack set <stack-id>";
396
416
  let stackId;
397
417
  let autoLand;
398
418
  let autoReview;
399
419
  let autoPatch;
420
+ let autoResolveConflicts;
421
+ let autoFixCi;
400
422
  let asJson = false;
401
423
  for (let i = 0; i < args.length; i++) {
402
424
  const arg = args[i];
@@ -421,6 +443,18 @@ export function parseStackSetArgs(args) {
421
443
  else if (arg.startsWith("--auto-patch=")) {
422
444
  autoPatch = parseOnOffDefault(arg.slice("--auto-patch=".length), "--auto-patch", verb);
423
445
  }
446
+ else if (arg === "--auto-resolve-conflicts") {
447
+ autoResolveConflicts = parseOnOffDefault(args[++i], "--auto-resolve-conflicts", verb);
448
+ }
449
+ else if (arg.startsWith("--auto-resolve-conflicts=")) {
450
+ autoResolveConflicts = parseOnOffDefault(arg.slice("--auto-resolve-conflicts=".length), "--auto-resolve-conflicts", verb);
451
+ }
452
+ else if (arg === "--auto-fix-ci") {
453
+ autoFixCi = parseOnOffDefault(args[++i], "--auto-fix-ci", verb);
454
+ }
455
+ else if (arg.startsWith("--auto-fix-ci=")) {
456
+ autoFixCi = parseOnOffDefault(arg.slice("--auto-fix-ci=".length), "--auto-fix-ci", verb);
457
+ }
424
458
  else if (arg.startsWith("-") || stackId !== undefined) {
425
459
  throw new CommandError(usage);
426
460
  }
@@ -428,7 +462,11 @@ export function parseStackSetArgs(args) {
428
462
  stackId = arg;
429
463
  }
430
464
  }
431
- if (autoLand === undefined && autoReview === undefined && autoPatch === undefined) {
465
+ if (autoLand === undefined &&
466
+ autoReview === undefined &&
467
+ autoPatch === undefined &&
468
+ autoResolveConflicts === undefined &&
469
+ autoFixCi === undefined) {
432
470
  throw new CommandError(usage);
433
471
  }
434
472
  return {
@@ -436,6 +474,8 @@ export function parseStackSetArgs(args) {
436
474
  ...(autoLand !== undefined ? { autoLand } : {}),
437
475
  ...(autoReview !== undefined ? { autoReview } : {}),
438
476
  ...(autoPatch !== undefined ? { autoPatch } : {}),
477
+ ...(autoResolveConflicts !== undefined ? { autoResolveConflicts } : {}),
478
+ ...(autoFixCi !== undefined ? { autoFixCi } : {}),
439
479
  asJson,
440
480
  };
441
481
  }
@@ -445,7 +485,7 @@ export function parseStackResetArgs(args) {
445
485
  }
446
486
  }
447
487
  export async function cmdStackCreate(args, deps = {}) {
448
- const { name, onto, trunk: trunkFlag, extend, autoLand, autoReview, autoPatch, asJson, } = parseStackCreateArgs(args);
488
+ const { name, onto, trunk: trunkFlag, extend, autoLand, autoReview, autoPatch, autoResolveConflicts, autoFixCi, asJson, } = parseStackCreateArgs(args);
449
489
  const cwd = deps.cwd ?? process.cwd();
450
490
  const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
451
491
  const worktreeDirtyFn = deps.worktreeDirty ?? worktreeDirty;
@@ -525,6 +565,8 @@ export async function cmdStackCreate(args, deps = {}) {
525
565
  autoEnqueueWhenReady: autoLand,
526
566
  autoReviewOverride: autoReview,
527
567
  autoPatchOverride: autoPatch,
568
+ autoResolveConflictsOverride: autoResolveConflicts,
569
+ autoFixCiOverride: autoFixCi,
528
570
  });
529
571
  }
530
572
  catch (err) {
@@ -573,7 +615,7 @@ export async function cmdStackCreate(args, deps = {}) {
573
615
  await present("Stack", created);
574
616
  }
575
617
  export async function cmdStackSubmit(args, deps = {}) {
576
- const { extend, autoLand, autoReview, autoPatch, rest, asJson } = parseStackSubmitArgs(args);
618
+ const { extend, autoLand, autoReview, autoPatch, autoResolveConflicts, autoFixCi, rest, asJson } = parseStackSubmitArgs(args);
577
619
  const api = rest ? "rest" : githubApiFromEnv(deps.env ?? process.env);
578
620
  const progress = (message) => {
579
621
  if (asJson)
@@ -687,6 +729,8 @@ export async function cmdStackSubmit(args, deps = {}) {
687
729
  autoLand: pick(autoLand, stored?.autoEnqueueWhenReady),
688
730
  autoReview: pick(autoReview, stored?.autoReviewOverride),
689
731
  autoPatch: pick(autoPatch, stored?.autoPatchOverride),
732
+ autoResolveConflicts: pick(autoResolveConflicts, stored?.autoResolveConflictsOverride),
733
+ autoFixCi: pick(autoFixCi, stored?.autoFixCiOverride),
690
734
  });
691
735
  const existingPrs = new Map();
692
736
  for (const plan of plans) {
@@ -1062,6 +1106,12 @@ export function stackSetSummary(parsed) {
1062
1106
  if (parsed.autoPatch !== undefined) {
1063
1107
  lines.push(`Auto-patch ${tri(parsed.autoPatch)}`);
1064
1108
  }
1109
+ if (parsed.autoResolveConflicts !== undefined) {
1110
+ lines.push(`Auto-resolve merge conflicts ${tri(parsed.autoResolveConflicts)}`);
1111
+ }
1112
+ if (parsed.autoFixCi !== undefined) {
1113
+ lines.push(`Auto-fix failing CI ${tri(parsed.autoFixCi)}`);
1114
+ }
1065
1115
  return lines;
1066
1116
  }
1067
1117
  export function withWatch(body, watch) {
@@ -1076,6 +1126,10 @@ export async function cmdStackSet(args, deps = {}) {
1076
1126
  ...(parsed.autoLand !== undefined ? { autoEnqueueWhenReady: parsed.autoLand } : {}),
1077
1127
  ...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
1078
1128
  ...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
1129
+ ...(parsed.autoResolveConflicts !== undefined
1130
+ ? { autoResolveConflictsOverride: parsed.autoResolveConflicts }
1131
+ : {}),
1132
+ ...(parsed.autoFixCi !== undefined ? { autoFixCiOverride: parsed.autoFixCi } : {}),
1079
1133
  }, cfg);
1080
1134
  const watch = openStackWatch(parsed.stackId, { unread: true });
1081
1135
  if (parsed.asJson) {
@@ -1088,9 +1142,9 @@ export async function cmdStackSet(args, deps = {}) {
1088
1142
  ]);
1089
1143
  }
1090
1144
  async function cmdStackAdopt(args) {
1091
- const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
1145
+ const { owner, repo, prNumber, autoLand, autoReview, autoPatch, autoResolveConflicts, autoFixCi, asJson, } = parseStackAdoptArgs(args);
1092
1146
  const cfg = await loadConfig();
1093
- const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
1147
+ const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch, autoResolveConflicts, autoFixCi }));
1094
1148
  const adoptedId = typeof body === "object" && body !== null
1095
1149
  ? body.stack?.id
1096
1150
  : undefined;
@@ -44,6 +44,7 @@ export type StackRepairHint = {
44
44
  liveParent: string | null;
45
45
  files: string[];
46
46
  steps: string;
47
+ note?: string;
47
48
  } | {
48
49
  kind: "ci_failure";
49
50
  prNumber: number;
@@ -51,6 +52,7 @@ export type StackRepairHint = {
51
52
  branch: string;
52
53
  failingCheck: string | null;
53
54
  steps: string;
55
+ note?: string;
54
56
  } | {
55
57
  kind: "vortex_findings";
56
58
  prNumber: number;
@@ -161,6 +163,8 @@ export declare const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no rev
161
163
  export declare const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
162
164
  export declare const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
163
165
  export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a person";
166
+ export declare function autoResolveActive(stack: StackDto, layer: StackLayerDto): boolean;
167
+ export declare function autoFixCiActive(stack: StackDto, layer: StackLayerDto): boolean;
164
168
  export declare const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
165
169
  export declare const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
166
170
  export declare const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
@@ -201,7 +205,7 @@ export declare function restackFailedRepairSteps(branch: string, liveParent: str
201
205
  kind: string;
202
206
  detail?: string | null;
203
207
  }): string;
204
- export declare function agentsCannotClear(reason: StackBlockerReason): boolean;
208
+ export declare function agentsCannotClear(reason: StackBlockerReason, stack?: StackDto, layer?: StackLayerDto): boolean;
205
209
  export declare function stackRepair(stack: StackDto, attention: ClassifiedStackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null): StackRepairHint | null;
206
210
  export declare const LAND_PR_MISSING_PREFIX = "No open land PR for ";
207
211
  /** Pair-gate attention plus live hard blocks on every other open layer. */
@@ -86,10 +86,63 @@ const CYCLONE_HANDOFF_WORDS = {
86
86
  unsupported_language: "the findings are in a language Cyclone does not patch",
87
87
  policy_owner_changed: "the stack's owner changed while the patch was queued",
88
88
  patch_job_lost: "Cyclone's patch job was lost before it finished",
89
+ conflict_needs_person: "Cyclone could not resolve the conflict and left it for a person",
90
+ conflict_unresolved: "Cyclone's conflict resolution failed",
91
+ auto_resolve_daily_cap: "Cyclone already resolved conflicts on this PR twice today, its daily cap",
92
+ stack_auto_resolve_off: "auto-resolve conflicts is off for this stack",
93
+ account_auto_resolve_off: "auto-resolve conflicts is off for the account and this stack does not turn it on",
94
+ ci_fix_did_not_hold: "CI still fails on Cyclone's own CI fix",
95
+ auto_fix_ci_daily_cap: "Cyclone already fixed CI on this PR twice today, its daily cap",
96
+ ci_billing_block: "GitHub Actions is blocked by billing, so no code change turns CI green",
97
+ ci_missing_secrets: "the failing job needs secrets its run does not have",
98
+ ci_red_on_base: "the same check also fails on the base branch",
99
+ ci_rerun_exhausted: "Cyclone already had Surge re-run this check once at this head",
100
+ ci_rerun_unavailable: "the failure looked flaky and Surge could not re-run it",
101
+ ci_unverified: "Cyclone could not confirm a fix locally, so it pushed nothing",
102
+ ci_unreadable: "Cyclone could not read the failing checks",
103
+ ci_needs_actions_read: "Cyclone's GitHub App cannot read the Actions logs on this repo",
104
+ ci_unresolved: "Cyclone's CI fix failed",
105
+ stack_auto_fix_ci_off: "auto-fix CI is off for this stack",
106
+ account_auto_fix_ci_off: "auto-fix CI is off for the account and this stack does not turn it on",
89
107
  };
90
108
  function cycloneHandoffWords(reason) {
91
109
  return CYCLONE_HANDOFF_WORDS[reason] ?? reason.replaceAll("_", " ");
92
110
  }
111
+ function isLandPrLayer(stack, layer) {
112
+ return layer.prNumber === stack.unit?.landPrNumber || (!!stack.unit?.branch && layer.branch === stack.unit.branch);
113
+ }
114
+ export function autoResolveActive(stack, layer) {
115
+ return !stack.cycloneConflictOff && !isLandPrLayer(stack, layer) &&
116
+ !sameHead(layer.cycloneConflictHandoff?.headSha, layer.headSha);
117
+ }
118
+ function conflictHandoffNote(stack, layer) {
119
+ if (isLandPrLayer(stack, layer))
120
+ return null;
121
+ const handoff = layer.cycloneConflictHandoff;
122
+ if (handoff && sameHead(handoff.headSha, layer.headSha)) {
123
+ return `Cyclone tried to resolve this conflict at ${layer.headSha ?? handoff.headSha} and left it for a person (${cycloneHandoffWords(handoff.reason)}), so it will not try this head again.`;
124
+ }
125
+ if (stack.cycloneConflictOff) {
126
+ return `Cyclone will not resolve this conflict by itself: ${cycloneHandoffWords(stack.cycloneConflictOff)}.`;
127
+ }
128
+ return null;
129
+ }
130
+ export function autoFixCiActive(stack, layer) {
131
+ return !stack.cycloneCiOff && !isLandPrLayer(stack, layer) &&
132
+ !sameHead(layer.cycloneCiHandoff?.headSha, layer.headSha);
133
+ }
134
+ function ciHandoffNote(stack, layer) {
135
+ if (isLandPrLayer(stack, layer))
136
+ return null;
137
+ const handoff = layer.cycloneCiHandoff;
138
+ if (handoff && sameHead(handoff.headSha, layer.headSha)) {
139
+ return `Cyclone tried to fix this failing CI at ${layer.headSha ?? handoff.headSha} and left it for a person (${cycloneHandoffWords(handoff.reason)}), so it will not try this head again.`;
140
+ }
141
+ if (stack.cycloneCiOff) {
142
+ return `Cyclone will not fix this failing CI by itself: ${cycloneHandoffWords(stack.cycloneCiOff)}.`;
143
+ }
144
+ return null;
145
+ }
93
146
  function vortexFindingsLeftForPerson(stack, layer) {
94
147
  return vortexChangesRequestedAtHead(layer) && !layerAgentsBusy(layer).cyclone &&
95
148
  cycloneHandoffAtHead(stack, layer) !== null;
@@ -530,7 +583,10 @@ const AGENTS_CANNOT_CLEAR = [
530
583
  "vortex_skipped",
531
584
  "unit_abandoned",
532
585
  ];
533
- export function agentsCannotClear(reason) {
586
+ const AUTO_RESOLVE_REASONS = ["restack_conflict", "merge_conflict"];
587
+ export function agentsCannotClear(reason, stack, layer) {
588
+ if (stack && layer && AUTO_RESOLVE_REASONS.includes(reason) && autoResolveActive(stack, layer))
589
+ return false;
534
590
  return AGENTS_CANNOT_CLEAR.includes(reason);
535
591
  }
536
592
  export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
@@ -553,6 +609,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
553
609
  const mergeConflict = reason === "merge_conflict";
554
610
  if (restackConflict || mergeConflict) {
555
611
  const liveParent = landPr ? landPrLiveParent(stack, layer) : conflictLiveParent(stack, layer);
612
+ const note = conflictHandoffNote(stack, layer);
556
613
  return {
557
614
  kind: restackConflict ? "restack_conflict" : "merge_conflict",
558
615
  prNumber: layer.prNumber,
@@ -561,6 +618,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
561
618
  liveParent,
562
619
  files: conflictFiles(layer, bounce),
563
620
  steps: conflictRepairSteps(layer.branch, liveParent),
621
+ ...(note ? { note } : {}),
564
622
  };
565
623
  }
566
624
  if (reason === "restack_failed" && layer.restackError) {
@@ -714,6 +772,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
714
772
  }
715
773
  if (reason === "ci_failed" || attention.bounceKind === "ci_failure") {
716
774
  const failingCheck = bounce?.bounceDetail?.failingCheck?.trim() || layer.checks?.failingName?.trim() || null;
775
+ const note = ciHandoffNote(stack, layer);
717
776
  return {
718
777
  kind: "ci_failure",
719
778
  prNumber: layer.prNumber,
@@ -721,6 +780,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null) {
721
780
  branch: layer.branch,
722
781
  failingCheck,
723
782
  steps: `Read the failing ${failingCheck ? `check ${failingCheck}` : "checks"} on ${layer.branch} (gh run list --branch ${layer.branch}, then gh run view <run-id> --log-failed), fix it on ${layer.branch} with the smallest patch, run that check locally, confirm the remote head is still ${layer.headSha ?? "the head you started from"}, then an ordinary git push. Do not retarget the PR base.`,
783
+ ...(note ? { note } : {}),
724
784
  };
725
785
  }
726
786
  return null;
@@ -872,7 +932,7 @@ export function stackBlockers(stack, entries = [], cursor = {}, nowMs = Date.now
872
932
  const landGatePending = landLayer && landing && landGateIsPending(landing)
873
933
  ? { prNumber: landLayer.prNumber, headSha: landLayer.headSha ?? null, reason: landing }
874
934
  : null;
875
- if (busy.length && attention && classified && agentsCannotClear(classified.reason)) {
935
+ if (busy.length && attention && classified && agentsCannotClear(classified.reason, stack, attentionLayer)) {
876
936
  const held = { ...attention, actAfter: "agents_idle", waitingOn: busy.map((entry) => entry.agent) };
877
937
  return { attention: null, held, issues: [attention, ...issues], currentCandidate, bounce: undefined, busy, agents,
878
938
  repair: stackRepair(stack, classified, attentionLayer, undefined, queueWait), landGatePending, queueWait: queued };
@@ -115,7 +115,8 @@ export type StackAgentsBusy = {
115
115
  headSha: string | null;
116
116
  };
117
117
  export declare const STACK_AGENT_QUEUED_STALE_MS: number;
118
- export declare function deriveStackAgentsBusy(layer: Pick<StackLayerDto, "headSha" | "vortexStatus" | "cycloneStatus" | "agentRuns" | "vortexReview">, opts?: {
118
+ export declare const AUTO_RESOLVE_ENQUEUE_GRACE_MS: number;
119
+ export declare function deriveStackAgentsBusy(layer: Pick<StackLayerDto, "headSha" | "vortexStatus" | "cycloneStatus" | "agentRuns" | "vortexReview" | "cycloneConflictPendingSince" | "cycloneCiPendingSince">, opts?: {
119
120
  cycloneClaimed?: boolean;
120
121
  nowMs?: number;
121
122
  }): StackAgentsBusy;
@@ -212,6 +213,10 @@ export type StackLayerDto = {
212
213
  vortexReview?: StackVortexReviewView;
213
214
  agentsBusy?: StackAgentsBusy | null;
214
215
  cycloneHandoff?: StackCycloneHandoff | null;
216
+ cycloneConflictHandoff?: StackCycloneHandoff | null;
217
+ cycloneConflictPendingSince?: string | null;
218
+ cycloneCiHandoff?: StackCycloneHandoff | null;
219
+ cycloneCiPendingSince?: string | null;
215
220
  conflictDetail: string | null;
216
221
  restackError?: RestackError | null;
217
222
  lastRestackedSha: string | null;
@@ -305,6 +310,8 @@ export type StackUnitDto = {
305
310
  };
306
311
  export type CycloneOwnerMatch = "same" | "different" | "none" | "lookup_failed";
307
312
  export type CyclonePatchOffReason = "stack_auto_patch_off" | "account_auto_patch_off" | "cyclone_not_connected";
313
+ export type CycloneConflictOffReason = "stack_auto_resolve_off" | "account_auto_resolve_off" | "cyclone_not_connected";
314
+ export type CycloneCiOffReason = "stack_auto_fix_ci_off" | "account_auto_fix_ci_off" | "cyclone_not_connected";
308
315
  export type StackDto = {
309
316
  id: string;
310
317
  owner: string;
@@ -361,6 +368,8 @@ export type StackDto = {
361
368
  * account `auto_patch_enabled` flag; a boolean wins in both directions.
362
369
  */
363
370
  autoPatchOverride?: boolean | null;
371
+ autoResolveConflictsOverride?: boolean | null;
372
+ autoFixCiOverride?: boolean | null;
364
373
  /**
365
374
  * Bearer / stack-row user. Cyclone apply follows this user's auto-patch
366
375
  * policy and credits for an adopted stack (HOUSE-1), even when
@@ -382,6 +391,8 @@ export type StackDto = {
382
391
  */
383
392
  cycloneOwnerMatch?: CycloneOwnerMatch;
384
393
  cyclonePatchOff?: CyclonePatchOffReason;
394
+ cycloneConflictOff?: CycloneConflictOffReason;
395
+ cycloneCiOff?: CycloneCiOffReason;
385
396
  infrastructureInstallUserId?: string | null;
386
397
  infrastructureOwnerMatch?: CycloneOwnerMatch;
387
398
  layers: StackLayerDto[];
package/dist/stack-dto.js CHANGED
@@ -24,6 +24,7 @@ export function restackRetryPending(error, layer) {
24
24
  */
25
25
  export const AUTO_LAND_OFF_REASONS = ["user", "archive", "watcher", "cancelled", "landed", "bounced"];
26
26
  export const STACK_AGENT_QUEUED_STALE_MS = 15 * 60 * 1000;
27
+ export const AUTO_RESOLVE_ENQUEUE_GRACE_MS = 2 * 60_000;
27
28
  function agentShaMayBeHead(sha, head) {
28
29
  const left = sha?.trim().toLowerCase();
29
30
  const right = head?.trim().toLowerCase();
@@ -36,18 +37,26 @@ export function deriveStackAgentsBusy(layer, opts = {}) {
36
37
  const nowMs = opts.nowMs ?? Date.now();
37
38
  const runs = (layer.agentRuns ?? []).filter((run) => !run.finishedAt);
38
39
  const vortexRuns = runs.filter((run) => run.agent === "vortex" && agentShaMayBeHead(run.sha, headSha));
39
- const freshQueued = vortexRuns.some((run) => {
40
+ const queuedFresh = (run) => {
40
41
  const queuedAt = Date.parse(run.startedAt ?? "");
41
42
  return run.status === "queued" && Number.isFinite(queuedAt) && nowMs - queuedAt <= STACK_AGENT_QUEUED_STALE_MS;
42
- });
43
+ };
44
+ const freshQueued = vortexRuns.some(queuedFresh);
43
45
  const review = layer.vortexReview;
44
46
  const reviewOnHead = !!review && agentShaMayBeHead(review.head_sha, headSha);
45
47
  const vortex = (reviewOnHead && review.status === "reviewing") ||
46
48
  vortexRuns.some((run) => run.status === "reviewing") ||
47
49
  freshQueued ||
48
50
  (layer.agentRuns === undefined && review === undefined && layer.vortexStatus === "reviewing");
51
+ const withinGrace = (since) => {
52
+ const sinceMs = Date.parse(since ?? "");
53
+ return Number.isFinite(sinceMs) && nowMs - sinceMs <= AUTO_RESOLVE_ENQUEUE_GRACE_MS;
54
+ };
49
55
  const cyclone = opts.cycloneClaimed === true || layer.cycloneStatus === "patching" ||
50
- runs.some((run) => run.agent === "cyclone" && run.status === "patching");
56
+ runs.some((run) => run.agent === "cyclone" && run.status === "patching") ||
57
+ runs.some((run) => run.agent === "cyclone" && agentShaMayBeHead(run.sha, headSha) && queuedFresh(run)) ||
58
+ withinGrace(layer.cycloneConflictPendingSince) ||
59
+ withinGrace(layer.cycloneCiPendingSince);
51
60
  return { vortex, cyclone, headSha };
52
61
  }
53
62
  /** Every bounce kind understood by the shared contract. */
@@ -9,6 +9,8 @@ export type LocalStack = {
9
9
  /** Per-open override carried from `stack create` to `stack submit`; absent = account flag. */
10
10
  autoReviewOverride?: boolean;
11
11
  autoPatchOverride?: boolean;
12
+ autoResolveConflictsOverride?: boolean;
13
+ autoFixCiOverride?: boolean;
12
14
  };
13
15
  export type StackMeta = {
14
16
  version: 2;
@@ -30,6 +32,8 @@ export type LocalStackPolicy = {
30
32
  autoEnqueueWhenReady?: boolean;
31
33
  autoReviewOverride?: boolean;
32
34
  autoPatchOverride?: boolean;
35
+ autoResolveConflictsOverride?: boolean;
36
+ autoFixCiOverride?: boolean;
33
37
  };
34
38
  export declare function setActivePolicy(meta: StackMeta, policy: LocalStackPolicy): StackMeta;
35
39
  export declare function saveStackMeta(meta: StackMeta, cwd?: string, stacksRoot?: string): Promise<void>;
@@ -12,6 +12,8 @@ const LOCAL_STACK_FLAGS = [
12
12
  "autoEnqueueWhenReady",
13
13
  "autoReviewOverride",
14
14
  "autoPatchOverride",
15
+ "autoResolveConflictsOverride",
16
+ "autoFixCiOverride",
15
17
  ];
16
18
  /** Exported only for legacy migration/tests. New state never uses this path. */
17
19
  export const STACK_META_REL = LEGACY_STACK_META_REL;
package/dist/ui/banner.js CHANGED
@@ -167,6 +167,12 @@ const WHATS_NEW = {
167
167
  "0.3.43": [
168
168
  "watch --ignore holds a blocker as its minutes tick",
169
169
  ],
170
+ "0.3.44": [
171
+ "Cyclone can auto-resolve stack conflicts",
172
+ ],
173
+ "0.3.45": [
174
+ "Cyclone can auto-fix red stack CI",
175
+ ],
170
176
  };
171
177
  function densityFromVariant(variant) {
172
178
  if (variant === "mini" || variant === "nano")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
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",
@@ -7,6 +7,10 @@ description: Watch stacks explicitly named by the human while Auto land is on, p
7
7
 
8
8
  Use only these shipped Mergestorm MCP tools: `stack_wait`, `stack_status`, `queue_status`, and `stack_set` with `auto_land: true` only at enrollment on the human's request or as "Re-arm after a bounce" allows. Use local checkout, test, and Git operations to make the patch, and the `mg stack watch` CLI command to wait in the background. Auto land owns landing.
9
9
 
10
+ ## Who fixes what
11
+
12
+ Cyclone, with auto-patch on, patches only Vortex review findings: Core and seam review findings on any layer, the land PR included. It does not fix merge conflicts, restack conflicts, failing CI, or any other landing blocker by itself. Those are yours, whatever the auto-patch setting. With auto-resolve conflicts on for the stack, Cyclone resolves a layer conflict itself (never the land PR's conflict with main): while `busy[]` shows Cyclone, wait, and fix the conflict only after its handoff, when the watch returns `attention`. With auto-fix CI on for the stack, Cyclone fixes a settled red check on a layer itself (never the land PR), or asks Surge for one re-run of a flaky one: while `busy[]` shows Cyclone, wait, and fix the failure only after its handoff, when the watch returns `attention`. Do not turn auto-patch off to fix them; it changes nothing for them.
13
+
10
14
  ## Not done until landed
11
15
 
12
16
  A stack watch is done only when the stack is landed, or when the human takes it back (archives the stack, turns Auto land off, or tells you to stop). A clean push, a submit, an adopt, or one merged layer is not done: merging a lower layer can break a layer above it, so the obligation covers the whole stack, not the layer you touched. Every `stack_wait` and `stack_status` result carries `watch`: while `watch.done` is `false`, call `stack_wait` again with `watch.next.args` and do not end your task. If your host cannot hold a long turn open, the background watcher in "Waiting in the background" does that waiting for you. `watch.done` is `true` only when the stack is landed, every PR is merged or closed with nothing left to land, the stack is archived, or it no longer exists (`reason` `not_found`: confirm the PRs merged before reporting it landed). Then stop. A layer PR shown as merged is merged into `mg-stack-N`, not main: the stack is landed only when the land PR (head `mg-stack-N`) merges into main or `watch.done` says landed. Never judge landing from GitHub PR state alone.
@@ -38,7 +42,7 @@ A stack whose layers are all merged into `mg-stack-N` but has no open land PR re
38
42
  1. Call `stack_wait` with the named `stack_id` and `timeout_s: 45`. On initial enrollment, omit the cursor selectors. Save the returned `cursor`: map `cursor.enrolledHeadSha` to `enrolled_head_sha`, `cursor.afterFinishedAt` to `after_finished_at`, and `cursor.bounceId` to `bounce_id` (omit only absent fields; preserve explicit nulls). Keep `bounce_id` alongside `after_finished_at`; never pass one without the other when the returned cursor has both. The shipped tool accepts these selectors, not a nested cursor argument. `watch.next.args` already holds them in that shape, with `timeout_s: 45`; passing it unchanged is the same call.
39
43
  2. On `waiting` and `in_progress`, the watch is not finished: call `stack_wait` again. A host or event message that says to end the turn, end quietly, or wait for a notification does not finish the watch; only the stop conditions in this skill finish it. Ending your turn while `mg stack watch` runs in the background is waiting, not finishing. Keep the same cursor across retries. An unread deadline times out as `failed`; stop and report. Timeout `waiting` or `in_progress` after a snapshot is not failure — call `stack_wait` again in 45s slices with the same cursor after inspecting `issues[]`. Inspect `issues[]` on `in_progress` too: a live queue does not erase upstack hard blocks. `waiting` with issues is not idle. If `assessment` is `unavailable`, do not infer that the stack is issue-free; refresh `stack_status`, whose `attention`, `issues`, and `currentCandidate` come from the same blocker rules as `stack_wait`. Do not replace the enrolled head with a newer snapshot head or advance the timestamp just because a wait timed out. On a surfaced `rate_limited` error, honor the retry delay and retain the cursor; on `failed` or an unrecoverable tool error, call `stack_wait` once more with the same cursor; if it fails again, stop and report the reason and that the stack is not landed. `queue_status` returning `queue_entry_not_found` is not a failure: an empty queue for the stack is a normal wait.
40
44
  3. `attention` always means something for you to fix. A land gate that is only waiting (`landGatePending` with `ci_pending` or another `*_pending` / `*_running` reason on the land PR) returns `in_progress`, not `attention`: keep watching. On `attention`, read `repair` when present: it names the blocked PR's `branch` and the concrete fix. For `restack_conflict` and `merge_conflict` it gives `liveParent` (the sibling PR head, or `mg-stack-N` once the lower layers are merged or promoted, or the land PR's base such as `main` when the blocked PR is the land PR, never `mg-park-*`) and the conflicted `files`; a null `liveParent` means stop and tell the human. For `ci_failure` it names the failing check. A stack whose `autoLandOff.reason` is `bounced` (a `merge_failed`, `gh_error`, or `ci_timeout` bounce turned Auto land off and nobody took the stack back since) returns `attention` with the blocker `Auto land off after <kind> bounce`, the bounce in `issues[]`, and a `repair` of kind `auto_land_off`; handle it as "Re-arm after a bounce" describes. A stack whose review unit was abandoned because a human moved its bottom PR off `mg-stack-N` returns `attention` with the blocker `Review unit abandoned: based on <base>` and a `repair` of kind `unit_abandoned`: do not patch or retarget anything. Ask the human whether to re-root it: if at least two PRs remain open, tell them that `mg stack adopt owner/repo#<bottom PR>` re-roots it on a new review unit; with only one open PR, re-adopting will not create a review unit, so tell them to close and resubmit it to create a fresh stack, then stop. Auto land records why it is waiting in the stack's `autoLandWait` (`reason`, `prNumber`, `headSha`, `since`), and the watch returns `attention` on that PR when the wait cannot clear on its own: a failing check Auto land reads live on GitHub while the ledger may still show CI green (blocker `CI failed — Auto land reads a failing check on GitHub at this head`, `repair` kind `ci_failure`); a check pending for 45 minutes (`repair` kind `ci_pending`: find the check that never finished and re-run it, or tell the human when no workflow reports it); no approving Vortex review for 30 minutes with no Vortex run working (`repair` kind `vortex_not_green`: fix or dismiss the findings, or tell the human when no review exists at the head); and a promote GitHub refused 3 times with `merge_failed`, after which Auto land stops retrying that head (`repair` kind `promote_failed`: fix the cause on the PR and push a new head, or tell the human when the cause is outside the PR; never toggle Auto land yourself for it). The watch also returns `attention` when GitHub refuses to let Mergestorm read the head's check runs or commit statuses (blocker `Mergestorm cannot read ... on this head`, `repair` kind `ci_unreadable`: do not patch or push; tell the human to grant the Mergestorm GitHub App the permission the repair names), and when any other wait reason has held for 45 minutes, a land gate left pending or running on the land PR included (blocker `Auto land has waited <N>m: <reason in plain words>`, `repair` kind `auto_land_waiting`: read `stack_status`, fix what a commit on the named branch can fix, otherwise tell the human what Auto land waits for and since when). A merge queue entry that has sat queued for 45 minutes with nothing working on it or ahead of it returns `attention` with the blocker `Merge queue stuck: ...` and a `repair` whose kind is queue_stalled: do not patch or push; tell the human. A Vortex review skipped because the PR author is on the ignored authors list, or because reviews are turned off for the repository, returns `attention` at once with a `repair` of kind `vortex_skipped`; a Vortex retry that is scheduled (`Vortex hit a temporary error on this head and will retry by itself`) is a wait listed in `issues[]`, never `attention`. `Review failed` carries a `repair` of kind `vortex_failed`: comment `@mergestorm-vortex review` on the PR or push a new commit, and tell the human if it fails again. A PR open on `mg-stack-N` that is not a layer of the stack returns `attention` at once with the blocker `The land PR cannot be queued: ...` and a `repair` of kind `members_remaining`: do not patch or push; close, retarget or adopt that PR (a draft or fork PR cannot be adopted). A layer whose live head has a Vortex verdict that requests changes after Cyclone left those findings for a person (a hold directive, the Vortex patch cap, auto-patch off, no billing, a batch it gave up on, or a PR state it could not read) returns `attention` with the blocker `Vortex findings need a person` and a `repair` of kind `vortex_findings` naming the Cyclone reason (`handoff`) and where the review is (`findings`): verify each finding against the checkout, fix the real ones with the smallest patch and an ordinary push, and tell the human about the ones you judge wrong. `repair` does not replace the checks below. On `attention`, fix the named PR: `prNumber` and `headSha` identify the blocked PR, while `currentCandidate` identifies the promote candidate. The gate covers the bottom and its direct child only when the child has current-head DIRTY on the live bottom branch. A GitHub DIRTY whose `parentBranch` starts with `mg-park-` is not that gate. Refresh `stack_status`; use `queue_status` with the same `stack_id` when bounce details are needed. Verify the blocker still applies to the current PR head. Never treat `verifyHeadSha` as the current head: it is a queue verification SHA and can be stale or synthetic. Compare the live PR branch head, the current stack layer head, and the checkout before editing. Read the live PR head with `git ls-remote origin refs/heads/<branch>` or `gh api repos/OWNER/REPO/pulls/N --jq .head.sha`. If you need CI logs, use `gh run list --branch <branch>`, `gh run view <run-id> --log-failed`, or `gh api repos/OWNER/REPO/commits/SHA/check-runs`. Read GitHub only through git and these REST calls; do not use other `gh pr` subcommands. Restack `clean` does not mean mergeable or CI-green. Attention is a diagnosis, not permission to mutate stack or queue policy; the only policy change you make is the re-arm in "Re-arm after a bounce". A bounce is the reason to look, not the moment to commit. `stack_wait` and `stack_status` hold the blocked PR's attention themselves while Vortex or Cyclone is still working on that PR, whatever the blocker (a red CI failure, a merge conflict, a bounce, Draft, a failed restack or review): they return `in_progress` instead of `attention`, with `busy[]` naming the PR (`prNumber`, `headSha`), the `blocker`, and the `agent` (`vortex` or `cyclone`), and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and `busy` flags. `in_progress` with a nonempty `busy[]` means do not patch yet: keep calling `stack_wait` with the same cursor until that work has finished and the watch returns `attention`, then make the follow-up commit. You do not need an extra `stack_status` refresh each slice for this check. A nonempty `busy[]` reports still-working agents on that PR's current head. Vortex holds when `vortexReview` is `reviewing` at the head, an unfinished vortex `agentRuns` row at the head is `reviewing`, or an unfinished vortex run at the head is `queued` and started within the last 15 minutes (with no `agentRuns` or `vortexReview` on the layer, `vortexStatus` `reviewing` holds). Cyclone holds when `cycloneStatus` is `patching`, an unfinished cyclone `agentRuns` row is `patching`, or the server sees a live Cyclone patch lease or an open Cyclone check run on the head. `seam_pending` alone, and a `queued` run older than 15 minutes, are not treated as work in progress. `all_clear`, `findings`, `skipped`, `failed`, `throttled`, `awaiting_fix`, and a null status are finished. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A blocker no Vortex or Cyclone run can clear is not hidden by the hold: a merge conflict vs the live parent, a restack `Conflict` or `Restack failed` (a parked freeze included), a Draft PR, `Seam review pending, no review running`, or `Review skipped, out of review quota`. For those the envelope stays `in_progress` but names the blocker in the top-level `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"`, `waitingOn` listing the busy agents, and `repair` when there is one. Plan the fix from it, but do not patch, merge, or push while `actAfter` is set: keep calling `stack_wait` with the same cursor, and act when a snapshot after the agents finish returns `attention`; a Cyclone push can change or clear the blocker, so re-read it then. Blockers the agents can change (a red CI failure, a failed review, seam findings, a bounce) keep the plain hold: `blocker` is null, `actAfter` is null, and only `busy[]` names them. If a hold looks stuck, refresh `stack_status` and tell the human. Do not turn Cyclone off or Auto land off, and do not turn Auto land on to end the wait; turn it on only as "Re-arm after a bounce" allows. If the remote head moves while you wait, stop and tell the human.
41
- 4. Use a clean worktree on that named PR's head branch and record the remote starting SHA. Fix the current blocker with the smallest patch, and run the relevant checks. For conflicts, the live parent is the sibling PR head or the unit branch (`mg-stack-N`, Mergestorm's owned trunk, not an alias for main) — not the DTO `parentBranch` when that field starts with `mg-park-*`. If `parentBranch` is `mg-park-*`, never merge or rebase onto it; if the real live parent is unknown, stop. The land PR is the one PR whose head is `mg-stack-N`: its live parent is its base (`main`), and merging that base into `mg-stack-N` with an ordinary push is its conflict repair, the same as for any other layer. Cyclone patches a land PR when auto-patch is on for that stack, otherwise you do: when `busy[]` shows no Cyclone run on it, fix its blocker with the smallest patch on `mg-stack-N` and push with an ordinary push, never a force push; while Cyclone is patching it the envelope stays `in_progress` with `busy[]`, so wait. For a conflict, prefer merging that live parent into the PR branch, resolving locally, then an ordinary push. Do not click Dashboard Continue yourself: Continue is a human action, and you relay it to the human only when step 5 or a `repair` names it. When the live parent is a sibling, never rebase onto trunk or `mg-stack-N` instead. Parked-freeze DIRTY alone needs no conflict patch. Leave GitHub bases as Mergestorm set them: the bottom of a 2+ PR stack is based on `mg-stack-N`, layer 2 on layer 1, PR3+ on `mg-park-*`.
45
+ 4. Use a clean worktree on that named PR's head branch and record the remote starting SHA. Fix the current blocker with the smallest patch, and run the relevant checks. For conflicts, the live parent is the sibling PR head or the unit branch (`mg-stack-N`, Mergestorm's owned trunk, not an alias for main) — not the DTO `parentBranch` when that field starts with `mg-park-*`. If `parentBranch` is `mg-park-*`, never merge or rebase onto it; if the real live parent is unknown, stop. The land PR is the one PR whose head is `mg-stack-N`: its live parent is its base (`main`), and merging that base into `mg-stack-N` with an ordinary push is its conflict repair, the same as for any other layer. Cyclone patches Vortex findings on a land PR when auto-patch is on for that stack; every other land PR blocker, a conflict with main or failing CI included, is yours: when `busy[]` shows no Cyclone run on it, fix it with the smallest patch on `mg-stack-N` and push with an ordinary push, never a force push; while Cyclone is patching findings on it the envelope stays `in_progress` with `busy[]`, so wait. For a conflict, prefer merging that live parent into the PR branch, resolving locally, then an ordinary push. Do not click Dashboard Continue yourself: Continue is a human action, and you relay it to the human only when step 5 or a `repair` names it. When the live parent is a sibling, never rebase onto trunk or `mg-stack-N` instead. Parked-freeze DIRTY alone needs no conflict patch. Leave GitHub bases as Mergestorm set them: the bottom of a 2+ PR stack is based on `mg-stack-N`, layer 2 on layer 1, PR3+ on `mg-park-*`.
42
46
  5. CI-only fixes may run while Cyclone auto-patch is still on for the stack. Auto-patch may stay on, and you still wait until the current Vortex run and the current Cyclone run on that PR have finished before the follow-up commit. When the gate is clean, inspect `issues[]` and fix a live upstack CI failure one PR at a time, refreshing the gate before the next PR. Do not use higher-layer bounce history as a patch target. Do not fight Cyclone: if the remote head moved, stop and tell the human. Do not take over Cyclone's review-finding work while it is active; if the blocker requires that or a policy/queue action rather than a concrete patch, stop and explain what needs human attention. On a `Seam findings` blocker, read that PR's seam finding, the Vortex review "Integration review — IU <n> seam" (`gh api repos/OWNER/REPO/pulls/N/reviews` and `gh api repos/OWNER/REPO/pulls/N/comments`), and patch its concrete findings on that PR's head; a "Decision required" seam finding needs a human, so tell them and do not guess. A seam finding you verified is wrong is recorded with `mg dismiss` (the audited dismissal in `mergestorm-pr-loop`) at that PR's live head and that seam review's id; a `mergestorm-loop: dismiss` comment alone does not clear the seam gate. On `Seam review failed`, do not patch; tell the human, unless a new push already has a seam re-review held in `busy[]`. On `Seam review pending, no review running`, do not patch; tell the human: the seam review was skipped (usually because the monthly review limit is reached) and nothing re-runs it until they raise the limit and click Continue on the stack, or push a new head. On `Seam review stuck in reviewing, no review running`, `Seam re-review pending, no review running`, or `Seam verdict is for an older head, no review running` (repair kind `seam_review_stuck`), do not patch or push; tell the human: no seam review is queued or running for that PR's live head, and clicking Continue on the stack re-queues it (a new pushed head also does). On `Review skipped, out of review quota`, do not patch; tell the human: the review credit limit for the period is reached and nothing re-reviews that head until they raise the limit and push a new head or re-run the review.
43
47
  6. Recheck Auto land (off is expected only after a disarming bounce you are handling) and the remote PR head (`git ls-remote origin refs/heads/<branch>`) immediately before pushing. Commit the patch and push the PR head with an ordinary push. A rebase onto that same live parent followed by force-with-lease is allowed only if the remote is still exactly the SHA you started from and Cyclone did not move it; use an explicit lease bound to that starting SHA. This exception never permits rebasing onto a parked freeze or substituting trunk / the unit for a sibling parent. If the remote head moved or the push is rejected because it moved, stop; do not rebase and retry.
44
48
  7. After your successful push, leave a clean worktree and keep watching the named stack. Start a new watch cycle with `enrolled_head_sha` set to the SHA you pushed (in the background: `mg stack watch <stack-id> --head <pushed-sha>`). If you handled a bounce, retain the returned `cursor.afterFinishedAt` as `after_finished_at` and `cursor.bounceId` as `bounce_id` so that attempt is not handled again. Keep these selectors unchanged across subsequent retries. Continue watching; do not enqueue anything. If the bounce you handled turned Auto land off, turn it back on as "Re-arm after a bounce" allows; if that section does not allow it, report and stop.