Develop
Development and architecture
How Cortex is built, how its parts fit together, and how to build, test and run it, as the code stands today. Design decisions are in the ADRs (docs/adr/, in Portuguese); the roadmap is in Planning/Plan.md and Planning/TODO.md. Items marked not implemented are planned work, not features.
1. Platform#
Cortex runs agents on Windows only: processes are launched suspended into a Job Object and
terminal mode uses ConPTY, both through windows-sys. The workspace still compiles elsewhere
because cortex-infrastructure has #[cfg(not(windows))] stubs (processes.rs, pty.rs,
orphans.rs) that return ErrorKind::Unsupported; there, spawn fails with
process.job_failed, and the adapters look only for claude.exe, codex.exe and
opencode.exe. No minimum Windows version is declared in the repository.
2. Components#
Workspace members are listed in the root Cargo.toml (edition 2024, rust-version = "1.89").
| Component | Path | Responsibility |
|---|---|---|
cortex-domain |
crates/cortex-domain |
Entities and state machines (Core phase, Agent, Session, Process, Objective/Task), AgentManager, SessionRegistry, ProcessSlots, CortexError, the ports (ProcessPort, StoragePort, LogPort, EventBusPort, ...) and the AgentRuntime contract (version 2). The testing feature exposes fakes and a reusable runtime-contract suite. |
cortex-application |
crates/cortex-application |
The Cortex API (CortexApi): every use case (projects, agents, start/stop/restart, input/output, terminal, runtime health, objectives and tasks, boot recovery, MCP credentials and OAuth). Single entry point of every surface. |
cortex-infrastructure |
crates/cortex-infrastructure |
Port implementations: file storage with atomic writes, JSON-lines logger, in-memory event bus, Process Manager (Job Objects, ConPTY, orphan reconciliation), instance lock, directories, config.json loading, repositories, OAuth store, secrets, clock and ids. |
cortex-runtime-claude-code, -codex, -opencode |
crates/cortex-runtime-* |
One adapter per CLI. The only code that knows executables, flags and protocols. |
cortex-mcp |
crates/cortex-mcp |
MCP server over HTTP on 127.0.0.1 (std sockets, one thread per connection). Holds no state or business rule. |
cortex-bootstrap |
crates/cortex-bootstrap |
Composition root. start / start_with wire every layer and return a RunningCore; shutdown drains and stops it. |
cortex-desktop |
apps/desktop/src-tauri |
Tauri 2 host. CoreState holds the Core, 28 #[tauri::command]s (listed in main.rs) map to CortexApi calls, and events.rs forwards a subset of Core events to the window as cortex://event. |
| UI | apps/desktop/ui |
React 19, TypeScript, Vite, Vitest. Talks to the host only through src/api/tauriClient.ts. Frameless window with its own title bar (WindowControls.tsx, DesktopWindowProvider.tsx). |
Not part of the app or the Cargo workspace: apps/landing (public static site, separate npm
project, ADR 0007), apps/commercial (a Remotion video project) and
Cortex-layout/ (the visual reference the UI was built from).
Layering rules#
cortex-desktop ──► cortex-application, cortex-bootstrap
cortex-mcp ──────► cortex-application
cortex-bootstrap ► domain, application, infrastructure, mcp and the three adapters
cortex-application, cortex-infrastructure, cortex-runtime-* ──► cortex-domain only
cortex-domain ───► nothing in CortexEnforced by cargo test -p cortex-bootstrap --test architecture (tests/architecture.rs):
- Dependencies per crate are read with
cargo metadataand compared with an allow-list. Every workspace crate must declare its layer there, or the test fails.tauriandtauri-buildare allowed only incortex-desktop. - No CLI executable, flag, protocol message or CLI variable (
claude.exe,stream-json,--sandbox,session/prompt,OPENCODE_PERMISSION, ...) may appear in production sources outside the adapter crates. Test code is excluded. A second test proves the scan finds a hit. cortex-mcpmust never reference the desktop-only OAuth use cases (approve, list, revoke clients), so a bearer cannot approve its own request.
The UI has its own check (apps/desktop/ui/src/test/architecture.test.ts): only
api/tauriClient.ts may import @tauri-apps/api, and no file may import from outside
apps/desktop/ui. CI is limited to .github/workflows/windows-installer.yml, which runs on a v*.*.* tag or by hand on a Windows runner: UI npm ci, npm run typecheck, npm test and npm run build, then cargo test --workspace --locked, then the NSIS installer build, its SHA-256 and a workflow artifact. It does not run cargo fmt or cargo clippy, and nothing runs on pushes or pull requests.
3. Surfaces and boundaries#
Desktop. main.rs calls cortex_bootstrap::start(BootOptions::default()) and then opens the
window. If the Core cannot start, the window still opens with the startup error
(core_status.startup_error) and the exit code is 1. The error, remediation, causes and
correlation id are also printed to stderr, which is visible only in debug builds (release builds
have no console). Closing the window runs RunningCore::shutdown. Desktop calls run as the user
(Origin::Desktop).
MCP server. It runs inside the desktop process, started by the composition root after the
Core is READY; it exists only while the app is open. Each connection carries one request, and
the 32 tools (catalog in crates/cortex-mcp/src/tools.rs) cover projects, runtimes, agents,
messages, events, objectives, tasks, plan proposals (ADR 0031: a planner's DAG is stored apart and only the user's approval creates the objective), skills and the message bus between agents. Every request is
authenticated through CortexApi::authenticate; the permission policy (BasicPermissions) lets
the user and authorized OAuth clients do what the desktop can, and lets an agent (Session
credential) read and use the message bus (post_message, ack_message;
ADR 0025), nothing else. An agent with the Orchestrator role
(ADR 0035) may also create_agent, update_agent and
start_agent for the Workers of its own project, and nothing more. If the
port or the token record fails, the Core logs mcp.start_failed and runs without MCP. Details and
threat model: ADR 0008, ADR 0010.
CLI. Not implemented. The workspace has one binary, cortex-desktop. The cortex CLI is
planned (see Planning/Plan.md), including the open decision on how it reaches the Core
without a second owner of the data directory.
4. Agent lifecycle and process management#
An Agent is persistent (stable id, display name, project, runtime id, config, interaction mode).
Its Session and Process are volatile: created on start, never persisted or resumed. The Core phase
is STARTING → READY → DRAINING → STOPPED. Agent states (cortex-domain/src/lifecycle.rs):
CREATED → STARTING → READY → STOPPING → STOPPED
STARTING → FAILED | CRASHED | STOPPING READY → BUSY → READY
READY | BUSY → FAILED | CRASHED | STOPPING READY → PAUSED → READY
PAUSED → STOPPING | FAILED | CRASHED STOPPING → CRASHED
STOPPED | FAILED | CRASHED → STARTINGBUSY means the Agent is running the turn of a Task. PAUSED (ADR 0035) is a hold: an idle structured READY Agent keeps its Session and process, and nothing is handed to it (Tasks, console input, the idle release) until it is resumed. Session and process states have their own
machines in session.rs and process.rs.
Operations (cortex-application/src/runtime.rs):
- Start: validates the runtime, the project root and the interaction mode, moves to
STARTINGand opens a Session, which takes a process slot. The adapter launches the process; the Agent becomesREADYonce the runtime proves it accepts input (60 s limit). Any failure ends what was created and leaves the AgentFAILEDwith the cause inlast_failure. Starting an Agent that isSTARTING,READYorBUSYreturns it unchanged. - Stop:
STOPPING, the runtime is asked to end, the whole process tree is ended after a grace period (5 s for Claude Code and OpenCode), thenSTOPPED. Idempotent. - Restart: stop plus start under one correlation id, with a new Session and process. The slot
is not held between the halves, so it can fail with
resource.limit_reached. - Pause / Resume:
pause_agentclaims the Session atomically with the state change (READY → PAUSED, only with no turn or attempt running; a terminal Agent is refused), so a console input cannot start a turn on a paused Agent.resume_agentisPAUSED → READY. Both are idempotent and desktop-only. Stop and Restart work fromPAUSED. - Unexpected exit: the Session becomes
LOSTand the AgentCRASHED(runtime.exited_unexpectedly, even with exit code 0). In terminal mode, exit code 0 is an orderly end and the Agent goes toSTOPPED. - Limit:
resources.max_active_processesslots (default 6), one per Session. A start over the limit is refused withresource.limit_reachedand changes nothing; there is no queue. - Tasks:
create_objective(desktop or MCP) creates up to 10 Tasks. A dispatcher handsREADYTasks toREADYstructured Agents of runtimes that declareturn_completion, at mostorchestrator.max_parallel_tasksat a time (default 2), and per project or runtime at most the optionalmax_parallel_tasks_per_project/max_parallel_tasks_per_runtime; the Orchestrator refuses a start past any of them (ADR 0024). Default retry policy: one retry after a 5 s backoff (doubling per retry, capped at 60 s); a Task may ask for up to 5. See ADR 0011.
Process Manager (cortex-infrastructure/src/processes.rs). Each process is created suspended,
assigned to its own Job Object (kill-on-close, so the OS ends the tree if Cortex dies), recorded in
processes.json (PID, OS creation time, executable) and only then resumed. Reader threads drain
its output continuously and a waiter observes the exit. Runtimes inherit Cortex's environment, plus
CORTEX_MCP_URL and CORTEX_MCP_TOKEN (a per-Session credential) while the MCP server is on.
| Runtime id | Process strategy | Interaction | Notes |
|---|---|---|---|
claude-code |
persistent, claude.exe -p over stream-json pipes |
structured or terminal |
Structured mode denies anything that would need approval. Terminal mode runs the CLI's own TUI in a ConPTY; it has no send, interrupt or Tasks. |
codex |
per turn: one codex exec per input |
structured or terminal |
Structured: readiness is codex login status; runs with a read-only sandbox. Terminal: codex --no-daemon, the CLI's own TUI in a ConPTY, with its own sandbox and approval prompts. |
opencode |
persistent, opencode acp |
structured or terminal |
Structured: the adapter denies anything that would need approval. Terminal: the CLI's own TUI in a ConPTY (OpenCode 2 with --standalone), with edits, commands and web fetches set to ask you in the terminal. |
Orchestrator role (ADR 0035). An Agent has a role, worker
(default) or orchestrator, persisted in agents.json (omitted for a Worker, so existing files do not change). It is
enforced in four independent places, none of them a prompt: the permission policy (CallerKind::Orchestrator), the
Core's answer to every tool request of its session (orchestrator_answer: Cortex tools allowed, everything else denied
at once, outside Automatic Decisions and the user), the runtime adapter (orchestrator_role capability; Claude Code
starts it with --tools "" and --strict-mcp-config), and the dispatcher, which never offers it a Task. The start is
refused (runtime.capability_missing, agent.orchestrator_needs_structured, mcp.disabled) instead of running it as a
Worker. Only Claude Code declares the capability today.
For all three, the only Agent config key is model. The desktop "New agent" dialog always creates
claude-code Agents (DEFAULT_RUNTIME_ID in apps/desktop/ui/src/runtimes.ts); codex and
opencode Agents are created through the MCP create_agent tool (always structured). Cortex does
not read or store any CLI credential: the CLIs must already be logged in for the user who runs
Cortex, and only Codex checks it before an Agent becomes READY. Details: ADRs 0003, 0006, 0009.
Recovery after a crash (recovery.rs, run by the composition root while STARTING, before any
command): recorded processes still alive are ended only if creation time and executable path match
the record (never by name); every Agent persisted as STARTING, READY, BUSY, PAUSED or STOPPING
becomes CRASHED with recovery.cortex_restart; Tasks that were RUNNING go through the retry
policy. A failure here is a startup error.
Shutdown (RunningCore::shutdown, idempotent): the MCP server stops accepting, the phase moves
to DRAINING (new commands refused), running Agents are stopped in parallel under a 30 s global
deadline (an Agent not stopped in time stays STOPPING, is logged as runtime.shutdown_incomplete
and is repaired by the next boot recovery), the phase moves to STOPPED and the instance lock is
released.
5. Configuration and persistence#
Data directory, in order: BootOptions.data_dir (unused by the desktop host), the
CORTEX_DATA_DIR environment variable, %LOCALAPPDATA%\Cortex. It must be absolute
(dirs.invalid_data_dir). One instance owns a directory through the OS file lock cortex.lock; a
second fails with instance.already_running. For a separate instance set CORTEX_DATA_DIR and,
if MCP is on, a different mcp.port.
| File in the data directory | Content |
|---|---|
config.json |
Configuration (below). Created with defaults on the first start. |
projects.json, agents.json, objectives.json |
Projects, Agents, Objectives with Tasks and attempts. Created on first write. |
processes.json |
Records of live runtime processes, used by boot recovery. |
mcp.json |
user_token of the MCP surface (64 lowercase hex characters), created on the first start with MCP enabled and never replaced automatically. |
mcp_oauth.json |
OAuth clients and refresh tokens approved by the user. Created on first write. |
cortex.lock |
Instance lock. |
logs/cortex.log, cortex.1.log, ... |
Log files. |
Each record is one JSON file with schema_version (currently 1), written to a temporary
.<name>.<uuid>.tmp and renamed; a leftover temporary file is ignored, not removed, and a UTF-8
BOM is accepted on read. Agent output (256 KiB per Agent, lines cut at 8 KiB) and terminal bytes
(1 MiB) live in memory only. WebView2 keeps its own data outside this directory, by default in
%LOCALAPPDATA%\dev.cortex.desktop (ADR 0001).
config.json must carry "schema_version": 1; every other key is optional and unknown keys
are rejected. An invalid value stops the Core with config.invalid, naming the field. There is no
UI for it: edit the file and restart.
| Key | Default | Rule |
|---|---|---|
logging.level |
"info" |
error, warn, info, debug, trace |
logging.max_file_bytes |
5242880 | 65536 to 1073741824 |
logging.max_files |
3 | 1 to 20, including the active file |
resources.max_active_processes |
6 | 1 to 32 |
orchestrator.max_parallel_tasks |
2 | 1 to 32 |
orchestrator.max_parallel_tasks_per_project |
none | 1 to 32 |
orchestrator.max_parallel_tasks_per_runtime |
none | 1 to 32 |
mcp.enabled |
true |
|
mcp.port |
47821 | 0 (system picks) or 1024 to 65535 |
mcp.public_url |
none | https://host[:port], no path; enables remote access (ADR 0010) |
runtimes.<id>.executable |
none | Absolute path to the CLI executable, used instead of the PATH lookup; <id> is claude-code, codex or opencode |
Executable discovery is per adapter: the configured path, else the first match in PATH
(claude.exe, codex.exe, opencode.exe; OpenCode also accepts the native binary beside its npm
opencode.cmd shim). .cmd shims are never run: Codex refuses a configured path that is not an
.exe, OpenCode refuses a configured .cmd or .bat, and Claude Code only requires the
configured file to exist, so point it at claude.exe. The lookup uses the PATH of the Cortex
process and runs at every start. A missing CLI never stops the Core: the runtime reports
unavailable and a start fails with runtime.not_installed and a remediation.
6. Logging and error handling#
Log. logs/cortex.log holds one JSON object per line: ts, level, module, event,
correlation_id, fields. It rotates at max_file_bytes into cortex.1.log, cortex.2.log, ...
keeping max_files files. Secrets are wrapped in Secret and written as [REDACTED]. Startup
events include core.starting, config.loaded and mcp.started; a failed MCP start logs
mcp.start_failed with the cause. If the log cannot be written, one line goes to stderr; before
the log opens, a startup failure of a release build is visible only in the window.
Errors. Every layer returns CortexError: a stable dotted code (runtime.not_installed,
agent.state_conflict, resource.limit_reached, config.invalid, ...), a category (User,
Integration, Internal), a message, an optional remediation and cause chain, and a
correlation_id. Messages never contain secrets. Each use case runs under its own correlation id,
shared by its log records, events and errors; startup and shutdown have their own. A failed use
case is logged once, at the API boundary, as api.command_failed (warn for User errors,
error otherwise). The UI shows the correlation id from ErrorDto, so a failure can be found in
the log. Writes are refused with core.not_ready unless the Core is READY.
7. Build, test and run#
Prerequisites (no toolchain file pins any of them): Rust 1.89 or newer (rust-version) with the
MSVC host toolchain on Windows; Node.js and npm for the UI (vite build needs ^20.19.0 or
>=22.12.0; the locked test dependencies, jsdom among them, need ^22.22.2, ^24.15.0 or
>=26, so use one of those for npm test); the WebView2 runtime to run the app; and, optionally,
Claude Code, Codex or OpenCode installed and logged in by you.
Build. The host embeds apps/desktop/ui/dist at compile time
(ADR 0004); build the UI first or Cargo refuses to compile
cortex-desktop.
| Command | Directory | Purpose |
|---|---|---|
npm ci |
apps/desktop/ui |
Install UI dependencies |
npm run build |
apps/desktop/ui |
tsc --noEmit and vite build into dist/ |
cargo build -p cortex-desktop [--release] |
repo root | Build target\debug\cortex-desktop.exe or target\release\... |
Run. Start.bat (repo root) runs npm install if node_modules is missing, builds the UI and
the host, and starts it; Start.bat release does so in release mode (no console window).
cargo run -p cortex-desktop also works once dist/ exists. To keep development data out of
%LOCALAPPDATA%\Cortex, set CORTEX_DATA_DIR first (a commented line in Start.bat). npm run dev starts only Vite: the UI reaches the Core solely through Tauri IPC and the tree has no browser
fallback, so UI work is checked with Vitest against src/test/fakeCore.ts and by running the host.
Test.
| Command | Directory | Notes |
|---|---|---|
cargo test --workspace --exclude cortex-desktop |
repo root | Whole Rust suite without the UI build |
cargo test -p cortex-desktop |
repo root | Needs ui/dist; real commands on a mock Tauri runtime |
cargo test -p cortex-bootstrap --test architecture |
repo root | Layering checks only |
npm run typecheck, npm test |
apps/desktop/ui |
tsc --noEmit, vitest run (jsdom) |
cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings |
repo root | Style and lint |
cargo test --workspace runs the first two together. Caveats:
- Tests use temporary data directories and either disable the MCP server or bind an ephemeral port, so they can run while a real Cortex is open.
crates/cortex-infrastructure/tests/processes.rsis#![cfg(windows)]and launches real processes (cmd.exe).- Seven tests are
#[ignore]d because they drive real CLIs, which must be installed and logged in, and they spend model tokens. Run them on purpose:- Claude Code:
cargo test -p cortex-bootstrap --test runtime -- --ignored --test-threads=1 - Codex:
cargo test -p cortex-bootstrap --test codex -- --ignored(CORTEX_CODEX_EXECUTABLEoverrides thePATHlookup) - OpenCode:
cargo test -p cortex-bootstrap --test opencode -- --ignored --test-threads=1(OPENCODE_EXEoverrides thePATHlookup)
- Claude Code:
- ADR 0006 records that antivirus once flagged the
codexintegration test binary as a false positive and that validation run excluded it.
8. Packaging#
The commands above only produce cortex-desktop.exe. The Windows installer (NSIS, per-user) is
built with npm run installer in apps/desktop/ui, which runs tauri build with the bundle
settings of apps/desktop/src-tauri/tauri.conf.json. Versioning, the build, signing (none yet),
validation and the release list are in Release and packaging. On Windows the window is
frameless (tauri.windows.conf.json); the title bar is drawn by the UI
(components/WindowControls.tsx, state/DesktopWindowProvider.tsx).
9. Where to read more#
The decision records are docs/adr/0001 to 0013: foundation (0001), Agents and event bus (0002), Claude Code runtime (0003), UI and IPC (0004), start/stop/recovery (0005), interactive terminal (0006), landing page (0007), MCP (0008, 0010), runtime adapters (0009), Objectives and Tasks (0011), frameless window and installer (0012), documentation site (0013). The full design and invariants are in Planning/Plan.md; the roadmap and session status are in Planning/TODO.md.