Skip to content

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-generated resources/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/*:

BucketSignalAction
Mergeda merged PR has it as head (gh pr list --head <b> --state merged), or it's in origin/main by historydelete candidate
Open PRan open PR has it as headkeep — never delete
Orphanno PR, not mergedsurface 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.md scratch note). The worse smell; removal is git 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:

TargetAct here?Else hand over
Merged branches✗ proxy blocks deletegit push origin --delete <branch>
Tracked junkgit rm + commit
Untracked junk✗ ephemeral hererm <file> (run locally)
Stale issues✓ MCP close on confirm
Logs✗ gitignored/ephemeral: > storage/logs/laravel.log

Workflow

  1. Pick targets — default all four; honour a narrower ask ("just branches").
  2. Sweep each, classifying as above; capture the evidence per candidate. The four disjoint read-only sweeps may run as parallel haiku scouts per docs/agents/orchestration.md (lists merged per docs/agents/deterministic-merge.md); judgement, presenting and every action stay in-thread.
  3. Present per target — candidates with evidence, plus a separate review-only group for orphan branches. AskUserQuestion (multi-select) which to action. Never act unprompted.
  4. Act only on the confirmed, here or handing over per the matrix; retry a transient push/MCP error.
  5. 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.