Skip to content

fix: worktree management - #223

Merged
compozybot merged 5 commits into
mainfrom
worktree-fix
Jul 3, 2026
Merged

compozybot merged 5 commits into
mainfrom
worktree-fix

Conversation

@compozybot

@compozybot compozybot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes
    • Added stricter preflight checks for --parallel-tasks / --multiple --parallel, rejecting detached HEAD and preventing parallel runs inside managed worktrees before daemon contact.
    • Improved worktree purge behavior with deferred handling, safer containment checks, and correct behavior for nested active runs and missing workspace roots.
    • Updated git subprocess execution to ignore inherited repository-scoped Git environment variables.
  • Tests
    • Expanded unit and end-to-end coverage for parallel execution, linked worktrees, purge guards, and git env isolation.
  • Chores
    • Bumped Go toolchain patch version (1.26.3 → 1.26.4).

pedronauck and others added 2 commits July 3, 2026 13:24
Worktree-backed parallel execution (#200, #217) assumed the workspace
root is a primary checkout and was never tested from inside a linked
git worktree. Root-caused and fixed four confirmed failure modes:

- Git env leakage: daemon/CLI git runners inherited GIT_DIR,
  GIT_WORK_TREE, GIT_INDEX_FILE, GIT_COMMON_DIR and could resolve the
  wrong repository. Added internal/core/gitenv with a shared sanitizer
  applied to the worktree allocator, review-watch runner, CLI preflight,
  and snapshot runner.
- Repo-wide prune blast radius: scoped cleanup no longer runs
  git worktree prune across the whole repo family; owned paths are
  removed explicitly and Prune was dropped from the production
  WorktreeLifecycle surface.
- Purge vs live nested runs: run purge now defers when a planned
  worktree hosts another active run's workspace root, and when the
  registered workspace root no longer exists; shutdown logs and skips
  deferred purges instead of failing the pass.
- Late detached-HEAD failure: parallel task modes now preflight in the
  CLI before daemon bootstrap, rejecting compozy-managed worktree
  recursion and detached HEAD with actionable errors before any run
  rows, events, or worktrees are created.

Refuted with new linked-worktree coverage: fast-forward semantics,
purge branch-retention scope, artifact mirroring, workspace registry
double identity, and snapshot/scope on .git-file worktrees. Added a
linked-worktree fixture and S1-S6 scenario tests (unit + e2e).

Co-Authored-By: Claude Fable 5 <[email protected]>
@compozybot compozybot self-assigned this Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46632b1e-0361-4524-b861-59dad184b9ad

📥 Commits

Reviewing files that changed from the base of the PR and between 7b8d4e4 and f2049f8.

⛔ Files ignored due to path filters (1)
  • skeeper.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • internal/core/gitenv/gitenv.go
  • internal/core/gitenv/gitenv_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/core/gitenv/gitenv_test.go
  • internal/core/gitenv/gitenv.go

Walkthrough

This PR adds a shared sanitized git subprocess helper, routes existing git call sites through it, adds parallel worktree preflight checks in the CLI, removes workspace-wide worktree pruning, and introduces deferred purge handling for active runs and missing workspace roots. It also updates the Go toolchain version.

Changes

Git environment sanitization and worktree lifecycle safety

Layer / File(s) Summary
Go toolchain bump
go.mod
Updates the Go toolchain directive.
gitenv sanitization package
internal/core/gitenv/gitenv.go, internal/core/gitenv/gitenv_test.go
Adds sanitized git subprocess helpers and tests for env filtering, command construction, and subprocess execution behavior.
Apply sanitized git execution
internal/core/worktree/snapshot.go, internal/daemon/review_watch_git.go, internal/daemon/task_multi_worktree.go, internal/core/worktree/snapshot_test.go, internal/daemon/review_watch_git_test.go, internal/daemon/task_multi_worktree_test.go
Routes git subprocess calls through gitenv and adds coverage for inherited git env isolation and linked-worktree behavior.
CLI parallel worktree preflight checks
internal/cli/daemon_commands.go, internal/cli/root_command_execution_test.go, internal/cli/tasks_run_parallel_e2e_test.go
Adds CLI preflight checks for parallel worktree runs and updates unit/e2e coverage for detached-head, managed-worktree, and linked-worktree scenarios.
Remove workspace-wide Prune from worktree lifecycle
internal/core/run/parallel/orchestrator.go, internal/core/run/parallel/orchestrator_test.go, internal/daemon/task_multi.go, internal/daemon/task_multi_worktree.go
Removes Prune from worktree lifecycle code paths, updates orchestrator cleanup/rollback, and adjusts tests for scoped cleanup and linked-worktree allocator behavior.
Deferred purge safety for active runs and missing roots
internal/daemon/worktree_purge.go, internal/daemon/shutdown.go, internal/daemon/purge_test.go
Adds deferred purge detection and logging for missing roots and active nested runs, with tests covering the new skip behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • compozy/compozy#162: Both PRs touch the tasks run --multiple execution plumbing in internal/cli/daemon_commands.go.
  • compozy/compozy#200: Both PRs modify the parallel tasks run --multiple path and related worktree-backed execution flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main theme of the PR: fixes around worktree management and related git/worktree behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix

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

@greptile-apps

greptile-apps Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR hardens worktree lifecycle management across three axes: stricter CLI preflight checks before daemon contact, safer purge deferral when worktrees are in use or their workspace roots are missing, and a centralized gitenv package that strips repository-scoped env vars from all internal git subprocesses.

  • CLI preflight (preflightParallelWorktreeMode): Parallel and multi-run modes now reject detached HEAD and workspace roots inside Compozy-managed worktree directories before ever contacting the daemon, producing exit code 2 with actionable messages.
  • Purge deferral: errRunPurgeDeferred lets Purge skip individual runs whose workspace root is gone or whose worktree hosts an active nested run, logging a warning and continuing rather than failing the entire sweep.
  • gitenv package + Prune() removal: All internal git subprocesses now share gitenv.SanitizedEnv() (filtering GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY, GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_NAMESPACE, GIT_PREFIX); explicit git worktree prune calls are removed to prevent metadata corruption during concurrent linked-worktree operations.

Confidence Score: 5/5

Safe to merge — the changes are additive preflight guards and targeted purge-deferral logic backed by solid test coverage including concurrent and linked-worktree scenarios.

The preflight checks, deferred-purge sentinel, and gitenv consolidation all follow clear, defensive patterns. Containment direction in taskWorktreeHostingActiveRun is correct, the errRunPurgeDeferred sentinel propagates cleanly through errors.Is, and the Prune() removal is intentional to eliminate concurrent metadata corruption. New tests cover detached-HEAD rejection, managed-worktree recursion, missing workspace root deferral, and concurrent linked-family worktree lifecycle.

No files require special attention.

Important Files Changed

Filename Overview
internal/core/gitenv/gitenv.go New package centralizing sanitized git env; filters a superset of vars compared to the old per-package implementations, with clean unit tests.
internal/daemon/worktree_purge.go Adds errRunPurgeDeferred sentinel, taskWorktreeHostingActiveRun guard, ensurePurgeWorkspaceRootAvailable check, and pathEqualOrInside helpers; logic and containment direction appear correct.
internal/daemon/task_multi_worktree.go Removes Prune() from interface, removes explicit git worktree prune calls, and delegates git execution to gitenv.Run; behaviorally equivalent for worktree management commands.
internal/daemon/shutdown.go Adds errRunPurgeDeferred handling in Purge loop — logs a warning and continues rather than aborting the entire purge when a run is deferred.
internal/cli/daemon_commands.go Adds preflightParallelWorktreeMode to reject detached HEAD and managed-worktree recursion before daemon contact; introduces duplicate path-containment helpers (previously flagged).
internal/core/run/parallel/orchestrator.go Removes Prune() from WorktreeLifecycle interface and drops its two callsites; rollback and cleanupCompleted no longer explicitly prune git worktree metadata.
internal/core/worktree/snapshot.go Replaces inline exec and local sanitizedGitEnv() with gitenv.Command(); appends LC_ALL=C and GIT_OPTIONAL_LOCKS=0 on top of the shared sanitized env — functionally equivalent but now filters a broader set of vars.
internal/daemon/review_watch_git.go Replaces local runReviewWatchGitCommand implementation with gitenv.Run(); equivalent behavior, reduces duplication.
internal/daemon/task_multi.go Removes parallelWorktreeLifecycle.Prune() method following interface removal; straightforward deletion.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[CLI: tasks run --parallel / --parallel-tasks] --> B[preflightParallelWorktreeMode]
    B --> C{workspace inside\nmanaged worktrees?}
    C -- yes --> D[exit 2: recursion rejected]
    C -- no --> E{detached HEAD?}
    E -- yes --> F[exit 2: detached HEAD rejected]
    E -- no --> G[contact daemon / bootstrap]
    G --> H[parallel orchestrator]
    H --> I[allocate task worktrees]
    I --> J[run tasks in waves]
    J --> K{success?}
    K -- yes --> L[cleanupCompleted: discard integration branch]
    K -- no --> M[rollback: discard integration branch]
    L --> N[done]
    M --> N

    subgraph Daemon Purge
        P[RunManager.Purge loop] --> Q[prepareRunWorktreePurge]
        Q --> R{workspace root\nexists?}
        R -- missing --> S[deferRunPurge: log warn, continue]
        R -- present --> T{task worktree hosts\nactive nested run?}
        T -- yes --> S
        T -- no --> U[removeTaskWorktrees + removeIntegrationWorktree]
        U --> V[purge complete for run]
    end

    subgraph gitenv isolation
        X[all git subprocesses] --> Y[gitenv.Command / gitenv.Run]
        Y --> Z[strip GIT_DIR, GIT_WORK_TREE,\nGIT_INDEX_FILE, GIT_COMMON_DIR,\nGIT_OBJECT_DIRECTORY, etc.]
        Z --> AA[set cmd.Dir = workspaceRoot]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[CLI: tasks run --parallel / --parallel-tasks] --> B[preflightParallelWorktreeMode]
    B --> C{workspace inside\nmanaged worktrees?}
    C -- yes --> D[exit 2: recursion rejected]
    C -- no --> E{detached HEAD?}
    E -- yes --> F[exit 2: detached HEAD rejected]
    E -- no --> G[contact daemon / bootstrap]
    G --> H[parallel orchestrator]
    H --> I[allocate task worktrees]
    I --> J[run tasks in waves]
    J --> K{success?}
    K -- yes --> L[cleanupCompleted: discard integration branch]
    K -- no --> M[rollback: discard integration branch]
    L --> N[done]
    M --> N

    subgraph Daemon Purge
        P[RunManager.Purge loop] --> Q[prepareRunWorktreePurge]
        Q --> R{workspace root\nexists?}
        R -- missing --> S[deferRunPurge: log warn, continue]
        R -- present --> T{task worktree hosts\nactive nested run?}
        T -- yes --> S
        T -- no --> U[removeTaskWorktrees + removeIntegrationWorktree]
        U --> V[purge complete for run]
    end

    subgraph gitenv isolation
        X[all git subprocesses] --> Y[gitenv.Command / gitenv.Run]
        Y --> Z[strip GIT_DIR, GIT_WORK_TREE,\nGIT_INDEX_FILE, GIT_COMMON_DIR,\nGIT_OBJECT_DIRECTORY, etc.]
        Z --> AA[set cmd.Dir = workspaceRoot]
    end
Loading

Reviews (3): Last reviewed commit: "fix: address gitenv review feedback" | Re-trigger Greptile

Comment thread internal/cli/daemon_commands.go Outdated
Comment on lines +1010 to +1044
return strings.TrimSpace(stdout.String()), nil
}

