mergestorm-mcp 0.2.27 → 0.2.28

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/dist/index.js CHANGED
@@ -3,24 +3,27 @@
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
- // src/tools/chat-wake.ts
6
+ // src/tools/chat-task.ts
7
7
  import { apiFetch } from "mergestorm/client";
8
8
  var CHAT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
9
  function breezeChatId(value) {
10
10
  const trimmed = value?.trim() ?? "";
11
11
  return CHAT_ID.test(trimmed) ? trimmed.toLowerCase() : null;
12
12
  }
13
- var CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
14
- var REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
15
- var CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
16
- async function registerChatWake(cfg, chatId, target2, signal) {
17
- const json = target2.kind === "vortex" ? { chat_id: chatId, kind: "vortex", owner: target2.owner, repo: target2.repo, pr_number: target2.prNumber, head_sha: target2.headSha, min_pass: target2.headSha ? target2.minPass : null } : { chat_id: chatId, kind: "stack", stack_id: target2.stackId };
13
+ function chatTaskOf(value) {
14
+ if (!value || typeof value !== "object") return null;
15
+ const { id, title } = value;
16
+ return typeof id === "string" && typeof title === "string" ? { id, title } : null;
17
+ }
18
+ function chatTaskNote(task) {
19
+ return `This chat tracks it as background task ${task.id} "${task.title}" and is told the result when it ends. If the user is waiting for the outcome, wait for it before you end the reply, then tell them what actually happened.`;
20
+ }
21
+ async function trackInChat(cfg, chatId, target2, signal) {
22
+ const json = target2.kind === "review" ? { chat_id: chatId, kind: "review", owner: target2.owner, repo: target2.repo, pr_number: target2.prNumber } : { chat_id: chatId, kind: "stack", stack_id: target2.stackId };
18
23
  try {
19
24
  const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 1e4 });
20
25
  const body = response.body;
21
- if (response.status !== 200 || !body || typeof body.id !== "string") return null;
22
- if (body.result !== "waiting" && body.result !== "existing" && body.result !== "finished") return null;
23
- return { id: body.id, result: body.result };
26
+ return response.status === 200 ? chatTaskOf(body?.task) : null;
24
27
  } catch {
25
28
  return null;
26
29
  }
