Skip to content

CLI Commands

Every mur command, its flags, and what each one does.

Commands

Command Purpose
mur build Build a .mur.zip from a source directory
mur new Scaffold a formation, or (beta) generate a murmur.yaml from a plain-language task description
mur publish Publish a built artifact into the global store
mur install Fetch and install artifacts from the global store, the configured sources, or a file
mur precompile Compile WASM artifact files for this machine, so their first launch skips compiling
mur list List installed artifacts in the project or global store
mur doctor Check every artifact declared in murmur.yaml against the project and global stores
mur version Print this mur's version, the WIT level it serves and that level's package versions
mur run Run a capsule with lockfile-aware artifact resolution
mur ps List the capsules running on this machine
mur token Print a door token of a running capsule
mur watch Stream live events from a running capsule's output to stdout
mur cancel Stop one running task on a capsule, leaving the session running
mur stop End one running capsule, or every member of a formation, and report what was left behind
mur control Show or change what a running capsule's control: block lets a controller change
mur deploy run Upload a capsule to an existing VM and return its public URL
mur deploy ls List all deployed capsules
mur destroy Remove a deployment record from the local tracking list
mur conversation ls List the durable conversation records, or place one message id in them
mur conversation show Print one context: its record header, every message and its harness session
mur conversation rm Remove one context's record directory, whole
mur conversation truncate Drop the oldest messages from a record, keeping the newest N
mur trace show Human-readable summary of a single trace.jsonl session
mur trace steps Turn-by-turn tree of what one session's agent did
mur trace diff Side-by-side metric comparison of two sessions
mur trace report Aggregate statistics across a set of sessions
mur trace list List the sessions of one workdir root, newest first
mur trace rm Remove one ended session's records directory, the work folder the runtime created for it, and its stale running record
mur eval show Human-readable (or JSON) summary of a single eval.jsonl session
mur eval diff Side-by-side scorer comparison of two eval sessions, or with --run the arms of one mur eval run
mur eval run Drive a multi-case dataset against a capsule, and optionally a variant, and judge each run by the case's check
mur search Search an artifact index for artifacts matching a keyword
mur index Write the artifact index for one release's .mur.zip assets
mur topology Render capsule sessions as a DAG from Grafana Tempo OTel data

Piped output

Every mur command stops writing when the program reading its output goes away, as mur trace show <session> | head -1 does after one line, and exits with the status it would have had with its output open.

What happens to the output Effect on the command
Its reader closes standard output or standard error The rest of that stream's output is dropped. The command finishes its work — an install or removal completes — and exits with its own status
Its reader closes the stream under mur watch The watch ends with exit 0. The capsule keeps running
A write to standard output fails for another reason, such as a full disk A command that otherwise succeeded reports E-IO-003 failed to write to standard output and exits 1. A command that failed keeps its own error
Its reader closes either stream under mur run The session keeps running to its own end and keeps the lines it could not write. See A closed stream

Driving mur from another program

A program that runs mur and parses its output asks mur version --json once what this mur serves, and looks its command up in commands:

What commands holds for the command What the program does
A number the program was written for Run the command and parse its output
A number the program does not know Refuse with a message: the command's output changed shape
No key Refuse with a message: this mur does not serve the command

Compare commands, not mur_version. A release reports X.Y.Z. A build from main reports X.Y.Z-dev, where X.Y.Z is the next release, and its commit names the build, or is null for a build made outside a git checkout.

A command's number moves only when its output breaks for an existing consumer:

  • a key removed or renamed;
  • a key's type or meaning changed;
  • a different number of lines or objects.

A new key does not move it, and a consumer ignores keys it does not know. A command that is removed loses its key.

mur --version prints murmur-cli <version>, for people and installers.

Key Number Output
control show --json 1 mur control
conversation ls --json 1 mur conversation ls
conversation show --json 1 mur conversation show
eval diff --run --json 1 mur eval diff --run
eval show --json 1 mur eval show
precompile --json 1 mur precompile
ps --json 1 mur ps --json
run --explain-scope --json 1 The --explain-scope row of mur run
run --json 1 Readiness line
run --roster --json 1 Launching a formation
token 1 mur token: standard output is the token
trace list --json 1 mur trace list
trace rm --json 1 mur trace rm
version --json 1 mur version

Session addresses

Every command that names a session spells the address the same way.

Form Example Names
Full ID ses_019f01a940ce7761854e768ecbe3d399 The session with that ID: ses_ followed by 32 hex characters
Suffix d399 The one session whose ID ends with those characters, matched case-insensitively. 4 characters or more. Two or more matches are refused, and the refusal lists them
Ordinal @1, @2 The most recent session, the second most recent, and so on. Session IDs sort in creation order, so @N counts back from the newest
Path ~/.murmur/records/…/ses_019f…/trace.jsonl The record file at that literal path, taken verbatim. mur run --resume also accepts the session's records directory itself

What an address is resolved against depends on what the command needs.

Command Candidate set @1 means
mur run --resume, mur trace show, mur trace steps, mur trace diff, mur trace report, mur eval show, mur eval diff The ses_* records directories of one workdir root, whether or not the session is still running The most recent recorded session
mur watch, mur cancel, mur stop, mur token The running-capsule records for this whole machine The most recent running session

The recorded set is the records group of one workdir root: the directory a run was given as --workdir, or <manifest-dir>/workdir. mur trace list prints it in @N order. The mur trace and mur eval commands take it as their own --workdir, ./workdir by default; mur run --resume uses its launch's own. Each group is under ~/.murmur/records, so these commands need a HOME that can hold ~/.murmur and are refused with E-IO-004 otherwise; the path form reads no group. See Records directory.

mur watch, mur cancel, mur stop and mur token have to reach a process, so they take the three forms that name a session and refuse the path form: a path names a directory on disk, which says nothing about whether a process is running. An address naming a session that has stopped reports E-RUN-022 rather than resolving to a different capsule.

Omitting the address selects a default:

Command Bare form means
mur run --resume @1
mur trace show @1
mur trace steps @1
mur trace diff @2 @1
mur trace report every session in the workdir
mur eval show @1
mur eval diff @2 @1
mur eval diff --run @1, the most recent eval run
mur watch @1
mur token @1

mur trace diff and mur eval diff take their arguments in before, after order, so the bare @2 @1 puts the older run in the Run A column and the delta column reads forwards in time. Both take two addresses or none; one address is refused.

An address matching no session, or several, is refused with E-TRC-002 under mur run and mur trace, E-EVAL-002 under mur eval, and E-RUN-022 under mur watch, mur cancel, mur stop and mur token.


Running-capsule records

Every running session holds one record on this machine, and removes it when the session ends. An agent session's record says where its A2A door is; a script capsule binds no door, and its record's url is null.

Property Value
Location ~/.murmur/running/<session_id>.json
Directory mode 0700
File mode 0600
Written by The runtime: for an agent session, once the capsule is serving its door and just before mur run prints its URL; for a script capsule, once its trace's session_start is written
Removed by The runtime, when the session ends

A session whose manifest declares control: also holds its control token beside its record, at ~/.murmur/running/<session_id>.control, mode 0600, removed with the record — see Control surface.

Each record carries the session id, the capsule address as url (null for a script capsule), the process id and its start time, the capsule name and version, the session's work folder as workdir, the session's records directory as records_dir, whether the session outlives its launcher, and the time it started. It carries nothing from the environment: no API key, no granted variable.

A record that is a JSON object with a string session_id and no records_dir key was written by an older mur. Its capsule may still be running, and only the mur that started it can stop it.

Command What it does with an older mur's record
mur ps Names it on stderr, as a skipped: line, and lists no row for it
Every command, mur ps included Leaves it in place: none reads, verifies, signals or removes it, so mur stop, mur watch, mur cancel and mur token answer as if it were absent

A session that is a member of a formation also records its formation, and a member mur run --roster launched records its roster name and whether it is the entry member. A session in no formation writes none of these keys. They group and label records; none grants anything.

Key Written when Value
formation_id The session is a member of a formation, a member's delegated sub-capsule and a session started by hand with MURMUR_FORMATION_ID included The formation id
formation_member mur run --roster launched the session as a member. A member's delegated sub-capsule and a session started by hand with MURMUR_FORMATION_ID write no such key The member's roster name, the session_start.formation_member of its trace. Members running the same capsule share a capsule_name; this name tells them apart
formation_entry mur run --roster launched the session as the formation's entry member, as its formation channel says. Every other member, a member's delegated sub-capsule and a session in no formation write no such key true

A record whose formation_id is not a formation id, or whose formation_member is not a member name, is unreadable, and is removed on the next read like any other unreadable record.

Three more keys say what ends the session other than mur stop. Each is written only when it applies, so a standalone session's record carries none of them.

Key Written when Value
formation_lifeline The session holds a formation lifeline: mur run --roster started it true
formation_launcher The session holds a formation lifeline and its formation channel named the launcher {"pid": <pid>, "process_start": "<start time>"} — the launcher's process id and start time, read the same way as the record's own
spawned_by The session is a delegated sub-capsule The session id of the session that delegated to it

mur stop <formation-id> reads these keys to find the launcher and to decide which members it stops itself. A formation_launcher that is not an object with an integer pid and a string process_start makes the record unreadable.

A session whose manifest declares network.authentication also records every door token it minted. A public door's record carries neither key.

Key Written when Value
door_token The manifest declares network.authentication The operator token. mur ps, mur stop, mur watch and mur cancel present it to the door when they name the session by address
credentials network.authentication.credentials declares at least one credential {"<name>": "<token>", …}, one entry per declared credential. The operator token is never an entry

mur token prints either. mur ps prints neither.

Taken together the records are a map of every reachable capsule on the machine, readable by anything running as the same user. The 0700 directory and 0600 files are what keep that map owner-only, and both modes are reapplied on every write.

A record is a hint

Nothing can guarantee a record is removed — a capsule killed outright writes no farewell — so every read verifies it in three layers, and a session is reported as running only when all three hold.

Layer Question Alone it proves
1 Is a process holding that process id? Little: process ids are handed out again
2 Did that process start when the record says it did? That the process id was not reused by something unrelated
3 Does the capsule's agent card answer, naming that session? That the capsule is the one being addressed and can still respond

Layer 3 reads the public card, or, for a record holding a door_token, the extended card with that token. A record with a null url has no door to ask: layers 1 and 2 decide it, and a process that passes both is running.

Reading the records removes a record only on evidence that its process is gone — that is the only sweep there is, and it is enough because a record is never treated as truth.

Reading The record Signalled by mur stop
Layer 1: no process holds the process id Removed No
Layer 2: the process started at another time Removed — the process id was reused No
Layer 2: the process's start time could not be read Kept, reported as unreachable No
Layer 3: the agent card did not answer for the session Kept, reported as unreachable Yes

A kept record names a process that is alive, and the command reports E-RUN-023 instead of throwing the address away. A capsule started by mur v0.4.0 or earlier fails layer 3 until it ends: this version cannot read a session from its agent card.

When ~/.murmur/running/ itself cannot be read, every command that reads the records fails with E-RUN-028 and removes nothing.

Ordinals count over records that pass layers 1 and 2, so a capsule that has stopped never shifts the numbering of the ones still running.

Whether a capsule outlives its launcher

The outlives_launcher field is read from whether the launching process has a controlling terminal, which /dev/tty answers regardless of where the streams were redirected.

Launch Controlling terminal outlives_launcher
Started in a terminal window Yes false — the capsule ends with that window
Started with no terminal attached — nohup, a service manager, a detached session No true

mur new

mur new has two forms:

Form Writes Available
mur new --roster <NAME> ./<NAME>/: a formation of two capsules, from fixed templates In every build
mur new "<task description>" ./murmur.yaml, generated by a model Beta: only in a mur built with the beta-mur-new feature, and only after mur beta enable mur-new

--roster cannot be combined with <task description>; together they are a usage error, exit status 2. Both forms take --index.

Both forms write every default artifact at the newest version that fits this mur in a published artifact index, chosen as mur install <scope>/<name> chooses a version: the index on the latest default-artifacts release, read through the built-in murmur source mur install reads it from, or the index --index names. mur new has no version of its own: with no index to read it refuses with E-NEW-006, and with no fitting version in it with E-NEW-007.

mur new --roster

Write a new directory holding a formation that roster admission accepts and mur run --roster launches without editing. The files are fixed templates filled in with the name, the inference: block of ~/.murmur/config.yaml, and the driver version read from an artifact index: no model is called, no key is read, and no store is contacted.

mur new --roster <NAME> [--index <PATH|URL>]
Argument / Flag Required Description
--roster <NAME> yes The formation's name: the directory written, and the prefix of each member's capsule name. <NAME>, <NAME>-lead and <NAME>-worker must all be artifact names: lowercase letters, digits and -, not starting or ending with -, at most 100 characters. Anything else is E-NEW-002
--index <PATH\|URL> no The artifact index the driver version comes from; see Driver version. Default: the index on the latest default-artifacts release

Layout

<NAME>/
  roster.yaml          the formation: lead is the entry member, and lead may call worker
  lead/murmur.yaml     the entry member, capsule local/<NAME>-lead
  worker/murmur.yaml   the member lead may call, capsule local/<NAME>-worker
Convention Value
Formation directory ./<NAME>/, the directory mur run --roster <NAME> takes
Roster <NAME>/roster.yaml, at the directory's root
Member sources <NAME>/<member>/murmur.yaml: one subdirectory per member, named after the member
Capsule names <NAME>-<member> in each member's murmur.yaml, so two scaffolded formations never share a capsule name. A member's .mur.zip is installed from a file, so the roster runs it as local/<NAME>-<member>
Versions 0.1.0 for every capsule
Install scope The global store, mur install -g. The directory has no top-level murmur.yaml, so mur install without -g refuses there

Generated files

Every key carries a YAML comment saying what it does, and every top-level key's comment links its section of this reference.

roster.yaml:

Key Value
roster_version 1
members[0] name: lead, capsule: local/<NAME>-lead, version: 0.1.0
members[0].entry true
members[1] name: worker, capsule: local/<NAME>-worker, version: 0.1.0
reachability One rule: from: lead, to: [worker]. Its comment explains that all would grant nothing here: all pairs only members that both serve peers, and lead does not

lead/murmur.yaml and worker/murmur.yaml:

Key lead worker
name <NAME>-lead <NAME>-worker
version 0.1.0 0.1.0
runtime capsule capsule
execution static: mur build packs only the murmur.yaml static
artifacts[0].name, .version The provider's driver, from Provider The same
artifacts[0].runtime driver driver
artifacts[0].gateway.endpoint The provider's endpoint The same
artifacts[0].gateway.api_key The provider's key reference, never a key The same
inference.transport http http
inference.model The provider's model The same
inference.driver.artifact The provider's driver The same
inference.system_prompt Names the member and the formation, and the hand-off: lead does not do the task itself, its first reply hands the task to worker with call-member, it ends the turn while worker works, and it answers with worker's answer when it arrives One sentence naming the member and the formation
lifecycle Absent. A comment in its place says that mur run --roster sets the entry member's lifecycle on every launch task_acceptance: queue, after_task: sleep: it waits at its door while the formation runs
capabilities.network.allow [localhost], the entry through which call-member reaches worker's door — see Giving a member work Absent: worker declares no capabilities
exports.peer_tasks.accept Absent: lead serves no peer true, because the rule calls worker
network.authentication.scheme bearer, because a roster with a rule needs every member's door authenticated bearer

Neither member declares capabilities.spawn.allow, which network.authentication refuses. Without lead's capabilities.network.allow, every call-member call fails and the launch prints W-RUN-008.

Provider

The provider comes from inference.provider, inference.endpoint and inference.model in ~/.murmur/config.yaml. Only the global file is read, and mur new --roster never writes it. inference.api_key is ignored. An empty or absent field takes the provider's default.

inference.provider Driver Default endpoint Default model gateway.api_key
anthropic, empty, or no inference: block murmur/driver-anthropic, 0.19.0 in the latest release https://api.anthropic.com claude-haiku-4-5-20251001 ${ANTHROPIC_API_KEY}
openai murmur/driver-openai, 0.19.0 in the latest release https://api.openai.com/v1 gpt-5.6-luna ${OPENAI_API_KEY}
Any other value Refused with E-NEW-004 — — —

The driver's version is resolved each time the command runs; see Driver version.

A configured inference.endpoint must pass gateway.endpoint validation, or the command refuses with E-MAN-003 naming inference.endpoint.

Driver version

Both members pin the newest version of the provider's driver that fits this mur: its WIT level and a payload for this platform, as mur search lists in its VERSION column and mur install <scope>/<name> with no version installs.

--index Index read
Omitted artifacts-index.json on the latest release of murmur-nexus/default-artifacts, through the built-in murmur source. A GITHUB_TOKEN, or the source's token:, is sent as mur install sends it
A value starting http:// or https:// That URL, fetched by mur
local Refused with E-NEW-005 before the config is read: the global store is not a published index
Any other value A file path, relative to the working directory or absolute

The index must be a schema 2 index (E-IDX-002 otherwise) of the driver's scope, murmur.

Output

On success, stdout lists the files written, then the steps that install and launch the formation. For mur new --roster crew with no inference: block:

Scaffolded formation 'crew' in ./crew
  crew/roster.yaml          lead is the entry member; lead may call worker
  crew/lead/murmur.yaml     capsule local/crew-lead@0.1.0
  crew/worker/murmur.yaml   capsule local/crew-worker@0.1.0, serves peers
  Driver: murmur/driver-anthropic@1.0.0, the newest that fits this mur in github:murmur-nexus/default-artifacts v1.0.0

Next:
  mur config set -g credentials.ANTHROPIC_API_KEY <your key>
  mur install -g murmur/driver-anthropic@1.0.0
  mur build crew/lead && mur install -g crew/lead/crew-lead-0.1.0.mur.zip
  mur build crew/worker && mur install -g crew/worker/crew-worker-0.1.0.mur.zip
  mur run --roster crew --task "<your task>"

The Driver: line names the driver version the members pin and the index it was taken from: <source> <release tag> for the default index, else the --index value as given.

Step Does
mur config set -g credentials.<KEY> <your key> Stores the key gateway.api_key references in ~/.murmur/config.yaml, where mur run re-reads it at launch — see gateway.api_key resolution. mur run --roster fails without it
mur install -g <driver>@<version> Installs the driver both members declare
mur build <NAME>/<member> && mur install -g … Packs each member and installs it into the global store, where admission finds it
mur run --roster <NAME> --task "…" Admits and launches the formation, with lead as the entry member

None of the steps changes a generated file.

Writing

  1. <NAME> and both capsule names are checked. A refusal writes nothing.
  2. --index local is refused.
  3. ~/.murmur/config.yaml is read and the provider chosen.
  4. ./<NAME> is refused if anything exists at that path: a directory, a file or a symlink.
  5. The artifact index is read and the driver version chosen. A refusal writes nothing.
  6. Every file is rendered and parsed: roster.yaml as a roster, each murmur.yaml as a capsule manifest. A refusal writes nothing.
  7. The files are written into a new ./.<NAME>.tmp-<id>, which is then renamed to ./<NAME>. A failure at any step removes the temporary directory.

Error codes:

Code Meaning
E-NEW-002 <NAME>, <NAME>-lead or <NAME>-worker is not an artifact name. The message quotes the name and the reason
E-NEW-003 ./<NAME> already exists
E-NEW-004 inference.provider names a provider other than anthropic or openai
E-NEW-005 --index local: the global store is not a published artifact index
E-NEW-006 The artifact index could not be read: the default source did not answer, or the --index URL or file could not be read. The hint names --index
E-NEW-007 The index is of another scope, does not list the driver, or lists no version of it that fits this mur. The message names the driver and the index
E-IDX-002 The index is not a schema 2 index
E-MAN-003 inference.endpoint is not a usable gateway.endpoint
E-IO-003 ~/.murmur/config.yaml could not be read or parsed, or a file could not be written

mur new "<task description>" (beta)

Beta feature

This form exists only in a mur built with the beta-mur-new feature, and runs only after mur beta enable mur-new. Without the flag, mur new "<task description>" exits 1 and names the command that enables it.

Generate a ready-to-refine murmur.yaml in the current directory from a plain-language task description. mur new cold-boots a short-lived generator capsule backed by the configured inference provider, which searches an artifact index and produces a manifest tailored to the task.

mur new "<task description>" [--index <PATH|URL>]
Argument / Flag Required Description
<task description> yes Plain-language description of what the capsule should do
--index no Artifact index the generator searches and its artifacts' versions come from; see The --index value. Omit for the index on the latest default-artifacts release. --registry is refused with exit status 2 and a hint naming --index

Prerequisites:

  • Inference provider configured — detected in this order:

    1. inference: section in ~/.murmur/config.yaml (recommended)
    2. ANTHROPIC_API_KEY env var (uses claude-haiku-4-5-20251001 by default)
    3. OPENAI_API_KEY env var (uses gpt-5.6-luna by default)
    4. Interactive first-run wizard (requires a TTY; saves result to ~/.murmur/config.yaml)

    mur new reads and writes the global file only — it does not consult or write the project-level <cwd>/.murmur/config.yaml file described in Configuration files. - The generator's own artifacts must be installed, each at the newest version that fits this mur in the same index (in the latest release: the version shown): - the driver for your chosen provider — murmur/driver-anthropic@0.19.0 or murmur/driver-openai@0.19.0 - murmur/tool-registry-search@0.5.0 - murmur/tool-editor@0.12.0 - murmur/skill-create-manifest@0.12.0

The generator reads them from the global store, so install them with mur install -g <name>, which installs the same version. mur new exits with E-RUN-008 naming the first one the global store does not hold, and the mur install -g <name>@<version> command that installs it.

The --index value

mur new reads --index before anything else, so a refused value needs no inference provider. The same index decides the version of the generator's driver, tools and skill, as Driver version describes for --roster: E-NEW-006 when it cannot be read, E-NEW-007 naming the first artifact it lists no fitting version of.

Value What the generator's murmur/tool-registry-search reads
Omitted The index on the latest default-artifacts release, https://github.com/murmur-nexus/default-artifacts/releases/latest/download/artifacts-index.json, passed as the tool's index input
A value starting http:// or https:// That URL, passed as the tool's index input. Its origin, <scheme>://<host>[:<port>], is added to the generator's capabilities.network.allow unless it is https://github.com or https://release-assets.githubusercontent.com, which the generator always lists
local Refused with E-NEW-005: the global store is not a published index, and the generator capsule cannot read it. mur search <query> --index local lists it
Any other value A file path, relative to the working directory or absolute. mur reads it once and checks it is a schema 2 artifact index, then copies it unchanged into the generator's work folder as artifacts-index.json and passes artifacts-index.json as the tool's index input

Output:

  • murmur.yaml written to the current working directory
  • Nothing is written if the generator fails or produces invalid YAML

Examples:

# Generate a manifest for a PR security review capsule
mur new "review this PR for security issues"

# Search another source's index
mur new "summarise a document" --index https://github.com/ana/my-artifacts/releases/latest/download/artifacts-index.json

# Search an index file in the working directory
mur new "summarise a document" --index ./artifacts-index.json

# Research/report task — generator adds a spawn capability
mur new "research climate change trends and produce a report"

Single capsule vs orchestrator:

The generator automatically infers whether the task is:

  • Single capsule — focused, bounded task (e.g. "review this PR", "summarise a document"): produces a minimal manifest without spawn.
  • Orchestrator — research, multi-step, pipeline, or report tasks: adds a capabilities.spawn block so the capsule can spawn child capsules.

Generator behavior:

The generator reads its manifest guide, calls murmur/tool-registry-search to find artifacts for the task, and writes the manifest to out/murmur.yaml in its own work folder. The CLI reads that file, validates the YAML, then writes it to murmur.yaml in the current directory through a temporary file and a rename, so an interrupted run leaves no partial manifest. A generator that writes no out/murmur.yaml fails with E-NEW-001, quoting whatever the agent did produce.

If the generated YAML fails validation, nothing is written to CWD and the error is printed to stderr.

The generated manifest is a starting point. Review it and refine versions, capabilities, and inference settings before running.

After generation:

mur build .           # package the capsule
mur run --manifest murmur.yaml  # run it locally

Error codes:

