mergestorm 0.3.13 → 0.3.16

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 (66) hide show
  1. package/README.md +16 -17
  2. package/dist/api.d.ts +103 -6
  3. package/dist/api.js +195 -9
  4. package/dist/automation-catalog.d.ts +37 -0
  5. package/dist/automation-catalog.js +13 -0
  6. package/dist/client/index.d.ts +3 -2
  7. package/dist/client/index.js +3 -2
  8. package/dist/commands/branches.d.ts +6 -2
  9. package/dist/commands/branches.js +29 -14
  10. package/dist/commands/browse.d.ts +39 -0
  11. package/dist/commands/browse.js +252 -0
  12. package/dist/commands/credits.js +7 -10
  13. package/dist/commands/help.d.ts +5 -0
  14. package/dist/commands/help.js +93 -0
  15. package/dist/commands/jobs.d.ts +11 -2
  16. package/dist/commands/jobs.js +64 -15
  17. package/dist/commands/login.js +4 -3
  18. package/dist/commands/logout.js +2 -1
  19. package/dist/commands/pr.d.ts +22 -0
  20. package/dist/commands/pr.js +127 -0
  21. package/dist/commands/queue.d.ts +12 -0
  22. package/dist/commands/queue.js +160 -0
  23. package/dist/commands/registry.js +98 -21
  24. package/dist/commands/review-client.d.ts +13 -0
  25. package/dist/commands/review-client.js +110 -0
  26. package/dist/commands/review.d.ts +6 -0
  27. package/dist/commands/review.js +18 -7
  28. package/dist/commands/settings.d.ts +17 -0
  29. package/dist/commands/settings.js +82 -0
  30. package/dist/commands/skill.d.ts +20 -0
  31. package/dist/commands/skill.js +145 -0
  32. package/dist/commands/stack.d.ts +57 -2
  33. package/dist/commands/stack.js +352 -87
  34. package/dist/commands/status.d.ts +2 -0
  35. package/dist/commands/status.js +30 -15
  36. package/dist/commands/whoami.d.ts +3 -1
  37. package/dist/commands/whoami.js +21 -7
  38. package/dist/errors.d.ts +1 -1
  39. package/dist/repo-identity.d.ts +1 -1
  40. package/dist/repo-identity.js +13 -2
  41. package/dist/shell.js +39 -31
  42. package/dist/stack-dto.d.ts +92 -1
  43. package/dist/stack-meta.d.ts +12 -0
  44. package/dist/stack-meta.js +32 -3
  45. package/dist/ui/ansi.d.ts +2 -0
  46. package/dist/ui/ansi.js +2 -0
  47. package/dist/ui/banner.d.ts +34 -2
  48. package/dist/ui/banner.js +166 -79
  49. package/dist/ui/findings.d.ts +2 -1
  50. package/dist/ui/findings.js +13 -7
  51. package/dist/ui/line-tabs.d.ts +27 -0
  52. package/dist/ui/line-tabs.js +170 -0
  53. package/dist/ui/logo.d.ts +10 -2
  54. package/dist/ui/logo.js +13 -9
  55. package/dist/ui/present.d.ts +10 -0
  56. package/dist/ui/present.js +13 -0
  57. package/dist/ui/prompt.d.ts +58 -1
  58. package/dist/ui/prompt.js +225 -42
  59. package/dist/ui/select.js +1 -1
  60. package/dist/ui/tabs.d.ts +125 -0
  61. package/dist/ui/tabs.js +480 -0
  62. package/dist/ui/usage.d.ts +4 -0
  63. package/dist/ui/usage.js +22 -2
  64. package/package.json +2 -1
  65. package/skill/SKILL.md +98 -0
  66. package/skill/mergestorm-pr-loop/SKILL.md +84 -0
package/README.md CHANGED
@@ -41,18 +41,16 @@ node dist/cli.js --help
41
41
 
42
42
  ## Interactive shell
43
43
 