func cliPathEqualOrInside(parent string, child string) (bool, error) {
parentPath, err := cliCleanContainmentPath(parent)
if err != nil {
return false, err
}
childPath, err := cliCleanContainmentPath(child)
if err != nil {
return false, err
}
rel, err := filepath.Rel(parentPath, childPath)
if err != nil {
return false, fmt.Errorf("relativize path %s to %s: %w", childPath, parentPath, err)
}
return rel == "." || (rel != "" && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))), nil
}

func cliCleanContainmentPath(path string) (string, error) {
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return "", errors.New("path is required")
}
absolute, err := filepath.Abs(trimmed)
if err != nil {
return "", fmt.Errorf("resolve path %s: %w", trimmed, err)
}
if resolved, err := filepath.EvalSymlinks(absolute); err == nil {
absolute = resolved
} else if !errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("resolve symlinks for %s: %w", absolute, err)
}
return filepath.Clean(absolute), nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Duplicated path-containment helpers across package boundaries

cliPathEqualOrInside and cliCleanContainmentPath are functionally identical to pathEqualOrInside and cleanContainmentPath in internal/daemon/worktree_purge.go. The isRelativeChildPath predicate in the daemon package matches the inline logic here exactly. If either copy drifts — say, a platform-specific separator fix or a symlink edge-case — one callsite will silently diverge from the other. Extracting these two helpers into a shared package (e.g., internal/core/pathutil) would give both the daemon and CLI a single implementation to update.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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

