mergestorm-mcp 0.1.6 → 0.1.8
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 +2 -2
- package/dist/index.js +41 -69
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@ The stack tools expose reads plus one scoped policy write:
|
|
|
7
7
|
- `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.
|
|
8
8
|
- `stack_list` returns the current API key owner's registered stacks.
|
|
9
9
|
- `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.
|
|
10
|
-
- `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state,
|
|
11
|
-
- `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
|
|
10
|
+
- `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.
|
|
11
|
+
- `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`.
|
|
12
12
|
|
|
13
13
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
14
14
|
|
package/dist/index.js
CHANGED
|
@@ -479,17 +479,26 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
479
479
|
);
|
|
480
480
|
} catch (err) {
|
|
481
481
|
if (err instanceof PrReviewPollTimeoutError) {
|
|
482
|
-
const status =
|
|
482
|
+
const status = "in_progress";
|
|
483
|
+
const expectedSha = afterSha?.trim().toLowerCase() ?? "";
|
|
484
|
+
const observedSha = err.lastEnvelope?.head_sha?.trim().toLowerCase() ?? "";
|
|
485
|
+
const headShaMatches = Boolean(
|
|
486
|
+
observedSha && expectedSha.length >= 7 && (observedSha.startsWith(expectedSha) || expectedSha.startsWith(observedSha))
|
|
487
|
+
);
|
|
488
|
+
const findingCount = headShaMatches ? err.lastEnvelope?.finding_count ?? null : null;
|
|
483
489
|
return {
|
|
484
490
|
summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
|
|
485
491
|
status,
|
|
486
492
|
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
487
|
-
finding_count:
|
|
493
|
+
finding_count: findingCount,
|
|
488
494
|
phase: err.lastEnvelope?.phase ?? null
|
|
489
495
|
}),
|
|
490
496
|
data: {
|
|
491
|
-
...err.lastEnvelope ?? {},
|
|
492
497
|
status,
|
|
498
|
+
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
499
|
+
pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
|
|
500
|
+
finding_count: findingCount,
|
|
501
|
+
phase: err.lastEnvelope?.phase ?? null,
|
|
493
502
|
error: err.message
|
|
494
503
|
}
|
|
495
504
|
};
|
|
@@ -719,64 +728,12 @@ import {
|
|
|
719
728
|
} from "mergestorm/client";
|
|
720
729
|
|
|
721
730
|
// src/tools/stack-summary.ts
|
|
722
|
-
import {
|
|
723
|
-
BOUNCE_KIND_LABELS,
|
|
724
|
-
mergeQueueBounceLabel
|
|
725
|
-
} from "mergestorm/client";
|
|
726
|
-
function sameHead(a, b) {
|
|
727
|
-
const left = a?.trim().toLowerCase();
|
|
728
|
-
const right = b?.trim().toLowerCase();
|
|
729
|
-
return !!left && !!right && (left.length === right.length ? left === right : left.startsWith(right) || right.startsWith(left));
|
|
730
|
-
}
|
|
731
|
-
function blockerLabel(stack, entries) {
|
|
732
|
-
if (stack.archivedAt) return null;
|
|
733
|
-
const queue = entries.filter((entry) => entry.stackId === stack.id);
|
|
734
|
-
if (queue.some((entry) => ["queued", "running", "waiting"].includes(entry.state))) return null;
|
|
735
|
-
const open = stack.layers.filter(
|
|
736
|
-
(layer2) => layer2.prNumber > 0 && layer2.state !== "merged" && layer2.state !== "closed"
|
|
737
|
-
);
|
|
738
|
-
const promote = open.filter(
|
|
739
|
-
(layer2) => layer2.prNumber !== stack.unit?.landPrNumber && !stack.unit?.members?.some(
|
|
740
|
-
(member) => member.prNumber === layer2.prNumber && member.promotedHeadSha
|
|
741
|
-
)
|
|
742
|
-
).sort((a, b) => a.position - b.position);
|
|
743
|
-
const layer = promote[0] ?? open.find((layer2) => layer2.prNumber === stack.unit?.landPrNumber) ?? stack.unit?.landPr;
|
|
744
|
-
const bounce = queue.filter((entry) => entry.state === "bounced").sort((a, b) => (Date.parse(b.finishedAt ?? "") || 0) - (Date.parse(a.finishedAt ?? "") || 0))[0];
|
|
745
|
-
const bounceKind = bounce?.bounceDetail?.kind;
|
|
746
|
-
const recoverableBounceSha = (bounceKind === "head_moved" ? bounce?.bounceDetail?.headSha ?? bounce?.verifyHeadSha : bounce?.verifyHeadSha ?? bounce?.bounceDetail?.headSha)?.trim().toLowerCase();
|
|
747
|
-
const currentRecoverableBounce = !!recoverableBounceSha && (bounceKind === "head_moved" || bounceKind === "must_consolidate") && !!layer && layer.state !== "merged" && layer.state !== "closed" && sameHead(layer.headSha, recoverableBounceSha);
|
|
748
|
-
if (layer && layer.state !== "merged" && layer.state !== "closed") {
|
|
749
|
-
if (layer.restackError && layer.state !== "restacking") {
|
|
750
|
-
return layer.restackError.kind === "rebase_conflict" ? "Conflict" : "Restack failed";
|
|
751
|
-
}
|
|
752
|
-
if (layer.state === "conflict") return "Conflict";
|
|
753
|
-
if (layer.draft) return "Draft PR";
|
|
754
|
-
if ((!layer.headSha || layer.mergeableHeadSha === layer.headSha) && (layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty")) return "Merge conflicts";
|
|
755
|
-
if (!currentRecoverableBounce && (layer.ciStatus === "failure" || (layer.checks?.failure ?? 0) > 0)) {
|
|
756
|
-
const name = layer.checks?.failingName?.trim();
|
|
757
|
-
return BOUNCE_KIND_LABELS.ci_failure + (name ? ` \u2014 ${name}` : "");
|
|
758
|
-
}
|
|
759
|
-
const tempest = layer.agentRuns?.find(
|
|
760
|
-
(run) => run.agent === "tempest" && ["findings", "failed"].includes(run.status) && sameHead(run.sha, layer.headSha)
|
|
761
|
-
);
|
|
762
|
-
if (tempest) return `Tempest ${tempest.status}`;
|
|
763
|
-
if (layer.vortexStatus === "failed") return "Review failed";
|
|
764
|
-
if (stack.unit?.landPrNumber === layer.prNumber) {
|
|
765
|
-
if (stack.unit.landingBlockReason?.trim()) return stack.unit.landingBlockReason.trim();
|
|
766
|
-
if (stack.unit.tempestLandStatus?.toLowerCase() === "failed") return "Tempest failed";
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
const kind = bounceKind;
|
|
770
|
-
if (!kind || kind === "head_moved" || kind === "must_consolidate") return null;
|
|
771
|
-
const sha = (bounce.bounceDetail?.headSha ?? bounce.verifyHeadSha)?.trim().toLowerCase();
|
|
772
|
-
const current = sha && layer && layer.prNumber > 0 && layer.state !== "merged" && layer.state !== "closed" && (bounce.bounceDetail?.prNumber == null || layer.prNumber === bounce.bounceDetail.prNumber) && sameHead(layer.headSha, sha);
|
|
773
|
-
return current ? mergeQueueBounceLabel(bounce) : null;
|
|
774
|
-
}
|
|
731
|
+
import { stackBlockers, stackBlockersSummary } from "mergestorm/client";
|
|
775
732
|
function stackSummary(stack, entries = []) {
|
|
776
733
|
const count = stack.layers.length;
|
|
777
734
|
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
778
735
|
const states = [...new Set(stack.layers.map((layer) => layer.state))];
|
|
779
|
-
const currentState = states.length > 0 ? `
|
|
736
|
+
const currentState = states.length > 0 ? `restack: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
|
|
780
737
|
const overrides = [];
|
|
781
738
|
if (typeof stack.autoReviewOverride === "boolean") {
|
|
782
739
|
overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
|
|
@@ -787,8 +744,8 @@ function stackSummary(stack, entries = []) {
|
|
|
787
744
|
if (stack.cycloneOwnerMatch) {
|
|
788
745
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
789
746
|
}
|
|
790
|
-
const
|
|
791
|
-
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${
|
|
747
|
+
const { attention, issues } = stackBlockers(stack, entries);
|
|
748
|
+
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${stackBlockersSummary(attention, issues)}`;
|
|
792
749
|
}
|
|
793
750
|
|
|
794
751
|
// src/tools/stack-list.ts
|
|
@@ -821,6 +778,7 @@ async function stackList(cfg) {
|
|
|
821
778
|
// src/tools/stack-wait.ts
|
|
822
779
|
import {
|
|
823
780
|
loadConfig as loadConfig6,
|
|
781
|
+
stackBlockersSummary as stackBlockersSummary2,
|
|
824
782
|
pollStackWatch,
|
|
825
783
|
StackWatchError,
|
|
826
784
|
StackWatchTimeoutError
|
|
@@ -851,7 +809,7 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
851
809
|
} catch (err) {
|
|
852
810
|
if (err instanceof StackWatchError) {
|
|
853
811
|
return {
|
|
854
|
-
summary: `Stack ${err.lastEnvelope.stackId} \xB7 ${err.lastEnvelope.status}`,
|
|
812
|
+
summary: `Stack ${err.lastEnvelope.stackId} \xB7 ${err.lastEnvelope.status}${stackBlockersSummary2(err.lastEnvelope.blocker && err.lastEnvelope.prNumber != null ? { prNumber: err.lastEnvelope.prNumber, blocker: err.lastEnvelope.blocker } : null, err.lastEnvelope.issues)}${err.lastEnvelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`,
|
|
855
813
|
data: {
|
|
856
814
|
...err.lastEnvelope,
|
|
857
815
|
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
@@ -860,10 +818,10 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
860
818
|
};
|
|
861
819
|
}
|
|
862
820
|
if (!(err instanceof StackWatchTimeoutError)) throw err;
|
|
863
|
-
envelope =
|
|
821
|
+
envelope = err.lastEnvelope;
|
|
864
822
|
}
|
|
865
823
|
return {
|
|
866
|
-
summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${envelope.blocker ?
|
|
824
|
+
summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary2(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`,
|
|
867
825
|
data: envelope
|
|
868
826
|
};
|
|
869
827
|
}
|
|
@@ -872,7 +830,8 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
872
830
|
import {
|
|
873
831
|
getEnrichedStack,
|
|
874
832
|
listMergeQueueEntries as listMergeQueueEntries2,
|
|
875
|
-
isCommandErrorCode as isCommandErrorCode5
|
|
833
|
+
isCommandErrorCode as isCommandErrorCode5,
|
|
834
|
+
stackBlockers as stackBlockers2
|
|
876
835
|
} from "mergestorm/client";
|
|
877
836
|
async function stackStatus(stackId, cfg) {
|
|
878
837
|
try {
|
|
@@ -891,10 +850,11 @@ async function stackStatus(stackId, cfg) {
|
|
|
891
850
|
isError: true
|
|
892
851
|
};
|
|
893
852
|
}
|
|
894
|
-
const entries = await listMergeQueueEntries2(cfg);
|
|
853
|
+
const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
|
|
854
|
+
const { attention, issues, currentCandidate } = stackBlockers2(stack, entries);
|
|
895
855
|
return {
|
|
896
856
|
summary: stackSummary(stack, entries),
|
|
897
|
-
data: { stack }
|
|
857
|
+
data: { stack, attention, issues, currentCandidate }
|
|
898
858
|
};
|
|
899
859
|
} catch (err) {
|
|
900
860
|
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
@@ -963,10 +923,20 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
963
923
|
`- 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).`,
|
|
964
924
|
"- Do not prefix that comment with cyclone-outcome: (reserved)."
|
|
965
925
|
].join("\n");
|
|
926
|
+
var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
927
|
+
"Stack-watch facts (mergestorm.stack_watch/v1):",
|
|
928
|
+
"- waiting with nonempty issues[] is not idle; in_progress can also carry issues[].",
|
|
929
|
+
"- assessment unavailable does not mean issue-free. timeout_s: 0 reads one enrich and one queue snapshot.",
|
|
930
|
+
"- Restack clean does not mean mergeable or CI-green.",
|
|
931
|
+
"- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
|
|
932
|
+
"- verifyHeadSha is a queue verification SHA, not the live PR head.",
|
|
933
|
+
"- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
|
|
934
|
+
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
935
|
+
].join("\n");
|
|
966
936
|
|
|
967
937
|
// src/server.ts
|
|
968
938
|
var MCP_SERVER_NAME = "mergestorm";
|
|
969
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
939
|
+
var MCP_SERVER_VERSION = "0.1.8";
|
|
970
940
|
function stripNulls(value) {
|
|
971
941
|
if (Array.isArray(value)) {
|
|
972
942
|
return value.map((item) => stripNulls(item));
|
|
@@ -1000,7 +970,9 @@ function createMergestormMcpServer() {
|
|
|
1000
970
|
name: MCP_SERVER_NAME,
|
|
1001
971
|
version: MCP_SERVER_VERSION
|
|
1002
972
|
},
|
|
1003
|
-
{ instructions: MCP_PR_LOOP_INSTRUCTIONS
|
|
973
|
+
{ instructions: `${MCP_PR_LOOP_INSTRUCTIONS}
|
|
974
|
+
|
|
975
|
+
${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
1004
976
|
);
|
|
1005
977
|
const addTool = server2.registerTool.bind(server2);
|
|
1006
978
|
addTool(
|
|
@@ -1231,7 +1203,7 @@ function createMergestormMcpServer() {
|
|
|
1231
1203
|
"stack_status",
|
|
1232
1204
|
{
|
|
1233
1205
|
title: "Get stack status",
|
|
1234
|
-
description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
|
|
1206
|
+
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. This tool is read-only.",
|
|
1235
1207
|
inputSchema: {
|
|
1236
1208
|
stack_id: z3.string().min(1)
|
|
1237
1209
|
}
|
|
@@ -1248,7 +1220,7 @@ function createMergestormMcpServer() {
|
|
|
1248
1220
|
"stack_wait",
|
|
1249
1221
|
{
|
|
1250
1222
|
title: "Wait for stack attention",
|
|
1251
|
-
description: "Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope;
|
|
1223
|
+
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. A timeout of failed means no assessment was produced. This tool is read-only.",
|
|
1252
1224
|
inputSchema: stackWaitSchema
|
|
1253
1225
|
},
|
|
1254
1226
|
async (args, extra) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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.22",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|