Skip to content

feat(core): worktree-per-task isolation for parallel batch execution - #441

Merged
frankbria merged 4 commits into
mainfrom
feature/issue-418-worktree-isolation
Mar 14, 2026
Merged

feat(core): worktree-per-task isolation for parallel batch execution#441
frankbria merged 4 commits into
mainfrom
feature/issue-418-worktree-isolation

Conversation

@frankbria

@frankbria frankbria commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Summary

Implements #418: Worktree-Per-Task Isolation for Parallel Batch Execution

Gives each task in parallel batch execution its own git worktree, eliminating race conditions from multiple agents modifying files in the same directory.

  • TaskWorktree class with create(), merge_back(), cleanup() lifecycle
  • MergeResult dataclass tracking merge success/conflict details
  • Worktrees at .codeframe/worktrees/<task-id> on branch cf/<task-id>
  • Merge conflicts detected, --abort on failure, details preserved
  • Cleanup never raises (best-effort, logged as warnings)
  • BatchRun.isolate field + start_batch(isolate=) parameter
  • _execute_task_subprocess accepts worktree_path for cwd override
  • Serial execution unchanged (no worktrees)

Acceptance Criteria

  • Parallel batch tasks each get their own worktree
  • Agent executes with cwd set to worktree path
  • Successful tasks merge back to base branch
  • Merge conflicts detected and reported
  • Worktrees cleaned up after merge (success or failure)
  • Serial execution is unchanged (no worktrees)
  • Tests: parallel batch with worktree isolation

Test Plan

  • 11 unit tests with real git worktree operations (create, merge, conflict, cleanup)
  • Regression test suite running
  • Ruff linting clean

Closes #418

Summary by CodeRabbit

  • New Features

    • Per-task workspace isolation for parallel batch execution (enabled by default)
    • Option to run batches without isolation when needed
    • Automatic merge-back of task results into the base branch, with conflict reporting
    • Automatic cleanup of task workspaces after completion
  • Tests

    • Comprehensive tests covering isolation, worktree lifecycle, merges (success/conflict), and cleanup

