Files
multica/CLI_AND_DAEMON.md
bb3747186e feat: let agents steer running turns they could start (MUL-8285) (#9630)
* 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]>
2026-10-28 17:09:59 +08:00

73 KiB
Raw Permalink Blame History

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

  1. On start, the daemon detects installed agent CLIs and registers a runtime for each agent in each watched workspace
  2. 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
  3. When a task arrives, it creates an isolated workspace directory, spawns the agent CLI, and streams results back
  4. Heartbeats are sent periodically (default: 15s) so the server knows the daemon is alive
  5. 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 done or cancelled and has been idle for MULTICA_GC_TTL, the entire task directory is removed.

  • Completed-task retention bound — MULTICA_GC_COMPLETED_TASK_TTL fully removes an inactive issue task once its .gc_meta.json completed_at age 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 a local_directory environment. A later rerun provisions a fresh environment instead of resuming the removed checkout.

    • The default depends on where the daemon points: 14d against Multica Cloud, and 0 (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 explicit 0 disables 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/ and logs/. The per-issue Codex session store lives outside MULTICA_WORKSPACES_ROOT under 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 above MULTICA_GC_INTERVAL: the active-root guard protects a task that is currently running, not one whose follow-up run is queued but unclaimed.
  • Orphan cleanup — task directories with no .gc_meta.json (e.g. left over from a daemon crash) are removed once they exceed MULTICA_GC_ORPHAN_TTL.

  • Artifact-only cleanup — when a task has been completed for at least MULTICA_GC_ARTIFACT_TTL but the issue is still open, regenerable build outputs whose directory basename matches MULTICA_GC_ARTIFACT_PATTERNS are removed. The daemon also reclaims the exact managed path codex-home/.sandbox-bin; old task metadata without completed_at becomes eligible for this managed-only cleanup after its .gc_meta.json file has been idle for MULTICA_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. Set MULTICA_GC_ARTIFACT_TTL=0 to 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 a git worktree off one of them rather than its own clone, so a task's .git is 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 for MULTICA_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 expire and git 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 with MULTICA_GC_REPO_MAINTENANCE_ENABLED=false without 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 for MULTICA_GC_HERMES_SESSION_TTL is 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 for MULTICA_GC_HERMES_MEMORY_TTL is 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, or MULTICA_AGENT_TEMP_BASE), exported to the agent as TMPDIR/TMP/TEMP. On Windows, every task instead inherits the daemon user's TMP/TEMP: Git Bash's /tmp mount 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. TMPDIR stays task-local, and for Claude Code CLAUDE_CODE_TMPDIR defaults 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 outside MULTICA_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_TTL defaults to 0, 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 a TMPDIR that 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 the TMPDIR of a task that is starting. Those shells are a few bytes each. Only entries carrying the multica-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_config must use the canonical envelope, {"mcpServers": {"<name>": {…}}}. Runtime-native config files that nest servers under servers, mcp, or mcp_servers are 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/env for stdio, url/headers/type for remote).
  • Remote transports depend on what the runtime declares. ACP v1 requires an omitted capability to be treated as unsupported, so http and sse entries are dropped with a warning unless the initialize response declares agentCapabilities.mcpCapabilities with that transport set to true. The built-in Hermes runtime is a verified exception: it declares no mcpCapabilities yet accepts both transports, so remote entries are still forwarded to it. That exception covers the Hermes binary only — a custom runtime profile with protocol_family: hermes runs 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/memories is 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):

  1. --workspace-id <id> flag on the command
  2. MULTICA_WORKSPACE_ID environment variable
  3. The default workspace stored in your current profile (set by multica workspace switch or multica 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 run multica 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.