mergestorm-mcp 0.2.26 → 0.2.27

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.
Files changed (71) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +450 -96
  3. package/dist/lib/brand-icons.d.ts +16 -0
  4. package/dist/lib/brand-icons.js +23 -0
  5. package/dist/lib/hosted.d.ts +7 -0
  6. package/dist/lib/hosted.js +21 -0
  7. package/dist/lib/index.d.ts +1 -0
  8. package/dist/lib/index.js +5 -0
  9. package/dist/lib/pr-loop-instructions.d.ts +13 -0
  10. package/dist/lib/pr-loop-instructions.js +62 -0
  11. package/dist/lib/server.d.ts +22 -0
  12. package/dist/lib/server.js +558 -0
  13. package/dist/lib/tools/agent-request.d.ts +6 -0
  14. package/dist/lib/tools/agent-request.js +25 -0
  15. package/dist/lib/tools/chat-wake.d.ts +21 -0
  16. package/dist/lib/tools/chat-wake.js +26 -0
  17. package/dist/lib/tools/credits.d.ts +3 -0
  18. package/dist/lib/tools/credits.js +38 -0
  19. package/dist/lib/tools/cyclone-job.d.ts +29 -0
  20. package/dist/lib/tools/cyclone-job.js +70 -0
  21. package/dist/lib/tools/envelope.d.ts +5 -0
  22. package/dist/lib/tools/envelope.js +30 -0
  23. package/dist/lib/tools/issue-create.d.ts +47 -0
  24. package/dist/lib/tools/issue-create.js +53 -0
  25. package/dist/lib/tools/queue-status.d.ts +3 -0
  26. package/dist/lib/tools/queue-status.js +79 -0
  27. package/dist/lib/tools/repo-settings.d.ts +6 -0
  28. package/dist/lib/tools/repo-settings.js +54 -0
  29. package/dist/lib/tools/review-dismiss.d.ts +16 -0
  30. package/dist/lib/tools/review-dismiss.js +68 -0
  31. package/dist/lib/tools/review-get-pr.d.ts +22 -0
  32. package/dist/lib/tools/review-get-pr.js +52 -0
  33. package/dist/lib/tools/review-get.d.ts +3 -0
  34. package/dist/lib/tools/review-get.js +27 -0
  35. package/dist/lib/tools/review-list.d.ts +3 -0
  36. package/dist/lib/tools/review-list.js +10 -0
  37. package/dist/lib/tools/review-start.d.ts +11 -0
  38. package/dist/lib/tools/review-start.js +21 -0
  39. package/dist/lib/tools/review-submit.d.ts +36 -0
  40. package/dist/lib/tools/review-submit.js +172 -0
  41. package/dist/lib/tools/review-wait-pr.d.ts +11 -0
  42. package/dist/lib/tools/review-wait-pr.js +74 -0
  43. package/dist/lib/tools/review-wait.d.ts +6 -0
  44. package/dist/lib/tools/review-wait.js +33 -0
  45. package/dist/lib/tools/settings-get.d.ts +8 -0
  46. package/dist/lib/tools/settings-get.js +33 -0
  47. package/dist/lib/tools/settings-set.d.ts +11 -0
  48. package/dist/lib/tools/settings-set.js +62 -0
  49. package/dist/lib/tools/stack-actions.d.ts +4 -0
  50. package/dist/lib/tools/stack-actions.js +22 -0
  51. package/dist/lib/tools/stack-adopt.d.ts +44 -0
  52. package/dist/lib/tools/stack-adopt.js +55 -0
  53. package/dist/lib/tools/stack-list.d.ts +3 -0
  54. package/dist/lib/tools/stack-list.js +30 -0
  55. package/dist/lib/tools/stack-publish.d.ts +78 -0
  56. package/dist/lib/tools/stack-publish.js +89 -0
  57. package/dist/lib/tools/stack-set.d.ts +16 -0
  58. package/dist/lib/tools/stack-set.js +69 -0
  59. package/dist/lib/tools/stack-status.d.ts +3 -0
  60. package/dist/lib/tools/stack-status.js +57 -0
  61. package/dist/lib/tools/stack-summary.d.ts +2 -0
  62. package/dist/lib/tools/stack-summary.js +29 -0
  63. package/dist/lib/tools/stack-wait.d.ts +24 -0
  64. package/dist/lib/tools/stack-wait.js +84 -0
  65. package/dist/lib/tools/surge-setup.d.ts +6 -0
  66. package/dist/lib/tools/surge-setup.js +4 -0
  67. package/dist/lib/tools/types.d.ts +5 -0
  68. package/dist/lib/tools/types.js +1 -0
  69. package/dist/lib/tools/whoami.d.ts +3 -0
  70. package/dist/lib/tools/whoami.js +27 -0
  71. package/package.json +11 -3
package/dist/index.js CHANGED
@@ -3,10 +3,33 @@
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
+ // src/tools/chat-wake.ts
7
+ import { apiFetch } from "mergestorm/client";
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
+ function breezeChatId(value) {
10
+ const trimmed = value?.trim() ?? "";
11
+ return CHAT_ID.test(trimmed) ? trimmed.toLowerCase() : null;
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 };
18
+ try {
19
+ const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 1e4 });
20
+ 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 };
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
6
29
  // src/server.ts
7
30
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
31
  import { z as z5 } from "zod";
9
- import { CommandError as CommandError14, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
32
+ import { CommandError as CommandError15, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
10
33
 
11
34
  // src/brand-icons.ts
12
35
  var MARK_RECTS = [
@@ -86,6 +109,13 @@ var issueCreateSchema = {
86
109
  repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/).optional(),
87
110
  cwd: z.string().trim().min(1).optional()
88
111
  };
112
+ var hostedIssueCreateSchema = {
113
+ title: issueCreateSchema.title,
114
+ body: issueCreateSchema.body,
115
+ labels: issueCreateSchema.labels,
116
+ owner: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/),
117
+ repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/)
118
+ };
89
119
  var inputSchema = z.object(issueCreateSchema);
