BUG-HUNT: [error-handling] GitWorktreeSandbox.commit() never aborts a failed merge — git repo stuck in MERGING state permanently #6520

Open
opened 2026-04-09 21:14:31 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: [error-handling] — Failed Merge Leaves Repo in MERGING State

Severity Assessment

  • Impact: Any merge conflict during commit() corrupts the original git repository (leaves MERGE_HEAD / MERGE_MSG artefacts). All subsequent plan executions against the same repo fail until a developer manually runs git merge --abort. In a multi-user or CI environment this is a blocker.
  • Likelihood: High — merge conflicts are expected whenever two plans modify overlapping files in the same repository
  • Priority: High

Location

  • File: src/cleveragents/infrastructure/sandbox/git_worktree.py
  • Function: GitWorktreeSandbox.commit
  • Lines: 433–452

Description

At line 434–438, commit() merges the sandbox branch into the original branch:

_run_git(
    ["merge", self._branch_name, "--no-edit"],
    cwd=self._original_path,
    timeout=self._git_timeout,
)

When git merge encounters a conflict it exits with a non-zero return code. _run_git raises subprocess.CalledProcessError. The exception handler at lines 447–452 catches it and:

except subprocess.CalledProcessError as exc:
    self._pre_merge_commit = None
    self._status = SandboxStatus.ERRORED
    raise SandboxCommitError(...) from exc

The handler never runs git merge --abort.

After git merge fails on a conflict, the original repository is left in MERGING state:

  • <repo>/.git/MERGE_HEAD exists
  • <repo>/.git/MERGE_MSG exists
  • git status reports You have unmerged paths

The cleanup() method that follows removes the worktree directory and deletes the sandbox branch, but it does not abort the pending merge on the original path. Once cleanup completes, the original repository is in an unresolvable MERGING state with no branch to abort from.

Subsequent attempts to create a new worktree sandbox or merge other plan branches into the same original repo will fail immediately because git refuses to start a new merge while one is already in progress.

Evidence

# git_worktree.py — commit() error handler (lines 447–452)
except subprocess.CalledProcessError as exc:
    self._pre_merge_commit = None   # Clears the pre-merge commit reference
    self._status = SandboxStatus.ERRORED
    raise SandboxCommitError(
        f"Failed to commit sandbox {self._sandbox_id}: {exc.stderr.strip()}"
    ) from exc
    # ↑ MERGE_HEAD is NOT aborted here; repo is left in MERGING state
# git_worktree.py — cleanup() (lines 548–610) does NOT abort pending merges
def cleanup(self) -> None:
    ...
    if self._worktree_path is not None and os.path.exists(self._worktree_path):
        _run_git(["worktree", "remove", "--force", self._worktree_path], ...)
    if self._branch_name is not None:
        _run_git(["branch", "-D", self._branch_name], ...)
    # ↑ no git merge --abort for self._original_path

Expected Behavior

When git merge fails (conflict or any error), the code must run git merge --abort in self._original_path before transitioning to ERRORED status, restoring the repository to a clean state.

Actual Behavior

The repository at self._original_path is left with MERGE_HEAD present. Any future git operation that depends on a clean HEAD (new merge, rebase, worktree add for the same branch) will fail with: error: You have not concluded your merge.

Suggested Fix

In the subprocess.CalledProcessError handler in commit(), detect whether the merge command caused the failure and abort the merge before re-raising:

except subprocess.CalledProcessError as exc:
    # If the failure was from the merge step, abort it to restore
    # the original repository to a clean state.
    self._pre_merge_commit = None
    self._status = SandboxStatus.ERRORED
    # Attempt to abort any in-progress merge on the original path
    try:
        import subprocess as _sp
        _sp.run(
            ["git", "merge", "--abort"],
            cwd=self._original_path,
            capture_output=True,
            timeout=self._git_timeout,
        )
    except Exception:
        logger.warning(
            "git merge --abort failed for sandbox %s; "
            "manual cleanup of %s may be required",
            self._sandbox_id,
            self._original_path,
        )
    raise SandboxCommitError(
        f"Failed to commit sandbox {self._sandbox_id}: {exc.stderr.strip()}"
    ) from exc

