mergestorm-mcp 0.2.26 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/index.js +450 -96
- package/dist/lib/brand-icons.d.ts +16 -0
- package/dist/lib/brand-icons.js +23 -0
- package/dist/lib/hosted.d.ts +7 -0
- package/dist/lib/hosted.js +21 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +5 -0
- package/dist/lib/pr-loop-instructions.d.ts +13 -0
- package/dist/lib/pr-loop-instructions.js +62 -0
- package/dist/lib/server.d.ts +22 -0
- package/dist/lib/server.js +558 -0
- package/dist/lib/tools/agent-request.d.ts +6 -0
- package/dist/lib/tools/agent-request.js +25 -0
- package/dist/lib/tools/chat-wake.d.ts +21 -0
- package/dist/lib/tools/chat-wake.js +26 -0
- package/dist/lib/tools/credits.d.ts +3 -0
- package/dist/lib/tools/credits.js +38 -0
- package/dist/lib/tools/cyclone-job.d.ts +29 -0
- package/dist/lib/tools/cyclone-job.js +70 -0
- package/dist/lib/tools/envelope.d.ts +5 -0
- package/dist/lib/tools/envelope.js +30 -0
- package/dist/lib/tools/issue-create.d.ts +47 -0
- package/dist/lib/tools/issue-create.js +53 -0
- package/dist/lib/tools/queue-status.d.ts +3 -0
- package/dist/lib/tools/queue-status.js +79 -0
- package/dist/lib/tools/repo-settings.d.ts +6 -0
- package/dist/lib/tools/repo-settings.js +54 -0
- package/dist/lib/tools/review-dismiss.d.ts +16 -0
- package/dist/lib/tools/review-dismiss.js +68 -0
- package/dist/lib/tools/review-get-pr.d.ts +22 -0
- package/dist/lib/tools/review-get-pr.js +52 -0
- package/dist/lib/tools/review-get.d.ts +3 -0
- package/dist/lib/tools/review-get.js +27 -0
- package/dist/lib/tools/review-list.d.ts +3 -0
- package/dist/lib/tools/review-list.js +10 -0
- package/dist/lib/tools/review-start.d.ts +11 -0
- package/dist/lib/tools/review-start.js +21 -0
- package/dist/lib/tools/review-submit.d.ts +36 -0
- package/dist/lib/tools/review-submit.js +172 -0
- package/dist/lib/tools/review-wait-pr.d.ts +11 -0
- package/dist/lib/tools/review-wait-pr.js +74 -0
- package/dist/lib/tools/review-wait.d.ts +6 -0
- package/dist/lib/tools/review-wait.js +33 -0
- package/dist/lib/tools/settings-get.d.ts +8 -0
- package/dist/lib/tools/settings-get.js +33 -0
- package/dist/lib/tools/settings-set.d.ts +11 -0
- package/dist/lib/tools/settings-set.js +62 -0
- package/dist/lib/tools/stack-actions.d.ts +4 -0
- package/dist/lib/tools/stack-actions.js +22 -0
- package/dist/lib/tools/stack-adopt.d.ts +44 -0
- package/dist/lib/tools/stack-adopt.js +55 -0
- package/dist/lib/tools/stack-list.d.ts +3 -0
- package/dist/lib/tools/stack-list.js +30 -0
- package/dist/lib/tools/stack-publish.d.ts +78 -0
- package/dist/lib/tools/stack-publish.js +89 -0
- package/dist/lib/tools/stack-set.d.ts +16 -0
- package/dist/lib/tools/stack-set.js +69 -0
- package/dist/lib/tools/stack-status.d.ts +3 -0
- package/dist/lib/tools/stack-status.js +57 -0
- package/dist/lib/tools/stack-summary.d.ts +2 -0
- package/dist/lib/tools/stack-summary.js +29 -0
- package/dist/lib/tools/stack-wait.d.ts +24 -0
- package/dist/lib/tools/stack-wait.js +84 -0
- package/dist/lib/tools/surge-setup.d.ts +6 -0
- package/dist/lib/tools/surge-setup.js +4 -0
- package/dist/lib/tools/types.d.ts +5 -0
- package/dist/lib/tools/types.js +1 -0
- package/dist/lib/tools/whoami.d.ts +3 -0
- package/dist/lib/tools/whoami.js +27 -0
- package/package.json +11 -3
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { breezeChatId } from "./tools/chat-wake.js";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CommandError, BEARER_SETTINGS } from "mergestorm/client";
|
|
5
|
+
import { MCP_SERVER_ICONS, MCP_WEBSITE_URL } from "./brand-icons.js";
|
|
6
|
+
import { credits } from "./tools/credits.js";
|
|
7
|
+
import { hostedIssueCreateSchema, issueCreate, issueCreateSchema } from "./tools/issue-create.js";
|
|
8
|
+
import { queueStatus } from "./tools/queue-status.js";
|
|
9
|
+
import { reviewDismiss } from "./tools/review-dismiss.js";
|
|
10
|
+
import { reviewStartPr, reviewStopPr } from "./tools/review-start.js";
|
|
11
|
+
import { CYCLONE_JOB_WAIT_DEFAULT_S, CYCLONE_JOB_WAIT_MAX_S, CYCLONE_TASKS, cycloneJob, cycloneStart } from "./tools/cyclone-job.js";
|
|
12
|
+
import { stackEnqueue, stackRestack } from "./tools/stack-actions.js";
|
|
13
|
+
import { surgeSetup } from "./tools/surge-setup.js";
|
|
14
|
+
import { reviewList } from "./tools/review-list.js";
|
|
15
|
+
import { reviewSubmit } from "./tools/review-submit.js";
|
|
16
|
+
import { reviewWaitPr } from "./tools/review-wait-pr.js";
|
|
17
|
+
import { reviewWait } from "./tools/review-wait.js";
|
|
18
|
+
import { settingsGet } from "./tools/settings-get.js";
|
|
19
|
+
import { settingsSet } from "./tools/settings-set.js";
|
|
20
|
+
import { repoSettingsGet, repoSettingsSet } from "./tools/repo-settings.js";
|
|
21
|
+
import { stackAdopt, stackAdoptSchema } from "./tools/stack-adopt.js";
|
|
22
|
+
import { stackList } from "./tools/stack-list.js";
|
|
23
|
+
import { stackPublish, stackPublishSchema, stackPublishStatus, stackPublishStatusSchema } from "./tools/stack-publish.js";
|
|
24
|
+
import { stackSet } from "./tools/stack-set.js";
|
|
25
|
+
import { stackWait, stackWaitSchema } from "./tools/stack-wait.js";
|
|
26
|
+
import { stackStatus } from "./tools/stack-status.js";
|
|
27
|
+
import { whoami } from "./tools/whoami.js";
|
|
28
|
+
import { MCP_FINDING_DEFERRAL_RULE, MCP_STACK_DONE_INSTRUCTIONS, MCP_PR_LOOP_INSTRUCTIONS, MCP_STACK_BASE_INSTRUCTIONS, MCP_STACK_WATCH_INSTRUCTIONS, } from "./pr-loop-instructions.js";
|
|
29
|
+
export const MCP_SERVER_NAME = "mergestorm";
|
|
30
|
+
export const MCP_SERVER_VERSION = "0.2.27";
|
|
31
|
+
export const MCP_TOOL_NAMES = [
|
|
32
|
+
"whoami",
|
|
33
|
+
"credits",
|
|
34
|
+
"review_list",
|
|
35
|
+
"review_submit",
|
|
36
|
+
"review_wait",
|
|
37
|
+
"review_wait_pr",
|
|
38
|
+
"review_dismiss",
|
|
39
|
+
"review_start_pr",
|
|
40
|
+
"review_stop_pr",
|
|
41
|
+
"cyclone_start",
|
|
42
|
+
"cyclone_job",
|
|
43
|
+
"issue_create",
|
|
44
|
+
"stack_adopt",
|
|
45
|
+
"stack_enqueue",
|
|
46
|
+
"stack_list",
|
|
47
|
+
"stack_publish",
|
|
48
|
+
"stack_publish_status",
|
|
49
|
+
"stack_restack",
|
|
50
|
+
"stack_set",
|
|
51
|
+
"stack_status",
|
|
52
|
+
"stack_wait",
|
|
53
|
+
"queue_status",
|
|
54
|
+
"settings_get",
|
|
55
|
+
"settings_set",
|
|
56
|
+
"repo_settings_get",
|
|
57
|
+
"repo_settings_set",
|
|
58
|
+
"surge_setup",
|
|
59
|
+
];
|
|
60
|
+
export const MCP_SERVER_INSTRUCTIONS = [
|
|
61
|
+
MCP_STACK_DONE_INSTRUCTIONS,
|
|
62
|
+
MCP_PR_LOOP_INSTRUCTIONS,
|
|
63
|
+
MCP_STACK_BASE_INSTRUCTIONS,
|
|
64
|
+
MCP_STACK_WATCH_INSTRUCTIONS,
|
|
65
|
+
].join("\n\n");
|
|
66
|
+
const STACK_ATTENTION_NOTE = " Attention covers every open layer, parked ones included, not only currentCandidate: it names the lowest layer with a blocker an agent must fix and no Vortex or Cyclone run working on it, and once that blocker clears the next read names the next blocked layer.";
|
|
67
|
+
const STACK_WATCH_RESULT_NOTE = " Follow watch.next.args until watch.done; background watching requires a confirmed host notification. See server instructions.";
|
|
68
|
+
function ok(payload) {
|
|
69
|
+
return {
|
|
70
|
+
...(payload.isError ? { isError: true } : {}),
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "text", text: payload.summary },
|
|
73
|
+
{ type: "text", text: JSON.stringify(payload.data) },
|
|
74
|
+
],
|
|
75
|
+
structuredContent: payload.data,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function fail(err) {
|
|
79
|
+
const message = err instanceof CommandError
|
|
80
|
+
? err.code
|
|
81
|
+
? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.`
|
|
82
|
+
: err.message
|
|
83
|
+
: err instanceof Error
|
|
84
|
+
? err.message
|
|
85
|
+
: String(err);
|
|
86
|
+
return ok({
|
|
87
|
+
isError: true,
|
|
88
|
+
summary: message,
|
|
89
|
+
data: {
|
|
90
|
+
error: {
|
|
91
|
+
message: err instanceof Error ? err.message : String(err),
|
|
92
|
+
...(err instanceof CommandError ? {
|
|
93
|
+
exit_code: err.exitCode,
|
|
94
|
+
...(err.code !== undefined ? { code: err.code } : {}),
|
|
95
|
+
...(err.reason !== undefined ? { reason: err.reason } : {}),
|
|
96
|
+
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
97
|
+
} : {}),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export const MCP_WORKSPACE_TOOL_NAMES = ["review_submit", "stack_publish", "stack_publish_status"];
|
|
103
|
+
export const MCP_HOSTED_TOOL_NAMES = MCP_TOOL_NAMES.filter((name) => !MCP_WORKSPACE_TOOL_NAMES.includes(name));
|
|
104
|
+
const 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.";
|
|
105
|
+
export function createMergestormMcpServer(options = {}) {
|
|
106
|
+
const { config, hosted = false } = options;
|
|
107
|
+
const chatId = breezeChatId(options.chatId ?? (hosted ? null : process.env.MERGESTORM_CHAT_ID));
|
|
108
|
+
const server = new McpServer({
|
|
109
|
+
name: MCP_SERVER_NAME,
|
|
110
|
+
title: "Mergestorm",
|
|
111
|
+
version: MCP_SERVER_VERSION,
|
|
112
|
+
websiteUrl: MCP_WEBSITE_URL,
|
|
113
|
+
icons: MCP_SERVER_ICONS,
|
|
114
|
+
}, { instructions: MCP_SERVER_INSTRUCTIONS });
|
|
115
|
+
// MCP SDK + zod generic inference hits TS2589 on several schemas; keep
|
|
116
|
+
// runtime registerTool, drop the instantiation from our typecheck.
|
|
117
|
+
const registerTool = server.registerTool.bind(server);
|
|
118
|
+
const addTool = (name, toolConfig, cb) => {
|
|
119
|
+
if (hosted && MCP_WORKSPACE_TOOL_NAMES.includes(name))
|
|
120
|
+
return;
|
|
121
|
+
registerTool(name, toolConfig, cb);
|
|
122
|
+
};
|
|
123
|
+
addTool("whoami", {
|
|
124
|
+
title: "Who am I",
|
|
125
|
+
description: "Current Mergestorm API key prefix, plan, API base, and the version of this running mergestorm-mcp server.",
|
|
126
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
127
|
+
}, async () => {
|
|
128
|
+
try {
|
|
129
|
+
return ok(await whoami(config, MCP_SERVER_VERSION));
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return fail(err);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
addTool("credits", {
|
|
136
|
+
title: "Credits",
|
|
137
|
+
description: "Current standard-credit usage and reset time.",
|
|
138
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
139
|
+
}, async () => {
|
|
140
|
+
try {
|
|
141
|
+
return ok(await credits(config));
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
return fail(err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
addTool("review_list", {
|
|
148
|
+
title: "List reviews",
|
|
149
|
+
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
152
|
+
},
|
|
153
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
154
|
+
}, async ({ limit }) => {
|
|
155
|
+
try {
|
|
156
|
+
return ok(await reviewList(limit, config));
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return fail(err);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
addTool("review_submit", {
|
|
163
|
+
title: "Submit review",
|
|
164
|
+
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.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
cwd: z.string().optional(),
|
|
167
|
+
base: z.string().optional(),
|
|
168
|
+
head: z.string().optional(),
|
|
169
|
+
diff: z.string().optional(),
|
|
170
|
+
router: z.enum(["off", "standard", "max", "manual"]).optional(),
|
|
171
|
+
specialists: z.array(z.string().min(1)).max(5).optional(),
|
|
172
|
+
context: z.string().optional(),
|
|
173
|
+
context_files: z.array(z.string().min(1)).max(10).optional(),
|
|
174
|
+
thread: z.string().min(1).max(120).optional(),
|
|
175
|
+
idempotency_key: z.string().min(1).max(128).optional(),
|
|
176
|
+
wait: z.boolean().optional(),
|
|
177
|
+
timeout_s: z.number().positive().max(300).optional(),
|
|
178
|
+
},
|
|
179
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
180
|
+
}, async (args) => {
|
|
181
|
+
try {
|
|
182
|
+
return ok(await reviewSubmit(args, config));
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return fail(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
addTool("review_wait", {
|
|
189
|
+
title: "Wait for review",
|
|
190
|
+
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.",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
job_id: z.string().min(1),
|
|
193
|
+
timeout_s: z.number().min(0).max(300).optional(),
|
|
194
|
+
},
|
|
195
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
196
|
+
}, async ({ job_id, timeout_s }) => {
|
|
197
|
+
try {
|
|
198
|
+
return ok(await reviewWait(job_id, timeout_s, config));
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
return fail(err);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
addTool("review_wait_pr", {
|
|
205
|
+
title: "Wait for GitHub PR Vortex review",
|
|
206
|
+
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.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
owner: z.string().min(1),
|
|
209
|
+
repo: z.string().min(1),
|
|
210
|
+
pr_number: z.number().int().min(1),
|
|
211
|
+
after_sha: z.string().optional(),
|
|
212
|
+
pass: z.number().int().min(1).optional(),
|
|
213
|
+
after_pass: z.number().int().min(1).optional(),
|
|
214
|
+
timeout_s: z.number().min(0).max(300).optional(),
|
|
215
|
+
},
|
|
216
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
217
|
+
}, async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
218
|
+
try {
|
|
219
|
+
return ok(await reviewWaitPr(owner, repo, pr_number, after_sha, timeout_s, config, { pass, afterPass: after_pass, ...(chatId ? { chatId } : {}) }));
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return fail(err);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
addTool("review_dismiss", {
|
|
226
|
+
title: "Dismiss Vortex findings",
|
|
227
|
+
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,
|
|
228
|
+
inputSchema: {
|
|
229
|
+
owner: z.string().min(1),
|
|
230
|
+
repo: z.string().min(1),
|
|
231
|
+
pr_number: z.number().int().min(1),
|
|
232
|
+
head_sha: z.string().regex(/^[0-9a-fA-F]{40}$/),
|
|
233
|
+
review_id: z.number().int().min(1),
|
|
234
|
+
finding_ids: z.array(z.string().min(1)).max(50).optional(),
|
|
235
|
+
scope: z.enum(["findings", "review"]).optional(),
|
|
236
|
+
reason: z.string().optional(),
|
|
237
|
+
evidence_url: z.string().url().optional(),
|
|
238
|
+
preview: z.boolean().optional(),
|
|
239
|
+
},
|
|
240
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
241
|
+
}, async (args) => {
|
|
242
|
+
try {
|
|
243
|
+
return ok(await reviewDismiss(args, config));
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
return fail(err);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const prInput = {
|
|
250
|
+
owner: z.string().min(1),
|
|
251
|
+
repo: z.string().min(1),
|
|
252
|
+
pr_number: z.number().int().min(1),
|
|
253
|
+
};
|
|
254
|
+
addTool("review_start_pr", {
|
|
255
|
+
title: "Start Vortex review",
|
|
256
|
+
description: "Start a Vortex review of an open GitHub pull request on the user's account, at its current head. head_sha, when given, must still be the head; otherwise nothing starts and the answer names the live head. Uses one standard credit. Findings post on the pull request; read them with review_wait_pr. A review already running at that head is reported, not started twice. When this server serves a Breeze chat, the chat is woken with the result.",
|
|
257
|
+
inputSchema: { ...prInput, head_sha: z.string().regex(/^[0-9a-fA-F]{7,40}$/).optional() },
|
|
258
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
259
|
+
}, async (args) => {
|
|
260
|
+
try {
|
|
261
|
+
return ok(await reviewStartPr(args, config, chatId));
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
return fail(err);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
addTool("review_stop_pr", {
|
|
268
|
+
title: "Stop Vortex review",
|
|
269
|
+
description: "Cancel the Vortex review running on a pull request. Starts nothing; says so when no review is running.",
|
|
270
|
+
inputSchema: prInput,
|
|
271
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
272
|
+
}, async (args) => {
|
|
273
|
+
try {
|
|
274
|
+
return ok(await reviewStopPr(args, config));
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
return fail(err);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
addTool("cyclone_start", {
|
|
281
|
+
title: "Start Cyclone job",
|
|
282
|
+
description: "Have Cyclone work on the user's repository and push the result. task fix_findings fixes the open Vortex findings on pr_number, fix_ci fixes its failing CI check (failing_check names one; default the first failing check), resolve_conflicts merges its base in and resolves the conflicts; each pushes a commit to the pull request's branch. task build builds a first working version of a new project from brief into an empty repository the user created and installed Cyclone on, then opens a pull request. Uses Cyclone credits. When the pull request has nothing to do (CI green, no conflicts, no open findings) nothing starts and the answer says why: tell the user that. Returns dispatch_id. Then wait for the job with cyclone_job until it is pushed, completed or failed, and report what it pushed. When this server serves a Breeze chat and the reply ends before the job does, the chat is woken with the result.",
|
|
283
|
+
inputSchema: {
|
|
284
|
+
task: z.enum(CYCLONE_TASKS),
|
|
285
|
+
owner: z.string().min(1),
|
|
286
|
+
repo: z.string().min(1),
|
|
287
|
+
pr_number: z.number().int().min(1).optional(),
|
|
288
|
+
failing_check: z.string().min(1).max(200).optional(),
|
|
289
|
+
brief: z.string().min(1).max(4000).optional(),
|
|
290
|
+
},
|
|
291
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
292
|
+
}, async (args) => {
|
|
293
|
+
try {
|
|
294
|
+
return ok(await cycloneStart(args, config, chatId));
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
return fail(err);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
addTool("cyclone_job", {
|
|
301
|
+
title: "Wait for Cyclone job",
|
|
302
|
+
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.`,
|
|
303
|
+
inputSchema: { dispatch_id: z.string().uuid(), timeout_s: z.number().int().min(0).max(CYCLONE_JOB_WAIT_MAX_S).optional() },
|
|
304
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
305
|
+
}, async ({ dispatch_id, timeout_s }, extra) => {
|
|
306
|
+
try {
|
|
307
|
+
return ok(await cycloneJob(dispatch_id, config, timeout_s ?? CYCLONE_JOB_WAIT_DEFAULT_S, { signal: extra.signal }));
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
return fail(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
addTool("issue_create", {
|
|
314
|
+
title: "Open GitHub issue",
|
|
315
|
+
description: hosted
|
|
316
|
+
? HOSTED_ISSUE_CREATE_DESCRIPTION
|
|
317
|
+
: "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.",
|
|
318
|
+
inputSchema: hosted ? hostedIssueCreateSchema : issueCreateSchema,
|
|
319
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
320
|
+
}, async (args) => {
|
|
321
|
+
try {
|
|
322
|
+
return ok(await issueCreate(args, config, hosted ? { parseGithubOriginRepo: () => null } : {}));
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return fail(err);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
addTool("stack_adopt", {
|
|
329
|
+
title: "Adopt PR stack",
|
|
330
|
+
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 — do not retry, tell the human what the message says. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + STACK_WATCH_RESULT_NOTE,
|
|
331
|
+
inputSchema: stackAdoptSchema,
|
|
332
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
333
|
+
}, async (args) => {
|
|
334
|
+
try {
|
|
335
|
+
return ok(await stackAdopt(args, config));
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return fail(err);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
addTool("stack_list", {
|
|
342
|
+
title: "List stacks",
|
|
343
|
+
description: "List the current user's Mergestorm stacks.",
|
|
344
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
345
|
+
}, async () => {
|
|
346
|
+
try {
|
|
347
|
+
return ok(await stackList(config));
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
return fail(err);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
addTool("stack_publish", {
|
|
354
|
+
title: "Publish branch through Mergestorm",
|
|
355
|
+
description: "Publish a local branch without any GitHub credentials. Reads the branch's commits from the local repository at cwd (never runs gh, git push or git fetch) and sends them to Mergestorm, which writes the exact commits and the branch on GitHub as the Surge App, opens a ready pull request when the branch has none, and registers the stack with Auto land on for a new stack, and Auto patch and Auto review left to the stack, repository and account settings, so Cyclone patches Vortex findings when Auto patch is on. Use it for the first publication and for every later commit to that branch. Defaults: owner and repo from origin, the checked-out branch, the repository's trunk as base, title and body from the tip commit. Two ways to publish: with no base (or trunk as base) the branch opens as a single PR onto trunk; to stack it on another branch, pass that branch as base and extend: true. A base other than trunk without extend is refused with implicit_stack. The result says which one it made. Mergestorm verifies each commit's Git tree before it moves the branch and returns the verified head and tree. It refuses with head_changed when the branch on GitHub holds work your local branch does not, base_changed when the base commit left the base branch, protected_branch for trunk and Mergestorm-owned branches, and payload_too_large beyond 20 commits, 500 changed paths, 2 MiB a file or 8 MiB in total; submodules and merge commits are not carried. replace_head replaces that exact GitHub head with the local branch and discards its commits. Safe to call again after a lost response or a failure: it resumes and never duplicates the branch, commits, pull request or stack." + STACK_WATCH_RESULT_NOTE,
|
|
356
|
+
inputSchema: stackPublishSchema,
|
|
357
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
358
|
+
}, async (args) => {
|
|
359
|
+
try {
|
|
360
|
+
return ok(await stackPublish(args, config));
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return fail(err);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
addTool("stack_publish_status", {
|
|
367
|
+
title: "Read branch publication",
|
|
368
|
+
description: "Read how far a branch published with stack_publish got: absent, pushed (branch on GitHub, no pull request), opened (pull request open, stack not registered) or registered, with the head commit and tree on GitHub, the pull request and the stack's Auto patch and Auto land state. Changes nothing. Call it after a lost stack_publish response, then call stack_publish again to resume.",
|
|
369
|
+
inputSchema: stackPublishStatusSchema,
|
|
370
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
371
|
+
}, async (args) => {
|
|
372
|
+
try {
|
|
373
|
+
return ok(await stackPublishStatus(args, config));
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
return fail(err);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
addTool("stack_set", {
|
|
380
|
+
title: "Set stack policy",
|
|
381
|
+
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings." + STACK_WATCH_RESULT_NOTE,
|
|
382
|
+
inputSchema: {
|
|
383
|
+
stack_id: z.string().min(1),
|
|
384
|
+
auto_land: z.boolean().optional(),
|
|
385
|
+
auto_review: z.boolean().nullable().optional(),
|
|
386
|
+
auto_patch: z.boolean().nullable().optional(),
|
|
387
|
+
auto_resolve_conflicts: z.boolean().nullable().optional(),
|
|
388
|
+
auto_fix_ci: z.boolean().nullable().optional(),
|
|
389
|
+
},
|
|
390
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
391
|
+
}, async ({ stack_id, auto_land, auto_review, auto_patch, auto_resolve_conflicts, auto_fix_ci }) => {
|
|
392
|
+
try {
|
|
393
|
+
return ok(await stackSet(stack_id, {
|
|
394
|
+
...(auto_land !== undefined ? { auto_land } : {}),
|
|
395
|
+
...(auto_review !== undefined ? { auto_review } : {}),
|
|
396
|
+
...(auto_patch !== undefined ? { auto_patch } : {}),
|
|
397
|
+
...(auto_resolve_conflicts !== undefined ? { auto_resolve_conflicts } : {}),
|
|
398
|
+
...(auto_fix_ci !== undefined ? { auto_fix_ci } : {}),
|
|
399
|
+
}, config));
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
return fail(err);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
addTool("stack_restack", {
|
|
406
|
+
title: "Restack stack",
|
|
407
|
+
description: "Rebase one owned stack's layer branches onto their parents and push them, after the trunk or a lower layer moved. Does not merge or enqueue anything." + STACK_WATCH_RESULT_NOTE,
|
|
408
|
+
inputSchema: { stack_id: z.string().min(1) },
|
|
409
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
410
|
+
}, async ({ stack_id }) => {
|
|
411
|
+
try {
|
|
412
|
+
return ok(await stackRestack(stack_id, config));
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
return fail(err);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
addTool("stack_enqueue", {
|
|
419
|
+
title: "Send stack to merge queue",
|
|
420
|
+
description: "Send one owned stack to the Mergestorm merge queue, which tests it and lands it when its checks pass. To restart a failed or stuck land, call stack_restack first and then this. When this server serves a Breeze chat, the chat is woken when the stack lands, is bounced or is archived." + STACK_WATCH_RESULT_NOTE,
|
|
421
|
+
inputSchema: { stack_id: z.string().min(1) },
|
|
422
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
423
|
+
}, async ({ stack_id }) => {
|
|
424
|
+
try {
|
|
425
|
+
return ok(await stackEnqueue(stack_id, config, chatId));
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
return fail(err);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
addTool("stack_status", {
|
|
432
|
+
title: "Get stack status",
|
|
433
|
+
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,
|
|
434
|
+
inputSchema: {
|
|
435
|
+
stack_id: z.string().min(1),
|
|
436
|
+
},
|
|
437
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
438
|
+
}, async ({ stack_id }) => {
|
|
439
|
+
try {
|
|
440
|
+
return ok(await stackStatus(stack_id, config));
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
return fail(err);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
addTool("stack_wait", {
|
|
447
|
+
title: "Wait for stack attention",
|
|
448
|
+
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,
|
|
449
|
+
inputSchema: stackWaitSchema,
|
|
450
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
451
|
+
}, async (args, extra) => {
|
|
452
|
+
try {
|
|
453
|
+
const payload = await stackWait(args, config, { signal: extra.signal, ...(chatId ? { chatId } : {}) });
|
|
454
|
+
return ok(payload);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
return fail(err);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
addTool("queue_status", {
|
|
461
|
+
title: "Get merge queue status",
|
|
462
|
+
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.",
|
|
463
|
+
inputSchema: {
|
|
464
|
+
stack_id: z.string().min(1).optional(),
|
|
465
|
+
},
|
|
466
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
467
|
+
}, async ({ stack_id }) => {
|
|
468
|
+
try {
|
|
469
|
+
return ok(await queueStatus(stack_id, config));
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
return fail(err);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
addTool("repo_settings_get", {
|
|
476
|
+
title: "Get repository settings",
|
|
477
|
+
description: "Read the repository-level Auto patch, Auto review and Auto land values for the repositories connected to this account. Each value is true, false, or null; null means not set, so the account setting applies. Auto patch and Auto review precedence is the stack override (stack_set), then the repository value, then the account setting (settings_get). The Auto land repository value only seeds new stacks at adoption; it does not override an existing stack. Pass repo as owner/repo for one repository, or omit it for all.",
|
|
478
|
+
inputSchema: { repo: z.string().min(3).optional() },
|
|
479
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
480
|
+
}, async (args) => {
|
|
481
|
+
try {
|
|
482
|
+
return ok(await repoSettingsGet(args ?? {}, config));
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
return fail(err);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
addTool("repo_settings_set", {
|
|
489
|
+
title: "Update repository settings",
|
|
490
|
+
description: "Set Auto patch (Cyclone), Auto review (Vortex) or the Auto land default for every stack and pull request in one repository connected to this account, and return the stored result. true or false sets the repository value; null clears it so the account setting applies again. A stack override (stack_set) still wins for that stack. auto_patch and auto_review apply to open stacks without an override from their next decision; nothing is cancelled or re-queued. auto_land only seeds new stacks. Only the account that connected the repository can change it. Never changes account settings.",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
repo: z.string().min(3),
|
|
493
|
+
auto_patch: z.boolean().nullable().optional(),
|
|
494
|
+
auto_review: z.boolean().nullable().optional(),
|
|
495
|
+
auto_land: z.boolean().nullable().optional(),
|
|
496
|
+
},
|
|
497
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
498
|
+
}, async (args) => {
|
|
499
|
+
try {
|
|
500
|
+
return ok(await repoSettingsSet(args ?? {}, config));
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
return fail(err);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
addTool("surge_setup", {
|
|
507
|
+
title: "Set up Surge CI",
|
|
508
|
+
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.",
|
|
509
|
+
inputSchema: { owner: z.string().min(1), repo: z.string().min(1) },
|
|
510
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
511
|
+
}, async (args) => {
|
|
512
|
+
try {
|
|
513
|
+
return ok(await surgeSetup(args, config));
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
return fail(err);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
addTool("settings_get", {
|
|
520
|
+
title: "Get settings",
|
|
521
|
+
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).",
|
|
522
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
523
|
+
}, async () => {
|
|
524
|
+
try {
|
|
525
|
+
return ok(await settingsGet(config));
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
return fail(err);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
addTool("settings_set", {
|
|
532
|
+
title: "Update settings",
|
|
533
|
+
description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. auto_land_settle_seconds is a whole number of seconds from 15 through 300; it applies to settle clocks that start after the write. merge_queue_batch_enabled turns merge queue batching on or off for the account, and merge_queue_batch_size is a whole number from 2 through 8. cyclone_connected and github_connected are read-only and cannot be set.",
|
|
534
|
+
inputSchema: {
|
|
535
|
+
...Object.fromEntries(BEARER_SETTINGS.map((row) => [row.key,
|
|
536
|
+
("kind" in row
|
|
537
|
+
? row.kind === "logins"
|
|
538
|
+
? z.array(z.string())
|
|
539
|
+
: row.kind === "seconds" || row.kind === "count"
|
|
540
|
+
? z.number().int().min(row.min).max(row.max)
|
|
541
|
+
: z.enum(row.values)
|
|
542
|
+
: z.boolean()).optional(),
|
|
543
|
+
])),
|
|
544
|
+
// Preserve these args so the handler rejects them even alongside a writable key.
|
|
545
|
+
cyclone_connected: z.unknown().optional(),
|
|
546
|
+
github_connected: z.unknown().optional(),
|
|
547
|
+
},
|
|
548
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
549
|
+
}, async (args) => {
|
|
550
|
+
try {
|
|
551
|
+
return ok(await settingsSet(args ?? {}, config));
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
return fail(err);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return server;
|
|
558
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { apiFetch, CommandError, loadConfig } from "mergestorm/client";
|
|
2
|
+
export async function agentRequest(route, init, cfg) {
|
|
3
|
+
const resolved = cfg ?? (await loadConfig());
|
|
4
|
+
const { status, body, retryAfterSeconds } = await apiFetch(resolved, route, { method: init.method, ...(init.json ? { json: init.json } : {}) });
|
|
5
|
+
const data = body && typeof body === "object" && !Array.isArray(body) ? body : { body };
|
|
6
|
+
if (status === 404 && data.error === undefined) {
|
|
7
|
+
throw new CommandError("This Mergestorm server does not offer that action yet.", 1, "not_found");
|
|
8
|
+
}
|
|
9
|
+
const message = typeof data.message === "string" ? data.message : `Mergestorm answered ${status}.`;
|
|
10
|
+
if (status >= 400) {
|
|
11
|
+
return {
|
|
12
|
+
summary: message,
|
|
13
|
+
isError: true,
|
|
14
|
+
data: {
|
|
15
|
+
...data,
|
|
16
|
+
error: {
|
|
17
|
+
code: typeof data.error === "string" ? data.error : `http_${status}`,
|
|
18
|
+
message,
|
|
19
|
+
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { summary: message, data };
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Config } from "mergestorm/client";
|
|
2
|
+
export type ChatWakeTarget = {
|
|
3
|
+
kind: "vortex";
|
|
4
|
+
owner: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
prNumber: number;
|
|
7
|
+
headSha: string | null;
|
|
8
|
+
minPass: number | null;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "stack";
|
|
11
|
+
stackId: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function breezeChatId(value: string | null | undefined): string | null;
|
|
14
|
+
export type ChatWake = {
|
|
15
|
+
id: string;
|
|
16
|
+
result: "waiting" | "existing" | "finished";
|
|
17
|
+
};
|
|
18
|
+
export declare const CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
19
|
+
export declare const REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
20
|
+
export declare const CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
|
|
21
|
+
export declare function registerChatWake(cfg: Config, chatId: string, target: ChatWakeTarget, signal?: AbortSignal): Promise<ChatWake | null>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { apiFetch } from "mergestorm/client";
|
|
2
|
+
const CHAT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
export function breezeChatId(value) {
|
|
4
|
+
const trimmed = value?.trim() ?? "";
|
|
5
|
+
return CHAT_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
6
|
+
}
|
|
7
|
+
export const CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
8
|
+
export const REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
9
|
+
export const CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
|
|
10
|
+
export async function registerChatWake(cfg, chatId, target, signal) {
|
|
11
|
+
const json = target.kind === "vortex"
|
|
12
|
+
? { chat_id: chatId, kind: "vortex", owner: target.owner, repo: target.repo, pr_number: target.prNumber, head_sha: target.headSha, min_pass: target.headSha ? target.minPass : null }
|
|
13
|
+
: { chat_id: chatId, kind: "stack", stack_id: target.stackId };
|
|
14
|
+
try {
|
|
15
|
+
const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 10_000 });
|
|
16
|
+
const body = response.body;
|
|
17
|
+
if (response.status !== 200 || !body || typeof body.id !== "string")
|
|
18
|
+
return null;
|
|
19
|
+
if (body.result !== "waiting" && body.result !== "existing" && body.result !== "finished")
|
|
20
|
+
return null;
|
|
21
|
+
return { id: body.id, result: body.result };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|