| name | repo-cleanup |
| description | End a session with every repo on a clean main, no open PRs, no uncommitted work. Companion to /repo-status — where status REPORTS drift, cleanup ACTS on it. Auto-runs /save-learnings first, then drains uncommitted work via PRs, auto-merges them (skill-scoped exception to the merge-discipline rule), tags releases, updates registry pinning, and prunes stale branches across all peer repos + cached install paths. Use when the user wants the whole org buttoned up — typically end-of-day, before a break, or after a flurry of half-shipped work. |
/repo-cleanup — drain pending work, end on clean main
Purpose
Where /repo-status reports drift, this skill acts on it. After this skill runs:
- Every repo in the workspace + cached install paths is on
main, clean, 0 ahead / 0 behind
- No open PRs across the
agentteamland/ org (modulo any blocked by sensitive-file safety)
- All version bumps that landed have corresponding tags pushed
- Registry pinning matches the latest team versions
- All stale-merged local branches pruned
This is the only context where Claude is authorized to run gh pr merge and equivalents. See "Authority" below.
Why this exists
The platform has 13+ public repos and growing. After a productive session, drift accumulates in many small ways: uncommitted brainstorm logs in workspace, untagged versions on team repos, stale registry pins, stale local branches checked out post-merge. Manually walking each repo at session-end doesn't scale. /repo-status surfaces it; this skill closes it.
🔓 Authority — skill-scoped merge exception
core/rules/team-repo-maintenance.md says: "Claude never merges pull requests. This is non-negotiable and has no scope limit."
This skill is the documented exception. The user creates + invokes this skill specifically to grant scoped, durable authorization for Claude to run merge actions. The exception is bounded:
- ONLY within an active execution of
/repo-cleanup
- ONLY for PRs whose author is the user (
mkurak) or Claude itself
- NOT for PRs from external contributors (those still need human review)
- NOT for PRs marked as draft, blocked by failing CI, or with
do-not-merge labels
Outside this skill, the original rule still holds: surface PR URL, stop, never merge. This skill's authorization does NOT leak into other contexts.
If the user wants to disable auto-merge for a single invocation: /repo-cleanup --no-merge (skill body will respect this flag — leaves PRs open for manual merge).
When to invoke
User-initiated only. Trigger phrases:
- "clean up all repos"
- "no unpushed work left in any repo"
- "handle pushing, merging, all of it for me"
- "/repo-cleanup" (explicit invocation)
If user invokes /repo-cleanup literally, run unconditionally.
DO NOT invoke automatically. This skill takes destructive-ish actions (auto-commit, auto-merge). It must always be a deliberate user choice — never chained from other skills' completion paths.
Steps
0. Invoke /save-learnings first
If the session produced learnings worth capturing, save them BEFORE the cleanup pass — so any new wiki pages, journal entries, agent children files, skill learnings, or core-rule updates ride through the same cleanup cycle.
Skill tool: skill="save-learnings"
/save-learnings writes locally and may open its own PR (workspace artifacts are common). Note any PR URLs it surfaces — those will be auto-merged in step 3.
If /save-learnings reports "nothing to save", continue.
1. Pre-flight sensitive-file scan
Before any commit, scan every repo's uncommitted changes for files that should NEVER land in public agentteamland/ repos:
Block patterns:
.env, .env.*, secrets.*, credentials.*, *.pem, *.key
*.local.md, *.local.* (workspace's local-only convention)
scratch.md, NOTES_*.md, TODO_*.md, NOTES_FOR_NEXT_SESSION.md (workspace personal-info hygiene)
- Personal project names matched by
~/.claude/repos/agentteamland/core/scripts/scan-personal-paths.sh --staged
For each repo whose uncommitted changes include any blocked pattern: abort that repo's cleanup, surface the violation in the final report, continue with other repos. Do not stop the whole skill on one repo's safety abort.
2. For each repo with uncommitted/unpushed work — branch + commit + push + PR + auto-merge
Iterate the cleanup target list:
workspace (the hub itself)
- All peer repos under
workspace/repos/ EXCEPT release-pipeline ones (homebrew-tap, scoop-bucket, winget-pkgs — managed by goreleaser, skip)
- All cached paths under
~/.claude/repos/agentteamland/ (rare to have manual changes, but check)
For each repo with git status --porcelain non-empty OR git log @{u}..HEAD non-empty:
- Detect or create branch:
- If currently on
main and changes are uncommitted: git checkout -b chore/repo-cleanup-<YYYY-MM-DD>
- If currently on a feature branch with the changes: stay on it
- Stage files (excluding sensitive patterns from step 1)
- Commit with a generated message:
- Prefer detection: if changes are scoped (e.g., only
.atl/wiki/, only team.json, only agents/), use a meaningful conventional-commit type + scope
- Fallback:
chore(<repo-area>): repo-cleanup auto-commit <date>
- Push the branch (
git push -u origin <branch>)
gh pr create with auto-detected title + body listing the changes. Always include the line "Auto-generated by /repo-cleanup." in the body for transparency.
gh pr merge --merge --delete-branch <pr-number> — the skill's auth applies. Default merge style: --merge (preserves history; matches existing convention from prior PRs).
- Verify the merge with
gh pr view <num> --json state,mergedAt --jq '.state' — expect MERGED.
- Switch back to
main + pull + delete local branch.
If --no-merge flag is set: stop after step 5, surface PR URLs, skip steps 6-8.
3. Auto-merge any pre-existing open PRs
Iterate gh pr list --state open across all 13 public org repos. For each open PR:
- If
author.login is mkurak or claude (Claude bot account if/when one exists): auto-merge per skill's auth.
- If author is anyone else (external contributor): SKIP, surface in final report. External-contributor PRs always need human review.
- If PR has
do-not-merge, wip, or blocked label: SKIP, surface.
- If PR is draft (
isDraft: true): SKIP, surface.
Use gh pr merge --merge --delete-branch <pr-number> for each eligible PR.
4. Tag versions
For each merged PR (from step 2 or step 3) that bumped team.json version (or internal/config/config.go Version for cli):
- Switch to
main on that team's peer repo (already done in step 2.8 for current-session PRs)
- Read the new version from the merge-commit's
team.json
- Check if a matching tag already exists:
git tag --list | grep "^v<X.Y.Z>$". If yes, skip.
- Create + push tag:
git tag v<X.Y.Z> && git push origin v<X.Y.Z>
- For
cli repo only: the tag push triggers goreleaser → builds 6 binaries → updates brew + scoop + winget. Surface this in the report so user knows downstream effects are happening.
5. Update registry pinning
For each team repo whose latest tag is newer than the version pinned in workspace/repos/registry/teams.json:
- Surgical edit with
sed (NOT Python json.dump — that rewrites inline arrays to multiline, ballooning the diff). Update latestVersion + rewrite description to mention what the new version added.
- Branch + commit + push + PR for registry repo with conventional commit:
chore(registry): pin <team> to v<X.Y.Z>
- Auto-merge the registry PR per skill's auth.
If multiple teams need registry updates, batch them in ONE PR per cleanup invocation when possible (less ceremony).
6. Final cleanup pass
For every repo in the cleanup target list + every cached path:
git checkout main
git pull --ff-only
git fetch --prune
git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ \
| grep gone | grep -v '^main\|^master' | awk '{print $1}' \
| xargs -r -n1 git branch -d
This catches any feature branches whose remotes were deleted by step 2.6 / 3 / 5 merges.
6b. Org-wide remote orphan-branch sweep — no branch left behind
Step 6 prunes local branches; this catches remote ones — branches left on GitHub by any prior session (a merge that skipped --delete-branch, or worse, a branch whose PR was never opened, or one whose PR was closed unmerged). Authoritative state comes from gh/git, never a UI's cached view — an editor/desktop client can show a PR as "open" long after GitHub merged it.
For every org repo EXCEPT release-pipeline / archived forks (homebrew-tap, scoop-bucket, winget-pkgs — their non-default branches are goreleaser/upstream refs, not ours):
for repo in $(gh repo list agentteamland --limit 50 --json name -q '.[].name'); do
case "$repo" in homebrew-tap|scoop-bucket|winget-pkgs) continue;; esac
for br in $(gh api "repos/agentteamland/$repo/branches" --jq '.[].name' | grep -vE '^(main|master)$'); do
state=$(gh pr list --repo "agentteamland/$repo" --head "$br" --state all --json state -q '.[0].state // "NO-PR"')
if [ "$state" = "MERGED" ]; then
gh api -X DELETE "repos/agentteamland/$repo/git/refs/heads/$br"
else
echo "⛔ ORPHAN: $repo/$br ($state) — surface, do NOT delete"
fi
done
done
The asymmetry is non-negotiable (mirrors core/rules/branch-hygiene.md):
- MERGED → delete the remote branch; its work is in
main. Re-verify MERGED via gh immediately before each delete — don't trust a stale list.
- NO-PR, or an unmerged/closed PR carrying real commits → 🚨 potential lost work. Never delete it. Surface it in the final report; the user opens a PR or decides. This is the one case the sweep never resolves on its own.
7. Final verification — invoke /repo-status
Skill tool: skill="repo-status"
The output should report:
- All repos on main, clean, 0 ahead/behind (except
winget-pkgs which is expected to be behind microsoft/master by 1000+ commits — note but ignore)
- All cached paths up-to-date
- No open PRs (modulo any external-contributor PRs from step 3)
- No stale branches anywhere
If /repo-status flags ANYTHING beyond expected drift, surface as "incomplete" in the final report.
8. Final report
Show:
🧹 /repo-cleanup complete — <ISO timestamp>
Pre-flight save-learnings: <ran / skipped — reason>
Repos cleaned (auto-merged):
• <repo>#<pr-number> — <one-line summary>
• ...
Pre-existing PRs merged:
• <repo>#<pr-number> — <author> — <one-line summary>
• ...
Tags pushed:
• <repo>: v<X.Y.Z> (and any goreleaser-triggered downstream)
Registry updates:
• <team> pinned to v<X.Y.Z> (PR #<n>, merged)
Sensitive-file aborts (skipped — needs your attention):
• <repo>: <files that triggered abort>
(or: "(none)")
External-contributor PRs (skipped — need your review):
• <repo>#<pr-number> — <author>
(or: "(none)")
Remote orphan branches deleted (merged, were unpruned):
• <repo>/<branch>
(or: "(none)")
Remote orphan branches surfaced (UNMERGED — your attention, NOT deleted):
• <repo>/<branch> (<PR state>)
(or: "(none)")
Final state (from /repo-status):
✅ all repos on main, clean
✅ no open PRs (own)
✅ no stale branches
(any anomalies listed here)
End-of-skill. No follow-up prompts.
Important rules
- Skill-scoped merge auth. The exception to team-repo-maintenance's "Claude never merges" rule is documented HERE and HERE ONLY. Do not let this auth bleed into other contexts (other skills, other conversations after this skill ends, manual
gh pr merge calls outside this skill execution). When the skill's execution completes (success or abort), the rule reverts to absolute prohibition.
- Sensitive-file safety is non-negotiable. A repo that fails the safety scan is SKIPPED, not force-pushed. Surfacing the violations in the report is the value — the user fixes them in the next session.
- External-contributor PRs are off-limits. The auth covers the user's and Claude's PRs only. Don't merge a community PR even if it looks trivial.
- Read-only verification at the end. The final
/repo-status invocation is observational; do not let it auto-fix anything (status's only role is to report).
- No partial merges within a PR. If a single repo's cleanup commit blocks safety scan, the whole repo aborts — don't try to commit "just the safe files." Atomicity prevents weird per-file logic.
- Goreleaser-relevant tags. When tagging
cli, surface the downstream effects clearly: brew + scoop + winget will get auto-updated within minutes of the tag push. Don't let that surprise the user.
- Idempotent. Running the skill twice in a row should be safe — the second run finds nothing to do and exits with a "(everything clean)" report. No "I already cleaned that, skip" state needed.
- Authoritative state from
gh/git, never a UI. Whether a PR is merged or a branch still exists comes from gh pr view <n> --json state,mergedAt / gh api repos/<owner>/<repo>/branches — not an editor or desktop client's PR tab, which caches and goes stale (a PR can show "open" long after GitHub merged it). Verify before acting on any "it's merged" / "it's still open" belief. Likewise, after merging, confirm CI was green via gh pr view --json statusCheckRollup rather than trusting a watch command's exit code.
Failure modes to recognize
- Pre-flight scan blocks a repo. Surface the blocked files; continue. The user fixes in the next session.
gh pr create fails because branch protection or stale upstream. Retry once after git fetch --prune && git pull. If still failing, surface for manual handling.
gh pr merge fails because CI is red. Don't force-merge. Surface for manual review (the user might want to fix CI first or override).
- Registry update fails JSON validation (schema length). Trim description; retry. If still failing, surface.
- Cached path has uncommitted changes. Unusual — the cached paths are normally read-only mirrors. Surface as anomaly; do NOT commit on user's behalf without confirmation.
Related
History
Created 2026-04-27 in response to growing repo count (13+ public agentteamland/ repos and counting). Manually walking each repo at session-end was no longer sustainable. /repo-status closed the detection gap; this skill closes the action gap. The merge-discipline exception is deliberate — the alternative (Claude opens 5 PRs and stops on each URL waiting for the user to merge them one by one) defeats the whole point of an automation skill. The skill's existence + body is the authorization scope; outside this skill, the rule is absolute.