Task: Add compass command to prune merged git branches and report branch/environment usage

Table of Contents

This page documents a task in the Compass quality-of-life improvements: remaining follow-ups story. It captures the goal, current status, acceptance, and any notes or results.

Goal

Give developers a single compass command to safely prune git branches that have already been merged into origin/main – both local and remote – without risking deletion of a branch another worktree/environment is actively using, and to surface a report of branch staleness and fleet ownership so cleanup decisions (or further manual deletions) can be made confidently.

Status

Field Value
State DONE
Parent story Compass quality-of-life improvements: remaining follow-ups
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-07-29

Acceptance

  • A new compass command (git/branches subcommand) identifies local branches fully merged into origin/main and remote branches fully merged into origin/main, excluding the current branch, main itself, and any branch checked out by another worktree (cross-referenced via 'compass fleet' / 'git worktree list' equivalent).
  • The command deletes the safe-to-delete set both locally ('git branch -d') and on the remote ('git push origin –delete'), in one step, with a summary of what was deleted.
  • The command (or a companion report mode) lists branches NOT deleted, bucketed by reason: checked out by another environment (name the environment/worktree), unmerged/active work, or ambiguous/needs-human-review.
  • The report flags 'stale' branches – e.g. no commits in N days, or merged-but-still-present-in-fleet – so a human can decide on cases the automatic prune correctly leaves alone (e.g. a worktree still parked on an already-merged branch after finishing its task).
  • Documented in the relevant recipe (doc/recipes/compass/…) with example output.
  • Manually exercised against the current live branch set (the same one audited in this session: ~28 local, ~28 remote branches) and confirmed it reaches the same safe-delete conclusions reached manually (6 local + 6 remote merged branches deleted, feature/write-up-wire-format-architecture and the then-current feature branch correctly left alone).

Plan

Added a new self-contained compass branches command (src/compass_branches.py, dispatched from compass.py alongside the other lazily-imported command modules like compass_services):

  • compass branches / compass branches report — read-only report. git fetch --prune origin, then classify every local (git for-each-ref refs/heads) and remote (git for-each-ref refs/remotes/origin, stripping origin/ and skipping the origin/HEAD symref) branch against git branch --merged origin/main / git branch -r --merged origin/main, and against the live fleet (git worktree list --porcelain, same source compass fleet uses). Buckets: safe to delete, or kept with a reason (current, main, fleet:<worktree>, unmerged).
  • compass branches prune [-y] — dry-run by default (prints what would be deleted); with -y, deletes the safe set local (git branch -d, not -D, so unmerged-commit branches are refused even if the --merged computation were ever wrong) then remote (git push origin --delete), then reprints what's left alone and why.

Manually exercised against the live 43-branch set (see Result) — matched the manual workflow's expected outcome.

Notes

Reference session: manual steps performed 2026-07-29