…ion (#418)

Create isolated git worktrees for each task in parallel batch execution,
preventing race conditions when multiple agents modify files concurrently.

- TaskWorktree class: create(), merge_back(), cleanup() lifecycle
- MergeResult dataclass for tracking merge success/conflicts
- Worktrees at .codeframe/worktrees/<task-id> on branch cf/<task-id>
- Merge conflicts detected and reported (merge --abort on failure)
- Cleanup never raises (logged as warnings)
- BatchRun.isolate field + start_batch(isolate=) parameter
- _execute_task_subprocess accepts worktree_path for cwd override
- 11 unit tests including real git worktree create/merge/cleanup/conflict

Closes #418
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new worktrees module for per-task git worktrees and updates the conductor to optionally create/use isolated worktrees for batch tasks (BatchRun gains an isolate flag; subprocess execution accepts an optional worktree cwd).

Changes

Cohort / File(s) Summary
Worktree Core Module
codeframe/core/worktrees.py
New module adding WORKTREE_DIR, MergeResult dataclass, and TaskWorktree with create, merge_back, and cleanup methods plus logging to manage per-task git worktrees and merge/conflict reporting.
Conductor Integration
codeframe/core/conductor.py
Added from pathlib import Path. BatchRun dataclass now includes isolate: bool. start_batch(..., isolate: bool = True) propagates the flag. _execute_task_subprocess(..., worktree_path: Optional[Path] = None) accepts a worktree path and uses it as cwd for subprocesses when provided.
Tests
tests/core/test_worktrees.py
New tests exercising TaskWorktree lifecycle (create/merge_back/cleanup), merge success and conflict scenarios, and BatchRun isolation semantics using temporary git repositories and real git operations.

Sequence Diagram

sequenceDiagram
    participant Conductor
    participant TaskWorktree
    participant GitRepo as "Git Repository"
    participant Agent

    Conductor->>TaskWorktree: create(workspace_path, task_id)
    TaskWorktree->>GitRepo: git worktree add .codeframe/worktrees/<task_id> -b cf/<task_id>
    TaskWorktree-->>Conductor: worktree_path

    Conductor->>Agent: execute task (cwd=worktree_path)
    Agent->>Agent: modify files / run gates
    Agent-->>Conductor: task finished

    Conductor->>TaskWorktree: merge_back(workspace_path, task_id)
    TaskWorktree->>GitRepo: git checkout <base_branch>
    TaskWorktree->>GitRepo: git merge --no-ff cf/<task_id>
    alt Merge success
        TaskWorktree-->>Conductor: MergeResult(success=True, merge_commit)
    else Merge conflict
        TaskWorktree->>GitRepo: git merge --abort
        TaskWorktree-->>Conductor: MergeResult(success=False, conflict_details)
    end

    Conductor->>TaskWorktree: cleanup(workspace_path, task_id)
    TaskWorktree->>GitRepo: git worktree remove .codeframe/worktrees/<task_id>
    TaskWorktree->>GitRepo: git branch -D cf/<task_id>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I dug a tiny worktree for each tiny task,
Branches cf/ where busy agents could bask,
They hopped, they edited, then merged back with glee,
I swept the burrows clean — conflict crumbs set free,
Hooray for tidy hops and tidy history.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main feature being implemented: worktree-per-task isolation for parallel batch execution.
Linked Issues check ✅ Passed The PR implements all core coding requirements: TaskWorktree class with lifecycle methods, MergeResult dataclass, isolate flag in BatchRun, worktree path handling in _execute_task_subprocess, and comprehensive tests with real git operations.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #418: new worktrees module, conductor modifications for worktree support, and tests validating the implementation. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-418-worktree-isolation
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Code Review: feat(core): worktree-per-task isolation for parallel batch execution

This is a solid foundation for issue #418 — the TaskWorktree class has a clean lifecycle API, the MergeResult dataclass is appropriately minimal, and the 11 unit tests exercise the real git operations. A few issues need to be resolved before this merges, ranging from a critical race condition to a missing integration wire-up.


Critical

1. merge_back() mutates shared workspace state while other tasks are still running

merge_back() runs git checkout <base_branch> in the main workspace_path, which is the same directory all parallel workers share for reading (e.g. loading context, reading PRD). If task A merges while task B is mid-execution, the main repo's HEAD moves and any subsequent git invocation from task B's subprocess (reading .git/HEAD, running tests, etc.) will see an inconsistent state.

The standard worktree pattern avoids this entirely: the worktree branch should be merged from within the worktree's directory using git merge --into-base (git 2.43+), or by running the merge from the base repo only after all tasks have completed and been serialized. Alternatively, merge using the porcelain git merge but only after acquiring a per-workspace lock.

# current — dangerous during parallel execution
subprocess.run(["git", "checkout", base_branch], cwd=str(workspace_path), ...)
subprocess.run(["git", "merge", branch_name, ...], cwd=str(workspace_path), ...)

2. worktree_path parameter is added to _execute_task_subprocess but never passed from the parallel execution loop

The diff shows the signature change and the cwd override inside _execute_task_subprocess, but there is no corresponding change to the call site in the parallel execution loop (the ThreadPoolExecutor path). Without the call site change, worktree_path will always be None and the feature is effectively a no-op for parallel batches. This needs to be included in this PR or the PR description should explicitly call it out as a follow-up.


Important

3. base_branch is hardcoded to "main" with no fallback detection

Both create() and merge_back() default to base_branch="main". Repositories using master, develop, or any other default branch will silently fail at git checkout main or produce a worktree based on the wrong branch. The active branch should be detected at call time:

result = subprocess.run(
    ["git", "rev-parse", "--abbrev-ref", "HEAD"],
    cwd=str(workspace_path), capture_output=True, text=True, check=True,
)
base_branch = result.stdout.strip()

4. cleanup() suppresses subprocess non-zero exit codes

The try/except Exception blocks only catch Python-level exceptions (e.g. FileNotFoundError when git is not on PATH). subprocess.run(...) without check=True swallows non-zero exit codes silently and never raises. If git worktree remove or git branch -D fail, the warning log is never written and the worktree/branch leaks. Either add check=True inside the try blocks, or explicitly check result.returncode:

try:
    result = subprocess.run([...], capture_output=True, text=True)
    if result.returncode != 0:
        logger.warning("Failed to remove worktree for %s: %s", task_id, result.stderr)
except Exception as exc:
    logger.warning("Failed to remove worktree for %s: %s", task_id, exc)

5. isolate=True is the default on BatchRun but there is no guard that restricts worktree creation to parallel strategy

isolate defaults to True even when strategy="serial". The PR description says "serial execution is unchanged", but the BatchRun dataclass does not enforce this. If isolate is ever read in serial paths, worktrees will be created unnecessarily. Either default to False and let callers opt in, or add a guard at the point of use: if batch.isolate and batch.strategy != "serial".


Architecture / Convention

6. worktrees.py is not referenced from conductor.py in this diff

Per CLAUDE.md, the core-first rule requires that the domain logic lives in core/ and the conductor wires it together. The new worktrees.py module exists in core/ correctly, but conductor.py does not import or call TaskWorktree anywhere in this diff. The integration between the two modules (create worktree before subprocess, merge back and cleanup after) is missing. This is the largest gap between the acceptance criteria and the actual diff.

7. conflict_details is truncated to 2000 chars but the field has no documented limit

MergeResult.conflict_details is truncated to [:2000] in merge_back() but the dataclass field is typed as str with no annotation. If this is stored in the database, the column should have a matching length constraint. If it is only in-memory, a docstring note is sufficient.


Test Quality

8. Tests will fail in CI without git config user.email and git config user.name

Every test that calls git commit relies on a global git identity being configured. In a clean CI environment (GitHub Actions runner, Docker container) with no ~/.gitconfig, the commits will fail with:

Author identity unknown
*** Please tell me who you are.

Add identity configuration to each test's setup:

subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "test@test.com"], capture_output=True)
subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "Test"], capture_output=True)

