# option-berth guide > option-berth (oberth) is the runtime facts layer for Git worktrees: what a checkout should run, whether it really is running, and why not. Canonical HTML: https://oberth.zayju.de/guide/ 中文 edition: https://oberth.zayju.de/guide/zh/ --- URL: https://oberth.zayju.de/guide/ # The option-berth guide What a checkout should run, whether it really is running, and why not. This book teaches the chain of facts behind every oberth command. `oberth` keeps one answer per Git worktree: what should run, whether it really is running, and why not. A person reads a table; a coding agent reads the same command with `--json`. This book teaches the chain behind both. ## Four checkouts, four guesses Git worktrees solved parallel code, not parallel runtime. Four checkouts of one repository share a port range, a database name and a shell history, so every session re-derives the same three facts — and derives them slightly differently each time. oberth writes those facts once, in one order, and lets every reader read the same thing: **worktree → manifest → service → run → runtime facts**. Nothing downstream re-invents what upstream already published. ## One chain, five stations The five stations of the chain of facts | Station | The question it settles | What it may not do | Worktree | Which checkout does a run belong to? | Belong to a checkout it did not start in. | Manifest | What should run, on which port, after what? | Be guessed from a running process, or adopted by a model. | Service | Which declaration starts, in which order, on which address? | Start a dependent service before its dependency is ready. | Run | pid, start, end, exit code, log path. | Count as listening because the process exists. | Runtime facts | Alive, listening, healthy, and why it ended. | Turn a table it could not read into “not running”. [The chain of facts](https://oberth.zayju.de/guide/chain/) follows the five stations in order, one chapter section each. ## The loop you actually run Five commands cover a working day. Each replaces a shell one-liner that had to be re-invented per checkout. The core commands and what they replace | You want to | oberth | The shell answer it replaces | Declare the stack | `oberth init` | a README paragraph and a script nobody updates | Start and wait for readiness | `oberth up --wait` | `npm run dev &`, sleep, then `curl` until it answers | Know what should run, and what does | `oberth status` | read `oberth.yaml`, then `ps`, `lsof` and `tail` against it | Read a failure | `oberth logs api --once` | tail the right file and guess the exit code | Stop this checkout only | `oberth down` | `kill` a pid assembled from `ps` oberth status · hello-worktree ``` hello-worktree · main api running :21730 health 200 worker running no port 2 services · manifest oberth.yaml ``` ## How to read this book - **First time here.**[Install oberth](https://oberth.zayju.de/guide/install/), then follow [Your first worktree](https://oberth.zayju.de/guide/quickstart/): one example project, five commands, from an empty checkout to a clean stop. - **You already run things.** Read [the chain of facts](https://oberth.zayju.de/guide/chain/) and [worktrees and runs](https://oberth.zayju.de/guide/worktrees/), then use the command chapters as you need them. - **You are wiring an agent.** Start at [Facts as JSON](https://oberth.zayju.de/guide/json/), then [coding agents](https://oberth.zayju.de/guide/agents/) for the three moments to call oberth. ## Two readers, one interface There is no second interface, no MCP server and no model in the core. The CLI is the contract; `--json` is the same contract in a machine shape. an agent's opening read ``` oberth status --json --brief oberth up --wait --json oberth logs api --once --json --run current --first-error ``` [llms.txt](https://oberth.zayju.de/llms.txt) lists this book for machines, and [llms-full.txt](https://oberth.zayju.de/llms-full.txt) carries every chapter as plain text. ## What this book is not **Not the contract.** The exact CLI surface, JSON fields and manifest rules live in [docs/cli.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/cli.md); this book teaches the chain and points there. **Not the product site.** The [homepage](https://oberth.zayju.de/) shows the product. **Not a framework tutorial.** The examples use [Hello Worktree](https://github.com/SheathedSharp/option-berth/tree/main/examples/hello-worktree) because it runs on Python 3 alone; the same chain applies to any stack. --- URL: https://oberth.zayju.de/guide/install/ # Install oberth One binary, no model account, no service started by the installer. Confirm the build with version and doctor, then declare your first worktree. The installer does three things: resolve the latest release, verify the platform archive, and put the binaries on PATH. It does not touch your project, and it does not start the daemon. ## One line, macOS and Linux install · macOS or Linux ``` curl -fsSL https://raw.githubusercontent.com/SheathedSharp/option-berth/main/install.sh | sh ``` The script reads `uname`, picks the matching darwin/linux and amd64/arm64 archive, verifies its same-named `.sha256`, then installs. No administrator rights are needed. - Binaries: `~/.local/bin/oberth` and `~/.local/bin/jev-attention`. - Licenses and build manifest: `~/.local/share/option-berth`. - PATH: one line added to an existing shell startup file, never a new one. - Runtime state: `~/.option-berth`, which the installer neither creates nor modifies. Pin a version with `--version v1.0.0`, change the directory with `--dir "$HOME/tools/bin"`, or leave your shell files alone with `--no-modify-path`. The script also probes mirrors when the official host is slow or unreachable; a mirror must report the same SHA256 before the install continues. ## Windows install · Windows PowerShell ``` irm https://raw.githubusercontent.com/SheathedSharp/option-berth/main/install.ps1 | iex ``` Binaries land in `%LOCALAPPDATA%\option-berth\bin`, and only the current user's PATH is changed. Pin with `-Version`, leave PATH with `-NoModifyPath`. Before replacing files, the installer stops a running `oberth` daemon; it does not stop your services for you. ## Other ways in Installation channels | Channel | Command | What it is for | go install | `go install github.com/sheathedsharp/option-berth/engine/cmd/oberth@latest` | Go 1.25+, lands in `GOPATH/bin`; a module proxy is enough. | Homebrew | `brew install sheathedsharp/tap/oberth` | The engine CLI from a self-hosted tap; not homebrew-core. | Release archive | download, verify `SHA256SUMS`, put it on PATH | When you would rather not run a script. | Linux packages | `.deb` or `.rpm` | Distribution-managed installs; details in the install contract. ## Check the build first commands ``` oberth version --json oberth doctor ``` `version --json` returns `version`, `commit`, `built` and `platform` for the binary you actually installed. `doctor` checks the manifest, the daemon, the CLI path, the log directory and whether the current process can see listeners. A blind listener check is reported as blind, with the next step; it is not written as “nothing is listening”. ## Platform boundaries Full service scenarios are verified on macOS and Linux. Windows has a native build and smoke test plus an arm64 cross-build; that is not a promise that the full lifecycle is verified there. The engine has no graphical interface: the macOS client is a separate product, and the coding-agent skill is another one. ## Upgrade and remove Upgrade by rerunning the installer for your platform; after upgrading, run `oberth daemon stop` once and the next call starts the new build. Uninstall with `uninstall.sh` on macOS and Linux, or `uninstall.ps1` on Windows. The uninstaller stops the daemon first and keeps `~/.option-berth` unless you pass `--purge-state`. The exact flags and per-platform paths are in [Install and uninstall](https://github.com/SheathedSharp/option-berth/blob/main/docs/install.md). ## What the installer will not do It starts no service, writes no Git state and needs no administrator rights. It does not install the macOS client or the coding-agent skill, and it does not configure your shell beyond the one PATH line you allowed. --- URL: https://oberth.zayju.de/guide/quickstart/ # Your first worktree One checkout, five commands, one honest answer: declare, start, read, log, stop. The example needs Python 3 and nothing else. This chapter takes one checkout from nothing to a running stack and back to a clean stop. The example is Hello Worktree: two Python 3 processes, no package installs, no accounts and no API keys. ## Before you start You need `oberth` on your `PATH` ([Install oberth](https://oberth.zayju.de/guide/install/)), Python 3, and the example directory, which ships in the repository. get the example ``` git clone https://github.com/SheathedSharp/option-berth.git cd option-berth/examples/hello-worktree ``` The checkout must live inside your home directory unless you pass `--allow-outside-home`; the engine refuses to manage a stack outside the home boundary by default. ## Declare the stack The example already carries an `oberth.yaml`. Read it before you run anything: it is the whole input. oberth.yaml · hello-worktree ``` name: hello-worktree services: - name: api cmd: python3 -u api.py port: auto health: /health - name: worker cmd: python3 -u worker.py depends_on: [api] env: API_URL: ${api.url} ``` Two services. `api` asks for an automatically assigned port and a health check; `worker` has no port, starts after `api`, and receives the API's real address through `${api.url}`. Nothing here is inferred from a running process. In your own project, `oberth init` writes this file. `oberth init draft` lets an agent propose one; only a person runs `oberth init adopt`. ## Start it and wait oberth up --wait ``` oberth up --wait --json ``` `--wait` returns only when each service is really there: a bound listener for `api`, a passing `/health`, and a live process for the portless worker. A process that is still booting is not readiness, and a timeout is not success. ## Read what is running oberth status ``` hello-worktree · main api running :21730 health 200 worker running no port 2 services · manifest oberth.yaml ``` The port is this checkout's own: another worktree of the same repository gets a different one, because `port: auto` is assigned per run rather than remembered per project. `no port` is a fact, not a failure — the worker is observed through its run record. Open the address that `status` prints to see the API answer with the port it actually bound. The same read as JSON is `oberth status --json --brief`, which is what an agent opens with. ## Read the worker's log oberth logs worker --once ``` worker: API health=200 worker: API health=200 worker: API health=200 ``` `--once` reads the current run's log and returns. Without it, `logs` follows. When a service has already failed, `--first-error` gives the first error of the current run instead of the last screen of output. ## Stop it oberth down ``` oberth down --force --json ``` `down` stops the services that this worktree started and hands their port reservations back. It does not touch another checkout, a normal user daemon, or processes the manifest never declared. ## See the isolation for yourself Create a second Git worktree of the same repository, enter `examples/hello-worktree` in both, and run `up` in each. The two API services get different ports. Run `down` in one; the other keeps running. This is not a container or an operating-system sandbox. It is run ownership and lifecycle isolation: a run belongs to the checkout that started it. [Worktrees and runs](https://oberth.zayju.de/guide/worktrees/) explains the rules, and [down](https://oberth.zayju.de/guide/down/) explains what a stop may never reach. --- URL: https://oberth.zayju.de/guide/chain/ # One chain of facts Worktree → manifest → service → run → runtime facts. Each station has one writer, and the next station reads only what the previous one published. Every command in this book reads or writes one chain: **worktree → manifest → service → run → runtime facts**. Each station has exactly one writer, and the next station reads only what the previous one published. There is no second source of state. ## Five stations What each station publishes and what the next one reads | Station | Who writes it | What it publishes | Worktree | Git, observed by the engine | The checkout root path and the `wt_…` identity a run belongs to. | Manifest | A person | `oberth.yaml`: services, ports, dependencies, health checks, references. | Service | The engine at start | Which declaration was started, in which order, on which address. | Run | The engine's run registry | pid, start, end, exit code and the log path of one process. | Runtime facts | The observations of this round | Alive, listening, healthy, and the reason an earlier run ended. ## The order is the contract A single “is it up?” flag would collapse the chain, and every reader would have to re-derive the rest. Instead each step is published before the next one is allowed to decide anything: attribution happens before lifecycle, lifecycle before observation, and observation before readiness. That is why `up` can wait honestly, and why `status` can say a service is running while its port is unknown without contradicting itself. ## A run belongs to the checkout that started it Attribution uses this round's path observation, the manifest's content index and the run identity. Two worktrees of the same repository therefore never cross services: not a port, not a database name, not a stop. Ports follow the same rule. `port: auto` is reserved per run and handed back on stop, so the second checkout asking for “the API port” gets its own answer instead of a collision. ## Unknown is a fact Failure, cancellation and an incomplete scan may not become “free”, “gone” or “healthy”. Absence from a listener table means the fact was not observed — never that the port is free. A successful signal means the signal was delivered; it does not mean the process exited. This is the rule that keeps the chain honest: an unread fact is written as unknown, and unknown is not success. [status](https://oberth.zayju.de/guide/status/) is where you see it, and [Where the product stops](https://oberth.zayju.de/guide/boundaries/) is where it becomes a product boundary. ## Where each station is taught - [Declare what should run](https://oberth.zayju.de/guide/declare/) — the manifest, and the one thing a model may not do to it. - [up](https://oberth.zayju.de/guide/up/) — dependency order, automatic ports and what readiness really proves. - [Worktrees and runs](https://oberth.zayju.de/guide/worktrees/) — attribution, data paths and leftover checkouts. - [logs](https://oberth.zayju.de/guide/logs/) — the run's own output, without reassembling a shell one-liner. - [status](https://oberth.zayju.de/guide/status/) — the four runtime facts, and the honest “I cannot see”. --- URL: https://oberth.zayju.de/guide/worktrees/ # Several worktrees at once A run belongs to the checkout that started it. Two worktrees can run the same repository without sharing a port, a database or a stop. 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 `` and `@`, 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. see the identities this machine has seen ``` 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. one identity, many uses ``` 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 `seq` as `after_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. - `--once` reads the snapshot only and registers no workspace membership; a human running it is mainly troubleshooting. --- URL: https://oberth.zayju.de/guide/declare/ # Declare what should run oberth.yaml is the input to every fact. A person confirms it; an agent may draft it, but only a person adopts it. Every fact starts with a declaration. `oberth.yaml` says what this checkout runs; a person confirms it, and the engine never infers a start command from a process it happened to see. ## Start from the worktree `oberth init` generates a reviewable starter from the current checkout. Listening ports and declarations found in project files appear only as comments, left for a person to decide. A failed listener scan does not abort `init`: the comment says `listening unknown` with the observation code and the next step, because “could not look” is not “nothing is listening”. generate and preview ``` oberth init oberth init --dry-run ``` `--dry-run` previews without writing; `--json` is the machine shape. A failed scan still leaves the project's own declarations and an entry shape to copy. ## Let an agent draft, a person adopt draft and adopt ``` oberth init draft claude oberth init adopt --dry-run --diff ``` `init draft [agent]` asks the configured local agent for a manifest. option-berth judges nothing, goes online for nothing and writes nothing: success only produces a draft, archived by default under `~/.option-berth/drafts/`. `init adopt` is the step that writes `oberth.yaml`. An existing manifest needs an explicit `--replace` or `--merge`; `--dry-run` and `--diff` show the result first. A draft is not authorization. ## The smallest manifest oberth.yaml · hello-worktree ``` name: hello-worktree services: - name: api cmd: python3 -u api.py port: auto health: /health - name: worker cmd: python3 -u worker.py depends_on: [api] env: API_URL: ${api.url} ``` The fields in the smallest manifest | Field | What it promises | `name` | The project group. Two worktrees of the same repository publish as `name` and `name@worktree`. | `services[].cmd` | The long-running command, split into argv without a hidden shell. | `port` | A fixed number, or `auto` so the engine allocates one per run and injects `PORT`. | `health` | An HTTP path, a TCP port or a command. A service that opens its port before its protocol is ready uses the command form. | `depends_on` | Start order and readiness waiting: a dependent starts only after its dependency is ready. | `env` | Literal values and references such as `${api.url}`, expanded from this worktree's address book. ## Fields that change behavior `prepare:` and `prepare_inputs:` : A prerequisite command runs before the long-running `cmd:`. When the declared inputs are unchanged since the last success, the next `up` skips it instead of reinstalling dependencies. `sources:` : The files that tell whether the process runs the current code. `status` publishes `code_changed_since_start`; `sources: []` turns the check off for a service that hot-reloads. `ready_timeout:` : The service's own readiness budget (Go duration, default 30s). Successors and `up --wait` use it; `prepare:` does not count against it. `env_file:` : A dotenv-style file read at start and never written back. Relative paths resolve from the manifest directory, so several worktrees can share one file outside the checkout. `machine:` : A dependency this project observes but never starts or stops: a database, middleware, a service someone else owns. `workspace:` : An address reference to a service in another checkout. It supplies `${name.url}` and takes no part in start, stop or `depends_on`. ## What the manifest may not do - Unknown keys are ignored by default; `BERTH_STRICT_MANIFEST=1` turns them into load errors with line and column positions. The published schema rejects them too. - One YAML document only. A second document, or the same service declaring `port` twice, is an error with a line number. - References are five shapes: the service's own `${port}`/`${url}`, another service's, a `machine:` or `workspace:` address, and `${worktree}`. Other `${...}` is kept as text. - `machine:` and `workspace:` are read-only. `up`, `down` and `restart` never start or stop the other side. - An agent's draft never writes `oberth.yaml`. Only a person's `init adopt` does. --- URL: https://oberth.zayju.de/guide/up/ # up: make it run, and wait for real Services start in depends_on order, and --wait returns only when they actually listen and pass their health checks. `up` starts what the manifest declares, in dependency order. `--wait` returns only when the selected services actually listen and pass their health checks. ## Start in dependency order Services start in the order of `depends_on`. A service with `prepare:` runs that prerequisite first, in the same working directory, and only starts its long-running `cmd:` on success. Services already running are skipped; each service keeps its own log file and run record. oberth up --wait ``` api started :21730 ready worker started no port ``` ## What --wait proves Without `--wait`, `up` has started processes; it has not proved they are usable. With `--wait`, a service with a port must listen; a service that declares `health:` must also pass that check; a portless worker is ready when its run record becomes active. Services that were already running are included in the wait. - Each service waits by its own `ready_timeout:` (default 30s). `--wait-timeout` is an overall ceiling that only tightens the per-service budgets. - `prepare:` does not count against the readiness budget; slow installs need `prepare_inputs:` caching or a larger script timeout on the caller's side. - Without waiting, a started service carries `ready: false` and the table says readiness was not confirmed. That is not a failure. - A service that times out is `failed` with reason `ready_timeout`; the message says whether the service's own budget or the caller's ceiling expired first. ## When it fails Each entry in `services[]` is `started`, `skipped` or `failed`; a failure carries a branchable `reason` and a `hint` that points at evidence that really exists. A partial success is still exactly one JSON value, with exit code 1. Start failure reasons | Reason | What happened | What to read first | `start_failed` | The command never started. | The hint: the executable, working directory or environment that refused it. | `port_occupied` | The fixed port was already taken. | `port_holder`: the port, pid, command and kind of the holder. | `dependency_not_ready` | A dependency exited during the wait. | The dependency's logs; the message carries its exit code and reason. | `dependency_timeout` | A dependency stayed alive but never became ready. | The dependency's own `ready_timeout:` and logs. | `ready_timeout` | The service itself did not become ready in budget. | The service's logs and its readiness declaration. ## Restart without losing the address `restart` stops the project and starts it again. With `--only` it restarts just the named services while the rest keep running, and it preserves the worktree's `port: auto` reservation, so a service gets its original address back. `restart` does not wait for readiness; use `up --wait` when the caller needs proof. keep the address ``` oberth restart --only api ``` ## Boundaries - Service working directories must be under `$HOME` by default. `--allow-outside-home` is an explicit decision, not a silent fallback. - `machine:` dependencies are observed, never started or stopped by `up`. - `workspace:` references supply addresses only; `up` never starts another checkout's service and never establishes `depends_on` across checkouts. - `up` writes no manifest and no Git state. A manifest is adopted by a person before it is read here. --- URL: https://oberth.zayju.de/guide/status/ # status: what is really running One read of the worktree, the manifest and the runs — including the honest answer “I cannot see”. `status` reads the worktree, its manifest and its runs, then returns the runtime facts as they are. It is the one command a session opens with, and it is allowed to say it cannot see. ## Four facts, not one The product keeps apart things a shell one-liner collapses. A live process, a bound port, a passing health check and an exit reason are four different facts, and none of them implies the next. The four runtime facts | Fact | The question it answers | Where it comes from | Process | Did the process stay alive? | The run record: pid, start, end, exit code. | Listener | Which address is actually bound? | The listener observation, with its own trust state. | Health | Does the service answer its health check? | The declared HTTP, TCP or command check. | Exit | If it ended, why? | The most recent exit reason and the log it points at. oberth status · hello-worktree ``` hello-worktree · main api running :21730 health 200 worker running no port 2 services · manifest oberth.yaml ``` ## Read the table The human table is ordered by project, service, dependencies, code and failure reason. Each service line carries both the declared port and the current runtime fact; the failure section shows only the most recent exit while it is still that service's current `last_exit`. - A portless worker is printed as `no port`. It is observed through its run record, not through a listener. - `port: auto` before assignment prints `auto port unassigned`; it is not a failure. - Only listeners the manifest did not declare are listed separately. A port that belongs to a service appears with that service. - `code_changed_since_start` says the process is running older code than the checkout, with a restart hint. It is a code fact, not a runtime failure. ## For agents: --brief and --json the opening read ``` oberth status --json --brief ``` `--json`'s top-level object is `worktree`. `--brief` keeps only runtime facts, so it is the cheapest place to start; the full read adds the git summary, drafts, the change list and per-service manifest fields. - `worktree.identity` is the `wt_…` value used by `down --identity` and by data paths. - `worktree.services[]` carries `running`, `port_actual`, `health_status`, `pid` and `run_id`. - `exits[]` carries the reason, the exit code and a `hint` that points at evidence that really exists. - `observation` appears only when the listener table is untrustworthy; its silence means the table was usable. ## When observation fails A sandbox, a permission wall or a platform change can break the listener scan. The engine then reports `observation_empty`, `observation_unrecognized`, `observation_incomplete`, `observation_blind` or `observation_bind_denied`. None of those codes means “nothing is listening”, and still less “not running”. The human table says `listening unknown`; a `port: auto` candidate is verified by binding rather than by trusting an empty table. check the observer itself ``` oberth doctor --only listener_observation ``` Run it in the same kind of process that failed. If the result is blind, retry from a normal terminal, or run `oberth serve --detach` there and retry; do not loop the same start inside the sandbox and read timeouts as failures. ## What status never says It is not a machine-wide port radar: it answers for this worktree's declared services, not for every socket on the host. It does not hand “why did it fail” to a model; a fixed reason code and a log are the answer. And an unread fact is never written as a fact: unknown is not success, and an absent field is not zero. --- URL: https://oberth.zayju.de/guide/logs/ # logs: the service's own output A failure has a log, an exit code and a reason. logs reads them directly, without reassembling a shell one-liner. A failure has a log, an exit code and a reason. `logs` reads the service's own output and the run it belongs to, instead of reassembling a shell one-liner. ## One read or a stream Ask by service name; the port form still works. `--once` reads once and exits; without it, `logs` prints the latest lines and keeps following. `--lines N` bounds a read, and follow mode writes NDJSON, one value per line. one read ``` oberth logs api --once ``` - A service with a listener reads that process's log; a portless worker reads its run's service log file. - The log file is appended across runs. `--run current` reads only the part of the current run, starting at its recorded offset. - `--json --once` returns one document: `source`, `lines`, `truncated`, `service`, `run_id`, `pid` and `status`. ## The failure you actually need `--run current --first-error` reads the current run and locates the first matching line from a fixed vocabulary: `error`, `exception`, `fatal`, `panic`, `traceback`, case-insensitive. It returns the line number and the original text. It is a position, not a root-cause judgment; when nothing matches, the JSON says `no_first_error`. the current run's first error ``` oberth logs api --once --json --run current --first-error ``` The document adds `log_offset`, an optional `run_start_unknown`, and `first_error` with `line` and `text`, or `no_first_error`. If the log rotated past the recorded offset, the read falls back to the start of the file and sets `run_start_unknown`, so you know the lines may include earlier runs. ## A portless worker still has a log A worker with no listener is not a service without output. `oberth logs worker --once` reads that run's service log file; the run record supplies the status and the offset. worker output ``` worker: API health=200 worker: API health=200 worker: API health=200 ``` ## What it will not do - It does not read another checkout's service. The name resolves in this worktree's manifest. - `--first-error` finds a position; it does not decide the cause, and it does not hide the lines around it. - It does not invent a start. A run record without an offset returns `not_found` with a suggestion to restart the service once; `--run` also needs the daemon. --- URL: https://oberth.zayju.de/guide/down/ # down: stop this project, not the machine Stopping is scoped to the worktree that started the services. It never crosses into another checkout. `down` stops the services this worktree's manifest declares. It never crosses into another checkout, and it returns a `port: auto` reservation only after the stop is confirmed. ## The normal stop With no argument, `down` uses the nearest manifest. It stops the project's services and returns the `port: auto` reservations once the action confirms they can be released. `--force` uses a forced signal directly; it does not skip the run-identity or reservation-release gates. stop this project ``` oberth down ``` ## When the checkout is gone `--worktree PATH` stops by checkout root path, and `--identity wt_…` stops by identity. Neither can be combined with a project name or a manifest path. `worktrees --gone` lists the identities whose checkout no longer exists, so you can find the services and data a deleted checkout left behind. stop after the directory is gone ``` oberth worktrees --gone oberth down --identity wt_0123456789ab ``` An identity the daemon has never seen returns `not_found`; it does not guess a group. When the target already has no runs or reservations, an explicit identity is still a successful empty stop. ## When the target is ambiguous Two checkouts can really take the same group name, for example two same-named directories under different parents. Their groups are already merged, so a group-scoped stop would hit both; `down` returns `ambiguous` instead of picking one for the caller. Name the process by port or pid with `kill`. name the process instead ``` oberth kill 21730 ``` ## What down never touches - `machine:` dependencies. Their listener state is read-only; `down` does not stop them. - Another worktree's services. A stop by project name is group-scoped, and the other checkout's group is a different name. - Your data. The engine reports identities and stops services; deleting a database or data directory is a person's decision. - Signals already sent are not undone by an error. An incomplete stop, a failed observation or a refreshed reservation keeps the reservation and returns the error. --- URL: https://oberth.zayju.de/guide/json/ # Facts as JSON Every command has one machine shape: one JSON value on stdout, diagnostics on stderr, stable error codes. Every command has one machine shape: one JSON value on stdout, diagnostics on stderr, stable error codes. The same command answers a person and an agent; only the reader changes. ## The rules - With plain `--json`, stdout holds exactly one JSON value. Diagnostics go to stderr and never mix into it. - Streaming commands use NDJSON: one value per line. - Exit codes are 0 for success, 1 for a run failure, 2 for an argument or option error, and 130 when interrupted by Ctrl-C. - An error is `{"error":{"code":"…","message":"…","hint":"…"}}`. The hint points at evidence that really exists. - No wire field is accepted without being read. A published field is a promise every client may rely on, so the protocol build fails on a field no handler acts on. ## The stable codes Stable error codes | Group | Codes | What they mean | Input | `invalid_params`, `not_found`, `invalid_config`, `ambiguous` | The request or the declaration is wrong; the message names the object. | Permission | `permission_denied` | An operation was refused; the hint says what to change. | Time | `timeout`, `daemon_busy` | The engine gave up, or another action holds the daemon; read the hint, then retry. | Observation | `observation_empty`, `observation_unrecognized`, `observation_incomplete`, `observation_blind`, `observation_bind_denied` | None means “nothing is listening”; check `observation.state` and `doctor --only listener_observation`. | Internal | `internal` | An unexpected failure; report the command and the version, not just the message. ## A worked read A partial success is still one value. This `up --json` started one service and failed the other; the exit code is 1, and both outcomes are in the same document. oberth up --json ``` {"services":[ {"service":"api","state":"started","ready":false,"pid":1234,"port":3000}, {"service":"worker","state":"failed","reason":"start_failed", "error":"exec: \"nosuchcmd\": executable file not found in $PATH", "hint":"check the command and its working directory"} ],"started":["api"],"skipped":[],"errors":["worker"]} ``` `state` : `started`, `skipped` or `failed`: the result of the start action, not a readiness claim. `reason` : A stable, branchable failure reason such as `start_failed`, `port_occupied` or `ready_timeout`. `hint` : A deterministic next step that points at evidence that exists: the service's logs only if the process really came up, the dependency's logs when a dependency was not ready, nothing when there is no executable step. `port_holder` : Present on `port_occupied`: the port, pid, command and kind of the holder, plus the managed group, service name and run id when it is one of ours. `ready` : The CLI's annotation, not a daemon fact: `true` after `--wait` confirms readiness, `false` when no wait was requested. `false` is not a failure. ## What an absent field does not mean Absent is not zero and not false. An absent `observation` means the listener table was usable; an absent `running_services` means “not observed”, not 0. An observation code is not “nothing is listening”, and unknown is not success. When a fact cannot be read, the document says so rather than filling the gap with a default. --- URL: https://oberth.zayju.de/guide/agents/ # Put oberth inside the agent's loop The interface is the CLI and --json. Integration means calling it from the tool's own setup, run and cleanup slots — no hooks, no MCP, no configuration written for you. The interface is the CLI and `--json`. Integration means calling it from the tool's own setup, run and cleanup slots — no hooks installed for you, no MCP server, no configuration written on your behalf. ## Three moments Where a tool offers lifecycle slots, use the same three calls: start after creation, read while running, stop at the end. the integration, in three calls ``` # after the checkout is created oberth up --wait --json # while it runs oberth status --json --brief # when the work ends oberth down --worktree /absolute/path/to/checkout ``` ## The reads an agent opens with `oberth --help` ends with a **For agents** section that gives a reading pattern, not commands. The three reads are enough for a session to orient itself without assembling `ps`, `lsof` or `tail`. the opening pattern ``` oberth status --json --brief oberth up --wait --json oberth logs --once --json --run current --first-error ``` - `status` first: what is declared, what runs, which port, whether health passed, why it last failed. - `up --wait` only when something should be running; it waits for real readiness instead of a sleep. - `logs --run current --first-error` only after a failure; it points at a line, not a guess. ## Recipes The tool's own configuration is written by the user. Project-level configuration can be committed; the product distributes no integration code and writes no tool configuration. Where to call oberth in common tools | Tool | Slot | What to put there | Cursor | `.cursor/worktrees.json` → `setup-worktree-unix` | `up --wait`; there is no teardown hook, so run `down --worktree` before deletion. | Claude Code | `WorktreeCreate` / `WorktreeRemove` hooks, or an outer wrapper | `up --wait`; `down --worktree`. Taking over creation replaces Claude's own worktree behavior. | Codex | an outer wrapper, or the desktop app's Local environments setup | `up --wait`; cleanup still runs `down` before deletion. | Conductor | `.conductor/settings.toml` setup and archive slots | `up --wait`; `down --worktree`. The most portable integration is a paragraph in `AGENTS.md` or `CLAUDE.md`. This is a compact version; the full section, including the `machine:` / `workspace:` boundary, is in [docs/integrations.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/integrations.md). The engine does not install it. AGENTS.md · local services ``` ## Local services The services in this worktree are managed by option-berth (`oberth`). Before starting or troubleshooting, read the facts with it first. - Run `oberth status --json` before guessing ports or digging through `ps`. - Start with `oberth up --wait --json`; do not reimplement dependency order. - On failure read `oberth logs --once --json`; do not substitute a timeout for the reason. - When `observation.state` is not `observable`, say you cannot see the listener and follow the hint. - Stop this checkout with `oberth down`; never kill by a guessed pid. ``` ## Pitfalls - Hooks often do not load your shell rc. Use the absolute path to `oberth`, or set `PATH` explicitly; `node`, `npm` and `python3` must be on it too. - The workspace root is not always the Git checkout root. `cd "$(git rev-parse --show-toplevel)"` first, and pass the same root to `down --worktree`. - Do not hardcode `PORT` for a `port: auto` service: a service's own `env: PORT` overrides the injected value and makes the wait watch the wrong port. - Prefer the framework's strict-port option. A framework that quietly switches ports turns a conflict into a late `ready_timeout`. - In a sandbox, observation may be blind. Do not retry the same start in a loop; follow the `observation_*` hint and retry outside it. ## The skill is a separate product [option-berth-skill](https://github.com/SheathedSharp/option-berth-skill) teaches an agent the same CLI and `--json` shapes. It is installed separately, for example with `npx skills add SheathedSharp/option-berth-skill -g`. The engine does not install, update or require it; whether to install it is the user's decision. --- URL: https://oberth.zayju.de/guide/workplace/ # Leave work where the next session finds it A worktree keeps work records: commands the engine ran, rounds handed to an agent, and how each one ended. A worktree keeps work records: the commands the engine ran, the rounds handed to an agent, and how each one ended. It is what the next session reads to take over. ## The record One worktree can hold several workplaces, each keeping its entries in order. A workplace is a named stream of work; the current one receives new entries, and `list` marks it with `●`. work records under this checkout ``` oberth workplace list oberth workplace new api-refactor oberth workplace show --limit 20 ``` Selectors match in order: exact name, full id, id prefix, name prefix; several matches return `ambiguous` rather than picking one. `--worktree` defaults to the current directory and resolves to the checkout root, so different spellings of one checkout are the same record. ## What gets recorded - `oberth start` writes the run into the current workplace. A detached run is closed by the daemon when it records that run's exit. - `oberth agent stream` writes one entry per round: `turn.started` opens it, and `turn.completed` closes it with the outcome the provider reported. - `oberth workplace record` writes what the engine cannot see: an agent's own attempts, the client's interactive shell, a native agent. Reporting the same `--id` again updates the entry, so a piece of work's start and end may arrive in any order. ## How an entry ends An entry is `running`, `completed`, `failed` or `interrupted`. An entry with no observed end stays `running` and is never treated as success. A run stopped by `oberth` or by the user is `interrupted`, whatever signal exit code it left behind. Each workplace keeps at most 500 entries and drops the oldest beyond that. a recorded block ``` {"id":"…","workplace":"…","seq":1,"kind":"shell","source":"engine", "input":"npm test","status":"completed","run_id":"…","pid":1234, "log_path":"…","exit_code":0,"started_at":"…","ended_at":"…"} ``` ## It is not runtime truth - `status` never reads it to decide whether a service is running. - `show` reads back what happened; it does not call the processes of that time back. - Without a daemon, reads still open the ledger read-only; writes need the daemon. `up` and `down` write no work record — that is a declared lifecycle, not a temporary work entry. - A record is evidence of work, not a session restore. It never pretends an unobserved end was a success. --- URL: https://oberth.zayju.de/guide/diagnose/ # When something looks wrong Most confusion about a local stack is a missing fact, not a broken service. Each symptom here has one first command and one chapter. Most confusion about a local stack is a missing fact, not a broken service. This page routes a symptom to the one command that produces the missing fact, and then to the chapter that explains it. ## Start with the read `oberth status` is the first command for almost every symptom: it prints the manifest, the runs and the observed runtime facts side by side. If it prints a failure reason, read that reason before reading any log — it names the service and the phase. the one read to start from ``` oberth status ``` ## Symptom to first command Each symptom and the command that explains it | What you see | Run this first | What it means | `listening unknown`, or an `observation_*` code | `oberth doctor --only listener_observation` | The observer, not the service, could not see the listener table. Retry from a normal terminal; do not read a timeout as a failure. | `up --wait` never returns | `oberth logs --once` | Readiness is listener plus health. The service's own output says which half failed. | A service line shows an exit and a code | `oberth logs --once --first-error` | The first error of the current run, not the last screen of output. | `auto port unassigned` | `oberth status --json` | Not a failure: `port: auto` has not been assigned yet. A candidate is verified by binding, not by trusting an empty listener table. | `code_changed_since_start` | `oberth restart` | The process runs older code than the checkout. It is a code fact, not a runtime failure. | A stop hits the wrong project, or refuses | `oberth down --worktree ` | Several checkouts share a project name. Identify the checkout explicitly; do not widen the stop. | Services still run after a checkout was deleted | `oberth worktrees --gone` | The identity ledger kept the path. Stop them by worktree or identity; deleting data stays your job. | “manifest not found”, or a draft you never adopted | `oberth init` | An agent may draft a manifest; only `oberth init adopt` makes it the input to anything. | The project lives outside your home directory | `oberth up --allow-outside-home` after review | The home boundary is a default, not an accident. Read the flag before you pass it. ## What to keep when you ask for help - The exact command and its exit code, not a paraphrase of what it printed. - `oberth status --json` from the same checkout, so the reader sees the same facts. - The first error of the current run: `oberth logs --once --first-error`. - What the observer said, if `observation` appeared at all: which code, and from which kind of process. ## What is not a fix Raising a timeout, killing a pid by hand or starting the service outside oberth all make the symptom disappear while leaving the fact unread. The next session inherits the same unknown, one more layer deep. Read the reason first; change the manifest second. --- URL: https://oberth.zayju.de/guide/boundaries/ # Where the product stops The boundaries are part of the product. These are the questions people and agents ask first. The boundaries are part of the product. These are the questions people and agents ask first, answered the way the product answers them. ## Questions people ask first Does it have a graphical interface? No. The [macOS client](https://github.com/SheathedSharp/option-berth-macos) is a separate product. It presents the same CLI facts and grows no capability the CLI lacks; it is not a second source of state. Is it a machine-wide port radar? No. It answers for this worktree's declared services, not for every socket on the host. `machine:` dependencies are observed read-only, and an unfiltered stream is only a compatibility view. Does it write my Git or my manifest? Git is read-only: no commit, no stage, no branch switch. `oberth.yaml` is written only by a person's `oberth init adopt`; an agent may draft it and nothing more. Is “no port” the same as “not running”? No. A portless worker is observed through its run record. Process, listener, health and exit are four separate facts, and one does not imply another. How much Windows support is there? There is a native build and smoke test plus an arm64 cross-build. Full service scenarios are verified on macOS and Linux; Windows lifecycle claims are not made from those runs. Does it need a model account or a cloud? No. The engine runs locally, has no built-in model inference and no cloud sync. The optional model assessment (`oberth jev`) is off unless a person enables it, and it never enters the control path. Does it install hooks, an MCP server or the agent skill? No. Integration means calling the CLI from your own scripts. The product installs no hooks, registers no MCP server, writes no tool configuration and does not install the [coding-agent skill](https://github.com/SheathedSharp/option-berth-skill). Can it work with Claude Code, Cursor, Codex or Conductor? Yes, from their own setup, run and cleanup slots or an outer wrapper. The engine modifies none of their configuration, and the recipes are documentation you paste. What can it delete? It stops services and reports identities. It never deletes a database, a data directory or a ledger row; that stays a person's call. Uninstall is a separate script, and purging `~/.option-berth` requires an explicit flag. ## The contract, by topic This book teaches the chain and the reasoning. The exact surface lives in the repository's contract documents, one home per fact. Where each contract lives | Topic | Document | What it owns | Product | [docs/product.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/product.md) | The need, the object model, write boundaries and what the product explicitly refuses to do. | CLI | [docs/cli.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/cli.md) | The command surface, JSON fields, error codes and manifest references. | Install | [docs/install.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/install.md) | Every channel, upgrade, uninstall and platform boundary. | Integrations | [docs/integrations.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/integrations.md) | Tool recipes, the AGENTS.md section and the pitfalls of each slot. | Work records | [docs/workplace.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/workplace.md) | The workplace commands, block fields and their boundaries. | Architecture | [docs/agent/architecture.md](https://github.com/SheathedSharp/option-berth/blob/main/docs/agent/architecture.md) | Implementation boundaries and the chain of facts as code enforces it. ## Machine entry points - [`llms.txt`](https://oberth.zayju.de/llms.txt) — a curated index of the guide and the contract, with one line per link. - [`llms-full.txt`](https://oberth.zayju.de/llms-full.txt) — every English chapter in one plain-text file. - [`sitemap.xml`](https://oberth.zayju.de/sitemap.xml) — every page in both languages. The HTML pages are the source; the machine files are generated from the same content at build time, so they cannot silently drift from the book.