mergestorm-mcp 0.2.27 → 0.2.29
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 +7 -3
- package/dist/index.js +651 -254
- package/dist/lib/hosted.d.ts +1 -1
- package/dist/lib/hosted.js +1 -1
- package/dist/lib/pr-loop-instructions.d.ts +1 -0
- package/dist/lib/pr-loop-instructions.js +13 -2
- package/dist/lib/server.d.ts +6 -4
- package/dist/lib/server.js +149 -28
- package/dist/lib/tools/chat-task.d.ts +18 -0
- package/dist/lib/tools/chat-task.js +28 -0
- package/dist/lib/tools/cyclone-job.d.ts +0 -4
- package/dist/lib/tools/cyclone-job.js +6 -5
- package/dist/lib/tools/issue-create.js +1 -1
- package/dist/lib/tools/issue-review-wait.d.ts +37 -0
- package/dist/lib/tools/issue-review-wait.js +57 -0
- package/dist/lib/tools/memory.d.ts +70 -0
- package/dist/lib/tools/memory.js +74 -0
- package/dist/lib/tools/repo-settings.d.ts +1 -2
- package/dist/lib/tools/repo-settings.js +27 -2
- package/dist/lib/tools/review-dismiss.d.ts +1 -2
- package/dist/lib/tools/review-dismiss.js +1 -1
- package/dist/lib/tools/review-start.js +3 -2
- package/dist/lib/tools/review-submit.d.ts +0 -7
- package/dist/lib/tools/review-submit.js +2 -2
- package/dist/lib/tools/review-wait-pr.js +5 -13
- package/dist/lib/tools/settings-set.d.ts +1 -9
- package/dist/lib/tools/settings-set.js +4 -4
- package/dist/lib/tools/stack-actions.js +4 -4
- package/dist/lib/tools/stack-adopt.js +2 -1
- package/dist/lib/tools/stack-publish.d.ts +2 -2
- package/dist/lib/tools/stack-publish.js +2 -1
- package/dist/lib/tools/stack-wait.d.ts +19 -1
- package/dist/lib/tools/stack-wait.js +72 -10
- package/dist/lib/tools/web.d.ts +27 -0
- package/dist/lib/tools/web.js +36 -0
- package/dist/lib/tools/whoami.js +6 -2
- package/package.json +4 -2
- package/dist/lib/tools/chat-wake.d.ts +0 -21
- package/dist/lib/tools/chat-wake.js +0 -26
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { agentRequest } from "./agent-request.js";
|
|
2
|
+
import { chatTaskNote, chatTaskOf } from "./chat-task.js";
|
|
2
3
|
export const CYCLONE_TASKS = ["fix_findings", "fix_ci", "resolve_conflicts", "build"];
|
|
3
4
|
export const CYCLONE_JOB_WAIT_DEFAULT_S = 45;
|
|
4
5
|
export const CYCLONE_JOB_WAIT_MAX_S = 100;
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const CYCLONE_JOB_POLL_MS = 5_000;
|
|
7
|
+
const CYCLONE_JOB_TERMINAL_STATES = ["pushed", "completed", "failed"];
|
|
7
8
|
export const 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.`;
|
|
8
|
-
export const CHAT_WAKE_CYCLONE_NOTE = "If your reply ends before Cyclone finishes, this chat is woken with the job's result.";
|
|
9
9
|
export const 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.`;
|
|
10
10
|
export async function cycloneStart(input, cfg, chatId) {
|
|
11
11
|
const payload = await agentRequest("/api/v1/agents/cyclone/jobs", {
|
|
@@ -22,9 +22,10 @@ export async function cycloneStart(input, cfg, chatId) {
|
|
|
22
22
|
}, cfg);
|
|
23
23
|
if (payload.isError || typeof payload.data.dispatch_id !== "string" || isCycloneJobTerminal(payload.data))
|
|
24
24
|
return payload;
|
|
25
|
-
|
|
25
|
+
const task = chatTaskOf(payload.data.background_task);
|
|
26
|
+
return { ...payload, summary: `${payload.summary} ${task ? chatTaskNote(task) : CYCLONE_START_WAIT_NOTE}` };
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
function isCycloneJobTerminal(job) {
|
|
28
29
|
return CYCLONE_JOB_TERMINAL_STATES.includes(job.state);
|
|
29
30
|
}
|
|
30
31
|
export function cycloneJobSummary(job) {
|
|
@@ -31,7 +31,7 @@ export async function issueCreate(input, cfg, deps = {}) {
|
|
|
31
31
|
}
|
|
32
32
|
const issue = await (deps.openStackIssue ?? openStackIssue)({ owner: target.owner, repo: target.repo, title, body: body ?? "", labels: labels ?? [] }, cfg);
|
|
33
33
|
return {
|
|
34
|
-
summary: `${issue.created ? "Opened" : "Already open:"} ${target.owner}/${target.repo}#${issue.number} ${issue.url}.
|
|
34
|
+
summary: `${issue.created ? "Opened" : "Already open:"} ${target.owner}/${target.repo}#${issue.number} ${issue.url}. Call issue_review_wait for #${issue.number} and open the pull request only once it is approved, with "Fixes #${issue.number}" in the commit or pull request body.`,
|
|
35
35
|
data: { owner: target.owner, repo: target.repo, ...issue },
|
|
36
36
|
};
|
|
37
37
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getIssueReview, pollIssueReview, type Config, type IssueReviewStatus } from "mergestorm/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolPayload } from "./types.js";
|
|
4
|
+
export declare const ISSUE_REVIEW_WAIT_MAX_S = 45;
|
|
5
|
+
export declare const issueReviewWaitSchema: {
|
|
6
|
+
owner: z.ZodString;
|
|
7
|
+
repo: z.ZodString;
|
|
8
|
+
number: z.ZodNumber;
|
|
9
|
+
timeout_s: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
};
|
|
11
|
+
declare const inputSchema: z.ZodObject<{
|
|
12
|
+
owner: z.ZodString;
|
|
13
|
+
repo: z.ZodString;
|
|
14
|
+
number: z.ZodNumber;
|
|
15
|
+
timeout_s: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
number: number;
|
|
18
|
+
owner: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
timeout_s?: number | undefined;
|
|
21
|
+
}, {
|
|
22
|
+
number: number;
|
|
23
|
+
owner: string;
|
|
24
|
+
repo: string;
|
|
25
|
+
timeout_s?: number | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
export type IssueReviewWaitInput = z.infer<typeof inputSchema>;
|
|
28
|
+
export type IssueReviewWaitDeps = {
|
|
29
|
+
getIssueReview?: typeof getIssueReview;
|
|
30
|
+
pollIssueReview?: typeof pollIssueReview;
|
|
31
|
+
pollIntervalMs?: number;
|
|
32
|
+
};
|
|
33
|
+
export declare function issueReviewSummary(status: IssueReviewStatus): string;
|
|
34
|
+
export declare const ISSUE_NOT_APPROVED_HINT = "Do not open the pull request yet: answer the points in a comment on each issue, call issue_review_wait until it returns approved, then try again.";
|
|
35
|
+
export declare function withIssueReviewHint(err: unknown, message: string): string;
|
|
36
|
+
export declare function issueReviewWait(input: IssueReviewWaitInput, cfg?: Config, deps?: IssueReviewWaitDeps): Promise<ToolPayload>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CommandError, IssueReviewTimeoutError, getIssueReview, loadConfig, pollIssueReview, } from "mergestorm/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const ISSUE_REVIEW_WAIT_MAX_S = 45;
|
|
4
|
+
export const issueReviewWaitSchema = {
|
|
5
|
+
owner: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/),
|
|
6
|
+
repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/),
|
|
7
|
+
number: z.number().int().min(1),
|
|
8
|
+
timeout_s: z.number().min(0).max(ISSUE_REVIEW_WAIT_MAX_S).optional(),
|
|
9
|
+
};
|
|
10
|
+
const inputSchema = z.object(issueReviewWaitSchema);
|
|
11
|
+
export function issueReviewSummary(status) {
|
|
12
|
+
const ref = `${status.owner}/${status.repo}#${status.number}`;
|
|
13
|
+
const round = status.round ? ` (round ${status.round} of ${status.maxRounds})` : "";
|
|
14
|
+
const head = status.state === "approved"
|
|
15
|
+
? `${ref} is approved by ${status.approvedBy === "human" ? "a person" : "Vortex"}; you may open the pull request.`
|
|
16
|
+
: status.state === "off"
|
|
17
|
+
? `Issue reviews are off for ${status.owner}/${status.repo}; ${ref} needs no approval.`
|
|
18
|
+
: status.state === "pending"
|
|
19
|
+
? `${ref}: Vortex is still reviewing${round}; call issue_review_wait again.`
|
|
20
|
+
: `${ref}: ${status.state}${round}, ${status.points.length} point${status.points.length === 1 ? "" : "s"} to answer. Do not open the pull request yet.`;
|
|
21
|
+
return status.next ? `${head} ${status.next}` : head;
|
|
22
|
+
}
|
|
23
|
+
export const ISSUE_NOT_APPROVED_HINT = "Do not open the pull request yet: answer the points in a comment on each issue, call issue_review_wait until it returns approved, then try again.";
|
|
24
|
+
export function withIssueReviewHint(err, message) {
|
|
25
|
+
return err instanceof CommandError && err.code === "issue_not_approved" ? `${message}\n${ISSUE_NOT_APPROVED_HINT}` : message;
|
|
26
|
+
}
|
|
27
|
+
function failure(code, message, extra = {}) {
|
|
28
|
+
return { summary: message, data: { error: { code, message, ...extra } }, isError: true };
|
|
29
|
+
}
|
|
30
|
+
export async function issueReviewWait(input, cfg, deps = {}) {
|
|
31
|
+
const parsed = inputSchema.safeParse(input);
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
return failure("invalid_input", `issue_review_wait needs owner, repo, a positive issue number and timeout_s from 0 to ${ISSUE_REVIEW_WAIT_MAX_S}`);
|
|
34
|
+
}
|
|
35
|
+
const { owner, repo, number, timeout_s } = parsed.data;
|
|
36
|
+
const target = { owner, repo, number };
|
|
37
|
+
try {
|
|
38
|
+
const resolved = cfg ?? (await loadConfig());
|
|
39
|
+
const status = timeout_s === 0
|
|
40
|
+
? await (deps.getIssueReview ?? getIssueReview)(resolved, target)
|
|
41
|
+
: await (deps.pollIssueReview ?? pollIssueReview)(resolved, target, {
|
|
42
|
+
timeoutMs: Math.max(1, Math.round((timeout_s ?? ISSUE_REVIEW_WAIT_MAX_S) * 1000)),
|
|
43
|
+
...(deps.pollIntervalMs !== undefined ? { intervalMs: deps.pollIntervalMs } : {}),
|
|
44
|
+
});
|
|
45
|
+
if (!status) {
|
|
46
|
+
return failure("issue_review_unavailable", "This Mergestorm server does not review issues yet. Open the pull request as before.");
|
|
47
|
+
}
|
|
48
|
+
return { summary: issueReviewSummary(status), data: { ...status } };
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof IssueReviewTimeoutError) {
|
|
52
|
+
return { summary: issueReviewSummary(err.lastStatus), data: { ...err.lastStatus } };
|
|
53
|
+
}
|
|
54
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
+
return failure(err instanceof CommandError ? err.code ?? "issue_review_wait_failed" : "issue_review_wait_failed", message, err instanceof CommandError && err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { MEMORY_KINDS, MEMORY_MODES, MEMORY_SCOPES, deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, type Config, type MemoryItem } from "mergestorm/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolPayload } from "./types.js";
|
|
4
|
+
export declare const listMemoriesSchema: {
|
|
5
|
+
scope: z.ZodOptional<z.ZodEnum<["user", "repo"]>>;
|
|
6
|
+
repo: z.ZodOptional<z.ZodString>;
|
|
7
|
+
kind: z.ZodOptional<z.ZodEnum<["rule", "style", "preference", "fact", "project"]>>;
|
|
8
|
+
};
|
|
9
|
+
export declare const searchMemorySchema: {
|
|
10
|
+
query: z.ZodString;
|
|
11
|
+
repo: z.ZodOptional<z.ZodString>;
|
|
12
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
};
|
|
14
|
+
export declare const rememberSchema: {
|
|
15
|
+
text: z.ZodString;
|
|
16
|
+
kind: z.ZodEnum<["rule", "style", "preference", "fact", "project"]>;
|
|
17
|
+
repo: z.ZodOptional<z.ZodString>;
|
|
18
|
+
keep_both: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
};
|
|
20
|
+
export declare const updateMemorySchema: {
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
text: z.ZodString;
|
|
23
|
+
kind: z.ZodOptional<z.ZodEnum<["rule", "style", "preference", "fact", "project"]>>;
|
|
24
|
+
};
|
|
25
|
+
export declare const forgetMemorySchema: {
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
};
|
|
28
|
+
export declare const setMemoryModeSchema: {
|
|
29
|
+
memory_mode: z.ZodEnum<["off", "on_request", "all"]>;
|
|
30
|
+
};
|
|
31
|
+
export type MemoryApi = {
|
|
32
|
+
listMemories: typeof listMemories;
|
|
33
|
+
searchMemories: typeof searchMemories;
|
|
34
|
+
saveMemory: typeof saveMemory;
|
|
35
|
+
updateMemory: typeof updateMemory;
|
|
36
|
+
deleteMemory: typeof deleteMemory;
|
|
37
|
+
getMemoryMode: typeof getMemoryMode;
|
|
38
|
+
setMemoryMode: typeof setMemoryMode;
|
|
39
|
+
};
|
|
40
|
+
export declare function memoryLine(memory: MemoryItem): string;
|
|
41
|
+
export declare function createMemoryToolHandlers(cfg: Config | undefined, chatId: string | null, api?: MemoryApi): {
|
|
42
|
+
list: (input: {
|
|
43
|
+
scope?: (typeof MEMORY_SCOPES)[number];
|
|
44
|
+
repo?: string;
|
|
45
|
+
kind?: (typeof MEMORY_KINDS)[number];
|
|
46
|
+
}) => Promise<ToolPayload>;
|
|
47
|
+
search: (input: {
|
|
48
|
+
query: string;
|
|
49
|
+
repo?: string;
|
|
50
|
+
limit?: number;
|
|
51
|
+
}) => Promise<ToolPayload>;
|
|
52
|
+
remember: ({ keep_both, ...input }: {
|
|
53
|
+
text: string;
|
|
54
|
+
kind: (typeof MEMORY_KINDS)[number];
|
|
55
|
+
repo?: string;
|
|
56
|
+
keep_both?: boolean;
|
|
57
|
+
}) => Promise<ToolPayload>;
|
|
58
|
+
update: (input: {
|
|
59
|
+
id: string;
|
|
60
|
+
text: string;
|
|
61
|
+
kind?: (typeof MEMORY_KINDS)[number];
|
|
62
|
+
}) => Promise<ToolPayload>;
|
|
63
|
+
forget: (input: {
|
|
64
|
+
id: string;
|
|
65
|
+
}) => Promise<ToolPayload>;
|
|
66
|
+
getMode: () => Promise<ToolPayload>;
|
|
67
|
+
setMode: (input: {
|
|
68
|
+
memory_mode: (typeof MEMORY_MODES)[number];
|
|
69
|
+
}) => Promise<ToolPayload>;
|
|
70
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CommandError, MEMORY_KINDS, MEMORY_MODES, MEMORY_SCOPES, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, } from "mergestorm/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const memoryId = z.string().uuid();
|
|
4
|
+
const memoryText = z.string().trim().min(1).max(500);
|
|
5
|
+
const repo = z.string().trim().min(3).max(200);
|
|
6
|
+
export const listMemoriesSchema = {
|
|
7
|
+
scope: z.enum(MEMORY_SCOPES).optional(),
|
|
8
|
+
repo: repo.optional(),
|
|
9
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
10
|
+
};
|
|
11
|
+
export const searchMemorySchema = {
|
|
12
|
+
query: z.string().trim().min(1).max(2000),
|
|
13
|
+
repo: repo.optional(),
|
|
14
|
+
limit: z.number().int().min(1).max(MEMORY_SEARCH_LIMIT_MAX).optional(),
|
|
15
|
+
};
|
|
16
|
+
export const rememberSchema = { text: memoryText, kind: z.enum(MEMORY_KINDS), repo: repo.optional(), keep_both: z.boolean().optional() };
|
|
17
|
+
export const updateMemorySchema = { id: memoryId, text: memoryText, kind: z.enum(MEMORY_KINDS).optional() };
|
|
18
|
+
export const forgetMemorySchema = { id: memoryId };
|
|
19
|
+
export const setMemoryModeSchema = { memory_mode: z.enum(MEMORY_MODES) };
|
|
20
|
+
const liveApi = { listMemories, searchMemories, saveMemory, updateMemory, deleteMemory, getMemoryMode, setMemoryMode };
|
|
21
|
+
export function memoryLine(memory) {
|
|
22
|
+
return `- [${memory.id}] (${memory.repo ? `${memory.kind}, ${memory.repo}` : memory.kind}) ${memory.text}`;
|
|
23
|
+
}
|
|
24
|
+
async function guarded(run) {
|
|
25
|
+
try {
|
|
26
|
+
return await run();
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
const code = err instanceof CommandError ? err.code ?? "memory_failed" : "memory_failed";
|
|
31
|
+
return {
|
|
32
|
+
summary: message,
|
|
33
|
+
data: { error: { code, message, ...(err instanceof CommandError && err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}) } },
|
|
34
|
+
isError: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createMemoryToolHandlers(cfg, chatId, api = liveApi) {
|
|
39
|
+
return {
|
|
40
|
+
list: (input) => guarded(async () => {
|
|
41
|
+
const listed = await api.listMemories(input, cfg);
|
|
42
|
+
const count = listed.items.length === 1 ? "1 memory" : `${listed.items.length} memories`;
|
|
43
|
+
return { summary: [`Memory is ${listed.memory_mode}. ${count}.`, ...listed.items.map(memoryLine)].join("\n"), data: listed };
|
|
44
|
+
}),
|
|
45
|
+
search: (input) => guarded(async () => {
|
|
46
|
+
const found = await api.searchMemories({ ...input, chatId }, cfg);
|
|
47
|
+
const all = [...found.rules, ...found.items];
|
|
48
|
+
const summary = found.memory_mode === "off"
|
|
49
|
+
? "Memory is off, so nothing was searched."
|
|
50
|
+
: all.length === 0 ? "Nothing in memory matches." : ["Memories that match, best first:", ...all.map(memoryLine)].join("\n");
|
|
51
|
+
return { summary, data: found };
|
|
52
|
+
}),
|
|
53
|
+
remember: ({ keep_both, ...input }) => guarded(async () => {
|
|
54
|
+
const saved = await api.saveMemory({ ...input, chatId, ...(keep_both ? { keepBoth: true } : {}) }, cfg);
|
|
55
|
+
return { summary: saved.merged ? `Memory ${saved.id} already says this.` : `Saved to memory as ${saved.id}.`, data: saved };
|
|
56
|
+
}),
|
|
57
|
+
update: (input) => guarded(async () => {
|
|
58
|
+
const updated = await api.updateMemory(input.id, { text: input.text, ...(input.kind ? { kind: input.kind } : {}) }, cfg);
|
|
59
|
+
return { summary: `Memory ${updated.id} updated.`, data: updated };
|
|
60
|
+
}),
|
|
61
|
+
forget: (input) => guarded(async () => {
|
|
62
|
+
const deleted = await api.deleteMemory(input.id, cfg);
|
|
63
|
+
return { summary: `Memory ${deleted.id} forgotten.`, data: deleted };
|
|
64
|
+
}),
|
|
65
|
+
getMode: () => guarded(async () => {
|
|
66
|
+
const mode = await api.getMemoryMode(cfg);
|
|
67
|
+
return { summary: `Memory is ${mode.memory_mode}.`, data: mode };
|
|
68
|
+
}),
|
|
69
|
+
setMode: (input) => guarded(async () => {
|
|
70
|
+
const mode = await api.setMemoryMode(input.memory_mode, cfg);
|
|
71
|
+
return { summary: `Memory is now ${mode.memory_mode}.`, data: mode };
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { type Config, type
|
|
1
|
+
import { type Config, type RepoSettingsPatch } from "mergestorm/client";
|
|
2
2
|
import type { ToolPayload } from "./types.js";
|
|
3
|
-
export declare function repoSettingsSummary(entry: RepoSettings): string;
|
|
4
3
|
export declare function repoSettingsPatchFromArgs(args: Record<string, unknown>): RepoSettingsPatch;
|
|
5
4
|
export declare function repoSettingsGet(args: Record<string, unknown>, cfg?: Config): Promise<ToolPayload>;
|
|
6
5
|
export declare function repoSettingsSet(args: Record<string, unknown>, cfg?: Config): Promise<ToolPayload>;
|
|
@@ -3,6 +3,11 @@ const ARG_BY_KEY = {
|
|
|
3
3
|
auto_patch_enabled: "auto_patch",
|
|
4
4
|
auto_review_enabled: "auto_review",
|
|
5
5
|
auto_land_default: "auto_land",
|
|
6
|
+
merge_queue_batch_enabled: "merge_queue_batch",
|
|
7
|
+
merge_queue_batch_size: "merge_queue_batch_size",
|
|
8
|
+
merge_queue_lands_alone_paths: "merge_queue_lands_alone_paths",
|
|
9
|
+
merge_queue_overdrive: "merge_queue_overdrive",
|
|
10
|
+
issue_review_enabled: "issue_review",
|
|
6
11
|
};
|
|
7
12
|
function parseRepo(raw) {
|
|
8
13
|
const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
|
|
@@ -11,9 +16,15 @@ function parseRepo(raw) {
|
|
|
11
16
|
return { owner: match[1], repo: match[2] };
|
|
12
17
|
}
|
|
13
18
|
function words(value) {
|
|
14
|
-
|
|
19
|
+
if (value === null)
|
|
20
|
+
return "inherit";
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.length === 0 ? "none" : value.join(",");
|
|
23
|
+
if (typeof value === "number")
|
|
24
|
+
return String(value);
|
|
25
|
+
return value ? "on" : "off";
|
|
15
26
|
}
|
|
16
|
-
|
|
27
|
+
function repoSettingsSummary(entry) {
|
|
17
28
|
return `${entry.owner}/${entry.repo}: ${REPO_SETTINGS_KEYS.map((key) => `${ARG_BY_KEY[key]} ${words(entry[key])}`).join(" · ")}`;
|
|
18
29
|
}
|
|
19
30
|
export function repoSettingsPatchFromArgs(args) {
|
|
@@ -22,6 +33,20 @@ export function repoSettingsPatchFromArgs(args) {
|
|
|
22
33
|
const value = args[ARG_BY_KEY[key]];
|
|
23
34
|
if (value === undefined)
|
|
24
35
|
continue;
|
|
36
|
+
if (key === "merge_queue_batch_size") {
|
|
37
|
+
if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
38
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes a whole number or null.`, 2, "usage");
|
|
39
|
+
}
|
|
40
|
+
patch[key] = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (key === "merge_queue_lands_alone_paths") {
|
|
44
|
+
if (value !== null && !(Array.isArray(value) && value.every((entry) => typeof entry === "string"))) {
|
|
45
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes a list of path patterns or null.`, 2, "usage");
|
|
46
|
+
}
|
|
47
|
+
patch[key] = value;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
25
50
|
if (value !== null && typeof value !== "boolean") {
|
|
26
51
|
throw new CommandError(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
|
|
27
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Config
|
|
1
|
+
import { type Config } from "mergestorm/client";
|
|
2
2
|
import type { ToolPayload } from "./types.js";
|
|
3
3
|
export type ReviewDismissInput = {
|
|
4
4
|
owner: string;
|
|
@@ -12,5 +12,4 @@ export type ReviewDismissInput = {
|
|
|
12
12
|
evidence_url?: string;
|
|
13
13
|
preview?: boolean;
|
|
14
14
|
};
|
|
15
|
-
export declare function reviewDismissSummary(result: PrFindingDismissResult): string;
|
|
16
15
|
export declare function reviewDismiss(input: ReviewDismissInput, cfg?: Config): Promise<ToolPayload>;
|
|
@@ -3,7 +3,7 @@ import { withWatchText } from "./stack-wait.js";
|
|
|
3
3
|
function findingText(finding) {
|
|
4
4
|
return `${finding.finding_id} (${finding.path ?? "review body"}: ${finding.title})`;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
function reviewDismissSummary(result) {
|
|
7
7
|
const pr = `${result.owner}/${result.repo}#${result.pr_number}`;
|
|
8
8
|
const kind = result.review_kind === "seam" ? "seam review" : "Vortex review";
|
|
9
9
|
const head = `${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agentRequest } from "./agent-request.js";
|
|
2
|
-
import {
|
|
2
|
+
import { chatTaskNote, chatTaskOf } from "./chat-task.js";
|
|
3
3
|
export async function reviewStartPr(input, cfg, chatId) {
|
|
4
4
|
const payload = await agentRequest("/api/v1/agents/vortex/review", {
|
|
5
5
|
method: "POST",
|
|
@@ -11,7 +11,8 @@ export async function reviewStartPr(input, cfg, chatId) {
|
|
|
11
11
|
...(chatId ? { chat_id: chatId } : {}),
|
|
12
12
|
},
|
|
13
13
|
}, cfg);
|
|
14
|
-
|
|
14
|
+
const task = chatTaskOf(payload.data.background_task);
|
|
15
|
+
return task ? { ...payload, summary: `${payload.summary} ${chatTaskNote(task)}` } : payload;
|
|
15
16
|
}
|
|
16
17
|
export async function reviewStopPr(input, cfg) {
|
|
17
18
|
return agentRequest("/api/v1/agents/vortex/stop", {
|
|
@@ -8,13 +8,6 @@ export declare const MCP_WAIT_CEILING_S = 300;
|
|
|
8
8
|
* slice. The skill loops until the review finishes.
|
|
9
9
|
*/
|
|
10
10
|
export declare const MCP_PR_WAIT_DEFAULT_S = 45;
|
|
11
|
-
export declare function assertPinnableSpecialists(specialists: string[] | undefined): void;
|
|
12
|
-
/**
|
|
13
|
-
* Host-configured root that agent-chosen `context_files` may not escape.
|
|
14
|
-
* The `cwd` tool argument is agent-controlled, so it cannot serve as the
|
|
15
|
-
* sandbox root; default to the MCP server's own working directory.
|
|
16
|
-
*/
|
|
17
|
-
export declare function mcpSandboxRoot(): string;
|
|
18
11
|
export type ReviewSubmitInput = {
|
|
19
12
|
cwd?: string;
|
|
20
13
|
base?: string;
|
|
@@ -21,7 +21,7 @@ const PINNABLE_SPECIALISTS = [
|
|
|
21
21
|
"api",
|
|
22
22
|
"frontend",
|
|
23
23
|
];
|
|
24
|
-
|
|
24
|
+
function assertPinnableSpecialists(specialists) {
|
|
25
25
|
if (!specialists?.length)
|
|
26
26
|
return;
|
|
27
27
|
const unknown = specialists.filter((id) => !PINNABLE_SPECIALISTS.includes(id));
|
|
@@ -34,7 +34,7 @@ export function assertPinnableSpecialists(specialists) {
|
|
|
34
34
|
* The `cwd` tool argument is agent-controlled, so it cannot serve as the
|
|
35
35
|
* sandbox root; default to the MCP server's own working directory.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
function mcpSandboxRoot() {
|
|
38
38
|
return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
|
|
39
39
|
}
|
|
40
40
|
export function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PrReviewPollTimeoutError, loadConfig, noPrReviewYetMessage, pollPrVortexReview, } from "mergestorm/client";
|
|
2
2
|
import { payloadFromPrRateLimit, payloadFromPrReview, prReviewSummary, prReviewTarget, reviewGetPr, } from "./review-get-pr.js";
|
|
3
3
|
import { MCP_PR_WAIT_DEFAULT_S, waitTimeoutMs } from "./review-submit.js";
|
|
4
|
-
import {
|
|
4
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
5
5
|
export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
6
6
|
const target = prReviewTarget(owner, repo, prNumber);
|
|
7
7
|
const resolved = cfg ?? (await loadConfig());
|
|
@@ -39,22 +39,14 @@ export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, c
|
|
|
39
39
|
finding_count: findingCount,
|
|
40
40
|
phase: err.lastEnvelope?.phase ?? null,
|
|
41
41
|
});
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
? await registerChatWake(resolved, opts.chatId, {
|
|
45
|
-
kind: "vortex",
|
|
46
|
-
owner: target.owner,
|
|
47
|
-
repo: target.repo,
|
|
48
|
-
prNumber: target.prNumber,
|
|
49
|
-
headSha: wakeHead,
|
|
50
|
-
minPass: opts.pass ?? (opts.afterPass !== undefined ? opts.afterPass + 1 : (headShaMatches || !expectedSha) && observedSha ? err.lastEnvelope?.pass ?? null : null),
|
|
51
|
-
})
|
|
42
|
+
const task = opts.chatId
|
|
43
|
+
? await trackInChat(resolved, opts.chatId, { kind: "review", owner: target.owner, repo: target.repo, prNumber: target.prNumber })
|
|
52
44
|
: null;
|
|
53
45
|
const shown = err.reviewSeen
|
|
54
46
|
? summary
|
|
55
47
|
: `${summary} · ${noPrReviewYetMessage(target.owner, target.repo, target.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
|
|
56
48
|
return {
|
|
57
|
-
summary:
|
|
49
|
+
summary: task ? `${shown} · ${chatTaskNote(task)}` : shown,
|
|
58
50
|
data: {
|
|
59
51
|
status,
|
|
60
52
|
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
@@ -62,7 +54,7 @@ export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, c
|
|
|
62
54
|
finding_count: findingCount,
|
|
63
55
|
phase: err.lastEnvelope?.phase ?? null,
|
|
64
56
|
error: err.message,
|
|
65
|
-
...(
|
|
57
|
+
...(task ? { background_task: task } : {}),
|
|
66
58
|
},
|
|
67
59
|
};
|
|
68
60
|
}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import { type Config
|
|
1
|
+
import { type Config } from "mergestorm/client";
|
|
2
2
|
import type { ToolPayload } from "./types.js";
|
|
3
|
-
/** Read-only on `/api/v1/settings`; the API owns them, agents never write them. */
|
|
4
|
-
export declare const SETTINGS_READ_ONLY_KEYS: readonly ["cyclone_connected", "github_connected"];
|
|
5
|
-
/**
|
|
6
|
-
* Build a PATCH body from raw tool args. Only the Bearer-writable allowlist
|
|
7
|
-
* passes through; read-only keys and empty patches are usage errors so the
|
|
8
|
-
* failure is the caller's wording, never a silent no-op.
|
|
9
|
-
*/
|
|
10
|
-
export declare function settingsPatchFromArgs(args: Record<string, unknown>): SettingsPatch;
|
|
11
3
|
export declare function settingsSet(args: Record<string, unknown>, cfg?: Config): Promise<ToolPayload>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommandError, BEARER_SETTINGS, SETTINGS_WRITABLE_KEYS, patchSettings, } from "mergestorm/client";
|
|
2
2
|
import { settingsSummary } from "./settings-get.js";
|
|
3
3
|
/** Read-only on `/api/v1/settings`; the API owns them, agents never write them. */
|
|
4
|
-
|
|
4
|
+
const SETTINGS_READ_ONLY_KEYS = [
|
|
5
5
|
"cyclone_connected",
|
|
6
6
|
"github_connected",
|
|
7
7
|
];
|
|
@@ -10,7 +10,7 @@ export const SETTINGS_READ_ONLY_KEYS = [
|
|
|
10
10
|
* passes through; read-only keys and empty patches are usage errors so the
|
|
11
11
|
* failure is the caller's wording, never a silent no-op.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
function settingsPatchFromArgs(args) {
|
|
14
14
|
for (const key of SETTINGS_READ_ONLY_KEYS) {
|
|
15
15
|
if (args[key] !== undefined) {
|
|
16
16
|
throw new CommandError(`${key} is read-only and cannot be set.`, 2, "usage");
|
|
@@ -34,8 +34,8 @@ export function settingsPatchFromArgs(args) {
|
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
36
|
if ("kind" in row) {
|
|
37
|
-
const valid = row.kind === "logins"
|
|
38
|
-
? Array.isArray(value) && value.every((
|
|
37
|
+
const valid = row.kind === "logins" || row.kind === "paths"
|
|
38
|
+
? Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
39
39
|
: typeof value === "string" && row.values.includes(value);
|
|
40
40
|
if (!valid)
|
|
41
41
|
throw new CommandError(`Invalid value for ${key}.`, 2, "usage");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { enqueueStack, loadConfig, restackStack, stackWatchObligation } from "mergestorm/client";
|
|
2
|
-
import {
|
|
2
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
3
3
|
import { withWatchText } from "./stack-wait.js";
|
|
4
4
|
export async function stackRestack(stackId, cfg) {
|
|
5
5
|
const result = await restackStack(stackId, cfg);
|
|
@@ -13,10 +13,10 @@ export async function stackEnqueue(stackId, cfg, chatId) {
|
|
|
13
13
|
const resolved = cfg ?? (await loadConfig());
|
|
14
14
|
const result = await enqueueStack(stackId, resolved);
|
|
15
15
|
const watch = stackWatchObligation({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
16
|
-
const
|
|
16
|
+
const task = chatId ? await trackInChat(resolved, chatId, { kind: "stack", stackId }) : null;
|
|
17
17
|
const summary = withWatchText(watch, `Sent stack ${stackId} to the Mergestorm merge queue`);
|
|
18
18
|
return {
|
|
19
|
-
summary:
|
|
20
|
-
data: { stack_id: stackId, result, watch, ...(
|
|
19
|
+
summary: task ? `${summary} ${chatTaskNote(task)}` : summary,
|
|
20
|
+
data: { stack_id: stackId, result, watch, ...(task ? { background_task: task } : {}) },
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -2,6 +2,7 @@ import { adoptStack, CommandError, stackWatchObligation } from "mergestorm/clien
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { stackSetPatch } from "./stack-set.js";
|
|
4
4
|
import { withWatchText } from "./stack-wait.js";
|
|
5
|
+
import { withIssueReviewHint } from "./issue-review-wait.js";
|
|
5
6
|
export const stackAdoptSchema = {
|
|
6
7
|
owner: z.string().trim().min(1),
|
|
7
8
|
repo: z.string().trim().min(1),
|
|
@@ -38,7 +39,7 @@ export async function stackAdopt(input, cfg) {
|
|
|
38
39
|
catch (err) {
|
|
39
40
|
const message = err instanceof Error ? err.message : String(err);
|
|
40
41
|
return {
|
|
41
|
-
summary: message,
|
|
42
|
+
summary: withIssueReviewHint(err, message),
|
|
42
43
|
data: {
|
|
43
44
|
error: {
|
|
44
45
|
code: err instanceof CommandError ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
@@ -31,8 +31,8 @@ declare const publishInput: z.ZodObject<{
|
|
|
31
31
|
repo: z.ZodOptional<z.ZodString>;
|
|
32
32
|
branch: z.ZodOptional<z.ZodString>;
|
|
33
33
|
}, "strip", z.ZodTypeAny, {
|
|
34
|
-
owner?: string | undefined;
|
|
35
34
|
title?: string | undefined;
|
|
35
|
+
owner?: string | undefined;
|
|
36
36
|
body?: string | undefined;
|
|
37
37
|
repo?: string | undefined;
|
|
38
38
|
cwd?: string | undefined;
|
|
@@ -42,8 +42,8 @@ declare const publishInput: z.ZodObject<{
|
|
|
42
42
|
extend?: boolean | undefined;
|
|
43
43
|
replace_head?: string | undefined;
|
|
44
44
|
}, {
|
|
45
|
-
owner?: string | undefined;
|
|
46
45
|
title?: string | undefined;
|
|
46
|
+
owner?: string | undefined;
|
|
47
47
|
body?: string | undefined;
|
|
48
48
|
repo?: string | undefined;
|
|
49
49
|
cwd?: string | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CommandError, publishWorkingBranch, stackWatchObligation, workingBranchPublication, } from "mergestorm/client";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { withIssueReviewHint } from "./issue-review-wait.js";
|
|
3
4
|
import { withWatchText } from "./stack-wait.js";
|
|
4
5
|
const target = {
|
|
5
6
|
cwd: z.string().trim().min(1).optional(),
|
|
@@ -22,7 +23,7 @@ const statusInput = z.object(stackPublishStatusSchema);
|
|
|
22
23
|
function failure(err, fallback) {
|
|
23
24
|
const message = err instanceof Error ? err.message : String(err);
|
|
24
25
|
return {
|
|
25
|
-
summary: message,
|
|
26
|
+
summary: withIssueReviewHint(err, message),
|
|
26
27
|
data: {
|
|
27
28
|
error: {
|
|
28
29
|
code: err instanceof CommandError ? err.code ?? fallback : fallback,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pollStackWatch, type Config, type StackRepairHint, type StackWatchObligation } from "mergestorm/client";
|
|
1
|
+
import { pollStackWatch, type Config, type StackRepairHint, type StackWatchObligation, type StackWatchNextArgs } from "mergestorm/client";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { ToolPayload } from "./types.js";
|
|
4
4
|
export declare const stackWaitSchema: {
|
|
@@ -17,8 +17,26 @@ export type StackWaitInput = {
|
|
|
17
17
|
};
|
|
18
18
|
export declare function repairSummary(repair: StackRepairHint | null | undefined): string;
|
|
19
19
|
export declare function withWatchText(watch: StackWatchObligation, body: string): string;
|
|
20
|
+
export type HostedStackWatch = Omit<StackWatchObligation, "next"> & {
|
|
21
|
+
next: {
|
|
22
|
+
tool: "stack_wait";
|
|
23
|
+
args: StackWatchNextArgs;
|
|
24
|
+
} | null;
|
|
25
|
+
};
|
|
26
|
+
export declare function hostedStackWatch(watch: StackWatchObligation): HostedStackWatch;
|
|
27
|
+
type McpTextResult = {
|
|
28
|
+
isError?: boolean;
|
|
29
|
+
content: {
|
|
30
|
+
type: "text";
|
|
31
|
+
text: string;
|
|
32
|
+
}[];
|
|
33
|
+
structuredContent?: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
export declare function hostedToolResult<T extends McpTextResult>(result: T): T;
|
|
20
36
|
export declare function stackWait(input: StackWaitInput, cfg?: Config, deps?: {
|
|
21
37
|
pollStackWatch?: typeof pollStackWatch;
|
|
22
38
|
signal?: AbortSignal;
|
|
23
39
|
chatId?: string;
|
|
40
|
+
hosted?: boolean;
|
|
24
41
|
}): Promise<ToolPayload>;
|
|
42
|
+
export {};
|