mergestorm 0.3.30 → 0.3.32

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.30 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.32 │
49
49
  │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
50
  │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
51
  │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
@@ -129,7 +129,7 @@ Use `mergestorm dismiss` when a Vortex finding is wrong and you have checked it
129
129
  - Retrying the same dismissal is safe; it reports the earlier record.
130
130
  - The seam gate clears only when every finding of that integration review is dismissed and the review is still the current seam verdict. CI, other reviews and Auto land policy still apply.
131
131
  - Vortex does not raise a dismissed finding again on the same diff (the same head, or a restack with an unchanged diff).
132
- - With `--json`, the result carries `gate` and, for a stack PR, `watch.next` (the next `stack_wait` call).
132
+ - With `--json`, the result carries `gate` and, for a stack PR, `watch.next` (the next `stack_wait` call, plus `watch.next.background`, the `mg stack watch` command to run in the background instead).
133
133
 
134
134
  `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`.
135
135
 
package/dist/api.d.ts CHANGED
@@ -29,6 +29,13 @@ export declare function devicePost(base: string, route: string, json: unknown, o
29
29
  status: number;
30
30
  body: any;
31
31
  }>;
32
+ export type SurgeUsage = {
33
+ includedMinutes: number;
34
+ usedMinutes: number;
35
+ walletMinutes: number;
36
+ windowStart: string;
37
+ pricePerMinuteUsd: number;
38
+ };
32
39
  export type MeResponse = {
33
40
  key: {
34
41
  prefix: string;
@@ -50,6 +57,7 @@ export type MeResponse = {
50
57
  bonus?: {
51
58
  remaining: number;
52
59
  };
60
+ surge?: SurgeUsage;
53
61
  };
54
62
  };
55
63
  export type JobListItem = {
package/dist/api.js CHANGED
@@ -570,6 +570,12 @@ async function queueMutation(route, failLabel, cfg) {
570
570
  }
571
571
  throw new CommandError("Merge queue API is not available on this server yet. Deploy the API update or use the dashboard.");
572
572
  }
573
+ if (status === 409) {
574
+ const refusal = body;
575
+ if (refusal?.error === "conflict_unchanged" && typeof refusal.message === "string") {
576
+ throw new CommandError(refusal.message);
577
+ }
578
+ }
573
579
  if (status < 200 || status >= 300) {
574
580
  throw new CommandError(`${failLabel} (HTTP ${status}): ${JSON.stringify(body)}`);
575
581
  }
@@ -8,4 +8,4 @@ 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
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";
11
- export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, type StackWatchDoneReason, type StackWatchNext, type StackWatchNextArgs, type StackWatchObligation, type StackWatchObligationInput, type StackWatchReason, } from "../stack-watch-obligation.js";
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";
@@ -8,4 +8,4 @@ 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
10
  export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, } from "../stack-blockers.js";
