mergestorm-mcp 0.2.1 → 0.2.2
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 +13 -3
- package/dist/index.js +201 -113
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# mergestorm-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
|
|
3
|
+
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_submit`, `review_wait`, `review_wait_pr`, `review_dismiss`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
|
|
4
4
|
|
|
5
|
-
Every tool declares MCP `annotations`. The reads carry `readOnlyHint: true`; `review_submit`, `stack_adopt`, `stack_set`, and `settings_set` carry `readOnlyHint: false` and `destructiveHint: false`.
|
|
5
|
+
Every tool declares MCP `annotations`. The reads carry `readOnlyHint: true`; `review_submit`, `review_dismiss`, `stack_adopt`, `stack_set`, and `settings_set` carry `readOnlyHint: false` and `destructiveHint: false`.
|
|
6
6
|
|
|
7
7
|
`review_wait({ job_id, timeout_s? })` reads a local review job by the `job_id` that `review_submit` returned. `review_wait_pr({ owner, repo, pr_number, after_sha?, pass?, after_pass?, timeout_s? })` reads the Vortex pass on a GitHub PR. Both poll one 45s slice by default; `timeout_s: 0` reads once without polling.
|
|
8
8
|
|
|
9
|
+
`review_dismiss({ owner, repo, pr_number, head_sha, review_id, finding_ids?, scope?, reason?, evidence_url?, preview? })` records an audited dismissal of Vortex findings the agent verified are wrong. The rules:
|
|
10
|
+
|
|
11
|
+
- **Exact identity.** `head_sha` is the PR's live head and `review_id` is a Vortex review (Core or seam) made at that head. `finding_ids` are that review's GitHub review comment ids (`offdiff-<n>` for a finding listed only in the review body, `review` for a body-only review). `scope: "review"` with no `finding_ids` covers every finding of the review.
|
|
12
|
+
- **Preview first.** `preview: true` lists the review's finding ids and the seam gate, and writes nothing.
|
|
13
|
+
- **Authorization.** The API key's account must monitor the repository or own the PR's stack, and its linked GitHub account needs write access to the repository.
|
|
14
|
+
- **Refusals.** A moved head (`stale_head`), another review (`review_mismatch`, `review_not_found`), an id outside that review (`unknown_finding`), a thin `reason` (`invalid_input`) or a missing permission (`forbidden`) is refused, and nothing is written.
|
|
15
|
+
- **Audit and retries.** Each finding is stored with the actor's GitHub login, the reason, `evidence_url`, the scope and the time. A retry reports the earlier record under `already_dismissed`.
|
|
16
|
+
- **Gates.** `gate.seam.cleared` is true only when every finding of that integration review is dismissed and it is still the member's current seam verdict. CI, other reviews and Auto land policy are unchanged (`gate.other_gates: "unchanged"`). For a stack PR the result carries `watch` with the next `stack_wait` call.
|
|
17
|
+
- **No resurfacing.** Vortex does not raise a dismissed finding again on the same diff: the same head, or a restack whose diff is unchanged.
|
|
18
|
+
|
|
9
19
|
The stack tools expose reads plus one scoped policy write:
|
|
10
20
|
|
|
11
21
|
- `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review` and `auto_patch` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership.
|
|
12
22
|
- `stack_list` returns the current API key owner's registered stacks.
|
|
13
23
|
- `stack_set({ stack_id, auto_land?, auto_review?, auto_patch? })` sets per-stack policy on one owned stack. `auto_land` flips Auto land. `auto_review` and `auto_patch` pin Vortex auto-review or Cyclone auto-patch for that stack in either direction (`true` or `false`); `null` clears the pin so the stack follows the account setting. Pass at least one key. Account settings are never changed here.
|
|
14
24
|
- `stack_status({ stack_id })` returns one owned 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. It returns a structured `stack_not_found` or `rate_limited` error.
|
|
15
|
-
- `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots. `timeout_s: 0` returns one snapshot; a positive timeout after a snapshot with no attention returns the current `waiting` or `in_progress` status with selectors to use for the next call. An unread deadline returns `failed`. While Cyclone or Vortex is still working on the blocked PR, `stack_wait` and `stack_status` return `in_progress` instead of `attention`, with `busy[]` naming the PR, blocker, and agent, and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and busy flags. A nonempty `busy[]` means wait. A queued Vortex run holds for at most 15 minutes, `seam_pending` alone does not hold, and reviewing or patching holds until the run finishes or its lease or check run is cleared. A
|
|
25
|
+
- `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots. `timeout_s: 0` returns one snapshot; a positive timeout after a snapshot with no attention returns the current `waiting` or `in_progress` status with selectors to use for the next call. An unread deadline returns `failed`. While Cyclone or Vortex is still working on the blocked PR, `stack_wait` and `stack_status` return `in_progress` instead of `attention`, with `busy[]` naming the PR, blocker, and agent, and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and busy flags. A nonempty `busy[]` means wait. A queued Vortex run holds for at most 15 minutes, `seam_pending` alone does not hold, and reviewing or patching holds until the run finishes or its lease or check run is cleared. A blocker no agent run clears (a merge conflict vs the live parent, a restack `Conflict` or `Restack failed`, a Draft PR) is still named during that `in_progress`, in `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"` and `waitingOn` listing the busy agents; act when a snapshot after the agents finish returns `attention`, re-reading the blocker then. `repair` is included when a concrete repair hint is available. Blockers the agents can change keep `blocker` and `actAfter` null. If a hold looks stuck, refresh `stack_status` and tell the human.
|
|
16
26
|
|
|
17
27
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
18
28
|
|
package/dist/index.js
CHANGED
|
@@ -162,6 +162,153 @@ async function queueStatus(stackId, cfg) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// src/tools/review-dismiss.ts
|
|
166
|
+
import {
|
|
167
|
+
dismissPrFindings,
|
|
168
|
+
stackWatchObligation as stackWatchObligation2
|
|
169
|
+
} from "mergestorm/client";
|
|
170
|
+
|
|
171
|
+
// src/tools/stack-wait.ts
|
|
172
|
+
import {
|
|
173
|
+
loadConfig as loadConfig3,
|
|
174
|
+
stackBlockersSummary,
|
|
175
|
+
pollStackWatch,
|
|
176
|
+
StackWatchError,
|
|
177
|
+
StackWatchTimeoutError,
|
|
178
|
+
stackWatchObligation
|
|
179
|
+
} from "mergestorm/client";
|
|
180
|
+
import { z } from "zod";
|
|
181
|
+
var stackWaitSchema = {
|
|
182
|
+
stack_id: z.string().trim().min(1),
|
|
183
|
+
timeout_s: z.number().finite().min(0).max(45).default(45),
|
|
184
|
+
enrolled_head_sha: z.string().nullable().optional(),
|
|
185
|
+
after_finished_at: z.string().nullable().optional(),
|
|
186
|
+
bounce_id: z.string().nullable().optional()
|
|
187
|
+
};
|
|
188
|
+
function repairSummary(repair) {
|
|
189
|
+
if (!repair) return "";
|
|
190
|
+
const files = "files" in repair && repair.files.length ? ` Files: ${repair.files.join(", ")}.` : "";
|
|
191
|
+
return `
|
|
192
|
+
Repair #${repair.prNumber} (${repair.kind}).${files} ${repair.steps}`;
|
|
193
|
+
}
|
|
194
|
+
function withWatchText(watch, body) {
|
|
195
|
+
return `${watch.message}
|
|
196
|
+
|
|
197
|
+
${body}`;
|
|
198
|
+
}
|
|
199
|
+
function envelopeWatch(envelope) {
|
|
200
|
+
return envelope.watch ?? stackWatchObligation({
|
|
201
|
+
stackId: envelope.stackId,
|
|
202
|
+
terminal: null,
|
|
203
|
+
cursor: envelope.cursor,
|
|
204
|
+
status: envelope.status
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function stackWaitSummary(envelope) {
|
|
208
|
+
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope)}${repairSummary(envelope.repair)}`);
|
|
209
|
+
}
|
|
210
|
+
function stackWaitStatusLine(envelope) {
|
|
211
|
+
const base = `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending, envelope.actAfter ?? null)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`;
|
|
212
|
+
if (envelopeWatch(envelope).done || envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
|
|
213
|
+
const selectors = [
|
|
214
|
+
`stack_id ${JSON.stringify(envelope.cursor.stackId)}`,
|
|
215
|
+
`enrolled_head_sha ${JSON.stringify(envelope.cursor.enrolledHeadSha)}`,
|
|
216
|
+
...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
|
|
217
|
+
...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
|
|
218
|
+
];
|
|
219
|
+
return `${base} \xB7 watch not finished: call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
220
|
+
}
|
|
221
|
+
async function stackWait(input, cfg, deps = {}) {
|
|
222
|
+
const args = z.object(stackWaitSchema).parse(input);
|
|
223
|
+
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
224
|
+
stackId: args.stack_id,
|
|
225
|
+
enrolledHeadSha: args.enrolled_head_sha ?? null,
|
|
226
|
+
...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
|
|
227
|
+
...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
|
|
228
|
+
} : void 0;
|
|
229
|
+
let envelope;
|
|
230
|
+
try {
|
|
231
|
+
envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig3(), args.stack_id, {
|
|
232
|
+
timeoutMs: args.timeout_s * 1e3,
|
|
233
|
+
cursor,
|
|
234
|
+
signal: deps.signal
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err instanceof StackWatchError) {
|
|
238
|
+
const envelope2 = { ...err.lastEnvelope, watch: envelopeWatch(err.lastEnvelope) };
|
|
239
|
+
return {
|
|
240
|
+
summary: stackWaitSummary(envelope2),
|
|
241
|
+
data: {
|
|
242
|
+
...envelope2,
|
|
243
|
+
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
244
|
+
},
|
|
245
|
+
isError: !envelope2.watch.done
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (!(err instanceof StackWatchTimeoutError)) throw err;
|
|
249
|
+
envelope = err.lastEnvelope;
|
|
250
|
+
}
|
|
251
|
+
const sealed = { ...envelope, watch: envelopeWatch(envelope) };
|
|
252
|
+
return {
|
|
253
|
+
summary: stackWaitSummary(sealed),
|
|
254
|
+
data: sealed
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/tools/review-dismiss.ts
|
|
259
|
+
function findingText(finding) {
|
|
260
|
+
return `${finding.finding_id} (${finding.path ?? "review body"}: ${finding.title})`;
|
|
261
|
+
}
|
|
262
|
+
function reviewDismissSummary(result) {
|
|
263
|
+
const pr = `${result.owner}/${result.repo}#${result.pr_number}`;
|
|
264
|
+
const kind = result.review_kind === "seam" ? "seam review" : "Vortex review";
|
|
265
|
+
const head = `${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}`;
|
|
266
|
+
const lines = [];
|
|
267
|
+
if (result.status === "preview") {
|
|
268
|
+
lines.push(`${pr} ${head}: ${result.remaining.length} open finding(s). Nothing was written.`);
|
|
269
|
+
for (const finding of result.remaining) lines.push(`- ${findingText(finding)}`);
|
|
270
|
+
} else {
|
|
271
|
+
lines.push(
|
|
272
|
+
`${pr} ${head}: dismissed ${result.dismissed.length}, already dismissed ${result.already_dismissed.length}, still open ${result.remaining.length}.`
|
|
273
|
+
);
|
|
274
|
+
for (const finding of result.remaining) lines.push(`- open: ${findingText(finding)}`);
|
|
275
|
+
}
|
|
276
|
+
const seam = result.gate.seam;
|
|
277
|
+
if (seam) {
|
|
278
|
+
lines.push(
|
|
279
|
+
seam.cleared ? "Seam gate cleared at this head. CI, other reviews and Auto land policy are unchanged." : seam.blocking ? `Seam gate still blocking (seam_state=${seam.state}).` : `Seam gate not blocking (seam_state=${seam.state}).`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
async function reviewDismiss(input, cfg) {
|
|
285
|
+
const request = {
|
|
286
|
+
owner: input.owner,
|
|
287
|
+
repo: input.repo,
|
|
288
|
+
prNumber: input.pr_number,
|
|
289
|
+
headSha: input.head_sha,
|
|
290
|
+
reviewId: input.review_id,
|
|
291
|
+
...input.scope ? { scope: input.scope } : {},
|
|
292
|
+
...input.finding_ids ? { findingIds: input.finding_ids } : {},
|
|
293
|
+
...input.reason !== void 0 ? { reason: input.reason } : {},
|
|
294
|
+
...input.evidence_url ? { evidenceUrl: input.evidence_url } : {},
|
|
295
|
+
...input.preview ? { preview: true } : {}
|
|
296
|
+
};
|
|
297
|
+
const outcome = await dismissPrFindings(request, cfg);
|
|
298
|
+
if (!outcome.ok) {
|
|
299
|
+
return {
|
|
300
|
+
summary: `review_dismiss refused (${outcome.error}): ${outcome.message}`,
|
|
301
|
+
isError: true,
|
|
302
|
+
data: { error: { code: outcome.error, message: outcome.message, status: outcome.status } }
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const result = outcome.result;
|
|
306
|
+
const summary = reviewDismissSummary(result);
|
|
307
|
+
if (!result.stack_id) return { summary, data: { ...result } };
|
|
308
|
+
const watch = stackWatchObligation2({ stackId: result.stack_id, terminal: null, unread: true, freshCursor: true });
|
|
309
|
+
return { summary: withWatchText(watch, summary), data: { ...result, watch } };
|
|
310
|
+
}
|
|
311
|
+
|
|
165
312
|
// src/tools/review-list.ts
|
|
166
313
|
import { listJobs, toReviewJobEnvelope } from "mergestorm/client";
|
|
167
314
|
async function reviewList(limit = 10, cfg) {
|
|
@@ -185,7 +332,7 @@ import {
|
|
|
185
332
|
collectReviewInput,
|
|
186
333
|
discoverTrunk,
|
|
187
334
|
formatReviewSubmitError,
|
|
188
|
-
loadConfig as
|
|
335
|
+
loadConfig as loadConfig4,
|
|
189
336
|
loadReviewContext,
|
|
190
337
|
pollReview,
|
|
191
338
|
rateLimitedMessage as rateLimitedMessage2,
|
|
@@ -289,7 +436,7 @@ async function resolveInput(input) {
|
|
|
289
436
|
return collectReviewInput(base, head, cwd, sandboxRoot);
|
|
290
437
|
}
|
|
291
438
|
async function reviewSubmit(input, cfg, opts = {}) {
|
|
292
|
-
const resolved = cfg ?? await
|
|
439
|
+
const resolved = cfg ?? await loadConfig4();
|
|
293
440
|
const wait = input.wait === true;
|
|
294
441
|
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
295
442
|
assertPinnableSpecialists(input.specialists);
|
|
@@ -376,7 +523,7 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
376
523
|
// src/tools/review-wait-pr.ts
|
|
377
524
|
import {
|
|
378
525
|
PrReviewPollTimeoutError,
|
|
379
|
-
loadConfig as
|
|
526
|
+
loadConfig as loadConfig5,
|
|
380
527
|
pollPrVortexReview
|
|
381
528
|
} from "mergestorm/client";
|
|
382
529
|
|
|
@@ -458,7 +605,7 @@ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
|
458
605
|
// src/tools/review-wait-pr.ts
|
|
459
606
|
async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
460
607
|
const target = prReviewTarget(owner, repo, prNumber);
|
|
461
|
-
const resolved = cfg ?? await
|
|
608
|
+
const resolved = cfg ?? await loadConfig5();
|
|
462
609
|
if (timeout_s === 0) {
|
|
463
610
|
return reviewGetPr(target.owner, target.repo, target.prNumber, resolved, {
|
|
464
611
|
afterSha,
|
|
@@ -527,7 +674,7 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
527
674
|
import {
|
|
528
675
|
CommandError as CommandError6,
|
|
529
676
|
ReviewPollTimeoutError as ReviewPollTimeoutError2,
|
|
530
|
-
loadConfig as
|
|
677
|
+
loadConfig as loadConfig6,
|
|
531
678
|
pollReview as pollReview2
|
|
532
679
|
} from "mergestorm/client";
|
|
533
680
|
|
|
@@ -560,7 +707,7 @@ async function reviewGet(jobId, cfg) {
|
|
|
560
707
|
|
|
561
708
|
// src/tools/review-wait.ts
|
|
562
709
|
async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
563
|
-
const resolved = cfg ?? await
|
|
710
|
+
const resolved = cfg ?? await loadConfig6();
|
|
564
711
|
const id = jobId.trim();
|
|
565
712
|
if (!id) {
|
|
566
713
|
throw new CommandError6("job_id is required", 2, "usage");
|
|
@@ -672,100 +819,11 @@ async function settingsSet(args, cfg) {
|
|
|
672
819
|
}
|
|
673
820
|
|
|
674
821
|
// src/tools/stack-adopt.ts
|
|
675
|
-
import { adoptStack, CommandError as CommandError9, stackWatchObligation as
|
|
822
|
+
import { adoptStack, CommandError as CommandError9, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
|
|
676
823
|
import { z as z2 } from "zod";
|
|
677
824
|
|
|
678
825
|
// src/tools/stack-set.ts
|
|
679
|
-
import { setStackPolicy, stackWatchObligation as
|
|
680
|
-
|
|
681
|
-
// src/tools/stack-wait.ts
|
|
682
|
-
import {
|
|
683
|
-
loadConfig as loadConfig6,
|
|
684
|
-
stackBlockersSummary,
|
|
685
|
-
pollStackWatch,
|
|
686
|
-
StackWatchError,
|
|
687
|
-
StackWatchTimeoutError,
|
|
688
|
-
stackWatchObligation
|
|
689
|
-
} from "mergestorm/client";
|
|
690
|
-
import { z } from "zod";
|
|
691
|
-
var stackWaitSchema = {
|
|
692
|
-
stack_id: z.string().trim().min(1),
|
|
693
|
-
timeout_s: z.number().finite().min(0).max(45).default(45),
|
|
694
|
-
enrolled_head_sha: z.string().nullable().optional(),
|
|
695
|
-
after_finished_at: z.string().nullable().optional(),
|
|
696
|
-
bounce_id: z.string().nullable().optional()
|
|
697
|
-
};
|
|
698
|
-
function repairSummary(repair) {
|
|
699
|
-
if (!repair) return "";
|
|
700
|
-
const files = "files" in repair && repair.files.length ? ` Files: ${repair.files.join(", ")}.` : "";
|
|
701
|
-
return `
|
|
702
|
-
Repair #${repair.prNumber} (${repair.kind}).${files} ${repair.steps}`;
|
|
703
|
-
}
|
|
704
|
-
function withWatchText(watch, body) {
|
|
705
|
-
return `${watch.message}
|
|
706
|
-
|
|
707
|
-
${body}`;
|
|
708
|
-
}
|
|
709
|
-
function envelopeWatch(envelope) {
|
|
710
|
-
return envelope.watch ?? stackWatchObligation({
|
|
711
|
-
stackId: envelope.stackId,
|
|
712
|
-
terminal: null,
|
|
713
|
-
cursor: envelope.cursor,
|
|
714
|
-
status: envelope.status
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
function stackWaitSummary(envelope) {
|
|
718
|
-
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope)}${repairSummary(envelope.repair)}`);
|
|
719
|
-
}
|
|
720
|
-
function stackWaitStatusLine(envelope) {
|
|
721
|
-
const base = `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`;
|
|
722
|
-
if (envelopeWatch(envelope).done || envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
|
|
723
|
-
const selectors = [
|
|
724
|
-
`stack_id ${JSON.stringify(envelope.cursor.stackId)}`,
|
|
725
|
-
`enrolled_head_sha ${JSON.stringify(envelope.cursor.enrolledHeadSha)}`,
|
|
726
|
-
...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
|
|
727
|
-
...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
|
|
728
|
-
];
|
|
729
|
-
return `${base} \xB7 watch not finished: call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
730
|
-
}
|
|
731
|
-
async function stackWait(input, cfg, deps = {}) {
|
|
732
|
-
const args = z.object(stackWaitSchema).parse(input);
|
|
733
|
-
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
734
|
-
stackId: args.stack_id,
|
|
735
|
-
enrolledHeadSha: args.enrolled_head_sha ?? null,
|
|
736
|
-
...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
|
|
737
|
-
...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
|
|
738
|
-
} : void 0;
|
|
739
|
-
let envelope;
|
|
740
|
-
try {
|
|
741
|
-
envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig6(), args.stack_id, {
|
|
742
|
-
timeoutMs: args.timeout_s * 1e3,
|
|
743
|
-
cursor,
|
|
744
|
-
signal: deps.signal
|
|
745
|
-
});
|
|
746
|
-
} catch (err) {
|
|
747
|
-
if (err instanceof StackWatchError) {
|
|
748
|
-
const envelope2 = { ...err.lastEnvelope, watch: envelopeWatch(err.lastEnvelope) };
|
|
749
|
-
return {
|
|
750
|
-
summary: stackWaitSummary(envelope2),
|
|
751
|
-
data: {
|
|
752
|
-
...envelope2,
|
|
753
|
-
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
754
|
-
},
|
|
755
|
-
isError: !envelope2.watch.done
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
if (!(err instanceof StackWatchTimeoutError)) throw err;
|
|
759
|
-
envelope = err.lastEnvelope;
|
|
760
|
-
}
|
|
761
|
-
const sealed = { ...envelope, watch: envelopeWatch(envelope) };
|
|
762
|
-
return {
|
|
763
|
-
summary: stackWaitSummary(sealed),
|
|
764
|
-
data: sealed
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// src/tools/stack-set.ts
|
|
826
|
+
import { setStackPolicy, stackWatchObligation as stackWatchObligation3 } from "mergestorm/client";
|
|
769
827
|
function triWord(value) {
|
|
770
828
|
return value === null ? "default" : value ? "on" : "off";
|
|
771
829
|
}
|
|
@@ -802,7 +860,7 @@ async function stackSet(stackId, input, cfg) {
|
|
|
802
860
|
if (input.auto_patch !== void 0) {
|
|
803
861
|
parts.push(`auto-patch ${triWord(input.auto_patch)}`);
|
|
804
862
|
}
|
|
805
|
-
const watch =
|
|
863
|
+
const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
806
864
|
return {
|
|
807
865
|
summary: withWatchText(watch, `${parts.join(", ")} for stack ${stackId}`),
|
|
808
866
|
data: {
|
|
@@ -846,7 +904,7 @@ async function stackAdopt(input, cfg) {
|
|
|
846
904
|
if (typeof stackId !== "string" || !stackId) {
|
|
847
905
|
return { summary, data: { owner, repo, pr_number, result } };
|
|
848
906
|
}
|
|
849
|
-
const watch =
|
|
907
|
+
const watch = stackWatchObligation4({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
850
908
|
return {
|
|
851
909
|
summary: withWatchText(watch, summary),
|
|
852
910
|
data: { owner, repo, pr_number, result, watch }
|
|
@@ -891,8 +949,8 @@ function stackSummary(stack, entries = []) {
|
|
|
891
949
|
if (stack.cycloneOwnerMatch) {
|
|
892
950
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
893
951
|
}
|
|
894
|
-
const { attention, issues, busy, landGatePending } = stackBlockers(stack, entries);
|
|
895
|
-
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${stackBlockersSummary2(attention, issues, busy, landGatePending)}`;
|
|
952
|
+
const { attention, held, issues, busy, landGatePending } = stackBlockers(stack, entries);
|
|
953
|
+
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${stackBlockersSummary2(attention ?? held, issues, busy, landGatePending, held?.actAfter ?? null)}`;
|
|
896
954
|
}
|
|
897
955
|
|
|
898
956
|
// src/tools/stack-list.ts
|
|
@@ -929,14 +987,14 @@ import {
|
|
|
929
987
|
isCommandErrorCode as isCommandErrorCode5,
|
|
930
988
|
stackBlockers as stackBlockers2,
|
|
931
989
|
stackTerminalReason,
|
|
932
|
-
stackWatchObligation as
|
|
990
|
+
stackWatchObligation as stackWatchObligation5
|
|
933
991
|
} from "mergestorm/client";
|
|
934
992
|
async function stackStatus(stackId, cfg) {
|
|
935
993
|
try {
|
|
936
994
|
const stack = await getEnrichedStack(stackId, cfg);
|
|
937
995
|
if (!stack) {
|
|
938
996
|
const message = `Stack not found or not owned by the current user: ${stackId}`;
|
|
939
|
-
const watch2 =
|
|
997
|
+
const watch2 = stackWatchObligation5({ stackId, terminal: "not_found" });
|
|
940
998
|
return {
|
|
941
999
|
summary: withWatchText(watch2, message),
|
|
942
1000
|
data: {
|
|
@@ -951,8 +1009,8 @@ async function stackStatus(stackId, cfg) {
|
|
|
951
1009
|
};
|
|
952
1010
|
}
|
|
953
1011
|
const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
|
|
954
|
-
const { attention, issues, currentCandidate, busy, agents, repair, landGatePending } = stackBlockers2(stack, entries);
|
|
955
|
-
const watch =
|
|
1012
|
+
const { attention, held, issues, currentCandidate, busy, agents, repair, landGatePending } = stackBlockers2(stack, entries);
|
|
1013
|
+
const watch = stackWatchObligation5({
|
|
956
1014
|
stackId: stack.id,
|
|
957
1015
|
terminal: stackTerminalReason(stack),
|
|
958
1016
|
cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
|
|
@@ -961,11 +1019,11 @@ async function stackStatus(stackId, cfg) {
|
|
|
961
1019
|
});
|
|
962
1020
|
return {
|
|
963
1021
|
summary: withWatchText(watch, `${stackSummary(stack, entries)}${repairSummary(repair)}`),
|
|
964
|
-
data: { stack, attention, issues, currentCandidate, busy, agents, repair, landGatePending, watch }
|
|
1022
|
+
data: { stack, attention, held, issues, currentCandidate, busy, agents, repair, landGatePending, watch }
|
|
965
1023
|
};
|
|
966
1024
|
} catch (err) {
|
|
967
1025
|
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
968
|
-
const watch =
|
|
1026
|
+
const watch = stackWatchObligation5({
|
|
969
1027
|
stackId,
|
|
970
1028
|
terminal: null,
|
|
971
1029
|
status: "rate_limited",
|
|
@@ -1036,7 +1094,9 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
1036
1094
|
"- Prefer the smallest correct patch. Do not refactor around a finding.",
|
|
1037
1095
|
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
1038
1096
|
`- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line must be: ${MCP_FINDING_DISMISS_MARKER}. Then one line per skipped finding (path, severity, why).`,
|
|
1039
|
-
"- Do not prefix that comment with cyclone-outcome: (reserved)."
|
|
1097
|
+
"- Do not prefix that comment with cyclone-outcome: (reserved).",
|
|
1098
|
+
"- A finding you verified is wrong can be dismissed on the record with review_dismiss (or mg dismiss): pass the PR's live head_sha, the review_id, the finding_ids (preview: true lists them) and a reason. That is the only agent path that clears a seam gate, and only once every finding of that seam review is dismissed. The dismiss comment alone resumes the review but does not clear the seam gate.",
|
|
1099
|
+
"- Never dismiss a finding you did not check, one a human must decide, or findings of a newer review."
|
|
1040
1100
|
].join("\n");
|
|
1041
1101
|
var MCP_STACK_DONE_INSTRUCTIONS = [
|
|
1042
1102
|
"When stack work is done:",
|
|
@@ -1048,7 +1108,7 @@ var MCP_STACK_BASE_INSTRUCTIONS = [
|
|
|
1048
1108
|
"Stack PR bases (Mergestorm sets these; leave them):",
|
|
1049
1109
|
"- Stack of 2+ PRs: after adopt (stack_adopt, mg stack adopt, or mg stack submit) the bottom PR's GitHub base is the owned trunk mg-stack-<n>, not main. That is correct. Layer 2 is based on layer 1; layer 3+ on the mg-park-* freeze; the git parent of layer 2+ is still the layer below. A 1-PR stack stays on main and gets no mg-stack-<n>.",
|
|
1050
1110
|
"- Never retarget a stack PR's GitHub base (gh pr edit --base, a REST base change, or the UI). Moving the bottom to main makes Mergestorm abandon the stack's review unit; setting mg-stack-<n> again does not restore it. If a base was already moved, stop and tell the human; do not run stack land or re-adopt to repair it.",
|
|
1051
|
-
"- mg-stack-<n> is the bottom PR's live parent: merging it into the bottom branch is that PR's conflict repair (bounce-watch rules). Never push to mg-stack-<n
|
|
1111
|
+
"- mg-stack-<n> is the bottom PR's live parent: merging it into the bottom branch is that PR's conflict repair (bounce-watch rules). Never push to mg-stack-<n> except as the land PR's own repair: once every layer is promoted, the land PR's head is mg-stack-<n> and its live parent is its base (main), so merging main into mg-stack-<n>, or a CI fix on it, with an ordinary push (no force) is that repair. Never rebase or merge layer 2+ onto mg-stack-<n> or onto main. mg-park-* is never a merge or rebase target.",
|
|
1052
1112
|
"- mg stack submit prints the base each PR opened with; the bottom moves to mg-stack-<n> right after. stack_status trunkBranch is the current trunk."
|
|
1053
1113
|
].join("\n");
|
|
1054
1114
|
var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
@@ -1059,7 +1119,8 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
1059
1119
|
"- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
|
|
1060
1120
|
"- verifyHeadSha is a queue verification SHA, not the live PR head.",
|
|
1061
1121
|
"- Do not patch a bounced PR while Cyclone or Vortex is still working on it. stack_wait and stack_status enforce this: they return in_progress instead of attention, with busy[] naming the PR, blocker, and agent, and agents carrying that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. in_progress with a nonempty busy[] means wait. Recheck the live remote PR head right before pushing.",
|
|
1062
|
-
"- seam_pending alone, and a queued Vortex run older than 15 minutes, do not hold. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared
|
|
1122
|
+
"- seam_pending alone, and a queued Vortex run older than 15 minutes, do not hold. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared; if it looks stuck, refresh stack_status and tell the human.",
|
|
1123
|
+
'- A blocker no Vortex or Cyclone run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR) stays in_progress during the hold but is named in blocker, prNumber, headSha, and issues[], with actAfter: "agents_idle" and waitingOn listing the busy agents. Plan the fix; act only when a snapshot after the agents finish returns attention, and re-read the blocker then. Blockers the agents can change keep blocker null and appear only in busy[].',
|
|
1063
1124
|
"- stack.autoEnqueueSettle with action ready or promote means Auto land found Cyclone and Vortex idle on that PR's green head and started its settle clock. action mergeability is published earlier, on a head that may not be green or idle yet.",
|
|
1064
1125
|
"- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
|
|
1065
1126
|
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
@@ -1067,7 +1128,7 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
1067
1128
|
|
|
1068
1129
|
// src/server.ts
|
|
1069
1130
|
var MCP_SERVER_NAME = "mergestorm";
|
|
1070
|
-
var MCP_SERVER_VERSION = "0.2.
|
|
1131
|
+
var MCP_SERVER_VERSION = "0.2.2";
|
|
1071
1132
|
var MCP_SERVER_INSTRUCTIONS = [
|
|
1072
1133
|
MCP_STACK_DONE_INSTRUCTIONS,
|
|
1073
1134
|
MCP_PR_LOOP_INSTRUCTIONS,
|
|
@@ -1244,6 +1305,33 @@ function createMergestormMcpServer() {
|
|
|
1244
1305
|
}
|
|
1245
1306
|
}
|
|
1246
1307
|
);
|
|
1308
|
+
addTool(
|
|
1309
|
+
"review_dismiss",
|
|
1310
|
+
{
|
|
1311
|
+
title: "Dismiss Vortex findings",
|
|
1312
|
+
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; CI, other reviews 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.` + STACK_WATCH_RESULT_NOTE,
|
|
1313
|
+
inputSchema: {
|
|
1314
|
+
owner: z3.string().min(1),
|
|
1315
|
+
repo: z3.string().min(1),
|
|
1316
|
+
pr_number: z3.number().int().min(1),
|
|
1317
|
+
head_sha: z3.string().regex(/^[0-9a-fA-F]{40}$/),
|
|
1318
|
+
review_id: z3.number().int().min(1),
|
|
1319
|
+
finding_ids: z3.array(z3.string().min(1)).max(50).optional(),
|
|
1320
|
+
scope: z3.enum(["findings", "review"]).optional(),
|
|
1321
|
+
reason: z3.string().optional(),
|
|
1322
|
+
evidence_url: z3.string().url().optional(),
|
|
1323
|
+
preview: z3.boolean().optional()
|
|
1324
|
+
},
|
|
1325
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
1326
|
+
},
|
|
1327
|
+
async (args) => {
|
|
1328
|
+
try {
|
|
1329
|
+
return ok(await reviewDismiss(args));
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
return fail(err);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1247
1335
|
addTool(
|
|
1248
1336
|
"stack_adopt",
|
|
1249
1337
|
{
|
|
@@ -1306,7 +1394,7 @@ function createMergestormMcpServer() {
|
|
|
1306
1394
|
"stack_status",
|
|
1307
1395
|
{
|
|
1308
1396
|
title: "Get stack status",
|
|
1309
|
-
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." + STACK_WATCH_RESULT_NOTE,
|
|
1397
|
+
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." + STACK_WATCH_RESULT_NOTE,
|
|
1310
1398
|
inputSchema: {
|
|
1311
1399
|
stack_id: z3.string().min(1)
|
|
1312
1400
|
},
|
|
@@ -1324,7 +1412,7 @@ function createMergestormMcpServer() {
|
|
|
1324
1412
|
"stack_wait",
|
|
1325
1413
|
{
|
|
1326
1414
|
title: "Wait for stack attention",
|
|
1327
|
-
description:
|
|
1415
|
+
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. A blocker no agent run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR) 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-*). A pending land gate (landGatePending) returns in_progress, not attention.` + STACK_WATCH_RESULT_NOTE,
|
|
1328
1416
|
inputSchema: stackWaitSchema,
|
|
1329
1417
|
annotations: { readOnlyHint: true }
|
|
1330
1418
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
|
-
"mergestorm": "0.3.
|
|
32
|
+
"mergestorm": "0.3.27",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|