mergestorm 0.3.25 → 0.3.27

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.25 │
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.25: stack wait holds a PR while Cyclone or Vortex works │
48
+ │ ▀▀▀▀▀▀▀▀▀▀▘ mergestorm v0.3.27 │
49
+ │ ▀▀▀▀▀▀▀▀▀ ● msk_live_… · maelstrom │
50
+ │ ▀▀▀▀▀▀ [████████░░░░░░░░░░░░░░░░░░░░░░] 25% used │
51
+ │ ▝▀▀▀▀▀ local reviews + stacked PRs · mergestorm.ai │
52
+ │ ▝▀▀▘ review a diff · usage for tabs · /help for all commands │
53
+ │ ▀ New: mg dismiss records an audited finding dismissal │
54
54
  ╰──────────────────────────────────────────────────────────────────────────────╯
55
55
  mergestorm
56
56
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -96,8 +96,21 @@ mergestorm stack restack <stack-id> Restack descendants
96
96
  mergestorm stack land <stack-id> Land / promote (into review unit when present)
97
97
  mergestorm stack wait <stack-id> [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait)
98
98
  mergestorm stack reset --force Clear local authoring state (not branches/PRs)
99
+ mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview List a Vortex review's finding ids and the seam gate
100
+ mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --reason <text> Dismiss verified-wrong findings (`--all` for the whole review, `--evidence <url>`, `--json`)
99
101
  ```
100
102
 
103
+ ### Dismissing a finding
104
+
105
+ Use `mergestorm dismiss` when a Vortex finding is wrong and you have checked it against the code. It is recorded server side with your GitHub login, the reason, the evidence URL, the scope and the time. Rules:
106
+
107
+ - You need write access to the repository on GitHub, and the repository must be monitored by your Mergestorm account or the PR must be in one of your stacks.
108
+ - `--head` must be the PR's live head and `--review` a Vortex review made at that head. A moved head, another review, or a finding id that is not in that review is refused, and nothing is written.
109
+ - Retrying the same dismissal is safe; it reports the earlier record.
110
+ - The seam gate clears only when every finding of that integration review is dismissed and the review is still the current seam verdict. CI, other reviews and Auto land policy still apply.
111
+ - Vortex does not raise a dismissed finding again on the same diff (the same head, or a restack with an unchanged diff).
112
+ - With `--json`, the result carries `gate` and, for a stack PR, `watch.next` (the next `stack_wait` call).
113
+
101
114
  `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
115
 
