mergestorm-mcp 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/index.js +828 -223
- 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 +14 -0
- package/dist/lib/pr-loop-instructions.js +73 -0
- package/dist/lib/server.d.ts +24 -0
- package/dist/lib/server.js +644 -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-task.d.ts +18 -0
- package/dist/lib/tools/chat-task.js +28 -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 +28 -0
- package/dist/lib/tools/cyclone-job.js +71 -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/memory.d.ts +68 -0
- package/dist/lib/tools/memory.js +74 -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 +78 -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 +22 -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 +66 -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 +44 -0
- package/dist/lib/tools/stack-wait.js +146 -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 +13 -3
package/dist/index.js
CHANGED
|
@@ -3,10 +3,36 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
|
+
// src/tools/chat-task.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
|
+
function chatTaskOf(value) {
|
|
14
|
+
if (!value || typeof value !== "object") return null;
|
|
15
|
+
const { id, title } = value;
|
|
16
|
+
return typeof id === "string" && typeof title === "string" ? { id, title } : null;
|
|
17
|
+
}
|
|
18
|
+
function chatTaskNote(task) {
|
|
19
|
+
return `This chat tracks it as background task ${task.id} "${task.title}" and is told the result when it ends. If the user is waiting for the outcome, wait for it before you end the reply, then tell them what actually happened.`;
|
|
20
|
+
}
|
|
21
|
+
async function trackInChat(cfg, chatId, target2, signal) {
|
|
22
|
+
const json = target2.kind === "review" ? { chat_id: chatId, kind: "review", owner: target2.owner, repo: target2.repo, pr_number: target2.prNumber } : { chat_id: chatId, kind: "stack", stack_id: target2.stackId };
|
|
23
|
+
try {
|
|
24
|
+
const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 1e4 });
|
|
25
|
+
const body = response.body;
|
|
26
|
+
return response.status === 200 ? chatTaskOf(body?.task) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
// src/server.ts
|
|
7
33
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
-
import { z as
|
|
9
|
-
import { CommandError as
|
|
34
|
+
import { z as z6 } from "zod";
|
|
35
|
+
import { CommandError as CommandError16, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
|
|
10
36
|
|
|
11
37
|
// src/brand-icons.ts
|
|
12
38
|
var MARK_RECTS = [
|
|
@@ -86,6 +112,13 @@ var issueCreateSchema = {
|
|
|
86
112
|
repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/).optional(),
|
|
87
113
|
cwd: z.string().trim().min(1).optional()
|
|
88
114
|
};
|
|
115
|
+
var hostedIssueCreateSchema = {
|
|
116
|
+
title: issueCreateSchema.title,
|
|
117
|
+
body: issueCreateSchema.body,
|
|
118
|
+
labels: issueCreateSchema.labels,
|
|
119
|
+
owner: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/),
|
|
120
|
+
repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/)
|
|
121
|
+
};
|
|
89
122
|
var inputSchema = z.object(issueCreateSchema);
|
|
90
123
|
async function issueCreate(input, cfg, deps = {}) {
|
|
91
124
|
const parsed = inputSchema.safeParse(input);
|
|
@@ -93,9 +126,9 @@ async function issueCreate(input, cfg, deps = {}) {
|
|
|
93
126
|
const message = "issue_create needs a one-line title of at most 256 characters, and owner and repo together";
|
|
94
127
|
return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
|
|
95
128
|
}
|
|
96
|
-
const { title, body, labels, owner, repo, cwd } = parsed.data;
|
|
129
|
+
const { title, body, labels, owner, repo: repo2, cwd } = parsed.data;
|
|
97
130
|
try {
|
|
98
|
-
const target2 = owner &&
|
|
131
|
+
const target2 = owner && repo2 ? { owner, repo: repo2 } : (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(cwd);
|
|
99
132
|
if (!target2) {
|
|
100
133
|
const message = "Could not read a GitHub owner/repo from origin. Pass owner and repo.";
|
|
101
134
|
return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
|
|
@@ -124,10 +157,96 @@ async function issueCreate(input, cfg, deps = {}) {
|
|
|
124
157
|
}
|
|
125
158
|
}
|
|
126
159
|
|
|
127
|
-
// src/tools/
|
|
160
|
+
// src/tools/memory.ts
|
|
128
161
|
import {
|
|
129
|
-
apiFetch,
|
|
130
162
|
CommandError as CommandError3,
|
|
163
|
+
MEMORY_KINDS,
|
|
164
|
+
MEMORY_MODES,
|
|
165
|
+
MEMORY_SCOPES,
|
|
166
|
+
MEMORY_SEARCH_LIMIT_MAX,
|
|
167
|
+
deleteMemory,
|
|
168
|
+
getMemoryMode,
|
|
169
|
+
listMemories,
|
|
170
|
+
saveMemory,
|
|
171
|
+
searchMemories,
|
|
172
|
+
setMemoryMode,
|
|
173
|
+
updateMemory
|
|
174
|
+
} from "mergestorm/client";
|
|
175
|
+
import { z as z2 } from "zod";
|
|
176
|
+
var memoryId = z2.string().uuid();
|
|
177
|
+
var memoryText = z2.string().trim().min(1).max(500);
|
|
178
|
+
var repo = z2.string().trim().min(3).max(200);
|
|
179
|
+
var listMemoriesSchema = {
|
|
180
|
+
scope: z2.enum(MEMORY_SCOPES).optional(),
|
|
181
|
+
repo: repo.optional(),
|
|
182
|
+
kind: z2.enum(MEMORY_KINDS).optional()
|
|
183
|
+
};
|
|
184
|
+
var searchMemorySchema = {
|
|
185
|
+
query: z2.string().trim().min(1).max(2e3),
|
|
186
|
+
repo: repo.optional(),
|
|
187
|
+
limit: z2.number().int().min(1).max(MEMORY_SEARCH_LIMIT_MAX).optional()
|
|
188
|
+
};
|
|
189
|
+
var rememberSchema = { text: memoryText, kind: z2.enum(MEMORY_KINDS), repo: repo.optional() };
|
|
190
|
+
var updateMemorySchema = { id: memoryId, text: memoryText, kind: z2.enum(MEMORY_KINDS).optional() };
|
|
191
|
+
var forgetMemorySchema = { id: memoryId };
|
|
192
|
+
var setMemoryModeSchema = { memory_mode: z2.enum(MEMORY_MODES) };
|
|
193
|
+
var liveApi = { listMemories, searchMemories, saveMemory, updateMemory, deleteMemory, getMemoryMode, setMemoryMode };
|
|
194
|
+
function memoryLine(memory) {
|
|
195
|
+
return `- [${memory.id}] (${memory.repo ? `${memory.kind}, ${memory.repo}` : memory.kind}) ${memory.text}`;
|
|
196
|
+
}
|
|
197
|
+
async function guarded(run) {
|
|
198
|
+
try {
|
|
199
|
+
return await run();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
const code = err instanceof CommandError3 ? err.code ?? "memory_failed" : "memory_failed";
|
|
203
|
+
return {
|
|
204
|
+
summary: message,
|
|
205
|
+
data: { error: { code, message, ...err instanceof CommandError3 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {} } },
|
|
206
|
+
isError: true
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function createMemoryToolHandlers(cfg, chatId, api = liveApi) {
|
|
211
|
+
return {
|
|
212
|
+
list: (input) => guarded(async () => {
|
|
213
|
+
const listed = await api.listMemories(input, cfg);
|
|
214
|
+
const count = listed.items.length === 1 ? "1 memory" : `${listed.items.length} memories`;
|
|
215
|
+
return { summary: [`Memory is ${listed.memory_mode}. ${count}.`, ...listed.items.map(memoryLine)].join("\n"), data: listed };
|
|
216
|
+
}),
|
|
217
|
+
search: (input) => guarded(async () => {
|
|
218
|
+
const found = await api.searchMemories({ ...input, chatId }, cfg);
|
|
219
|
+
const all = [...found.rules, ...found.items];
|
|
220
|
+
const summary = found.memory_mode === "off" ? "Memory is off, so nothing was searched." : all.length === 0 ? "Nothing in memory matches." : ["Memories that match, best first:", ...all.map(memoryLine)].join("\n");
|
|
221
|
+
return { summary, data: found };
|
|
222
|
+
}),
|
|
223
|
+
remember: (input) => guarded(async () => {
|
|
224
|
+
const saved = await api.saveMemory({ ...input, chatId }, cfg);
|
|
225
|
+
return { summary: saved.merged ? `That matched memory ${saved.id}, which now says this.` : `Saved to memory as ${saved.id}.`, data: saved };
|
|
226
|
+
}),
|
|
227
|
+
update: (input) => guarded(async () => {
|
|
228
|
+
const updated = await api.updateMemory(input.id, { text: input.text, ...input.kind ? { kind: input.kind } : {} }, cfg);
|
|
229
|
+
return { summary: `Memory ${updated.id} updated.`, data: updated };
|
|
230
|
+
}),
|
|
231
|
+
forget: (input) => guarded(async () => {
|
|
232
|
+
const deleted = await api.deleteMemory(input.id, cfg);
|
|
233
|
+
return { summary: `Memory ${deleted.id} forgotten.`, data: deleted };
|
|
234
|
+
}),
|
|
235
|
+
getMode: () => guarded(async () => {
|
|
236
|
+
const mode = await api.getMemoryMode(cfg);
|
|
237
|
+
return { summary: `Memory is ${mode.memory_mode}.`, data: mode };
|
|
238
|
+
}),
|
|
239
|
+
setMode: (input) => guarded(async () => {
|
|
240
|
+
const mode = await api.setMemoryMode(input.memory_mode, cfg);
|
|
241
|
+
return { summary: `Memory is now ${mode.memory_mode}.`, data: mode };
|
|
242
|
+
})
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/tools/queue-status.ts
|
|
247
|
+
import {
|
|
248
|
+
apiFetch as apiFetch2,
|
|
249
|
+
CommandError as CommandError4,
|
|
131
250
|
isCommandErrorCode,
|
|
132
251
|
loadConfig as loadConfig2,
|
|
133
252
|
rateLimitedMessage,
|
|
@@ -135,12 +254,12 @@ import {
|
|
|
135
254
|
} from "mergestorm/client";
|
|
136
255
|
function entrySummary(entry) {
|
|
137
256
|
const prNumber = entry.bounceDetail?.prNumber;
|
|
138
|
-
const
|
|
257
|
+
const repo2 = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
|
|
139
258
|
const reason = entry.bounceDetail?.kind ?? entry.bounceReason ?? entry.waitReason ?? void 0;
|
|
140
259
|
const sha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha ?? void 0;
|
|
141
260
|
return [
|
|
142
261
|
entry.state,
|
|
143
|
-
|
|
262
|
+
repo2,
|
|
144
263
|
`stack ${entry.stackId}`,
|
|
145
264
|
reason,
|
|
146
265
|
sha?.slice(0, 7)
|
|
@@ -150,17 +269,17 @@ async function queueStatus(stackId, cfg) {
|
|
|
150
269
|
try {
|
|
151
270
|
const resolved = cfg ?? await loadConfig2();
|
|
152
271
|
const id = stackId?.trim();
|
|
153
|
-
const { status, body, retryAfterSeconds } = await
|
|
272
|
+
const { status, body, retryAfterSeconds } = await apiFetch2(
|
|
154
273
|
resolved,
|
|
155
274
|
id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
|
|
156
275
|
);
|
|
157
276
|
if (status === 404) {
|
|
158
|
-
throw new
|
|
277
|
+
throw new CommandError4(
|
|
159
278
|
"Merge queue route returned 404. Deploy the API."
|
|
160
279
|
);
|
|
161
280
|
}
|
|
162
281
|
if (status === 429) {
|
|
163
|
-
throw new
|
|
282
|
+
throw new CommandError4(
|
|
164
283
|
rateLimitedMessage(retryAfterSeconds),
|
|
165
284
|
REVIEW_EXIT.rate_limited,
|
|
166
285
|
"rate_limited",
|
|
@@ -168,13 +287,13 @@ async function queueStatus(stackId, cfg) {
|
|
|
168
287
|
);
|
|
169
288
|
}
|
|
170
289
|
if (status !== 200) {
|
|
171
|
-
throw new
|
|
290
|
+
throw new CommandError4(
|
|
172
291
|
`Failed to get queue status (HTTP ${status}): ${JSON.stringify(body)}`
|
|
173
292
|
);
|
|
174
293
|
}
|
|
175
294
|
const entries = body.entries;
|
|
176
295
|
if (!Array.isArray(entries)) {
|
|
177
|
-
throw new
|
|
296
|
+
throw new CommandError4(
|
|
178
297
|
`Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
|
|
179
298
|
);
|
|
180
299
|
}
|
|
@@ -226,6 +345,7 @@ async function queueStatus(stackId, cfg) {
|
|
|
226
345
|
// src/tools/review-dismiss.ts
|
|
227
346
|
import {
|
|
228
347
|
dismissPrFindings,
|
|
348
|
+
dismissThreadsLine,
|
|
229
349
|
stackWatchObligation as stackWatchObligation2
|
|
230
350
|
} from "mergestorm/client";
|
|
231
351
|
|
|
@@ -239,13 +359,13 @@ import {
|
|
|
239
359
|
stackWatchBackgroundCommand,
|
|
240
360
|
stackWatchObligation
|
|
241
361
|
} from "mergestorm/client";
|
|
242
|
-
import { z as
|
|
362
|
+
import { z as z3 } from "zod";
|
|
243
363
|
var stackWaitSchema = {
|
|
244
|
-
stack_id:
|
|
245
|
-
timeout_s:
|
|
246
|
-
enrolled_head_sha:
|
|
247
|
-
after_finished_at:
|
|
248
|
-
bounce_id:
|
|
364
|
+
stack_id: z3.string().trim().min(1),
|
|
365
|
+
timeout_s: z3.number().finite().min(0).max(45).default(45),
|
|
366
|
+
enrolled_head_sha: z3.string().nullable().optional(),
|
|
367
|
+
after_finished_at: z3.string().nullable().optional(),
|
|
368
|
+
bounce_id: z3.string().nullable().optional()
|
|
249
369
|
};
|
|
250
370
|
function repairSummary(repair) {
|
|
251
371
|
if (!repair) return "";
|
|
@@ -259,6 +379,58 @@ function withWatchText(watch, body) {
|
|
|
259
379
|
|
|
260
380
|
${body}`;
|
|
261
381
|
}
|
|
382
|
+
var KEEP_EARLIER_CURSOR = "If you already hold a cursor from an earlier stack_wait on this stack, keep passing that cursor instead.";
|
|
383
|
+
function hostedStackWatch(watch) {
|
|
384
|
+
if (watch.done) {
|
|
385
|
+
return {
|
|
386
|
+
...watch,
|
|
387
|
+
next: null,
|
|
388
|
+
message: watch.reason === "not_found" ? "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, which can mean merged or closed, so landing is unconfirmed: check on GitHub that each PR is merged before reporting it landed. The watch is done." : watch.message
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const args = watch.next?.args;
|
|
392
|
+
const attention = watch.reason === "attention";
|
|
393
|
+
const retry = watch.reason === "rate_limited" ? " Wait retry_after_seconds first." : "";
|
|
394
|
+
const failed = watch.reason === "failed" ? " This read failed; if the next one fails too, tell the human the stack is no longer watched." : "";
|
|
395
|
+
const keep = watch.message.includes(KEEP_EARLIER_CURSOR) ? ` ${KEEP_EARLIER_CURSOR}` : "";
|
|
396
|
+
const resume = args ? attention ? ` After the blocker is handled, call stack_wait ${JSON.stringify(args)}.` : ` Next call: stack_wait ${JSON.stringify(args)}.` : "";
|
|
397
|
+
return {
|
|
398
|
+
...watch,
|
|
399
|
+
next: args && !attention ? { tool: "stack_wait", args } : null,
|
|
400
|
+
message: `This stack is not landed and the watch is not done.${retry}${failed}${resume}${keep}`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function hostedStackRepair(repair) {
|
|
404
|
+
const { steps: _steps, note: _note, findings: _findings, ...facts } = repair;
|
|
405
|
+
return facts;
|
|
406
|
+
}
|
|
407
|
+
function hostedRepairSummary(repair) {
|
|
408
|
+
const files = "files" in repair && repair.files.length ? ` Files: ${repair.files.join(", ")}.` : "";
|
|
409
|
+
const breeze = "publishedBy" in repair && repair.publishedBy === "breeze" ? ` ${repair.branch} is a Breeze branch${repair.liveParent ? ` on ${repair.liveParent}` : ""}: open_pull_request publishes it with single-parent commits only, so a merge commit cannot be published.` : "";
|
|
410
|
+
return `
|
|
411
|
+
Repair #${repair.prNumber} (${repair.kind}).${files}${breeze}`;
|
|
412
|
+
}
|
|
413
|
+
function hostedToolResult(result) {
|
|
414
|
+
const data = result.structuredContent;
|
|
415
|
+
const watch = data?.watch;
|
|
416
|
+
const repair = data?.repair;
|
|
417
|
+
const hasWatch = !!watch && typeof watch.message === "string";
|
|
418
|
+
const hasRepair = !!repair && typeof repair.steps === "string";
|
|
419
|
+
if (!data || !hasWatch && !hasRepair) return result;
|
|
420
|
+
let text = result.content[0]?.text ?? "";
|
|
421
|
+
const structured = { ...data };
|
|
422
|
+
if (hasWatch) {
|
|
423
|
+
const hosted = hostedStackWatch(watch);
|
|
424
|
+
structured.watch = hosted;
|
|
425
|
+
if (text.startsWith(watch.message)) text = `${hosted.message}${text.slice(watch.message.length)}`;
|
|
426
|
+
}
|
|
427
|
+
if (hasRepair) {
|
|
428
|
+
const hosted = hostedStackRepair(repair);
|
|
429
|
+
structured.repair = hosted;
|
|
430
|
+
text = text.replace(repairSummary(repair), hostedRepairSummary(repair));
|
|
431
|
+
}
|
|
432
|
+
return { ...result, content: [{ type: "text", text }, { type: "text", text: JSON.stringify(structured) }], structuredContent: structured };
|
|
433
|
+
}
|
|
262
434
|
function envelopeWatch(envelope) {
|
|
263
435
|
return envelope.watch ?? stackWatchObligation({
|
|
264
436
|
stackId: envelope.stackId,
|
|
@@ -267,10 +439,10 @@ function envelopeWatch(envelope) {
|
|
|
267
439
|
status: envelope.status
|
|
268
440
|
});
|
|
269
441
|
}
|
|
270
|
-
function stackWaitSummary(envelope) {
|
|
271
|
-
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope)}${repairSummary(envelope.repair)}`);
|
|
442
|
+
function stackWaitSummary(envelope, hosted = false) {
|
|
443
|
+
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope, hosted)}${repairSummary(envelope.repair)}`);
|
|
272
444
|
}
|
|
273
|
-
function stackWaitStatusLine(envelope) {
|
|
445
|
+
function stackWaitStatusLine(envelope, hosted) {
|
|
274
446
|
const base = `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending, envelope.actAfter ?? null, envelope.queueWait ?? null)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`;
|
|
275
447
|
if (envelopeWatch(envelope).done || envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
|
|
276
448
|
const selectors = [
|
|
@@ -279,10 +451,12 @@ function stackWaitStatusLine(envelope) {
|
|
|
279
451
|
...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
|
|
280
452
|
...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
|
|
281
453
|
];
|
|
282
|
-
|
|
454
|
+
const background = hosted ? "" : `only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId, envelope.cursor)} as a background command and end your turn; otherwise `;
|
|
455
|
+
return `${base} \xB7 watch not finished: ${background}call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
283
456
|
}
|
|
284
457
|
async function stackWait(input, cfg, deps = {}) {
|
|
285
|
-
const args =
|
|
458
|
+
const args = z3.object(stackWaitSchema).parse(input);
|
|
459
|
+
const config = cfg ?? await loadConfig3();
|
|
286
460
|
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
287
461
|
stackId: args.stack_id,
|
|
288
462
|
enrolledHeadSha: args.enrolled_head_sha ?? null,
|
|
@@ -291,7 +465,7 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
291
465
|
} : void 0;
|
|
292
466
|
let envelope;
|
|
293
467
|
try {
|
|
294
|
-
envelope = await (deps.pollStackWatch ?? pollStackWatch)(
|
|
468
|
+
envelope = await (deps.pollStackWatch ?? pollStackWatch)(config, args.stack_id, {
|
|
295
469
|
timeoutMs: args.timeout_s * 1e3,
|
|
296
470
|
cursor,
|
|
297
471
|
signal: deps.signal
|
|
@@ -300,7 +474,7 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
300
474
|
if (err instanceof StackWatchError) {
|
|
301
475
|
const envelope2 = { ...err.lastEnvelope, watch: envelopeWatch(err.lastEnvelope) };
|
|
302
476
|
return {
|
|
303
|
-
summary: stackWaitSummary(envelope2),
|
|
477
|
+
summary: stackWaitSummary(envelope2, deps.hosted),
|
|
304
478
|
data: {
|
|
305
479
|
...envelope2,
|
|
306
480
|
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
@@ -312,9 +486,12 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
312
486
|
envelope = err.lastEnvelope;
|
|
313
487
|
}
|
|
314
488
|
const sealed = { ...envelope, watch: envelopeWatch(envelope) };
|
|
489
|
+
const task = deps.chatId && !sealed.watch.done ? await trackInChat(config, deps.chatId, { kind: "stack", stackId: sealed.stackId }, deps.signal) : null;
|
|
315
490
|
return {
|
|
316
|
-
summary: stackWaitSummary(sealed)
|
|
317
|
-
|
|
491
|
+
summary: task ? `${stackWaitSummary(sealed, deps.hosted)}
|
|
492
|
+
|
|
493
|
+
${chatTaskNote(task)}` : stackWaitSummary(sealed, deps.hosted),
|
|
494
|
+
data: task ? { ...sealed, background_task: task } : sealed
|
|
318
495
|
};
|
|
319
496
|
}
|
|
320
497
|
|
|
@@ -335,6 +512,8 @@ function reviewDismissSummary(result) {
|
|
|
335
512
|
`${pr} ${head}: dismissed ${result.dismissed.length}, already dismissed ${result.already_dismissed.length}, still open ${result.remaining.length}.`
|
|
336
513
|
);
|
|
337
514
|
for (const finding of result.remaining) lines.push(`- open: ${findingText(finding)}`);
|
|
515
|
+
const threads = dismissThreadsLine(result);
|
|
516
|
+
if (threads) lines.push(threads);
|
|
338
517
|
}
|
|
339
518
|
const seam = result.gate.seam;
|
|
340
519
|
if (seam) {
|
|
@@ -378,6 +557,145 @@ async function reviewDismiss(input, cfg) {
|
|
|
378
557
|
return { summary: withWatchText(watch, summary), data: { ...result, watch } };
|
|
379
558
|
}
|
|
380
559
|
|
|
560
|
+
// src/tools/agent-request.ts
|
|
561
|
+
import { apiFetch as apiFetch3, CommandError as CommandError5, loadConfig as loadConfig4 } from "mergestorm/client";
|
|
562
|
+
async function agentRequest(route, init, cfg) {
|
|
563
|
+
const resolved = cfg ?? await loadConfig4();
|
|
564
|
+
const { status, body, retryAfterSeconds } = await apiFetch3(resolved, route, { method: init.method, ...init.json ? { json: init.json } : {} });
|
|
565
|
+
const data = body && typeof body === "object" && !Array.isArray(body) ? body : { body };
|
|
566
|
+
if (status === 404 && data.error === void 0) {
|
|
567
|
+
throw new CommandError5("This Mergestorm server does not offer that action yet.", 1, "not_found");
|
|
568
|
+
}
|
|
569
|
+
const message = typeof data.message === "string" ? data.message : `Mergestorm answered ${status}.`;
|
|
570
|
+
if (status >= 400) {
|
|
571
|
+
return {
|
|
572
|
+
summary: message,
|
|
573
|
+
isError: true,
|
|
574
|
+
data: {
|
|
575
|
+
...data,
|
|
576
|
+
error: {
|
|
577
|
+
code: typeof data.error === "string" ? data.error : `http_${status}`,
|
|
578
|
+
message,
|
|
579
|
+
...retryAfterSeconds !== void 0 ? { retry_after_seconds: retryAfterSeconds } : {}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return { summary: message, data };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/tools/review-start.ts
|
|
588
|
+
async function reviewStartPr(input, cfg, chatId) {
|
|
589
|
+
const payload = await agentRequest("/api/v1/agents/vortex/review", {
|
|
590
|
+
method: "POST",
|
|
591
|
+
json: {
|
|
592
|
+
owner: input.owner,
|
|
593
|
+
repo: input.repo,
|
|
594
|
+
pr_number: input.pr_number,
|
|
595
|
+
...input.head_sha ? { head_sha: input.head_sha } : {},
|
|
596
|
+
...chatId ? { chat_id: chatId } : {}
|
|
597
|
+
}
|
|
598
|
+
}, cfg);
|
|
599
|
+
const task = chatTaskOf(payload.data.background_task);
|
|
600
|
+
return task ? { ...payload, summary: `${payload.summary} ${chatTaskNote(task)}` } : payload;
|
|
601
|
+
}
|
|
602
|
+
async function reviewStopPr(input, cfg) {
|
|
603
|
+
return agentRequest("/api/v1/agents/vortex/stop", {
|
|
604
|
+
method: "POST",
|
|
605
|
+
json: { owner: input.owner, repo: input.repo, pr_number: input.pr_number }
|
|
606
|
+
}, cfg);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/tools/cyclone-job.ts
|
|
610
|
+
var CYCLONE_TASKS = ["fix_findings", "fix_ci", "resolve_conflicts", "build"];
|
|
611
|
+
var CYCLONE_JOB_WAIT_DEFAULT_S = 45;
|
|
612
|
+
var CYCLONE_JOB_WAIT_MAX_S = 100;
|
|
613
|
+
var CYCLONE_JOB_POLL_MS = 5e3;
|
|
614
|
+
var CYCLONE_JOB_TERMINAL_STATES = ["pushed", "completed", "failed"];
|
|
615
|
+
var CYCLONE_START_WAIT_NOTE = `Wait for it now: call cyclone_job with this dispatch_id and timeout_s ${CYCLONE_JOB_WAIT_DEFAULT_S}, again while it is queued or running, then tell the user what Cyclone pushed or why it failed.`;
|
|
616
|
+
var CYCLONE_JOB_STILL_RUNNING_NOTE = `Cyclone is not done. Call cyclone_job again with this dispatch_id and timeout_s ${CYCLONE_JOB_WAIT_DEFAULT_S}, and report the result once it is pushed, completed or failed.`;
|
|
617
|
+
async function cycloneStart(input, cfg, chatId) {
|
|
618
|
+
const payload = await agentRequest("/api/v1/agents/cyclone/jobs", {
|
|
619
|
+
method: "POST",
|
|
620
|
+
json: {
|
|
621
|
+
task: input.task,
|
|
622
|
+
owner: input.owner,
|
|
623
|
+
repo: input.repo,
|
|
624
|
+
...input.pr_number !== void 0 ? { pr_number: input.pr_number } : {},
|
|
625
|
+
...input.failing_check ? { failing_check: input.failing_check } : {},
|
|
626
|
+
...input.brief ? { brief: input.brief } : {},
|
|
627
|
+
...chatId ? { chat_id: chatId } : {}
|
|
628
|
+
}
|
|
629
|
+
}, cfg);
|
|
630
|
+
if (payload.isError || typeof payload.data.dispatch_id !== "string" || isCycloneJobTerminal(payload.data)) return payload;
|
|
631
|
+
const task = chatTaskOf(payload.data.background_task);
|
|
632
|
+
return { ...payload, summary: `${payload.summary} ${task ? chatTaskNote(task) : CYCLONE_START_WAIT_NOTE}` };
|
|
633
|
+
}
|
|
634
|
+
function isCycloneJobTerminal(job) {
|
|
635
|
+
return CYCLONE_JOB_TERMINAL_STATES.includes(job.state);
|
|
636
|
+
}
|
|
637
|
+
function cycloneJobSummary(job) {
|
|
638
|
+
const target2 = job.pr_number ? `${job.owner}/${job.repo}#${job.pr_number}` : `${job.owner}/${job.repo}`;
|
|
639
|
+
const parts = [`Cyclone ${job.task ?? "job"} on ${target2}: ${job.state}`];
|
|
640
|
+
if (typeof job.outcome === "string" && job.outcome) parts[0] += ` (${job.outcome})`;
|
|
641
|
+
if (typeof job.result_sha === "string" && job.result_sha) parts.push(`pushed ${job.result_sha.slice(0, 10)}`);
|
|
642
|
+
if (typeof job.detail === "string" && job.detail) parts.push(job.detail);
|
|
643
|
+
return parts.join(". ");
|
|
644
|
+
}
|
|
645
|
+
function sleepFor(ms, signal) {
|
|
646
|
+
return new Promise((resolve2) => {
|
|
647
|
+
if (signal?.aborted) return resolve2();
|
|
648
|
+
const timer = setTimeout(done, ms);
|
|
649
|
+
function done() {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
signal?.removeEventListener("abort", done);
|
|
652
|
+
resolve2();
|
|
653
|
+
}
|
|
654
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async function cycloneJob(dispatchId, cfg, timeoutS = 0, deps = {}) {
|
|
658
|
+
const now = deps.now ?? Date.now;
|
|
659
|
+
const sleep = deps.sleep ?? sleepFor;
|
|
660
|
+
const deadline = now() + Math.min(Math.max(timeoutS, 0), CYCLONE_JOB_WAIT_MAX_S) * 1e3;
|
|
661
|
+
const route = `/api/v1/agents/cyclone/jobs/${encodeURIComponent(dispatchId)}`;
|
|
662
|
+
for (; ; ) {
|
|
663
|
+
const payload = await agentRequest(route, { method: "GET" }, cfg);
|
|
664
|
+
if (payload.isError) return payload;
|
|
665
|
+
if (isCycloneJobTerminal(payload.data)) return { ...payload, summary: cycloneJobSummary(payload.data) };
|
|
666
|
+
const left = deadline - now();
|
|
667
|
+
if (left <= 0 || deps.signal?.aborted) return { ...payload, summary: `${cycloneJobSummary(payload.data)}. ${CYCLONE_JOB_STILL_RUNNING_NOTE}` };
|
|
668
|
+
await sleep(Math.min(deps.pollMs ?? CYCLONE_JOB_POLL_MS, left), deps.signal);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/tools/stack-actions.ts
|
|
673
|
+
import { enqueueStack, loadConfig as loadConfig5, restackStack, stackWatchObligation as stackWatchObligation3 } from "mergestorm/client";
|
|
674
|
+
async function stackRestack(stackId, cfg) {
|
|
675
|
+
const result = await restackStack(stackId, cfg);
|
|
676
|
+
const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
677
|
+
return {
|
|
678
|
+
summary: withWatchText(watch, `Restacked stack ${stackId}: its layer branches were rebased onto their parents and pushed`),
|
|
679
|
+
data: { stack_id: stackId, result, watch }
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function stackEnqueue(stackId, cfg, chatId) {
|
|
683
|
+
const resolved = cfg ?? await loadConfig5();
|
|
684
|
+
const result = await enqueueStack(stackId, resolved);
|
|
685
|
+
const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
686
|
+
const task = chatId ? await trackInChat(resolved, chatId, { kind: "stack", stackId }) : null;
|
|
687
|
+
const summary = withWatchText(watch, `Sent stack ${stackId} to the Mergestorm merge queue`);
|
|
688
|
+
return {
|
|
689
|
+
summary: task ? `${summary} ${chatTaskNote(task)}` : summary,
|
|
690
|
+
data: { stack_id: stackId, result, watch, ...task ? { background_task: task } : {} }
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/tools/surge-setup.ts
|
|
695
|
+
async function surgeSetup(input, cfg) {
|
|
696
|
+
return agentRequest("/api/v1/agents/surge/setup", { method: "POST", json: { owner: input.owner, repo: input.repo } }, cfg);
|
|
697
|
+
}
|
|
698
|
+
|
|
381
699
|
// src/tools/review-list.ts
|
|
382
700
|
import { listJobs, toReviewJobEnvelope } from "mergestorm/client";
|
|
383
701
|
async function reviewList(limit = 10, cfg) {
|
|
@@ -395,13 +713,13 @@ import { createHash } from "node:crypto";
|
|
|
395
713
|
import { realpath } from "node:fs/promises";
|
|
396
714
|
import { relative, resolve, sep } from "node:path";
|
|
397
715
|
import {
|
|
398
|
-
CommandError as
|
|
716
|
+
CommandError as CommandError6,
|
|
399
717
|
REVIEW_THREAD_SLUG_RE,
|
|
400
718
|
ReviewPollTimeoutError,
|
|
401
719
|
collectReviewInput,
|
|
402
720
|
discoverTrunk,
|
|
403
721
|
formatReviewSubmitError,
|
|
404
|
-
loadConfig as
|
|
722
|
+
loadConfig as loadConfig6,
|
|
405
723
|
loadReviewContext,
|
|
406
724
|
pollReview,
|
|
407
725
|
rateLimitedMessage as rateLimitedMessage2,
|
|
@@ -450,7 +768,7 @@ function assertPinnableSpecialists(specialists) {
|
|
|
450
768
|
(id) => !PINNABLE_SPECIALISTS.includes(id)
|
|
451
769
|
);
|
|
452
770
|
if (unknown.length === 0) return;
|
|
453
|
-
throw new
|
|
771
|
+
throw new CommandError6(
|
|
454
772
|
`Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`,
|
|
455
773
|
2,
|
|
456
774
|
"usage"
|
|
@@ -462,7 +780,7 @@ function mcpSandboxRoot() {
|
|
|
462
780
|
function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
|
|
463
781
|
const seconds = timeout_s ?? defaultSeconds;
|
|
464
782
|
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
465
|
-
throw new
|
|
783
|
+
throw new CommandError6("timeout_s must be a positive number of seconds", 2, "usage");
|
|
466
784
|
}
|
|
467
785
|
return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
|
|
468
786
|
}
|
|
@@ -495,25 +813,25 @@ async function resolveInput(input) {
|
|
|
495
813
|
]);
|
|
496
814
|
const relToSandbox = relative(sandboxReal, cwdReal);
|
|
497
815
|
if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
|
|
498
|
-
throw new
|
|
816
|
+
throw new CommandError6("cwd must be inside the configured sandbox root", 2, "usage");
|
|
499
817
|
}
|
|
500
818
|
} catch (err) {
|
|
501
|
-
if (err instanceof
|
|
502
|
-
throw new
|
|
819
|
+
if (err instanceof CommandError6) throw err;
|
|
820
|
+
throw new CommandError6("cwd must be inside the configured sandbox root", 2, "usage");
|
|
503
821
|
}
|
|
504
822
|
const base = input.base?.trim() || discoverTrunk(cwd);
|
|
505
823
|
return collectReviewInput(base, head, cwd, sandboxRoot);
|
|
506
824
|
}
|
|
507
825
|
async function reviewSubmit(input, cfg, opts = {}) {
|
|
508
|
-
const resolved = cfg ?? await
|
|
826
|
+
const resolved = cfg ?? await loadConfig6();
|
|
509
827
|
const wait = input.wait === true;
|
|
510
828
|
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
511
829
|
assertPinnableSpecialists(input.specialists);
|
|
512
830
|
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
513
|
-
throw new
|
|
831
|
+
throw new CommandError6("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
|
|
514
832
|
}
|
|
515
833
|
if (input.context === "-") {
|
|
516
|
-
throw new
|
|
834
|
+
throw new CommandError6(
|
|
517
835
|
'context "-" (stdin) is not available over MCP; pass the text directly',
|
|
518
836
|
2,
|
|
519
837
|
"usage"
|
|
@@ -563,7 +881,7 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
563
881
|
};
|
|
564
882
|
}
|
|
565
883
|
if (!jobId) {
|
|
566
|
-
throw new
|
|
884
|
+
throw new CommandError6("Review API returned no job_id");
|
|
567
885
|
}
|
|
568
886
|
if (!wait) {
|
|
569
887
|
return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
|
|
@@ -592,22 +910,22 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
592
910
|
// src/tools/review-wait-pr.ts
|
|
593
911
|
import {
|
|
594
912
|
PrReviewPollTimeoutError,
|
|
595
|
-
loadConfig as
|
|
913
|
+
loadConfig as loadConfig7,
|
|
596
914
|
noPrReviewYetMessage,
|
|
597
915
|
pollPrVortexReview
|
|
598
916
|
} from "mergestorm/client";
|
|
599
917
|
|
|
600
918
|
// src/tools/review-get-pr.ts
|
|
601
919
|
import {
|
|
602
|
-
CommandError as
|
|
920
|
+
CommandError as CommandError7,
|
|
603
921
|
getPrVortexReview,
|
|
604
922
|
isCommandErrorCode as isCommandErrorCode3
|
|
605
923
|
} from "mergestorm/client";
|
|
606
|
-
function prReviewTarget(owner,
|
|
924
|
+
function prReviewTarget(owner, repo2, prNumber) {
|
|
607
925
|
const cleanOwner = owner.trim();
|
|
608
|
-
const cleanRepo =
|
|
926
|
+
const cleanRepo = repo2.trim();
|
|
609
927
|
if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
|
|
610
|
-
throw new
|
|
928
|
+
throw new CommandError7(
|
|
611
929
|
"owner, repo, and a positive pr_number are required",
|
|
612
930
|
2,
|
|
613
931
|
"usage"
|
|
@@ -615,8 +933,8 @@ function prReviewTarget(owner, repo, prNumber) {
|
|
|
615
933
|
}
|
|
616
934
|
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
617
935
|
}
|
|
618
|
-
function prReviewSummary(owner,
|
|
619
|
-
const bits = [`${owner}/${
|
|
936
|
+
function prReviewSummary(owner, repo2, prNumber, envelope) {
|
|
937
|
+
const bits = [`${owner}/${repo2}#${prNumber}`, envelope.status];
|
|
620
938
|
const sha = envelope.head_sha?.trim();
|
|
621
939
|
if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
|
|
622
940
|
if (typeof envelope.finding_count === "number") {
|
|
@@ -627,16 +945,16 @@ function prReviewSummary(owner, repo, prNumber, envelope) {
|
|
|
627
945
|
if (envelope.phase) bits.push(String(envelope.phase));
|
|
628
946
|
return bits.join(" \xB7 ");
|
|
629
947
|
}
|
|
630
|
-
function payloadFromPrReview(owner,
|
|
948
|
+
function payloadFromPrReview(owner, repo2, prNumber, envelope) {
|
|
631
949
|
return {
|
|
632
|
-
summary: prReviewSummary(owner,
|
|
950
|
+
summary: prReviewSummary(owner, repo2, prNumber, envelope),
|
|
633
951
|
data: { ...envelope }
|
|
634
952
|
};
|
|
635
953
|
}
|
|
636
|
-
function payloadFromPrRateLimit(err, owner,
|
|
954
|
+
function payloadFromPrRateLimit(err, owner, repo2, prNumber) {
|
|
637
955
|
if (!isCommandErrorCode3(err, "rate_limited")) return null;
|
|
638
956
|
return {
|
|
639
|
-
summary: `${owner}/${
|
|
957
|
+
summary: `${owner}/${repo2}#${prNumber} \xB7 rate_limited`,
|
|
640
958
|
data: {
|
|
641
959
|
status: "rate_limited",
|
|
642
960
|
error: err.message,
|
|
@@ -644,8 +962,8 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
|
644
962
|
}
|
|
645
963
|
};
|
|
646
964
|
}
|
|
647
|
-
async function reviewGetPr(owner,
|
|
648
|
-
const target2 = prReviewTarget(owner,
|
|
965
|
+
async function reviewGetPr(owner, repo2, prNumber, cfg, opts = {}) {
|
|
966
|
+
const target2 = prReviewTarget(owner, repo2, prNumber);
|
|
649
967
|
try {
|
|
650
968
|
const envelope = await getPrVortexReview(
|
|
651
969
|
target2.owner,
|
|
@@ -673,9 +991,9 @@ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
|
673
991
|
}
|
|
674
992
|
|
|
675
993
|
// src/tools/review-wait-pr.ts
|
|
676
|
-
async function reviewWaitPr(owner,
|
|
677
|
-
const target2 = prReviewTarget(owner,
|
|
678
|
-
const resolved = cfg ?? await
|
|
994
|
+
async function reviewWaitPr(owner, repo2, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
995
|
+
const target2 = prReviewTarget(owner, repo2, prNumber);
|
|
996
|
+
const resolved = cfg ?? await loadConfig7();
|
|
679
997
|
if (timeout_s === 0) {
|
|
680
998
|
return reviewGetPr(target2.owner, target2.repo, target2.prNumber, resolved, {
|
|
681
999
|
afterSha,
|
|
@@ -718,15 +1036,18 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
718
1036
|
finding_count: findingCount,
|
|
719
1037
|
phase: err.lastEnvelope?.phase ?? null
|
|
720
1038
|
});
|
|
1039
|
+
const task = opts.chatId ? await trackInChat(resolved, opts.chatId, { kind: "review", owner: target2.owner, repo: target2.repo, prNumber: target2.prNumber }) : null;
|
|
1040
|
+
const shown = err.reviewSeen ? summary : `${summary} \xB7 ${noPrReviewYetMessage(target2.owner, target2.repo, target2.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
|
|
721
1041
|
return {
|
|
722
|
-
summary:
|
|
1042
|
+
summary: task ? `${shown} \xB7 ${chatTaskNote(task)}` : shown,
|
|
723
1043
|
data: {
|
|
724
1044
|
status,
|
|
725
1045
|
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
726
1046
|
pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
|
|
727
1047
|
finding_count: findingCount,
|
|
728
1048
|
phase: err.lastEnvelope?.phase ?? null,
|
|
729
|
-
error: err.message
|
|
1049
|
+
error: err.message,
|
|
1050
|
+
...task ? { background_task: task } : {}
|
|
730
1051
|
}
|
|
731
1052
|
};
|
|
732
1053
|
}
|
|
@@ -743,15 +1064,15 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
743
1064
|
|
|
744
1065
|
// src/tools/review-wait.ts
|
|
745
1066
|
import {
|
|
746
|
-
CommandError as
|
|
1067
|
+
CommandError as CommandError9,
|
|
747
1068
|
ReviewPollTimeoutError as ReviewPollTimeoutError2,
|
|
748
|
-
loadConfig as
|
|
1069
|
+
loadConfig as loadConfig8,
|
|
749
1070
|
pollReview as pollReview2
|
|
750
1071
|
} from "mergestorm/client";
|
|
751
1072
|
|
|
752
1073
|
// src/tools/review-get.ts
|
|
753
1074
|
import {
|
|
754
|
-
CommandError as
|
|
1075
|
+
CommandError as CommandError8,
|
|
755
1076
|
getReview,
|
|
756
1077
|
toReviewJobEnvelope as toReviewJobEnvelope3
|
|
757
1078
|
} from "mergestorm/client";
|
|
@@ -765,7 +1086,7 @@ async function reviewGet(jobId, cfg) {
|
|
|
765
1086
|
throw err;
|
|
766
1087
|
}
|
|
767
1088
|
if (!row || typeof row !== "object" || typeof row.status !== "string") {
|
|
768
|
-
throw new
|
|
1089
|
+
throw new CommandError8(`Unexpected response for review ${jobId}.`);
|
|
769
1090
|
}
|
|
770
1091
|
const envelope = toReviewJobEnvelope3(row, { jobId });
|
|
771
1092
|
const bits = [envelope.status];
|
|
@@ -778,10 +1099,10 @@ async function reviewGet(jobId, cfg) {
|
|
|
778
1099
|
|
|
779
1100
|
// src/tools/review-wait.ts
|
|
780
1101
|
async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
781
|
-
const resolved = cfg ?? await
|
|
1102
|
+
const resolved = cfg ?? await loadConfig8();
|
|
782
1103
|
const id = jobId.trim();
|
|
783
1104
|
if (!id) {
|
|
784
|
-
throw new
|
|
1105
|
+
throw new CommandError9("job_id is required", 2, "usage");
|
|
785
1106
|
}
|
|
786
1107
|
if (timeout_s === 0) return reviewGet(id, resolved);
|
|
787
1108
|
try {
|
|
@@ -806,7 +1127,7 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
|
806
1127
|
|
|
807
1128
|
// src/tools/settings-get.ts
|
|
808
1129
|
import {
|
|
809
|
-
CommandError as
|
|
1130
|
+
CommandError as CommandError10,
|
|
810
1131
|
getSettings
|
|
811
1132
|
} from "mergestorm/client";
|
|
812
1133
|
function settingsSummary(settings) {
|
|
@@ -824,7 +1145,7 @@ function settingsSummary(settings) {
|
|
|
824
1145
|
async function settingsGet(cfg) {
|
|
825
1146
|
const settings = await getSettings(cfg);
|
|
826
1147
|
if (!settings) {
|
|
827
|
-
throw new
|
|
1148
|
+
throw new CommandError10(
|
|
828
1149
|
"Settings are not available (offline or API too old). Try again or open https://mergestorm.ai/settings"
|
|
829
1150
|
);
|
|
830
1151
|
}
|
|
@@ -836,7 +1157,7 @@ async function settingsGet(cfg) {
|
|
|
836
1157
|
|
|
837
1158
|
// src/tools/settings-set.ts
|
|
838
1159
|
import {
|
|
839
|
-
CommandError as
|
|
1160
|
+
CommandError as CommandError11,
|
|
840
1161
|
BEARER_SETTINGS,
|
|
841
1162
|
SETTINGS_WRITABLE_KEYS,
|
|
842
1163
|
patchSettings
|
|
@@ -848,7 +1169,7 @@ var SETTINGS_READ_ONLY_KEYS = [
|
|
|
848
1169
|
function settingsPatchFromArgs(args) {
|
|
849
1170
|
for (const key of SETTINGS_READ_ONLY_KEYS) {
|
|
850
1171
|
if (args[key] !== void 0) {
|
|
851
|
-
throw new
|
|
1172
|
+
throw new CommandError11(`${key} is read-only and cannot be set.`, 2, "usage");
|
|
852
1173
|
}
|
|
853
1174
|
}
|
|
854
1175
|
const patch = {};
|
|
@@ -859,7 +1180,7 @@ function settingsPatchFromArgs(args) {
|
|
|
859
1180
|
if ("kind" in row && (row.kind === "seconds" || row.kind === "count")) {
|
|
860
1181
|
if (typeof value !== "number" || !Number.isInteger(value) || value < row.min || value > row.max) {
|
|
861
1182
|
const unit = row.kind === "seconds" ? " of seconds" : "";
|
|
862
|
-
throw new
|
|
1183
|
+
throw new CommandError11(
|
|
863
1184
|
`${key} takes a whole number${unit} from ${row.min} through ${row.max}.`,
|
|
864
1185
|
2,
|
|
865
1186
|
"usage"
|
|
@@ -869,18 +1190,18 @@ function settingsPatchFromArgs(args) {
|
|
|
869
1190
|
continue;
|
|
870
1191
|
}
|
|
871
1192
|
if ("kind" in row) {
|
|
872
|
-
const valid = row.kind === "logins" ? Array.isArray(value) && value.every((
|
|
873
|
-
if (!valid) throw new
|
|
1193
|
+
const valid = row.kind === "logins" || row.kind === "paths" ? Array.isArray(value) && value.every((entry) => typeof entry === "string") : typeof value === "string" && row.values.includes(value);
|
|
1194
|
+
if (!valid) throw new CommandError11(`Invalid value for ${key}.`, 2, "usage");
|
|
874
1195
|
Object.assign(patch, { [key]: value });
|
|
875
1196
|
continue;
|
|
876
1197
|
}
|
|
877
1198
|
if (typeof value !== "boolean") {
|
|
878
|
-
throw new
|
|
1199
|
+
throw new CommandError11(`${key} takes a boolean.`, 2, "usage");
|
|
879
1200
|
}
|
|
880
1201
|
Object.assign(patch, { [key]: value });
|
|
881
1202
|
}
|
|
882
1203
|
if (Object.keys(patch).length === 0) {
|
|
883
|
-
throw new
|
|
1204
|
+
throw new CommandError11(
|
|
884
1205
|
`At least one settings key is required: ${SETTINGS_WRITABLE_KEYS.join(", ")}.`,
|
|
885
1206
|
2,
|
|
886
1207
|
"usage"
|
|
@@ -899,7 +1220,7 @@ async function settingsSet(args, cfg) {
|
|
|
899
1220
|
|
|
900
1221
|
// src/tools/repo-settings.ts
|
|
901
1222
|
import {
|
|
902
|
-
CommandError as
|
|
1223
|
+
CommandError as CommandError12,
|
|
903
1224
|
REPO_SETTINGS_KEYS,
|
|
904
1225
|
listRepoSettings,
|
|
905
1226
|
patchRepoSettings
|
|
@@ -907,15 +1228,22 @@ import {
|
|
|
907
1228
|
var ARG_BY_KEY = {
|
|
908
1229
|
auto_patch_enabled: "auto_patch",
|
|
909
1230
|
auto_review_enabled: "auto_review",
|
|
910
|
-
auto_land_default: "auto_land"
|
|
1231
|
+
auto_land_default: "auto_land",
|
|
1232
|
+
merge_queue_batch_enabled: "merge_queue_batch",
|
|
1233
|
+
merge_queue_batch_size: "merge_queue_batch_size",
|
|
1234
|
+
merge_queue_lands_alone_paths: "merge_queue_lands_alone_paths",
|
|
1235
|
+
merge_queue_overdrive: "merge_queue_overdrive"
|
|
911
1236
|
};
|
|
912
1237
|
function parseRepo(raw) {
|
|
913
1238
|
const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
|
|
914
|
-
if (!match) throw new
|
|
1239
|
+
if (!match) throw new CommandError12("repo takes <owner>/<repo>.", 2, "usage");
|
|
915
1240
|
return { owner: match[1], repo: match[2] };
|
|
916
1241
|
}
|
|
917
1242
|
function words(value) {
|
|
918
|
-
|
|
1243
|
+
if (value === null) return "inherit";
|
|
1244
|
+
if (Array.isArray(value)) return value.length === 0 ? "none" : value.join(",");
|
|
1245
|
+
if (typeof value === "number") return String(value);
|
|
1246
|
+
return value ? "on" : "off";
|
|
919
1247
|
}
|
|
920
1248
|
function repoSettingsSummary(entry) {
|
|
921
1249
|
return `${entry.owner}/${entry.repo}: ${REPO_SETTINGS_KEYS.map((key) => `${ARG_BY_KEY[key]} ${words(entry[key])}`).join(" \xB7 ")}`;
|
|
@@ -925,13 +1253,27 @@ function repoSettingsPatchFromArgs(args) {
|
|
|
925
1253
|
for (const key of REPO_SETTINGS_KEYS) {
|
|
926
1254
|
const value = args[ARG_BY_KEY[key]];
|
|
927
1255
|
if (value === void 0) continue;
|
|
1256
|
+
if (key === "merge_queue_batch_size") {
|
|
1257
|
+
if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
1258
|
+
throw new CommandError12(`${ARG_BY_KEY[key]} takes a whole number or null.`, 2, "usage");
|
|
1259
|
+
}
|
|
1260
|
+
patch[key] = value;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (key === "merge_queue_lands_alone_paths") {
|
|
1264
|
+
if (value !== null && !(Array.isArray(value) && value.every((entry) => typeof entry === "string"))) {
|
|
1265
|
+
throw new CommandError12(`${ARG_BY_KEY[key]} takes a list of path patterns or null.`, 2, "usage");
|
|
1266
|
+
}
|
|
1267
|
+
patch[key] = value;
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
928
1270
|
if (value !== null && typeof value !== "boolean") {
|
|
929
|
-
throw new
|
|
1271
|
+
throw new CommandError12(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
|
|
930
1272
|
}
|
|
931
1273
|
patch[key] = value;
|
|
932
1274
|
}
|
|
933
1275
|
if (Object.keys(patch).length === 0) {
|
|
934
|
-
throw new
|
|
1276
|
+
throw new CommandError12(
|
|
935
1277
|
`At least one of ${Object.values(ARG_BY_KEY).join(", ")} is required.`,
|
|
936
1278
|
2,
|
|
937
1279
|
"usage"
|
|
@@ -950,21 +1292,21 @@ async function repoSettingsGet(args, cfg) {
|
|
|
950
1292
|
}
|
|
951
1293
|
const slug = `${wanted.owner}/${wanted.repo}`.toLowerCase();
|
|
952
1294
|
const entry = all.find((candidate) => `${candidate.owner}/${candidate.repo}`.toLowerCase() === slug);
|
|
953
|
-
if (!entry) throw new
|
|
1295
|
+
if (!entry) throw new CommandError12(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
|
|
954
1296
|
return { summary: repoSettingsSummary(entry), data: { ...entry } };
|
|
955
1297
|
}
|
|
956
1298
|
async function repoSettingsSet(args, cfg) {
|
|
957
|
-
const { owner, repo } = parseRepo(args.repo);
|
|
958
|
-
const entry = await patchRepoSettings(owner,
|
|
1299
|
+
const { owner, repo: repo2 } = parseRepo(args.repo);
|
|
1300
|
+
const entry = await patchRepoSettings(owner, repo2, repoSettingsPatchFromArgs(args), cfg);
|
|
959
1301
|
return { summary: repoSettingsSummary(entry), data: { ...entry } };
|
|
960
1302
|
}
|
|
961
1303
|
|
|
962
1304
|
// src/tools/stack-adopt.ts
|
|
963
|
-
import { adoptStack, CommandError as
|
|
964
|
-
import { z as
|
|
1305
|
+
import { adoptStack, CommandError as CommandError13, stackWatchObligation as stackWatchObligation5 } from "mergestorm/client";
|
|
1306
|
+
import { z as z4 } from "zod";
|
|
965
1307
|
|
|
966
1308
|
// src/tools/stack-set.ts
|
|
967
|
-
import { setStackPolicy, stackWatchObligation as
|
|
1309
|
+
import { setStackPolicy, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
|
|
968
1310
|
function triWord(value) {
|
|
969
1311
|
return value === null ? "default" : value ? "on" : "off";
|
|
970
1312
|
}
|
|
@@ -1011,7 +1353,7 @@ async function stackSet(stackId, input, cfg) {
|
|
|
1011
1353
|
if (input.auto_fix_ci !== void 0) {
|
|
1012
1354
|
parts.push(`auto-fix-ci ${triWord(input.auto_fix_ci)}`);
|
|
1013
1355
|
}
|
|
1014
|
-
const watch =
|
|
1356
|
+
const watch = stackWatchObligation4({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
1015
1357
|
return {
|
|
1016
1358
|
summary: withWatchText(watch, `${parts.join(", ")} for stack ${stackId}`),
|
|
1017
1359
|
data: {
|
|
@@ -1029,40 +1371,40 @@ async function stackSet(stackId, input, cfg) {
|
|
|
1029
1371
|
|
|
1030
1372
|
// src/tools/stack-adopt.ts
|
|
1031
1373
|
var stackAdoptSchema = {
|
|
1032
|
-
owner:
|
|
1033
|
-
repo:
|
|
1034
|
-
pr_number:
|
|
1035
|
-
auto_land:
|
|
1036
|
-
auto_review:
|
|
1037
|
-
auto_patch:
|
|
1038
|
-
auto_resolve_conflicts:
|
|
1039
|
-
auto_fix_ci:
|
|
1374
|
+
owner: z4.string().trim().min(1),
|
|
1375
|
+
repo: z4.string().trim().min(1),
|
|
1376
|
+
pr_number: z4.number().int().min(1),
|
|
1377
|
+
auto_land: z4.boolean().optional(),
|
|
1378
|
+
auto_review: z4.boolean().nullable().optional(),
|
|
1379
|
+
auto_patch: z4.boolean().nullable().optional(),
|
|
1380
|
+
auto_resolve_conflicts: z4.boolean().nullable().optional(),
|
|
1381
|
+
auto_fix_ci: z4.boolean().nullable().optional()
|
|
1040
1382
|
};
|
|
1041
|
-
var inputSchema2 =
|
|
1383
|
+
var inputSchema2 = z4.object(stackAdoptSchema);
|
|
1042
1384
|
async function stackAdopt(input, cfg) {
|
|
1043
1385
|
const parsed = inputSchema2.safeParse(input);
|
|
1044
1386
|
if (!parsed.success) {
|
|
1045
1387
|
const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
|
|
1046
1388
|
return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
|
|
1047
1389
|
}
|
|
1048
|
-
const { owner, repo, pr_number } = parsed.data;
|
|
1390
|
+
const { owner, repo: repo2, pr_number } = parsed.data;
|
|
1049
1391
|
try {
|
|
1050
1392
|
const result = await adoptStack(
|
|
1051
1393
|
owner,
|
|
1052
|
-
|
|
1394
|
+
repo2,
|
|
1053
1395
|
pr_number,
|
|
1054
1396
|
cfg,
|
|
1055
1397
|
stackSetPatch(parsed.data)
|
|
1056
1398
|
);
|
|
1057
|
-
const summary = `Adopted stack for ${owner}/${
|
|
1399
|
+
const summary = `Adopted stack for ${owner}/${repo2}#${pr_number}`;
|
|
1058
1400
|
const stackId = typeof result === "object" && result !== null ? result.stack?.id : void 0;
|
|
1059
1401
|
if (typeof stackId !== "string" || !stackId) {
|
|
1060
|
-
return { summary, data: { owner, repo, pr_number, result } };
|
|
1402
|
+
return { summary, data: { owner, repo: repo2, pr_number, result } };
|
|
1061
1403
|
}
|
|
1062
|
-
const watch =
|
|
1404
|
+
const watch = stackWatchObligation5({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
1063
1405
|
return {
|
|
1064
1406
|
summary: withWatchText(watch, summary),
|
|
1065
|
-
data: { owner, repo, pr_number, result, watch }
|
|
1407
|
+
data: { owner, repo: repo2, pr_number, result, watch }
|
|
1066
1408
|
};
|
|
1067
1409
|
} catch (err) {
|
|
1068
1410
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1070,11 +1412,11 @@ async function stackAdopt(input, cfg) {
|
|
|
1070
1412
|
summary: message,
|
|
1071
1413
|
data: {
|
|
1072
1414
|
error: {
|
|
1073
|
-
code: err instanceof
|
|
1415
|
+
code: err instanceof CommandError13 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
1074
1416
|
message,
|
|
1075
|
-
...err instanceof
|
|
1076
|
-
...err instanceof
|
|
1077
|
-
...err instanceof
|
|
1417
|
+
...err instanceof CommandError13 ? { exit_code: err.exitCode } : {},
|
|
1418
|
+
...err instanceof CommandError13 && err.reason !== void 0 ? { reason: err.reason } : {},
|
|
1419
|
+
...err instanceof CommandError13 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
1078
1420
|
}
|
|
1079
1421
|
},
|
|
1080
1422
|
isError: true
|
|
@@ -1145,38 +1487,39 @@ async function stackList(cfg) {
|
|
|
1145
1487
|
|
|
1146
1488
|
// src/tools/stack-publish.ts
|
|
1147
1489
|
import {
|
|
1148
|
-
CommandError as
|
|
1490
|
+
CommandError as CommandError14,
|
|
1149
1491
|
publishWorkingBranch,
|
|
1150
|
-
stackWatchObligation as
|
|
1492
|
+
stackWatchObligation as stackWatchObligation6,
|
|
1151
1493
|
workingBranchPublication
|
|
1152
1494
|
} from "mergestorm/client";
|
|
1153
|
-
import { z as
|
|
1495
|
+
import { z as z5 } from "zod";
|
|
1154
1496
|
var target = {
|
|
1155
|
-
cwd:
|
|
1156
|
-
owner:
|
|
1157
|
-
repo:
|
|
1158
|
-
branch:
|
|
1497
|
+
cwd: z5.string().trim().min(1).optional(),
|
|
1498
|
+
owner: z5.string().trim().min(1).optional(),
|
|
1499
|
+
repo: z5.string().trim().min(1).optional(),
|
|
1500
|
+
branch: z5.string().trim().min(1).optional()
|
|
1159
1501
|
};
|
|
1160
1502
|
var stackPublishSchema = {
|
|
1161
1503
|
...target,
|
|
1162
|
-
base:
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1504
|
+
base: z5.string().trim().min(1).optional(),
|
|
1505
|
+
extend: z5.boolean().optional(),
|
|
1506
|
+
title: z5.string().trim().min(1).max(256).optional(),
|
|
1507
|
+
body: z5.string().max(6e4).optional(),
|
|
1508
|
+
replace_head: z5.string().regex(/^[0-9a-f]{40}$/).optional(),
|
|
1509
|
+
auto_land: z5.boolean().optional()
|
|
1167
1510
|
};
|
|
1168
1511
|
var stackPublishStatusSchema = target;
|
|
1169
|
-
var publishInput =
|
|
1170
|
-
var statusInput =
|
|
1512
|
+
var publishInput = z5.object(stackPublishSchema);
|
|
1513
|
+
var statusInput = z5.object(stackPublishStatusSchema);
|
|
1171
1514
|
function failure(err, fallback) {
|
|
1172
1515
|
const message = err instanceof Error ? err.message : String(err);
|
|
1173
1516
|
return {
|
|
1174
1517
|
summary: message,
|
|
1175
1518
|
data: {
|
|
1176
1519
|
error: {
|
|
1177
|
-
code: err instanceof
|
|
1520
|
+
code: err instanceof CommandError14 ? err.code ?? fallback : fallback,
|
|
1178
1521
|
message,
|
|
1179
|
-
...err instanceof
|
|
1522
|
+
...err instanceof CommandError14 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
1180
1523
|
}
|
|
1181
1524
|
},
|
|
1182
1525
|
isError: true
|
|
@@ -1202,6 +1545,7 @@ async function stackPublish(input, cfg, deps) {
|
|
|
1202
1545
|
{
|
|
1203
1546
|
...targetOf(parsed.data, cfg),
|
|
1204
1547
|
...parsed.data.base ? { base: parsed.data.base } : {},
|
|
1548
|
+
...parsed.data.extend ? { extend: true } : {},
|
|
1205
1549
|
...parsed.data.title ? { title: parsed.data.title } : {},
|
|
1206
1550
|
...parsed.data.body !== void 0 ? { body: parsed.data.body } : {},
|
|
1207
1551
|
...parsed.data.replace_head ? { replaceHead: parsed.data.replace_head } : {},
|
|
@@ -1209,12 +1553,12 @@ async function stackPublish(input, cfg, deps) {
|
|
|
1209
1553
|
},
|
|
1210
1554
|
deps
|
|
1211
1555
|
);
|
|
1212
|
-
const watch =
|
|
1556
|
+
const watch = stackWatchObligation6({ stackId: result.stack.id, terminal: null, unread: true, freshCursor: true });
|
|
1213
1557
|
const moved = result.branchCreated ? "created" : result.branchUpdated ? "updated" : "already current";
|
|
1214
1558
|
return {
|
|
1215
1559
|
summary: withWatchText(
|
|
1216
1560
|
watch,
|
|
1217
|
-
`Published ${result.owner}/${result.repo} ${result.branch} (${moved}) at ${result.head.sha}, PR #${result.pr.number}, stack ${result.stack.id}`
|
|
1561
|
+
`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
1562
|
),
|
|
1219
1563
|
data: { result, watch }
|
|
1220
1564
|
};
|
|
@@ -1246,14 +1590,14 @@ import {
|
|
|
1246
1590
|
isCommandErrorCode as isCommandErrorCode5,
|
|
1247
1591
|
stackBlockers as stackBlockers2,
|
|
1248
1592
|
stackTerminalReason,
|
|
1249
|
-
stackWatchObligation as
|
|
1593
|
+
stackWatchObligation as stackWatchObligation7
|
|
1250
1594
|
} from "mergestorm/client";
|
|
1251
1595
|
async function stackStatus(stackId, cfg) {
|
|
1252
1596
|
try {
|
|
1253
1597
|
const stack = await getEnrichedStack(stackId, cfg);
|
|
1254
1598
|
if (!stack) {
|
|
1255
1599
|
const message = `Stack not found or not owned by the current user: ${stackId}`;
|
|
1256
|
-
const watch2 =
|
|
1600
|
+
const watch2 = stackWatchObligation7({ stackId, terminal: "not_found" });
|
|
1257
1601
|
return {
|
|
1258
1602
|
summary: withWatchText(watch2, message),
|
|
1259
1603
|
data: {
|
|
@@ -1269,7 +1613,7 @@ async function stackStatus(stackId, cfg) {
|
|
|
1269
1613
|
}
|
|
1270
1614
|
const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
|
|
1271
1615
|
const { attention, held, issues, currentCandidate, busy, agents, repair, landGatePending, queueWait } = stackBlockers2(stack, entries);
|
|
1272
|
-
const watch =
|
|
1616
|
+
const watch = stackWatchObligation7({
|
|
1273
1617
|
stackId: stack.id,
|
|
1274
1618
|
terminal: stackTerminalReason(stack),
|
|
1275
1619
|
cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
|
|
@@ -1282,7 +1626,7 @@ async function stackStatus(stackId, cfg) {
|
|
|
1282
1626
|
};
|
|
1283
1627
|
} catch (err) {
|
|
1284
1628
|
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
1285
|
-
const watch =
|
|
1629
|
+
const watch = stackWatchObligation7({
|
|
1286
1630
|
stackId,
|
|
1287
1631
|
terminal: null,
|
|
1288
1632
|
status: "rate_limited",
|
|
@@ -1308,18 +1652,18 @@ async function stackStatus(stackId, cfg) {
|
|
|
1308
1652
|
|
|
1309
1653
|
// src/tools/whoami.ts
|
|
1310
1654
|
import {
|
|
1311
|
-
CommandError as
|
|
1655
|
+
CommandError as CommandError15,
|
|
1312
1656
|
apiBase,
|
|
1313
1657
|
configPath,
|
|
1314
1658
|
getMe as getMe2,
|
|
1315
|
-
loadConfig as
|
|
1659
|
+
loadConfig as loadConfig9,
|
|
1316
1660
|
resolveApiKey
|
|
1317
1661
|
} from "mergestorm/client";
|
|
1318
1662
|
async function whoami(cfg, serverVersion) {
|
|
1319
|
-
const resolved = cfg ?? await
|
|
1663
|
+
const resolved = cfg ?? await loadConfig9();
|
|
1320
1664
|
const key = resolveApiKey(resolved);
|
|
1321
1665
|
if (!key) {
|
|
1322
|
-
throw new
|
|
1666
|
+
throw new CommandError15(
|
|
1323
1667
|
"No API key. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`) or set MERGESTORM_API_KEY.",
|
|
1324
1668
|
1,
|
|
1325
1669
|
"missing_api_key"
|
|
@@ -1327,7 +1671,7 @@ async function whoami(cfg, serverVersion) {
|
|
|
1327
1671
|
}
|
|
1328
1672
|
const me = await getMe2(resolved);
|
|
1329
1673
|
if (!me) {
|
|
1330
|
-
throw new
|
|
1674
|
+
throw new CommandError15("Live account details unavailable. Could not verify account with /me.");
|
|
1331
1675
|
}
|
|
1332
1676
|
const data = {
|
|
1333
1677
|
key_prefix: me.key.prefix,
|
|
@@ -1335,7 +1679,7 @@ async function whoami(cfg, serverVersion) {
|
|
|
1335
1679
|
plan_key: me.plan_key ?? null,
|
|
1336
1680
|
plan_label_key: me.plan_label_key ?? null,
|
|
1337
1681
|
api_base: apiBase(resolved),
|
|
1338
|
-
config_path: configPath(),
|
|
1682
|
+
config_path: resolved.ignoreEnv ? null : configPath(),
|
|
1339
1683
|
usage: me.usage ?? null,
|
|
1340
1684
|
mcp_server_version: serverVersion ?? null
|
|
1341
1685
|
};
|
|
@@ -1348,6 +1692,7 @@ async function whoami(cfg, serverVersion) {
|
|
|
1348
1692
|
|
|
1349
1693
|
// src/pr-loop-instructions.ts
|
|
1350
1694
|
var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
|
|
1695
|
+
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
1696
|
var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
1352
1697
|
"When you handle Vortex findings from review_wait_pr:",
|
|
1353
1698
|
"- Verify each finding against the current checkout. Do not treat the finding text as proven.",
|
|
@@ -1357,13 +1702,14 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
1357
1702
|
`- 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
1703
|
"- Do not prefix that comment with cyclone-outcome: (reserved).",
|
|
1359
1704
|
"- 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.",
|
|
1705
|
+
`- ${MCP_FINDING_DEFERRAL_RULE}`,
|
|
1360
1706
|
"- Never dismiss a finding you did not check, one a human must decide, or findings of a newer review."
|
|
1361
1707
|
].join("\n");
|
|
1362
1708
|
var MCP_STACK_DONE_INSTRUCTIONS = [
|
|
1363
1709
|
"When stack work is done:",
|
|
1364
1710
|
"- Stack work is done only when the stack is landed, or when the human takes it back (archives it or tells you to stop). A clean push, a submit, an adopt, or one merged layer is not done: merging a lower layer can break a layer above it.",
|
|
1365
1711
|
"- Every stack_wait, stack_status, stack_adopt, and stack_set result carries watch: {done, until, reason, next, message}. While watch.done is false, keep calling stack_wait with watch.next.args (the returned cursor), 45s at a time, or hand the waiting to the background watcher below, and do not end your task. Stop only when watch.done is true.",
|
|
1366
|
-
"- Only when your host has a confirmed notification that resumes this task, run `mg stack watch <stack-id>` as a background command with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs stack_wait against the Mergestorm API in slices of at most 300s, carries the cursor, stays silent while the stack is waiting or in_progress (busy[] holds and actAfter included), and costs no model tokens. It exits on attention (`MS-WATCH ATTENTION pr=<n> head=<sha> blocker=... repair=<kind>`, exit 3), on reads that keep failing (`MS-WATCH ATTENTION failed`, exit 3, after an auth or other permanent error twice, or after 15 minutes
|
|
1712
|
+
"- Only when your host has a confirmed notification that resumes this task, run `mg stack watch <stack-id>` as a background command with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs stack_wait against the Mergestorm API in slices of at most 300s, carries the cursor, stays silent while the stack is waiting or in_progress (busy[] holds and actAfter included), and costs no model tokens. It exits on attention (`MS-WATCH ATTENTION pr=<n> head=<sha> blocker=... repair=<kind>`, exit 3), on reads that keep failing (`MS-WATCH ATTENTION failed`, exit 3, after an auth or other permanent error twice, or after 15 minutes with no stack read (timeouts, 5xx, rate limits); tell the human the stack is no longer watched), or when the command cannot start (`MS-WATCH ATTENTION failed` with exit 2 for bad flags, exit 1 for no config or another startup error; tell the human the stack is not watched), on a closed, archived, or missing stack (`MS-WATCH ATTENTION stack=<id> reason=<reason>`, exit 3; verify missing stacks before reporting a landing), or on a confirmed landing (`MS-WATCH LANDED stack=<id> reason=landed`, exit 0). On an attention wake, read stack_status once, fix the blocker per mergestorm-bounce-watch, push, and start `mg stack watch <stack-id> --head <pushed-sha>` again. The command monitors the stack; it cannot wake an agent by itself. A confirmed notification is a host feature you can name that turns the command's exit or matching output into your next turn; a process left running, or output written to a log or terminal you do not read, is not one. Without a confirmed notification, keep using stack_wait with watch.next.args, or report that automatic follow-up is unavailable before ending the turn. Ending your turn while the watcher runs is not ending the task. Do not subscribe to GitHub events to wait on a stack.",
|
|
1367
1713
|
"- A layer PR shown as merged is merged into mg-stack-<n>, not main. The stack is landed only when the land PR (head mg-stack-<n>) merges into main or watch.done says landed. Never judge landing from GitHub PR state alone.",
|
|
1368
1714
|
"- attention means something for you to fix; repair, when present, names the concrete fix. A pending land gate (landGatePending: ci_pending) is a wait, not attention. attention covers every open layer, not only currentCandidate: it names the lowest layer with a blocker an agent must fix and no Vortex or Cyclone run working on it; once that blocker clears, the next read names the next blocked layer."
|
|
1369
1715
|
].join("\n");
|
|
@@ -1400,10 +1746,58 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
1400
1746
|
'- More states the watch names: a Vortex review skipped because the PR author is on the ignored authors list or because reviews are turned off for the repository is attention at once with repair kind vortex_skipped; a Vortex retry that is scheduled ("Vortex hit a temporary error on this head and will retry by itself") is a wait listed in issues[], never attention; "Review failed" carries repair kind vortex_failed (comment @mergestorm-vortex review or push a new commit; if it fails again tell the human); a PR open on mg-stack-N that is not a layer of the stack is attention at once, blocker "The land PR cannot be queued: ...", repair kind members_remaining (do not patch or push; close, retarget or adopt that PR).',
|
|
1401
1747
|
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
1402
1748
|
].join("\n");
|
|
1749
|
+
var MCP_HOSTED_SERVER_INSTRUCTIONS = [
|
|
1750
|
+
"Stacks:",
|
|
1751
|
+
"- A stack is done only when it is landed, or when the human takes it back (archives it or tells you to stop). A submit, an adopt or one merged layer is not done: merging a lower layer can break a layer above it.",
|
|
1752
|
+
"- A layer PR shown as merged is merged into mg-stack-<n>, not main. The stack is landed only when its land PR (head mg-stack-<n>) merges into main, which watch.done with reason landed reports. Never judge landing from GitHub PR state alone.",
|
|
1753
|
+
"- Results of the stack tools and review_dismiss carry watch: {done, reason, next, message}. While watch.done is false, call stack_wait with watch.next.args, which carry the cursor. Stop when watch.done is true.",
|
|
1754
|
+
"- attention means a layer needs a fix: it names the PR and the blocker, and repair, when present, names the fix. in_progress means Vortex or Cyclone is still working on it (busy[] says which) or a blocker waits for them to finish (actAfter agents_idle): wait, do not act. A pending land gate (landGatePending) is a wait, not attention.",
|
|
1755
|
+
"",
|
|
1756
|
+
"Vortex findings:",
|
|
1757
|
+
"- Dismiss a finding only after you checked it against the PR's live head and found it wrong or not actionable, with review_dismiss and a reason that gives your evidence. Never dismiss a finding you did not check or one from a newer review.",
|
|
1758
|
+
"- A finding a human must decide (Decision required, policy fork, needs a human) stays open: do not dismiss it; tell the human which finding it is."
|
|
1759
|
+
].join("\n");
|
|
1403
1760
|
|
|
1404
1761
|
// src/server.ts
|
|
1762
|
+
var BATCH_SIZE_ROW = BEARER_SETTINGS2.find((row) => row.key === "merge_queue_batch_size");
|
|
1405
1763
|
var MCP_SERVER_NAME = "mergestorm";
|
|
1406
|
-
var MCP_SERVER_VERSION = "0.2.
|
|
1764
|
+
var MCP_SERVER_VERSION = "0.2.28";
|
|
1765
|
+
var MCP_TOOL_NAMES = [
|
|
1766
|
+
"whoami",
|
|
1767
|
+
"credits",
|
|
1768
|
+
"review_list",
|
|
1769
|
+
"review_submit",
|
|
1770
|
+
"review_wait",
|
|
1771
|
+
"review_wait_pr",
|
|
1772
|
+
"review_dismiss",
|
|
1773
|
+
"review_start_pr",
|
|
1774
|
+
"review_stop_pr",
|
|
1775
|
+
"cyclone_start",
|
|
1776
|
+
"cyclone_job",
|
|
1777
|
+
"issue_create",
|
|
1778
|
+
"stack_adopt",
|
|
1779
|
+
"stack_enqueue",
|
|
1780
|
+
"stack_list",
|
|
1781
|
+
"stack_publish",
|
|
1782
|
+
"stack_publish_status",
|
|
1783
|
+
"stack_restack",
|
|
1784
|
+
"stack_set",
|
|
1785
|
+
"stack_status",
|
|
1786
|
+
"stack_wait",
|
|
1787
|
+
"queue_status",
|
|
1788
|
+
"settings_get",
|
|
1789
|
+
"settings_set",
|
|
1790
|
+
"repo_settings_get",
|
|
1791
|
+
"repo_settings_set",
|
|
1792
|
+
"surge_setup",
|
|
1793
|
+
"list_memories",
|
|
1794
|
+
"search_memory",
|
|
1795
|
+
"remember",
|
|
1796
|
+
"update_memory",
|
|
1797
|
+
"forget_memory",
|
|
1798
|
+
"get_memory_mode",
|
|
1799
|
+
"set_memory_mode"
|
|
1800
|
+
];
|
|
1407
1801
|
var MCP_SERVER_INSTRUCTIONS = [
|
|
1408
1802
|
MCP_STACK_DONE_INSTRUCTIONS,
|
|
1409
1803
|
MCP_PR_LOOP_INSTRUCTIONS,
|
|
@@ -1423,14 +1817,14 @@ function ok(payload) {
|
|
|
1423
1817
|
};
|
|
1424
1818
|
}
|
|
1425
1819
|
function fail(err) {
|
|
1426
|
-
const message = err instanceof
|
|
1820
|
+
const message = err instanceof CommandError16 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
|
|
1427
1821
|
return ok({
|
|
1428
1822
|
isError: true,
|
|
1429
1823
|
summary: message,
|
|
1430
1824
|
data: {
|
|
1431
1825
|
error: {
|
|
1432
1826
|
message: err instanceof Error ? err.message : String(err),
|
|
1433
|
-
...err instanceof
|
|
1827
|
+
...err instanceof CommandError16 ? {
|
|
1434
1828
|
exit_code: err.exitCode,
|
|
1435
1829
|
...err.code !== void 0 ? { code: err.code } : {},
|
|
1436
1830
|
...err.reason !== void 0 ? { reason: err.reason } : {},
|
|
@@ -1440,7 +1834,19 @@ function fail(err) {
|
|
|
1440
1834
|
}
|
|
1441
1835
|
});
|
|
1442
1836
|
}
|
|
1443
|
-
|
|
1837
|
+
var MCP_WORKSPACE_TOOL_NAMES = ["review_submit", "review_wait", "stack_publish", "stack_publish_status"];
|
|
1838
|
+
var MCP_HOSTED_TOOL_NAMES = MCP_TOOL_NAMES.filter(
|
|
1839
|
+
(name) => !MCP_WORKSPACE_TOOL_NAMES.includes(name)
|
|
1840
|
+
);
|
|
1841
|
+
var HOSTED_WAIT_MAX_S = 45;
|
|
1842
|
+
var HOSTED_REVIEW_WAIT_PR_DESCRIPTION = "Read the Vortex pass on a GitHub PR; review identity is head SHA + pass. timeout_s: 0 reads it once; otherwise it waits up to timeout_s (at most 45) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, start one with review_start_pr). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries. With timeout_s: 0, a pass that does not exist yet returns not_found. On rate_limited, wait retry_after_seconds before trying again. Returns the verdict and each finding.";
|
|
1843
|
+
var HOSTED_REVIEW_DISMISS_DESCRIPTION = `Dismiss Vortex findings you checked and found wrong or not actionable, at the PR's live head: head_sha, review_id, finding_ids (the GitHub review comment ids, offdiff-<n> for a body-only finding; preview: true lists them without writing) or scope "review" to dismiss every finding of that review, and a reason of at least 20 characters. A deferral needs evidence_url set to an open tracking issue in the same repository. Refused, with nothing written, when the head moved, an id is unknown or the user's GitHub account lacks write access; retries are idempotent. Returns which gates cleared (gate.vortex.cleared, else gate.vortex.message) and the stack watch.`;
|
|
1844
|
+
var HOSTED_STACK_STATUS_DESCRIPTION = "Read one owned stack: its layers, checks and agent state, with the same attention, issues, repair and currentCandidate as stack_wait. Use it to see where a stack stands now; to wait for a change, use stack_wait.";
|
|
1845
|
+
var HOSTED_STACK_WAIT_DESCRIPTION = "Wait up to timeout_s (1-45) for a stack to need attention, or read it once with 0. Returns a mergestorm.stack_watch/v1 envelope: attention names the PR, blocker and repair; waiting or in_progress means call again with watch.next.args; watch.done true means stop (landed, closed, archived or gone).";
|
|
1846
|
+
var HOSTED_ISSUE_CREATE_DESCRIPTION = "Open a GitHub issue through Mergestorm with no GitHub credentials: the Surge App creates it and names you in the body. Use it for the issue a pull request must have before it is opened, then put Fixes #<number> in the commit or pull request body. owner and repo name the repository. labels must already exist on the repository; an unknown one is refused with unknown_label and nothing is created. Calling it again with the same title returns the open issue it already made (created: false) when that issue is among the newest 100 open issues of the repository; an older one, or two calls at the same time, can still create a duplicate, so check the returned number before retrying after a lost response. Needs write access to the repository through your linked GitHub account and Surge installed on it.";
|
|
1847
|
+
function createMergestormMcpServer(options = {}) {
|
|
1848
|
+
const { config, hosted = false } = options;
|
|
1849
|
+
const chatId = breezeChatId(options.chatId ?? (hosted ? null : process.env.MERGESTORM_CHAT_ID));
|
|
1444
1850
|
const server2 = new McpServer(
|
|
1445
1851
|
{
|
|
1446
1852
|
name: MCP_SERVER_NAME,
|
|
@@ -1449,9 +1855,15 @@ function createMergestormMcpServer() {
|
|
|
1449
1855
|
websiteUrl: MCP_WEBSITE_URL,
|
|
1450
1856
|
icons: MCP_SERVER_ICONS
|
|
1451
1857
|
},
|
|
1452
|
-
{ instructions: MCP_SERVER_INSTRUCTIONS }
|
|
1858
|
+
{ instructions: hosted ? MCP_HOSTED_SERVER_INSTRUCTIONS : MCP_SERVER_INSTRUCTIONS }
|
|
1453
1859
|
);
|
|
1454
|
-
const
|
|
1860
|
+
const registerTool = server2.registerTool.bind(server2);
|
|
1861
|
+
const watchNote = hosted ? "" : STACK_WATCH_RESULT_NOTE;
|
|
1862
|
+
const waitMaxS = hosted ? HOSTED_WAIT_MAX_S : 300;
|
|
1863
|
+
const addTool = (name, toolConfig, cb) => {
|
|
1864
|
+
if (hosted && MCP_WORKSPACE_TOOL_NAMES.includes(name)) return;
|
|
1865
|
+
registerTool(name, toolConfig, hosted ? async (...args) => hostedToolResult(await cb(...args)) : cb);
|
|
1866
|
+
};
|
|
1455
1867
|
addTool(
|
|
1456
1868
|
"whoami",
|
|
1457
1869
|
{
|
|
@@ -1461,7 +1873,7 @@ function createMergestormMcpServer() {
|
|
|
1461
1873
|
},
|
|
1462
1874
|
async () => {
|
|
1463
1875
|
try {
|
|
1464
|
-
return ok(await whoami(
|
|
1876
|
+
return ok(await whoami(config, MCP_SERVER_VERSION));
|
|
1465
1877
|
} catch (err) {
|
|
1466
1878
|
return fail(err);
|
|
1467
1879
|
}
|
|
@@ -1476,7 +1888,7 @@ function createMergestormMcpServer() {
|
|
|
1476
1888
|
},
|
|
1477
1889
|
async () => {
|
|
1478
1890
|
try {
|
|
1479
|
-
return ok(await credits());
|
|
1891
|
+
return ok(await credits(config));
|
|
1480
1892
|
} catch (err) {
|
|
1481
1893
|
return fail(err);
|
|
1482
1894
|
}
|
|
@@ -1488,13 +1900,13 @@ function createMergestormMcpServer() {
|
|
|
1488
1900
|
title: "List reviews",
|
|
1489
1901
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
1490
1902
|
inputSchema: {
|
|
1491
|
-
limit:
|
|
1903
|
+
limit: z6.number().int().min(1).max(50).optional()
|
|
1492
1904
|
},
|
|
1493
1905
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1494
1906
|
},
|
|
1495
1907
|
async ({ limit }) => {
|
|
1496
1908
|
try {
|
|
1497
|
-
return ok(await reviewList(limit));
|
|
1909
|
+
return ok(await reviewList(limit, config));
|
|
1498
1910
|
} catch (err) {
|
|
1499
1911
|
return fail(err);
|
|
1500
1912
|
}
|
|
@@ -1506,24 +1918,24 @@ function createMergestormMcpServer() {
|
|
|
1506
1918
|
title: "Submit review",
|
|
1507
1919
|
description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Does not wait by default; returns job_id immediately after submission. Explicit wait: true polls one 45s slice by default. Record job_id and loop review_wait until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds and inspect or wait for an in-flight review before resubmitting. specialists may be security, performance, architecture, tests, data, api, frontend. governance and seam cannot be requested.",
|
|
1508
1920
|
inputSchema: {
|
|
1509
|
-
cwd:
|
|
1510
|
-
base:
|
|
1511
|
-
head:
|
|
1512
|
-
diff:
|
|
1513
|
-
router:
|
|
1514
|
-
specialists:
|
|
1515
|
-
context:
|
|
1516
|
-
context_files:
|
|
1517
|
-
thread:
|
|
1518
|
-
idempotency_key:
|
|
1519
|
-
wait:
|
|
1520
|
-
timeout_s:
|
|
1921
|
+
cwd: z6.string().optional(),
|
|
1922
|
+
base: z6.string().optional(),
|
|
1923
|
+
head: z6.string().optional(),
|
|
1924
|
+
diff: z6.string().optional(),
|
|
1925
|
+
router: z6.enum(["off", "standard", "max", "manual"]).optional(),
|
|
1926
|
+
specialists: z6.array(z6.string().min(1)).max(5).optional(),
|
|
1927
|
+
context: z6.string().optional(),
|
|
1928
|
+
context_files: z6.array(z6.string().min(1)).max(10).optional(),
|
|
1929
|
+
thread: z6.string().min(1).max(120).optional(),
|
|
1930
|
+
idempotency_key: z6.string().min(1).max(128).optional(),
|
|
1931
|
+
wait: z6.boolean().optional(),
|
|
1932
|
+
timeout_s: z6.number().positive().max(300).optional()
|
|
1521
1933
|
},
|
|
1522
1934
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
1523
1935
|
},
|
|
1524
1936
|
async (args) => {
|
|
1525
1937
|
try {
|
|
1526
|
-
return ok(await reviewSubmit(args));
|
|
1938
|
+
return ok(await reviewSubmit(args, config));
|
|
1527
1939
|
} catch (err) {
|
|
1528
1940
|
return fail(err);
|
|
1529
1941
|
}
|
|
@@ -1535,14 +1947,14 @@ function createMergestormMcpServer() {
|
|
|
1535
1947
|
title: "Wait for review",
|
|
1536
1948
|
description: "Read one local Mergestorm review job by the job_id that review_submit returned, as a job envelope. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300s). On timeout, returns in_progress; call again with the same job_id until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again.",
|
|
1537
1949
|
inputSchema: {
|
|
1538
|
-
job_id:
|
|
1539
|
-
timeout_s:
|
|
1950
|
+
job_id: z6.string().min(1),
|
|
1951
|
+
timeout_s: z6.number().min(0).max(300).optional()
|
|
1540
1952
|
},
|
|
1541
1953
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1542
1954
|
},
|
|
1543
1955
|
async ({ job_id, timeout_s }) => {
|
|
1544
1956
|
try {
|
|
1545
|
-
return ok(await reviewWait(job_id, timeout_s));
|
|
1957
|
+
return ok(await reviewWait(job_id, timeout_s, config));
|
|
1546
1958
|
} catch (err) {
|
|
1547
1959
|
return fail(err);
|
|
1548
1960
|
}
|
|
@@ -1552,29 +1964,29 @@ function createMergestormMcpServer() {
|
|
|
1552
1964
|
"review_wait_pr",
|
|
1553
1965
|
{
|
|
1554
1966
|
title: "Wait for GitHub PR Vortex review",
|
|
1555
|
-
description: "Read the Vortex pass on a GitHub PR from the DB-only endpoint; review identity is head SHA + pass. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, Vortex is not reviewing that head: comment @mergestorm-vortex review on the PR, run mg review, or push a new commit). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries; never replace it with the pass of an in-progress envelope, and omit after_pass for a head you hold no envelope for. With timeout_s: 0, a pass that does not exist yet returns not_found, so after a push use a positive timeout. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
|
|
1967
|
+
description: hosted ? HOSTED_REVIEW_WAIT_PR_DESCRIPTION : "Read the Vortex pass on a GitHub PR from the DB-only endpoint; review identity is head SHA + pass. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, Vortex is not reviewing that head: comment @mergestorm-vortex review on the PR, run mg review, or push a new commit). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries; never replace it with the pass of an in-progress envelope, and omit after_pass for a head you hold no envelope for. With timeout_s: 0, a pass that does not exist yet returns not_found, so after a push use a positive timeout. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
|
|
1556
1968
|
inputSchema: {
|
|
1557
|
-
owner:
|
|
1558
|
-
repo:
|
|
1559
|
-
pr_number:
|
|
1560
|
-
after_sha:
|
|
1561
|
-
pass:
|
|
1562
|
-
after_pass:
|
|
1563
|
-
timeout_s:
|
|
1969
|
+
owner: z6.string().min(1),
|
|
1970
|
+
repo: z6.string().min(1),
|
|
1971
|
+
pr_number: z6.number().int().min(1),
|
|
1972
|
+
after_sha: z6.string().optional(),
|
|
1973
|
+
pass: z6.number().int().min(1).optional(),
|
|
1974
|
+
after_pass: z6.number().int().min(1).optional(),
|
|
1975
|
+
timeout_s: z6.number().min(0).max(waitMaxS).optional()
|
|
1564
1976
|
},
|
|
1565
1977
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1566
1978
|
},
|
|
1567
|
-
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
1979
|
+
async ({ owner, repo: repo2, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
1568
1980
|
try {
|
|
1569
1981
|
return ok(
|
|
1570
1982
|
await reviewWaitPr(
|
|
1571
1983
|
owner,
|
|
1572
|
-
|
|
1984
|
+
repo2,
|
|
1573
1985
|
pr_number,
|
|
1574
1986
|
after_sha,
|
|
1575
1987
|
timeout_s,
|
|
1576
|
-
|
|
1577
|
-
{ pass, afterPass: after_pass }
|
|
1988
|
+
config,
|
|
1989
|
+
{ pass, afterPass: after_pass, ...chatId ? { chatId } : {} }
|
|
1578
1990
|
)
|
|
1579
1991
|
);
|
|
1580
1992
|
} catch (err) {
|
|
@@ -1586,24 +1998,100 @@ function createMergestormMcpServer() {
|
|
|
1586
1998
|
"review_dismiss",
|
|
1587
1999
|
{
|
|
1588
2000
|
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
|
|
2001
|
+
description: hosted ? HOSTED_REVIEW_DISMISS_DESCRIPTION : `Record an audited dismissal of Vortex findings you verified are wrong or not actionable on a GitHub PR. Identity is exact: head_sha must be the PR's live head, review_id a Vortex review (Core or seam) made at that head, and finding_ids the GitHub review comment ids of that review (offdiff-<n> for a body-only finding), or scope "review" with no finding_ids to dismiss every finding of that review. reason must say why (at least 20 characters); evidence_url is optional. Call with preview: true first to list the review's finding ids and the seam gate without writing. The caller's linked GitHub account needs write access to the repository. A moved head, another review, an unknown id or a missing permission is refused and nothing is written. Retries are idempotent. The seam gate clears only when every finding of that integration review is dismissed; the Vortex gate at the head clears only once every Vortex Blocker (error finding) at that head is dismissed and no Vortex review of it is running (gate.vortex.cleared, else gate.vortex.message); CI and Auto land policy are unchanged. Vortex does not raise a dismissed finding again on the same diff. Never dismiss a finding you did not check. ` + MCP_FINDING_DEFERRAL_RULE + STACK_WATCH_RESULT_NOTE,
|
|
1590
2002
|
inputSchema: {
|
|
1591
|
-
owner:
|
|
1592
|
-
repo:
|
|
1593
|
-
pr_number:
|
|
1594
|
-
head_sha:
|
|
1595
|
-
review_id:
|
|
1596
|
-
finding_ids:
|
|
1597
|
-
scope:
|
|
1598
|
-
reason:
|
|
1599
|
-
evidence_url:
|
|
1600
|
-
preview:
|
|
2003
|
+
owner: z6.string().min(1),
|
|
2004
|
+
repo: z6.string().min(1),
|
|
2005
|
+
pr_number: z6.number().int().min(1),
|
|
2006
|
+
head_sha: z6.string().regex(/^[0-9a-fA-F]{40}$/),
|
|
2007
|
+
review_id: z6.number().int().min(1),
|
|
2008
|
+
finding_ids: z6.array(z6.string().min(1)).max(50).optional(),
|
|
2009
|
+
scope: z6.enum(["findings", "review"]).optional(),
|
|
2010
|
+
reason: z6.string().optional(),
|
|
2011
|
+
evidence_url: z6.string().url().optional(),
|
|
2012
|
+
preview: z6.boolean().optional()
|
|
1601
2013
|
},
|
|
1602
2014
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
1603
2015
|
},
|
|
1604
2016
|
async (args) => {
|
|
1605
2017
|
try {
|
|
1606
|
-
return ok(await reviewDismiss(args));
|
|
2018
|
+
return ok(await reviewDismiss(args, config));
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
return fail(err);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
const prInput = {
|
|
2025
|
+
owner: z6.string().min(1),
|
|
2026
|
+
repo: z6.string().min(1),
|
|
2027
|
+
pr_number: z6.number().int().min(1)
|
|
2028
|
+
};
|
|
2029
|
+
addTool(
|
|
2030
|
+
"review_start_pr",
|
|
2031
|
+
{
|
|
2032
|
+
title: "Start Vortex review",
|
|
2033
|
+
description: "Start a Vortex review of an open GitHub pull request on the user's account, at its current head. head_sha, when given, must still be the head; otherwise nothing starts and the answer names the live head. Uses one standard credit. Findings post on the pull request; read them with review_wait_pr. A review already running at that head is reported, not started twice. When the result names a background task, its result also arrives through that task when the work finishes.",
|
|
2034
|
+
inputSchema: { ...prInput, head_sha: z6.string().regex(/^[0-9a-fA-F]{7,40}$/).optional() },
|
|
2035
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
2036
|
+
},
|
|
2037
|
+
async (args) => {
|
|
2038
|
+
try {
|
|
2039
|
+
return ok(await reviewStartPr(args, config, chatId));
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
return fail(err);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
);
|
|
2045
|
+
addTool(
|
|
2046
|
+
"review_stop_pr",
|
|
2047
|
+
{
|
|
2048
|
+
title: "Stop Vortex review",
|
|
2049
|
+
description: "Cancel the Vortex review running on a pull request. Starts nothing; says so when no review is running.",
|
|
2050
|
+
inputSchema: prInput,
|
|
2051
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
2052
|
+
},
|
|
2053
|
+
async (args) => {
|
|
2054
|
+
try {
|
|
2055
|
+
return ok(await reviewStopPr(args, config));
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
return fail(err);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
);
|
|
2061
|
+
addTool(
|
|
2062
|
+
"cyclone_start",
|
|
2063
|
+
{
|
|
2064
|
+
title: "Start Cyclone job",
|
|
2065
|
+
description: "Have Cyclone work on the user's repository and push the result. task fix_findings fixes the open Vortex findings on pr_number, fix_ci fixes its failing CI check (failing_check names one; default the first failing check), resolve_conflicts merges its base in and resolves the conflicts; each pushes a commit to the pull request's branch. When the user has not named the pull request, find it before you call, and ask them for owner/repo#number if you cannot. task build builds a first working version of a new project from brief into an empty repository the user created and installed Cyclone on, then opens a pull request. Uses Cyclone credits. When the pull request has nothing to do (CI green, no conflicts, no open findings) nothing starts and the answer says why: tell the user that. Returns dispatch_id. Wait for the job with cyclone_job until it is pushed, completed or failed, then report what it pushed. When the result names a background task, its result also arrives through that task when the work finishes.",
|
|
2066
|
+
inputSchema: {
|
|
2067
|
+
task: z6.enum(CYCLONE_TASKS),
|
|
2068
|
+
owner: z6.string().min(1),
|
|
2069
|
+
repo: z6.string().min(1),
|
|
2070
|
+
pr_number: z6.number().int().min(1).optional(),
|
|
2071
|
+
failing_check: z6.string().min(1).max(200).optional(),
|
|
2072
|
+
brief: z6.string().min(1).max(4e3).optional()
|
|
2073
|
+
},
|
|
2074
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
2075
|
+
},
|
|
2076
|
+
async (args) => {
|
|
2077
|
+
try {
|
|
2078
|
+
return ok(await cycloneStart(args, config, chatId));
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
return fail(err);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
);
|
|
2084
|
+
addTool(
|
|
2085
|
+
"cyclone_job",
|
|
2086
|
+
{
|
|
2087
|
+
title: "Wait for Cyclone job",
|
|
2088
|
+
description: `Wait for a Cyclone job started with cyclone_start, by its dispatch_id, and read it: queued, running, pushed, completed or failed, with the outcome, detail, pushed commit and changed files. timeout_s (default ${CYCLONE_JOB_WAIT_DEFAULT_S}, max ${CYCLONE_JOB_WAIT_MAX_S}) waits until the job is pushed, completed or failed, or the time is up; 0 reads it once. While it is queued or running, call again with the same dispatch_id.`,
|
|
2089
|
+
inputSchema: { dispatch_id: z6.string().uuid(), timeout_s: z6.number().int().min(0).max(CYCLONE_JOB_WAIT_MAX_S).optional() },
|
|
2090
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
2091
|
+
},
|
|
2092
|
+
async ({ dispatch_id, timeout_s }, extra) => {
|
|
2093
|
+
try {
|
|
2094
|
+
return ok(await cycloneJob(dispatch_id, config, timeout_s ?? CYCLONE_JOB_WAIT_DEFAULT_S, { signal: extra.signal }));
|
|
1607
2095
|
} catch (err) {
|
|
1608
2096
|
return fail(err);
|
|
1609
2097
|
}
|
|
@@ -1613,13 +2101,13 @@ function createMergestormMcpServer() {
|
|
|
1613
2101
|
"issue_create",
|
|
1614
2102
|
{
|
|
1615
2103
|
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,
|
|
2104
|
+
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.",
|
|
2105
|
+
inputSchema: hosted ? hostedIssueCreateSchema : issueCreateSchema,
|
|
1618
2106
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
1619
2107
|
},
|
|
1620
2108
|
async (args) => {
|
|
1621
2109
|
try {
|
|
1622
|
-
return ok(await issueCreate(args));
|
|
2110
|
+
return ok(await issueCreate(args, config, hosted ? { parseGithubOriginRepo: () => null } : {}));
|
|
1623
2111
|
} catch (err) {
|
|
1624
2112
|
return fail(err);
|
|
1625
2113
|
}
|
|
@@ -1629,13 +2117,13 @@ function createMergestormMcpServer() {
|
|
|
1629
2117
|
"stack_adopt",
|
|
1630
2118
|
{
|
|
1631
2119
|
title: "Adopt PR stack",
|
|
1632
|
-
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted overrides are not seeded. Never changes account settings. Adoption needs the Mergestorm Surge GitHub App on the repository. Stacks, the merge queue and Auto land run through that App; Cyclone is the auto-patch App. Without it the API returns cyclone_not_connected or cyclone_not_installed; the message, and error.reason starting with surge_ when Surge is the one to install, say which App is missing \u2014 do not retry, tell the human what the message says. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." +
|
|
2120
|
+
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted overrides are not seeded. Never changes account settings. Adoption needs the Mergestorm Surge GitHub App on the repository. Stacks, the merge queue and Auto land run through that App; Cyclone is the auto-patch App. Without it the API returns cyclone_not_connected or cyclone_not_installed; the message, and error.reason starting with surge_ when Surge is the one to install, say which App is missing \u2014 do not retry, tell the human what the message says. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + watchNote,
|
|
1633
2121
|
inputSchema: stackAdoptSchema,
|
|
1634
2122
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
1635
2123
|
},
|
|
1636
2124
|
async (args) => {
|
|
1637
2125
|
try {
|
|
1638
|
-
return ok(await stackAdopt(args));
|
|
2126
|
+
return ok(await stackAdopt(args, config));
|
|
1639
2127
|
} catch (err) {
|
|
1640
2128
|
return fail(err);
|
|
1641
2129
|
}
|
|
@@ -1650,7 +2138,7 @@ function createMergestormMcpServer() {
|
|
|
1650
2138
|
},
|
|
1651
2139
|
async () => {
|
|
1652
2140
|
try {
|
|
1653
|
-
return ok(await stackList());
|
|
2141
|
+
return ok(await stackList(config));
|
|
1654
2142
|
} catch (err) {
|
|
1655
2143
|
return fail(err);
|
|
1656
2144
|
}
|
|
@@ -1660,13 +2148,13 @@ function createMergestormMcpServer() {
|
|
|
1660
2148
|
"stack_publish",
|
|
1661
2149
|
{
|
|
1662
2150
|
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." +
|
|
2151
|
+
description: "Publish a local branch without any GitHub credentials. Reads the branch's commits from the local repository at cwd (never runs gh, git push or git fetch) and sends them to Mergestorm, which writes the exact commits and the branch on GitHub as the Surge App, opens a ready pull request when the branch has none, and registers the stack with Auto land on for a new stack, and Auto patch and Auto review left to the stack, repository and account settings, so Cyclone patches Vortex findings when Auto patch is on. Use it for the first publication and for every later commit to that branch. Defaults: owner and repo from origin, the checked-out branch, the repository's trunk as base, title and body from the tip commit. Two ways to publish: with no base (or trunk as base) the branch opens as a single PR onto trunk; to stack it on another branch, pass that branch as base and extend: true. A base other than trunk without extend is refused with implicit_stack. The result says which one it made. Mergestorm verifies each commit's Git tree before it moves the branch and returns the verified head and tree. It refuses with head_changed when the branch on GitHub holds work your local branch does not, base_changed when the base commit left the base branch, protected_branch for trunk and Mergestorm-owned branches, and payload_too_large beyond 20 commits, 500 changed paths, 2 MiB a file or 8 MiB in total; submodules and merge commits are not carried. replace_head replaces that exact GitHub head with the local branch and discards its commits. Safe to call again after a lost response or a failure: it resumes and never duplicates the branch, commits, pull request or stack." + watchNote,
|
|
1664
2152
|
inputSchema: stackPublishSchema,
|
|
1665
2153
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
1666
2154
|
},
|
|
1667
2155
|
async (args) => {
|
|
1668
2156
|
try {
|
|
1669
|
-
return ok(await stackPublish(args));
|
|
2157
|
+
return ok(await stackPublish(args, config));
|
|
1670
2158
|
} catch (err) {
|
|
1671
2159
|
return fail(err);
|
|
1672
2160
|
}
|
|
@@ -1682,7 +2170,7 @@ function createMergestormMcpServer() {
|
|
|
1682
2170
|
},
|
|
1683
2171
|
async (args) => {
|
|
1684
2172
|
try {
|
|
1685
|
-
return ok(await stackPublishStatus(args));
|
|
2173
|
+
return ok(await stackPublishStatus(args, config));
|
|
1686
2174
|
} catch (err) {
|
|
1687
2175
|
return fail(err);
|
|
1688
2176
|
}
|
|
@@ -1692,14 +2180,14 @@ function createMergestormMcpServer() {
|
|
|
1692
2180
|
"stack_set",
|
|
1693
2181
|
{
|
|
1694
2182
|
title: "Set stack policy",
|
|
1695
|
-
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings." +
|
|
2183
|
+
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings." + watchNote,
|
|
1696
2184
|
inputSchema: {
|
|
1697
|
-
stack_id:
|
|
1698
|
-
auto_land:
|
|
1699
|
-
auto_review:
|
|
1700
|
-
auto_patch:
|
|
1701
|
-
auto_resolve_conflicts:
|
|
1702
|
-
auto_fix_ci:
|
|
2185
|
+
stack_id: z6.string().min(1),
|
|
2186
|
+
auto_land: z6.boolean().optional(),
|
|
2187
|
+
auto_review: z6.boolean().nullable().optional(),
|
|
2188
|
+
auto_patch: z6.boolean().nullable().optional(),
|
|
2189
|
+
auto_resolve_conflicts: z6.boolean().nullable().optional(),
|
|
2190
|
+
auto_fix_ci: z6.boolean().nullable().optional()
|
|
1703
2191
|
},
|
|
1704
2192
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
1705
2193
|
},
|
|
@@ -1712,26 +2200,58 @@ function createMergestormMcpServer() {
|
|
|
1712
2200
|
...auto_patch !== void 0 ? { auto_patch } : {},
|
|
1713
2201
|
...auto_resolve_conflicts !== void 0 ? { auto_resolve_conflicts } : {},
|
|
1714
2202
|
...auto_fix_ci !== void 0 ? { auto_fix_ci } : {}
|
|
1715
|
-
})
|
|
2203
|
+
}, config)
|
|
1716
2204
|
);
|
|
1717
2205
|
} catch (err) {
|
|
1718
2206
|
return fail(err);
|
|
1719
2207
|
}
|
|
1720
2208
|
}
|
|
1721
2209
|
);
|
|
2210
|
+
addTool(
|
|
2211
|
+
"stack_restack",
|
|
2212
|
+
{
|
|
2213
|
+
title: "Restack stack",
|
|
2214
|
+
description: "Rebase one owned stack's layer branches onto their parents and push them, after the trunk or a lower layer moved. Does not merge or enqueue anything." + watchNote,
|
|
2215
|
+
inputSchema: { stack_id: z6.string().min(1) },
|
|
2216
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
2217
|
+
},
|
|
2218
|
+
async ({ stack_id }) => {
|
|
2219
|
+
try {
|
|
2220
|
+
return ok(await stackRestack(stack_id, config));
|
|
2221
|
+
} catch (err) {
|
|
2222
|
+
return fail(err);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
);
|
|
2226
|
+
addTool(
|
|
2227
|
+
"stack_enqueue",
|
|
2228
|
+
{
|
|
2229
|
+
title: "Send stack to merge queue",
|
|
2230
|
+
description: "Send one owned stack to the Mergestorm merge queue, which tests it and lands it when its checks pass. To restart a failed or stuck land, call stack_restack first and then this. When the result names a background task, its result also arrives through that task when the work finishes." + watchNote,
|
|
2231
|
+
inputSchema: { stack_id: z6.string().min(1) },
|
|
2232
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
2233
|
+
},
|
|
2234
|
+
async ({ stack_id }) => {
|
|
2235
|
+
try {
|
|
2236
|
+
return ok(await stackEnqueue(stack_id, config, chatId));
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
return fail(err);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
);
|
|
1722
2242
|
addTool(
|
|
1723
2243
|
"stack_status",
|
|
1724
2244
|
{
|
|
1725
2245
|
title: "Get stack status",
|
|
1726
|
-
description: `Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue. trunkBranch is mg-stack-<n> for an adopted 2+ PR stack and matches the bottom open PR's GitHub base; an mg-park-* parentBranch is a freeze. Neither is drift to fix. repair names the concrete fix for attention; landGatePending is a wait, not attention. held names a blocker no agent run clears while Vortex or Cyclone is still busy on that PR (actAfter agents_idle, waitingOn); repair then describes the fix to plan, not to apply yet. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
|
|
2246
|
+
description: hosted ? HOSTED_STACK_STATUS_DESCRIPTION : `Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue. trunkBranch is mg-stack-<n> for an adopted 2+ PR stack and matches the bottom open PR's GitHub base; an mg-park-* parentBranch is a freeze. Neither is drift to fix. repair names the concrete fix for attention; landGatePending is a wait, not attention. held names a blocker no agent run clears while Vortex or Cyclone is still busy on that PR (actAfter agents_idle, waitingOn); repair then describes the fix to plan, not to apply yet. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
|
|
1727
2247
|
inputSchema: {
|
|
1728
|
-
stack_id:
|
|
2248
|
+
stack_id: z6.string().min(1)
|
|
1729
2249
|
},
|
|
1730
2250
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1731
2251
|
},
|
|
1732
2252
|
async ({ stack_id }) => {
|
|
1733
2253
|
try {
|
|
1734
|
-
return ok(await stackStatus(stack_id));
|
|
2254
|
+
return ok(await stackStatus(stack_id, config));
|
|
1735
2255
|
} catch (err) {
|
|
1736
2256
|
return fail(err);
|
|
1737
2257
|
}
|
|
@@ -1741,13 +2261,13 @@ function createMergestormMcpServer() {
|
|
|
1741
2261
|
"stack_wait",
|
|
1742
2262
|
{
|
|
1743
2263
|
title: "Wait for stack attention",
|
|
1744
|
-
description: `Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure; waiting and in_progress end this slice, and the next call is stack_wait again with the same cursor. A timeout of failed means no assessment was produced. A stack that no longer exists returns status failed with watch.done true (reason not_found) and is not an error. While Cyclone or Vortex is still working on the blocked PR it returns in_progress, not attention, with busy[] naming the PR, blocker, and agent; agents carries that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. When another layer has a blocker no agent works on, attention names that blocker instead and busy[] is empty. A blocker no agent run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that in_progress, in blocker, prNumber, headSha, and issues[], with actAfter: "agents_idle" and waitingOn listing the busy agents: plan the fix, and act when a snapshot after the agents finish returns attention, re-reading the blocker then. Blockers the agents can change keep blocker and actAfter null. Attention never calls for changing a PR's GitHub base. When present, repair names the concrete fix for attention (restack_conflict and merge_conflict carry liveParent, never mg-park-*). For the three stuck-seam blockers, repair kind seam_review_stuck: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. A pending land gate (landGatePending) returns in_progress, not attention. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
|
|
2264
|
+
description: hosted ? HOSTED_STACK_WAIT_DESCRIPTION : `Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure; waiting and in_progress end this slice, and the next call is stack_wait again with the same cursor. A timeout of failed means no assessment was produced. A stack that no longer exists returns status failed with watch.done true (reason not_found) and is not an error. While Cyclone or Vortex is still working on the blocked PR it returns in_progress, not attention, with busy[] naming the PR, blocker, and agent; agents carries that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. When another layer has a blocker no agent works on, attention names that blocker instead and busy[] is empty. A blocker no agent run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that in_progress, in blocker, prNumber, headSha, and issues[], with actAfter: "agents_idle" and waitingOn listing the busy agents: plan the fix, and act when a snapshot after the agents finish returns attention, re-reading the blocker then. Blockers the agents can change keep blocker and actAfter null. Attention never calls for changing a PR's GitHub base. When present, repair names the concrete fix for attention (restack_conflict and merge_conflict carry liveParent, never mg-park-*). For the three stuck-seam blockers, repair kind seam_review_stuck: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. A pending land gate (landGatePending) returns in_progress, not attention. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. auto_land_switched_off (returned only when the PR has no live blocker of its own): Auto land was switched off without a bounce (queue entry cancelled, or nothing left to land); do not patch, push or turn it back on yourself, tell the human. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_ATTENTION_NOTE + STACK_WATCH_RESULT_NOTE,
|
|
1745
2265
|
inputSchema: stackWaitSchema,
|
|
1746
2266
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1747
2267
|
},
|
|
1748
2268
|
async (args, extra) => {
|
|
1749
2269
|
try {
|
|
1750
|
-
const payload = await stackWait(args,
|
|
2270
|
+
const payload = await stackWait(args, config, { signal: extra.signal, ...chatId ? { chatId } : {}, hosted });
|
|
1751
2271
|
return ok(payload);
|
|
1752
2272
|
} catch (err) {
|
|
1753
2273
|
return fail(err);
|
|
@@ -1760,13 +2280,13 @@ function createMergestormMcpServer() {
|
|
|
1760
2280
|
title: "Get merge queue status",
|
|
1761
2281
|
description: "List live merge queue entries (position >= 1) plus the newest bounced entries (position 0) with bounceDetail for the current user, optionally filtered by stack.",
|
|
1762
2282
|
inputSchema: {
|
|
1763
|
-
stack_id:
|
|
2283
|
+
stack_id: z6.string().min(1).optional()
|
|
1764
2284
|
},
|
|
1765
2285
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1766
2286
|
},
|
|
1767
2287
|
async ({ stack_id }) => {
|
|
1768
2288
|
try {
|
|
1769
|
-
return ok(await queueStatus(stack_id));
|
|
2289
|
+
return ok(await queueStatus(stack_id, config));
|
|
1770
2290
|
} catch (err) {
|
|
1771
2291
|
return fail(err);
|
|
1772
2292
|
}
|
|
@@ -1776,13 +2296,13 @@ function createMergestormMcpServer() {
|
|
|
1776
2296
|
"repo_settings_get",
|
|
1777
2297
|
{
|
|
1778
2298
|
title: "Get repository settings",
|
|
1779
|
-
description: "Read the repository-level Auto patch, Auto review
|
|
1780
|
-
inputSchema: { repo:
|
|
2299
|
+
description: "Read the repository-level Auto patch, Auto review, Auto land, merge queue (merge_queue_batch_enabled, merge_queue_batch_size, merge_queue_lands_alone_paths) and merge queue overdrive values for the repositories connected to this account. auto_patch_enabled, auto_review_enabled, auto_land_default, merge_queue_batch_enabled and merge_queue_overdrive are true, false, or null; merge_queue_batch_size is a whole number or null; merge_queue_lands_alone_paths is a list of path patterns or null. null means not set, so the account setting applies. Auto patch and Auto review precedence is the stack override (stack_set), then the repository value, then the account setting (settings_get). The Auto land repository value only seeds new stacks at adoption; it does not override an existing stack. merge_queue_overdrive is the repository value over the account default; while it resolves on, that repository batches up to 10 queued pull requests. Pass repo as owner/repo for one repository, or omit it for all.",
|
|
2300
|
+
inputSchema: { repo: z6.string().min(3).optional() },
|
|
1781
2301
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1782
2302
|
},
|
|
1783
2303
|
async (args) => {
|
|
1784
2304
|
try {
|
|
1785
|
-
return ok(await repoSettingsGet(args ?? {}));
|
|
2305
|
+
return ok(await repoSettingsGet(args ?? {}, config));
|
|
1786
2306
|
} catch (err) {
|
|
1787
2307
|
return fail(err);
|
|
1788
2308
|
}
|
|
@@ -1792,18 +2312,38 @@ function createMergestormMcpServer() {
|
|
|
1792
2312
|
"repo_settings_set",
|
|
1793
2313
|
{
|
|
1794
2314
|
title: "Update repository settings",
|
|
1795
|
-
description: "Set Auto patch (Cyclone), Auto review (Vortex)
|
|
2315
|
+
description: "Set Auto patch (Cyclone), Auto review (Vortex), the Auto land default, or the merge queue settings (merge_queue_batch, merge_queue_batch_size, merge_queue_lands_alone_paths, merge_queue_overdrive) for every stack and pull request in one repository connected to this account, and return the stored result. A value sets the repository value; null clears it so the account setting applies again. A stack override (stack_set) still wins for that stack. auto_patch and auto_review apply to open stacks without an override from their next decision; nothing is cancelled or re-queued. auto_land only seeds new stacks. merge_queue_lands_alone_paths is a list of folder prefixes or globs; a queued pull request that changes a matching file is never batched with others, and an empty list keeps only the built-in rule that a stack with more than one open pull request lands alone. merge_queue_overdrive true makes that repository batch up to 10 queued pull requests, even with batching off; false turns overdrive off for that repository whatever the account default is, and null clears the repository value so the account default (settings_get merge_queue_overdrive) applies again. Only the account that connected the repository can change it. Never changes account settings.",
|
|
1796
2316
|
inputSchema: {
|
|
1797
|
-
repo:
|
|
1798
|
-
auto_patch:
|
|
1799
|
-
auto_review:
|
|
1800
|
-
auto_land:
|
|
2317
|
+
repo: z6.string().min(3),
|
|
2318
|
+
auto_patch: z6.boolean().nullable().optional(),
|
|
2319
|
+
auto_review: z6.boolean().nullable().optional(),
|
|
2320
|
+
auto_land: z6.boolean().nullable().optional(),
|
|
2321
|
+
merge_queue_batch: z6.boolean().nullable().optional(),
|
|
2322
|
+
merge_queue_batch_size: z6.number().int().min(BATCH_SIZE_ROW.min).max(BATCH_SIZE_ROW.max).nullable().optional(),
|
|
2323
|
+
merge_queue_lands_alone_paths: z6.array(z6.string()).nullable().optional(),
|
|
2324
|
+
merge_queue_overdrive: z6.boolean().nullable().optional()
|
|
1801
2325
|
},
|
|
1802
2326
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
1803
2327
|
},
|
|
1804
2328
|
async (args) => {
|
|
1805
2329
|
try {
|
|
1806
|
-
return ok(await repoSettingsSet(args ?? {}));
|
|
2330
|
+
return ok(await repoSettingsSet(args ?? {}, config));
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
return fail(err);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
);
|
|
2336
|
+
addTool(
|
|
2337
|
+
"surge_setup",
|
|
2338
|
+
{
|
|
2339
|
+
title: "Set up Surge CI",
|
|
2340
|
+
description: "Set up Surge CI on one repository of the user's account: if Surge does not serve it yet, serve it (it joins the repositories Surge serves and gets the account's Surge settings), then open a pull request that points its workflow jobs at Surge runners and adds the merge queue batch trigger. Nothing in the repository's files changes until that pull request is merged. The answer says what happened, with the pull request link.",
|
|
2341
|
+
inputSchema: { owner: z6.string().min(1), repo: z6.string().min(1) },
|
|
2342
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
2343
|
+
},
|
|
2344
|
+
async (args) => {
|
|
2345
|
+
try {
|
|
2346
|
+
return ok(await surgeSetup(args, config));
|
|
1807
2347
|
} catch (err) {
|
|
1808
2348
|
return fail(err);
|
|
1809
2349
|
}
|
|
@@ -1813,12 +2353,12 @@ function createMergestormMcpServer() {
|
|
|
1813
2353
|
"settings_get",
|
|
1814
2354
|
{
|
|
1815
2355
|
title: "Get settings",
|
|
1816
|
-
description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), merge_queue_batch_enabled and merge_queue_batch_size (whether the merge queue tests several pull requests on one CI run, and how many), cyclone_connected (the Cyclone auto-patch App), and stacks_ready (whether stacks, the merge queue and Auto land can run for this account, which needs Mergestorm Surge).",
|
|
2356
|
+
description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), merge_queue_batch_enabled and merge_queue_batch_size (whether the merge queue tests several pull requests on one CI run, and how many), merge_queue_overdrive (the account default for overdrive: the merge queue batches up to 10 queued pull requests per repository; repo_settings_set overrides it per repository), cyclone_connected (the Cyclone auto-patch App), and stacks_ready (whether stacks, the merge queue and Auto land can run for this account, which needs Mergestorm Surge).",
|
|
1817
2357
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1818
2358
|
},
|
|
1819
2359
|
async () => {
|
|
1820
2360
|
try {
|
|
1821
|
-
return ok(await settingsGet());
|
|
2361
|
+
return ok(await settingsGet(config));
|
|
1822
2362
|
} catch (err) {
|
|
1823
2363
|
return fail(err);
|
|
1824
2364
|
}
|
|
@@ -1828,28 +2368,93 @@ function createMergestormMcpServer() {
|
|
|
1828
2368
|
"settings_set",
|
|
1829
2369
|
{
|
|
1830
2370
|
title: "Update settings",
|
|
1831
|
-
description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. auto_land_settle_seconds is a whole number of seconds from 15 through 300; it applies to settle clocks that start after the write. merge_queue_batch_enabled turns merge queue batching on or off for the account,
|
|
2371
|
+
description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. auto_land_settle_seconds is a whole number of seconds from 15 through 300; it applies to settle clocks that start after the write. merge_queue_batch_enabled turns merge queue batching on or off for the account, merge_queue_batch_size is a whole number from 2 through 10, merge_queue_lands_alone_paths is the list of folder prefixes or globs whose changed files keep a pull request out of a batch, and merge_queue_overdrive is the account default for overdrive: while it resolves on, a repository batches up to 10 queued pull requests, even with batching off, without changing merge_queue_batch_size, and it stays on until turned off (repo_settings_set overrides each of these per repository). cyclone_connected and github_connected are read-only and cannot be set.",
|
|
1832
2372
|
inputSchema: {
|
|
1833
2373
|
...Object.fromEntries(
|
|
1834
2374
|
BEARER_SETTINGS2.map((row) => [
|
|
1835
2375
|
row.key,
|
|
1836
|
-
("kind" in row ? row.kind === "logins" ?
|
|
2376
|
+
("kind" in row ? row.kind === "logins" || row.kind === "paths" ? z6.array(z6.string()) : row.kind === "seconds" || row.kind === "count" ? z6.number().int().min(row.min).max(row.max) : z6.enum(row.values) : z6.boolean()).optional()
|
|
1837
2377
|
])
|
|
1838
2378
|
),
|
|
1839
2379
|
// Preserve these args so the handler rejects them even alongside a writable key.
|
|
1840
|
-
cyclone_connected:
|
|
1841
|
-
github_connected:
|
|
2380
|
+
cyclone_connected: z6.unknown().optional(),
|
|
2381
|
+
github_connected: z6.unknown().optional()
|
|
1842
2382
|
},
|
|
1843
2383
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
1844
2384
|
},
|
|
1845
2385
|
async (args) => {
|
|
1846
2386
|
try {
|
|
1847
|
-
return ok(await settingsSet(args ?? {}));
|
|
2387
|
+
return ok(await settingsSet(args ?? {}, config));
|
|
1848
2388
|
} catch (err) {
|
|
1849
2389
|
return fail(err);
|
|
1850
2390
|
}
|
|
1851
2391
|
}
|
|
1852
2392
|
);
|
|
2393
|
+
const memory = createMemoryToolHandlers(config, chatId);
|
|
2394
|
+
const memoryTools = [
|
|
2395
|
+
{
|
|
2396
|
+
name: "list_memories",
|
|
2397
|
+
title: "List memories",
|
|
2398
|
+
description: "List what Mergestorm remembers about the user, newest first, with each memory's id. Filter by scope (user or repo), repo (owner/repo) or kind. Works with memory off, so the user can still see and delete what is stored.",
|
|
2399
|
+
inputSchema: listMemoriesSchema,
|
|
2400
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2401
|
+
run: memory.list
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
name: "search_memory",
|
|
2405
|
+
title: "Search memory",
|
|
2406
|
+
description: "Search what Mergestorm remembers about the user by meaning, best match first. Give repo as owner/repo to search that repository's memories and standing rules too. Returns nothing when memory is off.",
|
|
2407
|
+
inputSchema: searchMemorySchema,
|
|
2408
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2409
|
+
run: memory.search
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
name: "remember",
|
|
2413
|
+
title: "Remember",
|
|
2414
|
+
description: "Remember one thing about the user for later conversations, on every device: a standing rule for a repository, their code or writing style, how they like to work, a fact about them or their team, or what they built or decided. One thing per call, as a plain sentence under 500 characters that still makes sense months from now. kind: rule, style, preference, fact or project. Give repo as owner/repo only for a memory about one repository. When memory is on_request, save only what the user asks you to remember. Refused with memory_off when memory is off. Never keys, tokens, passwords or other secrets.",
|
|
2415
|
+
inputSchema: rememberSchema,
|
|
2416
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2417
|
+
run: memory.remember
|
|
2418
|
+
},
|
|
2419
|
+
{
|
|
2420
|
+
name: "update_memory",
|
|
2421
|
+
title: "Update memory",
|
|
2422
|
+
description: "Correct a memory that is wrong or out of date: its id from list_memories or search_memory, the new text, and optionally a new kind.",
|
|
2423
|
+
inputSchema: updateMemorySchema,
|
|
2424
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2425
|
+
run: memory.update
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
name: "forget_memory",
|
|
2429
|
+
title: "Forget memory",
|
|
2430
|
+
description: "Forget a memory that no longer holds, or anything the user asks you to forget, by its id from list_memories or search_memory.",
|
|
2431
|
+
inputSchema: forgetMemorySchema,
|
|
2432
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2433
|
+
run: memory.forget
|
|
2434
|
+
},
|
|
2435
|
+
{
|
|
2436
|
+
name: "get_memory_mode",
|
|
2437
|
+
title: "Read memory mode",
|
|
2438
|
+
description: "Read the account's memory mode: off (nothing is saved or recalled), on_request (save only what the user asks to remember) or all (save what will matter later).",
|
|
2439
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2440
|
+
run: memory.getMode
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
name: "set_memory_mode",
|
|
2444
|
+
title: "Set memory mode",
|
|
2445
|
+
description: "Set the account's memory mode when the user asks: off, on_request or all.",
|
|
2446
|
+
inputSchema: setMemoryModeSchema,
|
|
2447
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2448
|
+
run: memory.setMode
|
|
2449
|
+
}
|
|
2450
|
+
];
|
|
2451
|
+
for (const tool of memoryTools) {
|
|
2452
|
+
addTool(
|
|
2453
|
+
tool.name,
|
|
2454
|
+
{ title: tool.title, description: tool.description, ...tool.inputSchema ? { inputSchema: tool.inputSchema } : {}, annotations: tool.annotations },
|
|
2455
|
+
async (args) => ok(await tool.run(tool.inputSchema ? args : {}))
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
1853
2458
|
return server2;
|
|
1854
2459
|
}
|
|
1855
2460
|
|