9. test_merge_conflict_returns_failure does not verify that --abort is called

The test confirms result.success is False but does not assert that the merge was actually aborted (i.e. the repo is left in a clean state). If git merge --abort fails silently (see issue #4 above), the main repo would be stuck in a mid-merge state and all subsequent tests sharing that tmp_path would fail in unexpected ways. Consider asserting subprocess.run(["git", "-C", str(tmp_path), "status", "--porcelain"], ...).stdout == "" after the failed merge.


Minor / Nitpicks

  • The from __future__ import annotations import in worktrees.py is unnecessary for Python 3.10+ (which is the minimum for this project based on pyproject.toml). Not a blocker, but dead weight.
  • conflict_output = result.stdout + result.stderr concatenates without a separator; if both streams have content the boundary is invisible. Consider "\n".join(filter(None, [result.stdout, result.stderr])).
  • Log truncation at [:200] in the logger.warning call is inconsistent with the [:2000] stored in conflict_details.

Summary

Priority Count
Critical 2
Important 4
Architecture 2
Test quality 2
Minor 3

The core isolation concept is sound and the module structure follows CLAUDE.md conventions (pure core/ module, no FastAPI imports, subprocess-based git operations). The main blockers are: (1) the missing call-site wiring in the parallel execution loop, and (2) the merge_back() race condition caused by mutating the shared workspace checkout. Resolving those two will make this safe to land.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/core/test_worktrees.py (2)

43-55: Consider adding git user config for CI compatibility.

The tests use real git operations, but git commit may fail in CI environments where user.name and user.email are not configured. This could cause silent failures since capture_output=True suppresses error messages and return codes aren't checked.

♻️ Proposed fix to configure git user for commits
     def test_creates_worktree(self, tmp_path: Path) -> None:
         from codeframe.core.worktrees import TaskWorktree

         # Set up a real git repo
         subprocess.run(["git", "init", str(tmp_path)], capture_output=True)
+        subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "test@test.com"], capture_output=True)
+        subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "Test"], capture_output=True)
         subprocess.run(["git", "-C", str(tmp_path), "commit", "--allow-empty", "-m", "init"], capture_output=True)

Alternatively, consider creating a pytest fixture that sets up a configured git repo to avoid repetition across all test methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_worktrees.py` around lines 43 - 55, The test_creates_worktree
uses real git commands via subprocess.run which can fail in CI because git
user.name/user.email are unset; before running the commit in the test (or in a
pytest fixture used by TaskWorktree tests) run git config commands to set a
local user.name and user.email for the tmp_path repo and check subprocess.run
returncodes (or remove capture_output=True) so failures surface; update the test
that calls subprocess.run(["git", "init", str(tmp_path)]),
subprocess.run(["git", "-C", str(tmp_path), "commit", ...]) and/or introduce a
fixture that initializes and configures the repo used by TaskWorktree to ensure
commits succeed in CI.

47-48: Add check=True or validate return codes for git setup commands.

The git setup commands use capture_output=True but don't verify success. If git init or git commit fails (e.g., due to missing git binary or permission issues), tests would proceed with an invalid state and produce confusing failures.

♻️ Example fix for one test
-        subprocess.run(["git", "init", str(tmp_path)], capture_output=True)
-        subprocess.run(["git", "-C", str(tmp_path), "commit", "--allow-empty", "-m", "init"], capture_output=True)
+        subprocess.run(["git", "init", str(tmp_path)], capture_output=True, check=True)
+        subprocess.run(["git", "-C", str(tmp_path), "commit", "--allow-empty", "-m", "init"], capture_output=True, check=True)

Also applies to: 60-61, 72-73, 93-94, 115-118, 145-146, 165-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_worktrees.py` around lines 47 - 48, The git setup
subprocess.run calls that execute ["git", "init", str(tmp_path)] and ["git",
"-C", str(tmp_path), "commit", "--allow-empty", "-m", "init"] must enforce
success; update each such invocation in tests/core/test_worktrees.py (the
subprocess.run calls using capture_output=True with tmp_path) to either pass
check=True or explicitly inspect the returned CompletedProcess.returncode and
raise/assert on non-zero, and apply the same change to all occurrences (the
other similar pairs around the noted ranges) so failures surface immediately
instead of letting tests proceed with an invalid repo state.
codeframe/core/worktrees.py (1)

156-174: The exception handling doesn't catch git command failures.

subprocess.run() with default check=False doesn't raise exceptions when the command fails—it only returns a non-zero returncode. The try/except Exception blocks will only catch Python-level exceptions (e.g., FileNotFoundError if git isn't installed), not git operation failures.

