Skip to content

feat: cross-repo worktree cleanup with squash-merge detection#2616

Merged
marcusquinn merged 1 commit intomainfrom
feature/cross-repo-worktree-cleanup
Feb 28, 2026
Merged

feat: cross-repo worktree cleanup with squash-merge detection#2616
marcusquinn merged 1 commit intomainfrom
feature/cross-repo-worktree-cleanup

Conversation

@marcusquinn
Copy link
Owner

@marcusquinn marcusquinn commented Feb 28, 2026

Summary

  • pulse-wrapper.sh: cleanup_worktrees() now iterates ALL repos from repos.json (skipping local_only), not just the aidevops repo
  • worktree-helper.sh: cmd_clean() gains --force-merged flag that detects squash merges via gh pr list and force-removes dirty worktrees when the PR is confirmed merged
  • Cleans node_modules/.next/.turbo before git worktree remove to avoid multi-minute deletions

Root Cause

Worktrees accumulated indefinitely (97 in aidevops, 58 in webapp) due to four compounding failures:

  1. Single-repo cleanup: cleanup_worktrees() only ran against the aidevops repo — never webapp or other managed repos
  2. Squash-merge blindness: git branch --merged only detects traditional merges. GitHub squash merges create a new commit that is NOT an ancestor of the original branch, so they were invisible to cleanup
  3. Over-conservative dirty check: Dirty worktrees were always skipped, even when the PR was confirmed merged. Workers frequently leave untracked files (task briefs, build artifacts) — this is abandoned WIP, not valuable work
  4. No heavy-dir cleanup: Node.js monorepo worktrees have node_modules with 100k+ files, making git worktree remove take minutes. No pre-cleanup of these directories was done

Testing

  • ShellCheck clean on both files (only SC1091 external source info on worktree-helper, expected)
  • Manual verification: cleaned 97 aidevops worktrees and 58 webapp worktrees in this session using the same logic

Summary by CodeRabbit

  • New Features

    • Extended cleanup functionality to work across multiple repositories simultaneously
    • Improved detection and handling of squash-merged pull requests
    • Added enhanced cleanup of temporary build directories before removal
  • Improvements

    • Increased visibility with per-repository cleanup result logging and aggregated totals
    • Strengthened safety checks to prevent unintended worktree operations

Root cause: worktrees accumulated indefinitely because:
1. pulse-wrapper only cleaned aidevops repo, not other managed repos
2. cmd_clean missed squash-merged PRs (git branch --merged only detects
   traditional merges, not squash merges)
3. Dirty worktrees were always skipped, even when the PR was confirmed
   merged (dirty state = abandoned WIP from workers)
4. No cleanup of node_modules/.next/.turbo before removal, causing
   git worktree remove to take minutes per worktree

Fix:
- pulse-wrapper.sh: cleanup_worktrees() now iterates all repos from
  repos.json (skipping local_only), running worktree-helper in each
- worktree-helper.sh: cmd_clean() gains --force-merged flag that:
  - Uses gh pr list to detect squash-merged PRs
  - Force-removes dirty worktrees when PR is confirmed merged
  - Cleans node_modules/.next/.turbo before git worktree remove

Evidence: aidevops had 97 stale worktrees, awardsapp had 58. Both repos
use squash merge. The old cleanup only ran against aidevops and missed
all squash-merged branches.
@marcusquinn marcusquinn merged commit 7eb29d5 into main Feb 28, 2026
13 of 15 checks passed
@github-actions github-actions bot added the enhancement Auto-created from TODO.md tag label Feb 28, 2026
@marcusquinn marcusquinn deleted the feature/cross-repo-worktree-cleanup branch February 28, 2026 18:11
@gemini-code-assist
Copy link

gemini-code-assist bot commented Feb 28, 2026

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the worktree management system by addressing long-standing issues that led to worktree accumulation. It broadens the scope of cleanup operations to cover all managed repositories, introduces robust detection for GitHub squash merges, and refines the handling of dirty worktrees to ensure that abandoned development branches are properly removed. These changes aim to maintain a cleaner development environment and prevent disk space wastage by streamlining the worktree lifecycle.

Highlights

  • Cross-Repository Worktree Cleanup: The cleanup_worktrees() function in pulse-wrapper.sh was enhanced to iterate through all repositories listed in repos.json (excluding local_only ones), ensuring comprehensive worktree cleanup across the entire managed environment.
  • Squash-Merge Detection and Force Removal: A new --force-merged flag was added to worktree-helper.sh clean. This flag enables detection of GitHub squash merges using gh pr list and allows for the force-removal of dirty worktrees when their associated PRs are confirmed merged, treating dirty state as abandoned work-in-progress.
  • Optimized Worktree Removal: The worktree-helper.sh now performs a pre-cleanup of large directories like node_modules, .next, and .turbo within a worktree before attempting git worktree remove, significantly speeding up the removal process for Node.js monorepo worktrees.
