merge-steward 0.5.1 → 0.5.3

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.
@@ -8,6 +8,7 @@ export declare class ShellGitOperations implements GitOperations, SpeculativeBra
8
8
  private git;
9
9
  fetch(remote?: string): Promise<void>;
10
10
  headSha(branch: string): Promise<string>;
11
+ isAncestor(ancestor: string, descendant: string): Promise<boolean>;
11
12
  rebase(branch: string, onto: string): Promise<RebaseResult>;
12
13
  push(branch: string, force?: boolean): Promise<void>;
13
14
  buildSpeculative(prBranch: string, baseBranch: string, specName: string): Promise<MergeResult>;
@@ -33,8 +33,25 @@ export class ShellGitOperations {
33
33
  const result = await this.git(["rev-parse", branch]);
34
34
  return result.stdout.trim();
35
35
  }
36
+ async isAncestor(ancestor, descendant) {
37
+ const result = await this.git(["merge-base", "--is-ancestor", ancestor, descendant], { allowNonZero: true });
38
+ if (result.exitCode === 0)
39
+ return true;
40
+ if (result.exitCode === 1)
41
+ return false;
42
+ throw new Error(`git merge-base --is-ancestor failed: ${result.stderr || result.stdout}`);
43
+ }
36
44
  async rebase(branch, onto) {
37
- await this.git(["checkout", branch]);
45
+ const remoteBranchRef = `refs/remotes/origin/${branch}`;
46
+ const remoteBranchExists = await this.git(["show-ref", "--verify", remoteBranchRef], { allowNonZero: true });
47
+ if (remoteBranchExists.exitCode === 0) {
48
+ // Always start from the freshly fetched remote branch tip so stale local branches
49
+ // cannot reintroduce old commits into queue processing.
50
+ await this.git(["checkout", "-B", branch, `origin/${branch}`]);
51
+ }
52
+ else {
53
+ await this.git(["checkout", branch]);
54
+ }
38
55
  const result = await this.git(["rebase", onto], { allowNonZero: true });
39
56
  if (result.exitCode !== 0) {
40
57
  await this.git(["rebase", "--abort"], { allowNonZero: true });
@@ -6,6 +6,7 @@ import type { CIStatus, CheckResult, IncidentRecord, MergeResult, PRStatus, Queu
6
6
  export interface GitOperations {
7
7
  fetch(remote?: string): Promise<void>;
8
8
  headSha(branch: string): Promise<string>;
9
+ isAncestor(ancestor: string, descendant: string): Promise<boolean>;
9
10
  rebase(branch: string, onto: string): Promise<RebaseResult>;
10
11
  push(branch: string, force?: boolean): Promise<void>;
11
12
  }
@@ -154,6 +154,26 @@ async function performRebase(ctx, entry, baseSha) {
154
154
  return;
155
155
  }
156
156
  const headSha = result.newHeadSha ?? entry.headSha;
157
+ await ctx.git.fetch();
158
+ const latestRemoteHead = await ctx.git.headSha(ref(ctx, entry.branch));
159
+ const candidateKeepsLatestRemote = await ctx.git.isAncestor(latestRemoteHead, headSha);
160
+ if (!candidateKeepsLatestRemote) {
161
+ const detail = latestRemoteHead === entry.headSha
162
+ ? `candidate diverged from remote head: expected ${entry.headSha.slice(0, 8)}, ` +
163
+ `latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`
164
+ : `remote advanced during rebase: expected ${entry.headSha.slice(0, 8)}, ` +
165
+ `latest ${latestRemoteHead.slice(0, 8)}, candidate ${headSha.slice(0, 8)}`;
166
+ emit(ctx, entry, "branch_mismatch", { detail });
167
+ if (latestRemoteHead !== entry.headSha) {
168
+ ctx.store.updateHead(entry.id, latestRemoteHead);
169
+ }
170
+ else {
171
+ ctx.store.transition(entry.id, "queued", {
172
+ ...CLEAN_CI,
173
+ }, `stale local branch diverged from ${latestRemoteHead.slice(0, 8)}`);
174
+ }
175
+ return;
176
+ }
157
177
  await ctx.git.push(entry.branch, true);
158
178
  emit(ctx, entry, "rebase_succeeded", { baseSha });
159
179
  // Build speculative branch for downstream entries.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { formatEntryEvent, progressBar, relativeTime, shortSha, statusColor, summarizeQueueBlock } from "./format.js";
3
+ import { formatEntryEvent, humanStatus, nextStepLabel, progressBar, queueProgress, relativeTime, shortSha, statusColor, summarizeQueueBlock } from "./format.js";
4
4
  import { EntryStateGraph } from "./EntryStateGraph.js";
5
5
  import { ExternalRepairObservation } from "./ExternalRepairObservation.js";
6
6
  import { buildEntryStateGraph, buildExternalRepairObservations } from "./state-visualization.js";
@@ -17,5 +17,6 @@ export function DetailView({ detail, isHead, activeIndex, activeCount, headPrNum
17
17
  headPrNumber,
18
18
  queueBlock,
19
19
  });
20
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), _jsx(Text, { color: statusColor(entry.status), children: entry.status }), _jsxs(Text, { dimColor: true, children: ["pos ", entry.position] }), _jsxs(Text, { dimColor: true, children: ["generation ", entry.generation] }), _jsxs(Text, { dimColor: true, children: ["retry ", entry.retryAttempts, "/", entry.maxRetries] })] }), _jsx(Text, { children: entry.branch }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["head ", shortSha(entry.headSha)] }), _jsxs(Text, { dimColor: true, children: ["base ", shortSha(entry.baseSha)] }), entry.issueKey && _jsx(Text, { dimColor: true, children: entry.issueKey })] }), isHead && queueBlock && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", summarizeQueueBlock(queueBlock) ?? "main branch is unhealthy", "."] }), _jsxs(Text, { dimColor: true, children: [queueBlock.baseBranch, queueBlock.baseSha ? ` @ ${shortSha(queueBlock.baseSha)}` : ""] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? entry.prNumber, " will resume automatically once main recovers."] })] })), entry.maxRetries > 0 && (_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "retry" }), _jsx(Text, { children: progressBar(entry.retryAttempts, entry.maxRetries, 10) }), _jsxs(Text, { dimColor: true, children: [entry.retryAttempts, "/", entry.maxRetries] })] })), _jsx(EntryStateGraph, { main: graph.main, exits: graph.exits }), _jsx(ExternalRepairObservation, { observations: observations }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Incidents" }), incidents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No incidents." })) : (incidents.map((incident) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(incident.at).padStart(4, " ") }), _jsx(Text, { color: "red", children: incident.failureClass }), _jsx(Text, { dimColor: true, children: incident.outcome })] }, incident.id))))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Events" }), events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No events yet." })) : (events.slice(-16).map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEntryEvent(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
20
+ const pipeline = queueProgress(entry.status);
21
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), entry.issueKey ? _jsx(Text, { children: entry.issueKey }) : null, _jsx(Text, { color: statusColor(entry.status), children: humanStatus(entry.status) }), _jsxs(Text, { dimColor: true, children: ["pos ", entry.position] }), _jsxs(Text, { dimColor: true, children: ["generation ", entry.generation] }), _jsxs(Text, { dimColor: true, children: ["retry ", entry.retryAttempts, "/", entry.maxRetries] })] }), _jsx(Text, { children: entry.branch }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["head ", shortSha(entry.headSha)] }), _jsxs(Text, { dimColor: true, children: ["base ", shortSha(entry.baseSha)] }), entry.issueKey && _jsx(Text, { dimColor: true, children: entry.issueKey })] }), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "progress" }), _jsx(Text, { children: progressBar(pipeline.current, pipeline.total, 12) }), _jsx(Text, { dimColor: true, children: nextStepLabel(entry.status) })] }), isHead && queueBlock && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", summarizeQueueBlock(queueBlock) ?? "main branch is unhealthy", "."] }), _jsxs(Text, { dimColor: true, children: [queueBlock.baseBranch, queueBlock.baseSha ? ` @ ${shortSha(queueBlock.baseSha)}` : ""] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? entry.prNumber, " will resume automatically once main recovers."] })] })), entry.maxRetries > 0 && (_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "retry" }), _jsx(Text, { children: progressBar(entry.retryAttempts, entry.maxRetries, 10) }), _jsxs(Text, { dimColor: true, children: [entry.retryAttempts, "/", entry.maxRetries] })] })), _jsx(EntryStateGraph, { main: graph.main, exits: graph.exits }), _jsx(ExternalRepairObservation, { observations: observations }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Incidents" }), incidents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No incidents." })) : (incidents.map((incident) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(incident.at).padStart(4, " ") }), _jsx(Text, { color: "red", children: incident.failureClass }), _jsx(Text, { dimColor: true, children: incident.outcome })] }, incident.id))))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Events" }), events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No events yet." })) : (events.slice(-16).map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEntryEvent(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
21
22
  }
