Reference
Command reference
Note
There is no cortex command-line tool. It is planned (see Planning/Plan.md) and not implemented. Cortex is driven in two ways: through its window (see Interface actions) and through its MCP server (see MCP tools), which scripts and other tools can call. The executable cortex-desktop.exe takes no arguments.
What follows are the commands used to build and test Cortex, the 32 MCP tools, and the actions the window offers. Setup and configuration are in Installation, a walkthrough of the window is in User guide, and the architecture is in Development and architecture.
Development, build and test commands#
Prerequisites (Rust, Node.js, WebView2) are in Installation. All commands are for Windows.
Desktop UI#
| Command | Directory | What it does |
|---|---|---|
npm ci |
apps/desktop/ui |
Installs the locked UI dependencies |
npm run dev |
apps/desktop/ui |
Starts Vite only (vite). The UI reaches the Core only through Tauri, so this alone shows nothing useful |
npm run build |
apps/desktop/ui |
tsc --noEmit && vite build into dist/. cortex-desktop embeds dist/ at compile time, so build it before cargo build |
npm run typecheck |
apps/desktop/ui |
tsc --noEmit |
npm test |
apps/desktop/ui |
vitest run (jsdom). Needs a recent Node.js, see Development and architecture |
npm run installer |
apps/desktop/ui |
cd .. && tauri build: rebuilds the UI, builds the release host and packs target\release\bundle\nsis\Cortex_<version>_x64-setup.exe. See Release and packaging |
Rust workspace#
Run from the repository root.
| Command | What it does |
|---|---|
cargo build -p cortex-desktop |
Builds target\debug\cortex-desktop.exe (needs apps/desktop/ui/dist) |
cargo build -p cortex-desktop --release |
Builds target\release\cortex-desktop.exe; no installer |
cargo run -p cortex-desktop |
Builds and starts the host (needs apps/desktop/ui/dist) |
cargo test --workspace --exclude cortex-desktop |
The whole Rust suite without the UI build; tests that drive real CLIs are #[ignore]d |
cargo test -p cortex-desktop |
Tauri commands on a mock runtime (needs apps/desktop/ui/dist) |
cargo test --workspace |
The two rows above together |
cargo test -p cortex-bootstrap --test architecture |
Crate layering checks only |
cargo test -p cortex-mcp |
MCP server tests, including the check that this page lists every MCP tool and parameter |
cargo fmt --all --check |
Formatting check |
cargo clippy --workspace --all-targets -- -D warnings |
Lint with warnings as errors |
Opt-in tests against the real CLIs. The CLI must be installed and logged in by you, and the tests can spend model tokens.
| Command | What it does |
|---|---|
cargo test -p cortex-bootstrap --test runtime -- --ignored --test-threads=1 |
Claude Code |
cargo test -p cortex-bootstrap --test codex -- --ignored |
Codex; CORTEX_CODEX_EXECUTABLE overrides the PATH lookup |
cargo test -p cortex-bootstrap --test opencode -- --ignored --test-threads=1 |
OpenCode; OPENCODE_EXE overrides the PATH lookup |
Start.bat#
| Command | Directory | What it does |
|---|---|---|
Start.bat |
repo root | Runs npm install if apps/desktop/ui/node_modules is missing, then npm run build, cargo build -p cortex-desktop, and starts target\debug\cortex-desktop.exe with a console window |
Start.bat release |
repo root | The same with --release; starts target\release\cortex-desktop.exe, which has no console window |
Set CORTEX_DATA_DIR to an absolute directory before starting to keep development data out of %LOCALAPPDATA%\Cortex (a commented line in Start.bat). A second instance also needs a different mcp.port in that directory's config.json.
Landing page#
apps/landing is a separate Vite site, outside the Cargo workspace.
| Command | Directory | What it does |
|---|---|---|
npm install |
apps/landing |
Installs the dependencies |
npm run dev |
apps/landing |
Vite dev server (Vite's default port, 5173) |
npm run build |
apps/landing |
vite build into dist/ |
npm run preview |
apps/landing |
Serves dist/ |
start.bat |
apps/landing |
Runs npm install if node_modules is missing, then npm run dev -- --open |
Documentation site#
apps/docs is a separate Vite site, outside the Cargo workspace. Its pages are generated at build time from README.md and docs/*.md; the ADRs are not part of it.
| Command | Directory | What it does |
|---|---|---|
npm ci |
apps/docs |
Installs the locked dependencies |
npm run generate |
apps/docs |
Renders the Markdown into generated/ (pages and search-index.json) |
npm run dev |
apps/docs |
generate, then the Vite dev server. Run generate again after editing a .md file |
npm run build |
apps/docs |
generate, then vite build into dist/ |
npm run preview |
apps/docs |
Serves dist/ |
npm run check |
apps/docs |
Checks dist/: every local link, asset and #anchor resolves, no link still points at a .md file, and every search entry points at an existing section |
MCP tools#
The MCP server runs inside cortex-desktop while the window is open and mcp.enabled is true. The headings and parameter tables of this section are checked against the tool catalog by the test reference_docs in crates/cortex-mcp/src/tools.rs.
Endpoint. POST http://127.0.0.1:<port>/mcp, where <port> is mcp.port (default 47821; loopback only). The body is one JSON-RPC 2.0 message with Content-Type: application/json and a Content-Length of at most 1 MiB. There is no event stream: GET and DELETE answer 405. The methods are initialize (protocol versions 2025-11-25, 2025-06-18 and 2025-03-26; server name cortex), ping, tools/list and tools/call. Notifications get 202; JSON-RPC batches are refused. A tool name that does not exist is a JSON-RPC error -32602.
Authentication. Every request carries Authorization: Bearer <credential>:
| Caller | Credential | Can call |
|---|---|---|
| You, locally | The user token (64 hex characters). It is off until you turn it on in the MCP screen ("Local clients"), which copies it and regenerates it; mcp.json holds it sealed with Windows DPAPI. A refused or turned-off token is auth.credential_invalid. Whoever holds it acts as you |
All 32 tools |
| An agent session | The per-session CORTEX_MCP_TOKEN, with the endpoint in CORTEX_MCP_URL; Cortex sets both in the environment of each agent process while MCP is on |
Read tools, post_message and ack_message. Any other tool fails with permission.denied (kind authorization_error). The reads return the agent's own project only: list_agents, poll_agent_events, list_tasks and list_proposals leave out other projects, and get_agent_status or get_task_status on another project's agent or task fails as not found |
| An orchestrator session | The same per-session CORTEX_MCP_TOKEN of an agent whose role is orchestrator |
Everything an agent session can, plus create_agent, update_agent and start_agent, for the agents of its own project and workers only. Every other tool fails with permission.denied |
| An OAuth client | An OAuth access token. Authorization code with PKCE S256 and dynamic client registration, at /.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server, /register, /authorize, /authorize/wait and /token. You approve the client on the MCP screen. Access tokens last 1 hour and refresh tokens 30 days. The routes are served whether or not mcp.public_url is set; that setting (your own HTTPS tunnel, Cortex opens none) is what lets a client on another machine reach them |
All 32 tools |
A missing or refused credential gets HTTP 401 with a WWW-Authenticate: Bearer resource_metadata="..." challenge (plus error="invalid_token" when a credential was sent). Before a tool runs, the server can also refuse with 400, 403 (Host is not 127.0.0.1:<port> or localhost:<port>, or the host of mcp.public_url; or a non-local Origin while mcp.public_url is not set), 404, 405, 408, 411, 413, 415, 431 or 503 (too many connections, or the Core is stopped). These bodies are {"error": {"code": "...", "message": "..."}}; the 401 and the Core-stopped 503 carry the full error object described below.
Results. A tool call always returns a CallToolResult: content holds one text item with the JSON below as text, structuredContent holds the same object, and isError is false. A failure is isError: true with:
{ "error": { "kind": "not_found", "code": "agent.not_found", "message": "...", "remediation": "...", "correlation_id": "...", "agent_id": "..." } }| Field | Meaning |
|---|---|
kind |
Stable class to branch on: invalid_argument, not_found, state_conflict, unsupported, authorization_error, runtime_error or internal_error |
code |
Specific error code, such as agent.not_found, agent.state_conflict, validation.invalid_arguments, core.not_ready |
message |
What went wrong |
remediation |
What to do about it. Present only when the error has one |
correlation_id |
Search logs\cortex.log for it to see the entries behind the error |
agent_id |
Present only when the tool names an agent (get_agent_status, start_agent, send_message, interrupt_agent, stop_agent) and the failure comes from the Core |
Arguments that do not match the schema fail with kind invalid_argument and code validation.invalid_arguments, naming the field. Every schema is closed: an unknown property is refused by name. Absent or null arguments count as {}. Tools that change something (create, send, start, stop, interrupt, cancel) fail with core.not_ready (kind state_conflict) unless the Core is READY.
Timestamps are RFC 3339 strings. The objects named below (Project, Agent, Runtime, Objective, Task, Attempt, Event, Message, Proposal) are defined in Objects returned by the tools.
| Tool | Class | Agent sessions |
|---|---|---|
list_projects, list_runtimes, list_agents, get_agent_status, poll_agent_events, list_tasks, get_task_status, list_skills, read_messages, list_proposals |
read | allowed |
post_message, ack_message |
write | allowed |
create_agent, update_agent, send_message, create_objective, edit_proposal, reject_proposal |
write | denied (create_agent and update_agent are allowed to an orchestrator session) |
start_agent |
control | denied (allowed to an orchestrator session) |
interrupt_agent, stop_agent, cancel_task, cancel_objective, pause_objective, resume_objective, reassign_task, approve_task, retry_task, integrate_task, plan_objective, propose_objective, approve_proposal |
control | denied |
list_projects#
List the projects registered in Cortex. Use it to find the project_id that create_agent needs.
Annotations: read-only, not destructive.
Parameters: none.
Returns { "projects": [Project] }. Projects are created in the window (Add agent dialog); MCP cannot create them.
list_runtimes#
List the runtimes agents can run on: the runtime_id create_agent needs, what each one declares it supports (capabilities, process strategy, model) and whether it can start now (health, with a remediation when it cannot).
Annotations: read-only, not destructive.
Parameters: none.
Returns { "runtimes": [Runtime] }, ordered by runtime_id. The call never waits for a health check: a runtime whose health is stale or unknown starts a check in the background, and a later call sees the result.
list_agents#
List every agent with its state.
Annotations: read-only, not destructive.
Parameters: none.
Returns { "agents": [Agent] }.
get_agent_status#
Get one agent: its state, configuration and the reason of its last failure, if any.
Annotations: read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | yes | 1 to 128 characters. Id of the agent (see list_agents) |
Returns { "agent": Agent }. Fails with agent.not_found (kind not_found) for an unknown id.
create_agent#
Create an agent in the CREATED state; nothing runs until start_agent. Returns the new agent_id.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
display_name |
string | yes | Name shown for the agent. The Core trims it and requires 1 to 64 characters, at least one visible, with no control or text-direction characters (validation.invalid_name) |
project_id |
string | yes | 1 to 128 characters. Project the agent works in (see list_projects); project.not_found otherwise |
runtime_id |
string | yes | Id of a registered runtime (see list_runtimes): claude-code, codex or opencode. A value that is not a runtime id (1 to 32 characters from a-z, 0-9, _, -, starting with a letter or digit) fails with validation.invalid_runtime_id; an unknown but well-formed one with agent.runtime_not_registered |
config |
object | no | Runtime settings as text values; default {}. At most 32 entries; keys of 1 to 64 characters from a-z, 0-9, _, ., -; values of at most 1024 characters and no NUL (validation.invalid_config). Stored and logged as is, so never put secrets in it. The three runtimes understand only model (a non-blank model name); any other key makes the start fail with runtime.invalid_config |
role |
string | no | worker (default) or orchestrator. An orchestrator only coordinates other agents (see Orchestrator role); it needs a runtime that declares orchestrator_role (runtime.capability_missing otherwise). Only you and an authorized client can create one: an orchestrator creating an agent can ask for worker only (permission.denied) |
Returns { "agent": Agent } with state CREATED and interaction structured. Agents created over MCP are always structured, and this is the only way to create a Codex or OpenCode agent. An orchestrator or an agent session can create agents in its own project only (project.not_found for another).
update_agent#
Rename or reconfigure a CREATED, STOPPED, FAILED or CRASHED agent of the project. It fails with a state conflict while the agent has a live session (stop it first). A role is not changed here.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | yes | 1 to 128 characters. Id of the agent |
display_name |
string | no | New name; omit to keep it. Same rules as in create_agent |
config |
object | no | Replaces the runtime settings as a whole, as text values; omit to keep them. Same rules as in create_agent |
Returns { "agent": Agent }. Fails with agent.state_conflict while the agent has a live session (STARTING, READY, BUSY, PAUSED or STOPPING), with agent.not_found for an unknown agent or, from an orchestrator, one of another project, and with permission.denied for an agent session that is not an orchestrator.
start_agent#
Start a created or stopped agent. The agent is READY when the call returns; if the start fails, the call fails and the agent is FAILED, with the reason in last_failure.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | yes | 1 to 128 characters. Id of the agent |
Returns { "agent": Agent }, normally with state READY. A start waits up to 60 seconds for the runtime. Starting an agent that is already STARTING, READY or BUSY returns it unchanged, without waiting. Typical failures: agent.state_conflict (the agent is STOPPING), project.invalid_root (the project folder no longer exists), runtime.not_installed, resource.limit_reached (kind state_conflict; see resources.max_active_processes).
send_message#
Send a message to a READY agent; it starts a turn. The reply is not returned here: when the turn ends, poll_agent_events reports session.turn_completed with the result (or session.turn_failed), and the full output appears in the Cortex window. A BUSY agent (running a task) refuses it: cancel_task instead.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | yes | 1 to 128 characters. Id of the agent |
message |
string | yes | Text to send. Must not be blank and at most 100,000 characters (validation.invalid_input) |
Returns { "agent_id": string, "delivered": true }. delivered means the session accepted the message, not that the turn finished. Fails with agent.state_conflict when the agent is not READY, with runtime.unsupported_in_terminal_mode (kind unsupported) for a terminal-mode agent, and with runtime.turn_in_progress (kind state_conflict) when a turn is running and the runtime does not accept input during a turn (it lacks the interactive_input capability).
interrupt_agent#
Interrupt the current turn of a READY agent without ending its session; the turn then ends as session.turn_failed with cause interrupted. Does nothing when no turn runs. Fails with an unsupported error when the agent's runtime cannot interrupt (see the capabilities in list_runtimes). A BUSY agent (running a task) refuses it: cancel_task instead.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | yes | 1 to 128 characters. Id of the agent |
Returns { "agent": Agent, "interrupted": true }. interrupted is always true on success, also when no turn was running. Fails with runtime.capability_missing (kind unsupported) when the runtime lacks the interrupt capability, with runtime.unsupported_in_terminal_mode (kind unsupported) for a terminal-mode agent, and with agent.state_conflict when the agent is not READY.
stop_agent#
Stop an agent and end its session. Stopping a stopped agent is not an error. Stopping a BUSY agent cancels the task attempt it runs; the task waits for an agent again.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | yes | 1 to 128 characters. Id of the agent |
Returns { "agent": Agent }, normally with state STOPPED. An agent that is not running, or that another stop already owns (STOPPING), is returned as it is.
poll_agent_events#
Read the agent events (created, state changed, removed, and the end of each turn with its result) after a cursor. Pass the next_after of the previous call as after; omit it to read from the oldest event kept. dropped counts the events that were lost because the cursor was too old.
Annotations: read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
after |
integer | no | Minimum 0. The seq of the last event already seen. Omit it to read from the oldest event kept. A cursor from before the last start of Cortex is treated as omitted |
Returns
{ "events": [Event, ...], "dropped": 0, "next_after": 1790000000000123 }| Field | Meaning |
|---|---|
events |
Events with seq greater than after, oldest first |
dropped |
How many events the cursor missed because they left the feed. The feed keeps the last 256 events and counts from the start of the current run of Cortex |
next_after |
The seq of the last event returned, to pass as after next time. If no event was returned it is after again, or null when there was no valid cursor and no event |
The seq values only increase. They start from a number derived from the time Cortex started, not from 0, so never assume a value.
create_objective#
Create an objective: a goal split into 1 to 10 tasks that you list. Nothing is planned by an LLM. Each task runs as one turn on an existing READY structured agent of the project (assigned_agent_id pins one; otherwise any eligible READY agent takes it), and a task starts only after the tasks it depends on (depends_on names their keys inside this objective, no cycles) have COMPLETED; their results are passed on in its instruction. A task that cannot start yet waits: list_tasks says why. Returns the objective with the task_id of every task.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
project_id |
string | yes | 1 to 128 characters. Project the objective belongs to (see list_projects); project.not_found otherwise |
title |
string | yes | Name of the objective. Trimmed, 1 to 200 characters, at least one visible, no control characters |
tasks |
array of objects | yes | 1 to 10 items, in the order you want them listed. Each item has the properties below |
Properties of each item of tasks. The schema is closed, so other properties are refused.
| Property | Type | Required | Constraints |
|---|---|---|---|
key |
string | yes | Name of the task inside this objective; other tasks name it in depends_on. 1 to 64 visible characters, no control characters, unique inside the objective |
title |
string | yes | Short title of the task. Trimmed, 1 to 200 characters, no control characters |
description |
string | no | What the agent must do; it is the body of the instruction of the turn. Default empty; at most 20,000 characters, no control characters other than line breaks and tabs |
type |
string | no | Kind of work; informative. One of implementation (default), review, test, analysis, integration |
priority |
string | no | Order among tasks ready to run: higher first. One of low, normal (default), high |
depends_on |
array of strings | no | Keys of the tasks of this objective that must COMPLETE first. Default empty. An unknown key is refused with task.unknown_dependency and a cycle (a task depending on itself included) with task.dependency_cycle |
assigned_agent_id |
string | no | 1 to 128 characters. Run the task on this agent of the project (see list_agents); omit to let any eligible READY agent take it. The agent must exist (agent.not_found) and belong to the project, be structured and run on a runtime that reports the end of a turn (task.agent_not_eligible) |
requirements |
array of strings | no | Capabilities the runtime of the agent must declare (see list_runtimes). Default empty. Allowed values: interactive_input, interrupt, streaming, turn_completion, workspace_isolation, heartbeat. A capability the runtime does not declare counts as missing, never as supported: its agents are excluded (routing.capability_missing). heartbeat asks for a runtime whose liveness the Supervisor can watch (none of the shipped adapters declares it). With workspace_isolation every attempt runs in a Git worktree of its own, <data dir>/worktrees/<task_id> on the branch cortex/<task_id>, created from the project's HEAD (the project folder must be the top of a Git repository with a commit), and its result stays there until integrate_task; only a runtime that can run a turn in another folder takes it (today Codex) |
outputs |
array of strings | no | Files of the project this task produces, relative to its folder and / separated (no .., no drive, no \; at most 8, each at most 200 characters; repeated paths count once). Only these may reach the tasks that depend on it: when the task is approved, each one that exists is sealed with its size and SHA-256 in the attempt's handoff; a link is never followed and a file over 4 MiB is refused. A path that is not a plain relative one fails with validation.invalid_objective. Default empty |
paths |
array of strings | no | Files or folders of the project this task writes, with the syntax of outputs (at most 8); a folder covers what it holds, and paths compare without case. While an attempt of the task runs it holds a lease on them: a task of the project whose paths overlap waits with waiting.paths_leased until that attempt ends. Default empty: no lease |
timeout_secs |
integer | no | 10 to 86400. The longest one attempt may run, counted from its start (the worktree preparation included). Past it the Supervisor ends the attempt FAILED with the cause supervisor.deadline_exceeded, stops the agent that ran it (it becomes STOPPED; start it again) and the retry policy decides what follows, except that a task which declares paths and does not require workspace_isolation goes BLOCKED for a person, because what it wrote in the project folder belongs to no worktree. Default none: an attempt on a runtime that does not declare heartbeat is then not supervised, which is not a failure |
max_retries |
integer | no | 0 to 5. New attempts after a retryable failure. Default 1. A retry waits 5 s, doubling for each further retry up to 60 s, less a random jitter of up to half, so tasks that failed together do not retry together |
model |
string | no | The model its turns run on, e.g. sonnet, opus or haiku: it must be one a registered runtime that declares model_switching offers (else validation.invalid_objective), it beats Automatic Decisions and the Default Model, and only an agent of such a runtime runs the task (otherwise it waits with routing.capability_missing, missing: ["model_switching"]). Default none: Cortex chooses, see the attempt's model |
Returns { "objective": Objective }, with every task in CREATED or already READY (tasks without dependencies are made READY at once). Invalid objectives fail with validation.invalid_objective (kind invalid_argument).
plan_objective#
Ask Cortex's planner to split a goal into 1 to 10 tasks. The planner is one turn of Claude Code, the reference runtime, started in the project's root: it may read the project but not change it, and it gets no credential for these tools. Nothing is created. The draft is checked by every rule of create_objective; pass it to create_objective, unchanged or edited, to create the objective.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
project_id |
string | yes | 1 to 128 characters. Project the objective is for (see list_projects); project.not_found otherwise |
goal |
string | yes | What the objective must achieve. Trimmed, 1 to 20,000 characters, at least one visible, no control characters other than line breaks and tabs |
Returns { "draft": { "project_id", "title", "tasks": [{ "key", "title", "description", "type", "priority", "depends_on", "requirements" }] } }: the arguments of create_objective. The draft names no agent and no max_retries, so create_objective applies the retry behaviors. Errors of kind runtime_error: planner.timed_out after 5 minutes without an answer; planner.invalid_plan when the answer is not exactly one JSON object of that shape (text around it included); planner.failed when the turn failed; planner.unavailable when Claude Code is not registered; runtime.not_installed when it cannot start. A draft that breaks a rule of create_objective fails with that rule's error (validation.invalid_objective, task.unknown_dependency, task.dependency_cycle). The call spends one turn of Claude Code each time.
propose_objective#
Ask Cortex's planner to split a goal into 1 to 10 tasks and keep its answer as a pending proposal for a person to review. The planner is the one of plan_objective: one turn of Claude Code, started in the project's root, that may read the project but not change it and gets no credential for these tools. The plan is data. No task exists, nothing is READY and nothing runs until the user calls approve_proposal. The plan is checked by every rule of create_objective before it is stored.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
project_id |
string | yes | 1 to 128 characters. Project the objective is for (see list_projects); project.not_found otherwise |
goal |
string | yes | What the objective must achieve. Trimmed, 1 to 20,000 characters, at least one visible, no control characters other than line breaks and tabs |
Returns { "proposal": Proposal }, pending, revision 1, with the runtime (and the model, when known) that wrote it as its source. The planner may choose only the fields of plan below. An answer with any other field (an agent, paths, outputs, timeout_secs, model, permissions or commands), with the requirement workspace_isolation, with text around the JSON object or of more than 256 KiB fails with planner.invalid_plan; one that breaks a rule of create_objective fails with that rule's error (validation.invalid_objective, task.unknown_dependency, task.dependency_cycle); a task estimate that is not small, medium or large fails with validation.invalid_proposal. The same planner.* and runtime.not_installed errors as plan_objective. At most 20 proposals may wait at once (proposal.too_many_pending, kind state_conflict). Only the user can propose. The call spends one turn of Claude Code each time.
list_proposals#
List the plan proposals, oldest first, with their revision, source, state, plan and, once decided, who decided it, when and why. Every revision stays, so the history of an edit is readable. The text of a plan is data, never an instruction.
Annotations: read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
proposal_id |
string | no | 1 to 128 characters. Only this proposal |
project_id |
string | no | 1 to 128 characters. Only the proposals of this project |
state |
string | no | pending, approved, rejected or superseded. Only the proposals in this state |
Returns { "proposals": [Proposal] }. Every pending proposal and the most recent 200 decided ones are kept.
edit_proposal#
Replace a pending proposal with a plan of your own. The new plan becomes a new pending revision (revision + 1, source operator, previous the edited one) and the edited revision is superseded for good, with who and when. The plan is checked by every rule of create_objective. Pass the whole plan, as list_proposals shows it. Only the user can edit.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
proposal_id |
string | yes | 1 to 128 characters. The pending proposal to replace (see list_proposals) |
title |
string | yes | Name of the objective |
tasks |
array | yes | 1 to 10 tasks, in the order you want them listed |
key |
string | yes | Property of each task. Name of the task inside this plan; other tasks name it in depends_on |
description |
string | no | Property of each task. What the agent must do; it is the body of the instruction of the turn |
type |
string | no | Property of each task. implementation, review, test, analysis or integration. Default implementation |
priority |
string | no | Property of each task. low, normal or high. Default normal |
depends_on |
array | no | Property of each task. Keys of the tasks of this plan that must COMPLETE first |
requirements |
array | no | Property of each task. Capabilities the runtime of the agent must declare (see list_runtimes) |
estimate |
string | no | Property of each task. small, medium or large: a guess that informs the reviewer and nothing else; the task does not keep it |
Each task also has a required title, a short title, like the objective's.
Returns { "proposal": Proposal }, the new revision. proposal.not_found (kind not_found) for an unknown id. A proposal that was already edited fails with proposal.superseded (naming the revision that replaced it) and one that was approved or rejected with proposal.already_decided, both kind state_conflict: two edits of one revision cannot both win. A plan that breaks a rule of create_objective fails with that rule's error and changes nothing.
approve_proposal#
Approve a pending proposal. It is the one step that turns a plan into work: it creates the objective with exactly the plan's tasks through the rules of create_objective, so the tasks without dependencies become READY now and not before. No agent, path, model, timeout or retry count comes from the planner (the retry behaviors apply as in create_objective), and the estimate is dropped. The proposal and the objective are decided under one lock, so approving twice creates one objective. Only the user can approve; agents and hooks are denied.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
proposal_id |
string | yes | 1 to 128 characters. The pending proposal to approve (see list_proposals) |
Returns { "proposal": Proposal, "objective": Objective }, the proposal approved with its resolution.objective_id. A proposal that was edited fails with proposal.superseded; one already approved (the error names its objective) or rejected, with proposal.already_decided. If creating the objective fails, the proposal stays pending. If the objective was created and the approval could not be written, the error's remediation names the objective: reject the proposal instead of approving it again.
reject_proposal#
Reject a pending proposal for good. No objective is created. Only the user can reject.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
proposal_id |
string | yes | 1 to 128 characters. The pending proposal to reject (see list_proposals) |
reason |
string | no | Why it was rejected; trimmed, at most 500 characters, no control characters. Kept with the decision |
Returns { "proposal": Proposal }, rejected, with resolution.by, resolution.at and resolution.reason. The same errors as approve_proposal for a proposal that was already decided.
list_tasks#
List every objective with its tasks: the status of each task, why a task that has not started waits (waiting), its attempts and, once COMPLETED, its result. It is the same state the Cortex window shows.
Annotations: read-only, not destructive.
Parameters: none.
Returns { "objectives": [Objective] }, oldest first. This includes the objectives created from the window's Orchestrator panel.
get_task_status#
Get one task: its status, its attempts (the agent that ran each and how it ended), why it waits, the cause of a failure or cancellation and, once COMPLETED, its result.
Annotations: read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
task_id |
string | yes | 1 to 128 characters. Id of the task (see list_tasks) |
Returns { "task": Task }. Fails with task.not_found (kind not_found) for an unknown id.
cancel_task#
Cancel a task that has not ended, and every task that depends on it. A running attempt ends CANCELLED and its agent is released (the turn is interrupted); if the turn cannot be interrupted, the agent is stopped instead. Cancelling a CANCELLED task is not an error; a COMPLETED or FAILED one is a state conflict.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
task_id |
string | yes | 1 to 128 characters. Id of the task (see list_tasks) |
Returns { "task": Task }, the cancelled task. Fails with task.state_conflict (kind state_conflict) for a COMPLETED or FAILED task and with task.not_found for an unknown id.
cancel_objective#
Cancel an objective: every task of it that has not ended ends CANCELLED, with its running attempt, and the agents are released (the turn is interrupted; an agent that has not let go of it within 30 s is stopped). It also ends a pause and raises flow.generation. Cancelling an objective with nothing left to cancel changes nothing. Agents cannot cancel.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
objective_id |
string | yes | 1 to 128 characters. Id of the objective (see list_tasks) |
Returns { "objective": Objective }. Fails with objective.not_found (kind not_found) for an unknown id and with permission.denied for an agent.
pause_objective#
Pause an objective: none of its tasks starts until resume_objective, and each keeps its own state (a READY task stays READY and waits with waiting.objective_paused; a BLOCKED one can still be approved, retried or cancelled). With policy drain (the default) the attempts that run end by themselves; with interrupt their turns are interrupted, the attempts end CANCELLED (cause objective.paused) without spending a retry, and the tasks run again after the resume as the next attempt. A verification that already runs is not a turn and ends by itself. Pausing a paused objective changes nothing, except interrupt over a drain pause. Every decision raises flow.generation. Agents cannot pause.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
objective_id |
string | yes | 1 to 128 characters. Id of the objective (see list_tasks) |
policy |
string | no | drain (default) or interrupt. What becomes of the attempts that run |
note |
string | no | Up to 1,000 characters, no control characters. Why it was decided; kept with the decision |
Returns { "objective": Objective }, with status PAUSED and flow.state PAUSED. Fails with objective.state_conflict (kind state_conflict) when every task has ended, with objective.not_found for an unknown id and with permission.denied for an agent.
resume_objective#
Resume a paused objective: its graph is read afresh, and each task is eligible again or says why it waits (waiting). What a pause interrupted runs as the next attempt; a backoff that ran goes on from where it was. Resuming an objective that runs changes nothing. Agents cannot resume.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
objective_id |
string | yes | 1 to 128 characters. Id of the objective (see list_tasks) |
note |
string | no | Up to 1,000 characters, no control characters. Why it was decided; kept with the decision |
Returns { "objective": Objective }. Fails with objective.not_found for an unknown id and with permission.denied for an agent.
reassign_task#
Move a task that waits (CREATED, READY or BLOCKED, with no attempt running) to another agent of its project, or to none (omit to_agent_id: any eligible agent may run it). The move is recorded in reassignments with who decided, from whom, to whom and why; the status does not change. Refused while the agent of its last attempt still ends it (at most 30 s after a cancellation) or its verification still runs. Pass execution_id, the attempt you read, to move the task only if that is still its last attempt; asking again for the same move is not an error. Agents cannot reassign.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
task_id |
string | yes | 1 to 128 characters. Id of the task (see list_tasks) |
to_agent_id |
string | no | 1 to 128 characters. Agent of the task's project that runs it from now on (see list_agents); omit for any eligible agent |
execution_id |
string | no | 1 to 128 characters. The execution_id of the attempt you read; a newer attempt is a state conflict |
note |
string | no | Up to 1,000 characters, no control characters. Why it was decided; kept with the decision |
Returns { "task": Task }. Fails with task.state_conflict for a task that does not wait, that has a newer attempt than execution_id, or whose last attempt is still ending; with agent.not_found for an unknown agent and task.agent_not_eligible when the agent cannot run tasks of the project; and with permission.denied for an agent.
approve_task#
Approve a BLOCKED task whose result has no evidence: its project has no verification, or the verification did not pass or could not run. It becomes COMPLETED with the result of its last attempt, and the decision is recorded with who made it (decisions). Agents cannot approve. A task blocked for another cause can only be retried or cancelled. Pass execution_id, the attempt you read, to approve that result and nothing newer: a task that has a newer attempt is refused. Asking again for an approval already recorded for that attempt returns the task as it is, with no second decision.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
task_id |
string | yes | 1 to 128 characters. Id of the task (see list_tasks) |
execution_id |
string | no | 1 to 128 characters. The execution_id of the attempt whose block you decide. Without it, the decision applies to the last attempt and a repeat is a state conflict |
note |
string | no | Up to 1,000 characters, no control characters. Why it was decided; kept with the decision |
Returns { "task": Task }, the completed task. Fails with task.state_conflict (kind state_conflict) for a task that is not BLOCKED by its verification, and with permission.denied for an agent.
retry_task#
Ask for a new attempt of a BLOCKED task: it becomes READY at once and the decision is recorded. After a retry a person asked for, a failure blocks the task again instead of failing it. execution_id works as for approve_task: a retry already recorded for that attempt is not made twice.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
task_id |
string | yes | 1 to 128 characters. Id of the task (see list_tasks) |
execution_id |
string | no | 1 to 128 characters. The execution_id of the attempt whose block you decide. Without it, the decision applies to the last attempt and a repeat is a state conflict |
note |
string | no | Up to 1,000 characters, no control characters. Why it was decided; kept with the decision |
Returns { "task": Task }, the task, READY. Fails with task.state_conflict for a task that is not BLOCKED, and with permission.denied for an agent.
integrate_task#
Bring the result of a COMPLETED task that ran in a Git worktree of its own (requirement workspace_isolation) into its project folder. The changes of the worktree of the attempt that completed it are committed on its branch cortex/<task_id>, then squashed onto the project folder: they are staged there, never committed, for you to review and commit. Nothing is merged into your branch, pushed or deleted. The worktree is removed afterwards; the branch stays. Agents cannot integrate. A task is integrated once.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
task_id |
string | yes | 1 to 128 characters. Id of the task (see list_tasks) |
note |
string | no | Up to 1,000 characters, no control characters. Kept with the integration |
Returns { "task": Task }, with integration set. Fails, with nothing changed, with task.state_conflict for a task that is not COMPLETED, ran in the project folder or was integrated already; workspace.index_not_clean when the project folder has staged changes; workspace.conflict when the changes conflict with its HEAD (the paths are named; merge the branch yourself); workspace.local_changes when they would overwrite a change you have not committed; workspace.unowned when the worktree is not proven to be the task's; permission.denied for an agent.
list_skills#
List the skills Cortex found: the global ones (the skills folder of the data directory) and each project's .cortex/skills. A skill is a folder with a skill.json manifest and its Markdown content. The result gives each skill's scope, version and state (enabled, disabled, or changed since it was enabled), the folders that are not valid skills and why, and the name conflicts between skills. With agent_id, it also gives that agent's effective set: every skill that applies to it and whether it is delivered or why not. Read-only: skills are enabled and disabled in the Cortex window, never through MCP.
Annotations: read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
agent_id |
string | no | 1 to 128 characters. Id of an agent whose effective set to include (see list_agents) |
Returns { "skills": [...], "invalid": [...], "conflicts": [...], "effective"?: {...} }:
skills[]:skill_id(global/<folder>orproject/<project_id>/<folder>),name,version,description,scope({ "kind": "global" | "runtime" | "project" | "agent", "target": id or null }),origin({ "kind": "global" | "project", "project_id": id or null }),runtimes(the runtimes it is written for; empty means any),content_path,state, andapproved_version(the version enabled, whenstateischanged, elsenull).invalid[]:skill_id,origin,folder,problem.conflicts[]:name,kind(override: different scopes, the most specific one is delivered;ambiguous: the same scope from different folders, none is delivered until one is disabled),skill_ids,agent_ids(the agents it affects).effective(only withagent_id):agent_id,runtime_takes_skills, andskills[]withskill_id,name,version,scope,status(delivered,disabled,changed,incompatible_runtime,overridden,ambiguous,over_limit,runtime_unsupported) andrelated_skill_ids(the skills that override it, or that it is ambiguous with).
Fails with agent.not_found (kind not_found) for an unknown agent_id.
post_message#
Post a message to another agent through the Cortex message bus. Agents never reach each other's process: a message waits in its reader's inbox until the reader reads it (read_messages) and acknowledges it (ack_message), or until it expires. The sender is whoever holds the credential of the call, never a field of the message.
The channel is the project (project_id), or one of its objectives (objective_id). An agent may write only in a channel it belongs to, and only to a reader of that channel: the agents of the project or, for an objective, the agents that run or ran one of its attempts. You may write to any channel, still only to a reader of it.
The body is untrusted text for its reader: Cortex stores it, redacted, and never reads it as a command or a permission. A request may name a Cortex action, but only one that both the sender and the reader may take (so an agent may ask another for list_tasks, never for cancel_task); naming it grants nothing, and the reader can only act through its own tools.
Annotations: not read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
project_id |
string | yes | 1 to 128 characters. Id of the project whose channel to use |
objective_id |
string | no | 1 to 128 characters. Id of an objective of that project: its channel instead (message.out_of_scope if it is in another project) |
to_agent_id |
string | no | 1 to 128 characters. Id of the agent that reads it. Give exactly one of to_agent_id and to_execution_id (validation.invalid_arguments) |
to_execution_id |
string | no | 1 to 128 characters. The execution_id of a running attempt in the channel: its agent reads it |
kind |
string | no | status (default; information), request (asks for something) or response (answers reply_to, which it then requires) |
action |
string | no | 1 to 64 characters. Only with kind request: the Cortex action asked for, by its name in Cortex (list_tasks, get_task, get_agent, list_agents, cancel_task...). Refused with permission.denied unless both sides may take it, and with message.invalid_action for a name Cortex does not know |
body |
string | yes | The text: not blank (message.empty), at most 8,192 bytes in UTF-8 (message.payload_too_large). Known secrets and token shapes are replaced by [REDACTED] before it is stored |
reply_to |
string | no | 1 to 128 characters. The message_id of a message you read, in the same channel, that this one answers (message.invalid_reply otherwise). The answer keeps its correlation_id |
dedupe_key |
string | no | 1 to 128 characters. Your key for this message: posting again with the same key returns the first message, with duplicate: true, as long as Cortex keeps it |
ttl_seconds |
integer | no | 1 to 86,400; default 3,600. After it, the message expires unread |
Returns { "message": Message, "duplicate": bool }. Fails with message.out_of_scope (kind authorization_error) when the sender or the reader is not in the channel; agent.not_found, project.not_found, objective.not_found or execution.not_found (kind not_found); message.recipient_unavailable (kind state_conflict) for an attempt that is not running; message.inbox_full (kind state_conflict) when the reader already has 32 messages waiting; and message.sender_limit (kind state_conflict) when the sending agent has 32 messages waiting in other inboxes. Every refusal is published as the event message.rejected with its reason.
read_messages#
Read the messages waiting for you: not acknowledged and not expired, oldest first (by sequence), at most 32 per call. An agent reads only its own; your user credential reads every agent's. Reading changes nothing: a message stays until it is acknowledged or expires.
Agents do not need to poll: when Cortex starts a turn for an agent (a task, or your input) and messages wait for it, the turn ends with a line such as [Cortex] 2 messages from other agents wait in your inbox... that names these tools. The line gives the count only, never a message's text. An idle agent sees it with its next turn.
Annotations: read-only, not destructive.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
project_id |
string | no | 1 to 128 characters. Only the messages of this project's own channel (not those of its objectives). An agent must belong to it (message.out_of_scope) |
objective_id |
string | no | 1 to 128 characters. Only the messages of this objective's channel. An agent must be one of its participants |
after |
integer | no | Minimum 0. The sequence of the last message already read |
Returns { "messages": [Message, ...], "more": bool }. more is true when more messages wait after the last one returned: pass its sequence as after.
ack_message#
Acknowledge a message you read: it leaves your inbox and frees its place. Acknowledging it again returns it unchanged. Only its reader acknowledges a message (you may acknowledge any); another agent's message is message.not_found.
Annotations: not read-only, not destructive, idempotent.
| Parameter | Type | Required | Constraints |
|---|---|---|---|
message_id |
string | yes | 1 to 128 characters. The message_id of a message you read |
Returns { "message": Message }, now ACKED. Fails with message.not_found (kind not_found) and with message.expired (kind state_conflict) for a message whose TTL passed first.
Orchestrator role#
An agent whose role is orchestrator coordinates other agents and has no tool to read, change or run anything in the project (ADR 0035). Its session reaches Cortex only through these tools, with the credential in CORTEX_MCP_TOKEN:
| Allowed | Notes |
|---|---|
list_projects, list_runtimes, list_agents, get_agent_status, poll_agent_events, list_tasks, get_task_status, list_skills, list_proposals, read_messages |
Reads, limited to its own project like any agent session |
create_agent |
Workers only (role orchestrator is permission.denied), and only in its own project (another project is project.not_found) |
update_agent |
Rename or reconfigure a stopped worker of its own project; it cannot change a role |
start_agent |
Start an agent of its own project |
post_message, ack_message |
The message bus, as for any agent |
Everything else fails with permission.denied (kind authorization_error): stopping, interrupting or messaging an agent directly, creating objectives or tasks, deciding anything a person decides. The Core also answers every tool request the CLI of an orchestrator session makes: a Cortex tool is allowed and anything else (shell, file, web) is denied at once and listed in Approvals, decided by role, whatever Automatic Decisions say. The runtime must declare orchestrator_role (list_runtimes shows it) and the session is always structured; the start is refused with runtime.capability_missing, agent.orchestrator_needs_structured or mcp.disabled rather than run as a worker. An orchestrator is never given a task.
Objects returned by the tools#
Keys marked optional are omitted when they do not apply. Keys of the Task and Attempt objects are always present; an absent value is null.
Project
| Field | Type | Meaning |
|---|---|---|
project_id |
string | Id |
name |
string | Display name |
root_path |
string | Folder the agents of the project work in |
created_at |
string | Creation time |
Agent
| Field | Type | Meaning |
|---|---|---|
agent_id |
string | Id |
display_name |
string | Name shown for the agent |
project_id |
string | Project it works in |
runtime_id |
string | Runtime it runs on |
interaction |
string | structured or terminal. Agents created in the window can be terminal; send_message and tasks need structured |
role |
string | worker or orchestrator (see Orchestrator role) |
state |
string | CREATED, STARTING, READY, BUSY, PAUSED, STOPPING, STOPPED, FAILED or CRASHED. BUSY means it is running a task; PAUSED means a person holds an idle agent, which keeps its session but is handed nothing |
config |
object | The settings given at creation, text values (for example model) |
created_at, updated_at |
string | Times |
last_failure |
object | Optional. { "code": string, "message": string }, present when the agent has a recorded failure |
current_execution_id |
string | Optional. Present while the agent hosts a task attempt: the execution_id of that attempt |
Runtime
| Field | Type | Meaning |
|---|---|---|
runtime_id |
string | Id to pass to create_agent |
display_name |
string | Name for people |
adapter_version |
string | Version of the adapter |
cli_version |
string | Optional. Version of the CLI, once known |
process_strategy |
string | persistent (one long-lived process per session) or per_turn (one short process per turn) |
capabilities |
array of strings | What the runtime declares, from interactive_terminal, interactive_input, interrupt, streaming, turn_completion, skills, context_handoff (its turns may carry the handoffs of the tasks a task depends on; a task with dependencies runs only on a runtime that declares it), workspace_isolation, heartbeat (it reports a beat at least every 30 s while a turn runs; without one for 90 s the Supervisor ends the attempt CRASHED with supervisor.heartbeat_lost. A runtime that does not declare it is never ended for silence; no adapter declares it yet) |
capability_state |
string | Always declared: capabilities are what the adapter says, not tested |
model |
object | { "selectable": bool, "hint": string }. selectable says whether the model entry of config chooses the model; hint (optional) says what a model value looks like |
health |
object | { "state": "unknown" | "available" | "unavailable", "checked_at"?, "detail"?, "code"?, "message"?, "remediation"? }. checked_at is absent while unknown; detail says what an available runtime's check found; code, message and remediation say why an unavailable runtime cannot start |
Objective
| Field | Type | Meaning |
|---|---|---|
objective_id |
string | Id |
project_id |
string | Project |
title |
string | Title |
status |
string | PAUSED (a person paused it and some task has not ended), RUNNING (some task has not ended), BLOCKED (a task is BLOCKED and none runs or is ready), COMPLETED (every task completed), FAILED (every task ended and at least one failed) or CANCELLED (every task ended, none failed, at least one cancelled) |
flow |
object | { "state": "RUNNING" | "PAUSED", "generation": integer, "policy": "drain" | "interrupt" or null, "reason": string or null, "decided_by": { "origin", "caller" } or null, "changed_at": string or null }. generation counts the pauses, resumes and cancellations of the objective and only grows: the events of these decisions (objective.paused, objective.resumed, objective.cancelled) carry it, and an event whose number is not greater than the one in the snapshot you hold is old news. policy is set while PAUSED |
created_at |
string | Creation time |
tasks |
array | The Tasks, in the order given at creation |
Task
| Field | Type | Meaning |
|---|---|---|
task_id |
string | Id |
objective_id |
string | Objective it belongs to |
title, description |
string | As given |
type |
string | implementation, review, test, analysis or integration |
status |
string | CREATED, READY, RUNNING, BLOCKED, COMPLETED, FAILED or CANCELLED. A turn that ends well is not a completion: the project's verification must pass, or a person approves; until then the task is RUNNING (verifying) or BLOCKED |
priority |
string | low, normal or high |
dependencies |
array of strings | task_ids of the tasks it waits for (the depends_on keys, resolved to ids) |
requirements |
array of strings | Capabilities required, sorted without duplicates |
outputs |
array of strings | The project-relative files it declared it produces, in order, without duplicates |
paths |
array of strings | The project-relative files and folders it declared it writes, leased while an attempt runs |
timeout_secs |
integer or null | The longest one attempt may run (see create_objective); null declares none |
model |
string or null | The model the task named, if any |
assigned_agent_id |
string or null | The pinned agent, if any |
max_retries |
integer | New attempts allowed after a retryable failure |
attempts |
array | The Attempts, oldest first |
result |
object or null | Set once COMPLETED: { "execution_id": string, "text": string or null, "truncated": bool, "evidence": object or null }. text is the final message of the turn, cut to 4,096 characters (truncated then), and null if the runtime ended the turn without one. evidence is { "kind": "verification", "execution_id", "fingerprint", "exit_code" } or { "kind": "human_approval", "approver": { "origin", "caller" }, "decided_at" }; null only for a task completed before verification existed |
decisions |
array | What people decided about the task while BLOCKED: decision (approve, retry, cancel), task_id, objective_id, execution_id (the attempt whose block it resolved; null in a record made before it was kept), blocked_cause, approver (origin: desktop or mcp; caller: user or client:<id>), decided_at, note |
reassignments |
array | The moves of the task to another agent, oldest first: from, to (agent ids; null is any eligible agent), execution_id (its last attempt when it moved; null if it never ran), reason, decided_by (origin, caller), decided_at |
integration |
object or null | Set once integrate_task brought its worktree into the project folder: execution_id, commit (on cortex/<task_id>), files (how many the changes touch), approver, integrated_at, note. null until then |
waiting |
object or null | { "code": string, "message": string, "excluded": array }: why a CREATED or READY task does not run; null otherwise and for a task about to be dispatched. excluded lists, when the router was asked, every agent it ruled out: { "agent_id", "runtime_id", "code", "message", "rule", "missing" }, where rule is the deny rule (else null) and missing the capabilities its runtime does not declare (else []); empty for a wait that is not the router's. Exclusion codes: routing.not_assigned, routing.other_project, routing.runtime_missing, routing.runtime_unavailable (its last health check failed; for a per-turn runtime also when the check failed after the agent started), routing.capability_missing, routing.denied_by_rule, routing.agent_unavailable, routing.taken_this_round, routing.runtime_at_limit. Codes: waiting.dependencies, waiting.dependency_blocked (a task it depends on is BLOCKED), waiting.retry_backoff, waiting.parallel_limit, waiting.project_limit (its project runs orchestrator.max_parallel_tasks_per_project tasks), waiting.runtime_limit (every agent that could run it is on a runtime that runs orchestrator.max_parallel_tasks_per_runtime tasks), waiting.assigned_agent_missing, waiting.assigned_agent_ineligible, waiting.agent_unavailable, waiting.no_eligible_agent, waiting.no_available_agent, waiting.not_routable (the router excludes every agent for a reason the user can change: a deny rule, an unavailable runtime, manual routing without an assigned agent), waiting.paths_leased (a running task writes paths its paths overlap), and waiting.blocked for a BLOCKED task |
cause |
object or null | { "code": string, "message": string }: why the task is FAILED, CANCELLED or BLOCKED (verification.not_configured, verification.failed, verification.timed_out, verification.not_run, a workspace.* cause when its worktree could not be prepared, a supervisor.deadline_exceeded or supervisor.heartbeat_lost cause when the Supervisor ended its attempt, or the cause of an attempt a person asked for), or why it went back to READY |
not_before |
string or null | While READY after a failure: no attempt starts before this time |
created_at |
string | Creation time |
started_at, finished_at |
string or null | First start and end |
Attempt (one try of a task)
| Field | Type | Meaning |
|---|---|---|
execution_id |
string | Id of this attempt |
attempt_number |
integer | Order of the attempt, starting at 1 |
agent_id, runtime_id, session_id |
string | Where it ran |
process_refs |
array of strings | Ids of the processes it ran in |
started_at |
string | Start time |
finished_at |
string or null | End time; null while running |
outcome |
string or null | COMPLETED, FAILED, CRASHED or CANCELLED; null while running |
cause |
object or null | { "code": string, "message": string } when it did not complete |
failure_class |
string or null | For FAILED and CRASHED: retryable, fatal or user_action_required. Decides whether a retry follows |
handoff |
object or null | What the attempt handed over to the tasks that depend on its task (ADR 0026), published in the write that approves the task: { "version": 1, "source": { "execution_id", "task_id", "objective_id", "project_id", "attempt_number" }, "published_at", "outcome": { "status": "completed", "summary", "result", "truncated", "artifacts": [{ "path", "bytes", "sha256" }], "refused": [{ "path", "reason" }], "evidence" } }. summary is the first line of the result (at most 240 characters), result at most 4 KiB. reason is invalid, outside_project, missing, not_a_file, too_large or unreadable. null for an attempt that failed, crashed or was cancelled (its outcome, cause and failure_class say why: nothing of it reaches a dependent) and until a task blocked for approval is approved |
routing |
object or null | How the router chose its agent: decision_id, task_id, decided_at, correlation_id, mode (manual, policy or auto), considered and fallbacks ({ "agent_id", "runtime_id" }), excluded (as in a task's waiting), selected, selected_by, rule_applied (the prefer rule, when one decided) and reasons (for people). selected_by is why the selected agent went before the next eligible one, the first criterion that told them apart: routing.assigned, routing.only_eligible, routing.untried_after_failure, routing.fallback_after_failure, routing.preferred_by_rule, routing.preferred_runtime, routing.least_loaded (fewest working agents on its runtime, counting the ones taken in the same round), routing.least_recently_used (its last attempt started longest ago, or it never ran one) or routing.creation_order; absent in a decision recorded before it existed. The agent is checked again right before the attempt starts: one that no longer passes is not used, and the next round decides again (log orchestrator.dispatch_skipped, code routing.candidate_stale). null for an attempt started before the router |
workspace |
object or null | The Git worktree it ran in, for a task that requires workspace_isolation: { "path", "branch", "base" } (base is the commit it was created from). null when it ran in the project folder |
model |
object or null | The model its turn ran on (S13.1): model, source (task: the task named it; default: Automatic Decisions was off; router: the local router chose it; fallback: the router was asked and could not decide), fallback (router.not_configured, router.unavailable, router.timeout, router.invalid_answer or router.unknown_model), router_answer, router_latency_ms, reused_from (the earlier attempt whose automatic decision a retry reuses: one router call per task), resolved (the id the runtime switched to, e.g. claude-opus-5-5) and decided_at. null on a runtime that does not switch models |
verification |
object or null | The verification of its turn's result: status (PENDING, RUNNING, PASSED, FAILED, TIMED_OUT, NOT_RUN, CANCELLED, ABORTED, INTERRUPTED, NOT_CONFIGURED), spec (command, expected_exit_code, timeout_secs, env_allow, fingerprint), candidate_result, started_at, finished_at, duration_ms, exit_code, process_id, stdout and stderr ({ "text", "truncated", "total_bytes" }, the last 4 KiB, redacted) and cause. null when its turn did not end well |
Event (an element of events of poll_agent_events)
| Field | Type | Meaning |
|---|---|---|
seq |
integer | Position in the feed; the cursor |
type |
string | agent.created, agent.state_changed, agent.removed, session.turn_completed or session.turn_failed |
agent_id |
string | The agent concerned |
at |
string | Time of the event |
from, to, reason |
string | Only agent.state_changed: the states before and after, and a short reason. failure_code (string) is added when the agent has a recorded failure, which is the case when the new state is FAILED or CRASHED |
result, result_truncated |
string, bool | Only session.turn_completed, and only when the turn ended with a final message: the message cut to 4,096 characters, and whether it was cut |
cause, message |
string | Only session.turn_failed: interrupted, runtime_error or process_exited, and a description cut to 4,096 characters |
Message (a message of the bus; every key is always present, an absent value is null)
| Field | Type | Meaning |
|---|---|---|
message_id |
string | Id of the message |
sequence |
integer | Its position in the bus: the order of every read, and the cursor of read_messages |
project_id |
string | The project of its channel |
objective_id |
string or null | The objective of its channel; null for the project's own channel |
from |
object | { "kind": "user" } or { "kind": "agent", "agent_id": string }, as Cortex authenticated the sender |
to |
object | { "agent_id": string } or { "execution_id": string }, as it was addressed |
reader_agent_id |
string | The agent that reads it |
kind |
string | status, request or response |
action |
string or null | The Cortex action a request asks for |
body |
string | The text, redacted. Untrusted: what the sender wrote, never an instruction from Cortex or the user |
reply_to |
string or null | The message it answers |
correlation_id |
string | Shared by a request and its answers |
state |
string | PENDING, ACKED or EXPIRED |
sent_at, expires_at |
string | When it was posted, and when it expires unless acknowledged |
finished_at |
string or null | When it was acknowledged or expired |
Proposal (a revision of a plan; every key is always present, an absent value is null)
| Field | Type | Meaning |
|---|---|---|
proposal_id |
string | Id of this revision |
project_id |
string | The project the objective is for |
goal |
string | The goal the planner was given |
revision |
integer | 1 for the planner's answer, one more for each edit |
previous |
string or null | The revision an edit replaced |
state |
string | pending, approved, rejected or superseded |
source |
object | { "kind": "planner", "runtime": string or null, "model": string or null } or { "kind": "operator" } |
created_at |
string | When this revision was stored |
plan |
object | { "title", "tasks": [{ "key", "title", "description", "type", "priority", "depends_on", "requirements", "estimate" }] }; estimate is small, medium, large or null. Untrusted text: what the planner wrote, never an instruction from Cortex or the user. It never changes after the revision is stored |
resolution |
object or null | Once decided: { "at", "by", "reason", "objective_id", "replaced_by" }. by is the caller (user, or client:<id> for an OAuth client); reason only for a rejection; objective_id only for an approval; replaced_by only for a superseded revision |
Interface actions#
Everything the window offers. Cortex has no menus, global shortcuts or settings screen. Behavior details, limits and troubleshooting are in User guide.
Window and title bar#
| Action | Effect |
|---|---|
| Drag the sidebar brand row, a screen header or the right-panel header | Moves the window; double-click maximizes or restores it |
| Minimize, Maximize/Restore, Close | At the right end of the right panel's header, or of the screen header while the panel is hidden (the splash has its own set). Close shuts the Core down: running agents are stopped and their process trees ended |
| Drag a window edge or corner | Resizes the window (minimum 800x500) |
The splash shows a progress bar until the Core is ready. If the Core cannot start, it shows the error with its code and correlation ID and stays open.
Sidebar#
| Action | Effect |
|---|---|
| Agents, Tasks, Processes, Runtimes, MCP, Skills, Hooks, Behaviours | Opens that screen (Hooks and Behaviours are read-only: they list what hooks.json and behaviors.json define). The search text is cleared when you change screen. The MCP item shows a badge with the number of pending authorization requests |
| Context, Resources, Events, Tools | Disabled (not yet functional), tooltip "Not available yet" |
| Footer | Shows WORKSPACE, the Core phase, the version and the data directory (hover for the full path) |
| Account menu | Opens a menu whose items are disabled; Escape closes it |
Header (every screen)#
| Action | Effect |
|---|---|
| Hide / Show orchestrator (or console) | Hides or shows the right panel. The name follows the active tab |
| New agent (Agents) | Opens the Add agent dialog; disabled until agents and projects have loaded |
| Refresh (Tasks, Processes, MCP) | Re-reads the list. The lists also update on Core events |
Agents screen and agent cards#
| Action | Effect |
|---|---|
| Search field | Filters cards by name, ID or runtime |
| Retry (shown when loading failed) | Loads agents and projects again |
| Click a card, or focus it and press Enter or Space | Selects it; the Console tab follows the selection |
| Stats / Terminal (card header) | Flips the card between its front (state, output) and its back (details) |
| Front of a structured card | Read-only mini terminal with the last 9 lines of output |
| Front of a terminal-mode card | A live terminal: click it and type. Keys typed there go to the CLI and trigger no card or app shortcut |
| Start | Starts the agent. Enabled in CREATED, STOPPED, FAILED, CRASHED |
| Stop | Stops it. Enabled in STARTING, READY, BUSY, PAUSED; a BUSY agent's task attempt is cancelled and the task waits for an agent again |
| Restart | Stops if needed and starts again with a new session. Enabled in READY, BUSY, PAUSED, STOPPED, FAILED, CRASHED |
| Edit | Opens the Edit agent dialog. Enabled in CREATED, STOPPED, FAILED, CRASHED |
| Pause | Holds an idle agent: READY to PAUSED, keeping its process and session, and handing it nothing until Resume. Enabled in READY for a structured agent with no reply running; otherwise disabled with the reason as its tooltip |
Resume (the Pause button of a PAUSED agent) |
PAUSED to READY |
| Remove | Removes the agent after a second click on "Confirm remove". Enabled in CREATED, STOPPED, FAILED, CRASHED |
| Right click on a card, or the context-menu key or Shift+F10 on a focused card | Opens the agent's context menu (below) at the pointer, and selects the card |
Start, Stop, Restart and Edit are on the back of the card and in the Console header (Start, Stop and Restart only). While a command of an agent is in flight, Start and Restart are disabled; Stop stays available (except while a stop is itself in flight), so a start that hangs can be cancelled.
Agent context menu#
Cortex's own menu, in document.body, kept inside the window. It lists Pause (Resume for a PAUSED agent), Stop, Start, Edit, Restart and Remove, in that order, with the same enabling rules as the card buttons; a disabled row gives its reason as its tooltip and as its accessible description. While a command of the agent is in flight, every row but Stop is disabled.
| Input | Effect |
|---|---|
| Choose a row (click, Enter, Space) | Closes the menu and runs the action. Pause, Resume, Stop, Start and Restart go to the Core and report their outcome at the bottom of the window; Edit opens the Edit agent dialog; Remove opens a "Remove agent?" dialog (Cancel has the focus), and removal runs only on "Remove agent" |
| Arrow keys, Home, End | Move between the enabled rows |
| Escape | Closes the menu and returns the focus to the card |
| Click or right click elsewhere, wheel, window resize, window loses focus, Tab | Closes the menu |
The result messages are Paused, Resumed, Stopped, Started, Restarted and Removed followed by the name, a note (Stopping alpha…) while a Stop, Start or Restart runs, and Could not <action> alpha with the Core's message and remediation when it refuses. Successes disappear after 4 s and errors after 12 s; at most 4 messages show.
Add agent dialog#
The dialog is titled "Add agent". It opens from the New agent button of the Agents header, and from the one shown when there are no agents.
| Field or action | Effect |
|---|---|
| Name | Required |
| Project | Pick an existing project or "New project…" (shown only if a project exists) |
| Project name, Folder path | Required for a new project. The folder is typed, not picked, and must be an absolute path to an existing directory |
| Runtime | Read-only: Claude Code |
| Set orchestrator role | Checkbox, off by default. Available for a runtime that declares orchestrator_role (Claude Code); it forces Structured and disables the Interaction field. Only sent when it is checked |
| Interaction | Structured (console and Send) or Terminal (the CLI's own interface in the card) |
| Model | Optional; passed to Claude Code as --model |
| Create agent | Creates the project if new, then the agent in CREATED, and selects it. A failure is shown in the dialog |
| Cancel, Escape | Closes the dialog (not while it is creating) |
Edit agent dialog#
| Field or action | Effect |
|---|---|
| Name | Read-only |
| Interaction | Structured or Terminal; Terminal is disabled for a runtime without an interactive terminal; Claude Code, Codex and OpenCode all have one |
| Set orchestrator role | Sets or clears the role; it forces Structured while set. Only sent when it changed |
| Save | Applies the new interaction mode, model and role; enabled only when one changed. The Core refuses it while the agent has a live session and the dialog shows its error |
| Cancel, Escape | Closes the dialog |
Right panel#
| Action | Effect |
|---|---|
| Orchestrator / Console tabs | Switch tabs. Arrow Left and Right, Home and End move between tabs when a tab has focus. The text typed in each tab is kept when you switch |
Console tab (selected agent):
| Action | Effect |
|---|---|
| Start, Stop, Restart | As on the card |
| Output | The agent's stdout and stderr (structured agents). "Select an agent." with none selected; a note instead for a terminal-mode agent |
| Instruction field and Send | Sends the text to the agent; disabled unless the agent is READY and structured, or when the text is blank. While the agent replies ("Replying…" in the header) Send waits and the text stays. Enter sends, Shift+Enter inserts a newline. The Core accepts 1 to 100,000 characters |
| Dismiss (on an error notice) | Hides the command error |
| Details (on an error notice) | Shows the error code, the correlation id and the causes, for a report |
Orchestrator tab:
| Action | Effect |
|---|---|
| Instruction field and Send | Creates one objective with one task for the project of the selected agent (or the only project). Enter sends, Shift+Enter inserts a newline. The title is the first non-blank line, cut to 200 characters; the description is the full text, at most 20,000 characters. Disabled when no project can be chosen |
Cancel (on a RUNNING or PAUSED objective) |
Cancels every task of the objective that has not ended |
Pause (on a RUNNING objective) |
pause_objective with drain: nothing new starts, the running attempts end by themselves. The tasks keep their state and say they wait for the resume |
Interrupt (on a RUNNING objective, or a PAUSED one that drains) |
pause_objective with interrupt: nothing new starts and the running turns are interrupted (their attempts end CANCELLED, no retry is spent) |
Resume (on a PAUSED objective) |
resume_objective: the tasks are eligible again or say why they wait. What a pause interrupted runs as the next attempt |
| List of objectives | Read-only: each objective's status (and, when paused, what the pause does and why), the agent a running task is on, why a task waits, its result or its failure cause |
Tasks screen#
| Action | Effect |
|---|---|
| Refresh | Re-reads the objectives |
| Table | One row per task: Objective, Task, Status, Attempts, Detail |
Cancel (a CREATED, READY or RUNNING task) |
Cancels the task and the tasks that depend on it. A running attempt ends CANCELLED and the agent is released (or stopped if its turn cannot be interrupted) |
Move (a CREATED, READY or BLOCKED task with no attempt running) |
Opens a field with the agents of the task's project and "Any eligible agent"; Confirm calls reassign_task for the attempt shown. A refusal (the last attempt still ends, the task moved on) shows above the table and the field stays open |
| Graph line (an objective with more than one task) | Read-only: "Step N · after A (STATUS) · unblocks B (STATUS)". Tasks of one step wait for the same tasks and can run side by side |
| Evidence (a task that has any) | A disclosure under the row: why the task is complete (the check that passed, or who approved it), the verification, what its attempts handed over (artifacts and refused outputs), the worktree they ran in, the decisions and the moves |
Processes screen#
| Action | Effect |
|---|---|
| Search field | Filters by PID, command or owner |
| Refresh | Re-reads the runtime processes the Core has alive |
MCP screen#
| Action | Effect |
|---|---|
| Refresh | Re-reads the pending requests and connected clients |
| Server | Read-only: status (Running or Off), local endpoint, public URL or "Local only" |
| Deny (a pending request) | Refuses the remote client's authorization request, with one click |
| Approve | Opens a field for the code shown on the browser page that started the connection. The window never displays that code |
| Confirm | Approves the request if the typed code matches (otherwise an error appears under the field and the request stays pending). A request expires after 5 minutes |
| Cancel, Escape (in the code form) | Closes the code form without deciding |
| Revoke, then Confirm revoke | Takes away the access of a connected client. Cancel keeps it |
Skills screen#
Lists the skills Cortex finds: the folders under skills in the data directory (global) and under .cortex/skills in each project. A skill is a folder with a skill.json manifest and its Markdown content, and nothing else but files. A new skill is disabled.
| Action | Effect |
|---|---|
| Rescan | Reads the skill folders again |
| Enable | Approves the version and content shown and starts delivering the skill to the agents it applies to. If the folder changed since the list was read, the Core refuses it and the list shows the new version |
| Disable | Stops delivering the skill; its folder is kept. A changed skill (its files changed after it was enabled) offers Disable, or Approve to accept the new content |
| Not valid | Folders that are not skills, with the reason |
| Conflicts | Skills with the same name: override (the most specific scope is delivered) or ambiguous (same scope, different folders: none is delivered until one is disabled), with the agents affected |
| Effective set, Agent | For the chosen agent, every skill that applies to it and whether it is delivered or why not. Changes apply when an agent starts: a running agent keeps the skills it started with |
Keyboard summary#
| Keys | Where | Effect |
|---|---|---|
| Enter | Console and Orchestrator fields | Send |
| Shift+Enter | Console and Orchestrator fields | New line |
| Enter or Space | A focused agent card | Select the agent |
| Arrow Left, Arrow Right, Home, End | Side panel tabs | Move between Orchestrator and Console |
| Escape | Add agent and Edit agent dialogs, the MCP code form, the account menu | Close or cancel |
| Tab | Dialogs | Focus stays inside the dialog |