Code Meaning
E-NEW-005 --index local was given
E-IO-003 The --index file could not be read
E-IDX-002 The --index file is not a schema 2 artifact index
E-CFG-001 No inference provider configured and wizard cannot run in non-interactive mode
E-RUN-008 A required generator artifact is not installed
E-MAN-002 Generated YAML failed structural validation
E-NEW-001 The generator produced no out/murmur.yaml
E-IO-003 out/murmur.yaml could not be read, or murmur.yaml could not be written to the current directory

See the mur new how-to guide for a full walkthrough.


mur build

Build a .mur.zip artifact. Two modes: standard build from a source directory with a murmur.yaml, or skill packaging from a skill folder or zip.

mur build <source> with Mode
<source>/murmur.yaml Standard build
No murmur.yaml, and a root SKILL.md / skill.md (any casing) Skill packaging, as if --skill were given
Neither Fails with E-IO-001 murmur.yaml not found at …
--skill Skill packaging

Standard build

mur build [source] [--output <path-or-dir>]
Argument / Flag Default Description
source . Source directory containing murmur.yaml
--output <source>/<name>-<version>.mur.zip Output path or directory
mur build .
# Built artifact: ./my-capsule-0.1.0.mur.zip (WIT level 3)
  • Reads murmur.yaml (requires name and version fields)
  • Scans murmur.yaml for literal secret patterns and emits warnings
  • Packages murmur.yaml plus exactly the files listed in requires_files: — the rest of the source directory (src/, Cargo.toml, README.md, editor files, build output) is not packaged. mur build never compiles anything, so a .wasm payload must already exist on disk and be declared. A native or static artifact that declares no requires_files: builds to a manifest-only archive; a wasm artifact does not — with no root *.wasm to pack it fails with E-BLD-003.
  • Validates the manifest name:, the requires_files: paths and the resulting payload shape, and warns about redundant or misplaced declarations — see Build Lints. name: is the artifact's bare name: a / in it is refused with E-BLD-001, because the scope comes from where the artifact is installed from — see Artifact names
  • Output written inside the source directory unless --output is specified
  • Appends murmur-build.json to the archive, recording the artifact's built-at WIT level: the newest level this mur knows that the packed component's murmur:* interfaces fit. A payload with no component fits every level. A requires_files: entry packing as murmur-build.json is refused with E-BLD-002
Built artifact: line ends Means
(WIT level <N>) murmur-build.json records level <N>
(fits no WIT level this mur knows) The payload fits no level, and no murmur-build.json is packed

A skill build writes the same entry and the same line; a skill fits every level.

Skill packaging (--skill)

Package a skill — a folder or .zip containing SKILL.md, such as a skill made for Claude Code, Codex or pi — into a .mur.zip artifact without authoring a murmur.yaml first. A folder with no murmur.yaml builds this way without --skill.

mur build <folder> [--output <path-or-dir>] [--version <version>] [--summary <text>]
mur build --skill [<name>] <path> [--output <path-or-dir>] [--version <version>] [--summary <text>]
Argument / Flag Description
--skill Enable skill-packaging mode
<name> Optional explicit artifact name. Omit to take the front block's name, else infer from the folder or filename.
<path> Path to a folder or .zip containing SKILL.md (case-insensitive). Defaults to .
--output Output directory (receives <name>-<version>.mur.zip) or output file. Default: the current directory
--version Artifact version for the generated manifest. Default: 0.1.0
--summary description: for the manifest. Default: the front block's description

Front block — when the folder has no murmur.yaml, the name and description keys of the ----fenced block at the top of SKILL.md name and describe the artifact; other keys are ignored. A block that is not valid YAML, such as an unquoted description: Fill forms: use when …, is read line by line: the text after the first : of a top-level name: or description: line, with one pair of surrounding quotes removed.

Generated field First that is set
name <name>, the front block's name, the inferred name
description --summary, the front block's description
version --version, 0.1.0

Name inference — when neither <name> nor a front-block name is given, the artifact name is derived from the last path component:

  1. Strip trailing / so foo/ resolves to foo
  2. Strip .zip extension (case-insensitive)
  3. Lowercase
  4. Replace any non-ASCII-alphanumeric, non-hyphen character with _
  5. Collapse consecutive underscores to one
  6. Strip leading/trailing _ and -

Examples: my-coding-skill/ → my-coding-skill; My Skill.zip → my_skill, which is refused with E-BLD-001 because an artifact name holds no _

<name> vs <path> disambiguation — if the value immediately following --skill contains /, \, or starts with ., it is treated as the input path (name inferred); otherwise it is the explicit artifact name and <path> is the next positional argument. A value with a / that is no existing path, such as ana/x, is read as a scoped name and refused with E-BLD-001: the name is bare.

murmur.yaml handling:

  • Absent — a manifest is generated: name, version, runtime: skill, and description when one is set
  • Present (only with --skill) — packed unchanged, and the front block is not read; --summary sets description in the packed copy. The runtime field must be skill or the build fails with E-MAN-003

Packed files — from a folder, every regular file at any depth, at its path relative to the folder, with the root SKILL.md stored as skill.md:

Entry Result
A name starting with ., at any depth (.git/, .DS_Store) Skipped
A file ending .mur.zip, and the output file Skipped
Root murmur.yaml Packed as the manifest
A symlink anywhere Refused, E-BLD-002
Both SKILL.md and skill.md at the root Refused, E-BLD-002
Root murmur-build.json Refused, E-BLD-002

From a .zip, only root entries are packed.

Output location — the current directory unless --output is given (not the source directory). Written atomically via a temp file + rename, so a failed write never leaves a partial zip.

# Skill folder with a front block naming it pdf-helper, no murmur.yaml
cd /tmp && mur build "Some Folder"
# Built artifact: /tmp/pdf-helper-0.1.0.mur.zip (WIT level 1)

# Infer name from folder
cd /tmp && mur build --skill my-skill/
# Built artifact: /tmp/my-skill-0.1.0.mur.zip

# Explicit name and version
mur build --skill wrapped-skill --version 1.2.0 my-skill/
# Built artifact: ./wrapped-skill-1.2.0.mur.zip

# Zip input
mur build --skill external-skill.zip
# Built artifact: ./external-skill-0.1.0.mur.zip

Error cases:

Code Meaning
E-BLD-001 The artifact name contains /, or is not lowercase letters, digits and inner hyphens — including a front-block name; pass --skill <name> to override it
E-BLD-002 The folder holds a symlink, both SKILL.md and skill.md, or a root murmur-build.json
E-IO-001 SKILL.md not found in the input folder or zip (case-insensitive search found no match)
E-MAN-002 murmur.yaml present but YAML is malformed
E-MAN-003 murmur.yaml present but runtime is not skill

See also: Package a skill into an artifact, mur publish, mur install


mur publish

Publish an existing artifact into the global store as local/<name>, at ~/.murmur/artifacts/local/<name>/<version>/. <name> is the name: the artifact's own murmur.yaml declares.