Since the docstring says "Never raises — cleanup failures are logged as warnings," you should check the return code instead:

♻️ Proposed fix to properly handle failures
         # Remove worktree
-        try:
-            subprocess.run(
-                ["git", "worktree", "remove", str(worktree_path), "--force"],
-                cwd=str(workspace_path),
-                capture_output=True,
-                text=True,
-            )
-        except Exception as exc:
-            logger.warning("Failed to remove worktree for %s: %s", task_id, exc)
+        result = subprocess.run(
+            ["git", "worktree", "remove", str(worktree_path), "--force"],
+            cwd=str(workspace_path),
+            capture_output=True,
+            text=True,
+        )
+        if result.returncode != 0:
+            logger.warning("Failed to remove worktree for %s: %s", task_id, result.stderr)

         # Delete branch
-        try:
-            subprocess.run(
-                ["git", "branch", "-D", branch_name],
-                cwd=str(workspace_path),
-                capture_output=True,
-                text=True,
-            )
-        except Exception as exc:
-            logger.warning("Failed to delete branch %s: %s", branch_name, exc)
+        result = subprocess.run(
+            ["git", "branch", "-D", branch_name],
+            cwd=str(workspace_path),
+            capture_output=True,
+            text=True,
+        )
+        if result.returncode != 0:
+            logger.warning("Failed to delete branch %s: %s", branch_name, result.stderr)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/worktrees.py` around lines 156 - 174, The current try/except
around subprocess.run(...) for the git worktree removal and git branch deletion
(the two subprocess.run calls invoking ["git", "worktree", "remove", ...] and
["git", "branch", "-D", ...]) won't catch git failures because subprocess.run
defaults to check=False; change these blocks to capture the CompletedProcess
result (e.g., result = subprocess.run(..., capture_output=True, text=True)) and
then check result.returncode != 0 and log a warning via logger.warning including
task_id or branch_name and result.stderr/result.stdout to surface git error
details; keep the outer try/except for Python-level exceptions (e.g.,
FileNotFoundError) but ensure git command failures are handled by inspecting
returncode and logging accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/core/conductor.py`:
- Line 537: The BatchRun.isolate flag is set but never used when running
parallel batches; update the orchestration so that when BatchRun.isolate (and
the start_batch(isolate=) parameter) is True you create per-task worktrees and
pass their paths into _execute_task_subprocess via worktree_path. Specifically:
import TaskWorktree from codeframe.core.worktrees, create a TaskWorktree for
each task in _execute_parallel and _execute_group_parallel when isolate is True,
ensure the created worktree path is passed to _execute_task_subprocess (which
already uses cwd=str(worktree_path) if provided), and clean up or close the
TaskWorktree after the subprocess completes; keep the non-isolate path unchanged
so existing behavior is preserved.

In `@codeframe/core/worktrees.py`:
- Around line 92-99: merge_back() currently runs "git checkout base_branch" in
the main workspace_path which can race with other concurrent tasks; change
merge_back() to avoid mutating the main workspace by creating a temporary git
worktree (use "git worktree add <temp_path> <base_branch>"), perform the merge
there (checkout base_branch in the temp worktree, merge the branch/ref to be
merged, push if needed), then remove the worktree, or alternatively perform a
merge using explicit ref paths without checking out (e.g., fetch refs and run
git merge <remote>/<branch> in a separate repo path). Update the code that calls
subprocess.run around workspace_path to operate on the temp worktree path and
document that if worktrees are not used, merge_back() must be called serially to
avoid interference; reference the merge_back(), workspace_path and base_branch
symbols when applying the change.
- Around line 40-72: The create() method accepts base_branch but never uses it
when creating the worktree; update the subprocess.run call in create
(referencing branch_name and base_branch) so the new branch is created from the
given base_branch by passing base_branch as the start-point to the git worktree
command (i.e., include base_branch as the final argument to the ["git",
"worktree", "add", "-b", branch_name, ...] invocation), ensuring worktree_path
and cwd usage remain unchanged.

---

Nitpick comments:
In `@codeframe/core/worktrees.py`:
- Around line 156-174: The current try/except around subprocess.run(...) for the
git worktree removal and git branch deletion (the two subprocess.run calls
invoking ["git", "worktree", "remove", ...] and ["git", "branch", "-D", ...])
won't catch git failures because subprocess.run defaults to check=False; change
these blocks to capture the CompletedProcess result (e.g., result =
subprocess.run(..., capture_output=True, text=True)) and then check
result.returncode != 0 and log a warning via logger.warning including task_id or
branch_name and result.stderr/result.stdout to surface git error details; keep
the outer try/except for Python-level exceptions (e.g., FileNotFoundError) but
ensure git command failures are handled by inspecting returncode and logging
accordingly.

