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 --force is passed.
  • unibench samples new <name> [--target <dir>] — materialize one of the blessed sample projects (unibench samples list for the catalog) into <target>. Writes the manifest plus the sample's code files. Default target is ~/Documents/unibench/<name>/; pass --target for any folder you control.
  • GUI's "Try a sample…" picker — same as samples new but 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. Postgres PGDATA, MinIO storage, Qdrant collections live here. The user-visible side effect of unibench 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:

  1. Install — fetch the binary into the user-global cache. Idempotent — already-cached versions skip straight through.
  2. Init — first-run setup (initdb for Postgres, generate root creds for MinIO, etc.). Writes into <project>/.unibench-data/<service>/. Skipped on subsequent starts.
  3. Start — spawn the supervised process. The orchestrator owns the PID and surfaces logs through its ring buffer.
  4. Healthcheck — three retry attempts (5s / 15s / 45s by default; configurable per service via healthcheck_retries). The service moves from StartingRetryingHealthy or Failed.
  5. Stopunibench stop or 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 --yes is passed.
  • Seed — wipe + run every script in the manifest's [seed].scripts list (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_project so 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.