mergestorm 0.3.17 → 0.3.19
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 -3
- package/dist/api.d.ts +7 -2
- package/dist/api.js +42 -2
- package/dist/automation-catalog.d.ts +48 -0
- package/dist/automation-catalog.js +11 -0
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.js +3 -1
- package/dist/commands/browse.d.ts +1 -1
- package/dist/commands/browse.js +3 -3
- package/dist/commands/help.js +1 -1
- package/dist/commands/queue.d.ts +1 -1
- package/dist/commands/queue.js +21 -6
- package/dist/commands/registry.js +15 -7
- package/dist/commands/review-client.js +6 -11
- package/dist/commands/review.js +9 -1
- package/dist/commands/settings.d.ts +12 -4
- package/dist/commands/settings.js +78 -16
- package/dist/commands/skill.d.ts +4 -3
- package/dist/commands/skill.js +16 -6
- package/dist/commands/stack.d.ts +14 -2
- package/dist/commands/stack.js +89 -4
- package/dist/errors.d.ts +1 -1
- package/dist/stack-dto.d.ts +54 -2
- package/dist/stack-dto.js +37 -1
- package/dist/stack-watch.d.ts +44 -0
- package/dist/stack-watch.js +232 -0
- package/dist/ui/banner.js +6 -0
- package/package.json +1 -1
- package/skill/SKILL.md +6 -4
- package/skill/mergestorm-bounce-watch/SKILL.md +50 -0
- package/skill/mergestorm-pr-loop/SKILL.md +8 -4
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { getSettings, patchSettings, } from "../api.js";
|
|
2
|
-
import { BEARER_SETTINGS_FLAGS } from "../automation-catalog.js";
|
|
2
|
+
import { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS } from "../automation-catalog.js";
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { CommandError } from "../errors.js";
|
|
5
5
|
import { present } from "../ui/present.js";
|
|
6
6
|
import { buildConfigRows, canBrowse, openTabsBrowser } from "./browse.js";
|
|
7
7
|
/**
|
|
8
|
-
* CLI flag → Bearer settings key. on|off
|
|
8
|
+
* CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
|
|
9
9
|
* read-only on the API and deliberately have no flag here.
|
|
10
10
|
*/
|
|
11
11
|
export const SETTINGS_FLAGS = BEARER_SETTINGS_FLAGS;
|
|
12
|
-
|
|
12
|
+
function canonicalLogin(raw) {
|
|
13
|
+
const login = raw.trim().toLowerCase().replace(/\[bot\]$/, "");
|
|
14
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(login)) {
|
|
15
|
+
throw new CommandError(`Invalid bot login: ${raw}.`, 2, "usage");
|
|
16
|
+
}
|
|
17
|
+
return login;
|
|
18
|
+
}
|
|
19
|
+
/** Parse boolean and enum flags plus ordered ignore-list operations. */
|
|
13
20
|
export function parseSettingsArgs(args) {
|
|
14
21
|
let json = false;
|
|
22
|
+
const ignoreBots = [];
|
|
15
23
|
const patch = {};
|
|
16
24
|
for (let i = 0; i < args.length; i++) {
|
|
17
25
|
const arg = args[i];
|
|
@@ -26,37 +34,91 @@ export function parseSettingsArgs(args) {
|
|
|
26
34
|
throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
|
|
27
35
|
}
|
|
28
36
|
const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
|
|
37
|
+
const row = BEARER_SETTINGS.find((row) => row.key === key);
|
|
38
|
+
if ("kind" in row && row.kind === "logins") {
|
|
39
|
+
if (value === "clear")
|
|
40
|
+
ignoreBots.push({ action: "clear" });
|
|
41
|
+
else if ((value === "add" || value === "remove") && args[i + 1] && !args[i + 1].startsWith("--")) {
|
|
42
|
+
ignoreBots.push({ action: value, login: canonicalLogin(args[++i]) });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new CommandError(`${flag} takes add <login>, remove <login>, or clear.`, 2, "usage");
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if ("kind" in row && row.kind === "enum") {
|
|
50
|
+
if (value === undefined || !row.values.includes(value)) {
|
|
51
|
+
throw new CommandError(`${flag} takes ${row.values.join(" or ")}.`, 2, "usage");
|
|
52
|
+
}
|
|
53
|
+
Object.assign(patch, { [key]: value });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
29
56
|
if (value !== "on" && value !== "off") {
|
|
30
57
|
throw new CommandError(`${flag} takes on or off.`, 2, "usage");
|
|
31
58
|
}
|
|
32
|
-
patch[key]
|
|
59
|
+
Object.assign(patch, { [key]: value === "on" });
|
|
33
60
|
}
|
|
34
|
-
return { json, patch };
|
|
61
|
+
return { json, patch, ...(ignoreBots.length ? { ignoreBots } : {}) };
|
|
35
62
|
}
|
|
36
|
-
/** Static printout
|
|
63
|
+
/** Static printout includes the Config toggles plus enum and login settings. */
|
|
37
64
|
export function formatSettingsLines(settings) {
|
|
38
|
-
const rows =
|
|
65
|
+
const rows = [
|
|
66
|
+
...buildConfigRows(settings),
|
|
67
|
+
...BEARER_SETTINGS.filter((row) => "kind" in row).map((row) => ({
|
|
68
|
+
...row, writable: true, value: settings[row.key],
|
|
69
|
+
})),
|
|
70
|
+
];
|
|
39
71
|
const labelWidth = Math.max(...rows.map((r) => r.label.length));
|
|
40
72
|
return rows.map((row) => {
|
|
41
|
-
const value = row.
|
|
42
|
-
?
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
const value = "kind" in row && row.value === undefined
|
|
74
|
+
? "(unavailable)"
|
|
75
|
+
: Array.isArray(row.value)
|
|
76
|
+
? row.value.join(", ") || "(empty)"
|
|
77
|
+
: typeof row.value === "string"
|
|
78
|
+
? row.value
|
|
79
|
+
: row.writable
|
|
80
|
+
? row.value
|
|
81
|
+
? "on"
|
|
82
|
+
: "off"
|
|
83
|
+
: row.value
|
|
84
|
+
? "connected"
|
|
85
|
+
: "not connected";
|
|
48
86
|
return ` ${row.label.padEnd(labelWidth + 2)}${value}`;
|
|
49
87
|
});
|
|
50
88
|
}
|
|
51
89
|
export async function cmdSettings(args, opts = {}) {
|
|
52
|
-
const { json, patch } = parseSettingsArgs(args);
|
|
53
|
-
const hasPatch = Object.keys(patch).length > 0;
|
|
90
|
+
const { json, patch, ignoreBots } = parseSettingsArgs(args);
|
|
91
|
+
const hasPatch = Object.keys(patch).length > 0 || Boolean(ignoreBots?.length);
|
|
54
92
|
// Bare `settings` on a TTY: the tabbed browser, opened on Config.
|
|
55
93
|
if (!hasPatch && !json && canBrowse()) {
|
|
56
94
|
await openTabsBrowser("config");
|
|
57
95
|
return;
|
|
58
96
|
}
|
|
59
97
|
const cfg = await loadConfig();
|
|
98
|
+
if (ignoreBots?.length) {
|
|
99
|
+
// A leading clear needs no read. Otherwise fetch before writing the full list.
|
|
100
|
+
const current = ignoreBots[0].action === "clear" ? null : await getSettings(cfg);
|
|
101
|
+
if (ignoreBots[0].action !== "clear" && !current) {
|
|
102
|
+
throw new CommandError("Settings are not available; cannot update ignored bot logins.");
|
|
103
|
+
}
|
|
104
|
+
let logins = [...new Set((current?.ignored_bot_logins ?? []).flatMap((raw) => {
|
|
105
|
+
try {
|
|
106
|
+
return [canonicalLogin(raw)];
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}))];
|
|
112
|
+
for (const op of ignoreBots) {
|
|
113
|
+
if (op.action === "clear")
|
|
114
|
+
logins = [];
|
|
115
|
+
else if (op.action === "remove")
|
|
116
|
+
logins = logins.filter((login) => login !== op.login);
|
|
117
|
+
else if (!logins.includes(op.login))
|
|
118
|
+
logins.push(op.login);
|
|
119
|
+
}
|
|
120
|
+
patch.ignored_bot_logins = logins;
|
|
121
|
+
}
|
|
60
122
|
let settings;
|
|
61
123
|
if (hasPatch) {
|
|
62
124
|
settings = await patchSettings(patch, cfg);
|
package/dist/commands/skill.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare const SKILL_NAME = "mergestorm-review";
|
|
2
2
|
export declare const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const BOUNCE_WATCH_SKILL_NAME = "mergestorm-bounce-watch";
|
|
4
|
+
export declare const SKILL_NAMES: readonly ["mergestorm-review", "mergestorm-pr-loop", "mergestorm-bounce-watch"];
|
|
4
5
|
export type SkillName = (typeof SKILL_NAMES)[number];
|
|
5
|
-
export declare const SKILL_USAGE
|
|
6
|
-
export type SkillTarget = "claude" | "cursor";
|
|
6
|
+
export declare const SKILL_USAGE: string;
|
|
7
|
+
export type SkillTarget = "claude" | "cursor" | "agents";
|
|
7
8
|
export type SkillInstallRequest = {
|
|
8
9
|
help: boolean;
|
|
9
10
|
json: boolean;
|
package/dist/commands/skill.js
CHANGED
|
@@ -7,14 +7,16 @@ import { gitTopLevel } from "../repo-identity.js";
|
|
|
7
7
|
import { present } from "../ui/present.js";
|
|
8
8
|
export const SKILL_NAME = "mergestorm-review";
|
|
9
9
|
export const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
|
|
10
|
-
export const
|
|
10
|
+
export const BOUNCE_WATCH_SKILL_NAME = "mergestorm-bounce-watch";
|
|
11
|
+
export const SKILL_NAMES = [SKILL_NAME, PR_LOOP_SKILL_NAME, BOUNCE_WATCH_SKILL_NAME];
|
|
11
12
|
export const SKILL_USAGE = `usage:
|
|
12
|
-
mergestorm skill install --claude|--cursor [--json]
|
|
13
|
+
mergestorm skill install --claude|--cursor|--agents [--json]
|
|
13
14
|
|
|
14
|
-
Copies
|
|
15
|
+
Copies ${SKILL_NAMES.join(", ")} into this repo (git toplevel, else cwd):
|
|
15
16
|
--claude .claude/skills/<name>/SKILL.md
|
|
16
17
|
--cursor .cursor/skills/<name>/SKILL.md
|
|
17
|
-
|
|
18
|
+
--agents .agents/skills/<name>/SKILL.md
|
|
19
|
+
Pass multiple flags to write multiple trees. Overwrites existing copies.`;
|
|
18
20
|
export function parseSkillArgs(args) {
|
|
19
21
|
const [sub, ...rest] = args;
|
|
20
22
|
if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
|
|
@@ -40,13 +42,18 @@ export function parseSkillArgs(args) {
|
|
|
40
42
|
targets.push("cursor");
|
|
41
43
|
continue;
|
|
42
44
|
}
|
|
45
|
+
if (arg === "--agents") {
|
|
46
|
+
if (!targets.includes("agents"))
|
|
47
|
+
targets.push("agents");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
43
50
|
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
44
51
|
return { help: true, json: false, targets: [] };
|
|
45
52
|
}
|
|
46
53
|
throw new CommandError(`${SKILL_USAGE}\nunknown flag: ${arg}`, 1, "usage");
|
|
47
54
|
}
|
|
48
55
|
if (targets.length === 0) {
|
|
49
|
-
throw new CommandError(`${SKILL_USAGE}\npass --claude and/or --
|
|
56
|
+
throw new CommandError(`${SKILL_USAGE}\npass --claude, --cursor, and/or --agents`, 1, "usage");
|
|
50
57
|
}
|
|
51
58
|
return { help: false, json, targets };
|
|
52
59
|
}
|
|
@@ -65,6 +72,9 @@ export function skillDestination(root, target, name = SKILL_NAME) {
|
|
|
65
72
|
if (target === "claude") {
|
|
66
73
|
return path.join(root, ".claude", "skills", name, "SKILL.md");
|
|
67
74
|
}
|
|
75
|
+
if (target === "agents") {
|
|
76
|
+
return path.join(root, ".agents", "skills", name, "SKILL.md");
|
|
77
|
+
}
|
|
68
78
|
return path.join(root, ".cursor", "skills", name, "SKILL.md");
|
|
69
79
|
}
|
|
70
80
|
export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL_NAME) {
|
|
@@ -90,7 +100,7 @@ export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL
|
|
|
90
100
|
}
|
|
91
101
|
async function assertNoSymlink(dest) {
|
|
92
102
|
const parts = dest.split(path.sep);
|
|
93
|
-
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor");
|
|
103
|
+
const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor" || p === ".agents");
|
|
94
104
|
const first = marker === -1 ? parts.length - 1 : marker;
|
|
95
105
|
for (let i = first; i < parts.length; i++) {
|
|
96
106
|
const candidate = parts.slice(0, i + 1).join(path.sep);
|
package/dist/commands/stack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
1
|
+
import { adoptStack, ensureUpperPark, getEnrichedStack, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
3
|
import { createPr, findOpenPrNumber } from "../gh.js";
|
|
4
4
|
import { parseGithubOriginRepo } from "../git.js";
|
|
@@ -6,6 +6,7 @@ import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defa
|
|
|
6
6
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
7
7
|
import { gitTopLevel } from "../repo-identity.js";
|
|
8
8
|
import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
|
|
9
|
+
import { pollStackWatch } from "../stack-watch.js";
|
|
9
10
|
/** A registered stack layer that would become the parent of a new layer. */
|
|
10
11
|
export type RegisteredParentHit = {
|
|
11
12
|
stackId: string;
|
|
@@ -120,6 +121,15 @@ export type StackSubmitDeps = {
|
|
|
120
121
|
ensureUpperPark?: typeof ensureUpperPark;
|
|
121
122
|
};
|
|
122
123
|
export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
|
|
124
|
+
export declare function cmdStackWait(args: string[], deps?: {
|
|
125
|
+
loadConfig?: typeof loadConfig;
|
|
126
|
+
pollStackWatch?: typeof pollStackWatch;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}): Promise<void>;
|
|
129
|
+
export declare function cmdStackStatus(args: string[], deps?: {
|
|
130
|
+
loadConfig?: typeof loadConfig;
|
|
131
|
+
getEnrichedStack?: typeof getEnrichedStack;
|
|
132
|
+
}): Promise<void>;
|
|
123
133
|
export type StackSetDeps = {
|
|
124
134
|
loadConfig?: typeof loadConfig;
|
|
125
135
|
setStackPolicy?: typeof setStackPolicy;
|
|
@@ -127,4 +137,6 @@ export type StackSetDeps = {
|
|
|
127
137
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
128
138
|
export declare function stackSetSummary(parsed: StackSetArgs): string[];
|
|
129
139
|
export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
|
|
130
|
-
export declare function cmdStack(args: string[]
|
|
140
|
+
export declare function cmdStack(args: string[], options?: {
|
|
141
|
+
signal?: AbortSignal;
|
|
142
|
+
}): Promise<void>;
|
package/dist/commands/stack.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
|
|
1
|
+
import { adoptStack, ensureUpperPark, getEnrichedStack, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
|
|
2
2
|
import { loadConfig } from "../config.js";
|
|
3
|
-
import { CommandError } from "../errors.js";
|
|
3
|
+
import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
|
|
4
4
|
import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
|
|
5
5
|
import { parseGithubOriginRepo } from "../git.js";
|
|
6
6
|
import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
|
|
7
7
|
import { buildPrBodyFromCommit } from "../pr-body.js";
|
|
8
8
|
import { gitTopLevel } from "../repo-identity.js";
|
|
9
9
|
import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
|
|
10
|
+
import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "../stack-watch.js";
|
|
10
11
|
import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
|
|
11
12
|
import { ansi } from "../ui/ansi.js";
|
|
12
13
|
import { runLineTabsBrowser } from "../ui/line-tabs.js";
|
|
@@ -18,6 +19,8 @@ const STACK_USAGE = `usage:
|
|
|
18
19
|
mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
19
20
|
mergestorm stack reset --force
|
|
20
21
|
mergestorm stack list [--json]
|
|
22
|
+
mergestorm stack status <stack-id> [--json]
|
|
23
|
+
mergestorm stack wait <stack-id> [--json] [--timeout <s>]
|
|
21
24
|
mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
|
|
22
25
|
mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
|
|
23
26
|
mergestorm stack restack <stack-id> [--json]
|
|
@@ -143,7 +146,7 @@ export function stackPolicyLabels(s) {
|
|
|
143
146
|
}
|
|
144
147
|
return labels;
|
|
145
148
|
}
|
|
146
|
-
function formatStackHuman(s) {
|
|
149
|
+
function formatStackHuman(s, includeLayers = true) {
|
|
147
150
|
const lines = [];
|
|
148
151
|
lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
|
|
149
152
|
stackPolicyLabels(s)
|
|
@@ -154,6 +157,8 @@ function formatStackHuman(s) {
|
|
|
154
157
|
lines.push(ansi.dim(" (no layers)"));
|
|
155
158
|
return lines;
|
|
156
159
|
}
|
|
160
|
+
if (!includeLayers)
|
|
161
|
+
return lines;
|
|
157
162
|
for (const layer of s.layers) {
|
|
158
163
|
const title = layer.title ? ` ${layer.title}` : "";
|
|
159
164
|
const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
|
|
@@ -787,6 +792,82 @@ async function cmdStackList(args) {
|
|
|
787
792
|
listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
|
|
788
793
|
await present("Stacks", listed);
|
|
789
794
|
}
|
|
795
|
+
export async function cmdStackWait(args, deps = {}) {
|
|
796
|
+
const usage = "usage: mergestorm stack wait <stack-id> [--json] [--timeout <s>]";
|
|
797
|
+
const positional = [];
|
|
798
|
+
let timeoutS = 45;
|
|
799
|
+
for (let i = 0; i < args.length; i++) {
|
|
800
|
+
const arg = args[i];
|
|
801
|
+
if (arg === "--json")
|
|
802
|
+
continue;
|
|
803
|
+
if (arg === "--timeout" || arg.startsWith("--timeout=")) {
|
|
804
|
+
const value = arg === "--timeout" ? args[++i] : arg.slice("--timeout=".length);
|
|
805
|
+
timeoutS = value?.trim() ? Number(value) : NaN;
|
|
806
|
+
if (!Number.isFinite(timeoutS) || timeoutS < 0 || timeoutS > 300) {
|
|
807
|
+
throw new CommandError(`${usage}\n--timeout must be between 0 and 300 seconds`, REVIEW_EXIT.usage, "usage");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else if (arg.startsWith("-")) {
|
|
811
|
+
throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
positional.push(arg);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (positional.length !== 1)
|
|
818
|
+
throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
|
|
819
|
+
const stackId = requireStackId(positional[0], usage);
|
|
820
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
821
|
+
const print = (envelope, retryAfterSeconds) => console.log(args.includes("--json")
|
|
822
|
+
? JSON.stringify({
|
|
823
|
+
...envelope,
|
|
824
|
+
...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
|
|
825
|
+
}, null, 2)
|
|
826
|
+
: `Stack ${envelope.stackId} · ${envelope.status}${envelope.blocker ? ` · ${envelope.blocker}` : ""}`);
|
|
827
|
+
try {
|
|
828
|
+
print(await (deps.pollStackWatch ?? pollStackWatch)(cfg, stackId, {
|
|
829
|
+
timeoutMs: timeoutS * 1000,
|
|
830
|
+
signal: deps.signal,
|
|
831
|
+
}));
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
if (err instanceof StackWatchError && err.lastEnvelope.status === "rate_limited") {
|
|
835
|
+
print(err.lastEnvelope, err.retryAfterSeconds);
|
|
836
|
+
throw new CommandError(rateLimitedMessage(err.retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds: err.retryAfterSeconds });
|
|
837
|
+
}
|
|
838
|
+
if (!(err instanceof StackWatchTimeoutError))
|
|
839
|
+
throw err;
|
|
840
|
+
print(err.lastEnvelope);
|
|
841
|
+
throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
export async function cmdStackStatus(args, deps = {}) {
|
|
845
|
+
const usage = "usage: mergestorm stack status <stack-id> [--json]";
|
|
846
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
847
|
+
if (positional.length !== 1)
|
|
848
|
+
throw new CommandError(usage, 2, "usage");
|
|
849
|
+
const stackId = requireStackId(positional[0], usage);
|
|
850
|
+
const cfg = await (deps.loadConfig ?? loadConfig)();
|
|
851
|
+
const stack = await (deps.getEnrichedStack ?? getEnrichedStack)(stackId, cfg);
|
|
852
|
+
if (!stack) {
|
|
853
|
+
throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
|
|
854
|
+
}
|
|
855
|
+
if (args.includes("--json")) {
|
|
856
|
+
console.log(JSON.stringify(stack, null, 2));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const lines = formatStackHuman(stack, false);
|
|
860
|
+
for (const layer of stack.layers) {
|
|
861
|
+
const head = layer.headSha ? `@${layer.headSha.slice(0, 7)}` : "";
|
|
862
|
+
const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
|
|
863
|
+
lines.push(` ${String(layer.position).padStart(2)} ${pr}${head} ${layer.state.padEnd(14)} ${layer.branch}` +
|
|
864
|
+
` CI: ${layer.ciStatus ?? "unknown"} review: ${layer.reviewStatus ?? "unknown"}` +
|
|
865
|
+
(layer.vortexStatus ? ` Vortex: ${layer.vortexStatus}` : "") +
|
|
866
|
+
(layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
|
|
867
|
+
(layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
|
|
868
|
+
}
|
|
869
|
+
await present("Stack status", lines);
|
|
870
|
+
}
|
|
790
871
|
/** Human summary lines for a `stack set` write, one per flag. */
|
|
791
872
|
export function stackSetSummary(parsed) {
|
|
792
873
|
const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
|
|
@@ -885,7 +966,7 @@ async function cmdStackLand(args) {
|
|
|
885
966
|
` on ${id}`),
|
|
886
967
|
]);
|
|
887
968
|
}
|
|
888
|
-
export async function cmdStack(args) {
|
|
969
|
+
export async function cmdStack(args, options = {}) {
|
|
889
970
|
const sub = args[0]?.toLowerCase();
|
|
890
971
|
const rest = args.slice(1);
|
|
891
972
|
// Help must exit 0 — print usage, do not throw CommandError.
|
|
@@ -905,6 +986,10 @@ export async function cmdStack(args) {
|
|
|
905
986
|
return cmdStackReset(rest);
|
|
906
987
|
if (sub === "list" || sub === "ls")
|
|
907
988
|
return cmdStackList(rest);
|
|
989
|
+
if (sub === "status")
|
|
990
|
+
return cmdStackStatus(rest);
|
|
991
|
+
if (sub === "wait")
|
|
992
|
+
return cmdStackWait(rest, { signal: options.signal });
|
|
908
993
|
if (sub === "set")
|
|
909
994
|
return cmdStackSet(rest);
|
|
910
995
|
if (sub === "adopt")
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Stable machine codes for CommandError control flow.
|
|
3
3
|
* UX copy in `message` may change freely; match on `code`, never on message text.
|
|
4
4
|
*/
|
|
5
|
-
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
|
|
5
|
+
export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "busy" | "adopt_failed";
|
|
6
6
|
/** Stable one-shot exit codes for machine callers of `mergestorm review`. */
|
|
7
7
|
export declare const REVIEW_EXIT: {
|
|
8
8
|
readonly success: 0;
|
package/dist/stack-dto.d.ts
CHANGED
|
@@ -20,7 +20,32 @@ export type StackReviewStatus = "approved" | "changes_requested" | "reviewed" |
|
|
|
20
20
|
* `incomplete` is a posted verdict whose head still has unreviewed files
|
|
21
21
|
* (#1947): Work paints Incomplete, never Reviewed or all clear.
|
|
22
22
|
*/
|
|
23
|
-
export type StackVortexStatus = "reviewing" | "seam_pending" | "all_clear" | "findings" | "throttled" | "failed" | "incomplete";
|
|
23
|
+
export type StackVortexStatus = "queued" | "skipped" | "reviewing" | "seam_pending" | "all_clear" | "findings" | "throttled" | "failed" | "incomplete";
|
|
24
|
+
/**
|
|
25
|
+
* MS-14 viewer: the one lifecycle status of the Vortex review on a PR head.
|
|
26
|
+
* `pr_reviews` is the authority when a row exists for the SHA; a leftover
|
|
27
|
+
* `github_webhook_jobs` row paints `queued` / `reviewing` only when no pass row
|
|
28
|
+
* exists yet. `none` = never dispatched. `done` = a pass posted; the finer
|
|
29
|
+
* verdict detail lives on `vortexStatus` / `agentRuns`.
|
|
30
|
+
*/
|
|
31
|
+
export type StackVortexReviewViewStatus = "none" | "queued" | "reviewing" | "skipped" | "done" | "failed";
|
|
32
|
+
export type StackVortexReviewView = {
|
|
33
|
+
status: StackVortexReviewViewStatus;
|
|
34
|
+
/** `pr_reviews.skip_reason` for `skipped`; null when the writer recorded none. */
|
|
35
|
+
skip_reason: string | null;
|
|
36
|
+
/** SHA + pass identity of the attempt (#2027); null before a pass row exists. */
|
|
37
|
+
pass: number | null;
|
|
38
|
+
/** Head the status speaks for; null when neither table named one. */
|
|
39
|
+
head_sha: string | null;
|
|
40
|
+
/** In-flight sub-state while `reviewing` (fanout / synthesizing). */
|
|
41
|
+
phase: string | null;
|
|
42
|
+
/** Earliest live job start while `reviewing`; null when lease-only. */
|
|
43
|
+
started_at: string | null;
|
|
44
|
+
/** Stop control visibility: true only while a pass can actually be cancelled. */
|
|
45
|
+
stoppable: boolean;
|
|
46
|
+
/** Which table spoke: the authority row, a live job, a lease, or nothing. */
|
|
47
|
+
source: "pr_reviews" | "github_webhook_jobs" | "lease" | "none";
|
|
48
|
+
};
|
|
24
49
|
export type StackCycloneStatus = "patching" | "awaiting_fix";
|
|
25
50
|
export type StackTempestStatus = "reviewing" | "findings" | "clear" | "failed" | "stale";
|
|
26
51
|
export type StackAgentName = "vortex" | "cyclone" | "tempest";
|
|
@@ -36,6 +61,8 @@ export type StackAgentRun = {
|
|
|
36
61
|
status: string;
|
|
37
62
|
/** Full SHA when known. UI shortens to 7. */
|
|
38
63
|
sha: string | null;
|
|
64
|
+
pass?: number | null;
|
|
65
|
+
skipReason?: string | null;
|
|
39
66
|
startedAt?: string | null;
|
|
40
67
|
finishedAt?: string | null;
|
|
41
68
|
/**
|
|
@@ -138,6 +165,12 @@ export type StackLayerDto = {
|
|
|
138
165
|
* Undefined = producer predates runs (clients fall back to the rollup).
|
|
139
166
|
*/
|
|
140
167
|
agentRuns?: StackAgentRun[];
|
|
168
|
+
/**
|
|
169
|
+
* MS-14 viewer output for this layer's head. Every surface renders the
|
|
170
|
+
* lifecycle status from here; `vortexStatus` / `agentRuns` only add verdict
|
|
171
|
+
* detail once it says `done`. Undefined = producer predates the viewer.
|
|
172
|
+
*/
|
|
173
|
+
vortexReview?: StackVortexReviewView;
|
|
141
174
|
conflictDetail: string | null;
|
|
142
175
|
restackError?: RestackError | null;
|
|
143
176
|
lastRestackedSha: string | null;
|
|
@@ -247,6 +280,21 @@ export type StackDto = {
|
|
|
247
280
|
* Missing on older payloads is off.
|
|
248
281
|
*/
|
|
249
282
|
autoEnqueueWhenReady?: boolean;
|
|
283
|
+
/**
|
|
284
|
+
* Auto land Settling certificate. The stacks watcher publishes the identity
|
|
285
|
+
* of the settle clock it actually started (action, exact PR and head, server
|
|
286
|
+
* start time) and clears it when that clock is consumed or invalidated.
|
|
287
|
+
* Absent (older payloads) or `null` means no certified settle: paint nothing.
|
|
288
|
+
* Remaining time derives from `startedAt`; a refresh never restarts it.
|
|
289
|
+
* Partial or invalid server rows map to `null`, never to a guessed value.
|
|
290
|
+
*/
|
|
291
|
+
autoEnqueueSettle?: {
|
|
292
|
+
action: "ready" | "promote";
|
|
293
|
+
prNumber: number;
|
|
294
|
+
headSha: string;
|
|
295
|
+
/** ISO timestamp of the watcher's clock start. */
|
|
296
|
+
startedAt: string;
|
|
297
|
+
} | null;
|
|
250
298
|
/**
|
|
251
299
|
* Per-stack Vortex auto-review override. `null` (or missing) follows the
|
|
252
300
|
* account `auto_review_enabled` flag; a boolean wins in both directions.
|
|
@@ -284,7 +332,11 @@ export type StackDto = {
|
|
|
284
332
|
export type MergeQueueEntryState = "queued" | "running" | "waiting" | "landed" | "bounced" | "cancelled";
|
|
285
333
|
export type MergeQueueEnqueuedBy = "human" | "agent";
|
|
286
334
|
export type MergeQueueEnqueuedVia = "dashboard" | "cli" | "chat" | "api" | "mcp";
|
|
287
|
-
export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "seam_findings" | "restack_conflict" | "pr_draft" | "merge_failed" | "gh_error";
|
|
335
|
+
export type MergeQueueBounceKind = "ci_failure" | "ci_timeout" | "head_moved" | "tempest_findings" | "seam_findings" | "restack_conflict" | "pr_draft" | "must_consolidate" | "merge_failed" | "gh_error";
|
|
336
|
+
/** Every bounce kind understood by the shared contract. */
|
|
337
|
+
export declare const MERGE_QUEUE_BOUNCE_KINDS: readonly ["ci_failure", "ci_timeout", "head_moved", "tempest_findings", "seam_findings", "restack_conflict", "pr_draft", "must_consolidate", "merge_failed", "gh_error"];
|
|
338
|
+
export declare const BOUNCE_KIND_LABELS: Readonly<Record<MergeQueueBounceKind, string>>;
|
|
339
|
+
export declare function mergeQueueBounceLabel(entry: MergeQueueEntryDto): string;
|
|
288
340
|
/** Structured bounce handoff — enough for the bar chip and the chat quote. */
|
|
289
341
|
export type MergeQueueBounceDetail = {
|
|
290
342
|
kind: MergeQueueBounceKind;
|
package/dist/stack-dto.js
CHANGED
|
@@ -1 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
/** Every bounce kind understood by the shared contract. */
|
|
2
|
+
export const MERGE_QUEUE_BOUNCE_KINDS = [
|
|
3
|
+
"ci_failure",
|
|
4
|
+
"ci_timeout",
|
|
5
|
+
"head_moved",
|
|
6
|
+
"tempest_findings",
|
|
7
|
+
"seam_findings",
|
|
8
|
+
"restack_conflict",
|
|
9
|
+
"pr_draft",
|
|
10
|
+
"must_consolidate",
|
|
11
|
+
"merge_failed",
|
|
12
|
+
"gh_error",
|
|
13
|
+
];
|
|
14
|
+
export const BOUNCE_KIND_LABELS = {
|
|
15
|
+
ci_failure: "CI failed",
|
|
16
|
+
ci_timeout: "CI timed out",
|
|
17
|
+
head_moved: "head moved",
|
|
18
|
+
tempest_findings: "Tempest findings",
|
|
19
|
+
seam_findings: "seam review findings",
|
|
20
|
+
restack_conflict: "restack conflict",
|
|
21
|
+
pr_draft: "draft PR",
|
|
22
|
+
must_consolidate: "needs promote",
|
|
23
|
+
merge_failed: "merge failed",
|
|
24
|
+
gh_error: "GitHub error",
|
|
25
|
+
};
|
|
26
|
+
export function mergeQueueBounceLabel(entry) {
|
|
27
|
+
const detail = entry.bounceDetail;
|
|
28
|
+
const kindLabel = detail ? BOUNCE_KIND_LABELS[detail.kind] : undefined;
|
|
29
|
+
if (kindLabel) {
|
|
30
|
+
const specifics = detail?.failingCheck ??
|
|
31
|
+
detail?.conflictBranch ??
|
|
32
|
+
detail?.message ??
|
|
33
|
+
(detail?.prNumber != null ? `PR #${detail.prNumber}` : null);
|
|
34
|
+
return specifics ? `${kindLabel} — ${specifics}` : kindLabel;
|
|
35
|
+
}
|
|
36
|
+
return entry.bounceReason?.trim() || "bounced";
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { apiFetch } from "./api.js";
|
|
2
|
+
import type { Config } from "./config.js";
|
|
3
|
+
export declare const STACK_WATCH_SCHEMA: "mergestorm.stack_watch/v1";
|
|
4
|
+
export declare const STACK_WATCH_DEFAULT_TIMEOUT_MS = 45000;
|
|
5
|
+
/** Resume selectors are fixed for the entire wait, including transient retries. */
|
|
6
|
+
export type StackWatchCursor = {
|
|
7
|
+
readonly stackId: string;
|
|
8
|
+
readonly enrolledHeadSha: string | null;
|
|
9
|
+
readonly afterFinishedAt?: string | null;
|
|
10
|
+
readonly bounceId?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type StackWatchEnvelope = {
|
|
13
|
+
schema: typeof STACK_WATCH_SCHEMA;
|
|
14
|
+
status: "attention" | "waiting" | "in_progress" | "rate_limited" | "failed";
|
|
15
|
+
stackId: string;
|
|
16
|
+
blocker: string | null;
|
|
17
|
+
bounceKind: string | null;
|
|
18
|
+
prNumber: number | null;
|
|
19
|
+
headSha: string | null;
|
|
20
|
+
cursor: StackWatchCursor;
|
|
21
|
+
};
|
|
22
|
+
export type PollStackWatchOptions = {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
intervalMs?: number;
|
|
25
|
+
cursor?: StackWatchCursor;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
onTick?: (envelope: StackWatchEnvelope) => void;
|
|
28
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
random?: () => number;
|
|
31
|
+
/** Test seam at the apiFetch boundary, including HTTP status and Retry-After. */
|
|
32
|
+
fetch?: typeof apiFetch;
|
|
33
|
+
};
|
|
34
|
+
export declare class StackWatchTimeoutError extends Error {
|
|
35
|
+
readonly lastEnvelope: StackWatchEnvelope;
|
|
36
|
+
constructor(lastEnvelope: StackWatchEnvelope);
|
|
37
|
+
}
|
|
38
|
+
export declare class StackWatchError extends Error {
|
|
39
|
+
readonly lastEnvelope: StackWatchEnvelope;
|
|
40
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
41
|
+
constructor(message: string, lastEnvelope: StackWatchEnvelope, options?: ErrorOptions, retryAfterSeconds?: number | undefined);
|
|
42
|
+
}
|
|
43
|
+
/** Wait on queue invalidations, then evaluate a fresh stack snapshot. */
|
|
44
|
+
export declare function pollStackWatch(cfg: Config, stackId: string, opts?: PollStackWatchOptions): Promise<StackWatchEnvelope>;
|