Additionally, cleanup() should check for and abort any pending merge in self._original_path as a defensive measure.

Category

error-handling / resource

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: [error-handling] — Failed Merge Leaves Repo in MERGING State ### Severity Assessment - **Impact**: Any merge conflict during `commit()` corrupts the original git repository (leaves `MERGE_HEAD` / `MERGE_MSG` artefacts). All subsequent plan executions against the same repo fail until a developer manually runs `git merge --abort`. In a multi-user or CI environment this is a blocker. - **Likelihood**: High — merge conflicts are expected whenever two plans modify overlapping files in the same repository - **Priority**: High ### Location - **File**: `src/cleveragents/infrastructure/sandbox/git_worktree.py` - **Function**: `GitWorktreeSandbox.commit` - **Lines**: 433–452 ### Description At line 434–438, `commit()` merges the sandbox branch into the original branch: ```python _run_git( ["merge", self._branch_name, "--no-edit"], cwd=self._original_path, timeout=self._git_timeout, ) ``` When `git merge` encounters a conflict it exits with a non-zero return code. `_run_git` raises `subprocess.CalledProcessError`. The exception handler at lines 447–452 catches it and: ```python except subprocess.CalledProcessError as exc: self._pre_merge_commit = None self._status = SandboxStatus.ERRORED raise SandboxCommitError(...) from exc ``` **The handler never runs `git merge --abort`.** After `git merge` fails on a conflict, the original repository is left in `MERGING` state: - `<repo>/.git/MERGE_HEAD` exists - `<repo>/.git/MERGE_MSG` exists - `git status` reports `You have unmerged paths` The `cleanup()` method that follows removes the worktree directory and deletes the sandbox branch, but it does **not** abort the pending merge on the original path. Once cleanup completes, the original repository is in an unresolvable MERGING state with no branch to abort from. Subsequent attempts to create a new worktree sandbox or merge other plan branches into the same original repo will fail immediately because git refuses to start a new merge while one is already in progress. ### Evidence ```python # git_worktree.py — commit() error handler (lines 447–452) except subprocess.CalledProcessError as exc: self._pre_merge_commit = None # Clears the pre-merge commit reference self._status = SandboxStatus.ERRORED raise SandboxCommitError( f"Failed to commit sandbox {self._sandbox_id}: {exc.stderr.strip()}" ) from exc # ↑ MERGE_HEAD is NOT aborted here; repo is left in MERGING state ``` ```python # git_worktree.py — cleanup() (lines 548–610) does NOT abort pending merges def cleanup(self) -> None: ... if self._worktree_path is not None and os.path.exists(self._worktree_path): _run_git(["worktree", "remove", "--force", self._worktree_path], ...) if self._branch_name is not None: _run_git(["branch", "-D", self._branch_name], ...) # ↑ no git merge --abort for self._original_path ``` ### Expected Behavior When `git merge` fails (conflict or any error), the code must run `git merge --abort` in `self._original_path` before transitioning to `ERRORED` status, restoring the repository to a clean state. ### Actual Behavior The repository at `self._original_path` is left with `MERGE_HEAD` present. Any future git operation that depends on a clean HEAD (new merge, rebase, worktree add for the same branch) will fail with: `error: You have not concluded your merge`. ### Suggested Fix In the `subprocess.CalledProcessError` handler in `commit()`, detect whether the merge command caused the failure and abort the merge before re-raising: ```python except subprocess.CalledProcessError as exc: # If the failure was from the merge step, abort it to restore # the original repository to a clean state. self._pre_merge_commit = None self._status = SandboxStatus.ERRORED # Attempt to abort any in-progress merge on the original path try: import subprocess as _sp _sp.run( ["git", "merge", "--abort"], cwd=self._original_path, capture_output=True, timeout=self._git_timeout, ) except Exception: logger.warning( "git merge --abort failed for sandbox %s; " "manual cleanup of %s may be required", self._sandbox_id, self._original_path, ) raise SandboxCommitError( f"Failed to commit sandbox {self._sandbox_id}: {exc.stderr.strip()}" ) from exc ``` Additionally, `cleanup()` should check for and abort any pending merge in `self._original_path` as a defensive measure. ### Category error-handling / resource ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 21:27:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6520
No description provided.