mergestorm 0.3.9 → 0.3.15

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 (76) hide show
  1. package/README.md +67 -24
  2. package/dist/api.d.ts +106 -40
  3. package/dist/api.js +245 -9
  4. package/dist/client/index.d.ts +5 -0
  5. package/dist/client/index.js +5 -0
  6. package/dist/commands/branches.d.ts +6 -2
  7. package/dist/commands/branches.js +29 -14
  8. package/dist/commands/browse.d.ts +39 -0
  9. package/dist/commands/browse.js +259 -0
  10. package/dist/commands/credits.js +30 -12
  11. package/dist/commands/help.d.ts +5 -0
  12. package/dist/commands/help.js +93 -0
  13. package/dist/commands/jobs.d.ts +11 -2
  14. package/dist/commands/jobs.js +64 -15
  15. package/dist/commands/login.js +32 -10
  16. package/dist/commands/logout.js +2 -1
  17. package/dist/commands/pr.d.ts +22 -0
  18. package/dist/commands/pr.js +127 -0
  19. package/dist/commands/queue.d.ts +12 -0
  20. package/dist/commands/queue.js +160 -0
  21. package/dist/commands/registry.d.ts +1 -1
  22. package/dist/commands/registry.js +115 -33
  23. package/dist/commands/review-client.d.ts +106 -0
  24. package/dist/commands/review-client.js +399 -0
  25. package/dist/commands/review.d.ts +36 -6
  26. package/dist/commands/review.js +395 -145
  27. package/dist/commands/settings.d.ts +17 -0
  28. package/dist/commands/settings.js +89 -0
  29. package/dist/commands/skill.d.ts +20 -0
  30. package/dist/commands/skill.js +145 -0
  31. package/dist/commands/stack.d.ts +36 -2
  32. package/dist/commands/stack.js +341 -113
  33. package/dist/commands/status.d.ts +18 -2
  34. package/dist/commands/status.js +236 -21
  35. package/dist/commands/whoami.d.ts +3 -1
  36. package/dist/commands/whoami.js +21 -7
  37. package/dist/errors.d.ts +18 -2
  38. package/dist/errors.js +22 -1
  39. package/dist/git-stack.d.ts +2 -0
  40. package/dist/git-stack.js +4 -0
  41. package/dist/git.d.ts +12 -1
  42. package/dist/git.js +52 -7
  43. package/dist/shell.d.ts +1 -1
  44. package/dist/shell.js +39 -28
  45. package/dist/stack-dto.d.ts +266 -0
  46. package/dist/stack-dto.js +7 -0
  47. package/dist/submit-pr-base.d.ts +31 -0
  48. package/dist/submit-pr-base.js +69 -0
  49. package/dist/ui/ansi.d.ts +2 -0
  50. package/dist/ui/ansi.js +2 -0
  51. package/dist/ui/banner.d.ts +34 -2
  52. package/dist/ui/banner.js +170 -79
  53. package/dist/ui/box.d.ts +13 -9
  54. package/dist/ui/box.js +28 -44
  55. package/dist/ui/findings.d.ts +4 -1
  56. package/dist/ui/findings.js +14 -7
  57. package/dist/ui/job-envelope.d.ts +68 -0
  58. package/dist/ui/job-envelope.js +35 -0
  59. package/dist/ui/line-tabs.d.ts +27 -0
  60. package/dist/ui/line-tabs.js +170 -0
  61. package/dist/ui/logo.d.ts +10 -2
  62. package/dist/ui/logo.js +13 -9
  63. package/dist/ui/present.d.ts +10 -0
  64. package/dist/ui/present.js +13 -0
  65. package/dist/ui/prompt.d.ts +148 -0
  66. package/dist/ui/prompt.js +433 -71
  67. package/dist/ui/select.js +1 -1
  68. package/dist/ui/tabs.d.ts +125 -0
  69. package/dist/ui/tabs.js +480 -0
  70. package/dist/ui/usage.d.ts +29 -1
  71. package/dist/ui/usage.js +71 -2
  72. package/dist/ui/width.d.ts +26 -0
  73. package/dist/ui/width.js +262 -0
  74. package/package.json +6 -1
  75. package/skill/SKILL.md +98 -0
  76. package/skill/mergestorm-pr-loop/SKILL.md +76 -0