In `@tests/core/test_worktrees.py`:
- Around line 43-55: The test_creates_worktree uses real git commands via
subprocess.run which can fail in CI because git user.name/user.email are unset;
before running the commit in the test (or in a pytest fixture used by
TaskWorktree tests) run git config commands to set a local user.name and
user.email for the tmp_path repo and check subprocess.run returncodes (or remove
capture_output=True) so failures surface; update the test that calls
subprocess.run(["git", "init", str(tmp_path)]), subprocess.run(["git", "-C",
str(tmp_path), "commit", ...]) and/or introduce a fixture that initializes and
configures the repo used by TaskWorktree to ensure commits succeed in CI.
- Around line 47-48: The git setup subprocess.run calls that execute ["git",
"init", str(tmp_path)] and ["git", "-C", str(tmp_path), "commit",
"--allow-empty", "-m", "init"] must enforce success; update each such invocation
in tests/core/test_worktrees.py (the subprocess.run calls using
capture_output=True with tmp_path) to either pass check=True or explicitly
inspect the returned CompletedProcess.returncode and raise/assert on non-zero,
and apply the same change to all occurrences (the other similar pairs around the
noted ranges) so failures surface immediately instead of letting tests proceed
with an invalid repo state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c1672e6d-2bba-4590-aa9c-a058428817cf

📥 Commits

Reviewing files that changed from the base of the PR and between e75afed and 7e6cc43.

📒 Files selected for processing (3)
  • codeframe/core/conductor.py
  • codeframe/core/worktrees.py
  • tests/core/test_worktrees.py

stall_timeout_s: int = 300
stall_action: str = "blocker"
concurrency: ConcurrencyConfig = field(default_factory=ConcurrencyConfig)
isolate: bool = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if TaskWorktree is imported or used anywhere in conductor.py
rg -n "TaskWorktree|worktrees" codeframe/core/conductor.py

# Check if worktree_path is ever passed as non-None
rg -n "worktree_path=" codeframe/core/conductor.py

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Check imports at top of conductor.py
head -50 codeframe/core/conductor.py

# Check if TaskWorktree exists anywhere in the codebase
rg -l "class TaskWorktree" codeframe/

# Check the signature of _execute_parallel and how it calls _execute_task_subprocess
rg -A 10 "def _execute_parallel" codeframe/core/conductor.py | head -30

# Check _execute_group_parallel similarly
rg -A 10 "def _execute_group_parallel" codeframe/core/conductor.py | head -30

Repository: frankbria/codeframe

Length of output: 2523


🏁 Script executed:

# Check if TaskWorktree is defined in worktrees.py
rg -n "class TaskWorktree" codeframe/core/worktrees.py

# Look at the full _execute_parallel implementation
rg -A 50 "def _execute_parallel\(" codeframe/core/conductor.py | head -70

# Look at the full _execute_group_parallel implementation  
rg -A 50 "def _execute_group_parallel\(" codeframe/core/conductor.py | head -70

# Check the _execute_task_subprocess signature
rg -B 2 -A 10 "def _execute_task_subprocess\(" codeframe/core/conductor.py

Repository: frankbria/codeframe

Length of output: 4146


🏁 Script executed:

# Check how _execute_task_subprocess is called within _execute_group_parallel
rg -B 5 -A 15 "def execute_task\(" codeframe/core/conductor.py | head -40

# Check the complete execute_task implementation
rg -A 25 "def execute_task\(task_id: str\)" codeframe/core/conductor.py | head -35

# Check if batch.isolate is ever checked or used
rg -n "batch\.isolate\|isolate\s*==" codeframe/core/conductor.py

# Check where TaskWorktree is actually imported or used
rg -n "TaskWorktree\|from.*worktrees" codeframe/core/conductor.py

Repository: frankbria/codeframe

Length of output: 2029


🏁 Script executed:

# Check if isolate flag is stored in BatchRun but never used
rg -n "self.isolate\|batch.isolate" codeframe/core/conductor.py

# Check where BatchRun is defined and what fields it has
rg -B 5 -A 30 "class BatchRun" codeframe/core/conductor.py | head -50

# Check if worktree_path is used inside _execute_task_subprocess
rg -A 30 "def _execute_task_subprocess" codeframe/core/conductor.py | head -40

Repository: frankbria/codeframe

Length of output: 2331


🏁 Script executed:

# Check how worktree_path is actually used in _execute_task_subprocess
rg -A 50 "def _execute_task_subprocess" codeframe/core/conductor.py | tail -30

# Check if there's any orchestration code that creates TaskWorktree
rg -n "TaskWorktree(" codeframe/core/

# Check the events to see if there's any worktree-related event emission
rg -n "worktree\|WORKTREE" codeframe/core/events.py

Repository: frankbria/codeframe

Length of output: 1113


🏁 Script executed:

# Check where _execute_task_subprocess is called to see if worktree_path is ever passed
rg "worktree_path\s*=" codeframe/core/conductor.py