🧹 Nitpick comments (4)
internal/core/run/parallel/orchestrator_test.go (1)

241-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This regression guard is tautological and can never fail.

Prune was removed from the WorktreeLifecycle interface (orchestrator.go Lines 83-98), so the orchestrator only sees interface methods and can never invoke fakeWorktreeLifecycle.Prune. wasPruned() will therefore always return false regardless of orchestrator logic, so this scenario cannot detect a repo-wide-prune regression.

Consider asserting the actual scoped behavior instead (e.g. that only per-path Remove was called for the expected worktrees, plus DiscardIntegrationBranch), which fails if scoped cleanup logic changes. The now-unreachable Prune/pruned/wasPruned scaffolding (Lines 1074-1079, 1111-1115) can be dropped once the assertion is behavior-based.

As per path instructions: "Verify tests can fail when business logic changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/run/parallel/orchestrator_test.go` around lines 241 - 262, The
regression test in
runParallelExecutionOrchestratorDoesNotPruneAfterScopedCleanup is tautological
because WorktreeLifecycle no longer exposes Prune, so
fakeWorktreeLifecycle.wasPruned() can never reflect orchestrator behavior.
Replace this assertion with checks on observable scoped cleanup behavior in
NewParallelExecutionOrchestrator.Run, such as verifying the expected per-path
Remove calls and DiscardIntegrationBranch invocation, using the existing
fakeWorktreeLifecycle and launcher helpers. Once the test asserts real behavior,
remove the unreachable Prune/pruned/wasPruned scaffolding from the fake
lifecycle.

