From 1143bf08c9c59030baf13ab51b44ba3fa35769f7 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 7 Apr 2026 09:21:11 -0700 Subject: [PATCH 01/11] Skip AI conflict resolution for lock files Lock files like bun.lock and Cargo.lock contain auto-generated content that Claude cannot meaningfully merge. Remove them during conflict resolution so they get regenerated cleanly. --- src/commands/helpers.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 7a22a02..ef58ac5 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -81,6 +81,9 @@ export async function teardownSession(root: string, branch: string, worktree: st await state.removeSession(root, branch) } +/** Files that should be resolved by accepting "theirs" instead of using Claude. */ +const SKIP_RESOLVE = new Set(["bun.lock", "Cargo.lock"]) + /** Resolve conflict markers in files using Claude, then stage them. */ export async function resolveConflicts( files: string[], @@ -90,6 +93,12 @@ export async function resolveConflicts( for (let i = 0; i < files.length; i++) { const file = files[i] onFile(file, i + 1, files.length) + + if (SKIP_RESOLVE.has(basename(file))) { + await $`git rm -f -- ${file}`.cwd(cwd).nothrow().quiet() + continue + } + const content = await Bun.file(join(cwd, file)).text() const resolved = await vm.claudePipe( From 29cbf29b76100eb24e7764a688fe41dd68839f9f Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 7 Apr 2026 09:26:55 -0700 Subject: [PATCH 02/11] Remove lock files instead of resolving them during conflict resolution Lock files like bun.lock and Cargo.lock get regenerated, so resolving their conflict markers is unnecessary work. --- src/commands/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index ef58ac5..6872fe3 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -81,7 +81,7 @@ export async function teardownSession(root: string, branch: string, worktree: st await state.removeSession(root, branch) } -/** Files that should be resolved by accepting "theirs" instead of using Claude. */ +/** Files that should be removed rather than resolved — they get regenerated. */ const SKIP_RESOLVE = new Set(["bun.lock", "Cargo.lock"]) /** Resolve conflict markers in files using Claude, then stage them. */ From e694ab06d77716c78fa6223954ca4f6140f9176b Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 7 Apr 2026 17:55:40 -0700 Subject: [PATCH 03/11] Accept theirs for conflicting lock files instead of deleting them Removing lock files during conflict resolution caused missing dependencies after the merge. Checking out theirs and staging preserves a valid lockfile. --- src/commands/helpers.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 6872fe3..308dd99 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -81,7 +81,7 @@ export async function teardownSession(root: string, branch: string, worktree: st await state.removeSession(root, branch) } -/** Files that should be removed rather than resolved — they get regenerated. */ +/** Lock files to skip AI resolution — accept theirs and move on (basename match covers subdirs too). */ const SKIP_RESOLVE = new Set(["bun.lock", "Cargo.lock"]) /** Resolve conflict markers in files using Claude, then stage them. */ @@ -95,7 +95,11 @@ export async function resolveConflicts( onFile(file, i + 1, files.length) if (SKIP_RESOLVE.has(basename(file))) { - await $`git rm -f -- ${file}`.cwd(cwd).nothrow().quiet() + const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) { + throw new Error(`Failed to resolve lock file ${file}: ${result.stderr.toString().trim()}`) + } + await git.stageFile(file, cwd) continue } From cc61e09384412eef02abc8ce7a85ccf1080fb64d Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 08:07:43 -0700 Subject: [PATCH 04/11] Propagate git-add failures instead of silently swallowing them The old `stageFile` call discarded non-zero exit codes, hiding issues like unresolved conflicts or missing files from the caller. Co-Authored-By: Claude Opus 4.6 --- src/commands/helpers.ts | 2 +- src/git.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 308dd99..fdaba79 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -81,7 +81,7 @@ export async function teardownSession(root: string, branch: string, worktree: st await state.removeSession(root, branch) } -/** Lock files to skip AI resolution — accept theirs and move on (basename match covers subdirs too). */ +/** Lock files to skip AI resolution — accept theirs and move on (basename match so `packages/foo/bun.lock` is also covered). */ const SKIP_RESOLVE = new Set(["bun.lock", "Cargo.lock"]) /** Resolve conflict markers in files using Claude, then stage them. */ diff --git a/src/git.ts b/src/git.ts index 6afb717..810146b 100644 --- a/src/git.ts +++ b/src/git.ts @@ -135,7 +135,10 @@ export async function commit(message: string, cwd: string): Promise { /** Stage a file. */ export async function stageFile(file: string, cwd: string): Promise { - await $`git add ${file}`.cwd(cwd).nothrow().quiet() + const result = await $`git add ${file}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) { + throw new Error(`Failed to stage ${file}: ${result.stderr.toString().trim()}`) + } } /** Finalize a merge commit after resolving conflicts. */ From 85cf9cc02f906ea5f753ca4069eff9763681249d Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 08:17:55 -0700 Subject: [PATCH 05/11] Expand lock-file skip list and extract checkoutTheirs into git module Covers all common ecosystem lock files (npm, yarn, pnpm, Go, Ruby, PHP, Poetry) so merge conflicts in any of them are auto-resolved with --theirs rather than sent to Claude. --- src/commands/helpers.ts | 19 +++++++++++++------ src/git.ts | 8 ++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index fdaba79..8dfa7b8 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -82,7 +82,17 @@ export async function teardownSession(root: string, branch: string, worktree: st } /** Lock files to skip AI resolution — accept theirs and move on (basename match so `packages/foo/bun.lock` is also covered). */ -const SKIP_RESOLVE = new Set(["bun.lock", "Cargo.lock"]) +const SKIP_RESOLVE = new Set([ + "bun.lock", + "Cargo.lock", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "go.sum", + "Gemfile.lock", + "composer.lock", + "poetry.lock", +]) /** Resolve conflict markers in files using Claude, then stage them. */ export async function resolveConflicts( @@ -95,10 +105,7 @@ export async function resolveConflicts( onFile(file, i + 1, files.length) if (SKIP_RESOLVE.has(basename(file))) { - const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() - if (result.exitCode !== 0) { - throw new Error(`Failed to resolve lock file ${file}: ${result.stderr.toString().trim()}`) - } + await git.checkoutTheirs(file, cwd) await git.stageFile(file, cwd) continue } @@ -111,7 +118,7 @@ export async function resolveConflicts( ) if (resolved.exitCode !== 0) { - throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr}`) + throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr.toString().trim()}`) } await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n") diff --git a/src/git.ts b/src/git.ts index 810146b..3f55b02 100644 --- a/src/git.ts +++ b/src/git.ts @@ -133,6 +133,14 @@ export async function commit(message: string, cwd: string): Promise { } } +/** Accept "theirs" version of a conflicted file. */ +export async function checkoutTheirs(file: string, cwd: string): Promise { + const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() + if (result.exitCode !== 0) { + throw new Error(`Failed to checkout theirs for ${file}: ${result.stderr.toString().trim()}`) + } +} + /** Stage a file. */ export async function stageFile(file: string, cwd: string): Promise { const result = await $`git add ${file}`.cwd(cwd).nothrow().quiet() From cbb6bac1b9a4bb1f141b287bfbf2f49a59fd3c6f Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 08:29:45 -0700 Subject: [PATCH 06/11] Expand the lock-file skip list for merge conflict resolution Add bun.lockb, mix.lock, Pipfile.lock, Podfile.lock, pubspec.lock, and uv.lock to SKIP_RESOLVE so generated files from additional ecosystems are auto-resolved with theirs during rebases. Sort entries alphabetically. --- src/commands/helpers.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 8dfa7b8..e114f46 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -81,17 +81,23 @@ export async function teardownSession(root: string, branch: string, worktree: st await state.removeSession(root, branch) } -/** Lock files to skip AI resolution — accept theirs and move on (basename match so `packages/foo/bun.lock` is also covered). */ +/** Generated files to skip AI resolution — accept theirs and move on (basename match so `packages/foo/bun.lock` is also covered). */ const SKIP_RESOLVE = new Set([ "bun.lock", + "bun.lockb", "Cargo.lock", - "package-lock.json", - "yarn.lock", - "pnpm-lock.yaml", - "go.sum", - "Gemfile.lock", "composer.lock", + "Gemfile.lock", + "go.sum", + "mix.lock", + "package-lock.json", + "Pipfile.lock", + "pnpm-lock.yaml", + "Podfile.lock", "poetry.lock", + "pubspec.lock", + "uv.lock", + "yarn.lock", ]) /** Resolve conflict markers in files using Claude, then stage them. */ From 374454f0fb0713d93627fc5cc9c714d8dd2eeab3 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 08:44:17 -0700 Subject: [PATCH 07/11] Add missing lockfiles to skip list and show fallback for empty stderr Several ecosystem lockfiles (Nix flake, Gradle, npm-shrinkwrap, Swift PM) were missing from SKIP_RESOLVE, causing unnecessary conflict resolution attempts. Empty stderr on failure produced confusing error messages. --- src/commands/helpers.ts | 7 ++++++- src/git.ts | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index e114f46..af8262c 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -96,6 +96,10 @@ const SKIP_RESOLVE = new Set([ "Podfile.lock", "poetry.lock", "pubspec.lock", + "flake.lock", + "gradle.lockfile", + "npm-shrinkwrap.json", + "Package.resolved", "uv.lock", "yarn.lock", ]) @@ -124,7 +128,8 @@ export async function resolveConflicts( ) if (resolved.exitCode !== 0) { - throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr.toString().trim()}`) + const stderr = resolved.stderr.toString().trim() + throw new Error(`Claude failed to resolve ${file}: ${stderr || "(no output)"}`) } await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n") diff --git a/src/git.ts b/src/git.ts index 3f55b02..de40efc 100644 --- a/src/git.ts +++ b/src/git.ts @@ -137,7 +137,8 @@ export async function commit(message: string, cwd: string): Promise { export async function checkoutTheirs(file: string, cwd: string): Promise { const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - throw new Error(`Failed to checkout theirs for ${file}: ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to checkout theirs for ${file}: ${stderr || "(no output)"}`) } } @@ -145,7 +146,8 @@ export async function checkoutTheirs(file: string, cwd: string): Promise { export async function stageFile(file: string, cwd: string): Promise { const result = await $`git add ${file}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - throw new Error(`Failed to stage ${file}: ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to stage ${file}: ${stderr || "(no output)"}`) } } From b4d0d9e948237a931880cb578232e086ad10d0fe Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 08:54:17 -0700 Subject: [PATCH 08/11] Add "(no output)" fallback to all git error messages The helpers already handled empty stderr but the git module did not, leading to confusing error messages with a trailing colon and no detail. --- src/commands/helpers.ts | 2 +- src/git.ts | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index af8262c..8c92ef5 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -128,7 +128,7 @@ export async function resolveConflicts( ) if (resolved.exitCode !== 0) { - const stderr = resolved.stderr.toString().trim() + const stderr = resolved.stderr.trim() throw new Error(`Claude failed to resolve ${file}: ${stderr || "(no output)"}`) } diff --git a/src/git.ts b/src/git.ts index de40efc..1871287 100644 --- a/src/git.ts +++ b/src/git.ts @@ -72,7 +72,8 @@ export async function createWorktree(branch: string, worktreePath: string, cwd: } if (result.exitCode !== 0) { if (switchedFromBranch) await checkout(branch, cwd).catch(() => {}) - throw new Error(`Failed to create worktree for "${branch}": ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to create worktree for "${branch}": ${stderr || "(no output)"}`) } return { branchCreated: exists !== "local" } } @@ -98,7 +99,8 @@ export async function deleteLocalBranch(branch: string, cwd: string): Promise { const result = await $`git checkout ${branch}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - throw new Error(`Failed to checkout branch "${branch}": ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to checkout branch "${branch}": ${stderr || "(no output)"}`) } } @@ -117,7 +119,8 @@ export async function merge(branch: string, cwd: string, opts?: { squash?: boole // Not a conflict — some other merge failure const label = opts?.squash ? "squash-merge" : "merge" - throw new Error(`Failed to ${label} branch "${branch}": ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to ${label} branch "${branch}": ${stderr || "(no output)"}`) } /** Return the staged diff as text. */ @@ -129,7 +132,8 @@ export async function diffStaged(cwd: string): Promise { export async function commit(message: string, cwd: string): Promise { const result = await $`git commit -m ${message}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - throw new Error(`Failed to commit: ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to commit: ${stderr || "(no output)"}`) } } @@ -155,7 +159,8 @@ export async function stageFile(file: string, cwd: string): Promise { export async function commitMerge(cwd: string): Promise { const result = await $`git commit --no-edit`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - throw new Error(`Failed to commit merge: ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Failed to commit merge: ${stderr || "(no output)"}`) } } @@ -193,7 +198,8 @@ export async function rebaseContinue(cwd: string): Promise { const files = unmerged.trim().split("\n").filter(Boolean) if (files.length > 0) return files - throw new Error(`Rebase --continue failed: ${result.stderr.toString().trim()}`) + const stderr = result.stderr.toString().trim() + throw new Error(`Rebase --continue failed: ${stderr || "(no output)"}`) } /** Abort an in-progress rebase. */ From fbb8680376c06070a59ba4c5777453c152a51414 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 09:41:39 -0700 Subject: [PATCH 09/11] Guard against empty stdout when resolving merge conflicts Claude can exit 0 but produce no output, leaving the file blank. --- src/commands/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 8c92ef5..d29bdf0 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -127,7 +127,7 @@ export async function resolveConflicts( "resolve this merge conflict. output ONLY the resolved file content with no markdown fences, no explanation, no surrounding text.", ) - if (resolved.exitCode !== 0) { + if (resolved.exitCode !== 0 || !resolved.stdout.trim()) { const stderr = resolved.stderr.trim() throw new Error(`Claude failed to resolve ${file}: ${stderr || "(no output)"}`) } From 101651b107aa209166a6b6040e1d92936856d0b9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 10:02:23 -0700 Subject: [PATCH 10/11] Extract shared gitError helper to deduplicate stderr formatting Also clear activity on failed merge and improve error context for conflicted file reads. Co-Authored-By: Claude Opus 4.6 --- src/commands/helpers.ts | 8 +++++--- src/git.ts | 30 ++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index d29bdf0..19beb06 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -120,7 +120,9 @@ export async function resolveConflicts( continue } - const content = await Bun.file(join(cwd, file)).text() + const content = await Bun.file(join(cwd, file)).text().catch(() => { + throw new Error(`Failed to read conflicted file: ${file}`) + }) const resolved = await vm.claudePipe( content, @@ -128,8 +130,7 @@ export async function resolveConflicts( ) if (resolved.exitCode !== 0 || !resolved.stdout.trim()) { - const stderr = resolved.stderr.trim() - throw new Error(`Claude failed to resolve ${file}: ${stderr || "(no output)"}`) + throw new Error(`Claude failed to resolve ${file}: ${resolved.stderr.trim() || "(no output)"}`) } await Bun.write(join(cwd, file), resolved.stdout.trimEnd() + "\n") @@ -187,6 +188,7 @@ export async function mergeAndClose(branch: string, opts?: { squash?: boolean; f } catch (err) { const message = err instanceof Error ? err.message : String(err) spin.fail(message) + if (session) await vm.clearActivity(session.worktree, branch) await git.abortMerge(root) process.exit(1) } finally { diff --git a/src/git.ts b/src/git.ts index 1871287..162de00 100644 --- a/src/git.ts +++ b/src/git.ts @@ -2,6 +2,12 @@ import { existsSync } from "fs" import { rm } from "fs/promises" import { $ } from "bun" +/** Format a git error with a fallback for empty stderr. */ +function gitError(action: string, stderr: Buffer | string): Error { + const msg = stderr.toString().trim() + return new Error(`${action}: ${msg || "(no output)"}`) +} + /** Get the repo root from a working directory. */ export async function repoRoot(cwd?: string): Promise { const result = await $`git rev-parse --show-toplevel`.cwd(cwd ?? ".").nothrow().quiet() @@ -72,8 +78,7 @@ export async function createWorktree(branch: string, worktreePath: string, cwd: } if (result.exitCode !== 0) { if (switchedFromBranch) await checkout(branch, cwd).catch(() => {}) - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to create worktree for "${branch}": ${stderr || "(no output)"}`) + throw gitError(`Failed to create worktree for "${branch}"`, result.stderr) } return { branchCreated: exists !== "local" } } @@ -99,8 +104,7 @@ export async function deleteLocalBranch(branch: string, cwd: string): Promise { const result = await $`git checkout ${branch}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to checkout branch "${branch}": ${stderr || "(no output)"}`) + throw gitError(`Failed to checkout branch "${branch}"`, result.stderr) } } @@ -119,8 +123,7 @@ export async function merge(branch: string, cwd: string, opts?: { squash?: boole // Not a conflict — some other merge failure const label = opts?.squash ? "squash-merge" : "merge" - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to ${label} branch "${branch}": ${stderr || "(no output)"}`) + throw gitError(`Failed to ${label} branch "${branch}"`, result.stderr) } /** Return the staged diff as text. */ @@ -132,8 +135,7 @@ export async function diffStaged(cwd: string): Promise { export async function commit(message: string, cwd: string): Promise { const result = await $`git commit -m ${message}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to commit: ${stderr || "(no output)"}`) + throw gitError("Failed to commit", result.stderr) } } @@ -141,8 +143,7 @@ export async function commit(message: string, cwd: string): Promise { export async function checkoutTheirs(file: string, cwd: string): Promise { const result = await $`git checkout --theirs -- ${file}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to checkout theirs for ${file}: ${stderr || "(no output)"}`) + throw gitError(`Failed to checkout theirs for ${file}`, result.stderr) } } @@ -150,8 +151,7 @@ export async function checkoutTheirs(file: string, cwd: string): Promise { export async function stageFile(file: string, cwd: string): Promise { const result = await $`git add ${file}`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to stage ${file}: ${stderr || "(no output)"}`) + throw gitError(`Failed to stage ${file}`, result.stderr) } } @@ -159,8 +159,7 @@ export async function stageFile(file: string, cwd: string): Promise { export async function commitMerge(cwd: string): Promise { const result = await $`git commit --no-edit`.cwd(cwd).nothrow().quiet() if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim() - throw new Error(`Failed to commit merge: ${stderr || "(no output)"}`) + throw gitError("Failed to commit merge", result.stderr) } } @@ -198,8 +197,7 @@ export async function rebaseContinue(cwd: string): Promise { const files = unmerged.trim().split("\n").filter(Boolean) if (files.length > 0) return files - const stderr = result.stderr.toString().trim() - throw new Error(`Rebase --continue failed: ${stderr || "(no output)"}`) + throw gitError("Rebase --continue failed", result.stderr) } /** Abort an in-progress rebase. */ From a7285a43474335e8cfc38a2719b64959620638c9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 10 Apr 2026 10:06:00 -0700 Subject: [PATCH 11/11] Add comment explaining why clearActivity is called before process.exit process.exit(1) skips the finally block, so cleanup must happen explicitly in the catch block. --- src/commands/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/helpers.ts b/src/commands/helpers.ts index 19beb06..9af1561 100644 --- a/src/commands/helpers.ts +++ b/src/commands/helpers.ts @@ -188,7 +188,7 @@ export async function mergeAndClose(branch: string, opts?: { squash?: boolean; f } catch (err) { const message = err instanceof Error ? err.message : String(err) spin.fail(message) - if (session) await vm.clearActivity(session.worktree, branch) + if (session) await vm.clearActivity(session.worktree, branch) // process.exit below skips finally await git.abortMerge(root) process.exit(1) } finally {