Changelog
  • .agents/scripts/pulse-wrapper.sh
    • Updated cleanup_worktrees function to iterate over all repositories defined in repos.json (excluding local_only entries) instead of just the current repository.
    • Modified cleanup_worktrees to call worktree-helper.sh clean with the new --force-merged flag.
    • Added logging for the number of worktrees removed per repository.
    • Implemented a fallback mechanism to clean only the current repository if repos.json or jq is not available.
  • .agents/scripts/worktree-helper.sh
    • Added a new --force-merged option to the cmd_clean function.
    • Implemented logic to detect squash-merged PRs using gh pr list when the --force-merged flag is active.
    • Modified the dirty worktree check: if --force-merged is enabled and a PR is confirmed merged, dirty worktrees are now force-removed; otherwise, they are skipped.
    • Introduced pre-removal cleanup for node_modules, .next, and .turbo directories within a worktree to accelerate the git worktree remove operation.
    • Updated the help message to include documentation for the new --force-merged option.
Activity
  • Manual verification was performed, successfully cleaning 97 aidevops worktrees and 58 webapp worktrees using the new logic.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1169411 and 578b257.

📒 Files selected for processing (2)
  • .agents/scripts/pulse-wrapper.sh
  • .agents/scripts/worktree-helper.sh

Walkthrough

Enhanced worktree cleanup across multiple repositories by introducing --force-merged flag for handling dirty worktrees detected as merged via git or GitHub PR detection. Multi-repo orchestration in pulse-wrapper.sh iterates over repos.json with fallback to legacy single-repo cleanup behavior when dependencies unavailable.

Changes

Cohort / File(s) Summary
Worktree Cleanup Enhancement
.agents/scripts/pulse-wrapper.sh, .agents/scripts/worktree-helper.sh
Multi-repo cleanup iteration with --force-merged flag support; added merged PR detection via gh pr list to identify squash-merged branches; enhanced dirty-state handling with force-removal semantics; expanded pre-removal cleanup to strip heavy directories (node_modules, .next, .turbo) and runtime files; fallback to legacy clean --auto when repos.json unavailable or jq missing; added safety checks for git paths and worktree count; improved logging visibility with per-repo and aggregated removal counts.

Sequence Diagram

sequenceDiagram
    participant Pulse as pulse-wrapper.sh
    participant Repos as repos.json
    participant Helper as worktree-helper.sh
    participant Git as git
    participant GH as gh CLI
    participant FS as File System

    Pulse->>Repos: Load repos list (with fallback)
    Pulse->>Pulse: Filter valid git paths
    
    loop For each repository
        Pulse->>Helper: cmd_clean --force-merged
        Helper->>Git: List merged branches
        Helper->>GH: gh pr list (detect squash-merged)
        GH-->>Helper: Merged PR heads
        
        Helper->>Helper: Identify merged worktrees
        Helper->>Git: Check for uncommitted changes
        
        alt Merged && Dirty && Force-Merged
            Helper->>FS: Remove node_modules/.next/.turbo
            Helper->>FS: Remove aidesvops runtime
            Helper->>Git: git worktree remove --force
        else Merged && !Dirty
            Helper->>Git: git worktree remove
        else Not Merged
            Helper->>Helper: Skip worktree
        end
        
        Helper->>Git: Branch deletion
        Helper-->>Pulse: Removal count
    end
    
    Pulse->>Pulse: Aggregate total removals
    Pulse-->>Pulse: Log results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🔧 Worktrees once scattered, now unified and wise,
Force-merged detection sees through squash's disguise,
Heavy dirs collapse, dirty branches comply,
Multi-repo cleanup soars—no orphans left behind! ✨

✨ 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/cross-repo-worktree-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 106 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sat Feb 28 18:12:00 UTC 2026: Code review monitoring started
Sat Feb 28 18:12:01 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 106

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 106
  • VULNERABILITIES: 0

Generated on: Sat Feb 28 18:12:03 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@sonarqubecloud
Copy link

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request enhances the worktree cleanup process to operate across multiple repositories and to correctly identify squash-merged branches. However, it introduces a high-severity issue where build artifacts in the main worktree can be unintentionally deleted if its current branch is merged. A medium-severity regex injection vulnerability in the branch matching logic is also identified, with a comprehensive suggestion provided to address it, improving both security and performance. Additionally, the review suggests improving debuggability by removing several instances of 2>/dev/null suppression, aligning with repository rules.

Comment on lines +843 to +845
rm -rf "$worktree_path/node_modules" 2>/dev/null || true
rm -rf "$worktree_path/.next" 2>/dev/null || true
rm -rf "$worktree_path/.turbo" 2>/dev/null || true

Choose a reason for hiding this comment

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

security-high high