90
120
  async function issueCreate(input, cfg, deps = {}) {
91
121
  const parsed = inputSchema.safeParse(input);
@@ -126,7 +156,7 @@ async function issueCreate(input, cfg, deps = {}) {
126
156
 
127
157
  // src/tools/queue-status.ts
128
158
  import {
129
- apiFetch,
159
+ apiFetch as apiFetch2,
130
160
  CommandError as CommandError3,
131
161
  isCommandErrorCode,
132
162
  loadConfig as loadConfig2,
@@ -150,7 +180,7 @@ async function queueStatus(stackId, cfg) {
150
180
  try {
151
181
  const resolved = cfg ?? await loadConfig2();
152
182
  const id = stackId?.trim();
153
- const { status, body, retryAfterSeconds } = await apiFetch(
183
+ const { status, body, retryAfterSeconds } = await apiFetch2(
154
184
  resolved,
155
185
  id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
156
186
  );
@@ -226,6 +256,7 @@ async function queueStatus(stackId, cfg) {
226
256
  // src/tools/review-dismiss.ts
227
257
  import {
228
258
  dismissPrFindings,
259
+ dismissThreadsLine,
229
260
  stackWatchObligation as stackWatchObligation2
230
261
  } from "mergestorm/client";
231
262
 
@@ -283,6 +314,7 @@ function stackWaitStatusLine(envelope) {
283
314
  }
284
315
  async function stackWait(input, cfg, deps = {}) {
285
316
  const args = z2.object(stackWaitSchema).parse(input);
317
+ const config = cfg ?? await loadConfig3();
286
318
  const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
287
319
  stackId: args.stack_id,
288
320
  enrolledHeadSha: args.enrolled_head_sha ?? null,
@@ -291,7 +323,7 @@ async function stackWait(input, cfg, deps = {}) {
291
323
  } : void 0;
292
324
  let envelope;
293
325
  try {
294
- envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig3(), args.stack_id, {
326
+ envelope = await (deps.pollStackWatch ?? pollStackWatch)(config, args.stack_id, {
295
327
  timeoutMs: args.timeout_s * 1e3,
296
328
  cursor,
297
329
  signal: deps.signal
@@ -312,9 +344,12 @@ async function stackWait(input, cfg, deps = {}) {
312
344
  envelope = err.lastEnvelope;
313
345
  }
314
346
  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;
315
348
  return {
316
- summary: stackWaitSummary(sealed),
317
- data: sealed
349
+ summary: wake ? `${stackWaitSummary(sealed)}
350
+
351
+ ${CHAT_WAKE_STACK_NOTE}` : stackWaitSummary(sealed),
352
+ data: wake ? { ...sealed, chat_wake: wake } : sealed
318
353
  };
319
354
  }
320
355
 
@@ -335,6 +370,8 @@ function reviewDismissSummary(result) {
335
370
  `${pr} ${head}: dismissed ${result.dismissed.length}, already dismissed ${result.already_dismissed.length}, still open ${result.remaining.length}.`
336
371
  );
337
372
  for (const finding of result.remaining) lines.push(`- open: ${findingText(finding)}`);
373
+ const threads = dismissThreadsLine(result);
374
+ if (threads) lines.push(threads);
338
375
  }
339
376
  const seam = result.gate.seam;
340
377
  if (seam) {
@@ -378,6 +415,144 @@ async function reviewDismiss(input, cfg) {
378
415
  return { summary: withWatchText(watch, summary), data: { ...result, watch } };
379
416
  }
380
417
 
418
+ // src/tools/agent-request.ts
419
+ import { apiFetch as apiFetch3, CommandError as CommandError4, loadConfig as loadConfig4 } from "mergestorm/client";
420
+ async function agentRequest(route, init, cfg) {
421
+ const resolved = cfg ?? await loadConfig4();
422
+ const { status, body, retryAfterSeconds } = await apiFetch3(resolved, route, { method: init.method, ...init.json ? { json: init.json } : {} });
423
+ const data = body && typeof body === "object" && !Array.isArray(body) ? body : { body };
424
+ if (status === 404 && data.error === void 0) {
425
+ throw new CommandError4("This Mergestorm server does not offer that action yet.", 1, "not_found");
426
+ }
427
+ const message = typeof data.message === "string" ? data.message : `Mergestorm answered ${status}.`;
428
+ if (status >= 400) {
429
+ return {
430
+ summary: message,
431
+ isError: true,
432
+ data: {
433
+ ...data,
434
+ error: {
435
+ code: typeof data.error === "string" ? data.error : `http_${status}`,
436
+ message,
437
+ ...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
438
+ }
439
+ }
440
+ };
441
+ }
442
+ return { summary: message, data };
443
+ }
444
+
445
+ // src/tools/review-start.ts
446
+ async function reviewStartPr(input, cfg, chatId) {
447
+ const payload = await agentRequest("/api/v1/agents/vortex/review", {
448
+ method: "POST",
449
+ json: {
450
+ owner: input.owner,
451
+ repo: input.repo,
452
+ pr_number: input.pr_number,
453
+ ...input.head_sha ? { head_sha: input.head_sha } : {},
454
+ ...chatId ? { chat_id: chatId } : {}
455
+ }
456
+ }, cfg);
457
+ return payload.data.chat_wake ? { ...payload, summary: `${payload.summary} ${REVIEW_START_WAIT_NOTE}` } : payload;
458
+ }
459
+ async function reviewStopPr(input, cfg) {
460
+ return agentRequest("/api/v1/agents/vortex/stop", {
461
+ method: "POST",
462
+ json: { owner: input.owner, repo: input.repo, pr_number: input.pr_number }
463
+ }, cfg);
464
+ }
465
+
466
+ // src/tools/cyclone-job.ts
467
+ var CYCLONE_TASKS = ["fix_findings", "fix_ci", "resolve_conflicts", "build"];
468
+ var CYCLONE_JOB_WAIT_DEFAULT_S = 45;
469
+ var CYCLONE_JOB_WAIT_MAX_S = 100;
470
+ var CYCLONE_JOB_POLL_MS = 5e3;
471
+ var CYCLONE_JOB_TERMINAL_STATES = ["pushed", "completed", "failed"];
472
+ 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
+ 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
+ async function cycloneStart(input, cfg, chatId) {
476
+ const payload = await agentRequest("/api/v1/agents/cyclone/jobs", {
477
+ method: "POST",
478
+ json: {
479
+ task: input.task,
480
+ owner: input.owner,
481
+ repo: input.repo,
482
+ ...input.pr_number !== void 0 ? { pr_number: input.pr_number } : {},
483
+ ...input.failing_check ? { failing_check: input.failing_check } : {},
484
+ ...input.brief ? { brief: input.brief } : {},
485
+ ...chatId ? { chat_id: chatId } : {}
486
+ }
487
+ }, cfg);
488
+ 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(" ") };
490
+ }
491
+ function isCycloneJobTerminal(job) {
492
+ return CYCLONE_JOB_TERMINAL_STATES.includes(job.state);
493
+ }
494
+ function cycloneJobSummary(job) {
495
+ const target2 = job.pr_number ? `${job.owner}/${job.repo}#${job.pr_number}` : `${job.owner}/${job.repo}`;
496
+ const parts = [`Cyclone ${job.task ?? "job"} on ${target2}: ${job.state}`];
497
+ if (typeof job.outcome === "string" && job.outcome) parts[0] += ` (${job.outcome})`;
498
+ if (typeof job.result_sha === "string" && job.result_sha) parts.push(`pushed ${job.result_sha.slice(0, 10)}`);
499
+ if (typeof job.detail === "string" && job.detail) parts.push(job.detail);
500
+ return parts.join(". ");
501
+ }
502
+ function sleepFor(ms, signal) {
503
+ return new Promise((resolve2) => {
504
+ if (signal?.aborted) return resolve2();
505
+ const timer = setTimeout(done, ms);
506
+ function done() {
507
+ clearTimeout(timer);
508
+ signal?.removeEventListener("abort", done);
509
+ resolve2();
510
+ }
511
+ signal?.addEventListener("abort", done, { once: true });
512
+ });
513
+ }
514
+ async function cycloneJob(dispatchId, cfg, timeoutS = 0, deps = {}) {
515
+ const now = deps.now ?? Date.now;
516
+ const sleep = deps.sleep ?? sleepFor;
517
+ const deadline = now() + Math.min(Math.max(timeoutS, 0), CYCLONE_JOB_WAIT_MAX_S) * 1e3;
518
+ const route = `/api/v1/agents/cyclone/jobs/${encodeURIComponent(dispatchId)}`;
519
+ for (; ; ) {
520
+ const payload = await agentRequest(route, { method: "GET" }, cfg);
521
+ if (payload.isError) return payload;
522
+ if (isCycloneJobTerminal(payload.data)) return { ...payload, summary: cycloneJobSummary(payload.data) };
523
+ const left = deadline - now();
524
+ if (left <= 0 || deps.signal?.aborted) return { ...payload, summary: `${cycloneJobSummary(payload.data)}. ${CYCLONE_JOB_STILL_RUNNING_NOTE}` };
525
+ await sleep(Math.min(deps.pollMs ?? CYCLONE_JOB_POLL_MS, left), deps.signal);
526
+ }
527
+ }
528
+
529
+ // src/tools/stack-actions.ts
530
+ import { enqueueStack, loadConfig as loadConfig5, restackStack, stackWatchObligation as stackWatchObligation3 } from "mergestorm/client";
531
+ async function stackRestack(stackId, cfg) {
532
+ const result = await restackStack(stackId, cfg);
533
+ const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
534
+ return {
535
+ summary: withWatchText(watch, `Restacked stack ${stackId}: its layer branches were rebased onto their parents and pushed`),
536
+ data: { stack_id: stackId, result, watch }
537
+ };
538
+ }
539
+ async function stackEnqueue(stackId, cfg, chatId) {
540
+ const resolved = cfg ?? await loadConfig5();
541
+ const result = await enqueueStack(stackId, resolved);
542
+ const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
543
+ const wake = chatId ? await registerChatWake(resolved, chatId, { kind: "stack", stackId }) : null;
544
+ const summary = withWatchText(watch, `Sent stack ${stackId} to the Mergestorm merge queue`);
545
+ return {
546
+ summary: wake ? `${summary} ${CHAT_WAKE_STACK_NOTE}` : summary,
547
+ data: { stack_id: stackId, result, watch, ...wake ? { chat_wake: wake } : {} }
548
+ };
549
+ }
550
+
551
+ // src/tools/surge-setup.ts
552
+ async function surgeSetup(input, cfg) {
553
+ return agentRequest("/api/v1/agents/surge/setup", { method: "POST", json: { owner: input.owner, repo: input.repo } }, cfg);
554
+ }
555
+
381
556
  // src/tools/review-list.ts
382
557
  import { listJobs, toReviewJobEnvelope } from "mergestorm/client";
383
558
  async function reviewList(limit = 10, cfg) {
@@ -395,13 +570,13 @@ import { createHash } from "node:crypto";
395
570
  import { realpath } from "node:fs/promises";
396
571
  import { relative, resolve, sep } from "node:path";
397
572
  import {
398
- CommandError as CommandError4,
573
+ CommandError as CommandError5,
399
574
  REVIEW_THREAD_SLUG_RE,
400
575
  ReviewPollTimeoutError,
401
576
  collectReviewInput,
402
577
  discoverTrunk,
403
578
  formatReviewSubmitError,
404
- loadConfig as loadConfig4,
579
+ loadConfig as loadConfig6,
405
580
  loadReviewContext,
406
581
  pollReview,
407
582
  rateLimitedMessage as rateLimitedMessage2,
@@ -450,7 +625,7 @@ function assertPinnableSpecialists(specialists) {
450
625
  (id) => !PINNABLE_SPECIALISTS.includes(id)
451
626
  );
452
627
  if (unknown.length === 0) return;
453
- throw new CommandError4(
628
+ throw new CommandError5(
454
629
  `Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`,
455
630
  2,
456
631
  "usage"
@@ -462,7 +637,7 @@ function mcpSandboxRoot() {
462
637
  function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
463
638
  const seconds = timeout_s ?? defaultSeconds;
464
639
  if (!Number.isFinite(seconds) || seconds <= 0) {
465
- throw new CommandError4("timeout_s must be a positive number of seconds", 2, "usage");
640
+ throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
466
641
  }
467
642
  return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
468
643
  }
@@ -495,25 +670,25 @@ async function resolveInput(input) {
495
670
  ]);
496
671
  const relToSandbox = relative(sandboxReal, cwdReal);
497
672
  if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
498
- throw new CommandError4("cwd must be inside the configured sandbox root", 2, "usage");
673
+ throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
499
674
  }
500
675
  } catch (err) {
501
- if (err instanceof CommandError4) throw err;
502
- throw new CommandError4("cwd must be inside the configured sandbox root", 2, "usage");
676
+ if (err instanceof CommandError5) throw err;
677
+ throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
503
678
  }
504
679
  const base = input.base?.trim() || discoverTrunk(cwd);
505
680
  return collectReviewInput(base, head, cwd, sandboxRoot);
506
681
  }
507
682
  async function reviewSubmit(input, cfg, opts = {}) {
508
- const resolved = cfg ?? await loadConfig4();
683
+ const resolved = cfg ?? await loadConfig6();
509
684
  const wait = input.wait === true;
510
685
  const timeoutMs = waitTimeoutMs(input.timeout_s);
511
686
  assertPinnableSpecialists(input.specialists);
512
687
  if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
513
- throw new CommandError4("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
688
+ throw new CommandError5("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
514
689
  }
515
690
  if (input.context === "-") {
516
- throw new CommandError4(
691
+ throw new CommandError5(
517
692
  'context "-" (stdin) is not available over MCP; pass the text directly',
518
693
  2,
519
694
  "usage"
@@ -563,7 +738,7 @@ async function reviewSubmit(input, cfg, opts = {}) {
563
738
  };
564
739
  }
565
740
  if (!jobId) {
566
- throw new CommandError4("Review API returned no job_id");
741
+ throw new CommandError5("Review API returned no job_id");
567
742
  }
568
743
  if (!wait) {
569
744
  return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
@@ -592,14 +767,14 @@ async function reviewSubmit(input, cfg, opts = {}) {
592
767
  // src/tools/review-wait-pr.ts
593
768
  import {
594
769
  PrReviewPollTimeoutError,
595
- loadConfig as loadConfig5,
770
+ loadConfig as loadConfig7,
596
771
  noPrReviewYetMessage,
597
772
  pollPrVortexReview
598
773
  } from "mergestorm/client";
599
774
 
600
775
  // src/tools/review-get-pr.ts
601
776
  import {
602
- CommandError as CommandError5,
777
+ CommandError as CommandError6,
603
778
  getPrVortexReview,
604
779
  isCommandErrorCode as isCommandErrorCode3
605
780
  } from "mergestorm/client";
@@ -607,7 +782,7 @@ function prReviewTarget(owner, repo, prNumber) {
607
782
  const cleanOwner = owner.trim();
608
783
  const cleanRepo = repo.trim();
609
784
  if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
610
- throw new CommandError5(
785
+ throw new CommandError6(
611
786
  "owner, repo, and a positive pr_number are required",
612
787
  2,
613
788
  "usage"
@@ -675,7 +850,7 @@ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
675
850
  // src/tools/review-wait-pr.ts
676
851
  async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
677
852
  const target2 = prReviewTarget(owner, repo, prNumber);
678
- const resolved = cfg ?? await loadConfig5();
853
+ const resolved = cfg ?? await loadConfig7();
679
854
  if (timeout_s === 0) {
680
855
  return reviewGetPr(target2.owner, target2.repo, target2.prNumber, resolved, {
681
856
  afterSha,
@@ -718,15 +893,26 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
718
893
  finding_count: findingCount,
719
894
  phase: err.lastEnvelope?.phase ?? null
720
895
  });
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;
905
+ const shown = err.reviewSeen ? summary : `${summary} \xB7 ${noPrReviewYetMessage(target2.owner, target2.repo, target2.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
721
906
  return {
722
- summary: err.reviewSeen ? summary : `${summary} \xB7 ${noPrReviewYetMessage(target2.owner, target2.repo, target2.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`,
907
+ summary: wake ? `${shown} \xB7 ${CHAT_WAKE_REVIEW_NOTE}` : shown,
723
908
  data: {
724
909
  status,
725
910
  head_sha: err.lastEnvelope?.head_sha ?? null,
726
911
  pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
727
912
  finding_count: findingCount,
728
913
  phase: err.lastEnvelope?.phase ?? null,
729
- error: err.message
914
+ error: err.message,
915
+ ...wake ? { chat_wake: wake } : {}
730
916
  }
731
917
  };
732
918
  }
@@ -743,15 +929,15 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
743
929
 
744
930
  // src/tools/review-wait.ts
745
931
  import {
746
- CommandError as CommandError7,
932
+ CommandError as CommandError8,
747
933
  ReviewPollTimeoutError as ReviewPollTimeoutError2,
748
- loadConfig as loadConfig6,
934
+ loadConfig as loadConfig8,
749
935
  pollReview as pollReview2
750
936
  } from "mergestorm/client";
751
937
 
752
938
  // src/tools/review-get.ts
753
939
  import {
754
- CommandError as CommandError6,
940
+ CommandError as CommandError7,
755
941
  getReview,
756
942
  toReviewJobEnvelope as toReviewJobEnvelope3
757
943
  } from "mergestorm/client";
@@ -765,7 +951,7 @@ async function reviewGet(jobId, cfg) {
765
951
  throw err;
766
952
  }
767
953
  if (!row || typeof row !== "object" || typeof row.status !== "string") {
768
- throw new CommandError6(`Unexpected response for review ${jobId}.`);
954
+ throw new CommandError7(`Unexpected response for review ${jobId}.`);
769
955
  }
770
956
  const envelope = toReviewJobEnvelope3(row, { jobId });
771
957
  const bits = [envelope.status];
@@ -778,10 +964,10 @@ async function reviewGet(jobId, cfg) {
778
964
 
779
965
  // src/tools/review-wait.ts
780
966
  async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
781
- const resolved = cfg ?? await loadConfig6();
967
+ const resolved = cfg ?? await loadConfig8();
782
968
  const id = jobId.trim();
783
969
  if (!id) {
784
- throw new CommandError7("job_id is required", 2, "usage");
970
+ throw new CommandError8("job_id is required", 2, "usage");
785
971
  }
786
972
  if (timeout_s === 0) return reviewGet(id, resolved);
787
973
  try {
@@ -806,7 +992,7 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
806
992
 
807
993
  // src/tools/settings-get.ts
808
994
  import {
809
- CommandError as CommandError8,
995
+ CommandError as CommandError9,
810
996
  getSettings
811
997
  } from "mergestorm/client";
812
998
  function settingsSummary(settings) {
@@ -824,7 +1010,7 @@ function settingsSummary(settings) {
824
1010
  async function settingsGet(cfg) {
825
1011
  const settings = await getSettings(cfg);
826
1012
  if (!settings) {
827
- throw new CommandError8(
1013
+ throw new CommandError9(
828
1014
  "Settings are not available (offline or API too old). Try again or open https://mergestorm.ai/settings"
829
1015
  );
830
1016
  }
@@ -836,7 +1022,7 @@ async function settingsGet(cfg) {
836
1022
 
837
1023
  // src/tools/settings-set.ts
838
1024
  import {
839
- CommandError as CommandError9,
1025
+ CommandError as CommandError10,
840
1026
  BEARER_SETTINGS,
841
1027
  SETTINGS_WRITABLE_KEYS,
842
1028
  patchSettings
@@ -848,7 +1034,7 @@ var SETTINGS_READ_ONLY_KEYS = [
848
1034
  function settingsPatchFromArgs(args) {
849
1035
  for (const key of SETTINGS_READ_ONLY_KEYS) {
850
1036
  if (args[key] !== void 0) {
851
- throw new CommandError9(`${key} is read-only and cannot be set.`, 2, "usage");
1037
+ throw new CommandError10(`${key} is read-only and cannot be set.`, 2, "usage");
852
1038
  }
853
1039
  }
854
1040
  const patch = {};
@@ -859,7 +1045,7 @@ function settingsPatchFromArgs(args) {
859
1045
  if ("kind" in row && (row.kind === "seconds" || row.kind === "count")) {
860
1046
  if (typeof value !== "number" || !Number.isInteger(value) || value < row.min || value > row.max) {
861
1047
  const unit = row.kind === "seconds" ? " of seconds" : "";
862
- throw new CommandError9(
1048
+ throw new CommandError10(
863
1049
  `${key} takes a whole number${unit} from ${row.min} through ${row.max}.`,
864
1050
  2,
865
1051
  "usage"
@@ -870,17 +1056,17 @@ function settingsPatchFromArgs(args) {
870
1056
  }
871
1057
  if ("kind" in row) {
872
1058
  const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
873
- if (!valid) throw new CommandError9(`Invalid value for ${key}.`, 2, "usage");
1059
+ if (!valid) throw new CommandError10(`Invalid value for ${key}.`, 2, "usage");
874
1060
  Object.assign(patch, { [key]: value });
875
1061
  continue;
876
1062
  }
877
1063
  if (typeof value !== "boolean") {
878
- throw new CommandError9(`${key} takes a boolean.`, 2, "usage");
1064
+ throw new CommandError10(`${key} takes a boolean.`, 2, "usage");
879
1065
  }
880
1066
  Object.assign(patch, { [key]: value });
881
1067
  }
882
1068
  if (Object.keys(patch).length === 0) {
883
- throw new CommandError9(
1069
+ throw new CommandError10(
884
1070
  `At least one settings key is required: ${SETTINGS_WRITABLE_KEYS.join(", ")}.`,
885
1071
  2,
886
1072
  "usage"
@@ -899,7 +1085,7 @@ async function settingsSet(args, cfg) {
899
1085
 
900
1086
  // src/tools/repo-settings.ts
901
1087
  import {
902
- CommandError as CommandError10,
1088
+ CommandError as CommandError11,
903
1089
  REPO_SETTINGS_KEYS,
904
1090
  listRepoSettings,
905
1091
  patchRepoSettings
@@ -911,7 +1097,7 @@ var ARG_BY_KEY = {
911
1097
  };
912
1098
  function parseRepo(raw) {
913
1099
  const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
914
- if (!match) throw new CommandError10("repo takes <owner>/<repo>.", 2, "usage");
1100
+ if (!match) throw new CommandError11("repo takes <owner>/<repo>.", 2, "usage");
915
1101
  return { owner: match[1], repo: match[2] };
916
1102
  }
917
1103
  function words(value) {
@@ -926,12 +1112,12 @@ function repoSettingsPatchFromArgs(args) {
926
1112
  const value = args[ARG_BY_KEY[key]];
927
1113
  if (value === void 0) continue;
928
1114
  if (value !== null && typeof value !== "boolean") {
929
- throw new CommandError10(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
1115
+ throw new CommandError11(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
930
1116
  }
931
1117
  patch[key] = value;
932
1118
  }
933
1119
  if (Object.keys(patch).length === 0) {
934
- throw new CommandError10(
1120
+ throw new CommandError11(
935
1121
  `At least one of ${Object.values(ARG_BY_KEY).join(", ")} is required.`,
936
1122
  2,
937
1123
  "usage"
@@ -950,7 +1136,7 @@ async function repoSettingsGet(args, cfg) {
950
1136
  }
951
1137
  const slug = `${wanted.owner}/${wanted.repo}`.toLowerCase();
952
1138
  const entry = all.find((candidate) => `${candidate.owner}/${candidate.repo}`.toLowerCase() === slug);
953
- if (!entry) throw new CommandError10(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
1139
+ if (!entry) throw new CommandError11(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
954
1140
  return { summary: repoSettingsSummary(entry), data: { ...entry } };
955
1141
  }
956
1142
  async function repoSettingsSet(args, cfg) {
@@ -960,11 +1146,11 @@ async function repoSettingsSet(args, cfg) {
960
1146
  }
961
1147
 
962
1148
  // src/tools/stack-adopt.ts
963
- import { adoptStack, CommandError as CommandError11, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
1149
+ import { adoptStack, CommandError as CommandError12, stackWatchObligation as stackWatchObligation5 } from "mergestorm/client";
964
1150
  import { z as z3 } from "zod";
965
1151
 
966
1152
  // src/tools/stack-set.ts
967
- import { setStackPolicy, stackWatchObligation as stackWatchObligation3 } from "mergestorm/client";
1153
+ import { setStackPolicy, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
968
1154
  function triWord(value) {
969
1155
  return value === null ? "default" : value ? "on" : "off";
970
1156
  }
@@ -1011,7 +1197,7 @@ async function stackSet(stackId, input, cfg) {
1011
1197
  if (input.auto_fix_ci !== void 0) {
1012
1198
  parts.push(`auto-fix-ci ${triWord(input.auto_fix_ci)}`);
1013
1199
  }
1014
- const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
1200
+ const watch = stackWatchObligation4({ stackId, terminal: null, unread: true, freshCursor: true });
1015
1201
  return {
1016
1202
  summary: withWatchText(watch, `${parts.join(", ")} for stack ${stackId}`),
1017
1203
  data: {
@@ -1059,7 +1245,7 @@ async function stackAdopt(input, cfg) {
1059
1245
  if (typeof stackId !== "string" || !stackId) {
1060
1246
  return { summary, data: { owner, repo, pr_number, result } };
1061
1247
  }
1062
- const watch = stackWatchObligation4({ stackId, terminal: null, unread: true, freshCursor: true });
1248
+ const watch = stackWatchObligation5({ stackId, terminal: null, unread: true, freshCursor: true });
1063
1249
  return {
1064
1250
  summary: withWatchText(watch, summary),
1065
1251
  data: { owner, repo, pr_number, result, watch }
@@ -1070,11 +1256,11 @@ async function stackAdopt(input, cfg) {
1070
1256
  summary: message,
1071
1257
  data: {
1072
1258
  error: {
1073
- code: err instanceof CommandError11 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
1259
+ code: err instanceof CommandError12 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
1074
1260
  message,
1075
- ...err instanceof CommandError11 ? { exit_code: err.exitCode } : {},
1076
- ...err instanceof CommandError11 && err.reason !== void 0 ? { reason: err.reason } : {},
1077
- ...err instanceof CommandError11 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
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 } : {}
1078
1264
  }
1079
1265
  },
1080
1266
  isError: true
@@ -1145,9 +1331,9 @@ async function stackList(cfg) {
1145
1331
 
1146
1332
  // src/tools/stack-publish.ts
1147
1333
  import {
1148
- CommandError as CommandError12,
1334
+ CommandError as CommandError13,
1149
1335
  publishWorkingBranch,
1150
- stackWatchObligation as stackWatchObligation5,
1336
+ stackWatchObligation as stackWatchObligation6,
1151
1337
  workingBranchPublication
1152
1338
  } from "mergestorm/client";
1153
1339
  import { z as z4 } from "zod";
@@ -1160,6 +1346,7 @@ var target = {
1160
1346
  var stackPublishSchema = {
1161
1347
  ...target,
1162
1348
  base: z4.string().trim().min(1).optional(),
1349
+ extend: z4.boolean().optional(),
1163
1350
  title: z4.string().trim().min(1).max(256).optional(),
1164
1351
  body: z4.string().max(6e4).optional(),
1165
1352
  replace_head: z4.string().regex(/^[0-9a-f]{40}$/).optional(),
@@ -1174,9 +1361,9 @@ function failure(err, fallback) {
1174
1361
  summary: message,
1175
1362
  data: {
1176
1363
  error: {
1177
- code: err instanceof CommandError12 ? err.code ?? fallback : fallback,
1364
+ code: err instanceof CommandError13 ? err.code ?? fallback : fallback,
1178
1365
  message,
1179
- ...err instanceof CommandError12 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1366
+ ...err instanceof CommandError13 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1180
1367
  }
1181
1368
  },
1182
1369
  isError: true
@@ -1202,6 +1389,7 @@ async function stackPublish(input, cfg, deps) {
1202
1389
  {
1203
1390
  ...targetOf(parsed.data, cfg),
1204
1391
  ...parsed.data.base ? { base: parsed.data.base } : {},
1392
+ ...parsed.data.extend ? { extend: true } : {},
1205
1393
  ...parsed.data.title ? { title: parsed.data.title } : {},
1206
1394
  ...parsed.data.body !== void 0 ? { body: parsed.data.body } : {},
1207
1395
  ...parsed.data.replace_head ? { replaceHead: parsed.data.replace_head } : {},
@@ -1209,12 +1397,12 @@ async function stackPublish(input, cfg, deps) {
1209
1397
  },
1210
1398
  deps
1211
1399
  );
1212
- const watch = stackWatchObligation5({ stackId: result.stack.id, terminal: null, unread: true, freshCursor: true });
1400
+ const watch = stackWatchObligation6({ stackId: result.stack.id, terminal: null, unread: true, freshCursor: true });
1213
1401
  const moved = result.branchCreated ? "created" : result.branchUpdated ? "updated" : "already current";
1214
1402
  return {
1215
1403
  summary: withWatchText(
1216
1404
  watch,
1217
- `Published ${result.owner}/${result.repo} ${result.branch} (${moved}) at ${result.head.sha}, PR #${result.pr.number}, stack ${result.stack.id}`
1405
+ `Published ${result.owner}/${result.repo} ${result.branch} (${moved}) at ${result.head.sha}, PR #${result.pr.number} (${result.stacked ? `a layer stacked on ${result.baseBranch}` : `a single PR onto ${result.baseBranch}`}), stack ${result.stack.id}`
1218
1406
  ),
1219
1407
  data: { result, watch }
1220
1408
  };
@@ -1246,14 +1434,14 @@ import {
1246
1434
  isCommandErrorCode as isCommandErrorCode5,
1247
1435
  stackBlockers as stackBlockers2,
1248
1436
  stackTerminalReason,
1249
- stackWatchObligation as stackWatchObligation6
1437
+ stackWatchObligation as stackWatchObligation7
1250
1438
  } from "mergestorm/client";
1251
1439
  async function stackStatus(stackId, cfg) {
1252
1440
  try {
1253
1441
  const stack = await getEnrichedStack(stackId, cfg);
1254
1442
  if (!stack) {
1255
1443
  const message = `Stack not found or not owned by the current user: ${stackId}`;
1256
- const watch2 = stackWatchObligation6({ stackId, terminal: "not_found" });
1444
+ const watch2 = stackWatchObligation7({ stackId, terminal: "not_found" });
1257
1445
  return {
1258
1446
  summary: withWatchText(watch2, message),
1259
1447
  data: {
@@ -1269,7 +1457,7 @@ async function stackStatus(stackId, cfg) {
1269
1457
  }
1270
1458
  const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
1271
1459
  const { attention, held, issues, currentCandidate, busy, agents, repair, landGatePending, queueWait } = stackBlockers2(stack, entries);
1272
- const watch = stackWatchObligation6({
1460
+ const watch = stackWatchObligation7({
1273
1461
  stackId: stack.id,
1274
1462
  terminal: stackTerminalReason(stack),
1275
1463
  cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
@@ -1282,7 +1470,7 @@ async function stackStatus(stackId, cfg) {
1282
1470
  };
1283
1471
  } catch (err) {
1284
1472
  if (isCommandErrorCode5(err, "rate_limited")) {
1285
- const watch = stackWatchObligation6({
1473
+ const watch = stackWatchObligation7({
1286
1474
  stackId,
1287
1475
  terminal: null,
1288
1476
  status: "rate_limited",
@@ -1308,18 +1496,18 @@ async function stackStatus(stackId, cfg) {
1308
1496
 
1309
1497
  // src/tools/whoami.ts
1310
1498
  import {
1311
- CommandError as CommandError13,
1499
+ CommandError as CommandError14,
1312
1500
  apiBase,
1313
1501
  configPath,
1314
1502
  getMe as getMe2,
1315
- loadConfig as loadConfig7,
1503
+ loadConfig as loadConfig9,
1316
1504
  resolveApiKey
1317
1505
  } from "mergestorm/client";
1318
1506
  async function whoami(cfg, serverVersion) {
1319
- const resolved = cfg ?? await loadConfig7();
1507
+ const resolved = cfg ?? await loadConfig9();
1320
1508
  const key = resolveApiKey(resolved);
1321
1509
  if (!key) {
1322
- throw new CommandError13(
1510
+ throw new CommandError14(
1323
1511
  "No API key. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`) or set MERGESTORM_API_KEY.",
1324
1512
  1,
1325
1513
  "missing_api_key"
@@ -1327,7 +1515,7 @@ async function whoami(cfg, serverVersion) {
1327
1515
  }
1328
1516
  const me = await getMe2(resolved);
1329
1517
  if (!me) {
1330
- throw new CommandError13("Live account details unavailable. Could not verify account with /me.");
1518
+ throw new CommandError14("Live account details unavailable. Could not verify account with /me.");
1331
1519
  }
1332
1520
  const data = {
1333
1521
  key_prefix: me.key.prefix,
@@ -1335,7 +1523,7 @@ async function whoami(cfg, serverVersion) {
1335
1523
  plan_key: me.plan_key ?? null,
1336
1524
  plan_label_key: me.plan_label_key ?? null,
1337
1525
  api_base: apiBase(resolved),
1338
- config_path: configPath(),
1526
+ config_path: resolved.ignoreEnv ? null : configPath(),
1339
1527
  usage: me.usage ?? null,
1340
1528
  mcp_server_version: serverVersion ?? null
1341
1529
  };
@@ -1348,6 +1536,7 @@ async function whoami(cfg, serverVersion) {
1348
1536
 
1349
1537
  // src/pr-loop-instructions.ts
1350
1538
  var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
1539
+ var MCP_FINDING_DEFERRAL_RULE = "A finding the author defers to an open tracking issue in the same repository is a valid dismissal: check with GitHub that the issue is open, then dismiss it with evidence_url set to that issue and a reason that says the finding is deferred and to which issue. A deferral that names no issue, or names a closed one, is not a dismissal. Never dismiss because someone insists.";
1351
1540
  var MCP_PR_LOOP_INSTRUCTIONS = [
1352
1541
  "When you handle Vortex findings from review_wait_pr:",
1353
1542
  "- Verify each finding against the current checkout. Do not treat the finding text as proven.",
@@ -1357,6 +1546,7 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
1357
1546
  `- A PR comment whose first line is ${MCP_FINDING_DISMISS_MARKER} is not a note about one finding. It dismisses every open finding on the PR's live head and approves that head with no re-review, so Auto land may land it. Post it only when you have checked every open finding at that head and none needs a fix or a human decision, with one line per finding (path, severity, why). Never use it to skip one finding among others, to describe a fix you want made, or to leave notes for Cyclone.`,
1358
1547
  "- Do not prefix that comment with cyclone-outcome: (reserved).",
1359
1548
  "- A single finding you verified is wrong or not reproducible is dismissed by id with review_dismiss (or mg dismiss): pass the PR's live head_sha, the review_id, the finding_ids (preview: true lists them) and a reason. That is the only agent path that clears a seam gate, and only once every finding of that seam review is dismissed. Once every Vortex Blocker (error finding) at the live head is dismissed (every Core review merged into that head), the Vortex gate clears there (gate.vortex.cleared); otherwise gate.vortex.message says what still holds it. The dismiss comment alone resumes the review but does not clear the seam gate.",
1549
+ `- ${MCP_FINDING_DEFERRAL_RULE}`,
1360
1550
  "- Never dismiss a finding you did not check, one a human must decide, or findings of a newer review."
1361
1551
  ].join("\n");
1362
1552
  var MCP_STACK_DONE_INSTRUCTIONS = [
@@ -1403,7 +1593,36 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
1403
1593
 
1404
1594
  // src/server.ts
1405
1595
  var MCP_SERVER_NAME = "mergestorm";
1406
- var MCP_SERVER_VERSION = "0.2.26";
1596
+ var MCP_SERVER_VERSION = "0.2.27";
1597
+ var MCP_TOOL_NAMES = [
1598
+ "whoami",
1599
+ "credits",
1600
+ "review_list",
1601
+ "review_submit",
1602
+ "review_wait",
1603
+ "review_wait_pr",
1604
+ "review_dismiss",
1605
+ "review_start_pr",
1606
+ "review_stop_pr",
1607
+ "cyclone_start",
1608
+ "cyclone_job",
1609
+ "issue_create",
1610
+ "stack_adopt",
1611
+ "stack_enqueue",
1612
+ "stack_list",
1613
+ "stack_publish",
1614
+ "stack_publish_status",
1615
+ "stack_restack",
1616
+ "stack_set",
1617
+ "stack_status",
1618
+ "stack_wait",
1619
+ "queue_status",
1620
+ "settings_get",
1621
+ "settings_set",
1622
+ "repo_settings_get",
1623
+ "repo_settings_set",
1624
+ "surge_setup"
1625
+ ];
1407
1626
  var MCP_SERVER_INSTRUCTIONS = [
1408
1627
  MCP_STACK_DONE_INSTRUCTIONS,
1409
1628
  MCP_PR_LOOP_INSTRUCTIONS,
@@ -1423,14 +1642,14 @@ function ok(payload) {
1423
1642
  };
1424
1643
  }
1425
1644
  function fail(err) {
1426
- const message = err instanceof CommandError14 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(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);
1427
1646
  return ok({
1428
1647
  isError: true,
1429
1648
  summary: message,
1430
1649
  data: {
1431
1650
  error: {
1432
1651
  message: err instanceof Error ? err.message : String(err),
1433
- ...err instanceof CommandError14 ? {
1652
+ ...err instanceof CommandError15 ? {
1434
1653
  exit_code: err.exitCode,
1435
1654
  ...err.code !== void 0 ? { code: err.code } : {},
1436
1655
  ...err.reason !== void 0 ? { reason: err.reason } : {},
@@ -1440,7 +1659,14 @@ function fail(err) {
1440
1659
  }
1441
1660
  });
1442
1661
  }
1443
- function createMergestormMcpServer() {
1662
+ var MCP_WORKSPACE_TOOL_NAMES = ["review_submit", "stack_publish", "stack_publish_status"];
1663
+ var MCP_HOSTED_TOOL_NAMES = MCP_TOOL_NAMES.filter(
1664
+ (name) => !MCP_WORKSPACE_TOOL_NAMES.includes(name)
1665
+ );
1666
+ 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
+ function createMergestormMcpServer(options = {}) {
1668
+ const { config, hosted = false } = options;
1669
+ const chatId = breezeChatId(options.chatId ?? (hosted ? null : process.env.MERGESTORM_CHAT_ID));
1444
1670
  const server2 = new McpServer(
1445
1671
  {
1446
1672
  name: MCP_SERVER_NAME,
@@ -1451,7 +1677,11 @@ function createMergestormMcpServer() {
1451
1677
  },
1452
1678
  { instructions: MCP_SERVER_INSTRUCTIONS }
1453
1679
  );
1454
- const addTool = server2.registerTool.bind(server2);
1680
+ const registerTool = server2.registerTool.bind(server2);
1681
+ const addTool = (name, toolConfig, cb) => {
1682
+ if (hosted && MCP_WORKSPACE_TOOL_NAMES.includes(name)) return;
1683
+ registerTool(name, toolConfig, cb);
1684
+ };
1455
1685
  addTool(
1456
1686
  "whoami",
1457
1687
  {
@@ -1461,7 +1691,7 @@ function createMergestormMcpServer() {
1461
1691
  },
1462
1692
  async () => {
1463
1693
  try {
1464
- return ok(await whoami(void 0, MCP_SERVER_VERSION));
1694
+ return ok(await whoami(config, MCP_SERVER_VERSION));
1465
1695
  } catch (err) {
1466
1696
  return fail(err);
1467
1697
  }
@@ -1476,7 +1706,7 @@ function createMergestormMcpServer() {
1476
1706
  },
1477
1707
  async () => {
1478
1708
  try {
1479
- return ok(await credits());
1709
+ return ok(await credits(config));
1480
1710
  } catch (err) {
1481
1711
  return fail(err);
1482
1712
  }
@@ -1494,7 +1724,7 @@ function createMergestormMcpServer() {
1494
1724
  },
1495
1725
  async ({ limit }) => {
1496
1726
  try {
1497
- return ok(await reviewList(limit));
1727
+ return ok(await reviewList(limit, config));
1498
1728
  } catch (err) {
1499
1729
  return fail(err);
1500
1730
  }
@@ -1523,7 +1753,7 @@ function createMergestormMcpServer() {
1523
1753
  },
1524
1754
  async (args) => {
1525
1755
  try {
1526
- return ok(await reviewSubmit(args));
1756
+ return ok(await reviewSubmit(args, config));
1527
1757
  } catch (err) {
1528
1758
  return fail(err);
1529
1759
  }
@@ -1542,7 +1772,7 @@ function createMergestormMcpServer() {
1542
1772
  },
1543
1773
  async ({ job_id, timeout_s }) => {
1544
1774
  try {
1545
- return ok(await reviewWait(job_id, timeout_s));
1775
+ return ok(await reviewWait(job_id, timeout_s, config));
1546
1776
  } catch (err) {
1547
1777
  return fail(err);
1548
1778
  }
@@ -1573,8 +1803,8 @@ function createMergestormMcpServer() {
1573
1803
  pr_number,
1574
1804
  after_sha,
1575
1805
  timeout_s,
1576
- void 0,
1577
- { pass, afterPass: after_pass }
1806
+ config,
1807
+ { pass, afterPass: after_pass, ...chatId ? { chatId } : {} }
1578
1808
  )
1579
1809
  );
1580
1810
  } catch (err) {
@@ -1586,7 +1816,7 @@ function createMergestormMcpServer() {
1586
1816
  "review_dismiss",
1587
1817
  {
1588
1818
  title: "Dismiss Vortex findings",
1589
- 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.` + STACK_WATCH_RESULT_NOTE,
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,
1590
1820
  inputSchema: {
1591
1821
  owner: z5.string().min(1),
1592
1822
  repo: z5.string().min(1),
@@ -1603,7 +1833,83 @@ function createMergestormMcpServer() {
1603
1833
  },
1604
1834
  async (args) => {
1605
1835
  try {
1606
- return ok(await reviewDismiss(args));
1836
+ return ok(await reviewDismiss(args, config));
1837
+ } catch (err) {
1838
+ return fail(err);
1839
+ }
1840
+ }
1841
+ );
1842
+ const prInput = {
1843
+ owner: z5.string().min(1),
1844
+ repo: z5.string().min(1),
1845
+ pr_number: z5.number().int().min(1)
1846
+ };
1847
+ addTool(
1848
+ "review_start_pr",
1849
+ {
1850
+ 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() },
1853
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1854
+ },
1855
+ async (args) => {
1856
+ try {
1857
+ return ok(await reviewStartPr(args, config, chatId));
1858
+ } catch (err) {
1859
+ return fail(err);
1860
+ }
1861
+ }
1862
+ );
1863
+ addTool(
1864
+ "review_stop_pr",
1865
+ {
1866
+ title: "Stop Vortex review",
1867
+ description: "Cancel the Vortex review running on a pull request. Starts nothing; says so when no review is running.",
1868
+ inputSchema: prInput,
1869
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1870
+ },
1871
+ async (args) => {
1872
+ try {
1873
+ return ok(await reviewStopPr(args, config));
1874
+ } catch (err) {
1875
+ return fail(err);
1876
+ }
1877
+ }
1878
+ );
1879
+ addTool(
1880
+ "cyclone_start",
1881
+ {
1882
+ 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.",
1884
+ 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()
1891
+ },
1892
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1893
+ },
1894
+ async (args) => {
1895
+ try {
1896
+ return ok(await cycloneStart(args, config, chatId));
1897
+ } catch (err) {
1898
+ return fail(err);
1899
+ }
1900
+ }
1901
+ );
1902
+ addTool(
1903
+ "cyclone_job",
1904
+ {
1905
+ title: "Wait for Cyclone job",
1906
+ 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() },
1908
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1909
+ },
1910
+ async ({ dispatch_id, timeout_s }, extra) => {
1911
+ try {
1912
+ return ok(await cycloneJob(dispatch_id, config, timeout_s ?? CYCLONE_JOB_WAIT_DEFAULT_S, { signal: extra.signal }));
1607
1913
  } catch (err) {
1608
1914
  return fail(err);
1609
1915
  }
@@ -1613,13 +1919,13 @@ function createMergestormMcpServer() {
1613
1919
  "issue_create",
1614
1920
  {
1615
1921
  title: "Open GitHub issue",
1616
- 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. Defaults to the origin repository of cwd; pass owner and repo together to name another. 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.",
1617
- inputSchema: issueCreateSchema,
1922
+ description: hosted ? 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. Defaults to the origin repository of cwd; pass owner and repo together to name another. 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.",
1923
+ inputSchema: hosted ? hostedIssueCreateSchema : issueCreateSchema,
1618
1924
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1619
1925
  },
1620
1926
  async (args) => {
1621
1927
  try {
1622
- return ok(await issueCreate(args));
1928
+ return ok(await issueCreate(args, config, hosted ? { parseGithubOriginRepo: () => null } : {}));
1623
1929
  } catch (err) {
1624
1930
  return fail(err);
1625
1931
  }
@@ -1635,7 +1941,7 @@ function createMergestormMcpServer() {
1635
1941
  },
1636
1942
  async (args) => {
1637
1943
  try {
1638
- return ok(await stackAdopt(args));
1944
+ return ok(await stackAdopt(args, config));
1639
1945
  } catch (err) {
1640
1946
  return fail(err);
1641
1947
  }
@@ -1650,7 +1956,7 @@ function createMergestormMcpServer() {
1650
1956
  },
1651
1957
  async () => {
1652
1958
  try {
1653
- return ok(await stackList());
1959
+ return ok(await stackList(config));
1654
1960
  } catch (err) {
1655
1961
  return fail(err);
1656
1962
  }
@@ -1660,13 +1966,13 @@ function createMergestormMcpServer() {
1660
1966
  "stack_publish",
1661
1967
  {
1662
1968
  title: "Publish branch through Mergestorm",
1663
- 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. 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,
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,
1664
1970
  inputSchema: stackPublishSchema,
1665
1971
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1666
1972
  },
1667
1973
  async (args) => {
1668
1974
  try {
1669
- return ok(await stackPublish(args));
1975
+ return ok(await stackPublish(args, config));
1670
1976
  } catch (err) {
1671
1977
  return fail(err);
1672
1978
  }
@@ -1682,7 +1988,7 @@ function createMergestormMcpServer() {
1682
1988
  },
1683
1989
  async (args) => {
1684
1990
  try {
1685
- return ok(await stackPublishStatus(args));
1991
+ return ok(await stackPublishStatus(args, config));
1686
1992
  } catch (err) {
1687
1993
  return fail(err);
1688
1994
  }
@@ -1712,13 +2018,45 @@ function createMergestormMcpServer() {
1712
2018
  ...auto_patch !== void 0 ? { auto_patch } : {},
1713
2019
  ...auto_resolve_conflicts !== void 0 ? { auto_resolve_conflicts } : {},
1714
2020
  ...auto_fix_ci !== void 0 ? { auto_fix_ci } : {}
1715
- })
2021
+ }, config)
1716
2022
  );
1717
2023
  } catch (err) {
1718
2024
  return fail(err);
1719
2025
  }
1720
2026
  }
1721
2027
  );
2028
+ addTool(
2029
+ "stack_restack",
2030
+ {
2031
+ 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) },
2034
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
2035
+ },
2036
+ async ({ stack_id }) => {
2037
+ try {
2038
+ return ok(await stackRestack(stack_id, config));
2039
+ } catch (err) {
2040
+ return fail(err);
2041
+ }
2042
+ }
2043
+ );
2044
+ addTool(
2045
+ "stack_enqueue",
2046
+ {
2047
+ 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) },
2050
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
2051
+ },
2052
+ async ({ stack_id }) => {
2053
+ try {
2054
+ return ok(await stackEnqueue(stack_id, config, chatId));
2055
+ } catch (err) {
2056
+ return fail(err);
2057
+ }
2058
+ }
2059
+ );
1722
2060
  addTool(
1723
2061
  "stack_status",
1724
2062
  {
@@ -1731,7 +2069,7 @@ function createMergestormMcpServer() {
1731
2069
  },
1732
2070
  async ({ stack_id }) => {
1733
2071
  try {
1734
- return ok(await stackStatus(stack_id));
2072
+ return ok(await stackStatus(stack_id, config));
1735
2073
  } catch (err) {
1736
2074
  return fail(err);
1737
2075
  }
@@ -1747,7 +2085,7 @@ function createMergestormMcpServer() {
1747
2085
  },
1748
2086
  async (args, extra) => {
1749
2087
  try {
1750
- const payload = await stackWait(args, void 0, { signal: extra.signal });
2088
+ const payload = await stackWait(args, config, { signal: extra.signal, ...chatId ? { chatId } : {} });
1751
2089
  return ok(payload);
1752
2090
  } catch (err) {
1753
2091
  return fail(err);
@@ -1766,7 +2104,7 @@ function createMergestormMcpServer() {
1766
2104
  },
1767
2105
  async ({ stack_id }) => {
1768
2106
  try {
1769
- return ok(await queueStatus(stack_id));
2107
+ return ok(await queueStatus(stack_id, config));
1770
2108
  } catch (err) {
1771
2109
  return fail(err);
1772
2110
  }
@@ -1782,7 +2120,7 @@ function createMergestormMcpServer() {
1782
2120
  },
1783
2121
  async (args) => {
1784
2122
  try {
1785
- return ok(await repoSettingsGet(args ?? {}));
2123
+ return ok(await repoSettingsGet(args ?? {}, config));
1786
2124
  } catch (err) {
1787
2125
  return fail(err);
1788
2126
  }
@@ -1803,7 +2141,23 @@ function createMergestormMcpServer() {
1803
2141
  },
1804
2142
  async (args) => {
1805
2143
  try {
1806
- return ok(await repoSettingsSet(args ?? {}));
2144
+ return ok(await repoSettingsSet(args ?? {}, config));
2145
+ } catch (err) {
2146
+ return fail(err);
2147
+ }
2148
+ }
2149
+ );
2150
+ addTool(
2151
+ "surge_setup",
2152
+ {
2153
+ title: "Set up Surge CI",
2154
+ 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) },
2156
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
2157
+ },
2158
+ async (args) => {
2159
+ try {
2160
+ return ok(await surgeSetup(args, config));
1807
2161
  } catch (err) {
1808
2162
  return fail(err);
1809
2163
  }
@@ -1818,7 +2172,7 @@ function createMergestormMcpServer() {
1818
2172
  },
1819
2173
  async () => {
1820
2174
  try {
1821
- return ok(await settingsGet());
2175
+ return ok(await settingsGet(config));
1822
2176
  } catch (err) {
1823
2177
  return fail(err);
1824
2178
  }
@@ -1844,7 +2198,7 @@ function createMergestormMcpServer() {
1844
2198
  },
1845
2199
  async (args) => {
1846
2200
  try {
1847
- return ok(await settingsSet(args ?? {}));
2201
+ return ok(await settingsSet(args ?? {}, config));
1848
2202
  } catch (err) {
1849
2203
  return fail(err);
1850
2204
  }