The session that captured this task ran the following sequence by hand, in the clever_dijkstra worktree, right after merging PR #1719. This is the workflow to formalise into a compass command.

  1. Fetch and prune remote-tracking refs first, so deleted remotes don't show up as phantom candidates:

    git fetch --prune origin
    
  2. List local branches with tracking/verbose info, and remote branches, to see the full candidate set:

    git branch -vv
    git branch -r
    

    On this run: ~28 local branches, ~28 remote branches. Local branches checked out by other worktrees are marked with a leading + by git branch -vv (not deletable from this worktree without first switching that worktree off it); the worktree's path is printed after the commit hash, e.g. (/mnt/development/OreStudio/ores_dev_jolly_knuth).

  3. Compute the merged set against origin/main (not local main, which can be stale in a worktree that isn't actively tracking it):

    git branch --merged origin/main
    git branch -r --merged origin/main
    

    --merged against local main under-reports if that worktree's local main is behind (seen here: local main was 9 commits behind origin/main, tracked in a different worktree entirely).

  4. Cross-reference the merged set against the live fleet before deleting anything:

    compass fleet
    

    Exclude from deletion:

    • the current branch (can't delete the branch you're on),
    • main itself,
    • any branch git branch -vv marks with + (checked out in another worktree) or that compass fleet shows as another environment's active BRANCH column,
    • any branch merged into origin/main but still parked as another environment's current branch even though its task/PR already closed (e.g. feature/write-up-wire-format-architecture was fully merged but still checked out by solid_dirac – correctly left alone rather than force-cleaned, since another session may still be using that worktree state).
  5. Delete the safe set, local then remote, as two explicit batches (not a loop with individual confirmations – one command per side listing every branch to delete):

    git branch -d <branch1> <branch2> ...
    git push origin --delete <branch1> <branch2> ...
    

    -d (not -D) deliberately – it refuses to delete a branch with unmerged commits, which is the safety net that matters if the --merged computation above was wrong for any reason.

  6. Report back to the user: what was deleted (local count + remote count, names), and what was deliberately left alone and why (bucketed: "checked out by another environment", "is the current branch", "is main", "unmerged/active work").

Concrete before/after from that session

  • Started with ~28 local / ~28 remote branches.
  • Deleted 6 local + 6 remote (one extra remote-only deletion, claude/windows-cxx-linking-hotfix-upn21a, which had no corresponding local branch to check but was independently confirmed via git branch -r --merged origin/main).
  • Left alone: feature/write-up-wire-format-architecture (merged, but checked out by solid_dirac), feature/codegen-qt-parent-scoped-list (was the current branch at the time; deleted locally in a later step once the worktree moved off it onto the next task's branch), main (checked out by the ores.git worktree), and every branch representing another environment's still-open, unmerged work.
  • This asymmetry – a branch fully merged into main but still the active branch of a live worktree – is exactly the "stale but fleet-owned" case the acceptance criteria call out; the command should report it, not silently delete or silently ignore it.

Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario, run through the QA Validation Runner panel) that verify this task. Link new ones here as they're created; the scenario doc itself links back via its "Verifies task" field.

Scenario State Notes
     

PRs

PR Title
#1866 [compass] Add compass branches: report/prune git branches merged into origin/main

Review

# Comment summary File Decision Notes
1 Batch git branch -d / push –delete return non-zero on partial failure but code treated whole batch as undeleted ("0 deleted") compass_branches.py Fixed Recompute deleted set from before/after local_branches()/remote_branches() diff instead of trusting the batch return code
1 fleet_active_branches doesn't check subprocess returncode, unlike the _git() helper used elsewhere compass_branches.py Fixed Added explicit returncode check, consistent with _git()
1 No dedicated unit tests for classify() compass_branches.py Declined Matches existing convention – no other compass_*.py command module has dedicated tests either; covered by manual exercise against the live branch set
1 fetch_prune failure is non-fatal for prune -y, could work off stale origin/main compass_branches.py Declined Judgment call flagged by reviewer, not a bug; git branch -d's own safety net (refuses unmerged branches) still applies even with a stale view
1 _MAIN_BRANCHES includes "master", dead weight since merge-base is hardcoded to origin/main compass_branches.py Declined Harmless; kept for defence-in-depth in case a checkout ever has a stray master branch

Result

Added compass branches (report/prune) and its recipe (How do I prune merged git branches with compass?). Manually exercised against the live branch set in this worktree: report correctly bucketed 42 safe-to-delete branches (41 local + 1 remote-only, bot/nightly-format) against 32 kept branches (9 fleet-owned across other worktrees, main local+remote, 21 unmerged/active); prune -y deleted exactly that safe set (git branch -d locally, git push origin --delete for the remote) and left every fleet-owned/unmerged/current/main branch untouched, matching the reference session's expected asymmetry (merged-but- fleet-owned branches are reported, not force-deleted). All acceptance criteria met.

Emacs 29.3 (Org mode 9.6.15)