44
- On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a **full-width** welcome panel (logo + status on the left, tips / what's new on the right) and a **full-width** bordered input box with a live `/` slash-command dropdown. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
44
+ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a compact **full-width** welcome panel (mark, version, status, usage bar) and a **full-width** bordered input box with a live `/` slash-command dropdown. The welcome panel reflows on terminal resize instead of wrapping or smearing. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
45
45
 
46
46
  ```
47
- ╭─ mergestorm v0.3.13 ─────────────────────────────────────────────────────────╮
48
- │ ▟██████████▛ Tips for getting started │
49
- │ ▜████████▛ review review origin/HEAD or main │
50
- │ ▝▜████▛▘ stack create → submit → restack → land │
51
- │ ▜██▛ /help list all commands │
52
- │ ██ │
53
- │ ▝▘ What's new in v0.3.13 │
54
- │ ● msk… · maelstrom • MIT license + public source (mergestorm-cli) │
55
- │ [████░░░░░░░░░░] 12% used │
47
+ ╭──────────────────────────────────────────────────────────────────────────────╮
48
+ │ ▟██▛ mergestorm v0.3.16 │
49
+ │ ▜▙ ● msk_live_… · maelstrom │
50
+ │ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used · Resets Sep 5, 10:53am (UTC)│
51
+ │ │
52
+ │ review a diff · usage for tabs · /help for all commands │
53
+ │ New in v0.3.16: verify findings, small diff, public dismiss on skip │
56
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
57
55
  mergestorm
58
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -60,7 +58,7 @@ mergestorm
60
58
  ╰──────────────────────────────────────────────────────────────────────────────╯
61
59
  /review Review git diff (default origin/HEAD or main or master)
62
60
 
63
- mergestorm> credits
61
+ mergestorm> usage
64
62
  mergestorm> branches
65
63
  mergestorm> chain
66
64
  mergestorm> exit
@@ -68,7 +66,7 @@ mergestorm> exit
68
66
 
69
67
  Bare command names still work exactly as before (`review`, `help`, `exit`, …) — typing `/` at the start of a line opens a compact autocomplete overlay (aliases folded onto the primary name, Up/Down scroll a viewport, Esc dismisses, Tab completes, Enter accepts a prefix or submits an exact name as typed, Left/Right move the cursor). Arrow-key history, Ctrl+A/E, Ctrl+U/K work as in a regular shell. At the idle prompt, **Ctrl+C twice** (within ~1.5s) exits the shell; a single Ctrl+C clears the line and shows a confirm hint. While `review` / `status` is waiting, Ctrl+C detaches or aborts that wait (job may keep running) — it does not exit the shell. Ctrl+D on an empty line, or `exit` / `quit`, also leave the shell. On a dumb terminal, without color, or when piped, the box falls back to plain ASCII borders (or no shell at all for non-TTY input).
70
68
 
71
- `credits` / `usage` print one static panel: key · plan, a credit bar sized to the terminal, `Resets … (UTC)`, then the last five jobs. `--json` adds `recent_jobs`. `branches` (alias `chains`) lists recently reviewed branches; on a TTY you arrow-select one to open its review-chain timeline. `chain [slug]` shows that timeline directly (defaults to `local/<current-git-branch>`).
69
+ `credits` / `usage` on a TTY open a tabbed **Status / Usage / Jobs** browser (Usage selected): Left/Right switch tabs, Up/Down scroll or select, Enter on a job opens its `status` detail in place, Esc backs out, `q` closes. `status` with no job id opens the same browser on the Status tab. Piped / non-TTY output keeps the static panel: key · plan, a credit bar sized to the terminal, `Resets … (UTC)`, then the last five jobs; `--json` adds `recent_jobs` and is unchanged. `branches` (alias `chains`) lists recently reviewed branches; on a TTY you arrow-select one to open its review-chain timeline. `chain [slug]` shows that timeline directly (defaults to `local/<current-git-branch>`).
72
70
 
73
71
  ## Commands
74
72
 
@@ -82,11 +80,13 @@ mergestorm login --key Paste an existing API key instead (headless/CI)
82
80
  mergestorm logout Remove the stored API key
83
81
  mergestorm review [base] [head] Review git diff base...head (default: origin/HEAD or main or master)
84
82
  mergestorm status <job_id> Fetch a review job (envelope; --json --wait --timeout)
85
- mergestorm credits [--json] Usage panel (bar + last 5 jobs)
83
+ mergestorm status Tabbed Status / Usage / Jobs browser (TTY)
84
+ mergestorm credits [--json] Usage panel (bar + last 5 jobs); tabbed browser on a TTY
86
85
  mergestorm jobs [n] [--json] Recent review jobs (default 10, max 50)
87
86
  mergestorm branches [n] Recently reviewed branches (arrow-pick on TTY)
88
87
  mergestorm chain [slug] Branch review-chain timeline (default: current branch)
89
88
  mergestorm whoami [--json] Key prefix, plan, API base, config path
89
+ mergestorm skill install --claude|--cursor Copy the mergestorm-review and mergestorm-pr-loop skills into this repo
90
90
  mergestorm thread <slug> Jobs in a review thread
91
91
  mergestorm stack create [name] New local stack layer (optional `--onto` / `--trunk` / `--extend`)
92
92
  mergestorm stack submit Push layers, open PRs (`gh`), register via adopt (optional `--extend`)
@@ -94,13 +94,12 @@ mergestorm stack list [--json] List registered stacks
94
94
  mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
95
95
  mergestorm stack restack <stack-id> Restack descendants
96
96
  mergestorm stack land <stack-id> Land / promote (into review unit when present)
97
- mergestorm stack auto-promote on|off <stack-id> Toggle auto-promote when green
98
97
  mergestorm stack reset --force Clear local authoring state (not branches/PRs)
99
98
  ```
100
99
 
101
- `mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack create`. Happy path: **create → commit → submit → restack → land**. On review-unit stacks, `stack land` **promotes** the tip into the unit (same gates as the dashboard); otherwise it lands the bottom open PR. `stack auto-promote` turns on land-when-green for a registered stack (`auto-land` remains a deprecated alias). `stack adopt` is for importing a chain that already exists on GitHub (legacy / Graphite). Restack/land use the same login key (`/api/v1/stacks`).
100
+ `mg` is a short alias for `mergestorm` (same binary), e.g. `mg stack create`. Happy path: **create → commit → submit → restack → land**. On review-unit stacks, `stack land` **promotes** the tip into the unit (same gates as the dashboard); otherwise it lands the bottom open PR. `stack adopt` is for importing a chain that already exists on GitHub (legacy / Graphite). Restack/land use the same login key (`/api/v1/stacks`). Unattended landing is `mg queue add`.
102
101
 
103
- `stack create` checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and records `{ branch, parentBranch }` in CLI-managed state under **`~/.mergestorm/stacks/<repo-id>/stack.json`**. The CLI updates this automatically; never edit it. Linked worktrees share state, while independent clones remain isolated. Creating onto trunk with a non-empty active stack starts a new one. `--trunk` only sets trunk metadata — it does **not** change the parent; use `--onto main` (etc.) for a fresh stack from trunk.
102
+ `stack create` checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and records `{ branch, parentBranch }` in CLI-managed state under **`~/.mergestorm/stacks/<repo-id>/stack.json`**. The CLI updates this automatically; never edit it. Each worktree has its own authoring state; independent clones stay isolated too. Creating onto trunk with a non-empty active stack starts a new one. `--trunk` only sets trunk metadata — it does **not** change the parent; use `--onto main` (etc.) for a fresh stack from trunk.
104
103
 
105
104
  If the parent branch is already a layer of a **registered** (submitted) stack, `create` and `submit` refuse unless you pass **`--extend`**. That blocks accidentally gluing an unrelated PR onto an open unit. Growing a registered stack on purpose: `mg stack create --onto <tip> --extend` → commit → `mg stack submit --extend`.
106
105
 
@@ -120,7 +119,7 @@ Removes `~/.mergestorm/config.json` so the stored key is no longer used.
120
119
 
121
120
  ### `review [base] [head]`
122
121
 
123
- Collects `git diff base...head` (default: discovered trunk — `origin/HEAD`, `main`, or `master`), attaches the changed file contents (up to 40 files, skipping files > 400 KB), submits the job, and polls until it finishes (~8 min max — Core plus a Max fleet can exceed 3 minutes). The thread slug defaults to the current branch (`local/<branch>`); pass `--thread` to override so follow-ups chain on a chosen slug. The CLI does not scan `.mergestorm/context/`.
122
+ Collects `git diff base...head` (default: discovered trunk — `origin/HEAD`, `main`, or `master`), attaches the changed file contents (up to 40 files, skipping files > 400 KB), submits the job, and polls until it finishes (~8 min max — Core plus a Max fleet can exceed 3 minutes). The thread slug defaults to the current branch (`local/<branch>`); pass `--thread` to override so follow-ups chain on a chosen slug. The CLI still does not scan a repo folder for review context. GitHub Vortex will read house-rule files in a later PR (`AGENTS.md`, `CLAUDE.md`, alwaysApply Cursor rules). That ingest is not shipping yet.
124
123
 
125
124
  ```bash
126
125
  mergestorm review # discovered trunk (origin/HEAD, main, or master)...HEAD
package/dist/api.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type Config } from "./config.js";
2
- import type { StackDto } from "./stack-dto.js";
3
- export type { StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackTempestStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
2
+ import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, type BearerSettingsKey as SettingsWritableKey } from "./automation-catalog.js";
3
+ import type { MergeQueueEntryDto, StackDto } from "./stack-dto.js";
4
+ export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackTempestStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
4
5
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
5
6
  export declare const DEFAULT_API_TIMEOUT_MS = 30000;
6
7
  /** Message prefix of the CommandError thrown when a request exceeds its timeout. */
@@ -94,33 +95,129 @@ export type ThreadDetail = {
94
95
  finished_at?: string | null;
95
96
  }[];
96
97
  };
98
+ export type PrReviewFinding = {
99
+ path?: string;
100
+ line?: number;
101
+ severity: string;
102
+ body: string;
103
+ title?: string;
104
+ specialist?: string;
105
+ };
106
+ export type PrReviewFindings = {
107
+ specialists_run?: string[];
108
+ inline?: PrReviewFinding[];
109
+ off_diff?: PrReviewFinding[];
110
+ offDiff?: PrReviewFinding[];
111
+ patchPolicy?: unknown;
112
+ };
113
+ export type PrVortexReview = {
114
+ schema: "mergestorm.pr_review/v1";
115
+ id: string;
116
+ owner: string;
117
+ repo: string;
118
+ pr_number: number;
119
+ status: string;
120
+ raw_status?: string;
121
+ phase: string | null;
122
+ verdict: string | null;
123
+ summary?: string | null;
124
+ head_sha: string | null;
125
+ review_count: number;
126
+ skip_reason: string | null;
127
+ reviewed_at: string | null;
128
+ finding_count: number;
129
+ findings: PrReviewFindings | null;
130
+ patch_policy: unknown | null;
131
+ };
132
+ /**
133
+ * Bearer-writable settings keys, mirroring the `/api/v1/settings` allowlist.
134
+ * The connected flags are read-only there and deliberately absent here.
135
+ */
136
+ export { SETTINGS_WRITABLE_KEYS };
137
+ export type { SettingsWritableKey };
138
+ export type SettingsPatch = Partial<Record<SettingsWritableKey, boolean>>;
139
+ /** GET and PATCH `/api/v1/settings` both answer with this shape. */
140
+ export type SettingsResponse = Record<SettingsWritableKey, boolean> & {
141
+ cyclone_connected: boolean;
142
+ github_connected: boolean;
143
+ };
144
+ /**
145
+ * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
146
+ * degrade (same contract as {@link getMe}), while 401 still surfaces.
147
+ */
148
+ export declare function getSettings(cfg?: Config, opts?: {
149
+ timeoutMs?: number;
150
+ }): Promise<SettingsResponse | null>;
151
+ /**
152
+ * PATCH Bearer /api/v1/settings and return the stored result. Rejections
153
+ * (unknown key, cyclone_not_connected, …) surface the API's own `message`
154
+ * so the CLI never invents a reason.
155
+ */
156
+ export declare function patchSettings(patch: SettingsPatch, cfg?: Config, opts?: {
157
+ timeoutMs?: number;
158
+ }): Promise<SettingsResponse>;
97
159
  /** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
98
160
  export declare function getMe(cfg?: Config, opts?: {
99
161
  timeoutMs?: number;
100
162
  }): Promise<MeResponse | null>;
101
- export declare function listJobs(limit?: number, cfg?: Config): Promise<JobListItem[]>;
163
+ export declare function listJobs(limit?: number, cfg?: Config, opts?: {
164
+ timeoutMs?: number;
165
+ }): Promise<JobListItem[]>;
102
166
  /** Fetch one review job (Bearer GET /api/v1/reviews/:id). */
103
167
  export declare function getReview(jobId: string, cfg?: Config, opts?: {
104
168
  signal?: AbortSignal;
169
+ timeoutMs?: number;
105
170
  }): Promise<unknown>;
171
+ /** Fetch the latest Vortex pass for one GitHub PR. */
172
+ export declare function getPrVortexReview(owner: string, repo: string, prNumber: number, cfg?: Config, opts?: {
173
+ signal?: AbortSignal;
174
+ timeoutMs?: number;
175
+ }): Promise<PrVortexReview>;
106
176
  /** List stacks (Bearer GET /api/v1/stacks). */
107
177
  export declare function listStacks(cfg?: Config, opts?: {
108
178
  timeoutMs?: number;
109
179
  }): Promise<StackDto[]>;
180
+ /** Fetch one owned stack with enriched GitHub checks and agent state. */
181
+ export declare function getEnrichedStack(stackId: string, cfg?: Config, opts?: {
182
+ timeoutMs?: number;
183
+ }): Promise<StackDto | null>;
184
+ /**
185
+ * Per-stack automation policy. Auto land is a boolean; the review / patch
186
+ * overrides are tri-state (`null` clears back to the account flag). Absent
187
+ * keys are left untouched.
188
+ */
189
+ export type StackPolicyPatch = {
190
+ autoEnqueueWhenReady?: boolean;
191
+ autoReviewOverride?: boolean | null;
192
+ autoPatchOverride?: boolean | null;
193
+ };
194
+ /** Drop undefined keys so the wire body only carries what was requested. */
195
+ export declare function stackPolicyBody(policy: StackPolicyPatch | undefined): StackPolicyPatch;
110
196
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
111
- export declare function adoptStack(owner: string, repo: string, prNumber: number, cfg?: Config): Promise<unknown>;
197
+ export declare function adoptStack(owner: string, repo: string, prNumber: number, cfg?: Config, policy?: StackPolicyPatch): Promise<unknown>;
112
198
  /** Mint or reuse the PR3+ park freeze (Bearer POST /api/v1/stacks/:id/ensure-upper-park). */
113
199
  export declare function ensureUpperPark(stackId: string, cfg?: Config): Promise<{
114
200
  freezeBranch: string;
115
201
  created: boolean;
116
202
  }>;
203
+ /** Arm or disarm Auto land on one owned stack. */
204
+ export declare function setStackAutoLand(stackId: string, enabled: boolean, cfg?: Config): Promise<unknown>;
205
+ /**
206
+ * Patch any subset of one owned stack's automation policy
207
+ * (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
208
+ */
209
+ export declare function setStackPolicy(stackId: string, policy: StackPolicyPatch, cfg?: Config): Promise<unknown>;
117
210
  /** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
118
211
  export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
119
212
  /** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
120
213
  export declare function landNextStack(stackId: string, cfg?: Config, opts?: {
121
214
  prNumber?: number;
122
215
  }): Promise<unknown>;
123
- /** Toggle auto-promote when green (Bearer PATCH /api/v1/stacks/:id). */
124
- export declare function setStackAutoPromote(stackId: string, autoPromoteWhenGreen: boolean, cfg?: Config): Promise<unknown>;
216
+ /** List the authenticated user's live merge-queue entries. */
217
+ export declare function listMergeQueueEntries(cfg?: Config): Promise<MergeQueueEntryDto[]>;
218
+ /** Enqueue a stack for verified landing. */
219
+ export declare function enqueueStack(stackId: string, cfg?: Config): Promise<unknown>;
220
+ /** Cancel a live merge-queue entry by entry id. */
221
+ export declare function cancelMergeQueueEntry(entryId: string, cfg?: Config): Promise<unknown>;
125
222
  /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
126
223
  export declare function listThreads(limit?: number, cfg?: Config): Promise<ThreadListItem[]>;
package/dist/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { apiBase, loadConfig, resolveApiKey } from "./config.js";
2
+ import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, } from "./automation-catalog.js";
2
3
  import { CommandError, REVIEW_EXIT, rateLimitedMessage } from "./errors.js";
3
4
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
4
5
  export const DEFAULT_API_TIMEOUT_MS = 30_000;
@@ -154,6 +155,63 @@ export async function devicePost(base, route, json, opts = {}) {
154
155
  }
155
156
  return { status: res.status, body: parsed };
156
157
  }
158
+ /**
159
+ * Bearer-writable settings keys, mirroring the `/api/v1/settings` allowlist.
160
+ * The connected flags are read-only there and deliberately absent here.
161
+ */
162
+ export { SETTINGS_WRITABLE_KEYS };
163
+ /**
164
+ * Fetch Bearer /api/v1/settings; null on 404/network/timeouts so panels can
165
+ * degrade (same contract as {@link getMe}), while 401 still surfaces.
166
+ */
167
+ export async function getSettings(cfg, opts) {
168
+ const resolved = cfg ?? (await loadConfig());
169
+ try {
170
+ const { status, body } = await apiFetch(resolved, "/api/v1/settings", {
171
+ timeoutMs: opts?.timeoutMs,
172
+ });
173
+ if (status !== 200)
174
+ return null;
175
+ return body;
176
+ }
177
+ catch (err) {
178
+ if (err instanceof CommandError) {
179
+ if (err.code === "api_timeout")
180
+ return null;
181
+ throw err;
182
+ }
183
+ return null;
184
+ }
185
+ }
186
+ /**
187
+ * PATCH Bearer /api/v1/settings and return the stored result. Rejections
188
+ * (unknown key, cyclone_not_connected, …) surface the API's own `message`
189
+ * so the CLI never invents a reason.
190
+ */
191
+ export async function patchSettings(patch, cfg, opts) {
192
+ const resolved = cfg ?? (await loadConfig());
193
+ if (Object.keys(patch).length === 0) {
194
+ throw new CommandError("No settings to update.", 2, "usage");
195
+ }
196
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/settings", {
197
+ method: "PATCH",
198
+ json: patch,
199
+ timeoutMs: opts?.timeoutMs,
200
+ });
201
+ if (status === 404) {
202
+ throw new CommandError("Settings API is not available on this server yet. Deploy the API update or use the dashboard.");
203
+ }
204
+ if (status === 429) {
205
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
206
+ }
207
+ if (status !== 200) {
208
+ const message = body?.message;
209
+ throw new CommandError(typeof message === "string" && message.trim()
210
+ ? message
211
+ : `Failed to update settings (HTTP ${status}): ${JSON.stringify(body)}`);
212
+ }
213
+ return body;
214
+ }
157
215
  /** Fetch /api/v1/me; returns null on 404/network so callers can degrade. */
158
216
  export async function getMe(cfg, opts) {
159
217
  const resolved = cfg ?? (await loadConfig());
@@ -178,10 +236,12 @@ export async function getMe(cfg, opts) {
178
236
  return null;
179
237
  }
180
238
  }
181
- export async function listJobs(limit = 10, cfg) {
239
+ export async function listJobs(limit = 10, cfg, opts) {
182
240
  const resolved = cfg ?? (await loadConfig());
183
241
  const capped = Math.min(Math.max(limit, 1), 50);
184
- const { status, body } = await apiFetch(resolved, `/api/v1/reviews?limit=${capped}`);
242
+ const { status, body } = await apiFetch(resolved, `/api/v1/reviews?limit=${capped}`, {
243
+ timeoutMs: opts?.timeoutMs,
244
+ });
185
245
  if (status === 404) {
186
246
  throw new CommandError("Job list is not available on this server yet. Update the API or use the dashboard.");
187
247
  }
@@ -198,7 +258,7 @@ export async function getReview(jobId, cfg, opts) {
198
258
  if (!id) {
199
259
  throw new CommandError("job_id is required", 2, "usage");
200
260
  }
201
- const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/reviews/${encodeURIComponent(id)}`, { signal: opts?.signal });
261
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/reviews/${encodeURIComponent(id)}`, { signal: opts?.signal, timeoutMs: opts?.timeoutMs });
202
262
  if (status === 404) {
203
263
  throw new CommandError(`Review not found: ${id}`);
204
264
  }
@@ -210,27 +270,97 @@ export async function getReview(jobId, cfg, opts) {
210
270
  }
211
271
  return body;
212
272
  }
273
+ /** Fetch the latest Vortex pass for one GitHub PR. */
274
+ export async function getPrVortexReview(owner, repo, prNumber, cfg, opts) {
275
+ const resolved = cfg ?? (await loadConfig());
276
+ const cleanOwner = owner.trim();
277
+ const cleanRepo = repo.trim();
278
+ if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
279
+ throw new CommandError("owner, repo, and a positive pr_number are required", 2, "usage");
280
+ }
281
+ const query = new URLSearchParams({
282
+ owner: cleanOwner,
283
+ repo: cleanRepo,
284
+ pr_number: String(prNumber),
285
+ });
286
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/stacks/pr-review?${query.toString()}`, { signal: opts?.signal, timeoutMs: opts?.timeoutMs });
287
+ if (status === 404) {
288
+ throw new CommandError(`Vortex review not found: ${cleanOwner}/${cleanRepo}#${prNumber}`, REVIEW_EXIT.failed, "not_found");
289
+ }
290
+ if (status === 429) {
291
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
292
+ }
293
+ if (status !== 200) {
294
+ throw new CommandError(`Failed to get PR review (HTTP ${status}): ${JSON.stringify(body)}`, REVIEW_EXIT.failed, "review_failed");
295
+ }
296
+ return body;
297
+ }
213
298
  /** List stacks (Bearer GET /api/v1/stacks). */