The cmd_clean function iterates over all worktrees returned by git worktree list. It does not distinguish between the main worktree and linked worktrees. If the main worktree is currently on a branch that has been merged (and is not the default branch), the script will proceed to "clean up" heavy directories like node_modules, .next, and .turbo using rm -rf before attempting to remove the worktree. While git worktree remove will fail for the main worktree, the rm -rf commands will succeed, leading to the loss of build artifacts and dependencies in the main repository.

Remediation: The script should identify and skip the main worktree. This can be done by checking if the worktree path is the same as the base repository path or by skipping the first entry in the git worktree list --porcelain output.

# GitHub squash merges create a new commit — the original branch is NOT
# an ancestor of the target, so git branch --merged misses it. The remote
# branch may still exist if "auto-delete head branches" is off.
elif [[ -n "$merged_pr_branches" ]] && echo "$merged_pr_branches" | grep -qx "$worktree_branch"; then

Choose a reason for hiding this comment

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

security-medium medium

This line has two issues:

  1. Regex Injection Vulnerability (Medium Severity): The grep -qx "$worktree_branch" command treats the branch name as a regular expression, which can lead to unintended matches and potential deletion of unrelated worktrees if the branch name contains regex metacharacters.
  2. Performance Issue: Using grep inside a loop is inefficient.

To address both, it's recommended to use grep -Fqx to treat the branch name as a literal string, and for performance, consider loading merged branch names into a bash associative array for O(1) lookup. The suggested code below uses an associative array lookup, which also removes 2>/dev/null from the gh command, improving debuggability.

Suggested change
elif [[ -n "$merged_pr_branches" ]] && echo "$merged_pr_branches" | grep -qx "$worktree_branch"; then
elif [[ -v "merged_pr_lookup[$worktree_branch]" ]]; then

if [[ -f "$repos_json" ]] && command -v jq &>/dev/null; then
# Iterate all repos, skip local_only (no GitHub remote for PR detection)
local repo_paths
repo_paths=$(jq -r '.[] | select(.local_only != true) | .path' "$repos_json" 2>/dev/null || echo "")

Choose a reason for hiding this comment

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

medium

Avoid redirecting stderr to /dev/null. If repos.json is malformed or jq fails for another reason, the error message will be suppressed, making debugging difficult. The || echo "" already provides a safe fallback if the command fails, so stderr can be allowed to print for better diagnostics.

Suggested change
repo_paths=$(jq -r '.[] | select(.local_only != true) | .path' "$repos_json" 2>/dev/null || echo "")
repo_paths=$(jq -r '.[] | select(.local_only != true) | .path' "$repos_json" || echo "")
References
  1. When reporting errors for failed file operations in shell scripts, such as 'jq' writes, include the file path in the error message. Avoid suppressing stderr with '2>/dev/null' to ensure that diagnostic information about malformed files or write failures is visible.

[[ ! -d "$repo_path/.git" ]] && continue

local cleaned_output
cleaned_output=$(git -C "$repo_path" worktree list 2>/dev/null | wc -l)

Choose a reason for hiding this comment

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

medium

Avoid redirecting stderr to /dev/null. While a check for the .git directory exists, the git command could still fail for other reasons (e.g., a corrupted repository). Suppressing stderr hides these potentially important errors, hindering debugging.

Suggested change
cleaned_output=$(git -C "$repo_path" worktree list 2>/dev/null | wc -l)
cleaned_output=$(git -C "$repo_path" worktree list | wc -l)
References
  1. When using git commands (like 'init', 'remote') in shell scripts, use the '-q' flag to suppress standard output instead of '2>/dev/null', ensuring that error messages on stderr remain visible for debugging.

@@ -698,6 +703,13 @@ cmd_clean() {
# Fetch to get current remote branch state (detects deleted branches)
git fetch --prune origin 2>/dev/null || true

Choose a reason for hiding this comment

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

medium

Avoid redirecting stderr to /dev/null. If git fetch fails (e.g., due to network or authentication issues), the error message will be suppressed, making debugging difficult. The || true already prevents the script from exiting on failure, so it's safe to let error messages be displayed.

Suggested change
git fetch --prune origin 2>/dev/null || true
git fetch --prune origin || true
References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

remove_flag="--force"
fi
# shellcheck disable=SC2086
if ! git worktree remove $remove_flag "$worktree_path" 2>/dev/null; then

Choose a reason for hiding this comment

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

medium

Avoid redirecting stderr to /dev/null. The error message from git worktree remove can be more informative than the custom message, especially if the failure is due to something other than uncommitted changes (e.g., permissions). Allowing the original error to be displayed will aid in debugging.

Suggested change
if ! git worktree remove $remove_flag "$worktree_path" 2>/dev/null; then
if ! git worktree remove $remove_flag "$worktree_path"; then
References
  1. Avoid using '2>/dev/null' for blanket suppression of command errors in shell scripts to ensure that authentication, syntax, or system issues remain visible for debugging.

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

Labels

enhancement Auto-created from TODO.md tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant