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.
- package/README.md +15 -2
- package/dist/index.js +450 -96
- package/dist/lib/brand-icons.d.ts +16 -0
- package/dist/lib/brand-icons.js +23 -0
- package/dist/lib/hosted.d.ts +7 -0
- package/dist/lib/hosted.js +21 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +5 -0
- package/dist/lib/pr-loop-instructions.d.ts +13 -0
- package/dist/lib/pr-loop-instructions.js +62 -0
- package/dist/lib/server.d.ts +22 -0
- package/dist/lib/server.js +558 -0
- package/dist/lib/tools/agent-request.d.ts +6 -0
- package/dist/lib/tools/agent-request.js +25 -0
- package/dist/lib/tools/chat-wake.d.ts +21 -0
- package/dist/lib/tools/chat-wake.js +26 -0
- package/dist/lib/tools/credits.d.ts +3 -0
- package/dist/lib/tools/credits.js +38 -0
- package/dist/lib/tools/cyclone-job.d.ts +29 -0
- package/dist/lib/tools/cyclone-job.js +70 -0
- package/dist/lib/tools/envelope.d.ts +5 -0
- package/dist/lib/tools/envelope.js +30 -0
- package/dist/lib/tools/issue-create.d.ts +47 -0
- package/dist/lib/tools/issue-create.js +53 -0
- package/dist/lib/tools/queue-status.d.ts +3 -0
- package/dist/lib/tools/queue-status.js +79 -0
- package/dist/lib/tools/repo-settings.d.ts +6 -0
- package/dist/lib/tools/repo-settings.js +54 -0
- package/dist/lib/tools/review-dismiss.d.ts +16 -0
- package/dist/lib/tools/review-dismiss.js +68 -0
- package/dist/lib/tools/review-get-pr.d.ts +22 -0
- package/dist/lib/tools/review-get-pr.js +52 -0
- package/dist/lib/tools/review-get.d.ts +3 -0
- package/dist/lib/tools/review-get.js +27 -0
- package/dist/lib/tools/review-list.d.ts +3 -0
- package/dist/lib/tools/review-list.js +10 -0
- package/dist/lib/tools/review-start.d.ts +11 -0
- package/dist/lib/tools/review-start.js +21 -0
- package/dist/lib/tools/review-submit.d.ts +36 -0
- package/dist/lib/tools/review-submit.js +172 -0
- package/dist/lib/tools/review-wait-pr.d.ts +11 -0
- package/dist/lib/tools/review-wait-pr.js +74 -0
- package/dist/lib/tools/review-wait.d.ts +6 -0
- package/dist/lib/tools/review-wait.js +33 -0
- package/dist/lib/tools/settings-get.d.ts +8 -0
- package/dist/lib/tools/settings-get.js +33 -0
- package/dist/lib/tools/settings-set.d.ts +11 -0
- package/dist/lib/tools/settings-set.js +62 -0
- package/dist/lib/tools/stack-actions.d.ts +4 -0
- package/dist/lib/tools/stack-actions.js +22 -0
- package/dist/lib/tools/stack-adopt.d.ts +44 -0
- package/dist/lib/tools/stack-adopt.js +55 -0
- package/dist/lib/tools/stack-list.d.ts +3 -0
- package/dist/lib/tools/stack-list.js +30 -0
- package/dist/lib/tools/stack-publish.d.ts +78 -0
- package/dist/lib/tools/stack-publish.js +89 -0
- package/dist/lib/tools/stack-set.d.ts +16 -0
- package/dist/lib/tools/stack-set.js +69 -0
- package/dist/lib/tools/stack-status.d.ts +3 -0
- package/dist/lib/tools/stack-status.js +57 -0
- package/dist/lib/tools/stack-summary.d.ts +2 -0
- package/dist/lib/tools/stack-summary.js +29 -0
- package/dist/lib/tools/stack-wait.d.ts +24 -0
- package/dist/lib/tools/stack-wait.js +84 -0
- package/dist/lib/tools/surge-setup.d.ts +6 -0
- package/dist/lib/tools/surge-setup.js +4 -0
- package/dist/lib/tools/types.d.ts +5 -0
- package/dist/lib/tools/types.js +1 -0
- package/dist/lib/tools/whoami.d.ts +3 -0
- package/dist/lib/tools/whoami.js +27 -0
- 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
|
|
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
|
|
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)(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
673
|
+
throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
|
|
499
674
|
}
|
|
500
675
|
} catch (err) {
|
|
501
|
-
if (err instanceof
|
|
502
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
932
|
+
CommandError as CommandError8,
|
|
747
933
|
ReviewPollTimeoutError as ReviewPollTimeoutError2,
|
|
748
|
-
loadConfig as
|
|
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
|
|
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
|
|
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
|
|
967
|
+
const resolved = cfg ?? await loadConfig8();
|
|
782
968
|
const id = jobId.trim();
|
|
783
969
|
if (!id) {
|
|
784
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
1259
|
+
code: err instanceof CommandError12 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
1074
1260
|
message,
|
|
1075
|
-
...err instanceof
|
|
1076
|
-
...err instanceof
|
|
1077
|
-
...err instanceof
|
|
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
|
|
1334
|
+
CommandError as CommandError13,
|
|
1149
1335
|
publishWorkingBranch,
|
|
1150
|
-
stackWatchObligation as
|
|
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
|
|
1364
|
+
code: err instanceof CommandError13 ? err.code ?? fallback : fallback,
|
|
1178
1365
|
message,
|
|
1179
|
-
...err instanceof
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1499
|
+
CommandError as CommandError14,
|
|
1312
1500
|
apiBase,
|
|
1313
1501
|
configPath,
|
|
1314
1502
|
getMe as getMe2,
|
|
1315
|
-
loadConfig as
|
|
1503
|
+
loadConfig as loadConfig9,
|
|
1316
1504
|
resolveApiKey
|
|
1317
1505
|
} from "mergestorm/client";
|
|
1318
1506
|
async function whoami(cfg, serverVersion) {
|
|
1319
|
-
const resolved = cfg ?? await
|
|
1507
|
+
const resolved = cfg ?? await loadConfig9();
|
|
1320
1508
|
const key = resolveApiKey(resolved);
|
|
1321
1509
|
if (!key) {
|
|
1322
|
-
throw new
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
}
|