Source: Path instructions

internal/core/gitenv/gitenv.go (1)

1-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a shared Command constructor to eliminate duplicated subprocess-setup logic.

review_watch_git.go and task_multi_worktree.go both duplicate an identical pattern: build cmd := exec.CommandContext(ctx, "git"), then manually overwrite cmd.Args with append([]string{"git"}, cmdArgs...) and set cmd.Env = gitenv.SanitizedEnv(). Since this package already centralizes environment sanitization, it could also expose a small helper (e.g., gitenv.Command(ctx, dir string, args ...string) *exec.Cmd) that wires Env automatically, so future call sites can't forget to sanitize and the three near-identical implementations converge to one.

Also worth adding a dedicated gitenv_test.go exercising SanitizedEnv/IsRepositoryEnvName directly, since coverage currently only exists indirectly via the daemon/worktree re-exec tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv.go` around lines 1 - 42, The git subprocess setup
is duplicated in multiple call sites, so factor it into a shared helper in
gitenv to keep Env sanitization and argument wiring consistent. Add a small
constructor like gitenv.Command that wraps exec.CommandContext, sets Args/Dir,
and always applies gitenv.SanitizedEnv, then update review_watch_git.go and
task_multi_worktree.go to use it. Keep SanitizedEnv and IsRepositoryEnvName as
the core sanitization logic, and add gitenv_test.go to directly cover both
functions so the behavior is validated without relying only on indirect
daemon/worktree tests.
internal/cli/daemon_commands.go (2)

992-1011: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate git subprocess runner.

runTaskRunGitPreflight reimplements the same "-C <dir> + sanitized env + stdout/stderr capture" pattern already used by runTaskMultiWorktreeGitCommand in internal/daemon/task_multi_worktree.go (and, per the PR summary, the same shape was also just applied to internal/core/worktree/snapshot.go and internal/daemon/review_watch_git.go). The gitenv package was introduced specifically to centralize the sanitization contract; leaving the surrounding exec/capture boilerplate duplicated across four call sites risks drift (e.g., one site forgetting gitenv.SanitizedEnv() in a future edit).

Consider hoisting a shared RunGit(ctx, dir string, args ...string) (string, error) helper (e.g., into gitenv or a small sibling package) that all four call sites use.

♻️ Sketch of a shared helper
// internal/core/gitenv/exec.go
func RunGit(ctx context.Context, dir string, args ...string) (string, error) {
	cmd := exec.CommandContext(ctx, "git")
	cmd.Args = append([]string{"git", "-C", strings.TrimSpace(dir)}, args...)
	cmd.Env = SanitizedEnv()
	var stdout, stderr strings.Builder
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		message := strings.TrimSpace(stderr.String())
		if message == "" {
			message = strings.TrimSpace(stdout.String())
		}
		if message != "" {
			return "", fmt.Errorf("%w: %s", err, message)
		}
		return "", err
	}
	return strings.TrimSpace(stdout.String()), nil
}
-func runTaskRunGitPreflight(ctx context.Context, workspaceRoot string, args ...string) (string, error) {
-	cmdArgs := append([]string{"-C", strings.TrimSpace(workspaceRoot)}, args...)
-	cmd := exec.CommandContext(ctx, "git")
-	cmd.Args = append([]string{"git"}, cmdArgs...)
-	cmd.Env = gitenv.SanitizedEnv()
-	var stdout, stderr strings.Builder
-	cmd.Stdout = &stdout
-	cmd.Stderr = &stderr
-	if err := cmd.Run(); err != nil {
-		message := strings.TrimSpace(stderr.String())
-		if message == "" {
-			message = strings.TrimSpace(stdout.String())
-		}
-		if message != "" {
-			return "", fmt.Errorf("%w: %s", err, message)
-		}
-		return "", err
-	}
-	return strings.TrimSpace(stdout.String()), nil
-}
+func runTaskRunGitPreflight(ctx context.Context, workspaceRoot string, args ...string) (string, error) {
+	return gitenv.RunGit(ctx, workspaceRoot, args...)
+}
Based on learnings from the PR stack context: Git command execution explicitly sets cmd.Env = gitenv.SanitizedEnv() in `internal/daemon/task_multi_worktree.go`, confirming this exact pattern already exists elsewhere.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/daemon_commands.go` around lines 992 - 1011, The git subprocess
logic in runTaskRunGitPreflight duplicates the same command setup, sanitized
environment, and stdout/stderr capture already implemented in
runTaskMultiWorktreeGitCommand and the other git call sites. Extract this
repeated pattern into a shared helper such as RunGit(ctx, dir string, args
...string) in gitenv or a nearby package, and update runTaskRunGitPreflight to
use it so all git execution paths share the same SanitizedEnv and error
handling.