# Check if batch.isolate is checked anywhere in the file
rg -n "\.isolate" codeframe/core/conductor.py

# Check if TaskWorktree is instantiated anywhere in the codebase
rg "TaskWorktree\(" codeframe/core/

# Look for any worktree-related logic in start_batch or elsewhere
rg -B 3 -A 3 "isolate" codeframe/core/conductor.py

Repository: frankbria/codeframe

Length of output: 638


🏁 Script executed:

# Check the full start_batch function to see if it creates worktrees
rg -A 60 "def start_batch\(" codeframe/core/conductor.py | head -80

# Check if there's any orchestration logic that creates worktrees before calling _execute_parallel
rg -B 5 -A 5 "_execute_parallel\(workspace" codeframe/core/conductor.py

# Verify TaskWorktree is not imported at all in conductor.py
head -30 codeframe/core/conductor.py | grep -i "import\|from"

Repository: frankbria/codeframe

Length of output: 3757


The isolate flag is stored but never used to create worktrees.

The BatchRun.isolate field and start_batch(isolate=) parameter are added, but the parallel execution paths (_execute_parallel, _execute_group_parallel) never check this flag. Additionally, TaskWorktree (defined in codeframe/core/worktrees.py) is not imported or instantiated in conductor.py, and worktree_path is never passed a non-None value when calling _execute_task_subprocess().

The infrastructure exists—_execute_task_subprocess() correctly handles worktree_path via cwd=str(worktree_path) if worktree_path else workspace.repo_path—but the orchestration logic that would create worktrees when isolate=True and pass their paths to subprocesses is missing. Is this deferred to a follow-up PR, or should issue #418 be considered incomplete?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/conductor.py` at line 537, The BatchRun.isolate flag is set
but never used when running parallel batches; update the orchestration so that
when BatchRun.isolate (and the start_batch(isolate=) parameter) is True you
create per-task worktrees and pass their paths into _execute_task_subprocess via
worktree_path. Specifically: import TaskWorktree from codeframe.core.worktrees,
create a TaskWorktree for each task in _execute_parallel and
_execute_group_parallel when isolate is True, ensure the created worktree path
is passed to _execute_task_subprocess (which already uses cwd=str(worktree_path)
if provided), and clean up or close the TaskWorktree after the subprocess
completes; keep the non-isolate path unchanged so existing behavior is
preserved.

Comment thread codeframe/core/worktrees.py
Comment thread codeframe/core/worktrees.py
CI environments may use 'master' instead of 'main' as the default
branch. Use git rev-parse --abbrev-ref HEAD to detect the actual name.
@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Follow-up Review

The latest commit adds the _get_default_branch() helper to the test file. That is a good improvement and directly addresses the test-level side of my previous review issue #3 (hardcoded main branch name). Tests will now detect the actual default branch rather than assuming it.

However, the five blocking issues from my previous review are still open in the production code:

Issue Status Notes
Critical #1: merge_back() runs git checkout on shared workspace during parallel execution Open worktrees.py:92 still mutates main workspace HEAD
Critical #2: worktree_path never passed from parallel execution loop Open No call-site change in conductor.py parallel path
Important #3: base_branch defaults to main in production Partial Tests fixed; worktrees.py create/merge_back still hard-code main
Important #4: cleanup() silently ignores non-zero git exit codes Open CodeRabbit latest comment also flags this
Important #5: isolate=True default with no serial-path guard Open conductor.py:534

The production-side fix for #3 mirrors the test helper: call git rev-parse --abbrev-ref HEAD to detect the current branch at runtime instead of hard-coding main.

