agentleFS
open-mercatoopen-mercato/cezar/AGENTS.md

cezar

AGENTS.md962 starsChanged 35 days agoMITView on GitHub

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…

  • Reads credentials
  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repositoryOne other file it gives its agents.

The file

We split this AGENTS.md into the rules it gives an agent

5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When a feature seems to need configuration, the design is wrong.

    line 15
  2. Distinctive:

    Prefer a proxy-free, daemon-free mechanism when one exists — and when it doesn't, keep the mechanism invisible: no process to manage, no …

    line 27
  3. Distinctive:

    Never trade a working default for a knob.

    line 28
  4. Very specific:

    Keep them short. A code comment is one or two lines on WHY, never a retelling of WHAT the next lines do.

    line 160
  5. Distinctive:

    Before any commit or PR, run in order:

    line 168

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

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 encrypted per-project records in cezarHomeDir()/tracker-connections/, written by Settings and decrypted only into the project connection object, never into process.env or agent environments.

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 fifteen minutes per machine, and only alongside the cockpit's other requests (no timer of its own) — 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.
  • Owner-approved exception (2026-10-22): HTML artifacts (spec .ai/specs/2026-10-22-html-artifacts-and-impact.md, O1) may load scripts and styles from a fixed CDN allowlist — cdnjs, jsDelivr, unpkg, plus Google Fonts — by default; CEZ_ARTIFACTS_CDN=0 drops every https: source. The page is still sandboxed to an opaque origin with connect-src 'none' and data:/blob:-only images, so the CDN seeing the request is the whole widening. The CSP lives in one helper, artifactCsp.
  • 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:

PathPackageWhat it is
packages/cezar@open-mercato/cezarThe 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-contractThe 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-clientThe 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-webThe 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 first. It maps the shared provider seam, registration points, UI metadata and verification requirements.

When the task involves…Read firstKey rules
CLI entry, serve/run/init subcommands, flagspackages/cezar/src/index.tsUses 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 / backendsAGENT_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/pi-runner.ts, packages/cezar/src/core/omp-rpc-runner.ts, packages/cezar/src/core/backend-detect.tsOne 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 routespackages/cezar/src/server/server.tsHono 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 AppTypeand 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/healthonly (cross-origin discovery); never widen it. SSE endpoints replay from NDJSON then stream live, deduped byseq`.
API request/response shapes (any new field, route or payload)packages/contract/src/*.ts, then packages/cezar/src/server/validators.tsOne 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.tsTwo 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.tsxOne 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 logicpackages/cezar/src/git-worktree.ts, packages/cezar/src/git-diff-base.ts, packages/cezar/src/server/git.tsOne 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.tsMust 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.tsA 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.tsA 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 persistencepackages/cezar/src/runs/store.tsPlain 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/routeReact 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.tsRead 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.mdxA 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 reasoning lives there. An explanation that needs a paragraph belongs in the spec or the commit message, not above a function. The same goes for PR descriptions, review comments and CHANGELOG entries: lead with what changed, in a few lines.

Validation

Before any commit or PR, run in order:

npm run typecheck   # tsc --noEmit (api-client + server + web)
npm test            # vitest — server + cockpit unit suites
npm run test:unit   # node:test — fast core-module coverage (packages/cezar/test/unit/)
npm run build       # tsc → dist/, vite → packages/cezar/web/dist/, then the check:pack tarball gate
npm run test:package # pack/install the release tarball and exercise the built CLI (packages/cezar/test/e2e/)

npm test and npm run test:unit are the fast unit gate: no server, no browser. They must stay that way. npm run test:package needs a completed npm run build (it packs the tarball).

Opening a fix PR — any bug fix, telemetry fixes included — also needs a backport decision. Find the newest line (git branch -r --list 'origin/release/v*'); if the bug exists in that line's code (git merge-base --is-ancestor <introducing commit> origin/release/vX.Y.x, or just read the file there), label the PR backport release/vX.Y.x; otherwise say in the PR body why not. "The reported occurrence is already fixed on the line" is not a reason to skip when the code you are hardening is on it too. Rules: SDLC.md § Release lines and backports.

Run vitest through npm, never npx vitest. It is a devDependency of this repo, so npm test uses the installed, version-pinned binary; npx will happily reach past it and fetch a different version from the registry, which is a slow, networked, silently-different test run. To narrow a run, pass vitest's own arguments after --:

npm test -- packages/web/src/routes/settings   # one directory
npm test -- --testTimeout=30000 path/to/one.test.ts
npm test -- -t "the name of one test"

The UI smoke suite is a separate command — it boots the real app and drives it in a real Chrome through the agent-browser provider (.ai/browsers/agent-browser.md):

npm run test:e2e    # .ai/scripts/e2e.sh → test-env-up.sh + vitest (packages/web/e2e/)

It boots the app on a free port with CEZ_DRY_RUN=1 (agent CLIs mocked — no login, no network), reuses an already-healthy instance instead of double-booting, and writes .ai/qa/test-env.json so QA skills attach to the same instance. Stop it with .ai/scripts/test-env-down.sh. Exit contract:

ExitMarkerMeaning
0TEST_E2E_STATUS=passedevery spec passed
0TEST_E2E_STATUS=skippedagent-browser could not be provisioned (no network / unsupported platform); prints a loud banner — not a pass
non-zeroTEST_E2E_STATUS=faileda spec failed, or the env could not boot

CEZ_DRY_RUN=1 npm run dev still exercises the whole cockpit offline for manual verification.

Related documents

  • AGENT_PROTOCOL.md — the agent protocol: the runner seam, the v1 AgentEvent + v2 UiEvent streams, per-backend mapping, the golden-fixture testing contract, and the checklist for adding a new runner.
  • SDLC.md — ticket flow, label state machine, QA gate, claim protocol.
  • CODE_REVIEW.md — what reviewers check and how severities are assigned.
  • BACKWARD_COMPATIBILITY.md — the public surfaces you must not break silently.
  • .ai/agentic.config.json — machine-readable pipeline config every om-* skill reads (base branch, validation commands, labels).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.