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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CommandError, REPO_SETTINGS_KEYS, listRepoSettings, patchRepoSettings, } from "mergestorm/client";
|
|
2
|
+
const ARG_BY_KEY = {
|
|
3
|
+
auto_patch_enabled: "auto_patch",
|
|
4
|
+
auto_review_enabled: "auto_review",
|
|
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
|
+
};
|
|
11
|
+
function parseRepo(raw) {
|
|
12
|
+
const match = typeof raw === "string" ? /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(raw.trim()) : null;
|
|
13
|
+
if (!match)
|
|
14
|
+
throw new CommandError("repo takes <owner>/<repo>.", 2, "usage");
|
|
15
|
+
return { owner: match[1], repo: match[2] };
|
|
16
|
+
}
|
|
17
|
+
function words(value) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return "inherit";
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.length === 0 ? "none" : value.join(",");
|
|
22
|
+
if (typeof value === "number")
|
|
23
|
+
return String(value);
|
|
24
|
+
return value ? "on" : "off";
|
|
25
|
+
}
|
|
26
|
+
export function repoSettingsSummary(entry) {
|
|
27
|
+
return `${entry.owner}/${entry.repo}: ${REPO_SETTINGS_KEYS.map((key) => `${ARG_BY_KEY[key]} ${words(entry[key])}`).join(" · ")}`;
|
|
28
|
+
}
|
|
29
|
+
export function repoSettingsPatchFromArgs(args) {
|
|
30
|
+
const patch = {};
|
|
31
|
+
for (const key of REPO_SETTINGS_KEYS) {
|
|
32
|
+
const value = args[ARG_BY_KEY[key]];
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
if (key === "merge_queue_batch_size") {
|
|
36
|
+
if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
37
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes a whole number or null.`, 2, "usage");
|
|
38
|
+
}
|
|
39
|
+
patch[key] = value;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (key === "merge_queue_lands_alone_paths") {
|
|
43
|
+
if (value !== null && !(Array.isArray(value) && value.every((entry) => typeof entry === "string"))) {
|
|
44
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes a list of path patterns or null.`, 2, "usage");
|
|
45
|
+
}
|
|
46
|
+
patch[key] = value;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (value !== null && typeof value !== "boolean") {
|
|
50
|
+
throw new CommandError(`${ARG_BY_KEY[key]} takes true, false, or null.`, 2, "usage");
|
|
51
|
+
}
|
|
52
|
+
patch[key] = value;
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(patch).length === 0) {
|
|
55
|
+
throw new CommandError(`At least one of ${Object.values(ARG_BY_KEY).join(", ")} is required.`, 2, "usage");
|
|
56
|
+
}
|
|
57
|
+
return patch;
|
|
58
|
+
}
|
|
59
|
+
export async function repoSettingsGet(args, cfg) {
|
|
60
|
+
const wanted = args.repo === undefined ? null : parseRepo(args.repo);
|
|
61
|
+
const all = await listRepoSettings(cfg);
|
|
62
|
+
if (!wanted) {
|
|
63
|
+
return {
|
|
64
|
+
summary: all.length === 0 ? "No repositories are connected to this account." : all.map(repoSettingsSummary).join("\n"),
|
|
65
|
+
data: { repos: all },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const slug = `${wanted.owner}/${wanted.repo}`.toLowerCase();
|
|
69
|
+
const entry = all.find((candidate) => `${candidate.owner}/${candidate.repo}`.toLowerCase() === slug);
|
|
70
|
+
if (!entry)
|
|
71
|
+
throw new CommandError(`${wanted.owner}/${wanted.repo} is not connected to this account.`);
|
|
72
|
+
return { summary: repoSettingsSummary(entry), data: { ...entry } };
|
|
73
|
+
}
|
|
74
|
+
export async function repoSettingsSet(args, cfg) {
|
|
75
|
+
const { owner, repo } = parseRepo(args.repo);
|
|
76
|
+
const entry = await patchRepoSettings(owner, repo, repoSettingsPatchFromArgs(args), cfg);
|
|
77
|
+
return { summary: repoSettingsSummary(entry), data: { ...entry } };
|
|
78
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Config, type PrFindingDismissResult } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
export type ReviewDismissInput = {
|
|
4
|
+
owner: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
pr_number: number;
|
|
7
|
+
head_sha: string;
|
|
8
|
+
review_id: number;
|
|
9
|
+
finding_ids?: string[];
|
|
10
|
+
scope?: "findings" | "review";
|
|
11
|
+
reason?: string;
|
|
12
|
+
evidence_url?: string;
|
|
13
|
+
preview?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function reviewDismissSummary(result: PrFindingDismissResult): string;
|
|
16
|
+
export declare function reviewDismiss(input: ReviewDismissInput, cfg?: Config): Promise<ToolPayload>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dismissPrFindings, dismissThreadsLine, stackWatchObligation, } from "mergestorm/client";
|
|
2
|
+
import { withWatchText } from "./stack-wait.js";
|
|
3
|
+
function findingText(finding) {
|
|
4
|
+
return `${finding.finding_id} (${finding.path ?? "review body"}: ${finding.title})`;
|
|
5
|
+
}
|
|
6
|
+
export function reviewDismissSummary(result) {
|
|
7
|
+
const pr = `${result.owner}/${result.repo}#${result.pr_number}`;
|
|
8
|
+
const kind = result.review_kind === "seam" ? "seam review" : "Vortex review";
|
|
9
|
+
const head = `${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}`;
|
|
10
|
+
const lines = [];
|
|
11
|
+
if (result.status === "preview") {
|
|
12
|
+
lines.push(`${pr} ${head}: ${result.remaining.length} open finding(s). Nothing was written.`);
|
|
13
|
+
for (const finding of result.remaining)
|
|
14
|
+
lines.push(`- ${findingText(finding)}`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
lines.push(`${pr} ${head}: dismissed ${result.dismissed.length}, already dismissed ${result.already_dismissed.length}, still open ${result.remaining.length}.`);
|
|
18
|
+
for (const finding of result.remaining)
|
|
19
|
+
lines.push(`- open: ${findingText(finding)}`);
|
|
20
|
+
const threads = dismissThreadsLine(result);
|
|
21
|
+
if (threads)
|
|
22
|
+
lines.push(threads);
|
|
23
|
+
}
|
|
24
|
+
const seam = result.gate.seam;
|
|
25
|
+
if (seam) {
|
|
26
|
+
lines.push(seam.cleared
|
|
27
|
+
? "Seam gate cleared at this head. CI, other reviews and Auto land policy are unchanged."
|
|
28
|
+
: seam.blocking
|
|
29
|
+
? `Seam gate still blocking (seam_state=${seam.state}).`
|
|
30
|
+
: `Seam gate not blocking (seam_state=${seam.state}).`);
|
|
31
|
+
}
|
|
32
|
+
const vortex = result.gate.vortex;
|
|
33
|
+
if (vortex?.cleared) {
|
|
34
|
+
lines.push("Vortex gate cleared at this head: every Vortex Blocker (error finding) at this head is dismissed. CI and Auto land policy are unchanged.");
|
|
35
|
+
}
|
|
36
|
+
else if (vortex?.message && vortex.reason !== "review_findings_open") {
|
|
37
|
+
lines.push(`Vortex gate not cleared: ${vortex.message}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
export async function reviewDismiss(input, cfg) {
|
|
42
|
+
const request = {
|
|
43
|
+
owner: input.owner,
|
|
44
|
+
repo: input.repo,
|
|
45
|
+
prNumber: input.pr_number,
|
|
46
|
+
headSha: input.head_sha,
|
|
47
|
+
reviewId: input.review_id,
|
|
48
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
49
|
+
...(input.finding_ids ? { findingIds: input.finding_ids } : {}),
|
|
50
|
+
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
51
|
+
...(input.evidence_url ? { evidenceUrl: input.evidence_url } : {}),
|
|
52
|
+
...(input.preview ? { preview: true } : {}),
|
|
53
|
+
};
|
|
54
|
+
const outcome = await dismissPrFindings(request, cfg);
|
|
55
|
+
if (!outcome.ok) {
|
|
56
|
+
return {
|
|
57
|
+
summary: `review_dismiss refused (${outcome.error}): ${outcome.message}`,
|
|
58
|
+
isError: true,
|
|
59
|
+
data: { error: { code: outcome.error, message: outcome.message, status: outcome.status } },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const result = outcome.result;
|
|
63
|
+
const summary = reviewDismissSummary(result);
|
|
64
|
+
if (!result.stack_id)
|
|
65
|
+
return { summary, data: { ...result } };
|
|
66
|
+
const watch = stackWatchObligation({ stackId: result.stack_id, terminal: null, unread: true, freshCursor: true });
|
|
67
|
+
return { summary: withWatchText(watch, summary), data: { ...result, watch } };
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Config, type PrVortexReview } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
export declare function prReviewTarget(owner: string, repo: string, prNumber: number): {
|
|
4
|
+
owner: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
prNumber: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function prReviewSummary(owner: string, repo: string, prNumber: number, envelope: {
|
|
9
|
+
status: string;
|
|
10
|
+
head_sha?: string | null;
|
|
11
|
+
finding_count?: number | null;
|
|
12
|
+
phase?: string | null;
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function payloadFromPrReview(owner: string, repo: string, prNumber: number, envelope: PrVortexReview): ToolPayload;
|
|
15
|
+
export declare function payloadFromPrRateLimit(err: unknown, owner: string, repo: string, prNumber: number): ToolPayload | null;
|
|
16
|
+
/** SHA + pass selection (#2027): `pass` is one exact attempt, `afterPass` a later one on the head. */
|
|
17
|
+
export type ReviewGetPrOptions = {
|
|
18
|
+
afterSha?: string;
|
|
19
|
+
pass?: number;
|
|
20
|
+
afterPass?: number;
|
|
21
|
+
};
|
|
22
|
+
export declare function reviewGetPr(owner: string, repo: string, prNumber: number, cfg?: Config, opts?: ReviewGetPrOptions): Promise<ToolPayload>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CommandError, getPrVortexReview, isCommandErrorCode, } from "mergestorm/client";
|
|
2
|
+
export function prReviewTarget(owner, repo, prNumber) {
|
|
3
|
+
const cleanOwner = owner.trim();
|
|
4
|
+
const cleanRepo = repo.trim();
|
|
5
|
+
if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
|
|
6
|
+
throw new CommandError("owner, repo, and a positive pr_number are required", 2, "usage");
|
|
7
|
+
}
|
|
8
|
+
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
9
|
+
}
|
|
10
|
+
export function prReviewSummary(owner, repo, prNumber, envelope) {
|
|
11
|
+
const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
|
|
12
|
+
const sha = envelope.head_sha?.trim();
|
|
13
|
+
if (sha)
|
|
14
|
+
bits.push(`sha ${sha.slice(0, 7)}`);
|
|
15
|
+
if (typeof envelope.finding_count === "number") {
|
|
16
|
+
bits.push(`${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`);
|
|
17
|
+
}
|
|
18
|
+
if (envelope.phase)
|
|
19
|
+
bits.push(String(envelope.phase));
|
|
20
|
+
return bits.join(" · ");
|
|
21
|
+
}
|
|
22
|
+
export function payloadFromPrReview(owner, repo, prNumber, envelope) {
|
|
23
|
+
return {
|
|
24
|
+
summary: prReviewSummary(owner, repo, prNumber, envelope),
|
|
25
|
+
data: { ...envelope },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
29
|
+
if (!isCommandErrorCode(err, "rate_limited"))
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
summary: `${owner}/${repo}#${prNumber} · rate_limited`,
|
|
33
|
+
data: {
|
|
34
|
+
status: "rate_limited",
|
|
35
|
+
error: err.message,
|
|
36
|
+
retry_after_seconds: err.retryAfterSeconds,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
41
|
+
const target = prReviewTarget(owner, repo, prNumber);
|
|
42
|
+
try {
|
|
43
|
+
const envelope = await getPrVortexReview(target.owner, target.repo, target.prNumber, cfg, { afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass });
|
|
44
|
+
return payloadFromPrReview(target.owner, target.repo, target.prNumber, envelope);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const rateLimited = payloadFromPrRateLimit(err, target.owner, target.repo, target.prNumber);
|
|
48
|
+
if (rateLimited)
|
|
49
|
+
return rateLimited;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CommandError, getReview, toReviewJobEnvelope, } from "mergestorm/client";
|
|
2
|
+
import { payloadFromRateLimit } from "./envelope.js";
|
|
3
|
+
export async function reviewGet(jobId, cfg) {
|
|
4
|
+
let row;
|
|
5
|
+
try {
|
|
6
|
+
row = await getReview(jobId, cfg);
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
const rateLimited = payloadFromRateLimit(err, jobId);
|
|
10
|
+
if (rateLimited)
|
|
11
|
+
return rateLimited;
|
|
12
|
+
throw err;
|
|
13
|
+
}
|
|
14
|
+
if (!row ||
|
|
15
|
+
typeof row !== "object" ||
|
|
16
|
+
typeof row.status !== "string") {
|
|
17
|
+
throw new CommandError(`Unexpected response for review ${jobId}.`);
|
|
18
|
+
}
|
|
19
|
+
const envelope = toReviewJobEnvelope(row, { jobId });
|
|
20
|
+
const bits = [envelope.status];
|
|
21
|
+
if (envelope.verdict)
|
|
22
|
+
bits.push(envelope.verdict);
|
|
23
|
+
return {
|
|
24
|
+
summary: `${envelope.job_id ?? jobId} · ${bits.join(" · ")}`,
|
|
25
|
+
data: envelope,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { listJobs, toReviewJobEnvelope } from "mergestorm/client";
|
|
2
|
+
export async function reviewList(limit = 10, cfg) {
|
|
3
|
+
const items = await listJobs(limit, cfg);
|
|
4
|
+
const envelopes = items.map((row) => toReviewJobEnvelope(row));
|
|
5
|
+
const n = envelopes.length;
|
|
6
|
+
return {
|
|
7
|
+
summary: n === 0 ? "No review jobs" : `${n} review job${n === 1 ? "" : "s"}`,
|
|
8
|
+
data: { items: envelopes },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Config } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
export type PullRequestInput = {
|
|
4
|
+
owner: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
pr_number: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function reviewStartPr(input: PullRequestInput & {
|
|
9
|
+
head_sha?: string;
|
|
10
|
+
}, cfg?: Config, chatId?: string | null): Promise<ToolPayload>;
|
|
11
|
+
export declare function reviewStopPr(input: PullRequestInput, cfg?: Config): Promise<ToolPayload>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { agentRequest } from "./agent-request.js";
|
|
2
|
+
import { chatTaskNote, chatTaskOf } from "./chat-task.js";
|
|
3
|
+
export async function reviewStartPr(input, cfg, chatId) {
|
|
4
|
+
const payload = await agentRequest("/api/v1/agents/vortex/review", {
|
|
5
|
+
method: "POST",
|
|
6
|
+
json: {
|
|
7
|
+
owner: input.owner,
|
|
8
|
+
repo: input.repo,
|
|
9
|
+
pr_number: input.pr_number,
|
|
10
|
+
...(input.head_sha ? { head_sha: input.head_sha } : {}),
|
|
11
|
+
...(chatId ? { chat_id: chatId } : {}),
|
|
12
|
+
},
|
|
13
|
+
}, cfg);
|
|
14
|
+
const task = chatTaskOf(payload.data.background_task);
|
|
15
|
+
return task ? { ...payload, summary: `${payload.summary} ${chatTaskNote(task)}` } : payload;
|
|
16
|
+
}
|
|
17
|
+
export async function reviewStopPr(input, cfg) {
|
|
18
|
+
return agentRequest("/api/v1/agents/vortex/stop", {
|
|
19
|
+
method: "POST",
|
|
20
|
+
json: { owner: input.owner, repo: input.repo, pr_number: input.pr_number },
|
|
21
|
+
}, cfg);
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Config } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
/** Hard maximum for an explicitly requested MCP poll slice. */
|
|
4
|
+
export declare const MCP_WAIT_CEILING_S = 300;
|
|
5
|
+
/**
|
|
6
|
+
* Default bound for local and PR review waits. Reviews can take minutes or hours;
|
|
7
|
+
* Cursor kills a single MCP call around a minute, so this is one poll
|
|
8
|
+
* slice. The skill loops until the review finishes.
|
|
9
|
+
*/
|
|
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
|
+
export type ReviewSubmitInput = {
|
|
19
|
+
cwd?: string;
|
|
20
|
+
base?: string;
|
|
21
|
+
head?: string;
|
|
22
|
+
diff?: string;
|
|
23
|
+
router?: string;
|
|
24
|
+
specialists?: string[];
|
|
25
|
+
context?: string;
|
|
26
|
+
context_files?: string[];
|
|
27
|
+
thread?: string;
|
|
28
|
+
idempotency_key?: string;
|
|
29
|
+
wait?: boolean;
|
|
30
|
+
timeout_s?: number;
|
|
31
|
+
};
|
|
32
|
+
export type ReviewSubmitOptions = {
|
|
33
|
+
pollIntervalMs?: number;
|
|
34
|
+
};
|
|
35
|
+
export declare function waitTimeoutMs(timeout_s?: number, defaultSeconds?: number): number;
|
|
36
|
+
export declare function reviewSubmit(input: ReviewSubmitInput, cfg?: Config, opts?: ReviewSubmitOptions): Promise<ToolPayload>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { relative, resolve, sep } from "node:path";
|
|
4
|
+
import { CommandError, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, discoverTrunk, formatReviewSubmitError, loadConfig, loadReviewContext, pollReview, rateLimitedMessage, submitReview, } from "mergestorm/client";
|
|
5
|
+
import { payloadFromRateLimit, payloadFromRow } from "./envelope.js";
|
|
6
|
+
/** Hard maximum for an explicitly requested MCP poll slice. */
|
|
7
|
+
export const MCP_WAIT_CEILING_S = 300;
|
|
8
|
+
/**
|
|
9
|
+
* Default bound for local and PR review waits. Reviews can take minutes or hours;
|
|
10
|
+
* Cursor kills a single MCP call around a minute, so this is one poll
|
|
11
|
+
* slice. The skill loops until the review finishes.
|
|
12
|
+
*/
|
|
13
|
+
export const MCP_PR_WAIT_DEFAULT_S = 45;
|
|
14
|
+
/** Pinnable L0 lanes. `governance` and `seam` are L1 and cannot be requested. */
|
|
15
|
+
const PINNABLE_SPECIALISTS = [
|
|
16
|
+
"security",
|
|
17
|
+
"performance",
|
|
18
|
+
"architecture",
|
|
19
|
+
"tests",
|
|
20
|
+
"data",
|
|
21
|
+
"api",
|
|
22
|
+
"frontend",
|
|
23
|
+
];
|
|
24
|
+
export function assertPinnableSpecialists(specialists) {
|
|
25
|
+
if (!specialists?.length)
|
|
26
|
+
return;
|
|
27
|
+
const unknown = specialists.filter((id) => !PINNABLE_SPECIALISTS.includes(id));
|
|
28
|
+
if (unknown.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
throw new CommandError(`Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`, 2, "usage");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Host-configured root that agent-chosen `context_files` may not escape.
|
|
34
|
+
* The `cwd` tool argument is agent-controlled, so it cannot serve as the
|
|
35
|
+
* sandbox root; default to the MCP server's own working directory.
|
|
36
|
+
*/
|
|
37
|
+
export function mcpSandboxRoot() {
|
|
38
|
+
return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
|
|
39
|
+
}
|
|
40
|
+
export function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
|
|
41
|
+
const seconds = timeout_s ?? defaultSeconds;
|
|
42
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
43
|
+
throw new CommandError("timeout_s must be a positive number of seconds", 2, "usage");
|
|
44
|
+
}
|
|
45
|
+
return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1_000));
|
|
46
|
+
}
|
|
47
|
+
async function resolveInput(input) {
|
|
48
|
+
const cwd = input.cwd ?? process.cwd();
|
|
49
|
+
const head = input.head?.trim() || "HEAD";
|
|
50
|
+
if (input.diff?.trim()) {
|
|
51
|
+
const branch = "local";
|
|
52
|
+
const thread = input.thread?.trim() ||
|
|
53
|
+
`local/diff-${createHash("sha256").update(input.diff).digest("hex").slice(0, 12)}`;
|
|
54
|
+
return {
|
|
55
|
+
thread,
|
|
56
|
+
branch,
|
|
57
|
+
baseLabel: input.base?.trim() ||
|
|
58
|
+
(() => {
|
|
59
|
+
try {
|
|
60
|
+
return discoverTrunk(cwd);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return "main";
|
|
64
|
+
}
|
|
65
|
+
})(),
|
|
66
|
+
headLabel: head,
|
|
67
|
+
diff: input.diff,
|
|
68
|
+
files: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const sandboxRoot = mcpSandboxRoot();
|
|
72
|
+
try {
|
|
73
|
+
const [sandboxReal, cwdReal] = await Promise.all([
|
|
74
|
+
realpath(resolve(sandboxRoot)),
|
|
75
|
+
realpath(resolve(cwd)),
|
|
76
|
+
]);
|
|
77
|
+
const relToSandbox = relative(sandboxReal, cwdReal);
|
|
78
|
+
if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
|
|
79
|
+
throw new CommandError("cwd must be inside the configured sandbox root", 2, "usage");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err instanceof CommandError)
|
|
84
|
+
throw err;
|
|
85
|
+
throw new CommandError("cwd must be inside the configured sandbox root", 2, "usage");
|
|
86
|
+
}
|
|
87
|
+
const base = input.base?.trim() || discoverTrunk(cwd);
|
|
88
|
+
return collectReviewInput(base, head, cwd, sandboxRoot);
|
|
89
|
+
}
|
|
90
|
+
export async function reviewSubmit(input, cfg, opts = {}) {
|
|
91
|
+
const resolved = cfg ?? (await loadConfig());
|
|
92
|
+
const wait = input.wait === true;
|
|
93
|
+
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
94
|
+
assertPinnableSpecialists(input.specialists);
|
|
95
|
+
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
96
|
+
throw new CommandError("thread must be 1–120 chars [A-Za-z0-9._/-]", 2, "usage");
|
|
97
|
+
}
|
|
98
|
+
if (input.context === "-") {
|
|
99
|
+
throw new CommandError('context "-" (stdin) is not available over MCP; pass the text directly', 2, "usage");
|
|
100
|
+
}
|
|
101
|
+
const collected = await resolveInput(input);
|
|
102
|
+
if (!collected) {
|
|
103
|
+
return payloadFromRow(null, { status: "no_changes" });
|
|
104
|
+
}
|
|
105
|
+
if (input.thread)
|
|
106
|
+
collected.thread = input.thread;
|
|
107
|
+
const extras = await loadReviewContext({
|
|
108
|
+
context: input.context,
|
|
109
|
+
contextFiles: input.context_files ?? [],
|
|
110
|
+
cwd: input.cwd ?? process.cwd(),
|
|
111
|
+
sandboxCwd: mcpSandboxRoot(),
|
|
112
|
+
});
|
|
113
|
+
const submitted = await submitReview(resolved, collected, {
|
|
114
|
+
routerMode: input.router,
|
|
115
|
+
specialists: input.specialists,
|
|
116
|
+
context: extras.context,
|
|
117
|
+
contextFiles: extras.contextFiles,
|
|
118
|
+
idempotencyKey: input.idempotency_key,
|
|
119
|
+
});
|
|
120
|
+
const jobId = submitted.row.job_id ?? submitted.row.id ?? null;
|
|
121
|
+
if (submitted.status === 402) {
|
|
122
|
+
return payloadFromRow(submitted.row, {
|
|
123
|
+
jobId,
|
|
124
|
+
status: "quota_exceeded",
|
|
125
|
+
error: "Review limit reached for this period.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (submitted.status === 429) {
|
|
129
|
+
return payloadFromRow(submitted.row, {
|
|
130
|
+
jobId,
|
|
131
|
+
status: "rate_limited",
|
|
132
|
+
error: rateLimitedMessage(submitted.retryAfterSeconds),
|
|
133
|
+
retryAfterSeconds: submitted.retryAfterSeconds,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (submitted.status !== 202 && submitted.status !== 200) {
|
|
137
|
+
return {
|
|
138
|
+
...payloadFromRow({ ...submitted.row, status: "failed" }, {
|
|
139
|
+
jobId,
|
|
140
|
+
error: formatReviewSubmitError(submitted.status, submitted.row),
|
|
141
|
+
}),
|
|
142
|
+
isError: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (!jobId) {
|
|
146
|
+
throw new CommandError("Review API returned no job_id");
|
|
147
|
+
}
|
|
148
|
+
if (!wait) {
|
|
149
|
+
return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
|
|
150
|
+
}
|
|
151
|
+
let row;
|
|
152
|
+
try {
|
|
153
|
+
row = await pollReview(resolved, jobId, {
|
|
154
|
+
timeoutMs,
|
|
155
|
+
intervalMs: opts.pollIntervalMs,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (err instanceof ReviewPollTimeoutError) {
|
|
160
|
+
return payloadFromRow(err.lastRow, {
|
|
161
|
+
jobId,
|
|
162
|
+
status: err.lastRow.status ?? "in_progress",
|
|
163
|
+
error: "Timed out waiting for review.",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const rateLimited = payloadFromRateLimit(err, jobId);
|
|
167
|
+
if (rateLimited)
|
|
168
|
+
return rateLimited;
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
return payloadFromRow(row, { jobId });
|
|
172
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Config } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
export type ReviewWaitPrOptions = {
|
|
4
|
+
pollIntervalMs?: number;
|
|
5
|
+
/** Exact pass on the head (#2027). */
|
|
6
|
+
pass?: number;
|
|
7
|
+
/** Only a pass later than this one on the head; fixed for the whole wait (#2027). */
|
|
8
|
+
afterPass?: number;
|
|
9
|
+
chatId?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function reviewWaitPr(owner: string, repo: string, prNumber: number, afterSha?: string, timeout_s?: number, cfg?: Config, opts?: ReviewWaitPrOptions): Promise<ToolPayload>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PrReviewPollTimeoutError, loadConfig, noPrReviewYetMessage, pollPrVortexReview, } from "mergestorm/client";
|
|
2
|
+
import { payloadFromPrRateLimit, payloadFromPrReview, prReviewSummary, prReviewTarget, reviewGetPr, } from "./review-get-pr.js";
|
|
3
|
+
import { MCP_PR_WAIT_DEFAULT_S, waitTimeoutMs } from "./review-submit.js";
|
|
4
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
5
|
+
export async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
6
|
+
const target = prReviewTarget(owner, repo, prNumber);
|
|
7
|
+
const resolved = cfg ?? (await loadConfig());
|
|
8
|
+
if (timeout_s === 0) {
|
|
9
|
+
return reviewGetPr(target.owner, target.repo, target.prNumber, resolved, {
|
|
10
|
+
afterSha,
|
|
11
|
+
pass: opts.pass,
|
|
12
|
+
afterPass: opts.afterPass,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const envelope = await pollPrVortexReview(resolved, target.owner, target.repo, target.prNumber, {
|
|
17
|
+
timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
|
|
18
|
+
afterSha,
|
|
19
|
+
pass: opts.pass,
|
|
20
|
+
afterPass: opts.afterPass,
|
|
21
|
+
intervalMs: opts.pollIntervalMs,
|
|
22
|
+
});
|
|
23
|
+
return payloadFromPrReview(target.owner, target.repo, target.prNumber, envelope);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (err instanceof PrReviewPollTimeoutError) {
|
|
27
|
+
const status = err.reviewSeen ? "in_progress" : "none";
|
|
28
|
+
const expectedSha = afterSha?.trim().toLowerCase() ?? "";
|
|
29
|
+
const observedSha = err.lastEnvelope?.head_sha?.trim().toLowerCase() ?? "";
|
|
30
|
+
const headShaMatches = Boolean(observedSha &&
|
|
31
|
+
expectedSha.length >= 7 &&
|
|
32
|
+
(observedSha.startsWith(expectedSha) || expectedSha.startsWith(observedSha)));
|
|
33
|
+
const findingCount = headShaMatches
|
|
34
|
+
? err.lastEnvelope?.finding_count ?? null
|
|
35
|
+
: null;
|
|
36
|
+
const summary = prReviewSummary(target.owner, target.repo, target.prNumber, {
|
|
37
|
+
status,
|
|
38
|
+
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
39
|
+
finding_count: findingCount,
|
|
40
|
+
phase: err.lastEnvelope?.phase ?? null,
|
|
41
|
+
});
|
|
42
|
+
const task = opts.chatId
|
|
43
|
+
? await trackInChat(resolved, opts.chatId, { kind: "review", owner: target.owner, repo: target.repo, prNumber: target.prNumber })
|
|
44
|
+
: null;
|
|
45
|
+
const shown = err.reviewSeen
|
|
46
|
+
? summary
|
|
47
|
+
: `${summary} · ${noPrReviewYetMessage(target.owner, target.repo, target.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`;
|
|
48
|
+
return {
|
|
49
|
+
summary: task ? `${shown} · ${chatTaskNote(task)}` : shown,
|
|
50
|
+
data: {
|
|
51
|
+
status,
|
|
52
|
+
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
53
|
+
pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
|
|
54
|
+
finding_count: findingCount,
|
|
55
|
+
phase: err.lastEnvelope?.phase ?? null,
|
|
56
|
+
error: err.message,
|
|
57
|
+
...(task ? { background_task: task } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const rateLimited = payloadFromPrRateLimit(err, target.owner, target.repo, target.prNumber);
|
|
62
|
+
if (rateLimited)
|
|
63
|
+
return rateLimited;
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Config } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
export type ReviewWaitOptions = {
|
|
4
|
+
pollIntervalMs?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function reviewWait(jobId: string, timeout_s?: number, cfg?: Config, opts?: ReviewWaitOptions): Promise<ToolPayload>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CommandError, ReviewPollTimeoutError, loadConfig, pollReview, } from "mergestorm/client";
|
|
2
|
+
import { payloadFromRateLimit, payloadFromRow } from "./envelope.js";
|
|
3
|
+
import { reviewGet } from "./review-get.js";
|
|
4
|
+
import { waitTimeoutMs } from "./review-submit.js";
|
|
5
|
+
export async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
6
|
+
const resolved = cfg ?? (await loadConfig());
|
|
7
|
+
const id = jobId.trim();
|
|
8
|
+
if (!id) {
|
|
9
|
+
throw new CommandError("job_id is required", 2, "usage");
|
|
10
|
+
}
|
|
11
|
+
if (timeout_s === 0)
|
|
12
|
+
return reviewGet(id, resolved);
|
|
13
|
+
try {
|
|
14
|
+
const row = await pollReview(resolved, id, {
|
|
15
|
+
timeoutMs: waitTimeoutMs(timeout_s),
|
|
16
|
+
intervalMs: opts.pollIntervalMs,
|
|
17
|
+
});
|
|
18
|
+
return payloadFromRow(row, { jobId: id });
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err instanceof ReviewPollTimeoutError) {
|
|
22
|
+
return payloadFromRow(err.lastRow, {
|
|
23
|
+
jobId: id,
|
|
24
|
+
status: err.lastRow.status ?? "in_progress",
|
|
25
|
+
error: "Timed out waiting for review.",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const rateLimited = payloadFromRateLimit(err, id);
|
|
29
|
+
if (rateLimited)
|
|
30
|
+
return rateLimited;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Config, type SettingsResponse } from "mergestorm/client";
|
|
2
|
+
import type { ToolPayload } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* One line an agent can act on without opening the data: is Cyclone allowed
|
|
5
|
+
* to patch, and is it even connected.
|
|
6
|
+
*/
|
|
7
|
+
export declare function settingsSummary(settings: SettingsResponse): string;
|
|
8
|
+
export declare function settingsGet(cfg?: Config): Promise<ToolPayload>;
|