mergestorm 0.3.61 → 0.3.63

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 CHANGED
@@ -45,12 +45,12 @@ On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a comp
45
45
 
46
46
  ```
47
47
  ╭──────────────────────────────────────────────────────────────────────────────╮
48
- │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.61 │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.63 │
49
49
  │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
50
  │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
51
  │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
52
52
  │ ▝▀▀▘ review a diff · usage for tabs · /help for all commands │
53
- │ ▀ New in v0.3.61: Watch names a job Cyclone's push skips │
53
+ │ ▀ New in v0.3.63: Stack create starts a new stack unless --extend│
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -93,7 +93,7 @@ mergestorm thread <slug> Jobs in a review thread
93
93
  mergestorm issue create --title <t> Open a GitHub issue through Mergestorm, no `gh` (`--body`, `--body-file`, `--label`, `--repo`)
94
94
  mergestorm stack create [name] New local stack layer (optional `--onto` / `--trunk` / `--extend`)
95
95
  mergestorm stack submit Push layers, open PRs (`gh`), register via adopt (optional `--extend`, `--server-side`)
96
- mergestorm stack publish Publish the current branch through Mergestorm with no GitHub credentials (`--status` to read it)
96
+ mergestorm stack publish Publish the current branch through Mergestorm with no GitHub credentials (`--base <branch> --extend` to stack, `--status` to read it)
97
97
  mergestorm stack list [--json] List registered stacks
98
98
  mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
99
99
  mergestorm stack restack <stack-id> Restack descendants
@@ -111,7 +111,7 @@ mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --
111
111
 
112
112
  - It calls `stack wait` in slices of at most 300s and carries the cursor (`enrolled_head_sha`, `after_finished_at`, `bounce_id`) from one slice to the next.
113
113
  - It prints nothing while the stack is `waiting` or `in_progress`, including while Cyclone or Vortex holds the blocked PR (`busy[]`, `actAfter: "agents_idle"`).
114
- - On attention it prints `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` and exits 3.
114
+ - On attention it prints `MS-WATCH ATTENTION pr=<n> head=<sha> blocker="<blocker>" repair=<kind>` and exits 3. Attention names the lowest layer with a blocker an agent must fix, on any layer of the stack, not only the current candidate.
115
115
  - A confirmed landing prints `MS-WATCH LANDED stack=<id> reason=landed` and exits 0. `closed`, `archived` and `not_found` end the watch with `MS-WATCH ATTENTION stack=<id> reason=<reason>` and exit 3. A missing stack does not prove a landing; verify each PR before reporting it merged.
116
116
  - A failed read is retried once. If the retry fails too, it prints `MS-WATCH ATTENTION failed stack=<id> error="<message>"` and exits 3. A rate limit is waited out.
117
117
  - `--head <sha>` enrolls the SHA you just pushed. After fixing a blocker, restart the watcher with it.
@@ -138,9 +138,14 @@ Use `mergestorm dismiss` when a Vortex finding is wrong and you have checked it
138
138
 
139
139
  `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`.
140
140
 
141
- `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.
141
+ `stack create` always starts a new single PR from trunk unless you pass **`--extend`**, even when the worktree is still on an earlier PR's branch: it checks out the new branch from `origin/<trunk>` (or local trunk; from `HEAD` when trunk is checked out). It 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.
142
142
 
