Appearance
bob-reap — Working-Tree Residue Reaper
What
bob-reapmay delete, what it can never delete, and how a project declares its reapable surfaces (#1664). (Back to reference index.)
Runtime tooling leaves regenerable residue in a project tree: swarm scratch, stale warp-drive state, dev-server logs and pidfiles, build output, aging QA media. bob-reap keeps the tree uncluttered without ever touching durable episodic data or authored files. Per the Prime Directive, the reapable surface is declared in exactly one place (the manifest), and the reaper is idempotent and observable (dry-run report by default).
Usage
bash
bob-reap [project-root] [options] # dry-run report — deletes nothing
bob-reap --apply # actually delete
bob-reap --apply --owner warp-drive,dev-up,qa # only those owners' surfaces| Option | Effect |
|---|---|
--apply | Perform deletion (default is a dry-run report: path, age, why). |
--owner <t>[,<t>...] | Restrict to surfaces owned by these tools. One or more of warp-drive, swarm, dev-up, docs-site, dash, qa, misc. |
--manifest <path> | Explicit manifest path (default <root>/reap.json). |
--json | Machine-readable report. |
Exit codes follow the CLI conventions established by the swarm hardening work: 0 clean (including "no manifest — nothing declared"), 1 a deletion failed, 2 config or usage error — bad manifest, unknown owner, or a keep-list refusal. A missing reap.json is a clean no-op unless the path was passed explicitly via --manifest, which fails loudly.
The manifest (reap.json)
Reapable surfaces are declared in reap.json at the project root, validated against schemas/reap.schema.json (wired into make check via check-schemas.js). No path list lives in the reaper code. Each surface:
json
{
"name": "playwright-media",
"glob": ".playwright-mcp/",
"owner": "qa",
"ttl_days": 7,
"reason": "QA browser session media — useful for about a week, then clutter"
}Four glob forms are supported: a directory (x/ — the directory and all contents), an exact file path (dev.json), a root-level glob (*.png — top-level files only), and a recursive basename (**/.DS_Store — anywhere outside .git). Anything else is refused as a config error.
ttl_days is an age floor: a candidate is reaped only when its mtime is older than that many days (0 / absent = no age requirement).
Safety model
In order of authority:
- Keep-list (hard floor).
.claude/.decisions.jsonl,.claude/session-ledger.jsonl,.claude/loop-notes.json, anddocs/audits/**are hardcoded in the reaper, enforced independently of manifest content. A manifest entry whose scope definitely covers a keep-list path (a directory or exact-file glob) is refused — reported, exit2, never honored. A recursive or root-glob entry that only incidentally matches a keep path (a.DS_Storeinsidedocs/audits/) has that file skipped by a per-file backstop while the rest of the surface reaps normally. - Tracked-file protection. A git-tracked file is never deleted, even when a manifest glob matches it. A directory retaining a tracked file is not pruned.
- Liveness guards. A surface is skipped — with a stated reason — while its owning tool is active: warp-drive state in a non-terminal phase (an unreadable state file is conservatively treated as live), an in-flight swarm run (
.swarm/run.jsonwith launched-but-not-done items), or a live.dev-server.pidprocess. Live session state is never deleted. This guards against the past incident where tooling resolving the wrong cwd wiped a live warp-drive state file. - TTL. Candidates younger than the surface's
ttl_daysare skipped.
gitignore alignment
Every manifest glob is expected to be git-ignored — reapable residue should never be committable. Each run probes every surface with git check-ignore and reports [drift] for any glob that is not ignored. Drift is report-only (the tracked-file protection above is what actually prevents damage).
Session-end integration
- warp-drive: the
session_endingphase runsbob-reap --apply --owner warp-drive,dev-up,qa— the ending session's own surfaces. Its own state file is still live at that point and is skipped by the liveness guard; it is removed by thesession_endedtransition itself. - swarm: the run composition ends with a
reapstep (bob-reap --apply --owner swarm) aftersummary, dropping the run's.swarm/scratch once every branch is done.
Both integrations are non-blocking: a missing manifest is a clean no-op, and an exit 2 (manifest needs attention) is noted in the session summary rather than retried.
What is deliberately out of scope
Git stashes (human-gated by standing convention), .codex/ and AGENTS.md (tool-experiment artifacts, human decision), provisions/ports.json drift (pending commit work), and docs/audits/ (durable record — keep-listed). The spurious root dev.json (#1481) is a reap target as an interim mitigation; the root-cause fix stays with that issue.
Tests
make test-bob-reap runs tests/test-bob-reap.js — manifest validation, keep-list refusal and backstop, liveness-guard skips, TTL logic, tracked-file protection, owner filtering, and drift reporting, over throwaway git fixtures.