bob-reap
bob-reap — Working-Tree Residue Reaper
Section titled “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).
bob reap [project-root] [options] # dry-run report — deletes nothingbob reap --apply # actually deletebob reap --apply --owner warp-drive,dev-up,qa # only those owners' surfacesbob reap --sync-ignore # write the managed ignore block (#1688)| 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, bob swarm, bob dev up, docs-site, dash, qa, ci-runner, misc. |
--sync-ignore |
Write the managed ignore block for every declared surface. Independent of --apply; neither a bare run nor --apply touches an ignore file. |
--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)
Section titled “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:
{ "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). It is how a surface
gets a retention: the scrollback surface (.claude/scrollback/, owner
misc, ttl_days: 7) keeps the tmux scrollback bob warp reap saves before
a kill for a week (#3552). It is owned by misc, not warp-drive, so the
warp-drive liveness guard does not hold it while a run is going — the TTL
alone protects a fresh log.
Safety model
Section titled “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, or — forci-runner, a self-hosted Actions runner home (#1732) — aRunner.Workerrunning from itsbin/, which means a job is in progress (whenpgrepis unavailable the answer is unknown and treated as live). 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
Section titled “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] <name> (<glob>) — not git-ignored (fix: bob reap --sync-ignore)
for any glob that is not ignored. A directory surface is probed through a path
inside it, so an ignored directory that does not exist yet is not drift. Drift
is report-only in a bare run and under --apply (the tracked-file protection
above is what actually prevents damage). A root that is not inside a git work
tree — a runner home — has nothing tracked or ignored, so it reports no drift.
The managed ignore block (#1688)
Section titled “The managed ignore block (#1688)”bob reap --sync-ignore makes the ignore state follow the manifest: a surface
is ignored because reap.json declares it, not because someone remembered to
add a line. It writes one delimited block, regenerated wholesale on every sync
and never hand-edited — the convention bob provision’s block already follows
(#1479):
# --- BoB-reapable (managed by bob reap --sync-ignore — do not edit) ---/.swarm//--help//.dash/**/.DS_Store/*.png# --- end BoB-reapable ---- One line per surface, in manifest order. Every form except
**/nameis anchored to the root with a leading/, because manifest globs are root-relative. - Placement is the one
bob provisionuses:_meta.ignore_placementin the project’s manifest (provisions/<dir name>.json, elseprovisions/_default.json, through the operator resolver) —.gitignoreby default,.git/info/excludeforexclude. The exclude file is found throughgit rev-parse --git-path, so a linked worktree writes the one git reads. Switching placement moves the block; a project never carries two. - Coexistence. The block has its own markers, distinct from
# --- BoB-provisioned …and# --- BoB-overlay …. A sync rewrites its block where it stands and leaves every other line alone; abob provisionrefresh strips and re-appends only its own block. Both are written by the shared writerscripts/lib/managed-block.js. - Idempotent. A second sync with the same manifest is byte-identical and
reports
[ignore-block] unchanged. - Tracked files are refused. An exact-path surface that names a git-tracked
file is left out of the block and reported as
[not-ignored] <name> (<glob>) — git-tracked …with the way out (git rm --cached <glob>, or drop the surface): ignoring a tracked path does nothing in git and would read as a false fix. A keep-list-refused surface is left out too. The--jsonreport carries the result asignore_sync(placement,file,lines,refused[],changed). - Outside a git work tree the sync is a stated no-op
(
[ignore-block] skipped — not a git work tree).
BoB’s own source checkout carries the block in its .gitignore. Its hand-written
.dash/ entry moved into the block, so after a sync bob reap reports no drift
there. Re-run bob reap --sync-ignore after editing reap.json and commit the result.
--sync-ignore is deliberately not implied by --apply: deleting residue and
rewriting an authored, tracked config file are different blast radii.
Runner homes (#1732)
Section titled “Runner homes (#1732)”A fleet machine’s Actions runner homes are reaped by the same CLI.
scripts/fleet/runner.sh hygiene --apply writes a reap.json into each runner
home from the machine’s declared ci_runner.retention in machines.json —
_work/ at work_days, _diag/ at diag_days, both owner ci-runner — and a
systemd timer, bob-runner-reap, that runs
reap.js <runner home> --apply --owner ci-runner for each one on the declared
schedule. The timer runs as the runner’s own account (#3655), from a copy of
the reaper hygiene keeps in that account’s home. The liveness guard above is the floor: nothing in a runner is reaped
while a job runs in it. See the
self-hosted runner runbook.
Session-end integration
Section titled “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
Section titled “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.
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.
make test-reap-ignore-block runs tests/test-reap-ignore-block.js — the
--sync-ignore block end to end: creation, idempotency, regeneration on a
manifest change, .git/info/exclude placement (a linked worktree included),
coexistence with a real bob provision refresh, the tracked-file refusal, and
the absent-directory drift probe.