1013-1044: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Extract the containment check into one shared helper. cliPathEqualOrInside/cliCleanContainmentPath duplicate pathEqualOrInside/cleanContainmentPath; keeping one implementation avoids drift in this security-sensitive check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/daemon_commands.go` around lines 1013 - 1044, The containment
logic in cliPathEqualOrInside and cliCleanContainmentPath duplicates the
existing pathEqualOrInside and cleanContainmentPath helpers, so consolidate to
one shared implementation and have the CLI helper reuse it. Update the
containment check path used by the daemon commands to call the shared helper
directly, keeping the behavior identical while avoiding drift in this
security-sensitive validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/daemon/task_multi_worktree_test.go`:
- Around line 1798-1839:
`TestRunTaskMultiWorktreeGitCommandIgnoresInheritedGitEnv` should follow the
required `t.Run("Should...")` pattern like
`TestRunReviewWatchGitCommandIgnoresInheritedGitEnv` does. Move the
assertion-bearing logic in this test into a `t.Run("Should ignore inherited git
repository env vars", ...)` subtest, keeping the existing helper-process branch
and setup intact, so all checks run under a properly named subtest for
consistency with the test conventions.

---

Nitpick comments:
In `@internal/cli/daemon_commands.go`:
- Around line 992-1011: The git subprocess logic in runTaskRunGitPreflight
duplicates the same command setup, sanitized environment, and stdout/stderr
capture already implemented in runTaskMultiWorktreeGitCommand and the other git
call sites. Extract this repeated pattern into a shared helper such as
RunGit(ctx, dir string, args ...string) in gitenv or a nearby package, and
update runTaskRunGitPreflight to use it so all git execution paths share the
same SanitizedEnv and error handling.
- Around line 1013-1044: The containment logic in cliPathEqualOrInside and
cliCleanContainmentPath duplicates the existing pathEqualOrInside and
cleanContainmentPath helpers, so consolidate to one shared implementation and
have the CLI helper reuse it. Update the containment check path used by the
daemon commands to call the shared helper directly, keeping the behavior
identical while avoiding drift in this security-sensitive validation.

In `@internal/core/gitenv/gitenv.go`:
- Around line 1-42: The git subprocess setup is duplicated in multiple call
sites, so factor it into a shared helper in gitenv to keep Env sanitization and
argument wiring consistent. Add a small constructor like gitenv.Command that
wraps exec.CommandContext, sets Args/Dir, and always applies
gitenv.SanitizedEnv, then update review_watch_git.go and task_multi_worktree.go
to use it. Keep SanitizedEnv and IsRepositoryEnvName as the core sanitization
logic, and add gitenv_test.go to directly cover both functions so the behavior
is validated without relying only on indirect daemon/worktree tests.

In `@internal/core/run/parallel/orchestrator_test.go`:
- Around line 241-262: The regression test in
runParallelExecutionOrchestratorDoesNotPruneAfterScopedCleanup is tautological
because WorktreeLifecycle no longer exposes Prune, so
fakeWorktreeLifecycle.wasPruned() can never reflect orchestrator behavior.
Replace this assertion with checks on observable scoped cleanup behavior in
NewParallelExecutionOrchestrator.Run, such as verifying the expected per-path
Remove calls and DiscardIntegrationBranch invocation, using the existing
fakeWorktreeLifecycle and launcher helpers. Once the test asserts real behavior,
remove the unreachable Prune/pruned/wasPruned scaffolding from the fake
lifecycle.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b2f7a8dd-97c3-4928-8e47-0febefa5e552

📥 Commits

Reviewing files that changed from the base of the PR and between 73ced7f and ca04dd7.

