Repository navigation
fix: worktree management - #223
Conversation
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]>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis 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. ChangesGit environment sanitization and worktree lifecycle safety
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
| 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
%%{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
Reviews (3): Last reviewed commit: "fix: address gitenv review feedback" | Re-trigger Greptile
| 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 | ||
| } |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
internal/core/run/parallel/orchestrator_test.go (1)
241-262: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThis regression guard is tautological and can never fail.
Prunewas removed from theWorktreeLifecycleinterface (orchestrator.go Lines 83-98), so the orchestrator only sees interface methods and can never invokefakeWorktreeLifecycle.Prune.wasPruned()will therefore always returnfalseregardless 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
Removewas called for the expected worktrees, plusDiscardIntegrationBranch), which fails if scoped cleanup logic changes. The now-unreachablePrune/pruned/wasPrunedscaffolding (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 winConsider a shared
Commandconstructor to eliminate duplicated subprocess-setup logic.
review_watch_git.goandtask_multi_worktree.goboth duplicate an identical pattern: buildcmd := exec.CommandContext(ctx, "git"), then manually overwritecmd.Argswithappend([]string{"git"}, cmdArgs...)and setcmd.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 wiresEnvautomatically, 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.goexercisingSanitizedEnv/IsRepositoryEnvNamedirectly, 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 winDuplicate git subprocess runner.
runTaskRunGitPreflightreimplements the same "-C <dir>+ sanitized env + stdout/stderr capture" pattern already used byrunTaskMultiWorktreeGitCommandininternal/daemon/task_multi_worktree.go(and, per the PR summary, the same shape was also just applied tointernal/core/worktree/snapshot.goandinternal/daemon/review_watch_git.go). Thegitenvpackage 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 forgettinggitenv.SanitizedEnv()in a future edit).Consider hoisting a shared
RunGit(ctx, dir string, args ...string) (string, error)helper (e.g., intogitenvor a small sibling package) that all four call sites use.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.♻️ 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...) +}🤖 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 winExtract the containment check into one shared helper.
cliPathEqualOrInside/cliCleanContainmentPathduplicatepathEqualOrInside/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
⛔ Files ignored due to path filters (10)
.agents/skills/cmux-orchestration/SKILL.mdis excluded by!**/*.md,!.agents/**.agents/skills/cmux-orchestration/references/plan-mode.mdis excluded by!**/*.md,!.agents/**.claude/specs/worktree-nested-execution-spec.mdis excluded by!**/*.md,!.claude/**.github/actions/setup-go/action.ymlis excluded by!**/*.yml.github/versions.ymlis excluded by!**/*.yml.github/workflows/auto-docs.ymlis excluded by!**/*.yml.github/workflows/ci.ymlis excluded by!**/*.yml.github/workflows/release.ymlis excluded by!**/*.ymlskeeper.lockis excluded by!**/*.lockskills-lock.jsonis excluded by!**/*.json
📒 Files selected for processing (17)
go.modinternal/cli/daemon_commands.gointernal/cli/root_command_execution_test.gointernal/cli/tasks_run_parallel_e2e_test.gointernal/core/gitenv/gitenv.gointernal/core/run/parallel/orchestrator.gointernal/core/run/parallel/orchestrator_test.gointernal/core/worktree/snapshot.gointernal/core/worktree/snapshot_test.gointernal/daemon/purge_test.gointernal/daemon/review_watch_git.gointernal/daemon/review_watch_git_test.gointernal/daemon/shutdown.gointernal/daemon/task_multi.gointernal/daemon/task_multi_worktree.gointernal/daemon/task_multi_worktree_test.gointernal/daemon/worktree_purge.go
💤 Files with no reviewable changes (1)
- internal/daemon/task_multi.go
There was a problem hiding this comment.
🧹 Nitpick comments (2)
internal/core/gitenv/gitenv_test.go (1)
1-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing test coverage for
Run().
SanitizedEnv,IsRepositoryEnvName, andCommandare all tested, butRun()— which contains the stderr/stdout fallback and error-wrapping logic reused byreview_watch_git.goandtask_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 winAdd 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
errwith no indication of which git command/dir failed, unlike the message branch which is well wrapped. As per coding guidelines, prefer wrapped context viafmt.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
⛔ Files ignored due to path filters (1)
skeeper.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
internal/cli/daemon_commands.gointernal/core/gitenv/gitenv.gointernal/core/gitenv/gitenv_test.gointernal/core/run/parallel/orchestrator_test.gointernal/core/worktree/snapshot.gointernal/daemon/review_watch_git.gointernal/daemon/task_multi_worktree.gointernal/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
There was a problem hiding this comment.
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 winTable 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 usingt.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 winRedact git args before formatting this error.
review_watch_git.Pushpassesoptions.PushRemoteintogitenv.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 valueIndependent
TestRunsubtests could uset.Parallel().Each subtest allocates its own
t.TempDir()and doesn't share mutable state, so they're good candidates fort.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 valueRebuild the display command from
cmd.Argsinstead of re-joiningargs.
cmd.Argsalready holds["git", args...]; manually re-deriving it via"git" + strings.Join(args, " ")duplicates that data and could drift ifCommand'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
⛔ Files ignored due to path filters (1)
skeeper.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
internal/core/gitenv/gitenv.gointernal/core/gitenv/gitenv_test.go
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]>
Summary by CodeRabbit
--parallel-tasks/--multiple --parallel, rejecting detached HEAD and preventing parallel runs inside managed worktrees before daemon contact.