enhancement(#40): integrate branch pruning into /gsd-cleanup archival workflow (#564)

* enhancement(#40): integrate branch pruning into /gsd-cleanup archival workflow

Adds a prune_local_branches step to cleanup.md (between archive_phases and
commit) that force-deletes local branches whose upstream is gone — keeping
local clones symmetric with delete_branch_on_merge on GitHub.

Key design choices vs. PR #562 (the local-model draft):
- dry-run step shows stale branches using cached tracking refs only; git
  fetch --prune is deferred to the execution step so the dry-run is
  non-side-effecting
- awk uses { if ($1 != "*") print $1 } form to explicitly exclude the
  currently checked-out branch (the * prefix in git branch -vv output),
  not a prose note that lets xargs receive literal * as an argument
- git fetch --prune runs exactly once, in prune_local_branches, eliminating
  the TOCTOU window between a preview fetch and an execution fetch
- two new negative-contract tests: identify_completed_milestones must not
  run git branch commands; show_dry_run must not run git fetch --prune

Closes #40

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: regenerate changeset using repo script (correct format)

Replaces hand-written fragment (used `/** ... */` comment syntax)
with one generated by `npm run changeset -- --type Changed --pr 562`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address codex review blockers — protect main/next/trunk, align dry-run with execution

Codex adversarial review (pre-PR gate) flagged two blockers:

1. awk filter only excluded '*' (current branch) but not protected names.
   main/next/trunk/develop could be force-deleted if their upstream was
   gone. Fix: use !~ /^\*$|^main$|^next$|^trunk$|^develop$/ regex match.

2. Dry-run enumerated from cached tracking refs; execution re-ran
   git fetch --prune, creating a TOCTOU window between what the user
   confirmed and what got deleted. Fix: move git fetch --prune into
   show_dry_run (prefetch for display accuracy); prune_local_branches
   now enumerates from the already-fetched state with no second fetch.

Updated 14 structural tests to match new design (added protected-name
exclusion test; inverted show_dry_run fetch assertion).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-31 22:31:54 -04:00
committed by GitHub
parent 4ee10f875f
commit 48c1827028
4 changed files with 343 additions and 4 deletions

View File

@@ -776,12 +776,19 @@ v1.40.0, [#2792](https://github.com/open-gsd/gsd-core/issues/2792)).
### `/gsd-cleanup`
Archive accumulated phase directories from completed milestones.
Archive accumulated phase directories from completed milestones and prune local branches whose upstream has been deleted.
```bash
/gsd-cleanup
```
On confirmation, the workflow performs two actions:
1. **Phase archival** — moves phase directories from `.planning/phases/` into milestone archive directories under `.planning/milestones/v{X.Y}-phases/`, using archived ROADMAP snapshots to determine phase membership.
2. **Branch pruning** — runs `git fetch --prune` to update remote-tracking refs, then identifies and force-deletes local branches whose upstream is marked gone. The currently checked-out branch is always skipped.
The dry-run summary shows both phase directories to archive and candidate local branches for deletion before confirmation.
---
## Spiking & Sketching Commands