⛔ Files ignored due to path filters (10)
  • .agents/skills/cmux-orchestration/SKILL.md is excluded by !**/*.md, !.agents/**
  • .agents/skills/cmux-orchestration/references/plan-mode.md is excluded by !**/*.md, !.agents/**
  • .claude/specs/worktree-nested-execution-spec.md is excluded by !**/*.md, !.claude/**
  • .github/actions/setup-go/action.yml is excluded by !**/*.yml
  • .github/versions.yml is excluded by !**/*.yml
  • .github/workflows/auto-docs.yml is excluded by !**/*.yml
  • .github/workflows/ci.yml is excluded by !**/*.yml
  • .github/workflows/release.yml is excluded by !**/*.yml
  • skeeper.lock is excluded by !**/*.lock
  • skills-lock.json is excluded by !**/*.json
📒 Files selected for processing (17)
  • go.mod
  • internal/cli/daemon_commands.go
  • internal/cli/root_command_execution_test.go
  • internal/cli/tasks_run_parallel_e2e_test.go
  • internal/core/gitenv/gitenv.go
  • internal/core/run/parallel/orchestrator.go
  • internal/core/run/parallel/orchestrator_test.go
  • internal/core/worktree/snapshot.go
  • internal/core/worktree/snapshot_test.go
  • internal/daemon/purge_test.go
  • internal/daemon/review_watch_git.go
  • internal/daemon/review_watch_git_test.go
  • internal/daemon/shutdown.go
  • internal/daemon/task_multi.go
  • internal/daemon/task_multi_worktree.go
  • internal/daemon/task_multi_worktree_test.go
  • internal/daemon/worktree_purge.go
💤 Files with no reviewable changes (1)
  • internal/daemon/task_multi.go

Comment thread internal/daemon/task_multi_worktree_test.go

@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.

🧹 Nitpick comments (2)
internal/core/gitenv/gitenv_test.go (1)

1-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test coverage for Run().

SanitizedEnv, IsRepositoryEnvName, and Command are all tested, but Run() — which contains the stderr/stdout fallback and error-wrapping logic reused by review_watch_git.go and task_multi_worktree.go — has no dedicated test. As per path instructions, tests should focus on critical paths including error handling.

