`./cli.py cleanup` previously called only the env-var-selected
backend's `prepare_cleanup` / `cleanup` — so a leftover smolvm
machine + bundle container + bundle network from a crashed
smolmachines bottle would survive a default `docker`-mode cleanup
indefinitely.
Smolmachines now has a real `cleanup` module (alongside
`enumerate.py` from issue #77) that walks:
- smolvm machines named `claude-bottle-*` (via
`smolvm machine ls --json`)
- bundle containers `claude-bottle-sidecars-*`
- bundle networks `claude-bottle-bundle-*`
Cleanup runs stop+delete on the machines, force-rm on the
containers, network rm on the networks. Each step is best-effort
so a failed rm doesn't block the rest.
`cli.py cleanup` walks every backend in `known_backend_names()`
and runs each backend's `cleanup` after a single y/N prompt that
shows a combined plan.
State dirs (`~/.claude-bottle/state/<slug>/`) are shared layout
with the docker backend, which still owns the orphan-state-dir
bucket. It now consults `enumerate_active_bottles()` for the
cross-backend live identity set so a running smolmachines
bottle's state dir isn't reaped during a cleanup.
Tests: smolmachines cleanup (prepare + cleanup ordering + failure
handling); cross-backend orphan protection on the docker
state-dir check; CLI cmd_cleanup walks both backends, short-
circuits on all-empty, aborts on N. 617 unit tests pass.
End-to-end verified on this host:
$ smolvm machine ls --json | jq '.[].name'
"claude-bottle-researcher-m3hxd"
$ ./cli.py cleanup
--- smolmachines backend ---
smolvm machine: claude-bottle-researcher-m3hxd
remove all of the above? [y/N]
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PRD 0018 chunk 4. `claude-bottle cleanup` now derives its work
from `docker compose ls --all --format json`, filtered to projects
whose name starts with `claude-bottle-`. Per project: one `compose
down --volumes` removes the containers + the compose-managed
networks atomically.
The plan also enumerates three fallback buckets:
- Stray containers — `claude-bottle-*` containers with no
`com.docker.compose.project` label (left over from pre-compose
code paths). Cleared via `docker rm -f`.
- Stray networks — `claude-bottle-*` networks with no compose
project label. Cleared via `docker network rm`.
- Orphan state dirs — per-bottle `~/.claude-bottle/state/<id>/`
dirs with no live project AND no `.preserve` marker. The
`.preserve` marker (capability-block or auto-preserve-on-crash)
explicitly opts-out of reaping; manual `rm -rf` is the only
path for preserved state.
cli/cleanup.py collapses to a single y/N prompt — backend.prepare_cleanup
returns everything in one plan, backend.cleanup processes everything,
no more double-prompt for state. The CLI-side state-dir enumeration
+ `_state_summary` flags from PR #25 are gone; the backend's
orphan-detection rules subsume them.