mergestorm 0.3.15 → 0.3.17
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 +4 -4
- package/dist/api.d.ts +44 -4
- package/dist/api.js +55 -12
- package/dist/automation-catalog.d.ts +37 -0
- package/dist/automation-catalog.js +13 -0
- package/dist/client/index.d.ts +4 -2
- package/dist/client/index.js +4 -2
- package/dist/commands/browse.d.ts +2 -2
- package/dist/commands/browse.js +7 -11
- package/dist/commands/credits.js +1 -0
- package/dist/commands/pr.d.ts +4 -0
- package/dist/commands/pr.js +32 -5
- package/dist/commands/registry.js +7 -4
- package/dist/commands/review-client.d.ts +10 -3
- package/dist/commands/review-client.js +75 -24
- package/dist/commands/review.js +17 -14
- package/dist/commands/settings.d.ts +2 -2
- package/dist/commands/settings.js +2 -9
- package/dist/commands/stack.d.ts +55 -2
- package/dist/commands/stack.js +274 -37
- package/dist/commands/status.js +4 -2
- package/dist/commands/whoami.js +9 -9
- package/dist/git.js +1 -1
- package/dist/repo-identity.d.ts +1 -1
- package/dist/repo-identity.js +13 -2
- package/dist/stack-dto.d.ts +55 -2
- package/dist/stack-dto.js +0 -6
- package/dist/stack-meta.d.ts +12 -0
- package/dist/stack-meta.js +32 -3
- package/dist/ui/banner.js +6 -0
- package/dist/ui/human-summary.d.ts +2 -0
- package/dist/ui/human-summary.js +153 -0
- package/dist/ui/usage.d.ts +1 -2
- package/dist/ui/usage.js +3 -4
- package/package.json +1 -1
- package/skill/SKILL.md +3 -3
- package/skill/mergestorm-pr-loop/SKILL.md +36 -21
package/dist/stack-dto.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export type RestackError = {
|
|
2
|
+
kind: "rebase_conflict" | "checkout_failed" | "head_unresolved" | "push_failed";
|
|
3
|
+
detail: string;
|
|
4
|
+
headSha: string;
|
|
5
|
+
attemptedAt: string;
|
|
6
|
+
attempts: number;
|
|
7
|
+
backupRef: string | null;
|
|
8
|
+
};
|
|
1
9
|
/**
|
|
2
10
|
* Wire types returned by the stack list/enrich HTTP endpoints.
|
|
3
11
|
*
|
|
@@ -7,8 +15,12 @@
|
|
|
7
15
|
export type StackBranchState = "clean" | "needs_restack" | "restacking" | "conflict" | "merged" | "closed";
|
|
8
16
|
export type StackCiStatus = "success" | "pending" | "failure" | "neutral" | "none" | "unknown";
|
|
9
17
|
export type StackReviewStatus = "approved" | "changes_requested" | "reviewed" | "failed" | "none" | "unknown";
|
|
10
|
-
/**
|
|
11
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `failed` is the newest Vortex `pr_reviews` row. Work paints Review failed.
|
|
20
|
+
* `incomplete` is a posted verdict whose head still has unreviewed files
|
|
21
|
+
* (#1947): Work paints Incomplete, never Reviewed or all clear.
|
|
22
|
+
*/
|
|
23
|
+
export type StackVortexStatus = "reviewing" | "seam_pending" | "all_clear" | "findings" | "throttled" | "failed" | "incomplete";
|
|
12
24
|
export type StackCycloneStatus = "patching" | "awaiting_fix";
|
|
13
25
|
export type StackTempestStatus = "reviewing" | "findings" | "clear" | "failed" | "stale";
|
|
14
26
|
export type StackAgentName = "vortex" | "cyclone" | "tempest";
|
|
@@ -38,6 +50,15 @@ export type StackAgentRun = {
|
|
|
38
50
|
* Only on resting Vortex `findings` runs.
|
|
39
51
|
*/
|
|
40
52
|
findingCount?: number | null;
|
|
53
|
+
/**
|
|
54
|
+
* File coverage at the posted head from `vortex_pr_review_state.file_states`
|
|
55
|
+
* (#1947): files reviewed at this head over all changed files. Only on
|
|
56
|
+
* resting Vortex `incomplete` runs, so the chip tooltip can say 21/32.
|
|
57
|
+
*/
|
|
58
|
+
coverage?: {
|
|
59
|
+
reviewed: number;
|
|
60
|
+
total: number;
|
|
61
|
+
} | null;
|
|
41
62
|
};
|
|
42
63
|
export type StackLayerChecks = {
|
|
43
64
|
total: number;
|
|
@@ -118,6 +139,7 @@ export type StackLayerDto = {
|
|
|
118
139
|
*/
|
|
119
140
|
agentRuns?: StackAgentRun[];
|
|
120
141
|
conflictDetail: string | null;
|
|
142
|
+
restackError?: RestackError | null;
|
|
121
143
|
lastRestackedSha: string | null;
|
|
122
144
|
/** GitHub PR mergeability from enrich; null when unknown. */
|
|
123
145
|
mergeable: boolean | null;
|
|
@@ -207,6 +229,7 @@ export type StackUnitDto = {
|
|
|
207
229
|
*/
|
|
208
230
|
landPr: StackLayerDto | null;
|
|
209
231
|
};
|
|
232
|
+
export type CycloneOwnerMatch = "same" | "different" | "none" | "lookup_failed";
|
|
210
233
|
export type StackDto = {
|
|
211
234
|
id: string;
|
|
212
235
|
owner: string;
|
|
@@ -224,6 +247,36 @@ export type StackDto = {
|
|
|
224
247
|
* Missing on older payloads is off.
|
|
225
248
|
*/
|
|
226
249
|
autoEnqueueWhenReady?: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Per-stack Vortex auto-review override. `null` (or missing) follows the
|
|
252
|
+
* account `auto_review_enabled` flag; a boolean wins in both directions.
|
|
253
|
+
*/
|
|
254
|
+
autoReviewOverride?: boolean | null;
|
|
255
|
+
/**
|
|
256
|
+
* Per-stack Cyclone auto-patch override. `null` (or missing) follows the
|
|
257
|
+
* account `auto_patch_enabled` flag; a boolean wins in both directions.
|
|
258
|
+
*/
|
|
259
|
+
autoPatchOverride?: boolean | null;
|
|
260
|
+
/**
|
|
261
|
+
* Bearer / stack-row user. Cyclone apply follows this user's auto-patch
|
|
262
|
+
* policy and credits for an adopted stack (HOUSE-1), even when
|
|
263
|
+
* `cycloneInstallUserId` is a different Mergestorm account on a shared repo
|
|
264
|
+
* (LOOP-5).
|
|
265
|
+
*/
|
|
266
|
+
keyUserId?: string;
|
|
267
|
+
/**
|
|
268
|
+
* Cyclone GitHub App install user for this owner/repo, when resolved. Its
|
|
269
|
+
* installation clones and pushes; it does not decide auto-patch for another
|
|
270
|
+
* user's stack.
|
|
271
|
+
*/
|
|
272
|
+
cycloneInstallUserId?: string | null;
|
|
273
|
+
/**
|
|
274
|
+
* `same`: the only Cyclone install user is the key user.
|
|
275
|
+
* `different`: a Cyclone install user is not the key user (or more than one).
|
|
276
|
+
* `none`: no Cyclone install monitors this repo.
|
|
277
|
+
* `lookup_failed`: the comparison could not be loaded. Fail closed.
|
|
278
|
+
*/
|
|
279
|
+
cycloneOwnerMatch?: CycloneOwnerMatch;
|
|
227
280
|
layers: StackLayerDto[];
|
|
228
281
|
/** Present when a review-unit row exists for this stack. */
|
|
229
282
|
unit?: StackUnitDto;
|
package/dist/stack-dto.js
CHANGED
package/dist/stack-meta.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export type StackLayerMeta = {
|
|
|
5
5
|
};
|
|
6
6
|
export type LocalStack = {
|
|
7
7
|
layers: StackLayerMeta[];
|
|
8
|
+
autoEnqueueWhenReady?: boolean;
|
|
9
|
+
/** Per-open override carried from `stack create` to `stack submit`; absent = account flag. */
|
|
10
|
+
autoReviewOverride?: boolean;
|
|
11
|
+
autoPatchOverride?: boolean;
|
|
8
12
|
};
|
|
9
13
|
export type StackMeta = {
|
|
10
14
|
version: 2;
|
|
@@ -20,6 +24,14 @@ export declare function loadStackMeta(cwd?: string, stacksRoot?: string): Promis
|
|
|
20
24
|
export declare function emptyStackMeta(trunk: string): StackMeta;
|
|
21
25
|
export declare function activeStack(meta: StackMeta): LocalStack;
|
|
22
26
|
export declare function activeLayers(meta: StackMeta): StackLayerMeta[];
|
|
27
|
+
export declare function setActiveAutoEnqueue(meta: StackMeta, enabled: boolean | undefined): StackMeta;
|
|
28
|
+
/** Per-open automation flags remembered on the active local stack until submit. */
|
|
29
|
+
export type LocalStackPolicy = {
|
|
30
|
+
autoEnqueueWhenReady?: boolean;
|
|
31
|
+
autoReviewOverride?: boolean;
|
|
32
|
+
autoPatchOverride?: boolean;
|
|
33
|
+
};
|
|
34
|
+
export declare function setActivePolicy(meta: StackMeta, policy: LocalStackPolicy): StackMeta;
|
|
23
35
|
export declare function saveStackMeta(meta: StackMeta, cwd?: string, stacksRoot?: string): Promise<void>;
|
|
24
36
|
/** Explicit recovery. Does not touch branches, PRs, or registered server stacks. */
|
|
25
37
|
export declare function resetStackMeta(cwd?: string, stacksRoot?: string): Promise<void>;
|
package/dist/stack-meta.js
CHANGED
|
@@ -8,6 +8,11 @@ import path from "node:path";
|
|
|
8
8
|
import { CommandError, isCommandErrorCode } from "./errors.js";
|
|
9
9
|
import { git } from "./git.js";
|
|
10
10
|
import { cliStacksRoot, gitTopLevel, LEGACY_STACK_META_REL, legacyStackMetaPath, repoStateDir, stackMetaPath as resolveStackMetaPath, } from "./repo-identity.js";
|
|
11
|
+
const LOCAL_STACK_FLAGS = [
|
|
12
|
+
"autoEnqueueWhenReady",
|
|
13
|
+
"autoReviewOverride",
|
|
14
|
+
"autoPatchOverride",
|
|
15
|
+
];
|
|
11
16
|
/** Exported only for legacy migration/tests. New state never uses this path. */
|
|
12
17
|
export const STACK_META_REL = LEGACY_STACK_META_REL;
|
|
13
18
|
export function stackMetaPath(cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
@@ -35,10 +40,18 @@ function normalizeMeta(raw) {
|
|
|
35
40
|
if (!stack ||
|
|
36
41
|
typeof stack !== "object" ||
|
|
37
42
|
!Array.isArray(stack.layers) ||
|
|
38
|
-
!stack.layers.every(isLayer)
|
|
43
|
+
!stack.layers.every(isLayer) ||
|
|
44
|
+
LOCAL_STACK_FLAGS.some((flag) => stack[flag] !== undefined &&
|
|
45
|
+
typeof stack[flag] !== "boolean")) {
|
|
39
46
|
throw new Error("Stack authoring state has a malformed stack entry; run `mg stack reset --force` to clear it.");
|
|
40
47
|
}
|
|
41
|
-
|
|
48
|
+
const entry = { layers: stack.layers };
|
|
49
|
+
for (const flag of LOCAL_STACK_FLAGS) {
|
|
50
|
+
const value = stack[flag];
|
|
51
|
+
if (typeof value === "boolean")
|
|
52
|
+
entry[flag] = value;
|
|
53
|
+
}
|
|
54
|
+
stacks.push(entry);
|
|
42
55
|
}
|
|
43
56
|
const active = obj.active;
|
|
44
57
|
if (typeof active !== "number" ||
|
|
@@ -165,6 +178,22 @@ export function activeStack(meta) {
|
|
|
165
178
|
export function activeLayers(meta) {
|
|
166
179
|
return activeStack(meta).layers;
|
|
167
180
|
}
|
|
181
|
+
export function setActiveAutoEnqueue(meta, enabled) {
|
|
182
|
+
return setActiveStackFlag(meta, "autoEnqueueWhenReady", enabled);
|
|
183
|
+
}
|
|
184
|
+
export function setActivePolicy(meta, policy) {
|
|
185
|
+
let next = meta;
|
|
186
|
+
for (const flag of LOCAL_STACK_FLAGS) {
|
|
187
|
+
next = setActiveStackFlag(next, flag, policy[flag]);
|
|
188
|
+
}
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
function setActiveStackFlag(meta, flag, value) {
|
|
192
|
+
if (value === undefined)
|
|
193
|
+
return meta;
|
|
194
|
+
const stacks = meta.stacks.map((stack, index) => index === meta.active ? { ...stack, [flag]: value } : stack);
|
|
195
|
+
return { ...meta, stacks };
|
|
196
|
+
}
|
|
168
197
|
export async function saveStackMeta(meta, cwd = process.cwd(), stacksRoot = cliStacksRoot()) {
|
|
169
198
|
const file = stackMetaPath(cwd, stacksRoot);
|
|
170
199
|
const dir = path.dirname(file);
|
|
@@ -229,7 +258,7 @@ export function appendLayer(meta, layer) {
|
|
|
229
258
|
}
|
|
230
259
|
}
|
|
231
260
|
const stacks = meta.stacks.map((stack, index) => index === meta.active
|
|
232
|
-
? { layers: [...stack.layers, layer] }
|
|
261
|
+
? { ...stack, layers: [...stack.layers, layer] }
|
|
233
262
|
: stack);
|
|
234
263
|
return { ...meta, stacks };
|
|
235
264
|
}
|
package/dist/ui/banner.js
CHANGED
|
@@ -81,6 +81,12 @@ const WHATS_NEW = {
|
|
|
81
81
|
"mg pr --wait --after-sha, mg settings, mergestorm-pr-loop skill on npm",
|
|
82
82
|
"mg skill install copies review + pr-loop from the packed tarball",
|
|
83
83
|
],
|
|
84
|
+
"0.3.16": [
|
|
85
|
+
"PR loop: verify each finding, prefer a small diff, post mergestorm-loop: dismiss on skip",
|
|
86
|
+
],
|
|
87
|
+
"0.3.17": [
|
|
88
|
+
"nginx 413 names the upload limit instead of Failed: {}",
|
|
89
|
+
],
|
|
84
90
|
};
|
|
85
91
|
function densityFromVariant(variant) {
|
|
86
92
|
if (variant === "mini" || variant === "nano")
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const TOOL_PAYLOAD_KEYS = new Set([
|
|
2
|
+
"function_call",
|
|
3
|
+
"tool",
|
|
4
|
+
"tool_call",
|
|
5
|
+
"tool_calls",
|
|
6
|
+
]);
|
|
7
|
+
function parseJsonContainer(value) {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
10
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]")))) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(trimmed);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function looksLikeToolPayload(value) {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.some(looksLikeToolPayload);
|
|
23
|
+
if (!value || typeof value !== "object")
|
|
24
|
+
return false;
|
|
25
|
+
const record = value;
|
|
26
|
+
const keys = Object.keys(record);
|
|
27
|
+
if (keys.some((key) => TOOL_PAYLOAD_KEYS.has(key.toLowerCase())))
|
|
28
|
+
return true;
|
|
29
|
+
const lower = new Set(keys.map((key) => key.toLowerCase()));
|
|
30
|
+
if (record.type === "tool_use" && lower.has("name") && lower.has("input"))
|
|
31
|
+
return true;
|
|
32
|
+
if (lower.has("verdict") && lower.has("comments"))
|
|
33
|
+
return true;
|
|
34
|
+
return Object.values(record).some(looksLikeToolPayload);
|
|
35
|
+
}
|
|
36
|
+
function matchingJsonEnd(text, start) {
|
|
37
|
+
const open = text[start];
|
|
38
|
+
if (open !== "{" && open !== "[")
|
|
39
|
+
return null;
|
|
40
|
+
const stack = [open];
|
|
41
|
+
let quoted = false;
|
|
42
|
+
let escaped = false;
|
|
43
|
+
for (let i = start + 1; i < text.length; i += 1) {
|
|
44
|
+
const char = text[i];
|
|
45
|
+
if (quoted) {
|
|
46
|
+
if (escaped)
|
|
47
|
+
escaped = false;
|
|
48
|
+
else if (char === "\\")
|
|
49
|
+
escaped = true;
|
|
50
|
+
else if (char === '"')
|
|
51
|
+
quoted = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '"') {
|
|
55
|
+
quoted = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (char === "{" || char === "[")
|
|
59
|
+
stack.push(char);
|
|
60
|
+
else if (char === "}" || char === "]") {
|
|
61
|
+
const expected = char === "}" ? "{" : "[";
|
|
62
|
+
if (stack.pop() !== expected)
|
|
63
|
+
return null;
|
|
64
|
+
if (stack.length === 0)
|
|
65
|
+
return i + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function fencedLines(text) {
|
|
71
|
+
const lines = text.split("\n");
|
|
72
|
+
const protectedLines = new Set();
|
|
73
|
+
const openFences = [];
|
|
74
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
75
|
+
if (/^\s*```[a-z0-9_-]+\s*$/i.test(lines[i])) {
|
|
76
|
+
openFences.push(i);
|
|
77
|
+
}
|
|
78
|
+
else if (/^\s*```\s*$/.test(lines[i])) {
|
|
79
|
+
if (openFences.length > 0) {
|
|
80
|
+
const start = openFences.pop();
|
|
81
|
+
for (let line = start; line <= i; line += 1)
|
|
82
|
+
protectedLines.add(line);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
openFences.push(i);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return protectedLines;
|
|
90
|
+
}
|
|
91
|
+
function stripBoundaryPayload(text) {
|
|
92
|
+
let result = text;
|
|
93
|
+
let changed = true;
|
|
94
|
+
while (changed) {
|
|
95
|
+
changed = false;
|
|
96
|
+
const protectedLines = fencedLines(result);
|
|
97
|
+
for (let start = 0; start < result.length; start += 1) {
|
|
98
|
+
if (result[start] !== "{" && result[start] !== "[")
|
|
99
|
+
continue;
|
|
100
|
+
if (protectedLines.has(result.slice(0, start).split("\n").length - 1))
|
|
101
|
+
continue;
|
|
102
|
+
const end = matchingJsonEnd(result, start);
|
|
103
|
+
if (end == null)
|
|
104
|
+
continue;
|
|
105
|
+
const parsed = parseJsonContainer(result.slice(start, end));
|
|
106
|
+
if (parsed !== undefined &&
|
|
107
|
+
looksLikeToolPayload(parsed)) {
|
|
108
|
+
const before = result.slice(0, start);
|
|
109
|
+
const after = result.slice(end);
|
|
110
|
+
result = `${before}${before.endsWith("\n") && after.startsWith("\n") ? after.slice(1) : after}`;
|
|
111
|
+
changed = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function stripOrphanLines(text) {
|
|
119
|
+
const lines = text.split("\n");
|
|
120
|
+
const keptLines = new Set();
|
|
121
|
+
const protectedLines = fencedLines(text);
|
|
122
|
+
for (const line of protectedLines)
|
|
123
|
+
keptLines.add(line);
|
|
124
|
+
for (let start = 0; start < text.length; start += 1) {
|
|
125
|
+
if (text[start] !== "{" && text[start] !== "[")
|
|
126
|
+
continue;
|
|
127
|
+
const end = matchingJsonEnd(text, start);
|
|
128
|
+
if (end == null || parseJsonContainer(text.slice(start, end)) === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
const i = text.slice(0, start).split("\n").length - 1;
|
|
131
|
+
const lastLine = text.slice(0, end).split("\n").length - 1;
|
|
132
|
+
for (let line = i; line <= lastLine; line += 1) {
|
|
133
|
+
keptLines.add(line);
|
|
134
|
+
if (i !== lastLine)
|
|
135
|
+
protectedLines.add(line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return lines
|
|
139
|
+
.filter((line, i) => protectedLines.has(i) ||
|
|
140
|
+
(parseJsonContainer(line) === undefined &&
|
|
141
|
+
(keptLines.has(i) || !/^\s*(?:```|[{}])\s*$/.test(line))))
|
|
142
|
+
.join("\n");
|
|
143
|
+
}
|
|
144
|
+
/** Remove tool/LLM JSON that is unsafe to show in human summary output. */
|
|
145
|
+
export function humanSummary(summary) {
|
|
146
|
+
let result = summary.replace(/\r\n?/g, "\n");
|
|
147
|
+
result = result.replace(/```json[ \t]*\n([\s\S]*?)```/gi, (block, body) => parseJsonContainer(body) === undefined ? block : "");
|
|
148
|
+
result = stripBoundaryPayload(result);
|
|
149
|
+
result = stripOrphanLines(result)
|
|
150
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
151
|
+
.trim();
|
|
152
|
+
return result || null;
|
|
153
|
+
}
|
package/dist/ui/usage.d.ts
CHANGED
|
@@ -22,8 +22,6 @@ export declare function nextUtcMonthStart(now?: Date): string;
|
|
|
22
22
|
export declare function formatResetLabel(iso: string | null | undefined, now?: Date): string;
|
|
23
23
|
/** How long until `iso` (the API `resets_at`). Empty when the date is unusable. */
|
|
24
24
|
export declare function formatDaysLeft(iso: string | null | undefined, now?: Date): string;
|
|
25
|
-
/** Alias kept for callers that still import `formatResetDate`. */
|
|
26
|
-
export declare function formatResetDate(iso: string | null | undefined): string;
|
|
27
25
|
/** Bar cells so ` [bar] 100% used` stays inside the terminal. */
|
|
28
26
|
export declare function usageBarWidth(columns: number): number;
|
|
29
27
|
export type UsagePanelJob = {
|
|
@@ -38,6 +36,7 @@ export type UsagePanelInput = {
|
|
|
38
36
|
plan: string;
|
|
39
37
|
used: number;
|
|
40
38
|
limit: number | null;
|
|
39
|
+
bonusRemaining?: number;
|
|
41
40
|
resetsAt: string | null | undefined;
|
|
42
41
|
jobs: UsagePanelJob[];
|
|
43
42
|
/** When the jobs fetch failed, show this instead of "No review jobs yet." */
|
package/dist/ui/usage.js
CHANGED
|
@@ -83,10 +83,6 @@ export function formatDaysLeft(iso, now = new Date()) {
|
|
|
83
83
|
return `${hours}h left`;
|
|
84
84
|
return "resets soon";
|
|
85
85
|
}
|
|
86
|
-
/** Alias kept for callers that still import `formatResetDate`. */
|
|
87
|
-
export function formatResetDate(iso) {
|
|
88
|
-
return formatResetLabel(iso);
|
|
89
|
-
}
|
|
90
86
|
/** Bar cells so ` [bar] 100% used` stays inside the terminal. */
|
|
91
87
|
export function usageBarWidth(columns) {
|
|
92
88
|
return Math.max(8, frameWidth(80, columns) - 13);
|
|
@@ -112,6 +108,9 @@ export function formatUsagePanel(input) {
|
|
|
112
108
|
` ${identity}`,
|
|
113
109
|
` ${bar}`,
|
|
114
110
|
` ${formatResetLabel(input.resetsAt)}`,
|
|
111
|
+
...(input.bonusRemaining == null
|
|
112
|
+
? []
|
|
113
|
+
: [` Bonus credits: ${input.bonusRemaining} remaining`]),
|
|
115
114
|
"",
|
|
116
115
|
];
|
|
117
116
|
if (input.jobs.length === 0) {
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -62,11 +62,11 @@ Default: omit `router` (account setting).
|
|
|
62
62
|
|
|
63
63
|
## Submit
|
|
64
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
|
|
65
|
+
Call `review_submit` with `wait: false` (the default) 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
66
|
|
|
67
|
-
Optional: `thread`, `idempotency_key`, `wait`, `timeout_s`.
|
|
67
|
+
Optional: `thread`, `idempotency_key`, `wait`, `timeout_s`. Submit returns without waiting. Record `job_id` as soon as it returns, then loop `review_wait` with that `job_id` and `timeout_s: 45` until the job finishes. Each call is one 45s slice. On timeout, call `review_wait` again with the same `job_id`. Do not pass 300; hosts drop long MCP calls. Explicit `wait: true` also defaults to one 45s slice.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Call `review_list` before any resubmit, including when a submit or wait call is interrupted. Resume the existing job with `review_get` or `review_wait`; do not create a second job because a slice died or the submit response was lost.
|
|
70
70
|
|
|
71
71
|
Status handling:
|
|
72
72
|
|
|
@@ -18,24 +18,36 @@ mg skill install --claude --cursor
|
|
|
18
18
|
## Preconditions (in order)
|
|
19
19
|
|
|
20
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
|
|
22
|
-
3.
|
|
21
|
+
2. Auto-patch (Cyclone) must be off for this PR so you are the only patcher. Do not take anyone's word for it: the settings API and the PR's stack are the source of truth, and step 1 of the loop verifies them before anything else runs. Cyclone is off for the PR when its adopted stack has `autoPatchOverride` `false`, or when that override is unset and the account `auto_patch_enabled` is `false`.
|
|
22
|
+
3. The Bearer key and the Cyclone GitHub App install must be the same Mergestorm account. `stack_list` / `stack_status` expose `keyUserId`, `cycloneInstallUserId`, and `cycloneOwnerMatch`. Cyclone apply always uses the install account, not the key. If those identities differ, your `settings_get` mute reading is about the wrong person.
|
|
23
|
+
4. `whoami` works on the `mergestorm` MCP server (or `mg whoami --json` exits 0). Otherwise stop and ask for install/login.
|
|
23
24
|
|
|
24
25
|
## Loop (in order)
|
|
25
26
|
|
|
26
|
-
1.
|
|
27
|
+
1. Find the PR's stack with `stack_list` (the stack whose `layers` contain this PR's `prNumber` for this owner/repo). If you already have its `stack_id`, `stack_status` is the same payload.
|
|
28
|
+
- If there is no stack and the human asked you to mute Cyclone or continue the loop: call `stack_adopt` with `{ owner, repo, pr_number, auto_patch: false }` (equivalent to `mg stack adopt … --auto-patch off`). Then call `stack_status` with the returned `result.stack.id`, confirm this PR is in the stack and `autoPatchOverride` is `false`. If adoption or verification fails, refuse the loop. Otherwise continue through the ownership check below and the remaining loop steps; do not stop and ask the human to adopt. Do not turn account auto-patch off.
|
|
29
|
+
- Adoption does not fix installer ≠ key. After adoption, the same `cycloneOwnerMatch` refusal below still applies.
|
|
30
|
+
- Read `cycloneOwnerMatch` on that stack. If it is missing, `lookup_failed`, or `different`: refuse the loop. Do not wait, patch, or push. Tell the human the API key and the Cyclone GitHub App install are different Mergestorm accounts (or the comparison could not be loaded). Cyclone still patches as the installer. Do not turn account auto-patch off, and do not set `auto_patch: false` to paper over this.
|
|
31
|
+
- If it is `same` or `none`: continue. `none` means no Cyclone install monitors this repo.
|
|
32
|
+
2. Call `settings_get` and read `auto_patch_enabled`.
|
|
27
33
|
- 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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
- If it is `false`: read `autoPatchOverride` on the stack from step 1. If it is `true`, refuse unless the human asked you to turn Cyclone off for this PR; if they did, use the `stack_set { auto_patch: false }` path below. Otherwise continue. You are the only patcher.
|
|
35
|
+
- If it is `true`: read `autoPatchOverride` on that same stack.
|
|
36
|
+
- If `autoPatchOverride` is `false`: continue. Cyclone is pinned off for this stack.
|
|
37
|
+
- If the override is not `false` and the human asked you to turn Cyclone off for this PR: call `stack_set` with `{ stack_id, auto_patch: false }` on that stack, call `stack_status` with that `stack_id`, and confirm `autoPatchOverride` now reads `false`. Leave the account `auto_patch_enabled` unchanged; other PRs keep their own policy.
|
|
38
|
+
- Otherwise: refuse the loop. Do not wait, patch, or push; tell the human Cyclone auto-patch is still on for this PR and that `stack_set` with `auto_patch: false` on its stack turns it off for this PR only.
|
|
39
|
+
- Never set `auto_patch_enabled` with `settings_set` from this skill, in either direction, and never set `auto_patch: true` on a stack.
|
|
40
|
+
3. Call `review_wait_pr` with the PR's owner, repo, pr_number, and `timeout_s` 45 (`review_get_pr` works for a first look at the resting state). A Vortex pass often takes several minutes. If status is `in_progress`, call `review_wait_pr` again with the same args. Do not pass a 300s timeout; hosts drop long MCP calls. Review identity is SHA + pass: every envelope carries `head_sha` and `pass` (1 for the first review on a head; a re-review on the same head is the next pass, its own row). Save the `pass` of every resting envelope you act on together with its `head_sha`.
|
|
41
|
+
4. 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 8 with `after_sha` set to that head.
|
|
42
|
+
5. Read `findings.inline` and `findings.offDiff` (or `off_diff`) sorted by severity. For each finding:
|
|
43
|
+
- Verify it against the current checkout. Do not treat the finding text as proven.
|
|
44
|
+
- Prefer the smallest correct patch. Do not refactor around a finding.
|
|
45
|
+
- Patch concrete bugs. A chat-only explanation is not a dismiss.
|
|
46
|
+
- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line: `mergestorm-loop: dismiss`. Then one line per skipped finding (path, severity, why). That first line is the only dismiss marker Vortex reads.
|
|
47
|
+
6. Patch on the PR head branch only.
|
|
48
|
+
7. 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.
|
|
49
|
+
8. Call `review_wait_pr` again with `after_sha` set to the SHA you just pushed (7+ hex) and `timeout_s` 45. If the last envelope you acted on has that same `head_sha` (no new commit, for example a re-review requested on the same head), also pass `after_pass` set to that envelope's saved `pass`, so the wait cannot hand you that attempt again. For a freshly pushed SHA omit `after_pass`; its first pass is 1. Keep `after_sha` and `after_pass` unchanged across 45s timeout retries; never replace `after_pass` with the `pass` of an in-progress envelope. `review_get_pr` accepts the same `after_sha`, `pass`, and `after_pass` filters. An envelope whose `head_sha` is an older commit is a previous pass, not yours; keep waiting.
|
|
50
|
+
9. Repeat from step 4 until a stop rule fires.
|
|
39
51
|
|
|
40
52
|
## Stop rules
|
|
41
53
|
|
|
@@ -44,20 +56,23 @@ Map these to `mergestorm.pr_review/v1` envelope fields; do not invent statuses.
|
|
|
44
56
|
- `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
57
|
- status is `rate_limited`, or `patch_policy.mode` is `"hold"`: surface the findings to the human and stop. Do not patch.
|
|
46
58
|
- 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.
|
|
59
|
+
- 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` and the same `after_pass`.
|
|
60
|
+
- 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` and the same `after_pass`. 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
61
|
- status is `skipped`, `stopped`, `trial_expired`, or `synced`: surface the status and reason to the human and stop. Do not patch or push.
|
|
50
62
|
|
|
51
63
|
## Never
|
|
52
64
|
|
|
53
65
|
- 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
|
-
-
|
|
66
|
+
- land, merge, enqueue, or touch stack authoring beyond adopting the existing PR, or the merge queue
|
|
67
|
+
- change the account `auto_patch_enabled` from this skill, for any reason; Cyclone is muted per stack with `stack_adopt` or `stack_set`, never per account
|
|
68
|
+
- treat the key user's mute or `auto_patch_enabled` as covering a different Cyclone install user
|
|
69
|
+
- set `auto_patch: true` on any stack
|
|
56
70
|
- push any branch other than the PR head
|
|
57
71
|
- force-push, or rebase and retry a push that was rejected because the remote moved
|
|
58
72
|
- patch a pass whose `head_sha` is not the commit you have checked out
|
|
59
73
|
- wrap the CLI in a shell escape hatch
|
|
60
74
|
- claim a finding is fixed without a diff that touches its path
|
|
75
|
+
- claim a finding is dismissed without posting a `mergestorm-loop: dismiss` comment on the PR
|
|
61
76
|
|
|
62
77
|
## Fallback (no MCP)
|
|
63
78
|
|
|
@@ -67,10 +82,10 @@ Verify auto-patch with the CLI instead of `settings_get`:
|
|
|
67
82
|
mg settings --json
|
|
68
83
|
```
|
|
69
84
|
|
|
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`
|
|
85
|
+
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. Run `mg stack list --json` and find the stack whose layers contain this PR. If there is no stack, or its `cycloneOwnerMatch` is missing, `lookup_failed`, or `different`, refuse the loop. Tell the human the API key and the Cyclone install are different accounts (or adopt first, or the lookup failed). Do not run `mg settings --auto-patch off`. If `cycloneOwnerMatch` is `same` or `none` and `autoPatchOverride` is `false`, continue. If it is unset and the account flag is `false`, continue. If it is `true`, or it is unset while the account flag is `true`, refuse unless the human asked you to turn Cyclone off for this PR; only then run `mg stack set <stack-id> --auto-patch off` and re-read with `mg stack list --json`.
|
|
71
86
|
|
|
72
87
|
```bash
|
|
73
|
-
mg pr <owner/repo>#<n> --json --wait --after-sha <sha> --timeout <s>
|
|
88
|
+
mg pr <owner/repo>#<n> --json --wait --after-sha <sha> --after-pass <saved-pass> --timeout <s>
|
|
74
89
|
```
|
|
75
90
|
|
|
76
|
-
Parse stdout as one `mergestorm.pr_review/v1` object. Same fields, same stop rules.
|
|
91
|
+
Parse stdout as one `mergestorm.pr_review/v1` object. Same fields, same stop rules. `--after-pass` follows the same rule as `after_pass` above: only when the last envelope you acted on is on that same `head_sha`; omit it for a freshly pushed SHA. `--pass <n>` reads one exact attempt on the head.
|