103
116
  `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.
package/dist/api.d.ts CHANGED
@@ -267,3 +267,62 @@ export declare function enqueueStack(stackId: string, cfg?: Config): Promise<unk
267
267
  export declare function cancelMergeQueueEntry(entryId: string, cfg?: Config): Promise<unknown>;
268
268
  /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
269
269
  export declare function listThreads(limit?: number, cfg?: Config): Promise<ThreadListItem[]>;
270
+ export type PrFindingDismissInput = {
271
+ owner: string;
272
+ repo: string;
273
+ prNumber: number;
274
+ headSha: string;
275
+ reviewId: number;
276
+ findingIds?: string[];
277
+ scope?: "findings" | "review";
278
+ reason?: string;
279
+ evidenceUrl?: string;
280
+ preview?: boolean;
281
+ };
282
+ export type PrDismissedFinding = {
283
+ finding_id: string;
284
+ finding_key: string;
285
+ path: string | null;
286
+ line: number | null;
287
+ title: string;
288
+ };
289
+ export type PrFindingDismissResult = {
290
+ status: "dismissed" | "preview";
291
+ owner: string;
292
+ repo: string;
293
+ pr_number: number;
294
+ head_sha: string;
295
+ review_id: number;
296
+ review_kind: "seam" | "core";
297
+ scope: "findings" | "review";
298
+ dismissed: PrDismissedFinding[];
299
+ already_dismissed: Array<PrDismissedFinding & {
300
+ actor_login: string;
301
+ dismissed_at: string;
302
+ }>;
303
+ remaining: PrDismissedFinding[];
304
+ all_dismissed: boolean;
305
+ gate: {
306
+ seam: {
307
+ state: string;
308
+ reviewed_sha: string | null;
309
+ review_id: number | null;
310
+ cleared: boolean;
311
+ blocking: boolean;
312
+ } | null;
313
+ other_gates: "unchanged";
314
+ };
315
+ stack_id: string | null;
316
+ };
317
+ export type PrFindingDismissOutcome = {
318
+ ok: true;
319
+ result: PrFindingDismissResult;
320
+ } | {
321
+ ok: false;
322
+ status: number;
323
+ error: string;
324
+ message: string;
325
+ };
326
+ export declare function dismissPrFindings(input: PrFindingDismissInput, cfg?: Config, opts?: {
327
+ signal?: AbortSignal;
328
+ }): Promise<PrFindingDismissOutcome>;
package/dist/api.js CHANGED
@@ -597,3 +597,43 @@ export async function listThreads(limit = 20, cfg) {
597
597
  const items = body.items;
598
598
  return Array.isArray(items) ? items : [];
599
599
  }
600
+ export async function dismissPrFindings(input, cfg, opts) {
601
+ const resolved = cfg ?? (await loadConfig());
602
+ const json = {
603
+ owner: input.owner.trim(),
604
+ repo: input.repo.trim(),
605
+ pr_number: input.prNumber,
606
+ head_sha: input.headSha.trim(),
607
+ review_id: input.reviewId,
608
+ ...(input.scope ? { scope: input.scope } : {}),
609
+ ...(input.findingIds && input.findingIds.length > 0 ? { finding_ids: input.findingIds } : {}),
610
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
611
+ ...(input.evidenceUrl ? { evidence_url: input.evidenceUrl } : {}),
612
+ ...(input.preview ? { preview: true } : {}),
613
+ };
614
+ const { status, body, retryAfterSeconds } = await apiFetch(resolved, "/api/v1/stacks/pr-review/dismiss", {
615
+ method: "POST",
616
+ json,
617
+ signal: opts?.signal,
618
+ });
619
+ if (status === 429) {
620
+ throw new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", {
621
+ retryAfterSeconds,
622
+ });
623
+ }
624
+ if (status === 200)
625
+ return { ok: true, result: body };
626
+ const rejection = (body ?? {});
627
+ if (status === 404 && typeof rejection.error !== "string") {
628
+ throw new CommandError("Finding dismissal is not available on this server yet. Deploy the API update.", REVIEW_EXIT.failed, "not_found");
629
+ }
630
+ if (typeof rejection.error === "string") {
631
+ return {
632
+ ok: false,
633
+ status,
634
+ error: rejection.error,
635
+ message: typeof rejection.message === "string" ? rejection.message : rejection.error,
636
+ };
637
+ }
638
+ throw new CommandError(`Failed to dismiss findings (HTTP ${status}): ${JSON.stringify(body)}`, REVIEW_EXIT.failed, "review_failed");
639
+ }
@@ -1,10 +1,11 @@
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, 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
+ export { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, SETTINGS_WRITABLE_KEYS, adoptStack, apiFetch, cancelMergeQueueEntry, devicePost, dismissPrFindings, 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 PrDismissedFinding, type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult, type PrReviewFinding, type PrReviewFindings, type PrVortexReview, type SettingsPatch, type SettingsResponse, type SettingsWritableKey, type StackDto, type StackPolicyPatch, 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, 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";
7
7
  export { discoverTrunk } from "../git-stack.js";
8
8
  export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel, type StackAgentsBusy } from "../stack-dto.js";
9
9
  export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, type PollStackWatchOptions, type StackWatchCursor, type StackWatchEnvelope, } from "../stack-watch.js";
10
- export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, type StackAgentBusy, type StackBlocker, type StackLayerAgents, } from "../stack-blockers.js";
10
+ export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, type StackAgentBusy, type StackBlocker, type StackBusyAgent, type StackHeldBlocker, type StackLandGatePending, type StackLayerAgents, type StackRepairHint, } from "../stack-blockers.js";
11
+ export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, type StackWatchDoneReason, type StackWatchNext, type StackWatchNextArgs, type StackWatchObligation, type StackWatchObligationInput, type StackWatchReason, } from "../stack-watch-obligation.js";
@@ -1,10 +1,11 @@
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, enqueueStack, ensureUpperPark, getEnrichedStack, getMe, getPrVortexReview, getReview, getSettings, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } from "../api.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, landNextStack, listJobs, listMergeQueueEntries, listStacks, listThreads, parseRetryAfterSeconds, patchSettings, restackStack, setStackAutoLand, setStackPolicy, stackPolicyBody, } 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, 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";
7
7
  export { discoverTrunk } from "../git-stack.js";
8
8
  export { BOUNCE_KIND_LABELS, deriveStackAgentsBusy, mergeQueueBounceLabel } from "../stack-dto.js";
9
9
  export { STACK_WATCH_SCHEMA, STACK_WATCH_DEFAULT_TIMEOUT_MS, STACK_WATCH_BUSY_RECHECK_S, StackWatchError, StackWatchTimeoutError, pollStackWatch, } from "../stack-watch.js";
10
- export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, } from "../stack-blockers.js";
10
+ export { stackBlockers, stackBlockersSummary, sameHead, layerAgents, layerAgentsBusy, conflictLiveParent, stackRepair, agentsCannotClear, } from "../stack-blockers.js";
11
+ export { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchNextArgs, stackWatchObligation, } from "../stack-watch-obligation.js";
@@ -0,0 +1,15 @@
1
+ import { type PrFindingDismissInput, type PrFindingDismissOutcome, type PrFindingDismissResult } from "../api.js";
2
+ import { type Config } from "../config.js";
3
+ import { type StackWatchObligation } from "../stack-watch-obligation.js";
4
+ export declare const DISMISS_USAGE: string;
5
+ export type ParsedDismissArgs = PrFindingDismissInput & {
6
+ json: boolean;
7
+ };
8
+ export declare function parseDismissArgs(args: string[]): ParsedDismissArgs;
9
+ export declare function dismissWatch(result: PrFindingDismissResult): StackWatchObligation | null;
10
+ export declare function formatDismissResult(result: PrFindingDismissResult, watch: StackWatchObligation | null): string;
11
+ export type DismissDeps = {
12
+ loadConfig?: () => Promise<Config>;
13
+ dismiss?: (input: PrFindingDismissInput, cfg: Config) => Promise<PrFindingDismissOutcome>;
14
+ };
15
+ export declare function cmdDismiss(args: string[], deps?: DismissDeps): Promise<void>;
@@ -0,0 +1,145 @@
1
+ import { dismissPrFindings, } from "../api.js";
2
+ import { loadConfig } from "../config.js";
3
+ import { CommandError, REVIEW_EXIT } from "../errors.js";
4
+ import { stackWatchObligation } from "../stack-watch-obligation.js";
5
+ import { parseAdoptTarget, withWatch } from "./stack.js";
6
+ export const DISMISS_USAGE = [
7
+ "usage: mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id>",
8
+ " (--finding <id> [--finding <id>…] | --all) --reason <text> [--evidence <url>] [--json]",
9
+ " mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview [--json]",
10
+ ].join("\n");
11
+ function usage(message) {
12
+ return new CommandError(message ? `${message}\n${DISMISS_USAGE}` : DISMISS_USAGE, REVIEW_EXIT.usage, "usage");
13
+ }
14
+ export function parseDismissArgs(args) {
15
+ const target = [];
16
+ const findingIds = [];
17
+ let headSha = "";
18
+ let reviewId;
19
+ let reason;
20
+ let evidenceUrl;
21
+ let all = false;
22
+ let preview = false;
23
+ let json = false;
24
+ const value = (flag, raw) => {
25
+ if (raw === undefined || raw.startsWith("--"))
26
+ throw usage(`${flag} needs a value`);
27
+ return raw;
28
+ };
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ if (arg === "--head")
32
+ headSha = value(arg, args[++i]).trim().toLowerCase();
33
+ else if (arg === "--review") {
34
+ const raw = value(arg, args[++i]).trim();
35
+ if (!/^[1-9]\d*$/.test(raw) || !Number.isSafeInteger(Number(raw)))
36
+ throw usage("--review needs the numeric GitHub review id");
37
+ reviewId = Number(raw);
38
+ }
39
+ else if (arg === "--finding")
40
+ findingIds.push(...value(arg, args[++i]).split(",").map((id) => id.trim()).filter(Boolean));
41
+ else if (arg === "--all")
42
+ all = true;
43
+ else if (arg === "--reason")
44
+ reason = value(arg, args[++i]);
45
+ else if (arg === "--evidence")
46
+ evidenceUrl = value(arg, args[++i]).trim();
47
+ else if (arg === "--preview")
48
+ preview = true;
49
+ else if (arg === "--json")
50
+ json = true;
51
+ else if (arg.startsWith("--"))
52
+ throw usage(`unknown flag ${arg}`);
53
+ else
54
+ target.push(arg);
55
+ }
56
+ let parsedTarget;
57
+ try {
58
+ parsedTarget = parseAdoptTarget(target);
59
+ }
60
+ catch {
61
+ throw usage("name the PR as <owner/repo>#<n>");
62
+ }
63
+ if (!/^[0-9a-f]{40}$/.test(headSha))
64
+ throw usage("--head needs the full 40-character PR head SHA");
65
+ if (reviewId === undefined)
66
+ throw usage("--review is required");
67
+ if (!preview) {
68
+ if (all && findingIds.length > 0)
69
+ throw usage("pass --finding or --all, not both");
70
+ if (!all && findingIds.length === 0)
71
+ throw usage("pass --finding <id> for each finding, or --all for the whole review");
72
+ if (!reason?.trim())
73
+ throw usage("--reason is required: say why the finding is wrong or not actionable");
74
+ }
75
+ return {
76
+ ...parsedTarget,
77
+ headSha,
78
+ reviewId,
79
+ findingIds: all ? [] : findingIds,
80
+ scope: all ? "review" : "findings",
81
+ ...(reason !== undefined ? { reason } : {}),
82
+ ...(evidenceUrl ? { evidenceUrl } : {}),
83
+ preview,
84
+ json,
85
+ };
86
+ }
87
+ export function dismissWatch(result) {
88
+ if (!result.stack_id)
89
+ return null;
90
+ return stackWatchObligation({ stackId: result.stack_id, terminal: null, unread: true, freshCursor: true });
91
+ }
92
+ function findingLine(finding) {
93
+ const where = finding.path ? `${finding.path}${finding.line ? `:${finding.line}` : ""}` : "review body";
94
+ return ` ${finding.finding_id} ${where} ${finding.title}`;
95
+ }
96
+ export function formatDismissResult(result, watch) {
97
+ const lines = [];
98
+ const pr = `${result.owner}/${result.repo}#${result.pr_number}`;
99
+ const kind = result.review_kind === "seam" ? "seam review" : "Vortex review";
100
+ if (result.status === "preview") {
101
+ lines.push(`${pr} ${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}: ${result.remaining.length} open finding(s)`);
102
+ lines.push(...result.remaining.map(findingLine));
103
+ }
104
+ else {
105
+ lines.push(`${pr} ${kind} ${result.review_id} at ${result.head_sha.slice(0, 7)}`);
106
+ if (result.dismissed.length)
107
+ lines.push("Dismissed:", ...result.dismissed.map(findingLine));
108
+ if (result.already_dismissed.length) {
109
+ lines.push("Already dismissed:", ...result.already_dismissed.map((f) => `${findingLine(f)} (by @${f.actor_login})`));
110
+ }
111
+ if (result.remaining.length)
112
+ lines.push("Still open:", ...result.remaining.map(findingLine));
113
+ }
114
+ const seam = result.gate.seam;
115
+ if (seam) {
116
+ lines.push(seam.cleared
117
+ ? "Seam gate: cleared (approved at this head). CI, other reviews and Auto land policy still apply."
118
+ : seam.blocking
119
+ ? `Seam gate: still blocking (seam_state=${seam.state}).`
120
+ : `Seam gate: not blocking (seam_state=${seam.state}).`);
121
+ }
122
+ if (watch?.next)
123
+ lines.push(`Next: ${watch.next.command}`);
124
+ return lines.join("\n");
125
+ }
126
+ export async function cmdDismiss(args, deps = {}) {
127
+ const parsed = parseDismissArgs(args);
128
+ const cfg = await (deps.loadConfig ?? loadConfig)();
129
+ const { json, ...input } = parsed;
130
+ const outcome = await (deps.dismiss ?? ((i, c) => dismissPrFindings(i, c)))(input, cfg);
131
+ if (!outcome.ok) {
132
+ if (json) {
133
+ console.log(JSON.stringify({ error: { code: outcome.error, message: outcome.message } }, null, 2));
134
+ process.exitCode = REVIEW_EXIT.failed;
135
+ return;
136
+ }
137
+ throw new CommandError(`Dismissal refused (${outcome.error}): ${outcome.message}`, REVIEW_EXIT.failed, "review_failed");
138
+ }
139
+ const watch = dismissWatch(outcome.result);
140
+ if (json) {
141
+ console.log(JSON.stringify(withWatch(outcome.result, watch), null, 2));
142
+ return;
143
+ }
144
+ console.log(formatDismissResult(outcome.result, watch));
145
+ }
@@ -3,6 +3,7 @@ import { CommandError } from "../errors.js";
3
3
  import { cmdBranches, cmdChain } from "./branches.js";