package/dist/ui/usage.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ansi } from "./ansi.js";
2
+ import { frameWidth } from "./box.js";
3
+ import { padVisible, visibleWidth } from "./width.js";
2
4
  /**
3
5
  * Usage meter — long thin bar + percent only:
4
6
  * `[████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 7% used`
5
7
  *
6
8
  * No used/limit clutter. Edge cases:
7
- * - `limit === 0` → dim "not included" (premium on non-Maelstrom)
9
+ * - `limit === 0` → dim "not included"
8
10
  * - `limit == null` → "unlimited" (dev-bypass)
9
11
  */
10
12
  export function usageBar(used, limit, opts = {}) {
@@ -28,7 +30,7 @@ export function usageBar(used, limit, opts = {}) {
28
30
  if (!color) {
29
31
  return `[${filledSeg}${emptySeg}]${suffix}`;
30
32
  }
31
- let fillColor = ansi.green;
33
+ let fillColor = ansi.brightGreen;
32
34
  if (ratio >= 0.9)
33
35
  fillColor = ansi.red;
34
36
  else if (ratio >= 0.75)
@@ -61,7 +63,74 @@ export function formatResetLabel(iso, now = new Date()) {
61
63
  const mm = String(m).padStart(2, "0");
62
64
  return `Resets ${month} ${day}, ${h}:${mm}${ampm} (UTC)`;
63
65
  }
66
+ /** How long until `iso` (the API `resets_at`). Empty when the date is unusable. */
67
+ export function formatDaysLeft(iso, now = new Date()) {
68
+ if (!iso)
69
+ return "";
70
+ const d = new Date(iso);
71
+ if (Number.isNaN(d.getTime()))
72
+ return "";
73
+ const ms = d.getTime() - now.getTime();
74
+ if (ms <= 0)
75
+ return "reset due";
76
+ const days = Math.floor(ms / 86_400_000);
77
+ const hours = Math.floor((ms % 86_400_000) / 3_600_000);
78
+ if (days >= 2)
79
+ return `${days} days left`;
80
+ if (days === 1)
81
+ return "1 day left";
82
+ if (hours >= 1)
83
+ return `${hours}h left`;
84
+ return "resets soon";
85
+ }
64
86
  /** Alias kept for callers that still import `formatResetDate`. */
65
87
  export function formatResetDate(iso) {
66
88
  return formatResetLabel(iso);
67
89
  }
90
+ /** Bar cells so ` [bar] 100% used` stays inside the terminal. */
91
+ export function usageBarWidth(columns) {
92
+ return Math.max(8, frameWidth(80, columns) - 13);
93
+ }
94
+ function clipLine(line, columns) {
95
+ if (visibleWidth(line) <= columns)
96
+ return line;
97
+ return padVisible(line, columns);
98
+ }
99
+ /**
100
+ * Static `/usage` panel: identity, a bar sized from `frameWidth()`, reset
101
+ * line, then the last five jobs. Every row is clipped to `columns`.
102
+ */
103
+ export function formatUsagePanel(input) {
104
+ const columns = Math.max(20, input.columns);
105
+ const color = input.color ?? false;
106
+ const identity = `● ${input.keyPrefix} · ${input.plan}`;
107
+ const bar = usageBar(input.used, input.limit, {
108
+ width: usageBarWidth(columns),
109
+ color,
110
+ });
111
+ const lines = [
112
+ ` ${identity}`,
113
+ ` ${bar}`,
114
+ ` ${formatResetLabel(input.resetsAt)}`,
115
+ "",
116
+ ];
117
+ if (input.jobs.length === 0) {
118
+ lines.push(input.jobsError ? ` ${input.jobsError}` : " No review jobs yet.");
119
+ return lines.map((line) => clipLine(line, columns));
120
+ }
121
+ const inner = Math.max(20, columns - 2);
122
+ const jobW = 8;
123
+ const verdictW = 15;
124
+ const creditsW = 7;
125
+ const whenW = 8;
126
+ const gaps = 8;
127
+ const threadW = Math.max(6, inner - jobW - verdictW - creditsW - whenW - gaps);
128
+ const header = ["JOB", "VERDICT", "THREAD", "CREDITS", "WHEN"];
129
+ const widths = [jobW, verdictW, threadW, creditsW, whenW];
130
+ const fmt = (cols) => " " + cols.map((c, i) => padVisible(c, widths[i])).join(" ");
131
+ lines.push(fmt(header));
132
+ for (const job of input.jobs.slice(0, 5)) {
133
+ lines.push(fmt([job.job, job.verdict, job.thread, job.credits, job.when]));
134
+ }
135
+ return lines.map((line) => clipLine(line, columns));
136
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Terminal cell arithmetic. Everything that frames, pads, or parks a cursor
3
+ * must count *cells*, not UTF-16 units: CJK is two cells, combining marks
4
+ * and ZWJ are zero, an astral emoji is one grapheme. Getting this wrong is
5
+ * why the right border of the prompt box drifted on non-Latin input.
6
+ */
7
+ export declare function stripSgr(str: string): string;
8
+ /** Cells for one code point on its own (no cluster context). */
9
+ export declare function codePointWidth(cp: number): number;
10
+ /**
11
+ * Cells one grapheme cluster occupies. A cluster is as wide as its widest
12
+ * base (a ZWJ family emoji is 2, not 6); VS16 promotes a text-style symbol
13
+ * to emoji presentation (2 cells); a cluster of nothing but marks is 0.
14
+ */
15
+ export declare function graphemeWidth(cluster: string): number;
16
+ /** Terminal cells a string occupies, ignoring SGR colour codes. */
17
+ export declare function visibleWidth(str: string): number;
18
+ /**
19
+ * Take the cells `[start, start + cells)` of `str`, keeping every SGR code
20
+ * (so colours survive) and substituting spaces for a wide glyph cut by
21
+ * either edge, so the result's geometry is exactly what the caller asked
22
+ * for even when a CJK character straddles the window.
23
+ */
24
+ export declare function sliceVisible(str: string, start: number, cells: number): string;
25
+ /** Pad or truncate to an exact cell width (ANSI-safe). */
26
+ export declare function padVisible(line: string, width: number): string;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Terminal cell arithmetic. Everything that frames, pads, or parks a cursor
3
+ * must count *cells*, not UTF-16 units: CJK is two cells, combining marks
4
+ * and ZWJ are zero, an astral emoji is one grapheme. Getting this wrong is
5
+ * why the right border of the prompt box drifted on non-Latin input.
6
+ */
7
+ const SGR_PATTERN = /\u001b\[[0-9;]*m/g;
8
+ const SGR_SPLIT = /(\u001b\[[0-9;]*m)/;
9
+ export function stripSgr(str) {
10
+ return str.replace(SGR_PATTERN, "");
11
+ }
12
+ /**
13
+ * East Asian Wide / Fullwidth ranges (plus emoji presentation blocks).
14
+ * Deliberately excludes Ambiguous-width ranges such as box drawing
15
+ * (U+2500..257F) and block elements (U+2580..259F, the TORNADO_LOGO
16
+ * glyphs), which every terminal we target renders in one cell.
17
+ */
18
+ const WIDE_RANGES = [
19
+ [0x1100, 0x115f], // Hangul Jamo
20
+ [0x231a, 0x231b],
21
+ [0x2329, 0x232a],
22
+ [0x23e9, 0x23ec],
23
+ [0x23f0, 0x23f0],
24
+ [0x23f3, 0x23f3],
25
+ [0x25fd, 0x25fe],
26
+ [0x2614, 0x2615],
27
+ [0x2648, 0x2653],
28
+ [0x267f, 0x267f],
29
+ [0x2693, 0x2693],
30
+ [0x26a1, 0x26a1],
31
+ [0x26aa, 0x26ab],
32
+ [0x26bd, 0x26be],
33
+ [0x26c4, 0x26c5],
34
+ [0x26ce, 0x26ce],
35
+ [0x26d4, 0x26d4],
36
+ [0x26ea, 0x26ea],
37
+ [0x26f2, 0x26f3],
38
+ [0x26f5, 0x26f5],
39
+ [0x26fa, 0x26fa],
40
+ [0x26fd, 0x26fd],
41
+ [0x2705, 0x2705],
42
+ [0x270a, 0x270b],
43
+ [0x2728, 0x2728],
44
+ [0x274c, 0x274c],
45
+ [0x274e, 0x274e],
46
+ [0x2753, 0x2755],
47
+ [0x2757, 0x2757],
48
+ [0x2795, 0x2797],
49
+ [0x27b0, 0x27b0],
50
+ [0x27bf, 0x27bf],
51
+ [0x2b1b, 0x2b1c],
52
+ [0x2b50, 0x2b50],
53
+ [0x2b55, 0x2b55],
54
+ [0x2e80, 0x303e], // CJK radicals, Kangxi, ideographic description, CJK symbols
55
+ [0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul compat, enclosed CJK
56
+ [0x3400, 0x4dbf], // CJK ext A
57
+ [0x4e00, 0x9fff], // CJK unified
58
+ [0xa000, 0xa4cf], // Yi
59
+ [0xa960, 0xa97f], // Hangul Jamo ext A
60
+ [0xac00, 0xd7a3], // Hangul syllables
61
+ [0xf900, 0xfaff], // CJK compat ideographs
62
+ [0xfe10, 0xfe19], // vertical forms
63
+ [0xfe30, 0xfe6f], // CJK compat forms, small forms
64
+ [0xff00, 0xff60], // fullwidth forms
65
+ [0xffe0, 0xffe6],
66
+ [0x16fe0, 0x16fe4],
67
+ [0x17000, 0x18aff], // Tangut, Khitan
68
+ [0x1b000, 0x1b2ff], // Kana supplement / ext
69
+ [0x1f004, 0x1f004],
70
+ [0x1f0cf, 0x1f0cf],
71
+ [0x1f18e, 0x1f18e],
72
+ [0x1f191, 0x1f19a],
73
+ [0x1f1e6, 0x1f1ff], // regional indicators (flags pair to one 2-cell cluster)
74
+ [0x1f200, 0x1f202],
75
+ [0x1f210, 0x1f23b],
76
+ [0x1f240, 0x1f248],
77
+ [0x1f250, 0x1f251],
78
+ [0x1f260, 0x1f265],
79
+ [0x1f300, 0x1f320],
80
+ [0x1f32d, 0x1f335],
81
+ [0x1f337, 0x1f37c],
82
+ [0x1f37e, 0x1f393],
83
+ [0x1f3a0, 0x1f3ca],
84
+ [0x1f3cf, 0x1f3d3],
85
+ [0x1f3e0, 0x1f3f0],
86
+ [0x1f3f4, 0x1f3f4],
87
+ [0x1f3f8, 0x1f43e],
88
+ [0x1f440, 0x1f440],
89
+ [0x1f442, 0x1f4fc],
90
+ [0x1f4ff, 0x1f53d],
91
+ [0x1f54b, 0x1f54e],
92
+ [0x1f550, 0x1f567],
93
+ [0x1f57a, 0x1f57a],
94
+ [0x1f595, 0x1f596],
95
+ [0x1f5a4, 0x1f5a4],
96
+ [0x1f5fb, 0x1f64f],
97
+ [0x1f680, 0x1f6c5],
98
+ [0x1f6cc, 0x1f6cc],
99
+ [0x1f6d0, 0x1f6d2],
100
+ [0x1f6d5, 0x1f6d7],
101
+ [0x1f6dc, 0x1f6df],
102
+ [0x1f6eb, 0x1f6ec],
103
+ [0x1f6f4, 0x1f6fc],
104
+ [0x1f7e0, 0x1f7eb],
105
+ [0x1f7f0, 0x1f7f0],
106
+ [0x1f90c, 0x1f93a],
107
+ [0x1f93c, 0x1f945],
108
+ [0x1f947, 0x1f9ff],
109
+ [0x1fa70, 0x1faff],
110
+ [0x20000, 0x2fffd], // CJK ext B..F
111
+ [0x30000, 0x3fffd], // CJK ext G..
112
+ ];
113
+ const ZERO_WIDTH_RE = /^[\p{M}\p{Cc}\p{Cf}\u200b-\u200f\u2028-\u202e\u2060-\u2064\ufeff]$/u;
114
+ function isWide(cp) {
115
+ // Binary search over the sorted range table.
116
+ let lo = 0;
117
+ let hi = WIDE_RANGES.length - 1;
118
+ while (lo <= hi) {
119
+ const mid = (lo + hi) >> 1;
120
+ const [start, end] = WIDE_RANGES[mid];
121
+ if (cp < start)
122
+ hi = mid - 1;
123
+ else if (cp > end)
124
+ lo = mid + 1;
125
+ else
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+ /** Cells for one code point on its own (no cluster context). */
131
+ export function codePointWidth(cp) {
132
+ if (cp === 0xfe0f)
133
+ return 0; // VS16 handled at cluster level
134
+ const ch = String.fromCodePoint(cp);
135
+ if (ZERO_WIDTH_RE.test(ch))
136
+ return 0;
137
+ if (cp >= 0xfe00 && cp <= 0xfe0f)
138
+ return 0; // variation selectors
139
+ if (cp >= 0xe0100 && cp <= 0xe01ef)
140
+ return 0; // VS supplement
141
+ if (cp >= 0xe0020 && cp <= 0xe007f)
142
+ return 0; // emoji tag sequence
143
+ if (cp >= 0xd800 && cp <= 0xdfff)
144
+ return 1; // lone surrogate (broken slice)
145
+ return isWide(cp) ? 2 : 1;
146
+ }
147
+ let segmenter;
148
+ /**
149
+ * Grapheme clusters of `text`, yielded lazily so an early exit (e.g.
150
+ * `sliceVisible` cutting a cell window) never segments the whole tail of a
151
+ * long buffer. Falls back to code-point iteration without Intl.Segmenter.
152
+ */
153
+ function* graphemes(text) {
154
+ if (segmenter === undefined) {
155
+ try {
156
+ segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
157
+ }
158
+ catch {
159
+ segmenter = null;
160
+ }
161
+ }
162
+ if (segmenter) {
163
+ for (const seg of segmenter.segment(text))
164
+ yield seg.segment;
165
+ return;
166
+ }
167
+ for (const ch of text)
168
+ yield ch;
169
+ }
170
+ /**
171
+ * Cells one grapheme cluster occupies. A cluster is as wide as its widest
172
+ * base (a ZWJ family emoji is 2, not 6); VS16 promotes a text-style symbol
173
+ * to emoji presentation (2 cells); a cluster of nothing but marks is 0.
174
+ */
175
+ export function graphemeWidth(cluster) {
176
+ let width = 0;
177
+ let vs16 = false;
178
+ for (const ch of cluster) {
179
+ const cp = ch.codePointAt(0);
180
+ if (cp === 0xfe0f) {
181
+ vs16 = true;
182
+ continue;
183
+ }
184
+ width = Math.max(width, codePointWidth(cp));
185
+ }
186
+ if (vs16 && width === 1)
187
+ return 2;
188
+ return width;
189
+ }
190
+ /** Terminal cells a string occupies, ignoring SGR colour codes. */
191
+ export function visibleWidth(str) {
192
+ const plain = stripSgr(str);
193
+ if (!plain)
194
+ return 0;
195
+ let total = 0;
196
+ for (const cluster of graphemes(plain))
197
+ total += graphemeWidth(cluster);
198
+ return total;
199
+ }
200
+ /**
201
+ * Take the cells `[start, start + cells)` of `str`, keeping every SGR code
202
+ * (so colours survive) and substituting spaces for a wide glyph cut by
203
+ * either edge, so the result's geometry is exactly what the caller asked
204
+ * for even when a CJK character straddles the window.
205
+ */
206
+ export function sliceVisible(str, start, cells) {
207
+ if (cells <= 0)
208
+ return "";
209
+ const end = start + cells;
210
+ let out = "";
211
+ let offset = 0;
212
+ const parts = str.split(SGR_SPLIT);
213
+ for (let i = 0; i < parts.length; i++) {
214
+ const part = parts[i];
215
+ if (!part)
216
+ continue;
217
+ if (i % 2 === 1) {
218
+ out += part;
219
+ continue;
220
+ }
221
+ // Offsets only grow, so once we're past the window no later content can
222
+ // land in it; stop segmenting the tail of a long buffer.
223
+ if (offset > end)
224
+ continue;
225
+ for (const cluster of graphemes(part)) {
226
+ if (offset > end)
227
+ break;
228
+ const w = graphemeWidth(cluster);
229
+ const from = offset;
230
+ const to = offset + w;
231
+ offset = to;
232
+ if (w === 0) {
233
+ // Marks attach to the previous glyph; keep them only when that glyph
234
+ // was emitted.
235
+ if (from > start && from <= end)
236
+ out += cluster;
237
+ continue;
238
+ }
239
+ if (to <= start || from >= end)
240
+ continue;
241
+ if (from >= start && to <= end) {
242
+ out += cluster;
243
+ }
244
+ else {
245
+ out += " ".repeat(Math.min(to, end) - Math.max(from, start));
246
+ }
247
+ }
248
+ }
249
+ return out;
250
+ }
251
+ /** Pad or truncate to an exact cell width (ANSI-safe). */
252
+ export function padVisible(line, width) {
253
+ const target = Math.max(0, width);
254
+ const w = visibleWidth(line);
255
+ if (w === target)
256
+ return line;
257
+ if (w < target)
258
+ return `${line}${" ".repeat(target - w)}`;
259
+ const cut = sliceVisible(line, 0, target);
260
+ const cutWidth = visibleWidth(cut);
261
+ return cutWidth < target ? `${cut}${" ".repeat(target - cutWidth)}` : cut;
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.9",
3
+ "version": "0.3.15",
4
4
  "description": "Mergestorm CLI (mg) — local reviews & stacked PRs. MIT; build from source: https://github.com/marginsystems/mergestorm-cli",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,10 @@
18
18
  "mergestorm": "./dist/cli.js",
19
19
  "mg": "./dist/cli.js"
20
20
  },
21
+ "exports": {
22
+ ".": "./dist/cli.js",
23
+ "./client": "./dist/client/index.js"
24
+ },
21
25
  "homepage": "https://mergestorm.ai/docs#review-jobs-api",
22
26
  "repository": {
23
27
  "type": "git",
@@ -28,6 +32,7 @@
28
32
  },
29
33
  "files": [
30
34
  "dist",
35
+ "skill",
31
36
  "LICENSE",
32
37
  "README.md"
33
38
  ],
package/skill/SKILL.md ADDED
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: mergestorm-review
3
+ description: Review a local branch with Mergestorm Vortex specialists before opening a PR, then apply the findings yourself.
4
+ ---
5
+
6
+ # Mergestorm review
7
+
8
+ Use this skill to review a local git branch with Mergestorm, then apply the findings yourself.
9
+
10
+ ## When to use
11
+
12
+ Before opening a PR; after a large refactor; when the user says review, audit, second opinion, or "is this safe to merge".
13
+
14
+ Not for merging, landing, or pushing. For a PR that is already open on GitHub, use the `mergestorm-pr-loop` skill instead.
15
+
16
+ ## Install (Claude Code / Cursor)
17
+
18
+ ```bash
19
+ mg skill install --claude
20
+ mg skill install --cursor
21
+ # or both: mg skill install --claude --cursor
22
+ ```
23
+
24
+ That writes `.claude/skills/mergestorm-review/SKILL.md` and/or `.cursor/skills/mergestorm-review/SKILL.md` in the current repo.
25
+
26
+ Hand-copy still works: Claude Code project skill `.claude/skills/mergestorm-review/SKILL.md` (or `~/.claude/skills/mergestorm-review/SKILL.md`). Cursor: the same file under `.cursor/skills/mergestorm-review/SKILL.md`; otherwise a rule at `.cursor/rules/mergestorm-review.mdc` with this description and `alwaysApply: false`.
27
+
28
+ MCP server:
29
+
30
+ ```bash
31
+ curl -fsSL https://mergestorm.ai/install.sh | bash -s -- --mcp
32
+ # or: npm i -g mergestorm mergestorm-mcp
33
+ claude mcp add mergestorm -- npx -y mergestorm-mcp
34
+ ```
35
+
36
+ Cursor `~/.cursor/mcp.json`:
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "mergestorm": {
42
+ "command": "npx",
43
+ "args": ["-y", "mergestorm-mcp"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Preconditions
50
+
51
+ 1. Prefer the `mergestorm` MCP server. Call `whoami`. If MCP is missing, require `mg` on PATH with `mg whoami --json` exit 0. If neither is available, stop and print the install one-liner above.
52
+ 2. Call `credits` (or `mg credits --json`). If remaining standard credits are 0, say so and stop.
53
+
54
+ ## Hire policy
55
+
56
+ Default: omit `router` (account setting).
57
+
58
+ - `router: "off"` for tiny diffs or docs-only
59
+ - `manual` with pins when the change is clearly one domain: `security` (auth/crypto/input), `frontend` (components), `data` (migrations), `api` (route contracts), `tests` (mostly tests), `performance` (hot paths), `architecture` (cross-module moves)
60
+ - `max` only when the user asks for a full pass
61
+ - Never request `governance` or `seam`. Neither exists for local reviews. Seam runs automatically during Mergestorm stack promotes and is controlled by the Agents settings toggle (default on).
62
+
63
+ ## Submit
64
+
65
+ Call `review_submit` on the repo cwd (git collect when `diff` is omitted). Pass one or two sentences of intent in `context`. Pass an ADR or spec as `context_files` when one exists in the repo under review — never from outside it. Do not paste the diff into context.
66
+
67
+ Optional: `thread`, `idempotency_key`, `wait`, `timeout_s`. Default wait is 300s; on timeout the tool returns `status: in_progress` and the job id — then call `review_wait`.
68
+
69
+ Use `review_get` or `review_list` to resume a known job.
70
+
71
+ Status handling:
72
+
73
+ - status is `no_changes`: success. The branch matches the selected base, no review job was submitted, and no review credit was used. Stop unless the user expected a different base.
74
+ - status is `rate_limited`: wait `retry_after_seconds`. Use `review_list`, `review_get`, or `review_wait` for an existing in-flight job before resubmitting. Do not immediately resubmit the same diff.
75
+
76
+ ## Apply findings
77
+
78
+ Read `findings.inline` (and `off_diff` / `offDiff`) sorted by severity. Fix or explain each. Never silence a finding by deleting the test. Then submit again on the same branch so the thread chains, with `context` stating what was addressed.
79
+
80
+ ## Never
81
+
82
+ - land, merge, push, or open a PR
83
+ - call stack authoring or land commands
84
+ - wrap the CLI in a shell escape hatch
85
+ - claim a finding is fixed without a diff that touches its path
86
+ - resubmit an identical failing diff (that burns a credit)
87
+ - treat `no_changes` or `rate_limited` as a failed review that needs an immediate resubmit
88
+ - pass `--context-file` or `--context` contents from outside the repo under review
89
+
90
+ ## Fallback (no MCP)
91
+
92
+ ```bash
93
+ mg review --json --context "…"
94
+ # optional: --router … --specialists … --context-file … --thread … --wait / --no-wait --timeout
95
+ # context files must live inside the repo under review
96
+ ```
97
+
98
+ Parse stdout as one JSON object (`mergestorm.review_job/v1`). Exit 0 includes `no_changes`. Exit 5 means detach and poll with `mg status <id> --json --wait`. Exit 7 means `rate_limited`: wait `retry_after_seconds` before trying again.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: mergestorm-pr-loop
3
+ description: Patch an already-open GitHub PR from Mergestorm Vortex findings until it comes back clean. Wait for the PR's Vortex pass, fix, push the PR head, wait for the pass at the pushed SHA. Not for local pre-PR reviews (use mergestorm-review for those).
4
+ ---
5
+
6
+ # Mergestorm PR loop
7
+
8
+ Vortex already reviews every push to an open PR on a monitored repo; nothing here starts a review. This skill reads the latest pass, patches the PR head branch, pushes, and waits for the follow-up pass.
9
+
10
+ Two worlds, never mixed: reviewing a local branch before a PR exists is the `mergestorm-review` skill. This skill is only for a PR that is already open on GitHub.
11
+
12
+ ## Install (Claude Code / Cursor)
13
+
14
+ ```bash
15
+ mg skill install --claude --cursor
16
+ ```
17
+
18
+ ## Preconditions (in order)
19
+
20
+ 1. There is an already-open GitHub PR on a Vortex-monitored repo. No PR, wrong skill.
21
+ 2. Auto-patch (Cyclone) must be off so you are the only patcher. Do not take anyone's word for it: the settings API is the source of truth, and step 1 of the loop verifies it before anything else runs.
22
+ 3. `whoami` works on the `mergestorm` MCP server (or `mg whoami --json` exits 0). Otherwise stop and ask for install/login.
23
+
24
+ ## Loop (in order)
25
+
26
+ 1. Call `settings_get` and read `auto_patch_enabled`.
27
+ - If `settings_get` errors or its result has no `auto_patch_enabled`: refuse the loop. Do not wait, patch, or push; tell the human that Cyclone auto-patch could not be verified.
28
+ - If it is `false`: continue. You are the only patcher.
29
+ - If it is `true` and the human asked you to turn Cyclone off: call `settings_set` with `auto_patch_enabled: false`, then call `settings_get` again and confirm it now reads `false`.
30
+ - If it is `true` otherwise: refuse the loop. Do not wait, patch, or push; tell the human Cyclone auto-patch is still on and where to turn it off.
31
+ - Never set `auto_patch_enabled` to `true`.
32
+ 2. Call `review_wait_pr` with the PR's owner, repo, and pr_number (`review_get_pr` works for a first look at the resting state).
33
+ 3. Confirm the envelope's `head_sha` is the commit you have checked out as the PR head (compare its leading 7+ hex characters with `git rev-parse HEAD`). If it is not, someone else pushed after this pass and its findings are not yours to patch: do not edit. Fetch and check out the PR head, then go to step 7 with `after_sha` set to that head.
34
+ 4. Read `findings.inline` and `findings.offDiff` (or `off_diff`) sorted by severity. Fix or explain each.
35
+ 5. Patch on the PR head branch only.
36
+ 6. Push that head branch with an ordinary push. If the push is rejected because the remote branch moved, another patcher already pushed for this pass. Do not rebase, do not force, do not retry the push: stop and tell the human that a second patcher is active on this PR.
37
+ 7. Call `review_wait_pr` again with `after_sha` set to the SHA you just pushed (7+ hex). An envelope whose `head_sha` is an older commit is a previous pass, not yours; keep waiting.
38
+ 8. Repeat from step 3 until a stop rule fires.
39
+
40
+ ## Stop rules
41
+
42
+ Map these to `mergestorm.pr_review/v1` envelope fields; do not invent statuses.
43
+
44
+ - `finding_count` is 0 (empty inline and offDiff): stop. Tell the human the PR is clear and that Auto land owns landing. Never merge it yourself.
45
+ - status is `rate_limited`, or `patch_policy.mode` is `"hold"`: surface the findings to the human and stop. Do not patch.
46
+ - status is `failed`: report to the human. Do not keep pushing to retrigger.
47
+ - status is `in_progress` and the envelope has a `head_sha`: the pass is still running; call `review_wait_pr` again with the same `after_sha`.
48
+ - status is `in_progress` and the envelope has no `head_sha`: the wait timed out before Vortex wrote a pass for your SHA. Call `review_wait_pr` once more with the same `after_sha`. If it comes back the same way a second time in a row, stop and tell the human that Vortex did not pick up the push. Do not push again to retrigger.
49
+ - status is `skipped`, `stopped`, `trial_expired`, or `synced`: surface the status and reason to the human and stop. Do not patch or push.
50
+
51
+ ## Never
52
+
53
+ - submit a local review job, or call the job-id tools `review_wait` / `review_get` here. Local jobs are `mergestorm-review`'s world: they cost credits and do not fire Vortex on the PR.
54
+ - land, merge, enqueue, or touch stack authoring or the merge queue
55
+ - turn `auto_patch_enabled` on, from this skill, for any reason
56
+ - push any branch other than the PR head
57
+ - force-push, or rebase and retry a push that was rejected because the remote moved
58
+ - patch a pass whose `head_sha` is not the commit you have checked out
59
+ - wrap the CLI in a shell escape hatch
60
+ - claim a finding is fixed without a diff that touches its path
61
+
62
+ ## Fallback (no MCP)
63
+
64
+ Verify auto-patch with the CLI instead of `settings_get`:
65
+
66
+ ```bash
67
+ mg settings --json
68
+ ```
69
+
70
+ Read `auto_patch_enabled` from stdout. If the command errors or the result has no `auto_patch_enabled`, refuse the loop and tell the human that Cyclone auto-patch could not be verified. If it is `true` and the human asked you to turn Cyclone off, run `mg settings --auto-patch off` and re-read; otherwise refuse the loop, same rule as step 1.
71
+
72
+ ```bash
73
+ mg pr <owner/repo>#<n> --json --wait --after-sha <sha> --timeout <s>
74
+ ```
75
+
76
+ Parse stdout as one `mergestorm.pr_review/v1` object. Same fields, same stop rules.