* feat(server): let agents steer running turns they could start (MUL-8285) An agent comment can now be added to another agent's running turn, the way a member steers from the composer: exactly when the comment could start that agent (canInvokeAgent, judged on the human at the top of the chain), never its own turn. - steer_task_ids now work for agent authors; steer: true asks the server to add the comment to the current running turn of every recipient without a named turn. - Agent-authored bindings are capped per turn (10) and per authoring agent (3). The task row is locked in its own statement before the bind, so the cap count sees what an earlier lock holder committed. - An agent comment that steered a turn but was never read is replayed on completion like the mention it replaced. Member receipts keep today's behaviour (the member sees the failure and can retry or resend). - The supplement claim returns author_type and the agent's name. A daemon that does not send task-supplement-author-v1 gets the attribution in the content, so an agent message never reads as a human's. - can_supplement on task-runs is computed for an agent caller by the same rule. Co-authored-by: Tim Smart <[email protected]> Co-authored-by: WitMiao <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(daemon): present an agent's added message as an agent's (MUL-8285) Claims send the task-supplement-author-v1 capability and render agent messages as peer input that cannot replace the objective or override humans. Co-authored-by: Tim Smart <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(cli): add --steer and --steer-active to issue comment add (MUL-8285) Docs: CLI help, CLI_AND_DAEMON.md, the CLI docs in every locale, the squad leader briefing, and a steering section in the multica-platform mentions reference. Co-authored-by: Tim Smart <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix: close the review gaps in agent steering (MUL-8285) - Replay an unread agent steer even when it reached the turn through an implicit route (a worker's reply to its leader): a comment bound to the completing run counts as planned input for the replay filter. - Drop the receipt of an agent steer once completion has replayed it, so no client (old or new) offers to resend what the follow-up already handles. - Never hand an agent's message to a daemon without task-supplement-author-v1: it would frame it as a human's. The message stays pending, settles when the turn ends, and is replayed as a mention. - Tell a squad leader about --steer only when its daemon advertises cli-comment-steer-v1, since an older CLI rejects the flag; the skill reference says to drop the flag on 'unknown flag'. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Tim Smart <[email protected]> Co-authored-by: WitMiao <[email protected]> Co-authored-by: multica-agent <[email protected]>
73 KiB
CLI and Agent Daemon Guide
The multica CLI connects your local machine to Multica. It handles authentication, workspace management, issue tracking, and runs the agent daemon that executes AI tasks locally.
Installation
Homebrew (macOS/Linux)
brew install multica-ai/tap/multica
Build from Source
git clone https://github.com/multica-ai/multica.git
cd multica
make build
cp server/bin/multica /usr/local/bin/multica
Update
brew upgrade multica-ai/tap/multica
For install script or manual installs, use:
multica update
multica update uses GitHub Releases by default. Self-hosted installations can
point the CLI at a GitHub Releases-compatible metadata mirror and an artifact
mirror without changing the command. To let a daemon poll that source, also
enable self-update explicitly because self-hosted auto-update is disabled by
default:
export MULTICA_RELEASE_API_BASE_URL=https://updates.example/api
export MULTICA_RELEASE_DOWNLOAD_BASE_URL=https://updates.example/releases/download
export MULTICA_DAEMON_AUTO_UPDATE=true
multica update
The metadata mirror must serve /repos/multica-ai/multica/releases/latest and
/repos/multica-ai/multica/releases/tags/<tag>. The artifact mirror must serve
/<tag>/<asset-name> and preserve the published checksums.txt contents.
When these variables are unset, the GitHub defaults remain unchanged.
Quick Start
# One-command setup: configure, authenticate, and start the daemon
multica setup
# For self-hosted (local) deployments:
multica setup self-host
Or step by step:
# 1. Authenticate (opens browser for login)
multica login
# 2. Start the agent daemon
multica daemon start
# 3. Done — agents in your watched workspaces can now execute tasks on your machine
multica login automatically discovers all workspaces you belong to and adds them to the daemon watch list.
Authentication
Browser Login
multica login
Opens your browser for OAuth authentication, creates a 90-day personal access token, and auto-configures your workspaces.
Token Login
multica login --token <mul_...>
Authenticate using a personal access token directly. Useful for headless environments. Pass --token= with an empty value to be prompted interactively (so the token never lands in shell history).
Check Status
multica auth status
Shows your current server, user, and token validity.
Logout
multica auth logout
Removes the stored authentication token.
Agent Daemon
The daemon is the local agent runtime. It detects available AI CLIs on your machine, registers them with the Multica server, and executes tasks when agents are assigned work.
Start
multica daemon start
By default, the daemon runs in the background and writes its log into the state
directory of the profile it was started with — not always ~/.multica/:
| Profile | State directory |
|---|---|
Default (no --profile) |
~/.multica/ |
Named (--profile <name>) |
~/.multica/profiles/<name>/ |
That directory holds daemon.log (the log), daemon.pid (the background
daemon's PID), and daemon.err.log (raw crash output; near-empty on a healthy
daemon, since normal logging goes to daemon.log).
The Desktop app runs its own named profile, so on a machine that has ever run
both, ~/.multica/daemon.log and ~/.multica/profiles/<name>/daemon.log both
exist and both read as plausible logs — only one is being written to. Don't
guess: multica daemon logs prints the absolute path it resolved (see
Logs).
To run in the foreground (useful for debugging):
multica daemon start --foreground
Following a replaced binary
A CLI-launched daemon periodically compares its own compile-time version against
the --version output of the multica binary it would re-exec. When they differ
— brew upgrade multica, a re-download, a local make build — it waits for any
running task to finish, then restarts into the new binary. A running task is
never interrupted; if the daemon is busy the restart is deferred to the next
check, and multica daemon status shows why it's still on the old version.
This is separate from the GitHub self-update poller: disabling that does not stop the daemon from following a binary you installed yourself. To turn it off:
MULTICA_DAEMON_AUTO_RELOAD=0 multica daemon start
# or
multica daemon start --no-auto-reload
# or persist it
multica config set disable_auto_reload true
Agent CLIs (codex, claude, ...) are handled differently: when one of them is upgraded in place, the daemon re-probes its version and re-registers the runtime without restarting, so subsequent tasks pick up the new CLI while Multica's availability stays independent of a third party's release cadence.
Desktop-managed daemons ignore both, because the Desktop app owns its bundled CLI's lifecycle.
Boot autostart
Opt-in: only multica daemon autostart enable registers the profile's
daemon with the OS, so the machine brings it back after a reboot or re-login
— without that, every reboot takes the runtime offline and queued runs sit
unclaimed. daemon start never registers on its own: it prints a one-line
hint when nothing is registered, and otherwise stays out of the way.
| Platform | Mechanism | Where |
|---|---|---|
| Windows | Per-user Run key | HKCU\Software\Microsoft\Windows\CurrentVersion\Run, value Multica |
| macOS | launchd LaunchAgent | ~/Library/LaunchAgents/ai.multica.daemon.plist (runs at login) |
| Linux | systemd user unit | ~/.config/systemd/user/multica-daemon.service; without systemd, an XDG autostart entry under ~/.config/autostart/ |
Each entry runs multica daemon start --foreground for that profile — named
profiles get their own entry, so several daemons on one machine never collide.
Daemon settings are read from the profile's config (multica config set ...)
at start. Shell environment variables do not travel into a login session:
only PATH is snapshotted at registration (and refreshed while Multica owns
the entry), which is what keeps agent CLIs installed via Homebrew, nvm, or a
user bin directory discoverable — persist everything else with
multica config set or your user environment. On Linux, enable also tells
you (it does not run it for you) how to loginctl enable-linger $USER so a
headless machine starts the unit at boot rather than at first login.
multica daemon autostart enable # the only thing that registers
multica daemon autostart status # what is registered, and where
multica daemon autostart status --output json
multica daemon autostart disable # remove the registration
Refresh: while an entry exists and carries Multica's ownership marker,
daemon start silently rewrites it so a moved executable (a Homebrew
upgrade, a self-update) heals. An entry at the same path that Multica did
not create — your own systemd unit, a hand-written LaunchAgent — is never
refreshed and never overwritten: enable/disable refuse it and say which
file is in the way, and the refresh is additionally skipped when the daemon
was launched by an external supervisor (systemd INVOCATION_ID for a unit
that is not ours).
daemon stop stops the daemon but keeps the registration — it says nothing
about the next boot; autostart disable is what turns that off. Disabling
never disturbs a running daemon: on Linux the wants link is removed while
the managed unit file stays (unlinked), so the current session keeps the
unit's restart policy across a binary update and only the next-login start
goes away; on macOS the plist is deleted without a launchctl bootout, so
the process launchd is supervising keeps running. Daemons started by the
Multica Desktop app are never registered or refreshed here: the app owns
that daemon's lifecycle through its own app-start daemon preference.
Stop
multica daemon stop
Status
multica daemon status
multica daemon status --output json
Shows PID, uptime, detected agents, and watched workspaces.
Logs
multica daemon logs # Last 50 lines
multica daemon logs -f # Follow (tail -f)
multica daemon logs -n 100 # Last 100 lines
multica daemon logs --profile staging
Every run first prints the absolute path it resolved, so you always know which profile's log you are looking at:
$ multica daemon logs -n 100
Reading /Users/you/.multica/profiles/desktop-mbp/daemon.log (profile: desktop-mbp)
...
That line goes to stderr, before the tail starts — so it also shows up under
-f, and piping or redirecting the command still yields log content only:
multica daemon logs -n 500 | grep ERROR # the path line is not in the pipe
Without --profile, the default profile's log is read. If it doesn't exist the
command says so and names the path it looked for, which is the fastest way to
find out that the daemon you care about is running on a different profile —
multica daemon status --profile <name> confirms which one is live.
Supported Agents
The daemon auto-detects these AI CLIs on your PATH:
| CLI | Command | Description |
|---|---|---|
| Claude Code | claude |
Anthropic's coding agent |
| Antigravity CLI | agy |
Google Antigravity CLI |
| CodeBuddy Code | codebuddy |
Tencent CodeBuddy Code (reads CODEBUDDY.md, not CLAUDE.md) |
| Huawei Cloud CodeArts | codearts |
Huawei Cloud coding agent (OpenCode-compatible JSON protocol) |
| DevEco Code | deveco |
OpenHarmony DevEco Code |
| Codex | codex |
OpenAI's coding agent |
| GitHub Copilot CLI | copilot |
GitHub's coding agent (model routed by your GitHub entitlement) |
| OpenCode | opencode |
Open-source coding agent |
| OpenClaw | openclaw |
Open-source coding agent |
| Hermes | hermes |
Nous Research coding agent |
| Pi | pi |
Pi coding agent |
| Oh-My-Pi | omp |
Oh-My-Pi coding agent (Pi fork) |
| Cursor Agent | cursor-agent |
Cursor's headless coding agent |
| Kimi | kimi |
Moonshot coding agent |
| Reasonix | reasonix |
DeepSeek-focused ACP coding agent (run reasonix setup first) |
| Dim | dim |
DimCode ACP coding agent (speaks the ACP protocol via dim acp) |
| Devin CLI | devin |
Version 3000.3.22+. Host-local ACP (devin acp), not cloud Devin VMs. Run devin auth login first. |
| Kiro CLI | kiro-cli |
Kiro ACP coding agent |
| Qoder CLI | qodercli |
Qoder ACP coding agent |
| Qoder CN CLI | qoderclicn |
Qoder CN ACP coding agent |
| Trae | traecli |
ByteDance TRAE CLI (ACP via traecli acp serve) |
| Grok Build CLI | grok |
xAI Grok Build CLI (ACP via grok agent stdio) |
| Qwen Code | qwen |
Alibaba Qwen Code (qwen -p with stream-json) |
| QwenPaw | qwenpaw |
QwenPaw ACP coding agent (ACP via qwenpaw acp; model is fixed by its own configuration) |
| MiniMax Code | mcode |
MiniMax Code ACP coding agent (ACP via mcode acp; model is managed by MCode) |
| DeepSeek Harness | dsh |
DeepSeek Harness (dsh --profile multica --stdio; requires the Multica runtime profile to be installed; reads AGENTS.md and .dsh/skills/) |
You need at least one installed. The daemon registers each detected CLI as an available runtime.
How It Works
- On start, the daemon detects installed agent CLIs and registers a runtime for each agent in each watched workspace
- The server pushes a wake signal over the WebSocket connection when work is waiting, and the daemon claims across all of its runtimes in one batch. A periodic poll (default: 30s) runs as the catch-up path — a wake signal cuts the wait short, so this interval puts no floor under normal task pickup; it bounds how long work can sit when a signal is missed or the connection is down
- When a task arrives, it creates an isolated workspace directory, spawns the agent CLI, and streams results back
- Heartbeats are sent periodically (default: 15s) so the server knows the daemon is alive
- On shutdown, all runtimes are deregistered
Configuration
Daemon behavior is configured via flags or environment variables:
| Setting | Flag | Env Variable | Default |
|---|---|---|---|
| Poll interval | --poll-interval |
MULTICA_DAEMON_POLL_INTERVAL |
30s (catch-up fallback; WebSocket wake signals deliver work sooner) |
| Healthy WebSocket claim poll upper bound | --ws-claim-poll-interval |
MULTICA_DAEMON_WS_CLAIM_POLL_INTERVAL |
3m (configured independently of --poll-interval; downward jitter makes the normal interval 2m30s–2m45s, while old servers and uncertain claims retain the ordinary poll interval) |
| Heartbeat interval | --heartbeat-interval |
MULTICA_DAEMON_HEARTBEAT_INTERVAL |
15s |
| Agent timeout | --agent-timeout |
MULTICA_AGENT_TIMEOUT |
0 (no cap; bounded by the watchdogs) |
| Agent idle watchdog | — | MULTICA_AGENT_IDLE_WATCHDOG |
2h (0 disables the whole watchdog suite) |
| Agent tool watchdog | — | MULTICA_AGENT_TOOL_WATCHDOG |
same as the idle watchdog (0 = never force-stop during a tool call) |
| Codex semantic inactivity timeout | --codex-semantic-inactivity-timeout |
MULTICA_CODEX_SEMANTIC_INACTIVITY_TIMEOUT |
same as the idle watchdog (Codex's timer is not tool-aware, so it tracks the larger of the idle / tool budgets) |
| Codex first-turn no-progress timeout | — | MULTICA_CODEX_FIRST_TURN_TIMEOUT |
0 (keeps the built-in 60s ceiling) |
| Codex handshake timeout | --codex-handshake-timeout |
MULTICA_CODEX_HANDSHAKE_TIMEOUT |
30s; thread/start and thread/resume: 60s (an explicit value overrides both budgets globally) |
| Codex turn-interrupt timeout | — | MULTICA_CODEX_TURN_INTERRUPT_TIMEOUT |
2s (bounded grace period for turn/interrupt acknowledgement and turn/completed; tune from the logged interrupt latency on unusually slow hosts) |
| OpenCode idle watchdog | — | MULTICA_OPENCODE_IDLE_WATCHDOG |
10m (0 falls back to the generic idle watchdog; cannot extend it) |
| Max concurrent tasks | --max-concurrent-tasks |
MULTICA_DAEMON_MAX_CONCURRENT_TASKS |
20 |
| Daemon ID | --daemon-id |
MULTICA_DAEMON_ID |
hostname |
| Device name | --device-name |
MULTICA_DAEMON_DEVICE_NAME |
hostname |
| Runtime name | --runtime-name |
MULTICA_AGENT_RUNTIME_NAME |
Local Agent |
| Workspaces root | — | MULTICA_WORKSPACES_ROOT |
~/multica_workspaces |
| GC enabled | — | MULTICA_GC_ENABLED |
true (set false/0 to disable) |
| GC scan interval | — | MULTICA_GC_INTERVAL |
2h |
| GC TTL (done/cancelled issues) | — | MULTICA_GC_TTL |
24h |
| GC completed-task TTL (issue tasks) | — | MULTICA_GC_COMPLETED_TASK_TTL |
14d on Multica Cloud, 0 (disabled) elsewhere |
GC orphan TTL (no .gc_meta.json) |
— | MULTICA_GC_ORPHAN_TTL |
72h |
| GC artifact TTL (completed tasks) | — | MULTICA_GC_ARTIFACT_TTL |
12h (set 0 to disable) |
| GC artifact patterns | — | MULTICA_GC_ARTIFACT_PATTERNS |
node_modules,.next,.turbo |
GC repo cache TTL (.repos) |
— | MULTICA_GC_REPO_TTL |
720h (30d; set 0 to disable) |
| GC repo maintenance | — | MULTICA_GC_REPO_MAINTENANCE_ENABLED |
true (set false/0 to disable heavy Git maintenance only) |
GC Hermes memory TTL (per-agent memories/) |
— | MULTICA_GC_HERMES_MEMORY_TTL |
2160h (90d; set 0 to disable) |
GC Hermes session TTL (per-conversation state.db) |
— | MULTICA_GC_HERMES_SESSION_TTL |
336h (14d; set 0 to disable) |
GC task temp legacy TTL (pre-lock multica-task-*) |
— | MULTICA_GC_TASK_TEMP_LEGACY_TTL |
0 (disabled; set a duration to opt in) |
Workspace garbage collection
The daemon periodically scans MULTICA_WORKSPACES_ROOT and applies several disk-reclamation policies:
-
Full task cleanup — when an issue's status is
doneorcancelledand has been idle forMULTICA_GC_TTL, the entire task directory is removed. -
Completed-task retention bound —
MULTICA_GC_COMPLETED_TASK_TTLfully removes an inactive issue task once its.gc_meta.jsoncompleted_atage exceeds the configured duration, even while the parent issue remains open. Cleanup waits for a successful parent-issue status check, never removes an active environment, and never fully removes alocal_directoryenvironment. A later rerun provisions a fresh environment instead of resuming the removed checkout.- The default depends on where the daemon points:
14dagainst Multica Cloud, and0(disabled, retain indefinitely) for self-host and every other origin — including cloud staging and previews. Set the variable to opt in or out on either side; an explicit0disables the policy on Cloud too. - Removing an environment discards work an agent left uncommitted or unpushed on its branch, along with that task's
output/andlogs/. The per-issue Codex session store lives outsideMULTICA_WORKSPACES_ROOTunder its own TTL, so a later rerun still resumes the agent's prior session — it just starts from a fresh checkout. Size the TTL against that trade, and keep it comfortably aboveMULTICA_GC_INTERVAL: the active-root guard protects a task that is currently running, not one whose follow-up run is queued but unclaimed.
- The default depends on where the daemon points:
-
Orphan cleanup — task directories with no
.gc_meta.json(e.g. left over from a daemon crash) are removed once they exceedMULTICA_GC_ORPHAN_TTL. -
Artifact-only cleanup — when a task has been completed for at least
MULTICA_GC_ARTIFACT_TTLbut the issue is still open, regenerable build outputs whose directory basename matchesMULTICA_GC_ARTIFACT_PATTERNSare removed. The daemon also reclaims the exact managed pathcodex-home/.sandbox-bin; old task metadata withoutcompleted_atbecomes eligible for this managed-only cleanup after its.gc_meta.jsonfile has been idle forMULTICA_GC_ORPHAN_TTL. The rest of the task (source,.git,output/,logs/,.gc_meta.json, Codex auth/config/session state) is preserved so the agent can resume it. -
Managed-cache reclamation — the exact managed path above is reclaimed for every task kind once the task has been completed for
MULTICA_GC_ARTIFACT_TTL, not just for issue tasks whose issue is still open. It applies even while the parent record says the directory itself must stay — an active chat session, a still-running autopilot run — and even when the parent record could not be reached this cycle, because the contents are regenerable and the next run re-provisions them on demand. A task currently running on the directory is never touched. SetMULTICA_GC_ARTIFACT_TTL=0to disable this along with the rest of artifact cleanup. -
Repo cache eviction — the bare git clones under
.repos/are shared object stores: each task workdir is agit worktreeoff one of them rather than its own clone, so a task's.gitis only a pointer file. They are evicted only when all of the following hold: the repo is no longer attached to any workspace this daemon watches, it has no worktrees left, and no task has created a worktree from it forMULTICA_GC_REPO_TTL. A cache created before this stamp existed is not treated as ancient — its clock starts at the first GC cycle that sees it, so upgrading does not wipe every cache. Evicting is safe by construction: the next task that needs the repo re-clones it on demand, so a wrong eviction costs a clone, not a failure.Short worktree cleanup and eligible cache eviction continue on every GC cycle, including while agents are active. Heavy repo maintenance (
reflog expireandgit gc) starts only while the daemon is otherwise idle. A checkout or newly claimed task cancels it and takes priority; interrupted work remains pending for a later idle GC cycle. Operators can disable only these heavy commands withMULTICA_GC_REPO_MAINTENANCE_ENABLED=falsewithout disabling worktree cleanup or cache eviction. -
Hermes session store reclamation — a conversation's Hermes transcript (
state.db) lives at<profile dir>/hermes-sessions/<agent-id>/<hermes-profile>/<conversation>/, outside any task directory, so a follow-up turn can resume it (see Hermes agent memory). A store untouched forMULTICA_GC_HERMES_SESSION_TTLis removed. The default matches the Codex session store rather than the memory store above: these hold full transcripts, and reclaiming an idle one costs a thread that starts fresh (with a continuity notice), not an agent that forgot what it learned. A store a running task holds is never reclaimed. -
Hermes memory store reclamation — a Hermes agent's long-term memory (
memories/) lives at<profile dir>/hermes-state/<agent-id>/<hermes-profile>/, outside any task directory, so it survives across tasks and issues (see Hermes agent memory). A store untouched forMULTICA_GC_HERMES_MEMORY_TTLis removed, giving a deleted agent's memory an eventual-reclamation guarantee. The default is deliberately long: these are a handful of markdown files, and reclaiming one is user-visible amnesia rather than a cache miss. A store a running task holds is never reclaimed. -
Task temp dir reclamation — every task gets a private temp directory (
multica-task-*) under the system temp base (/tmp, orMULTICA_AGENT_TEMP_BASE), exported to the agent asTMPDIR/TMP/TEMP. On Windows, every task instead inherits the daemon user'sTMP/TEMP: Git Bash's/tmpmount is shared by all of the user's live Git Bash processes, whichever runtime started them, so it must never point at a task directory that will be deleted.TMPDIRstays task-local, and for Claude CodeCLAUDE_CODE_TMPDIRdefaults to that directory unless the user overrides it. It is removed when the run ends, but that removal never happens when the daemon is killed and does not succeed while a file inside is still open — common on Windows, where an open handle makes the delete fail outright. These directories live outsideMULTICA_WORKSPACES_ROOT, so nothing else reclaimed them and whatever the end-of-run removal missed accumulated forever. Every GC cycle now sweeps the temp base. Liveness is decided by the directory's.task_lock— the same OS advisory lock an env root uses, which the kernel releases when the holding process dies — not by age: a directory still in use is never removed however old it is, including one owned by a different daemon sharing the same temp base, and a directory whose owner is gone is removed on the next cycle however new it is. Directories left by a daemon predating that lock carry no lock file, so nothing can be proven about them and age is the only signal available. Reclaiming those is an operator's explicit decision:MULTICA_GC_TASK_TEMP_LEGACY_TTLdefaults to0, which leaves them in place. Set it to a duration only once you know no pre-lock daemon is still running tasks on this machine — a task may legitimately run for weeks (there is no default agent timeout), a daemon on another profile can still be on the old binary, and every daemon on the machine shares one temp base, so a TTL here can delete aTMPDIRthat is still in use. Each GC cycle logs how many such directories it left alone. Even with a TTL set, a directory holding no task content is never reclaimed on age — an old empty leftover, or a shell left by a daemon that died between creating the directory and publishing its lock — because holding no content is exactly what a directory currently being published looks like, and deleting one of those would take theTMPDIRof a task that is starting. Those shells are a few bytes each. Only entries carrying themultica-task-prefix are ever considered — the temp base itself is usually shared with other programs — and a directory this sweep cannot read is never touched.
Configured patterns are basename-only — entries containing / or \ are silently dropped — and .git subtrees are never descended into. The managed Codex cache is matched by its exact relative path, so a repository's own .sandbox-bin is not removed unless an operator explicitly adds that basename to MULTICA_GC_ARTIFACT_PATTERNS. The default list (node_modules, .next, .turbo) is intentionally narrow; extend it per deployment if your repos consistently produce other regenerable directories (for example, MULTICA_GC_ARTIFACT_PATTERNS=node_modules,.next,.turbo,target,__pycache__). To disable artifact cleanup entirely, including the managed Codex cache, set MULTICA_GC_ARTIFACT_TTL=0.
multica daemon disk-usage reports the .repos footprint on its own line rather than folding it into the per-task totals — every task in a workspace checks out from that shared cache, so attributing it to individual task directories would double-count it. Note that the repo cache is reclaimed on the schedule above and not by any per-issue status change, so it is normal for it to persist after every task directory is gone.
Agent-specific overrides:
| Variable | Description |
|---|---|
MULTICA_CLAUDE_PATH |
Custom path to the claude binary |
MULTICA_CLAUDE_MODEL |
Override the Claude model used |
MULTICA_CLAUDE_ARGS |
Default extra arguments for Claude Code runs |
MULTICA_ANTIGRAVITY_PATH |
Custom path to the agy binary |
MULTICA_ANTIGRAVITY_MODEL |
Override the Antigravity model used |
MULTICA_CODEBUDDY_PATH |
Custom path to the codebuddy binary |
MULTICA_CODEBUDDY_MODEL |
Override the CodeBuddy model used |
MULTICA_CODEBUDDY_ARGS |
Default extra arguments for CodeBuddy runs |
MULTICA_CODEARTS_PATH |
Custom path to the codearts launcher or binary |
MULTICA_CODEARTS_MODEL |
Override the CodeArts model used |
MULTICA_DEVECO_PATH |
Custom path to the deveco binary |
MULTICA_DEVECO_MODEL |
Override the DevEco Code model used |
MULTICA_CODEX_PATH |
Custom path to the codex binary |
MULTICA_CODEX_MODEL |
Override the Codex model used |
MULTICA_CODEX_ARGS |
Default extra arguments for Codex runs |
MULTICA_COPILOT_PATH |
Custom path to the copilot binary |
MULTICA_COPILOT_MODEL |
Override the Copilot model used (note: GitHub Copilot routes models through your account entitlement, so this may not be honoured) |
MULTICA_OPENCODE_PATH |
Custom path to the opencode binary |
MULTICA_OPENCODE_MODEL |
Override the OpenCode model used |
MULTICA_OPENCLAW_PATH |
Custom path to the openclaw binary |
MULTICA_OPENCLAW_MODEL |
Select the registered OpenClaw agent ID used by default; the agent's runtime_config.model_override persistently overrides its underlying model for all newly claimed runs until changed or cleared |
MULTICA_OPENCLAW_CLI_TIMEOUT |
Deadline for each openclaw config ... call during task preparation (default 30s; accepts 45s or 45). Raise it when the local CLI is slow to start; the daemon also reads it from backends.openclaw.cli_timeout in the CLI config |
MULTICA_HERMES_PATH |
Custom path to the hermes binary |
MULTICA_HERMES_MODEL |
Override the Hermes model used |
MULTICA_PI_PATH |
Custom path to the pi binary |
MULTICA_PI_MODEL |
Override the Pi model used |
MULTICA_CURSOR_PATH |
Custom path to the cursor-agent binary |
MULTICA_CURSOR_MODEL |
Override the Cursor Agent model used |
MULTICA_KIMI_PATH |
Custom path to the kimi binary |
MULTICA_KIMI_MODEL |
Override the Kimi model used |
MULTICA_REASONIX_PATH |
Custom path to the reasonix binary |
MULTICA_REASONIX_MODEL |
Override the Reasonix model used |
MULTICA_DIM_PATH |
Custom path to the dim binary |
MULTICA_DIM_MODEL |
Override the Dim model used |
MULTICA_DEVIN_PATH |
Custom path to the devin binary |
MULTICA_DEVIN_MODEL |
Override the Devin model used |
MULTICA_KIRO_PATH |
Custom path to the kiro-cli binary |
MULTICA_KIRO_MODEL |
Override the Kiro model used |
MULTICA_QODER_PATH |
Custom path to the qodercli binary |
MULTICA_QODER_MODEL |
Override the Qoder model used |
MULTICA_QODERCLICN_PATH |
Custom path to the qoderclicn binary |
MULTICA_QODERCLICN_MODEL |
Override the Qoder CN model used |
MULTICA_TRAECLI_PATH |
Custom path to the traecli binary |
MULTICA_TRAECLI_MODEL |
Override the Trae model used (a model id from your logged-in traecli catalog, e.g. Doubao-Seed-2.1-Pro) |
MULTICA_GROK_PATH |
Custom path to the grok binary (defaults to grok on PATH; often ~/.grok/bin/grok) |
MULTICA_GROK_MODEL |
Override the Grok model used (e.g. grok-4.5) |
MULTICA_QWEN_PATH |
Custom path to the qwen binary |
MULTICA_QWEN_MODEL |
Override the Qwen Code model used |
MULTICA_QWEN_ARGS |
Daemon-wide extra Qwen arguments (POSIX shellword parsing; managed protocol flags are filtered) |
MULTICA_QWENPAW_PATH |
Custom path to the qwenpaw binary |
MULTICA_QWENPAW_ARGS |
Daemon-wide extra QwenPaw arguments (POSIX shellword parsing; managed protocol flags are filtered) |
MULTICA_MCODE_PATH |
Custom path to the mcode binary |
MULTICA_DSH_PATH |
Custom path to the dsh binary |
MULTICA_DSH_MODEL |
Override the DeepSeek Harness model used (a model id from the dsh catalog, e.g. deepseek-official/deepseek-chat) |
If a previously generated ~/.multica/hooks wrapper is first on PATH and calls the same command name again, the daemon skips that hooks directory during built-in agent discovery and records the real binary path behind it. If your interactive shell still recurses when you run claude, codex, or hermes manually, remove the hooks entry from your shell startup file or replace the wrapper body with an absolute exec /path/to/real-binary "$@".
The daemon launches Qoder and Qoder CN as qodercli --yolo --acp and qoderclicn --yolo --acp, respectively, matching their ACP “bypass permissions” mode so tool runs do not block on interactive approval in headless runs.
The daemon launches Qwen Code as qwen -p <prompt> --output-format stream-json. It writes the task brief to QWEN.md; when an agent has managed mcp_config, the daemon writes a 0600 per-run JSON file and passes it through --mcp-config <path>, then removes it after the process exits. A null config preserves Qwen Code native MCP settings.
mcp_config on ACP runtimes
ACP-family runtimes — Hermes, Kimi, Kiro, Grok, Qoder, Reasonix, Trae, QwenPaw, MiniMax Code, Dim, and any custom runtime profile whose protocol_family is one of them — receive MCP servers over the ACP session protocol, not through a config file. The daemon translates the agent's mcp_config into ACP's McpServer array and sends it with session/new, and again with that runtime's resume request (session/resume on Hermes, Kimi, Qoder and Reasonix; session/load on Kiro, Grok, Trae, QwenPaw and Dim) so a resumed task keeps the same tools. MiniMax Code 0.1.2 advertises no session-loading capability, so a later run falls back to a fresh session.
Nothing is written to the runtime's own config file, and the runtime's own file is not read or merged. ~/.hermes/…, ~/.jcode/mcp.json and the like stay untouched; an agent's servers travel with its tasks instead of being installed per machine.
Two consequences are worth knowing before debugging a missing MCP tool:
mcp_configmust use the canonical envelope,{"mcpServers": {"<name>": {…}}}. Runtime-native config files that nest servers underservers,mcp, ormcp_serversare stored as-is but yield no servers; the daemon logs a warning naming the key it found. Entries themselves use the Claude-style shape (command/args/envfor stdio,url/headers/typefor remote).- Remote transports depend on what the runtime declares. ACP v1 requires an omitted capability to be treated as unsupported, so
httpandsseentries are dropped with a warning unless theinitializeresponse declaresagentCapabilities.mcpCapabilitieswith that transport set to true. The built-in Hermes runtime is a verified exception: it declares nomcpCapabilitiesyet accepts both transports, so remote entries are still forwarded to it. That exception covers the Hermes binary only — a custom runtime profile withprotocol_family: hermesruns a different implementation and keeps the standard rule. Stdio is never gated.
If a configured server produces no tools, check the daemon log for those warnings first, then confirm the runtime itself exposes the server's tools to the model — some ACP adapters apply their own tool-profile filtering after connecting.
The daemon launches QwenPaw as qwenpaw acp --workspace <per-task dir>. It writes the task brief to AGENTS.md, and materialises the run's bound skills into <per-task dir>/skills/ plus a skill.json manifest, so QwenPaw discovers them through its own workspace skill discovery. acp and --workspace are reserved: custom_args cannot override them. QwenPaw is the one runtime with no MULTICA_QWENPAW_MODEL: its session/set_model writes to a shared, persistent agent config rather than the session, so Multica never sends it a model and leaves that choice to QwenPaw's own configuration.
The daemon launches MiniMax Code as mcode acp, writes the task brief to AGENTS.md, and injects bound skills under .minimax/skills/. MCode owns model selection and currently advertises loadSession: false; Multica therefore starts a fresh MCode session when a later run cannot load the saved session.
Hermes agent memory
Hermes discovers skills only from its own home, so binding Multica skills to a Hermes agent makes the daemon build a per-task HERMES_HOME overlay for that agent. The agent's long-term memory (memories/) does not live inside that task-scoped overlay: it is linked to a persistent store at
<profile dir>/hermes-state/<agent-id>/<hermes-profile>/
so the same agent keeps its memory across tasks and issues. <hermes-profile> is the profile the agent resolves to (default, a named profile from -p/--profile or active_profile, or a hash for an out-of-tree custom HERMES_HOME) — pointing an agent at a different profile gives it a different memory line, matching Hermes' own "a profile is an isolated instance" model.
Consequences worth knowing:
- Memory is agent-scoped but runtime-local. One agent's memory is never visible to another, and the user's own
~/.hermes/memoriesis never read or written. The store lives in this runtime's Multica profile directory, so it does not follow the agent to another machine — an agent that runs on two runtimes has a separate memory line on each. Everything else in the home — auth, config, plugins — is still shared from the user's real home by symlink, so the agent does not need its own login. - To carry existing local memory in, copy it into the store once:
cp -R ~/.hermes/memories/. "<profile dir>/hermes-state/<agent-id>/default/". To wipe an agent's memory, delete that directory. - Conversation history is covered too, in a separate store. Hermes keeps every ACP session in
<HERMES_HOME>/state.db, which the overlay links to a per-conversation store at<profile dir>/hermes-sessions/<agent-id>/<hermes-profile>/<issue-id | chat_\<chat-session-id\>>/, so a follow-up turn resumes the actual transcript. The shard is per conversation rather than per agent on purpose: tasks of one conversation run one after another, so a shard has a single writer at a time, while two issues never share a database. A host that cannot create the link (Windows without symlink privileges) keeps the database task-local instead, untouched — the link is proven creatable before anything is moved, and a copy is never used, because a copied SQLite database would absorb the turn's writes into a file the next task discards. - Concurrent tasks of one agent are last-writer-wins. Hermes rewrites its memory files whole, so two tasks writing memory at the same time can overwrite each other.
- Every Hermes agent gets the overlay in practice, so every one of them gets a persistent memory store. The daemon builds the overlay only when a task carries skills, but the server appends Multica's built-in skills to every agent's skill set (
LoadAgentSkillBundles), so that list is never empty — leaving an agent's own skill list empty does not opt out of the overlay, and is not a way to keep using the host's~/.hermes/memories.
MULTICA_CLAUDE_ARGS, MULTICA_CODEX_ARGS, MULTICA_CODEBUDDY_ARGS, MULTICA_QWEN_ARGS, and MULTICA_QWENPAW_ARGS are parsed with POSIX shellword quoting, so values such as --model "gpt-5.1 codex" --sandbox read-only are split like a shell command line. Agent arguments are applied in this order: hardcoded Multica defaults, daemon-wide env defaults, then per-agent custom_args from the task.
Self-Hosted Server
When connecting to a self-hosted Multica instance, the easiest approach is:
# One command — configures for localhost, authenticates, starts daemon
multica setup self-host
# Or for on-premise with custom domains:
multica setup self-host --server-url https://api.example.com --app-url https://app.example.com
Or configure manually:
# Set URLs individually
multica config set server_url http://localhost:8080
multica config set app_url http://localhost:3000
# For production with TLS:
# multica config set server_url https://api.example.com
# multica config set app_url https://app.example.com
multica login
multica daemon start
Profiles
Profiles let you run multiple daemons on the same machine — for example, one for production and one for a staging server.
# Set up a staging profile
multica setup self-host --profile staging --server-url https://api-staging.example.com --app-url https://staging.example.com
# Start its daemon
multica daemon start --profile staging
# Default profile runs separately
multica daemon start
Each profile gets its own config directory (~/.multica/profiles/<name>/), daemon state, health port, and workspace root. Daemon state means that profile's own daemon.log, daemon.err.log, daemon.pid, and daemon.port live in that directory too — see Start for the layout, and pass --profile <name> to daemon status / daemon logs to act on it. The daemon normally uses the profile-derived health port. If the OS refuses that port, it binds an available loopback port and records the actual port in daemon.port for lifecycle commands. The record is valid only while its daemon is alive; do not edit it to configure a port.
Workspaces
Working with multiple workspaces
Every command runs against a single workspace. The CLI resolves which one in this order (highest priority first):
--workspace-id <id>flag on the commandMULTICA_WORKSPACE_IDenvironment variable- The default workspace stored in your current profile (set by
multica workspace switchormultica login)
multica workspace switch <id|slug> is the day-to-day way to change the default workspace. For scripting and headless setups where you don't want any stored state, prefer the --workspace-id flag or the env variable. multica config set workspace_id <id> is the low-level equivalent of switch (it writes the same setting but skips the access check).
If you need full isolation between organizations or accounts — separate tokens, separate daemons, separate config dirs — use --profile <name> instead. Each profile keeps its own default workspace.
List Workspaces
multica workspace list
multica workspace list --full-id
multica workspace list --output json
The current default workspace is marked with *. Table output shows short UUID prefixes — pass --full-id when you need the canonical UUIDs.
Switch Default Workspace
multica workspace switch <workspace-id>
multica workspace switch <slug>
Verifies you have access to the workspace, then sets it as the default for the current profile. Subsequent commands without --workspace-id and MULTICA_WORKSPACE_ID target this workspace. Pair --profile if you want to change a non-default profile's workspace.
Get Details
multica workspace get <workspace-id>
multica workspace get <workspace-id> --output json
Passing no <workspace-id> resolves to the current default workspace, so multica workspace get doubles as "what workspace am I on?".
List Members
multica workspace member list <workspace-id>
Issues
List Issues
multica issue list
multica issue list --status in_progress
multica issue list --priority urgent --assignee "Agent Name"
multica issue list --assignee-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
multica issue list --full-id
multica issue list --limit 20 --output json
multica issue list --status todo --sort position # board order (the default)
multica issue list --sort created_at --direction desc # newest first
multica issue list --output json --fields=id,title,status,priority # narrow the JSON payload
multica issue list --output json --resolve-properties # property names beside the ids
Table output shows a routable issue KEY such as MUL-123; copy that key into follow-up commands like issue get, issue comment list, issue status, or --parent. Add --full-id when you need canonical UUIDs. Available filters: --status, --priority, --assignee / --assignee-id, --project, --metadata, --property, plus --limit and --offset for paging. Use --assignee-id <uuid> for unambiguous filtering when names overlap.
If the server cannot count the matching issues, the list request fails with failed to count issues instead of returning the page length as a fabricated total. Successful response fields are unchanged. This trades availability of a partial page for an explicit failure that callers can retry.
--fields (JSON output only) whitelists which top-level issue keys come back — pass a comma-separated list such as --fields=id,title,status,priority. Omit it for the full issue object, unchanged from before this flag existed. Filtering happens client-side after the CLI fetches the full response, so this shrinks CLI output size and agent context cost — not network transfer or server-side work. Field names are the real API keys, not table-display labels — assignee is assignee_type/assignee_id rather than a single assignee field. An unknown name is rejected up front with the valid list rather than silently dropped. Has no effect on --output table.
Results come back in board order (position, ascending) by default. Pass --sort to change the column (position, title, created_at, start_date, due_date, priority, or property:<name-or-id> for a custom property — select properties order by option order, and issues without the property sort last) and --direction asc|desc to flip the order. position is always ascending (it is the manual drag order), so --direction is rejected when --sort is position or omitted — use it only with title, created_at, start_date, due_date, priority, or a property: sort.
One call returns one page. --limit is the page size, 1 to 100 (the server returns at most 100 issues per request), and --offset is how many issues to skip. A limit outside 1 to 100 or a negative offset is rejected rather than quietly clamped. In table mode a line on stderr says when you are looking at a page of a larger set (Showing 1-50 of 2706 issues. Next page: --offset 50). It prints whenever the JSON has_more described below would be true or --offset is above zero, and stays silent otherwise. The of N is dropped when the server's count cannot be trusted (Showing issues 1-100. Next page: --offset 100), for the reasons below.
With --output json the envelope carries total, limit, offset and has_more. limit and offset echo the request, so limit holds steady across a walk and offset is the one you passed. The number of issues in this page is issues | length, and that is what to advance --offset by: it equals limit on a full page and stays right on any page that comes back shorter. An empty page always reports has_more: false. total cannot end a walk on its own: when the server's count query fails it reports the size of the page it just returned, and a newer backend may drop the field. So a full page reports has_more: true whenever total is missing or no larger than the page itself. That rule means a list that fills exactly one page costs one extra request that comes back empty; the alternative is a walk that ends one page in whenever the count is unhealthy. To walk a whole list, sort by created_at (--sort created_at --direction asc): the default position order is re-ranked by board drags and status changes, so pages can shift under a walk. Sorting by created_at keeps them still, but no offset walk is a consistent snapshot while others are writing.
Use --metadata key=value (repeatable; combined with AND) to filter by per-issue metadata. The value is JSON-parsed: true/false become bool, numbers become numbers, anything else is a string. Wrap as '"42"' to force a string when the value would otherwise sniff as a number:
multica issue list --metadata pipeline_status=waiting_review
multica issue list --metadata pr_number=482 --metadata is_blocked=true
Use --property "Name=Value" (repeatable; one member per flag) to filter by custom property. Names and select option values are case-insensitive and resolve to ids; repeating the same property matches any of its members, different properties must all match. Values are option names or ids (select types), true/false (checkbox), a member name, email, or id (actor types), or the stored value itself for text, url, number, and date (YYYY-MM-DD). The reserved value __none__ matches issues where the property is unset:
multica issue list --property "Impact=High" --property "Impact=Medium"
multica issue list --property "Impact=__none__" --status in_review
multica issue list --property "Score=42" --property "Ship Date=2026-08-28"
Comparisons go in the same flag. number takes >, >=, < and <=; date takes > (after) and < (before); text and url take ~= (contains, case-insensitive, matched exactly as typed). Quote the flag so the shell does not read < and > as redirection:
multica issue list --property "Score>=3" --property "Impact=High"
multica issue list --property "Ship Date<2026-09-01" --status todo
multica issue list --property "Notes~=follow up"
A comparison on a type that cannot honour it is rejected with an error naming the type. So is != (the server has no not-equal filter), and so are >= and <= on a date: the server only compares dates strictly, and the error names the strict form to use instead. Repeating a property ORs its members, so "Score>1" plus "Score<10" is not a range. A property whose name contains =, <, >, ! or ~ has to be given by its UUID.
In JSON output, properties is a map from definition id to the stored value: an option id for select, a list of option ids for multi_select, a member:<uuid> reference for the actor types, and the value itself otherwise. Pass --resolve-properties to replace that map with the rows issue property list prints, one per set property, in catalog order: property_id, name, type, the stored value, a human display, display_values (the per-item names of a multi_select or multi_actor value) and archived when the definition is archived. Archived definitions still resolve, since their values stay on the issue. An option that is no longer in the definition, or a member who has left the workspace, keeps its raw id in display. The flag adds at most two requests: the catalog, shared with --property and --sort property:, and the member list, fetched only when an actor --property filter or an actor value on the page needs it and shared between the two. If either request fails the command fails rather than printing ids. In JSON output it combines with --fields only when that list keeps properties; a --fields list without it is rejected rather than resolving a key the same command would delete. The flag has no effect on --output table, where it and --fields are both ignored and neither is rejected.
multica issue list --output json --resolve-properties | jq '.issues[] | {identifier, properties: [.properties[]? | {name, display}]}'
Get Issue
multica issue get <id>
multica issue get <id> --output json
multica issue get <id> --resolve-properties # property names beside the ids, as in issue list
Create Issue
multica issue create --title "Fix login bug" --description "..." --priority high --assignee "Lambda"
multica issue create --title "Fix login bug" --assignee-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
Flags: --title (required), --description, --status, --priority, --assignee / --assignee-id, --parent, --project, --due-date. Pass --assignee-id <uuid> (mutually exclusive with --assignee) when scripting against the IDs returned by multica workspace member list --output json / multica agent list --output json.
Update Issue
multica issue update <id> --title "New title" --priority urgent
multica issue update <id> --position 4.5
multica issue update <id> --attachment revised.png
--position sets the raw ordering value within the board column (lower sorts first). For relative moves, issue reorder is easier because it works out the value for you.
--attachment uploads a local file and appends its Markdown reference to the end of the issue description. Repeat the flag to attach multiple files. To replace an existing image, also use --description-file to remove the old reference from the description.
Reorder Issue
Move an issue within its current status column. The new ordering value is computed the same way the board's drag-and-drop computes it, so the CLI and UI agree on where the issue lands.
multica issue reorder <id> --top # top of its status column
multica issue reorder <id> --bottom # bottom of its status column
multica issue reorder <id> --before <other> # directly above another issue in the same column
multica issue reorder <id> --after <other> # directly below another issue in the same column
Pick exactly one of --top, --bottom, --before, or --after. Reorder stays inside the issue's current column, so --before / --after must name an issue in that same column. To move an issue to a different column, change its status first with issue status, then reorder within the new column.
Reorder reads the project-scoped column before computing the new position. With older servers that omit the total or substitute the page length after a failed count, it continues to an empty page instead of trusting that count. This may cost one extra request. A failed page request, malformed issue, or repeated issue aborts the operation before a position is written. These checks do not provide a consistent snapshot across concurrent edits.
Assign Issue
multica issue assign <id> --to "Lambda"
multica issue assign <id> --to-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
multica issue assign <id> --unassign
Pass --to-id <uuid> to assign by canonical UUID (mutually exclusive with --to); useful when names overlap across members and agents.
Change Status
multica issue status <id> in_progress
Built-in statuses: backlog, todo, in_progress, in_review, done, blocked,
cancelled. A workspace can define custom statuses on top of these; their keys are
shown in Settings → Issue Statuses, and passing an unknown value returns the full
list.
To cancel an issue because it duplicates another, mark it so the original lists it:
multica issue status <id> cancelled --duplicate-of <original>
--duplicate-of takes an issue key or full UUID and requires status cancelled.
issue update <id> --duplicate-of <original> does the same, but cannot be combined with
description or attachment changes. Moving the issue to any other status removes the mark.
Comments
# List comments — flat timeline, chronological. Hard cap of 2000 rows; on
# long-running issues prefer one of the thread-aware reads below to keep
# context windows tight.
multica issue comment list <issue-id>
# Single thread (root + every descendant). Anchor may be the root itself
# or any reply inside the thread — the server walks up to the root.
multica issue comment list <issue-id> --thread <comment-id>
# Single thread, capped to the N most recent replies. The thread root is
# always included (even with --tail 0), so an agent landing on a long
# thread keeps the "what is this about" context without dragging hundreds
# of replies into its prompt.
multica issue comment list <issue-id> --thread <comment-id> --tail 30
# Scroll older replies inside the same thread. --before / --before-id are
# the reply cursor that the previous response emitted on stderr as
# `Next reply cursor: --before <ts> --before-id <reply-id>`.
multica issue comment list <issue-id> --thread <comment-id> --tail 30 \
--before <ts> --before-id <reply-id>
# Most recently active threads (root + every descendant), grouped by
# thread. Returns N complete conversational arcs, oldest-active first so
# the freshest thread sits closest to "now" in an agent prompt.
multica issue comment list <issue-id> --recent 10
# Scroll older threads. Under --recent, --before / --before-id are a
# THREAD cursor (thread last_activity_at + root id), emitted on stderr as
# `Next thread cursor: --before <ts> --before-id <root-id>`.
multica issue comment list <issue-id> --recent 10 \
--before <ts> --before-id <root-id>
# Incremental polling. Combines with --thread or --recent; filters out
# replies created on or before <ts> from the page (the thread root is
# exempt so the agent always gets context).
multica issue comment list <issue-id> --thread <comment-id> --tail 30 \
--since <RFC3339-timestamp>
# Add a comment
multica issue comment add <issue-id> --content "Looks good, merging now"
# Reply to a specific comment
multica issue comment add <issue-id> --parent <comment-id> --content "Thanks!"
# Add the comment to an agent's running turn instead of queueing a
# follow-up run (--steer is repeatable; --steer-active picks every running
# agent you may steer). You may steer an agent exactly when your comment could
# start it; an agent never steers itself and adds at most 3 messages to one
# run. Prints per run whether it was steered.
multica issue comment add <issue-id> --content-file note.md --steer <run-id>
# Delete a comment
multica issue comment delete <comment-id>
--before / --before-id semantics depend on the paging mode, by
design — same flag, different scope:
| Mode | What the cursor walks | stderr label |
|---|---|---|
--recent N |
Older threads (last_activity_at, root_id) | Next thread cursor |
--thread <id> --tail N |
Older replies inside that thread (created_at, id) | Next reply cursor |
Outside those two modes (--thread without --tail, or no --thread
and no --recent) the cursor flags are rejected so they cannot silently
no-op. The server emits the cursor headers (X-Multica-Next-Before /
X-Multica-Next-Before-Id) only when an older page actually exists —
exact-boundary pages (e.g. --tail 3 on a thread with exactly 3
replies) intentionally return no cursor so callers stop paginating.
When --since is combined with --recent or --thread --tail, the
server additionally suppresses the cursor once the cursor target itself
is older than since. Older pages walk strictly older rows, so they
cannot satisfy > since either — emitting a cursor there would just
hand back root-only pages until the caller reaches the start of the
thread / issue. Incremental polling stops at the first page whose
cursor target falls before the watermark.
Metadata
Per-issue metadata is a small KV map agents use to track pipeline state (PR number, pipeline status, waiting_on, ...). Keys match ^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$, values are primitives (string / number / bool), max 50 keys per issue, blob capped at 8KB.
The bar for writing is high: pin a value only when it is materially important to the issue AND likely to be re-read by future runs on this same issue (the PR URL, the deploy URL, what we're blocked on). Most runs write zero new keys — that's the expected case. Don't pin runtime bookkeeping like attempts, single-run investigation notes, large logs, secrets/tokens, or description/comment copies — see the agent runtime prompt for the full anti-pattern list.
# List every key on an issue
multica issue metadata list <issue-id>
# Read a single key
multica issue metadata get <issue-id> --key pipeline_status
# Write a single key — value auto-typed (true/false → bool, numbers → number, else string)
multica issue metadata set <issue-id> --key pipeline_status --value waiting_review
multica issue metadata set <issue-id> --key pr_number --value 482
multica issue metadata set <issue-id> --key is_blocked --value true
# Force a specific type when sniffing would pick the wrong one
multica issue metadata set <issue-id> --key code --value 42 --type string
# Remove a key
multica issue metadata delete <issue-id> --key pipeline_status
All writes are single-key atomic — concurrent agents writing different keys do not lose each other's updates. To query, use multica issue list --metadata key=value (see List Issues above).
Subscribers
# List subscribers of an issue
multica issue subscriber list <issue-id>
# Subscribe yourself to an issue
multica issue subscriber add <issue-id>
# Subscribe another member or agent by name
multica issue subscriber add <issue-id> --user "Lambda"
# Unsubscribe yourself
multica issue subscriber remove <issue-id>
# Unsubscribe another member or agent
multica issue subscriber remove <issue-id> --user "Lambda"
Subscribers receive notifications about issue activity (new comments, status changes, etc.). Without --user, the command acts on the caller.
Execution History
# List all execution runs for an issue
multica issue runs <issue-id>
multica issue runs <issue-id> --full-id
multica issue runs <issue-id> --output json
# Only work in flight (queued / dispatched / running / waiting_local_directory)
multica issue runs <issue-id> --active --output json
# ...and across the sub-issue family: the issue's parent (or itself, when it has
# no parent) plus every child of that parent, each row labelled with its issue.
# Answers "is another agent already working next to me?" before you start
# overlapping code or PR work. Advisory only — it reserves nothing.
#
# Returns a compact per-run row — task_id, issue_id, issue_identifier,
# issue_title, agent_id, status, created_at, started_at — not the full
# execution-log record. Follow a task_id with `issue run-messages` for detail.
#
# Ordered running-first, newest-first within a status, capped at 20 rows. When
# the cap truncates the answer the server sets X-Active-Runs-Truncated and the
# CLI warns on stderr, so a short list is never mistaken for a complete one.
multica issue runs <issue-id> --siblings --output json
# View messages for a specific execution run
multica issue run-messages <task-id>
multica issue run-messages <short-task-id> --issue <issue-id>
multica issue run-messages <task-id> --output json
# Incremental fetch (only messages after a given sequence number)
multica issue run-messages <task-id> --since 42 --output json
# Aggregated token usage for an issue (sum across all its task runs)
multica issue usage <issue-id>
multica issue usage <issue-id> --output json
The usage command returns the aggregated token usage for an issue, summed across all of its task runs: input tokens, output tokens, cache read/write tokens, and the run count (task_count). It wraps GET /api/issues/<id>/usage — the same figures the issue detail view shows. Use --output json to feed billing/cost tooling.
The runs command shows all past and current executions for an issue, including running tasks. Table output uses short task UUID prefixes by default; pass --full-id to print canonical task UUIDs. The run-messages command accepts full task UUIDs directly; copied short task prefixes must be scoped with --issue <issue-id> so the CLI only checks that issue's runs. It shows the detailed message log (tool calls, thinking, text, errors) for a single run. Use --since for efficient polling of in-progress runs.
Projects
Projects group related issues (e.g. a sprint, an epic, a workstream). Every project belongs to a workspace and can optionally have a lead (member or agent).
List Projects
multica project list
multica project list --status in_progress
multica project list --output json
Available filters: --status.
Get Project
multica project get <id>
multica project get <id> --output json
Create Project
multica project create --title "2026 Week 16 Sprint" --icon "🏃" --lead "Lambda"
Flags: --title (required), --description, --status, --icon, --lead, --start-date, --due-date. Dates are calendar days (YYYY-MM-DD).
Update Project
multica project update <id> --title "New title" --status in_progress
multica project update <id> --lead "Lambda"
multica project update <id> --due-date 2026-04-15
Flags: --title, --description, --status, --icon, --lead, --start-date, --due-date. For the date flags, pass an empty string (e.g. --start-date "") to clear the date.
Change Status
multica project status <id> in_progress
Valid statuses: planned, in_progress, paused, completed, cancelled.
Delete Project
multica project delete <id>
Associating Issues with Projects
Use the --project flag on issue create / issue update to attach an issue to a
project, or on issue list to filter issues by project:
multica issue create --title "Login bug" --project <project-id>
multica issue update <issue-id> --project <project-id>
multica issue list --project <project-id>
Setup
# One-command setup for Multica Cloud: configure, authenticate, and start the daemon
multica setup
# For local self-hosted deployments
multica setup self-host
# Custom ports
multica setup self-host --port 9090 --frontend-port 4000
# On-premise with custom domains
multica setup self-host --server-url https://api.example.com --app-url https://app.example.com
multica setup configures the CLI, opens your browser for authentication, and starts the daemon — all in one step. Use multica setup self-host to connect to a self-hosted server instead of Multica Cloud.
Configuration
View Config
multica config show
Shows config file path, server URL, app URL, and default workspace.
Set Values
multica config set server_url https://api.example.com
multica config set app_url https://app.example.com
multica config set workspace_id <workspace-id>
config set workspace_id <id> is the low-level interface — it writes the value verbatim without checking that the workspace exists or that you have access. Prefer multica workspace switch <id|slug> for day-to-day workspace changes; it does both checks before saving.
Autopilot Commands
Autopilots are scheduled/triggered automations that dispatch agent tasks (either by creating an issue or by running an agent directly).
List Autopilots
multica autopilot list
multica autopilot list --full-id
multica autopilot list --status active --output json
Autopilot table IDs are short UUID prefixes; follow-up autopilot commands accept copied prefixes when they are unique in the current workspace. Use --full-id to print canonical UUIDs.
Get Autopilot Details
multica autopilot get <id>
multica autopilot get <id> --output json # includes triggers
In JSON output triggers is a top-level key alongside autopilot, not nested
inside it — the payload is {"autopilot": {...}, "triggers": [...], "collaborators": [...]}.
Read trigger ids with jq '.triggers[].id', or use autopilot trigger-list below.
The table output shows only the autopilot's own fields, not its triggers.
Create / Update / Delete
multica autopilot create \
--title "Nightly bug triage" \
--description "Scan todo issues and prioritize." \
--agent "Lambda" \
--mode create_issue \
--subscriber "Alice"
multica autopilot update <id> --status paused
multica autopilot update <id> --description "New prompt"
multica autopilot update <id> --subscriber "Alice" --subscriber "Bob"
multica autopilot update <id> --clear-subscribers
multica autopilot delete <id>
--mode accepts create_issue (creates a new issue on each run and assigns it to the agent) or run_only (enqueues a direct agent task without creating an issue). --agent accepts either a name or UUID.
--subscriber accepts a workspace member name or user ID and may be repeated; on update it replaces the autopilot's subscriber template. Subscribers receive inbox notifications for issues created by a create_issue autopilot. Use --clear-subscribers to remove all autopilot subscribers.
Manual Trigger
multica autopilot trigger <id> # Fires the autopilot once, returns the run
The command exits non-zero unless the run actually started (issue_created or
running). A skipped run — admission refused, runtime offline, quota
exhausted, a duplicate already in flight — dispatched nothing; its
failure_reason and reason_code are printed to stderr, and --output json
still writes the full run to stdout first.
Run as an agent (inside a task, or over A2A), the trigger is authorized as the human that run acts for, not as the owner of the machine it executes on. That human needs exactly the write access they would need to trigger it themselves — and it is the only access checked: the machine's owner needs no grant on the autopilot, only workspace membership. A run carrying no originator cannot trigger at all, and says so rather than failing generically.
Run History
multica autopilot runs <id>
multica autopilot runs <id> --limit 50 --output json
Schedule Triggers
multica autopilot trigger-list <autopilot-id> # ids, kind, schedule, next run
multica autopilot trigger-list <autopilot-id> --full-id # canonical UUIDs
multica autopilot trigger-add <autopilot-id> --cron "0 9 * * 1-5" --timezone "America/New_York"
multica autopilot trigger-update <autopilot-id> <trigger-id> --enabled=false
multica autopilot trigger-delete <autopilot-id> <trigger-id>
trigger-list is the way to obtain the <trigger-id> that trigger-update,
trigger-delete and trigger-rotate-url require. Like autopilot ids, trigger ids
may be passed as a short prefix as long as it is unique within that autopilot; use
--full-id to print canonical UUIDs. Webhook credentials are redacted in this
output — use autopilot get <id> --output json --show-secrets to reveal them.
The CLI exposes cron-based schedule triggers via trigger-add, and webhook
triggers via trigger-add --kind webhook plus trigger-rotate-url. The data model
also defines an api kind, which is not surfaced here.
Other Commands
multica version # Show CLI version and commit hash
multica update # Update to latest version
multica agent list # List agents in the current workspace
Output Formats
Most commands support --output with two formats:
table— human-readable table (default for list commands)json— structured JSON (useful for scripting and automation)
multica issue list --output json
multica daemon status --output json
Error Messages
The CLI funnels command errors returned to the top-level handler through a
single user-facing translation layer (server/internal/cli/errors.go) so that
what you see on the terminal is a short, actionable sentence rather than a raw
Go error, an HTTP status line, or an internal resolve issue: ... chain. (A
few commands print their own output or run deliberate fast probes — for example
setup's short /health reachability check — and don't go through this
layer.) The underlying detail is still available on demand (see --debug).
What you see
- Friendly, single-line message. Transport failures (timeout, DNS,
connection refused, TLS) and HTTP status failures (401/403/404/409/400·422/
429/5xx) are each rendered as one clear sentence with a next step — for
example a timeout suggests checking the network or raising
MULTICA_HTTP_TIMEOUT, and a 401 tells you to runmultica login. - Server-provided validation messages are preserved. For a 400/422 that
carries a message from the server, that message is shown verbatim
(
Invalid request: <server message>); only when there is none do you get the generic "check your values / run with --help" hint. - No leaked internals by default. Raw URLs, status lines, JSON bodies, and the internal verb chain are hidden unless you ask for them.
Language
Messages default to English, matching the rest of the CLI's help output.
If a Chinese locale is detected in LC_ALL, LC_MESSAGES, or LANG (in that
precedence order), messages switch to Chinese. No flag is needed; set the
locale as usual:
LANG=zh_CN.UTF-8 multica issue get MUL-9999 # 错误信息显示为中文
Exit codes
The process exit code is tiered so scripts can branch on the failure class:
| Exit code | Meaning |
|---|---|
0 |
success |
1 |
generic / unclassified error |
2 |
network error (timeout, DNS, connection refused, TLS, offline) |
3 |
authentication / authorization (HTTP 401, 403) |
4 |
not found (HTTP 404) |
5 |
validation (HTTP 400, 422) |
multica issue get MUL-9999
if [ $? -eq 4 ]; then echo "no such issue"; fi
Seeing the full detail (--debug)
Pass the global --debug flag (or set MULTICA_DEBUG=1) to print the complete
original error chain — the internal verb chain, the request method/path/status,
and the raw server body — underneath the friendly message. Use it when you need
to file a bug or understand exactly what the server returned:
multica issue list --debug
MULTICA_DEBUG=1 multica issue update MUL-1234 --title "x"
Request timeout
API requests use a default timeout of 30 seconds. Override it with
MULTICA_HTTP_TIMEOUT when you are on a slow network; it accepts a Go duration
(45s, 2m) or a plain number of seconds (45). Command-level deadlines are
always at least this value, so raising it takes effect across all commands.
MULTICA_HTTP_TIMEOUT=60s multica issue list
Stall detection (skill commands)
A total-elapsed timeout punishes the transfer that is working: a large skill
arriving steadily over a slow link is cut off mid-body, while a dead connection
is held open for the full budget. The skill commands therefore fail on a lack
of progress instead:
- A read that receives no bytes for 15 seconds fails immediately, reported as a stalled transfer rather than a timeout.
- A transfer that keeps producing bytes runs to completion, however long it takes, behind a loose 10 minute whole-request ceiling.
Override the no-progress budget with MULTICA_HTTP_STALL_TIMEOUT (same format
as MULTICA_HTTP_TIMEOUT). If only MULTICA_HTTP_TIMEOUT is set it applies on
this path too, as the no-progress budget — it keeps meaning "the longest I will
wait for this server", not "the longest this download may take".
MULTICA_HTTP_STALL_TIMEOUT=45s multica skill get <id>
Every other command still uses the total-elapsed timeout above. Stall detection starts here because skill payloads are the largest responses the CLI reads; the mechanism itself is not skill-specific.
Skill payload size
multica skill get and multica skill files list return metadata only by
default — path, byte size and content hash for each file, plus the size and
hash of the SKILL.md body. Sizes are what tell you which file makes a skill
large, and they stay available no matter how large it gets.
Pass --with-content when you actually need the bodies:
multica skill files list <id> # paths and sizes
multica skill files list <id> --with-content # bodies inlined
On the API, both endpoints accept ?include=content and ?include=metadata.
A request that sends neither still gets content, on both endpoints, so a
server upgrade never changes what an un-upgraded client receives — it is the
CLI that asks for the smaller shape.
Custom runtime compatibility targets
Create custom Oh-My-Pi profiles with multica runtime profile create --runtime-type omp --command-name omp --display-name "Custom Oh-My-Pi".
The immutable runtime_type selects model discovery, skills paths, and launch behavior;
the server derives protocol_family (pi for omp). Custom command/path overrides and
fixed arguments still apply, and the runtime retains its custom-profile provenance.
Existing profiles and the legacy --protocol-family flag retain their original target.