143
- 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`.
143
+ Both paths are one command:
144
+
145
+ - New stack from trunk: `mg stack create <name>` (from any branch), then `mg stack submit`.
146
+ - Grow a stack: `mg stack create <name> --extend` on the branch to stack onto (or `--onto <branch> --extend`), then `mg stack submit --extend` when the parent is a layer of a registered stack. `mg stack publish --base <branch> --extend` does the same for publish.
147
+
148
+ `--onto` or `--base` with a branch other than trunk and no `--extend` is refused, so nothing is added to a stack by accident. Every create, submit and publish prints which one it made: a new single PR onto trunk, or a layer stacked on its parent.
144
149
 
145
150
  `stack submit` walks the **active** local stack: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; adopt then moves the bottom of a 2+ PR stack onto `mg-stack-<n>` — leave it there; skips heads that already have an open PR; body from the tip commit — Summary + Test plan, preserving Fixes/Closes/Resolves), registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key), then drops that local stack entry so the next create onto trunk is clean. Push is always your local `git` credentials. PRs open as you when `gh` is installed and `gh api user` succeeds; otherwise `stack submit` asks the Mergestorm API (`POST /api/v1/stacks/pulls`) to open them as the Mergestorm Surge App on your behalf, after checking your GitHub account has write access, and assigns you. Stacks, the merge queue and Auto land run through Surge, and Cyclone is the auto-patch App. Without Surge, submit stops before pushing and prints the server's message, which names the App to install and links its install page. `mg settings` prints a `Stacks, merge queue, Auto land` line when the server reports whether that App is ready.
146
151
 
@@ -148,7 +153,7 @@ If the parent branch is already a layer of a **registered** (submitted) stack, `
148
153
 
149
154
  `mg stack submit --server-side` and `mg stack publish` are for a machine that can reach Mergestorm but cannot authenticate to GitHub, such as a cloud coding sandbox. They never run `gh`, `git push` or `git fetch`. `mg` reads your commits from the local repository and sends them to Mergestorm with your API key; Mergestorm writes those exact commits and the branch on GitHub as the Surge App, opens a ready pull request, and registers the stack. Surge must be installed on the repository and your linked GitHub account needs write access to it.
150
155
 
151
- These are your own changes, not Cyclone's: the stack is registered with **Auto patch pinned off**, Auto land on for a new stack (`--auto-land off` to keep it off), and Auto review left to your account setting. `--auto-patch on` is refused in this mode. Fix review findings yourself and send the new commits with `mg stack publish`.
156
+ The stack is registered with Auto land on for a new stack (`--auto-land off` to keep it off). Auto patch and Auto review follow the stack, repository and account settings like any other stack; `--auto-patch` and `--auto-review` pin them on the stack after it is registered. Send later commits with `mg stack publish`.
152
157
 
