CORTEX Docs

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#

text
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 Cortex

Enforced by cargo test -p cortex-bootstrap --test architecture (tests/architecture.rs):

  • Dependencies per crate are read with cargo metadata and compared with an allow-list. Every workspace crate must declare its layer there, or the test fails. tauri and tauri-build are allowed only in cortex-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-mcp must 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):

text
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 → STARTING

BUSY 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 STARTING and opens a Session, which takes a process slot. The adapter launches the process; the Agent becomes READY once the runtime proves it accepts input (60 s limit). Any failure ends what was created and leaves the Agent FAILED with the cause in last_failure. Starting an Agent that is STARTING, READY or BUSY returns 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), then STOPPED. 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_agent claims 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_agent is PAUSED → READY. Both are idempotent and desktop-only. Stop and Restart work from PAUSED.
  • Unexpected exit: the Session becomes LOST and the Agent CRASHED (runtime.exited_unexpectedly, even with exit code 0). In terminal mode, exit code 0 is an orderly end and the Agent goes to STOPPED.
  • Limit: resources.max_active_processes slots (default 6), one per Session. A start over the limit is refused with resource.limit_reached and changes nothing; there is no queue.
  • Tasks: create_objective (desktop or MCP) creates up to 10 Tasks. A dispatcher hands READY Tasks to READY structured Agents of runtimes that declare turn_completion, at most orchestrator.max_parallel_tasks at a time (default 2), and per project or runtime at most the optional max_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.rs is #![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_EXECUTABLE overrides the PATH lookup)
    • OpenCode: cargo test -p cortex-bootstrap --test opencode -- --ignored --test-threads=1 (OPENCODE_EXE overrides the PATH lookup)
  • ADR 0006 records that antivirus once flagged the codex integration 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.

Type to search.