Appearance
janitor
janitor
standalonewriteshands-on
Use this when: merged branches, junk files or stale issues are piling up
Problem it solves — Clutter accrues from every direction — merged branches, stray backup and scratch files, stale issues no one will action, bloated logs. This sweeps all four, surfacing what's safe to bin with the evidence to judge it, and deleting or closing only what you confirm.
Used in workflows: Health Check
Janitor
The repo accretes clutter — merged branches, stray backup files, issues no one will action, logs nobody reads. Janitor sweeps all four, but deletes nothing on its own: each target runs classify → surface with evidence → you pick → act — it empties the bins; it doesn't decide what's rubbish.
The heart — surface with evidence, act only on what's ticked
This is the skill. Every deletion or close is destructive and mostly outward, so each target runs the same loop: enumerate, classify into delete-candidate vs keep, present each candidate with the evidence that makes it safe to bin (the PR that merged it, the last-write date, the file's git status), then act on only what you confirm. Never delete or close unprompted. It's fine to find nothing — say so and stop.
Two coined guards carry most of the safety:
- gitignored ≠ junk — and a tracked file CAN be junk. Most ignored paths are deliberate build output (
vendor/,node_modules/,public/build/, the Wayfinder-generatedresources/js/{actions,routes,wayfinder}). Junk is what looks accidental — a.bak, a swap file, a scratch dump — wherever it sits. Classify by accidental-looking, not by ignore status. - PR-merge status beats history (the branch trap, below).
Target 1 — merged branches
git branch -r --merged origin/main misses squash- and rebase-merged branches: those merges leave no merge commit, so history alone calls a done branch unmerged. Classify by the PR's merge status first (gh-poi's approach), history second.
After git fetch --prune origin, bucket each origin/*:
| Bucket | Signal | Action |
|---|---|---|
| Merged | a merged PR has it as head (gh pr list --head <b> --state merged), or it's in origin/main by history | delete candidate |
| Open PR | an open PR has it as head | keep — never delete |
| Orphan | no PR, not merged | surface separately — could be work pushed nowhere else; let the user judge |
Never a candidate: the default branch (main), the checked-out branch (git rev-parse --abbrev-ref HEAD), anything protected. Filter these before bucketing.
Target 2 — junk files
Hunt accidental-looking files, tracked or not: *.bak, *.orig, *.rej (conflict leftovers), *~, *.swp/*.swo, .DS_Store, Thumbs.db, and scratch dumps under tmp/, temp/, .tmp/, scratch/. Split them:
- Tracked junk — an accidental commit (e.g. a committed
.tmp/pr-NN.mdscratch note). The worse smell; removal isgit rm+ commit. - Untracked junk — local cruft; removal is a plain
rm.
Never propose: deliberate build output and caches the workflow needs warm — .temp/ (pest mutation cache; cold-cache mis-attributes coverage), .codegraph/ (the index), .phpunit.cache, vendor/, node_modules/, public/build/, bootstrap/ssr/, coverage/, anything .env*, and storage/logs/ (Target 4).
Target 3 — stale issues
Pull open issues (GitHub MCP list_issues/search_issues, oldest-updated first). Flag stale-by-age — no activity in ≥ 90 days — plus obvious closeables (references a since-merged PR). Surface each with its number (as a Markdown link), title, last-updated, labels, readiness Status. Propose ping or close; closing runs via the MCP on confirm, one issue at a time.
This is age/clutter only. The built-vs-intended-vs-tracked deltas — and stale issues whose blocker has cleared — belong to find-untracked-work; don't re-do its job here.
Target 4 — log files
Flag logs by size and age: storage/logs/*.log (laravel.log grows unbounded without rotation) and stray *.log dumps. Surface size + last-write; propose truncate (: > file) or deleting a rotated log. Never blind-truncate a log that may hold needed debug output — surface, then confirm. Leave storage/pail (Pail owns it).
Where each action can run
Classifying + surfacing always happens here. Acting splits by environment — in the web execution env the git proxy 403s push --delete and untracked/log files are ephemeral, so those hand back as a command for the user's machine:
| Target | Act here? | Else hand over |
|---|---|---|
| Merged branches | ✗ proxy blocks delete | git push origin --delete <branch> |
| Tracked junk | ✓ git rm + commit | — |
| Untracked junk | ✗ ephemeral here | rm <file> (run locally) |
| Stale issues | ✓ MCP close on confirm | — |
| Logs | ✗ gitignored/ephemeral | : > storage/logs/laravel.log |
Workflow
- Pick targets — default all four; honour a narrower ask ("just branches").
- Sweep each, classifying as above; capture the evidence per candidate. The four disjoint read-only sweeps may run as parallel
haikuscouts perdocs/agents/orchestration.md(lists merged perdocs/agents/deterministic-merge.md); judgement, presenting and every action stay in-thread. - Present per target — candidates with evidence, plus a separate review-only group for orphan branches.
AskUserQuestion(multi-select) which to action. Never act unprompted. - Act only on the confirmed, here or handing over per the matrix; retry a transient push/MCP error.
- Report — deleted/closed (with the PR/issue each), kept + why, and what was handed back to run locally.
Guards
- Default / current / protected branches and any open-PR branch never reach the candidate list; deliberate build output and warm caches are never junk.
- Each target's destructive action is confirmed independently — picking one never green-lights another.
How this gets triggered
Invoke directly — "tidy the repo", "clean up merged branches", "stale issues?", "prune the logs". Good cadence: after a batch of PRs merge. Deliberately not hook-fired — a destructive sweep is reached for, never fired on a tool event.