11
- export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.js";
11
+ export { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackWatchBackgroundCommand, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.js";
@@ -221,6 +221,7 @@ export async function openTabsBrowser(initial) {
221
221
  used: me.usage.standard.used,
222
222
  limit: me.usage.standard.limit,
223
223
  bonusRemaining: me.usage.bonus?.remaining,
224
+ surge: me.usage.surge,
224
225
  resetsAt: me.resets_at,
225
226
  jobs: jobs.slice(0, 5).map(toPanelJob),
226
227
  jobsError: jobsError ?? undefined,
@@ -37,6 +37,7 @@ export async function cmdCredits(args) {
37
37
  used: s.used,
38
38
  limit: s.limit,
39
39
  bonusRemaining: me.usage.bonus?.remaining,
40
+ surge: me.usage.surge,
40
41
  resetsAt: me.resets_at,
41
42
  jobs: recentJobs.map(toPanelJob),
42
43
  columns,
@@ -120,7 +120,7 @@ export function formatDismissResult(result, watch) {
120
120
  : `Seam gate: not blocking (seam_state=${seam.state}).`);
121
121
  }
122
122
  if (watch?.next)
123
- lines.push(`Next: ${watch.next.command}`);
123
+ lines.push(`Next: ${watch.next.background} as a background command, or ${watch.next.command}`);
124
124
  return lines.join("\n");
125
125
  }
126
126
  export async function cmdDismiss(args, deps = {}) {
@@ -1,5 +1,5 @@
1
1
  import { stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
2
- import { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
2
+ import { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
3
3
  import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, landNextStack, listStacks, openStackPull, restackStack, setStackPolicy, } from "../api.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
@@ -36,7 +36,10 @@ export function stackWatchTextLines(watch) {
36
36
  return [watch.message];
37
37
  const next = watch.next;
38
38
  return [
39
- ...(next ? [`Next: ${next.command} (MCP: stack_wait ${JSON.stringify(next.args)})`] : []),
39
+ ...(next ? [
40
+ `Next: ${next.background} as a background command (notify on ${STACK_WATCH_NOTIFY_PATTERN}, then end your turn)`,
41
+ `Or poll: ${next.command} (MCP: stack_wait ${JSON.stringify(next.args)})`,
42
+ ] : []),
40
43
  STACK_WATCH_NOT_DONE_SENTENCE,
41
44
  ];
42
45
  }
@@ -390,6 +390,8 @@ export type MergeQueueBounceDetail = {
390
390
  conflictBranch?: string;
391
391
  conflictDetail?: string;
392
392
  conflictPaths?: string[];
393
+ baseBranch?: string;
394
+ baseSha?: string;
393
395
  message?: string;
394
396
  warning?: string;
395
397
  batch?: MergeQueueBounceBatch;
@@ -1,5 +1,7 @@
1
1
  import type { StackDto } from "./stack-dto.js";
2
2
  export declare const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
3
+ export declare const STACK_WATCH_NOTIFY_PATTERN = "MS-WATCH (ATTENTION|LANDED)";
4
+ export declare function stackWatchBackgroundCommand(stackId: string): string;
3
5
  export type StackWatchDoneReason = "landed" | "closed" | "archived" | "not_found";
4
6
  export type StackWatchReason = StackWatchDoneReason | "open" | "attention" | "unread" | "rate_limited" | "failed";
5
7
  export type StackWatchNextArgs = {
@@ -13,6 +15,7 @@ export type StackWatchNext = {
13
15
  tool: "stack_wait";
14
16
  args: StackWatchNextArgs;
15
17
  command: string;
18
+ background: string;
16
19
  };
17
20
  export type StackWatchObligation = {
18
21
  done: boolean;
@@ -1,4 +1,8 @@
1
1
  export const STACK_WATCH_NOT_DONE_SENTENCE = "This stack is not landed. Your task is not done. Call stack_wait again with this cursor.";
2
+ export const STACK_WATCH_NOTIFY_PATTERN = "MS-WATCH (ATTENTION|LANDED)";
3
+ export function stackWatchBackgroundCommand(stackId) {
4
+ return `mg stack watch ${stackId}`;
5
+ }
2
6
  const DONE_MESSAGES = {
3
7
  landed: "This stack is landed. The watch is done; stop calling stack_wait for it.",
4
8
  closed: "Every PR in this stack is merged or closed and nothing is left to land, but not every PR landed. The watch is done; tell the human which PRs were closed without landing.",
@@ -49,8 +53,10 @@ export function stackWatchObligation(input) {
49
53
  : input.status === "failed"
50
54
  ? "failed"
51
55
  : input.attention ? "attention" : input.unread ? "unread" : "open";
56
+ const background = stackWatchBackgroundCommand(input.stackId);
52
57
  const parts = [
53
58
  STACK_WATCH_NOT_DONE_SENTENCE,
59
+ `If you cannot hold a long turn open, run \`${background}\` as a background command instead, notify on output matching ${STACK_WATCH_NOTIFY_PATTERN}, and end your turn: it keeps calling stack_wait for you, stays silent while the stack waits, and wakes you on attention or when the watch is done.`,
54
60
  `Watch stack ${input.stackId} as a whole until it lands or the human takes it back. A clean push, a submit, or one merged layer does not finish it: merging a lower layer can break a layer above it.`,
55
61
  ];
56
62
  if (reason === "rate_limited") {
@@ -60,7 +66,7 @@ export function stackWatchObligation(input) {
60
66
  parts.push(`This read failed, which does not end the task. Call stack_wait once more with the same cursor; if that fails too, tell the human that stack ${input.stackId} is not landed and is no longer being watched.`);
61
67
  }
62
68
  else if (reason === "attention" && input.attention) {
63
- parts.push(`First fix #${input.attention.prNumber} (${input.attention.blocker}); repair, when present, names the fix. Calling stack_wait again before fixing it returns the same attention. After your push, call stack_wait with this cursor and enrolled_head_sha set to the SHA you pushed.`);
69
+ parts.push(`First fix #${input.attention.prNumber} (${input.attention.blocker}); repair, when present, names the fix. Calling stack_wait again before fixing it returns the same attention. After your push, call stack_wait with this cursor and enrolled_head_sha set to the SHA you pushed, or start \`${background} --head <pushed-sha>\` in the background.`);
64
70
  }
65
71
  else if (reason === "unread") {
66
72
  parts.push("This tool did not read the stack's state; stack_wait will.");
@@ -73,7 +79,7 @@ export function stackWatchObligation(input) {
73
79
  done: false,
74
80
  until: "landed",
75
81
  reason,
76
- next: { tool: "stack_wait", args, command: `mg stack wait ${input.stackId} --json` },
82
+ next: { tool: "stack_wait", args, command: `mg stack wait ${input.stackId} --json`, background },
77
83
  message: parts.join(" "),
78
84
  };
79
85
  }
package/dist/ui/banner.js CHANGED
@@ -123,6 +123,12 @@ const WHATS_NEW = {
123
123
  "0.3.30": [
124
124
  "stack watch --json prints one JSON document",
125
125
  ],
126
+ "0.3.31": [
127
+ "every stack Next line offers mg stack watch",
128
+ ],
129
+ "0.3.32": [
130
+ "queue add explains a held merge conflict",
131
+ ],
126
132
  };
127
133
  function densityFromVariant(variant) {
128
134
  if (variant === "mini" || variant === "nano")
@@ -24,6 +24,12 @@ export declare function formatResetLabel(iso: string | null | undefined, now?: D
24
24
  export declare function formatDaysLeft(iso: string | null | undefined, now?: Date): string;
25
25
  /** Bar cells so ` [bar] 100% used` stays inside the terminal. */
26
26
  export declare function usageBarWidth(columns: number): number;
27
+ export type SurgeUsageSummary = {
28
+ includedMinutes: number;
29
+ usedMinutes: number;
30
+ walletMinutes: number;
31
+ };
32
+ export declare function formatSurgeSummary(surge: SurgeUsageSummary): string;
27
33
  export type UsagePanelJob = {
28
34
  job: string;
29
35
  verdict: string;
@@ -37,6 +43,7 @@ export type UsagePanelInput = {
37
43
  used: number;
38
44
  limit: number | null;
39
45
  bonusRemaining?: number;
46
+ surge?: SurgeUsageSummary;
40
47
  resetsAt: string | null | undefined;
41
48
  jobs: UsagePanelJob[];
42
49
  /** When the jobs fetch failed, show this instead of "No review jobs yet." */
package/dist/ui/usage.js CHANGED
@@ -87,6 +87,16 @@ export function formatDaysLeft(iso, now = new Date()) {
87
87
  export function usageBarWidth(columns) {
88
88
  return Math.max(8, frameWidth(80, columns) - 13);
89
89
  }
90
+ function formatMinutes(value) {
91
+ return value.toLocaleString("en-US", { maximumFractionDigits: 1 });
92
+ }
93
+ export function formatSurgeSummary(surge) {
94
+ const wallet = `wallet ${formatMinutes(surge.walletMinutes)} min`;
95
+ if (surge.includedMinutes <= 0)
96
+ return `Surge: ${wallet}`;
97
+ return (`Surge: ${formatMinutes(surge.usedMinutes)} of ${formatMinutes(surge.includedMinutes)} included min this period` +
98
+ ` · ${wallet}`);
99
+ }
90
100
  function clipLine(line, columns) {
91
101
  if (visibleWidth(line) <= columns)
92
102
  return line;
@@ -111,6 +121,7 @@ export function formatUsagePanel(input) {
111
121
  ...(input.bonusRemaining == null
112
122
  ? []
113
123
  : [` Bonus credits: ${input.bonusRemaining} remaining`]),
124
+ ...(input.surge ? [` ${formatSurgeSummary(input.surge)}`] : []),
114
125
  "",
115
126
  ];
116
127
  if (input.jobs.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.30",
3
+ "version": "0.3.32",
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",
@@ -15,7 +15,7 @@ A stack watch is done only when the stack is landed, or when the human takes it
15
15
 
16
16
  If your host cannot hold a long turn open (Cursor, Claude Code, Codex, and other turn-based agents), do not poll `stack_wait` inside your turn and do not subscribe to GitHub events. Hand the waiting to the CLI watcher:
17
17
 
18
- 1. Run `mg stack watch <stack-id>` as a background command (add `--head <sha>` right after you pushed that SHA) with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs the same stack wait against the Mergestorm API in slices of at most 300s, carries the cursor (`enrolled_head_sha`, `after_finished_at`, `bounce_id`) from one slice to the next, and prints nothing while the stack is `waiting` or `in_progress`, including `busy[]` holds and `actAfter: "agents_idle"`. It spends no model tokens while it waits. Ending your turn while it runs is not ending the task.
18
+ 1. Run `mg stack watch <stack-id>` as a background command (add `--head <sha>` right after you pushed that SHA) with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs the same stack wait against the Mergestorm API in slices of at most 300s, carries the cursor (`enrolled_head_sha`, `after_finished_at`, `bounce_id`) from one slice to the next, and prints nothing while the stack is `waiting` or `in_progress`, including `busy[]` holds and `actAfter: "agents_idle"`. It spends no model tokens while it waits. Ending your turn while it runs is not ending the task. Every `watch.next` carries this command as `watch.next.background`.
19
19
  2. It prints one line and exits for one of three reasons:
20
20
  - `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` (exit 3): the watch returned `attention`.
21
21
  - `MS-WATCH ATTENTION failed stack=<id> error="<message>"` (exit 3): a read failed and its one retry failed too.