mur publish [artifact_path] [--platform <os-arch>]
  • If artifact_path is omitted, CLI infers <name>-<version>.mur.zip from local murmur.yaml
  • --registry is refused with exit status 2 and a hint naming the global store
  • --platform overrides the platform tag (format: os-arch, e.g. darwin-aarch64). When omitted for a native artifact (implementation: native in the zip's murmur.yaml), the platform is auto-detected from the current build host. WASM artifacts publish without a platform tag regardless.

Example — WASM artifact (no platform tag):

mur publish my-tool-0.1.0.mur.zip
Published local/my-tool@0.1.0

Example — native artifact (auto-detected platform):

mur publish my-native-tool-0.1.0.mur.zip
Platform: darwin-aarch64 (auto-detected)
Published local/my-native-tool@0.1.0

Reserved versions rejected:

  • latest
  • stable
  • edge

mur install

Fetch and install artifacts from the global store, the configured sources, or a file. mur install is the canonical way to seed a project's dependencies before mur run — equivalent to npm install or cargo fetch in those ecosystems.

# Install all artifacts declared in the project manifest (reads murmur.yaml in or above CWD)
mur install

# Install a specific artifact by scope/name@version: the global store, then the configured sources
mur install <scope/name@version>

# Install from a GitHub source directly
mur install github:<owner>/<repo>@<tag>

# Install into the global store (~/.murmur/artifacts/) instead of the project store
mur install -g <ref>

# Download all platform variants into the global store (CI / cross-platform seeding)
mur install --all-platforms <scope/name@version>
Form Behavior
mur install (no args) Reads murmur.yaml in or above CWD; fetches all declared artifacts in parallel into the project-local store (.murmur/artifacts/ next to the manifest)
mur install <scope/name@version> Fetches a specific artifact into the project-local store. A local/ artifact is taken from the global store only; no source is asked. A version that does not fit this mur is refused with E-WIT-LEVEL-001 before anything is fetched
mur install <scope/name> Fetches the newest version that fits this mur from the source of the scope
mur install github:<owner>/<repo>@<tag> Fetches every asset of a GitHub release into the project-local store, each under the repository's scope
mur install <path> Installs a .mur.zip file as local/<name>, <name> being the name its murmur.yaml declares
mur install -g <ref> Fetches into the global store (~/.murmur/artifacts/)
mur install --all-platforms <scope/name@version> Downloads all platform variants into the global store, filing each under its own platform tag; useful for CI and cross-platform build seeding
mur install --no-precompile Installs without compiling and without keeping host facts; a project install then writes nothing under ~/.murmur. Without the flag, mur install compiles each WASM tool, driver and hook it installs into ~/.murmur/compiled/, for this machine and this mur build, so the first mur run loads it instead of compiling it. With no arguments that includes each manifest artifact already in the store, and one whose compiled form is already there is not compiled again. A compile that fails does not fail the install or change its output; that artifact compiles on its first launch. In a project it also keeps the host facts of the manifest's capabilities.shell.allow binaries, Behavior step 6 below

mur install (no args) is the standard pre-run step. It reads murmur.yaml, resolves every artifact listed in it, and if an artifact is not in the global store it fetches it from the source of its scope. A pinned version that does not fit this mur refuses the whole install before anything is fetched or written. An install refused for any reason before its first write leaves the project as it was, with no .murmur/artifacts/ created. See Resolution order. --registry is refused with exit status 2 and a hint naming that order.

Example — seed a project before running:

mur install
mur run

Example — install a specific artifact:

mur install murmur/tool-git@0.6.0

Behavior:

  1. Resolve a name@version artifact from the global store ~/.murmur/artifacts/
  2. On a hit, verify the bytes against the SHA-256 the global store records; on a miss, look the artifact up in the index of its scope's source, download it, and check it against the index — see Index resolution
  3. Store into the project-local store (or global store with -g)
  4. Pin the name, resolved version, SHA-256 and, for a payload a source served, the scope's owner id in murmur.lock as origin: operator — project installs only, since -g has no project to pin. An entry a capsule pinned through manage.pull() is adopted as operator-declared, with one line after the lock is written:

    Adopted ana/some-tool@1.2.3 as operator-declared (it was pulled at runtime by session ses_0190a1b2c3d4...)
    
  5. Compile each WASM tool, driver and hook for this machine into ~/.murmur/compiled/, unless --no-precompile is given (see the table above)

  6. Keep the host facts of each capabilities.shell.allow binary in ~/.murmur/host_facts/, worked out as a mur run of the project without --workdir would work them out, so the first launch keeps them instead of running each interpreter. Project installs only, and not with --no-precompile. When at least one binary's facts are kept, install prints one line after the lines reporting what it installed:

    Kept host facts for 3 allowlisted binaries
    

    A binary that is not a recognised interpreter is kept too, and counts in that line. A binary that is not on PATH, or an interpreter that does not answer, is skipped without a message and never fails the install; the first launch works out what install could not

A local/ artifact missing from the global store is refused without asking a source:

error[E-REG-001]: artifact local/x@1.0.0 is not in the store, and no source serves the 'local' scope
  hint: a local/ artifact is installed from its .mur.zip file: mur install ./x-1.0.0.mur.zip

A name that is not <scope>/<name> is refused with E-REG-007. The reference forms and what each one stores are in Installing artifacts.


mur precompile

Compile .mur.zip files for this machine into ~/.murmur/compiled/, so the first mur run that stages them loads the compiled form instead of compiling. It reads only the files it is given: it resolves nothing, installs nothing and writes no murmur.lock. mur deploy run runs it on the target.

mur precompile [--workdir <dir>] [--json] <ZIP>...
Flag Default Description
<ZIP>... — One or more .mur.zip files (required)
--workdir — The --workdir of the mur run that will launch these artifacts. When ~/.murmur is inside it, nothing is stored, as that launch would neither read nor write compiled/
--json off Print the report as one JSON object on stdout

Each WASM tool, driver and hook is compiled under this machine's mur build and MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES value; a later launch loads the form only under the same two. Files are compiled in parallel and reported in argument order, one outcome each:

Outcome (text) Outcome (JSON) Meaning
compiled compiled Compiled, and stored unless compiled/ cannot be written or --workdir holds it back
already stored already_stored A usable compiled form was already in compiled/; nothing was written
not wasm not_wasm A native tool, a skill or a capsule; nothing to compile
failed failed The file could not be read, is not a .mur.zip, has no murmur.yaml that parses, or its WASM does not compile. Its first launch compiles it, or fails, as it would have

Text output — one line per file: the outcome, then name@version, or the path when the file has no readable manifest.

compiled        tool-echo@1.0.0
not wasm        tool-git@0.6.0
failed          ./broken.mur.zip

JSON output (--json) — exactly one line:

{"mur_version":"1.0.0","decompression_ceiling":524288000,"artifacts":[{"path":"./echo.mur.zip","name":"tool-echo","version":"1.0.0","outcome":"compiled"},{"path":"./broken.mur.zip","name":null,"version":null,"outcome":"failed"}]}
Field Type Description
mur_version string Version of the mur that compiled
decompression_ceiling integer The MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES value the forms were compiled under, in bytes
artifacts[].path string The path as given
artifacts[].name, artifacts[].version string or null From the file's murmur.yaml, so the name is the bare name; null when there is none that parses
artifacts[].outcome string One of the JSON outcomes above

Exit status: 0 when no file is failed; 1, after the full report, when any is.

A mur release older than this command exits with error: unrecognized subcommand 'precompile'. A deploy whose target runs such a release prints W-DEPLOY-001 and continues; set deploy.mur_version in the manifest to a release that has mur precompile, or pass --mur-binary.


mur list

List installed artifacts by full name, <scope>/<name>, sorted by name. Which store is listed follows where you run the command.

# Inside a project directory — show the project store
mur list

# Show the global store (~/.murmur/artifacts/)
mur list -g

# Show both stores with a STORE column (project / global)
mur list --all

# Show only artifacts declaring a WIT interface under murmur:hook
mur list -g --contract murmur:hook
Flag Shows
(none) Project store (.murmur/artifacts/ next to murmur.yaml) when inside a project directory
-g Global store (~/.murmur/artifacts/)
--all Both stores; output leads with a STORE column (project or global)
--contract <PREFIX> Only artifacts whose recorded WIT contracts include an interface name starting with PREFIX; output includes a CONTRACTS column naming the matches. Combines with -g and --all

--contract matches the imports and the exports alike: a package version bump renames the interface for an artifact that imports it as much as for one that exports it.

Example output (mur list):

NAME                     VERSION  RUNTIME  PLATFORMS
murmur/driver-anthropic  0.19.0   driver   —
murmur/tool-git          0.6.0    tool     darwin-aarch64

Example output (mur list --all):

STORE    NAME                     VERSION  RUNTIME  PLATFORMS
project  murmur/driver-anthropic  0.19.0   driver   —
global   murmur/tool-git          0.6.0    tool     darwin-aarch64

Example output (mur list -g --contract murmur:tool):

NAME                     VERSION  RUNTIME  PLATFORMS       CONTRACTS
murmur/driver-anthropic  0.19.0   driver   —               murmur:tool-registry/invoke@0.2.0
murmur/tool-git          0.6.0    tool     darwin-aarch64  murmur:tool/run@0.2.0

mur doctor

Check that every artifact declared in the current project's murmur.yaml is available to a session — the same project-store-then-global-store, current-platform resolution mur run performs before staging. In a formation directory, check that its roster.yaml is admitted.

mur doctor [--bind <ADDR>]
Flag Default Description
--bind 127.0.0.1 The address mur run --bind would bind the capsule's door on. With an address off loopback and no network.authentication, prints W-SEC-032

What it checks depends on the current directory:

Current directory Checks
Holds roster.yaml and no murmur.yaml The formation: the Roster block only. It does not walk up
Anything else The project: walks up from the current directory to find murmur.yaml (same walk mur install uses), loads it, and prints everything below

In a project, it prints one checklist line per declared artifact:

Line Meaning
✓ name@version <platform> A native tool whose bin/<name> binary was read and identified as this host's platform
✓ name@version platform-independent The payload runs the same on every platform: a skill, a WASM tool, a driver, a hook
✓ name@version platform unverified A native tool whose bin/<name> payload is in a format the platform check does not recognise, such as a shell script
✓ name@version local source Declared with a source: path; resolved from the filesystem at stage time, never checked against a store or a lockfile
✗ name@version <platform> — missing Resolved from neither store
✗ name@version <platform> — native binary is built for <binary-platform>, this host is <platform> The artifact holds a host executable this machine cannot run; mur run refuses it at staging with E-RUN-021
✗ name@version — built against <interface>, this mur serves <package>@<version> The artifact names an interface version this mur does not serve, and mur run refuses it at launch. The line ends this mur serves no version of <package> when this mur has no such package, and (+N more) follows when the artifact names several. See WIT level

Every green line means the artifact resolved from the project store (.murmur/artifacts/) or the global store (~/.murmur/artifacts/), and agrees with murmur.lock if one is present (see below). The host platform appears on a green line only for an artifact whose binary was identified and matched.

There is no hardcoded artifact list: the checklist is derived entirely from murmur.yaml's artifacts: block. Editing a version pin or adding/removing an artifact changes what mur doctor checks, with no code change.

For a transport: http capsule whose drivers are installed, a Model limits: block follows the checklist, resolved the way mur run resolves it at launch — see Output cap:

Line Meaning
✓ <choice> (<driver>@<version>, model <model>): output cap <n> (<source>), context window <n \| none> (<source>), input ceiling <n \| none> (<source>), input budget <n \| none> (<basis>) One per driver choice, primary first. Each source is manifest, driver or, for a window or a ceiling, none. The basis is max_input_tokens, context_window_less_output_cap or, with no window, none — see input budget
[mur doctor] warning[W-RUN-009]: … on stderr The text W-RUN-009 prints at launch. Counted as a warning with a Fix: line naming context.max_tokens; the exit code stays 0
✗ error[E-RUN-052]: … A choice with no output cap, or a driver with a malformed models: table. Adds a Fix: line and makes mur doctor exit 1
✗ error[E-RUN-055]: … A choice, or the compaction call, left no input budget: E-RUN-055. Adds a Fix: line and makes mur doctor exit 1
Model limits: not checked, driver <name> is not installed A choice's driver is missing; the checklist already names it

Ahead of the checklist it prints these blocks. None of them affects the exit code.

Block What it reports
AppArmor / user namespaces The running binary's resolved path and the profile confining it. AppArmor attaches profiles by executable path, so a mur run from a build output or any other unusual location is told that no profile attaches to it, and which paths the shipped profile does attach to. Then the userns grant: line, and a network namespace: line with the answer of the probe mur run uses for E-CAP-005: created, or refused — <failed call> failed with <ERRNO>. See Where the user namespace comes from
Landlock The kernel's Landlock ABI and the ABI a capsule's ruleset is built at (the kernel's, capped at 3). Below ABI 2 it adds that moving or hard-linking a file between folders inside the workdir fails with EXDEV on this host; below ABI 3, that truncate(2) on a file outside the workdir is not refused. See Which Landlock ABI the ruleset is built at
Filesystem preopens One line per runtime: tool, runtime: driver and runtime: hook entry, naming the directory that artifact works out of — see The filesystem default. An entry whose capabilities.filesystem.scope mur run would refuse prints <unresolved>, with an E-CAP-002 warning on stderr naming it
Read-only paths The subtrees capabilities.filesystem.read_only protects, and whether that protection is enforced for every call the runtime can read as a write or advisory against a named interpreter
Install grant The install skill and install tool entries of capabilities.install
Memory bound capabilities.resources.cgroup_memory_bytes and the two cgroup files a launch writes from it — see Memory bound
WIT level The WIT level this mur serves and its packages, then every installed artifact with its built-at level and whether it fits. Always printed. See WIT level
Artifact stores Each directory of the project or global store in the flat <name>/<version>/ layout, which nothing reads, as global store: <path>: old layout, nothing reads it, safe to delete. Printed only when there is one. See Store layout

Read-only paths and Install grant print the same lines, in the same words, as mur run --explain-scope.

For a capsule declaring capabilities.spawn.allow, mur doctor also reports whether mur-roost — the daemon that capsule registers with at launch — is installed and answering, naming E-RUN-019 as the error a mur run would meet. It tells apart three states: the binary is not on PATH, MURMUR_ROOST_URL is not set, and nothing answers at the URL that variable names. A reachable daemon prints nothing, and a capsule declaring no spawn.allow gets no such line. Like the blocks above, this warning goes to stderr and never changes the exit code.

For the same capsule, mur doctor also prints an Environment requirements block: what the capsule's spawn closure — that capsule plus the transitive closure of its capabilities.spawn.allow — needs from the environment before its first token is spent. It resolves each named capsule to an exact version: murmur.lock pins it if the lockfile holds an entry for the name, otherwise the project store (.murmur/artifacts/) decides alone if it holds the name at all, otherwise the global store (~/.murmur/artifacts/). No version is guessed — a name that no single source settles is listed as one the walk could not inspect. Nothing is launched and no daemon is contacted.

The block also reports every variable the project manifest itself references as ${VAR} and neither this shell nor the workspace .env sets. gateway.api_key on an artifact entry is the manifest field that takes such a reference. A capsule declaring no spawn.allow has nothing to walk, so it gets a block only when it has such a reference to report.

mur doctor always prints one door: line saying who may call the capsule's A2A door:

Line Manifest
door: public (no network.authentication) No network.authentication
door: bearer — operator, <name> (<scopes>), … network.authentication, naming each declared credential and its scopes

When --bind is off loopback and the door is public, it also prints the W-SEC-032 line, byte for byte the line mur run --bind prints for the same manifest. Loopback is an IPv4 address in 127.0.0.0/8, ::1 or localhost.

mur doctor parses the project manifest without resolving what it references, so a reference this shell cannot satisfy is a line in this block rather than a refusal ahead of it. mur run needs the value, and refuses the same manifest with E-MAN-003.

Four findings, of which two change the exit code:

Finding Line Exit code
A name that neither this shell nor the workspace .env sets: an entry in the closure's capabilities.env.allow, or a variable the project manifest references ✗ NAME unset — <capsule> non-zero, E-CAP-014 on stderr
A capsule declaring a capabilities.env.allow entry the capsule that spawns it does not hold — the spawn mur-roost refuses declarations mur-roost will refuse: non-zero, E-CAP-015 on stderr
A capsule the walk could not read: not installed, more than one version installed with nothing pinning which, or an unreadable archive could not inspect N of M capsules in the spawn closure: 0, W-REG-002 on stderr
A spawn.allow edge pointing back at a capsule already on the walk spawn.allow cycle: a@1 → b@2 0

This block is stricter than the manifest blocks above, which report a refusal of the root capsule as a warning: a failure in the spawn closure lands at depth, after the parent has already spent tokens reaching the point of delegating.

Only names are printed. No variable's value is read into the report or written anywhere, and set/unset is decided by presence alone, so a name set to the empty string counts as set. The workspace .env counts because mur run loads it; a .env that cannot be parsed is reported by file and line, adds a Fix: entry, and leaves the variable list computed from this shell's environment alone.

A variable line names every capsule that needs the name. A name a capsule declares in capabilities.env.allow is attributed to that capsule alone; a name reached through any other manifest field carries that field in brackets, as solo@0.0.1 (artifacts.murmur/driver-anthropic.gateway.api_key).

Output — a spawn closure with one unset variable:

Environment requirements
  capsules: root-capsule@0.0.1, worker@0.1.0, deep-worker@0.2.0
  variables:
    ✓  ANTHROPIC_API_KEY   set     — root-capsule@0.0.1, worker@0.1.0
    ✗  WORKER_TOKEN        unset   — worker@0.1.0

Output — a capsule that delegates to nobody, with one unset reference:

Environment requirements
  capsules: solo@0.0.1
  variables:
    ✗  SOLO_PROVIDER_KEY   unset   — solo@0.0.1 (artifacts.murmur/driver-anthropic.gateway.api_key)

Output — happy path:

Filesystem preopens
  - murmur/driver-anthropic (driver): the whole accessible workdir — no capabilities.filesystem.scope declared
  - murmur/tool-git (tool): one subtree of the accessible workdir — capabilities.filesystem.scope: repo

Read-only paths
  read_only:
    - tests
    - bench/fixtures
  read_only enforcement: enforced for every tool call and every shell command the dispatch check can read

Checking /path/to/murmur.yaml for darwin-aarch64...
  ✓  murmur/driver-anthropic@0.19.0   platform-independent
  ✓  murmur/tool-git@0.6.0            darwin-aarch64

All checks passed.

Output — one or more artifacts missing:

Checking /path/to/murmur.yaml for darwin-aarch64...
  ✗  murmur/tool-git@0.6.0   darwin-aarch64   — missing

0 checks passed, 1 error found.

Fix: mur install murmur/tool-git@0.6.0

Output — a native binary built for another platform:

Checking /path/to/murmur.yaml for linux-x86_64...
  ✗  murmur/tool-git@0.6.0   linux-x86_64   — native binary is built for darwin-aarch64, this host is linux-x86_64

0 checks passed, 1 error found.

Fix: murmur/tool-git: native binary is built for darwin-aarch64 — reinstall murmur/tool-git@0.6.0 on this host

Memory bound

The Memory bound block shows capabilities.resources.cgroup_memory_bytes, the aggregate memory bound on the capsule's native subprocess tree, as the cgroup scope files a launch on this host writes:

Memory bound
  cgroup_memory_bytes: 268435456
  memory.max 268435456
  memory.swap.max 0
Line Meaning
cgroup_memory_bytes: <bytes> The declared value, or the default 4294967296
memory.max <bytes> The scope's memory ceiling: the same value
memory.swap.max 0 The scope's swap ceiling. Swap is bounded at zero, so a tree over memory.max is OOM-killed rather than pushed out to swap
memory.swap.max: swap is not bounded on this host — … This kernel exposes no memory.swap.max — swap support compiled out, or swap accounting off at boot — so memory.max bounds resident memory only. The line ends saying whether /proc/swaps lists an active swap area
memory.swap.max 0 where this kernel exposes it — … No cgroup this process can see has the memory controller, so whether the kernel exposes the file is unknown. The line ends saying whether swap is active
not applied on this host: cgroup bounds are Linux-only, … Off Linux, in place of the memory.max and memory.swap.max lines

A scope is created only for a capsule that can start a native subprocess.

Lock integrity

Each artifact resolved from a store is also checked against murmur.lock when one is present — see Lock integrity. A disagreement produces one of three failure lines:

  • ✗ name@version <platform> — murmur.lock missing artifact entry for 'name' — the lock exists but has no entry for this artifact
  • ✗ name@version <platform> — murmur.lock version mismatch for 'name': manifest requested X, lock pinned Y — the lock pins a different version than murmur.yaml declares
  • ✗ name@version <platform> — artifact integrity check failed for name@version (with expected sha256 (murmur.lock): / actual sha256 (on disk): detail lines) — the installed bytes don't hash to the lock's recorded sha256
  • ✗ name@version <platform> — murmur.lock has no sha256 for 'name' on <platform>: it pins <platforms> — the lock was written on another platform and has never been installed against on this one

A pin a capsule wrote through manage.pull() (origin: runtime) adds one of three lines:

Line Meaning Effect
⚠ name@version — pulled at runtime by session <session> A declared tool or skill whose pin a capsule pulled. mur run stages it, marked untrusted Warning: Fix: mur install name@version. Exit code unchanged
✗ name@version — murmur.lock pins 'name' from a runtime pull by session <session>; murmur.yaml declares it with <declared> <declared> is runtime: hook, runtime: driver, gateway: or inference.system_prompt_artifact. mur run refuses it with E-RUN-043 Failure: Fix: mur install name@version. Exit 1
· name@version pulled at runtime by session <session> — not declared in murmur.yaml, so mur run does not stage it A pulled entry murmur.yaml does not declare, printed after the checklist None

Output — runtime-pulled pins:

Checking /path/to/murmur.yaml for linux-x86_64...
  ⚠  ana/demo-skill@0.1.0   — pulled at runtime by session ses_0190a1b2c3d4
  ✗  ana/demo-tool@0.1.0    — murmur.lock pins 'ana/demo-tool' from a runtime pull by session ses_0190a1b2c3d4; murmur.yaml declares it with gateway:
  ·  ana/other-tool@2.0.0   pulled at runtime by session ses_0190a1b2c3d4 — not declared in murmur.yaml, so mur run does not stage it

1 check passed, 1 error found, 1 warning.

Fix: mur install ana/demo-tool@0.1.0
Fix: mur install ana/demo-skill@0.1.0

Output — lock hash mismatch:

Checking /path/to/murmur.yaml for darwin-aarch64...
  ✗  ana/demo-skill@0.1.0   darwin-aarch64   — artifact integrity check failed for ana/demo-skill@0.1.0
        expected sha256 (murmur.lock): deadbeef
        actual sha256 (on disk):       0e29c7e8c291a2800a266a01c28300e24f2a640d4a21a085e4fd9aee01adfaef

0 checks passed, 1 error found.

Fix: ana/demo-skill: artifact on disk does not match murmur.lock — re-publish or delete the lock

Runtime directory

After the checklist, mur doctor prints the Runtime directory, Records directory and Temporary folder blocks mur run --explain-scope prints, for the project's declared containment and <manifest-dir>/workdir — see Runtime directory, Records directory and The session's temporary folder. When ~/.murmur/sessions lies inside that workdir it also prints W-SEC-033 to stderr. Neither changes the exit code.

Murmur home

Once the manifest loads, mur doctor prints a Murmur home block: the mode of ~/.murmur and of each entry in it, whatever the project declares. The known entries are always listed, present or not, followed by any other name in the directory.

Murmur home (/home/alice/.murmur)
  .: 0755  the murmur home, expected owner-only
  config.yaml: 0644  provider credentials, expected owner-only
  deploy_keys: 0700  SSH private keys, expected owner-only
    wider than 0600: deploy_keys/dep_x/id_ed25519 is 0644
  deploy_staging: absent  deployment staging copies, expected owner-only
  deployments.json: 0600  deployment records, expected owner-only
  spend: 0700  the machine spend ledger, expected owner-only
  conversations: 0700  conversation records, expected owner-only
  running: absent  running-capsule records, expected owner-only
  state: 0700  capsule state stores, expected owner-only
  artifacts: 0755  installed artifacts
  compiled: 0700  compiled WASM cache, safe to delete, expected owner-only
  host_facts: 0700  facts about allowlisted host binaries, safe to delete, expected owner-only
  bin: absent  cached mur binaries
  formations: 0700  formation members' working directories, each kept until its formation has ended and the entry member's trace.retain expires it, expected owner-only
  nexus-config.json: 0644  something not recognised by this build
Line Meaning
<name>: <mode> The entry's permission bits. . is ~/.murmur itself
<name>: absent Nothing exists at that name
<name>: unreadable (<error>) The entry exists and its metadata could not be read
each kept until … How long mur keeps what the entry holds. Only formations has one: see Removing formation directories
safe to delete Deleting the entry loses nothing, because mur writes it again when it needs it. Only compiled and host_facts are
expected owner-only The entry is held at the mode in ~/.murmur modes, and so is everything beneath it. Under formations, only the <frm_id>/ and <member>/ levels are held: what a member's sessions write inside its owner-only member directory is not reported
wider than <mode>: <path> is <mode> A directory beneath an owner-only entry wider than 0700, or a file wider than 0600. At most 20 are listed per entry, then and N more wider than expected

Each owner-only entry wider than expected, and each path listed beneath one, also prints W-SEC-028 on stderr. The block changes no mode and does not affect the exit code. When HOME cannot be resolved, the block is one not reported line.

Roster

When a roster.yaml sits beside the project's murmur.yaml, mur doctor admits it and prints a Roster block ahead of the checklist. It resolves every member from the project store and then the global store, and checks each one against murmur.lock, as described in Admission order. Nothing is launched and no file is written. Without a roster.yaml there is no block.

In a formation directory, one holding roster.yaml and no murmur.yaml, the Roster block is the whole check. It admits the roster exactly as mur run --roster <dir> does, from the directory's own .murmur/artifacts store, then the global store, and against the directory's murmur.lock:

  1. One line, No murmur.yaml in <dir>: checking its roster.yaml. Run mur doctor in a member's source directory to check that member's artifacts., then a blank line.
  2. The Roster block.
  3. The tally:

    Roster Tally Exit code
    Admitted, no warning All checks passed. 0
    Admitted, with warnings 0 checks passed, 0 errors found, N warning(s)., a blank line, and one Fix: <remedy> per warning 0
    Refused 0 checks passed, 1 error found., a blank line, and Fix: <hint> 1

No member's artifacts, environment requirements, door or host checks are reported, and --bind changes nothing.

Line Meaning
file: <path> The roster admitted
<name> <capsule>@<version> [entry] <serves peers\|refuses peers> <authenticated door\|public door> One line per member, in roster order. entry marks the entry member. The last two columns read the member's exports.peer_tasks.accept and network.authentication
reachability: none No member may call another
reachability: a → b, … The edges the rules declare, ordered by the members' order in the roster
reachability (all): a → b, … The edges reachability: all expands to
warning[W-ROS-001]: … More members may call this member than its lifecycle holds at once. One line per such member, in roster order, each counted as a warning with a Fix: entry. See W-ROS-001
✗ <message> The roster is refused. The member lines are not printed

A refused roster is an error: its code goes to stderr with a hint, the hint is added as a Fix: entry, and the exit code is 1. The codes are E-ROS-001 to E-ROS-008, and E-REG-005 for a member murmur.lock pins at another version or hash.

Output — an admitted roster:

Roster
  file: /path/to/roster.yaml
  planner    local/planner@0.3.0    entry   serves peers    authenticated door
  coder      local/coder@1.2.0              serves peers    authenticated door
  reviewer   local/reviewer@0.9.0           serves peers    authenticated door
  reachability: planner → coder, planner → reviewer, reviewer → coder
  warning[W-ROS-001]: roster.yaml lets 2 members call 'coder' (planner, reviewer), but it holds 1 task at once — lifecycle.task_acceptance: single — so a call that arrives while it is busy waits and is offered again, and is rejected only if it stays busy until the call's deadline; lifecycle.task_acceptance: queue with lifecycle.queue_depth: 1 would hold them all (https://docs.murmur.nexus/reference/diagnostics/#w-ros-001)

Output — a refused roster:

Roster
  file: /path/to/roster.yaml
  ✗  roster.yaml: member 'coder' declares no `network.authentication`, and this roster declares peer traffic, so every member's door must require authentication

Checking /path/to/murmur.yaml for linux-x86_64...

0 checks passed, 1 error found.

Fix: declare `network.authentication` in the murmur.yaml of the capsule 'coder' runs and rebuild it, or remove the peer traffic from roster.yaml. A capsule that declares network.authentication cannot also declare capabilities.spawn.allow (E-MAN-003)

WIT level

Just before the checklist, mur doctor prints the WIT level this mur serves and its package table, then every artifact installed in the project store (.murmur/artifacts/) and the global store (~/.murmur/artifacts/), declared or not, with its built-at level and whether it fits the served level. The block is always printed.

An artifact fits when every murmur: interface it names, exported or imported, is at exactly the version the served level lists for its package. wasi: interfaces and other namespaces are not compared. mur run refuses a driver that does not fit at launch, and a tool or hook that does not fit fails when it is first called.

The interfaces come from the store's record of the artifact, written from its own bytes every time it is installed or published; when the record carries none, they are read from the installed payload. The check never writes to either store.

Output:

WIT level
  this mur serves WIT level 3:
    PACKAGE                  VERSION
    murmur:artifact-manager  0.1.0
    murmur:capsule           0.1.0
    ...
    murmur:tool-registry     0.2.0

  installed:
    local/built-tool@0.1.0   global    built at 3                fits level 3
    local/notes@0.1.0        global    built at 3 (derived)      fits level 3
    local/stale-hook@0.3.0   global    built at none (derived)   does not fit level 3
  ⚠  local/stale-hook@0.3.0   global    exports murmur:hook/lifecycle@0.8.0 — this mur serves murmur:hook@0.9.0
  ⚠  local/stale-hook@0.3.0   global    imports murmur:runtime/inference@0.3.0 — this mur serves murmur:runtime@0.4.0
  mur run refuses these at launch (warning[W-REG-003], https://docs.murmur.nexus/reference/diagnostics/#w-reg-003)

Checking /path/to/murmur.yaml for linux-x86_64...

0 checks passed, 0 errors found, 1 warning.

Fix: mur install -g local/stale-hook@<a release built against murmur:hook@0.9.0, murmur:runtime@0.4.0>
Line Meaning
name@version <project\|global> built at <label> fits level <N> The artifact fits the served level
… does not fit level <N> It does not; one ⚠ line per interface follows the list
⚠ name@version <project\|global> <exports\|imports> <interface> — this mur serves <package>@<version> The artifact in that store names <interface>; this mur serves <package> only at <version>
… — this mur serves no version of <package> The served level has no package of that name
… <interface> (unversioned) — … The interface name carries no version
none installed Neither store holds an artifact
not checked (<project\|global> store): <error> The store's index could not be read, so nothing in it was checked. The exit code is unchanged
Built-at label Means
<N> The level mur build recorded in the artifact's murmur-build.json
<N> (derived) The artifact records no level; <N> is the newest level this mur knows that its interfaces fit
none (derived) The artifact records no level, and its interfaces fit no level this mur knows
<N> (newer than this mur knows) The recorded level is above the served level: a newer mur built it

Which tier a stale artifact lands in depends on whether the next mur run of this project launches it:

Artifact Finding Exit code
Not declared in murmur.yaml at that version Warning: W-REG-003 and Fix: mur install [-g] name@<a release built …> Unchanged
Declared in murmur.yaml at that version ✗ checklist line and Fix: name: pin a release built against … in murmur.yaml, then run mur install 1

The global store holds artifacts for every project on the machine, so an artifact nothing here launches is a warning. A declared one is an error because the next mur run refuses it. A declared artifact that already fails the lock checks shows only that failure, and an artifact declared with source: is not checked.

The fix is always a release that fits the served level. Reinstalling the same version fetches the same build.

Warnings

A finding that is worth reporting but is not a failure prints on its own checklist line, marked ⚠, and adds a Fix: line in the same block as the errors. The summary line counts warnings separately and the exit code ignores them, so a store that resolves everything it is asked for still exits 0. W-REG-001 is the one warning the checklist reports. A stale artifact this project does not declare adds its Fix: line in the same way, from the WIT level block.

Output — a native artifact with no recorded platform:

Checking /path/to/murmur.yaml for linux-x86_64...
  ⚠  murmur/tool-git@0.6.0   linux-x86_64   — native artifact with no recorded platform (warning[W-REG-001])

1 check passed, 0 errors found, 1 warning.

Fix: mur install murmur/tool-git@0.6.0

Exit codes:

  • 0 — every declared artifact resolved (or is local-source), agrees with murmur.lock if one is present, carries no binary built for another platform, and speaks only interface versions this mur serves; and the Environment requirements block found no unset variable and no declaration mur-roost will refuse. Warnings do not change this
  • 1 — one or more declared artifacts missing, disagree with murmur.lock, have a runtime-pulled pin mur run would refuse with E-RUN-043, hold a native binary this host cannot run, or speak an interface version this mur does not serve (checklist printed to stdout first); or the Environment requirements block found an unset variable or a predicted refusal; or a setup failure (no checklist printed; error goes to stderr)

Error codes:

Code Meaning
E-IO-001 No murmur.yaml found in the current directory or any parent, and the current directory holds no roster.yaml
E-MAN-001 / E-MAN-002 / E-MAN-003 Manifest failed to load — missing field, YAML syntax error, or invalid field, respectively
E-RUN-003 murmur.lock exists but failed to parse or validate — including a lock_version other than 3, which is refused rather than migrated
E-RUN-021 A declared native tool's binary is built for another platform — reported on the checklist line; mur run refuses the same artifact at staging
E-RUN-043 A declared artifact's pin was written by manage.pull() and murmur.yaml declares it as a hook, a driver, or with gateway: — reported on the checklist line; mur run refuses the same artifact at staging
E-CAP-014 A variable the spawn closure's capabilities.env.allow declares is unset in this environment
E-CAP-015 A capsule in the spawn closure declares a capabilities.env.allow entry the capsule that spawns it does not hold
W-REG-002 A capsule in the spawn closure could not be inspected, so what it declares is missing from the report — a warning; the exit code is unchanged
W-REG-003 An installed artifact this project does not declare speaks an interface version this mur does not serve — a warning; the exit code is unchanged

A setup failure (no project or formation directory found, the manifest fails to load, or the lockfile fails to parse) is reported on stderr before any checklist is printed — mur doctor never reports "all checks passed" against zero artifacts because the manifest or lockfile couldn't be read.


mur run

Run capsule component and resolve declared artifacts.

mur run [--manifest <path>] [--task <path-or-text>] [--json]
mur run --roster [<path>] [--task <path-or-text>] [--json]
Flag Default Description
--manifest ./murmur.yaml Path to the capsule manifest
--roster ./roster.yaml when the flag is given with no value Launch the formation a roster declares: for one task with --task, else until it is stopped. Takes the project directory or the roster.yaml inside it; any other file name refuses with E-ROS-001. Passes --task, --json, --verbose, --no-env-file, --containment and --deadline through to its members, and cannot be combined with any other flag. See Launching a formation
--capsule — Run an installed artifact by its full name, <scope>/<name>, instead of a project directory; a bare name is refused with E-REG-007. Requires --capsule-version, and cannot be combined with an explicitly given --manifest. The capsule is resolved from the project store <dir>/.murmur/artifacts, where <dir> is --workdir when it is given and the current directory otherwise, then from the global store ~/.murmur/artifacts. It is staged from the artifact bytes in memory: no murmur.yaml is read from disk, and no murmur.lock is read or written. This is the form a parent capsule's runtime launches a delegated child on
--capsule-version — Version of the --capsule artifact. Required with --capsule
--spawn-grant-stdin off Read one line from standard input as this launch's spawn approval, and present it when the session registers with mur-roost. Set by a parent capsule's runtime when it launches a delegated child. Standard input rather than an argument or an environment variable, both of which any process running as the same user can read out of /proc
--task — Written as task.md in the session's runtime directory before launch; the workdir is left alone. An existing file path is copied; any other value is written verbatim as UTF-8 text
--context a fresh ctx_… per task Context id this run's task runs under. Two runs given the same id continue one conversation record, whichever session directory each got. One path segment: no /, no . or .., not absolute, not starting with a dot — anything else refuses the launch with E-CAP-011
--resume @1 when the flag is given with no value Session whose conversation this run continues, as a session address. The session continued is the one the trace's own session_start line names, so a trace.jsonl copied into a folder of any name resumes as the session it records; a trace with no session_start resumes under its directory's name. Resolves that session's context id and runs under it, so it is --context with the id looked up for you. Loads the conversation record — or, under transport: process, hands the harness the session id that context's harness session map holds — even when the capsule declares lifecycle.conversation: stateless. A context with neither refuses the launch with E-RUN-017. Passing it together with --context refuses the launch with E-RUN-015. Reads the named session's trace.jsonl only as far as its first task, so a session whose process was killed — leaving the file ending mid-record — still resumes. mur trace show over that same file skips the torn last line and says so
--resume-mode full How --resume puts the loaded conversation in front of the model. full loads the record verbatim; compact runs the capsule's on-compaction hook over it first and continues from the summary, which is the answer when the conversation would not fit the context window at all. full is often the cheaper of the two: a verbatim reload can hit the provider's prompt cache, while compaction changes the prefix from the first altered token, guarantees a cache miss, and costs an extra inference call to produce the summary. compact with no hook bound to on-compaction refuses the launch with E-RUN-018, and compact under transport: process — where the harness holds the history and murmur has none to summarize — with E-RUN-037
--forget-session off Drop the harness session --context names, then run this launch's first task as a new conversation under the same context id. The answer to E-RUN-036, where the harness no longer holds the conversation a context names and every later task in it fails the same way. The entry is deleted from the harness session map and the run's trace records a harness_session_forgotten event naming the context, the id that was dropped and requested_by: "cli"; asked for a context with no entry, it drops nothing and records nothing. Applies to the launch's first task and to no later one. Requires --context, and cannot be combined with --resume, which asks for the opposite. Only transport: process has a harness session, so every other transport refuses the launch with E-RUN-039
--workdir <manifest-dir>/workdir/<session-id> The capsule's work folder. The runtime creates nothing in it, and the session's records are kept under ~/.murmur/records/<this directory>/. See Session directories
--bind 127.0.0.1 Address the capsule's HTTP server binds. Use 0.0.0.0 to accept connections from other machines. An address off loopback on a capsule declaring no network.authentication prints W-SEC-032 on stderr
--json off Emit launch info as a single JSON line instead of human-readable output. Takes precedence over --verbose
--verbose, -v off Add workdir:, records:, manifest:, driver: and skills: to the startup lines, and formation: for a formation member
--lifecycle-task-acceptance — Override lifecycle.task_acceptance (none|single|queue)
--lifecycle-after-task — Override lifecycle.after_task (exit|sleep). The resolved value is the one W-SEC-024 reports, so --explain-scope previews the warning this override produces; a value outside the two is refused there as on a real run
--no-env-file off Skip auto-loading the workspace-root .env file for this invocation. Recommended default for CI/CD pipelines
--containment — Require at least this containment class (advisory|scoped|sealed). Combines with the manifest's capabilities.containment and the workspace containment config by taking the strongest of the three — this flag can only raise the effective floor, never lower one another source already set. See Containment class
--explain-scope off Print the effective grant set and the declared/achieved containment classes, then exit 0 without staging or launching anything — no artifact pull, no component compile, no workdir. Reports even when the declared floor is not met. The Resource plane block, and io_max under --json, report whether the declared cgroup_io_bytes_per_sec ceiling applies on this host. Also lists every path the runtime writes into the work folder in a Runtime writes block — the line paths are relative to the work folder, then - peer-in (directory, peer-fetch) and - out (directory, script-session) — and as runtime_writes under --json; see What the capsule changed. A Runtime directory block after Containment gives the session's runtime directory and whether the capsule's commands can change it, a Records directory block after it does the same for the records directory, and a Temporary folder block after that gives the session's temporary folder, the value of TMPDIR, and whether a literal /tmp reaches the folder; --json carries them as runtime_dir, records_dir and temp_dir. Refused with E-IO-004 when HOME cannot hold ~/.murmur, as the launch would be. Unless the session composes a sealed root, a Not protected here block prints under Containment, naming what its filesystem mechanism leaves unrestricted and why a ~-based probe is not evidence of containment — see Testing containment honestly; --json carries the same statements as filesystem_boundary. An Images block reports the context.images limits, as images under --json — see The Images block
--deadline — End the session this many seconds after mur run started, staging included, whatever it is doing. A whole number from 1 to 315360000 (ten years). The runtime ends the session as a first SIGTERM does, records deadline_reached, and exits with E-RUN-053; see The deadline. Requires an agent capsule: on a manifest with no inference: block the run fails with error[E-IO-003] before anything is staged. Under --capsule the manifest checked is the one inside the artifact, so an installed capsule with no inference: block fails the same way. With --roster, the formation's one deadline, counted from the launcher's start
--system-prompt — Replace the manifest's system prompt for this invocation only. Overrides inference.system_prompt, inference.system_prompt_file and inference.system_prompt_artifact alike, whichever the manifest used — and applies just as well when it declared none. The files inference.instruction_files lists are still appended after it. The value is trimmed; an empty or whitespace-only value clears the prompt rather than setting one. murmur.yaml is not modified. Requires an agent capsule: on a manifest with no inference: block the run fails with error[E-IO-003] before anything is staged. Inert under --explain-scope. See Override the prompt for a single run

For the output modes, the read-only pre-flight checks and driving a capsule over HTTP, see Run a capsule from the CLI or from another program.

  • Auto-loads .env from nearest workspace containing murmur.yaml, unless --no-env-file is passed
  • Creates/uses murmur.lock in manifest directory, except under --capsule, which has no project directory to hold one
  • Refuses with E-IO-004, before any artifact store, lockfile or registry is read, when HOME is unset, relative, or names a directory in which ~/.murmur cannot be created

Readiness line. Under --json, standard output carries one JSON object the moment the capsule is addressable:

Key Present Value
url always host:port of the door, empty for a script capsule
pid always The mur run process id
session_id always The session id
name, version always The capsule's name and version
workdir always The session's work folder
records_dir always The session's records directory, where its trace is
formation_id a formation member The formation id
auth an authenticated door "bearer"

Door tokens. mur run prints no door token. A capsule declaring network.authentication says its door is authenticated instead, once the door is up:

Mode Output
Human murmur: auth bearer (mur token <session_id>), right after murmur: url
--json The readiness line gains "auth": "bearer". A capsule declaring no network.authentication prints the line without the key
murmur: url localhost:41873
murmur: auth bearer (mur token ses_019f01a940ce7761854e768ecbe3d399)
session: ses_019f01a940ce7761854e768ecbe3d399

Every token is in the session's running-capsule record; read one with mur token. A token is valid until the session ends. What each token reaches is in Agent Card: Security.

Formation membership. A session launched with MURMUR_FORMATION_ID set to a formation id is a member of that formation, and every child it delegates to joins the same one. A formation id is frm_ followed by 32 lowercase hex digits; mur run --roster mints one and hands it to every member it starts. A session launched without it belongs to no formation and prints neither of these:

Mode Output
Human --verbose adds formation: <formation-id> after the other startup lines
--json The readiness line gains "formation_id": "<formation-id>"
{"formation_id":"frm_019f01a93ff27c1e9a3b5d0c4e8f2a61","name":"researcher","pid":48213,"session_id":"ses_019f01a940ce7761854e768ecbe3d399","records_dir":"/home/me/.murmur/records/home/me/project/workdir/ses_019f01a940ce7761854e768ecbe3d399","url":"localhost:41873","version":"0.1.0","workdir":"/home/me/project/workdir/ses_019f01a940ce7761854e768ecbe3d399"}

The variable is read before the workspace .env is loaded, so a .env line naming it has no effect. A value that is not a formation id refuses the launch with E-RUN-044. mur run --resume joins a formation only when its own environment names one.

MURMUR_FORMATION_LIFELINE is set beside it by mur run --roster and is not for operators. A member launched by hand without it prints W-RUN-007 — see A member started by hand.

Registration. A capsule whose manifest declares capabilities.spawn.allow, and any capsule launched with --spawn-grant-stdin, registers with the daemon named by MURMUR_ROOST_URL at launch and is retired from it when the session ends. A registration that cannot be completed refuses the launch with E-RUN-019. Every other capsule opens no connection at all and needs no daemon running. See the mur-roost HTTP API.

Artifact pre-check: Before staging, mur run verifies that all artifacts declared in the manifest are installed locally. If any are missing it exits immediately with error[E-RUN-008] and a mur install hint. Run mur install first to fetch missing artifacts.

Current runtime constraints:

  • mur run accepts all four artifact runtimes: tool, driver, hook, and skill
  • tool artifacts are exposed as model-callable tools; driver, hook, and skill artifacts are staged for runtime use but are hidden from the model's tool inventory
  • tool artifacts are offered to the model under their bare name, the part after the /: murmur/tool-git is the tool tool-git. Two declared artifacts with one bare name are refused with E-CAP-020
  • skill artifacts install skill.md to tools/<name>/skill.md in the session's runtime directory, <name> being the bare name; the agent reads them by calling the skill, or at $MURMUR_RUNTIME_DIR/tools/<name>/skill.md
  • Capsule component discovery:
  • prefers capsule.wasm
  • otherwise requires exactly one root *.wasm
  • under --capsule, the root component of the artifact archive, with no project directory searched
  • Agent capsules require inference.driver.artifact in murmur.yaml on both transports: an http driver under transport: http, a process driver under transport: process. A missing driver exits with error[E-RUN-005]; a transport: process harness binary that cannot be resolved exits with error[E-RUN-006]

The Images block

mur run --explain-scope prints the context.images limits the session applies:

Images
  max_bytes:     5242880
  keep:          5
  max_per_turn:  5

Under transport: process the harness holds the conversation, so keep does nothing there, and max_per_turn counts the images of one tool call. The block says so:

Images
  max_bytes:     5242880
  keep:          5 (does nothing here: the harness holds the conversation)
  max_per_turn:  5 (counts the images of one tool call here)

--json carries the same facts as images: {"keep", "max_bytes", "max_per_turn", "keep_applies"}, with keep_applies false under transport: process. The launch writes the same object into session_start.effective_grants.

Status and exit code

When the session ends, mur run prints one status: line and exits:

status: Exit code The launch
ok 0 Ran, and every task that decides its outcome completed. A turn cut off at inference.max_tokens still completes its task: it is marked, and warned about with W-RUN-001
failed 1 Ran a task that failed — a failed driver call, a response it could not act on, a compaction hook error, a request-input wait past lifecycle.input_timeout_secs — and prints E-RUN-040 with the reason. Also the status of a launch that could not run at all, with that error's own code
max_turns_reached 1 Ran a task that used every turn inference.max_turns allows, and prints E-RUN-040
spend_ceiling_reached 1 Ran a task a spend ceiling stopped, and prints E-RUN-040
canceled 1 Ran a task that was canceled — by CancelTask, murmur.StopSession, mur stop, SIGTERM, or the end of the session that delegated it — and prints E-RUN-040
deadline_reached 1 Reached its --deadline, and prints E-RUN-053. The task it was running ends canceled
formation_ended 0 Was a formation member running a task when its formation ended, and the wind-down canceled that task. Prints no error. The task itself still ends canceled
trapped 1 Ran a script capsule that stopped with an error

The tasks that decide the outcome:

  • the task --task or task.md gives the launch, under every lifecycle;
  • an A2A task, when the capsule ends after it (lifecycle.after_task: exit, or task_acceptance: single), or while the session is closing out.

A peer's task on a capsule that sleeps between tasks reports through GetTask and its stream, and leaves the launch's status alone. The first deciding task that did not complete sets the status, and no later run replaces it. The trace's session_end.exit_status carries the same value.

--json prints no status: line; the exit code and error are the same.

A closed stream

Under --json, standard output carries one line: the readiness line, written the moment the capsule is addressable. A supervisor that launches mur run --json, reads that line and closes the pipe leaves the session running to its own end, with the exit status it would have had. Closing standard error has the same effect.

A line the runtime could not hand to a closed stream is kept where the session keeps its other diagnostics.

The line was headed for Where it lands instead
Either stream, once the records directory exists logs/bootstrap.log in the session's records directory
Either stream, before the records directory exists Nowhere: a warning raised while the manifest and its grants are still being read has no file to fall back to

The fallback writes to that file alone, so --json standard output holds the readiness line and nothing else.

This applies to every member of a --roster formation. Every other command ends its output instead, as Piped output describes.

SIGTERM

An agent capsule started with mur run that receives SIGTERM — from mur stop, kill, or a service manager — ends its session the way a clean exit does:

  1. Every live task is cancelled, as murmur.StopSession cancels them, and no new task is started.
  2. Each cancelled task's task_end is written, after its on-task-end hooks.
  3. The session teardown runs: on-session-end, waiting for asynchronous hooks to finish, shell_abandoned records for detached commands, session_end, and removal of the running-capsule record.
Bound Effect
A second SIGTERM The process exits at once, with status 143
20 seconds after the first SIGTERM The process exits with status 143, wherever the teardown is

A session that was running a task when the first SIGTERM arrived, and whose teardown finishes, ends status: canceled and exits 1; one that was waiting for a task ends status: ok. See Status and exit code.

A teardown cut short by either bound, or by SIGKILL, leaves the rest undone. A script capsule, and every session that mur eval run or mur new runs, has no SIGTERM handling: the process ends at once.

The deadline

--deadline <SECONDS> is a hard limit on the whole mur run, counted from the moment the command started.

Aspect Behaviour
Reached The session ends as a first SIGTERM ends it, under the same 20-second bound: every live task is cancelled and ends canceled, and the teardown runs. A task kept working by lifecycle.keep_working ends completed with its last turn end's answer instead, and so does a task whose done check is running or has reopened it. Every task the deadline ended writes task_end.exit_status deadline_reached
A foreground shell command running, on transport: http Stops being waited for at once: it is moved to the background, the task ends canceled with task_canceled.phase "shell", and the session's end kills the command
Trace deadline_reached before the wind-down's records, then session_end with exit_status: "deadline_reached"; session_start.deadline names it from the start. See trace.jsonl
Exit status: deadline_reached, exit code 1, E-RUN-053 — unless an earlier task already failed, which keeps its own status
Already past at launch The session ends before any task runs
A formation's deadline before its entry member started The launcher stops every member it started and exits 1 with E-RUN-053, naming each stopped member
The agent Is told once, on a tool result, when lifecycle.deadline_notice_secs are left, and its session-status tool reports the time left — see The deadline notice

A session runs under the earliest deadline placed on it. The others are not recorded.

Deadline Applies to Counted from
Its own --deadline The session Its own mur run's start
The formation's --deadline Every member of a formation The launcher's start, as one moment shared by every member
Its parent's deadline A child launched by delegate-task or a plan capsule step, and every child below it The parent's own deadline: the child runs no longer than its parent

session_start.deadline.source and deadline_reached.source say which one it was: own or inherited. A script capsule launched as a child takes no deadline from its parent; it ends when its parent does, through its spawner lifeline. lifecycle.delegation_deadline_secs bounds the parent's wait for a child separately, and whichever falls first ends the child.

Launching a formation (--roster)

mur run --roster launches every member of the formation a project's roster.yaml declares. Each member is a mur run --capsule <capsule> --capsule-version <version> process of its own; the launcher runs no session itself. The order and the rules are in Launching a formation.

What the entry member does, and when the formation ends, depends on --task:

Launch The entry member The formation ends
--task "…" Runs that one task, then exits When the entry member exits after its task
No --task Takes tasks at its door one after another, with no idle timeout. A client such as nexus drives the formation through that door, with the entry member's operator token On mur stop <formation-id>, a signal to the launcher, or --deadline

The launch sets the entry member's lifecycle either way, in place of the one its manifest declares. Either way the entry member's task is a task on its door from the moment the door is announced: ListTasks lists it, SubscribeToTask follows it and CancelTask reaches it. No peer ever calls the entry member.

Flag Passed to
--task The entry member
--json The entry member. Peers always run with --json
--verbose The entry member
--no-env-file Every member. The launcher itself never loads .env
--containment Every member
--deadline Every member, as the formation's one deadline, counted from the launcher's start. See The deadline

--roster cannot be combined with --manifest, --capsule, --capsule-version, --spawn-grant-stdin, --system-prompt, --context, --resume, --resume-mode, --forget-session, --lifecycle-task-acceptance, --lifecycle-after-task, --workdir, --bind or --explain-scope.

Standard output under --json carries exactly two lines:

  1. The formation line, printed once every peer's door answers and before the entry member starts.
  2. The entry member's own readiness line, unchanged, from the entry member's output.
{"deadline":null,"entry":"lead","formation_id":"frm_01a106501a1d78328710ed56d19a10a6","peers":[{"name":"worker","pid":1979177,"session_id":"ses_01a1065027fe7a5295b53597e7671a53","url":"http://localhost:36061","workdir":"/home/me/.murmur/formations/frm_01a106501a1d78328710ed56d19a10a6/worker"}]}
Formation line key Value
deadline {"at", "secs"} under --deadline: at, the RFC 3339 UTC moment every member ends, and secs, the --deadline value. null without one
entry The entry member's roster name
formation_id The id every member was launched with, frm_ followed by 32 lowercase hex digits
peers One object per non-entry member, in roster order: name, pid, session_id, url as http://host:port, and workdir, the peer's own directory ~/.murmur/formations/<frm_id>/<member>

Before anything starts, the launcher writes one W-ROS-001 line to stderr, behind [mur run], for each member more members may call than its lifecycle holds at once, with or without --json. The formation launches all the same.

Neither the formation line nor any member's output carries a door token. Without --json, the launcher writes the same information to stderr as a formation: block, and the entry member writes its usual startup lines to stdout. Under --deadline the block's second line is deadline <at> (<secs>s after mur run started).

Every line a peer writes to stderr, and every stdout line after its readiness line, goes to the launcher's stderr behind a [<member>] prefix:

[coder] [capsule-runtime] formation lifeline closed — the formation has ended; cancelling live tasks and ending the session

The entry member writes to the launcher's own stdout and stderr, unprefixed, and stays in the launcher's process group, so ^C at a terminal reaches it as it reaches a hand-run mur run.

A warning about the host rather than a member, W-SEC-013, is printed once by the launcher, unprefixed, after the roster is admitted and before any member starts. No member repeats it.

The launcher Exits with
The entry member's process ended The entry member's exit code, or 128 plus the signal that ended it
mur stop <formation-id> ended it 143: the stop is a SIGTERM to the launcher
A refusal before the entry member started — admission (E-ROS-*, E-REG-005), E-RUN-045, E-RUN-046 1
The launcher received SIGINT, SIGTERM or SIGHUP 130, 143 or 129

By the time the launcher exits, every member it started has been stopped and reaped. A launcher killed with SIGKILL, or by the OOM killer, stops none itself; each member then winds down on its own lifeline. See How a formation ends.

Once every member is reaped, and when the entry member declares trace.retain, the launcher removes the project's ended formation directories that policy expires, and names each on stderr, newest first. The exit code is unchanged. See Removing formation directories.

[mur run] retention: removed formation frm_01a106501a1d78328710ed56d19a10a6 (max_sessions)

mur ps

List the capsules running on this machine, with the address each agent session answers on.

mur ps [--verbose | --json]
Flag Default Description
--verbose off Name every record the listing removes, one line each, and add a summary line for every formation known only from removed records
--json off Print one JSON object, the stable interface, instead of the table. Removes nothing. Given with --verbose, it is a usage error, exit 2

mur ps is host-scoped, exactly like docker ps: a capsule deployed onto another machine writes its record on that machine, so it is that machine's mur ps that lists it.

Column Width Carries
SESSION 36 The full session id, never abbreviated, so it can be copied into the next command
CAPSULE 24 name@version from the manifest the session was launched from
STATUS 12 running or unreachable
DETACHED 8 yes when the capsule outlives the window that launched it, no when it dies with it
UPTIME 9 HH:MM:SS since the session started, prefixed Nd past a day
FORMATION 36 The full formation id of a member, - for a session in no formation. Present only when a record that keys the listing names a formation
URL — The host:port the capsule's A2A door is bound to; - for a script capsule, which binds no door
SESSION                               CAPSULE                   STATUS        DETACHED  UPTIME     URL
ses_019f01a940ce7761854e768ecbe3d399  my-worker@0.1.0           running       yes       00:14:02   localhost:41235
ses_019f0193c7d871a5b2e30ff41a7c0ce2  my-agent@0.2.0            unreachable   no        01:03:55   localhost:41102

A machine running nothing prints one line and exits 0:

no running capsules

An absent ~/.murmur/running/ and an empty one are the same fact about the machine, and read the same way. A ~/.murmur/running/ that cannot be read — a file where the directory belongs, or a directory this user may not list — is neither: mur ps prints nothing on stdout and fails with E-RUN-028.

Listing also prunes the directory: mur ps removes the record of every capsule whose process is gone, and every file in the directory that is neither a readable record nor a record an older mur wrote. What it removed is printed on stderr, before the table. A read that removes nothing prints nothing on stderr.

A record an older mur wrote is left in place, counted in no summary, and named on stderr ahead of any pruned line, with or without --verbose. Stop that capsule with the mur that started it, then delete the file once that capsule is gone; mur ps never deletes it:

skipped: /home/me/.murmur/running/ses_019f0193c7d871a5b2e30ff41a7c0ce1.json — a record an older mur wrote, which this mur cannot read; it is left in place, so stop that capsule with the mur that started it, then delete this file once that capsule is gone
Mode Stderr
Default One line counting every removal
--verbose One pruned: line per removal
pruned 302 stale records; mur ps --verbose names each

With --verbose, a removed record names its session and the reason, a formation member's line ends with its formation, and an unreadable file is named by its path:

pruned: ses_019f0193c7d871a5b2e30ff41a7c0ce2 — no process holds pid 48213
pruned: ses_019f0193c7d871a5b2e30ff41a7c0ce3 — no process holds pid 48214 (formation frm_019f01a93ff27c1e9a3b5d0c4e8f2a61)
pruned: /home/me/.murmur/running/ses_019f0193c7d871a5b2e30ff41a7c0ce4.json — not a readable record

Exit codes:

  • 0 — the records were read, whether or not any row was printed
  • 1 — ~/.murmur/running/ could not be read (E-RUN-028)

What each row was verified against

Every record is put through all three layers described under A record is a hint before its row is printed, and what the layers say decides both the STATUS column and whether the record survives the read.

Layers STATUS The record Reason on the pruned: line
All three pass running Kept —
A script capsule's record: the process is the one that wrote it running Kept —
The process is the one that wrote the record; the door did not answer unreachable Kept —
A process holds the process id and its start time could not be read unreachable Kept —
No process holds the process id No row Unlinked no process holds pid N
The process holding the process id started at another time No row Unlinked pid N is held by a process that started at another time

Neither a quiet door nor a start time that could not be read is evidence that the process is gone, and unlinking the record would throw away the only handle anyone has on something still running. A capsule's door is served apart from its turns, so a capsule busy with a turn still reads running. A capsule whose process is alive reads unreachable when:

  • the process is suspended, for example by SIGSTOP or a debugger
  • the host is too loaded to schedule the capsule within the probe's deadline
  • the capsule's runtime has every thread it serves requests on occupied

Rows are sorted by session id descending — the same order @N counts in — so the first row is what @1 names. When a formation is listed, its members are kept together, so past the first row count @N by session id, not by row.

mur ps --json

List the same capsules as the table, as one pretty-printed JSON object, and remove nothing.

mur ps --json
{
  "capsules": [
    {
      "session_id": "ses_019f01a9c3e47d1a9b2c3d4e5f6a7b8c",
      "status": "running",
      "url": "localhost:41235",
      "pid": 48213,
      "process_start": "9123456",
      "capsule_name": "planner",
      "capsule_version": "0.1.0",
      "workdir": "/home/me/proj",
      "records_dir": "/home/me/.murmur/records/home/me/proj/ses_019f01a9c3e47d1a9b2c3d4e5f6a7b8c",
      "outlives_launcher": true,
      "started_at": "2026-10-20T09:12:44Z",
      "formation_id": "frm_019f01a93ff27c1e9a3b5d0c4e8f2a61",
      "formation_member": "planner",
      "formation_entry": true
    }
  ],
  "formations": [
    {
      "formation_id": "frm_019f01a93ff27c1e9a3b5d0c4e8f2a61",
      "entry_member": "planner",
      "listed": 1,
      "ended": 1,
      "gone_without_ending": 1,
      "not_listed": [
        {
          "session_id": "ses_019f01a9b1d27c3e8f0a4b5c6d7e8f90",
          "formation_member": "worker",
          "reason": "gone_without_ending",
          "record_kept": true
        },
        {
          "session_id": "ses_019f01a940ce7761854e768ecbe3d399",
          "formation_member": "reviewer",
          "reason": "ended",
          "record_kept": false
        }
      ],
      "searched_roots": 3,
      "unreadable_roots": 0
    }
  ]
}
Key Type Meaning
capsules array One entry for each record mur ps would print a row for, sorted by session id descending. Empty when nothing runs, and when ~/.murmur/running/ is absent or empty
capsules[].session_id string The session this record is for
capsules[].status string running or unreachable: the STATUS column mur ps prints for this record, from the same three layers, door probe included for an agent session
capsules[].url string or null The host:port the A2A door is bound to, with no scheme. null for a script capsule, which binds no door
capsules[].pid integer The process running the session
capsules[].process_start string That process's start time, an opaque token local to this machine
capsules[].capsule_name string name: from the manifest the session was launched from
capsules[].capsule_version string version: from that manifest
capsules[].workdir string The session's work folder, as the record holds it
capsules[].records_dir string The session's records directory
capsules[].outlives_launcher boolean Whether the capsule survives the window that launched it
capsules[].started_at string When the session started, RFC 3339 UTC, as the record holds it
capsules[].formation_id string or null The formation this session is a member of. null for a session in no formation
capsules[].formation_member string or null The member's roster name, set only for a member mur run --roster launched. null otherwise
capsules[].formation_entry boolean true for the entry member of a formation, false for every other session
formations array One entry for each formation an entry of capsules names, in the order of that formation's first entry in capsules. Empty when no entry of capsules names a formation. A formation known only from records whose process is gone has no entry
formations[].formation_id string The frm_ id
formations[].entry_member string or null The entry member's roster name, from the record in this read that says it is the entry member: an entry of capsules, else a record of the formation whose process is gone. null when no record in this read says so
formations[].listed integer How many entries of capsules carry this formation_id
formations[].ended integer How many entries of not_listed have reason: "ended"
formations[].gone_without_ending integer How many entries of not_listed have reason: "gone_without_ending"
formations[].not_listed array Every member of this formation this read found that has no entry in capsules, sorted by session id descending: each record of the formation whose process is gone, and each session in the session roots whose trace's session_start names the formation and that has no record
formations[].not_listed[].session_id string The member's session
formations[].not_listed[].formation_member string or null The roster name: the record's formation_member when the record is kept and names one, else the trace's session_start.formation_member. null when neither names one, as for a member's delegated child
formations[].not_listed[].reason string ended: the member's trace holds a session_end. gone_without_ending: no session_end was found — the member was killed outright, its trace could not be read, or it never recorded one
formations[].not_listed[].record_kept boolean true when the member's record is on disk and its process is gone: the record plain mur ps removes and counts as pruned now
formations[].searched_roots integer Session roots that were listed
formations[].unreadable_roots integer Session roots that exist and could not be listed

A formations entry counts what this read found and never says a formation is complete. There is no key for a formation's total member count: this read does not consult the roster, so a member that left neither a record nor a trace in a searched session root is counted nowhere.

A record's door token and credentials are never printed.

Record mur ps --json Plain mur ps
All three layers pass Listed, running Listed, running
Process alive, door quiet or start time unreadable Listed, unreachable Listed, unreachable
Process gone Left out, record kept; a formation member is counted in its formation's not_listed with record_kept: true Left out, record removed; a formation member is counted as pruned now
File that is not a readable record Kept Removed
A record an older mur wrote Kept, named on stderr as a skipped: line Kept, named on stderr as a skipped: line

A missing ~/.murmur/running/ is not created. Stderr carries only the skipped: lines.

These keys are a stable interface. Each keeps its name, type and meaning. New keys may be added, and a consumer ignores keys it does not know.

Exit When
0 The records were read, whether or not any capsule is listed
1 E-RUN-028: ~/.murmur/running/ exists and cannot be listed, or HOME cannot locate it
2 --json and --verbose were given together

A failure prints nothing on stdout.

Formations

When a record that keys the listing carries a formation id, the listing changes in three ways. When none does, the output is the plain listing above.

Mode Records that key the listing
Default Every listed record
--verbose Every listed record, and every record this read pruned
  1. The FORMATION column appears between UPTIME and URL.
  2. Rows are grouped. A formation's listed members form one group, and a session in no formation is a group of one. Groups are ordered by their newest session, newest first, and rows within a group by session id descending. The first row is still the newest session.
  3. After the table — or after no running capsules — come a blank line and one summary line per formation: listed formations in row order, then, with --verbose, formations known only from records pruned in this read.
SESSION                               CAPSULE                   STATUS        DETACHED  UPTIME     FORMATION                             URL
ses_019f01a9b1d27c3e8f0a4b5c6d7e8f90  researcher@0.1.0          running       yes       00:02:11   frm_019f01a93ff27c1e9a3b5d0c4e8f2a61  localhost:41873
ses_019f01a940ce7761854e768ecbe3d399  writer@0.1.0              unreachable   yes       00:02:14   frm_019f01a93ff27c1e9a3b5d0c4e8f2a61  localhost:41235
ses_019f01a95a0b7e21a3c4d5e6f7a8b9c0  my-agent@0.2.0            running       no        00:02:13   -                                     localhost:41102

formation frm_019f01a93ff27c1e9a3b5d0c4e8f2a61: 2 listed (1 running, 1 unreachable), 1 pruned now; not listed: 1 ended

A summary line counts what it found and never reports a formation as complete. It reads:

formation <id>: <L> listed (<statuses>)[, <P> pruned now][; <not-listed>][; <U> session root(s) could not be read]
Part Says
<L> listed (<statuses>) How many of the formation's members have a row, as <n> running and <n> unreachable, each only when non-zero. With no row, 0 listed and no parentheses
, <P> pruned now Member records this read removed. Only when non-zero
; not listed: <n> ended, <n> with no record Members found in the session roots that had no record in this read. ended traces hold a session_end; with no record traces do not — a member killed outright, or one whose record was never written. Each count appears only when non-zero
; no other member found in <k> session root(s) Replaces not listed when the session roots hold no member beyond the ones with a record
; <U> session root(s) could not be read Session roots that exist and could not be listed. Only when non-zero

The same counts are in mur ps --json's formations entry. For the same records, read with --json first, since it removes nothing, and plain mur ps second:

Summary line part formations[] in mur ps --json
<L> listed listed
, <P> pruned now Entries of not_listed with record_kept: true
not listed: <n> ended Entries of not_listed with reason: "ended" and record_kept: false
not listed: … <n> with no record Entries of not_listed with reason: "gone_without_ending" and record_kept: false
no other member found in <k> session root(s) searched_roots, printed when both counts above are zero
<U> session root(s) could not be read unreadable_roots

The session roots searched are:

  • each records group holding the records_dir of a listed member, or of a member whose process is gone — the record plain mur ps prunes and --json keeps;
  • each peer's records group, the one its directory under ~/.murmur/formations/<formation-id>/ names, whether or not a record of that peer remains — see Member directories.

A member that ran under another root and has no record is not counted. mur trace show <formation-id> lists the members by name.


mur stop

End one running capsule, or every member of a formation, and report what was left behind.

mur stop <SESSION> [--timeout <SECONDS>]
mur stop <FORMATION-ID> [--timeout <SECONDS>]
Argument Default Description
SESSION — A session address naming a running capsule, or a formation id (frm_…) to stop every member — see Stopping a formation
--timeout 10 Seconds to wait after SIGTERM before escalating to SIGKILL. 0 escalates immediately, with no grace period

Three steps, in this order:

Step What it does
1. murmur.StopSession through the A2A door Cancels every task the session still holds and reads what it leaves running, then waits up to 5 seconds for the trace to record how each cancelled task ended
2. SIGTERM to the recorded process Ends the capsule. An agent capsule records its remaining endings and runs its teardown first — see SIGTERM
3. SIGKILL after --timeout seconds Only if the process is still there

A cancelled task's ending is its task_end record, or, for a task cancelled before it started, its task_canceled record with phase: "queued".

A script capsule has no door, so step 1 is skipped and no residue: line is printed: mur stop begins with SIGTERM, which ends a script capsule at once, with no session_end in its trace.

The door step is the only moment anything can ask the capsule what it was running. The record of a detached shell command dies with the process, so the question is asked while the capsule is still answering; the session's end in step 2 then kills the command with every other process the session started. A delegated sub-capsule — one started by delegate-task or by a plan's capsule step — is ended by the cancelled task that started it, and is named too.

stopped: ses_019f01a940ce7761854e768ecbe3d399
capsule: my-worker@0.1.0
signal:  SIGTERM
canceled: tsk_019ed5211c827f63a8fe4be623277c55
running: wrk_9f2a1c  detached shell  sleep 30
ended:   dlg_7b31de  delegation  worker@0.1.0

The running: and ended: lines are the ones mur cancel prints for the same items.

The three answers about what it left behind

Line Means
running: … or ended: …, one per item The capsule answered and named these: running: for a detached shell command, running when it was named and killed when the session ended; ended: for a delegation, whose sub-capsule the cancelled task ended
residue: nothing else was left running The capsule answered and had nothing to name
residue: unknown — the capsule could not be asked: … The door did not answer, and the reason says why

A capsule that answered with nothing and a capsule that could not be asked are different facts about the machine, so they are different lines. Both exit 0: the session was ended either way, and only the accounting is incomplete.

One more line appears only when an ending could not be recorded — the capsule was still busy when the wait and --timeout ran out, and SIGKILL ended it. It prints after the canceled: lines, one per task:

Line Means
unended: <task_id> the trace does not record how this task ended The task was cancelled, but trace.jsonl has no ending for it

A stop whose capsule recorded every ending prints no unended: line.

There is no --url

Two of the three steps signal a local process id, so a URL-addressed stop could only ever perform the first one. mur stop --url is refused by the argument parser rather than silently doing a third of the job. A capsule on another machine records on that machine, so it is that machine's mur stop that ends it.

Exit codes:

  • 0 — the session was ended, whether or not its door answered
  • 1 — the address named no running session (E-RUN-022), the session could not be ended and is still running (E-RUN-024), or the running-capsule records could not be read (E-RUN-028)

mur stop refuses to signal a process id it cannot confirm, and no signal of any kind is sent.

The recorded process Refusal The record
Started at another time than the host reports — the number was inherited E-RUN-022 Unlinked
Start time could not be read E-RUN-024 Kept

Stopping a formation

An argument starting frm_ is a formation id, the full id in the FORMATION column of mur ps. mur stop ends the formation the way it ends itself: it sends SIGTERM to the formation's launcher, the mur run --roster process, which closes every member's lifeline. Every member, the entry member included, records formation_ended and winds down. No member holding a lifeline is sent a signal or called through its door.

The launcher writes no record. mur stop finds it through the formation_launcher key of its members' records, and verifies its process id and start time immediately before each signal, as for a session.

Each running member is one of three kinds:

Member Its record mur stop
Started by the launcher formation_lifeline: true Signals nothing; the launcher's teardown ends it
Delegated sub-capsule spawned_by Signals nothing; it ends with the session that delegated to it
Started by hand with MURMUR_FORMATION_ID (warned W-RUN-007) Neither key Stops it as mur stop <session> would: door, SIGTERM, SIGKILL after --timeout

The steps, in order:

Step What it does
1. Read the records Every record with this formation_id whose process is running is a member. A record whose process is gone is removed and not listed
2. Signal the launcher SIGTERM to the one launcher the members record, then SIGKILL to its process id alone after --timeout seconds
3. Stop each member started by hand One at a time, newest first
4. Wait Up to 25 seconds, the grace the launcher gives its own members, for every member's process to be gone. Each record is removed as its process goes

The report, on stdout:

stopped:  frm_019f01a93ff27c1e9a3b5d0c4e8f2a61
launcher: pid 48190, SIGTERM
member:   ses_019f01a95a2c7d10b3e4f5a6b7c8d9e0  coder@0.1.0  started by hand — stopped as one session below
member:   ses_019f01a940ce7761854e768ecbe3d399  planner@0.1.0  formation_ended
member:   ses_019f01a93ff27c1e9a3b5d0c4e8f2a62  reviewer@0.1.0  formation_ended

stopped: ses_019f01a95a2c7d10b3e4f5a6b7c8d9e0
capsule: coder@0.1.0
signal:  SIGTERM
residue: nothing else was left running

One member: line per member, newest session first. Each member started by hand then gets the block mur stop <session> prints, after a blank line, or error: <message> when it could not be ended.

launcher: line Means
pid <pid>, SIGTERM The launcher exited within --timeout seconds of SIGTERM
pid <pid>, SIGKILL after <timeout>s The launcher was still running after --timeout seconds and was sent SIGKILL
none — pid <pid> had already exited, so every lifeline was already closed The recorded launcher had exited; nothing was signalled
none — no running member records one No running member names a launcher: every member was started by hand or delegated

A member's ending is the first row that applies:

Ending Means
started by hand — stopped as one session below A member started by hand; its block follows
still running Its process was still running when the wait ran out
formation_ended Its trace records formation_ended
spawner_ended Its trace records spawner_ended
session_end <exit_status> Its trace records session_end, with that exit status, and neither of the above
no session_end Its trace records no ending

--timeout is the grace between SIGTERM and SIGKILL for each process mur stop signals itself: the launcher, and each member started by hand. It never applies to a member holding a lifeline. --timeout 0 sends the launcher SIGKILL at once; the kernel then closes every lifeline it held, and every member still records formation_ended.

Exit codes:

  • 0 — every member's process is gone
  • 1 — one of these:
Code Message Signalled
E-RUN-022 '<arg>' is not a formation id: … Nothing
E-RUN-022 no member of formation <id> is running on this machine, or formation <id> is not running: <n> member record(s) named processes that are gone, and were removed Nothing
E-RUN-024 formation <id> could not be ended: its members record <n> different launchers (pid <a>, pid <b>) Nothing
E-RUN-024 formation <id> could not be ended: SIGTERM to its launcher, pid <pid>, was refused: … Nothing
E-RUN-024 formation <id> could not be ended: its launcher, pid <pid>, was still running 5 seconds after SIGKILL, or … SIGKILL to its launcher, pid <pid>, was refused: … The launcher
E-RUN-024 formation <id> could not be ended: <n> of its members were still running 25 seconds later: <session>, … The launcher and each member started by hand; the records of the members still running are kept
E-RUN-028 The running-capsule records could not be read Nothing

An error that follows the launcher's signal is printed after the report.

A formation id is matched in full; a part of one names nothing.


mur token

Print one door token of a running capsule that declares network.authentication.

mur token [<SESSION>] [--credential <NAME>]
Argument Default Description
SESSION @1 A session address naming a running capsule
--credential operator The credential whose token to print: operator, or a name network.authentication.credentials declares

The token is read from the session's running-capsule record; the door is not asked anything, so a capsule slow to answer still has its token printed. Standard output is the token and a newline, nothing else, so a shell substitution holds exactly the token:

MURMUR_DOOR_TOKEN=$(mur token) mur watch --url localhost:41873
Code Cause
E-RUN-022 The address names no running session
E-RUN-048 The session's door is public: its capsule declares no network.authentication
E-RUN-049 The session minted no credential by that name. The message lists the names it did mint, operator first

mur watch

Stream live SSE events from a running capsule's output to stdout. The command opens a murmur.WatchSession connection to the capsule and prints each event in a human-readable format until the capsule closes or Ctrl+C is pressed.

mur watch [SESSION]
mur watch --url <host:port>
Argument Default Description
SESSION @1 A session address naming a running capsule
--url — A capsule's address, reached without resolving anything. Conflicts with SESSION

On a capsule declaring network.authentication, a session address presents the operator token from the running-capsule record, and --url presents MURMUR_DOOR_TOKEN when it is set. A door that answers 401 or 403 fails the command with E-IO-003, naming the status and MURMUR_DOOR_TOKEN.

The session is resolved against the running-capsule record and verified before the connection is opened.

Ctrl-C ends the watch, not the capsule. The capsule keeps running and keeps answering; only this connection to it closes. On connect, mur watch prints one line to stderr naming the session it is watching and saying so, which keeps stdout to SSE events alone for piping. To end the capsule itself, use mur stop.

When the program reading stdout goes away, the watch ends with exit 0 at the next event it would have printed, and the capsule keeps running. See Piped output.

Output format:

[working]  inference turn 1
[artifact] tool: bash [ok, exit 0, 12ms] | <untrusted-content source=tool:bash>
  Exit code: 0
  Stdout:
  hello

  </untrusted-content>
[working]  inference turn 2
[completed]

A status line is [<state>] <message>; a task's terminal status is [<state>] alone, and a completed task's answer follows it, wrapped and indented. <state> is murmur's word for the TASK_STATE_* name the door writes, so TASK_STATE_INPUT_REQUIRED prints as [input-required]. Streamed model text is printed as it arrives, with nothing between two pieces; a status or artifact line after text that did not end its line starts on a new line. On a capsule declaring lifecycle.conversation: threaded, the bracket is [<context id> / turn <n> / <state>]. The context id is shortened to ctx_ and its next 8 characters, then ...; an id without the ctx_ prefix shows its first 12 characters, then ....

The bracket after a tool name is the call's outcome, read from the artifact frame:

Segment Shown when
ok or error Always — error when isError is true
exit <n> The call ran a subprocess to completion
<n>ms The frame reports a duration
truncated The tool marked its result truncated

A capsule whose runtime reports no outcome prints no bracket.

Heartbeat

mur watch reads the capsule's heartbeat and discards it; no heartbeat appears in its output, and mur watch keeps waiting on a quiet connection for as long as it stays open. A pause in the output means the capsule wrote no frame: a turn waiting on an inference call, a tool call or a shell command writes none until the call returns. What a pause in the heartbeat itself means is under Heartbeat on the protocol page.

mur watch does not reconnect. When the connection ends without a capsule-closed frame, it names the last event id it received and exits. A capsule that exits, including one ended with mur stop, closes the connection without that frame:

error[E-IO-003]: connection to ses_019ed2af53da75c2aefee84ee10c34af lost after event id 4 — the capsule may still be running; run mur watch again to reattach

Exit codes:

  • 0 — the capsule closed the stream with a capsule-closed frame
  • 1 — the session address named nothing running (E-RUN-022) or a script capsule (E-RUN-056), the capsule did not answer (E-RUN-023), the connection failed, or the connection was lost before the capsule closed the stream (E-IO-003)

mur cancel

Stop one running task on a capsule. The capsule's session, its conversation and its queue are untouched: queued tasks proceed, and the capsule keeps answering.

mur cancel <SESSION> <TASK_ID>
mur cancel --url <host:port> <TASK_ID>
Argument Default Description
SESSION — A session address naming a running capsule
TASK_ID — The tsk_ id SendMessage returned, or the one GetTask reports
--url — A capsule's address, reached without resolving anything. Takes the place of SESSION
Environment variable Read by Holds
MURMUR_DOOR_TOKEN mur cancel --url, mur watch --url A door token mur token printed, presented as Authorization: Bearer to a capsule declaring network.authentication. A session address reads the operator token from the running-capsule record instead

A door that answers 401 or 403 fails the command with E-IO-003, naming the status and MURMUR_DOOR_TOKEN.

The in-flight inference call is dropped rather than waited out, and the task reaches the terminal state canceled. What the task had in flight is named in the output, one line each:

Line Item What the cancel did to it
running: A detached shell command Nothing: it runs until it finishes or its session ends, which kills it
ended: A sub-capsule the task delegated to, through delegate-task or a plan's capsule step Ended it, with the task. A plan stops with it, and runs none of its remaining steps
task:    tsk_0199c4e2f1b7712a9d3e4f5061728394
state:   canceled
running: wrk_9f2a1c  detached shell  sleep 30
ended:   dlg_7b31de  delegation  worker@0.1.0

The state: line is the task's state in murmur's word, mapped from the TASK_STATE_* name the door answers; a value that is no A2A v1.0 state is printed as sent.

Cancelling a task that has already reached completed, failed, rejected or canceled changes nothing. The door answers -32002, and the command prints the state the task ended in, names no residue, and exits 0:

task:    tsk_0199c4e2f1b7712a9d3e4f5061728394
state:   completed
nothing to cancel: the task had already ended

A task id the capsule never held fails the command with E-IO-003: cancel of <id> failed: Task not found.

Exit codes:

  • 0 — the capsule holds this task; the line printed says what state it is in
  • 1 — the capsule does not hold this task id, the session address named nothing running (E-RUN-022) or a script capsule (E-RUN-056), or the capsule did not answer (E-RUN-023)

mur control

Show or change what a running capsule's control: block lets a controller change, over its control surface.

mur control show [SESSION] [--json]
mur control set <SETTING> <VALUE> [SESSION]
mur control secret <NAME> [SESSION]
mur control forget <NAME> [SESSION]
Subcommand Does
show Lists the declared settings with their current values, and the declared secrets with whether each is set
set Changes a declared setting from the capsule's next inference call
secret Supplies a declared secret's value, read from standard input
forget Drops a declared secret's value, so its gateway's keyed requests are refused again
Argument Default Description
SESSION @1 A session address naming a running capsule
SETTING — A setting control.settings declares, e.g. inference.max_tokens
VALUE — The new value. Sent as JSON when it parses as JSON, as a string otherwise; the capsule decides what the setting takes
NAME — A secret control.secrets declares
--json off show only: print the control surface's JSON as it answered

Each subcommand resolves SESSION through its running record, reads the control token beside it, and makes one request to the capsule. There is no --url: the token is found only through the record.

mur control secret reads the value from standard input and strips exactly one trailing \n or \r\n. On a terminal it prompts value for NAME: on stderr and turns echo off for the read, restoring it on return, SIGINT, SIGTERM or SIGHUP. The value is sent only when the capsule is reached over loopback; the control surface refuses it otherwise.

Output:

$ mur control show
session:  ses_0199c4e2f1b7712a9d3e4f5061728394
settings:
  inference.max_tokens   4096
secrets:
  CARD_TOKEN             not set

$ mur control set inference.max_tokens 2048
setting:  inference.max_tokens
previous: 4096
value:    2048
applies:  next inference call

$ printf '%s\n' "$CARD_TOKEN_VALUE" | mur control secret CARD_TOKEN
secret: CARD_TOKEN
set:    yes (new)

$ mur control forget CARD_TOKEN
secret: CARD_TOKEN
set:    no

set: reads yes (replaced) when a value was already held. Neither the token nor a secret value appears in any output or error.

Exit codes:

  • 0 — the control surface accepted the request
  • 1 — the session has no control surface or the surface refused the request (E-RUN-042, which states the HTTP status and the surface's reason), the session address named nothing running (E-RUN-022) or a script capsule (E-RUN-056), or the capsule did not answer (E-RUN-023)

mur deploy run

Upload the mur binary and capsule files to an existing VM via SSH, start the capsule, and print the public A2A endpoint. The VM must already exist and be reachable via SSH — mur deploy run never provisions or terminates VMs on your behalf.

mur deploy run --host <ip> [--ssh-user <user>] [--ssh-key <path>]
               [--manifest <path>] [--workdir <path>] [--mur-binary <path>]
               [--env KEY=VALUE] [--env-file <path>] [--deploy-platform <platform>]
               [--no-precompile]
Flag Default Description
--host — IP address or hostname of the target VM (required)
--ssh-user root SSH username on the VM
--ssh-key — Path to SSH private key; uses SSH agent if omitted
--manifest ./murmur.yaml Path to the capsule manifest to deploy
--workdir — Local directory to upload as the capsule's working directory
--mur-binary — Path to a mur binary for --deploy-platform to upload. When omitted, the release named by deploy.mur_version in the manifest (or the running mur version) is downloaded from GitHub and cached at ~/.murmur/bin/mur-{version}-{platform}
--env — Environment variable in KEY=VALUE format; repeat for multiple vars
--env-file — Path to a .env file of KEY=VALUE lines, # comments ignored. Takes precedence over the .env beside the manifest, which is loaded when neither --env nor --env-file is given
--deploy-platform linux-x86_64 Platform the uploaded artifacts and mur binary are resolved for
--no-precompile off Skip compiling the uploaded WASM artifacts on the target; they compile on the capsule's first launch instead

Output — a summary box on stderr. mur deploy run emits no JSON; progress and the final box both go to stderr. Standard output carries one door: line alone, when the capsule declares network.authentication.

  ┌────────────────────────────────┐
  │  ∞  my-agent                   │
  │                                │
  │  url   http://1.2.3.4:9000     │
  │  dep   dep_01954a3b            │
  │  time  42s                     │
  └────────────────────────────────┘
Row Description
url Public A2A endpoint — http://<VM_PUBLIC_IP>:<PORT>. Use for SendMessage, GetTask, and /.well-known/agent-card.json.
dep The deployment ID, abbreviated to its dep_ prefix and first 8 hex characters. The full dep_ + UUID v7 is stored in ~/.murmur/deployments.json and listed by mur deploy ls; mur destroy accepts any unambiguous prefix.
time Elapsed wall-clock seconds

To script against a deployment, read ~/.murmur/deployments.json or parse mur deploy ls — the box is for humans and its layout is not a stable interface.

Deployment flow:

  1. Validate --manifest, --workdir, --mur-binary and every --env entry (no network calls)
  2. Resolve every declared artifact for --deploy-platform, and the mur binary
  3. Wait up to 30s for SSH to become available on the VM
  4. Upload the mur binary via scp to /usr/local/bin/mur
  5. Upload the manifest, the files it references and the optional workdir to /root/mur-<id>/
  6. Upload every artifact into the VM's global store, /root/.murmur/artifacts/<scope>/<name>/<version>/. A declared local/ artifact missing from the local stores is refused without asking a source
  7. Write the environment variables to /root/mur-<id>/.env, mode 600, when there are any
  8. Run mur precompile --json --workdir /root/mur-<id> on the VM over every uploaded artifact, with the .env exported; skipped under --no-precompile or when the capsule declares no artifacts
  9. Run mur run --manifest <path> --workdir /root/mur-<id> --json on the VM with the same .env exported; wait up to 120s for the JSON line
  10. Parse localhost:PORT from the JSON output; open the port and construct the public URL
  11. Say who may call the published door:

    Manifest Output
    No network.authentication W-SEC-032 on stderr, naming the public URL
    network.authentication door: bearer — read a token on the target: ssh <user>@<host> /usr/local/bin/mur token <session_id> on stdout. No token leaves the VM
  12. Persist to ~/.murmur/deployments.json; print the summary box

Artifacts are pre-staged in step 6, so the remote mur run finds them installed and starts without fetching anything. Step 8 compiles them with the VM's own mur, so the capsule's first launch loads compiled forms instead of compiling; see Precompile on the target.

Measured: release mur, a driver, a WASM tool and a native tool, 8 logical CPUs Median of 5 deploys
Time step 8 adds 0.52 s
First launch to first inference request, with step 8 0.20 s
First launch to first inference request, --no-precompile 0.63 s
Later launches, either way 0.20 s

The compile moves out of the capsule's start rather than adding to it, so a deploy's total time barely changes.

The flow depends on mur run --json — see mur run for the --json output shape.

Precompile on the target

Steps 8 and 9 start from the same .env, so the compile and the capsule share one MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES value, the VM's: set it with --env, or it is the default. This machine's value does not reach the VM. When the two differ, deploy prints W-DEPLOY-002; the capsule still starts warm.

Step 8 never fails a deploy:

What happened on the VM Deploy prints The capsule
Every artifact compiled, was already stored, or is not WASM nothing Loads every compiled form on its first launch
Some artifacts failed W-DEPLOY-001, naming each Compiles those on its first launch
No report: the VM's mur predates mur precompile, or the SSH command failed W-DEPLOY-001, quoting the first error line Compiles every artifact on its first launch

Example:

mur deploy run \
  --host 1.2.3.4 \
  --manifest ./my-agent/murmur.yaml \
  --mur-binary ./target/x86_64-unknown-linux-musl/release/mur \
  --env ANTHROPIC_API_KEY=sk-ant-...
# summary box on stderr: url http://1.2.3.4:9000 / dep dep_01954a3b / time 42s

Error codes:

Code Meaning
E-IO-001 --manifest, --workdir, or --mur-binary path not found
E-DEPLOY-001 No --host given, or an --env value is not KEY=VALUE
E-DEPLOY-003 SSH connection or remote command failed
E-DEPLOY-004 Capsule did not emit usable startup JSON within 120s
E-DEPLOY-006 The pinned mur release could not be fetched from GitHub

Warnings (the deploy continues and exits 0):

Code Meaning
W-DEPLOY-001 The VM compiled none, or not all, of the uploaded artifacts
W-DEPLOY-002 The VM's MURMUR_MAX_ARTIFACT_DECOMPRESSED_BYTES differs from this machine's

mur deploy ls

List all deployed capsules tracked in ~/.murmur/deployments.json.

mur deploy ls

Output columns:

Column Description
DEPLOYMENT_ID Id assigned at deploy time (dep_ + UUID v7)
PROVIDER Always manual — VMs are created by the user, not by mur deploy run
REGION Empty for every record mur deploy run writes; the VM is one you created, and its region is never queried
STATUS Always running for present entries (mur destroy removes the entry)
URL Public A2A endpoint (http://IP:PORT)

Prints no deployments when ~/.murmur/deployments.json is absent or empty.

Example:

DEPLOYMENT_ID                           PROVIDER      REGION        STATUS      URL
----------------------------------------------------------------------------------------------------
dep_01954a3b5c7d8e9f0a1b2c3d4e5f6a7b    manual                      running     http://1.2.3.4:9000

mur destroy

Remove a deployment entry from ~/.murmur/deployments.json. Does not stop or delete the VM — shut down the VM from your cloud provider's dashboard separately.

mur destroy <deployment_id>
  • deployment_id — the id returned by mur deploy run (also listed by mur deploy ls); a unique prefix is enough
  • Exits non-zero with a clear error if the id is not found in ~/.murmur/deployments.json

Example:

mur destroy dep_01954a3b
# destroyed dep_01954a3b5c7d8e9f0a1b2c3d4e5f6a7b (1.2.3.4)

deployments.json

Location: ~/.murmur/deployments.json

A JSON array that tracks all active deployments. Written on mur deploy run; entries removed on mur destroy. Schema per entry:

{
  "deployment_id":  "dep_01954a3b...",
  "provider":       "manual",
  "provider_vm_id": "",
  "provider_key_id": "",
  "region":         "",
  "ip":             "1.2.3.4",
  "url":            "http://1.2.3.4:9000",
  "manifest_path":  "/Users/you/my-agent/murmur.yaml",
  "started_at":     "2026-06-03T12:00:00+00:00",
  "status":         "running"
}
Field Description
deployment_id dep_ + UUID v7 — the deployment's identity across all commands
provider Always "manual" — VMs are created by the user outside of mur
provider_vm_id Always empty — reserved for future provider integrations
provider_key_id Always empty — reserved for future provider integrations
region Always empty — reserved for future provider integrations
ip Public IPv4 of the VM (the value passed to --host)
url http://IP:PORT — the public A2A endpoint
manifest_path Absolute local path to the manifest used at deploy time
started_at RFC 3339 timestamp of when the deployment was created
status Always "running" — entries are removed on destroy, not updated

mur conversation

Inspect and prune the durable conversation records under ~/.murmur/conversations/. These commands read and write that store directly; they do not stage or launch a capsule, and they need no manifest.

A context id is unique inside one record store and nowhere else. When one appears under more than one store, show, rm and truncate refuse with E-CNV-002 rather than guess, and --record <NAME> says which store to act on.

mur conversation ls

mur conversation ls [--record <NAME>] [--message <MSG-ID>] [--json]
Flag Default Description
--record every store Limit to one directory under ~/.murmur/conversations/
--message — Report where one msg_ id stands instead of listing records
--json off Print the same values as JSON

Without --message, one row per context:

RECORD                   CONTEXT                      MESSAGES       SIZE  LAST TOUCHED         TRUNCATED
shey                     ctx_0199f2a1                       48   12.4 KiB  2026-08-29 09:14:02  500 dropped
Column Contents
RECORD The record store: the directory under ~/.murmur/conversations/
CONTEXT The context id: the directory under the record store
MESSAGES Message lines. The header line is not a message and is not counted
SIZE Bytes of conversation.jsonl
LAST TOUCHED Last write to conversation.jsonl, in UTC
TRUNCATED Messages this record has dropped over its life, or -

--json prints an array whose objects carry record, context_id, path, messages, bytes, last_touched_ms, capsule (null for a record no capsule owns) and truncated (null, or an object with dropped, oldest_surviving_id, last_dropped_id and at_ms).

mur conversation ls --message

Answers one of three things about a msg_ id, which is what an artifact that stored a source_id and now finds nothing needs to know:

Answer When Reported
present The id is a line in a record The record, the context, and its position
truncated The id is not a line, the record's header carries a truncation marker, and the id's own uuid-v7 timestamp is at or before the last_dropped_id's The record, the context, how many were dropped, and the oldest surviving id
unknown Anything else Nothing further

mur conversation show

Print one context: its record header, every message of its conversation.jsonl, and its harness session.

mur conversation show <CONTEXT-ID> [--record <NAME>] [--json]
Argument / Flag Default Description
<CONTEXT-ID> — The context id: the directory under a record store. mur trace list --json prints it as context_id
--record every store The record store to read, when the context id appears under more than one
--json off Print one JSON object, the stable interface below, instead of the summary

The context is found by its id alone, in every record store under ~/.murmur/conversations/. A context exists for show when its directory holds conversation.jsonl, harness-session.json, or both, so a context on inference.transport: process, which has only the second, is found too. The command reads only.

With --json, it prints one pretty-printed JSON object:

{
  "record": "my-agent",
  "context_id": "ctx_019f0193c7e07c3a9d5e1b2f4a6c8e01",
  "dir": "/home/me/.murmur/conversations/my-agent/ctx_019f0193c7e07c3a9d5e1b2f4a6c8e01",
  "header": {
    "capsule": "my-agent",
    "created_ms": 1760951564123,
    "truncated": null
  },
  "messages": [
    {"role": "user", "content": "Summarise the open issues.", "id": "msg_019f0193c7e1712a8b3c4d5e6f7a8b90"},
    {"role": "assistant", "content": [{"type": "text", "text": "There are three."}], "id": "msg_019f0193c9a27b3c9d4e5f6a7b8c9d01"}
  ],
  "harness_session": null
}
Key Type Meaning
record string The record store the context was found in: the directory under ~/.murmur/conversations/
context_id string The context id: the directory under that store
dir string The context directory, absolute
header object or null The header line, when the first line of conversation.jsonl is one: capsule (string), created_ms (integer) and truncated (null, or an object with dropped, oldest_surviving_id, last_dropped_id and at_ms). null when there is no conversation.jsonl, or its first line is not a header, as in a record no capsule owns
messages array Every message line of conversation.jsonl, oldest first, each the line's JSON object as recorded with every key, id included. A line that is not a JSON object with a string role is skipped, as the runtime skips it: the header, a torn final line, a line being written. Empty when there is no conversation.jsonl
harness_session object or null The context's harness session: session_id, harness, driver, created_ms and updated_ms. null when the file is absent, does not parse, carries another type, or names no session: the cases in which the runtime starts a new conversation

These keys are a stable interface. Each keeps its name, type and meaning. New keys may be added, and a consumer ignores keys it does not know.

Without --json, it prints a summary, then one line per message with its position, its role and the first line of its text:

record:   my-agent
context:  ctx_019f0193c7e07c3a9d5e1b2f4a6c8e01
messages: 2
   1  user       Summarise the open issues.
   2  assistant  There are three.

A context with a harness session also prints harness session: <session_id> (<harness>, <driver>) after messages:. The summary's layout may change; --json is the interface to parse.

Exit When
0 The context was found and read
1 E-CNV-001: no record store holds the context, or its directory holds neither file
1 E-CNV-002: more than one store holds it and --record was not given
1 E-IO-003: conversation.jsonl exists and cannot be read, or HOME cannot locate ~/.murmur/conversations

A failure prints nothing on stdout.

mur conversation rm

mur conversation rm <CONTEXT-ID> [--record <NAME>]

Removes that context directory whole and reports the path and the message count it held. This is how to reclaim a record whose capsule no longer runs: the age sweep skips a record whose header line names no capsule.

mur conversation truncate

mur conversation truncate <CONTEXT-ID> --keep <N> [--record <NAME>]

Drops everything before the newest N messages and reports what went. N must be at least 1; --keep 0 is refused with E-CNV-003, because truncating a record to nothing is mur conversation rm.

The rewrite is atomic: the kept tail plus a header line recording the drop is staged beside the record and renamed over it, so an interrupted truncation leaves the original whole. Every surviving message keeps the exact id it carried. Once the rewrite is in place, every image blob in the record's blobs/ that no kept message names is removed.

dropped 2 messages from /home/me/.murmur/conversations/shey/ctx-1/conversation.jsonl (2 kept; oldest surviving is msg_0199f2a1c3…)
removed 1 image blobs no kept message names

The second line is printed only when a blob was removed. A record already at or under N prints <path> already holds <n> messages; nothing dropped.


mur trace

Read and analyze trace.jsonl files produced by mur run, and remove ended sessions. Every command but mur trace rm is read-only: it modifies no file and launches no capsule.

See Session trace (trace.jsonl) for the file format.

mur trace show

Print a human-readable summary of a single session, the recorded body behind one of its content hashes, or the members of a formation.

mur trace show [<session>] [--workdir <dir>] [--body <selector> --turn <n>]
mur trace show <formation-id> [--workdir <dir>]
Argument / Flag Default Description
<session> @1, the most recent session in the workdir A session address, or a formation id (frm_…), which prints only the Formation section
--workdir ./workdir The workdir root whose sessions are read: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records. With a formation id, the first group searched — see Listing a formation
--body — Print the body behind one hash and nothing else. Selectors below
--turn — The turn whose hashes --body system, tools, response and message:<i> name, as the Wire section numbers it. Must name exactly one turn. Required with those four, invalid without --body

A trace no session_end closed — a session killed outright, or one still running — is shown from the lines it has, and the command exits 0:

Line Reads
status: ended without a session_end (last event: <event type> at <RFC 3339 UTC time>)
duration: From the first line's timestamp to the last line's, followed by (first to last event)
totals: counted from the events recorded; no session_end recorded the session's own. The turn, token, tool-call and shell-call totals below are counted from the trace's inference, tool_call, shell and shell_detached lines

A final line that is not valid JSON — what a process killed mid-write leaves — is skipped, and the status: line ends with line <n>, the last, is not valid JSON and was skipped. A malformed line anywhere else, or a trace with no session_start, fails with E-TRC-001 naming the file. mur trace diff and mur trace report compare recorded totals, so they refuse a trace with no session_end or a torn line.

Output sections, in the order they are printed:

Section Printed Contents
Session always session_id, capsule name+version, model, exit status, duration, granted capability categories, declared tools, containment: <declared> → <achieved>, workdir exec, userns, and the system prompt's source and hash. For a capsule another capsule launched, a Spawned by <session> (delegation <id>) line follows session. For a formation member, a formation: <formation-id> line follows those. For a session that staged a runtime pin, a runtime pins: <name>@<version> (pulled by <session>), … line follows tools
Harness one or more harness_warning, harness_failed, harness_interrupt or harness_quota records One line per warning (warning <code>: <message>), failed turn (harness failed (<kind>): <message>) and interrupt (interrupt <method>: …), in file order. Then one line per subscription window name, with the latest values reported under it, in the order the names were first reported: quota <name>: used <fraction>, resets <RFC 3339 UTC>, <n> min window, <state>. A value the harness did not report is left off the line: quota five-hour: used 0.5, warning
Hook failures one or more hook_dispatch_error records One ✗ <hook> <lifecycle event> <arm> row per fault
Retention one or more retention records One <store> <reason> removed <n> row per pair, followed by the names of what went
Context one or more context_seed records Per seeding hook: outcome, tokens committed, tokens proposed, the budget, the rejection reason, and the ids of the messages seeded
Turns always Turn count and configured max. When the task continued with the outcomes of work it handed off, a continued: <N> (<A> member answer(s), <D> delegation outcome(s)) line follows, counting its task_continued records. When any turn stopped at the output cap, a capped: <N> turn(s) stopped at the inference.max_tokens output cap line follows
Tokens always Input tokens, output tokens, total, per-turn averages, and a provider: line summing each reported count — in, out, cached, cache write, thinking — over the turns that reported it. A count no turn reported is left off the line. Under transport: process the harness's input and output counts are the totals above, so only the cache and thinking counts appear here
Wire one or more turns carrying content hashes, or one or more tools_refreshed records Per turn: the abbreviated system, tools and response hashes and how many messages the request carried. Then one refreshed: turn <n> <trigger> +<added> -<removed> row per turn whose tool list changed, and the --body command that prints one of the hashes
Tool calls always Count, ok/error breakdown, success rate, average latency, plus a per-turn breakdown of every call
Redundant calls always Calls that re-read a resource nothing had changed since. Agent turns and plan steps are scored against one shared history, so either can be named as the call or as the earlier read it duplicates
Skill calls always Count, ok/error breakdown, success rate, average latency, plus a per-turn breakdown of every call. The call of a skill whose skill_call.trust is untrusted ends in runtime/untrusted: turn 1 pulled-style 2ms ✓ runtime/untrusted
Pulled at runtime one or more artifact_pulled records One <name>@<version> <runtime> <origin>/<trust> row per pull, in file order
Shell calls always Count, exit code distribution, average latency
Compaction always Whether it fired, with turn number and before/after token counts, followed by one declined: row per turn that crossed the compaction threshold and was left uncompacted, naming its turn, the context occupancy and the reason
Call settings one or more inference records carrying max_output_tokens One row per distinct caller, output cap and driver config, in order of first appearance, counting failed calls: <caller> max_tokens <n> driver_config <JSON \| none> <k> call(s) turn(s) <list>. The caller is agent for an agent-loop turn and the record's origin for a hook's run-inference, such as hook:compactor; none is a driver handed no config
Cancelled one or more task_canceled records One task_canceled at <phase> row per cancel, naming the detached shell commands still running and the delegations in flight
Rejected one or more task_rejected records One task_rejected <task id> <cause> source <source> row per refused task, in file order
Failed one or more task_failed records One task_failed <cause> <task id> at turn <n> row per failed attempt, followed by its reason:
Reopens one or more task_reopened records Per reopen: its ordinal, the hook that asked, whether the next attempt continued the task's conversation or restarted it, the turns it had left, and the hook's feedback — reopen 1 by gatekeeper continued, 7 turns left “…”
Resource plane one or more resource_list/resource_read records Counts by outcome
Peer files one or more peer_handle_mint/peer_handle_redeem/peer_file_fetch records Counts by outcome
Delegations one or more delegation_start/delegation records One row per delegation: its dlg_ id, capsule@version, the child session, and the outcome — in flight for a delegation this trace never saw end. The reason follows on any outcome that recorded one, and the path to the child's own trace, in the records directory the child reported, follows on any delegation that launched one
Plan one or more plan_start/plan_step/plan_end records Per plan run: its id, outcome, duration and step totals by status, the step that ended it, and one row per step in the order the plan declared them — kind, status, duration, attempt count when it retried more than once, what it waited on, and its error. A step the run never reached reads not run
A2A one or more a2a_task_received/a2a_send records Tasks received, counted by who the door let each in as, most first — received: 3 tasks — member 2, operator 1, the kinds being operator, member, credential, public and unknown caller for a line that names none — then messages sent, and the peer URLs they went to
Tasks more than one task in the session Per-task breakdown: turns, tokens, exit status and duration, then the task's reopens and capped: <n>, its turns stopped at the output cap, when either is non-zero
Member calls one or more member_call_busy/member_call_start/member_call records One row per call-member call: its mcl_ id, the member, the member's task id — (not started) for a call the member never held — and how it ended with its duration, outstanding for a call this trace never saw end, not delivered for an answer the task never received. busy ×<n> follows when the member turned <n> offers of the task away busy, and no answer below: <member> (<status>), … when the call's member_call.no_answer_below names members further down that gave it no answer
Formation the session is a formation member The Formation section for the session root this session is in and the formation's peer directories: every member by roster name, every call-member call between them, and every task a member received that no member sent

Listing a formation

mur trace show <formation-id> lists every session whose trace names that formation, by session id, with its roster name and how it ended, then every call-member call between the members and every task a member's door let in that no member sent: the operator's, a declared credential's, or a public-door caller's. It searches these records groups, in order:

--workdir Groups searched
Not given The group of ./workdir, then the group of ., then each peer's group
Given That directory's group, then each peer's group

The entry member's --workdir is the roster's directory, so run the command from that directory, or pass --workdir <roster directory>. Each peer's group is the records group of ~/.murmur/formations/<formation-id>/<member> — see Member directories. A member's own mur trace show prints the same section, searching the group it is in and then each peer's group.

Run from the roster's directory /home/me/project:

── Formation ────────────────────────────────────
formation:  frm_019f01a93ff27c1e9a3b5d0c4e8f2a61
searched:   /home/me/.murmur/records/home/me/project/workdir
searched:   /home/me/.murmur/records/home/me/project
searched:   /home/me/.murmur/records/home/me/.murmur/formations/frm_019f01a93ff27c1e9a3b5d0c4e8f2a61/lead-a
searched:   /home/me/.murmur/records/home/me/.murmur/formations/frm_019f01a93ff27c1e9a3b5d0c4e8f2a61/lead-b
searched:   /home/me/.murmur/records/home/me/.murmur/formations/frm_019f01a93ff27c1e9a3b5d0c4e8f2a61/worker-a1
searched:   /home/me/.murmur/records/home/me/.murmur/formations/frm_019f01a93ff27c1e9a3b5d0c4e8f2a61/worker-a2
searched:   /home/me/.murmur/records/home/me/.murmur/formations/frm_019f01a93ff27c1e9a3b5d0c4e8f2a61/worker-b1
searched:   /home/me/.murmur/records/home/me/.murmur/formations/frm_019f01a93ff27c1e9a3b5d0c4e8f2a61/worker-b2
ses_019f01a940ce7761854e768ecbe3d399  chief      chief@0.1.0      ok  may call lead-a, lead-b
ses_019f01a944a17b2c9d0e1f2a3b4c5d6e  lead-a     team-lead@0.1.0  ok  may call worker-a1, worker-a2
ses_019f01a945b27c3d0e1f2a3b4c5d6e7f  lead-b     team-lead@0.1.0  ok  may call worker-b1, worker-b2
ses_019f01a946c37d4e1f2a3b4c5d6e7f80  worker-a1  worker@0.1.0     ok
ses_019f01a947d47e5f2a3b4c5d6e7f8091  worker-a2  worker@0.1.0     ok
ses_019f01a948e57f603b4c5d6e7f8091a2  worker-b1  worker@0.1.0     ok
ses_019f01a949f6807a4c5d6e7f8091a2b3  worker-b2  worker@0.1.0     no session_end
ses_019f01a9b1d27c3e8f0a4b5c6d7e8f90  -          checker@0.1.0    ok  spawned by ses_019f01a944a17b2c9d0e1f2a3b4c5d6e
calls:      6
tasks:      2 not from a member — operator 2
tsk_019f01a9502b7a1c8d9e0f1a2b3c4d5e      operator → chief      ok         52.3s             message: mur trace show /home/me/.murmur/records/home/me/project/ses_019f01a940ce7761854e768ecbe3d399/trace.jsonl --body 3f2a9c1e0b7d
  mcl_019f01a9512a7b3c8d9e0f1a2b3c4d5e    chief → lead-a        completed  48.2s  delivered
    mcl_019f01a9563b7c4d9e0f1a2b3c4d5e6f  lead-a → worker-a1    completed  21.7s  delivered
    mcl_019f01a9564c7d5e0f1a2b3c4d5e6f70  lead-a → worker-a2    completed  19.4s  delivered
  mcl_019f01a9513d7e6f1a2b3c4d5e6f7081    chief → lead-b        completed  51.0s  delivered
    mcl_019f01a9575e7f702b3c4d5e6f708192  lead-b → worker-b1    completed  24.9s  delivered
    mcl_019f01a9576f80813c4d5e6f708192a3  lead-b → worker-b2    completed  38.6s  delivered  busy ×3
tsk_019f01a9c18e7a2b3c4d5e6f708192a3      operator → worker-b2  ok         12.0s             message not captured
Row Carries
formation: The formation id
searched: One line per session root searched, in the order searched, whether or not it exists. A root after the first that could not be listed ends could not be read: <why>, and fails nothing
One per member Session id, the roster name from session_start.formation_member — - for a member's delegated child, which has none — name@version, and the session_end exit status — no session_end for a member that was killed or is still running. spawned by <session> follows for a member another member delegated to, and may call <member>, … for a member the roster lets call others, its callees in roster order
delegated children not under these roots Members' delegated children whose traces are in another records group. Only when there are any. The member's own mur trace show prints each child trace: path in its Delegations section
calls: How many call rows follow. Only when the members' traces record at least one call
tasks: <n> not from a member — operator <a>, credential <b>, public <c>, unknown caller <d>: how many task rows follow, and how many of them each kind of sender sent, in that order, a kind with none left out. Only when there is at least one task row
One per call <call id> <caller> → <callee> <status> <duration> <delivery>, then the note for a missing side when there is one, busy ×<n> when the callee turned <n> offers away busy, and no answer below: <member> (<status>), … when the call names members further down that gave no answer, joined by ;. Columns are aligned
One per task <task id> <sender> → <member> <status> <duration>, an empty delivery column, then where the task's message is. Aligned with the call rows

Each call row carries:

Field Value
Call id The mcl_ id the caller's model was given. (no call id) for a task received with a message id that names none
<caller> → <callee> Roster names: the caller is the calling session's formation_member, or its session id when it has none; the callee is the call's member_call.member. A call only the callee recorded takes its caller from a2a_task_received.caller_member
Status The member_call.status — completed, failed, canceled, rejected, timed_out, unreachable, abandoned or no_answer. outstanding for a call with a member_call_start or member_call_busy and no member_call; unknown for a call only the callee recorded
Duration From the tool call to the outcome. - for outstanding and unknown
Delivery delivered, or not delivered for an answer the calling task never received. Empty for outstanding and unknown

Each task row carries one task a member's door let in, from its a2a_task_received and task_end:

Field Value
Task id The receiving member's tsk_ id for the task
<sender> → <member> The member is the receiving session's roster name. The sender is one of the labels below
Status The task's task_end.exit_status, verbatim. outstanding for a task with no task_end
Duration The task's task_end.duration_ms. - for outstanding
Message message: mur trace show <session dir>/trace.jsonl --body <first 12 hex digits> when the message text is stored behind its message_sha256, which it is under trace.capture: content. That command prints the text from any directory. message not captured otherwise
Sender a2a_task_received
operator caller: "operator": the session's operator token, as nexus presents it
credential <name> caller: "credential" and caller_credential: "<name>": a declared credential's token
public caller: "public": a door with no network.authentication
unknown caller No caller and no caller_member, as a trace older than caller records a task no member sent; a caller with another value; or a caller whose name field is missing

A task a member sent is a call, never a task row: its a2a_task_received has caller: "member", or, in a trace older than caller, a caller_member.

A call whose other side was not found still has its row, with one of these notes:

Note The row is
never held by <callee> A call that ended without the callee's door holding it: rejected because the callee stayed busy for the whole deadline or its session was closing, abandoned while the callee was still busy, or failed to start
<callee>'s trace not found A call whose callee task no searched root records
<callee>'s task ended <exit status> An outstanding call: how the callee's task ended, from its task_end
<callee>'s task has no task_end An outstanding call whose callee task has not ended, or was killed
<caller>'s trace not found; <callee>'s task ended <exit status> A task the callee received from a member no searched root records. …; <callee>'s task has no task_end when that task has not ended

Calls are nested and ordered by these rules:

  1. A call made inside a task that another call handed over is nested directly under that call, indented two spaces per level: in a two-tier formation, each lead's calls to its workers sit under the entry member's call to that lead.
  2. A call made inside a task that has a task row is nested directly under that row: a lead the operator steered sits its calls under the operator → lead row.
  3. Rows with nothing above them come first, by start time — a task row's is when the task was received — each followed by the calls under it. A call made inside the launch's own --task has nothing above it.
  4. The calls under one row are ordered by start time.

These have no task row:

  • the launch's own --task
  • a message that continues a task in TASK_STATE_INPUT_REQUIRED
  • a message the door refused busy

A member that ran from another project, or with another --workdir, is listed by running the command against that root.

Situation Exit
<formation-id> is not frm_ followed by 32 lowercase hex digits E-TRC-002 '<arg>' is not a formation id
No session under any searched root names the formation E-TRC-002 no session under <root>, <root>… belongs to formation <formation-id>
--body or --turn with a formation id E-TRC-001

A member killed before it wrote session_end is shown from the lines it has, like any trace with no ending. Its Session section still carries its formation: line and the Formation section follows:

session:    ses_019f01a95a0b7e21a3c4d5e6f7a8b9c0
formation:  frm_019f01a93ff27c1e9a3b5d0c4e8f2a61
capsule:    writer v0.1.0
model:      claude-sonnet-4-5
status:     ended without a session_end (last event: inference at 2026-10-07T09:14:03.512Z)
duration:   41.2s  (first to last event)
totals:     counted from the events recorded; no session_end recorded the session's own

Printing one recorded body

--body prints the bytes behind one hash to stdout — no headers, no added trailing newline — so the output pipes into sha256sum and matches the blob's own name. The bodies live in <session>/blobs/ and are stored only under trace.capture: content.

Selector Resolves to
system the named turn's system_sha
tools the named turn's tools_sha
response the named turn's response_sha
message:<i> entry i (0-based) of the named turn's message_shas
<sha256> that hash — a full 64-character lowercase hex string, or a prefix of 8 or more characters naming exactly one hash anywhere in the trace, session_start.system_prompt_sha256 included. Needs no --turn
mur trace show --body system --turn 1 | sha256sum

Every --body failure exits non-zero with E-TRC-001:

Situation Message
A named selector with no --turn --turn is required with --body <selector>; this trace has turns 1, 2, 3
--turn names no inference record turn 7 has no inference record in this trace
--turn names several turns: one per task in a session that ran several tasks, or a trace written by a runtime that restarted the count when a task continued --turn 0 names 2 turns in this trace: …, then each turn's task id, full system hash and full response hash. Pass one hash to --body instead
The turn recorded no hashes turn 3 recorded no content hashes — the session ran under trace.capture: none
message:<i> past the end of the list turn 2 recorded 4 messages; there is no message 7
The hash is recorded and the body is not turn 1 system prompt <sha>: recorded under capture: meta; no body was stored
A hash nothing in the trace names no hash in this trace matches <arg>
A prefix matching several hashes The refusal lists every hash it matched
--turn without --body --turn has no meaning without --body

The Tasks section appears only for sessions that ran more than one task.

The Turns section lists every failed model call under count:, in trace order. Each gets two lines: failed: with the turn, the hook that made the call (for a hook's run-inference), the error_code and the provider's HTTP status when there was one; then the call's error, cut to 200 characters with a trailing …. Whether a failed call adds to count: follows the failed-call table. For a capsule whose provider rejected its key:

── Turns ────────────────────────────────────────
count:      0  (max: no cap)
failed:     turn 0  credential_rejected  HTTP 401
            the provider rejected the inference credential credentials.OPENAI_API_KEY in /home/me/.murmur/config.yaml (HTTP 401)

A model call that failed transiently and was made again under inference.retry adds one retry: line per retry, ahead of the failed: lines. Each names:

  1. the turn;
  2. the attempt that failed, out of the most the call may make (max_retries + 1);
  3. the provider's HTTP status, or, when there was none, the reason: connection or driver;
  4. the wait before the next attempt;
  5. the hook's origin, for a hook's run-inference.

A failed call that took more than one attempt ends its failed: line with attempts N, and with retry stopped: <reason> when its last failure was transient but was not retried. The reason is the inference record's retry_stopped.

── Turns ────────────────────────────────────────
count:      0  (max: no cap)
retry:      turn 0  inference_retry  attempt 1 of 3  HTTP 503  wait 5ms
retry:      turn 0  inference_retry  attempt 2 of 3  HTTP 503  wait 10ms
failed:     turn 0  driver_error  HTTP 503  attempts 3  retry stopped: max_retries
            HTTP 503: {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}

Below the Tool calls summary line, each turn that made at least one tool call gets its own row: tool name, duration, a ✓/✗ status icon, and — when the call carried an input — its compact-JSON input, truncated to 120 characters with a trailing … if longer. A call with no recorded input shows no input segment at all.

Example (single-task session — no Tasks section):

── Session ──────────────────────────────────────
session:    ses_aaaaaaaaaaaa4aaa8aaa000000000001
capsule:    my-agent v0.1.0
model:      claude-3-5-sonnet
status:     ok
duration:   500ms
capabilities: shell
tools:      bash
containment: sealed → scoped
workdir exec: no
userns:     profile_confining
prompt:     manifest  cf07194ee232…

── Turns ────────────────────────────────────────
count:      2  (max: 10)

── Tokens ───────────────────────────────────────
input:      2,200  (avg 1100/turn)
output:     350  (avg 175/turn)
total:      2,550
provider:   in 2,090, out 320, cached 1,840, cache write 210

── Wire ─────────────────────────────────────────
turn 1  system bbc5e661e106…  tools f9d35d43770d…  response afb8c1747105…  3 messages
turn 2  system bbc5e661e106…  tools f9d35d43770d…  response 4ed87cafe960…  5 messages
bodies:     mur trace show --body system --turn 1

── Tool calls ───────────────────────────────────
count:      1  (1 ok, 0 error)  success 100.0%
latency:    avg 100ms
  turn 1  bash 100ms ✓  {"command":"cargo test --workspace"}
  turn 2  end_turn

── Redundant calls ──────────────────────────────
count:      0

── Skill calls ──────────────────────────────────
count:      0

── Shell calls ──────────────────────────────────
count:      1
exit codes: 1 ok
latency:    avg 50ms

── Compaction ───────────────────────────────────
fired:      no

Example (multi-task session — Tasks section added):

── Session ──────────────────────────────────────
...

── Compaction ───────────────────────────────────
fired:      no
── Tasks ───────────────────────────────────────
task 1  08ecee82  turns: 1  in: 39  out: 20  ok  178ms
task 2  d014bbd7  turns: 1  in: 39  out: 20  ok  2ms

Each task row shows the first 8 characters of the task_id, per-task turns, input tokens, output tokens, exit status, and duration.

A ── Denied calls ── section is printed when a policy hook refused a shell command or tool call, one line per refusal:

── Denied calls ─────────────────────────────────
turn 0  on-shell  /usr/bin/bash  by branch-policy  “protected branch”

mur trace steps

Print what the agent did, turn by turn.

mur trace steps [<session>] [--verbose] [--workdir <dir>]
Argument / Flag Default Description
<session> @1, the most recent session in the workdir A session address. A formation id (frm_…) is refused with E-TRC-001: use mur trace show <formation-id> for a formation's members and calls
--verbose off Append a truncated summary of each tool call's input
--workdir ./workdir The workdir root whose sessions are read: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records

A trace whose lines carry event_id renders as the session → task → turn tree that its parent_id chain describes: each turn's tool, shell and skill calls under their turn, each turn under its task, and each task under the session. A turn-level line whose parent_id names no line in the file is attributed by its task_id.

Session ses_019f01a940ce7761854e768ecbe3d399  (1 task, 2 turns)

task tsk_11112222…  ctx_11112222…  (a2a)
  context_seed memory-hook  trimmed  1,204 tokens
  turn 1  tool_call  bash
    tool_call  bash  120ms  ✓
    shell      /usr/bin/bash  exit 0  50ms
  turn 2  end_turn

A call to a skill whose murmur.lock pin a running capsule pulled ends in its origin and trust class, and a successful manage.pull() renders as an artifact_pulled row:

    skill_call pulled-style  2ms  ✓ (runtime/untrusted)
artifact_pulled pulled-style@0.1.0 (runtime/untrusted)

A call a policy hook refused renders as a call_denied row under its turn:

  turn 0  tool_call  bash
    call_denied on-shell  /usr/bin/bash  denied by branch-policy

What sits beside that row depends on the transport:

Transport Rows for a refused call
transport: http The call_denied row alone. Nothing ran, so there is no tool_call or shell row
transport: process The call_denied row, and a tool_call row marked ✗. The tool still did not run; the harness reports the refusal it was handed as its own failed call, and that report is recorded

A call refused because its input lacked a field the tool's input_schema requires renders as a tool_input_refused row naming every missing field, followed by a tool_call row marked ✗ on both transports:

  turn 0  tool_call  tool-write
    tool_input_refused tool-write  missing content
    tool_call  tool-write  0ms  ✗
Transport Where the tool_call row comes from
transport: http The runtime records the refused call as a failed tool call
transport: process The harness reports the refusal it was handed as its own failed call

No call_denied row sits beside it: the policy hook is not asked about a call missing a field.

A failed attempt renders as a task_failed row under its task, naming the cause and the first 120 characters of the reason:

task tsk_11112222…  ctx_11112222…  (task_md, user/trusted, lane user)
  task_failed driver_error  {"error":"driver: failed to parse Anthropic response JSON: EOF while parsing a string at line 1 column 38","stop_reason"

A failed model call renders on its turn row as error, its error_code and the provider's HTTP status when there was one; a hook's failed run-inference adds the same to its inference row. The header's turn count leaves out every failed call outside a hook, so for a process turn that failed while open it reads one lower than mur trace show's count::

Session ses_019f01a940ce7761854e768ecbe3d399  (1 task, 0 turns)

task tsk_11112222…  ctx_11112222…  (task_md, user/trusted, lane user)
  turn 0  error  credential_rejected  HTTP 401
  task_failed credential_rejected  the provider rejected the inference credential credentials.OPENAI_API_KEY in /home/me/.murmur/config.yaml…

A task the session refused when it stopped taking work renders as a task_rejected row under the session, naming the cause and the task. It has no task row of its own, because it never started:

Session ses_019f01a940ce7761854e768ecbe3d399  (1 task, 1 turn)

task tsk_11112222…  ctx_11112222…  (task_md, user/trusted, lane user)
  turn 0  end_turn
task_rejected session_ended  tsk_33334444…

A task that continued with the outcomes of its call-member calls and delegate-task delegations renders a task_continued row under its task, naming the continuation, how many answers and outcomes it delivered, and how long the task waited for them:

  task_continued continuation 1  3 member answer(s)  1 delegation outcome(s)  waited 12.3s

A plan run renders as its own subtree: a plan_start row naming the plan and its step count, then a plan_step_start row as each step is handed to a worker and a plan_step row when it settles, then plan_end with the outcome. A step that was never dispatched has only the plan_step row.

plan_start release  3 steps
  plan_step_start build  tool
  plan_step  build  tool  success  300ms
  plan_step_start ship  capsule  after build
  plan_step  ship  capsule  failed  900ms
  plan_end   failed  failed at ship

A model call made again under inference.retry renders one inference_retry row per retry, ahead of the call's own row, which ends with attempts N:

task tsk_01a16d19…  ctx_01a16d19…  (task_md, user/trusted, lane user)
  inference_retry  attempt 1 of 4  HTTP 429  wait 1.0s
  turn 0  end_turn  attempts 2

A trace whose lines carry no event_id renders one row per turn: turn number, decision, tool name, duration.

Session ses_aaaaaaaaaaaa4aaa8aaa000000000001  (2 turns)

  1  tool_call    bash        100ms
  2  end_turn     —           —

mur trace diff

Compare two sessions side by side, with a delta and directional indicator per metric.

mur trace diff [<before> <after>] [--workdir <dir>]
Argument / Flag Default Description
<before> @2 The run in the Run A column, as a session address
<after> @1 The run in the Run B column, as a session address
--workdir ./workdir The workdir root whose sessions are read: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records

Both addresses or neither: one argument is refused with E-TRC-002.

Example (A = 2 turns, ok; B = 5 turns, max_turns_reached):

Metric                 Run A            Run B            Delta
────────────────────── ──────────────── ──────────────── ──────────────────────────
turns                  2                5                +3 (A better)
duration               500ms            1.7s             +1.2s (A better)
input tokens           2,200            9,700            +7500 (A better)
output tokens          350              1,090            +740 (A better)
input/turn (avg)       1100             1940             +840.0 (A better)
output/turn (avg)      175              218              +43.0 (A better)
tool calls             1                5                +4 (A better)
tool success rate      100.0%           80.0%            -20.0 (A better)
avg tool latency       100ms            188ms            +88ms (A better)
shell calls            1                5                +4 (A better)
avg shell latency      50ms             36ms             -14ms (B better)
compaction             none             turn 3           —
exit status            ok               max_turns_reached —
  • Numeric metrics that are lower-is-better (turns, tokens, latency) flag the lower run as (X better).
  • tool success rate is higher-is-better.
  • Non-numeric or non-comparable fields (compaction, exit status) show — in the Delta column.

Prefix divergence

Below the table, a Prefix divergence section reports where the two runs' requests stopped agreeing — the answer to why a provider-side prompt cache missed. It reads the content hashes each run's inference lines recorded, so both runs need trace.capture meta or content.

── Prefix divergence ────────────────────────────
system prompt: differs    A d27e9be1c0de…  B aaaaaaaaaaaa…
tool schemas:  identical  143f541e445d…
turn 1:  diverges at message 1  A 4d3fd85ffaa2…  B ffffffffffff…
turn 2:  identical  (2 messages)
Line Reports
system prompt: The system_sha each run's first agent-loop turn recorded. A run that changes its system prompt mid-session gets a note: line naming the turn
tool schemas: The tools_sha each run's first agent-loop turn recorded
turn <n>: The two runs' message_shas for that turn, compared element-wise: the index of the first entry that differs, identical (<n> messages) when every entry agrees, or only in run A when the other run has no such turn. When one array is a prefix of the other, the divergence index is the shorter array's length and the line reports both lengths

Divergence has no polarity, so no (A better)/(B better) marker appears in this section. When a run recorded no hashes at all, one line names it and says it ran under trace.capture: none, and nothing is compared.

mur trace report

Aggregate statistics across a set of sessions. Useful for repeated-run experiments.

mur trace report [<session>...] [--last <n>] [--since <duration>] [--workdir <dir>]
Argument / Flag Default Description
<session>... every session in the workdir One or more session addresses. Cannot be combined with --last or --since
--last — Limit to the n most recently created sessions. Must be at least 1
--since — Limit to sessions created within a duration, written <n>m, <n>h or <n>d
--workdir ./workdir The workdir root whose sessions are read: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records

Output: a short block per session, then mean, population stddev, min, and max for each numeric metric, followed by exit status distribution. If any sessions contain more than one task, a Per-task averages section is appended showing per-task metrics across all multi-task sessions.

The aggregate section, for 3 sessions with no multi-task session:

Sessions: 3  (./workdir)

Metric                 Mean           StdDev         Min            Max
────────────────────── ────────────── ────────────── ────────────── ──────────────
turns                  2.7            1.7            1.0            5.0
duration (ms)          800            648            200            1,700
input tokens           4,133          3,996          500            9,700
output tokens          513            420            100            1,090
tool calls             2.0            2.2            0.0            5.0
tool success (%)       90.0           10.0           80.0           100.0
shell calls            2.0            2.2            0.0            5.0
redundant calls        0.0            0.0            0.0            0.0

Exit status:
  max_turns_reached        1  (33.3%)
  ok                       2  (66.7%)

Example (the set includes multi-task sessions):

Sessions: 2  (./workdir)

Metric                 Mean           StdDev         Min            Max
...

── Per-task averages (multi-task sessions only) ──────────────
Metric                 Mean           StdDev         Min            Max
────────────────────── ────────────── ────────────── ────────────── ──────────────
task turns             2.0            1.0            1.0            3.0
task input tokens      1,000          500            500            1,500
task output tokens     200            100            100            300
task duration (ms)     400            200            200            600
Tasks: 6

Notes:

  • Sessions whose trace.jsonl holds no events are skipped, and a note: skipped <n> incomplete session(s) line on stderr says how many.
  • Sessions with no tool calls are excluded from the tool success (%) row rather than counted as 0%.
  • A single session produces stddev = 0.
  • The Per-task averages section appears when at least one session ran more than one task. Traces carrying no task events are excluded from per-task aggregation.
  • Exits non-zero if the workdir does not exist or holds no sessions.

mur trace list

List the sessions of one workdir root, most recent first, with each one's records directory.

mur trace list [--workdir <dir>] [--json]
Flag Default Description
--workdir ./workdir The workdir root whose sessions are listed: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records
--json off Print one JSON object, the stable interface below, instead of the table

A workdir root that no longer exists still lists its sessions: their records outlive it.

The listing changes nothing. It creates no directory, and it removes and signals no running-capsule record.

With --json, it prints one pretty-printed JSON object:

{
  "workdir_root": "/home/me/proj/workdir",
  "records_group": "/home/me/.murmur/records/home/me/proj/workdir",
  "sessions": [
    {
      "session_id": "ses_019f0193c7d871a5b2e30ff41a7c0ce1",
      "records_dir": "/home/me/.murmur/records/home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1",
      "started_at": "2026-10-17T09:12:44.123Z",
      "context_id": "ctx_019f0193c7e07c3a9d5e1b2f4a6c8e01",
      "running": false
    }
  ]
}
Key Type Meaning
workdir_root string The workdir root that was read: --workdir made absolute against the current directory, or ./workdir
records_group string ~/.murmur/records/<workdir root>, the group mur run keeps that root's sessions in. It may not exist
sessions array One entry per session directory in the group, most recent first: entry i is the session @(i+1) names. Empty when the group holds none or does not exist
sessions[].session_id string The session id, ses_ followed by 32 hex characters
sessions[].records_dir string The session's records directory, <records_group>/<session_id>, absolute. mur run --resume takes it as an address
sessions[].started_at string or null When the session was staged, read from its session id: RFC 3339 UTC to the millisecond. null only for a ses_ directory whose name carries no time, which mur never creates
sessions[].context_id string or null The conversation the session's first task ran under: the context_id of the first task_start in its trace.jsonl, as mur run --resume reads it. null when the session ran no task, has no trace.jsonl, or its trace has a line that is not valid JSON before the first task
sessions[].running boolean true when a running-capsule record names this session and the process it names is alive, or alive with a start time that could not be read. The capsule's A2A door is not asked

These keys are a stable interface. Each keeps its name, type and meaning. New keys may be added, and a consumer ignores keys it does not know.

Without --json, it prints a table and the group it read:

SESSION                               STARTED                   STATUS   CONTEXT
ses_019f0193c9a871a5b2e30ff41a7c0ce2  2026-10-17T09:14:02.210Z  running  ctx_019f0193c9b27a4e8c1d3f5b7a9e0c12
ses_019f0193c7d871a5b2e30ff41a7c0ce1  2026-10-17T09:12:44.123Z  ended    ctx_019f0193c7e07c3a9d5e1b2f4a6c8e01
records: /home/me/.murmur/records/home/me/proj/workdir

STATUS is running or ended, and - stands for a null start time or context. A group that holds no session prints one line:

no sessions in workdir at /home/me/proj/workdir (records in /home/me/.murmur/records/home/me/proj/workdir)
Exit When
0 The group was read, including when it holds no session or does not exist
1 E-IO-004: HOME cannot hold ~/.murmur
1 E-IO-003: the group, or a session's trace.jsonl, exists and cannot be read
1 E-RUN-028: ~/.murmur/running/ exists and cannot be listed

A failure prints nothing on stdout, with or without --json.

mur trace rm

Remove everything mur keeps for one ended session of one workdir root.

mur trace rm <session> [--workdir <dir>] [--json]
Argument / Flag Default Description
<session> — @N, a full ses_ id, or a suffix of 4 or more characters of one, as under session addresses. A literal path is refused
--workdir ./workdir The workdir root the session belongs to: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records
--json off Print one JSON object, the stable interface below, instead of one line per removal

@N and a suffix name a session in the root's records group, as mur trace list lists it. A full ses_ id is also accepted while only the session's work folder is left, so a session an earlier removal only half removed can be removed again.

What is removed, in this order:

What Where Removed when
Work folder ./workdir/<session_id>, the folder the runtime created for a run without --workdir --workdir is not passed, and the folder exists
Records directory <records group>/<session_id>, as mur trace list names it It exists
Stale running record ~/.murmur/running/<session_id>.json and its .control file A record names the session and its process is gone

With --workdir, only the records directory goes, with its out/, home/, tmp/ and children/, and nothing in the operator's folder is touched.

Both directories are removed without following a link. A link, or a file where a directory belongs, anywhere on the way is refused with E-RUN-050, and nothing the link points at is touched. A removal that fails part way leaves the session listed by mur trace list, and running mur trace rm again finishes it.

A session is running when a record names it and its process is alive, or alive with a start time that could not be read: the rule behind running in mur trace list --json. A running session is refused with E-RUN-024 and nothing is removed. End it with mur stop first.

Nothing else is removed:

  • the records group
  • anything in a --workdir folder
  • the session of any capsule this one delegated to, apart from its work folder in children/
  • the conversation record. A conversation spans every session that resumed it; mur conversation rm removes one

Without --json, it prints one line per removal:

removed work folder /home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1
removed records /home/me/.murmur/records/home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1
removed running record /home/me/.murmur/running/ses_019f0193c7d871a5b2e30ff41a7c0ce1.json

With --json, it prints one pretty-printed JSON object:

{
  "session_id": "ses_019f0193c7d871a5b2e30ff41a7c0ce1",
  "records_dir": "/home/me/.murmur/records/home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1",
  "session_dir": "/home/me/proj/workdir/ses_019f0193c7d871a5b2e30ff41a7c0ce1",
  "running_record": null
}
Key Type Meaning
session_id string The session that was removed
records_dir string or null The records directory removed, absolute. null when there was none
session_dir string or null The work folder removed, absolute. Always null under --workdir, and null when there was none
running_record string or null The stale running record removed. null when no record named the session

These keys are a stable interface. Each keeps its name, type and meaning. New keys may be added, and a consumer ignores keys it does not know.

Exit When
0 The session was removed
1 E-TRC-002: the argument is a literal path, or the address names no session of this root, including a full id with neither directory left
1 E-IO-004: HOME cannot hold ~/.murmur
1 E-RUN-028: ~/.murmur/running/ exists and cannot be listed
1 E-RUN-024: the session is running
1 E-RUN-050: a link or the wrong kind of file is in the way
1 E-IO-003: a removal failed for another reason, or the root cannot be made absolute

A failure prints nothing on stdout.


mur eval

Read and analyze eval.jsonl files produced by murmur/hook-eval, drive a capsule against a dataset, or compare a capsule with a variant of it on the same cases. These commands are read-only except for mur eval run, which launches real capsule sessions.

See Structured evaluation (eval.jsonl) for the file format.

mur eval show

Print a human-readable summary of a single session's scored events, or emit a JSON object for programmatic use.

mur eval show [<session>] [--workdir <dir>] [--json]
Argument / Flag Default Description
<session> @1, the most recent session in the workdir A session address, resolved to hooks/eval.jsonl in that session's records directory
--workdir ./workdir The workdir root whose sessions are read: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records
--json off Emit a single pretty-printed JSON object instead of human-readable text

Human output sections:

Section Contents
Scorers Per-scorer pass count, total count, and pass rate (%)
Overall pass, fail, or no_scores
Score summary Per-scorer float score from the dataset_run summary record
Worst events Up to 5 failing event_score records, sorted by scorer then turn

With --json, emits a single pretty-printed JSON object:

{
  "overall": "pass",
  "scorers": {
    "turn_limit": { "pass": 1, "fail": 0, "total": 1, "pass_rate": 1.0 }
  },
  "dataset_run": { "overall": "pass", "scores": { "turn_limit": 1.0 }, ... }
}

Exit codes: 0 on success (including empty files and no-scorer sessions), 1 on I/O error or parse error.

mur eval diff

Compare two eval sessions side by side with a delta column.

mur eval diff [<a> <b>] [--workdir <dir>]
Argument / Flag Default Description
<a> @2 The run in the Run A column, as a session address
<b> @1 The run in the Run B column, as a session address
--workdir ./workdir The workdir root whose sessions are read: the directory a run was given as --workdir, or <manifest-dir>/workdir. Its sessions' records directories are read from ~/.murmur/records

Both addresses or neither: one argument is refused with E-EVAL-002.

Example output:

Scorer                   Run A          Run B          Delta
──────────────────────── ────────────── ────────────── ──────────────────────────
success_check            0.0%           100.0%         +100.0pp (B better)
token_budget             100.0%         100.0%         =
turn_limit               100.0%         100.0%         =

overall                  fail           pass
  • Delta is expressed in percentage points (pp).
  • Scorers present in only one file are shown as (A only) or (B only).
  • An equal pass rate shows =.

Comparing the arms of an eval run

mur eval diff --run [<run>] [--workdir <dir>] [--json]

With --run, mur eval diff reads one eval-run record instead of two sessions and prints how the baseline and the variant of that mur eval run compare. It reads nothing but the record, so a session that trace.retain has pruned since leaves the comparison unchanged.

Argument / Flag Default Description
<run> @1, the most recent run @N counting back from the newest run, the full evr_ id, its last 4 or more characters (any case), or a path to the record file: anything containing / or ending .json
--workdir ./workdir The baseline's workdir root, <baseline capsule dir>/workdir, whose records group holds the run's record
--json off Print the comparison as one JSON object

--run and the two session arguments cannot be given together. A run that names nothing, an ordinal out of range and a group with no run are refused with E-EVAL-002.

The output has a header, one block per case and a total block:

── Eval run: evr_01a16d24d4d073c1bed590ba1bf056d3 ─────────────────────────────
  dataset:  /home/dev/proj/eval.jsonl
  repeat:   3
  status:   complete
  baseline: /home/dev/proj/base/murmur.yaml
  variant:  /home/dev/proj/variant/murmur.yaml
  model:    claude-haiku-5-5

── case: fix_parser ────────────────────────
                       baseline                 variant                  delta (variant − baseline)
  check                1/3 pass                 3/3 pass                 +2
  turns                4 (3–6)                  3 (3–4)                  -1
  input tokens         21840 (18210–30117)      15022 (14873–16540)      -6818
  cached tokens        9120 (0–11002)           8890 (7310–9020)         -230
  cache write tokens   4102 (3980–4570)         4022 (3870–4190)         -80
  output tokens        812 (640–1210)           590 (512–700)            -222
  time                 21.4s (18.0s–33.9s)      15.2s (14.1s–17.3s)      -6.2s
  mur trace diff /home/dev/.murmur/records/…/ses_01a1…/trace.jsonl /home/dev/.murmur/records/…/ses_01a2…/trace.jsonl
Row Each column Delta
check k/n pass over the arm's runs, plus (j not run) when a run never reached its check; — for a case with no check Variant passes minus baseline passes
turns, input tokens, cached tokens, cache write tokens, output tokens, time The median with the min–max spread when the arm has more than one run, the value itself for one, — when no run reported it Variant median minus baseline median
scorers k/n pass of the runs' murmur/hook-eval overall; printed only when some run has one Variant passes minus baseline passes

time is the agent's own time, the sum of the session's task_end.duration_ms. Tokens are the provider's counts as the trace records them; cached tokens and cache write tokens are counted beside input tokens, never inside them. Each repeat ends with the mur trace diff line pairing its two sessions. The total block sums every run of each arm, and its delta is the difference of the sums. A run with no variant prints one column and no delta; an interrupted run's header reads incomplete: k of m runs recorded.

With --json:

{
  "run_id": "evr_01a16d24d4d073c1bed590ba1bf056d3",
  "complete": true,
  "repeat": 3,
  "runs_recorded": 6,
  "runs_expected": 6,
  "arms": [{"name": "baseline", "manifest": "…", "manifest_sha256": "…", "model": "…"}, …],
  "cases": [{
    "case_id": "fix_parser",
    "arms": {
      "baseline": {
        "runs": 3, "check_pass": 1, "check_runs": 3, "check_not_run": 0,
        "turns": {"median": 4.0, "min": 3, "max": 6},
        "input_tokens": {…}, "cached_tokens": {…}, "cache_write_tokens": {…},
        "output_tokens": {…}, "agent_ms": {…},
        "scores_pass": null, "scores_runs": 0
      },
      "variant": {…}
    },
    "trace_diffs": [["<baseline trace.jsonl>", "<variant trace.jsonl>"], …]
  }],
  "total": {"baseline": {"runs": 3, "check_pass": 1, "check_runs": 3, "check_not_run": 0, "turns": 13, …}, "variant": {…}}
}
Key Value
cases[].arms.<arm>.<measure> {median, min, max} over the runs that reported the measure, or null when none did. median is a number that may be fractional: the mean of the middle two for an even count
cases[].arms.<arm>.check_pass, check_runs, check_not_run The check row's counts. check_runs is 0 for a case with no check
cases[].arms.<arm>.scores_pass Runs whose hook-eval overall is pass, or null when no run has one
total.<arm> The same counts, and each measure summed over every run of the arm, null when no run reported it

An outer capsule that edits a harness reads cases[].arms.variant.check_pass against check_runs to decide whether the change helped, and the measures to weigh what it cost.

mur eval run

Run a capsule once per case in a dataset, collect eval.jsonl from each run, judge each run's work by the case's check, and print a per-case summary and the comparison.

mur eval run [<capsule-dir>] [--dataset <dataset.jsonl>] [--variant <capsule-dir | manifest>] [--repeat <n>]
Argument / Flag Default Description
<capsule-dir> . The baseline capsule: <capsule-dir>/murmur.yaml
--dataset ./eval.jsonl The dataset file
--variant none A second manifest every case also runs against: a capsule directory, read as its murmur.yaml, or a manifest file. Its project directory is the manifest's directory
--repeat 1 How many times each case runs on each manifest. 0 is refused

Dataset format — one JSON object per line:

{ "case_id": "case_001", "task_path": "/path/to/task.md" }
{ "case_id": "case_002", "task_path": "tasks/fix.md", "start": "cases/repo", "check": "make test" }
{ "case_id": "case_003", "task_path": "tasks/fix.md", "start": "cases/repo", "check": { "command": "grep -q fixed main.txt", "files": "cases/checks", "timeout_secs": 120 } }
Field Required Description
case_id yes Identifier passed as MURMUR_CASE_ID to hooks; appears in dataset_run records
task_path yes File to copy into the session's task.md before launch
expected no Any JSON value. mur eval run reads nothing from it and passes it to no hook or scorer
start no A directory copied into the session's work folder before launch
check no A command run in the work folder after the session ends: a string, or an object
check.command yes, in the object form The command, split into a binary and its arguments the way the shell tool splits one: quotes group words, a backslash escapes one character, and nothing is expanded
check.files no A directory copied into the work folder's root just before the check's command runs. An existing file of the same name is overwritten
check.timeout_secs no, 600 How long the command may run before it is killed and the check reads timed_out

Relative paths in task_path, start and check.files are resolved against the current directory. start and check.files may hold only directories and regular files.

Before anything runs, every case is checked against every manifest. Nothing is staged and no provider is called when one is refused:

Refusal Code
A start or check.files that is missing, is not a directory, or holds a symbolic link or special file; a check.command with no words; check.timeout_secs: 0 E-EVAL-003
A check whose binary is not in a manifest's capabilities.shell.allow; a variant whose inference.model differs from the baseline's E-EVAL-004

Order. Cases run one after another. Within a case, repeat 1 runs the baseline then the variant, repeat 2 the variant then the baseline, and so on alternately.

What happens per run:

  1. Stages the session with case_id and dataset_id injected into the hook environment. Its work folder is <manifest-dir>/workdir/<session-id>, fresh and empty.
  2. With start, copies it into the work folder, keeping every entry's permission bits, and compares the copy's start digest with the source's. A different digest is recorded as start_mismatch, and that run is not launched.
  3. Copies task_path to task.md in the session's runtime directory. If the file does not exist, a warning is printed and the session runs without it.
  4. Launches the session.
  5. With check, once the launch has returned and every process the session started is dead: copies check.files into the work folder, keeping every entry's permission bits as for start, and runs the command there, then writes its output to logs/check.log in the session's records directory. See The check.
  6. Reads the session's trace for its turns, tokens, agent time and ending, and hooks/eval.jsonl from its records directory, where a scoring hook declared capabilities.filesystem.records: true writes it. A run with no such file reports no_scores and a warning naming that grant.
  7. Prints a result line, result: pass|fail|no_scores session: <id>, and check: <result> for a case with a check. With --variant or --repeat, each run's lines start with arm: baseline|variant repeat: <n>.
  8. Rewrites the eval-run record.

After all runs it prints the summary table, then the comparison mur eval diff --run prints, then the run's id:

── Summary ──────────────────────────────────────
pass: 2/2

  case_001                 pass  success_check=1.00 turn_limit=1.00  (/path/to/workdir/...)
  case_002                 pass  success_check=1.00 turn_limit=1.00  (/path/to/workdir/...)

── Eval run: evr_01a16d24d4d073c1bed590ba1bf056d3 ─────────────────────────────
…

run: evr_01a16d24d4d073c1bed590ba1bf056d3
  hint: mur eval diff --run evr_01a16d24d4d073c1bed590ba1bf056d3

The hint adds --workdir <baseline capsule dir>/workdir when that is not ./workdir, the directory mur eval diff --run reads by default. mur eval run exits 0 when the run completes, whatever the checks and scores say.

The check

The check runs under the containment the session's own shell commands had, rebuilt from the same policy, declared floor and host reading:

Applies to the check As for the session
Its binary Must be in capabilities.shell.allow
Landlock, seccomp, the composed root under sealed The session's scope
Network The session's capabilities.network.allow, in a network namespace of the check's own
Resource limits The session's rlimits, in a cgroup scope of the check's own
$HOME, $TMPDIR The session's home/ and tmp/ in its records directory

The check is killed with everything it started at its timeout, and whatever it left running is killed when it exits. It is not part of the session: its command, its files and its output appear in no task.md, MURMUR.md, provider request, session environment or trace.jsonl line, and its files reach the work folder only after the session's last process is dead. A symbolic link the agent left where a check file goes is not followed: the check reads error, naming the path.

Result When
pass The command exited 0
fail The command exited non-zero, or was killed for a resource limit, which the record names
timed_out The command outran check.timeout_secs
error check.files could not be copied, the containment could not be built, or the command could not start
not_run The run never reached its check: staging failed or its start did not match

Non-obvious behaviour:

  • mur eval run reads murmur.yaml from <capsule-dir>/murmur.yaml. The capsule must declare murmur/hook-eval in its artifacts: block for a result: other than no_scores — the CLI does not inject the hook automatically.
  • Each manifest reads its own murmur.lock and .env from its own directory, and resolves artifacts from its own .murmur/artifacts before the global store. If the baseline has no lockfile, one is created on its first staged run and reused; a variant's missing lockfile is never written. .env values load into one process, so a variable both files set takes the baseline's value.
  • A run that fails to stage (e.g. missing artifact) is recorded as stage_failed and does not count toward pass; the run goes on with the next.
  • MURMUR_DATASET_ID is taken from observability.eval.dataset_id in the manifest, not from the dataset file.

mur topology

Query a Grafana Tempo instance for capsule session traces and render them as an interactive DAG in the default browser.

mur topology --otel-endpoint <URL> [--window <DURATION>] [--output <PATH>] [--port <PORT>]
Flag Default Description
--otel-endpoint required (or MURMUR_OTEL_ENDPOINT env) Grafana Tempo HTTP query API endpoint (e.g. http://localhost:3200) — this is the query port, not the OTLP ingest port
--window 1h Time window to query: 30m, 1h, 6h, 24h, 7d
--output — Write HTML to this file path instead of opening a browser
--port — Serve the HTML on a local port and open browser at http://127.0.0.1:<port>

What the page shows:

  • Each node is one capsule.session span — capsule name, version, exit status, total duration
  • Node color reflects exit status: green = ok, red = failed, yellow = running, orange = error/unknown
  • Edges are directed parent → child, derived from W3C TraceContext parent span references across traces
  • Edge weight encodes call volume (multiple A2A sends from the same parent to the same child)
  • Node tooltip includes per-span timing: inference ms, tool call ms, shell ms

The graph requires capsules to have observability.otel_endpoint configured in their manifests. See Work with capsule trace spans in Grafana for the full setup guide.

Examples:

# open in browser from last hour
mur topology --otel-endpoint http://localhost:3200

# write HTML to file (no browser opened)
mur topology --otel-endpoint http://localhost:3200 --output /tmp/topology.html

# query last 6 hours, serve on port 8080
mur topology --otel-endpoint http://localhost:3200 --window 6h --port 8080

# read endpoint from environment
MURMUR_OTEL_ENDPOINT=http://localhost:3200 mur topology

Exit codes:

  • 0 — Tempo reachable; HTML written (even when no traces found — empty graph with message)
  • 1 — Tempo unreachable (E-TOP-001), HTTP query failed (E-TOP-002), parse error (E-TOP-003), or I/O error (E-IO-003)

When Tempo is reachable but no capsule.session spans exist in the time window, the command exits 0 and the HTML shows "No capsule sessions found in the selected time window."

The generated HTML is self-contained: all graph data is embedded as window.TOPOLOGY_DATA JSON; vis.js Network is loaded from CDN. No server required to view the file.


Search an artifact index for artifacts matching a keyword, and show each at the newest version that fits this mur.

mur search <query> [--index <PATH|URL|local>] [--limit <n>]
Argument / Flag Default Description
<query> required Case-insensitive keyword matched against the artifact's full name <scope>/<name>, its description, and its tags. "" matches every artifact
--index registry.index_url, else the index on the latest default-artifacts release local lists the global store ~/.murmur/artifacts/; a value starting http:// or https:// is fetched; any other value is a file path, relative to the working directory or absolute. --registry is refused with exit status 2 and a hint naming --index
--limit 10 Maximum number of rows to show

Default behaviour (no --index): fetches the artifact index from registry.index_url, else from the latest default-artifacts release, https://github.com/murmur-nexus/default-artifacts/releases/latest/download/artifacts-index.json. Override the URL with registry.index_url in the effective (global + project-level, merged) config — see Artifact index and custom registry URL and Configuration files. The index must be schema 2; a schema 1 index is refused with E-IDX-002.

Output format:

NAME                VERSION  RUNTIME  DESCRIPTION
murmur/hook-old     —        hook     —
murmur/tool-editor  0.7.0    tool     Read, write and patch files in the workdir.
murmur/tool-git     0.2.0    tool     Structured git interface.

murmur/hook-old: 1.0.0 needs murmur:hook/lifecycle@0.8.0, this mur serves murmur:hook@0.9.0
murmur/tool-editor: 0.8.0 needs murmur:tool/run@0.3.0, this mur serves murmur:tool@0.2.0
murmur/tool-git: 0.3.0 has no payload for linux-x86_64
Column Value
NAME The full name, <scope>/<name>. From an index, the scope is the index's scope
VERSION The newest version that fits this mur on this host, or — when none does
RUNTIME The artifact's role
DESCRIPTION The description, or — when there is none

Under the table, each artifact whose newest version does not fit gets one line naming that version and the reason:

Line Reason
<scope>/<name>: <version> needs <interface>, this mur serves <package>@<version> The version names an interface at a version this mur does not serve. (+N more) follows when more than one interface is affected
<scope>/<name>: <version> needs <interface>, this mur serves no version of <package> This mur serves no version of the interface's package
<scope>/<name>: <version> has no payload for <platform> Every payload of the version is for another platform

With --index local, rows are the global store's artifacts under their full names. A version's interfaces are the ones its store record lists, or read from its payload when the record lists none, and its platforms are the ones it is installed for.

When no artifacts match, prints No results found. and exits 0.

Examples:

# Search the latest default-artifacts release for git-related artifacts
mur search "git"

# Search only locally installed artifacts
mur search "editor" --index local

# Search another source's index
mur search "git" --index https://github.com/ana/my-artifacts/releases/latest/download/artifacts-index.json

# Read an index file in the working directory
mur search "git" --index ./artifacts-index.json

# Cap results at 3
mur search "tool" --limit 3

Errors:

Code When
E-IO-003 The index file is missing or unreadable, the URL cannot be reached, or the server answers with a non-2xx status. The message names the path or URL
E-IDX-002 The index is not a schema 2 index: schema 1, another schema_version, not JSON, or a field that breaks the format. The message names the path or URL

mur index

Write the artifact index for one release's .mur.zip assets.

mur index <DIR> --repo <OWNER/REPO> --release <TAG> [--merge <INDEX>] [-o <FILE>]
Argument / Flag Default Description
<DIR> required Directory holding the release's assets. Every regular file directly in it whose name ends .mur.zip is indexed; every other entry is ignored
--repo required The GitHub repository <owner>/<repo> the release belongs to. The index's scope is murmur for murmur-nexus/default-artifacts, otherwise the owner lowercased
--release required The release tag holding the assets, recorded on every version in <DIR>. Must be non-empty with no whitespace
--merge none An earlier schema 2 index of the same scope. Every version it lists that <DIR> does not hold is kept unchanged. May name the output file
-o, --output artifacts-index.json File to write. Replaced whole, through a temporary file beside it, so a refusal leaves an existing file as it was

Each .mur.zip must hold a packed murmur.yaml whose name: and version: match its file name, <name>-<version>.mur.zip. A native payload carries its platform, <name>-<version>-<platform>.mur.zip; a WASM or static payload carries none. A WASM payload must carry a component mur index can read — its capsule.wasm or one root *.wasm — or it is refused with E-IDX-001 naming why. How each field is derived is under How mur index derives each field.

Each version's built_at_level is the level its files record in murmur-build.json. A version whose files record none gets the newest WIT level this mur knows that its interfaces fit, and no built_at_level when they fit none. Files of one version that record different levels are refused with E-IDX-001.

With --merge, a version both indexes hold is written from <DIR>, keeping the earlier index's built_at_level when it has one. When its files differ from the ones the earlier index lists, it is counted as replaced. A kept version with no built_at_level is given the derived one.

Output:

Indexed 26 artifacts (36 versions) for scope murmur into artifacts-index.json; 10 versions kept from previous.json, 16 replaced

The part after ; is printed only with --merge.

Example:

mur index release --repo ana/my-artifacts --release v1.1.0 --merge previous/artifacts-index.json -o release/artifacts-index.json

Errors:

Code When
E-IDX-001 A .mur.zip in <DIR> cannot be indexed, or <DIR> holds none
E-IDX-002 The --merge index is not a schema 2 index
E-IDX-003 --repo or --release is unusable, or the --merge index has another scope
E-IO-003 <DIR> or the --merge file cannot be read, or the output cannot be written

mur version

Print this mur's version, the commit it was built from, the WIT level it serves, that level's package versions, and the machine-readable commands it serves.

mur version [--json]
Flag Default Description
--json off Print one JSON object instead of text: mur_version, commit, wit_level, packages and commands

Output:

mur 1.1.0-dev
commit 0123456789abcdef0123456789abcdef01234567
WIT level 3

PACKAGE                  VERSION
murmur:artifact-manager  0.1.0
murmur:capsule           0.1.0
...
murmur:tool-registry     0.2.0

The second line is commit unknown for a build made without a git checkout or a .cargo_vcs_info.json. One row per package of the served level, in ascending order, each column padded to its widest cell plus two spaces.

Output (--json): one pretty-printed object and a newline.

{
  "mur_version": "1.1.0-dev",
  "commit": "0123456789abcdef0123456789abcdef01234567",
  "wit_level": 3,
  "packages": [
    {
      "package": "murmur:artifact-manager",
      "version": "0.1.0"
    }
  ],
  "commands": {
    "control show --json": 1,
    "conversation ls --json": 1,
    ...
    "version --json": 1
  }
}
Key Type Holds
mur_version string This mur's version, as mur --version prints it: X.Y.Z for a release, X.Y.Z-dev for a build from main, where X.Y.Z is the next release
commit string or null The 40-character lowercase hex git commit this mur was built from. null when the build had neither a git checkout nor a .cargo_vcs_info.json
wit_level integer The WIT level this mur serves
packages array Every package of that level, ascending, each {"package", "version"}
commands object One key per machine-readable command, as it is typed after mur, in ascending order. Each value is the positive schema number of that command's output. See Driving mur from another program

The keys print in this order. mur --version prints murmur-cli <version> only.


mur beta

Manage opt-in beta features. Beta features are capabilities that are compiled into the binary but hidden behind a runtime flag until explicitly enabled.

mur beta list
mur beta enable  <feature>
mur beta disable <feature>

mur beta list

Reads the effective (global + project-level, merged) config — see Configuration files — so a beta.enabled flag set in either ~/.murmur/config.yaml or <cwd>/.murmur/config.yaml shows as enabled. Lists all beta features compiled into this build and their current enabled status. On a standard release build with no beta features compiled in, prints:

This build has no beta features.

When beta features are present:

Beta features compiled into this build:

  blueprint            disabled  Blueprint file support in taskflow stage slots
  dag-topology         enabled   DAG-based multi-stage topology (Fleet v1.1 preview)

Use `mur beta enable <name>` or `mur beta disable <name>` to opt in or out.

mur beta enable <feature>

Adds feature to the enabled list in ~/.murmur/config.yaml (global — there is no -g/project flag on this command). If feature is not compiled into this build, a warning is printed and the flag is saved anyway (useful for pre-enabling before upgrading to a build that includes the feature).

mur beta enable blueprint
# Warning: 'blueprint' is not compiled into this build. The flag will be saved
# but has no effect until a build that includes it is installed.
# Beta feature 'blueprint' enabled.

Idempotent: calling enable on an already-enabled feature prints "already enabled" and makes no change to the config.

mur beta disable <feature>

Removes feature from the enabled list. Idempotent: if the feature is not currently enabled, prints "already disabled" and exits 0.

mur beta disable blueprint
# Beta feature 'blueprint' disabled.

mur beta disable blueprint
# Beta feature 'blueprint' is already disabled.

Persistence: enabled flags are written to ~/.murmur/config.yaml under the beta: section. See Configuration files.


mur config

Read and write individual keys in the CLI config files described in Configuration files.

mur config set <key> <value> [-g|--global]

mur config set <key> <value>

Writes <key> to the project-level file at <cwd>/.murmur/config.yaml by default. Pass -g/--global to write ~/.murmur/config.yaml instead.

These dotted keys are settable:

Key Maps to
registry.index_url registry.index_url
inference.provider inference.provider
inference.model inference.model
inference.api_key inference.api_key. Also prints a note that mur run reads provider keys from credentials.<NAME>
inference.endpoint inference.endpoint
credentials.<NAME> One entry of credentials:. -g only; NAME matches [A-Z_][A-Z0-9_]*. Running capsules use a replaced entry on their next inference request

registry.sources and beta.enabled are list-typed and not settable with config set — edit registry.sources by hand in the YAML file, and use mur beta enable/mur beta disable for beta.enabled.

Setting a key never clobbers other keys already present in the target file:

mur config set registry.index_url https://example.com/artifacts-index.json
# Set registry.index_url in ./.murmur/config.yaml

mur config set inference.model claude-haiku-4-5-20251001 -g
# Set inference.model in ~/.murmur/config.yaml

Any other dotted key — a known config field or not — is rejected with E-CFG-002 and writes nothing:

mur config set nonsense.field value
# error[E-CFG-002]: unsupported config key 'nonsense.field'
#   hint: supported keys: registry.index_url, inference.provider, inference.model, inference.api_key, inference.endpoint, credentials.<NAME>

credentials.<NAME> without -g, or with a NAME outside the grammar, is refused with the same code:

mur config set credentials.ANTHROPIC_API_KEY sk-ant-...
# error[E-CFG-002]: 'credentials.ANTHROPIC_API_KEY' can only be set in the global config
#   hint: credentials are read from the global config (~/.murmur/config.yaml) only; run `mur config set -g credentials.ANTHROPIC_API_KEY <key>`

inference.api_key is always global

inference.api_key is the one key that ignores the project-wins merge rule below — the effective config always takes it from the global file, never the project file. Running mur config set inference.api_key <value> without -g still writes the value to the project file, but it prints a warning first and the value has no effect on what mur actually uses:

warning: writing a literal inference.api_key to ./.murmur/config.yaml has no effect; inference.api_key is always read from the global config (~/.murmur/config.yaml) — this project-level value will be ignored when resolving effective config

No warning is printed for a ${VAR}-shaped value — see inference.api_key is always global.