The two critical issues (#1 and #2) remain the main blockers for a safe merge. The feature is architecturally present but not yet wired into the parallel execution loop, and merge_back() still has a race condition caused by mutating the shared workspace checkout. No new concerns from this commit.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Follow-up Review (commit 4a8593fa)

The latest commit — "fix(worktrees): pass base_branch to git worktree add command" — confirms that base_branch is being correctly passed as the start-point argument to git worktree add. This aligns with the correct git syntax (git worktree add <path> -b <branch> <commit-ish>) and addresses the concern raised by CodeRabbit on that specific line.

However, the four remaining open issues from my prior reviews are still unresolved in the production code:

# Issue Location Status
Critical 1 merge_back() runs git checkout <base_branch> on the shared workspace_path while parallel agents may still be executing in that same directory worktrees.py:92 Still open
Critical 2 worktree_path is added to _execute_task_subprocess but never passed from the parallel ThreadPoolExecutor loop — the feature is a no-op for actual parallel batches conductor.py parallel call site Still open
Important 3 base_branch defaults to "main" in both create() and merge_back() — repositories using any other default branch will fail; _get_default_branch() helper in tests shows the right pattern for the production fix worktrees.py:44, 74 Still open
Important 4 cleanup() catches Exception but subprocess.run() without check=True never raises on git errors — non-zero exit codes are silently ignored and worktrees/branches can leak worktrees.py:156–174 Still open

The two critical issues remain the blockers for a safe merge. Specifically, Critical 1 means the merge-back step re-introduces the exact race condition that worktrees are designed to prevent. Critical 2 means the isolation layer has no effect during actual parallel execution until the call site is wired up.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/core/worktrees.py`:
- Around line 156-174: The cleanup currently calls subprocess.run for git
commands without check=True so non-zero git exits are not raised or logged;
update the two subprocess.run invocations (the ["git", "worktree", "remove",
...] call and the ["git", "branch", "-D", ...] call in this module) to detect
failures and log them as warnings: either pass check=True so CalledProcessError
is raised and caught by the existing except block, or capture the
CompletedProcess result and on non-zero returncode call logger.warning with a
descriptive message that includes task_id/branch_name and the process's
stdout/stderr; ensure you still catch and log other Exceptions as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f301569-300e-4ce6-b9ba-0b662296c405

📥 Commits

Reviewing files that changed from the base of the PR and between c1b99db and 4a8593f.

📒 Files selected for processing (1)
  • codeframe/core/worktrees.py

Comment on lines +156 to +174
try:
subprocess.run(
["git", "worktree", "remove", str(worktree_path), "--force"],
cwd=str(workspace_path),
capture_output=True,
text=True,
)
except Exception as exc:
logger.warning("Failed to remove worktree for %s: %s", task_id, exc)

# Delete branch
try:
subprocess.run(
["git", "branch", "-D", branch_name],
cwd=str(workspace_path),
capture_output=True,
text=True,
)
except Exception as exc:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Git command failures are silently ignored in cleanup.

The docstring promises "cleanup failures are logged as warnings," but subprocess.run without check=True won't raise on non-zero exit codes. The try/except only catches Python-level exceptions (e.g., git not found), not git command failures. If the worktree removal or branch deletion fails, nothing is logged.

🐛 Proposed fix to log git command failures
         # Remove worktree
         try:
-            subprocess.run(
+            result = subprocess.run(
                 ["git", "worktree", "remove", str(worktree_path), "--force"],
                 cwd=str(workspace_path),
                 capture_output=True,
                 text=True,
             )
+            if result.returncode != 0:
+                logger.warning("Failed to remove worktree for %s: %s", task_id, result.stderr)
         except Exception as exc:
             logger.warning("Failed to remove worktree for %s: %s", task_id, exc)

         # Delete branch
         try:
-            subprocess.run(
+            result = subprocess.run(
                 ["git", "branch", "-D", branch_name],
                 cwd=str(workspace_path),
                 capture_output=True,
                 text=True,
             )
+            if result.returncode != 0:
+                logger.warning("Failed to delete branch %s: %s", branch_name, result.stderr)
         except Exception as exc:
             logger.warning("Failed to delete branch %s: %s", branch_name, exc)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/worktrees.py` around lines 156 - 174, The cleanup currently
calls subprocess.run for git commands without check=True so non-zero git exits
are not raised or logged; update the two subprocess.run invocations (the ["git",
"worktree", "remove", ...] call and the ["git", "branch", "-D", ...] call in
this module) to detect failures and log them as warnings: either pass check=True
so CalledProcessError is raised and caught by the existing except block, or
capture the CompletedProcess result and on non-zero returncode call
logger.warning with a descriptive message that includes task_id/branch_name and
the process's stdout/stderr; ensure you still catch and log other Exceptions as
before.

@claude

claude Bot commented Mar 14, 2026

Copy link
Copy Markdown

Follow-up Review (commit 866a9c52)

The latest commit updates tests/core/test_worktrees.py (the only file changed between 4a8593fa and 866a9c52). Based on the CodeRabbit processing summary this is a test-only change, likely adding git identity configuration (user.email/user.name) to fix CI failures, which directly addresses my previous review issue Test 8.

Status of open issues

# Issue Location Status
Critical 1 merge_back() runs git checkout <base_branch> on the shared workspace_path while parallel agents may still be running — re-introduces the race condition worktrees are designed to prevent worktrees.py:92 Still open
Critical 2 worktree_path is added to _execute_task_subprocess but never passed from the parallel ThreadPoolExecutor loop — isolation is a no-op for actual parallel batches conductor.py parallel call site Still open
Important 3 base_branch defaults to "main" in both create() and merge_back() — repositories with any other default branch will fail silently worktrees.py:44, 74 Still open
Important 4 cleanup() catches Exception but subprocess.run() without check=True never raises on git errors — non-zero exit codes are swallowed and worktrees/branches can leak worktrees.py:156-174 Still open
Test 8 Tests needed git config user.email and user.name to run in CI test_worktrees.py Fixed (assumed)

No new concerns introduced by this commit. The two critical issues remain the blockers for a safe merge: the feature is architecturally present but not wired into the parallel execution path, and merge_back() still mutates shared workspace state mid-parallel-batch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/core/test_worktrees.py`:
- Around line 12-18: Tests call git via subprocess.run without check=True and
_get_default_branch falls back to "main"; add a helper _git(repo_path: Path,
*args: str) that wraps subprocess.run(..., check=True, capture_output=True,
text=True) and an _init_repo(tmp_path: Path) that runs git init, sets user.email
and user.name, makes an initial commit, then call _get_default_branch which
should use _git(..., "rev-parse", "--abbrev-ref", "HEAD") and return
result.stdout.strip() (remove the "or 'main'" fallback); replace all bare
subprocess.run(["git", ...]) calls in tests with _git(...) or use
_init_repo(...) for repo setup so git failures fail fast and identity is
configured.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9476b08-e744-439f-b512-8f2347c22d76

📥 Commits

Reviewing files that changed from the base of the PR and between 4a8593f and 866a9c5.

📒 Files selected for processing (1)
  • tests/core/test_worktrees.py

Comment on lines +12 to +18
def _get_default_branch(repo_path: Path) -> str:
"""Get the default branch name of a git repo."""
result = subprocess.run(
["git", "-C", str(repo_path), "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True, text=True,
)
return result.stdout.strip() or "main"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import ast
from pathlib import Path

p = Path("tests/core/test_worktrees.py")
tree = ast.parse(p.read_text())

for n in ast.walk(tree):
    if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute):
        if isinstance(n.func.value, ast.Name) and n.func.value.id == "subprocess" and n.func.attr == "run":
            is_git = (
                n.args
                and isinstance(n.args[0], ast.List)
                and n.args[0].elts
                and isinstance(n.args[0].elts[0], ast.Constant)
                and n.args[0].elts[0].value == "git"
            )
            if not is_git:
                continue
            check_kw = next((kw for kw in n.keywords if kw.arg == "check"), None)
            ok = bool(check_kw and isinstance(check_kw.value, ast.Constant) and check_kw.value.value is True)
            if not ok:
                print(f"Line {n.lineno}: git subprocess.run without check=True")
PY

Repository: frankbria/codeframe

Length of output: 1206


🏁 Script executed:

head -20 tests/core/test_worktrees.py

Repository: frankbria/codeframe

Length of output: 566


🏁 Script executed:

cat -n tests/core/test_worktrees.py | head -150

Repository: frankbria/codeframe

Length of output: 7081


🏁 Script executed:

cat -n tests/core/test_worktrees.py | tail -80

Repository: frankbria/codeframe

Length of output: 3612


Add check=True to all git subprocess calls; fail fast on command failures.

Lines 14, 55–56, 69–70, 82–83, 90, 104–105, 113–114, 127, 129–130, 138–139, 143–144, 158–159, 170, 179–180: git commands run without check=True, silencing failures. When git commit fails (common in CI without configured user email/name), tests continue with a broken repo state and fail later with misleading assertions. Additionally, line 18's or "main" fallback masks _get_default_branch failures instead of failing fast.

Add a helper function to configure git user identity once, then use subprocess with check=True consistently:

Suggested pattern
def _git(repo_path: Path, *args: str) -> subprocess.CompletedProcess[str]:
    """Run git command with check=True."""
    return subprocess.run(
        ["git", "-C", str(repo_path), *args],
        capture_output=True,
        text=True,
        check=True,
    )

def _init_repo(tmp_path: Path) -> str:
    """Initialize test repo with user config."""
    subprocess.run(["git", "init", str(tmp_path)], capture_output=True, text=True, check=True)
    _git(tmp_path, "config", "user.email", "test@example.com")
    _git(tmp_path, "config", "user.name", "Test User")
    _git(tmp_path, "commit", "--allow-empty", "-m", "init")
    return _get_default_branch(tmp_path)

def _get_default_branch(repo_path: Path) -> str:
    """Get the default branch name of a git repo."""
    result = _git(repo_path, "rev-parse", "--abbrev-ref", "HEAD")
    return result.stdout.strip()

Then replace all test setup subprocess.run(["git", ...]) calls with _git(...) or _init_repo(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_worktrees.py` around lines 12 - 18, Tests call git via
subprocess.run without check=True and _get_default_branch falls back to "main";
add a helper _git(repo_path: Path, *args: str) that wraps subprocess.run(...,
check=True, capture_output=True, text=True) and an _init_repo(tmp_path: Path)
that runs git init, sets user.email and user.name, makes an initial commit, then
call _get_default_branch which should use _git(..., "rev-parse", "--abbrev-ref",
"HEAD") and return result.stdout.strip() (remove the "or 'main'" fallback);
replace all bare subprocess.run(["git", ...]) calls in tests with _git(...) or
use _init_repo(...) for repo setup so git failures fail fast and identity is
configured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 4] Worktree-Per-Task Isolation for Parallel Batch Execution

1 participant