214
299
  export async function listStacks(cfg, opts) {
215
300
  const resolved = cfg ?? (await loadConfig());
216
- const { status, body } = await apiFetch(resolved, "/api/v1/stacks", {
301
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/stacks", {
217
302
  timeoutMs: opts?.timeoutMs,
218
303
  });
219
304
  if (status === 404) {
220
305
  throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
221
306
  }
307
+ if (status === 429) {
308
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
309
+ }
222
310
  if (status !== 200) {
223
311
  throw new CommandError(`Failed to list stacks (HTTP ${status}): ${JSON.stringify(body)}`);
224
312
  }
225
313
  const stacks = body.stacks;
226
314
  return Array.isArray(stacks) ? stacks : [];
227
315
  }
316
+ /** Fetch one owned stack with enriched GitHub checks and agent state. */
317
+ export async function getEnrichedStack(stackId, cfg, opts) {
318
+ const resolved = cfg ?? (await loadConfig());
319
+ const id = stackId.trim();
320
+ if (!id) {
321
+ throw new CommandError("stack_id is required", 2, "usage");
322
+ }
323
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, `/api/v1/stacks/enrich?stackId=${encodeURIComponent(id)}`, { timeoutMs: opts?.timeoutMs });
324
+ if (status === 404) {
325
+ throw new CommandError("Stack enrichment is not available on this server yet. Deploy the API update or use the dashboard.");
326
+ }
327
+ if (status === 429) {
328
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds });
329
+ }
330
+ if (status !== 200) {
331
+ throw new CommandError(`Failed to get stack status (HTTP ${status}): ${JSON.stringify(body)}`);
332
+ }
333
+ const stacks = body.stacks;
334
+ if (!Array.isArray(stacks)) {
335
+ throw new CommandError(`Failed to get stack status (HTTP 200): ${JSON.stringify(body)}`);
336
+ }
337
+ return stacks.find((stack) => stack?.id === id) ?? null;
338
+ }
339
+ /** Drop undefined keys so the wire body only carries what was requested. */
340
+ export function stackPolicyBody(policy) {
341
+ const body = {};
342
+ if (typeof policy?.autoEnqueueWhenReady === "boolean") {
343
+ body.autoEnqueueWhenReady = policy.autoEnqueueWhenReady;
344
+ }
345
+ if (policy?.autoReviewOverride !== undefined) {
346
+ body.autoReviewOverride = policy.autoReviewOverride;
347
+ }
348
+ if (policy?.autoPatchOverride !== undefined) {
349
+ body.autoPatchOverride = policy.autoPatchOverride;
350
+ }
351
+ return body;
352
+ }
228
353
  /** Adopt an open PR chain (Bearer POST /api/v1/stacks/adopt). */
