Maia CLI, model tool, and JSON reference
Status: Implemented and Source candidate commands under
Commands today are verified against apps/cli/src (the
command tree in apps/cli/src/command-tree/) on 2026-10-03, as
maia help --json reports them. Everything else is Planned for the
ROADMAP slice named beside it (S5, S12–S21, R1, R7, R8, R10a, R11, MN0–MN5,
T2, RL2–RL4).
This file owns command grammar, the Maia Agent surfaces (launch flags, interactive commands, model tools, voice tool) and the R11 capability disposition. Contracts own identifiers, schemas, preview and confirmation, receipts, events and generation. Operations owns observability and recovery. The CLI README is the package overview.
The package's library entry exports makeCli and runCli, which return an
Effect of the exit code (decision); the maia binary
runs that Effect on one runtime per process.
Naming: container commands address the organizational Workspace; workspace
commands address an execution context. R1b renames them to workspace and
environment through a
versioned migration;
no current command changes meaning before then.
Contract rules
packages/protocolowns every operation ID, schema, error, risk class, authority scope, event and receipt policy. Each CLI usage maps to one registered operation. Each command is authored once, as a leaf of a command-tree fragment in its feature'stree/scope (for exampleapps/cli/src/session/tree/), with its usage, availability, operations and kind;apps/cli/src/command-tree/holds the grammar, help and suggestions. The typed tree test checks every leaf against the inventory inapps/cli/src/testing/node-operation-registry.ts, which the compiler holds exhaustive over the protocol methods and SDK operations, andscripts/quality/registry-parity.mjschecks the tree's usages against the protocol feature descriptors (decision).- CLI, SDK, MCP, voice and model tools project the same operation-owned data.
- Lists are bounded and cursor-paginated:
--limit,--after <cursor>, andnextCursorin the result. Cursors are opaque. A list that merges Nodes gives each Node's cursor inresult.nodes. - Consequential mutations use exact IDs, expected revisions, a command ID
(idempotency key), preview and human confirmation where the operation
requires it, and a receipt. Only
setup,node installandnode removetake--yes. After an uncertain result, reconcile withmaia receipt show <command-id>and retry with the same command ID. A consequential mutation that needs a preview is two-phase:--dry-runpreviews (with--input-stdinwhere the request is JSON on stdin), and--confirm-stdinconfirms the preview piped back. - Identity comes from the authenticated transport; no flag sets the actor
(
receipt list --actoronly filters, by exact stored actor ID, D-138). - Output never contains credentials, prompt bodies, hidden reasoning, raw
audio or raw provider payloads. A read made to show content shows the
bodies it reads:
session attach,session history read, a read with--include-content, and the plannedmaia dashboardSession view (decision). Printed command equivalents,--plainoutput and receipts never carry a body. A JSON preview carries its confirmation proof for the person who confirms; human text and every other result omit it. - Large or sensitive input uses bounded stdin flags (for example
--input-stdin,--prompt-stdin,--body-stdin,--api-key-stdin), never arguments. - Lists, search and observation follow the
coordinator visibility contract:
an omitted filter never widens scope, and no mutation targets
all. - There is no public Fleet, Workflow, Bot or Subagent command root,
and no
rulescommand (AGENTS.mdloads through context; decision). Session-local tasks exist only as the Agenttasktool and/task(decision). - Manual CLI controls never need an LLM or Jev (recovery).
Global grammar
maia [--json] [--profile <name>] [--node <name>] [--no-input] <node-backed command> ...
maia [--json] help [<command>...]
maia [--json] <command>... (-h | --help)
maia [--json] version
maia completion <bash|zsh|fish>
maia [--json] doctor
maia agent ...
Commands are scope-first: maia <scope> [<sub-scope>] <verb> [<id>] [flags],
for example maia session turn send --session <session-id> .... The ID of
the resource a command is about is positional (session show <session-id>);
the ID of a parent or related resource is a flag (--workspace, --run,
--session, --project). The earlier paths (approve, turn, monitor,
recovery, *-preview and *-confirm, and the rest) are retired with no
alias (decision). An unknown or retired path prints a
short Did you mean 'maia ...'? and runs nothing.
- Global options (
--json,--profile,--node,--no-input,-h/--help) may appear once each, anywhere before--; none is parsed undermaia agent. Everything after--belongs to the command,--jsonincluded.-hand--helpare never an option's value. --profile <name>selects an existing local Maia Node profile for that invocation only (defaultdefault). It is not a configuration or execution profile: those are--execution-profile(workspace execution set) and, for a remote Node,--remote-profile(node install,node register).version,completion,maia agentand model-provider login reject it; help ignores it.--node <name>routes a Node-backed command,maia mcpincluded, to a remote Node the local Node registered (maia node installormaia node register), through one SSH stdio bridge per invocation. It takes one exact registered name (no prefix, noall), needs the Nodereadyand fails closed (node_unavailable, exit 69) without falling back to the local Node; a mutation previews and confirms against that Node.maia node install|register|list|removerejects it, and so does a Session credential (role_denied).maia --node <name> login codex|clauderuns the pinned vendor sign-in on that host overssh -t; a registered Node has none pinned, so it refuses.--node localnames this machine's Node, so no remote Node takes the namelocal. A name no registered Node has is a usage error (invalid_argument, exit 64) that names it. An attached command (--attached:session inbox check,session inbox ack,session message send) speaks for its Session on this machine's channel and refuses--nodethe same way (exit 64).context show --attachedreads the attached Session's own context, so--nodenames no other Node for it. A parity test holds every other Node-backed command to it.- Without
--node, the commands below address every Node (D-175): this machine's and each registered remote Node, four at a time and 12 s each, asmaia node listasks them. Every other command acts on this machine's Node, as before; so does a command under a Session credential orMAIA_NODE, and every command when no remote Node is registered (or this machine's Node keeps no registry). Which Node takes an ID follows client-core's routing rule, the one Desktop uses.-
An exact ID selects the one Node that holds it, for a read and a mutation alike:
- a Project ID:
project show,project guard show|set(--confirm-stdinby the piped preview'sprojectId),routine create(both phases, by the input'stemplate.projectId), and--projectonproject context record|list,workspace list,routine list,session listandsession create; - a Workspace ID:
workspace show|select,workspace execution show|set|clear,workspace membership attach, and--workspaceonterminal create|listandsession create(session createtakes one of--projectand--workspace; both refuse with exit 64 before any Node is asked, with--nodetoo); - a terminal ID:
terminal show|read|resize|signal|close,terminal controller acquire|release,terminal input write, and--terminalonterminal events listandterminal controller request create|show; - a Routine ID:
routine show|pause|resume|disable|run(--confirm-stdinby the piped preview'sroutineId); - a Session ID:
session show|wait|rename|start|stop|resume|archive|diff|attach,session history read(aclaude:orcodex:ID reads this machine),session turn queue, and--sessiononsession turn send,session events listandsession approval list; - an approval ID:
session approval show|approve|deny|answer. The decision still needs a person at an interactive terminal; the CLI reads the request on its holder and answers it there. It asks the Nodes first, so a caller that is no person meets a silent or ambiguous Node's refusal before its own; - a turn ID:
session turn show|wait|cancel|interrupt.turn waitroutes by its first turn ID, so the turns it waits for share one Node; - a command ID:
receipt show. A command ID is the caller's own key, not a Node-made ID, so every well-formed one asks every Node; two Nodes can hold the same key, and then the command refuses as below.
session attachfollows a Session on a remote Node, routed or named with--node, over the SSH bridge's attach stream. Detaching (or the CLI's end) closes the stream and its Node socket; the Session runs on. A lost link or a stream the bridge ended aslostreattaches from the view's cursor. Replay recovery shows every item of one epoch once; any snapshot (after a Node restart, or when a replay no longer reaches back far enough) can print text again, as a local attach does. Any other failure ends the command at once with its own code (role_denied77,resource_not_found66). A bridge built before streams (an older Node) refuses the subscribe and exits 65; the CLI then fails withcommand_not_implemented(exit 64) and namessession turn waitandsession turn show --include-contentinstead. A Session credential still cannot address another Node with--node.The CLI asks the Nodes on every command and keeps no answer. Text output prints
node: <name> (holds <id>)on stderr; JSON adds aresolvedentry with"argument": "--node", the Node asidand the ID asreference. ID prefixes and names still resolve on this machine's Node, and the command acts there; with remote Nodes registered, the CLI first reads the registry, and a not-found then adds the line (JSON:hint) "Names and prefixes resolve on this machine's Node; pass --node <name> or use an exact ID." A list scoped this way reads the holder alone and keeps its own shape.project registerandworkspace repository attachname a path, and a path names a machine, so they act on this machine's Node unless--nodenames another; so doproject create|renameand the Session commands not named above. - a Project ID:
-
It never guesses: an ID two Nodes hold is
node_ambiguous(exit 64), an ID no Node holdsresource_not_found(exit 66), and an ID a Node that did not answer could hold isnode_unavailable(exit 69, retryable), each naming the Nodes. The JSON error addsnodes, each Node'sstate:holds,absentorunavailable.--nodesettles any of them. A remote Node that is notreadygives its state, and aninstall_failedorremove_failedone namesmaia node remove <name>. -
project list,routine listwithout--project,session list --allandsession approval listwithout--sessionwith remote Nodes merge every Node (approvals: every Node's pending requests before the resolved ones--alladds, each text entry led by its Node): each row carries itsnode, text output adds aNODEcolumn, andresult.nodesgives each Node'snextCursororerror. A Node that fails shows as its own error, labeled with its Node (text:error: <node>: <message>), beside the others' rows, with"ok": false, that failure as the top-levelerror, and its exit code.--limitbounds each Node's page. A cursor pages one Node, so there--afterneeds--node. A list on one Node,--nodeincluded, keeps its own shape: no labels,nextCursorat the top.
-
--no-inputforbids prompts: a command that would ask a person fails with exit 64 and names its flags.MAIA_PROFILE,MAIA_NODEandMAIA_NO_INPUTstand in for--profile,--nodeand--no-inputon the commands that take them; a flag wins. WithMAIA_NODEset and no--json, the command first printsnode: <name> (MAIA_NODE)on stderr.--timeoutand--stall-aftertake a duration:30s,10mor1h.maia agentforwards every following token to the Agent unchanged.- Arguments outside
maia agentare bounded: at most 32 tokens, 4 KiB per token, 16 KiB in total. maia helpprints the root commands by section;maia help <command>...and-hor--helpanywhere print one command: a group lists its subcommands, a leaf its usage, typed arguments, an example and its operation.--helpafter a leaf's arguments still shows that leaf. Help never reaches a Node.versionand--versionprint the version;maia version --jsonreports{command, name, version, protocol, commit?}, withcommitonly in a build made withMAIA_CLI_BUILD_COMMIT.-vis not an option.maia completion <bash|zsh|fish>prints a shell completion script.- Every resource argument accepts a name or an ID prefix as well as an ID (References and defaults). A list returns 20 items per page unless its usage shows another range.
- Inside a Codex or Claude Code Session the Node launched,
MAIA_SESSION_CREDENTIALis that Session's own credential andMAIA_NODE_ORIGINthe Node's exact loopback origin. WithMAIA_SESSION_CREDENTIALset, every Node-backed command,maia mcpincluded, acts as that Session within its role (workerororchestrator): it never reads the operator's profile, descriptor or bearer, and--profilechanges nothing. A missing or invalidMAIA_NODE_ORIGINfails the command withconfiguration_invalid(exit 70), with no fallback. An operation outside the role, and any--attachedoperation, fails withrole_denied(exit 77).
JSON output and exit codes
With --json, success writes one line to stdout. result.command names the
command (project.register, turn.wait, ...); the rest is that command's
projection, usually the operation result under result.result:
{
"ok": true,
"result": { "command": "version", "name": "maia", "version": "<version>" }
}
A failure writes one line to stderr; recovery appears when a mutation's
outcome is unknown, and a usage error (exit 64) adds the nearest command's
usage lines and a hint with an example and the help to run. (maia doctor
is the exception: it always writes its check result to stdout, a failed one
included.) The exit code carries a command's outcome: maia doctor with a
failed check, or session turn wait with a turn that failed or did not
settle, prints "ok": true with the result (result.result.ok, or each
turn's state and satisfied) and exits 70, 74 or 76.
A session create task batch with a refused or unknown item prints
"ok": false with the same result (every item, a Session that failed to
start with its error.failure code and message) on stdout and exits 70 or 76.
Its top-level error is the first failed item's: the failure code and
message when the Node gave one, else the stopped step's code. A merged list
with a failed Node does the same with that Node's error, so error.code is
set whenever ok is false.
{
"ok": false,
"error": {
"code": "outcome_unknown",
"message": "Local request outcome unknown.",
"retryable": false
},
"recovery": { "commandId": "command_01..." }
}
When the CLI resolved a reference or a default for the command (see
References and defaults), the success line also
carries resolved: one entry per argument with its argument (the option or
positional placeholder), exact id, kind, label when the resource has a
name or title, match (name, prefix, path for the current folder or
selection for the selected Workspace) and the typed reference, absent for
a default.
{
"ok": true,
"resolved": [
{
"argument": "--project",
"id": "project_01...",
"kind": "project",
"label": "maiaos",
"match": "path"
}
],
"result": { "command": "session.list", "result": { "items": [] } }
}
Error codes are lowercase protocol codes (invalid_argument,
resource_not_found, saturated, ...). Without --json, errors print
maia: <message>, then the usage and hint of a usage error or, for an
unknown outcome, the maia receipt show command to run.
The CLI owns two codes of its own, for what it decides before or instead of a Node (no protocol operation returns them):
| Code | Exit | Meaning |
|---|---|---|
node_unavailable | 69 | No Node answered: the local Node is not running, a remote Node is not ready or its bridge failed, or a Node that could hold an exact ID did not answer |
node_ambiguous | 64 | Two Nodes hold an exact ID; --node <name> chooses one |
| Exit | Meaning |
|---|---|
| 0 | success |
| 64 | usage error: invalid argument, unknown command or an ID on two Nodes |
| 66 | resource not found |
| 69 | Maia Node unavailable, an unsupported Node.js runtime, or no terminal |
| 70 | other failure, a waited turn that failed, or a failed doctor check |
| 74 | deadline exceeded, or a session wait or session turn wait timed out |
| 75 | temporary: saturated, held, a full guard or retryable; try again later |
| 76 | outcome unknown: reconcile by command ID |
| 77 | authentication failed, forbidden context override or role_denied |
| 130 | interrupted by a signal; nothing more is printed |
session wait exits 74 when its timeout ends before the Session reaches
the state. session turn wait exits 70 when a turn it waited for failed, 74 at the
deadline and 76 when a turn's outcome is unknown. maia doctor exits 70 when
a check fails and names the next command beside each failure.
maia agent exits 2 on any error it reports itself, and 70 when the Agent
payload cannot load.
Agent contract
LLM agents and scripts can rely on these; human text may change, they may
not. maia help --json returns the same rules as its contract block, with
every command in commands.
- Pass
--jsonand readresult; failures are one JSON line on stderr with the exit codes above. - Discover commands with
maia help --json: every group and leaf with its summary, kind, availability, usage, example and operations. A command's own--help --json(for examplemaia session start --help --json) adds typed arguments: type, choices, integer range, required and repeatable. In a usage,<x>is a value,[x]optional,(a | b)one of,<a|b>a choice,<1..100>an integer range andx...repeatable. - The CLI prompts only when stdin and stderr are terminals and neither
--jsonnor--no-input(MAIA_NO_INPUT=1) is present; otherwise a command that needs a person fails with exit 64 and names its flags. - A two-phase command previews with
--dry-run(or--input-stdin) and confirms that preview from stdin with--confirm-stdin, for examplemaia --json hold place --input-stdin --dry-run < in.json | maia hold place --confirm-stdin. - Durations are
30s,10mor1h. - A mutation that guards a revision takes the current
--expected-revision(or a named--expected-*-revision). Pass a--command-idyou choose: after exit 76, runmaia receipt show <command-id>, then retry with the same command ID, and the Node replays the first result. The CLI itself retries asaturatedanswer whosesaturationisadmission(the Node's request slots were full) four times with short jittered backoff and the same request body before it exits 75; anaccountrefusal (an account's Session slots or vendor limit) exits 75 at once. - Lists take
--limitand--after <cursor>; passnextCursoras--after, and with a merged list'sresult.nodesentry, itsnodeas--node. - An explicit flag wins over its environment variable (
MAIA_PROFILE,MAIA_NODE,MAIA_NO_INPUT), which wins over the default. - A resource argument takes a full ID, a unique ID prefix or a unique name or
Session title; an ambiguous one exits 64 listing the candidates, and
resolvedsays what the CLI resolved and how (next section).
References and defaults
Every positional reference and scope option a command's usage names as a
Session, Project, Workspace, Connection or Container (<session-id>,
--project <project-id>, <connection>, ...) takes:
- a full ID (
session_and 32 hexadecimal digits), sent as typed with no lookup, so a Session's own credential, which cannot make one, still works; - a unique ID prefix of at least 4 hexadecimal digits, with or without the
tag (
3fa9,session_3fa9); - or a unique name: a Project, Workspace, Container or Connection name, or a Session title. A name that matches no resource exactly may still match one that differs only in letter case.
Before the command runs, the CLI asks the Node (reference.resolve, one read
per distinct reference, at most 32) and sends the exact ID it returns, also
for --name=value, so a mutation binds the resource the person named and a
two-phase preview shows that ID. A compare-and-set guard (--expected-*) is
never resolved: it takes the ID an earlier read returned. A Project resolves
first, then a default scope, and a Session or Workspace then resolves inside
that Project. More than one match exits 64 and lists up to eight candidates
with their names; no match exits 66 and the message names the kind; an answer
of another kind exits 70. The CLI never guesses. Option errors still exit 64
before any request.
A command that requires --project or --workspace and gets neither uses,
in order:
- the registered Workspace checkout or Project repository containing the current folder (the CLI sends the folder's path; the Node maps it, through symbolic links, and the CLI never reads Git);
- the selected Workspace (
maia workspace select), or its Project; - otherwise it exits 64 and names the flags to pass.
An attached command (--attached) speaks for its Session through this
machine's invocation channel and takes exact IDs as typed.
The same order applies to a mutation (session create without
--workspace creates in the folder's Workspace); the CLI says which default
it used on stderr and in resolved, so pass the flag to choose another.
The default option goes before any --.
When the folder's Project has several Workspaces and none is selected, a
command that also takes --project (session create) falls back to the
Project; another lists the candidates. With --node, the CLI sends no
folder: the remote Node uses its selected Workspace. In human output each
resolution prints one line on stderr, for example
--project: project_01... (maiaos) from the current folder.
Commands today
maia help --json lists every command with each usage's availability
(implemented or source_implemented_candidate), and a command's
--help --json describes it alone. A source candidate is callable from a
source build but release-disabled. Below, …
stands for maia [--json] [--profile <name>].
Implemented
… status
… node status
… node monitor
… project register <path> [--name <name>] [--command-id <id>]
… project list [--limit <1..100>] [--after <cursor>]
… project show <project-id>
… project context record --project <project-id> --body-stdin [--command-id <id>]
… project context list --project <project-id> [--limit <1..100>] [--after <cursor>]
… project context show <note-id>
… workspace list --project <project-id> [--limit <1..100>] [--after <cursor>]
… workspace show <workspace-id>
… workspace repository attach <path> --workspace <workspace-id> --expected-revision <revision> [--name <name>] [--command-id <id>]
… session create (--workspace <workspace-id> | --project <project-id>) [--command-id <id>] [--role <worker|orchestrator>] [--connection <connection-id> [--executor <codex-cli|claude-code>] [--model <id>] | --executor <codex-cli|claude-code> | --agent <agent-id> | --agent-definition <key> --agent-definition-version <version> --agent-definition-digest <sha256>] [--effort <low|medium|high|xhigh|max|ultra>] [--base <branch-tag-or-commit> | --host-authority [--permission <mode>]] [--cwd <path>] [--title <text> | --name <text>] [--label <key=value>...] [--message <text> | --prompt-stdin | --from-file <path>]
… session list (--project <project-id> | --all) [--state <state>[,<state>...]] [--label <key=value>...] [--attention] [--settled [--idle-for <duration>]] [--limit <1..100>] [--after <cursor>]
… session show <session-id>
… session events list --session <session-id> [--limit <1..32>] [--after <session-cursor>]
… session observe [<session-id>...] [--stall-after <duration>]
… receipt list [--limit <1..32>] [--after <cursor>] [--actor <actor-id>] [--method <method>] [--since <recorded-at>] [--until <recorded-at>]
… receipt show <command-id>
… events read [--kind <kind>]... [--since <cursor>] [--subject <type>:<id>] [--limit <1..100>]
Plus help, version, completion, doctor and the
Agent launch grammar.
session events listandterminal events list(for an unavailable terminal; a live terminal reports an evicted range as agapevent) addoldestRetainedCursorto their JSON when a prune removed events after--after(D-191) and lead the text withearlier history pruned; oldest retained cursor N.receipt showaddseventsPrunedand, in text, anevents-prunedcolumn.events read(source candidate) prints one page of the Node's public events after--since; see events.node statusreports Node readiness with storage and process health;--jsonreturns a compact projection, includingadmissionCapswithmaximumActiveSessionsanddefaultAccountSessions. SetMAIA_NODE_MAX_ACTIVE_SESSIONS(default 8) andMAIA_NODE_DEFAULT_ACCOUNT_SESSIONS(default 2) in the Node environment before startup; each must be an integer in1..64, otherwise startup fails withconfiguration_invalid. The account default applies to new Connections only; explicit account limits and existing records keep their values. Restarting with a lower Node cap stops no Session and admits no new one until the count falls below it.maia statusis a shortcut for it. Session create, start and resume also use a startup-configured host headroom gate (Source candidate), shared by scheduled starts. It defaults toMAIA_NODE_HEADROOM_ENABLED=true,MAIA_NODE_MAX_LOAD_PER_CPU=1.0,MAIA_NODE_MIN_MEMORY_AVAILABLE_PERCENT=10andMAIA_NODE_MIN_DISK_FREE_PERCENT=10. Equality passes; load is the one-minute host load divided by logical CPU count, memory is LinuxMemAvailable(macOS: memory available to the Node process), and disk is available blocks on the Node's state volume. Thresholds must be finite and nonnegative; percentages cannot exceed 100. Invalid values fail config load. Set the enable switch tofalsebefore startup to disable this gate. A refusal is retryablesaturatedwithcpu|memory|diskand measured/required values; unavailable measurements fail closed. Receipt replay preserves its result. See Node admission settings. When the Node reports journal retention, the human output adds anevents:line (bytes over the limit, andcheckpoint blockedwhen a reader holds the log) and the compact projection carriesstorage.events. On Linux, a Node outside a delegated cgroup reports process resources ascgroup_delegation_unavailablewith next steprun_node_in_delegated_cgroup, because every launch would fail closed. A Linux Node on a kernel below Landlock ABI 6 (Linux 6.12) reportslandlock_scoped_unavailablewith next stepupgrade_kernelfor the same reason. A failed probe reports it too. A Linux Node also lists what the vendors' own sandboxes need from its host, one line each with the owner's fix when it is missing:bubblewrapandsocat(foundormissingon the Node'sPATH) anduser namespaces(allowed,no_nestingorrestricted);--jsoncarries them asexecutorHost(prerequisites).node monitorprints a loopback monitor URL carrying a short-lived bootstrap token.doctoris a read-only check of this installation, the Node and the worker accounts. Before the product CLI loads it checks the bootstrap, the exact Node.js runtime, the CLI payload and the Agent payload; a failed one reportsnext: reinstall Maia. Each check carries ascope:machine(the bootstrap, runtime, payload and CLI checks, labeled "(this machine)" in text) ornode.doctor --node <name>runs thenodechecks on that Node and addsnodeto the result. A sound installation then checks the CLI, the Node, and for each installed vendor CLI its account and approved executable, for Codex the Node's worker prompt check (codex-cli.worker-promptfromconnection detect: a failure readsskills.* keys no longer honored by Codex <version>withnext: update Maia, and an inconclusive check passes withprobe inconclusive), and that a worker is ready. Each failed check names the next command to run (maia setup,maia login codex,maia integration plan codex, ...). It reads no Project configuration, loads no extension, calls no model and repairs nothing. A failed check exits 70, and--jsonstill writes the checks to stdout.completion <bash|zsh|fish>prints a shell completion script generated from the command tree.version --jsonreports{command, name, version, protocol, commit?}.project contextstores user-authored notes that outlive Sessions (coordinator).session createwith--message <text>,--prompt-stdinor--from-file <path>creates, starts and sends the first turn of each Session through the SDK'screateSessions: one Session for the message or standard input, one per non-empty line of the file (at most 32 tasks of at most 16 KiB each; the file at most 524,352 bytes). A refused line sends nothing and exits 64. The batch runs in the named Workspace, or with--projectin the Project's first ready Workspace, its main checkout first. Text output for one task prints labeled lines:stage(sent, orstartedwithout a task,refused,unknown),session,turn,error(<operation>:<code>) and, for anunknowntask only, thecommandID to reconcile withreceipt show; a line with no value is left out. Several tasks print a table with the task's line number, the same columns and-for none. Receipt IDs are in JSON only. JSON returnssession.create.taskswithitemsandworkspaceId; each item carries the Node'sreceiptsby step (create,start,send). A failedsession.create,session.startorturn.senderror carries itscommandId, refused or unknown; only anunknownitem also carriesrecovery.commandIdto reconcile withreceipt show. It exits 0 when every item settled, 76 when one is unknown and 70 otherwise. The CLI runs a batch once: running the command again starts a new batch with new Sessions, so reconcile an unknown item instead. Only an SDK caller that keeps the outcomes retries in place.--command-idnames the one Session of--messageor--prompt-stdin(its start and first turn take<id>.startand<id>.send) and is refused with--from-file, as are--title,--nameand--label(each Session of a file takes its task's first line). The Agent definition flags do not apply.session create --label <key>=<value>(repeatable) puts up to 8 labels on a new Session, such asowner=ci-bot,lane=e1orgoal=fix-build. A key is a lower-case name of up to 32 characters (a-z, digits,_,-; it starts with a letter); a value is 1 to 64 printable ASCII characters without spaces or commas. Labels are set at creation, are part of the command's identity (a retry with the same command ID must repeat them), stay with the Session across Node restarts, and show in every list: never put a secret in one. JSONsession listandsession showcarrylabels(absent when a Session has none); the text list adds aLABELScolumn ofkey=valuepairs (-when none) and aLAST ACTIVEcolumn, which is the Session'supdatedAt(when its record last changed, not when its last turn ran); humansession showprintslabelsandupdatedtoo. A prompted create (--message,--prompt-stdin) keeps its labels.session list --label <key>=<value>(repeatable) keeps the Sessions that carry every given label, with the exact value, before the page is cut. The SDK takes the samelabelsobject onsession.list.- One manager per Project and owner (D-201):
session create --role orchestrator --label owner=<name>(also with--host-authority) andsession adopt --role orchestrator --label owner=<name>create a Project's manager. A top-level orchestrator needs theownerlabel. While another top-level orchestrator of the same Project andowneris not archived, the Node refuses withmanager_exists(exit 70, not retryable) and names that Session; reattach to it, resume it or archive it first. A stopped manager keeps its slot; the slot frees whensession archivefinishes, and an archived Session cannot start or resume. A manager's default title is<project> · <owner>;--title(alias--name) on create or adopt wins.session create --host-authoritytakes no role exceptorchestratorwith anownerlabel. A manager created without--titleor--nameis titled<project name> · <owner>. An orchestrator child is not a manager. The guarantee holds per Node database, andowneris a label the caller asserts, not a login. session list --alllists every Session of the Node in creation order;--statekeeps the named states and--attentionthe Sessions that need a person. JSON carries the Node'sattentionvalue of each listed Session that has one; text output andsession observeprint it as a column (-when none), and text output adds the title.- A list command's text output is a plain table: a header row, then one row
per item with columns aligned by two spaces and no borders, then
next: --after <cursor>when another page follows (a merged list printsnext: --node <name> --after <cursor>per Node). Approval, question, terminal, model and inbox reads keep their own text; scripts use--json.session create,session show,session start,session stop,session resume,session turn send,cancel,interrupt,showandwait,project showandproject registerprint one object as alignedlabel: valuelines instead, leaving out a field with no value. session create --agent <agent-id>starts the Session from an Agent definition at its current revision and fixes the harness, Connection, model, effort and instruction, so--connection,--executor,--modeland--effortare refused with exit 64.--basestill applies. The Session keeps the Agent ID and revision it started from.session createoptions--connection,--executorand the Agent definition flags are mutually exclusive, except that a gateway--connectiontakes--executor(required) and--model(optional, an id fromconnection models list).--connectionbinds one account, by name or ID;--executorlets the Node pick the least-used account that can take the Session (rules, decision). The binding is final. When no account fits, the CLI exits 75 (saturated: every account busy, limited or used up), 70 (configuration_invalid: every account disabled or signed out) or 66 (resource_not_found: no account of that kind).- The three Agent definition flags are all or none. Key and version are exact
string tokens; the digest is
sha256:plus 64 lowercase hex characters. They bind metadata only, never activation or provider authority; the Node rejects unknown references.
session create --effort <level>sets the reasoning effort of a vendor executor Session and needs--connectionor--executor. Claude Code takeslow,medium,high,xhighandmax; Codex also takesultra(a model may support fewer). The level reaches the vendor unchanged, and an unsupported level is refused with exit 64 or 70, never mapped to another. A Session without it keeps the vendor default. Maia agent Sessions take no effort.- On a Linux Node,
session create(and so a Routine fire or an Agent) refuses, exit 70 before it records anything, an executor whose own sandbox the host cannot run:host_bubblewrap_missing,host_socat_missingorhost_user_namespaces_restricted, with a message that names the package or the sysctl and profile the owner changes. Codex needs them at any access; Claude Code only with--base(prerequisites). session create --host-authority(D-198, pending owner) opts one Codex or Claude Code Session out of the Maia sandbox. The harness runs in the Workspace's checkout (or--cwd) as the Node's user, with that user's network,git push,gh, installs and vendor configuration. Host admission skips the Linux sandbox prerequisites above, since no vendor sandbox runs.--permission <mode>passes the vendor's own permission mode or approval policy unchanged (the values areSESSION_PERMISSION_MODESin the protocol). Without it Claude Code uses its own configured default, and Codex uses the fixed approval policyon-request, which overrides the user's Codex configuration. Maia answers every command-approval request Codex emits with a decline, and commands that need no approval still run with host authority.--permissionwithout--host-authorityis refused. The flag needs--connectionor--executorand refuses--baseand--agent;--role orchestratorrecords the role and the manager slot (D-201) but grants no credential, since the Session runs outside the Maia sandbox. The Node's owner must start it withMAIA_HOST_AUTHORITY=enabled: otherwise create, start and resume of such a Session fail withhost_authority_disabled, andnode statusshowshost authority: disabled. A Session credential or an external coordinator cannot request it (role_denied).session show,session list(aCONTAINMENTcolumn) and--jsonreportcontainment: nonefor it and-for a sandboxed Session;session showand--jsonalso report the storedpermissionmode (such asbypassPermissions). It runs on the user's own vendor configuration, so it binds a system-default account: the Node picks one granted to the Project, and a named Maia-managed account (or a gateway) is refused at create. Claude Code and Codex host authority are source candidates. Codex accepts the policiesuntrusted,on-requestandnever.session create --rolesets what the Session's own credential reaches:worker(the default) ororchestrator(Session roles and Project guards).session observereads live runtime state for every running Session or for 1–32 named IDs: runtime (codex-cli,claude-code,pi), running turn and elapsed time, native activity (record count, last record kind, idle time, declined vendor requests) and process usage (CPU time, RSS, process count; an unmeasurable value is absent, never zero). Health isidlebetween turns,workingwhile records arrive, andstalledafter--stall-afterof silence (10s to 24h, default 3m). It never returns prompt or model text. Durable Session truth stays insession showandsession events list. A Session that needs the orchestrator carries oneattentionvalue, most urgent first:failed,approval_pending,question(an unanswered recorded question),turn_failed(a running Session whose latest turn failed or ended with an unknown outcome, until a later turn is sent or the Session stops; a turn a stop ended raises nothing),stalledorsettled_unreviewed(a Run ledger dispatch whose turns ended and nobody settled); a requested Session without a live runtime still reports it.
Source candidates (release-disabled)
Remote Nodes
The remote Nodes this Node installed or registered over SSH (S14,
decision, operations).
node install previews (probe and plan), asks for confirmation and waits until
the Node is ready (--node-runtime defaults to the Node.js running this command
when that is linux-x64 24.22.1; otherwise the flag is required); node register records a Node already running on the
host: it greets that Node over the bridge --node uses, through the launcher
--launcher names or install's layout under the remote HOME, previews what
it records and records it ready once confirmed
(decision); node list adds the reachability this CLI observes now,
for every ready host at once (its text STATE shows unreachable or stale
for a ready host that did not answer as recorded, never ready): the release, lifecycle, pressure and tree use
(used/capacity) the Node greeted with, its pid and latency, or the reason the
host failed, and one failing host never fails the others
(decision); node remove uninstalls, and --purge
also deletes the profile's state; for a registered Node it forgets the
record and leaves the Node running. SSH never accepts an unknown or changed host
key for the destination: trust it once with ssh <host> first. A failing host
shows one fixed reason, never what ssh printed, and a failed install step
shows no raw command diagnostic from the host (validated facts such as paths
still appear, and login shows its own ssh -t session)
(limits).
install, register and remove are two-phase: at a terminal they preview and ask y/N,
--yes skips the question for a script (install and remove only), --dry-run only previews, and
--confirm-stdin confirms a preview piped back. --json prints one lossless
result.
… node install <host> --release <maia-node.release.json> [--node-runtime <node>] [--name <name>] [--root <dir>] [--remote-profile <name>] [--keep <2..8>] [--codex-bin <path>] [--claude-bin <path>] [--command-id <id>] [--dry-run | --confirm-stdin | --yes]
… node list [--all]
… node register <host> [--launcher <path>] [--name <name>] [--remote-profile <name>] [--command-id <id>] [--dry-run | --confirm-stdin]
… node remove <node> [--purge] [--command-id <id>] [--dry-run | --confirm-stdin | --yes]
First-owner bootstrap
Creates the first User, local Device, CLI client, team Workspace and owner
membership of an empty local Node
(specification).
It is local only: it takes --profile but refuses --node, and it has no
--yes, --dry-run or stdin confirmation, because only a person at the
controlling terminal can confirm.
… node bootstrap --owner-name <display-name> --workspace-name <name> [--command-id <id>]
- The command starts an empty
maia-nodefor the profile (found onPATH) with a private bootstrap channel, previews, prints every identity the apply creates, then asks forYES <preview-id>on/dev/tty(POSIX only). Standard input is never read. A missing or redirected terminal exits 64 before a Node starts; any other answer exits 1 withCancelled; nothing changed. - A Node already running for the profile, or one that already has an owner,
refuses with
bootstrap_channel_invalidorbootstrap_already_completedand exits 70. Bootstrap is never repeated. --jsonprints one line,{"ok":true,"result":{"command":"team-workspace.bootstrap.apply","result":<apply result>}}. The apply result carries the created IDs,role: "owner",delivery(neworduplicate) and the receipt; it carries no proof, credential or path. Text output prints the same fields as tab-separated rows.- After an unknown outcome the CLI reads
receipt show <command-id>on the same profile and returns the stored result. It sends one identical retry only when no receipt exists. An owned Node without the receipt exits 70 withbootstrap_integrity_failed. Any inconclusive retry (saturated,node_unavailable,deadline_exceeded, a transport failure or an unrecognised code) keeps the unknown outcome: exit 76 with the recovery command, because the first apply may still be running or may have committed. Only a success, a replay or a typed bootstrap refusal (bootstrap_already_completed,bootstrap_authority_unavailable,bootstrap_binding_mismatch,bootstrap_confirmation_required,bootstrap_preview_expired) replaces it. - The command stops its Node when it ends. When the outcome stays unknown, the
error names the profile and the Node to start before reading the receipt:
maia-node --desktop --profile <name>, thenmaia --profile <name> receipt show <command-id>. With--jsonthe failure carriesrecovery: {"commandId":"<id>","profile":"<name>"}.
Login, Connections, and models
Worker accounts, model providers and approved vendor executables (v1 vendor executors, S2a; decision). The model catalog itself is planned with R10 (Providers).
… login
… login <claude|codex> [--name <name>] [--device-code] [--max-sessions <1..64>]
… login <model-provider> [--api-key-stdin]
… login gateway --name <name> --base-url <url> --api-key-stdin [--max-sessions <1..64>] [--command-id <id>]
… connection create --name <name> --executor <codex-cli|claude-code> --root <maia-managed|system-default> [--max-sessions <1..64>] [--command-id <id>]
… connection list [--limit <1..100>] [--after <cursor>]
… connection show <connection>
… connection detect [--refresh]
… connection update-policy set <claude-code|codex-cli> <off|notify> [--command-id <id>] [--dry-run | --confirm-stdin | --yes]
… connection login start <connection> [--device-code] [--command-id <id>]
… connection usage show <connection> [--refresh]
… connection models list --connection <connection>
… connection capacity set <connection> --max-sessions <1..64> --expected-revision <revision> [--command-id <id>]
… connection enable <connection> --expected-revision <revision> [--command-id <id>]
… connection disable <connection> --expected-revision <revision> [--command-id <id>]
… connection grant list [--connection <connection>] [--project <project-id> | --container <container-id>] [--state <active|revoked>] [--limit <1..100>] [--after <cursor>]
… connection grant set <connection> (--project <project-id> | --container <container-id> | --confirm-stdin) [--revoke] [--command-id <id>] [--dry-run]
… connection pool list [--project <project-id> | --container <container-id>] [--limit <1..100>] [--after <cursor>]
… connection pool set (--project <project-id> | --container <container-id> | --confirm-stdin) --mode <off|rotate|single> [<connection>...] [--command-id <id>] [--dry-run]
… setup [--max-sessions <1..64>] [--command-id <id>] [--dry-run | --confirm-stdin | --yes]
… integration plan <codex|claude-code>
… integration status <codex|claude-code>
… integration apply <codex|claude-code> --plan <plan-id> [--command-id <id>]
… integration remove <codex|claude-code> [--command-id <id>]
… provider pool list
-
maia loginis the one sign-in (decision; credential rules in Providers; an example follows the sign-in bullets). At a terminal it lists worker accounts and model providers and offers a menu; without a terminal or with--jsonit prints the list and changes nothing. -
login claude|codexproposes the next free name (codex-1,codex-2, ...), creates the Maia-managed account, runs the vendor's own sign-in for its private root and shows its usage; an existing name signs in again. Without a terminal,login codexneeds--nameand returns a device code. Claude Code signs in only from a terminal. -
login <model-provider>runs Pi's sign-in for a Maia Agent model provider (for examplevercel-ai-gateway,anthropic,openai-codex).--api-key-stdinreads the key from stdin and never replaces a stored subscription.maia agent loginopens an interactive Agent without a session file for Pi's own/login. -
Headless sign-in: a shell is headless over SSH (
SSH_CONNECTION,SSH_CLIENTorSSH_TTYset) or, on Linux and BSD, withoutDISPLAYorWAYLAND_DISPLAY. Codex then uses its device code (--device-codeforces it); Claude Code prints a link and waits for the pasted code. -
connection usage showshows the last usage snapshot: plan, 5-hour and weekly windows (percent used, reset time) and reset credits.--refreshreads the vendor now.connection login startasks the Node to start a Codex sign-in, for a Node on another machine; to sign in from this terminal uselogin <claude|codex> --name <name>.connection capacity setchanges an account's--max-sessions. -
login gatewaystores a gateway Connection: the base URL (https://, orhttp://to a loopback host, without credentials, query or trailing slash) and the API key read from stdin. The key never appears in output; leave/v1out of the URL. After a failure that names a command id, rerun with--command-id <id>to finish the same gateway.connection models listlists the model ids the gateway serves. A gateway has no usage or sign-in;connection disableremoves it reversibly. -
connection grant setoffers a Connection to one Project or to one Workspace (--container, the organizational Workspace), or takes the offer back with--revoke; a Session binds only a granted account, and a refusal names this command.connection pool setnames the accounts a scope's Sessions use:--mode singlewith one account,--mode rotatewith one or more (at most 32, each already granted to that scope),--mode offwith none to defer to the next scope. Both previews show the current and the proposed state; a person confirms at the prompt, or pipe--json --dry-runinto--confirm-stdin(decision). A confirm that loses its answer names the--command-idto reconcile withreceipt show. -
Sign-in example:
maia connection detect # the installed claude and codex as the Node sees them maia setup [--max-sessions 4] # use them as they are: system-default accounts maia login # list worker accounts and model providers maia login claude # add or re-sign a Claude Code account maia login codex [--name codex-2] [--device-code] maia login <model-provider> # a model provider from Pi's catalog maia connection usage show codex-1 [--refresh] printf %s "$GATEWAY_API_KEY" | maia login gateway --name <name> \ --base-url <url> --api-key-stdin maia connection models list --connection <name> maia session create --workspace <workspace-id> --connection <name> \ --executor claude-code --model <model-id>Maia never reads or stores a vendor account's credential:
login claudeandlogin codexrun the vendor's own sign-in for a Maia-managed account root. Model providers store one credential per provider in Maia'sauth.json. A person approves vendor executables withintegration planandintegration apply --plan <plan-id>. -
connection detect(MCPconnection_detect) shows what the Node sees of the installed Claude Code and Codex CLIs: path, version, whether its own PATH finds them, the sign-in kind (vendor-loginwith the vendor's method,custom-endpointwith the endpoint host and whether a helper or a key variable supplies the credential,noneorunknown), the system-default account and whether the executable is approved. It reads no credential and shows no account identity (decision). For Codex,--json(andmaia doctor) also reportsworkerPrompt: the Node runscodex debug prompt-inputtwice (no model call, network or sign-in), each in its own temporaryHOMEandCODEX_HOMEholding one sentinel Skill, once plain and once with the keys every Codex worker launches with.okwhen the plain render shows the sentinel, the Skills instructions and the multi-agent role and the worker render exits 0 with none of them;failedwhen the worker render still shows one or exits non-zero (a Codex release that renamed or rejects the keys);inconclusivewhen the plain render fails or lacks one, a render times out, or a signal ended the worker render (an exit of 128 or more), which says nothing about the keys (decision).connection detectruns the renders;setupdoes not, since its preview and confirmation never read the result. -
connection detectalso reports, per installed CLI, how current it is (decision): whether the approved build launches (yeswhen the installed build is the approved one, elseunknown), the newest version the registry last named, the update policy and a proposal.--refreshfirst asks the public npm registry (bounded, advisory, a proxy for the vendor's own channel); a CLI the registry does not answer for keeps its last answer, and stderr names it. The Node holds the answer in memory, so a restart clears it until the next--refresh. A proposal is a command a person runs: the vendor's update command (claude update,codex update) once a newer version exists, ormaia integration plan <vendor>when a newer build is installed but not approved. Maia runs no vendor updater. The CLI asks for these facts (upkeep: true); a client that does not ask gets the result it always got. -
connection update-policy set <claude-code|codex-cli> <off|notify>previews the change, asks (or takes--yes) and records it with a receipt naming the authenticated actor.notifyis the default;offkeeps the facts and drops the proposal. It needs the CLI's executable approved. A manualintegration applykeeps it andintegration removeends it. Setting the policy a CLI already has changes nothing.autois Planned. -
setupuses them as they are: it previews, for each usable CLI, the system-default account it creates (claudeorcodex, elseclaude-systemorcodex-system;--max-sessionssets its cap, default 2) and the executable it approves, asks (or takes--yes), then confirms that exact preview.--dry-runonly previews and--confirm-stdinconfirms a preview piped back; the Node's preview repeats the--max-sessionsit was given, so the piped confirmation carries it. Accounts and approvals that exist show as such, with their Session cap; an existing account keeps that cap when--max-sessionsdiffers, and anote(notesin--json) namesconnection capacity setto change it; a skipped CLI says why.--jsonprints{ preview, result }and needs--yes; with--json,connection detectandsetupmark each binary pathpathScope: "node-local", a path on the Node's machine. When no installed CLI is usable, setup fails withresource_not_found(exit 66) and names each CLI's reason. -
integration plan <codex|claude-code>records the exact executable path, identity and SHA-256 thatintegration apply <vendor> --plan <plan-id>makes active. A changed binary fails closed until planned and applied again. For Claude Code,planapproves a Maia-owned copy of the resolvedclaudebinary.removealso retires an authorization fenced ascleanup_unknown;integration statusthen names that step (after a crash). -
provider pool listshows configured provider pools without credentials.
Sessions, turns and the coordinator MCP server
Vendor executor Sessions (S3, S4a, S4c, S6), coding Sessions (S5) and the live item stream (S12b).
… session rename <session-id> --title <text> --expected-revision <revision> [--command-id <id>]
… session diff <session-id> [--stat]
… session archive <session-id> --expected-revision <revision> [--summary <text>] [--transcript <path>] [--command-id <id>]
… session attach <session-id> [--after <epoch>:<sequence>] [--no-follow]
… session start <session-id> --expected-revision <revision> [--command-id <id>] [--wait [--timeout <duration>]]
… session stop <session-id> --expected-revision <revision> [--command-id <id>]
… session resume <session-id> --expected-revision <revision> [--command-id <id>]
… session wait <session-id> [--until <state>[,<state>...]] [--activity <activity>[,<activity>...]] [--timeout <duration>]
… session turn send --session <session-id> --prompt-stdin --expected-revision <revision> [--wait] [--command-id <id>]
… session turn show (<turn-id> | --session <session-id> --latest) [--include-content]
… session turn wait <turn-id>... [--any] [--timeout <duration>]
… session turn queue <session-id>
… session turn cancel <turn-id> --expected-turn-revision <revision> [--command-id <id>]
… session turn interrupt <turn-id> --expected-session-revision <revision> [--command-id <id>]
… session agent-permission set (--input-stdin --dry-run | --confirm-stdin)
… mcp [--worker]
- A Session has a title:
session create --title(or its alias--name; both together exit 64), else the first line of its first turn (whitespace collapsed, at most 80 characters with an ellipsis).session renamechanges it at the Session's revision with a receipt and asession.renamedevent. Titles need not be unique; a title that names one Session works as a reference, andsession listshows it. - Every Session mutation takes the Session's revision,
session turn interruptincluded (as--expected-session-revision):session showreturns it, and so doessession turn showassessionRevisionfor the Session's newest turn. A stale revision fails with exit 75, and the message names the current one. - A Session holds at most 8 pending turns, the running one included.
session turn sendqueues by default and dispatches in send order; the 9th fails withturn_queue_full(exit 70, not retryable).session turn queuelists the running turn and the queued ones in dispatch order as a table (POSITION STATE TURN REVISION INPUT); a queued entry shows its position and a one-line input preview, and an empty queue prints a message.session turn cancelends a queued turn at the turn's own revision (fromqueue); a turn that already left the queue fails withturn_not_queued, so usesession turn interruptfor it. A cancelled turn readsfailedwith reasonturn_rejected. - A script that drives a Session sends and waits in one step:
session turn send --session <ref> --prompt-stdin --expected-revision <revision> --wait. The Node compares the revision, so a change in between fails with exit 75 and the message names the revision to retry at; retry with the same--command-idso the Node replays the receipt instead of sending twice.--waitthen waits until the turn's outcome, at most 24h (theturn waitmaximum; exit 74 after that), and exits assession turn waitdoes (0 settled, 70 failed, 74 not settled, 76 outcome unknown); the JSON result addswait, theturn.waitresult. Without--waitthe command returns once the turn is queued.session waitwaits for a Session state, not for a turn: usesession turn wait <turn-id>(or--wait) to know a turn is done. A terminal ID or an unknown Session ID fails withresource_not_found(exit 66). To read the last result without keeping the turn ID,session turn show --session <ref> --latest --include-contentshows the Session's newest turn that ran: running or settled, never a queued turn or one that failed before it ran (cancelled, rejected by the runtime or cut by a stop), so such a follow-up leaves the previous result in place (itslatestTurnId, onsession showandsession list). A Session with no such turn gives an empty result (noturnfield) and exit 0.latestTurnIdis an optional field added to Session responses: a reader that rejects unknown fields must ignore it or update. - Steering a running turn is Planned (S26b, D-144).
- Stop, resume and wait semantics are in
vendor executors.
CLI specifics:
session start --waitthen polls until the Session runs or stops, by default at most two minutes (--timeout <duration>, 1s to 1h, needs--wait), and prints its state: exit 0 when running, 70 when failed, stopping or stopped, 74 at the deadline.session stopwaits at most 3 seconds for runtime teardown;session resumeexits 75 while the previous runtime is still stopping;session turn waitdefaults to--timeout 10m(1s to 24h), exits 0 when the turns settled, 70 when a waited turn failed, 74 when the turns have not settled and 76 when a turn's outcome is unknown, and never returns turn text (session turn show --include-contentdoes).session listandsession show(also with--json) report each Session'sstatus(what to do about it:starting,working,compacting,rate_limited,idle,question,approval,error,done,endedorunknown; events), and while it isquestionorapprovalthe pendingprompt(session showprints it with its options). They also report each Session'sactivity(stopped,waiting_approval,workingoridle) and its newest turn that ran aslatestTurnState(besidelatestTurnId; absent until a turn runs). A Session is done when its activity isidleand itsstatusisdone(its newest attempt completed, not an attempt a runtime refused to start); the text output printsdonefor it.session wait <id> --activity idle,waiting_approval,stoppedreturns when the Session is no longer working, and the reachedlatestTurnStatetells how the newest turn that ran ended.session waitblocks until the Session is in the--untilstate. Name several states with a comma list or by repeating--until, and it returns when the Session is in any of them, so an agent writessession wait <id> --until idle --until failedand reads the state reached in the output. Waiting on one state keeps waiting if the Session ends in another. It exits 0 when a state is reached, 74 when--timeout(default 10m, 1s to 24h) ends first, with the current Session printed either way; an unknown state exits 64 and an unknown Session 66. It repeats bounded Node waits inside one deadline, so it needs no polling and its total stays within the timeout.session show,session turn showandsession turn waitadd afailed: <code>: <message>line for a failure with a reason, andsession events listappends the reason and detail tosession.runtime.failedandturn.failed.session turn showprints a completed turn's tokens, with its model calls when the runtime counted them (Codex from its token-usage notifications; Claude Code from its result'snum_turns, the SDK's count of model round-trips in the turn). It adds acontext:line, the input tokens of the turn's last model call against the model's context window and as a percentage, when the runtime reported both (Codex:lastandmodelContextWindow; Claude Code: the last main-thread call and the result'smodelUsagewindow for that model, never a guess when the model has no entry). The turn'stokenUsagecarries them as the optionalcontextTokensandcontextWindow. Maia's decoders reject unknown fields, so upgrade the client and the Node together: an older client refuses a turn that carries them, and a downgraded Node cannot read the stored usage of such a turn. Usage is stored only for a completed turn, so a running, failed or interrupted turn shows no context, and an older turn has none: read it between turns, withsession turn show --session <ref> --latest. session create --cwd <path>starts a Codex or Claude Code Session in a folder of its Workspace. A relative path resolves against the Workspace root, or against the Session's worktree with--base, never against the client's working directory: the Node may be remote, and the worktree path does not exist before the Session does. Example:session create --executor codex-cli --base main --cwd packages/sdk. An absolute path must lie inside the Workspace root; with--baseit is refused, because the client cannot know the worktree path. The path has no..segment and at most 4096 bytes.--cwdneeds--connectionor--executor;--agentrefuses it. The Node checks the folder when the Session starts, with every symlink followed: it must exist, be a directory and stay inside the root, else the start fails closed (path_invalid, with no path in the message) and the Session isfailed. The Node re-checks it on every start and resume, and again right before the launch, so a folder swapped in between fails closed. Claude Code starts in the folder and keeps the whole Workspace or worktree as its sandbox. Codex CLI starts its thread in the folder; its:workspace_rootsfollow the thread's working directory and Codex 0.163 has no separate setting, so with Codex the write sandbox is the start directory, never wider than the root. Claude keeps the Workspace or worktree root.session create --basemakes a coding Session: the Node creates its own worktree Workspace from that ref on branchmaia/<workspace-id>and the worker may write inside it (coding Sessions).session diffprints one line per changed file and a total, then the patch (at most 48 KiB) unless--stat;session archiveprints the Session, state, revision, branch, snapshot and the time the Node prunes the worktree. A Session without a worktree (a read-only or failed one) has nothing to keep:session archivestops it, ends its runtime and prints the lineno worktree to keep. That archive is final too:session startandsession resumerefuse the Session, and a manager's slot frees with it (D-201).session archive --summary <text>(up to 2 KiB of UTF-8, written by the caller) and--transcript <path>(a path the caller names, up to 4 KiB; the Node records the path and never reads or stores transcript content) are bound into the command, kept in its receipt, replayed with it and returned assummaryandtranscriptPathin the JSON result. A retry must repeat them. The Node guesses no vendor transcript path.session list --settled [--idle-for <duration>]keeps the Sessions that are not archived and are stopped, failed, done (idle with a completed latest turn, settled at once), or idle for longer than the duration (30s,10m,24h; default24h, at most720h). A Session is idle since its newest turn changed, or since its record changed when it has no turn, so a rename never postpones retirement. The filter runs before the page is cut; each JSON item carriessettledReason(stopped,failedoridle) and the text table aSETTLEDcolumn. Retire a settled Session withsession archive.session attachfollows one Session's item stream (design): the replay or snapshot, then each message, reasoning summary, command, tool call, file change, error and token usage as it streams, deltas included. It reads; turns still go throughsession turn send. Text output prints## turnlines,user>/assistant>/thinking>items,$ <command>with its output and[exit <code>], and atokens:line with cached tokens and, when counted, model calls. Each line of Session text that Maia breaks prints indented two spaces; terminal soft wrap can still put text at column 0, and copied output carries the indent.--jsongives the exact text: it writes eachsession.attachframe as one NDJSON line, and every record carriesepochandsequence.--after <epoch>:<sequence>resumes after a record; a different epoch (the Node restarted) or a gap over 1,000 records or 8 MiB restarts from a snapshot. An item that started before that record prints when it completes, not as it streams.--no-followexits 0 at thesynchronizedframe. Onresync(a slow terminal) or a lost connection the command reattaches after its cursor; five reattaches without progress exit 69. An unknown Session exits 66. Codex and Claude Code Sessions stream today; Pi Sessions show turn boundaries only.session agent-permission setis a two-phase, human-only Agent grant:--input-stdin --dry-runpreviews and--confirm-stdinconfirms; it enables no bound worker.maia mcp --workerrequires the Session credential and loopback origin, never discovers an operator profile, and exposes exactly the tools of its credential's role (an orchestrator's add its child tools; an unreadable role gets the worker's). Either Session variable also selects this surface for plainmaia mcp; incomplete variables fail closed. Coding vendor workers receive this server in launch configuration outside their tool sandbox.maia mcpserves the coordinator tools of the protocol registry over stdio for Claude Code or Codex (its server test initializes with MCP protocol 2025-06-18). An invalid argument names the failing field in the error message. It exposes no human-only operation exceptturn.send, and reaches the Node as the coordinator principal, with--nodetoo (the SSH stdio bridge carries the mark): the Node admits the coordinator tool operations and refuses every other one withrole_denied, so a coordinator answers an approval only where its Project grants it (D-199) and never confirms a preview (details).- Without
--node, plainmaia mcpaddresses every Node as the commands above do (D-175), with the same CLI code and one SSH stdio bridge per Node for the whole MCP session. With no remote Node registered, every call goes to this machine's Node as before.- A tool that names an exact ID runs on the one Node that holds it, read or
mutation: a Session ID (
session_show|start|stop|resume|archive,session_rename|diff|events_list,turn_send,turn_queue_list,approval_list, andsession_history_readfor a Maia Session), a turn ID (turn_show,turn_interrupt,turn_cancel), a Project ID (project_show,project_guard_show,workspace_list,reference_resolve,session_list,routine_list), a Workspace ID (workspace_show, andsession_create), an approval, Routine or command ID (approval_show,routine_show,receipt_get). The server first checks the arguments with the protocol's request schema, as a Node does, so an invalid call refuses (invalid_argument) before any Node is asked. It then asks each Node with that kind's show tool; two holders (node_ambiguous), a Node that could hold the ID and did not answer (node_unavailable) or no holder (resource_not_found) refuse before the call is sent. An ID list (turn_wait,session_observe) runs on the Node that holds every ID; IDs on different Nodes refuse withinvalid_argument. - Without an ID,
project_list,session_list,routine_list,approval_listandsession_observemerge every Node's page: each item names itsnode, andnodeshas one entry per Node with its next cursor or its error. In a mergedsession_observe, the entry of a Node that answered also carries that Node's ownobservedAtandstallAfterSeconds; a failed Node's entry has neither.limitbounds each Node's page;session_observetakes nolimit. A Node's failure fails the call (isError) andresultkeeps the other Nodes' rows. With more than one Node,afterCursorrefuses (invalid_argument): page one Node withmaia --node <name> mcp. A call that one Node answers, because only one Node exists or one Node holds every ID it names, keeps the protocol's own result shape. - Everything else stays on this machine's Node: a
session_createwithout an exact Workspace ID, connections, port leases, vendor discovery,node_status, containers, and the Agent and Run ledger tools, whose IDs no route kind covers yet. Reach another Node's withmaia --node <name> mcp. - Each Node gets every routed call, its holder check included, as the
coordinator, so that Node's own coordinator gate decides it: no tool,
role or operation is added, and Routine mutations stay outside the
coordinator tools.
maia mcp --workerand a Session credential keep every call on the Session's own Node.
- A tool that names an exact ID runs on the one Node that holds it, read or
mutation: a Session ID (
Approvals and worker questions
What running workers wait on a person for (S1, S4b; decision).
… session approval list [--session <session-id>] [--all] [--limit <1..50>]
… session approval show <approval-id>
… session approval approve <approval-id> [--for-session] [--as-coordinator] [--command-id <id>]
… session approval deny <approval-id> [--reason <text>] [--as-coordinator] [--command-id <id>]
… session approval answer <approval-id> (--text <answer> | --option <option-id>) [--as-coordinator] [--command-id <id>]
… session answer <session-id> <answer> --prompt <approval-id> [--as-coordinator] [--command-id <id>]
session approval listandshowmap toapproval.listandapproval.show, also the MCP toolsapproval_listandapproval_show;listshows pending requests, newest first, and--alladds resolved ones. Each request names its Session, turn, kind (command,file_change,tool,question), expiry and the exact text a person decides on: the command and its folder, a patch's files with line counts and the digest of every diff, a tool call with its whole input, or a question with its options.session approval approve,denyandanswermap toapproval.respond. They are a person's: they refuse to run without an interactive terminal (an agent's shell or a pipe has none), and the Node refuses every worker Session credential. Two exceptions. A person sets the first on the Project guard (D-199, proposed;project guard set --approval-coordinator none|answer|decide):--as-coordinatorsends the request as the coordinator principal, with no terminal and no[y/N], and the Node answersrole_deniedunless the Session's Project grants it (answer: questions only;decide: also approve and deny); the same grant opens theapproval_respondMCP tool. The Node checks the grant when it commits the answer, so a revocation refuses every later answer and leaves one already committed. The receipt namescoordinator:actor_local_userand the guard revision that admitted it.MAIA_NODE_HEADLESS_APPROVALS=offat Node start turns every grant off for that run, andnode.statusreports it. The second is an orchestrator Session: it mayanswera question of its direct child without a terminal through itsapproval_list,approval_showandapproval_respondtools, never approve or deny (D-186). Each first shows the request, then sends itsrequestDigest, so the Node applies the decision only to the request the person read;approvealso asks a person[y/N].--for-sessionapproves the same request for the Session until its worker restarts, where the vendor offers it (Codex).--optionanswers with that option's label.session answeranswers the prompt a Session waits on, the onesession showprints underprompt:. It runs the same show, confirm andapproval.respondpath assession approval, so it adds no authority: a person, the Project's coordinator grant and an orchestrator's own child are the callers allowed above, and the Node checks each. A question takes the text (or an option id, sent as its label) as its answer, so the literalapproveanswers a question; a command, file or tool request takesapproveordeny.--prompt <approval-id>is required: it names the prompt thatsession showprinted, so an answer meant for one prompt never lands on the one that replaced it. The prompt must belong to the Session, and the Node refuses it once it is resolved, expired or replaced (approval_closed,approval_expired,approval_request_changed). A retry repeats--promptwith the original--command-id, and the Node replays the receipt. A Session whose status isunknowngets no answer: the command reads the Session first and stops.--as-coordinatoris optional.- A decision is final: a retry with the same command id replays its receipt;
any other decision on a resolved request is
approval_closed, after the expiryapproval_expired, and on a changed requestapproval_request_changed.
Port leases
Advisory port leases (S16 port kind).
… port lease acquire --purpose <label> [--session <session-id>] [--count <1..8>] [--preferred <port>] [--command-id <id>]
… port lease renew <lease-id> --expected-revision <revision> [--command-id <id>]
… port lease release <lease-id> [--command-id <id>]
… port lease list [--session <session-id>] [--limit <1..100>] [--after <cursor>]
- One to one with
port.lease.acquire,renew,releaseandlist(theport leaseleaves), also served as the MCP toolsport_lease_acquire,port_lease_renew,port_lease_releaseandport_lease_list. - A lease holds 1 to 8 TCP ports from the Node's range (
MAIA_PORT_RANGE, default41000-41999) for an exact Session or, without--session, for the caller. A port is granted after a loopback connect and bind probe finds it free;--preferredis a hint inside the range. - Leases expire after 15 minutes unless renewed at their exact revision, end
when their Session stops or fails, and are reconciled at Node start.
releaseis idempotent. Codex and Claude Code Sessions holding leases at launch getMAIA_PORTS=<port>,<port>; nothing else in their environment changes. - Leases are advisory (decision): the Node never
restricts binds, blocks a Session over ports or signals a process.
listreports per port whether something listens on it now (probes[].inUse; an asterisk in text output). Typed failures:port_pool_exhausted(retryable),port_lease_not_found,port_lease_expired,port_lease_conflict.
Session roles and Project guards
Session-bound worker credentials and fan-out guards (S18).
… project guard show <project-id>
… project guard set (<project-id> --expected-revision <revision> --max-depth <1..4> --max-live-children <1..64> --max-messages-per-minute <1..600> --max-leases-per-session <1..64> [--approval-coordinator <none|answer|decide>] [--command-id <id>] [--dry-run] | --confirm-stdin)
project guard showisproject.guard.show(also the MCP toolproject_guard_show).project guard setalso takes--approval-coordinator <none|answer|decide>: the grant that lets a coordinator answer a Project's pending approvals without a terminal (D-199, proposed); omitted, it keeps its level. It is two-phase overproject.guard.previewandproject.guard.confirm, both human-only: no MCP tool, no automatic confirmation. With the limits it previews and, at a terminal, asks y/N;--dry-runonly previews;--confirm-stdinconfirms a preview piped back.showprints the source (defaultat revision 0 until a person confirms new guards, thenproject), the revision and the four limits; JSON output is the lossless result. Defaults: depth 1 (Sessions an operator creates cannot create children), 8 live children per parent, 60 messages per Session per minute and 8 port leases per Session.- The preview takes all four limits at the guard's exact revision;
--command-iddefaults to a fresh one. It returns the current guard, the proposed limits,inputDigest,proofand a 30-second expiry. Human text shows the current and proposed lines and omits the proof; JSON output carries it. --confirm-stdinreads that--json --dry-runpreview from stdin (at most 64 KiB), for examplemaia --json project guard set <project-id> ... --dry-run | maia project guard set --confirm-stdin. It replaces the guard and returns it with its receipt (target typeproject_guard). An unknown outcome names the command ID formaia receipt show.- The Node enforces the guards when a Session creates a child, sends a
message or leases a port.
guard_live_children_exceeded,guard_messages_per_minute_exceededandguard_leases_per_session_exceededare retryable (exit 75).guard_depth_exceededis not (exit 70): a person must raise--max-depth. - A Session's role comes from
session create --role. With its own credential (see Global grammar) aworkerreachescontext.show,node.statusand its own port leases; anorchestratoralso reachesproject.guard.showandsession.createin its Workspace, andsession.show,session.start,turn.send,turn.showandsession.stopon its own child Sessions, at most 2 levels deep (D-151).turn.showreads a turn through its Session's link. The Node checks the child link per request: a Session that is not its child fails withrole_deniedlike anything else (exit 77). A Connection's Session cap still bounds children: the one past it failssaturated. A coding child (--base-ref) gets its worktree only after the guards admit it, and its worktree receipts name the parent Session.
Emergency holds
Emergency holds (S20) on exact Projects and accounts.
… hold list [--state <active|released>] [--limit <1..100>] [--after <cursor>]
… hold place (--input-stdin --dry-run | --confirm-stdin)
… hold release (--input-stdin --dry-run | --confirm-stdin)
hold listishold.list.hold placeandhold releaseare two-phase leaves overhold.place.previewandhold.place.confirm, andhold.release.previewandhold.release.confirm. All four are human-only: no MCP tool, no automatic confirmation. The request arrives on stdin, so these leaves never prompt:--input-stdin --dry-runpreviews and--confirm-stdinconfirms that preview piped back, for examplemaia --json hold place --input-stdin --dry-run < in.json | maia hold place --confirm-stdin. Without--dry-runor--confirm-stdinthe command exits 64 and names them.hold place --input-stdintakes strict JSON{ commandId, reason, running, targets }on stdin (8 KiB, 32 chunks).targetslists 1 to 64 exact, existing{ "kind": "project" | "connection", "id": ... }entries; there is no wildcard and noall.reasonis a 1–128 character label.runningiskeep(reject new work) orstop(also stop the Sessions running on the targets). The preview returns the NodeholdId, the Sessions nowstarting,runningorrecoveringon the targets,inputDigest,proofand a 30-second expiry; forstopthe digest binds that Session list.- Confirmation takes the preview (its input plus
inputDigestandproof) and returns the hold, its receipt (target typehold),stoppedSessionIdsand the delivery status. It fails with exit 75 and places no hold when a Session started on the targets after the preview. When a stopped runtime has not ended within 3 seconds, it fails with retryablesaturated(exit 75) although the hold is placed; repeat the same confirmation to wait for it. hold release --input-stdintakes{ commandId, holdId, expectedRevision }; confirmation addsinputDigestandproofand returns the released hold (revision 2) with its receipt. A released hold stays listed as evidence; Sessions it stopped stay stopped untilsession resume.- While a hold is active,
session create,session start,session resume,session turn send,run create, Session spawn and the voice relay on a held target fail with error codeheldand exit 75; the message names the hold and the target.session create --executorskips held accounts. JSON preview output carries the confirmation proof; human text omits it. Design, measurements and deferred targets (Node, capability, peer): operations.
Scheduled Sessions
Routines (S30): a cron expression and an IANA time zone fire one Session from a stored template and send its first turn (decision, contracts).
… routine create (--input-stdin --dry-run | --confirm-stdin)
… routine list [--project <project-id>] [--state <active|paused|disabled>] [--limit <1..100>] [--after <cursor>]
… routine show <routine-id>
… routine pause <routine-id> --expected-revision <revision> [--command-id <id>]
… routine resume <routine-id> --expected-revision <revision> [--command-id <id>]
… routine disable <routine-id> --expected-revision <revision> [--command-id <id>] [--dry-run | --confirm-stdin]
… routine run <routine-id> --expected-revision <revision> [--command-id <id>] [--dry-run | --confirm-stdin]
routine createis two-phase overroutine.create.previewandroutine.create.confirm, human-only likehold place: the request arrives on stdin, so it never prompts.--input-stdin --dry-runpreviews and--confirm-stdinconfirms that preview piped back. Without either flag the command exits 64 and names them. A preview expires after 2 minutes.routine create --input-stdintakes strict JSON{ commandId, name, paused?, schedule: { expression, timezone }, template: { projectId, prompt, executor?, connectionId?, model? } }(8 KiB).expressionhas five POSIX cron fields.templatenames anexecutoror aconnectionId;modelneeds aconnectionId. With"paused": truethe routine startspausedwith no next fire, so it fires after aroutine resumeand not before; the preview binds the choice and--confirm-stdinmust repeat it. A paused routine counts toward the Project's limit of 20. The preview returns the routine ID, the next 3 fire times,inputDigest,proofand the expiry. The Node refuses withroutine_schedule_invalid,routine_interval_too_short(a gap under 15 minutes),routine_limit_reached(20 active or paused routines in the Project) orconfiguration_invalid(a template its Connection can never serve, such as a gatewayconnectionIdwithoutexecutor); each exits 70 and changes nothing.routine pause,resumeanddisableare compare-and-set on--expected-revisionand record a receipt. Pause stops fires; resume continues from the next slot after now and never replays slots paused over, so resuming a batch of routines fires none of them at once. The last column of each result is the next fire (-while the routine does not fire).routine disableis final, so it is two-phase overroutine.disable.previewandroutine.disable.confirm, likeproject guard set. The Node previews the routine; a person at a terminal reads its name and revision and confirms, or--dry-runpreviews and--confirm-stdinconfirms that preview piped back (with--json). Without a flag and without a terminal the command exits 64 and names the flags. A preview expires after 2 minutes. Create a new routine to schedule again.routine runfires anactiveroutine once now, as its schedule would. It starts a Session, so it is two-phase overroutine.run.previewandroutine.run.confirm, with the flags, prompt and stdin ofroutine disable. The confirmation records alaunchingfire with triggermanualand a receipt, and the Node's next tick launches it through the same path, holds, guards and Session cap. That tick judges it again: a routine that is not active then, a hold or a disabled Connection fails it with its reason. The output is the fire ID, outcome, routine, receipt and delivery;routine showfollows the fire. The same--command-idand proof return the first fire. A refused preview or confirmation records nothing and exits 75 withroutine_busywhile the previous fire's first turn is unsettled, or withheld; 70 withroutine_not_active(paused or disabled) orconnection_disabled; 66 for an unknown routine. It never moves the next fire.routine listandroutine showalso reach coordinators as the MCP toolsroutine_listandroutine_show; every change is human-only.routine showlists the newest 20 fires, newest first: scheduled time, trigger (cronormanual), outcome (started,skipped,failed, orlaunchingwhile the Session is created), typed reason, Session, first-turn state,missedslots folded into a catch-up and, when the first turn failed, its cause: the failure reason (for exampleruntime_failed) with the runtime's code and message when it gave them. When the fire's Session failed to start, the cause is that Session's code and message, such as why Claude Code did not initialize. A fire skipped while the previous first turn is unsettled has reasonoverlap. A failed fire has reasonheld,connection_disabled,routine_not_active(a manual fire whose routine is not active at the claim),saturated(the Node has no free active Session slot),workspace_unavailable,launch_failedoroutcome_unknown; the next tick is the retry.session showon the fire's Session says why a launch failed.- Fires run on the Node's clock, checked every 5 seconds. Slots missed while the
Node was down run once at start. The Session starts through the same path as
session create,session startandsession turn send, so holds, guards and Connection state apply. A Node allows 8 active Sessions, so each tick stops the Session of a settled fire, whatever its outcome, once its turns are finished. The Session keeps its history and diff, andsession resumewakes it. A Session a person has sent another turn stays running.
Webhooks
Source candidate. Signed outbound webhooks for Node events (E12, EV3). The delivery contract is in events.
… webhooks list [--limit <1..100>] [--after <cursor>]
… webhooks deliveries [--webhook <webhook-id>] [--failed] [--limit <1..100>] [--after <cursor>]
… webhooks add (--url <http-url> --secret-env <VARIABLE> --event <kind>... [--label <key=value>...] [--project <project-id>] [--node-id <node-id>] [--allow-private] [--command-id <id>] [--dry-run] | --confirm-stdin)
… webhooks test (<webhook-id> --expected-revision <revision> [--command-id <id>] [--dry-run] | --confirm-stdin)
… webhooks remove (<webhook-id> --expected-revision <revision> [--command-id <id>] [--dry-run] | --confirm-stdin)
… webhooks redeliver (<delivery-id> --expected-revision <revision> [--command-id <id>] [--dry-run] | --confirm-stdin)
webhooks listiswebhooks.listandwebhooks deliveriesiswebhooks.deliveries; neither is an MCP tool.add,test,removeandredeliverare four leaves over one two-phase pair,webhooks.change.previewandwebhooks.change.confirm, operator-only: an attached Session credential is refused, and there is no automatic confirmation. At a terminal a change previews and asks y/N;--dry-runonly previews;--confirm-stdinconfirms a--json --dry-runpreview piped back to the leaf that made it. A preview of another action exits 64 before any request.--eventand--labelrepeat.--label team=corefilters onlabel.team.--secret-envnames an environment variable of the Node; the CLI never reads, prints or stores the secret.- A change is bound to its action, input, actor, hook revision, destination and
--command-id, and expires 30 seconds after the preview. A removal preview states how many deliveries the removal deletes.testandredeliversend to an external receiver, so a send already transmitted is uncertain.
Agent definitions
Versioned Agent definitions of a Project (S49, D-120).
… agents list --project <project-id> [--state <active|disabled>] [--limit <1..100>] [--after <agent-id>]
… agents show <agent-id> [--revision <revision>]
… agents detect
… agents create (--project <project-id> --name <text> --harness <claude-code|codex-cli|maia-agent|pi> --accountable <actor-id> --instruction <text> [--connection <connection-id>] [--model <id>] [--effort <low|medium|high|xhigh|max|ultra>] [--manager <agent-id>] [--home-session <session-id>] [--command-id <id>] [--dry-run] | --confirm-stdin)
… agents revise (<agent-id> --expected-revision <revision> <the create options but --project> [--dry-run] | --confirm-stdin)
… agents disable (<agent-id> --expected-revision <revision> [--command-id <id>] [--dry-run] | --confirm-stdin)
agents listisagents.listandagents showisagents.show; both are also MCP tools (agents_list,agents_show).create,reviseanddisableare three leaves over one two-phase pair,agents.change.previewandagents.change.confirm, both human-only: no MCP tool, no automatic confirmation. A Session credential getsrole_denied(exit 77). At a terminal a change previews and asks y/N;--dry-runonly previews;--confirm-stdinconfirms a--json --dry-runpreview piped back (at most 64 KiB) to the leaf that made it, for examplemaia --json agents disable <agent-id> --expected-revision 1 --dry-run | maia agents disable --confirm-stdin; a preview of another change exits 64 before any request.agents detect(agents.detect, Source candidate, AI1, D-190) reads the machine, not a Project's definitions: which of Claude Code and Codex the Node finds, with their versions, and the folders at user scope that hold installed Skills. Project scope is not part of this slice (AI1c). It works on the Node--nodenames, one at a time, and is not an MCP tool. The Node reads each version with--versionand lists user Skills through the pinnedskillsCLI (skills list -g --json), the exact 1.7.0 release the Node bundles as a dependency (D-193), run by the Node's own runtime from the real home, with the worker read denials, a small environment, a 4 s deadline over the whole read (the SDK gives up at 5 s) and a 256 KiB stream bound. The pinned CLI cuts a pipe at 64 KiB, so about 320 Skills or more read asinvalid_output, and a heavily loaded host can reach the 4 s deadline (timeout) on the first read; more than 8 distinct folders for one harness read asoutput_too_large, and a folder whose shown path passes 512 characters is dropped. Detection never downloads or installs anything, and askillsbinary in a Project never runs. Maia keeps no table of harness folders: a folder appears only once the pinned CLI lists an installed Skill in it, so"folders": []means none reported, not unsupported. Output carries no Skill name, binary path or account; home shows as~, relative to the real home. The user scope isokorunavailablewith a reason (failed,timeout,output_too_large,invalid_output,unsupportedoff Linux;failedalso on Linux below 6.12, where hard containment is unavailable, see operations). An unavailable scope keeps the result, printsok: falsewith the reason aserror.codeunder--json, and exits 70. A Session credential getsauthentication_failed.- A change is bound to its exact input, actor and
--command-id(fresh by default), and expires 30 seconds after the preview.reviseanddisablename the revision they expect; a revision that moved since the preview, or an expired or altered preview, fails withstorage_conflictand changes nothing. Confirmation returns the Agent and a receipt (target typeagent, methodagents.change.confirm). An unknown outcome names the command ID formaia receipt show. - A definition holds the name, harness, accountable actor, instruction (at most
16 KiB), optional Connection, model and effort, and an optional manager
Agent and home Session. The accountable actor is the actor making the change
(
configuration_invalidotherwise): the Node knows no other actor to hold accountable. Placement islocal; remote placement is refused.revisereplaces the whole definition and bumps the revision; the Project, the ID and earlier revisions stay readable withshow --revision. disableis final: a disabled Agent stays listed as evidence, takes no new Session and cannot be revised.showandlistprint the headline (ID, state, revision, harness, name);showadds the digest, the spec and the instruction. Human text omits the proof; JSON output carries it.- Fallback routes and Skill references are not part of a definition yet: they
join it with agent reliability (S60a) and the Skill catalog (S22), which are
the first readers of them.
session create --agentstartsclaude-codeandcodex-cliAgents; amaia-agentorpiAgent exits 70 (configuration_invalid). The instruction reaches Claude Code as its system prompt and Codex as developer instructions.
Session discovery and adoption
Find the Claude Code and Codex sessions on this machine, read their past messages and adopt an ended one as a Maia Session (S11, decision).
… import scan [--source claude,codex] [--since <days>d] [--root <path>] [--running] [--adopted | --not-adopted] [--include-content] [--limit <1..100>] [--after <cursor>]
… session adopt (<claude|codex>:<native-id> --workspace <workspace-id> [--attest-ended] [--host-authority [--permission <mode>]] [--role <worker|orchestrator>] [--label <key=value>...] [--title <title>] [--command-id <id>] [--dry-run] | --confirm-stdin)
… session import (--from <file> [--dry-run] | --confirm-stdin)
… session history read <session-id|claude:<id>|codex:<id>> [--offset <n>] [--limit <1..50>]
- One to one with
discovery.sessions.list,session.adopt.preview,session.adopt.confirmandsession.history.read.session adoptis one two-phase leaf:--dry-runonly previews,--confirm-stdinconfirms a preview piped back, and at a terminal it previews and asks y/N. The two reads are also coordinator MCP tools (discovery_sessions_list,session_history_read); the preview and confirmation are human-only. A Maia-launched worker getsauthentication_failedfor all four. scanlists metadata: native ID, directory, Git repository and branch, last activity, model when known, live, ended or unknown with its evidence, and the store that lists the session. Sessions are grouped by Git common directory, so a linked worktree joins its repository; a directory that no longer exists is reported as orphaned. Titles, first prompts and previews come from message content and appear only with--include-content. The window defaults to 30 days; pages default to 50 sessions. The list holds every session in the stores, interactive or started by a program (Claude Code headless and SDK runs, Codexexecruns), running or ended. Each item carries two facts.livenessis adoption's conservative check (D-078): a Claude Code session whosesessions/<pid>.jsonrecord exists islive, even when a crash left the record behind, soadoptrefuses rather than risk a second resume.observedis what a live process shows:runningwhen that record names a process that is Claude Code (read from/proc),not-runningfor a stale or absent record. For Codex it isrunningwhen a Codex process holds the thread's rollout file open (the kernel reports the open files, so arguments cannot fake it; a process of another user shows none) or was started withresume <thread-id>orexec resume <thread-id>; an ID elsewhere in the arguments selects nothing. A process counts as Claude Code or Codex when it is that command, or the script anodeorbunruns;vim claudeis neither. Any other Codex thread isunknown. A host without a readable process table, or a table Maia read only in part (a process it could not read, more than 8,192 processes), also reportsunknown, nevernot-running.--runningkeeps therunningsessions and leaves outunknownones, so an empty--runningresult does not prove that no Codex process is running.--adoptedand--not-adoptedkeep the sessions Maia adopted or did not, so a hygiene check can ask for "running and not adopted". Filters apply before paging. Each store reads at most 3,000 sessions of the window per call, newest first, counting the ones Maia leaves out (its own Sessions, workers, sessions without a directory). Sessions share one order: newest first and, at one time, by native ID. When a store holds more, the call keeps the first 3,000, names the store intruncatedSources(the text output prints atruncatedline) and ends its page with anextCursorafter the last session read, even when the page holds nothing; the next call continues there, so following the cursor reaches every session, even when thousands share one millisecond. A call also reads at most 10,000 rows per store, counting the earlier ones a cursor skips; a cursor deeper than that ends the call truncated with nonextCursor. Maia's own Sessions and Codex worker threads are counted, not listed. A store Maia cannot read (an unapprovedcodex, for example) reports a source error and the other still answers.adoptneeds an ended session, a system-default Connection for the vendor (maia connection create --name claude --executor claude-code --root system-default) and a Workspace of the same repository. The Session runs in the Workspace's primary checkout:originalwhen the session ran there,relocatedotherwise (Codex only; Claude Code resumes a session from its own directory). Claude Code is live while itssessions/<pid>.jsonrecord exists; Codex keeps no liveness Maia can read, so a person passes--attest-endedafter closing it elsewhere. The confirmation checks liveness and the claim again and creates a stopped Session bound to the native ID;maia session resumecontinues the native session. A native session is adopted at most once.--host-authority(optionally--permission <mode>, as forsession create) binds the adopted Session to host authority (D-198):maia session resumethen continues the native session as the Node's user with no Maia sandbox, using the user's own vendor configuration. Claude Code only; it needs an operator, theMAIA_HOST_AUTHORITY=enabledswitch at Node start, and showscontainment: nonelike a created host Session. The mode is part of the preview digest and cannot change after adoption.session importadopts many sessions after one review. It composessession.adopt.previewandsession.adopt.confirmonce per entry; it adds no operation, store or migration.--from <file>names a JSON file{ "entries": [ ... ] }of 1 to 64 entries and at most 65,536 bytes, each{ "harness": "claude-code"|"codex-cli", "nativeId", "workspaceId", "attestEnded"?, "hostAuthority"?: { "permission"? }, "title"?, "labels"?, "role"? }.title,labelsandrolemean what they mean forsession adopt(anorchestratorneeds anownerlabel and takes the Project's manager slot, D-201); each is bound into the entry's preview digest. The caller resolves each Workspace; the CLI does no directory matching.--dry-runpreviews every entry, mints one command ID per entry and prints an artifact of at most 65,536 bytes (the stdin bound; a larger one fails, so split the file). Each entry shows its Workspace, account, working directory, host authority with any permission mode, and any title, labels and role.--confirm-stdinconfirms exactly the previews in that artifact and never rereads the file; they expire after 15 minutes. If a confirmation answers a different command ID, input digest, executor, native ID or account than its preview, the entry endsunknownand the rest arenot-attempted. Each entry ends asadopted,skipped(the native session already belongs to a Session, found at preview or at a competing confirmation),refused(with the Node's reason, for example Codex withhostAuthority, D-198),unknown(the answer may have committed; the rest arenot-attempted; runmaia receipt show <command-id>before importing again) ornot-attempted. A rerun after success adopts nothing. The exit code is non-zero unless every entry isadoptedorskipped; then--jsonprintsok: falsewith the first failure'serror(adoption_refused, oroutcome_unknownfor anunknownentry) and the fullresultlisting every entry.session.adopt.previewanswers{ alreadyAdopted: { sessionId } }, with no preview ID and no change, when the native session already belongs to a Session;session adoptkeeps its refusal text. A confirmation that loses the claim to another adoption fails withalready_adopted(HTTP 409).- Planned (S11b, D-171): scans run on their own,
every origin but Maia's own is onboarded for its owner, an ended session is
claimed without
adopt, and a running one is observed through a managed hook.adoptstays the manual claim until then. - Previews expire after 15 minutes; repeating a confirmation with the same
preview ID replays its receipt. Repeating
adoptwith the same--command-idand input replays the open preview under a new preview ID (the Node keeps only a digest of each ID, so the earlier one stops working); the same command ID with other input, or after confirmation, fails withstorage_conflict, as does every repeat that overlaps an earlier one still replaying.historyreturns user and assistant text and one-line tool summaries, at most 50 messages of 4,000 characters per page; the Node keeps none of it.
Run ledger
The S19 Run ledger over executor Sessions (coordination).
… run ledger create --workspace <workspace-id> --graph-stdin [--command-id <id>]
… run ledger list --workspace <workspace-id> [--limit <1..100>] [--after <cursor>]
… run ledger task list --run <run-id> [--ready]
… run ledger task show <task-id> --run <run-id>
… run ledger task dispatch <task-id> --run <run-id> --session <session-id> --expected-task-revision <revision> --expected-session-revision <revision> [--command-id <id>]
… run ledger task settle <task-id> --run <run-id> --session <session-id> --outcome <succeeded|failed> --expected-task-revision <revision> --evidence-stdin [--turn <turn-id>] [--command-id <id>]
- One to one with
run.ledger.create,list,task.list,task.show,task.dispatchandtask.settle, also served as the MCP toolsrun_ledger_*. The graph on stdin is{ "tasks": [{ "clientKey", "dependsOn", "objective" }] }(64 KiB); evidence on stdin is 1 to 8192 bytes. dispatchqueues the Task's objective as a turn on a running Codex or Claude Code Session of the Run's Workspace and returns that turn;settleneeds the turn ended, andsucceededneeds it completed. A third failed dispatch stops the Task. Text output prints one line per Run and Task: state, revision, failed dispatches and the last dispatch asstate:session:turn.- Typed failures:
run_task_not_ready,run_task_not_activeandrun_turn_unsettled(retryable, exit 75); a changed revision is a retryable conflict.
Messages and Runs
Registered; S17 completes them. run create and its siblings are the
local-v1 terminal Run, separate from the Run ledger above. A launched Session
reaches the three session message and session inbox operations as the
worker MCP tools session_message_send, session_inbox_check and
session_inbox_ack (same parameters, no sender field: the Node names the
Session from its credential, and today the recipient must share the
sender's execution workspace (checkout); Project reach is planned,
D-166). The CLI leaves below stay the operator's path with --attached;
inside a Session they fail with role_denied.
… session message send --session <recipient-session-id> --attached --subject <text> --body-stdin [--command-id <id>]
… session inbox check --attached [--limit <1..16>]
… session inbox ack <delivery-id> --attached --expected-mailbox-revision <revision> [--command-id <id>]
… run create --workspace <workspace-id> --terminal <terminal-id> --graph-stdin [--command-id <id>]
… run list --workspace <workspace-id> [--limit <1..100>] [--after <cursor>]
… run show <run-id>
… run task list --run <run-id> [--limit <1..32>] [--after <cursor>]
… run task show <task-id> --run <run-id>
… run inbox check --run <run-id> --owner <run|dispatch> --owner-id <id> --coordinator-epoch 1 [--limit <1..16>]
… run inbox ack <delivery-id> --run <run-id> --owner <run|dispatch> --owner-id <id> --expected-mailbox-revision <revision> --coordinator-epoch 1 [--command-id <id>]
… run question open --run <run-id> --dispatch <dispatch-id> --subject <text> --body-stdin --expected-run-revision <revision> --expected-dispatch-revision <revision> --coordinator-epoch 1 [--command-id <id>]
… run question reply <question-id> --run <run-id> --dispatch <dispatch-id> --answer-stdin --expected-run-revision <revision> --expected-dispatch-revision <revision> --expected-question-revision <revision> --coordinator-epoch 1 [--command-id <id>]
… run question cancel <question-id> --run <run-id> --dispatch <dispatch-id> --expected-run-revision <revision> --expected-dispatch-revision <revision> --expected-question-revision <revision> --coordinator-epoch 1 [--command-id <id>]
… run dispatch show --run <run-id>
… run dispatch settle-terminal <dispatch-id> --run <run-id> --expected-run-revision <revision> --expected-dispatch-revision <revision> --coordinator-epoch 1 [--command-id <id>]
Dashboard
Source candidate, the renderer foundation of D-161 (D1a0). The command draws one static frame; the Tree it will show arrives in a later version.
… dashboard [--plain]
dashboardneeds a terminal on standard input and output, else it exits 64 and names--plain. It draws on the alternate screen a status line naming the CLI profile and one line saying the Tree arrives in a later version; a terminal below 80x24 gets one line stating that size instead.qand Ctrl-C quit with exit 0. It makes no Node request.--plainprints the same two lines once, reads no input and exits 0 with or without a terminal. With--jsonit prints{command, profile}.- Under
MAIA_SESSION_CREDENTIALit exits 77 before anything else, in every mode. - SIGINT, SIGTERM and SIGHUP end it with exit 130 after the terminal is
restored; SIGTSTP or Ctrl-Z restores it and sends SIGTSTP to its process
group, as the terminal's suspend key does. A shell with job control stops
the job, and once it continues the job the dashboard enters the screen again
and draws it whole. Where no shell has job control and the kernel drops the
stop, it enters again at once. A stop that nothing continues would strand
the dashboard, so in a Maia terminal pane, which sets
MAIA_TERMINAL_PANE=1(a flag only, never identity or authority), and on Windows Ctrl-Z and SIGTSTP do nothing. Raw mode turns the terminal's signal keys off, so the dashboard reads Ctrl-Z itself, in the legacy and Kitty keyboard encodings. The CLI records each terminal mode it enables (alternate screen, cursor, bracketed paste, keyboard protocol) and resets exactly those on every exit. A failure inside the renderer exits 70 with the terminal restored. A missing@earendil-works/pi-tuiexits 70 withnext: reinstall Maia.
Workspaces, Projects, views and questions
Candidates for R0, R1 and R1a.
… container list [--limit <1..100>] [--after <cursor>]
… container show <container-id> [--limit <1..100>] [--after <cursor>]
… container index list [--limit <1..500>] [--after <cursor>]
… container create (--input-stdin --dry-run | --confirm-stdin)
… container rename (--input-stdin --dry-run | --confirm-stdin)
… project create (--input-stdin --dry-run | --confirm-stdin)
… project rename (--input-stdin --dry-run | --confirm-stdin)
… reference resolve <connection|container|project|session|workspace> [<reference>] [--project <project-id>]
… workspace context show
… workspace membership attach <workspace-id> --project <project-id> --repository <repository-id> --expected-workspace-revision <revision> --expected-context-revision <revision> [--command-id <id>]
… workspace select <workspace-id> --project <project-id> --repository <repository-id> --expected-active-workspace-id <workspace-id> --expected-active-context-revision <revision> --expected-workspace-revision <revision> --expected-context-revision <revision> [--command-id <id>]
… context show [--attached]
… presentation catalog show
… presentation workspace-directory show --input-stdin
… presentation project-list show --input-stdin
… presentation session-list show --input-stdin
… session interaction create --input-stdin
… session interaction list --session <session-id> --project <project-id> [--status <pending|answered|cancelled|expired>] [--limit <1..100>] [--after <cursor>]
… session interaction show <interaction-id>
… session interaction respond --input-stdin
… session interaction cancel --input-stdin
reference resolve <kind> <reference>prints the exact ID of a name, ID prefix or title, or every candidate when more than one matches (exit 0); without a reference, aprojectorworkspacekind prints the current folder's default (the selected Workspace's when no registered folder contains it).--projectnarrows a Session or Workspace reference. JSON returns the Node'sreference.resolveanswer.containerandprojectcreate and rename are two-phase leaves. They take strict JSON on stdin (8 KiB, 32 chunks), so they never prompt:--input-stdin --dry-runpreviews and--confirm-stdinconfirms the preview piped back, which carries itsinputDigestandproof. For examplemaia --json project create --input-stdin --dry-run < in.json | maia project create --confirm-stdin. Inputs and rules: containers, Project metadata.container index listpages every container with its Projects and each Project's repository path;--limitcounts rows (a Project, or a container with none) and a container a page cuts continues on the next page. Text prints one line per container with its Project count, then one indented line per Project.presentationreads return catalog-bound view specifications (presentation runtime).session interactionrecords an external question and its answer (128 KiB, 32 chunks per input); an answer is data, never approval to act. A question or selection attaches to an idle Session and a person answers it. Inputs are strict JSON on stdin. After an uncertain result, reuse the same command ID and reconcile withmaia receipt show. Shape in Interaction and response and the interaction contract.
Execution environments and terminals
… workspace execution show <workspace-id>
… workspace execution set <workspace-id> (--execution-profile <profile-id> | --policy <policy-id>) --expected-revision <revision> [--command-id <id>]
… workspace execution clear <workspace-id> --expected-revision <revision> [--command-id <id>]
… execution catalog show
… execution command run <instance-id> --expected-revision <revision> [--command-id <id>] -- <absolute-executable> [arguments...]
… execution file read <instance-id> <relative-path> --expected-revision <revision> [--command-id <id>]
… execution checkpoint create --instance <instance-id> --expected-instance-revision <revision> [--command-id <id>]
… execution checkpoint show <checkpoint-id>
… execution worktree create --project <project-id> --repository <repository-id> --base <base-revision> --expected-project-revision <revision> [--command-id <id>]
… execution worktree import <root-path> --project <project-id> --repository <repository-id> --expected-project-revision <revision> [--command-id <id>]
… execution worktree status <workspace-id>
… execution worktree reconcile <operation-id> --expected-operation-revision <revision> [--command-id <id>]
… execution worktree remove plan <workspace-id>
… execution worktree remove apply <workspace-id> --plan <plan-id> --plan-digest <digest> --expected-workspace-revision <revision> [--command-id <id>]
… execution placement create (--session <session-id> --expected-session-revision <revision> | --attempt <attempt-id> --run <run-id> --task <task-id> --expected-attempt-revision <revision>) --instance <instance-id> --expected-workspace-revision <revision> --expected-instance-revision <revision> [--command-id <id>]
… execution placement show (--session <session-id> | --attempt <attempt-id>)
… execution relocation plan <session-id> --expected-session-revision <revision> --expected-placement-revision <revision> --expected-epoch <epoch>
… execution relocation apply <session-id> --plan <plan-id> --plan-digest <digest> --expected-session-revision <revision> --expected-placement-revision <revision> --expected-epoch <epoch> [--command-id <id>]
… execution relocation show <relocation-id>
… execution relocation reconcile <relocation-id> --expected-operation-revision <revision> [--command-id <id>]
… terminal create --workspace <workspace-id> [--interactive --columns <2..500> --rows <2..300>] [--command-id <id>] [-- <program> [arguments...]]
… terminal list --workspace <workspace-id> [--limit <1..100>] [--after <cursor>]
… terminal show <terminal-id>
… terminal read <terminal-id> [--limit <1..8>] [--after <chunk-cursor>]
… terminal events list --terminal <terminal-id> [--limit <1..32>] [--after <terminal-cursor>]
… terminal controller request create --terminal <terminal-id> --terminal-generation <1..65535> --lease-ms <1000..30000> [--command-id <id>]
… terminal controller request show <controller-request-id> --terminal <terminal-id> --terminal-generation <1..65535>
… terminal controller acquire <terminal-id> --request <controller-request-id> --terminal-generation <1..65535> --expected-revision <revision> [--command-id <id>]
An interactive terminal runs on macOS and Linux, in the Workspace's checkout: the Project's root, or its worktree. The Node's terminal list decides what it starts:
- Shell. The account's login shell must be on the list (
/bin/bash,/bin/zsh,/usr/bin/bash,/usr/bin/zsh) and present on the host. It starts as<shell> -l, so it reads its login profile.SHELLnames it. - Program. A name after
--runs in place of the shell:claudeorcodex. The login shell starts it, as below, so the profile and rc files setPATHfirst. Each argument stays one argument; no shell parses it. Every token after--belongs to the program,--jsonincluded, so put--jsonbefore--. - Anything else is refused, and nothing starts:
- an unlisted program fails with
terminal_program_not_admitted, exit 77, before the Node records anything; - an unlisted login shell fails the same way, once the Node has recorded the request;
- a name that is not a plain lowercase command name, such as
/bin/shorCodex, fails validation, exit 64.
- an unlisted program fails with
- No terminal on this Node. When the Node records the Terminal as
unavailable (
terminal_containment_unsupported, orplatform_unsupportedoff macOS and Linux),terminal createfails with that code,ok: false, exit 69. The message names the Terminal and its receipt.
For example, this opens Codex in a Workspace, and the Node starts the second line:
maia --json terminal create --workspace <workspace-id> --interactive -- codex --model <model>
<shell> -l -i -c 'exec "$0" "$@"' codex --model <model>
terminal read pages output the Node holds in memory and never writes to
SQLite (D-175). The buffer keeps at most 8 MiB and
50,000 lines per terminal, and 128 MiB and 131,072 chunks across terminals,
and drops the oldest chunks first. A Node restart discards it. When output
after --after is gone, evicted or lost with a restart, the page holds what
is left and carries gap (reason: cursor_expired,
recovery: resync-required, oldestAvailableCursor); human output leads
with a gap row. A cursor past the head fails with storage_conflict.
Cursors keep counting across a restart: each terminal generation numbers its
chunks in its own cursor range, so a cursor from before a restart gets the
gap, never storage_conflict, and never repeats. The upgrade that removed
stored output also deletes it from every migration-backups/ file and
rewrites them, keeping the rest so a rollback still works. Planned: when
terminal.session.delete ships (it is unsupported today), delete frees the
terminal's output buffer entry, and a test proves its bytes and per-terminal
state are gone.
The remaining terminal mutations each require the controller fence
--controller-id <id> --controller-epoch <epoch> --controller-revision <revision> --controller-expires-at <timestamp> --terminal-generation <1..65535> --expected-revision <revision> [--command-id <id>]:
… terminal controller release <terminal-id> <fence>
… terminal input write <terminal-id> (--input-stdin | [--text <text>] [--keys <names>]) --input-id <id> --input-sequence <1..2147483647> <fence>
… terminal resize <terminal-id> --columns <2..500> --rows <2..300> <fence>
… terminal signal <terminal-id> --signal <hangup|interrupt|terminate> <fence>
… terminal close <terminal-id> <fence>
terminal input write takes its bytes from --input-stdin, or from typed
input: --text <text> (literal UTF-8) and --keys "<name> ..."
(whitespace-separated key names), each at most once, sent in the order they
appear on the command line, and never with --input-stdin. Example:
--text "npm test" --keys Enter sends npm test, then CR;
--keys Enter --text abc sends CR, then abc. The CLI turns the
keys into bytes; the Node sees the same terminal.input.write as before. Key
names are case-sensitive:
Enter(CR),Tab,Escape,Backspace(DEL),Space,Delete,Insert;Up,Down,Left,Right,Home,End,PageUp,PageDown, sent as normal-mode xterm sequences (ESC [ A); a program in application cursor mode needs--input-stdin;F1..F12;C-a..C-z(the control byte0x01..0x1a);M-<char>(ESC, then the character).
An unknown name, or more than 32768 bytes in all, exits 64. For text that
interleaves text, keys and text again, or any other byte, use --input-stdin.
Planned command families
Each family lands through protocol → SDK → Node → CLI → MCP with its slice. The owner document holds the design; this table is the index.
| Slice | Commands | Design |
|---|---|---|
| S12 | session attach <session-id>: live renderer; /detach closes only this client | Live Session item stream |
| S13 | config.resolve, config.plan, config.apply, config.drift, config.import | Harness configuration (decision) |
| S14 | node install <host> over SSH | ROADMAP S14 |
| S15 | port lease list attribution of listening sockets to Sessions (--port <n>, --unleased); host and agent metrics | Shared resources |
| S16 | lock kind (wave 2): lock acquire <key> [--ttl] [--reason] [--wait], lock renew, lock release, lock list | Shared resources (decision) |
| S17 | enables the registered session message send, session inbox, run inbox above | Safety (decision) |
| S20 | hold and release of one exact Project, Connection, Node or capability; capability.revoke | Kill switches (decision) |
| S21 | process start, process list, process logs, process stop over execution.process.* | ROADMAP S21 |
| TM7 | terminal input write and terminal read for an agent's own terminals, and a bounded terminal wait (a pattern or the exit) | D-175 |
| TM4 | terminal attach <terminal-id>: the Rust client, one full-screen pane | D-175 |
| TM5–TM6 | the multiplexer: panes, tabs, and Workspace and Project views | ROADMAP TM |
| TM9 | routine run <id>; node rename, node disable, node enable | ROADMAP TM |
| R1 | --schema[=input|output] on registered commands | Schema-only inspection |
| R1, S12 | observe reads and streams (below) | Live observability contract |
| R8 | routine (below) | Routine model |
| R10a | evaluation operations; names are frozen with protocol, SDK, CLI and agent parity | Shared semantic evaluation |
| R11 | Agent launch flags, interactive commands and model tools | Maia Agent surface reference |
| MN0–MN5 | Execution Node enrollment, drain and handoff (below); MN0 freezes the bounds and the wire schemas; each later slice freezes its own startup mode, operation IDs, grammar and fixtures | ROADMAP MN0–MN5 |
| T2 | telemetry status|query|level|purge; export in T7 | Local telemetry (decision) |
| RL2 | node.hello handshake before any other operation | Compatibility handshake (decision) |
| RL3 | remote install <host> | Install |
| RL4 | update [--version], node update --host <host> | Updates |
| Later | a2a (below), after S17 | A2A boundary (decision) |
node backup|restore (below) and the maintainer
contract generate|check|fixtures verify
(contracts) are target grammar
without a roadmap slice yet.
Live observability
Planned: R1 bounded reads and S12 streams over the
observation plane. Today
session observe and session events list are the implemented reads;
node monitor prints a monitor URL.
maia observe health [--watch]
maia observe feed [--domain <agent|swarm|node|provider|voice|a2a|security|storage|scheduler>] [--importance <authoritative|diagnostic>] [--project <id>] [--session <id>] [--event-type <type>] [--include-content] [--after <cursor>] [--follow]
maia observe sessions [--project <id>] [--state <state>] [--watch]
maia observe session <session-id> [--after <cursor>] [--follow]
maia observe activations [--session <id>] [--state <state>] [--watch]
maia observe effects [--project <id>] [--state <state>] [--watch]
maia observe routines [--project <id>] [--watch]
maia observe queue [--project <id>] [--watch]
maia observe nodes [--watch]
maia observe providers [--connection <id>] [--watch]
maia observe voice [--session <id>] [--watch]
maia observe a2a [--project <id>] [--watch]
maia observe pipeline [--processor <id>] [--partition <0..31>] [--watch]
maia observe trace verify [--project <id>] [--deep]
--jsonreturns one bounded snapshot.--watchand--followstream with--after,--heartbeat-ms <1000..30000>and--max-buffered <1..1000>, using the frames and cursors in contracts. A retention gap returnsevent_cursor_expiredwith a fresh snapshot cursor; exceeding--max-bufferedends the stream withstream_buffer_exceeded. Records are never dropped silently.observe feedis the one cross-domain stream. Filters intersect with the caller's grants and owner discovery ceiling; content is redacted unless--include-contentis used with a separate content grant. Clients use one aggregate feed, not one connection per Session.observe pipelineis operator-only and exposes partitions, checkpoints, lag, gaps and subscribers without payload content.observe sessionis the causal turn timeline used by terminal, Desktop and mobile views (below).
Live Session timeline
maia observe session <id> --json returns the Session, its current activation
and attempt, a bounded causal timeline, an evidence watermark and coverage.
The timeline always carries lifecycle and correlation evidence, so a renderer
can diagnose a stuck turn without reading private content; content grants may
add redacted previews or artifact references. Planned data:
{
"session": {
"agentSessionId": "session_01...",
"lifecycleStatus": "running",
"revision": 13
},
"activation": {
"activationId": "activation_01...",
"state": "running",
"reservationId": "reservation_01..."
},
"attempt": {
"attemptId": "attempt_01...",
"number": 1,
"nodeId": "node_01...",
"workerId": "worker_01...",
"assignmentEpoch": 4,
"lastHeartbeatAt": "2026-09-01T12:00:04.000Z"
},
"timeline": [
{
"kind": "message.accepted",
"eventId": "event_01...",
"messageId": "message_01...",
"ownerSequence": 249,
"correlationId": "correlation_01...",
"occurredAt": "2026-09-01T12:00:00.000Z"
},
{
"kind": "tool.completed",
"eventId": "event_02...",
"toolCallId": "tool-call_01...",
"operationId": "workspace.file.read",
"effectId": null,
"receiptId": null,
"durationMs": 18,
"ownerSequence": 250,
"occurredAt": "2026-09-01T12:00:01.000Z"
}
],
"evidenceWatermark": {
"eventSequence": 250,
"jsonlProjectedSequence": 250,
"lagRecords": 0,
"state": "live"
},
"coverage": { "requiredEdges": 8, "observedEdges": 8, "missing": [] }
}
Backup and restore
No roadmap slice yet; rules in operations.
maia node backup preview
maia node backup create <preview-id> --confirmation-token-stdin --command-id <id>
maia node backup list [--limit <1..100>] [--after <cursor>]
maia node backup verify <backup-id-or-path>
maia node restore verify <backup-path>
maia node restore apply <restore-preview-id> --confirmation-token-stdin --command-id <id>
Routines, Execution Nodes and A2A
R8 Routines. S30 ships the schedule commands (Scheduled Sessions); the rest stays Planned with S50:
maia routine validate <routine-ref-or-path> [--project <project-id>]
maia routine trigger <routine-ref> --project <project-id> --expected-revision <n> [--command-id <id>]
maia routine ticks <routine-ref> --project <project-id> [--state <state>] [--limit <1..100>] [--after <cursor>]
maia routine reconcile <tick-id> --expected-revision <n> --command-id <id>
MN1–MN4 Execution Nodes (draft spelling; requires verified owner or admin authority and a separate machine grant, not the local bearer):
maia node list [--state <state>] [--limit <1..100>] [--after <cursor>]
maia node enroll preview --pairing-code-stdin --node-evidence-stdin
maia node enroll apply <preview-id> --confirmation-token-stdin --command-id <id>
maia node drain <node-id> --expected-revision <n> --command-id <id>
maia node handoff preview <session-id> --to-node <node-id> --expected-assignment-revision <n>
maia node handoff apply <preview-id> --confirmation-token-stdin --command-id <id>
The first remote qualification is read-only; remote coding waits for a qualified containment adapter, not CLI placement.
A2A peers and tasks (after S17). Each accepted A2A task maps to one durable Maia task with both identities kept; peers get no lease or mutation authority (decision):
maia a2a peer list [--project <id>] [--limit <1..100>] [--after <cursor>]
maia a2a peer enroll --project <id> --agent-card <https-url> --policy-stdin --expected-project-revision <n> --command-id <id>
maia a2a peer revoke <peer-id> --expected-revision <n> --command-id <id>
maia a2a task list [--project <id>] [--state <state>] [--limit <1..100>] [--after <cursor>]
maia a2a task show <a2a-task-id>
maia a2a task reconcile <a2a-task-id> --expected-revision <n> --command-id <id>
Cross-owner coordinator work uses parent-mediated delegation only; there is no peer bridge command (decision).
Maia Agent surface reference
maia agent is Pi rebranded for Maia: standalone Pi interaction and extension
semantics, with Maia owning the command prefix, state namespace, safety
boundary and durable services (decision).
Pi extensions are the only Agent plugin model: one extension can register
tools, slash commands, flags, shortcuts, renderers, providers and lifecycle
hooks.
The Agent has three separate surfaces:
| Surface | Caller | Syntax | Contract owner |
|---|---|---|---|
| process launch | shell, user, process supervisor | maia agent ... flags | CLI parser and activation schema |
| interactive command | person inside an Agent session | /command ... | command registry projecting protocol or UI operations |
| model tool | LLM only | JSON Schema tool call | protocol tool-action registry |
A launch flag never becomes a model tool, a slash command never enters the model's tool schema, and typing raw tool JSON never calls a tool.
Agent launch flags
Observed Maia Agent implementation today
maia agent [--continue] [-e|--extension <path>] [-ne|--no-extensions] [--model <provider/model>] [--thinking <level>] [extension flags] [--] [prompt]
maia agent --print [--no-session] [-e|--extension <path>] [-ne|--no-extensions] [--model <provider/model>] [--thinking <level>] [extension flags] [--] <prompt>
maia agent --json-events [--no-session] [-e|--extension <path>] [-ne|--no-extensions] [--model <provider/model>] [--thinking <level>] [extension flags] [--] <prompt>
maia agent extension digest <path>
maia agent login
Short forms: -c (--continue), -p (--print), -h, -v. --verbose
is also accepted.
-ne/--no-extensionsskips discovered executable extensions without resolving configured Project extension paths; explicit-epaths still load, and reviewed Maia kernel extensions, Skills andAGENTS.mdstay.- Loaded extensions run in Pi's native runtime: their tools join the model
tool list, their slash commands join the interactive registry, and their
long flags reach the extension. Explicit paths resolve against the launch
directory; Project-discovered extensions are digest-pinned. Maia never reads
~/.pior calls an installedpi. - Model tools today: Pi's built-in
read,bash,editandwrite(Pi's default active set; itsgrep,findandlsare not active) and Maia'stask,goalandprocessextension tools. Theirdetailsobjects are ad-hoc, not the target contract below. A session that activatesfindorgrepgets 80 hits by default and at most 200, and above 80 only with apathorglob. Known gap, observed through the runtime entry in a test home: when they run, Pi looks forfdandrgin~/.pi/agent/binbeforePATH, because Pi fixes that directory when it loads, before Maia binds its agent directory (R11 AG1). - Slash commands today, standalone agent only:
/goal [status|show],/goal <objective>or/goal rewrite <objective>,/goal pause|resume|done|abandon, and/tasks [open|all], which changes nothing. They act on the same Session ledgers as thegoalandtasktools without a revision check./goal rawand/goal clearare rejected, and a verb with extra words, such as/goal pause the deploy, is refused rather than read as a new goal. Pi's native commands apply. The managed worker registers no slash command.
Target launch flags (R11 AG1)
The standalone Pi launch grammar under the maia agent prefix, adding Maia
flags only where Pi has none.
maia agent
[--provider <provider>] [--model <model>] [--thinking <level>]
[--continue | --resume | --session <id-or-path>]
[--print | --mode <text|json|rpc>] [--no-session]
[--tools <names> | --no-tools | --no-builtin-tools]
[-e | --extension <path>] [-ne | --no-extensions]
[--skill <path>] [--prompt-template <path>] [--theme <path>]
[extension-defined flags] [@files...] [messages...]
Local mode is an in-process Session whose files are not Node receipts. Durable
work uses Node Sessions and S12 session attach. --tools can narrow active
tools but never grants an operation that policy denied.
Interactive user commands: human only
Planned (R11). With --json, an operation command returns the
target envelope with meta.surface set to
agent-command. A local UI command never mutates the
Node or the workspace and returns maia.agent-command.v1 JSON without a
receipt. A command that needs a mutation must project a registered operation.
| Command | Kind | Operation or data |
|---|---|---|
/help [command] [--json] | local UI | command names, usage, availability, schema digests |
/status [--json] | operation | local activation or status.show projection |
/session [--json] | operation/read | exact Session identity, mode, revision, usage, context digest |
/model list [--json] | operation/read | model.list bounded catalog |
/model use <connection-id> <model-id> [--json] | local narrowing | allowed model for the next local activation; no operation ID |
/thinking <level> [--json] | local activation | effective thinking level and activation revision |
/login <provider> --name <name> [--json] | operation | connection.login; secrets stay outside command output |
/logout <connection-id> [--json] | operation | connection.logout, confirmed when required |
/connection list|show|test [<id>] [--json] | operation/read | redacted connection.* projection |
/tools [--json] | local activation | registered tools, actions, source, grant, availability, schema digest |
/context inspect|lint [path], /context explain <source-id> | operation/read | context.* manifest, diagnostics or source metadata |
/skills [--json] | operation/read | eligible Skill metadata only |
/skill load <skill-ref> [--json] | operation/local context | exact Skill digest, measurements, next-turn disposition |
/task <create,list,get,update,archive-completed> … [--json] | operation | session.task.* result or receipt |
/goal <show,rewrite,update,verify,pause,resume,done,abandon> … [--json] | operation | session.goal.* result or receipt |
/process <start,list,output,write,stop,clear> … [--json] | operation | execution.process.* result; absent without an execution profile |
/agent <start,list,inspect,send,wait,stop> … [--json] | operation | session.* result or receipt |
/inbox <list,read> … [--json] | operation/read | bounded Session messages with thread, reply and delivery fields |
/interaction <list,show,respond,cancel> … [--json] | operation | pending questions; respond answers questions only |
/mcp <status,search,describe> … [--json] | operation/read | allowlisted MCP metadata; configuration stays outside the Session |
/memory <list,search,get> … [--json] | operation/read | authorized bounded memory references or content |
/doctor <check,advise> … [--json] | operation | deterministic diagnostics or maintenance proposals |
/trace <tail,verify> … [--json] | operation/read | authorized redacted trace records or integrity |
/compact [focus] [--json] | operation/local context | compaction digest, retained range, token and byte measurements |
/reload [--json] | operation/preview | candidate config/context snapshot and diagnostics; never silent admission |
/interrupt [--json] | operation | cancellation request or acknowledgement, never invented completion |
/copy | local UI | copies the latest allowed assistant text |
/detach | attached renderer only | session.attachment.close (S12); the durable Session continues |
/export <path> [--json] | operation | redacted Session export manifest and receipt; create-new only |
/new | local UI | replaces the local Session with a fresh local identity |
/quit | local UI | ends the process after bounded cleanup |
Commands do not mirror every model action: the model can call web_search
while a person asks in prose, and provider or MCP configuration stays in the
outer CLI. Native Pi and extension slash commands remain available locally.
Interactive commands use the attached or local Session implicitly and never take an actor, Project, workspace, Connection secret or attachment ID from model text. Mutating forms derive a command ID when omitted and use the observed revision; a stale revision is shown, never retried silently.
/task create --subject <text> [--description-stdin] [--active-form <text>]
/task list [--status <status>]
/task get <task-id>
/task update <task-id> [--subject <text>] [--description-stdin] [--active-form <text>] [--status <status>] --expected-revision <n>
/task archive-completed --expected-ledger-revision <n>
/goal show
/goal rewrite --objective-stdin [--next-stdin] --expected-revision <n|null>
/goal update [--next-stdin] [--success-stdin] [--constraints-stdin] --expected-revision <n>
/goal verify --evidence-stdin --expected-revision <n>
/goal pause|resume|done|abandon --expected-revision <n>
/process start --argv-stdin [--cwd <relative-path>] [--stdin <pipe|closed>]
/process list
/process output <process-id> [--after <cursor>]
/process write <process-id> --input-stdin --expected-revision <n>
/process stop <process-id> --expected-revision <n>
/process clear <process-id> --expected-revision <n>
/agent start <agent-ref> --instruction-stdin [--max-wall-time-ms <n>] [--max-tokens <n>] [--max-output-bytes <n>]
/agent list [--relation <child|parent|peer>]
/agent inspect <session-id>
/agent send <session-id> --behavior <ask-question|assign-work|reply|report-progress> [--thread <id>] [--reply-to <message-id>] --message-stdin --expected-revision <n>
/agent wait <session-id>... [--after-revision <n>] [--timeout-ms <n>]
/agent stop <session-id> --expected-revision <n>
/interaction list [--state <pending|answered|cancelled|expired>]
/interaction show <interaction-id>
/interaction respond <interaction-id> --answer-stdin --expected-revision <n>
/interaction cancel <interaction-id> --expected-revision <n>
/inbox list [--thread <id>] [--state <state>]
/inbox read <message-id>
/compact [--focus-stdin]
/reload
/interrupt
/export <path>
Text fields are at most 16 KiB; argv has 1–128 elements and 32 KiB in total;
lists return at most 100 items. /reload only compiles a candidate and shows
its diff; admission is a separate previewed operation. /compact writes a
compaction record with source range, retained summary digest,
model/tokenizer, token counts, trigger and resulting context digest.
Model-tool envelope
Planned (R11). Pi requires content blocks for the model; Maia puts the
canonical result in details. The text block is a short summary; automation
reads details.
{
"content": [{ "type": "text", "text": "Session session_01... started." }],
"details": {
"schemaVersion": "maia.tool-result.v1",
"requestId": "req_01...",
"operationId": "session.start",
"status": "accepted",
"data": { "agentSession": {} },
"meta": { "pagination": null, "warnings": [] },
"interaction": null,
"receipt": {},
"error": null
}
}
type ToolResultV1 =
| {
schemaVersion: "maia.tool-result.v1";
requestId: string;
operationId: string;
status: "ok" | "accepted" | "running";
data: unknown;
meta: ToolResultMetaV1;
interaction: null;
receipt: ReceiptV1 | null;
error: null;
}
| {
schemaVersion: "maia.tool-result.v1";
requestId: string;
operationId: string;
status: "blocked";
data: null;
meta: ToolResultMetaV1;
interaction: {
interactionId: string;
kind: "confirmation" | "question" | "authority" | "outcome-unknown";
expiresAt: string | null;
resumeOperationId: string | null;
};
receipt: ReceiptV1 | null;
error: null;
}
| {
schemaVersion: "maia.tool-result.v1";
requestId: string;
operationId: string;
status: "error";
data: null;
meta: ToolResultMetaV1;
interaction: null;
receipt: ReceiptV1 | null;
error: MaiaErrorV1;
};
interface ToolResultMetaV1 {
readonly pagination: {
readonly limit: number;
readonly nextCursor: string | null;
readonly hasMore: boolean;
} | null;
readonly warnings: ReadonlyArray<string>;
}
Each operation replaces unknown with its output schema. blocked never
overloads error and always names how or whether the operation resumes.
Pagination lives only in meta.pagination. ReceiptV1 is the
receipt contract.
Error example:
{
"schemaVersion": "maia.tool-result.v1",
"requestId": "req_edit_conflict",
"operationId": "workspace.file.edit",
"status": "error",
"data": null,
"meta": { "pagination": null, "warnings": [] },
"interaction": null,
"receipt": null,
"error": {
"code": "content_digest_mismatch",
"message": "The file changed after the edit was prepared.",
"retryable": false,
"details": {
"path": "src/a.ts",
"expectedDigest": "sha256:old",
"observedDigest": "sha256:new"
}
}
}
Error details has a schema per code. Raw provider, MCP and web error bodies
are never serialized.
Model-tool catalogue: LLM only
Planned (R11 AG2–AG3). Names, parameters and error codes follow D-172. Only tools granted to the exact activation are registered; optional adapters are absent by default. Tool schemas and descriptions are stable within an activation and included in its context manifest digest.
Common rules:
- Mutations require
idempotencyKey; compare-and-set mutations also requireexpectedRevisionor an exact content digest. Error and blocked results never encode success in text. - Paths are workspace-relative unless the activation grants an absolute root.
- Limits below are protocol maxima; Agent, Project or operator policy may lower them.
- One action maps to exactly one operation, except
maia__tool, which the Node routes by the target tool's admitted risk class; there are no wildcard operation IDs. Each row generates closed<operation-id>.input.v1and<operation-id>.output.v1schemas. - Authorization scope is the operation ID plus the activation's Project, Session, workspace grant and any exact target or Connection ID in the input. No row allows an omitted cross-Project target.
readoperations have no receipt; amutationrequires idempotency, emits the named event and returns a receipt.- Availability is
core,execution-profile,web-adapter,mcp-adapter,memory-adapterormaintenance-agent.
Error sets:
R:invalid_input,capability_unavailable,unauthorized_scope,hard_limit_exceeded,timeout,interrupted,integrity_failure,internal_error.C:Rplusrevision_conflictandidempotency_conflict.F:Cpluscontent_digest_mismatchandpath_outside_workspace.E:Cplusexternal_unavailableandoutcome_unknown.X:Rplusexternal_unavailableandoutcome_unknown, for external reads.
| Tool action | Operation ID | Complete input | Success data | Contract: owner; risk; errors; event; availability |
|---|---|---|---|---|
read_file | workspace.file.read | path; offset? integer at least 0; limit? 1..2000 | path, content or artifactRef, startLine, endLine, truncated, digest | workspace; read; R; none; core |
write_file | workspace.file.write | create mode: mode=create, path, content at most 1 MiB, idempotencyKey; replace mode: same plus mode=replace, expectedDigest | path, created, byteSize, priorDigest, resultDigest, revision | workspace; mutation; F; workspace.file.written; core |
edit_file | workspace.file.edit | path; edits 1..128 entries with oldText, newText, replaceAll?; expectedDigest; idempotencyKey | path, appliedEdits, diff, firstChangedLine, priorDigest, resultDigest, revision | workspace; mutation; F; workspace.file.edited; core |
grep | workspace.search.grep | pattern; path? default .; glob?; ignoreCase?; literal?; context? 0..20; limit? 1..200, default 50; outputMode? files_with_matches (default), content or count | file paths for files_with_matches, path/line/column/preview matches for content, or match counts for count; truncated, matchLimitReached | workspace; read; R; none; core |
glob | workspace.search.find | pattern; path? default .; limit? 1..500 | paths, truncated, resultLimitReached | workspace; read; R; none; core |
list_directory | workspace.directory.list | path? default .; limit? 1..500; cursor? opaque | entries with name/path/kind, truncated; pagination in meta | workspace; read; R; none; core |
run_command | execution.shell.run | argv 1..128 strings; cwd?; timeoutMs? 1..600000; maxOutputBytes? 1024..1048576; idempotencyKey | executionId, exitCode, signal, stdout, stderr, truncated, durationMs | execution; mutation; E; execution.shell.completed; execution-profile |
start_process | execution.process.start | argv 1..128 strings; cwd?; name? at most 120; timeoutMs? 1..600000; maxOutputBytes? 1024..1048576; expectedSessionRevision; idempotencyKey | process with ID/name/lifecycle/revision/outputCursor | execution; mutation; E; execution.process.started; execution-profile |
list_processes | execution.process.list | lifecycleStatus? from starting/running/exited/stopped/failed/outcome-unknown; limit? 1..100; cursor? opaque | processes; pagination in meta | execution; read; R; none; execution-profile |
read_process_output | execution.process.output | processId; afterCursor? opaque; limit? 1..200 chunks; maxBytes? 1024..1048576 | processId, chunks, nextOutputCursor, truncated, revision | execution; read; R; none; execution-profile |
write_process_input | execution.process.write | processId; input at most 64 KiB; end?; expectedRevision; idempotencyKey | processId, acceptedBytes, inputClosed, revision | execution; mutation; E; execution.process.input-written; execution-profile |
stop_process | execution.process.stop | processId; expectedRevision; idempotencyKey | processId, lifecycleStatus, revision | execution; mutation; E; execution.process.stop-requested; execution-profile |
clear_process | execution.process.clear | processId; expectedRevision; idempotencyKey | processId, clearedThroughCursor, revision | execution; mutation; C; execution.process.output-cleared; execution-profile |
create_task | session.task.create | subject at most 240; description? at most 4096; activeForm? at most 160; expectedLedgerRevision; idempotencyKey | task, ledgerRevision | Session work; mutation; C; session.task.created; core |
list_tasks | session.task.list | lifecycleStatus?; includeArchived?; limit? 1..100; cursor? opaque | agentSessionId, tasks, ledgerRevision; pagination in meta | Session work; read; R; none; core |
get_task | session.task.get | taskId | task, ledgerRevision | Session work; read; R; none; core |
update_task | session.task.update | taskId; one or more of subject?, description?, activeForm?, lifecycleStatus? from pending/in-progress/blocked/completed/cancelled; expectedRevision; expectedLedgerRevision; idempotencyKey | task, ledgerRevision | Session work; mutation; C; session.task.updated; core |
archive_completed_tasks | session.task.archive-completed | expectedLedgerRevision; idempotencyKey | archivedTaskIds, ledgerRevision | Session work; mutation; C; session.tasks.archived; core |
show_goal | session.goal.show | no fields | goal or lifecycleStatus=none | Session goal; read; R; none; core |
rewrite_goal | session.goal.rewrite | objective at most 2048; next? at most 1024; success? up to 16 strings; constraints? up to 16 strings; expectedRevision number or null; idempotencyKey | goal | Session goal; mutation; C; session.goal.rewritten; core |
update_goal | session.goal.update | one or more of objective?, next?, success?, constraints?; expectedRevision; idempotencyKey | goal | Session goal; mutation; C; session.goal.updated; core |
verify_goal | session.goal.verify | evidenceRefs 1..32 strings; expectedRevision; idempotencyKey | goal, verification | Session goal; mutation; C; session.goal.verified; core |
pause_goal | session.goal.pause | expectedRevision; idempotencyKey | goal | Session goal; mutation; C; session.goal.paused; core |
resume_goal | session.goal.resume | expectedRevision; idempotencyKey | goal | Session goal; mutation; C; session.goal.resumed; core |
complete_goal | session.goal.done | expectedRevision; idempotencyKey | goal | Session goal; mutation; C; session.goal.completed; core |
abandon_goal | session.goal.abandon | expectedRevision; idempotencyKey | goal | Session goal; mutation; C; session.goal.abandoned; core |
start_agent | session.start | agentRef; instruction at most 16 KiB; expectedParentRevision; expectedTreeRevision; narrowing limits? with maxWallTimeMs 1..1800000, maxTokens 1..1000000, maxOutputBytes 1024..1048576; exact resultContractRef?; idempotencyKey | agentSession | Session; mutation; E; session.created; core |
list_agents | session.list | relation? one of child/parent/peer; lifecycleStatus?; limit? 1..100; cursor? opaque | agentSessions; pagination in meta | Session; read; R; none; core |
inspect_agent | session.show | agentSessionId | agentSession | Session; read; R; none; core |
message_agent | session.send | agentSessionId; behavior one of ask_question, assign_work, reply, or report_progress; threadId?; replyTo?; message at most 16 KiB; expectedRevision; idempotencyKey | messageId, agentSessionId, acceptedRevision, wakeDisposition | Session; mutation; E; session.message.accepted; core |
wait_for_agent | session.wait | agentSessionIds 1..8; afterRevisions? exact ID-to-revision map; timeoutMs? 0..120000 | changed, timedOut, observedAt | Session; read; R; none; core |
stop_agent | session.stop | agentSessionId; expectedRevision; idempotencyKey | agentSessionId, lifecycleStatus, revision | Session; mutation; E; session.stop-requested; core |
ask_question | interaction.create | kind one of question/selection; subject at most 240; body at most 4096; options? 2..8 entries with stable ID/label; answerSchemaRef?; expiresAt?; expectedSessionRevision; idempotencyKey | blocked result with interaction | interaction; mutation; C; interaction.created; core |
show_interaction | interaction.show | interactionId | interaction without secret proof | interaction; read; R; none; core |
respond_to_interaction | interaction.respond | interactionId; typed answer; wakeDisposition one of resume-waiting-session/record-only; expectedRevision; idempotencyKey | interaction, responseDigest, wakeDisposition | interaction; mutation; C; interaction.answered; core |
cancel_interaction | interaction.cancel | interactionId; expectedRevision; idempotencyKey | interactionId, lifecycleStatus, revision | interaction; mutation; C; interaction.cancelled; core |
inspect_context | context.inspect | pathScope? | contextManifest | context compiler; read; R; none; core |
lint_context | context.lint | pathScope? | diagnostics, valid, contextManifestDigest | context compiler; read; R; none; core |
explain_context | context.explain | sourceId | source metadata, measurements, disposition | context compiler; read; R; none; core |
show_context_usage | context.usage | no fields | contextManifestDigest, bytes, estimatedTokens, hardLimitBytes, sources | context compiler; read; R; none; core |
list_skills | skill.list | eligibleOnly? true; limit? 1..100; cursor? opaque | Skill metadata only; pagination in meta | Skill loader; read; R; none; core |
maia__skill | skill.load | name (the qualified Skill identity); pathScope?; expectedActivationRevision; idempotencyKey | skillRef, digest, byteSize, loadDisposition, activationRevision | Skill loader; mutation; C; context.skill-selected; core |
show_skill_status | skill.status | skillRef | Skill metadata, eligibility, digest, measurements | Skill loader; read; R; none; core |
web_search | integration.web.search | queries 1..4; mode? one of search/answer; limitPerQuery? 1..10; allowedDomains?; blockedDomains?; publishedAfter?; publishedBefore? | results with fetchable URLs, truncated, citations | web adapter; external read; X; integration.web.observed; web-adapter |
web_fetch | integration.web.fetch | url; maxBytes? 1024..262144 | one document with URL, title, content or artifact, digest and truncated; citations | web adapter; external read; X; integration.web.observed; web-adapter |
show_mcp_status | integration.mcp.status | no fields | bounded server statuses | MCP adapter; read; R; none; mcp-adapter |
maia__search | integration.mcp.search | query; serverIds?; limit? 1..50 | bounded server/tool matches | MCP adapter; read; R; none; mcp-adapter |
describe_tool | integration.mcp.describe | name (<connection>__<tool>) | safe metadata, serverRevision, toolSchemaDigest, riskPolicyDigest, exact call variant | MCP adapter; read; R; none; mcp-adapter |
maia__tool (read) | integration.mcp.call.read | name (<connection>__<tool>); input JSON object at most 64 KiB and depth 16; serverRevision; toolSchemaDigest; riskPolicyDigest | name, classified bounded result, pinned digests | MCP adapter; external read; X; integration.mcp.read-completed; mcp-adapter |
maia__tool (mutation) | integration.mcp.call.mutation | name (<connection>__<tool>); input JSON object at most 64 KiB and depth 16; expectedServerRevision; toolSchemaDigest; riskPolicyDigest; idempotencyKey | blocked preview, then classified bounded result with pinned digests | MCP adapter; mutation; E; integration.mcp.mutation-completed; mcp-adapter |
list_memories | memory.list | scope one of current-session/current-project/project; projectId?; limit? 1..100; cursor? opaque | memories, truncated; pagination in meta | memory; read; R; none; memory-adapter |
search_memories | memory.search | scope; projectId?; query at most 4096; roles? from user/assistant/tool/system-summary; tags? up to 16 strings of 64 chars; limit? 1..20; cursor? opaque | memories, truncated; pagination in meta | memory; read; R; none; memory-adapter |
get_memory | memory.get | memoryRef | memory with exact scope, classification, provenance and bounded content | memory; read; R; none; memory-adapter |
run_doctor | doctor.check | snapshotDigest; scope one of project/agent/routine/context | valid, diagnostics, snapshotDigest | maintenance; privileged read; R; none; maintenance-agent |
query_trace | trace.query | filters with projectId, optional agent/session/routine IDs, time range and classification; limit? 1..100; cursor? opaque | redacted records; pagination in meta | maintenance; privileged read; R; none; maintenance-agent |
list_evaluations | eval.list | subjectId; limit? 1..100; cursor? opaque | evaluations; pagination in meta | maintenance; privileged read; R; none; maintenance-agent |
propose_change | maintenance.proposal.create | kind; rationale at most 4096; suggestedChanges 1..32 non-executable records; evidenceRefs 1..64; idempotencyKey | proposal | maintenance; proposal mutation; C; maintenance.proposal.created; maintenance-agent |
rewrite_goal'sexpectedRevisionisnullonly when no goal exists.start_agentuses the calling Session as parent and cannot override workspace, Connection, model or authority. Limits only narrow the resolved Agent policy;resultContractRefmust resolve to a registered, namespaced output schema. Long work is delegated withstart_agent.respond_to_interactionis available to authenticated people and to delegated Agents granted that exact operation, never to a voice model.describe_toolresolves one immutable call variant from operator policy and fresh capability evidence. Both call variants reject drift in server revision, schema digest or risk-policy digest. A mutationmaia__toolcall first returns a blockedoperation.preview; only an authenticated person can confirm it, and the model never receives the proof. A tool without a trusted static risk annotation is unavailable, never downgraded to read.run_commandtakes argv, never an interpolated shell string. The process tools are Session-owned background execution, not a durable service. The task tools are a small planning ledger without dependencies, not a child Agent or scheduler.- Local mode implements the same schemas in process, keeps a local receipt ledger for mutations and has no direct-mutation escape hatch. Paths are canonicalized with symlink containment; output caps and deadlines are mandatory. Project instructions and Skills cannot register or grant tools.
Task and goal records (one Session owns them; tasks do not execute, allocate
capacity, grant authority or form a dependency graph; a Session has at most
one non-terminal goal, and done needs verification evidence or an explicit
unverified-completion policy):
{
"task": {
"taskId": "task_01...",
"agentSessionId": "session_01...",
"subject": "Verify the Agent tool contract",
"description": "Check every tool schema and result example.",
"activeForm": "Verifying Agent tools",
"lifecycleStatus": "in-progress",
"revision": 3,
"createdAt": "2026-09-01T10:00:00.000Z",
"updatedAt": "2026-09-01T12:00:00.000Z"
},
"goal": {
"agentSessionId": "session_01...",
"objective": "Deliver implementation-ready Maia documentation",
"next": "Implement the first protocol vertical slice",
"success": ["Every command and tool has one checked contract"],
"constraints": ["Keep one native Agent harness"],
"lifecycleStatus": "active",
"verification": { "lifecycleStatus": "not-run", "evidenceRefs": [] },
"revision": 2,
"updatedAt": "2026-09-01T12:00:00.000Z"
}
}
Voice tool surface
Source candidate: the realtime voice model gets one tool, maia, with two
actions
(protocol):
askwith anutterance(at most 4 KiB) prepares an exact message for trusted on-screen confirmation. The model receives a proof-free confirmation-required result; the trusted client confirms and the Node relays it (voice.pi.relay, reconciled byvoice.pi.relay.reconcile).statusreturns one bounded Node snapshot. Agent output in it remains untrusted data.
A trusted Desktop projection may instead route ask through the managed-Agent
preview (voice.managed-turn.preview, confirmed with
voice.managed-turn.confirm), and status then reports that turn's phase and
up to 2,048 bytes of Agent text. Calls are idempotent per scope and tool-call
ID and capped at 16 per scope.
Neither action confirms, executes generic operations or grants shell,
filesystem, network or credential authority.
Planned (R7, voice contract;
decision): ask binds to the exact authorized
Session the person is viewing (global, Workspace or Project coordinator,
worker or ordinary Session), with one client-turn identity shared by typed
send and voice finalization. The Node binds principal, owner and target, never
the model. Speech can request a confirmation ceremony but never satisfy it;
the authenticated client confirms. Text stays the default composer, and no
voice capability is needed for any text or CLI control.
Shared JSON shapes
Target result envelope
Planned for R11 operation-backed interactive commands, and the shape the other surfaces converge on. The outer CLI keeps its current output; moving it has no roadmap slice (decision, pending owner).
{
"schemaVersion": "maia.cli.v1",
"operationId": "session.task.create",
"requestId": "req_01...",
"ok": true,
"data": { "task": {}, "ledgerRevision": 1 },
"meta": {
"generatedAt": "2026-09-01T12:00:00.000Z",
"surface": "agent-command",
"pagination": null,
"warnings": []
},
"receipt": { "operationId": "session.task.create", "outcome": "accepted" },
"error": null
}
ok: truerequireserror: null;ok: falserequiresdata: nulland anerror.receiptis non-null only when the registry declares a receipt.meta.surfaceiscli,agent-command,model-tool,voice-tool,sdkorhttp; it never changes the operation-owneddata.meta.paginationis always present ({ limit, nextCursor, hasMore }ornull).datanever repeats the operation ID, transport fields, receipt or error.
Interaction and response
Source candidate (R1a): the projection maia.interaction/v1
(packages/protocol/src/interaction):
{
"schemaVersion": "maia.interaction/v1",
"interactionId": "interaction_01...",
"kind": "selection",
"projectId": "project_01...",
"sessionId": "session_01...",
"activationId": null,
"requestingActorId": "actor_01...",
"origin": { "kind": "external-cli" },
"subject": "Show routine progress updates?",
"body": "Choose whether routine progress appears in Desktop.",
"options": [
{ "optionId": "yes", "label": "Yes" },
{ "optionId": "no", "label": "No" }
],
"answerSchemaDigest": "sha256:...",
"lifecycleStatus": "pending",
"expiresAt": null,
"revision": "1",
"createdAt": "2026-10-01T12:00:00.000Z",
"answeredAt": null,
"wakeDisposition": "record-only"
}
- A
questionhas nooptionsand takes a text answer; aselectionhas 2–8 frozen options. lifecycleStatusispending,answered,cancelledorexpired. Revision is"1"while pending or expired and"2"after the single answer or cancellation.- List, create and cancel results never contain the answer.
showandrespondaddanswer({ "kind": "text", "value": ... }or{ "kind": "selection", "optionIds": [...] }),respondingActorIdandresponseDigestfor the authorized principal. - An answer is recorded data, never approval. Consequential actions use the operation's own preview and human confirmation.
- The planned R11 model tool
ask_questionis the activation-bound form: it returns ablockedtool result, and its answer may setwakeDispositiontoresume-waiting-session.
Overlap rules
The contract check (maintainer workflow in contracts) fails when:
- a CLI usage or tool name appears twice in one activation;
- a command or tool action maps to zero or several operations, except
maia__tool's declared read or mutation routing by admitted risk class; - an operation has more than one durable owner;
- an output field has different schemas on different surfaces;
- a list DTO embeds another aggregate's full mutable state;
- a command returns a credential or secret-class field;
data,meta,receiptorerroroverlap;- a mutation lacks risk, authority, idempotency or receipt declarations;
- a paginated operation lacks a bound or cursor schema;
- a model or voice tool exposes an operation not granted to that activation.
Reserved field names:
| Field | Only meaning |
|---|---|
lifecycleStatus | domain aggregate lifecycle |
health / degraded | component health observation |
availability | provider, model or executor capacity |
revision | compare-and-set revision of one aggregate |
eventSequence | ordered event position |
observedAt | time an external projection was observed |
createdAt / updatedAt | persisted aggregate timestamps |
nextCursor | opaque pagination continuation |
New canonical DTOs do not use the ambiguous top-level names id, state,
status, timestamp or cursor.
Agent Codex and Pi Config capability disposition
The single feature and action map for replacing the daily Pi harness with Maia (R11). The migration plan owns inventory, provenance, qualification and reversible cutover. Every required source workflow needs a verified Maia equivalent or a deviation the user accepted; a rejection code is not parity. Legacy names never become Maia operation IDs.
AG0 builds one closed migration record per observed source action (tools,
commands, flags, shortcuts, background hooks):
{sourceExtension, sourceAction, disposition, targetOperationId?, rejectionCode?, replacement?},
with exactly one of targetOperationId or rejectionCode. Stable rejection
codes: MAIA_WORKFLOW_DSL_UNSUPPORTED, MAIA_TASK_GRAPH_UNSUPPORTED,
MAIA_AMBIENT_CONFIG_MUTATION_UNSUPPORTED,
MAIA_AUTOMATIC_MEMORY_INJECTION_UNSUPPORTED and
MAIA_EXTERNAL_EXECUTOR_GUARANTEE_UNAVAILABLE. The contract check fails on an
unmapped source action. Agent definitions, prompts, Skills, package resources,
UI preferences, shortcuts and unexplained top-level helpers are classified the
same way; nothing is imported by directory recursion.
The table covers Pi's built-ins and the 33 observed pi-* extensions, then
names that appear in older Pi setups but not in the observed source.
| Source | Maia disposition | Required proof |
|---|---|---|
Pi built-ins read, bash, edit, write, grep, find, ls | wrap read/edit/write/grep/find/ls as read_file, edit_file, write_file, grep, glob and list_directory; replace bash with bounded argv run_command under an execution profile | exact input/result contract, containment, mutation receipts, output limits |
pi-safety | typed tool authorization and execution/workspace policy outside prompts; never a prompt or deletion tripwire in place of containment | destructive and escape adversarial tests |
pi-agents, pi-swarm | agent tools (start_agent, message_agent and their siblings) creating normal Maia Sessions under Node/Swarm authority; message_agent replaces steer; optional exact result contract; task-owned independent review | recursion, budget, lineage, structured result, cancellation, worktree-policy tests |
pi-tasks (task, TaskCreate/List/Get/Update/Batch/Output/Stop/Execute, /tasks, /plan-gate) | Session-local task; execution becomes start_agent; approvals become interactions or operation previews; no hub, graph or task scheduler | ledger recovery, CAS, lifecycle, source-action diagnostics |
pi-goal, /goal | one-goal-per-Session goal tool and /goal; raw and clear are rejected in favor of rewrite and terminal transitions | lifecycle, revision, evidence, terminal-state tests |
pi-interactions (/inbox) | protocol interactions plus the Session inbox; questions stay distinct from confirmation proof; private session links become durable session.send with thread, reply and behavior fields | replay, expiry, actor/device binding, idempotent delivery, outcome-unknown tests |
pi-a2a (/a2a) | Node A2A v1 ingress and egress with Swarm admission; never internal lease or task truth | identity mapping, tenant authorization, artifact and update tests |
pi-claims, pi-worktree | exact cooperative reservations (S16 leases) and explicit checkout create/list/remove (S5); neither is OS containment | ownership and recovery tests |
pi-daemon (shell, process, /daemon, /ps*), pi-rpc (rpc_session) | bounded run_command and Session process tools; durable services are Node operator operations; rpc_session becomes the out-of-process Maia Agent worker and public session.* | profile enforcement, process-tree cleanup, crash/restart, replay, interrupt tests |
pi-crons, pi-routines (/crons, /loop, /routine) | Routines with manual or cron start-or-wake (R8); no looping model process or workflow graph | tick uniqueness, DST, downtime catch-up, queue tests |
pi-context (usage, consumption, status, restore, history, doctor, prune) | context.inspect, context.lint, context.explain, context.usage over exact Node history; restore and prune wait for an explicit retention operation | manifest accounting, budget, compaction, retention tests |
pi-custom-compactor | bounded context compiler and protocol-owned compaction record | deterministic retained range, injection framing, token/byte evidence |
pi-memory (list, search, get, list-projects, /memory) | optional read-only memory.list/search/get; ambient history injection rejected; R4 adds scoped learning separately | scope/export authorization, bounded manifests; PF-06/DF-06 for R4 |
pi-config (list, get, describe, sources, set, unset, edit, reload) | outer configuration operations (S13 config.resolve, plan, apply, drift, import); edits create candidates and /reload only previews one | closed schemas, no hidden grant, admission and CAS tests |
pi-model-policy | typed Connection and model selection order with explicit fallback policy | pre-dispatch, no-output and outcome-unknown conformance |
pi-codex-pool (/codex) | named Connections; session create --executor picks the least-used account; S2 adds a recorded connectionPool rotating at Session admission; no private usage endpoints or quota bypass | multi-account isolation, refresh, logout race, rotation, limit, cross-Workspace denial tests |
pi-vercel-policy | Model Provider policy and Vercel Gateway adapter; no provider-specific command root | capability evidence, payload redaction, fallback tests |
pi-exa (/web-search, web_search, web_fetch), pi-search | provider-neutral optional web_search and web_fetch; glob and grep with hard bounds | domain/date/URL policy, citation, injection, rg/fd absence, truncation tests |
pi-mcp (status, search, describe, connect, call, doctor, history, reload, config, /mcp) | the Agent gets allowlisted status, search, describe and call; server configuration and lifecycle stay with the operator CLI and Node | annotation risk, secret redaction, reconnect, malicious-result tests |
pi-prompt, pi-prompt-conformance | optional client helper plus deterministic context and contract tests; never required for kernel correctness or instruction authority | no hidden prompt source, snapshot digest tests |
pi-projects (/projects), pi-cd (/cd) | project.* operations; no ambient model cwd: path scope is explicit per activation and tool call | exact root and admission, cwd confusion, containment tests |
pi-session-chrome, pi-session-title, pi-session-scratch, pi-terminal-ui (/dashboard) | Session metadata, owned disposable scratch and replaceable SDK projections; the old UI is not ported, a minimal PiTuiRenderer renders SDK Session events | no authority in titles or status; renderer parity fixture |
pi-events (/events), pi-telemetry | Node ordered events through session events list, session observe and R1 observe; redacted events, traces and evaluation evidence (T-slices) | cursor gap, backpressure, reconnect, consent, retention, secret-scan tests |
Not in the observed source: pi-subagents, pi-session-links, pi-fleet, pi-workflows, pi-cursor, pi-rlm | subagents and session links as pi-agents and pi-interactions; multi-Node orchestration as Projects, Sessions, Routines, queues and receipts; no workflow DSL; Cursor as external executor Sessions (session.send, session.stop); RLM exec rejected until a sandbox, authority and receipt design exists | explicit unsupported diagnostics; no duplicate dispatch |
R4 coordinator learning adds scoped
automatic knowledge, memory and Skill authoring in Node-owned records. Ambient
Pi-history injection stays rejected
(MAIA_AUTOMATIC_MEMORY_INJECTION_UNSUPPORTED), and the read-only
memory-adapter tools are not write authority or an R4 prerequisite.
System-prompt and Skill injection disposition
Maia allows the narrowest injection point that works:
| Source behavior | Maia rule |
|---|---|
| tool description, snippet or guidelines | only for registered granted tools; included in the tool-manifest and context digest |
| on-demand Skill | only by exact eligible Skill ID through skill.load; the body is budgeted and recorded |
| root instructions | one root AGENTS.md at Session start; no rules/ tree |
| nested instructions | disabled by default; when enabled, nearest exact path only for one turn |
APPEND_SYSTEM.md, arbitrary SYSTEM.md, full extension README/catalog | never discovered or loaded |
| dynamic goal and task context | one bounded active goal plus a hot task summary, digest-framed; full ledgers load only through tools |
| automatic memory | ambient injection rejected; R4 scoped retrieval uses Node admission and bounded untrusted context manifests |
| Orchestration or RLM dynamic system prompts | rejected: those runtimes do not exist in Maia Agent |
| peer, A2A or Cursor messages | untrusted conversation data, never system authority |
| trace or evaluation material | untrusted evaluator input, never operational Agent context |
| extension or provider history rewrite | rejected unless a protocol operation owns and audits it |
Prompt contributors have stable owner IDs, byte and token caps, exact digests, one insertion point and idempotent compilation. A contributor cannot change tool grants, actor authority, confirmation, Connection choice or protocol policy.
Imported from Maia OS docs at 3dc3e07