Component boundaries
| Component | Owns |
|---|---|
cmd/berth |
CLI parsing, JSON and text output, cancellation and exit status |
app |
Ownership, operation locking, setup state, safe lifecycle and configuration scope |
runner.Session |
The single execution context shared by services, run, hooks and probes |
process |
The native process-compose adapter: rendering, readiness and pinned binary installation |
worktree, copyfs, ports, state |
Narrow reusable primitives — checkout creation, copy-on-write dependency copies, port reservation, the locked registry |
gc |
Conservative eligibility selection, revalidated under the lifecycle lock before anything is collected |
Why the design looks like this
No syscall-interception backend
There is no libc, dyld or seccomp injection backend. Adding a third OS-specific syscall-compatibility implementation is not necessary for the core parallel-development workflow: projects that accept a port and a data path through configuration get full isolation of the things that actually collide, without paying for a compatibility layer that would only ever be partial.
The native fast path stays independent
The native backend depends on no container engine. The isolated path reuses a shared Linux engine and a prepared image with one namespace and one container per workspace. Future engine-specific optimisations belong behind Session rather than in individual command handlers, so a new engine does not leak into every command.
No daemon, deliberately
A separate daemon is not introduced because it would add a lifecycle problem without solving one: registry updates are short, workspace operations hold only their own lock, and the existing process-compose or container engine already supplies long-lived supervision.
Separate workspace operations can therefore proceed concurrently, while commands in the same workspace serialize. A shared read/execution lease is a possible later extension if measurements justify it — not something to build pre-emptively.
Invariants
- Primary repositories, their ancestors and wrong Git identities are never removal targets.
- Automatic GC never grants force authority. Clean is not equivalent to preserved.
- Imported or adopted records never gain deletion ownership through migration.
- Port allocation and registry insertion form one cross-process transaction.
- Unknown process or engine state prevents destructive cleanup.
- Incomplete setup is recorded and retried, not treated as completed initialisation.
- Copies of writable dependencies are independent, including on fallback paths.
- Service, test, migration, hook and probe share the workspace execution context.
- Image or backend failure does not silently run a command on the host instead.
- JSON stdout is not mixed with setup or teardown command output.
Registry, adoption and recovery
The registry moves from version 1 to 2. Legacy ownership stays empty until an explicit adoption, which grants only adopted ownership. A crash after Git creates a checkout but before berth registers it leaves an unregistered checkout that can be adopted — no destructive guess is made about it.
Runtime control endpoints live outside newly created native checkouts. PID checks after a native shutdown are only used to wait, never to kill a potentially reused PID, and native API tokens isolate the control endpoints.
Recovery intent is stored in the existing workspace record under its operation lock: reset_pending precedes runtime and data destruction, and removal_head follows teardown and shutdown, preceding checkout removal. Reset retries finish the reset before setup. Removal retries keep repository identity checks and delete only the recorded commit, using Git’s compare-and-delete. GC never discards pending removal records or grants them force authority. No separate journal service or background recovery process is needed.
How it is validated
Unit and regression coverage includes independent copies, same-source copies, primary-checkout protection, adopted-checkout preservation, clean unpublished commits, setup retry using branch-local configuration, unknown-stop safety, GC versus active operations, independent-process port allocation, runtime argument generation, host and internal port semantics, strict config parsing, checksums and readiness.
Recovery tests interrupt a reset with invalid data paths and interrupt a removal while holding real Git ref locks, including the changed, already-deleted and checked-out branch cases.
The Linux container acceptance test holds a host sentinel on the same original port used inside two concurrent workspaces. It verifies that the host sentinel and the workspace servers do not get confused, and checks hooks, private data, Git, argv and shell execution, reuse across up/down, cancellation, sibling survival and safe removal. A missing engine or image fails that test rather than skipping it.
CI runs native CLI acceptance on all three hosted operating systems: parallel workspace creation, real HTTP, JSON output, hooks, argv and exit codes, restart, reset and safe removal. Docker and Podman Desktop execution on macOS and Windows, and rootless engine variations, still require those actual environments.
No unmeasured latency or memory figure is a release service-level objective. Benchmarks are expected to separate cold image preparation, warm workspace creation, dependency copying, process readiness and incremental run latency, because those are the phases that behave independently.