@@ -1,23 +1,29 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { Box, Text, useStdout } from "ink";
4
- import { formatEventSummary, relativeTime, statusColor, summarizeQueueBlock, truncate } from "./format.js";
4
+ import { formatEventSummary, humanStatus, nextStepLabel, progressBar, queueProgress, relativeTime, statusColor, summarizeQueueBlock, truncate } from "./format.js";
5
+ const ENTRY_ROW_HEIGHT = 2;
5
6
  const CHROME_ROWS = 13;
6
- function QueueRow({ entry, selected, branchWidth, isHead, queueBlock, }) {
7
+ function QueueRow({ entry, selected, infoWidth, isHead, queueBlock, }) {
7
8
  const retryText = `${entry.retryAttempts}/${entry.maxRetries}`;
8
- const ciText = entry.ciRetries > 0 ? ` ci:${entry.ciRetries}` : "";
9
+ const ciText = entry.ciRetries > 0 ? `CI retries ${entry.ciRetries}` : null;
9
10
  const blockedOnMain = isHead && queueBlock?.reason === "main_broken" && queueBlock.headPrNumber === entry.prNumber;
10
- const renderedStatus = blockedOnMain ? "blocked/main" : entry.status;
11
+ const renderedStatus = blockedOnMain ? "blocked by broken main" : humanStatus(entry.status);
11
12
  const renderedColor = blockedOnMain ? "red" : statusColor(entry.status);
12
- return (_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? "›" : " " }), _jsx(Text, { color: blockedOnMain ? "red" : isHead ? "green" : "gray", children: blockedOnMain ? "!" : isHead ? "*" : " " }), _jsxs(Text, { children: [" ", String(entry.position).padStart(2, " "), " "] }), _jsxs(Text, { bold: true, children: ["#", String(entry.prNumber).padStart(4, " ")] }), _jsx(Text, { children: " " }), _jsx(Text, { color: renderedColor, children: renderedStatus.padEnd(14, " ") }), _jsx(Text, { children: " " }), _jsx(Text, { children: retryText.padEnd(4, " ") }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: relativeTime(entry.updatedAt).padStart(4, " ") }), _jsx(Text, { children: " " }), _jsx(Text, { children: truncate(entry.branch, branchWidth) }), _jsx(Text, { dimColor: true, children: ciText })] }));
13
+ const progress = queueProgress(entry.status);
14
+ const branchLabel = truncate(entry.branch, Math.max(12, infoWidth - 34));
15
+ const nextStep = blockedOnMain
16
+ ? summarizeQueueBlock(queueBlock) ?? "waiting for main to recover"
17
+ : nextStepLabel(entry.status);
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? "›" : " " }), _jsx(Text, { color: blockedOnMain ? "red" : isHead ? "green" : "gray", children: blockedOnMain ? "!" : isHead ? "#" : " " }), _jsx(Text, { bold: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` pos ${entry.position}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(entry.updatedAt)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: renderedColor, children: renderedStatus })] }), _jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: progressBar(progress.current, progress.total, 8) }), _jsx(Text, { dimColor: true, children: branchLabel }), _jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { dimColor: true, children: nextStep }), _jsx(Text, { dimColor: true, children: ` | retry ${retryText}` }), ciText ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { dimColor: true, children: ciText })] })) : null] })] }));
13
19
  }
14
20
  export function QueueListView({ entries, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
15
21
  const { stdout } = useStdout();
16
22
  const rows = stdout?.rows ?? 24;
17
23
  const cols = stdout?.columns ?? 100;
18
- const branchWidth = Math.max(12, cols - 36);
19
- const eventRows = Math.min(8, Math.max(4, rows - entries.length - CHROME_ROWS));
24
+ const infoWidth = Math.max(32, cols - 4);
25
+ const eventRows = Math.min(8, Math.max(4, rows - (entries.length * ENTRY_ROW_HEIGHT) - CHROME_ROWS));
20
26
  const displayedEvents = useMemo(() => recentEvents.slice(-eventRows), [eventRows, recentEvents]);
21
27
  const queueBlockLabel = summarizeQueueBlock(queueBlock);
22
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [queueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "main is unhealthy", queueBlock.baseSha ? ` at ${truncate(queueBlock.baseSha, 10)}` : "", "."] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? "?", " will resume automatically once main recovers."] })] })), _jsx(Text, { dimColor: true, children: " s h pos pr status rt ago branch" }), entries.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue entries in this filter." })) : (entries.map((entry) => (_jsx(QueueRow, { entry: entry, selected: entry.id === selectedEntryId, branchWidth: branchWidth, isHead: entry.id === headEntryId, queueBlock: queueBlock }, entry.id)))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Events" }), displayedEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (displayedEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEventSummary(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
28
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [queueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "main is unhealthy", queueBlock.baseSha ? ` at ${truncate(queueBlock.baseSha, 10)}` : "", "."] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? "?", " will resume automatically once main recovers."] })] })), entries.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue entries in this filter." })) : (entries.map((entry) => (_jsx(QueueRow, { entry: entry, selected: entry.id === selectedEntryId, infoWidth: infoWidth, isHead: entry.id === headEntryId, queueBlock: queueBlock }, entry.id)))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Events" }), displayedEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (displayedEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEventSummary(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
23
29
  }
@@ -17,14 +17,19 @@ export function StatusBar({ snapshot, connected, filter, lastSnapshotReceivedAt,
17
17
  const avgWaitMs = activeEntries.length > 0
18
18
  ? activeEntries.reduce((sum, e) => sum + (Date.now() - new Date(e.enqueuedAt).getTime()), 0) / activeEntries.length
19
19
  : 0;
20
+ const queueHealth = queueBlockLabel
21
+ ? `paused on broken main`
22
+ : summary.headPrNumber !== null
23
+ ? `head #${summary.headPrNumber} active`
24
+ : "queue idle";
20
25
  const leftParts = [
21
26
  snapshot.repoFullName,
22
27
  `base:${snapshot.baseBranch}`,
23
28
  `${summary.total} entries ${summary.active} active`,
24
- summary.headPrNumber !== null ? `head #${summary.headPrNumber}` : null,
25
- queueBlockLabel ? `blocked ${queueBlockLabel}` : null,
29
+ queueHealth,
30
+ queueBlockLabel ?? null,
26
31
  avgWaitMs > 0 ? `wait ~${formatDuration(avgWaitMs)}` : null,
27
- `tick ${runtimeLabel(runtime)} ${relativeTime(runtime.lastTickCompletedAt ?? runtime.lastTickStartedAt)}`,
32
+ `last tick ${runtimeLabel(runtime)} ${relativeTime(runtime.lastTickCompletedAt ?? runtime.lastTickStartedAt)}`,
28
33
  filter,
29
34
  ].filter(Boolean).join(" | ");
30
35
  const availableLeft = Math.max(1, width - 28);
@@ -2,6 +2,12 @@ import type { CheckResult, QueueBlockState, QueueEntryStatus, QueueEventRecord,
2
2
  export declare function shortSha(value: string | null | undefined): string;
3
3
  export declare function relativeTime(iso: string | null | undefined): string;
4
4
  export declare function statusColor(status: QueueEntryStatus): "yellow" | "cyan" | "green" | "red" | "gray";
5
+ export declare function humanStatus(status: QueueEntryStatus): string;
6
+ export declare function queueProgress(status: QueueEntryStatus): {
7
+ current: number;
8
+ total: number;
9
+ };
10
+ export declare function nextStepLabel(status: QueueEntryStatus): string;
5
11
  export declare function runtimeLabel(runtime: QueueRuntimeStatus): string;
6
12
  export declare function formatEventSummary(event: QueueEventSummary): string;
7
13
  export declare function formatEntryEvent(event: QueueEventRecord): string;
@@ -38,6 +38,57 @@ export function statusColor(status) {
38
38
  return "gray";
39
39
  }
40
40
  }
41
+ export function humanStatus(status) {
42
+ switch (status) {
43
+ case "queued":
44
+ return "queued";
45
+ case "preparing_head":
46
+ return "refreshing branch";
47
+ case "validating":
48
+ return "running CI";
49
+ case "merging":
50
+ return "merging to main";
51
+ case "merged":
52
+ return "merged";
53
+ case "evicted":
54
+ return "removed from queue";
55
+ case "dequeued":
56
+ return "dequeued";
57
+ }
58
+ }
59
+ export function queueProgress(status) {
60
+ switch (status) {
61
+ case "queued":
62
+ return { current: 1, total: 4 };
63
+ case "preparing_head":
64
+ return { current: 2, total: 4 };
65
+ case "validating":
66
+ return { current: 3, total: 4 };
67
+ case "merging":
68
+ case "merged":
69
+ case "evicted":
70
+ case "dequeued":
71
+ return { current: 4, total: 4 };
72
+ }
73
+ }
74
+ export function nextStepLabel(status) {
75
+ switch (status) {
76
+ case "queued":
77
+ return "waiting for head-of-line turn";
78
+ case "preparing_head":
79
+ return "rebasing onto latest main";
80
+ case "validating":
81
+ return "waiting for CI result";
82
+ case "merging":
83
+ return "final GitHub merge";
84
+ case "merged":
85
+ return "landed on main";
86
+ case "evicted":
87
+ return "needs external repair";
88
+ case "dequeued":
89
+ return "removed manually";
90
+ }
91
+ }
41
92
  export function runtimeLabel(runtime) {
42
93
  if (runtime.tickInProgress) {
43
94
  return "running";
@@ -21,6 +21,12 @@ export async function startWatch(configPath, initialPrNumber) {
21
21
  const config = loadConfig(configPath);
22
22
  const gatewayBase = resolveGatewayBaseUrl();
23
23
  const baseUrl = `${gatewayBase}/repos/${config.repoId}`;
24
- const instance = render(createElement(App, { baseUrl, ...(initialPrNumber !== undefined ? { initialPrNumber } : {}) }), { stdout: process.stderr, stdin: process.stdin, patchConsole: false });
25
- await instance.waitUntilExit();
24
+ process.stderr.write("\u001b[?1049h\u001b[2J\u001b[H");
25
+ try {
26
+ const instance = render(createElement(App, { baseUrl, ...(initialPrNumber !== undefined ? { initialPrNumber } : {}) }), { stdout: process.stderr, stdin: process.stdin, patchConsole: false });
27
+ await instance.waitUntilExit();
28
+ }
29
+ finally {
30
+ process.stderr.write("\u001b[?1049l");
31
+ }
26
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "merge-steward",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Serial merge queue for GitHub — rebase, CI-gate, and merge PRs one at a time",
5
5
  "type": "module",
6
6
  "repository": {