# Cleanup

> Git Cleanup

- Skill: `roxabi/cleanup` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add roxabi/cleanup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/roxabi/cleanup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Roxabi (https://skillmd.com/u/roxabi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/roxabi/cleanup

---


# Git Cleanup

Let: β := branch | ω := worktree | π := open PR | Π := protected branch (main/master/staging) | safe(β) ⟺ fully_merged(β) ∧ ¬π(β) | merged(β) := regular_merge(β) ∨ squash_merge(β) | N := scope issue number (∅ if unscoped) | orphan_shell := leftover path under `~/.grok/worktrees/<slug>/` or `.claude/worktrees/` that is **not** in `git worktree list` (empty parent, unregistered content, or `worktrees.db` row with missing path) | principal := main checkout (must remain on Π — feature work lives only in ω; see harness-worktree.md)

**Stack:** Read `.dev/stack.yml` first — every `{field}` placeholder below resolves from it. ¬∃ → output: "`.dev/stack.yml` not found — run `/R-env-setup` to generate it." and stop.

Safely clean local β, ω, and remote branches with **mandatory merge-status verification** before any deletion. End-of-session sweep also strips stuck pipeline labels from closed PRs, cancels long-queued CI runs, and surfaces **orphan worktree shells** that git no longer tracks.

## Entry: parse $ARGUMENTS

At skill entry, before any other action:

```
REPORT_ONLY=false
YES=false
SCOPE=""
_next_is_scope=false
for arg in $ARGUMENTS; do
  if [ "$_next_is_scope" = true ]; then
    SCOPE="${arg#\#}"
    _next_is_scope=false
    continue
  fi
  case "$arg" in
    --report-only) REPORT_ONLY=true ;;
    --yes)         YES=true ;;
    --scope)       _next_is_scope=true ;;
    --scope=*)     SCOPE="${arg#--scope=}"; SCOPE="${SCOPE#\#}" ;;
  esac
done
```

`REPORT_ONLY=true` ⇒ **zero mutations** throughout all steps (cron-safe).  
`YES=true` ⇒ skip confirmation prompts for destructive actions (implies user already consented).  
If both set: `REPORT_ONLY` wins — no mutations.
`SCOPE=<N>` ⇒ restrict branch/worktree analysis (Steps 2–6) to issue N only — see below. `SCOPE=""` (default) ⇒ repo-wide, unchanged behavior.

## Instructions

### 1. Gather State

```bash
bash ${CLAUDE_SKILL_DIR}/gather-state.sh
```

Emits: `current`, branch list with tracking info, worktree list, **orphan worktree shells** (`---orphan-worktree-shells---` via `scan-orphan-worktree-shells.sh`), open PRs, closed PRs with pipeline labels, and queued/stuck CI runs. Unscoped — always full-repo; Steps 7–8 (label/CI sweeps) and the orphan-shell scan consume gather-state as-is regardless of `--scope` (see Options).

### 2. Analyze Branches

```bash
if [ -n "$SCOPE" ]; then
  bash ${CLAUDE_SKILL_DIR}/analyze-branches.sh --scope "$SCOPE"
else
  bash ${CLAUDE_SKILL_DIR}/analyze-branches.sh
fi
```

Use `--json` when you need structured output for scripting. Use `--no-fetch` only in tests or when origin was fetched immediately before.

**Scoping (`--scope <#N>`):** restricts local/remote branches and worktrees to the ones belonging to issue N, using the same anchored issue-number extraction as `dev/scan-state.sh`'s `N_ANCHOR` (char before the number is start/non-digit, char after is `/`, `-`, `_`, or end) — so `--scope 1` cannot pick up issue #14's branch (`extract_issue_number()` in `analyze-branches.sh`). Without `--scope`, behavior is unchanged (repo-wide). This is what `/R-dev`'s Ship phase relies on: `cleanup --scope #N` after an issue's PR merges must only touch that issue's branch/worktree, not every stale branch in the repo.

The script analyzes ∀ β ∉ {Π, current branch} (∧ β ∈ scope N, if set) with these checks (base branch = `staging` if `origin/staging` exists, else `main`):

| Check | Implementation | Safe to delete? |
|-------|----------------|-----------------|
| Merged into base? | `git log --oneline <base>..<branch>` empty | Yes |
| Squash-merged? | `gh pr list --state all` → `MERGED` on head, or `git log --grep` on issue#/branch name | Yes if found |
| Has open PR? | Batched `gh pr list` indexed by `headRefName` | **No** — active work |
| Has worktree? | `git worktree list --porcelain` | Remove worktree first |
| Last commit age | `git log -1 --format="%cr"` | Info only |

Emits section markers: `---local-branches---`, `---remote-branches---`, `---worktrees---`, `---safe-local---`, `---safe-remote---`, plus a human `---summary-table---`. **Analyze-only** — never deletes.

### 3. Present Summary Table

```
Git Cleanup Summary
═══════════════════

Branches:
  Branch              │ Merged │ PR    │ Worktree  │ Last Commit  │ Action
  feat/19-auth        │ ✅ yes │ —     │ —         │ 3 days ago   │ 🗑 Safe to delete
  feat/33-i18n        │ ❌ no  │ #42   │ ../repo-33│ 2 hours ago  │ ⚠️ Active work
  fix/old-bug         │ ✅ yes │ —     │ —         │ 2 weeks ago  │ 🗑 Safe to delete
  experiment/test     │ ❌ no  │ —     │ —         │ 1 month ago  │ ⚠️ Unmerged

Worktrees:
  Path                │ Branch        │ Status
  /home/user/project  │ main          │ Main (keep)
  /home/user/rox-33   │ feat/33-i18n  │ Active PR #42

Legend: 🗑 = safe to delete, ⚠️ = needs attention, 🔒 = protected
```

### 4. Ask for Confirmation

If `REPORT_ONLY=true` → skip this confirmation **and** Step 5 (zero deletions); Step 3's table is the report. Continue to Step 6.

→ present multi-select
- Present only safe(β) items as default selections
- Show unmerged β separately with warning; **NEVER auto-select unmerged β**
- ∃ unmerged β → separate question with explicit warning
- Always include "Skip / Do nothing"

### 5. Execute Cleanup

∀ confirmed deletion:

```bash
# Optional: project-specific teardown hook (e.g. DB branch drop)
# [ -n "{commands.worktree_teardown}" ] && {commands.worktree_teardown} <issue_number>

# ω ∃ for β → remove ω FIRST
# For .claude/worktrees/ (EnterWorktree-created): ExitWorktree(action: "remove") if in active session
# For legacy/manual worktrees: git worktree remove <path>
git worktree remove <path>

git branch -d <branch>        # merged branches (safe)
git branch -D <branch>        # unmerged — only if explicitly confirmed

git worktree prune
```

### 5b. Orphan worktree shells (Grok / Claude leftovers)

`git worktree list` only knows registered worktrees. After `git worktree remove`, agents often leave:

| kind | Example | Safe cleanup |
|------|---------|--------------|
| `empty_parent` | `~/.grok/worktrees/gosilex-spark/` empty | `rmdir` (or `rm -rf` if confirmed empty) |
| `unregistered` | partial dir (e.g. only `node_modules`) under Grok slug or `.claude/worktrees/` **not** in `git worktree list` | `rm -rf <path>` after confirm — **never** if path is still a live registered ω |
| `db_stale` | row in `~/.grok/worktrees.db` with `status=alive` but path missing | delete the DB row (path already gone) |

Source: `gather-state.sh` → `---orphan-worktree-shells---` (`path|kind|detail`). Scope is **this repo only** (Grok slug = owner alnum-stripped + `-` + repo name, e.g. `go-silex/spark` → `gosilex-spark`). Do **not** touch other repos' Grok worktree trees (e.g. metalyde while cleaning spark).

#### 5b-present

```
Orphan worktree shells
══════════════════════

  Path                                              │ Kind          │ Detail
  ~/.grok/worktrees/gosilex-spark                   │ empty_parent  │ empty Grok parent
  ~/.grok/worktrees/gosilex-spark/2026-…-c4624b74   │ db_stale      │ worktrees.db alive, path missing
  (none found)
```

If `REPORT_ONLY=true` → print table and skip deletion. Else → multi-select (default: all listed); always offer "Skip".

#### 5b-execute (confirmed only)

```bash
# empty_parent / unregistered on disk:
rmdir <path> 2>/dev/null || rm -rf <path>   # rmdir first; rm -rf only if user confirmed content orphans

# db_stale (path already missing — metadata only):
sqlite3 ~/.grok/worktrees.db "DELETE FROM worktrees WHERE path = '<path>';"
```

**Safety:** never `rm -rf` a path that still appears in `git worktree list`. Never wipe `~/.grok/worktrees/` wholesale — only this repo's slug children / matching `source_repo` DB rows.

### 6. Clean Remote Branches

Use `---remote-branches---` and `---safe-remote---` from Step 2 (`analyze-branches.sh`). Do **not** re-analyze manually.

**CRITICAL for squash merges**: `git branch -r --merged` will NOT detect squash-merged branches. `analyze-branches.sh` already checks PR `MERGED` state and issue# grep. Post-merge commits on a `MERGED` PR β → still safe to delete.

#### 6c. Present remote summary table

```
Remote Branch Cleanup
═════════════════════

  Remote Branch                       │ Merged │ Open PR │ Last Commit  │ Action
  origin/feat/19-auth                 │ ✅ yes │ —       │ 5 days ago   │ 🗑 Safe to delete
  origin/docs/28-coding-standards     │ ✅ yes │ —       │ 1 week ago   │ 🗑 Safe to delete
  origin/feat/33-i18n                 │ ❌ no  │ #42     │ 2 hours ago  │ ⚠️ Active work
  origin/experiment/test              │ ❌ no  │ —       │ 1 month ago  │ ⚠️ Unmerged
```

#### 6d. Ask for confirmation

If `REPORT_ONLY=true` → skip this confirmation **and** Step 6e (zero remote deletions); Step 6c's table is the report. Continue to Step 7.

→ present multi-select present merged remote β with ¬π as safe; show unmerged separately; **NEVER auto-delete remote β**; always include "Skip / Keep all remote branches".

#### 6e. Execute remote cleanup

```bash
git push origin --delete <branch>
```

### 7. Sweep: stuck pipeline labels on closed/merged PRs

Pipeline labels should be removed when a PR is closed or merged. This step identifies and strips them.

#### Pipeline label list

<!-- sync manually when plugins/dev-core/skills/shared/adapters/config-helpers.ts changes -->
```
PRIORITY_LABELS : P0-critical  P1-high  P2-medium  P3-low
SIZE_LABELS     : size:S  size:F-lite  size:F-full
LANE_LABELS     : graph:lane/a1  graph:lane/a2  graph:lane/a3
                  graph:lane/b
                  graph:lane/c1  graph:lane/c2  graph:lane/c3
                  graph:lane/d  graph:lane/e  graph:lane/f
                  graph:lane/g  graph:lane/h  graph:lane/i
                  graph:lane/j  graph:lane/k  graph:lane/l
                  graph:lane/m  graph:lane/n  graph:lane/o
                  graph:lane/standalone
STATUS_LABELS   : status:Backlog  status:Analysis  status:Specs
                  "status:In Progress"  status:Review  status:Done
PIPELINE_OTHER  : reviewed
```

All of the above are "pipeline labels" — they should not remain on closed/merged PRs.

#### 7a. Find candidates (from gather-state.sh `---closed-prs-with-labels---` section)

The script emits at most 20 closed PRs. For each, check which labels from the pipeline label list are present.

#### 7b. Present findings

```
Stuck Labels on Closed PRs
══════════════════════════

  PR    │ Title (truncated)          │ Labels to remove
  #123  │ feat: add auth module       │ status:In Progress, P2-medium
  #117  │ fix: broken build           │ reviewed, status:Review
  (none found)
```

If `REPORT_ONLY=true` → print table and **stop this step** (zero mutations).

#### 7c. Confirm and strip

→ present multi-select
- Default: strip all listed labels from all listed PRs
- "Skip / Keep labels as-is" always available

If `YES=true` → proceed without prompt.

For each confirmed PR + label:

```bash
gh pr edit <number> --remove-label "<label>"
```

Gracefully handle `gh` permission errors — report which labels could not be removed and continue.

### 8. Sweep: queued/stuck CI runs

#### 8a. Find candidates (from gather-state.sh `---queued-runs---` section)

The script emits at most 30 runs filtered to `queued` or `in_progress` status. Consider a run "stuck" if:
- status = `queued` and created ≥ 30 min ago, OR
- status = `in_progress` and created ≥ 60 min ago (use run's `createdAt` field)

#### 8b. Present findings

```
Queued / Stuck CI Runs
══════════════════════

  Run ID    │ Workflow              │ Branch              │ Status      │ Age
  12345678  │ ci.yml                │ feat/old-branch     │ queued      │ 2h 15m
  12345679  │ release.yml           │ main                │ in_progress │ 75m
  (none found)
```

If `REPORT_ONLY=true` → print table and **stop this step** (zero mutations).

#### 8c. Confirm and cancel

→ present multi-select
- Present stuck runs as candidates; runs on protected branches (main/master/staging) shown as informational — NEVER auto-cancel
- "Skip / Cancel none" always available

If `YES=true` → proceed without prompt (still skips protected branches).

For each confirmed run:

```bash
gh run cancel <run-id> 2>/dev/null || echo "⚠️  Cannot cancel run <run-id> (permission denied or already completed)"
```

Always degrade gracefully — permission errors are non-fatal; report and continue.

### 9. Final Report

```
Cleanup Complete
════════════════
  Local:
    ✅ Deleted branch: feat/19-auth
    ✅ Deleted branch: fix/old-bug
    ⏭ Skipped: feat/33-i18n (active PR)
    ⏭ Skipped: experiment/test (unmerged, user chose to keep)

  Orphan shells:
    ✅ Removed empty parent: ~/.grok/worktrees/gosilex-spark
    ✅ Deleted worktrees.db row: …/2026-07-28-c4624b74
    ⏭ Skipped: (none)

  Remote:
    ✅ Deleted remote: origin/feat/19-auth
    ✅ Deleted remote: origin/docs/28-coding-standards
    ⏭ Skipped remote: origin/feat/33-i18n (active PR #42)

  Labels:
    ✅ Stripped: #123 — status:In Progress, P2-medium
    ⏭ Skipped: #117 (user chose to keep)

  CI Runs:
    ✅ Cancelled: run 12345678 (feat/old-branch, queued 2h 15m)
    ⏭ Skipped: run 12345679 (main — protected branch)

  Remaining branches: main, feat/33-i18n, experiment/test
```

If `REPORT_ONLY=true`, prefix the header with `[report-only — no mutations performed]`.

## Options

| Flag | Description |
|------|-------------|
| (none) / `--all` | Analyze branches, worktrees, labels, and runs |
| `--report-only` | Gather and print findings; perform zero mutations (cron-safe) |
| `--yes` | Skip confirmation prompts for all destructive actions |
| `--scope <#N>` | Restrict branch/worktree analysis + cleanup (Steps 2–6) to issue N — anchored match, not a substring search (see Step 2). Steps 7–8 (label/CI sweeps) stay repo-wide — they are end-of-session hygiene, not per-issue. |

## Safety Rules

1. **NEVER delete `main`, `master`, or `staging`**
2. **NEVER delete the current branch**
3. **NEVER delete a branch with an open PR** unless explicitly confirmed
4. **NEVER delete an unmerged branch** without a separate, explicit confirmation
5. **ALWAYS show merge status** before any deletion
6. **ALWAYS use `git branch -d`** for merged branches; **`git branch -D` only** when user explicitly confirms unmerged deletion
7. **ALWAYS remove worktree before deleting its branch**
8. **NEVER delete remote branches automatically** — always require explicit confirmation per branch
9. **ALWAYS scan all remote branches** for stale merged branches, not just locally deleted ones
10. **`--report-only` = zero mutations** — no label edits, no run cancels, no branch deletes
11. **NEVER auto-cancel runs on protected branches** (main/master/staging) — show as info only
12. **Degrade gracefully on `gh` permission errors** — report failure and continue; never abort entire sweep

## Edge Cases

- **Squash merges**: `git branch -d` won't detect squash merges → use `git log --oneline --grep` on β name or issue# in main.
- **Squash merges on remote**: `git branch -r --merged` does NOT detect squash merges → verify via issue# grep AND `gh pr list --state all --head <branch>` for `MERGED`. Post-merge commits on a `MERGED` PR → still safe.
- **Remote tracking branches**: Step 6 scans **all** remote β independently — always require explicit confirmation.
- **Stale worktrees**: ω path ∉ disk → `git worktree prune`.
- **EnterWorktree worktrees**: worktrees in `.claude/worktrees/` are session-managed — `git worktree list` shows them alongside legacy worktrees; clean with `git worktree remove` or `ExitWorktree` if in active session.
- **Orphan shells after remove**: `git worktree remove` does not delete empty parent dirs under `~/.grok/worktrees/<slug>/`, nor Grok `worktrees.db` rows. Step 5b + `scan-orphan-worktree-shells.sh` cover these.

## Chain Position

- **Phase:** Ship
- **Predecessor:** merge (after `/R-dev-review` APPROVED → merge)
- **Successor:** — (pipeline complete)
- **Class:** adv (tail — last step in `/R-dev` pipeline)

## Task Integration

- `/R-dev` owns the dev-pipeline task lifecycle externally
- This skill does NOT update its own dev-pipeline task
- Sub-tasks created: none

## Exit

- **Success via `/R-dev`:** stale branches/worktrees removed → return control silently. ¬write summary. `/R-dev` re-scans, all steps done/skipped, shows completion banner.
- **Success standalone:** print summary (branches deleted, worktrees pruned, labels stripped, runs cancelled). Stop.
- **`--report-only`:** print findings report, no mutations. Exit 0.
- **Failure:** return error. `/R-dev` presents Retry | Skip | Abort.

$ARGUMENTS

