mergestorm-mcp 0.2.0 → 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 +257 -106
- 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,11 +819,11 @@ async function settingsSet(args, cfg) {
|
|
|
672
819
|
}
|
|
673
820
|
|
|
674
821
|
// src/tools/stack-adopt.ts
|
|
675
|
-
import { adoptStack, CommandError as CommandError9 } from "mergestorm/client";
|
|
676
|
-
import { z } from "zod";
|
|
822
|
+
import { adoptStack, CommandError as CommandError9, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
|
|
823
|
+
import { z as z2 } from "zod";
|
|
677
824
|
|
|
678
825
|
// src/tools/stack-set.ts
|
|
679
|
-
import { setStackPolicy } from "mergestorm/client";
|
|
826
|
+
import { setStackPolicy, stackWatchObligation as stackWatchObligation3 } from "mergestorm/client";
|
|
680
827
|
function triWord(value) {
|
|
681
828
|
return value === null ? "default" : value ? "on" : "off";
|
|
682
829
|
}
|
|
@@ -713,28 +860,30 @@ async function stackSet(stackId, input, cfg) {
|
|
|
713
860
|
if (input.auto_patch !== void 0) {
|
|
714
861
|
parts.push(`auto-patch ${triWord(input.auto_patch)}`);
|
|
715
862
|
}
|
|
863
|
+
const watch = stackWatchObligation3({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
716
864
|
return {
|
|
717
|
-
summary: `${parts.join(", ")} for stack ${stackId}
|
|
865
|
+
summary: withWatchText(watch, `${parts.join(", ")} for stack ${stackId}`),
|
|
718
866
|
data: {
|
|
719
867
|
stack_id: stackId,
|
|
720
868
|
...typeof input.auto_land === "boolean" ? { auto_land: input.auto_land } : {},
|
|
721
869
|
...input.auto_review !== void 0 ? { auto_review: input.auto_review } : {},
|
|
722
870
|
...input.auto_patch !== void 0 ? { auto_patch: input.auto_patch } : {},
|
|
723
|
-
result
|
|
871
|
+
result,
|
|
872
|
+
watch
|
|
724
873
|
}
|
|
725
874
|
};
|
|
726
875
|
}
|
|
727
876
|
|
|
728
877
|
// src/tools/stack-adopt.ts
|
|
729
878
|
var stackAdoptSchema = {
|
|
730
|
-
owner:
|
|
731
|
-
repo:
|
|
732
|
-
pr_number:
|
|
733
|
-
auto_land:
|
|
734
|
-
auto_review:
|
|
735
|
-
auto_patch:
|
|
879
|
+
owner: z2.string().trim().min(1),
|
|
880
|
+
repo: z2.string().trim().min(1),
|
|
881
|
+
pr_number: z2.number().int().min(1),
|
|
882
|
+
auto_land: z2.boolean().optional(),
|
|
883
|
+
auto_review: z2.boolean().nullable().optional(),
|
|
884
|
+
auto_patch: z2.boolean().nullable().optional()
|
|
736
885
|
};
|
|
737
|
-
var inputSchema =
|
|
886
|
+
var inputSchema = z2.object(stackAdoptSchema);
|
|
738
887
|
async function stackAdopt(input, cfg) {
|
|
739
888
|
const parsed = inputSchema.safeParse(input);
|
|
740
889
|
if (!parsed.success) {
|
|
@@ -750,9 +899,15 @@ async function stackAdopt(input, cfg) {
|
|
|
750
899
|
cfg,
|
|
751
900
|
stackSetPatch(parsed.data)
|
|
752
901
|
);
|
|
902
|
+
const summary = `Adopted stack for ${owner}/${repo}#${pr_number}`;
|
|
903
|
+
const stackId = typeof result === "object" && result !== null ? result.stack?.id : void 0;
|
|
904
|
+
if (typeof stackId !== "string" || !stackId) {
|
|
905
|
+
return { summary, data: { owner, repo, pr_number, result } };
|
|
906
|
+
}
|
|
907
|
+
const watch = stackWatchObligation4({ stackId, terminal: null, unread: true, freshCursor: true });
|
|
753
908
|
return {
|
|
754
|
-
summary:
|
|
755
|
-
data: { owner, repo, pr_number, result }
|
|
909
|
+
summary: withWatchText(watch, summary),
|
|
910
|
+
data: { owner, repo, pr_number, result, watch }
|
|
756
911
|
};
|
|
757
912
|
} catch (err) {
|
|
758
913
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -778,7 +933,7 @@ import {
|
|
|
778
933
|
} from "mergestorm/client";
|
|
779
934
|
|
|
780
935
|
// src/tools/stack-summary.ts
|
|
781
|
-
import { stackBlockers, stackBlockersSummary } from "mergestorm/client";
|
|
936
|
+
import { stackBlockers, stackBlockersSummary as stackBlockersSummary2 } from "mergestorm/client";
|
|
782
937
|
function stackSummary(stack, entries = []) {
|
|
783
938
|
const count = stack.layers.length;
|
|
784
939
|
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
@@ -794,8 +949,8 @@ function stackSummary(stack, entries = []) {
|
|
|
794
949
|
if (stack.cycloneOwnerMatch) {
|
|
795
950
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
796
951
|
}
|
|
797
|
-
const { attention, issues, busy } = stackBlockers(stack, entries);
|
|
798
|
-
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${
|
|
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)}`;
|
|
799
954
|
}
|
|
800
955
|
|
|
801
956
|
// src/tools/stack-list.ts
|
|
@@ -825,108 +980,65 @@ async function stackList(cfg) {
|
|
|
825
980
|
}
|
|
826
981
|
}
|
|
827
982
|
|
|
828
|
-
// src/tools/stack-wait.ts
|
|
829
|
-
import {
|
|
830
|
-
loadConfig as loadConfig6,
|
|
831
|
-
stackBlockersSummary as stackBlockersSummary2,
|
|
832
|
-
pollStackWatch,
|
|
833
|
-
StackWatchError,
|
|
834
|
-
StackWatchTimeoutError
|
|
835
|
-
} from "mergestorm/client";
|
|
836
|
-
import { z as z2 } from "zod";
|
|
837
|
-
var stackWaitSchema = {
|
|
838
|
-
stack_id: z2.string().trim().min(1),
|
|
839
|
-
timeout_s: z2.number().finite().min(0).max(45).default(45),
|
|
840
|
-
enrolled_head_sha: z2.string().nullable().optional(),
|
|
841
|
-
after_finished_at: z2.string().nullable().optional(),
|
|
842
|
-
bounce_id: z2.string().nullable().optional()
|
|
843
|
-
};
|
|
844
|
-
function stackWaitSummary(envelope) {
|
|
845
|
-
const base = `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary2(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`;
|
|
846
|
-
if (envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
|
|
847
|
-
const selectors = [
|
|
848
|
-
`stack_id ${JSON.stringify(envelope.cursor.stackId)}`,
|
|
849
|
-
`enrolled_head_sha ${JSON.stringify(envelope.cursor.enrolledHeadSha)}`,
|
|
850
|
-
...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
|
|
851
|
-
...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
|
|
852
|
-
];
|
|
853
|
-
return `${base} \xB7 watch not finished: call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
854
|
-
}
|
|
855
|
-
async function stackWait(input, cfg, deps = {}) {
|
|
856
|
-
const args = z2.object(stackWaitSchema).parse(input);
|
|
857
|
-
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
858
|
-
stackId: args.stack_id,
|
|
859
|
-
enrolledHeadSha: args.enrolled_head_sha ?? null,
|
|
860
|
-
...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
|
|
861
|
-
...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
|
|
862
|
-
} : void 0;
|
|
863
|
-
let envelope;
|
|
864
|
-
try {
|
|
865
|
-
envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig6(), args.stack_id, {
|
|
866
|
-
timeoutMs: args.timeout_s * 1e3,
|
|
867
|
-
cursor,
|
|
868
|
-
signal: deps.signal
|
|
869
|
-
});
|
|
870
|
-
} catch (err) {
|
|
871
|
-
if (err instanceof StackWatchError) {
|
|
872
|
-
return {
|
|
873
|
-
summary: stackWaitSummary(err.lastEnvelope),
|
|
874
|
-
data: {
|
|
875
|
-
...err.lastEnvelope,
|
|
876
|
-
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
877
|
-
},
|
|
878
|
-
isError: true
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
if (!(err instanceof StackWatchTimeoutError)) throw err;
|
|
882
|
-
envelope = err.lastEnvelope;
|
|
883
|
-
}
|
|
884
|
-
return {
|
|
885
|
-
summary: stackWaitSummary(envelope),
|
|
886
|
-
data: envelope
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
|
|
890
983
|
// src/tools/stack-status.ts
|
|
891
984
|
import {
|
|
892
985
|
getEnrichedStack,
|
|
893
986
|
listMergeQueueEntries as listMergeQueueEntries2,
|
|
894
987
|
isCommandErrorCode as isCommandErrorCode5,
|
|
895
|
-
stackBlockers as stackBlockers2
|
|
988
|
+
stackBlockers as stackBlockers2,
|
|
989
|
+
stackTerminalReason,
|
|
990
|
+
stackWatchObligation as stackWatchObligation5
|
|
896
991
|
} from "mergestorm/client";
|
|
897
992
|
async function stackStatus(stackId, cfg) {
|
|
898
993
|
try {
|
|
899
994
|
const stack = await getEnrichedStack(stackId, cfg);
|
|
900
995
|
if (!stack) {
|
|
901
996
|
const message = `Stack not found or not owned by the current user: ${stackId}`;
|
|
997
|
+
const watch2 = stackWatchObligation5({ stackId, terminal: "not_found" });
|
|
902
998
|
return {
|
|
903
|
-
summary: message,
|
|
999
|
+
summary: withWatchText(watch2, message),
|
|
904
1000
|
data: {
|
|
905
1001
|
error: {
|
|
906
1002
|
code: "stack_not_found",
|
|
907
1003
|
message,
|
|
908
1004
|
stack_id: stackId
|
|
909
|
-
}
|
|
1005
|
+
},
|
|
1006
|
+
watch: watch2
|
|
910
1007
|
},
|
|
911
1008
|
isError: true
|
|
912
1009
|
};
|
|
913
1010
|
}
|
|
914
1011
|
const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
|
|
915
|
-
const { attention, issues, currentCandidate, busy, agents } = stackBlockers2(stack, entries);
|
|
1012
|
+
const { attention, held, issues, currentCandidate, busy, agents, repair, landGatePending } = stackBlockers2(stack, entries);
|
|
1013
|
+
const watch = stackWatchObligation5({
|
|
1014
|
+
stackId: stack.id,
|
|
1015
|
+
terminal: stackTerminalReason(stack),
|
|
1016
|
+
cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
|
|
1017
|
+
attention: attention ? { prNumber: attention.prNumber, blocker: attention.blocker } : null,
|
|
1018
|
+
freshCursor: true
|
|
1019
|
+
});
|
|
916
1020
|
return {
|
|
917
|
-
summary: stackSummary(stack, entries),
|
|
918
|
-
data: { stack, attention, issues, currentCandidate, busy, agents }
|
|
1021
|
+
summary: withWatchText(watch, `${stackSummary(stack, entries)}${repairSummary(repair)}`),
|
|
1022
|
+
data: { stack, attention, held, issues, currentCandidate, busy, agents, repair, landGatePending, watch }
|
|
919
1023
|
};
|
|
920
1024
|
} catch (err) {
|
|
921
1025
|
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
1026
|
+
const watch = stackWatchObligation5({
|
|
1027
|
+
stackId,
|
|
1028
|
+
terminal: null,
|
|
1029
|
+
status: "rate_limited",
|
|
1030
|
+
retryAfterSeconds: err.retryAfterSeconds,
|
|
1031
|
+
freshCursor: true
|
|
1032
|
+
});
|
|
922
1033
|
return {
|
|
923
|
-
summary: err.message,
|
|
1034
|
+
summary: withWatchText(watch, err.message),
|
|
924
1035
|
data: {
|
|
925
1036
|
error: {
|
|
926
1037
|
code: "rate_limited",
|
|
927
1038
|
message: err.message,
|
|
928
1039
|
retry_after_seconds: err.retryAfterSeconds
|
|
929
|
-
}
|
|
1040
|
+
},
|
|
1041
|
+
watch
|
|
930
1042
|
},
|
|
931
1043
|
isError: true
|
|
932
1044
|
};
|
|
@@ -982,13 +1094,21 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
982
1094
|
"- Prefer the smallest correct patch. Do not refactor around a finding.",
|
|
983
1095
|
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
984
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).`,
|
|
985
|
-
"- 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."
|
|
1100
|
+
].join("\n");
|
|
1101
|
+
var MCP_STACK_DONE_INSTRUCTIONS = [
|
|
1102
|
+
"When stack work is done:",
|
|
1103
|
+
"- Stack work is done only when the stack is landed, or when the human takes it back (archives it or tells you to stop). A clean push, a submit, an adopt, or one merged layer is not done: merging a lower layer can break a layer above it.",
|
|
1104
|
+
"- Every stack_wait, stack_status, stack_adopt, and stack_set result carries watch: {done, until, reason, next, message}. While watch.done is false, keep calling stack_wait with watch.next.args (the returned cursor), 45s at a time, and do not end your task. Stop only when watch.done is true.",
|
|
1105
|
+
"- attention means something for you to fix; repair, when present, names the concrete fix. A pending land gate (landGatePending: ci_pending, tempest_pending, tempest_running) is a wait, not attention."
|
|
986
1106
|
].join("\n");
|
|
987
1107
|
var MCP_STACK_BASE_INSTRUCTIONS = [
|
|
988
1108
|
"Stack PR bases (Mergestorm sets these; leave them):",
|
|
989
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>.",
|
|
990
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.",
|
|
991
|
-
"- 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.",
|
|
992
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."
|
|
993
1113
|
].join("\n");
|
|
994
1114
|
var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
@@ -999,7 +1119,8 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
999
1119
|
"- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
|
|
1000
1120
|
"- verifyHeadSha is a queue verification SHA, not the live PR head.",
|
|
1001
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.",
|
|
1002
|
-
"- 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[].',
|
|
1003
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.",
|
|
1004
1125
|
"- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
|
|
1005
1126
|
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
@@ -1007,7 +1128,14 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
1007
1128
|
|
|
1008
1129
|
// src/server.ts
|
|
1009
1130
|
var MCP_SERVER_NAME = "mergestorm";
|
|
1010
|
-
var MCP_SERVER_VERSION = "0.2.
|
|
1131
|
+
var MCP_SERVER_VERSION = "0.2.2";
|
|
1132
|
+
var MCP_SERVER_INSTRUCTIONS = [
|
|
1133
|
+
MCP_STACK_DONE_INSTRUCTIONS,
|
|
1134
|
+
MCP_PR_LOOP_INSTRUCTIONS,
|
|
1135
|
+
MCP_STACK_BASE_INSTRUCTIONS,
|
|
1136
|
+
MCP_STACK_WATCH_INSTRUCTIONS
|
|
1137
|
+
].join("\n\n");
|
|
1138
|
+
var STACK_WATCH_RESULT_NOTE = ' The result carries watch {done, until: "landed", reason, next, message}: while watch.done is false the stack is not landed and your task is not done; call stack_wait with watch.next.args.';
|
|
1011
1139
|
function stripNulls(value) {
|
|
1012
1140
|
if (Array.isArray(value)) {
|
|
1013
1141
|
return value.map((item) => stripNulls(item));
|
|
@@ -1044,11 +1172,7 @@ function createMergestormMcpServer() {
|
|
|
1044
1172
|
websiteUrl: MCP_WEBSITE_URL,
|
|
1045
1173
|
icons: MCP_SERVER_ICONS
|
|
1046
1174
|
},
|
|
1047
|
-
{ instructions:
|
|
1048
|
-
|
|
1049
|
-
${MCP_STACK_BASE_INSTRUCTIONS}
|
|
1050
|
-
|
|
1051
|
-
${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
1175
|
+
{ instructions: MCP_SERVER_INSTRUCTIONS }
|
|
1052
1176
|
);
|
|
1053
1177
|
const addTool = server2.registerTool.bind(server2);
|
|
1054
1178
|
addTool(
|
|
@@ -1181,11 +1305,38 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1181
1305
|
}
|
|
1182
1306
|
}
|
|
1183
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
|
+
);
|
|
1184
1335
|
addTool(
|
|
1185
1336
|
"stack_adopt",
|
|
1186
1337
|
{
|
|
1187
1338
|
title: "Adopt PR stack",
|
|
1188
|
-
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review and auto_patch accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted auto_review and auto_patch overrides are not seeded. Never changes account settings. Adoption requires a Cyclone GitHub App install; without it the API returns cyclone_not_connected \u2014 do not retry, tell the human. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main.",
|
|
1339
|
+
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review and auto_patch accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted auto_review and auto_patch overrides are not seeded. Never changes account settings. Adoption requires a Cyclone GitHub App install; without it the API returns cyclone_not_connected \u2014 do not retry, tell the human. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + STACK_WATCH_RESULT_NOTE,
|
|
1189
1340
|
inputSchema: stackAdoptSchema,
|
|
1190
1341
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
1191
1342
|
},
|
|
@@ -1216,7 +1367,7 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1216
1367
|
"stack_set",
|
|
1217
1368
|
{
|
|
1218
1369
|
title: "Set stack policy",
|
|
1219
|
-
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review and auto_patch pin Vortex auto-review or Cyclone auto-patch for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings.",
|
|
1370
|
+
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review and auto_patch pin Vortex auto-review or Cyclone auto-patch for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings." + STACK_WATCH_RESULT_NOTE,
|
|
1220
1371
|
inputSchema: {
|
|
1221
1372
|
stack_id: z3.string().min(1),
|
|
1222
1373
|
auto_land: z3.boolean().optional(),
|
|
@@ -1243,7 +1394,7 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1243
1394
|
"stack_status",
|
|
1244
1395
|
{
|
|
1245
1396
|
title: "Get stack status",
|
|
1246
|
-
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.",
|
|
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,
|
|
1247
1398
|
inputSchema: {
|
|
1248
1399
|
stack_id: z3.string().min(1)
|
|
1249
1400
|
},
|
|
@@ -1261,7 +1412,7 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1261
1412
|
"stack_wait",
|
|
1262
1413
|
{
|
|
1263
1414
|
title: "Wait for stack attention",
|
|
1264
|
-
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,
|
|
1265
1416
|
inputSchema: stackWaitSchema,
|
|
1266
1417
|
annotations: { readOnlyHint: true }
|
|
1267
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": {
|