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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +41 -69
  3. 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, or a structured `stack_not_found` 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, or a waiting result with selectors to use for the next call.
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 = err.lastEnvelope?.status ?? "in_progress";
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: err.lastEnvelope?.finding_count ?? null,
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 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
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 blocker = blockerLabel(stack, entries);
791
- return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${blocker ? ` \xB7 blocked: ${blocker}` : ""}`;
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 = { ...err.lastEnvelope, status: "waiting" };
821
+ envelope = err.lastEnvelope;
864
822
  }
865
823
  return {
866
- summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${envelope.blocker ? ` \xB7 ${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.6";
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; timeout returns waiting. This tool is read-only. Do not pass 300; hosts drop long MCP calls.",
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.6",
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.19",
32
+ "mergestorm": "0.3.22",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {