# cezar reference Everything the [README](../README.md) leaves out: the full configuration, every environment variable, agent backends, remote access, multiple projects and local development. - [Core concepts](#core-concepts) - [Cockpit tour](#cockpit-tour) - [Multiple projects, one cockpit](#multiple-projects-one-cockpit) - [Workflow format](#workflow-format) - [How it runs agents](#how-it-runs-agents) (environment variables, troubleshooting) - [Talking to a running cockpit](#talking-to-a-running-cockpit) (`cez runs`, `cez task`, `cez automation`, the HTTP API) - [Coding agent backends](#coding-agent-backends) - [Remote access (host cezar on a server)](#remote-access-host-cezar-on-a-server) - [Configuration](#configuration-optional) - [Worktree lifecycle scripts](#worktree-lifecycle-scripts) - [Nightly and preview builds](#nightly-and-preview-builds) - [Local development](#local-development) --- ## Core concepts Three words, no jargon β€” **task**, **skill**, **chain**: - πŸ“‹ **Tasks** are the unit of work. Every task is a **run**: `queued β†’ running β†’ review / done / failed / cancelled`, with a live event log, per-step token and cost usage, cancel/delete, and β€” for anything with a diff β€” a review gate. Attach screenshots, PDFs, `.txt` or `.md` files to the task (paperclip, ⌘V or drag-drop; the agent gets each one as a real file on disk), or send follow-up messages into the live session while it works. - πŸ“– **Skills** are Markdown playbooks. Drop them in `.ai/skills/` or `.ai/cezar/skills/`, or pull them from a shared **team skills repo** (a bare git clone cached globally in `~/.cache/cez/`). A workflow step references one by `skill: ` and its body becomes the agent's extra system prompt β€” so you shape *how* the agent reasons without touching code. - πŸ”— **Chains (workflows)** stitch steps into a pipeline: agent steps plus shell checks, with bounded `onFail` retry loops. Write the YAML yourself, build one by drag-ordering skills in the **Workflows** tab, or press **Plan first** and let the AI draft a chain for your task that you review, trim and start. The built-in `quick-task` (one agent step) works with zero setup. Five moves that make the cockpit worth the browser tab: - πŸ—ƒοΈ **Queue + orchestration.** Start as many tasks as you like: cezar runs up to `maxParallel` at once across every project (default **2**; a non-git directory always runs one) and holds the rest in a FIFO queue with visible positions (`#1`, `#2`, …). Cancel a queued task before it starts; the queue even survives a cockpit restart β€” everything still `queued` is re-enqueued in order. It's the orchestration layer that turns "one agent at a time" into a backlog that drains itself. - 🧠 **Memory-aware runs.** Each run's whole process tree is sampled (~2 s) for CPU and RSS, and its **peak memory** is recorded and shown in the task table. Set an optional per-task **memory ceiling** (`memoryLimitMb`) and a run that crosses it is *paused* β€” freeing its tree so the queue keeps advancing β€” and resumes on demand. Event logs are append-only NDJSON and streamed rather than re-serialized, and live UI deltas are coalesced so they never hit disk. - πŸͺž **Parallel variants (Γ—2 / Γ—3).** Run the same task as competing agents in separate worktrees, then compare their diffs side by side and **pick** one β€” the losers are archived and their worktrees cleaned up. - 🧹 **Bounded worktree disk.** Each task runs in its own full checkout, so a busy cockpit would otherwise grow without limit. cezar keeps only the last `worktreeRetention` **finished** worktrees on disk (default **10**; `0` = unlimited) and reclaims the rest β€” directory only, the `cez/` branch is always kept, so the work stays recoverable. Settings β†’ Resources shows every worktree's disk use with per-row delete and a **Reclaim now** button. - πŸ›‘οΈ **Review gate.** A finished run with changes waits in `review`. Read the diff, type notes that go straight back into the agent's session, or push a `gh pr create --draft`. You stay the merge button. - πŸ“± **Runs on your coding server, drives from your pocket.** The cockpit is a responsive web app streaming over SSE, so the box running cezar can be a **VPS, cloud, or dedicated server** you never sit in front of. Point a browser β€” laptop or **phone** β€” at it and run an **always-on coding team** on the move: start tasks, watch them live, and hit the review gate from anywhere. --- ## Cockpit tour One browser window, with live task updates over Server-Sent Events: | View | What's in it | |---|---| | **Dashboard** | **Overview** shows attention, completed/failed outcomes, median cycle time, project comparisons, live work, enabled automations with next run/check times, and recent results; **Usage & cost** shows reported usage and trends. Counters open matching tasks. Drag widgets to reorder them, customize optional tiles and export the current view to PDF/CSV; saved layout is shared by browsers using this workspace. | | **Tasks** | Every task with its status, live event stream (agent text Β· tool calls Β· tool results Β· pasted/generated screenshots and file attachments), tokens and cost. Continue, cancel, open in terminal (`claude --resume`), review the diff, or push a draft PR. The composer can also **start a task later** β€” at a date and time in the server's time zone, or after the account's observed usage-limit reset β€” and such tasks wait under **Scheduled** until then (#1340). They are stored with the project's automations in `.ai/cezar/automations.json`; observed reset times are cached, without any model call, in `~/.cezar/usage-windows.json`. The server must be running at the start time. | | **All tasks** | Every *registered project's* tasks in one table, filtered and grouped by tag, project, status or workflow β€” see [Grouping connected repositories](#grouping-connected-repositories-tags-and-the-all-tasks-page). Appears once a second project is registered. | | **Inbox** | **Opt-in** (`CEZ_FOLLOWUPS=1`; hidden by default). Follow-ups an agent left behind (`todos.json`) β€” one click turns a suggestion into the next task, pre-wired to its suggested skill. Off, agents are never asked to leave follow-ups; each task's own **Notes** handoff journal is unaffected. | | **Git** | Branch, working-tree status, diff vs HEAD, recent commits (click one for its inline patch + GitHub link), and the configurable base branch that worktrees fork from and PRs target. **Files** browses the whole repository β€” the tree on the left, the file on the right (syntax-highlighted, images inline, Markdown rendered with a toggle to the source), and a filter box that finds a path as you type. Every file is its own URL (`/git/files/`), so a view can be refreshed or pasted to a colleague. The tree is git's own view of the repository, so `.gitignore`d build output and `node_modules` never appear β€” and a file git does not list cannot be opened, which is what keeps an ignored `.env` out of reach. Read-only: nothing here edits the checkout. | | **GitHub** | Open issues and PRs of the repo's origin, read through your logged-in `gh`. Hand an issue straight to the agent β€” pick a workflow and skills, one click runs it. GitHub Enterprise works the same on any domain: a remote counts as GitHub when its host is `github.com`, `*.ghe.com`, or one `gh` is signed in to (`gh auth login --hostname `, or `GH_HOST`). A self-hosted server on any other domain needs that login first. github.com and Enterprise projects work side by side; cezar pins the right host per repo, so `GH_HOST` never has to be exported. | | **Skills** | Local skills plus the team skills repo, with a rendered body + prompt preview. Refresh pulls the latest from the remote. | | **Workflows** | Build a workflow as a node graph, or let the planner draft one from a description; save it as YAML (a plain stack of skills stays in the portable `skills:` form), import/export, or delete. Built-ins always come back. | | **Settings** | Appearance (dark/light theme, accent, density), agent backends, notifications, and the skills catalog. | **Scheduled-start recovery:** a creation-key replay does not create another schedule, and the scheduler reconciles retained run records before retrying a partial start. This is not a global exactly-once execution guarantee: independent cockpits can recover the same queued run, and **Start missing runs** can recreate a variant whose record was deleted manually or by retention. Check existing work before retrying; **Dismiss** closes an ambiguous entry without starting work. The cockpit is a React app served pre-built from the package β€” `npx cezar-run` still means no build step and no dev server on your machine β€” with a dark/light theme, a ⌘K command palette, and bookmarklets that launch a task straight from a GitHub page. **⌘P / Ctrl+P** acts on one task: on a task page it lists that task's actions (pin, archive, rename, stop, the git step, views, layouts, Open in…, copy). The prefix picks the mode: `@` to switch tasks (a PR or issue number finds every task about it β€” in ⌘K too), `>` for commands only, `#` for the task's files. Repeated ⌘P walks back through recent tasks; Tab on a task row (here or in ⌘K) acts on that task without opening it; βŒ₯↡ opens a view beside the current one. Shortcuts run those commands with the palette closed: ⌘1β€“βŒ˜9 switch the task's tabs and ⌘/ opens the terminal in a column; hold ⌘ to see each tab's number. Rebind any command with ⇧↡ on its row, or in Settings β†’ Keyboard. Shortcuts are saved in `~/.cezar/ui-state.json`. --- ## Multiple projects, one cockpit One `cezar serve` hosts **every repo you work in**, not just the one you started it in. Projects live in a per-user registry at `~/.cezar/config.json` β€” the workspace file that also holds the global knobs (the parallel cap, the memory ceiling, the browse root, and the checkout root). Nothing is added to the repo: per-project state stays exactly where it was, in that repo's `.ai/cezar/`. **Your first run registers the repo you start it in** β€” that is the whole setup. After that the registry is yours to curate: starting cezar somewhere else serves that folder as usual (its own tasks, its own `.ai/cezar/`, reachable at `/p//`) and opens the browser on it, but does not add it to the list. While you are in it the sidebar shows it marked **not saved**; switch to another project and it drops out, so the sidebar keeps showing the projects you chose. A bare `/` opens the page you were last on, or the most recent project. Keeping the folder is the explicit click: **Add project** in its Settings (or `cezar projects add` from a terminal). Project views are scoped to their repository; **Dashboard** at `/dashboard`, **All tasks**, and global settings span the workspace: ``` /p// tasks Β· git Β· github Β· skills Β· workflows Β· settings ``` `` is a slug derived from the folder name (`my-app`, then `my-app-2` on a collision), and `/p/default/…` always means the project cezar was started in. The sidebar shows one collapsible group per project β€” each with its own nav and task list β€” and the new-task composer names the project it will run in. **Adding a project** β€” the **+** button beside *New task*: - πŸ“‚ **Open local folder…** browses from the configured browse root (**Settings β†’ Projects**, default `~/`) in a folder picker and registers the folder you pick. - ⬇️ **Clone from GitHub…** clones with your logged-in `gh` into the checkout root (**Settings β†’ Projects**, default `~/cezar/projects`) with live progress, then registers the clone. Close the dialog and the clone is killed and its partial directory removed. Removing a project (**Settings β†’ Projects**) drops the registry entry only β€” the repo and its `.ai/cezar/` are never touched, so re-adding it later finds all its tasks intact. The project cezar is currently serving can't be removed from the cockpit β€” stop the server and use `cezar projects remove ` instead. **From the terminal** β€” the same registry, no cockpit required (handy over ssh): ```bash cezar projects # list: id, branch or status, path, tags cezar projects add ~/code/api # register a folder (defaults to the current repo) cezar projects remove api # drop the registry entry; the repo is untouched cezar projects tag api storefront backend # set the grouping tags (no tags clears them) ``` These read and write `~/.cezar/config.json` directly, so they work with the server stopped, and `CEZ_HOME` selects which workspace they operate on. Settings is one full-screen surface with one navigation (the app sidebar is hidden there; **Back** or Escape returns to the page you came from). The **General settings** group β€” **Appearance**, **Notifications**, **Resources**, **Skills**, **Secrets**, **Agent accounts**, **Projects**, **Privacy** β€” is yours or the machine's and lives at `/settings/global`. Below it, a **Project settings** tree lists every registered project; each opens into **General** (the project's folder, its registry facts, its parallel-task ceiling, and Remove), **Tracker**, **Secrets**, **External integrations**, **Agents**, **Instructions** (system prompt, branch naming, prompt templates), **Tasks**, **Agent config**, **Worktrees** and **Bookmarklets**, which describe that one repo and live under `/p//settings`. On any project page, clicking another project in the tree opens the same place there: the same section, or its General page when General is open; the previous project folds. ### Grouping connected repositories: tags and the All tasks page Work rarely stops at a repo boundary. A storefront is an API, a web app and a design system; a platform is a handful of services plus the infra that runs them. **Tags** are how you say so, and **All tasks** is where saying so pays off. **Tag a repo** in **Settings β†’ Projects**: type into the *Tags* cell on its row and press Enter (comma works too; the Γ— on a chip, or Backspace in an empty field, removes one). The field **autocompletes from the tags already used in the workspace** β€” click the field to see them all, arrow keys and Enter to pick β€” which is what keeps the second repo landing on the first one's spelling instead of inventing `store-front` next to `storefront`. Anything not on the list is just typed. A tag is a free-form label β€” `storefront`, `infra`, `client-acme` β€” and a project can carry several, because a repo can belong to more than one piece of work. Tags are trimmed, deduplicated case-insensitively (`API` and `api` are one tag) and stored in `~/.cezar/config.json` beside the rest of the registry, so they are yours and this machine's, never something added to the repo. **All tasks** β€” the top item in the sidebar, `/tasks`, or `⌘K β†’ All tasks` β€” then shows every registered project's work in one table: - **Filter** by tag, status and workflow. Tags are one-click chips; status and workflow are searchable multi-selects. Every facet ORs inside itself and ANDs across, so *"anything running or waiting in storefront or infra"* is one set of clicks. Each option carries how many tasks it would leave, so a filter that would empty the table says so before you click it. The search box matches title, project, workflow, branch and tags. - **Group by** tag, project, status or workflow β€” click the pressed one again to ungroup. Grouping by tag is the reason tags exist: three repos tagged `storefront` become one section, and a repo tagged twice appears under both β€” it genuinely belongs to both. The filters, the grouping and the Active/Archived tab live in the **URL**, so a filtered view survives a refresh, pastes into a chat, and sits in a bookmark β€” `/tasks?tag=storefront&status=running&group=tag` is a link to exactly what you were looking at. Only what you changed shows up: Active is the default, so the Archived view is `?archived=1` and a normal link carries no key for it. Each row shows **every** PR and issue it references β€” a task opened on an issue that landed a PR shows both β€” plus its cost and live CPU/memory, and can be marked **read/unread** (the eye) or **archived** (or restored) right there. Every task title, project name and project group heading links into that project, so the thread, its diff and its worktree are one click away and stay exactly where they were. There is deliberately **no project filter**: narrowing this page to one project is that project's own Tasks page, which is a better version of the same answer (live updates, the full column set, the composer). So picking a project *leaves* for it rather than turning the global view into a worse local one. Nothing else in cezar reads tags, on purpose: a tag is a lens, not a permission, a queue or a routing rule. Removing one changes what you see and nothing else. > The page reads a workspace-wide index capped at the newest 200 tasks per > project β€” it says so, and names the projects it capped, rather than showing a > short list as if it were complete. Older tasks are always in that project's own > Tasks page. **Old page URLs keep working.** Every unprefixed page path β€” `/`, `/tasks/`, `/settings` β€” still answers, bound to the project cezar was started in; the cockpit redirects flat paths to their `/p//…` twin, so existing bookmarks and bookmarklets need no change. The HTTP API is the exception: it moved to `/api/v1/…` (see the CHANGELOG and [The HTTP API](#the-http-api)), so a script that calls it needs the extra segment. > **Hosted cockpit?** The folder picker is confined to the independent browse > root. Set `CEZ_BROWSE_ROOT` narrowly before first boot (or save it in > **Settings β†’ Projects**) when a remote viewer should not enumerate the host's > whole home. Clones continue to use the separate checkout root. --- ## Workflow format A workflow is a small YAML file in `.ai/cezar/workflows/`: ```yaml name: fix-and-verify description: Implement the task, then verify; retry with failing output on red. steps: - id: implement name: Implement prompt: "{{task}}" skill: project-conventions # optional β€” from .ai/skills or .ai/cezar/skills # model: opus # optional per-step model override # runner: codex # optional per-step backend: claude Β· codex Β· junie Β· opencode Β· pi # allowedTools: [Read, Edit, Write, Grep, Glob, Bash] - id: verify name: Verify command: "npm test" # a check step: exit 0 passes onFail: retry: implement # loop back to an earlier step… max: 2 # …at most twice # retryOn: [1] # optional: only these exit codes loop back ``` `{{task}}` is replaced with the task text you typed. When a check fails and loops back, its failing output β€” with host secrets, the run's registered secrets and known token shapes redacted β€” is appended to the retried agent's prompt so the next attempt can see what broke. A check step runs `bash -lc ` in the task's worktree with the server's environment plus the **secrets** it may read, which no agent step ever receives. A secret has a scope β€” the project (Settings β†’ *Secrets*, `cezar secrets …`) or the whole workspace (Settings β†’ Global β†’ *Secrets*, `cezar secrets … --workspace`) β€” and audiences: `checks` (check steps) and/or `cezar` (cezar's own features). By default a check step gets every `checks`-audience secret of the workspace and the project, the project's winning on a shared name; a step can bind exactly the ones it needs instead: ```yaml - id: e2e command: npx e2e run secrets: - E2E_GATEWAY_KEY # as itself - { name: STAGING_LOGIN, as: APP_PASSWORD } # renamed on the way in ``` A bound name that is not stored, or that `checks` may not read, is said on the step as a note; `secrets:` on an agent step is a load-time error. Manage secrets with `cezar secrets list | set [--audience checks,cezar] | unset ` (`set` reads the value from stdin; nothing is taken from argv or printed back) or in Settings. Names are `[A-Z_][A-Z0-9_]*`; `CEZ_*`, `DYLD_*` and shell/loader variables (`PATH`, `HOME`, `NODE_OPTIONS`, `LD_PRELOAD`, …) are refused. This is where an e2e runner's model key belongs: an exported `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` also reaches the agents and switches them to API billing. Values are stored encrypted (AES-256-GCM) under `~/.cezar/secrets/`, with one per-machine data key in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager) through the optional `@napi-rs/keyring` package. Without a usable keychain β€” a server, a container, an install that skipped optional packages, or `CEZ_SECRETS_KEYCHAIN=0` β€” the data key is a `0600` file beside the secrets, which makes the store exactly as strong as a private plaintext file; Settings and `cezar secrets list` say which one is in use. A secret is written for one machine: a reset keychain or a deleted key file leaves the store unreadable (reported on the run as a note) until the secrets are stored again. cezar also tells a check step about the run it is verifying. These are set by cezar, not settings β€” a variable the run does not have is absent (never empty), and a value inherited from the server's own environment is replaced: | Variable | Value | |---|---| | `CEZ_RUN_ID` | the run id | | `CEZ_PROJECT_ID` | the project id | | `CEZ_WORKTREE` | absolute path of the task's worktree (the check's cwd) | | `CEZ_BRANCH` | the run's branch | | `CEZ_BASE` | the recorded fork point | | `CEZ_STEP_ID` | this check step's id | | `CEZ_ATTEMPT` | 1-based attempt number of this check within the run | | `CEZ_SHARED_CACHE_DIR` | `~/.cezar/cache/` (`0700`), shared by the project's tasks β€” e.g. an e2e replay cache | | `CEZ_GITHUB_REPO`, `CEZ_GITHUB_NUMBER`, `CEZ_GITHUB_EVENT` | runs a GitHub automation launched only | | `CEZ_PR_HEAD_SHA`, `CEZ_PR_HEAD_REF`, `CEZ_PR_BASE_REF` | runs a `checkout: "pr-head"` automation launched on a pull request's head only | `CEZ_PROJECT_ID` is the one name in that table cezar also reads as an *input* (it is how `cez task` and `cez automation` address a cockpit, see [backward compatibility](../BACKWARD_COMPATIBILITY.md) Β§1). A run that knows its own project replaces it; a headless `cezar run` that does not leaves whatever you exported alone, so a check step can still reach the cockpit you named. A GitHub automation whose events are all `pull_request.*` can set `"checkout": "pr-head"` in its `task`: each launched run forks its worktree from the matched PR's head (fetched into `refs/cezar/pr/`) instead of the base branch, so its check steps verify the PR itself. A closed PR or a fork head launches nothing (`skipped` in the execution log) unless `"allowForkHeads": true` admits forks β€” whose checks then run without secrets. Such a run cannot open a draft PR (`409`). See [Verify a pull request](e2e-verification.md#verify-a-pull-request). `onFail.retryOn` narrows the loop to the exit codes that mean *the work is wrong*. Omitted, any non-zero code loops back β€” right for `npm test`, which exits 1 whether a test failed or the runner could not start. A richer check distinguishes the two: an `e2e` browser run exits 1 on a failed test, but 2 on a config or credential error and 3 on an engine failure, none of which the agent can fix and each of which would otherwise cost a full agent attempt per retry. `retryOn: [1]` loops on the verdict and fails the run on the infrastructure, naming the code. See [browser and mobile e2e as a verification step](e2e-verification.md) for the worked chains, including an independent QA exploration as the gate. Prefer skills over steps? A workflow can also be written in the portable shorthand β€” an ordered list of skill names, each becoming one agent step: ```yaml name: triage-and-fix skills: [reproduce, root-cause, implement, self-review] ``` --- ### Workflow graphs (`version: 2`) The **Workflows** page is a node editor: every step is a block on a canvas, and the graph shows exactly how a task will run β€” which edge a failing check takes, where a loop goes back, what happens when it gives up. It saves a `version: 2` file next to the step-list ones (both keep loading; a step-list file opens as a graph): ```yaml version: 2 name: implement-test-review nodes: - { id: start, type: start } - { id: implement, type: agent, prompt: "{{task}}" } - { id: tests, type: check, command: npm test } - { id: retry, type: loop, max: 3 } - id: fix type: agent session: { continue: implement } # reopen implement's session (claude Β· codex Β· pi) prompt: "Tests failed ({{nodes.retry.iteration}}/{{nodes.retry.max}}): {{nodes.tests.output}}" - { id: review, type: agent, skill: code-review, verdicts: [approve, changes] } - { id: ok, type: end, status: success } edges: - { from: start, to: implement } - { from: implement.done, to: tests } - { from: tests.pass, to: review } - { from: tests.fail, to: retry } - { from: retry.repeat, to: fix } - { from: fix.done, to: tests } - { from: review.approve, to: ok } - { from: review.changes, to: retry } ``` **How a graph runs.** One node runs at a time. A node finishes by leaving through exactly one **output port**; the edge wired to that port is the next step. An unwired port ends the run β€” failed for failure ports (`failed`, `fail`, `exhausted`, `reject`, `red`, `timeout`), successful otherwise. Every cycle must pass through a **loop** node, whose counter never resets within a run, so every run is finite. | Node | Ports | Outputs (`{{nodes..}}`) | | --- | --- | --- | | `start` / `end` | `next` / β€” (`status: success\|failed`) | β€” | | `loop` (`max`) | `repeat`, `exhausted` | `iteration`, `max` | | `agent` (`prompt`, `skill`, `runner`, `model`, `session.continue`, `verdicts`; after a fork also `review`, `budgetUsd`) | `done`, `failed` β€” or one port per verdict | `summary`, `verdict`, `costUsd` (a fork branch: `runId`, `status`, `summary`) | | `check` (`command`) | `pass`, `fail` | `exitCode`, `output` | | `gate.human` (`message`, `timeoutMs?`) | `approve`, `reject` (+`timeout`) | `comment` | | `ask-user` (`question`, `options?`, `timeoutMs?`) | `answered` (+`timeout`) | `answer` | | `dispatch` (`prompt`, `runner?`, `budgetUsd?`) | `done`, `failed` | `runId`, `status`, `summary` | | `git.commit` (`message`) | `done`, `nothing`, `failed` | `sha` | | `github.draft-pr` (`title?`) | `created`, `failed` | `url`, `number` | | `github.wait-ci` (`timeoutMs`, `pollMs`) | `green`, `red`, `timeout`, `failed` (the task has no PR) | `status` | | `github.pr-comment` (`body`) | `done`, `failed` | β€” | | `fork` (`branches: 2–4`) | `1` … `4`, each wired to an agent | `runIds` | | `join` (`wait: all\|any`) | `done`, `failed` | `succeeded`, `failed` | | `workflow` (`workflow`, `prompt?`, `runner?`, `budgetUsd?`) | `done`, `failed` | `runId`, `status`, `summary` | | `if` (`condition`) | `true`, `false` | `result`, `value` | | `git.push` | `done`, `failed` | β€” | | `git.sync-base` | `done`, `conflict`, `failed` | `conflicts` | | `github.pr-update` (`ready?`, `addLabels?`, `reviewers?`) | `done`, `failed` | β€” | | `github.issue-comment` (`body`, `issue?` β€” default: the task's issue) | `done`, `failed` | `issue` | | `notify.webhook` (`url`, `body?` β€” needs `CEZ_WORKFLOW_WEBHOOKS=1`) | `done`, `failed` | `status` | - **Fork / join.** Each fork port leads to one agent node, and every one of those agents leads into the same `join`. The agents run at once as subtasks through dispatch β€” fresh sessions, each in its own worktree forked from the task branch (the fork commits the task's work first), with a budget carved from the parent's, at most four in flight. An agent with `review: true` runs as a reviewer of the task branch instead of an implementer. `wait: any` takes the first branch to succeed and cancels the rest. Sub-workflows run as subtasks the same way. - **`if` conditions**: `diff-lines` / `diff-files` (`op`, `value`) on the task's own diff, `paths-changed` (`glob`: `*` within a folder, `**` across), `output` (`ref: .`, `op`, `value` β€” numeric when both sides are numbers) and `branch` (`equals` / `matches` on the base branch). - **`git.sync-base`** merges the freshest base into the task branch. On a conflict it leaves the merge in progress and leaves by `conflict` β€” wire that to an agent to resolve it (`{{nodes..conflicts}}` lists the files). - **Built-in templates** β€” `implement-and-verify`, `implement-review-pr`, `review-council` (three fresh reviewers at once, then a judge that ships or sends it back), `fix-ci` β€” sit next to `quick-task` (still the default). Open one in the editor; it saves as your copy. - **Restarts** resume a walk parked at a gate, question, CI wait or subtask (or mid-check) at that node; an interrupted agent node keeps the usual Continue path. - **Verdicts.** An agent with `verdicts` ends its turn with `CEZ:VERDICT `; the graph branches on it. No verdict β†’ one reminder in the same session, then `failed`. - **Waits hold no slot.** A gate, question, subtask or CI wait gives its `maxParallel` slot back while it waits, and always has an exit: the answer, Cancel, Finish, its timeout, or a restart. - **`CEZ_DRY_RUN=1`** fakes the PR and reports CI green, so a graph can be walked end to end offline. In the editor, **Simulate** walks it without running anything. - **Watching a run.** A task started from a graph workflow gets a **Graph** tab: each node's status, iterations and cost, loop counters, and the edges the run took. ## How it runs agents cezar shells out to your locally installed, logged-in agent CLI β€” **your subscription, no API key**. With the default Claude Code backend that means headless `stream-json` mode, tool access via `--allowedTools`, with unapproved tools denied without prompting (`--permission-mode dontAsk`) inside the task's worktree β€” but note the zero-config default list (`Read`, `Edit`, `Write`, `Grep`, `Glob`, `Bash`) grants unrestricted `Bash` unless a step sets `bashAllowlist`, so treat a run as having full shell access in its worktree, not a sandboxed allowlist. Set `CEZ_APPROVAL_GATE=1` to select Claude's `acceptEdits` mode, but cezar has no cockpit permission response channel, so approval prompts are not actionable in the cockpit. Codex, Junie and OpenCode are driven through their own native protocols and don't honor `allowedTools` at all β€” see [Coding agent backends](#coding-agent-backends) for what each one actually locks down. Nothing runs on a server you don't own. Useful environment variables: | Var | Effect | |---|---| | `CEZ_DRY_RUN=1` | Use the bundled mock instead of the real `claude` CLI β€” the entire cockpit works offline, for demos and development. | | `CEZ_REMOTE_AGENT_ACCOUNTS=1` | Allow **Settings β†’ Agent accounts** in a remote/self-hosted cockpit. Enable this only behind an authenticated perimeter: account folders expose absolute host paths and the details view can expose account identity. Remotely added folders are confined to `CEZ_BROWSE_ROOT`, including symlink ancestry. Local desktop actions stay disabled; **Connect** returns the exact login command to run on the host. | | `CEZ_TELEMETRY=0` | Block usage telemetry even after you opted in. Telemetry is opt-in β€” nothing is sent until you answer yes in the cockpit, Settings β†’ Privacy or `cezar telemetry enable` ([docs/telemetry.md](telemetry.md)). `DO_NOT_TRACK=1` works too. `CEZ_TELEMETRY=1` only lifts the CI / test-runner / `CEZ_DRY_RUN=1` block; it never grants consent. | | `CEZ_INSTANCE_ID` | Internal server-install identity set automatically in generated systemd/launchd services; normally leave unset. It is surfaced additively by `/api/v1/health` for install verification. | | `CEZ_AGENT_MODELS_LOCKED=1` | Globally lock each runner to the model configured in its native Claude/Codex/OpenCode settings while keeping runner selection available. Exact `1` also delegates authentication and provider enablement to those native agents, so Cezar skips its credential probes and provider-disable preferences. Existing Cezar presets are preserved but ignored, and an environment change requires a restart. The config-file equivalent is `"modelsLocked": true` in global `~/.cezar/config.json` or one repository's `.ai/cezar/config.json`; config-file locks do not disable provider checks. | | `CEZ_APPROVAL_GATE=1` | Select Claude's `acceptEdits` mode. Cezar has no cockpit permission response channel; by default, unapproved tools are denied without interrupting the run. | | `CEZ_RUNS_KEEP=` / `CEZ_ARCHIVED_RUNS_KEEP=` | How many settled runs (and archived ones) a project's index keeps before the oldest are pruned. Defaults 300 / 500, minimum 1; live runs and runs pinned by an active dispatch tree are never pruned. | | `CEZ_DESIGN_MODE=0` | Turn off Design Mode in a task's **Preview** tab (spec `.ai/specs/2026-10-09-design-mode.md`). Unset, it is on for a local cockpit: switching it on makes cezar re-serve the previewed local `http://` dev server on a second `127.0.0.1` port with an element picker injected, so you can click an element and send the agent a note about it. The port opens only on that click and closes when idle. Always off on a hosted cockpit, where a loopback port reaches nobody β€” there is no `=1`. | | `CEZ_FOLLOWUPS=1` | Turn on the global follow-up **Inbox**: agents are asked to leave follow-ups in `todos.json` when they finish, and the Inbox view appears. Off by default β€” each task's own **Notes** handoff journal runs either way. | | `CEZ_AUTOMATIONS=0` | Turn **automations** off. On by default since the automations redesign (spec `.ai/specs/2026-09-14-automations-redesign.md`): the Automations view lists GitHub-triggered and scheduled automations, and cezar polls GitHub or fires schedules on each enabled one while it is running β€” nothing runs until you enable an automation yourself. An agent can also **create an automation from a prompt**: type "whenever a PR is opened, review it" into New task β€” pick the built-in `create-cezar-automation` skill, or just ask; every task's system prompt teaches it to recognise the intent β€” and the agent writes the definition and creates it through `cez automation create` (paused, with a `cez automation check` preview of what it would match), then links the Automations page. Only the exact value `0` opts out (`CEZ_AUTOMATIONS=1`, the old opt-in, is accepted and changes nothing); opted out, the nav item is absent, the endpoints answer `409`, and the workspace scheduler never starts. Read at boot, so restart after changing it; definitions, receipts and high-watermarks are kept either way. | | `CEZ_WEBHOOKS=1` | Enable project lifecycle notifications in Settings β†’ Webhooks. Off by default; independent of graph workflow notifications. See the [receiver guide](../packages/cezar/docs/webhooks.md). | | `CEZ_WORKFLOW_WEBHOOKS=1` | Let a workflow graph's **`notify.webhook`** node POST to the URL it names (JSON: `text`, `run`, `node`). Off by default: a workflow file β€” which can be imported from anywhere β€” calling out to an arbitrary URL widens exposure, so it is opt-in. Off, the node leaves by `failed` with a note; under `CEZ_DRY_RUN=1` nothing is sent either way. | | `CEZ_TERMINAL=1` / `=0` | The task view's interactive **terminal** (spec `.ai/specs/2026-10-07-task-workspace.md` Β§6). Unset it is on for a local cockpit and off on a hosted one (`CEZ_REMOTE=1` or a non-loopback bind). `1` allows it there too β€” without a sign-in in front of cezar this hands a shell on the server to anyone your reverse proxy admits. `0` turns it off everywhere. A platform with no prebuilt PTY binary reports the terminal unavailable with a reason instead of failing. | | `CEZ_DISPATCH=0` | Turn OFF **task dispatch**, which is on by default: a running task may start other cezar tasks with `cez task create` β€” each in its OWN worktree forked off the parent's branch, with a budget carved out of the parent's β€” and they report back into the parent's session when they settle (`cez task report`). Children appear nested under their parent in the task lists. Tasks talk through a tree directory (`.ai/cezar/dispatch//`: the brief, each task's order/notes/report, an inbox per task) and cezar wakes a parked recipient when a file lands. A `--kind review` child judges another task's branch and answers with a verdict. ON by default (the owner-approved exception to "cost-widening features are opt-in" β€” see `AGENTS.md`), and only the exact value `0` turns it off; with it off the `/runs/:id/dispatch` and `/runs/:id/report` routes answer `409`, no task is told about the CLI, and the cockpit hides the "Review open PRs" template. A headless `cezar run` never dispatches either way β€” there is no cockpit for the CLI to reach, so no task is told about it. Read at boot, so restart after changing it. This is the widest cost-widening flag here β€” one task can start four more agents β€” so give dispatching tasks a budget. | | `CEZ_TASK_PROCESS_REAP=0` | Turn OFF **leftover-process reaping**, which is on by default (#1411): when a task settles, cezar stops every process still carrying that task's `CEZ_TASK_ID` in its environment β€” a hung test binary, a dev server, a `setsid`ed background job β€” with SIGTERM five seconds after the settle and SIGKILL three seconds later, and notes what it stopped in the task's timeline. On boot it does the same for tasks that settled while cezar was down. A task you Continue within the grace is left alone. Linux (`/proc`) and macOS (`ps -E`) only. Only the exact value `0` turns it off. | | `CEZ_AUTOSAVE=1` | Re-enable the periodic (90 s) autosave commit in task worktrees. Off by default (#471) β€” turn-end and pre-PR flushes always run, so branches still end complete. Every autosave names its trigger in the commit subject (`cezar autosave (periodic)` vs `(turn end)` / `(run finalize)` / `(pre-PR)`), so the flushes you keep are distinguishable from the timer you disabled. | | `CEZ_CLAUDE_BIN=/path/to/claude` | Override which `claude` binary is used. Rarely needed: when it is unset, cezar takes `claude` from PATH, and failing that looks where Claude Code's own installers put it β€” `~/.local/bin`, `~/.claude/local`, `/opt/homebrew/bin`, `/usr/local/bin` β€” so an install the launching shell never added to PATH is still found. | | `CEZ_CODEX_BIN=/path/to/codex` | Override which `codex` binary is used. | | `CEZ_JUNIE_BIN=/path/to/junie` | Override which `junie` binary is used. | | `CEZ_OPENCODE_BIN=/path/to/opencode` | Override which `opencode` binary is used. | | `CEZ_CURSOR_AGENT_BIN=/path/to/cursor-agent` | Override which Cursor Agent CLI binary is used. Unset: `cursor-agent` on PATH, then `~/.local/bin/cursor-agent`, then `agent` on PATH. | | `CEZ_PI_BIN=/path/to/pi` | Override which `pi` binary is used. | | `CEZ_COPILOT_BIN=/path/to/copilot` | Override which GitHub Copilot CLI binary is used. | | `CLAUDE_CONFIG_DIR`, `CODEX_HOME` | The agents' **own** variables, honoured where the vendor documents one. Setting one moves that agent's **default account** β€” the config folder cezar discovers. A *second* login of the same CLI is deliberately not an environment setting, since one process-wide value cannot differ per project: add it under **Settings β†’ Agent accounts** and pick it per project. | | `CEZ_BROWSE_ROOT=~/` | Default root for **Add project β†’ Open local folder…**. The picker cannot navigate above it; a saved workspace value overrides the environment default and must name an existing folder. | | `CEZ_PROJECTS_DIR=~/cezar/projects` | Default destination for **Clone from GitHub**. Saved workspace settings override it, and missing directories are created recursively. | | `CEZ_SKILLS_AUTO_UPDATE=0` | Disable automatic checks and updates for upstream-CLI-tracked Open Mercato skill installations. On by default; a saved global Skills setting overrides this environment default. Checks are delayed, bounded, cached, and non-blocking. | | `CEZ_UPDATE_CHANNEL=nightly` | Release channel the self-updater follows: `stable` (npm `latest`, the default), `nightly`, or `development` (no automatic updates; pick a cezar worktree or an open PR's preview build by hand). A channel saved from the version chip's dialog overrides this seed. | | `CEZ_SUPERVISED=1` | A supervisor relaunches cezar (the desktop shell sets `CEZ_DESKTOP=1`, which implies it): after an update the process exits with status 75 instead of re-exec'ing itself, so the supervisor starts the new version. Off by default. | | `CEZ_AUTONOMOUS_DEFAULT=0` | Seed the New Task Autonomous default (`0` or `1`). Without a seed, skills default on and workflows off; a saved global Resources setting overrides it. | | `CEZ_WORKTREE_DEFAULT=1` | Seed the New Task Worktree default (`0` or `1`). Without a seed, eligible runs default on; a saved global Resources setting overrides it. | | `CEZ_DISABLE_REPO_LOCK=1` | **Dangerous escape hatch:** allow any run executing in the repository root β€” an explicit `worktree=false` run, non-Git degradation, or a continuation whose worktree cannot be restored β€” to proceed without Cezar’s repository-root lease. Agents can overwrite each other’s files or Git state; isolated worktree runs are unaffected. Off by default; only the exact value `1` enables it. | | `CEZ_SINGLE_PROJECT=1` | Opt into a launch-project-only cockpit: only the exact value `1` enables it. Project add, edit, checkout, folder browsing, and removal are refused and only the launch project is shown. Off by default; stored registry rows are retained, so unsetting it and restarting restores the full multi-project workspace without migration or data loss. | | `CEZ_HIDE_TOKEN_USAGE=1` | Hide raw input/output token counts throughout the browser cockpit while leaving backend-reported cost visible. Only the exact value `1` enables it; telemetry and API payloads are unchanged, and a restart is required after changing it. | | `CEZ_HIDE_COST=1` | Hide backend-reported monetary cost throughout the browser cockpit while leaving raw input/output token counts visible. Only the exact value `1` enables it; telemetry and API payloads are unchanged, and a restart is required after changing it. | | `CEZ_HIDE_TOKEN_METRICS=1` | Legacy master switch that hides both token usage and cost. It takes precedence over the two independent flags; only the exact value `1` enables it, payloads are unchanged, and a restart is required. | | `GITHUB_TOKEN` | Fallback for GitHub reads/PRs when `gh` isn't authenticated. | | `CEZ_ENV_PASSTHROUGH=A,B` | Forward these extra host env vars to spawned agents. By default agents get a least-privilege env (safe shell/toolchain vars + the backend's own auth + `GITHUB_TOKEN` + `CEZ_*`), not your full environment β€” use this to add a var an agent needs. | | `CEZ_AGENT_ENV_FULL=1` | Escape hatch: give spawned agents the full host environment (pre-hardening behavior). Off by default; only set it if you understand that this hands every host secret to the agent process. | | `CEZ_REMOTE=1` | Hosted/server-install mode. The configured port is authoritative so an occupied port fails the real bind with `EADDRINUSE` instead of drifting away from the reverse-proxy upstream; interactive local launches keep bounded auto-increment. Read at startup. | | `CEZ_AGENT_TMPDIR=0` | Stop giving each task its own temp directory and hand agents the host `TMPDIR` again (pre-#785 behavior). On by default: every run gets `TMPDIR`/`TEMP`/`TMP` pointing at `.ai/cezar/tmp/`, created and write-probed before the agent spawns and reaped when the run ends, so concurrent tasks stop sharing one directory and a task refuses to start rather than run against a temp directory that silently swallows its shell output (see Troubleshooting below). Only an exact `0` disables it, and it disables the whole thing β€” the pre-spawn check included, so this stays an escape hatch you can actually take. | | `CEZ_SECRETS_KEYCHAIN=0` | Keep the secret store's data key out of the OS keychain and use the `0600` key file `~/.cezar/secrets/.key` only. By default the key goes to the keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager, via the optional `@napi-rs/keyring`) when one is usable, and to the file otherwise; Settings β†’ Secrets and `cezar secrets list` say which. Only the exact value `0`. | | `CEZ_REDACT_SECRETS=0` | Disable scrubbing of credential values/token shapes from the on-disk state (the NDJSON transcript and the free-text fields of `runs.json`). On by default; leave it on. Best-effort defense-in-depth, not a guarantee: it catches known token shapes and the values of your own secret-named env vars, so a credential in neither category can still get through. | | `CEZ_TITLE_UPDATES=0` | Turn off the live task-title refresh (namer re-runs on each turn end). The Settings β†’ Tasks toggle overrides this default. | | `CEZ_AUTONAME=0` | Disable ALL LLM task naming (creation + live) β€” titles stay heuristic (`437: /om-auto-review-pr`). Under `CEZ_DRY_RUN=1` naming is already off unless forced with `CEZ_AUTONAME=1`. | | `CEZ_REVIEW_GATE=1` | Turn ON the optional diff-first review gate (#489): a successful, non-autonomous run with changes parks at `review` (Accept / Send back / Draft PR) instead of finishing. Off by default β€” changed runs settle to `done` with the diff left in the worktree. Only `1` enables. The Settings β†’ Tasks toggle overrides this; autonomous runs always skip it. | | `CEZ_NO_BANNER=1` | Silence every promo: the `open-mercato/skills` banner and the star-ask line on `cezar serve` startup, the sidebar's ⭐ chip, and the star-ask dialog (shown at most three times per browser, only after a task ends well once three have, and only while you are at the screen). It is also the star count's off switch β€” with it set, `GET /api/v1/star-count` answers `available: false` and cezar makes no request to github.com for it. Unset, the count refreshes every 10 seconds while a cockpit tab is visible, paced to GitHub's rate limit (about once a minute unauthenticated; every 10 s with `GITHUB_TOKEN`/`GH_TOKEN` set, which is then sent with the request). Dismissing the banner in the cockpit (back when it had one) still silences the terminal half on its own. | | `VITE_CEZ_API_BASE=http://localhost:4321` | **Build time only**, and only when the cockpit bundle is deployed apart from the service it talks to. Empty (the default) means "the origin that served this page", which is right for both normal cases: the CLI serves the bundle itself, and `npm run dev` proxies `/api` to the local service. A deployment that must be configured without a rebuild can put `` in the served HTML instead, which wins over this. | ### Troubleshooting: the agent's shell returns nothing **Symptom.** A task on the Claude backend keeps working, but every shell command comes back with no output and a spurious non-zero exit status β€” `echo hello` included. Redirecting into a file inside the worktree still produces the right content, so the commands genuinely run; only the *capture* is lost. Codex tasks on the same machine are unaffected, because that backend streams over stdio pipes instead of round-tripping a command's output through a temp file. **Diagnosis.** The temp directory the agent was given is out of space or out of quota. One line tells you: ```bash echo probe > "${TMPDIR:-/tmp}/probe" # "Disk quota exceeded" / "No space left on device" df -i "${TMPDIR:-/tmp}" # a tmpfs can exhaust inodes long before bytes ``` Under quota the file is *created* and the write then fails, so the backend reads back a zero-byte capture file and hands the agent an empty result. **Fix.** Since #785 cezar gives each task its own `TMPDIR` under `.ai/cezar/tmp/` and write-probes it before spawning, so a broken temp directory fails the task with `agent temp directory is not writable: …` on the task thread instead of corrupting its work. If you see that error, free space on the disk holding the repo. `CEZ_AGENT_TMPDIR=0` turns the whole mechanism off β€” per-task directory and pre-spawn check alike β€” and hands agents the host `TMPDIR` again, which is the way out if the check itself is wrong on your platform. --- ## Talking to a running cockpit The cockpit is a local HTTP server; three CLIs and any script can talk to it while it runs. They find it through `CEZ_API_URL` β€” the address the cockpit printed after `cockpit β†’` when it started (`http://127.0.0.1:4321` unless that port was busy or you passed `--port`). ### From a shell: `cez runs`, `cez task`, `cez automation` Inside a task, cezar sets `CEZ_API_URL`, `CEZ_PROJECT_ID` and `CEZ_TASK_ID` for the agent. From your own terminal, set the address yourself: ```bash export CEZ_API_URL=http://127.0.0.1:4321 cez runs list # this project's tasks, newest first cez runs list --status running,review --json cez automation list # automations, with state and counts cez task tree # a dispatch tree ``` On a cockpit that serves several projects, add `CEZ_PROJECT_ID=` (`cezar projects` lists the ids); without it every command addresses the project the cockpit was started in. `CEZ_API_URL` is never guessed. For an agent, its absence is how the cockpit says dispatch or automations are unavailable, and a fallback would let a task write to whatever cockpit owns the port. So a command run without it exits `2` and explains how to set it (inside a task, it tells the agent to stop and report instead). `cez runs` is read-only. Cancel, relaunch and archive from the cockpit, or call the API below. ### The HTTP API - **Everything is under `/api/v1`.** The unversioned `/api/*` spelling was removed on purpose and answers `404` β€” `GET /api/v1/runs` is right, `GET /api/runs` is not. - **Project scope.** Every project route also answers under `/api/v1/p//…`; the unscoped path is bound to the project the cockpit was started in. Workspace-wide routes (`/api/v1/projects`, `/api/v1/workspace/…`) have no scoped twin. - **Discovery.** `GET /api/v1/health` answers without the request-origin guard, so it is the call to probe whether a cockpit is up and which version it runs. - **Routes and shapes.** The route inventory is section 2 of [BACKWARD_COMPATIBILITY.md](../BACKWARD_COMPATIBILITY.md); every request and response is a zod schema in [`packages/contract`](../packages/contract/src). The common ones: `GET/POST /runs`, `GET /runs/:id`, `POST /runs/:id/{cancel,messages,continue,archive}`, `GET /automations`. - **No built-in auth.** The cockpit binds `127.0.0.1` and trusts the machine; see [Remote access](#remote-access-host-cezar-on-a-server) before exposing it. --- ## Coding agent backends cezar is not married to one vendor. Every agent step runs through a single `AgentRunner` seam with five built-in backends: | Backend | CLI | How cezar drives it | Tool access | |---|---|---|---| | **Claude Code** (default) | [`claude`](https://github.com/anthropics/claude-code) | Headless `stream-json` mode. | Per-tool `--allowedTools` (`bashAllowlist` scopes `Bash`); `dontAsk` denies unapproved tools without prompting (`CEZ_APPROVAL_GATE=1` selects `acceptEdits`, but cezar cannot answer permission prompts). | | **Codex** | [`codex`](https://github.com/openai/codex) | `codex app-server` β€” JSON-RPC over stdio, the same transport the Codex IDE extensions use. | Ignores `allowedTools`; the default auto mode uses `danger-full-access` with `approvalPolicy: never` (`CEZ_CODEX_NETWORK=0` opts into the network-blocked `workspace-write` sandbox). | | **Junie** _(experimental)_ | [`junie`](https://junie.jetbrains.com/cli) | `junie --acp=true` β€” the real Agent Client Protocol over stdio, the same transport JetBrains IDEs use. | Ignores `allowedTools`; every permission is auto-approved. | | **OpenCode** _(experimental)_ | [`opencode`](https://opencode.ai) | `opencode serve` β€” a local HTTP server with an SSE event stream. | Ignores `allowedTools` entirely; every permission is auto-approved. | | **Cursor** | [`agent`](https://cursor.com/docs/cli/overview) (Cursor Agent CLI) | Headless print mode: `agent -p --force --trust --output-format stream-json`. Continue is fresh-session in v1 (print mode exits per turn). Its documented output carries no token/cost figures, so a Cursor run's header always reads zero tokens and no cost β€” that is the backend not reporting usage, not the run using nothing. | Ignores `allowedTools`; `--force` / `--trust` auto-approves tool use for unattended runs. | | **pi** _(experimental)_ | [`pi`](https://github.com/badlogic/pi-mono) | Persistent `--mode rpc` over JSONL; models are picked with the `provider/model` convention. | Maps `allowedTools` onto pi's `--tools` allowlist; a configured `bashAllowlist` disables Bash because pi cannot express command-prefix rules. | > ⚠️ **Junie, OpenCode and pi support are experimental.** All three runners work > but are less battle-tested than the Claude Code and Codex backends, and Junie > and OpenCode auto-approve every permission (they ignore `allowedTools`). Treat > them as previews and expect rough edges. On startup cezar probes which CLIs are installed and the cockpit only offers the backends it found β€” install any one of the five and you're operational. **Models come from your own machine.** The model picker does not ship a list of vendor releases that goes stale between cezar versions. For Claude, Codex, Cursor, Junie and OpenCode cezar asks the CLI on your host what *it* currently offers (Claude Code's `list_models` control request; the Codex app-server's `model/list`; Junie's ACP `session/new` config options; `opencode models`) and shows exactly that, in that order β€” so a model your account gained yesterday is selectable today with no cezar release, and one your provider retired stops being offered. Discovery is read-only, costs no tokens, and is cached briefly in memory. If the CLI is missing, logged out, too old or slow, the picker quietly falls back to that runner's built-in entries (`auto` plus Claude's tier aliases) and says so in a status row; `pi`, which has no host-local catalog yet, always shows its built-in entries. `auto` β€” send no model at all and let the CLI decide β€” is always available, and a model you pinned by hand stays selectable even when it is no longer advertised. **Pick a backend at three levels** (most specific wins): 1. **Config default** β€” `"defaultRunner": "codex"` in `.ai/cezar/config.json`. 2. **Per task** β€” the backend picker next to the task box in the cockpit. 3. **Per workflow step** β€” `runner:` on any step in the YAML. Per-step overrides are what make **mixed-agent strategies** a one-liner: implement with one agent, review with another, and let a shell check referee: ```yaml name: implement-and-cross-review steps: - id: implement name: Implement prompt: "{{task}}" runner: codex # one vendor writes the code… - id: review name: Cross-review prompt: "Review the diff produced for: {{task}}. Fix real issues only." runner: claude # …another one reviews it - id: verify name: Verify command: "npm test" onFail: { retry: implement, max: 2 } ``` Parallel variants (Γ—2/Γ—3) of one task share that task's backend β€” mixing happens per task and per step, not inside a variant group. The seam is deliberately small: a backend is one class implementing the `AgentRunner` interface (`packages/cezar/src/core/agent-runner.ts`) that turns a prompt into a stream of normalized events. Other CLIs β€” pi, aider, whatever ships next β€” can slot in the same way. --- ## Remote access (host cezar on a server) cezar runs on `localhost` by default. To reach the cockpit from another machine β€” a shared team box, a VPS, your phone β€” put an **authenticated public front** in front of it. The built-in installer does this interactively, per **platform strategy**, and never escalates silently: every privileged command is printed and verified, and it ends with a real authenticated end-to-end check. ```bash npx cezar-run server-install --platform ubuntu-vps # stand it up npx cezar-run server-deploy --platform ubuntu-vps # roll out a new version (reload the service) npx cezar-run server-uninstall --platform ubuntu-vps # reverse it # host a SECOND cockpit for another domain on the same box (ubuntu-vps): npx cezar-run server-install --platform ubuntu-vps --domain shop.example.com ``` On `ubuntu-vps` a single host can run several independent cockpits β€” add `--domain ` and each gets its own port, nginx site, login and service; a new domain never resumes or clobbers the first install. **Already running a reverse proxy?** If Dokploy, Coolify, Caddy or your own nginx already owns `:80/:443`, cezar's would fight it for the ports. Install the service only and let your proxy front it: ```bash npx cezar-run server-install --platform ubuntu-vps \ --external-proxy --domain cezar.example.com --bind-host 172.17.0.1 ``` `--bind-host` is only needed when the proxy runs in a **container** (Traefik can't reach the host's loopback); a host-installed proxy uses the `127.0.0.1` default. In this mode **your proxy must enforce authentication** β€” cezar has none of its own. [Details β†’](server-install/ubuntu-vps.md#the-box-already-has-a-reverse-proxy-dokploy-coolify-caddy) | Provider | `--platform` | Public front | Guide | |----------|--------------|--------------|-------| | Ubuntu / Debian VPS | `ubuntu-vps` | nginx + Let's Encrypt HTTPS, htpasswd login, systemd | [Step-by-step β†’](server-install/ubuntu-vps.md) | | Ubuntu + existing proxy | `ubuntu-vps --external-proxy` | your Dokploy/Traefik/Caddy front; cezar ships the service only | [Step-by-step β†’](server-install/ubuntu-vps.md#the-box-already-has-a-reverse-proxy-dokploy-coolify-caddy) | | macOS + ngrok | `macosx-ngrok` | ngrok tunnel + `--basic-auth`, launchd | [Step-by-step β†’](server-install/macosx-ngrok.md) | See the **[Remote access overview](server-install/README.md)** for how it works and how to redeploy new versions. --- ## Configuration (optional) Zero config is the default β€” everything below is opt-in via `.ai/cezar/config.json` (a missing or invalid file simply uses the defaults, and never blocks startup; configured lifecycle transitions pause on malformed configuration rather than treating it as an empty cleanup list): ```jsonc { "skillsRepos": [{ "repo": "open-mercato/skills", "ref": "main" }], // team skills; [] disables // Team-skill repos are code-trusted: a skill body becomes an agent system prompt. // Only owner/name, https/ssh URLs, or local paths (`/abs`, `./rel`, `~/dir`, // `C:\dir`) are accepted β€” no ext::/fd:: transport helpers. Write a relative // path as `./name`, not a bare `name`. Pin `ref` to a full commit SHA to freeze // the source against a moving branch head β€” cezar verifies it resolves to // exactly that commit, and reports it as `team.commit`. // Skills repos shared by every project (e.g. your organisation's) are added in Settings β†’ // Global β†’ Skills, not here; they are stored in `~/.cezar/config.json` (`orgSkillsRepos`) // and merged after this list, with the same source rules. Only markdown under a folder named // `skills/` (any depth) is picked up from them: `…/skills//SKILL.md` is a directory skill // (its other files are its references), any other `…/skills/**/*.md` a flat one. "worktreeRetention": 10, // keep the last N finished worktrees on disk; 0 = unlimited (branch always kept) "defaultRunner": "claude", // agent backend: "claude" (default) Β· "codex" Β· "junie" Β· "opencode" Β· "pi" "modelsLocked": true, // optional: native per-runner model is fixed/read-only; runner stays selectable "plannerModel": "sonnet", // model the "Plan first" button uses to draft chains "baseBranch": "develop", // branch worktrees fork from + PRs target (also settable in the Git tab) "branchNaming": "your branch naming preferences", // optional: task branches are named from the first prompt under these // instructions (Settings β†’ Instructions); absent = cez/ // Trunk-based projects (Settings β†’ Tasks β†’ Development model); every key optional: "taskWorkspace": "in-place", // tasks work in the checkout, write tasks one at a time "landing": "auto", // finished in-place tasks commit, rebase, run requiredChecks, push // (ignored while this file is tracked by git) "requiredChecks": ["npm test"], "defaultSkill": "my-skill", // composer preselect; "askSkill" for read-only Ask tasks "startApp": { "services": ["npm run api", "npm run web"], "url": "http://localhost:5173" } // optional: what the Preview's Start app runs, each service in its own // terminal tab; url = the address to show (absent: the first one printed). // A `startApp` in your project entry of ~/.cezar/config.json wins. } ``` **Trunk-based projects** (many small changes that should land straight on the base branch β€” a docs or notes repo, a config repo, a solo project): set **Settings β†’ Tasks β†’ Development model** to *Trunk-based*. Tasks then run in the repo checkout instead of a worktree, write tasks take turns so two tasks never conflict. A finished task waits for your review in Changes; reply with corrections, then **Finish** commits, rebases on the remote, runs `requiredChecks` and pushes to the base branch. Autonomous tasks land on their own. A failed rebase or check goes back to the agent. The composer's **Ask** chip starts a read-only question: no branch, no lease, runs beside a write task. Spec: `.ai/specs/2026-10-20-trunk-based-projects.md`. Put the same `"modelsLocked": true` key in `~/.cezar/config.json` to apply it to every registered project. When the key is absent or `false` in both config files (and `CEZ_AGENT_MODELS_LOCKED` is not `1`), each runner's normal model selector uses that runner's discovered model list. While locked, the model is shown read-only and follows the selected runner's native settings; the runner itself remains selectable. Run data (`runs.json`, NDJSON event logs, worktrees, `todos.json`) is git-ignored automatically; your workflows and skills stay committable. Settings that belong to *you* rather than to a repo β€” the parallel cap (`maxParallel`, default **2**), the per-task memory ceiling and the checkout root β€” live once in `~/.cezar/config.json`, alongside the [project registry](#multiple-projects-one-cockpit), and are edited from **Settings β†’ Resources** and **Settings β†’ Projects**. A `maxParallel` left over in a repo's `.ai/cezar/config.json` is imported into the workspace file the first time cezar boots there, and ignored afterwards. **Settings β†’ Resources** opens on a live **Machine** card, and the sidebar carries the same numbers as a one-row **glance** (CPU, its 60 s sparkline, compact RAM) that links here. Samples arrive every ~2 s: a local cockpit gets them pushed over the `host` WebSocket topic, a remote one reads `GET /api/v1/workspace/host-usage` on mount, on a reconnect and when the tab becomes visible again - and follows a first answer that carries no CPU figure with exactly **one** warm-up read ~2.5 s later. That gap is honest, not a bug: CPU utilization is a delta between two samples, so the first read after an idle period has no window to measure and the readout shows `sampling…` instead of a number it cannot back. A metric the OS does not expose (swap outside Linux, load on Windows) is omitted rather than printed as a zero. The `updated N s ago` line is the age of the sample's own server timestamp and ticks every second while the card is mounted, so a cockpit that has stopped receiving data counts up instead of freezing at a fresh-looking value. The 60 s sparkline is **local-only**: a remote cockpit's route answers are sparse, so it shows the instantaneous bar and no chart rather than plotting minutes as if they were seconds. A remount always re-reads the route (the host query overrides the workspace's five-minute `staleTime`), so a cached sample can never be stamped as fresh. **Which numbers are effective.** The plain process reads **host totals**. When cezar runs inside a cgroup with a real limit - a Docker `--cpus`/`--cpuset-cpus`, a systemd scope, a sandbox - the same payload carries an optional `container` object with the process's OWN cgroup limits and usage, and the card and the glance show those as the effective values, labelled, with the host totals kept as context (`host 64 CPU Β· 755 GB`). A usage-only cgroup emits no `container` at all, so a normal host reads exactly as it always did. A limit whose value cannot be read shows `β€”`; the host figure is never substituted for it. When one is present, cpu/memory labels say `(effective)` and the load chip pairs with the host core count. One case is deliberately NOT rendered as "no limit": when the probe cannot read the process's cgroup at all (a masked `/proc`, no cgroupfs mounted) the payload carries `cgroupProbe: 'unavailable'` and the card says **no cgroup information available** β€” the process may well be capped, and host totals are not evidence that it is not. **When the sampler runs.** Below `md` the card's own subscription is the demand (sampling lasts while the card is on screen, exactly as before). On a local desktop the topic is held for the session, because the sidebar glance is always there; the sidebar's machine row carries the staleness clock (`stale` after ~10 s without a frame). A remote cockpit never opens a socket and keeps reading the route. The `host` topic is **trusted-only**: the hub admits a socket whose page origin matches the server's authority, and refuses the subscription (`forbidden topic`) for anything else - a dev server proxying a `localhost` page to `127.0.0.1` without `Sec-Fetch-Site: same-origin` is the common case. The card says so and falls back to the authenticated same-origin route instead of showing `sampling…` forever. ### Editing the agents' own config (Settings β†’ Agent config) cezar picks *which* agent runs; **Settings β†’ Agent config** lets you edit *how* it behaves β€” the raw config files Claude, Codex and OpenCode read for settings, MCP, and memory. In the multi-project cockpit the section is project-scoped: repo-relative files resolve from the selected project's root, while user-scope files continue to resolve from the agent's home. Each file keeps its native format and vendor-documented precedence. Tracked files reach task worktrees after commit; Claude's gitignored personal layer is seeded into each run's worktree. Editing is a local-machine capability, so a hosted cockpit (`CEZ_REMOTE=1`) is read-only and never serves home-file contents. --- ## Worktree lifecycle scripts **Project Settings β†’ Worktrees** contains two optional ordered lists: **After worktree creation** prepares a new directory before the agent starts; **Before worktree removal** cleans up while its files still exist. Both lists default to empty. There is no required config file, global inheritance, environment switch, or command execution for projects without hooks. These are local settings for the selected project, saved under `worktreeLifecycle` in `.ai/cezar/config.json`. Saving scripts does not execute them. Setup also runs when Continue recreates a reclaimed directory. Continuing in an already prepared directory does not repeat setup. Cleanup covers explicit worktree removal, task deletion, retention/Reclaim now, discarded variants, and known managed orphans. Finishing or archiving a task alone runs neither list; non-Git and worktree-disabled tasks run neither list. Retention keeps its original branch-preserving scope even when cleanup is forced. A failed cleanup may leave the project temporarily above its retention limit; the Worktrees page shows the operation requiring attention. Add a display name, multiline Bash command, and optional timeout for each entry; move entries up/down to set their order, then **Save**. Lists accept at most 32 commands each, command text up to 32 KiB and names up to 120 characters. Every entry has a stable UUID: editing or reordering keeps it; adding a new entry creates a new identity. Hand-authored entries must supply their own unique `id`. The **Variables** disclosure inserts these tokens at the command editor's caret: | Variable | Value | | --- | --- | | `{{ root_path }}` | Canonical absolute root of this project. | | `{{ worktree_path }}` | The actual directory being prepared or removed; never a fallback to the project root. | | `{{ worktree_id }}` | Stable resource identity `cez-`, retained across reclamation and recreation. | | `{{ task_id }}` | Original task ID, retained even if its task record is gone. | Tokens accept optional inner whitespace. **Leave them unquoted**: Cezar adds shell escaping, so spaces and shell punctuation in paths remain inert data. Static suffixes are supported: ```sh cp {{ root_path }}/.env.local {{ worktree_path }}/.env.local docker compose --project-directory {{ worktree_path }} -p {{ worktree_id }} up -d --build ``` For cleanup, a separate entry might use: ```sh docker compose --project-directory {{ worktree_path }} -p {{ worktree_id }} down --volumes ``` These are examples, never installed defaults. Unknown or unclosed tokens and tokens inside quotes, substitutions, backticks, arithmetic, comments, or here-documents are rejected before execution. Put complex logic in a script and pass variables as ordinary arguments. To pass literal double braces to another tool, type `\{{` in the editor, including inside quoted strings: ```sh docker inspect --format '\{{ .Name }}' {{ worktree_id }} ``` Cezar consumes the escape and sends `{{ .Name }}` literally. A JSON-authored command needs the usual additional JSON escaping for its backslash. **Preview** only renders and redacts the command; it never runs a shell. Choose a real worktree, or use the explicitly labelled example context. Commands run sequentially as noninteractive `bash -c`, without a login profile, in the worktree directory with closed stdin. Bash must be available on the host; Cezar does not install it or silently substitute another shell. Windows also uses native `taskkill /PID … /T /F` to terminate an owned command tree. Native Windows execution was not exercised by the implementation's local fixture tests. Each command has a **30-minute wall-clock timeout**, with an optional `timeoutSeconds` override from **1 through 86,400**. Silence is not an idle-timeout trigger. Stop/timeout terminates supervised children; uncertain process exit keeps the worktree protected until quiescence can be established. Commands inherit the Cezar host's environment and privileges. Repository `.env` files are not automatically loaded and managed tracker/account credentials are not injected. Source a file explicitly if your command needs it. The visiting browser does not run these commands, including in remote mode. Output is redacted before persistence/live delivery, limited to 1 MiB per execution with 16 KiB frames and a truncation marker; redaction covers known secrets and token patterns, not arbitrary sensitive text a script invents. Task Session and Worktrees show progress, attempts and expandable output. **Retry loads the current saved lists**, in their current order. Durable successes are reused only for the same operation, generation, entry ID, exact command, resolved context and renderer contract. Editing a successful command runs it again; changing only its name or timeout does not. Removed entries remain in history but no longer run. Changing a script file alone does not invalidate a successful entry: remove and add the entry to deliberately give it a new identity. Saving, including clearing both lists, never resumes a failed operation; choose **Retry** explicitly. Changes made while a command runs apply to later entries, and configuration is checked again before the transition commits. Setup recovery offers **Retry**, **Edit scripts**, **Start task anyway**, or **Cancel task**. Bypass accepts an incomplete environment; cancellation retains the directory and any resources already created. Cleanup recovery offers **Retry**, **Edit scripts**, **Keep worktree**, or **Force delete**. Keep suppresses automatic cleanup for that generation. Force skips remaining commands and carries out the original removal intent: reclamation keeps its branch, whereas deleting a task also deletes its record and managed branch. Partially stopped containers, volumes and other resources may remain after Keep, Cancel or Force. There is no automatic rollback of command effects. **Stop scripts** parks the operation after stopping the current command; it does not itself cancel a task or remove its directory. Normal shutdown and restart wait for owned scripts to stop and save their recovery state, including scripts in other opened projects. A crash leaves uncertain execution for an explicit choice, never an automatic replay. There is no exactly-once guarantee for external side effects. Completed scripts followed by a filesystem failure appear as a commit failure; Retry rechecks current settings before attempting the remaining transition. Headless `cezar run` uses the same preparation gate. If recovery is required, it prints the operation ID and cockpit recovery instruction and exits **1**, rather than waiting indefinitely or starting the agent. Normal done/review still exits 0. `CEZ_DRY_RUN=1` does not execute lifecycle commands or delete real worktrees through this mechanism. Lifecycle records and bounded output live independently under the ignored `.ai/cezar/lifecycle/` directory. Completed history is retained for **7 days**; active/attention operations and retained directories are not age-pruned. Stable identity survives as long as the task does, including after its directory was reclaimed, so a later recreation uses the same resource identity. **Before downgrading to a version without lifecycle support**, finish, Keep, or explicitly Force pending operations, then disable the configured hooks. Older binaries cannot enforce these gates and may remove directories without cleanup. Deleting Cezar state cannot undo external resources or reconstruct lost ownership information. --- ## Nightly and preview builds Every night we publish the trunk to npm, so the features landing in the next release are one command away: ```bash npx cezar-run@nightly # everything merged as of last night ``` **Come build this with us.** cezar is shaped by the people who run it on real repos: if you try a nightly and something feels wrong β€” a workflow that stalls, a diff that reads badly, a runner that should exist β€” [open an issue](https://github.com/open-mercato/cezar/issues) and tell us. That feedback, early, is worth more than a bug report six weeks after a release, and it is how most of the features here got their final shape. **Know what you're installing.** A nightly is verified (typecheck, unit suites, packaged-CLI e2e β€” the same gate a release runs) but it is *not* a release: it can be rough, a flag or a screen may change under you, and something occasionally breaks in a way no test caught. Nothing is at risk beyond your patience β€” every task runs in its own git worktree and cezar never auto-merges β€” but if you need a boring day, stay on the stable release. Pin a nightly you liked with its exact version (`npx cezar-run@0.9.2-nightly.20260813.126` β€” the cockpit prints the version it booted, and the date in it tells you how old the build is), and drop back to stable any time with a plain `npx cezar-run`. ### Preview builds Every green CI run also publishes an installable npm snapshot ([how it works](publishing.md)), so you can try code that has not even merged yet: ```bash npx cezar-run@develop # current develop head ``` Every pull request gets its own preview too β€” the CI bot posts a sticky comment on the PR with the exact pinned version to copy-paste (`npx cezar-run@-pr.`). Nightlies and previews are all prerelease versions under their own dist-tags; a plain `npx cezar-run` always resolves to the latest stable release. --- ## Local development End-to-end, from a fresh clone to a global `cezar` command you can run in **any** repo on your machine β€” no npm publish required. **1. Prerequisites** β€” Node 20+ and `git` (plus at least one logged-in agent CLI, as in [Quick start](../README.md#quick-start)). **2. Clone & install** ```bash git clone https://github.com/open-mercato/cezar.git cd cezar npm install ``` **3. Build** β€” compiles the api-client and the server (`tsc β†’ packages/cezar/dist/`) and the cockpit (`vite build β†’ packages/cezar/web/dist/`), then runs the pack gate: ```bash npm run build ``` **4. Install as a global command** β€” build + put `cezar` / `cez` / `cezar-cli` / `cezar-run` on your PATH pointing at *this checkout*: ```bash npm run install-as-command # live link (default) β€” see the change loop below # or: npm run install-as-command:global # self-contained snapshot copy ``` Now `cd` into any other repo and run it: ```bash cd ~/some-other-project cezar # cockpit for that repo, straight off your checkout cezar-run --help # same binary; the name matches `npx cezar-run` ``` **5. The change loop** - **Link mode** (default): edit source β†’ `npm run build` β†’ the global command reflects it immediately. No relink needed. (It is a live symlink into this checkout β€” don't move or delete the checkout while it's linked.) - **Snapshot mode** (`:global`): re-run `npm run install-as-command:global` to refresh the installed copy. It survives moving/deleting the checkout. **6. Uninstall** ```bash npm run uninstall-as-command # removes cezar / cez / cezar-cli / cezar-run (either flavor) ``` **7. Troubleshooting** - **`cezar: command not found`** after install β†’ your npm global bin dir isn't on PATH. The script prints the exact dir; add it to your shell profile (`export PATH="$(npm prefix -g)/bin:$PATH"`). - **`EACCES` / permission denied** β†’ your global prefix is root-owned. Point npm at a user-writable one and retry β€” **never** sudo: `npm config set prefix ~/.npm-global`. - **Already installed the published `@open-mercato/cezar` globally?** The link/snapshot install replaces it; `uninstall-as-command` removes ours, and `npm i -g @open-mercato/cezar` brings the published one back. ### Run the desktop app (or `cezar`) on a worktree The desktop app and the managed `cezar` launcher both run whatever `~/.cezar/versions/current` points at. A **linked checkout** puts a worktree there without copying it: the version entry is a symlink to the worktree's `packages/cezar`, so it runs straight off the worktree (and its own `node_modules`), and every rebuild is live on the next restart. A worktree has to be built to run. The cockpit does that for you; the desktop menu and the terminal need it done by hand (`npm install && npm run build` in the worktree). Pick one of: - **Cockpit**: open the version chip and switch the release channel to **Development**. The **Worktrees** tab lists every worktree of every registered cezar repo (task worktrees included), newest commit first, each with its task title, last commit, open PR and build age (a build older than the last commit is marked **needs rebuild**). The filter matches task, branch, commit subject or `#PR`. Pick one and choose **Switch & restart**. A worktree marked **not built** or **needs rebuild** offers **Build & switch** / **Rebuild & switch** instead: cezar runs `npm install` (when its dependencies are missing or its lockfile changed) and the server and cockpit builds in the worktree, with the output in the dialog, then switches. Picking the running worktree while it needs a rebuild offers **Rebuild & restart**. The **Pull requests** tab lists cezar's open pull requests with the preview build CI published for them (npm dist-tag `pr-`); pick one and choose **Install & restart**. A PR without a build (a fork, CI not green yet) is listed but cannot be picked. The development channel never offers updates on its own; switch back to **Stable** or **Nightly** to follow releases again. - **Desktop menu**: **Cezar β–Έ Versions** lists linked worktrees by branch (`cez/cb28888e β€” 0.13.0 (worktree)`). Clicking one switches and restarts. - **Terminal**: ```bash cezar link [] --use # link the checkout (default: cwd) and make it current cezar versions # installs, links, and cezar worktrees not linked yet cezar use 0.13.0 # back to a published release cezar unlink # forget a link (the worktree is untouched) ``` The desktop app picks up a terminal switch on its next start, and the Versions menu refreshes when the window regains focus. Links are hidden from the lists once their worktree is deleted. Switch away before you remove a worktree the app is running from. Cockpits in hosted mode never list worktrees and never link one. Releases older than this feature ignore links, but the desktop menu still switches from them. ### In-checkout scripts ```bash npm run dev # server (API :4321) + Vite dev server, opens the cockpit in the browser npm run dev:server # tsx packages/cezar/src/index.ts β€” the API server alone npm run dev:web # Vite dev server alone (proxies /api to :4321) npm run build # tsc β†’ packages/cezar/dist/, vite build β†’ packages/cezar/web/dist/, then the pack gate npm run typecheck # server + web (tsc --noEmit) npm test # vitest β€” server + cockpit unit suites npm run test:unit # node:test β€” fast core-module tests npm run test:package # pack/install and exercise the built CLI npm run test:e2e # real-browser cockpit suite (agent-browser) ``` The stack is deliberately small: **TypeScript** (strict, ESM), **Hono** + SSE for the server, **Zod** at every boundary, **YAML** for workflows, and a **React 19 + Vite + Tailwind v4 + shadcn/ui** cockpit shipped pre-built in `packages/cezar/web/dist/` β€” the published package carries the built app, so `npx` users never run a bundler. Every module is meant to be read in one sitting. ---