func TestRun(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git binary not available")
	}
	t.Run("Should return trimmed stdout on success", func(t *testing.T) {
		dir := t.TempDir()
		if _, err := Run(context.Background(), dir, "init", "-q"); err != nil {
			t.Fatalf("init error = %v", err)
		}
		out, err := Run(context.Background(), dir, "rev-parse", "--show-toplevel")
		if err != nil {
			t.Fatalf("Run() error = %v", err)
		}
		if out == "" {
			t.Fatal("Run() returned empty toplevel")
		}
	})
	t.Run("Should wrap error with stderr message on failure", func(t *testing.T) {
		_, err := Run(context.Background(), t.TempDir(), "rev-parse", "--verify", "nope")
		if err == nil {
			t.Fatal("expected error")
		}
	})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv_test.go` around lines 1 - 89, Add dedicated
coverage for Run() in gitenv_test.go, since it’s currently untested despite
containing the stdout/stderr fallback and error-wrapping behavior used by
review_watch_git.go and task_multi_worktree.go. Extend TestRun to verify the
success path returns trimmed stdout and the failure path wraps the git error
with stderr details; reference the Run function and use the existing
context/background setup alongside exec.LookPath("git") as in the diff.

Source: Path instructions

internal/core/gitenv/gitenv.go (1)

39-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add command context to the no-message error path.

When both stderr and stdout are empty (e.g., binary missing, context canceled before output), Line 53 returns the raw err with no indication of which git command/dir failed, unlike the message branch which is well wrapped. As per coding guidelines, prefer wrapped context via fmt.Errorf("context: %w", err).

♻️ Proposed fix
-		return "", err
+		return "", fmt.Errorf("git %s (dir %s): %w", strings.Join(args, " "), dir, err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv.go` around lines 39 - 56, The no-message error
path in Run returns a bare error without any git command context. Update the
branch after cmd.Run() so that when both stderr and stdout are empty, it still
wraps err with contextual information from Run/Command, including the git args
and dir, using fmt.Errorf with %w. Keep the existing message-based branch
unchanged, but make the empty-output branch consistent so failures like missing
binary or canceled context are easier to trace.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/core/gitenv/gitenv_test.go`:
- Around line 1-89: Add dedicated coverage for Run() in gitenv_test.go, since
it’s currently untested despite containing the stdout/stderr fallback and
error-wrapping behavior used by review_watch_git.go and task_multi_worktree.go.
Extend TestRun to verify the success path returns trimmed stdout and the failure
path wraps the git error with stderr details; reference the Run function and use
the existing context/background setup alongside exec.LookPath("git") as in the
diff.

In `@internal/core/gitenv/gitenv.go`:
- Around line 39-56: The no-message error path in Run returns a bare error
without any git command context. Update the branch after cmd.Run() so that when
both stderr and stdout are empty, it still wraps err with contextual information
from Run/Command, including the git args and dir, using fmt.Errorf with %w. Keep
the existing message-based branch unchanged, but make the empty-output branch
consistent so failures like missing binary or canceled context are easier to
trace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abc7878a-045f-44cd-a434-e0bc42dbb233

📥 Commits

Reviewing files that changed from the base of the PR and between ca04dd7 and 1d597c8.

⛔ Files ignored due to path filters (1)
  • skeeper.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • internal/cli/daemon_commands.go
  • internal/core/gitenv/gitenv.go
  • internal/core/gitenv/gitenv_test.go
  • internal/core/run/parallel/orchestrator_test.go
  • internal/core/worktree/snapshot.go
  • internal/daemon/review_watch_git.go
  • internal/daemon/task_multi_worktree.go
  • internal/daemon/task_multi_worktree_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/core/worktree/snapshot.go
  • internal/cli/daemon_commands.go
  • internal/daemon/task_multi_worktree_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 3, 2026

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/core/gitenv/gitenv_test.go (1)

31-55: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Table cases aren't run as individual subtests.

All 11 table rows are executed inside a single t.Run("Should identify repository-scoped git env vars", ...) block using t.Fatalf, so the first failing case aborts the loop and hides results for the remaining cases; failure output also won't identify which specific input failed by name. As per path instructions, MUST use t.Run("Should...") pattern for ALL test cases.

✅ Proposed per-case subtests
 func TestIsRepositoryEnvName(t *testing.T) {
-	t.Run("Should identify repository-scoped git env vars", func(t *testing.T) {
-		tests := []struct {
-			name string
-			want bool
-		}{
-			{name: "GIT_DIR", want: true},
-			{name: " GIT_WORK_TREE ", want: true},
-			{name: "GIT_INDEX_FILE", want: true},
-			{name: "GIT_COMMON_DIR", want: true},
-			{name: "GIT_OBJECT_DIRECTORY", want: true},
-			{name: "GIT_ALTERNATE_OBJECT_DIRECTORIES", want: true},
-			{name: "GIT_NAMESPACE", want: true},
-			{name: "GIT_PREFIX", want: true},
-			{name: "GIT_SSH_COMMAND", want: false},
-			{name: "HOME", want: false},
-			{name: "", want: false},
-		}
-		for _, tt := range tests {
-			if got := IsRepositoryEnvName(tt.name); got != tt.want {
-				t.Fatalf("IsRepositoryEnvName(%q) = %t, want %t", tt.name, got, tt.want)
-			}
-		}
-	})
+	tests := []struct {
+		name string
+		want bool
+	}{
+		{name: "GIT_DIR", want: true},
+		{name: " GIT_WORK_TREE ", want: true},
+		{name: "GIT_INDEX_FILE", want: true},
+		{name: "GIT_COMMON_DIR", want: true},
+		{name: "GIT_OBJECT_DIRECTORY", want: true},
+		{name: "GIT_ALTERNATE_OBJECT_DIRECTORIES", want: true},
+		{name: "GIT_NAMESPACE", want: true},
+		{name: "GIT_PREFIX", want: true},
+		{name: "GIT_SSH_COMMAND", want: false},
+		{name: "HOME", want: false},
+		{name: "", want: false},
+	}
+	for _, tt := range tests {
+		t.Run("Should evaluate "+tt.name, func(t *testing.T) {
+			if got := IsRepositoryEnvName(tt.name); got != tt.want {
+				t.Fatalf("IsRepositoryEnvName(%q) = %t, want %t", tt.name, got, tt.want)
+			}
+		})
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv_test.go` around lines 31 - 55, The table-driven
checks in TestIsRepositoryEnvName are wrapped in one t.Run and use t.Fatalf, so
each case is not executed independently and a single failure stops the rest.
Update the test to use a separate t.Run per table row, keeping the existing
TestIsRepositoryEnvName and IsRepositoryEnvName symbols, so each input runs as
its own subtest and failures clearly identify the specific case.

Source: Path instructions

internal/core/gitenv/gitenv.go (1)