229
- export async function adoptStack(owner, repo, prNumber, cfg) {
354
+ export async function adoptStack(owner, repo, prNumber, cfg, policy) {
230
355
  const resolved = cfg ?? (await loadConfig());
231
356
  const { status, body } = await apiFetch(resolved, "/api/v1/stacks/adopt", {
232
357
  method: "POST",
233
- json: { owner, repo, prNumber },
358
+ json: {
359
+ owner,
360
+ repo,
361
+ prNumber,
362
+ ...stackPolicyBody(policy),
363
+ },
234
364
  });
235
365
  if (status === 404) {
236
366
  throw new CommandError("Stacks API is not available on this server yet. Deploy the API update or use the dashboard.");
@@ -272,6 +402,21 @@ async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg)
272
402
  }
273
403
  return body;
274
404
  }
405
+ /** Arm or disarm Auto land on one owned stack. */
406
+ export async function setStackAutoLand(stackId, enabled, cfg) {
407
+ return setStackPolicy(stackId, { autoEnqueueWhenReady: enabled }, cfg);
408
+ }
409
+ /**
410
+ * Patch any subset of one owned stack's automation policy
411
+ * (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
412
+ */
413
+ export async function setStackPolicy(stackId, policy, cfg) {
414
+ const json = stackPolicyBody(policy);
415
+ if (Object.keys(json).length === 0) {
416
+ throw new CommandError("setStackPolicy needs at least one policy key");
417
+ }
418
+ return stackMutation(stackId, "", "PATCH", json, "Failed to set stack policy", cfg);
419
+ }
275
420
  /** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
276
421
  export async function restackStack(stackId, cfg) {
277
422
  return stackMutation(stackId, "/restack", "POST", {}, "Failed to restack", cfg);
@@ -283,9 +428,50 @@ export async function landNextStack(stackId, cfg, opts) {
283
428
  : {};
284
429
  return stackMutation(stackId, "/land-next", "POST", json, "Failed to land next", cfg);
285
430
  }
286
- /** Toggle auto-promote when green (Bearer PATCH /api/v1/stacks/:id). */
287
- export async function setStackAutoPromote(stackId, autoPromoteWhenGreen, cfg) {
288
- return stackMutation(stackId, "", "PATCH", { autoPromoteWhenGreen }, "Failed to update auto-promote", cfg);
431
+ /** List the authenticated user's live merge-queue entries. */
432
+ export async function listMergeQueueEntries(cfg) {
433
+ const resolved = cfg ?? (await loadConfig());
434
+ const { status, body } = await apiFetch(resolved, "/api/v1/stacks/queue");
435
+ if (status === 404) {
436
+ throw new CommandError("Merge queue API is not available on this server yet. Deploy the API update or use the dashboard.");
437
+ }
438
+ if (status !== 200) {
439
+ throw new CommandError(`Failed to list merge queue (HTTP ${status}): ${JSON.stringify(body)}`);
440
+ }
441
+ const entries = body.entries;
442
+ if (!Array.isArray(entries)) {
443
+ throw new CommandError(`Failed to list merge queue (HTTP 200): ${JSON.stringify(body)}`);
444
+ }
445
+ return entries;
446
+ }
447
+ async function queueMutation(route, failLabel, cfg) {
448
+ const resolved = cfg ?? (await loadConfig());
449
+ const { status, body } = await apiFetch(resolved, route, {
450
+ method: "POST",
451
+ json: {},
452
+ });
453
+ if (status === 404) {
454
+ const error = body?.error;
455
+ if (error === "stack_not_found") {
456
+ throw new CommandError("Stack not found");
457
+ }
458
+ if (error === "entry_not_found") {
459
+ throw new CommandError("Merge queue entry not found");
460
+ }
461
+ throw new CommandError("Merge queue API is not available on this server yet. Deploy the API update or use the dashboard.");
462
+ }
463
+ if (status < 200 || status >= 300) {
464
+ throw new CommandError(`${failLabel} (HTTP ${status}): ${JSON.stringify(body)}`);
465
+ }
466
+ return body;
467
+ }
468
+ /** Enqueue a stack for verified landing. */
469
+ export async function enqueueStack(stackId, cfg) {
470
+ return queueMutation(`/api/v1/stacks/${encodeURIComponent(stackId)}/enqueue`, "Failed to add stack to merge queue", cfg);
471
+ }
472
+ /** Cancel a live merge-queue entry by entry id. */
473
+ export async function cancelMergeQueueEntry(entryId, cfg) {
474
+ return queueMutation(`/api/v1/stacks/queue/${encodeURIComponent(entryId)}/cancel`, "Failed to remove merge queue entry", cfg);
289
475
  }
290
476
  /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
291
477
  export async function listThreads(limit = 20, cfg) {
@@ -0,0 +1,37 @@
1
+ export declare const BEARER_SETTINGS: readonly [{
2
+ readonly key: "auto_review_enabled";
3
+ readonly flag: "--auto-review";
4
+ readonly label: "Auto review";
5
+ }, {
6
+ readonly key: "auto_patch_enabled";
7
+ readonly flag: "--auto-patch";
8
+ readonly label: "Auto patch";
9
+ }, {
10
+ readonly key: "vortex_show_thinking_traces";
11
+ readonly flag: "--vortex-thinking";
12
+ readonly label: "Vortex thinking traces";
13
+ }, {
14
+ readonly key: "repo_overview_enabled";
15
+ readonly flag: "--repo-overview";
16
+ readonly label: "Repo overview";
17
+ }, {
18
+ readonly key: "review_unit_land_prs_enabled";
19
+ readonly flag: "--review-unit-land";
20
+ readonly label: "Review Unit land PRs";
21
+ }, {
22
+ readonly key: "cyclone_review_unit_land_prs_enabled";
23
+ readonly flag: "--cyclone-review-unit-land";
24
+ readonly label: "Cyclone Review Unit land PRs";
25
+ }, {
26
+ readonly key: "vortex_seam_specialist_enabled";
27
+ readonly flag: "--vortex-seam";
28
+ readonly label: "Vortex seam specialist";
29
+ }, {
30
+ readonly key: "auto_land_default";
31
+ readonly flag: "--auto-land";
32
+ readonly label: "Auto land (new stacks)";
33
+ }];
34
+ export type BearerSettingsKey = (typeof BEARER_SETTINGS)[number]["key"];
35
+ export declare const BEARER_SETTINGS_KEYS: readonly BearerSettingsKey[];
36
+ export declare const BEARER_SETTINGS_FLAGS: Record<string, BearerSettingsKey>;
37
+ export declare const BEARER_SETTINGS_LABELS: Record<BearerSettingsKey, string>;
@@ -0,0 +1,13 @@
1
+ export const BEARER_SETTINGS = [
2
+ { key: "auto_review_enabled", flag: "--auto-review", label: "Auto review" },
3
+ { key: "auto_patch_enabled", flag: "--auto-patch", label: "Auto patch" },
4
+ { key: "vortex_show_thinking_traces", flag: "--vortex-thinking", label: "Vortex thinking traces" },
5
+ { key: "repo_overview_enabled", flag: "--repo-overview", label: "Repo overview" },
6
+ { key: "review_unit_land_prs_enabled", flag: "--review-unit-land", label: "Review Unit land PRs" },
7
+ { key: "cyclone_review_unit_land_prs_enabled", flag: "--cyclone-review-unit-land", label: "Cyclone Review Unit land PRs" },
8
+ { key: "vortex_seam_specialist_enabled", flag: "--vortex-seam", label: "Vortex seam specialist" },
9
+ { key: "auto_land_default", flag: "--auto-land", label: "Auto land (new stacks)" },
10
+ ];
11
+ export const BEARER_SETTINGS_KEYS = BEARER_SETTINGS.map((r) => r.key);
12
+ export const BEARER_SETTINGS_FLAGS = Object.fromEntries(BEARER_SETTINGS.map((r) => [r.flag, r.key]));
13
+ export const BEARER_SETTINGS_LABELS = Object.fromEntries(BEARER_SETTINGS.map((r) => [r.key, r.label]));
@@ -1,5 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
2
- export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, adoptStack, apiFetch, devicePost, ensureUpperPark, getMe, getReview, landNextStack, listJobs, listStacks, listThreads, parseRetryAfterSeconds, restackStack, setStackAutoPromote, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type ThreadDetail, type ThreadListItem, } from "../api.js";
2
+ export { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, type BearerSettingsKey, } from "../automation-catalog.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type ThreadDetail, type ThreadListItem, } from "../api.js";
3
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
4
- export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
5
+ export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
5
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
@@ -1,5 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
- export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, adoptStack, apiFetch, devicePost, ensureUpperPark, getMe, getReview, landNextStack, listJobs, listStacks, listThreads, parseRetryAfterSeconds, restackStack, setStackAutoPromote, } from "../api.js";
2
+ export { BEARER_SETTINGS, BEARER_SETTINGS_FLAGS, BEARER_SETTINGS_KEYS, BEARER_SETTINGS_LABELS, } from "../automation-catalog.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.js";
3
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
4
- export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
5
+ export { REVIEW_POLL_BACKOFF_MS, REVIEW_POLL_DEFAULT_TIMEOUT_MS, REVIEW_POLL_INTERVAL_MS, REVIEW_POLL_MAX_INTERVAL_MS, REVIEW_POLL_MAX_TRANSIENT_RETRIES, REVIEW_POLL_STRETCH_AFTER_MS, REVIEW_POLL_STRETCH_SPAN_MS, REVIEW_THREAD_SLUG_RE, PrReviewPollTimeoutError, ReviewPollTimeoutError, collectReviewInput, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
5
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
@@ -2,9 +2,13 @@ import { type ThreadListItem } from "../api.js";
2
2
  /** One-line label for a branch/thread row (also used by tests). */
3
3
  export declare function formatBranchRow(item: ThreadListItem): string;
4
4
  /** Interactive list of recently reviewed branches → open chain on select. */
5
- export declare function cmdBranches(args?: string[]): Promise<void>;
5
+ export declare function cmdBranches(args?: string[], opts?: {
6
+ mode?: "oneshot" | "shell";
7
+ }): Promise<void>;
6
8
  /**
7
9
  * Show the review-chain timeline for a branch/thread.
8
10
  * Defaults to `local/<current-git-branch>` when no slug is given.
9
11
  */
10
- export declare function cmdChain(args?: string[]): Promise<void>;
12
+ export declare function cmdChain(args?: string[], opts?: {
13
+ mode?: "oneshot" | "shell";
14
+ }): Promise<void>;