mergestorm-mcp 0.1.6 → 0.1.7

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 +1 -1
  2. package/dist/index.js +35 -65
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,7 +8,7 @@ The stack tools expose reads plus one scoped policy write:
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
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.
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
  }
@@ -963,10 +921,20 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
963
921
  `- 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
922
  "- Do not prefix that comment with cyclone-outcome: (reserved)."
965
923
  ].join("\n");
924
+ var MCP_STACK_WATCH_INSTRUCTIONS = [
925
+ "Stack-watch facts (mergestorm.stack_watch/v1):",
926
+ "- waiting with nonempty issues[] is not idle; in_progress can also carry issues[].",
927
+ "- assessment unavailable does not mean issue-free. timeout_s: 0 reads one enrich and one queue snapshot.",
928
+ "- Restack clean does not mean mergeable or CI-green.",
929
+ "- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
930
+ "- verifyHeadSha is a queue verification SHA, not the live PR head.",
931
+ "- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
932
+ "- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
933
+ ].join("\n");
966
934
 
967
935
  // src/server.ts
968
936
  var MCP_SERVER_NAME = "mergestorm";
969
- var MCP_SERVER_VERSION = "0.1.6";
937
+ var MCP_SERVER_VERSION = "0.1.7";
970
938
  function stripNulls(value) {
971
939
  if (Array.isArray(value)) {
972
940
  return value.map((item) => stripNulls(item));
@@ -1000,7 +968,9 @@ function createMergestormMcpServer() {
1000
968
  name: MCP_SERVER_NAME,
1001
969
  version: MCP_SERVER_VERSION
1002
970
  },
1003
- { instructions: MCP_PR_LOOP_INSTRUCTIONS }
971
+ { instructions: `${MCP_PR_LOOP_INSTRUCTIONS}
972
+
973
+ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
1004
974
  );
1005
975
  const addTool = server2.registerTool.bind(server2);
1006
976
  addTool(
@@ -1248,7 +1218,7 @@ function createMergestormMcpServer() {
1248
1218
  "stack_wait",
1249
1219
  {
1250
1220
  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.",
1221
+ 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
1222
  inputSchema: stackWaitSchema
1253
1223
  },
1254
1224
  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.7",
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.20",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {