AGENTS.md · open-mercato/cezar
AGENTS.md from open-mercato/cezar: 223 lines covering Zero config, Changing a mechanism that already works, The HTTP API, Repository layout and more.
Template by open-mercato
Install
Works with Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code. Read the repository before you give any tool access to your data.
curl -fsSL https://raw.githubusercontent.com/open-mercato/cezar/main/AGENTS.md -o AGENTS.md
Read it and adapt it to your project before use.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: agent-orchestration, code-generation
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
What's in it
An AGENTS.md file: instructions coding agents (Codex, Cursor, Copilot, Gemini CLI and others) read before working in a repo.
- For
- Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code
- Length
- 223 lines
- File
AGENTS.md
Sections
- Zero config
- Changing a mechanism that already works
- The HTTP API
- Repository layout
- Task routing
- Comments
- Validation
- Related documents
First lines (shown under the repo's MIT license)
# AGENTS.md — working in this repository
cezar is a **parallel coding-agents orchestrator**: a local cockpit (CLI + browser GUI) for running and tracking AI coding-agent tasks in a repo. You type a task, pick a workflow and a backend — Claude Code, Codex or OpenCode (experimental), or a mix per step — and watch it work live: steps, tool calls, tokens, diffs. Each task runs in its own git worktree, ends at a review gate (never auto-merges), and can be pushed as a draft PR through `gh`. Everything is local: no accounts, no database, no cloud — state is plain JSON, NDJSON and Markdown under `.ai/cezar/`. The server stack stays deliberately small: strict TypeScript (ESM, Node 20+), Hono + SSE, Zod at every boundary, and YAML workflows. The cockpit is React 19 + Vite + Tailwind v4 + shadcn/ui, compiled to static assets (the legacy vanilla UI was retired in R7). Every module is meant to be read in one sitting.
## Zero config
cezar ships no config file the user must create and no setting they must set before it works. Every capability is discovered from what is already there — the repo, the environment, `gh`, the running processes — or it degrades quietly to a smaller cezar. `.ai/cezar/config.json` is optional and every key has a working default; A repository `.env` is never auto-loaded. Tracker credentials use managed per-project dotenv
files in `cezarHomeDir()/tracker-connections/`, written by Settings and parsed only into the
project connection object, never into `process.env`.
New state may be **written**, never **required**: `.ai/cezar/`, `~/.cache/cez/`, `~/.cezar/`. Delete any of them and cezar rebuilds what it needs on the next run. State that a user must author, migrate, or repair is not state — it is configuration, and it needs a reason. One deliberate exception inside that rule: the project registry keeps a `~/.cezar/config.json.bak` snapshot, so deleting `config.json` alone restores instead of resetting — delete the `~/.cezar` directory for a true reset.
Practical rules:
- When a feature seems to need configuration, the design is wrong. Discover it, or default it.
- Features that widen exposure or cost (network, other processes) are opt-in behind a `CEZ_*` flag, off by default — the zero-config default is also the safe default.
- Owner-approved exception (2026-09-12): task dispatch (`cez task create`, spec `.ai/specs/2026-09-10-dispatch.md`) is default-on; `CEZ_DISPATCH=0` turns it off. Its brakes live in the engine — four children in flight, a child's budget carved out of its parent's — and with dispatch off the agent fanned out through its own sub-agents instead, unbudgeted and invisible (live run 3f7eaf02, 4.3M tokens).
- Owner-approved exception (2026-09-14): automations (spec `.ai/specs/2026-09-14-automations-redesign.md`) are default-on; `CEZ_AUTOMATIONS=0` turns them off. The default does nothing on its own — the timer arms only for an automation the user ENABLED, and a poll left enabled from the #801 opt-in days is re-baselined at boot when it has been idle longer than its own lookback, so the flip can never launch a backlog.
- Owner-approved exception: Open Mercato skill updates are default-on. They start only after the server listens, run at most once per six-hour cache window, use explicit lock-authorized names and fixed bounded `npx` argument arrays under a cross-process lock, and never block boot. `CEZ_SKILLS_AUTO_UPDATE=0` or the global Settings override disables automatic application (background detection remains read-only).
- Telemetry is **opt-in** (docs/telemetry.md): nothing — not even a ping — leaves the machine until the user answers yes in the cockpit's consent bar, Settings → Privacy or `cezar telemetry enable`; the answer lives in `~/.cezar/config.json` (`telemetry`), and bumping `TELEMETRY_POLICY_VERSION` asks again. `CEZ_TELEMETRY=0`/`DO_NOT_TRACK=1` block it even after a yes; no env var can grant it. Never make it default-on, and never send before consent. It never sends prompts, paths, repo or project names — routes go out as shapes, labels are closed sets the server enforces. All of it lives in `packages/cezar/src/telemetry.ts` and `packages/web/src/lib/telemetry.ts`, plus the desktop shell's error reports in `packages/desktop/src-tauri/src/telemetry.rs` — the same answer and env blocks; without a yes its failure page offers a **Send error report** button, and that click is consent for that one report only (no install id).
- **Pending owner ratification (2026-09-30)**: the star ask (`GET /api/v1/star-count`, the sidebar ⭐ chip, the star-ask dialog, the `serve` banner line) is default-on and fetches cezar's own star count from github.com; `CEZ_NO_BANNER=1` — already the promo block's switch — turns all of it off, request included. It is listed here because a default-on network widening is exactly what the rule above asks to be a deliberate, recorded decision, and this one has not been ratified by the owner yet. The fetch is metadata about cezar's own repository, carries no identifier, happens at most once per six hours per machine (cached in `~/.cache/cez/star-count.json`), and every failure is `{ available: false }`. If the owner would rather it were opt-in, the change is one line in `starAskSilenced`.
- **Pending owner ratification (2026-09-30)**: the star ask (`GET /api/v1/star-count`, the sidebar ⭐ chip, the star-ask dialog, the `serve` banner line) is default-on and fetches cezar's own star count from github.com; `CEZ_NO_BANNER=1` — already the promo block's switch — turns all of it off, request included. It is listed here because a default-on network widening is exactly what the rule above asks to be a deliberate, recorded decision, and this one has not been ratified by the owner yet. The fetch is metadata about cezar's own repository, carries no identifier unless `GITHUB_TOKEN`/`GH_TOKEN` is set (then GitHub sees which account read it), happens at most once per ten seconds per machine while a cockpit tab is visible — slower when GitHub's rate-limit headers say the hourly budget cannot afford it (60/h unauthenticated, 5000/h with `GITHUB_TOKEN`/`GH_TOKEN`, which it sends when set) — cached in `~/.cache/cez/star-count.json`, and every failure is `{ available: false }`. If the owner would rather it were opt-in, the change is one line in `starAskSilenced`.
- **Pending owner ratification (2026-10-11)**: the fork-parent lookup in `git-remotes.ts` (#1383) is default-on. When `origin` is on github.com or a GitHub Enterprise host (`*.ghe.com`, or one `gh` is signed in to — `github-hosts.ts`) and the repo has a second remote, cezar runs one `gh api repos/<origin>` call against that host, with the user's own `gh` auth, to learn whether that remote is `origin`'s fork parent. It runs at most once per process, retries after 10 minutes if it got no answer, and never runs under `CEZ_DRY_RUN=1`. `gh repo set-default` skips it. Without it, a contributor's fork clone would fork tasks from and open PRs on the fork.
- Owner-approved exception (2026-10-17): Design Mode (spec `.ai/specs/2026-10-09-design-mode.md`, the task's Preview) is AVAILABLE by default on a LOCAL cockpit, but its toggle starts OFF every time the Preview opens — the user switches it on by clicking; `CEZ_DESIGN_MODE=0` turns it off, and it is always off on a hosted one. Switching it on opens a second `127.0.0.1` listener that mirrors the previewed loopback `http://` dev server with a picker script injected. It is loopback-only, mirrors only what already listens on loopback, opens only after an explicit click and closes when idle.
- A missing dependency, an absent peer, a read-only home: degrade to a smaller working cockpit, never fail the boot.
- Prefer a proxy-free, daemon-free mechanism when one exists — and when it doesn't, keep the mechanism invisible: no process to manage, no port to remember, no file to edit.
- Never trade a working default for a knob.
- Adding, renaming, or removing a `CEZ_*` env var — or changing what its default does — MUST update `.env.example` in the same commit (and the env table in `docs/reference.md` when the var is user-facing). `.env.example` is the env contract's single documentation surface; an undocumented env var is a bug.
## Changing a mechanism that already works
Replacing working behavior is the highest-risk change in this repo, and it fails in a
characteristic way: the new mechanism is correct, the tests are green, the spec is
thorough — and the DEFAULT path quietly lost a guarantee nobody wrote down. #810 and #811
are the worked examples; both shipped a well-specified improvement that left the
zero-config user worse off than before.
**Name what the old mechanism was load-bearing FOR, not what it was for.** Those are
different questions. `IDLE_TIMEOUT_MS` read as session hygiene, and #661 removed it from
the monitoring branch for a good reason (it was closing live sessions mid-CI and recording
them as `done`). It was also the only liveness bound on `CEZ:MONITORING` and the only
reason a parked monitor eventually stopped holding a `maxParallel` slot. Neither
dependency was named in the spec, so neither was replaced, and `monitoring` became a state
with no exit. Before deleting a timer, lock, cap or timeout, grep for everything that
reaches a terminal state *because* of it.
**A replacement that ships OFF is not a replacement.** Diff the default path, not the
feature. The question is always: *with every new knob at its shipped default, what does
the old scenario do now?* If the answer is "nothing", the change removed a mechanism and
added a setting. A spec's "Resolved assumptions" table answering a COST question with
"opt-in, default null" is not an answer to whether the zero-config path still works —
cost-safe and functional are separate reviews. See § Zero config: *never trade a working
default for a knob*.
**Enumerate the transitions out of every state you add or keep.** "Who fires this?" is the
question that finds these bugs in one step. A parked run has exactly three wake sources —
a user message (`deliverMessage`), the autonomous nudge (turn-end only), and the monitoring
wake timer. Cezar has no process-exit callback, no CI webhook and no sub-agent-completion
event, so a state whose only on-by-default exit is "a human types something" is a dead end,
however well it renders.
**Find every construction site of a shared in-memory object — grep the TYPE, not the
field.** `ActiveRun` is built in `execute` AND in `runContinuation`; #811 populated
`state.skills` in the first only, so registry `/skill` expansion worked on new tasks and
silently failed on every Continue and every restart recovery. The same shape recurs in the
two near-identical turn-end handlers in `workflows/run.ts` (streaming and non-streaming):
a lifecycle change applied to one of them ships half a fix. When you add a field that a
delivery path reads, add it everywhere in the same commit or route both sites through one
helper.
**A fail-open helper needs a populated-input guarantee, or it lies.**
`expandRegistrySlashSkill` returning its input unchanged on no-match is right on its own —
a backend's own slash commands must survive. Against an empty registry that same branch
turns "cezar never loaded the list" into a confident user-facing "Unknown skill". Pair
every silent pass-through with a test that pins the *empty/absent input* case.
**Prove the regression test fails without the fix.** `git stash push -- <source files>`,
run the new test, confirm red, `git stash pop`. A test written after the diagnosis passes
against the bug more often than anyone expects, and a green-either-way test is how the
same regression ships twice. Keep the guard tests that pass both ways — they pin the
behavior you did NOT want to change (park mode stays reachable; unknown slashes stay
untouched) — but know which is which.
**Read the run-history evidence before theorizing, and cite it.** `.ai/specs/` records the
design, `git log -S` and `git merge-base --is-ancestor <commit> <tag>` settle "was this in
the release the user is on", and a user's "it worked in 0.9.1" is a testable claim, not an
opinion. #810 was confirmed in one command before a line of code was read.
## The HTTP API
Four invariants. A feature that breaks any of them compiles on its own branch and stops working
when it lands — #694 arrived with eleven unreachable routes for exactly this reason.
- **Every request and response shape is a zod schema in `packages/contract`, with its TypeScript
type inferred from it (`z.infer`).** Never hand-write an API type, and never declare one in
`server.ts` or in the api-client. The api-client re-exports the contract; the cockpit imports
the schema when it wants to validate and the type when it wants to compile.
- **Register routes by CHAINING them into a family builder**, the way the ~24 families in
`server.ts` already do. Hono accumulates route types through the chain only: a loose
`app.get(…)` statement returns a value nobody keeps, so the route vanishes from `AppType` and
the typed client cannot see it — silently, with the server still serving it.
- **Validate bodies, path params and the query string as route MIDDLEWARE**, through the trio in
`src/server/validators.ts`. Parsing inside a handler is invisible to hono, which is what let
`POST /runs` accept `{ totalNonsense: 12345 }` from a typed client without complaint.
- **Everything answers under `/api/v1`** (project-scoped: `/api/v1/p/:projectId/…`). The
unversioned surface is gone.
The contract must describe EXACTLY what the route sends — no wider, no narrower. When a schema and
its route disagree, fix the SOURCE, never widen the schema; `contract-parity*.test.ts` asserts both
directions and a one-way check passes on real drift. Two mismatches recur often enough to name:
writing `key: maybeUndefined` types a key as always-present that `JSON.stringify` drops from the
wire (spread it conditionally), and an object-literal `type: 'x'` widens to `string` during hono's
inference, erasing a discriminant consumers narrow on (`as const`).
## Repository layout
Four npm workspaces under a `private` root that publishes nothing itself:
| Path | Package | What it is |
| --- | --- | --- |
| `packages/cezar` | `@open-mercato/cezar` | The service + CLI, and everything behind them (runs, workflows, agent runners, workspace state). The published artifact: `bin`, plus the built cockpit in `web/dist`. |
| `packages/contract` | `@open-mercato/cezar-contract` | The HTTP contract itself: every request and response as a zod schema with its TypeScript type inferred from it, so no shape is written twice. **Node-free on the same terms as the api-client** (`lib: ["ES2022"]`, `types: []` in its tsconfig make a `node:*` import a compile error). `private`, and that has a cost: the service imports a contract VALUE (`workspaceUiStateSchema`, in `workspace/migrations.ts`), so a published tarball naming a package npm has never seen would fail on install. `packages/cezar/scripts/inline-contract.mjs` runs as `postbuild` to fold the contract into `dist/contract/` — bundle AND declarations — and repoint the emitted references. Delete that script the day the contract is published, or the day nothing in the service imports a contract value. |
| `packages/api-client` | `@open-mercato/cezar-api-client` | The contract between the two: the typed client over `AppType`, the SSE/protocol types, and the scope helpers. Re-exports `packages/contract` so a consumer needs one import. **Node-free by construction** — no `node:*`, no `@types/node` — because it is bundled into a browser AND imported by the Node service. `private` for now: versioned with the release but not on npm. The service may therefore only import it in TESTS — a runtime import would make the published CLI depend on something npm cannot resolve. |
| `packages/web` | `@open-mercato/cezar-web` | The cockpit SPA. Private; its output is an artifact of the service (`vite build` writes into `packages/cezar/web/dist`, which the CLI ships and serves). |
Rules that follow from that:
- **The CLI is not a separate package and should not become one.** It is the same program as the service — `packages/cezar/src/index.ts` boots `startServer`, but also `RunManager`, the workspace registry and the worktree machinery, and `cezar run` executes a workflow with no server at all.
- A dependency belongs to the workspace that imports it. The root carries only what spans all four (`typescript`, `vitest`, `tsx`). A build script counts as an importer: `scripts/inline-contract.mjs` reaches for `esbuild`, so `esbuild` is a devDependency of `packages/cezar` rather than something inherited from whatever vite happens to hoist.
- Cross-package imports go through the package name, never a relative path — the exceptions are test-only reaches for golden fixtures, and they are ugly on purpose.
- The repo root keeps only what spans workspaces: `scripts/dev.mjs` (boots both halves) and `scripts/release*.mts`/`release-snapshot.mjs` (a release spans every package). Everything else lives in the package that owns it.
## Task routing
For a new issue-tracker integration, read [docs/adding-issue-tracker.md](docs/adding-issue-tracker.md) first. It maps the shared provider seam, registration points, UI metadata and verification requirements.
| When the task involves… | Read first | Key rules |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CLI entry, `serve`/`run`/`init` subcommands, flags | `packages/cezar/src/index.ts` | Uses `node:util` `parseArgs`, no CLI framework. `serve` is the default command. Headless `run` treats `review` as a terminal success status (exit 0). `init` never overwrites existing files. Keep `.ai/cezar/.gitignore` maintenance (`ensureDataGitignore`) in sync with any new state file — the list is a per-entry allowlist (`workflows/` and `skills/` beside it are meant to be committable), so anything you forget lands in the user's `git status`, and you cannot see that from inside this repo because its own root `.gitignore` ignores `.ai/cezar/` wholesale. `src/data-gitignore.test.ts` cross-checks the list against every `join(dataDir, '…')` in the service. |
| Agent runners / backends | `AGENT_PROTOCOL.md` (the contract), then `packages/cezar/src/core/agent-runner.ts`, `packages/cezar/src/core/runner-factory.ts`, `packages/cezar/src/core/claude-cli-runner.ts`, `packages/cezar/src/core/codex-app-server-runner.ts`, `packages/cezar/src/core/opencode-server-runner.ts`, `packages/cezar/src/core/cursor-agent-runner.ts`, `packages/cezar/src/core/backend-detect.ts` | One seam: every backend implements `AgentRunner`/`AgentSession` and emits both the v1 `AgentEvent` stream and the normalized v2 `UiEvent` protocol. **Read `AGENT_PROTOCOL.md` first — it is the contract a new runner must satisfy** (the event schema, per-backend mapping, golden-fixture testing, and the backend-parity requirement), with an explicit new-runner checklist. New backends slot in as one class — do not leak backend-specific types past the seam. `claude-cli` is a legacy backend id kept so old run records parse. `CEZ_DRY_RUN=1` must keep working (bundled mock, no real CLI). Tool access goes through `allowedTools`, but the zero-config default includes unrestricted `Bash` (no `bashAllowlist`) and Codex/OpenCode/Cursor don't honor `allowedTools` at all — Cursor's `--force`/`--trust` auto-approves tool use instead (see #430). |
| HTTP server & API routes | `packages/cezar/src/server/server.ts` | Hono app, binds to `127.0.0.1` only. A global `/api/*` request-origin guard (#426) runs before every route except `/api/v1/health`: it rejects non-loopback `Host` headers (DNS-rebinding) in local mode and cross-origin mutating requests (CSRF) in both modes — zero-config, no token, so the same-origin cockpit and the Vite dev proxy pass untouched. The loopback test is `isLoopbackHostHeader` (anchored 127.0.0.0/8, missing Host = untrusted); never match it with a `127.` string prefix. **Request validation is route MIDDLEWARE, not a `safeParse` inside the handler** — `jsonZodValidator` / `paramZodValidator` / `queryZodValidator` from `packages/cezar/src/server/validators.ts`, read from `c.req.valid('json'|'param'|'query')`. Hono records a validated shape in the route type only when validation happens as middleware, so a handler-side parse is invisible to `AppType` and the typed client would accept any body for that route. The rejection shape is unchanged: `{ error }` with 400/404/409. CORS is deliberately enabled for `/api/v1/health` only (cross-origin discovery); never widen it. SSE endpoints replay from NDJSON then stream live, deduped by `seq`. |
| API request/response shapes (any new field, route or payload) | `packages/contract/src/*.ts`, then `packages/cezar/src/server/validators.ts` | **One zod definition per shape, type inferred — never a hand-written interface.** Add the schema here FIRST, then chain the route and validate through the trio; `contract-parity*.test.ts` proves the schema and the route agree in both directions, and `typed-bodies.test.ts` proves the route reached `AppType` at all. See the HTTP API section above for why a loose `app.get(…)` disappears. |
| Real-time events (live UI signals, replacing polls) | `.ai/specs/2026-07-23-websocket-subscriptions.md`, then `packages/cezar/src/server/ws.ts` + the `health` topic in `packages/cezar/src/server/server.ts`, and `packages/web/src/api/ws.ts` | Two live channels: the SSE run/event stream (`/api/v1/workspace/events`, `packages/web/src/api/global-events.tsx`) and the **WebSocket subscription bus** (`/api/v1/ws`, `ws` lib server-side, native `WebSocket` client). The bus is the pattern for any new live signal instead of a `refetchInterval`. Its whole discipline is **demand-driven subscription**: a topic's server publisher starts on the `0→1` subscriber and stops on `1→0`, and one socket per cockpit ref-counts listeners per topic. **Subscribe at the scope that matches the data's demand lifetime, always return the unsubscribe (`useEffect(() => subscribeTopic(...))`), publish only on change** — a leaked subscription is a server publisher that never stops AND a component that keeps waking for a screen the user left. Per-view signals subscribe in the view; **session-global signals subscribe ONCE at the root**, not per reader — health is the worked example: one `useHealthSubscription()` controller in `GlobalEventsProvider` subscribes after bootstrap only for local mode, while `useHealth()` is a pure cache read the ~15 readers call without touching the socket. Remote mode must open no browser WebSocket because it cannot explicitly carry reverse-proxy credentials; it uses authenticated HTTP plus SSE reconciliation. One socket per local cockpit — never `new WebSocket` for a feature; add a topic. Liveness: server ping/pong reaping + app-level beat, client watchdog + reconnect. Topics are workspace-level (single-mount, never `/api/v1/p/`) and, because the upgrade guard admits any loopback origin (WS has no CORS), must only carry data safe for any local page — read the spec's security caveat before adding one. |
| Workspace registry / per-user state (`~/.cezar/`) | `packages/cezar/src/paths.ts`, then `packages/cezar/src/workspace/config.ts`, `packages/cezar/src/workspace/projects.ts`, `packages/cezar/src/workspace/migrations.ts`, `packages/cezar/src/workspace/semaphore.ts` | `~/.cezar/config.json` is the per-user workspace config **and** the project registry — every repo cezar has been booted in (spec `2026-07-20-multi-project-workspace`). Every path goes through `cezarHomeDir()` so `CEZ_HOME` keeps tests and containers off a real home; never re-derive `homedir()` elsewhere. Schema rules are load-bearing: every field optional with `.catch`, `.passthrough()` at every object level, per-entry salvage for `projects` (one bad row never evicts the registry), and writes only through `mergeWriteWorkspaceConfig` (read-modify-write + atomic tmp/rename, `0600`) so two processes converge. A corrupt or read-only home degrades to in-memory defaults with ONE warning — never a boot failure. Migrations are config-files-only, idempotent, additive and non-blocking; run state never migrates. `maxParallel`/`memoryLimitMb` are workspace-wide (`resources`): the per-repo keys are imported once by migration 001 and ignored by enforcement afterwards. Registration is suppressed for task worktrees and `$HOME` itself (`shouldRegisterProject`). A merge-write resolves its path ONCE and uses it for both halves — resolving it twice let a `CEZ_HOME` that changed mid-flight read one home and write another, wiping the user's registry. Two guards back that up: `assertCezarHomeWriteIsSandboxed` refuses any write into the real `~/.cezar` from a vitest process, and `packages/cezar/vitest.setup.ts` pins `CEZ_HOME` to a per-worker sandbox so no case ever runs unpinned. Every write that leaves projects behind also snapshots `config.json.bak`, which `loadWorkspaceConfig` restores from when the config is missing, empty or corrupt (a config that parses and is simply empty is the user's own state and is never overridden). | **Agent accounts** (spec `2026-07-29-agent-profiles`) live in this directory too, but in their OWN file — `~/.cezar/agent-accounts.json`, never a key in `config.json`, so a cezar that has never heard of accounts cannot drop them; `src/workspace/agent-accounts.ts` owns the store, `src/core/agent-profiles.ts` the vendor knowledge and `src/workspace/agent-profiles.ts` the resolution. |
| Project-scoped routes & contexts (`/api/v1/p/:projectId`, `/p/:projectId`) | `packages/cezar/src/server/project-context.ts`, then `packages/cezar/src/server/server.ts`, `packages/web/src/routes.tsx` | One lazily built `{store, manager, dataDir, launchKey}` context per registered project, disposed on removal; a `missing` root is never instantiated (unknown id → 404, gone root → 409). Every project route is registered **once** in a chained family builder and mounted under both `/api/v1/<path>` (bound to the boot project) and `/api/v1/p/:projectId/<path>` — `route-parity.test.ts` walks the exported route manifest and asserts `/api/v1/x`, `/api/v1/p/<boot>/x` and `/api/v1/p/default/x` answer byte-identically, so a new scoped route without its alias fails the suite. `default` is the reserved boot alias and is never an allocated slug. Workspace-level routes (`/projects`, `/workspace/*`, `/fs/browse`) are single-mount and never scope-prefixed. The cockpit mirrors the split: every view lives under `/p/:projectId/`, flat legacy PAGE paths redirect to the boot project, and global settings sit outside the scope at `/settings/global` — both settings roots render ONE full-screen frame with one nav (`routes/settings/settings-shell.tsx`; the app shell is chromeless on any `isSettingsPathname`, spec `2026-10-13-unified-settings`). **One versioned API surface**: the unversioned `/api/*` spelling was removed — everything answers under `/api/v1`. A chained builder is the only shape Hono can infer route types from, which is what makes `AppType` (`packages/cezar/src/server/app-type.ts`) and the typed client cover the API. **Add a route by adding a link to its family's chain, never a loose `api.get(…)` statement** — a statement reaches neither the typed client nor `AppType`; `versioned-surface.test.ts` fails on any route outside `/api/v1`, and `bc-route-inventory.test.ts` (which reads a built app's route table, not the source) requires it to be inventoried in BACKWARD_COMPATIBILITY.md §2. |
| Git / worktree logic | `packages/cezar/src/git-worktree.ts`, `packages/cezar/src/git-diff-base.ts`, `packages/cezar/src/server/git.ts` | One worktree per task at `.ai/cezar/worktrees/<runId>`, on a branch off the configured base branch — `cez/<id8>` by default, or a namer-generated name under `branchNaming` (spec 2026-10-11-branch-naming-instructions); the name lives on the run record and is never recomputed from the id. Helpers never throw (except `createWorktree`) — degradation is the caller's policy. Diffs are capped (`DIFF_CAP`). Orphaned worktrees are pruned at startup. **Never hard-code `origin`**: the PR-target ("base") remote and the push remote come from `resolveTaskRemotes` (`git-remotes.ts`) — `gh repo set-default`, else the remote GitHub confirms is `origin`'s fork parent, else `origin`; push = `remote.pushDefault`, else `origin` — so a contributor's fork clone forks tasks from, diffs against and opens PRs on the canonical repo, while a downstream copy (`upstream` = the project it tracks, not its GitHub parent) keeps `origin`. Never pick a remote by its name alone. **"Which ref anchors *this task's* diff" lives in exactly one place** — `resolveTaskDiffBase` (`git-diff-base.ts`): the merge-base against the **freshest** base ref (`origin/<base>` when the local branch is behind it — agents fetch, they never pull, and a stale local `main` is what turned an eight-line fix into `+59514 −12160`), and, when the agent checked another branch out into the worktree, that branch **as the run found it** (`<branch>@{<run start>}`), whichever of the two attributes fewer changed lines. Attributing a checked-out branch's history to this task is what produced five-figure numbers on review and QA runs (#591 fixed the Changes tab, #751 the stored `diffStat`); anchoring those runs at `HEAD` instead then reported `+0 −0` for work they really did commit. A new task-diff surface resolves through that helper and passes the run's `branch` **and** `startedAt`; `worktreeDiff`/`worktreeDiffStat` deliberately keep the whole-branch anchor and each carry a comment saying why. |
| GitHub integration (issues/PRs tab, draft PRs) | `packages/cezar/src/server/github.ts`, `packages/cezar/src/server/pr.ts` | Must degrade gracefully: no `gh`, no remote, offline all return `{ available: false, reason }` — never an error. `gh … --json` output is zod-validated at the boundary. `GITHUB_TOKEN` is the fallback when `gh` isn't authenticated. `createDraftPr` never throws; failures map to one-line human errors. `CEZ_DRY_RUN=1` fakes the PR URL. |
| Workflows (YAML chains, steps, retries) | `packages/cezar/src/workflows/types.ts`, then `packages/cezar/src/workflows/load.ts`, `packages/cezar/src/workflows/run.ts` | A step is agent (`prompt`/`skill`) XOR check (`command`); a file has `steps` XOR the portable `skills` shorthand — both enforced by zod refinements. `onFail.retry` may only reference an _earlier_ step (`stepsIssue`). `{{task}}` is the substitution token. `quick-task` is the built-in zero-config workflow; built-ins always come back after delete. |
| Skills (Markdown playbooks, team repos) | `packages/cezar/src/skills.ts`, `packages/cezar/src/skills-remote.ts` | A skill is a `.md` file with optional YAML frontmatter (`name`, `description`); its body becomes the agent's extra system prompt. Discovery precedence is local-first: `.ai/cezar/skills` → `.ai/skills` → `.agents/skills` + agent mirrors → global → team repo. Missing dirs are fine; team-skill loading never blocks on the network (background cache in `~/.cache/cez/`). |
| Runs store / state persistence | `packages/cezar/src/runs/store.ts` | Plain files in `.ai/cezar/`, no DB: `runs.json` index (zod-validated, atomic tmp+rename, debounced save) plus one append-only NDJSON event file per run. New `RunRecord` fields must be optional so old files still parse; corrupt files degrade to fresh, never crash. A **queued** run's prompt stays editable until the scheduler picks it up (#472): `task` plus the optional `queuedMessages` stack are folded into `{{task}}` at dequeue, and that fold is read-only — never write it back to the record. The store is also the in-process event bus for SSE. |
| Web UI (cockpit) | `packages/web/src/app.tsx`, then `packages/web/src/routes.tsx`, `packages/web/src/api/`, and the affected component/route | React 19 + Vite + Tailwind v4 + shadcn/ui; source lives in `packages/web/` (its own npm workspace, `@open-mercato/cezar-web`, with the React/Vite/Tailwind deps in ITS manifest, not the root's), build output in ignored `packages/cezar/web/dist/`. Anything the server and the cockpit BOTH speak — the v2 protocol types, the `/api/v1/p/:projectId` scope helpers, and (as it grows) the typed API client — lives in the third workspace `packages/api-client/` (`@open-mercato/cezar-api-client`), which must stay Node-free: no `node:*` import and no `@types/node`, because it is bundled into the browser AND imported by the Node service. Keep one global SSE connection and patch the TanStack Query cache in place; authoritative refetch happens on reconnect/visibility. Preserve light/dark/system theming, mobile safe areas, keyboard access, and unit coverage. The legacy vanilla web app was deleted in R7; when `packages/cezar/web/dist` is missing the server answers every shell route with a built-in "run `npm run build:web`" hint page (`packages/cezar/src/server/static-ui.ts`). |
| Agent config files (Settings → Agent config; grouped by agent, MCP as a per-agent subsection — spec 2026-07-17-agent-config-by-agent, descriptor table in `packages/web/src/routes/settings/agent-descriptors.ts`) | `packages/cezar/src/agent-config/` (`catalog.ts`, `files.ts`, `validate.ts`, `service.ts`, `seed.ts`), then `packages/cezar/src/paths.ts` and the `/api/v1/agent-config` routes in `packages/cezar/src/server/server.ts` | Read and edit the coding agents' OWN config files (Claude/Codex/OpenCode settings, MCP, memory), raw and per-scope (spec #404). `catalog.ts` is the ONLY place vendor knowledge about config FILES lives — paths + verbatim precedence strings; keep it accurate and dated. Its sibling `src/core/agent-profiles.ts` owns the other half: the env var that relocates each agent's whole home (spec `2026-07-29-agent-profiles`). Never re-serialize a file cezar opened (byte-exact round-trip). Files are addressed by catalog id, never a path. **Writing is a local-machine capability: every `PUT /api/agent-config/:id` 409s when `capabilities().localHandoff` is false — this closes a hooks-based RCE path, do not weaken it.** The gitignored personal layer is seeded into run worktrees (`seed.ts`, guarded by `git check-ignore`). |
| Branches, releases, backports (which branch a PR targets, versions, release workflows) | `SDLC.md` § Release lines and backports, then `docs/publishing.md` § Release lines, `scripts/release-line.mts`, `packages/cezar/src/release/` | Every PR targets `main`; a fix a released line needs carries `backport release/vX.Y.x`, and the Backport workflow opens the cherry-pick PR. Never push to `release/v*` or a `v*` tag and never bump a version by hand — *Release cut* and *Release* stamp manifests, lockfile and changelog, *Release publish* (started by the tag) publishes at the tagged commit, and only a maintainer runs them. Every write uses the release GitHub App's token, never `github.token`. Only the newest line moves npm `latest`; older lines publish under `latest-X.Y`. |
| User documentation (`docs/site`: feature pages, guides, architecture) | `.ai/skills/cezar-docs/SKILL.md`, then `docs/site/content/docs/05-contributing/01-docs-contribution-guide.mdx` | A Next.js + Fumadocs app with its own `package.json`, deliberately NOT an npm workspace, so the repo's `npm ci` never pulls Next.js. Pages are MDX under `content/docs` and follow the Next.js docs conventions: file-system routing with `NN-` order prefixes, required `title`/`description` frontmatter, `version` for non-default availability, `> **Good to know**:` notes. A user-visible behavior change updates its feature page. `npm run docs:dev` previews it on port 4322, and `npm run docs:check` / `docs:build` gate it. The check also rejects a `CEZ_*` variable that `.env.example` does not list. |
| Feature specs / design history | `.ai/specs/` | Numbered and dated specs are the design record — code comments cite them (`spec 006`, `#348`). Read the relevant spec before changing a feature it covers; keep new work consistent with it or update the spec. |
## Comments
Keep them short. A code comment is one or two lines on WHY, never a retelling of WHAT the next
lines do. Cite the spec or issue (`spec 006`, `#348`) instead of restating it — the reasoningSignals
- Stars
- 826
- Forks
- 152
- Open issues
- 153
- Last commit (this folder)
- Oct 23, 2026 (5 days ago)
- Last release
- Oct 23, 2026 (5 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/open-mercato/cezar
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from open-mercato
Cite this page
AGENTS.md · open-mercato/cezar. Agent Mart. https://agentmart.dev/templates/open-mercato-cezar-agents-md (last updated 2026-10-24)