153
158
  ```bash
154
159
  mg issue create --title "The thing is broken" --body-file issue.md --label bug --json # no gh
@@ -167,7 +172,7 @@ mg stack publish --status # absent | pushed | opened | registered
167
172
  - **Read-only home directory.** `mg` keeps the saved login and the local stack state of `stack create` / `stack submit` under `~/.mergestorm`. Where the home directory cannot be written, set `MERGESTORM_HOME` to a writable directory (outside the repository) and every command uses that instead. `MERGESTORM_API_KEY` supplies the key without `mg login`.
168
173
  - **Limits.** 20 commits, 500 changed paths, 2 MiB a file and 8 MiB in total per publication. Submodules and merge commits are refused with the path or the reason. `stack submit --server-side` publishes the first two layers of a stack; a third layer needs a park freeze, which this mode does not create, so add it once the first two are registered.
169
174
 
170
- Legacy repo-local `.mergestorm/stack.json` state migrates automatically on the next stack command. If local authoring state is stale or malformed, use `mg stack reset --force`; this clears only pre-submit CLI state and never deletes branches, PRs, or registered stacks.
175
+ If local authoring state is stale or malformed, use `mg stack reset --force`; this clears only pre-submit CLI state and never deletes branches, PRs, or registered stacks.
171
176
 
172
177
  ### `login`
173
178
 
package/dist/api.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { type Config } from "./config.js";
2
- import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS, type BearerSettingsKey as SettingsWritableKey } from "./automation-catalog.js";
2
+ import { BEARER_SETTINGS_KEYS as SETTINGS_WRITABLE_KEYS } from "./automation-catalog.js";
3
3
  import { CommandError } from "./errors.js";
4
4
  import type { MergeQueueEntryDto, StackDto } from "./stack-dto.js";
5
- export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackBranchState, StackCiStatus, StackDto, StackLayerChecks, StackLayerDto, StackReviewStatus, StackUnitDto, StackUnitMemberDto, } from "./stack-dto.js";
5
+ export type { MergeQueueBounceDetail, MergeQueueBounceKind, MergeQueueEnqueuedBy, MergeQueueEnqueuedVia, MergeQueueEntryDto, MergeQueueEntryState, StackDto, StackLayerDto, } from "./stack-dto.js";
6
6
  /** Default per-request timeout so a hung API cannot stall the shell/CI forever. */
7
7
  export declare const DEFAULT_API_TIMEOUT_MS = 30000;
8
8
  /** Message prefix of the CommandError thrown when a request exceeds its timeout. */
@@ -142,6 +142,14 @@ export type PrVortexReview = {
142
142
  reviewed_at: string | null;
143
143
  finding_count: number;
144
144
  findings: PrReviewFindings | null;
145
+ dismissed_findings?: Array<{
146
+ path: string;
147
+ line: number;
148
+ title: string;
149
+ severity: string;
150
+ finding_key: string;
151
+ github_comment_id: number | null;
152
+ }>;
145
153
  patch_policy: unknown | null;
146
154
  /**
147
155
  * MS-01: True when the returned row is a resting completion but a newer
@@ -167,7 +175,6 @@ export declare function applyPrReviewQuery(query: URLSearchParams, opts: PrRevie
167
175
  * The connected flags are read-only there and deliberately absent here.
168
176
  */
169
177
  export { SETTINGS_WRITABLE_KEYS };
170
- export type { SettingsWritableKey };
171
178
  export type SettingsPatch = Partial<import("./automation-catalog.js").BearerSettingsValues>;
172
179
  /** GET and PATCH `/api/v1/settings` both answer with this shape. */
173
180
  export type SettingsResponse = import("./automation-catalog.js").BearerSettingsValues & {
@@ -389,8 +396,6 @@ export declare function openStackIssue(input: {
389
396
  url: string;
390
397
  created: boolean;
391
398
  }>;
392
- /** Arm or disarm Auto land on one owned stack. */
393
- export declare function setStackAutoLand(stackId: string, enabled: boolean, cfg?: Config): Promise<unknown>;
394
399
  /**
395
400
  * Patch any subset of one owned stack's automation policy
396
401
  * (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
@@ -399,9 +404,7 @@ export declare function setStackPolicy(stackId: string, policy: StackPolicyPatch
399
404
  /** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
400
405
  export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
401
406
  /** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
402
- export declare function landNextStack(stackId: string, cfg?: Config, opts?: {
403
- prNumber?: number;
404
- }): Promise<unknown>;
407
+ export declare function landNextStack(stackId: string, cfg?: Config): Promise<unknown>;
405
408
  /** List the authenticated user's live merge-queue entries. */
406
409
  export declare function listMergeQueueEntries(cfg?: Config, opts?: {
407
410
  stackId?: string;
@@ -463,7 +466,12 @@ export type PrFindingDismissResult = {
463
466
  other_gates: "unchanged";
464
467
  };
465
468
  stack_id: string | null;
469
+ threads?: Array<{
470
+ finding_id: string;
471
+ state: "resolved" | "already_resolved" | "not_found" | "failed";
472
+ }>;
466
473
  };
474
+ export declare function dismissThreadsLine(result: Pick<PrFindingDismissResult, "threads">): string | null;
467
475
  export type PrFindingDismissOutcome = {
468
476
  ok: true;
469
477
  result: PrFindingDismissResult;
package/dist/api.js CHANGED
@@ -269,8 +269,6 @@ export async function getMe(cfg, opts) {
269
269
  const { status, body } = await apiFetch(resolved, "/api/v1/me", {
270
270
  timeoutMs: opts?.timeoutMs,
271
271
  });
272
- if (status === 404)
273
- return null;
274
272
  if (status !== 200)
275
273
  return null;
276
274
  return body;
@@ -432,18 +430,7 @@ export async function adoptStack(owner, repo, prNumber, cfg, policy) {
432
430
  const message = body && typeof body === "object" && "message" in body && typeof body.message === "string"
433
431
  ? body.message
434
432
  : undefined;
435
- const code = error === "auth_invalid" ||
436
- error === "api_timeout" ||
437
- error === "not_a_repo" ||
438
- error === "not_found" ||
439
- error === "missing_api_key" ||
440
- error === "registered_parent" ||
441
- error === "usage" ||
442
- error === "review_quota" ||
443
- error === "review_failed" ||
444
- error === "review_timeout" ||
445
- error === "rate_limited" ||
446
- error === "cyclone_not_installed" ||
433
+ const code = error === "cyclone_not_installed" ||
447
434
  error === "pr_merged" ||
448
435
  error === "pr_closed" ||
449
436
  error === "not_stacked" ||
@@ -689,10 +676,6 @@ async function stackMutation(stackId, pathSuffix, method, json, failLabel, cfg)
689
676
  }
690
677
  return body;
691
678
  }
692
- /** Arm or disarm Auto land on one owned stack. */
693
- export async function setStackAutoLand(stackId, enabled, cfg) {
694
- return setStackPolicy(stackId, { autoEnqueueWhenReady: enabled }, cfg);
695
- }
696
679
  /**
697
680
  * Patch any subset of one owned stack's automation policy
698
681
  * (Bearer PATCH /api/v1/stacks/:id). `null` clears an override.
@@ -709,11 +692,8 @@ export async function restackStack(stackId, cfg) {
709
692
  return stackMutation(stackId, "/restack", "POST", {}, "Failed to restack", cfg);
710
693
  }
711
694
  /** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
712
- export async function landNextStack(stackId, cfg, opts) {
713
- const json = opts?.prNumber != null && Number.isInteger(opts.prNumber) && opts.prNumber > 0
714
- ? { prNumber: opts.prNumber }
715
- : {};
716
- return stackMutation(stackId, "/land-next", "POST", json, "Failed to land next", cfg);
695
+ export async function landNextStack(stackId, cfg) {
696
+ return stackMutation(stackId, "/land-next", "POST", {}, "Failed to land next", cfg);
717
697
  }
718
698
  /** List the authenticated user's live merge-queue entries. */
719
699
  export async function listMergeQueueEntries(cfg, opts) {
@@ -793,6 +773,19 @@ export async function listThreads(limit = 20, cfg) {
793
773
  const items = body.items;
794
774
  return Array.isArray(items) ? items : [];
795
775
  }
776
+ export function dismissThreadsLine(result) {
777
+ const threads = result.threads ?? [];
778
+ if (threads.length === 0)
779
+ return null;
780
+ const closed = threads.filter((thread) => thread.state === "resolved" || thread.state === "already_resolved").length;
781
+ const failed = threads.filter((thread) => thread.state === "failed").map((thread) => thread.finding_id);
782
+ const missing = threads.filter((thread) => thread.state === "not_found").map((thread) => thread.finding_id);
783
+ return [
784
+ `GitHub threads: ${closed} of ${threads.length} resolved.`,
785
+ ...(failed.length ? [`Could not resolve ${failed.join(", ")}; run the same dismissal again to retry.`] : []),
786
+ ...(missing.length ? [`No thread found for ${missing.join(", ")}.`] : []),
787
+ ].join(" ");
788
+ }
796
789
  export async function dismissPrFindings(input, cfg, opts) {
797
790
  const resolved = cfg ?? (await loadConfig());
798
791
  const json = {
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, type Config, } from "../config.js";
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, 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, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type RepoSettings, type RepoSettingsKey, type RepoSettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, type StackPublication, type StackPublicationRequest, type StackPublicationStatus, type ThreadDetail, type ThreadListItem, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, type ApiFetchInit, type ApiFetchResult, type JobListItem, type MeResponse, type MergeQueueBounceDetail, type MergeQueueBounceKind, type MergeQueueEnqueuedBy, type MergeQueueEnqueuedVia, type MergeQueueEntryDto, type MergeQueueEntryState, type PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type RepoSettings, type RepoSettingsKey, type RepoSettingsPatch, type SettingsResponse, type StackDto, type StackPolicyPatch, type StackPublication, type StackPublicationRequest, type StackPublicationStatus, type ThreadDetail, type ThreadListItem, } from "../api.js";
4
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, type CommandErrorCode, type CommandErrorExtras, } from "../errors.js";
5
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, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, type PollReviewOptions, type PollPrVortexReviewOptions, type ReviewInput, type SubmitReviewOptions, } from "../commands/review-client.js";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, type ReviewEnvelopeFallbacks, type ReviewJobEnvelope, type ReviewJobRow, } from "../ui/job-envelope.js";
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_API, apiBase, clearConfig, configPath, keyDisplay, loadConfig, mergestormHome, resolveApiKey, saveConfig, } from "../config.js";
2
2
  export { BEARER_SETTINGS, BEARER_BOOLEAN_SETTINGS_KEYS, 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, dismissPrFindings, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
3
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, dismissThreadsLine, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, listRepoSettings, patchRepoSettings, REPO_SETTINGS_KEYS, getStackPublication, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, openStackIssue, parseRetryAfterSeconds, patchSettings, restackStack, setStackPolicy, stackPolicyBody, submitStackPublication, } from "../api.js";
4
4
  export { CommandError, DetachedError, RATE_LIMITED_HINT, REVIEW_EXIT, isCommandErrorCode, rateLimitedMessage, } from "../errors.js";
5
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, noPrReviewYetMessage, collectReviewInput, formatReviewSubmitError, isTransientReviewPollError, isTransientReviewPollStatus, loadReviewContext, pollPrVortexReview, pollReview, stretchedPollIntervalMs, submitReview, transientRetryWaitMs, } from "../commands/review-client.js";
6
6
  export { REVIEW_JOB_ENVELOPE_SCHEMA, toReviewJobEnvelope, } from "../ui/job-envelope.js";
@@ -1,4 +1,4 @@
1
- import { dismissPrFindings, } from "../api.js";
1
+ import { dismissPrFindings, dismissThreadsLine, } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError, REVIEW_EXIT } from "../errors.js";
4
4
  import { stackWatchObligation } from "../stack-watch-obligation.js";
@@ -110,6 +110,9 @@ export function formatDismissResult(result, watch) {
110
110
  }
111
111
  if (result.remaining.length)
112
112
  lines.push("Still open:", ...result.remaining.map(findingLine));
113
+ const threads = dismissThreadsLine(result);
114
+ if (threads)
115
+ lines.push(threads);
113
116
  }
114
117
  const seam = result.gate.seam;
115
118
  if (seam) {
@@ -6,4 +6,7 @@ import { type MeResponse } from "../api.js";
6
6
  */
7
7
  export declare function validateApiKey(cfg: Config): Promise<MeResponse>;
8
8
  export declare function pendingLoginPath(): string;
9
- export declare function cmdLogin(args: string[]): Promise<void>;
9
+ export interface LoginDeps {
10
+ openBrowser: (url: string) => void;
11
+ }
12
+ export declare function cmdLogin(args: string[], deps?: LoginDeps): Promise<void>;
@@ -200,7 +200,7 @@ async function loginFinish() {
200
200
  console.log(`Waiting for approval of code ${pending.userCode} at ${pending.openUrl}`);
201
201
  await pollDeviceLogin(pending, "mergestorm login --finish", "Run `mergestorm login --start`, then `mergestorm login --finish`.");
202
202
  }
203
- export async function cmdLogin(args) {
203
+ export async function cmdLogin(args, deps = { openBrowser }) {
204
204
  if (args.includes("--key")) {
205
205
  await loginWithKey();
206
206
  return;
@@ -219,8 +219,10 @@ export async function cmdLogin(args) {
219
219
  console.log("To sign in, open this URL and confirm the code:\n");
220
220
  console.log(` ${ansi.brightGreen(pending.openUrl)}`);
221
221
  console.log(`\n Code: ${ansi.bold(pending.userCode)}\n`);
222
- openBrowser(pending.openUrl);
223
- if (!process.stdout.isTTY) {
222
+ if (process.stdout.isTTY) {
223
+ deps.openBrowser(pending.openUrl);
224
+ }
225
+ else {
224
226
  console.log("No terminal here. If this command may be cut off before the approval, use `mergestorm login --start`, then `mergestorm login --finish`.");
225
227
  }
226
228
  console.log("Waiting for approval…");
@@ -264,7 +264,7 @@ export const COMMAND_REGISTRY = [
264
264
  "mergestorm stack list [--json] List registered stacks",
265
265
  "mergestorm stack status <stack-id> [--json] Enriched stack status",
266
266
  "mergestorm stack wait <stack-id> [--json] [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait). --json: one document on stdout; timeout notice on stderr, exit 5",
267
- "mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--json] Background watcher: silent while waiting; MS-WATCH ATTENTION exits 3, MS-WATCH LANDED exits 0",
267
+ "mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--bounce-id <id>] [--after-finished-at <iso>] [--json] Background watcher: silent while waiting; MS-WATCH ATTENTION exits 3, MS-WATCH LANDED exits 0",
268
268
  "mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--auto-resolve-conflicts on|off|default] [--auto-fix-ci on|off|default] [--json] Per-stack policy",
269
269
  "mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] Import an open PR chain",
270
270
  "mergestorm stack restack <stack-id> Restack stack descendants",
@@ -10,8 +10,6 @@ export declare const REVIEW_POLL_STRETCH_AFTER_MS = 60000;
10
10
  export declare const REVIEW_POLL_STRETCH_SPAN_MS = 60000;
11
11
  /** Same thread slug contract as POST /api/v1/reviews. */
12
12
  export declare const REVIEW_THREAD_SLUG_RE: RegExp;
13
- /** Same product limit as `api/src/v1/reviews.ts` `MAX_DIFF_BYTES`. */
14
- export declare const MAX_REVIEW_DIFF_BYTES = 1000000;
15
13
  export declare const MAX_CONTEXT_TEXT_CHARS = 60000;
16
14
  export declare const MAX_CONTEXT_FILES = 10;
17
15
  export declare const MAX_CONTEXT_FILE_CHARS = 16000;
@@ -59,6 +57,7 @@ export type LoadReviewContextOptions = {
59
57
  };
60
58
  /** Load --context / --context-file and enforce the API caps client-side. */
61
59
  export declare function loadReviewContext(opts: LoadReviewContextOptions): Promise<ReviewContextPayload>;
60
+ export declare function reviewIdempotencyKey(input: ReviewInput, opts?: SubmitReviewOptions, afterJobId?: string | null): string;
62
61
  export declare function submitReview(cfg: Config, input: ReviewInput, opts?: SubmitReviewOptions): Promise<{
63
62
  status: number;
64
63
  row: ReviewJobRow;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile, realpath } from "node:fs/promises";
2
3
  import { join, relative, resolve, sep } from "node:path";
3
4
  import { apiFetch, applyPrReviewQuery, isPositiveInteger, } from "../api.js";
@@ -12,8 +13,6 @@ export const REVIEW_POLL_STRETCH_AFTER_MS = 60_000;
12
13
  export const REVIEW_POLL_STRETCH_SPAN_MS = 60_000;
13
14
  /** Same thread slug contract as POST /api/v1/reviews. */
14
15
  export const REVIEW_THREAD_SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/;
15
- /** Same product limit as `api/src/v1/reviews.ts` `MAX_DIFF_BYTES`. */
16
- export const MAX_REVIEW_DIFF_BYTES = 1_000_000;
17
16
  export const MAX_CONTEXT_TEXT_CHARS = 60_000;
18
17
  export const MAX_CONTEXT_FILES = 10;
19
18
  export const MAX_CONTEXT_FILE_CHARS = 16_000;
@@ -128,7 +127,29 @@ export async function loadReviewContext(opts) {
128
127
  ...(contextFiles.length > 0 ? { contextFiles } : {}),
129
128
  };
130
129
  }
130
+ const REPLAYED_JOB_RESUBMIT_STATUSES = ["failed", "quota_exceeded"];
131
+ const MAX_REPLAYED_JOB_RESUBMITS = 5;
132
+ export function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
133
+ const request = [
134
+ input.thread, input.branch, input.repo ?? null, input.baseLabel, input.headLabel, input.diff, input.files,
135
+ opts.routerMode ?? null, [...new Set(opts.specialists ?? [])].sort(), opts.context ?? null, opts.contextFiles ?? [], opts.webhookUrl ?? null,
136
+ afterJobId,
137
+ ];
138
+ return `mg-auto-${createHash("sha256").update(JSON.stringify(request)).digest("hex")}`;
139
+ }
131
140
  export async function submitReview(cfg, input, opts = {}) {
141
+ const explicitKey = opts.idempotencyKey?.trim() || undefined;
142
+ let idempotencyKey = explicitKey ?? reviewIdempotencyKey(input, opts);
143
+ for (let resubmits = 0;; resubmits++) {
144
+ const submitted = await postReview(cfg, input, opts, idempotencyKey);
145
+ const replayedJobId = submitted.row.job_id ?? submitted.row.id ?? null;
146
+ if (explicitKey || submitted.status !== 200 || !replayedJobId || resubmits >= MAX_REPLAYED_JOB_RESUBMITS ||
147
+ !REPLAYED_JOB_RESUBMIT_STATUSES.includes(submitted.row.status ?? ""))
148
+ return submitted;
149
+ idempotencyKey = reviewIdempotencyKey(input, opts, replayedJobId);
150
+ }
151
+ }
152
+ async function postReview(cfg, input, opts, idempotencyKey) {
132
153
  const { status, body, retryAfterSeconds } = await apiFetch(cfg, "/api/v1/reviews", {
133
154
  method: "POST",
134
155
  json: {
@@ -143,7 +164,7 @@ export async function submitReview(cfg, input, opts = {}) {
143
164
  ...(opts.specialists?.length ? { specialists: opts.specialists } : {}),
144
165
  ...(opts.context ? { context: opts.context } : {}),
145
166
  ...(opts.contextFiles?.length ? { context_files: opts.contextFiles } : {}),
146
- ...(opts.idempotencyKey ? { idempotency_key: opts.idempotencyKey } : {}),
167
+ idempotency_key: idempotencyKey,
147
168
  ...(opts.webhookUrl ? { webhook_url: opts.webhookUrl } : {}),
148
169
  },
149
170
  signal: opts.signal,
@@ -4,7 +4,7 @@ import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackP
4
4
  import { loadConfig } from "../config.js";
5
5
  import { createPrs, findOpenPrNumber } from "../gh.js";
6
6
  import { parseGithubOriginRepo } from "../git.js";
7
- import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
7
+ import { branchExists, commitsAheadOf, createBranchFrom, createBranchFromHead, trunkStartRef, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
8
8
  import { buildPrBodyFromCommit } from "../pr-body.js";
9
9
  import { publishBranchServerSide, type WorkingBranchDeps } from "../server-publish.js";
10
10
  import { gitTopLevel } from "../repo-identity.js";
@@ -33,6 +33,11 @@ export declare function findRegisteredParent(stacks: StackDto[], parentBranch: s
33
33
  * Local unsubmitted parents are fine; only API-listed layers trip this.
34
34
  */
35
35
  export declare function assertMayParentOntoRegistered(parentBranch: string, stacks: StackDto[], extend: boolean, verb: "create" | "submit", owner?: string, repo?: string): RegisteredParentHit | null;
36
+ export declare function stackShapeLine(input: {
37
+ layers: number;
38
+ base: string;
39
+ extendedStack: boolean;
40
+ }): string;
36
41
  export declare function requireStackId(raw: string | undefined, usage: string): string;
37
42
  /** Parse `owner/repo#12` or `owner/repo 12`. */
38
43
  export declare function parseAdoptTarget(args: string[]): {
@@ -110,6 +115,8 @@ export type StackCreateDeps = {
110
115
  currentBranch?: typeof currentBranch;
111
116
  branchExists?: typeof branchExists;
112
117
  createBranchFromHead?: typeof createBranchFromHead;
118
+ createBranchFrom?: typeof createBranchFrom;
119
+ trunkStartRef?: typeof trunkStartRef;
113
120
  deleteBranch?: typeof deleteBranch;
114
121
  discoverTrunk?: typeof discoverTrunk;
115
122
  defaultLayerBranchName?: typeof defaultLayerBranchName;
@@ -156,6 +163,7 @@ export declare function parseStackPublishArgs(args: string[]): {
156
163
  title?: string;
157
164
  replaceHead?: string;
158
165
  autoLand?: boolean;
166
+ extend: boolean;
159
167
  status: boolean;
160
168
  asJson: boolean;
161
169
  };