From 83e248b6c1a08c140cd9e8606d42018d49e351ff Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:10:10 -0400 Subject: [PATCH 1/2] fix(refresh-repo,git-workflow-standards): catch zero-ahead branches, not just [gone] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clean_gone only deletes [gone] branches; it found nothing to clean on a repo carrying two dead branches whose commits were already fully contained in main (pure ancestors, never [gone] since their remotes were still live). The real signal for "nothing useful" is ahead-count against the default branch, not remote-tracking status — [gone] was only ever a narrowing proxy for "probably merged." - refresh-repo Step 5 (renamed Branch and Worktree Cleanup): the stale ahead-count check is now unconditional, no longer gated on [gone]. Three passes cover every ref shape: worktree-paired (existing), bare local branches with no worktree, and remote-only branches with no local ref at all. Any local delete now also deletes the matching live remote branch. - git-workflow-standards: same rule, condensed to match this file's prose style, kept in sync per its own documented expectation. Widening "ahead of default" to run without [gone] is only safe because a protected-branch guard (default branch, checked-out-anywhere, referenced by an open PR as --head or --base) now runs first and unconditionally on every one of the three passes independently — dropping that guard would let the rule autonomously delete origin/main on a git-flow repo, since main is routinely an ancestor of develop there. The remote-delete step also re-confirms ancestry immediately before the destructive push (not once at the top of the run), since git push origin --delete ignores tip state and a race would silently discard new commits. Independently reviewed (Opus, high effort) before landing; the review caught the git-flow guard gap, an incomplete open-PR check (--head only, missing --base), and the missing pre-push recheck — all three folded in. Assisted-by: Claude:claude-opus-4-8 Claude-Session: https://claude.ai/code/session_012s1AcPKsXaLT5e1n6G9M2u --- .../skills/git-workflow-standards/SKILL.md | 22 +++- github-workflows/skills/refresh-repo/SKILL.md | 113 ++++++++++++------ 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/git-standards/skills/git-workflow-standards/SKILL.md b/git-standards/skills/git-workflow-standards/SKILL.md index 432762af..ce1e57cc 100644 --- a/git-standards/skills/git-workflow-standards/SKILL.md +++ b/git-standards/skills/git-workflow-standards/SKILL.md @@ -18,12 +18,22 @@ Create a worktree for the change; remove it when the work is done. Every branch with commits MUST have an associated PR. Orphaned branches must get a PR or be deleted. -**Stale worktree**: A branch with no open PR, no uncommitted changes, and either a merged PR -whose `headRefOid` matches local `HEAD`, or a deleted remote (`[gone]`) with no commits ahead -of the default branch (`git log origin/..HEAD --oneline` is empty). Branches with open -PRs, local-only branches without merged PRs, local commits beyond the merged PR head, and -worktrees with uncommitted changes are NEVER stale. Use `git worktree remove` (never `--force`) -— Git natively blocks removal of dirty worktrees. +**Stale branch** (covers worktree-paired, bare local, and remote-only +branches — see `refresh-repo`, github-workflows, for the full three-pass +procedure): not the default branch, not checked out anywhere, not referenced +by an open PR as `--head` or `--base`, no uncommitted changes (when a +worktree exists), and either a merged PR whose `headRefOid` matches the +branch tip, or zero commits ahead of the default branch +(`git merge-base --is-ancestor origin/`) — unconditional, not +gated on `[gone]`. That widening is only safe because the protected-branch +check runs first and unconditionally: `origin/main` is routinely an ancestor +of `origin/develop` on a git-flow repo, so never skip the default-branch +guard to save a step. Everything else — open PRs (either side), local-only +branches without merged PRs, local commits beyond the merged PR head, +uncommitted changes — is NEVER stale. Use `git worktree remove` (never +`--force`) for worktree-paired branches — Git natively blocks removal of +dirty worktrees — and `git push origin --delete` for a live remote ref only +after re-confirming ancestry immediately before the push. ## Branch Hygiene diff --git a/github-workflows/skills/refresh-repo/SKILL.md b/github-workflows/skills/refresh-repo/SKILL.md index dca57a5b..875a8a5f 100644 --- a/github-workflows/skills/refresh-repo/SKILL.md +++ b/github-workflows/skills/refresh-repo/SKILL.md @@ -1,13 +1,13 @@ --- name: refresh-repo -description: Check PR merge readiness, sync local repo, cleanup stale worktrees; optional cross-repo sweep and stale-branch prune modes +description: Check PR merge readiness, sync local repo, cleanup stale branches and worktrees (local and remote); optional cross-repo sweep and stale-branch prune modes --- # Git Refresh -Check open PR merge-readiness status, sync the local repository, and cleanup stale worktrees. +Check open PR merge-readiness status, sync the local repository, and cleanup stale branches and worktrees. > **State warning**: Branch state, remote tracking, and PR status change between > invocations. Re-run all git/gh commands from Step 1. @@ -74,10 +74,7 @@ Replace ``, ``, `` per the placeholder legend in that sk of the current shell directory: `git -C merge --ff-only origin/`. If the default worktree is dirty or divergent, report it and skip instead of resetting. -6. Delete local branches already merged into the default branch with `git branch -d`. - Never delete main/master/develop/current branches, worktree-checked-out branches, or branches - with open PRs. -7. Conclude the operation without switching branches. Because Steps 4 and 5 used +6. Conclude the operation without switching branches. Because Steps 4 and 5 used `git -C ` to operate on the default worktree directly, the current shell's working directory and branch were never changed — each worktree owns its checkout. @@ -85,6 +82,10 @@ Do not use `git fetch --tags`, `git fetch --prune-tags`, or `git pull --tags` du normal refresh. Tags are audited separately in Step 4 so local-only non-release tags and tag rewrites are not deleted by a broad fetch refspec. +Local and remote branch cleanup (including bare local branches with no +worktree) happens in Step 5, not here — Step 5's stale definition is a strict +superset of "already merged into default." + ### 4. Tag Audit And Cleanup Treat `origin` as authoritative for release tags only. @@ -106,32 +107,75 @@ For local-only tags: For tags that exist both locally and on origin but point at different objects, report the mismatch and do not force-update it automatically. Never delete or rewrite remote tags. -### 5. Worktree Cleanup - -Only remove a worktree if it is confirmed stale. - -**Stale definition**: No open PR, no uncommitted changes, and either: - -- The branch has a merged PR (most recently merged by `mergedAt`) whose `headRefOid` matches the local branch `HEAD` - (`gh pr list --state merged --head --json number,headRefOid,mergedAt`) -- Its remote tracking branch was deleted (`[gone]` in `git branch -vv`) and it has no commits - ahead of the default branch (`git log origin/..HEAD --oneline` is empty) - -Branches with open PRs, local-only branches without PRs, and worktrees with uncommitted -changes are **NEVER** stale. - -For each worktree from `git worktree list`: - -1. Skip the default branch worktree, the current branch, and bare repo entries -2. If the branch has an open PR, skip — it is **never** stale -3. Check if stale using the definition above -4. If not stale, skip -5. Run `git worktree remove ` — **NEVER use `--force`** -6. If Git blocks removal (dirty worktree), report it and skip -7. If removed, also delete the branch: `git branch -d `. - If this fails only because a squash-merged branch is not reachable from local default, - use `git branch -D ` only when the merged PR `headRefOid` matched the local - branch `HEAD` before removing the worktree. +### 5. Branch and Worktree Cleanup + +Only remove a branch or worktree if it is confirmed stale. + +**Protected — never touch, regardless of any other check below.** Check this +FIRST, before computing staleness, on every one of the three passes below — +each pass runs independently and none may assume an earlier pass already +checked it: + +- The default branch (`main`/`master`/`develop`) and the branch currently + checked out in any worktree in this repo. (Git itself refuses + `branch -d`/`-D` on a branch checked out in some worktree, so passes 1–2 + are doubly protected — but pass 3's remote delete is a different ref and + is NOT protected by that, so check explicitly there too.) +- Any branch referenced by an open PR as **either** `--head` or `--base` — + `--head` alone misses a branch that's the base of someone's stacked PR: + + ```bash + gh pr list --head --state open --json number + gh pr list --base --state open --json number + ``` + +**Stale definition** (branch passes this check only after clearing +Protected, above): no uncommitted changes (when a worktree exists), and +either: + +- **Zero commits ahead of the default branch** — + `git merge-base --is-ancestor origin/` (equivalent to + `git log origin/.. --oneline` being empty). Unconditional — + does NOT require `[gone]` status. This is a deliberate widening from an + earlier version of this rule that gated on `[gone]` as a proxy for + "probably merged"; the ancestor check is the real signal. The Protected + guard above is what keeps this widening safe on git-flow repos, where + `origin/main` is routinely an ancestor of `origin/develop` (the actual + default there) — never skip that guard to save a step. +- The branch has a merged PR (most recently merged by `mergedAt`) whose + `headRefOid` matches the branch tip + (`gh pr list --state merged --head --json number,headRefOid,mergedAt`) — + still needed for squash-merges, where the branch's own commits are never + literal ancestors of the default branch. + +Apply Protected + Stale in three passes — each covers a different ref shape +existing checks miss: + +1. **Worktree-paired branches**, from `git worktree list` (skip bare repo + entries). If stale: `git worktree remove ` — never `--force`; if + Git blocks removal for a dirty worktree, report and skip. Then + `git branch -d ` (fall back to `-D` only when the merged-PR + `headRefOid` matched the branch tip before removal, e.g. a squash-merged + branch unreachable from local default). +2. **Bare local branches** — local branches with no worktree + (`git for-each-ref refs/heads`, excluding branches already covered by + pass 1). If stale: `git branch -d ` (same `-D` fallback as pass 1). +3. **Remote-only branches** — remote branches with no local ref at all + (`git branch -r`, excluding `origin/HEAD` and anything with a local + counterpart). Check Stale against `origin/` directly (no local + ancestry to walk, no local `git branch -d` to run — this pass has nothing + local to delete, so its only action IS the remote delete below). + +**Remote delete.** Passes 1 and 2 trigger it after their local +`git branch -d`/`-D` succeeds; pass 3 has no local step, so a Stale +remote-only branch goes straight here. If a live (non-`[gone]`) remote +tracking ref exists, delete it: `git push origin --delete` ignores tip +state — if someone pushed new commits to the branch between your last fetch +and now, a plain delete would silently discard them, no `--force` needed. So +immediately before this specific push (not once at the top of the run): +`git fetch` and re-confirm `git merge-base --is-ancestor origin/ +origin/` still holds; skip and report if it no longer does. +Otherwise: `git push origin --delete `. Finish with `git worktree prune`. @@ -144,9 +188,8 @@ current branch, and sync status. ## Cross-Repo Operating Modes Optional modes that change `/refresh-repo` from single-repo to workspace-wide. -Both modes reuse the stale-worktree definition from Step 5 and the deletion -rules from Step 5.7 — they only add new pre-filters, never weaken existing -safety. +Both modes reuse the Protected + Stale definition and deletion rules from +Step 5 — they only add new pre-filters, never weaken existing safety. ### `--sweep []` From 0c1fa6cd9c68074fe3dd645877674ee4e749ee61 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans-personal@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:35:27 -0400 Subject: [PATCH 2/2] refactor(refresh-repo): split branch/worktree cleanup into prune-branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /simplify (4-agent review: reuse, simplification, efficiency, altitude) on the prior commit's diff, plus a direct user request to split refresh-repo so invoking it doesn't load the full branch-cleanup procedure by default. - New skill github-workflows:prune-branches carries Step 5 (Protected/Stale/ three-pass/remote-delete) and the Cross-Repo Operating Modes wholesale. refresh-repo drops from ~264 lines back to 131 — a caller that only wants PR-readiness + sync no longer pays for cleanup prose it isn't using. - git-workflow-standards' "Stale branch" paragraph was re-deriving refresh- repo's mechanism and git-flow safety rationale instead of pointing at it (reuse + altitude agents, independently). Trimmed to the concept, now points at prune-branches as the procedure's new home. - Protected's open-PR check was 2 live `gh pr list` calls per candidate branch; now one repo-wide `--json headRefName,baseRefName` call up front, tested by set membership (efficiency agent). - Applied every negative-framed instruction rewrite the simplification agent proposed (never/NOT/NEVER -> positive statements) and cut the wording it flagged as filler or cross-file duplication. - wrap-up's Path A folds the old external `commit-commands:clean_gone` step into `/prune-branches` (invoked right after `/refresh-repo` in A1) — prune-branches is a strict superset of clean_gone's `[gone]`-only scope, so the separate step and the external plugin dependency both go away. - Updated every stale cross-reference this split or the clean_gone removal touched: plugin.json skill registration, both plugins' README/ARCHITECTURE diagrams, and the Related Skills sections in sync-main, troubleshoot- worktree, session-status, pr-sweep, skills-registry. Assisted-by: Claude:claude-opus-4-8 Claude-Session: https://claude.ai/code/session_012s1AcPKsXaLT5e1n6G9M2u --- AGENTS.md | 2 +- README.md | 3 +- .../ARCHITECTURE.md | 4 +- ai-cli-harness-better-practices/README.md | 4 +- .../skills/session-status/SKILL.md | 4 +- .../skills/wrap-up/SKILL.md | 34 ++--- git-standards/ARCHITECTURE.md | 3 +- .../skills/git-workflow-standards/SKILL.md | 30 ++-- git-workflows/skills/sync-main/SKILL.md | 3 +- .../skills/troubleshoot-worktree/SKILL.md | 3 +- github-workflows/.claude-plugin/plugin.json | 1 + github-workflows/ARCHITECTURE.md | 10 +- github-workflows/README.md | 6 +- github-workflows/skills/pr-sweep/SKILL.md | 6 +- .../skills/prune-branches/SKILL.md | 132 +++++++++++++++++ github-workflows/skills/refresh-repo/SKILL.md | 136 ++---------------- .../skills/skills-registry/SKILL.md | 3 +- 17 files changed, 197 insertions(+), 187 deletions(-) create mode 100644 github-workflows/skills/prune-branches/SKILL.md diff --git a/AGENTS.md b/AGENTS.md index 65c083cf..0845ba6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ This is a **Claude Code plugins repository** containing production-ready hooks f | **content-guards** | Pre/PostToolUse | Bash, Write, Edit | Token limits, markdown/README validation, webfetch guard, issue/PR backlog limits, public-repo leakage guard | | **git-guards** | PreToolUse | Bash, Edit, Write, NotebookEdit | Blocks dangerous git/gh commands and file edits on main branch | | **git-workflows** | Command/Skill | `/sync-main`, `/git-flow-next`, `/troubleshoot-rebase`, `/troubleshoot-precommit`, `/troubleshoot-worktree`, `/pre-commit-architecture` | Local git sync, branching model, and rebase/pre-commit/worktree troubleshooting | -| **github-workflows** | Command/Skill | `/ship`, `/finalize-pr`, `/refresh-repo` (incl. `--sweep` and `--prune-stale` modes), `/rebase-pr`, `/squash-merge-pr`, `/pr-sweep`, `/resolve-pr-threads`, `/gh-cli-patterns`, `/shape-issues`, `/trigger-ai-reviews` | GitHub PR/issue management workflows, cross-repo workspace sweep, stale-branch pruning, and risk-ranked open-PR sweep-to-zero | +| **github-workflows** | Command/Skill | `/ship`, `/finalize-pr`, `/refresh-repo`, `/prune-branches` (incl. `--sweep` and `--prune-stale` modes), `/rebase-pr`, `/squash-merge-pr`, `/pr-sweep`, `/resolve-pr-threads`, `/gh-cli-patterns`, `/shape-issues`, `/trigger-ai-reviews` | GitHub PR/issue management workflows, cross-repo workspace sweep, stale-branch pruning, and risk-ranked open-PR sweep-to-zero | | **infra-orchestration** | Skill | `/orchestrate-infra`, `/sync-inventory`, `/test-e2e` | Cross-repo infrastructure orchestration for Terraform and Ansible | | **code-standards** | Skill | `/code-quality-standards`, `/review-standards` | Code quality standards, documentation formatting, testing philosophy, and review guidelines | | **git-standards** | Skill | `/git-workflow-standards`, `/pr-standards` | Git workflow standards, branch hygiene, PR creation guards, workaround vs fix classification, and issue linking | diff --git a/README.md b/README.md index f2b90c95..ff0aea8d 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,8 @@ invocation. Skill-based plugins provide slash commands: /ship # Full automation: commit, push, PR, finalize /finalize-pr # Drive PR to merge-ready state /squash-merge-pr # Validate and squash merge -/refresh-repo # Sync main, check PRs, cleanup worktrees +/refresh-repo # Sync main, check PRs +/prune-branches # Delete dead branches and worktrees /wrap-up # Session-completion verdict + forward artifact /resolve-codeql # Fix CodeQL security alerts /delegate-to-ai # Route tasks to available AI models diff --git a/ai-cli-harness-better-practices/ARCHITECTURE.md b/ai-cli-harness-better-practices/ARCHITECTURE.md index 7381f187..73aacb0a 100644 --- a/ai-cli-harness-better-practices/ARCHITECTURE.md +++ b/ai-cli-harness-better-practices/ARCHITECTURE.md @@ -21,8 +21,8 @@ flowchart TD subgraph optional["Optional git enrichment"] refresh["/refresh-repo\n(github-workflows)"]:::external + prune["/prune-branches\n(github-workflows)"]:::external ghcli["/gh-cli-patterns\n(github-workflows)"]:::external - cleangone["/clean_gone\n(commit-commands)"]:::external end subgraph referred["Referred out, never rebuilt"] @@ -36,7 +36,7 @@ flowchart TD wrapup --> handoff wrapup --> retro wrapup -.->|"if git repo"| refresh - wrapup -.->|"if git repo"| cleangone + wrapup -.->|"if git repo"| prune status --> handoff status -.->|"if git repo"| ghcli diff --git a/ai-cli-harness-better-practices/README.md b/ai-cli-harness-better-practices/README.md index 7ff438f5..2d2f8f50 100644 --- a/ai-cli-harness-better-practices/README.md +++ b/ai-cli-harness-better-practices/README.md @@ -65,8 +65,8 @@ This plugin deliberately does not reimplement work that already exists: | Prose quality | `elements-of-style:writing-clearly-and-concisely` | The git-side actions the continuity skills delegate to live in their own -plugins: `github-workflows:refresh-repo`, `github-workflows:gh-cli-patterns`, -and `commit-commands:clean_gone`. +plugin: `github-workflows:refresh-repo`, `github-workflows:prune-branches`, +and `github-workflows:gh-cli-patterns`. ## Installation diff --git a/ai-cli-harness-better-practices/skills/session-status/SKILL.md b/ai-cli-harness-better-practices/skills/session-status/SKILL.md index 63e51526..2d4cb3bf 100644 --- a/ai-cli-harness-better-practices/skills/session-status/SKILL.md +++ b/ai-cli-harness-better-practices/skills/session-status/SKILL.md @@ -296,7 +296,7 @@ to `handoff` and `wrap-up`'s resume blocks, not this dashboard. Step 0 and handles conditional repository cleanup. - **resume** (this plugin) — cold pickup; reuses this skill's derivation. - **replan** (this plugin) — rebuilds a plan this skill shows has drifted. -- **refresh-repo** (github-workflows) — Checks PR merge-readiness, syncs local - main, and cleans worktrees. +- **refresh-repo** (github-workflows) — Checks PR merge-readiness and syncs local main. +- **prune-branches** (github-workflows) — Deletes stale branches and worktrees. - **retrospecting** (claude-retrospective) — Generates detailed retrospectives based on session logs and git diffs. diff --git a/ai-cli-harness-better-practices/skills/wrap-up/SKILL.md b/ai-cli-harness-better-practices/skills/wrap-up/SKILL.md index 516c8315..4ec4ad1d 100644 --- a/ai-cli-harness-better-practices/skills/wrap-up/SKILL.md +++ b/ai-cli-harness-better-practices/skills/wrap-up/SKILL.md @@ -84,16 +84,16 @@ skipped" in the summary, and run A2 and A4 as normal. Steps A2 and A4 are the part of a wrap-up that always applies. In a repository, run Steps A1 and A2 **in parallel** (they are independent). -Step A3 starts as soon as Step A1 completes (depends on its remote prune). Step -A4 runs after all prior steps finish. Provide a summary of actions taken. +Step A3 runs after both finish. Provide a summary of actions taken. -### A1. Refresh Repository (repository only) +### A1. Refresh and Prune Repository (repository only) -Invoke `/refresh-repo` to: +Invoke `/refresh-repo` to sync, then `/prune-branches` to delete anything +left with nothing useful: - Check merge-readiness of any remaining open PRs - Sync the local default branch with remote (main on trunk repos, develop on git-flow repos) -- Clean up stale worktrees (merged PRs, `[gone]` remote branches) +- Delete stale branches and worktrees — local, remote, or both - Report repository state ### A2. Quick Retrospective @@ -111,17 +111,7 @@ reporting a full retrospective. **Requires**: `claude-retrospective` plugin (external). If not installed, skip this step and note it was skipped. -### A3. Clean Gone Branches (repository only) - -Invoke `/clean_gone` to remove any local branches whose remote tracking branch has been deleted: - -- Identify branches marked `[gone]` -- Remove associated worktrees -- Delete the local branches - -**Requires**: `commit-commands` plugin (external). If not installed, skip this step and note it was skipped. - -### A4. Follow-Up Session Prompt +### A3. Follow-Up Session Prompt If `/session-status` in Step 0 surfaced follow-up work, invoke the `/handoff` skill to emit the next-session artifact. Pass it the "Recommended Prompt for Next @@ -159,7 +149,7 @@ Wrap-Up Summary ## Path B — Resume blocks (plan incomplete) -Skip Path A entirely. Skipping is intentional: `/refresh-repo` prunes stale +Skip Path A entirely. Skipping is intentional: `/prune-branches` deletes stale worktrees, which would delete the in-flight worktree the user needs to resume in. @@ -269,15 +259,15 @@ Sequence: Closes the gap where `gh pr close --delete-branch` removes only the remote branch and leaves the local branch + worktree behind. Reuses the worktree-removal command shape from `/troubleshoot-worktree` and aligns with -`/clean_gone`'s post-removal state. +`/prune-branches`'s post-removal state. ## Related Skills - **handoff** (this plugin) — builds the two-part next-session artifact (goal - statement under 4000 chars + full prompt) used by Path A Step A4 and Path B Step B2 -- **refresh-repo** (github-workflows) — PR readiness check + repo sync + - worktree cleanup (Path A Step A1 dependency); also provides `--sweep` and - `--prune-stale` modes + statement under 4000 chars + full prompt) used by Path A Step A3 and Path B Step B2 +- **refresh-repo** (github-workflows) — PR readiness check + default-branch sync (Path A Step A1) +- **prune-branches** (github-workflows) — stale branch and worktree cleanup + (Path A Step A1); also provides `--sweep` and `--prune-stale` modes - **shape-issues** (github-workflows) — Shape and create well-structured GitHub issues for code/repo defects and features; not for incidents (Zammad, see session-status Step 4 and pr-standards) diff --git a/git-standards/ARCHITECTURE.md b/git-standards/ARCHITECTURE.md index 0383e840..7b229822 100644 --- a/git-standards/ARCHITECTURE.md +++ b/git-standards/ARCHITECTURE.md @@ -19,6 +19,7 @@ flowchart LR subgraph consumers["Skills that load these standards"] SM["/sync-main\n(git-workflows)"]:::external RR["/refresh-repo\n(git-workflows)"]:::external + PB["/prune-branches\n(git-workflows)"]:::external RP["/rebase-pr\n(git-workflows)"]:::external SHIP["/ship\n(github-workflows)"]:::external FPR["/finalize-pr\n(github-workflows)"]:::external @@ -26,7 +27,7 @@ flowchart LR end GWS -.->|"informs branch ops"| SM - GWS -.->|"informs cleanup"| RR + GWS -.->|"informs cleanup"| PB GWS -.->|"informs merge"| RP PS -.->|"informs PR creation"| SHIP PS -.->|"informs PR finalization"| FPR diff --git a/git-standards/skills/git-workflow-standards/SKILL.md b/git-standards/skills/git-workflow-standards/SKILL.md index ce1e57cc..c42b6078 100644 --- a/git-standards/skills/git-workflow-standards/SKILL.md +++ b/git-standards/skills/git-workflow-standards/SKILL.md @@ -18,22 +18,15 @@ Create a worktree for the change; remove it when the work is done. Every branch with commits MUST have an associated PR. Orphaned branches must get a PR or be deleted. -**Stale branch** (covers worktree-paired, bare local, and remote-only -branches — see `refresh-repo`, github-workflows, for the full three-pass -procedure): not the default branch, not checked out anywhere, not referenced -by an open PR as `--head` or `--base`, no uncommitted changes (when a -worktree exists), and either a merged PR whose `headRefOid` matches the -branch tip, or zero commits ahead of the default branch -(`git merge-base --is-ancestor origin/`) — unconditional, not -gated on `[gone]`. That widening is only safe because the protected-branch -check runs first and unconditionally: `origin/main` is routinely an ancestor -of `origin/develop` on a git-flow repo, so never skip the default-branch -guard to save a step. Everything else — open PRs (either side), local-only -branches without merged PRs, local commits beyond the merged PR head, -uncommitted changes — is NEVER stale. Use `git worktree remove` (never -`--force`) for worktree-paired branches — Git natively blocks removal of -dirty worktrees — and `git push origin --delete` for a live remote ref only -after re-confirming ancestry immediately before the push. +**Stale branch** (worktree-paired, bare local, or remote-only — see +`prune-branches`, github-workflows, for the full procedure): a non-default +branch, checked out nowhere, unreferenced by any open PR (`--head` or +`--base`), with no uncommitted changes (when a worktree exists), and either +a merged PR whose `headRefOid` matches the branch tip, or zero commits ahead +of the default branch. Use `git worktree remove` (never `--force`) for +worktree-paired branches — Git blocks removal of dirty worktrees — and +`git push origin --delete` for a live remote ref, only after +`prune-branches`'s pre-push ancestry re-check. ## Branch Hygiene @@ -63,7 +56,7 @@ Sync workflow (replace `` with the repo's actual default branch — see `gh-cli-patterns`, github-workflows): ```bash -git fetch origin && git pull origin # in the worktree +git pull origin # in the worktree git merge origin/ --no-edit # in the feature worktree ``` @@ -99,7 +92,8 @@ contradictions, or security-sensitive code. ## Related Skills - **sync-main** (git-workflows) — Syncs the repo's default branch and merges into current or all PR branches -- **refresh-repo** (github-workflows) — Full repo sync including PR status and worktree cleanup +- **refresh-repo** (github-workflows) — PR status and default-branch sync +- **prune-branches** (github-workflows) — Stale branch and worktree cleanup - **gh-cli-patterns** (github-workflows) — Canonical default-branch detection (trunk vs git-flow) - **pr-standards** (git-standards) — PR creation guards, issue linking, and review standards - **git-flow-next** (git-workflows) — Dedicated git-flow-next guide, worktree setup, and promotion steps diff --git a/git-workflows/skills/sync-main/SKILL.md b/git-workflows/skills/sync-main/SKILL.md index 7626b9b1..a9961d91 100644 --- a/git-workflows/skills/sync-main/SKILL.md +++ b/git-workflows/skills/sync-main/SKILL.md @@ -101,7 +101,8 @@ Read files, understand both versions, combine intelligently, stage resolved file ## Related Skills -- **refresh-repo** (github-workflows) — Full repo sync including PR status and worktree cleanup +- **refresh-repo** (github-workflows) — PR status and default-branch sync +- **prune-branches** (github-workflows) — Stale branch and worktree cleanup - **rebase-pr** (github-workflows) — Rebase-merge workflow that builds on a synced base branch - **promote-release** (github-workflows) — Moves develop to main on git-flow repos; the fast-forward step here only reads main, never pushes it - **gh-cli-patterns** (github-workflows) — Canonical default-branch detection (trunk vs git-flow) diff --git a/git-workflows/skills/troubleshoot-worktree/SKILL.md b/git-workflows/skills/troubleshoot-worktree/SKILL.md index cf277235..8be7c2b9 100644 --- a/git-workflows/skills/troubleshoot-worktree/SKILL.md +++ b/git-workflows/skills/troubleshoot-worktree/SKILL.md @@ -68,4 +68,5 @@ echo "/" >> .gitignore - **troubleshoot-rebase** (git-workflows) — Diagnose and recover from git rebase failures - **troubleshoot-precommit** (git-workflows) — Troubleshoot pre-commit hook failures -- **refresh-repo** (github-workflows) — Full repo sync including worktree cleanup +- **refresh-repo** (github-workflows) — Default-branch sync +- **prune-branches** (github-workflows) — Worktree and branch cleanup diff --git a/github-workflows/.claude-plugin/plugin.json b/github-workflows/.claude-plugin/plugin.json index 3f321ab1..6b117b72 100644 --- a/github-workflows/.claude-plugin/plugin.json +++ b/github-workflows/.claude-plugin/plugin.json @@ -24,6 +24,7 @@ "./skills/rebase-pr", "./skills/promote-release", "./skills/refresh-repo", + "./skills/prune-branches", "./skills/gh-cli-patterns", "./skills/shape-issues", "./skills/resolve-pr-threads", diff --git a/github-workflows/ARCHITECTURE.md b/github-workflows/ARCHITECTURE.md index 33b4f148..c7dc23ff 100644 --- a/github-workflows/ARCHITECTURE.md +++ b/github-workflows/ARCHITECTURE.md @@ -24,9 +24,10 @@ graph LR ## 1. Skill Dependency Map -All skills and their cross-plugin dependencies. `/refresh-repo`, `/rebase-pr`, -`/squash-merge-pr`, `/promote-release`, and `/gh-cli-patterns` are local to this -plugin (no cross-plugin hop) — `/squash-merge-pr`, `/rebase-pr`, and +All skills and their cross-plugin dependencies. `/refresh-repo`, +`/prune-branches`, `/rebase-pr`, `/squash-merge-pr`, `/promote-release`, and +`/gh-cli-patterns` are local to this plugin (no cross-plugin hop) — +`/squash-merge-pr`, `/rebase-pr`, and `/promote-release` all consume the canonical PR-readiness gate and the default-branch detection from `/gh-cli-patterns` directly. On a git-flow repo, `/squash-merge-pr` and `/rebase-pr` both refuse a `main`-targeting PR and point @@ -41,6 +42,7 @@ flowchart TD RBP["/rebase-pr"]:::ai PRR["/promote-release"]:::ai RFR["/refresh-repo"]:::ai + PBR["/prune-branches"]:::ai GCP["/gh-cli-patterns"]:::ai TAR["/trigger-ai-reviews"]:::ai SI["/shape-issues"]:::ai @@ -152,7 +154,7 @@ flowchart TD direction TB W1["/refresh-repo\n(this plugin)"]:::ai W2["/retrospecting quick\n(claude-retrospective, external)"]:::external - W3["/clean_gone\n(commit-commands, external)"]:::external + W3["/prune-branches\n(this plugin)"]:::ai W4["Summary report"]:::ai W1 --> W3 --> W4 diff --git a/github-workflows/README.md b/github-workflows/README.md index 79b9af4c..441c2ee4 100644 --- a/github-workflows/README.md +++ b/github-workflows/README.md @@ -8,7 +8,8 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for integration diagrams and the master s - **`/ship`** - Full automation: commit, push, create PR(s), and auto-finalize in one command. - **`/finalize-pr`** - Finalize PRs for merge: single PR, all repo PRs (`all`), or all org PRs (`org`). Includes bot-authored PRs in all modes. -- **`/refresh-repo`** - Check PR merge-readiness, sync local repo, and cleanup stale worktrees +- **`/refresh-repo`** - Check PR merge-readiness and sync the local repo with its default branch +- **`/prune-branches`** - Delete branches and worktrees with nothing useful, local and remote; optional multi-repo sweep - **`/rebase-pr`** - Merge a PR using local git rebase + signed commits + a direct push to its base branch (main on trunk repos, develop on git-flow repos) - **`/squash-merge-pr`** - Validate PR readiness and squash merge into its base branch (errors if not ready); refuses on git-flow repos when the base is main - **`/promote-release`** - Merge-commit a develop → main promotion PR on a git-flow repo; release-please takes over from there @@ -32,7 +33,8 @@ claude plugins add jacobpevans-cc-plugins/github-workflows /finalize-pr 42 # Finalize specific PR by number /finalize-pr all # Finalize all open PRs in repo (including bots) /finalize-pr org # Finalize all open PRs across org (including bots) -/refresh-repo # PR check, sync, worktree cleanup +/refresh-repo # PR check, default-branch sync +/prune-branches # Delete dead branches and worktrees /rebase-pr # Rebase-merge current branch /rebase-pr 42 # Rebase-merge specific PR by number /squash-merge-pr # Validate and squash merge diff --git a/github-workflows/skills/pr-sweep/SKILL.md b/github-workflows/skills/pr-sweep/SKILL.md index 05b54762..3cc22bf8 100644 --- a/github-workflows/skills/pr-sweep/SKILL.md +++ b/github-workflows/skills/pr-sweep/SKILL.md @@ -7,8 +7,8 @@ description: "Drive open PRs toward zero across one or many repos by risk-rankin Take a pile of open PRs and drive it to zero: merge what is safe, surface what is not with a specific reason. This is triage-and-clear, distinct from -`/refresh-repo --sweep` (which syncs repos and cleans worktrees). This skill acts -on **open PRs**; that one acts on **repo/worktree state**. +`/prune-branches --sweep` (which deletes dead branches and worktrees). This +skill acts on **open PRs**; that one acts on **repo/worktree state**. It does not reimplement merging. It **risk-ranks**, then hands each merge to `/squash-merge-pr` (which itself runs the readiness gate and calls `/finalize-pr`). @@ -83,5 +83,5 @@ outcome, not a failure. - **squash-merge-pr** (github-workflows) — performs each merge; this skill decides which. - **finalize-pr** (github-workflows) — PR metadata/soft-block handling, via squash-merge-pr. -- **refresh-repo** (github-workflows) — the repo/worktree sweep; complementary, not this. +- **prune-branches** (github-workflows) — the repo/worktree sweep; complementary, not this. - **promote-release** (github-workflows) — the correct path for held promotion PRs. diff --git a/github-workflows/skills/prune-branches/SKILL.md b/github-workflows/skills/prune-branches/SKILL.md new file mode 100644 index 00000000..3349df02 --- /dev/null +++ b/github-workflows/skills/prune-branches/SKILL.md @@ -0,0 +1,132 @@ +--- +name: prune-branches +description: Delete branches and worktrees with nothing useful — zero commits ahead of default, or a matched merged PR — local and remote, single-repo or workspace-wide. Run after refresh-repo syncs the default branch. +--- + +# Prune Branches + +Delete branches and worktrees carrying no unique, unmerged work: local, +remote, or both. + +> **State warning**: Branch state, remote tracking, and PR status change between +> invocations. Re-run every git/gh command from the top on each run. + +## Protected + +Skip these before computing staleness, on every pass below — each pass +re-checks independently: + +- The default branch (`main`/`master`/`develop`) and any branch checked out + in a worktree. Git enforces this for passes 1–2 via `branch -d`/`-D`; pass + 3 deletes a different ref, so check explicitly there too. +- Any branch an open PR references as `--head` or `--base` — `--base` + catches a stacked PR's target. + +Build the open-PR reference set once, at the top of the run, not per branch: + +```bash +gh pr list --state open --json number,headRefName,baseRefName --limit 200 +``` + +Union `headRefName` and `baseRefName` into one set; test membership against +it in every pass below instead of a per-branch `gh pr list` call. + +## Stale + +A branch clears Protected and meets either: + +- **Zero commits ahead of the default branch** — + `git merge-base --is-ancestor origin/` (equivalent to + `git log origin/.. --oneline` returning nothing). Ignores + `[gone]` status — the ancestor check, not `[gone]`, is the real "merged" + signal. Run the default-branch guard first: `origin/main` is routinely an + ancestor of `origin/develop` on a git-flow repo. +- A merged PR (most recent by `mergedAt`) whose `headRefOid` matches the + branch tip — `gh pr list --state merged --head --json + number,headRefOid,mergedAt`. Needed for squash-merges, where the branch's + own commits are never literal ancestors of the default branch. + +## Passes + +Three passes, one per ref shape: + +1. **Worktree-paired branches** — `git worktree list` (skip bare repo + entries). If Stale: `git worktree remove ` — never `--force`; a + dirty worktree blocks removal, report and skip. Then `git branch -d + ` (fall back to `-D` only when the merged-PR `headRefOid` matched + the branch tip before removal — a squash-merged branch unreachable from + local default). +2. **Bare local branches** — local branches with no worktree + (`git for-each-ref refs/heads`, excluding pass-1 branches). If Stale: + `git branch -d ` (same `-D` fallback as pass 1). +3. **Remote-only branches** — remote branches with no local ref + (`git branch -r`, excluding `origin/HEAD` and anything with a local + counterpart). Check Stale against `origin/` directly; its only + action is the remote delete below. + +Finish with `git worktree prune`. + +## Remote delete + +Passes 1 and 2 trigger this after their local `git branch -d`/`-D` +succeeds; pass 3 goes straight here. If a live (non-`[gone]`) remote ref +exists: `git push origin --delete` ignores tip state, so immediately before +this specific push — not once at the top of the run — refresh and +re-confirm: + +```bash +git fetch origin +git merge-base --is-ancestor origin/ origin/ +``` + +Skip and report if it no longer holds. Otherwise: `git push origin --delete +`. + +## Cross-Repo Operating Modes + +Optional modes that widen this from single-repo to workspace-wide. Both +reuse Protected + Stale and the deletion rules above — they only add +pre-filters on top of every rule above. + +### `--sweep []` + +Multi-repo cleanup of abandoned local branches. For every default-branch +worktree in your workspace (pass a custom glob for a non-standard layout), +resolve that repo's own default branch first (`main` on trunk repos, +`develop` on git-flow repos), then for every local branch where +`git log origin/..HEAD` is non-empty: + +1. **Content-equivalence check**: compute merge base, diff each touched file + against current `origin/`. If every touched file is + content-equivalent to (or older than) `origin/`, delete the + branch and its worktree. An already-on-default contribution doesn't need + a PR. +2. **Workaround filter**: surface for human review, don't draft-PR, when the + diff (a) modifies 1 of N files sharing a common shape with no written + rationale for the asymmetry, or (b) names a "sync mechanism" / + "auto-update" that `grep -r .` finds zero matches for. +3. Only branches passing both checks become draft PRs. +4. Per-repo summary: branches deleted as content-equivalent, branches + surfaced for review, branches PR-ified, branches unchanged. + +**Origin**: the 2026-05-22 sweep opened 8 dead PRs against `ansible-splunk` +(6 already-on-main duplicates, 2 workaround anti-patterns). Both filters +above would have caught all 8 before any CI ran. + +### `--prune-stale ` (default 60) + +Delete local branches with no open PR and no push activity within ``. +Adds a time threshold on top of Stale; every rule above still applies. + +Per branch: if `gh pr list --head ` is empty and +`git log -1 --format=%cr ` is older than ``, run +`git worktree remove ` (never `--force`) then `git branch -d `. + +Use `--prune-stale 30` for an aggressive sweep, `--prune-stale 90` for +conservative. + +## Related Skills + +- **refresh-repo** (github-workflows) — sync the default branch first +- **git-workflow-standards** (git-standards) — worktree and branch conventions +- **gh-cli-patterns** (github-workflows) — canonical gh CLI command shapes, default-branch detection diff --git a/github-workflows/skills/refresh-repo/SKILL.md b/github-workflows/skills/refresh-repo/SKILL.md index 875a8a5f..167457fc 100644 --- a/github-workflows/skills/refresh-repo/SKILL.md +++ b/github-workflows/skills/refresh-repo/SKILL.md @@ -1,13 +1,14 @@ --- name: refresh-repo -description: Check PR merge readiness, sync local repo, cleanup stale branches and worktrees (local and remote); optional cross-repo sweep and stale-branch prune modes +description: Check PR merge readiness and sync the local repo with its default branch. See prune-branches (github-workflows) for stale branch and worktree cleanup. --- # Git Refresh -Check open PR merge-readiness status, sync the local repository, and cleanup stale branches and worktrees. +Check open PR merge-readiness status and sync the local repository with its +default branch. > **State warning**: Branch state, remote tracking, and PR status change between > invocations. Re-run all git/gh commands from Step 1. @@ -82,9 +83,8 @@ Do not use `git fetch --tags`, `git fetch --prune-tags`, or `git pull --tags` du normal refresh. Tags are audited separately in Step 4 so local-only non-release tags and tag rewrites are not deleted by a broad fetch refspec. -Local and remote branch cleanup (including bare local branches with no -worktree) happens in Step 5, not here — Step 5's stale definition is a strict -superset of "already merged into default." +Branch and worktree cleanup — local, remote, and bare-local — is +`prune-branches` (github-workflows), run separately after this sync. ### 4. Tag Audit And Cleanup @@ -107,128 +107,11 @@ For local-only tags: For tags that exist both locally and on origin but point at different objects, report the mismatch and do not force-update it automatically. Never delete or rewrite remote tags. -### 5. Branch and Worktree Cleanup - -Only remove a branch or worktree if it is confirmed stale. - -**Protected — never touch, regardless of any other check below.** Check this -FIRST, before computing staleness, on every one of the three passes below — -each pass runs independently and none may assume an earlier pass already -checked it: - -- The default branch (`main`/`master`/`develop`) and the branch currently - checked out in any worktree in this repo. (Git itself refuses - `branch -d`/`-D` on a branch checked out in some worktree, so passes 1–2 - are doubly protected — but pass 3's remote delete is a different ref and - is NOT protected by that, so check explicitly there too.) -- Any branch referenced by an open PR as **either** `--head` or `--base` — - `--head` alone misses a branch that's the base of someone's stacked PR: - - ```bash - gh pr list --head --state open --json number - gh pr list --base --state open --json number - ``` - -**Stale definition** (branch passes this check only after clearing -Protected, above): no uncommitted changes (when a worktree exists), and -either: - -- **Zero commits ahead of the default branch** — - `git merge-base --is-ancestor origin/` (equivalent to - `git log origin/.. --oneline` being empty). Unconditional — - does NOT require `[gone]` status. This is a deliberate widening from an - earlier version of this rule that gated on `[gone]` as a proxy for - "probably merged"; the ancestor check is the real signal. The Protected - guard above is what keeps this widening safe on git-flow repos, where - `origin/main` is routinely an ancestor of `origin/develop` (the actual - default there) — never skip that guard to save a step. -- The branch has a merged PR (most recently merged by `mergedAt`) whose - `headRefOid` matches the branch tip - (`gh pr list --state merged --head --json number,headRefOid,mergedAt`) — - still needed for squash-merges, where the branch's own commits are never - literal ancestors of the default branch. - -Apply Protected + Stale in three passes — each covers a different ref shape -existing checks miss: - -1. **Worktree-paired branches**, from `git worktree list` (skip bare repo - entries). If stale: `git worktree remove ` — never `--force`; if - Git blocks removal for a dirty worktree, report and skip. Then - `git branch -d ` (fall back to `-D` only when the merged-PR - `headRefOid` matched the branch tip before removal, e.g. a squash-merged - branch unreachable from local default). -2. **Bare local branches** — local branches with no worktree - (`git for-each-ref refs/heads`, excluding branches already covered by - pass 1). If stale: `git branch -d ` (same `-D` fallback as pass 1). -3. **Remote-only branches** — remote branches with no local ref at all - (`git branch -r`, excluding `origin/HEAD` and anything with a local - counterpart). Check Stale against `origin/` directly (no local - ancestry to walk, no local `git branch -d` to run — this pass has nothing - local to delete, so its only action IS the remote delete below). - -**Remote delete.** Passes 1 and 2 trigger it after their local -`git branch -d`/`-D` succeeds; pass 3 has no local step, so a Stale -remote-only branch goes straight here. If a live (non-`[gone]`) remote -tracking ref exists, delete it: `git push origin --delete` ignores tip -state — if someone pushed new commits to the branch between your last fetch -and now, a plain delete would silently discard them, no `--force` needed. So -immediately before this specific push (not once at the top of the run): -`git fetch` and re-confirm `git merge-base --is-ancestor origin/ -origin/` still holds; skip and report if it no longer does. -Otherwise: `git push origin --delete `. - -Finish with `git worktree prune`. - -### 6. Summary - -Report: PRs assessed as merge-ready (if any), tags deleted or reported, branches cleaned up, -worktrees removed, default-branch worktree restorations (with any stash references created), -current branch, and sync status. - -## Cross-Repo Operating Modes - -Optional modes that change `/refresh-repo` from single-repo to workspace-wide. -Both modes reuse the Protected + Stale definition and deletion rules from -Step 5 — they only add new pre-filters, never weaken existing safety. - -### `--sweep []` - -Multi-repo cleanup of abandoned local branches. For every default-branch -worktree in your workspace (caller can pass a custom glob if their layout -differs), resolve that repo's own default branch first (per Step 3 above — -`main` on trunk repos, `develop` on git-flow repos), then for every local -branch where `git log origin/..HEAD` is non-empty: - -1. **Content-equivalence check**: compute merge base, diff each touched file - against current `origin/`. If every touched file is content-equivalent - to (or older than) `origin/`, delete the branch and its worktree. - Already-on-the-default-branch contributions do not deserve a PR. -2. **Workaround filter**: if the diff (a) modifies 1 of N files sharing a - common shape with no written rationale for the asymmetry, or (b) references - a "sync mechanism" / "auto-update" by name that `grep -r .` returns - zero matches for, surface for human review. Do not open a draft. -3. Only branches passing both checks become draft PRs. -4. Per-repo summary: branches deleted as content-equivalent, branches surfaced - for review, branches PR-ified, branches unchanged. +### 5. Summary -**Origin**: the 2026-05-22 sweep opened 8 dead PRs against `ansible-splunk` -(6 already-on-main duplicates, 2 workaround anti-patterns). Both filters -above would have caught all 8 before any CI ran. - -### `--prune-stale ` (default 60) - -Delete local branches with no open PR and no push activity within ``. -Expands Step 5's stale definition with a time threshold; still respects every -safety rule (never delete branches with open PRs, uncommitted changes, or -the current checkout). - -Per branch: if `gh pr list --head ` is empty AND -`git log -1 --format=%cr ` is older than ``, run -`git worktree remove ` (per Step 5 — never `--force`) then -`git branch -d `. - -Use `--prune-stale 30` for an aggressive sweep, `--prune-stale 90` for -conservative. +Report: PRs assessed as merge-ready (if any), tags deleted or reported, +default-branch worktree restorations (with any stash references created), +current branch, and sync status. ## Common Mistake to Avoid @@ -241,6 +124,7 @@ explicit refspec prune and can delete local-only tags that are not release artif ## Related Skills +- **prune-branches** (github-workflows) — stale branch and worktree cleanup, run after this sync - **sync-main** (git-workflows) — Syncs the default branch and merges into current or all PR branches - **rebase-pr** (github-workflows) — Rebase-merge workflow for merging individual PRs - **git-workflow-standards** (git-standards) — Worktree structure and branch hygiene conventions diff --git a/project-standards/skills/skills-registry/SKILL.md b/project-standards/skills/skills-registry/SKILL.md index 2bee3042..8af8033c 100644 --- a/project-standards/skills/skills-registry/SKILL.md +++ b/project-standards/skills/skills-registry/SKILL.md @@ -10,7 +10,8 @@ description: Use when looking up available tools, skills, commands, agents, or p | Intent | Command | Plugin | Notes | | --- | --- | --- | --- | | Sync branch with main | `/sync-main` | `git-workflows` | Merge into current | -| Sync repo, cleanup | `/refresh-repo` | `git-workflows` | Also merges PRs | +| Sync repo | `/refresh-repo` | `git-workflows` | Also merges PRs | +| Prune dead branches, worktrees | `/prune-branches` | `github-workflows` | Local and remote | | Rebase + merge PR | `/rebase-pr` | `git-workflows` | Local rebase-merge | | Squash + merge PR | `/squash-merge-pr` | `github-workflows` | Squash-merge | | Troubleshoot rebase | `/troubleshoot-rebase` | `git-workflows` | Recover failures |