Skip to content

bob-reap

What bob reap may 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).

Terminal window
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
bob 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.

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.

In order of authority:

  1. Keep-list (hard floor). .claude/.decisions.jsonl, .claude/session-ledger.jsonl, .claude/loop-notes.json, and docs/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, exit 2, never honored. A recursive or root-glob entry that only incidentally matches a keep path (a .DS_Store inside docs/audits/) has that file skipped by a per-file backstop while the rest of the surface reaps normally.
  2. 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.
  3. 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.json with launched-but-not-done items), or a live .dev-server.pid process, or — for ci-runner, a self-hosted Actions runner home (#1732) — a Runner.Worker running from its bin/, which means a job is in progress (when pgrep is 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.
  4. TTL. Candidates younger than the surface’s ttl_days are skipped.

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.

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 **/name is anchored to the root with a leading /, because manifest globs are root-relative.
  • Placement is the one bob provision uses: _meta.ignore_placement in the project’s manifest (provisions/<dir name>.json, else provisions/_default.json, through the operator resolver) — .gitignore by default, .git/info/exclude for exclude. The exclude file is found through git 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; a bob provision refresh strips and re-appends only its own block. Both are written by the shared writer scripts/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 --json report carries the result as ignore_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.

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.

  • warp-drive: the session_ending phase runs bob 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 the session_ended transition itself.
  • swarm: the run composition ends with a reap step (bob reap --apply --owner swarm) after summary, 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.

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.