40-57: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Redact git args before formatting this error. review_watch_git.Push passes options.PushRemote into gitenv.Run, and that value is configurable from request/config. If a remote URL with embedded credentials is used, the returned error will echo the token verbatim.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv.go` around lines 40 - 57, The error formatting in
gitenv.Run currently echoes raw git args, which can leak credentials from
configurable remotes such as review_watch_git.Push. Update the error
construction in Run to redact or sanitize sensitive values from args before
joining them into the command string or message, and keep the existing failure
context from cmd.Dir and stderr/stdout. Use the Run and Command symbols to
locate the error path and ensure any remote URL or token-like value is not
returned verbatim.
🧹 Nitpick comments (2)
internal/core/gitenv/gitenv_test.go (1)

75-135: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Independent TestRun subtests could use t.Parallel().

Each subtest allocates its own t.TempDir() and doesn't share mutable state, so they're good candidates for t.Parallel() per the default table-driven pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv_test.go` around lines 75 - 135, The TestRun
subtests can run independently and should be marked parallel for better test
execution. Add t.Parallel() at the start of each subtest inside TestRun, and
keep the existing use of t.TempDir(), Run, and the git-based assertions
unchanged since they do not share mutable state.

Source: Coding guidelines

internal/core/gitenv/gitenv.go (1)

50-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rebuild the display command from cmd.Args instead of re-joining args.

cmd.Args already holds ["git", args...]; manually re-deriving it via "git" + strings.Join(args, " ") duplicates that data and could drift if Command's construction changes.

♻️ Proposed simplification
-		command := "git"
-		if len(args) > 0 {
-			command += " " + strings.Join(args, " ")
-		}
+		command := strings.Join(cmd.Args, " ")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/gitenv/gitenv.go` around lines 50 - 57, The error formatting in
gitenv.Command should use the actual executed command from cmd.Args instead of
rebuilding it from args; update the command string construction in the error
path to derive the display text directly from cmd.Args so it stays consistent
with how exec.Command was called. Keep the existing failure message handling in
gitenv.go, but replace the manual "git" plus strings.Join(args, " ")
reconstruction with the command arguments already stored on the Cmd object.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/core/gitenv/gitenv_test.go`:
- Around line 31-55: The table-driven checks in TestIsRepositoryEnvName are
wrapped in one t.Run and use t.Fatalf, so each case is not executed
independently and a single failure stops the rest. Update the test to use a
separate t.Run per table row, keeping the existing TestIsRepositoryEnvName and
IsRepositoryEnvName symbols, so each input runs as its own subtest and failures
clearly identify the specific case.

In `@internal/core/gitenv/gitenv.go`:
- Around line 40-57: The error formatting in gitenv.Run currently echoes raw git
args, which can leak credentials from configurable remotes such as
review_watch_git.Push. Update the error construction in Run to redact or
sanitize sensitive values from args before joining them into the command string
or message, and keep the existing failure context from cmd.Dir and
stderr/stdout. Use the Run and Command symbols to locate the error path and
ensure any remote URL or token-like value is not returned verbatim.

---

Nitpick comments:
In `@internal/core/gitenv/gitenv_test.go`:
- Around line 75-135: The TestRun subtests can run independently and should be
marked parallel for better test execution. Add t.Parallel() at the start of each
subtest inside TestRun, and keep the existing use of t.TempDir(), Run, and the
git-based assertions unchanged since they do not share mutable state.

In `@internal/core/gitenv/gitenv.go`:
- Around line 50-57: The error formatting in gitenv.Command should use the
actual executed command from cmd.Args instead of rebuilding it from args; update
the command string construction in the error path to derive the display text
directly from cmd.Args so it stays consistent with how exec.Command was called.
Keep the existing failure message handling in gitenv.go, but replace the manual
"git" plus strings.Join(args, " ") reconstruction with the command arguments
already stored on the Cmd object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 70516afb-bad8-41bb-9aa8-6ef75992906f

📥 Commits

Reviewing files that changed from the base of the PR and between 1d597c8 and 7b8d4e4.

⛔ Files ignored due to path filters (1)
  • skeeper.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • internal/core/gitenv/gitenv.go
  • internal/core/gitenv/gitenv_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 3, 2026
@compozybot
compozybot merged commit 1219df3 into main Jul 3, 2026
8 checks passed
@compozybot
compozybot deleted the worktree-fix branch July 3, 2026 19:28
pedronauck added a commit that referenced this pull request Jul 3, 2026
Add human-written release notes for the next release covering the work
merged since v0.2.10:

- feat: dependency-aware parallel task execution (--parallel-tasks) (#212)
- feat: agentic recovery for failed runs (--recovery) (#212)
- feat: COMPOZY_HOME for isolated per-project daemons (#216)
- feat: repo-level default overrides in compozy setup (#90)
- fix: safer worktree management for parallel runs (#217, #223)
- fix: ACP runs consistently apply the selected model (#215)
- fix: run TUI elapsed timer restored across all terminal outcomes (#221)

Co-Authored-By: Claude Fable 5 <[email protected]>
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.

2 participants