@@ -28,8 +31,8 @@ async function registerChatWake(cfg, chatId, target2, signal) {
28
31
 
29
32
  // src/server.ts
30
33
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
31
- import { z as z5 } from "zod";
32
- import { CommandError as CommandError15, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
34
+ import { z as z6 } from "zod";
35
+ import { CommandError as CommandError16, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
33
36
 
34
37
  // src/brand-icons.ts
35
38
  var MARK_RECTS = [
@@ -123,9 +126,9 @@ async function issueCreate(input, cfg, deps = {}) {
123
126
  const message = "issue_create needs a one-line title of at most 256 characters, and owner and repo together";
124
127
  return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
125
128
  }
126
- const { title, body, labels, owner, repo, cwd } = parsed.data;
129
+ const { title, body, labels, owner, repo: repo2, cwd } = parsed.data;
127
130
  try {
128
- const target2 = owner && repo ? { owner, repo } : (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(cwd);
131
+ const target2 = owner && repo2 ? { owner, repo: repo2 } : (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(cwd);
129
132
  if (!target2) {
130
133
  const message = "Could not read a GitHub owner/repo from origin. Pass owner and repo.";
131
134
  return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
@@ -154,10 +157,96 @@ async function issueCreate(input, cfg, deps = {}) {
154
157
  }
155
158
  }
156
159
 
160
+ // src/tools/memory.ts
161
+ import {
162
+ CommandError as CommandError3,
163
+ MEMORY_KINDS,
164
+ MEMORY_MODES,
165
+ MEMORY_SCOPES,
166
+ MEMORY_SEARCH_LIMIT_MAX,
167
+ deleteMemory,
168
+ getMemoryMode,
169
+ listMemories,
170
+ saveMemory,
171
+ searchMemories,
172
+ setMemoryMode,
173
+ updateMemory
174
+ } from "mergestorm/client";
175
+ import { z as z2 } from "zod";
176
+ var memoryId = z2.string().uuid();
177
+ var memoryText = z2.string().trim().min(1).max(500);
178
+ var repo = z2.string().trim().min(3).max(200);
179
+ var listMemoriesSchema = {
180
+ scope: z2.enum(MEMORY_SCOPES).optional(),
181
+ repo: repo.optional(),
182
+ kind: z2.enum(MEMORY_KINDS).optional()
183
+ };
184
+ var searchMemorySchema = {
185
+ query: z2.string().trim().min(1).max(2e3),
186
+ repo: repo.optional(),
187
+ limit: z2.number().int().min(1).max(MEMORY_SEARCH_LIMIT_MAX).optional()
188
+ };
189
+ var rememberSchema = { text: memoryText, kind: z2.enum(MEMORY_KINDS), repo: repo.optional() };
190
+ var updateMemorySchema = { id: memoryId, text: memoryText, kind: z2.enum(MEMORY_KINDS).optional() };
191
+ var forgetMemorySchema = { id: memoryId };
192
+ var setMemoryModeSchema = { memory_mode: z2.enum(MEMORY_MODES) };
193
+ var liveApi = { listMemories, searchMemories, saveMemory, updateMemory, deleteMemory, getMemoryMode, setMemoryMode };
194
+ function memoryLine(memory) {
195
+ return `- [${memory.id}] (${memory.repo ? `${memory.kind}, ${memory.repo}` : memory.kind}) ${memory.text}`;
196
+ }
197
+ async function guarded(run) {
198
+ try {
199
+ return await run();
200
+ } catch (err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ const code = err instanceof CommandError3 ? err.code ?? "memory_failed" : "memory_failed";
203
+ return {
204
+ summary: message,
205
+ data: { error: { code, message, ...err instanceof CommandError3 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {} } },
206
+ isError: true
207
+ };
208
+ }
209
+ }
210
+ function createMemoryToolHandlers(cfg, chatId, api = liveApi) {
211
+ return {
212
+ list: (input) => guarded(async () => {
213
+ const listed = await api.listMemories(input, cfg);
214
+ const count = listed.items.length === 1 ? "1 memory" : `${listed.items.length} memories`;
215
+ return { summary: [`Memory is ${listed.memory_mode}. ${count}.`, ...listed.items.map(memoryLine)].join("\n"), data: listed };
216
+ }),
217
+ search: (input) => guarded(async () => {
218
+ const found = await api.searchMemories({ ...input, chatId }, cfg);
219
+ const all = [...found.rules, ...found.items];
220
+ const summary = found.memory_mode === "off" ? "Memory is off, so nothing was searched." : all.length === 0 ? "Nothing in memory matches." : ["Memories that match, best first:", ...all.map(memoryLine)].join("\n");
221
+ return { summary, data: found };
222
+ }),
223
+ remember: (input) => guarded(async () => {
224
+ const saved = await api.saveMemory({ ...input, chatId }, cfg);
225
+ return { summary: saved.merged ? `That matched memory ${saved.id}, which now says this.` : `Saved to memory as ${saved.id}.`, data: saved };
226
+ }),
227
+ update: (input) => guarded(async () => {
228
+ const updated = await api.updateMemory(input.id, { text: input.text, ...input.kind ? { kind: input.kind } : {} }, cfg);
229
+ return { summary: `Memory ${updated.id} updated.`, data: updated };
230
+ }),
231
+ forget: (input) => guarded(async () => {
232
+ const deleted = await api.deleteMemory(input.id, cfg);
233
+ return { summary: `Memory ${deleted.id} forgotten.`, data: deleted };
234
+ }),
235
+ getMode: () => guarded(async () => {
236
+ const mode = await api.getMemoryMode(cfg);
237
+ return { summary: `Memory is ${mode.memory_mode}.`, data: mode };
238
+ }),
239
+ setMode: (input) => guarded(async () => {
240
+ const mode = await api.setMemoryMode(input.memory_mode, cfg);
241
+ return { summary: `Memory is now ${mode.memory_mode}.`, data: mode };
242
+ })
243
+ };
244
+ }
245
+
157
246
  // src/tools/queue-status.ts
158
247
  import {
159
248
  apiFetch as apiFetch2,
160
- CommandError as CommandError3,
249
+ CommandError as CommandError4,
161
250
  isCommandErrorCode,
162
251
  loadConfig as loadConfig2,
163
252
  rateLimitedMessage,
@@ -165,12 +254,12 @@ import {
165
254
  } from "mergestorm/client";
166
255
  function entrySummary(entry) {
167
256
  const prNumber = entry.bounceDetail?.prNumber;
168
- const repo = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
257
+ const repo2 = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
169
258
  const reason = entry.bounceDetail?.kind ?? entry.bounceReason ?? entry.waitReason ?? void 0;
170
259
  const sha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha ?? void 0;
171
260
  return [
172
261
  entry.state,
173
- repo,
262
+ repo2,
174
263
  `stack ${entry.stackId}`,
175
264
  reason,
176
265
  sha?.slice(0, 7)
@@ -185,12 +274,12 @@ async function queueStatus(stackId, cfg) {
185
274
  id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
186
275
  );
187
276
  if (status === 404) {
188
- throw new CommandError3(
277
+ throw new CommandError4(
189
278
  "Merge queue route returned 404. Deploy the API."
190
279
  );
191
280
  }
192
281
  if (status === 429) {
193
- throw new CommandError3(
282
+ throw new CommandError4(
194
283
  rateLimitedMessage(retryAfterSeconds),
195
284
  REVIEW_EXIT.rate_limited,
196
285
  "rate_limited",
@@ -198,13 +287,13 @@ async function queueStatus(stackId, cfg) {
198
287
  );
199
288
  }
200
289
  if (status !== 200) {
201
- throw new CommandError3(
290
+ throw new CommandError4(
202
291
  `Failed to get queue status (HTTP ${status}): ${JSON.stringify(body)}`
203
292
  );
204
293
  }
205
294
  const entries = body.entries;
206
295
  if (!Array.isArray(entries)) {
207
- throw new CommandError3(
296
+ throw new CommandError4(
208
297
  `Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
209
298
  );
210
299
  }
@@ -270,13 +359,13 @@ import {
270
359
  stackWatchBackgroundCommand,
271
360
  stackWatchObligation
272
361
  } from "mergestorm/client";
273
- import { z as z2 } from "zod";
362
+ import { z as z3 } from "zod";
274
363
  var stackWaitSchema = {
275
- stack_id: z2.string().trim().min(1),
276
- timeout_s: z2.number().finite().min(0).max(45).default(45),
277
- enrolled_head_sha: z2.string().nullable().optional(),
278
- after_finished_at: z2.string().nullable().optional(),
279
- bounce_id: z2.string().nullable().optional()
364
+ stack_id: z3.string().trim().min(1),
365
+ timeout_s: z3.number().finite().min(0).max(45).default(45),
366
+ enrolled_head_sha: z3.string().nullable().optional(),
367
+ after_finished_at: z3.string().nullable().optional(),
368
+ bounce_id: z3.string().nullable().optional()
280
369
  };
281
370
  function repairSummary(repair) {
282
371
  if (!repair) return "";
@@ -290,6 +379,58 @@ function withWatchText(watch, body) {
290
379
 
291
380
  ${body}`;
292
381
  }
382
+ var KEEP_EARLIER_CURSOR = "If you already hold a cursor from an earlier stack_wait on this stack, keep passing that cursor instead.";
383
+ function hostedStackWatch(watch) {
384
+ if (watch.done) {
385
+ return {
386
+ ...watch,
387
+ next: null,
388
+ message: watch.reason === "not_found" ? "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, which can mean merged or closed, so landing is unconfirmed: check on GitHub that each PR is merged before reporting it landed. The watch is done." : watch.message
389
+ };
390
+ }
391
+ const args = watch.next?.args;
392
+ const attention = watch.reason === "attention";
393
+ const retry = watch.reason === "rate_limited" ? " Wait retry_after_seconds first." : "";
394
+ const failed = watch.reason === "failed" ? " This read failed; if the next one fails too, tell the human the stack is no longer watched." : "";
395
+ const keep = watch.message.includes(KEEP_EARLIER_CURSOR) ? ` ${KEEP_EARLIER_CURSOR}` : "";
396
+ const resume = args ? attention ? ` After the blocker is handled, call stack_wait ${JSON.stringify(args)}.` : ` Next call: stack_wait ${JSON.stringify(args)}.` : "";
397
+ return {
398
+ ...watch,
399
+ next: args && !attention ? { tool: "stack_wait", args } : null,
400
+ message: `This stack is not landed and the watch is not done.${retry}${failed}${resume}${keep}`
401
+ };
402
+ }
403
+ function hostedStackRepair(repair) {
404
+ const { steps: _steps, note: _note, findings: _findings, ...facts } = repair;
405
+ return facts;
406
+ }
407
+ function hostedRepairSummary(repair) {
408
+ const files = "files" in repair && repair.files.length ? ` Files: ${repair.files.join(", ")}.` : "";
409
+ const breeze = "publishedBy" in repair && repair.publishedBy === "breeze" ? ` ${repair.branch} is a Breeze branch${repair.liveParent ? ` on ${repair.liveParent}` : ""}: open_pull_request publishes it with single-parent commits only, so a merge commit cannot be published.` : "";
410
+ return `
411
+ Repair #${repair.prNumber} (${repair.kind}).${files}${breeze}`;
412
+ }
413
+ function hostedToolResult(result) {
414
+ const data = result.structuredContent;
415
+ const watch = data?.watch;
416
+ const repair = data?.repair;
417
+ const hasWatch = !!watch && typeof watch.message === "string";
418
+ const hasRepair = !!repair && typeof repair.steps === "string";
419
+ if (!data || !hasWatch && !hasRepair) return result;
420
+ let text = result.content[0]?.text ?? "";
421
+ const structured = { ...data };
422
+ if (hasWatch) {
423
+ const hosted = hostedStackWatch(watch);
424
+ structured.watch = hosted;
425
+ if (text.startsWith(watch.message)) text = `${hosted.message}${text.slice(watch.message.length)}`;
426
+ }
427
+ if (hasRepair) {
428
+ const hosted = hostedStackRepair(repair);
429
+ structured.repair = hosted;
430
+ text = text.replace(repairSummary(repair), hostedRepairSummary(repair));
431
+ }
432
+ return { ...result, content: [{ type: "text", text }, { type: "text", text: JSON.stringify(structured) }], structuredContent: structured };
433
+ }
293
434
  function envelopeWatch(envelope) {
294
435
  return envelope.watch ?? stackWatchObligation({
295
436
  stackId: envelope.stackId,
@@ -298,10 +439,10 @@ function envelopeWatch(envelope) {
298
439
  status: envelope.status
299
440
  });
300
441
  }
301
- function stackWaitSummary(envelope) {
302
- return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope)}${repairSummary(envelope.repair)}`);
442
+ function stackWaitSummary(envelope, hosted = false) {
443
+ return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope, hosted)}${repairSummary(envelope.repair)}`);
303
444
  }
304
- function stackWaitStatusLine(envelope) {
445
+ function stackWaitStatusLine(envelope, hosted) {
305
446
  const base = `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending, envelope.actAfter ?? null, envelope.queueWait ?? null)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`;
306
447
  if (envelopeWatch(envelope).done || envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
307
448
  const selectors = [
@@ -310,10 +451,11 @@ function stackWaitStatusLine(envelope) {
310
451
  ...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
311
452
  ...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
312
453
  ];
313
- return `${base} \xB7 watch not finished: only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId, envelope.cursor)} as a background command and end your turn; otherwise call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
454
+ const background = hosted ? "" : `only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId, envelope.cursor)} as a background command and end your turn; otherwise `;
455
+ return `${base} \xB7 watch not finished: ${background}call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
314
456
  }
315
457
  async function stackWait(input, cfg, deps = {}) {
316
- const args = z2.object(stackWaitSchema).parse(input);
458
+ const args = z3.object(stackWaitSchema).parse(input);
317
459
  const config = cfg ?? await loadConfig3();
318
460
  const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
319
461
  stackId: args.stack_id,
@@ -332,7 +474,7 @@ async function stackWait(input, cfg, deps = {}) {
332
474
  if (err instanceof StackWatchError) {
333
475
  const envelope2 = { ...err.lastEnvelope, watch: envelopeWatch(err.lastEnvelope) };
334
476
  return {
335
- summary: stackWaitSummary(envelope2),
477
+ summary: stackWaitSummary(envelope2, deps.hosted),
336
478
  data: {
337
479
  ...envelope2,
338
480
  ...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
@@ -344,12 +486,12 @@ async function stackWait(input, cfg, deps = {}) {
344
486
  envelope = err.lastEnvelope;
345
487
  }
346
488
  const sealed = { ...envelope, watch: envelopeWatch(envelope) };
347
- const wake = deps.chatId && !sealed.watch.done ? await registerChatWake(config, deps.chatId, { kind: "stack", stackId: sealed.stackId }, deps.signal) : null;
489
+ const task = deps.chatId && !sealed.watch.done ? await trackInChat(config, deps.chatId, { kind: "stack", stackId: sealed.stackId }, deps.signal) : null;
348
490
  return {
349
- summary: wake ? `${stackWaitSummary(sealed)}
491
+ summary: task ? `${stackWaitSummary(sealed, deps.hosted)}
350
492
 
351
- ${CHAT_WAKE_STACK_NOTE}` : stackWaitSummary(sealed),
352
- data: wake ? { ...sealed, chat_wake: wake } : sealed
493
+ ${chatTaskNote(task)}` : stackWaitSummary(sealed, deps.hosted),
494
+ data: task ? { ...sealed, background_task: task } : sealed
353
495
  };
354
496
  }
355
497
 
@@ -416,13 +558,13 @@ async function reviewDismiss(input, cfg) {
416
558
  }
417
559
 
418
560
  // src/tools/agent-request.ts
419
- import { apiFetch as apiFetch3, CommandError as CommandError4, loadConfig as loadConfig4 } from "mergestorm/client";
561
+ import { apiFetch as apiFetch3, CommandError as CommandError5, loadConfig as loadConfig4 } from "mergestorm/client";
420
562
  async function agentRequest(route, init, cfg) {
421
563
  const resolved = cfg ?? await loadConfig4();
422
564
  const { status, body, retryAfterSeconds } = await apiFetch3(resolved, route, { method: init.method, ...init.json ? { json: init.json } : {} });
423
565
  const data = body && typeof body === "object" && !Array.isArray(body) ? body : { body };
424
566
  if (status === 404 && data.error === void 0) {
425
- throw new CommandError4("This Mergestorm server does not offer that action yet.", 1, "not_found");
567
+ throw new CommandError5("This Mergestorm server does not offer that action yet.", 1, "not_found");
426
568
  }
427
569
  const message = typeof data.message === "string" ? data.message : `Mergestorm answered ${status}.`;
428
570
  if (status >= 400) {
@@ -454,7 +596,8 @@ async function reviewStartPr(input, cfg, chatId) {
454
596
  ...chatId ? { chat_id: chatId } : {}
455
597
  }
456
598
  }, cfg);
457
- return payload.data.chat_wake ? { ...payload, summary: `${payload.summary} ${REVIEW_START_WAIT_NOTE}` } : payload;
599
+ const task = chatTaskOf(payload.data.background_task);
600
+ return task ? { ...payload, summary: `${payload.summary} ${chatTaskNote(task)}` } : payload;
458
601
  }
459
602
  async function reviewStopPr(input, cfg) {
460
603
  return agentRequest("/api/v1/agents/vortex/stop", {
@@ -470,7 +613,6 @@ var CYCLONE_JOB_WAIT_MAX_S = 100;
470
613
  var CYCLONE_JOB_POLL_MS = 5e3;
471
614
  var CYCLONE_JOB_TERMINAL_STATES = ["pushed", "completed", "failed"];
472
615
  var CYCLONE_START_WAIT_NOTE = `Wait for it now: call cyclone_job with this dispatch_id and timeout_s ${CYCLONE_JOB_WAIT_DEFAULT_S}, again while it is queued or running, then tell the user what Cyclone pushed or why it failed.`;
473
- var CHAT_WAKE_CYCLONE_NOTE = "If your reply ends before Cyclone finishes, this chat is woken with the job's result.";
474
616
  var CYCLONE_JOB_STILL_RUNNING_NOTE = `Cyclone is not done. Call cyclone_job again with this dispatch_id and timeout_s ${CYCLONE_JOB_WAIT_DEFAULT_S}, and report the result once it is pushed, completed or failed.`;
475
617
  async function cycloneStart(input, cfg, chatId) {
476
618
  const payload = await agentRequest("/api/v1/agents/cyclone/jobs", {
@@ -486,7 +628,8 @@ async function cycloneStart(input, cfg, chatId) {
486
628
  }
487
629
  }, cfg);
488
630
  if (payload.isError || typeof payload.data.dispatch_id !== "string" || isCycloneJobTerminal(payload.data)) return payload;
489
- return { ...payload, summary: [payload.summary, CYCLONE_START_WAIT_NOTE, ...payload.data.chat_wake ? [CHAT_WAKE_CYCLONE_NOTE] : []].join(" ") };
631
+ const task = chatTaskOf(payload.data.background_task);
632
+ return { ...payload, summary: `${payload.summary} ${task ? chatTaskNote(task) : CYCLONE_START_WAIT_NOTE}` };
490
633
  }
491
634
  function isCycloneJobTerminal(job) {
492
635
  return CYCLONE_JOB_TERMINAL_STATES.includes(job.state);
@@ -540,11 +683,11 @@ async function stackEnqueue(stackId, cfg, chatId) {
540
683
  const resolved = cfg ?? await loadConfig5();
541
684
  const result = await enqueueStack(stackId, resolved);
542
685
  const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
543
- const wake = chatId ? await registerChatWake(resolved, chatId, { kind: "stack", stackId }) : null;
686
+ const task = chatId ? await trackInChat(resolved, chatId, { kind: "stack", stackId }) : null;
544
687
  const summary = withWatchText(watch, `Sent stack ${stackId} to the Mergestorm merge queue`);
545
688
  return {
546
- summary: wake ? `${summary} ${CHAT_WAKE_STACK_NOTE}` : summary,
547
- data: { stack_id: stackId, result, watch, ...wake ? { chat_wake: wake } : {} }
689
+ summary: task ? `${summary} ${chatTaskNote(task)}` : summary,
690
+ data: { stack_id: stackId, result, watch, ...task ? { background_task: task } : {} }
548
691
  };
549
692
  }
550
693
 
@@ -570,7 +713,7 @@ import { createHash } from "node:crypto";
570
713
  import { realpath } from "node:fs/promises";
571
714
  import { relative, resolve, sep } from "node:path";
572
715
  import {
573
- CommandError as CommandError5,
716
+ CommandError as CommandError6,
574
717
  REVIEW_THREAD_SLUG_RE,
575
718
  ReviewPollTimeoutError,
576
719
  collectReviewInput,
@@ -625,7 +768,7 @@ function assertPinnableSpecialists(specialists) {
625
768
  (id) => !PINNABLE_SPECIALISTS.includes(id)
626
769
  );
627
770
  if (unknown.length === 0) return;
628
- throw new CommandError5(
771
+ throw new CommandError6(
629
772
  `Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`,
630
773
  2,
631
774
  "usage"
@@ -637,7 +780,7 @@ function mcpSandboxRoot() {
637
780
  function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
638
781
  const seconds = timeout_s ?? defaultSeconds;
639
782
  if (!Number.isFinite(seconds) || seconds <= 0) {
640
- throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
783
+ throw new CommandError6("timeout_s must be a positive number of seconds", 2, "usage");
641
784
  }
642
785
  return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
643
786
  }
@@ -670,11 +813,11 @@ async function resolveInput(input) {
670
813
  ]);
671
814
  const relToSandbox = relative(sandboxReal, cwdReal);
672
815
  if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
673
- throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
816
+ throw new CommandError6("cwd must be inside the configured sandbox root", 2, "usage");
674
817
  }
675
818
  } catch (err) {
676
- if (err instanceof CommandError5) throw err;
677
- throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
819
+ if (err instanceof CommandError6) throw err;
820
+ throw new CommandError6("cwd must be inside the configured sandbox root", 2, "usage");
678
821
  }
679
822
  const base = input.base?.trim() || discoverTrunk(cwd);
680
823
  return collectReviewInput(base, head, cwd, sandboxRoot);
@@ -685,10 +828,10 @@ async function reviewSubmit(input, cfg, opts = {}) {
685
828
  const timeoutMs = waitTimeoutMs(input.timeout_s);
686
829
  assertPinnableSpecialists(input.specialists);
687
830
  if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
688
- throw new CommandError5("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
831
+ throw new CommandError6("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
689
832
  }
690
833
  if (input.context === "-") {
691
- throw new CommandError5(
834
+ throw new CommandError6(
692
835
  'context "-" (stdin) is not available over MCP; pass the text directly',
693
836
  2,
694
837
  "usage"
@@ -738,7 +881,7 @@ async function reviewSubmit(input, cfg, opts = {}) {
738
881
  };
739
882
  }
740
883
  if (!jobId) {
741
- throw new CommandError5("Review API returned no job_id");
884
+ throw new CommandError6("Review API returned no job_id");
742
885
  }
743
886
  if (!wait) {
744
887
  return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
@@ -774,15 +917,15 @@ import {
774
917
 
775
918
  // src/tools/review-get-pr.ts
776
919
  import {
777
- CommandError as CommandError6,
920
+ CommandError as CommandError7,
778
921
  getPrVortexReview,
779
922
  isCommandErrorCode as isCommandErrorCode3
780
923
  } from "mergestorm/client";
781
- function prReviewTarget(owner, repo, prNumber) {
924
+ function prReviewTarget(owner, repo2, prNumber) {
782
925
  const cleanOwner = owner.trim();
783
- const cleanRepo = repo.trim();
926
+ const cleanRepo = repo2.trim();
784
927
  if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
785
- throw new CommandError6(
928
+ throw new CommandError7(
786
929
  "owner, repo, and a positive pr_number are required",
787
930
  2,
788
931
  "usage"
@@ -790,8 +933,8 @@ function prReviewTarget(owner, repo, prNumber) {
790
933
  }
791
934
  return { owner: cleanOwner, repo: cleanRepo, prNumber };
792
935
  }
793
- function prReviewSummary(owner, repo, prNumber, envelope) {
794
- const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
936
+ function prReviewSummary(owner, repo2, prNumber, envelope) {
937
+ const bits = [`${owner}/${repo2}#${prNumber}`, envelope.status];
795
938
  const sha = envelope.head_sha?.trim();
796
939
  if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
797
940
  if (typeof envelope.finding_count === "number") {
@@ -802,16 +945,16 @@ function prReviewSummary(owner, repo, prNumber, envelope) {
802
945
  if (envelope.phase) bits.push(String(envelope.phase));
803
946
  return bits.join(" \xB7 ");
804
947
  }
805
- function payloadFromPrReview(owner, repo, prNumber, envelope) {
948
+ function payloadFromPrReview(owner, repo2, prNumber, envelope) {
806
949
  return {
807
- summary: prReviewSummary(owner, repo, prNumber, envelope),
950
+ summary: prReviewSummary(owner, repo2, prNumber, envelope),
808
951
  data: { ...envelope }
809
952
  };
810
953
  }
811
- function payloadFromPrRateLimit(err, owner, repo, prNumber) {
954
+ function payloadFromPrRateLimit(err, owner, repo2, prNumber) {
812
955
  if (!isCommandErrorCode3(err, "rate_limited")) return null;
813
956
  return {
814
- summary: `${owner}/${repo}#${prNumber} \xB7 rate_limited`,
957
+ summary: `${owner}/${repo2}#${prNumber} \xB7 rate_limited`,
815
958
  data: {
816
959
  status: "rate_limited",
817
960
  error: err.message,
@@ -819,8 +962,8 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
819
962
  }
820
963
  };
821
964
  }
822
- async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
823
- const target2 = prReviewTarget(owner, repo, prNumber);
965
+ async function reviewGetPr(owner, repo2, prNumber, cfg, opts = {}) {
966
+ const target2 = prReviewTarget(owner, repo2, prNumber);
824
967
  try {
825
968
  const envelope = await getPrVortexReview(
826
969
  target2.owner,
@@ -848,8 +991,8 @@ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
848
991
  }
849
992
 
850
993
  // src/tools/review-wait-pr.ts
851
- async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
852
- const target2 = prReviewTarget(owner, repo, prNumber);
994
+ async function reviewWaitPr(owner, repo2, prNumber, afterSha, timeout_s, cfg, opts = {}) {
995
+ const target2 = prReviewTarget(owner, repo2, prNumber);
853
996
  const resolved = cfg ?? await loadConfig7();
854
997
  if (timeout_s === 0) {
855
998
  return reviewGetPr(target2.owner, target2.repo, target2.prNumber, resolved, {
@@ -893,18 +1036,10 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
893
1036
  finding_count: findingCount,
894
1037
  phase: err.lastEnvelope?.phase ?? null
895
1038
  });
896
- const wakeHead = expectedSha || observedSha || null;
897
- const wake = opts.chatId ? await registerChatWake(resolved, opts.chatId, {
898
- kind: "vortex",
899
- owner: target2.owner,
900
- repo: target2.repo,
901
- prNumber: target2.prNumber,
902
- headSha: wakeHead,
903
- minPass: opts.pass ?? (opts.afterPass !== void 0 ? opts.afterPass + 1 : (headShaMatches || !expectedSha) && observedSha ? err.lastEnvelope?.pass ?? null : null)
904
- }) : null;
1039
+ const task = opts.chatId ? await trackInChat(resolved, opts.chatId, { kind: "review", owner: target2.owner, repo: target2.repo, prNumber: target2.prNumber }) : null;
905
1040
  const shown = err.reviewSeen ? summary : `${summary} \xB7 ${noPrReviewYetMessage(target2.owner, target2.repo, target2.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
906
1041
  return {
907
- summary: wake ? `${shown} \xB7 ${CHAT_WAKE_REVIEW_NOTE}` : shown,
1042
+ summary: task ? `${shown} \xB7 ${chatTaskNote(task)}` : shown,
908
1043
  data: {
909
1044
  status,
910
1045
  head_sha: err.lastEnvelope?.head_sha ?? null,
@@ -912,7 +1047,7 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
912
1047
  finding_count: findingCount,
913
1048
  phase: err.lastEnvelope?.phase ?? null,
914
1049
  error: err.message,
915
- ...wake ? { chat_wake: wake } : {}
1050
+ ...task ? { background_task: task } : {}
916
1051
  }
917
1052
  };
918
1053
  }
@@ -929,7 +1064,7 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
929
1064
 
930
1065
  // src/tools/review-wait.ts
931
1066
  import {
932
- CommandError as CommandError8,
1067
+ CommandError as CommandError9,
933
1068
  ReviewPollTimeoutError as ReviewPollTimeoutError2,
934
1069
  loadConfig as loadConfig8,
935
1070
  pollReview as pollReview2
@@ -937,7 +1072,7 @@ import {
937
1072
 
938
1073
  // src/tools/review-get.ts
939
1074
  import {
940
- CommandError as CommandError7,
1075
+ CommandError as CommandError8,
941
1076
  getReview,
942
1077
  toReviewJobEnvelope as toReviewJobEnvelope3
943
1078
  } from "mergestorm/client";
@@ -951,7 +1086,7 @@ async function reviewGet(jobId, cfg) {
951
1086
  throw err;
952
1087
  }
953
1088
  if (!row || typeof row !== "object" || typeof row.status !== "string") {
954
- throw new CommandError7(`Unexpected response for review ${jobId}.`);
1089
+ throw new CommandError8(`Unexpected response for review ${jobId}.`);
955
1090
  }
956
1091
  const envelope = toReviewJobEnvelope3(row, { jobId });
957
1092
  const bits = [envelope.status];
@@ -967,7 +1102,7 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
967
1102
  const resolved = cfg ?? await loadConfig8();
968
1103
  const id = jobId.trim();
969
1104
  if (!id) {
970
- throw new CommandError8("job_id is required", 2, "usage");
1105
+ throw new CommandError9("job_id is required", 2, "usage");
971
1106
  }
972
1107
  if (timeout_s === 0) return reviewGet(id, resolved);
973
1108
  try {
@@ -992,7 +1127,7 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
992
1127
 
993
1128
  // src/tools/settings-get.ts
994
1129
  import {
995
- CommandError as CommandError9,
1130
+ CommandError as CommandError10,
996
1131
  getSettings
997
1132
  } from "mergestorm/client";
998
1133
  function settingsSummary(settings) {
@@ -1010,7 +1145,7 @@ function settingsSummary(settings) {
1010
1145
  async function settingsGet(cfg) {
1011
1146
  const settings = await getSettings(cfg);
1012
1147
  if (!settings) {
1013
- throw new CommandError9(
1148
+ throw new CommandError10(
1014
1149
  "Settings are not available (offline or API too old). Try again or open https://mergestorm.ai/settings"
1015
1150
  );
1016
1151
  }
@@ -1022,7 +1157,7 @@ async function settingsGet(cfg) {
1022
1157
 
1023
1158
  // src/tools/settings-set.ts
1024
1159
  import {
1025
- CommandError as CommandError10,
1160
+ CommandError as CommandError11,
1026
1161
  BEARER_SETTINGS,
1027
1162
  SETTINGS_WRITABLE_KEYS,
1028
1163
  patchSettings
@@ -1034,7 +1169,7 @@ var SETTINGS_READ_ONLY_KEYS = [
1034
1169
  function settingsPatchFromArgs(args) {
1035
1170
  for (const key of SETTINGS_READ_ONLY_KEYS) {
1036
1171
  if (args[key] !== void 0) {
1037
- throw new CommandError10(`${key} is read-only and cannot be set.`, 2, "usage");
1172
+ throw new CommandError11(`${key} is read-only and cannot be set.`, 2, "usage");
1038
1173
  }
1039
1174
  }
1040
1175
  const patch = {};
@@ -1045,7 +1180,7 @@ function settingsPatchFromArgs(args) {
1045
1180
  if ("kind" in row && (row.kind === "seconds" || row.kind === "count")) {
1046
1181
  if (typeof value !== "number" || !Number.isInteger(value) || value < row.min || value > row.max) {
1047
1182
  const unit = row.kind === "seconds" ? " of seconds" : "";
1048
- throw new CommandError10(
1183
+ throw new CommandError11(
1049
1184
  `${key} takes a whole number${unit} from ${row.min} through ${row.max}.`,
1050
1185
  2,
1051
1186
  "usage"
@@ -1055,18 +1190,18 @@ function settingsPatchFromArgs(args) {
1055
1190
  continue;
1056
1191
  }
1057
1192
  if ("kind" in row) {
1058
- const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
1059
- if (!valid) throw new CommandError10(`Invalid value for ${key}.`, 2, "usage");
1193
+ const valid = row.kind === "logins" || row.kind === "paths" ? Array.isArray(value) && value.every((entry) => typeof entry === "string") : typeof value === "string" && row.values.includes(value);
1194
+ if (!valid) throw new CommandError11(`Invalid value for ${key}.`, 2, "usage");
1060
1195
  Object.assign(patch, { [key]: value });
1061
1196
  continue;
1062
1197
  }
1063
1198
  if (typeof value !== "boolean") {
1064
- throw new CommandError10(`${key} takes a boolean.`, 2, "usage");
1199
+ throw new CommandError11(`${key} takes a boolean.`, 2, "usage");
1065
1200
  }
1066
1201
  Object.assign(patch, { [key]: value });
1067
1202
  }
1068
1203
  if (Object.keys(patch).length === 0) {
1069
- throw new CommandError10(
1204
+ throw new CommandError11(
1070
1205
  `At least one settings key is required: ${SETTINGS_WRITABLE_KEYS.join(", ")}.`,
1071
1206
  2,
1072
1207
  "usage"
@@ -1085,7 +1220,7 @@ async function settingsSet(args, cfg) {
1085
1220
 
1086
1221
  // src/tools/repo-settings.ts
1087
1222
  import {
1088
- CommandError as CommandError11,
1223
+ CommandError as CommandError12,
1089
1224
  REPO_SETTINGS_KEYS,
1090
1225
  listRepoSettings,
1091
1226
  patchRepoSettings
@@ -1093,15 +1228,22 @@ import {
1093
1228
  var ARG_BY_KEY = {
1094
1229
  auto_patch_enabled: "auto_patch",
1095
1230
  auto_review_enabled: "auto_review",
1096
- auto_land_default: "auto_land"
1231
+ auto_land_default: "auto_land",
1232
+ merge_queue_batch_enabled: "merge_queue_batch",
1233
+ merge_queue_batch_size: "merge_queue_batch_size",
1234
+ merge_queue_lands_alone_paths: "merge_queue_lands_alone_paths",
1235
+ merge_queue_overdrive: "merge_queue_overdrive"
1097
1236
  };
1098
1237
  function parseRepo(raw) {
1099
1238
  const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
1100
- if (!match) throw new CommandError11("repo takes <owner>/<repo>.", 2, "usage");
1239
+ if (!match) throw new CommandError12("repo takes <owner>/<repo>.", 2, "usage");
1101
1240
  return { owner: match[1], repo: match[2] };
1102
1241
  }
1103
1242
  function words(value) {
1104
- return value === null ? "inherit" : value ? "on" : "off";
1243
+ if (value === null) return "inherit";
1244
+ if (Array.isArray(value)) return value.length === 0 ? "none" : value.join(",");
1245
+ if (typeof value === "number") return String(value);
1246
+ return value ? "on" : "off";
1105
1247
  }
1106
1248
  function repoSettingsSummary(entry) {
1107
1249
  return `${entry.owner}/${entry.repo}: ${REPO_SETTINGS_KEYS.map((key) => `${ARG_BY_KEY[key]} ${words(entry[key])}`).join(" \xB7 ")}`;
@@ -1111,13 +1253,27 @@ function repoSettingsPatchFromArgs(args) {
1111
1253
  for (const key of REPO_SETTINGS_KEYS) {
1112
1254
  const value = args[ARG_BY_KEY[key]];
1113
1255
  if (value === void 0) continue;
1256
+ if (key === "merge_queue_batch_size") {
1257
+ if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
1258
+ throw new CommandError12(`${ARG_BY_KEY[key]} takes a whole number or null.`, 2, "usage");
1259
+ }
1260
+ patch[key] = value;
1261
+ continue;
1262
+ }
1263
+ if (key === "merge_queue_lands_alone_paths") {
1264
+ if (value !== null && !(Array.isArray(value) && value.every((entry) => typeof entry === "string"))) {
1265
+ throw new CommandError12(`${ARG_BY_KEY[key]} takes a list of path patterns or null.`, 2, "usage");
1266
+ }
1267
+ patch[key] = value;
1268
+ continue;
1269
+ }
1114
1270
  if (value !== null && typeof value !== "boolean") {
1115
- throw new CommandError11(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
1271
+ throw new CommandError12(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
1116
1272
  }
1117
1273
  patch[key] = value;
1118
1274
  }
1119
1275
  if (Object.keys(patch).length === 0) {
1120
- throw new CommandError11(
1276
+ throw new CommandError12(
1121
1277
  `At least one of ${Object.values(ARG_BY_KEY).join(", ")} is required.`,
1122
1278
  2,
1123
1279
  "usage"
@@ -1136,18 +1292,18 @@ async function repoSettingsGet(args, cfg) {
1136
1292
  }
1137
1293
  const slug = `${wanted.owner}/${wanted.repo}`.toLowerCase();
1138
1294
  const entry = all.find((candidate) => `${candidate.owner}/${candidate.repo}`.toLowerCase() === slug);
1139
- if (!entry) throw new CommandError11(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
1295
+ if (!entry) throw new CommandError12(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
1140
1296
  return { summary: repoSettingsSummary(entry), data: { ...entry } };
1141
1297
  }
1142
1298
  async function repoSettingsSet(args, cfg) {
1143
- const { owner, repo } = parseRepo(args.repo);
1144
- const entry = await patchRepoSettings(owner, repo, repoSettingsPatchFromArgs(args), cfg);
1299
+ const { owner, repo: repo2 } = parseRepo(args.repo);
1300
+ const entry = await patchRepoSettings(owner, repo2, repoSettingsPatchFromArgs(args), cfg);
1145
1301
  return { summary: repoSettingsSummary(entry), data: { ...entry } };
1146
1302
  }
1147
1303
 
1148
1304
  // src/tools/stack-adopt.ts
1149
- import { adoptStack, CommandError as CommandError12, stackWatchObligation as stackWatchObligation5 } from "mergestorm/client";
1150
- import { z as z3 } from "zod";
1305
+ import { adoptStack, CommandError as CommandError13, stackWatchObligation as stackWatchObligation5 } from "mergestorm/client";
1306
+ import { z as z4 } from "zod";
1151
1307
 
1152
1308
  // src/tools/stack-set.ts
1153
1309
  import { setStackPolicy, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
@@ -1215,40 +1371,40 @@ async function stackSet(stackId, input, cfg) {
1215
1371
 
1216
1372
  // src/tools/stack-adopt.ts
1217
1373
  var stackAdoptSchema = {
1218
- owner: z3.string().trim().min(1),
1219
- repo: z3.string().trim().min(1),
1220
- pr_number: z3.number().int().min(1),
1221
- auto_land: z3.boolean().optional(),
1222
- auto_review: z3.boolean().nullable().optional(),
1223
- auto_patch: z3.boolean().nullable().optional(),
1224
- auto_resolve_conflicts: z3.boolean().nullable().optional(),
1225
- auto_fix_ci: z3.boolean().nullable().optional()
1374
+ owner: z4.string().trim().min(1),
1375
+ repo: z4.string().trim().min(1),
1376
+ pr_number: z4.number().int().min(1),
1377
+ auto_land: z4.boolean().optional(),
1378
+ auto_review: z4.boolean().nullable().optional(),
1379
+ auto_patch: z4.boolean().nullable().optional(),
1380
+ auto_resolve_conflicts: z4.boolean().nullable().optional(),
1381
+ auto_fix_ci: z4.boolean().nullable().optional()
1226
1382
  };
1227
- var inputSchema2 = z3.object(stackAdoptSchema);
1383
+ var inputSchema2 = z4.object(stackAdoptSchema);
1228
1384
  async function stackAdopt(input, cfg) {
1229
1385
  const parsed = inputSchema2.safeParse(input);
1230
1386
  if (!parsed.success) {
1231
1387
  const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
1232
1388
  return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
1233
1389
  }
1234
- const { owner, repo, pr_number } = parsed.data;
1390
+ const { owner, repo: repo2, pr_number } = parsed.data;
1235
1391
  try {
1236
1392
  const result = await adoptStack(
1237
1393
  owner,
1238
- repo,
1394
+ repo2,
1239
1395
  pr_number,
1240
1396
  cfg,
1241
1397
  stackSetPatch(parsed.data)
1242
1398
  );
1243
- const summary = `Adopted stack for ${owner}/${repo}#${pr_number}`;
1399
+ const summary = `Adopted stack for ${owner}/${repo2}#${pr_number}`;
1244
1400
  const stackId = typeof result === "object" && result !== null ? result.stack?.id : void 0;
1245
1401
  if (typeof stackId !== "string" || !stackId) {
1246
- return { summary, data: { owner, repo, pr_number, result } };
1402
+ return { summary, data: { owner, repo: repo2, pr_number, result } };
1247
1403
  }
1248
1404
  const watch = stackWatchObligation5({ stackId, terminal: null, unread: true, freshCursor: true });
1249
1405
  return {
1250
1406
  summary: withWatchText(watch, summary),
1251
- data: { owner, repo, pr_number, result, watch }
1407
+ data: { owner, repo: repo2, pr_number, result, watch }
1252
1408
  };
1253
1409
  } catch (err) {
1254
1410
  const message = err instanceof Error ? err.message : String(err);
@@ -1256,11 +1412,11 @@ async function stackAdopt(input, cfg) {
1256
1412
  summary: message,
1257
1413
  data: {
1258
1414
  error: {
1259
- code: err instanceof CommandError12 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
1415
+ code: err instanceof CommandError13 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
1260
1416
  message,
1261
- ...err instanceof CommandError12 ? { exit_code: err.exitCode } : {},
1262
- ...err instanceof CommandError12 && err.reason !== void 0 ? { reason: err.reason } : {},
1263
- ...err instanceof CommandError12 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1417
+ ...err instanceof CommandError13 ? { exit_code: err.exitCode } : {},
1418
+ ...err instanceof CommandError13 && err.reason !== void 0 ? { reason: err.reason } : {},
1419
+ ...err instanceof CommandError13 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1264
1420
  }
1265
1421
  },
1266
1422
  isError: true
@@ -1331,39 +1487,39 @@ async function stackList(cfg) {
1331
1487
 
1332
1488
  // src/tools/stack-publish.ts
1333
1489
  import {
1334
- CommandError as CommandError13,
1490
+ CommandError as CommandError14,
1335
1491
  publishWorkingBranch,
1336
1492
  stackWatchObligation as stackWatchObligation6,
1337
1493
  workingBranchPublication
1338
1494
  } from "mergestorm/client";
1339
- import { z as z4 } from "zod";
1495
+ import { z as z5 } from "zod";
1340
1496
  var target = {
1341
- cwd: z4.string().trim().min(1).optional(),
1342
- owner: z4.string().trim().min(1).optional(),
1343
- repo: z4.string().trim().min(1).optional(),
1344
- branch: z4.string().trim().min(1).optional()
1497
+ cwd: z5.string().trim().min(1).optional(),
1498
+ owner: z5.string().trim().min(1).optional(),
1499
+ repo: z5.string().trim().min(1).optional(),
1500
+ branch: z5.string().trim().min(1).optional()
1345
1501
  };
1346
1502
  var stackPublishSchema = {
1347
1503
  ...target,
1348
- base: z4.string().trim().min(1).optional(),
1349
- extend: z4.boolean().optional(),
1350
- title: z4.string().trim().min(1).max(256).optional(),
1351
- body: z4.string().max(6e4).optional(),
1352
- replace_head: z4.string().regex(/^[0-9a-f]{40}$/).optional(),
1353
- auto_land: z4.boolean().optional()
1504
+ base: z5.string().trim().min(1).optional(),
1505
+ extend: z5.boolean().optional(),
1506
+ title: z5.string().trim().min(1).max(256).optional(),
1507
+ body: z5.string().max(6e4).optional(),
1508
+ replace_head: z5.string().regex(/^[0-9a-f]{40}$/).optional(),
1509
+ auto_land: z5.boolean().optional()
1354
1510
  };
1355
1511
  var stackPublishStatusSchema = target;
1356
- var publishInput = z4.object(stackPublishSchema);
1357
- var statusInput = z4.object(stackPublishStatusSchema);
1512
+ var publishInput = z5.object(stackPublishSchema);
1513
+ var statusInput = z5.object(stackPublishStatusSchema);
1358
1514
  function failure(err, fallback) {
1359
1515
  const message = err instanceof Error ? err.message : String(err);
1360
1516
  return {
1361
1517
  summary: message,
1362
1518
  data: {
1363
1519
  error: {
1364
- code: err instanceof CommandError13 ? err.code ?? fallback : fallback,
1520
+ code: err instanceof CommandError14 ? err.code ?? fallback : fallback,
1365
1521
  message,
1366
- ...err instanceof CommandError13 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1522
+ ...err instanceof CommandError14 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1367
1523
  }
1368
1524
  },
1369
1525
  isError: true
@@ -1496,7 +1652,7 @@ async function stackStatus(stackId, cfg) {
1496
1652
 
1497
1653
  // src/tools/whoami.ts
1498
1654
  import {
1499
- CommandError as CommandError14,
1655
+ CommandError as CommandError15,
1500
1656
  apiBase,
1501
1657
  configPath,
1502
1658
  getMe as getMe2,
@@ -1507,7 +1663,7 @@ async function whoami(cfg, serverVersion) {
1507
1663
  const resolved = cfg ?? await loadConfig9();
1508
1664
  const key = resolveApiKey(resolved);
1509
1665
  if (!key) {
1510
- throw new CommandError14(
1666
+ throw new CommandError15(
1511
1667
  "No API key. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`) or set MERGESTORM_API_KEY.",
1512
1668
  1,
1513
1669
  "missing_api_key"
@@ -1515,7 +1671,7 @@ async function whoami(cfg, serverVersion) {
1515
1671
  }
1516
1672
  const me = await getMe2(resolved);
1517
1673
  if (!me) {
1518
- throw new CommandError14("Live account details unavailable. Could not verify account with /me.");
1674
+ throw new CommandError15("Live account details unavailable. Could not verify account with /me.");
1519
1675
  }
1520
1676
  const data = {
1521
1677
  key_prefix: me.key.prefix,
@@ -1553,7 +1709,7 @@ var MCP_STACK_DONE_INSTRUCTIONS = [
1553
1709
  "When stack work is done:",
1554
1710
  "- Stack work is done only when the stack is landed, or when the human takes it back (archives it 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.",
1555
1711
  "- Every stack_wait, stack_status, stack_adopt, and stack_set result carries watch: {done, until, reason, next, message}. While watch.done is false, keep calling stack_wait with watch.next.args (the returned cursor), 45s at a time, or hand the waiting to the background watcher below, and do not end your task. Stop only when watch.done is true.",
1556
- "- Only when your host has a confirmed notification that resumes this task, run `mg stack watch <stack-id>` as a background command with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs stack_wait against the Mergestorm API in slices of at most 300s, carries the cursor, stays silent while the stack is waiting or in_progress (busy[] holds and actAfter included), and costs no model tokens. It exits on attention (`MS-WATCH ATTENTION pr=<n> head=<sha> blocker=... repair=<kind>`, exit 3), on reads that keep failing (`MS-WATCH ATTENTION failed`, exit 3, after an auth or other permanent error twice, or after 15 minutes of timeouts and 5xx; tell the human the stack is no longer watched), on a closed, archived, or missing stack (`MS-WATCH ATTENTION stack=<id> reason=<reason>`, exit 3; verify missing stacks before reporting a landing), or on a confirmed landing (`MS-WATCH LANDED stack=<id> reason=landed`, exit 0). On an attention wake, read stack_status once, fix the blocker per mergestorm-bounce-watch, push, and start `mg stack watch <stack-id> --head <pushed-sha>` again. The command monitors the stack; it cannot wake an agent by itself. A confirmed notification is a host feature you can name that turns the command's exit or matching output into your next turn; a process left running, or output written to a log or terminal you do not read, is not one. Without a confirmed notification, keep using stack_wait with watch.next.args, or report that automatic follow-up is unavailable before ending the turn. Ending your turn while the watcher runs is not ending the task. Do not subscribe to GitHub events to wait on a stack.",
1712
+ "- Only when your host has a confirmed notification that resumes this task, run `mg stack watch <stack-id>` as a background command with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs stack_wait against the Mergestorm API in slices of at most 300s, carries the cursor, stays silent while the stack is waiting or in_progress (busy[] holds and actAfter included), and costs no model tokens. It exits on attention (`MS-WATCH ATTENTION pr=<n> head=<sha> blocker=... repair=<kind>`, exit 3), on reads that keep failing (`MS-WATCH ATTENTION failed`, exit 3, after an auth or other permanent error twice, or after 15 minutes with no stack read (timeouts, 5xx, rate limits); tell the human the stack is no longer watched), or when the command cannot start (`MS-WATCH ATTENTION failed` with exit 2 for bad flags, exit 1 for no config or another startup error; tell the human the stack is not watched), on a closed, archived, or missing stack (`MS-WATCH ATTENTION stack=<id> reason=<reason>`, exit 3; verify missing stacks before reporting a landing), or on a confirmed landing (`MS-WATCH LANDED stack=<id> reason=landed`, exit 0). On an attention wake, read stack_status once, fix the blocker per mergestorm-bounce-watch, push, and start `mg stack watch <stack-id> --head <pushed-sha>` again. The command monitors the stack; it cannot wake an agent by itself. A confirmed notification is a host feature you can name that turns the command's exit or matching output into your next turn; a process left running, or output written to a log or terminal you do not read, is not one. Without a confirmed notification, keep using stack_wait with watch.next.args, or report that automatic follow-up is unavailable before ending the turn. Ending your turn while the watcher runs is not ending the task. Do not subscribe to GitHub events to wait on a stack.",
1557
1713
  "- 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.",
1558
1714
  "- attention means something for you to fix; repair, when present, names the concrete fix. A pending land gate (landGatePending: ci_pending) is a wait, not attention. attention covers every open layer, not only currentCandidate: it names the lowest layer with a blocker an agent must fix and no Vortex or Cyclone run working on it; once that blocker clears, the next read names the next blocked layer."
1559
1715
  ].join("\n");
@@ -1590,10 +1746,22 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
1590
1746
  '- More states the watch names: a Vortex review skipped because the PR author is on the ignored authors list or because reviews are turned off for the repository is attention at once with repair 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 repair kind vortex_failed (comment @mergestorm-vortex review or push a new commit; if it fails again tell the human); a PR open on mg-stack-N that is not a layer of the stack is attention at once, blocker "The land PR cannot be queued: ...", repair kind members_remaining (do not patch or push; close, retarget or adopt that PR).',
1591
1747
  "- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
1592
1748
  ].join("\n");
1749
+ var MCP_HOSTED_SERVER_INSTRUCTIONS = [
1750
+ "Stacks:",
1751
+ "- A stack is done only when it is landed, or when the human takes it back (archives it or tells you to stop). A submit, an adopt or one merged layer is not done: merging a lower layer can break a layer above it.",
1752
+ "- A layer PR shown as merged is merged into mg-stack-<n>, not main. The stack is landed only when its land PR (head mg-stack-<n>) merges into main, which watch.done with reason landed reports. Never judge landing from GitHub PR state alone.",
1753
+ "- Results of the stack tools and review_dismiss carry watch: {done, reason, next, message}. While watch.done is false, call stack_wait with watch.next.args, which carry the cursor. Stop when watch.done is true.",
1754
+ "- attention means a layer needs a fix: it names the PR and the blocker, and repair, when present, names the fix. in_progress means Vortex or Cyclone is still working on it (busy[] says which) or a blocker waits for them to finish (actAfter agents_idle): wait, do not act. A pending land gate (landGatePending) is a wait, not attention.",
1755
+ "",
1756
+ "Vortex findings:",
1757
+ "- Dismiss a finding only after you checked it against the PR's live head and found it wrong or not actionable, with review_dismiss and a reason that gives your evidence. Never dismiss a finding you did not check or one from a newer review.",
1758
+ "- A finding a human must decide (Decision required, policy fork, needs a human) stays open: do not dismiss it; tell the human which finding it is."
1759
+ ].join("\n");
1593
1760
 
1594
1761
  // src/server.ts
1762
+ var BATCH_SIZE_ROW = BEARER_SETTINGS2.find((row) => row.key === "merge_queue_batch_size");
1595
1763
  var MCP_SERVER_NAME = "mergestorm";
1596
- var MCP_SERVER_VERSION = "0.2.27";
1764
+ var MCP_SERVER_VERSION = "0.2.28";
1597
1765
  var MCP_TOOL_NAMES = [
1598
1766
  "whoami",
1599
1767
  "credits",
@@ -1621,7 +1789,14 @@ var MCP_TOOL_NAMES = [
1621
1789
  "settings_set",
1622
1790
  "repo_settings_get",
1623
1791
  "repo_settings_set",
1624
- "surge_setup"
1792
+ "surge_setup",
1793
+ "list_memories",
1794
+ "search_memory",
1795
+ "remember",
1796
+ "update_memory",
1797
+ "forget_memory",
1798
+ "get_memory_mode",
1799
+ "set_memory_mode"
1625
1800
  ];
1626
1801
  var MCP_SERVER_INSTRUCTIONS = [
1627
1802
  MCP_STACK_DONE_INSTRUCTIONS,
@@ -1642,14 +1817,14 @@ function ok(payload) {
1642
1817
  };
1643
1818
  }
1644
1819
  function fail(err) {
1645
- const message = err instanceof CommandError15 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
1820
+ const message = err instanceof CommandError16 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
1646
1821
  return ok({
1647
1822
  isError: true,
1648
1823
  summary: message,
1649
1824
  data: {
1650
1825
  error: {
1651
1826
  message: err instanceof Error ? err.message : String(err),
1652
- ...err instanceof CommandError15 ? {
1827
+ ...err instanceof CommandError16 ? {
1653
1828
  exit_code: err.exitCode,
1654
1829
  ...err.code !== void 0 ? { code: err.code } : {},
1655
1830
  ...err.reason !== void 0 ? { reason: err.reason } : {},
@@ -1659,10 +1834,15 @@ function fail(err) {
1659
1834
  }
1660
1835
  });
1661
1836
  }
1662
- var MCP_WORKSPACE_TOOL_NAMES = ["review_submit", "stack_publish", "stack_publish_status"];
1837
+ var MCP_WORKSPACE_TOOL_NAMES = ["review_submit", "review_wait", "stack_publish", "stack_publish_status"];
1663
1838
  var MCP_HOSTED_TOOL_NAMES = MCP_TOOL_NAMES.filter(
1664
1839
  (name) => !MCP_WORKSPACE_TOOL_NAMES.includes(name)
1665
1840
  );
1841
+ var HOSTED_WAIT_MAX_S = 45;
1842
+ var HOSTED_REVIEW_WAIT_PR_DESCRIPTION = "Read the Vortex pass on a GitHub PR; review identity is head SHA + pass. timeout_s: 0 reads it once; otherwise it waits up to timeout_s (at most 45) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, start one with review_start_pr). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries. With timeout_s: 0, a pass that does not exist yet returns not_found. On rate_limited, wait retry_after_seconds before trying again. Returns the verdict and each finding.";
1843
+ var HOSTED_REVIEW_DISMISS_DESCRIPTION = `Dismiss Vortex findings you checked and found wrong or not actionable, at the PR's live head: head_sha, review_id, finding_ids (the GitHub review comment ids, offdiff-<n> for a body-only finding; preview: true lists them without writing) or scope "review" to dismiss every finding of that review, and a reason of at least 20 characters. A deferral needs evidence_url set to an open tracking issue in the same repository. Refused, with nothing written, when the head moved, an id is unknown or the user's GitHub account lacks write access; retries are idempotent. Returns which gates cleared (gate.vortex.cleared, else gate.vortex.message) and the stack watch.`;
1844
+ var HOSTED_STACK_STATUS_DESCRIPTION = "Read one owned stack: its layers, checks and agent state, with the same attention, issues, repair and currentCandidate as stack_wait. Use it to see where a stack stands now; to wait for a change, use stack_wait.";
1845
+ var HOSTED_STACK_WAIT_DESCRIPTION = "Wait up to timeout_s (1-45) for a stack to need attention, or read it once with 0. Returns a mergestorm.stack_watch/v1 envelope: attention names the PR, blocker and repair; waiting or in_progress means call again with watch.next.args; watch.done true means stop (landed, closed, archived or gone).";
1666
1846
  var HOSTED_ISSUE_CREATE_DESCRIPTION = "Open a GitHub issue through Mergestorm with no GitHub credentials: the Surge App creates it and names you in the body. Use it for the issue a pull request must have before it is opened, then put Fixes #<number> in the commit or pull request body. owner and repo name the repository. labels must already exist on the repository; an unknown one is refused with unknown_label and nothing is created. Calling it again with the same title returns the open issue it already made (created: false) when that issue is among the newest 100 open issues of the repository; an older one, or two calls at the same time, can still create a duplicate, so check the returned number before retrying after a lost response. Needs write access to the repository through your linked GitHub account and Surge installed on it.";
1667
1847
  function createMergestormMcpServer(options = {}) {
1668
1848
  const { config, hosted = false } = options;
@@ -1675,12 +1855,14 @@ function createMergestormMcpServer(options = {}) {
1675
1855
  websiteUrl: MCP_WEBSITE_URL,
1676
1856
  icons: MCP_SERVER_ICONS
1677
1857
  },
1678
- { instructions: MCP_SERVER_INSTRUCTIONS }
1858
+ { instructions: hosted ? MCP_HOSTED_SERVER_INSTRUCTIONS : MCP_SERVER_INSTRUCTIONS }
1679
1859
  );
1680
1860
  const registerTool = server2.registerTool.bind(server2);
1861
+ const watchNote = hosted ? "" : STACK_WATCH_RESULT_NOTE;
1862
+ const waitMaxS = hosted ? HOSTED_WAIT_MAX_S : 300;
1681
1863
  const addTool = (name, toolConfig, cb) => {
1682
1864
  if (hosted && MCP_WORKSPACE_TOOL_NAMES.includes(name)) return;
1683
- registerTool(name, toolConfig, cb);
1865
+ registerTool(name, toolConfig, hosted ? async (...args) => hostedToolResult(await cb(...args)) : cb);
1684
1866
  };
1685
1867
  addTool(
1686
1868
  "whoami",
@@ -1718,7 +1900,7 @@ function createMergestormMcpServer(options = {}) {
1718
1900
  title: "List reviews",
1719
1901
  description: "List recent Mergestorm review jobs as job envelopes.",
1720
1902
  inputSchema: {
1721
- limit: z5.number().int().min(1).max(50).optional()
1903
+ limit: z6.number().int().min(1).max(50).optional()
1722
1904
  },
1723
1905
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1724
1906
  },
@@ -1736,18 +1918,18 @@ function createMergestormMcpServer(options = {}) {
1736
1918
  title: "Submit review",
1737
1919
  description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Does not wait by default; returns job_id immediately after submission. Explicit wait: true polls one 45s slice by default. Record job_id and loop review_wait until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds and inspect or wait for an in-flight review before resubmitting. specialists may be security, performance, architecture, tests, data, api, frontend. governance and seam cannot be requested.",
1738
1920
  inputSchema: {
1739
- cwd: z5.string().optional(),
1740
- base: z5.string().optional(),
1741
- head: z5.string().optional(),
1742
- diff: z5.string().optional(),
1743
- router: z5.enum(["off", "standard", "max", "manual"]).optional(),
1744
- specialists: z5.array(z5.string().min(1)).max(5).optional(),
1745
- context: z5.string().optional(),
1746
- context_files: z5.array(z5.string().min(1)).max(10).optional(),
1747
- thread: z5.string().min(1).max(120).optional(),
1748
- idempotency_key: z5.string().min(1).max(128).optional(),
1749
- wait: z5.boolean().optional(),
1750
- timeout_s: z5.number().positive().max(300).optional()
1921
+ cwd: z6.string().optional(),
1922
+ base: z6.string().optional(),
1923
+ head: z6.string().optional(),
1924
+ diff: z6.string().optional(),
1925
+ router: z6.enum(["off", "standard", "max", "manual"]).optional(),
1926
+ specialists: z6.array(z6.string().min(1)).max(5).optional(),
1927
+ context: z6.string().optional(),
1928
+ context_files: z6.array(z6.string().min(1)).max(10).optional(),
1929
+ thread: z6.string().min(1).max(120).optional(),
1930
+ idempotency_key: z6.string().min(1).max(128).optional(),
1931
+ wait: z6.boolean().optional(),
1932
+ timeout_s: z6.number().positive().max(300).optional()
1751
1933
  },
1752
1934
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1753
1935
  },
@@ -1765,8 +1947,8 @@ function createMergestormMcpServer(options = {}) {
1765
1947
  title: "Wait for review",
1766
1948
  description: "Read one local Mergestorm review job by the job_id that review_submit returned, as a job envelope. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300s). On timeout, returns in_progress; call again with the same job_id until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again.",
1767
1949
  inputSchema: {
1768
- job_id: z5.string().min(1),
1769
- timeout_s: z5.number().min(0).max(300).optional()
1950
+ job_id: z6.string().min(1),
1951
+ timeout_s: z6.number().min(0).max(300).optional()
1770
1952
  },
1771
1953
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1772
1954
  },
@@ -1782,24 +1964,24 @@ function createMergestormMcpServer(options = {}) {
1782
1964
  "review_wait_pr",
1783
1965
  {
1784
1966
  title: "Wait for GitHub PR Vortex review",
1785
- description: "Read the Vortex pass on a GitHub PR from the DB-only endpoint; review identity is head SHA + pass. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, Vortex is not reviewing that head: comment @mergestorm-vortex review on the PR, run mg review, or push a new commit). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries; never replace it with the pass of an in-progress envelope, and omit after_pass for a head you hold no envelope for. With timeout_s: 0, a pass that does not exist yet returns not_found, so after a push use a positive timeout. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
1967
+ description: hosted ? HOSTED_REVIEW_WAIT_PR_DESCRIPTION : "Read the Vortex pass on a GitHub PR from the DB-only endpoint; review identity is head SHA + pass. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, Vortex is not reviewing that head: comment @mergestorm-vortex review on the PR, run mg review, or push a new commit). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries; never replace it with the pass of an in-progress envelope, and omit after_pass for a head you hold no envelope for. With timeout_s: 0, a pass that does not exist yet returns not_found, so after a push use a positive timeout. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
1786
1968
  inputSchema: {
1787
- owner: z5.string().min(1),
1788
- repo: z5.string().min(1),
1789
- pr_number: z5.number().int().min(1),
1790
- after_sha: z5.string().optional(),
1791
- pass: z5.number().int().min(1).optional(),
1792
- after_pass: z5.number().int().min(1).optional(),
1793
- timeout_s: z5.number().min(0).max(300).optional()
1969
+ owner: z6.string().min(1),
1970
+ repo: z6.string().min(1),
1971
+ pr_number: z6.number().int().min(1),
1972
+ after_sha: z6.string().optional(),
1973
+ pass: z6.number().int().min(1).optional(),
1974
+ after_pass: z6.number().int().min(1).optional(),
1975
+ timeout_s: z6.number().min(0).max(waitMaxS).optional()
1794
1976
  },
1795
1977
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1796
1978
  },
1797
- async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
1979
+ async ({ owner, repo: repo2, pr_number, after_sha, pass, after_pass, timeout_s }) => {
1798
1980
  try {
1799
1981
  return ok(
1800
1982
  await reviewWaitPr(
1801
1983
  owner,
1802
- repo,
1984
+ repo2,
1803
1985
  pr_number,
1804
1986
  after_sha,
1805
1987
  timeout_s,
@@ -1816,18 +1998,18 @@ function createMergestormMcpServer(options = {}) {
1816
1998
  "review_dismiss",
1817
1999
  {
1818
2000
  title: "Dismiss Vortex findings",
1819
- description: `Record an audited dismissal of Vortex findings you verified are wrong or not actionable on a GitHub PR. Identity is exact: head_sha must be the PR's live head, review_id a Vortex review (Core or seam) made at that head, and finding_ids the GitHub review comment ids of that review (offdiff-<n> for a body-only finding), or scope "review" with no finding_ids to dismiss every finding of that review. reason must say why (at least 20 characters); evidence_url is optional. Call with preview: true first to list the review's finding ids and the seam gate without writing. The caller's linked GitHub account needs write access to the repository. A moved head, another review, an unknown id or a missing permission is refused and nothing is written. Retries are idempotent. The seam gate clears only when every finding of that integration review is dismissed; the Vortex gate at the head clears only once every Vortex Blocker (error finding) at that head is dismissed and no Vortex review of it is running (gate.vortex.cleared, else gate.vortex.message); CI and Auto land policy are unchanged. Vortex does not raise a dismissed finding again on the same diff. Never dismiss a finding you did not check. ` + MCP_FINDING_DEFERRAL_RULE + STACK_WATCH_RESULT_NOTE,
2001
+ description: hosted ? HOSTED_REVIEW_DISMISS_DESCRIPTION : `Record an audited dismissal of Vortex findings you verified are wrong or not actionable on a GitHub PR. Identity is exact: head_sha must be the PR's live head, review_id a Vortex review (Core or seam) made at that head, and finding_ids the GitHub review comment ids of that review (offdiff-<n> for a body-only finding), or scope "review" with no finding_ids to dismiss every finding of that review. reason must say why (at least 20 characters); evidence_url is optional. Call with preview: true first to list the review's finding ids and the seam gate without writing. The caller's linked GitHub account needs write access to the repository. A moved head, another review, an unknown id or a missing permission is refused and nothing is written. Retries are idempotent. The seam gate clears only when every finding of that integration review is dismissed; the Vortex gate at the head clears only once every Vortex Blocker (error finding) at that head is dismissed and no Vortex review of it is running (gate.vortex.cleared, else gate.vortex.message); CI and Auto land policy are unchanged. Vortex does not raise a dismissed finding again on the same diff. Never dismiss a finding you did not check. ` + MCP_FINDING_DEFERRAL_RULE + STACK_WATCH_RESULT_NOTE,
1820
2002
  inputSchema: {
1821
- owner: z5.string().min(1),
1822
- repo: z5.string().min(1),
1823
- pr_number: z5.number().int().min(1),
1824
- head_sha: z5.string().regex(/^[0-9a-fA-F]{40}$/),
1825
- review_id: z5.number().int().min(1),
1826
- finding_ids: z5.array(z5.string().min(1)).max(50).optional(),
1827
- scope: z5.enum(["findings", "review"]).optional(),
1828
- reason: z5.string().optional(),
1829
- evidence_url: z5.string().url().optional(),
1830
- preview: z5.boolean().optional()
2003
+ owner: z6.string().min(1),
2004
+ repo: z6.string().min(1),
2005
+ pr_number: z6.number().int().min(1),
2006
+ head_sha: z6.string().regex(/^[0-9a-fA-F]{40}$/),
2007
+ review_id: z6.number().int().min(1),
2008
+ finding_ids: z6.array(z6.string().min(1)).max(50).optional(),
2009
+ scope: z6.enum(["findings", "review"]).optional(),
2010
+ reason: z6.string().optional(),
2011
+ evidence_url: z6.string().url().optional(),
2012
+ preview: z6.boolean().optional()
1831
2013
  },
1832
2014
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1833
2015
  },
@@ -1840,16 +2022,16 @@ function createMergestormMcpServer(options = {}) {
1840
2022
  }
1841
2023
  );
1842
2024
  const prInput = {
1843
- owner: z5.string().min(1),
1844
- repo: z5.string().min(1),
1845
- pr_number: z5.number().int().min(1)
2025
+ owner: z6.string().min(1),
2026
+ repo: z6.string().min(1),
2027
+ pr_number: z6.number().int().min(1)
1846
2028
  };
1847
2029
  addTool(
1848
2030
  "review_start_pr",
1849
2031
  {
1850
2032
  title: "Start Vortex review",
1851
- description: "Start a Vortex review of an open GitHub pull request on the user's account, at its current head. head_sha, when given, must still be the head; otherwise nothing starts and the answer names the live head. Uses one standard credit. Findings post on the pull request; read them with review_wait_pr. A review already running at that head is reported, not started twice. When this server serves a Breeze chat, the chat is woken with the result.",
1852
- inputSchema: { ...prInput, head_sha: z5.string().regex(/^[0-9a-fA-F]{7,40}$/).optional() },
2033
+ description: "Start a Vortex review of an open GitHub pull request on the user's account, at its current head. head_sha, when given, must still be the head; otherwise nothing starts and the answer names the live head. Uses one standard credit. Findings post on the pull request; read them with review_wait_pr. A review already running at that head is reported, not started twice. When the result names a background task, its result also arrives through that task when the work finishes.",
2034
+ inputSchema: { ...prInput, head_sha: z6.string().regex(/^[0-9a-fA-F]{7,40}$/).optional() },
1853
2035
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1854
2036
  },
1855
2037
  async (args) => {
@@ -1880,14 +2062,14 @@ function createMergestormMcpServer(options = {}) {
1880
2062
  "cyclone_start",
1881
2063
  {
1882
2064
  title: "Start Cyclone job",
1883
- description: "Have Cyclone work on the user's repository and push the result. task fix_findings fixes the open Vortex findings on pr_number, fix_ci fixes its failing CI check (failing_check names one; default the first failing check), resolve_conflicts merges its base in and resolves the conflicts; each pushes a commit to the pull request's branch. task build builds a first working version of a new project from brief into an empty repository the user created and installed Cyclone on, then opens a pull request. Uses Cyclone credits. When the pull request has nothing to do (CI green, no conflicts, no open findings) nothing starts and the answer says why: tell the user that. Returns dispatch_id. Then wait for the job with cyclone_job until it is pushed, completed or failed, and report what it pushed. When this server serves a Breeze chat and the reply ends before the job does, the chat is woken with the result.",
2065
+ description: "Have Cyclone work on the user's repository and push the result. task fix_findings fixes the open Vortex findings on pr_number, fix_ci fixes its failing CI check (failing_check names one; default the first failing check), resolve_conflicts merges its base in and resolves the conflicts; each pushes a commit to the pull request's branch. When the user has not named the pull request, find it before you call, and ask them for owner/repo#number if you cannot. task build builds a first working version of a new project from brief into an empty repository the user created and installed Cyclone on, then opens a pull request. Uses Cyclone credits. When the pull request has nothing to do (CI green, no conflicts, no open findings) nothing starts and the answer says why: tell the user that. Returns dispatch_id. Wait for the job with cyclone_job until it is pushed, completed or failed, then report what it pushed. When the result names a background task, its result also arrives through that task when the work finishes.",
1884
2066
  inputSchema: {
1885
- task: z5.enum(CYCLONE_TASKS),
1886
- owner: z5.string().min(1),
1887
- repo: z5.string().min(1),
1888
- pr_number: z5.number().int().min(1).optional(),
1889
- failing_check: z5.string().min(1).max(200).optional(),
1890
- brief: z5.string().min(1).max(4e3).optional()
2067
+ task: z6.enum(CYCLONE_TASKS),
2068
+ owner: z6.string().min(1),
2069
+ repo: z6.string().min(1),
2070
+ pr_number: z6.number().int().min(1).optional(),
2071
+ failing_check: z6.string().min(1).max(200).optional(),
2072
+ brief: z6.string().min(1).max(4e3).optional()
1891
2073
  },
1892
2074
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1893
2075
  },
@@ -1904,7 +2086,7 @@ function createMergestormMcpServer(options = {}) {
1904
2086
  {
1905
2087
  title: "Wait for Cyclone job",
1906
2088
  description: `Wait for a Cyclone job started with cyclone_start, by its dispatch_id, and read it: queued, running, pushed, completed or failed, with the outcome, detail, pushed commit and changed files. timeout_s (default ${CYCLONE_JOB_WAIT_DEFAULT_S}, max ${CYCLONE_JOB_WAIT_MAX_S}) waits until the job is pushed, completed or failed, or the time is up; 0 reads it once. While it is queued or running, call again with the same dispatch_id.`,
1907
- inputSchema: { dispatch_id: z5.string().uuid(), timeout_s: z5.number().int().min(0).max(CYCLONE_JOB_WAIT_MAX_S).optional() },
2089
+ inputSchema: { dispatch_id: z6.string().uuid(), timeout_s: z6.number().int().min(0).max(CYCLONE_JOB_WAIT_MAX_S).optional() },
1908
2090
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1909
2091
  },
1910
2092
  async ({ dispatch_id, timeout_s }, extra) => {
@@ -1935,7 +2117,7 @@ function createMergestormMcpServer(options = {}) {
1935
2117
  "stack_adopt",
1936
2118
  {
1937
2119
  title: "Adopt PR stack",
1938
- description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted overrides are not seeded. Never changes account settings. Adoption needs the Mergestorm Surge GitHub App on the repository. Stacks, the merge queue and Auto land run through that App; Cyclone is the auto-patch App. Without it the API returns cyclone_not_connected or cyclone_not_installed; the message, and error.reason starting with surge_ when Surge is the one to install, say which App is missing \u2014 do not retry, tell the human what the message says. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + STACK_WATCH_RESULT_NOTE,
2120
+ description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted overrides are not seeded. Never changes account settings. Adoption needs the Mergestorm Surge GitHub App on the repository. Stacks, the merge queue and Auto land run through that App; Cyclone is the auto-patch App. Without it the API returns cyclone_not_connected or cyclone_not_installed; the message, and error.reason starting with surge_ when Surge is the one to install, say which App is missing \u2014 do not retry, tell the human what the message says. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + watchNote,
1939
2121
  inputSchema: stackAdoptSchema,
1940
2122
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1941
2123
  },
@@ -1966,7 +2148,7 @@ function createMergestormMcpServer(options = {}) {
1966
2148
  "stack_publish",
1967
2149
  {
1968
2150
  title: "Publish branch through Mergestorm",
1969
- description: "Publish a local branch without any GitHub credentials. Reads the branch's commits from the local repository at cwd (never runs gh, git push or git fetch) and sends them to Mergestorm, which writes the exact commits and the branch on GitHub as the Surge App, opens a ready pull request when the branch has none, and registers the stack with Auto land on for a new stack, and Auto patch and Auto review left to the stack, repository and account settings, so Cyclone patches Vortex findings when Auto patch is on. Use it for the first publication and for every later commit to that branch. Defaults: owner and repo from origin, the checked-out branch, the repository's trunk as base, title and body from the tip commit. Two ways to publish: with no base (or trunk as base) the branch opens as a single PR onto trunk; to stack it on another branch, pass that branch as base and extend: true. A base other than trunk without extend is refused with implicit_stack. The result says which one it made. Mergestorm verifies each commit's Git tree before it moves the branch and returns the verified head and tree. It refuses with head_changed when the branch on GitHub holds work your local branch does not, base_changed when the base commit left the base branch, protected_branch for trunk and Mergestorm-owned branches, and payload_too_large beyond 20 commits, 500 changed paths, 2 MiB a file or 8 MiB in total; submodules and merge commits are not carried. replace_head replaces that exact GitHub head with the local branch and discards its commits. Safe to call again after a lost response or a failure: it resumes and never duplicates the branch, commits, pull request or stack." + STACK_WATCH_RESULT_NOTE,
2151
+ description: "Publish a local branch without any GitHub credentials. Reads the branch's commits from the local repository at cwd (never runs gh, git push or git fetch) and sends them to Mergestorm, which writes the exact commits and the branch on GitHub as the Surge App, opens a ready pull request when the branch has none, and registers the stack with Auto land on for a new stack, and Auto patch and Auto review left to the stack, repository and account settings, so Cyclone patches Vortex findings when Auto patch is on. Use it for the first publication and for every later commit to that branch. Defaults: owner and repo from origin, the checked-out branch, the repository's trunk as base, title and body from the tip commit. Two ways to publish: with no base (or trunk as base) the branch opens as a single PR onto trunk; to stack it on another branch, pass that branch as base and extend: true. A base other than trunk without extend is refused with implicit_stack. The result says which one it made. Mergestorm verifies each commit's Git tree before it moves the branch and returns the verified head and tree. It refuses with head_changed when the branch on GitHub holds work your local branch does not, base_changed when the base commit left the base branch, protected_branch for trunk and Mergestorm-owned branches, and payload_too_large beyond 20 commits, 500 changed paths, 2 MiB a file or 8 MiB in total; submodules and merge commits are not carried. replace_head replaces that exact GitHub head with the local branch and discards its commits. Safe to call again after a lost response or a failure: it resumes and never duplicates the branch, commits, pull request or stack." + watchNote,
1970
2152
  inputSchema: stackPublishSchema,
1971
2153
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1972
2154
  },
@@ -1998,14 +2180,14 @@ function createMergestormMcpServer(options = {}) {
1998
2180
  "stack_set",
1999
2181
  {
2000
2182
  title: "Set stack policy",
2001
- description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings." + STACK_WATCH_RESULT_NOTE,
2183
+ description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings." + watchNote,
2002
2184
  inputSchema: {
2003
- stack_id: z5.string().min(1),
2004
- auto_land: z5.boolean().optional(),
2005
- auto_review: z5.boolean().nullable().optional(),
2006
- auto_patch: z5.boolean().nullable().optional(),
2007
- auto_resolve_conflicts: z5.boolean().nullable().optional(),
2008
- auto_fix_ci: z5.boolean().nullable().optional()
2185
+ stack_id: z6.string().min(1),
2186
+ auto_land: z6.boolean().optional(),
2187
+ auto_review: z6.boolean().nullable().optional(),
2188
+ auto_patch: z6.boolean().nullable().optional(),
2189
+ auto_resolve_conflicts: z6.boolean().nullable().optional(),
2190
+ auto_fix_ci: z6.boolean().nullable().optional()
2009
2191
  },
2010
2192
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
2011
2193
  },
@@ -2029,8 +2211,8 @@ function createMergestormMcpServer(options = {}) {
2029
2211
  "stack_restack",
2030
2212
  {
2031
2213
  title: "Restack stack",
2032
- description: "Rebase one owned stack's layer branches onto their parents and push them, after the trunk or a lower layer moved. Does not merge or enqueue anything." + STACK_WATCH_RESULT_NOTE,
2033
- inputSchema: { stack_id: z5.string().min(1) },
2214
+ description: "Rebase one owned stack's layer branches onto their parents and push them, after the trunk or a lower layer moved. Does not merge or enqueue anything." + watchNote,
2215
+ inputSchema: { stack_id: z6.string().min(1) },
2034
2216
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
2035
2217
  },
2036
2218
  async ({ stack_id }) => {
@@ -2045,8 +2227,8 @@ function createMergestormMcpServer(options = {}) {
2045
2227
  "stack_enqueue",
2046
2228
  {
2047
2229
  title: "Send stack to merge queue",
2048
- description: "Send one owned stack to the Mergestorm merge queue, which tests it and lands it when its checks pass. To restart a failed or stuck land, call stack_restack first and then this. When this server serves a Breeze chat, the chat is woken when the stack lands, is bounced or is archived." + STACK_WATCH_RESULT_NOTE,
2049
- inputSchema: { stack_id: z5.string().min(1) },
2230
+ description: "Send one owned stack to the Mergestorm merge queue, which tests it and lands it when its checks pass. To restart a failed or stuck land, call stack_restack first and then this. When the result names a background task, its result also arrives through that task when the work finishes." + watchNote,
2231
+ inputSchema: { stack_id: z6.string().min(1) },
2050
2232
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
2051
2233
  },
2052
2234
  async ({ stack_id }) => {
@@ -2061,9 +2243,9 @@ function createMergestormMcpServer(options = {}) {
2061
2243
  "stack_status",
2062
2244
  {
2063
2245
  title: "Get stack status",
2064
- description: `Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue. trunkBranch is mg-stack-<n> for an adopted 2+ PR stack and matches the bottom open PR's GitHub base; an mg-park-* parentBranch is a freeze. Neither is drift to fix. repair names the concrete fix for attention; landGatePending is a wait, not attention. held names a blocker no agent run clears while Vortex or Cyclone is still busy on that PR (actAfter agents_idle, waitingOn); repair then describes the fix to plan, not to apply yet. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
2246
+ description: hosted ? HOSTED_STACK_STATUS_DESCRIPTION : `Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue. trunkBranch is mg-stack-<n> for an adopted 2+ PR stack and matches the bottom open PR's GitHub base; an mg-park-* parentBranch is a freeze. Neither is drift to fix. repair names the concrete fix for attention; landGatePending is a wait, not attention. held names a blocker no agent run clears while Vortex or Cyclone is still busy on that PR (actAfter agents_idle, waitingOn); repair then describes the fix to plan, not to apply yet. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
2065
2247
  inputSchema: {
2066
- stack_id: z5.string().min(1)
2248
+ stack_id: z6.string().min(1)
2067
2249
  },
2068
2250
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2069
2251
  },
@@ -2079,13 +2261,13 @@ function createMergestormMcpServer(options = {}) {
2079
2261
  "stack_wait",
2080
2262
  {
2081
2263
  title: "Wait for stack attention",
2082
- description: `Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure; waiting and in_progress end this slice, and the next call is stack_wait again with the same cursor. A timeout of failed means no assessment was produced. A stack that no longer exists returns status failed with watch.done true (reason not_found) and is not an error. While Cyclone or Vortex is still working on the blocked PR it returns in_progress, not attention, with busy[] naming the PR, blocker, and agent; agents carries that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. When another layer has a blocker no agent works on, attention names that blocker instead and busy[] is empty. A blocker no agent run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that in_progress, in blocker, prNumber, headSha, and issues[], with actAfter: "agents_idle" and waitingOn listing the busy agents: plan the fix, and act when a snapshot after the agents finish returns attention, re-reading the blocker then. Blockers the agents can change keep blocker and actAfter null. Attention never calls for changing a PR's GitHub base. When present, repair names the concrete fix for attention (restack_conflict and merge_conflict carry liveParent, never mg-park-*). For the three stuck-seam blockers, repair kind seam_review_stuck: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. A pending land gate (landGatePending) returns in_progress, not attention. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
2264
+ description: hosted ? HOSTED_STACK_WAIT_DESCRIPTION : `Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure; waiting and in_progress end this slice, and the next call is stack_wait again with the same cursor. A timeout of failed means no assessment was produced. A stack that no longer exists returns status failed with watch.done true (reason not_found) and is not an error. While Cyclone or Vortex is still working on the blocked PR it returns in_progress, not attention, with busy[] naming the PR, blocker, and agent; agents carries that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. When another layer has a blocker no agent works on, attention names that blocker instead and busy[] is empty. A blocker no agent run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that in_progress, in blocker, prNumber, headSha, and issues[], with actAfter: "agents_idle" and waitingOn listing the busy agents: plan the fix, and act when a snapshot after the agents finish returns attention, re-reading the blocker then. Blockers the agents can change keep blocker and actAfter null. Attention never calls for changing a PR's GitHub base. When present, repair names the concrete fix for attention (restack_conflict and merge_conflict carry liveParent, never mg-park-*). For the three stuck-seam blockers, repair kind seam_review_stuck: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. A pending land gate (landGatePending) returns in_progress, not attention. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
2083
2265
  inputSchema: stackWaitSchema,
2084
2266
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2085
2267
  },
2086
2268
  async (args, extra) => {
2087
2269
  try {
2088
- const payload = await stackWait(args, config, { signal: extra.signal, ...chatId ? { chatId } : {} });
2270
+ const payload = await stackWait(args, config, { signal: extra.signal, ...chatId ? { chatId } : {}, hosted });
2089
2271
  return ok(payload);
2090
2272
  } catch (err) {
2091
2273
  return fail(err);
@@ -2098,7 +2280,7 @@ function createMergestormMcpServer(options = {}) {
2098
2280
  title: "Get merge queue status",
2099
2281
  description: "List live merge queue entries (position >= 1) plus the newest bounced entries (position 0) with bounceDetail for the current user, optionally filtered by stack.",
2100
2282
  inputSchema: {
2101
- stack_id: z5.string().min(1).optional()
2283
+ stack_id: z6.string().min(1).optional()
2102
2284
  },
2103
2285
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2104
2286
  },
@@ -2114,8 +2296,8 @@ function createMergestormMcpServer(options = {}) {
2114
2296
  "repo_settings_get",
2115
2297
  {
2116
2298
  title: "Get repository settings",
2117
- description: "Read the repository-level Auto patch, Auto review and Auto land values for the repositories connected to this account. Each value is true, false, or null; null means not set, so the account setting applies. Auto patch and Auto review precedence is the stack override (stack_set), then the repository value, then the account setting (settings_get). The Auto land repository value only seeds new stacks at adoption; it does not override an existing stack. Pass repo as owner/repo for one repository, or omit it for all.",
2118
- inputSchema: { repo: z5.string().min(3).optional() },
2299
+ description: "Read the repository-level Auto patch, Auto review, Auto land, merge queue (merge_queue_batch_enabled, merge_queue_batch_size, merge_queue_lands_alone_paths) and merge queue overdrive values for the repositories connected to this account. auto_patch_enabled, auto_review_enabled, auto_land_default, merge_queue_batch_enabled and merge_queue_overdrive are true, false, or null; merge_queue_batch_size is a whole number or null; merge_queue_lands_alone_paths is a list of path patterns or null. null means not set, so the account setting applies. Auto patch and Auto review precedence is the stack override (stack_set), then the repository value, then the account setting (settings_get). The Auto land repository value only seeds new stacks at adoption; it does not override an existing stack. merge_queue_overdrive is the repository value over the account default; while it resolves on, that repository batches up to 10 queued pull requests. Pass repo as owner/repo for one repository, or omit it for all.",
2300
+ inputSchema: { repo: z6.string().min(3).optional() },
2119
2301
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2120
2302
  },
2121
2303
  async (args) => {
@@ -2130,12 +2312,16 @@ function createMergestormMcpServer(options = {}) {
2130
2312
  "repo_settings_set",
2131
2313
  {
2132
2314
  title: "Update repository settings",
2133
- description: "Set Auto patch (Cyclone), Auto review (Vortex) or the Auto land default for every stack and pull request in one repository connected to this account, and return the stored result. true or false sets the repository value; null clears it so the account setting applies again. A stack override (stack_set) still wins for that stack. auto_patch and auto_review apply to open stacks without an override from their next decision; nothing is cancelled or re-queued. auto_land only seeds new stacks. Only the account that connected the repository can change it. Never changes account settings.",
2315
+ description: "Set Auto patch (Cyclone), Auto review (Vortex), the Auto land default, or the merge queue settings (merge_queue_batch, merge_queue_batch_size, merge_queue_lands_alone_paths, merge_queue_overdrive) for every stack and pull request in one repository connected to this account, and return the stored result. A value sets the repository value; null clears it so the account setting applies again. A stack override (stack_set) still wins for that stack. auto_patch and auto_review apply to open stacks without an override from their next decision; nothing is cancelled or re-queued. auto_land only seeds new stacks. merge_queue_lands_alone_paths is a list of folder prefixes or globs; a queued pull request that changes a matching file is never batched with others, and an empty list keeps only the built-in rule that a stack with more than one open pull request lands alone. merge_queue_overdrive true makes that repository batch up to 10 queued pull requests, even with batching off; false turns overdrive off for that repository whatever the account default is, and null clears the repository value so the account default (settings_get merge_queue_overdrive) applies again. Only the account that connected the repository can change it. Never changes account settings.",
2134
2316
  inputSchema: {
2135
- repo: z5.string().min(3),
2136
- auto_patch: z5.boolean().nullable().optional(),
2137
- auto_review: z5.boolean().nullable().optional(),
2138
- auto_land: z5.boolean().nullable().optional()
2317
+ repo: z6.string().min(3),
2318
+ auto_patch: z6.boolean().nullable().optional(),
2319
+ auto_review: z6.boolean().nullable().optional(),
2320
+ auto_land: z6.boolean().nullable().optional(),
2321
+ merge_queue_batch: z6.boolean().nullable().optional(),
2322
+ merge_queue_batch_size: z6.number().int().min(BATCH_SIZE_ROW.min).max(BATCH_SIZE_ROW.max).nullable().optional(),
2323
+ merge_queue_lands_alone_paths: z6.array(z6.string()).nullable().optional(),
2324
+ merge_queue_overdrive: z6.boolean().nullable().optional()
2139
2325
  },
2140
2326
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
2141
2327
  },
@@ -2152,7 +2338,7 @@ function createMergestormMcpServer(options = {}) {
2152
2338
  {
2153
2339
  title: "Set up Surge CI",
2154
2340
  description: "Set up Surge CI on one repository of the user's account: if Surge does not serve it yet, serve it (it joins the repositories Surge serves and gets the account's Surge settings), then open a pull request that points its workflow jobs at Surge runners and adds the merge queue batch trigger. Nothing in the repository's files changes until that pull request is merged. The answer says what happened, with the pull request link.",
2155
- inputSchema: { owner: z5.string().min(1), repo: z5.string().min(1) },
2341
+ inputSchema: { owner: z6.string().min(1), repo: z6.string().min(1) },
2156
2342
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2157
2343
  },
2158
2344
  async (args) => {
@@ -2167,7 +2353,7 @@ function createMergestormMcpServer(options = {}) {
2167
2353
  "settings_get",
2168
2354
  {
2169
2355
  title: "Get settings",
2170
- description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), merge_queue_batch_enabled and merge_queue_batch_size (whether the merge queue tests several pull requests on one CI run, and how many), cyclone_connected (the Cyclone auto-patch App), and stacks_ready (whether stacks, the merge queue and Auto land can run for this account, which needs Mergestorm Surge).",
2356
+ description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), merge_queue_batch_enabled and merge_queue_batch_size (whether the merge queue tests several pull requests on one CI run, and how many), merge_queue_overdrive (the account default for overdrive: the merge queue batches up to 10 queued pull requests per repository; repo_settings_set overrides it per repository), cyclone_connected (the Cyclone auto-patch App), and stacks_ready (whether stacks, the merge queue and Auto land can run for this account, which needs Mergestorm Surge).",
2171
2357
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2172
2358
  },
2173
2359
  async () => {
@@ -2182,17 +2368,17 @@ function createMergestormMcpServer(options = {}) {
2182
2368
  "settings_set",
2183
2369
  {
2184
2370
  title: "Update settings",
2185
- description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. auto_land_settle_seconds is a whole number of seconds from 15 through 300; it applies to settle clocks that start after the write. merge_queue_batch_enabled turns merge queue batching on or off for the account, and merge_queue_batch_size is a whole number from 2 through 8. cyclone_connected and github_connected are read-only and cannot be set.",
2371
+ description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. auto_land_settle_seconds is a whole number of seconds from 15 through 300; it applies to settle clocks that start after the write. merge_queue_batch_enabled turns merge queue batching on or off for the account, merge_queue_batch_size is a whole number from 2 through 10, merge_queue_lands_alone_paths is the list of folder prefixes or globs whose changed files keep a pull request out of a batch, and merge_queue_overdrive is the account default for overdrive: while it resolves on, a repository batches up to 10 queued pull requests, even with batching off, without changing merge_queue_batch_size, and it stays on until turned off (repo_settings_set overrides each of these per repository). cyclone_connected and github_connected are read-only and cannot be set.",
2186
2372
  inputSchema: {
2187
2373
  ...Object.fromEntries(
2188
2374
  BEARER_SETTINGS2.map((row) => [
2189
2375
  row.key,
2190
- ("kind" in row ? row.kind === "logins" ? z5.array(z5.string()) : row.kind === "seconds" || row.kind === "count" ? z5.number().int().min(row.min).max(row.max) : z5.enum(row.values) : z5.boolean()).optional()
2376
+ ("kind" in row ? row.kind === "logins" || row.kind === "paths" ? z6.array(z6.string()) : row.kind === "seconds" || row.kind === "count" ? z6.number().int().min(row.min).max(row.max) : z6.enum(row.values) : z6.boolean()).optional()
2191
2377
  ])
2192
2378
  ),
2193
2379
  // Preserve these args so the handler rejects them even alongside a writable key.
2194
- cyclone_connected: z5.unknown().optional(),
2195
- github_connected: z5.unknown().optional()
2380
+ cyclone_connected: z6.unknown().optional(),
2381
+ github_connected: z6.unknown().optional()
2196
2382
  },
2197
2383
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
2198
2384
  },
@@ -2204,6 +2390,71 @@ function createMergestormMcpServer(options = {}) {
2204
2390
  }
2205
2391
  }
2206
2392
  );
2393
+ const memory = createMemoryToolHandlers(config, chatId);
2394
+ const memoryTools = [
2395
+ {
2396
+ name: "list_memories",
2397
+ title: "List memories",
2398
+ description: "List what Mergestorm remembers about the user, newest first, with each memory's id. Filter by scope (user or repo), repo (owner/repo) or kind. Works with memory off, so the user can still see and delete what is stored.",
2399
+ inputSchema: listMemoriesSchema,
2400
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2401
+ run: memory.list
2402
+ },
2403
+ {
2404
+ name: "search_memory",
2405
+ title: "Search memory",
2406
+ description: "Search what Mergestorm remembers about the user by meaning, best match first. Give repo as owner/repo to search that repository's memories and standing rules too. Returns nothing when memory is off.",
2407
+ inputSchema: searchMemorySchema,
2408
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2409
+ run: memory.search
2410
+ },
2411
+ {
2412
+ name: "remember",
2413
+ title: "Remember",
2414
+ description: "Remember one thing about the user for later conversations, on every device: a standing rule for a repository, their code or writing style, how they like to work, a fact about them or their team, or what they built or decided. One thing per call, as a plain sentence under 500 characters that still makes sense months from now. kind: rule, style, preference, fact or project. Give repo as owner/repo only for a memory about one repository. When memory is on_request, save only what the user asks you to remember. Refused with memory_off when memory is off. Never keys, tokens, passwords or other secrets.",
2415
+ inputSchema: rememberSchema,
2416
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2417
+ run: memory.remember
2418
+ },
2419
+ {
2420
+ name: "update_memory",
2421
+ title: "Update memory",
2422
+ description: "Correct a memory that is wrong or out of date: its id from list_memories or search_memory, the new text, and optionally a new kind.",
2423
+ inputSchema: updateMemorySchema,
2424
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2425
+ run: memory.update
2426
+ },
2427
+ {
2428
+ name: "forget_memory",
2429
+ title: "Forget memory",
2430
+ description: "Forget a memory that no longer holds, or anything the user asks you to forget, by its id from list_memories or search_memory.",
2431
+ inputSchema: forgetMemorySchema,
2432
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2433
+ run: memory.forget
2434
+ },
2435
+ {
2436
+ name: "get_memory_mode",
2437
+ title: "Read memory mode",
2438
+ description: "Read the account's memory mode: off (nothing is saved or recalled), on_request (save only what the user asks to remember) or all (save what will matter later).",
2439
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2440
+ run: memory.getMode
2441
+ },
2442
+ {
2443
+ name: "set_memory_mode",
2444
+ title: "Set memory mode",
2445
+ description: "Set the account's memory mode when the user asks: off, on_request or all.",
2446
+ inputSchema: setMemoryModeSchema,
2447
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2448
+ run: memory.setMode
2449
+ }
2450
+ ];
2451
+ for (const tool of memoryTools) {
2452
+ addTool(
2453
+ tool.name,
2454
+ { title: tool.title, description: tool.description, ...tool.inputSchema ? { inputSchema: tool.inputSchema } : {}, annotations: tool.annotations },
2455
+ async (args) => ok(await tool.run(tool.inputSchema ? args : {}))
2456
+ );
2457
+ }
2207
2458
  return server2;
2208
2459
  }
2209
2460