The project model
A Unibench project is a folder containing a unibench.toml manifest plus
whatever code, data, and ancillary files the user keeps alongside it.
How to create one
Three entry points produce a manifest; pick whichever fits how you plan to use the project:
unibench init [path]— write a minimal starter manifest at<path>/unibench.toml. Hand-rolled stacks; you fill in the services. The produced file carries[project]+[project.schema]and a commented-out examples block. Refuses to clobber an existing manifest unless--forceis passed.unibench samples new <name> [--target <dir>]— materialize one of the blessed sample projects (unibench samples listfor the catalog) into<target>. Writes the manifest plus the sample's code files. Default target is~/Documents/unibench/<name>/; pass--targetfor any folder you control.- GUI's "Try a sample…" picker — same as
samples newbut the target row in the picker has a Change… button that opens a folder picker before you click Create project. The sandboxed GUI defaults to its container's Documents dir; override to land in e.g.~/Code/<name>/.
Whichever path you take, the result is the same: a folder with a
unibench.toml ready to be opened (unibench open <path> or the
GUI's Open Folder…).
What's in the folder
The manifest is the only file Unibench requires. Everything else is user-shaped:
my-rag/
├── unibench.toml # required — the manifest
├── README.md
├── indexer/
│ ├── requirements.txt
│ ├── index.py
│ └── ask.py
└── frontend/ # added by `unibench recipes apply frontend-vite`
├── package.json
└── src/
The orchestrator never writes to the project root. The two locations it does write to:
<project>/.unibench-data/<service>/— per-service data directories. PostgresPGDATA, MinIO storage, Qdrant collections live here. The user-visible side effect ofunibench start --fresh: this tree gets wiped before the start cycle (per service it spins up).~/Library/Caches/unibench/binaries/<service>/<version>/— the cross-project blessed-service binary cache. Shared across every project for the same service+version pair so each project doesn't re-download Postgres.
Lifecycle
Five lifecycle phases per service, in order:
- Install — fetch the binary into the user-global cache. Idempotent — already-cached versions skip straight through.
- Init — first-run setup (initdb for Postgres, generate root creds
for MinIO, etc.). Writes into
<project>/.unibench-data/<service>/. Skipped on subsequent starts. - Start — spawn the supervised process. The orchestrator owns the PID and surfaces logs through its ring buffer.
- Healthcheck — three retry attempts (5s / 15s / 45s by default;
configurable per service via
healthcheck_retries). The service moves fromStarting→Retrying→HealthyorFailed. - Stop —
unibench stopor quitting the GUI tears down services in reverse topological order so dependents go down before their dependencies.
Failure of any one service marks transitively-dependent services
Failed without trying to start them; siblings keep running. The
graph's red nodes tell you exactly which leaf failed.
Sandbox modes
unibench start takes a --fresh or --seed flag (or starts in the
default Resume mode):
- Resume (default) — non-destructive. Start with whatever's in
<project>/.unibench-data/<service>/. - Fresh — wipe every service's data dir before the start cycle
begins. Confirmation prompt unless
--yesis passed. - Seed — wipe + run every script in the manifest's
[seed].scriptslist (in declaration order) after the stack settles. Used for fixture loading.
Sandbox mode is a per-invocation choice; the manifest doesn't pin a
default sandbox (samples carry a default_sandbox field that the GUI
sample picker uses as a hint, but unibench start always defaults to
Resume).
Open / close semantics
A project is either open or closed from the daemon's perspective:
unibench open <path>reads the manifest, builds the dependency graph, allocates an orchestrator. Idempotent — opening an already-open project is a no-op (the existing orchestrator handle stays live; manifest changes don't re-read).unibench close <path>stops every service and drops the orchestrator handle.
A single daemon instance can hold multiple projects open at once. The Global view in the GUI surfaces every open project's services side by side; shared-isolation services (Ollama, decision 2.46) appear once across projects rather than per-project.
Recents + state persistence
The daemon's state store (~/Library/Application Support/unibench/state.db)
remembers:
- the last-N opened projects (powers the sidebar's Recents list)
- the last
last_active_projectso the GUI restores it on launch - per-(project, service) last-known states for "resume on relaunch" semantics (decision 2.42)
- the audit log (every project start/stop, every service health transition, every recipe apply)
This store has no PII beyond the project paths and is local-only. Wiping it (or just deleting the file) resets the daemon's memory but doesn't touch the projects themselves.
Single-window, single-daemon
By design (decision 2.42) there is one Unibench daemon process per macOS user account and one GUI window. The daemon is spawned on demand by the first IPC client connection and exits a few seconds after the last client disconnects.
The CLI (unibench …) and GUI both go through the same IPC surface, so
mixing them works without surprises — open a project in the GUI, drive
unibench start from the terminal, watch the graph update live.