Several worktrees at once
A run belongs to the checkout that started it. Two worktrees can run the same repository at once without sharing a port, a database or a stop.
A run belongs to its checkout
Different worktrees of one repository publish as <project> and <project>@<worktree>, so a group-scoped stop on one does not reach the other. status --json publishes the checkout's identity, and that identity is what down --identity uses after a directory is gone.
oberth status --json
oberth worktrees
Ports are allocated, not guessed
A service with port: auto gets its address from the run, and the engine injects it through PORT. A service's own env: PORT: 3000 overrides the injected value and makes up --wait wait on the wrong port; use ${port} instead. If a framework quietly switches to another port when one is taken, prefer its strict-port option so a conflict becomes a readable failure fact.
Data can follow the worktree
${worktree} is a stable identity derived from the canonicalized checkout root. It is the same on every start for one worktree and different for the main checkout and each linked worktree, which makes it suitable for database names and relative data directories.
DB_PATH: .berth/${worktree}/app.db
DATABASE_URL: postgres://localhost/app_${worktree}
Moving the checkout directory yields a new identity; the old data is not moved with it. That is deliberate: the engine reports the correspondence between identities and paths, and a person decides what to keep.
When a checkout disappears
worktrees --gone keeps only identities whose checkout no longer exists. down --identity still stops the runs and listeners belonging to one and returns its port: auto reservations. The engine only reports and stops; it never deletes a database, a data directory or a ledger row.
Follow changes without polling
oberth events is the read-only subscription for parallel work. It takes a worktree, repository or explicit workspace filter and writes NDJSON: the first line is a state.snapshot, later lines are state.changed summaries.
- The first record is a snapshot; later records are changes. A reconnect after a drop passes the last applied
seqasafter_seq. - Only runtime fact summaries cross the stream — no logs, commands, working directories or environment variables.
- When the queue window has expired or the state chain is discontinuous, the stream returns a full snapshot instead of pretending the gap did not happen.
--oncereads the snapshot only and registers no workspace membership; a human running it is mainly troubleshooting.