4
4
  import { canBrowse, openTabsBrowser } from "./browse.js";
5
5
  import { cmdCredits } from "./credits.js";
6
+ import { cmdDismiss } from "./dismiss.js";
6
7
  import { cmdJobs, cmdThread } from "./jobs.js";
7
8
  import { cmdLogin } from "./login.js";
8
9
  import { cmdLogout } from "./logout.js";
@@ -79,6 +80,21 @@ export const COMMAND_REGISTRY = [
79
80
  });
80
81
  },
81
82
  },
83
+ {
84
+ name: "dismiss",
85
+ summary: "Dismiss Vortex findings you verified are wrong (audited)",
86
+ usage: [
87
+ "mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --preview",
88
+ " Lists the review's findings (ids) and the seam gate; writes nothing",
89
+ "mergestorm dismiss <owner/repo>#<n> --head <sha> --review <id> --finding <id> --reason <text>",
90
+ " [--finding <id>]… | --all (every finding of that review) [--evidence <url>] [--json]",
91
+ " Needs write access on GitHub; refuses a moved head, another review or an unknown id",
92
+ " Clears the seam gate only when every finding of that seam review is dismissed",
93
+ ],
94
+ async run(args) {
95
+ await cmdDismiss(args);
96
+ },
97
+ },
82
98
  {
83
99
  name: "status",
84
100
  summary: "Job panel (id) or Status / Usage / Jobs tabs",
@@ -224,7 +240,7 @@ export const COMMAND_REGISTRY = [
224
240
  "mergestorm stack reset --force Clear this repo's pre-submit authoring state",
225
241
  "mergestorm stack list [--json] List registered stacks",
226
242
  "mergestorm stack status <stack-id> [--json] Enriched stack status",
227
- "mergestorm stack wait <stack-id> [--json] [--timeout <s>] Wait for stack attention (default 45s, max 300s; 0 = one snapshot, no wait)",
243
+ "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",
228
244
  "mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json] Per-stack policy",
229
245
  "mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] Import an open PR chain",
230
246
  "mergestorm stack restack <stack-id> Restack stack descendants",
@@ -1,3 +1,5 @@
1
+ import { type StackHeldBlocker, type StackRepairHint } from "../stack-blockers.js";
2
+ import { type StackWatchObligation } from "../stack-watch-obligation.js";
1
3
  import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, listStacks, openStackPull, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
2
4
  import { loadConfig } from "../config.js";
3
5
  import { createPrs, findOpenPrNumber } from "../gh.js";
@@ -7,6 +9,12 @@ import { buildPrBodyFromCommit } from "../pr-body.js";
7
9
  import { gitTopLevel } from "../repo-identity.js";
8
10
  import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
9
11
  import { pollStackWatch } from "../stack-watch.js";
12
+ export declare function stackHeldLine(held: StackHeldBlocker | null | undefined): string[];
13
+ export declare function stackRepairLine(repair: StackRepairHint | null | undefined): string[];
14
+ export declare function stackWatchTextLines(watch: StackWatchObligation | null | undefined): string[];
15
+ export declare function openStackWatch(stackId: string, opts?: {
16
+ unread?: boolean;
17
+ }): StackWatchObligation;
10
18
  /** A registered stack layer that would become the parent of a new layer. */
11
19
  export type RegisteredParentHit = {
12
20
  stackId: string;
@@ -140,6 +148,7 @@ export type StackSetDeps = {
140
148
  };
141
149
  /** Human summary lines for a `stack set` write, one per flag. */
142
150
  export declare function stackSetSummary(parsed: StackSetArgs): string[];
151
+ export declare function withWatch(body: unknown, watch: StackWatchObligation | null): unknown;
143
152
  export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
144
153
  export declare function cmdStack(args: string[], options?: {
145
154
  signal?: AbortSignal;
@@ -1,4 +1,5 @@
1
- import { stackBlockersSummary } from "../stack-blockers.js";
1
+ import { stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
2
+ import { STACK_WATCH_NOT_DONE_SENTENCE, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
2
3
  import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, landNextStack, listStacks, openStackPull, restackStack, setStackPolicy, } from "../api.js";
3
4
  import { loadConfig } from "../config.js";
4
5
  import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
@@ -15,6 +16,32 @@ import { runLineTabsBrowser } from "../ui/line-tabs.js";
15
16
  import { present } from "../ui/present.js";
16
17
  import { canBrowse } from "./browse.js";
17
18
  import { openHelpBrowser } from "./help.js";
19
+ export function stackHeldLine(held) {
20
+ if (!held)
21
+ return [];
22
+ const agents = held.waitingOn.map((agent) => agent === "cyclone" ? "Cyclone" : "Vortex").join(" and ");
23
+ return [`Held: #${held.prNumber} ${held.blocker}. Plan the fix; act once ${agents} ${held.waitingOn.length > 1 ? "are" : "is"} idle and the watch returns attention.`];
24
+ }
25
+ export function stackRepairLine(repair) {
26
+ if (!repair)
27
+ return [];
28
+ const files = "files" in repair && repair.files.length ? ` (files: ${repair.files.join(", ")})` : "";
29
+ return [`Repair #${repair.prNumber} ${repair.kind}${files}: ${repair.steps}`];
30
+ }
31
+ export function stackWatchTextLines(watch) {
32
+ if (!watch)
33
+ return [];
34
+ if (watch.done)
35
+ return [watch.message];
36
+ const next = watch.next;
37
+ return [
38
+ ...(next ? [`Next: ${next.command} (MCP: stack_wait ${JSON.stringify(next.args)})`] : []),
39
+ STACK_WATCH_NOT_DONE_SENTENCE,
40
+ ];
41
+ }
42
+ export function openStackWatch(stackId, opts = {}) {
43
+ return stackWatchObligation({ stackId, terminal: null, unread: opts.unread, freshCursor: true });
44
+ }
18
45
  const STACK_USAGE = `usage:
19
46
  mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
20
47
  mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]
@@ -22,6 +49,8 @@ const STACK_USAGE = `usage:
22
49
  mergestorm stack list [--json]
23
50
  mergestorm stack status <stack-id> [--json]
24
51
  mergestorm stack wait <stack-id> [--json] [--timeout <s>]
52
+ --json writes exactly one JSON document to stdout. The timeout or error notice goes to stderr
53
+ (exit 5 on timeout), so do not merge the streams (2>&1) before parsing stdout.
25
54
  mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
26
55
  mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
27
56
  mergestorm stack restack <stack-id> [--json]
@@ -780,6 +809,7 @@ export async function cmdStackSubmit(args, deps = {}) {
780
809
  stackId: stackId ?? null,
781
810
  trunk: data.stack?.trunkBranch ?? meta.trunk,
782
811
  chain: data.chain ?? null,
812
+ watch: stackId ? openStackWatch(stackId) : null,
783
813
  }, null, 2));
784
814
  return;
785
815
  }
@@ -799,6 +829,8 @@ export async function cmdStackSubmit(args, deps = {}) {
799
829
  summary.push(ansi.dim(" Stack registration returned no stack ID"));
800
830
  }
801
831
  summary.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
832
+ if (stackId)
833
+ summary.push(...stackWatchTextLines(openStackWatch(stackId)).map((line) => ` ${line}`));
802
834
  await present("Stack", summary);
803
835
  }
804
836
  async function cmdStackReset(args) {
@@ -868,7 +900,11 @@ export async function cmdStackWait(args, deps = {}) {
868
900
  ...envelope,
869
901
  ...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
870
902
  }, null, 2)
871
- : `Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`);
903
+ : [
904
+ `Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending ?? null, envelope.actAfter ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`,
905
+ ...stackRepairLine(envelope.repair),
906
+ ...stackWatchTextLines(envelope.watch),
907
+ ].join("\n"));
872
908
  try {
873
909
  print(await (deps.pollStackWatch ?? pollStackWatch)(cfg, stackId, {
874
910
  timeoutMs: timeoutS * 1000,
@@ -892,6 +928,7 @@ export async function cmdStackWait(args, deps = {}) {
892
928
  }
893
929
  export async function cmdStackStatus(args, deps = {}) {
894
930
  const usage = "usage: mergestorm stack status <stack-id> [--json]";
931
+ const asJson = args.includes("--json");
895
932
  const positional = args.filter((arg) => arg !== "--json");
896
933
  if (positional.length !== 1)
897
934
  throw new CommandError(usage, 2, "usage");
@@ -899,10 +936,25 @@ export async function cmdStackStatus(args, deps = {}) {
899
936
  const cfg = await (deps.loadConfig ?? loadConfig)();
900
937
  const stack = await (deps.getEnrichedStack ?? getEnrichedStack)(stackId, cfg);
901
938
  if (!stack) {
939
+ const watch = stackWatchObligation({ stackId, terminal: "not_found" });
940
+ if (asJson) {
941
+ console.log(JSON.stringify({ error: { code: "not_found", stack_id: stackId }, watch }, null, 2));
942
+ process.exitCode = 1;
943
+ return;
944
+ }
945
+ console.log(watch.message);
902
946
  throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
903
947
  }
904
- if (args.includes("--json")) {
905
- console.log(JSON.stringify(stack, null, 2));
948
+ const { attention, held, currentCandidate, repair } = stackBlockers(stack);
949
+ const watch = stackWatchObligation({
950
+ stackId: stack.id,
951
+ terminal: stackTerminalReason(stack),
952
+ cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
953
+ attention: attention ? { prNumber: attention.prNumber, blocker: attention.blocker } : null,
954
+ freshCursor: true,
955
+ });
956
+ if (asJson) {
957
+ console.log(JSON.stringify({ ...stack, held, repair, watch }, null, 2));
906
958
  return;
907
959
  }
908
960
  const lines = formatStackHuman(stack, false);
@@ -915,6 +967,7 @@ export async function cmdStackStatus(args, deps = {}) {
915
967
  (layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
916
968
  (layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
917
969
  }
970
+ lines.push(...[...stackHeldLine(held), ...stackRepairLine(repair), ...stackWatchTextLines(watch)].map((line) => ` ${line}`));
918
971
  await present("Stack status", lines);
919
972
  }
920
973
  /** Human summary lines for a `stack set` write, one per flag. */
@@ -932,6 +985,11 @@ export function stackSetSummary(parsed) {
932
985
  }
933
986
  return lines;
934
987
  }
988
+ export function withWatch(body, watch) {
989
+ if (!watch || typeof body !== "object" || body === null || Array.isArray(body))
990
+ return body;
991
+ return { ...body, watch };
992
+ }
935
993
  export async function cmdStackSet(args, deps = {}) {
936
994
  const parsed = parseStackSetArgs(args);
937
995
  const cfg = await (deps.loadConfig ?? loadConfig)();
@@ -940,18 +998,26 @@ export async function cmdStackSet(args, deps = {}) {
940
998
  ...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
941
999
  ...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
942
1000
  }, cfg);
1001
+ const watch = openStackWatch(parsed.stackId, { unread: true });
943
1002
  if (parsed.asJson) {
944
- console.log(JSON.stringify(body, null, 2));
1003
+ console.log(JSON.stringify(withWatch(body, watch), null, 2));
945
1004
  return;
946
1005
  }
947
- await present("Stack", stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)));
1006
+ await present("Stack", [
1007
+ ...stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)),
1008
+ ...stackWatchTextLines(watch).map((line) => ` ${line}`),
1009
+ ]);
948
1010
  }
949
1011
  async function cmdStackAdopt(args) {
950
1012
  const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
951
1013
  const cfg = await loadConfig();
952
1014
  const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
1015
+ const adoptedId = typeof body === "object" && body !== null
1016
+ ? body.stack?.id
1017
+ : undefined;
1018
+ const watch = typeof adoptedId === "string" && adoptedId ? openStackWatch(adoptedId, { unread: true }) : null;
953
1019
  if (asJson) {
954
- console.log(JSON.stringify(body, null, 2));
1020
+ console.log(JSON.stringify(withWatch(body, watch), null, 2));
955
1021
  return;
956
1022
  }
957
1023
  if (typeof body !== "object" || body === null) {
@@ -969,6 +1035,7 @@ async function cmdStackAdopt(args) {
969
1035
  imported.push(ansi.dim(` trunk: ${stack.trunkBranch}`));
970
1036
  }
971
1037
  imported.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
1038
+ imported.push(...stackWatchTextLines(watch).map((line) => ` ${line}`));
972
1039
  await present("Stack", imported);
973
1040
  }
974
1041
  async function cmdStackRestack(args) {
package/dist/shell.js CHANGED
@@ -79,7 +79,7 @@ export async function runShell() {
79
79
  commands: COMMANDS,
80
80
  ...(banner
81
81
  ? {
82
- header: (columns, variant) => banner.rows(columns, variant),
82
+ header: (columns, variant, frame) => banner.rows(columns, variant, frame),
83
83
  }
84
84
  : {}),
85
85
  });