consolidate-git · diff
v1.0.0 to v1.0.0
79 added, 262 removed. Audit A to A.
---
name: consolidate-git
- description-budget: 250
- description: "Cleans up branch sprawl. Triggers: consolidate branches, clean up branches, stale branches, merge all branches."
+ description: "Cleans up branch sprawl — consolidates and prunes stale branches."
version: 1.0.0
---
# Consolidate Git — Branch + Worktree Cleanup
## Overview
- Reduce branch and worktree sprawl to a single clean workstream branch. Inventories all local and remote branches AND all worktrees, absorbs any unique commits into the current branch, deletes stale branches, and removes stale worktrees (including locked ones whose work has been absorbed).
-
- **This skill does NOT merge to main by default.** Consolidation and shipping are separate decisions. The goal here is to leave the repo with one current branch holding all of *my* in-flight work, no leftover sibling branches, no stale worktrees. Whether that branch is ready to ship to main is a separate judgment for `/merge-to-main`, made after this skill reports. At the end, the EM MAY ask the PM whether to chain into `/merge-to-main` if the consolidated branch looks merge-ready — but the default exit is "consolidation complete, branch is yours."
-
- **Worktrees are not exempt.** A worktree whose branch tip is already absorbed into main (or the current branch) is *stale state*, not active work — the branch lock is a stale signal, not a permanent reservation. The skill removes such worktrees by default. Worktrees with genuine unique commits proceed through the same absorb-or-skip evidence gate as any other branch.
-
- **Announce at start:** "I'm using the coordinator:consolidate-git skill to consolidate branches and worktrees into the current branch. Merge to main is a separate step."
-
- ## The Process
-
- ### Step 1: Inventory Branches
-
- List all local and remote branches, determine ownership, and categorize.
-
- **1a. Identify the current user:**
-
- ```bash
- MY_EMAIL=$(git config user.email)
- ```
-
- **1b. List all branches with tip author:**
-
- ```bash
- git branch -a
- ```
-
- For each branch (excluding main and the current branch), check the author of the most recent commit on the tip:
-
- ```bash
- # Local branch:
- git log -1 --format='%ae' <branch>
-
- # Remote-only branch:
- git log -1 --format='%ae' origin/<branch>
- ```
-
- **1c. Categorize each branch:**
-
- | Category | Definition | Action |
- |----------|-----------|--------|
- | **current** | The checked-out branch | Absorb target — everything merges here |
- | **main** | The trunk branch | Merge target — current branch merges here at the end |
- | **mine (stale)** | Tip author matches `$MY_EMAIL` | Check for unique commits, absorb if any, then delete |
- | **other's** | Tip author does NOT match `$MY_EMAIL` | **Leave untouched** |
-
- **1d. Present the inventory to the PM as a table:**
-
- ```
- | Branch | Local | Remote | Owner | Category |
- |--------|-------|--------|-------|----------|
- | main | yes | yes | — | trunk |
- | work/machine-a/2026-03-20 | yes | yes | me | mine (stale) |
- | feature/foo | no | yes | me | mine (stale) |
- | feature/bar | no | yes | alice@co.com | other's — skipped |
- | work/machine-a/2026-03-23 | yes (current) | yes | me | current |
- ```
-
- **Only branches categorized as "mine (stale)" proceed to Steps 2–5.** Other people's branches are reported but never touched.
-
- ### Step 1.5: Inventory Worktrees
-
- Enumerate all worktrees and join against the branch inventory. A worktree is just another checkout of a branch — its branch is subject to the same absorb-or-skip logic as any other branch. The only special handling: the worktree directory itself must be removed before the branch can be deleted.
-
- ```bash
- git worktree list --porcelain
- ```
-
- For each worktree (excluding the primary `.git`-owning checkout):
-
- 1. Record the worktree path, branch, HEAD sha, and lock state (the `locked` line in porcelain output).
- 2. Treat the branch the same way as any local branch in Step 1c — owner check by tip-commit author, category by reachability:
- - Branch tip reachable from `origin/main` OR from the current branch → **stale (worktree absorbed)** → candidate for removal.
- - Branch tip has unique commits → **stale (worktree has unique work)** → goes through Steps 2–4 like any other branch.
- - Tip author is not me → **other's worktree — skipped** (report only).
-
- Add a Worktrees section to the inventory table:
-
- ```
- ### Worktrees
- | Path | Branch | Locked | Owner | Category |
- |------|--------|--------|-------|----------|
- | .claude/worktrees/agent-a07… | worktree-agent-a07… | yes | me | stale (absorbed into main) |
- | .claude/worktrees/agent-aec… | worktree-agent-aec… | yes | me | stale (absorbed into main) |
- ```
-
- **Locked is not a veto.** A lock from a long-finished isolation run is exactly the state this skill exists to clean up. If the branch is absorbed, the worktree gets removed in Step 5 with `--force` after the lock is cleared (`git worktree unlock <path>`). If the worktree has uncommitted changes (`git -C <path> status --porcelain` non-empty), surface them to the PM before removing — that is genuine in-flight work and warrants pause.
-
- ### Step 2: Check for Unique Commits
-
- For each stale branch, check if it has commits not in the current branch:
-
- ```bash
- # For local branches:
- git log --oneline <current-branch>..<stale-branch>
-
- # For remote-only branches:
- git log --oneline <current-branch>..origin/<stale-branch>
- ```
-
- Categorize the result:
- - **No unique commits** — safe to delete immediately
- - **Has unique commits** — proceeds to Step 3 (inspection), then Step 4 (verdict)
-
- **Do NOT label a branch "superseded," "stale-experiment," "already-applied," or any other verdict at this step.** The only categories Step 2 emits are *no unique commits* and *has unique commits — count N*. Anything else is a Step 4 verdict and requires Step 3 evidence first.
-
- ### Step 3: Inspect Unique Commits (Evidence Gate)
-
- **Hard gate: no verdict may be emitted before this step's output is on the page.**
-
- For each branch flagged "has unique commits" in Step 2, run BOTH:
-
- ```bash
- git log --oneline <current-branch>..<stale-branch-or-origin/...>
- git show --stat <each-commit-sha>
- ```
-
- Then emit an inspection table to the PM **before** proposing any absorb/skip decision:
-
- ```
- ### Inspection: <branch-name> (<N> unique commits)
- - <sha> <subject> — files: <path1>, <path2> (+X/-Y)
- - <sha> <subject> — files: <path1> (+X/-Y)
- ```
-
- For commits whose diff is non-trivial (>~20 lines or touches shared infra / plugin internals / configs), also run `git show <sha>` and quote the load-bearing hunks in 1–3 lines.
-
- **Self-check before moving to Step 4 — answer each in one sentence, in the message:**
- 1. Have I actually run `git show --stat` (or `git show`) on every listed sha? (If no: stop, run it.)
- 2. For each commit, what file(s) on the current branch supersede it, named by path? (If "current branch has a newer version" without a path, that's a guess, not evidence.)
- 3. Are any of these commits touching files the current branch never touched? (If yes: not superseded — must absorb.)
-
- A "superseded" verdict that names no superseding path on the current branch is a doctrine violation — go back and look.
-
- ### Step 4: Absorb or Skip (Verdict)
-
- Only after Step 3's inspection table and self-check are on the page, for each branch:
-
- 1. **Choose absorption strategy (inline override required on each git op that switches branches):**
- - **Cherry-pick** (default for 1-3 commits):
- ```bash
- # Inline override required: cherry-pick involves switching from the active work branch;
- # COORDINATOR_OVERRIDE_BRANCH=1 is still required even after block-off-daily-branch.sh retirement.
- COORDINATOR_OVERRIDE_BRANCH=1 COORDINATOR_OVERRIDE_BRANCH_REASON="consolidate-git step 3 cherry-pick from <stale-branch>" \
- git cherry-pick <commit> --no-edit
- ```
- - **Merge** (for branches with many commits):
- ```bash
- COORDINATOR_OVERRIDE_BRANCH=1 COORDINATOR_OVERRIDE_BRANCH_REASON="consolidate-git step 3 merge <stale-branch>" \
- git merge <stale-branch> --no-edit
- ```
- 3. **If conflicts arise:**
- - Inspect the conflicting files — determine if the current branch already supersedes the change
- - If superseded: abort (`git cherry-pick --abort` or `git merge --abort`) and skip — note this in the report
- - If genuinely needed: resolve conflicts, then continue
- - Do NOT force through conflicts blindly — each conflict is a signal that needs inspection
-
- **Report each absorption decision to the PM:**
- > "Branch `work/machine-a/2026-03-20` has 2 unique commits — both are experiment data snapshots already superseded by current branch. Skipping."
-
- or:
-
- > "Branch `feature/auth-rewrite` has 5 unique commits with real code changes. Cherry-picking into current branch."
-
- ### Step 5: Delete Stale Branches and Worktrees
-
- After all unique commits are absorbed (or explicitly skipped), remove stale worktrees first (a branch checked out in a worktree cannot be `git branch -d`'d), then delete the branches.
-
- **5a. Remove stale worktrees:**
-
- For each worktree categorized as stale in Step 1.5 (and not flagged as having uncommitted changes the PM elected to preserve):
-
- ```bash
- # Clear the lock if present — the lock is leftover isolation-mode state, not a live reservation.
- git worktree unlock <path> 2>/dev/null || true
-
- # Remove the worktree. --force is needed because locked/checked-out worktrees
- # refuse plain `git worktree remove`. Safe here: we already verified the branch
- # tip is reachable from main / current and the working tree is clean (or PM-approved).
- git worktree remove --force <path>
- ```
-
- After removing all stale worktrees, prune any administrative remnants:
-
- ```bash
- git worktree prune
- ```
-
- **5b. Delete branches:**
-
- ```bash
- # Local branches — use safe delete (-d), not force delete (-D)
- git branch -d <branch>
-
- # Remote branches — batch deletions into one push
- git push origin --delete <branch1> <branch2> <branch3>
- ```
-
- **Use `-d` (safe delete), not `-D`.** Safe delete will refuse if the branch has unmerged commits — this is a final safety net. If `-d` refuses, investigate before escalating to `-D` with PM approval.
-
- After deletion, prune stale remote tracking refs:
-
- ```bash
- git fetch --prune
- ```
-
- ### Step 6: Post-Absorb Re-Verify Shared Infra (example-repo T1.7)
-
- When conflicts were resolved during cherry-pick or merge in Step 4 — especially on shared files (plugin internals, shared scripts, configs) — re-verify that the absorbed changes actually survived.
-
- **Why this matters:** Last-writer-wins silently reverts edits when both sides touched the same hunk and the conflict was resolved naively. An absorption that "succeeded" may have silently dropped changes from the source branch.
+ Reduce branch and worktree sprawl to a single clean workstream branch. The assembler inventories
+ every local and remote branch AND every worktree, computes ownership, unique-commit evidence,
+ and the absorb/delete sequencing, and returns one decision object. An unconditional directive in
+ that object (no-unique-commit deletes, clean-worktree removal, prune, fetch-prune) executes as
+ soon as you reach it. What follows is the judgment residue the assembler cannot resolve for you:
+ supersession verdicts on branches with unique commits, conflict resolution, the locked/dirty
+ worktree pause, and the merge-ready call.
- **Verification steps:**
+ **Consolidation and shipping are separate decisions — this skill never merges to main.** It
+ leaves the repo with one current branch holding all of *my* in-flight work, no leftover sibling
+ branches, no stale worktrees. Whether that branch is ready to ship is `/merge-to-main`'s call,
+ made after this skill reports and optionally offered via the assembler's `merge-ready` judgment
+ point.
- 1. For each file with a known specific change:
- ```bash
- git show HEAD:<file-path> | grep -F "<canonical phrase from your change>"
- ```
- 2. If a canonical phrase is missing, your change was overwritten. Re-apply it with a follow-up commit.
- 3. This is especially important for `~/.claude/` plugin files and project-wide config files touched by multiple branches in the consolidation.
+ **Announce at start:** "I'm using the coordinator:consolidate-git skill to consolidate branches
+ and worktrees into the current branch. Merge to main is a separate step."
- ### Step 7: Report
+ ## Run the Assembler
- ```
- ## Branch Consolidation Complete
+ `"${COORDINATOR_SETTINGS_HOME:-$HOME/.coordinator-claude-settings}/bin/consolidate-assemble" brief`
+ computes the eight-key decision object: current-user + branch inventory (ownership category per
+ branch and worktree), unique-commit evidence per stale branch, absorb/delete directives, and the
+ judgment points below. It is READ-ONLY — every mutating action surfaces as a directive or a
+ judgment point, never fires silently.
- ### Absorbed
- - `work/machine-a/2026-03-20` — no unique commits (already merged)
- - `feature/foo` — 3 commits cherry-picked into current branch
+ **Worktrees are not exempt.** A worktree whose branch tip is already reachable from main or the
+ current branch is stale state, not active work — a lock from a long-finished isolation run is a
+ stale signal, not a veto, and the assembler proposes its removal like any other stale branch. A
+ worktree with uncommitted changes is a genuine pause, not an auto-fire.
- ### Skipped (superseded)
- - `work/machine-a/2026-03-19` — 1 commit (stale experiment data, current branch has newer version)
+ **Only branches/worktrees owned by the current git identity are candidates.** Everyone else's
+ are reported in the decision object's `gates` but never touched.
- ### Deleted
- - Local: work/machine-a/2026-03-20, work/machine-a/2026-03-19
- - Remote: origin/work/machine-a/2026-03-20, origin/work/machine-a/2026-03-19, origin/feature/foo
- - Worktrees: .claude/worktrees/agent-a07… (branch absorbed into main), .claude/worktrees/agent-aec… (branch absorbed into main)
+ ## Resolve the Judgment Points
- ### Left Untouched (other owners)
- - `feature/bar` (alice@co.com) — not ours, skipped
+ Each judgment point below carries its own evidence and per-option guidance in the decision
+ object — decide from it, never invent a verdict the evidence doesn't support.
- ### Current State
- - On branch: `work/machine-a/2026-03-23` @ {sha}
- - Ahead of `origin/main` by N commits — not merged (consolidation only)
- - All of *my* sibling branches and worktrees absorbed or removed; only main + other owners' branches remain
- ```
+ **`j-absorb-<branch>` — supersession verdict.** The assembler hands you the unique-commit list
+ plus a `git show --stat` per commit; it never labels a commit "superseded" itself. Before
+ choosing **skip**, name the file(s) on the current branch that supersede each commit — "current
+ branch has a newer version" without a path is a guess, not evidence, and any commit touching a
+ file the current branch never touched must be absorbed, not skipped. Choosing **absorb** applies
+ the assembler's own cherry-pick/merge selection (cherry-pick for small counts, merge for large)
+ via the closed CLI table — you are not hand-choosing the git verb.
- ### Step 8: Optional Merge-to-Main Prompt (EM Judgment)
+ **Conflict resolution (surfaces mid-directive-apply, not as a separate judgment point).**
+ Inspect conflicting files — if the current branch already supersedes the change, abort and skip,
+ noting it in the report; if genuinely needed, resolve and continue. Do not force through
+ conflicts blindly — each one is a signal that needs inspection, not a merge-strategy retry.
- After reporting, the EM MAY ask the PM whether to chain into `/merge-to-main` — but only when the consolidated branch looks merge-ready. Default exit is to stop here.
+ **Post-absorb re-verify on conflict-resolved shared infra.** Last-writer-wins silently reverts
+ edits when both sides touched the same hunk and the conflict was resolved naively. For any file
+ with a known specific change that went through conflict resolution, confirm the canonical phrase
+ from that change is still present in the result; if missing, re-apply it with a follow-up commit.
+ Weight this check toward shared files — plugin internals, shared scripts, project-wide configs —
+ touched by multiple branches in the consolidation.
- Skip the prompt when:
- - The current branch is already main (nothing to merge).
- - Consolidation produced unresolved skips, dirty worktrees flagged for PM review, or conflicts that warranted pause.
- - The branch is mid-workstream (recent commits suggest active development, not a ready checkpoint).
+ **`j-worktree-dirty-<path>`.** Worktrees are forbidden — an override exists but requires explicit
+ PM permission via the EM — so a dirty worktree found here is stray debris to drain and remove,
+ never state to keep. The question is never "keep or remove" — it's "how do we safely get rid of
+ this without destroying uncommitted work." Dirty worktrees are surfaced, never removed silently.
+ **rescue** saves the uncommitted state (stash it out or commit it to a throwaway branch) before
+ removing the worktree, when the changes look worth keeping; **proceed** discards the uncommitted
+ state along with the worktree, when it's genuinely disposable. Default posture is pause for a
+ decision, not silent proceed — a lock is stale-signal, dirt is not.
- Offer the prompt when:
- - Consolidated branch contains finished, reviewed work and would have been a `/merge-to-main` candidate before consolidation started.
- - The PM's framing for invoking consolidation suggested shipping intent.
+ **`j-behind-main`.** Fires only when the current branch is behind main. **merge-main-first**
+ ensures the final state includes everything before absorbing; **proceed-anyway** is the branch's
+ call if a stale main is expected.
- Phrase as a recommendation, not a question to ratify default behavior:
+ **`j-merge-ready`.** Fires whenever any directive or judgment point exists. Offer
+ **chain-to-merge** only when the branch genuinely looks ship-ready (finished, reviewed work,
+ consolidation didn't leave unresolved skips or PM-flagged dirty worktrees, framing suggested
+ shipping intent) — otherwise **stop-here** is the default. Never turn this into a question that
+ ratifies default behavior; phrase chain-to-merge as a recommendation with the evidence behind it.
- > "Consolidated branch looks merge-ready (N commits ahead of main, all absorbed work passes review). Want me to chain into `/merge-to-main`?"
+ ## Report
- Otherwise: stop. Merge to main is a separate invocation.
+ Summarize per the assembler's own `branches`/`worktrees` gates: absorbed, skipped (with the
+ superseding evidence named), deleted (branches + worktrees), and left untouched (other owners).
+ Close with current branch, ahead-of-main count, and the merge-ready disposition.
## Edge Cases
- **If on main with no other branches:** Abort early — nothing to consolidate.
-
- **If the current branch is behind main:** Merge main into the current branch first before absorbing other branches — ensures the final state includes everything.
-
- **If a stale branch has diverged significantly:** Prefer merge over cherry-pick. If the merge has extensive conflicts, flag to the PM rather than resolving silently — the PM may want to inspect before committing.
+ **On main with no other branches:** abort early — nothing to consolidate.
- **If remote branches have no local counterpart:** Fetch them first (`git fetch origin <branch>`) to inspect their commits, then delete the remote after inspection.
+ **Remote branches with no local counterpart:** fetch first to let the assembler inspect their
+ commits before it proposes the remote delete.
## What This Does NOT Do
- - **Merge to main** — consolidation lands everything on the current branch and stops. Shipping is `/merge-to-main`, invoked separately by the PM (optionally suggested by the EM in Step 8 when the branch looks merge-ready).
- - **Rebase** — merges and cherry-picks only. Rebasing rewrites history and adds risk for no benefit in a cleanup operation.
+ - **Merge to main.** Consolidation lands everything on the current branch and stops; shipping is
+ `/merge-to-main`, invoked separately by the PM (optionally recommended via `j-merge-ready`).
+ - **Rebase** — merges and cherry-picks only. Rebasing rewrites history for no benefit in a
+ cleanup operation.
- **Touch other repos** — scoped to the current repository only.
- - **Delete main** — main is always preserved as the merge target.
- - **Force-delete branches** — uses `-d` (safe) by default. `-D` only with explicit PM approval.
- - **Touch other people's branches or worktrees** — only branches/worktrees where the tip commit author matches the current user's `git config user.email` are candidates. Everyone else's are reported but never modified or deleted.
- - **Remove worktrees with uncommitted changes silently** — dirty worktrees are surfaced to the PM. The default for stale-but-clean (including locked) is removal; the default for dirty is pause.
+ - **Delete main** — always preserved as the merge target.
+ - **Force-delete branches** — safe delete (`-d`) only; `-D` needs explicit PM approval if `-d`
+ refuses.
+ - **Touch other people's branches or worktrees** — ownership is tip-commit-author-scoped; every
+ non-owned branch/worktree is reported, never modified.