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
<NAME>and both capsule names are checked. A refusal writes nothing.--index localis refused.~/.murmur/config.yamlis read and the provider chosen../<NAME>is refused if anything exists at that path: a directory, a file or a symlink.- The artifact index is read and the driver version chosen. A refusal writes nothing.
- Every file is rendered and parsed:
roster.yamlas a roster, eachmurmur.yamlas a capsule manifest. A refusal writes nothing. - 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:
inference:section in~/.murmur/config.yaml(recommended)ANTHROPIC_API_KEYenv var (usesclaude-haiku-4-5-20251001by default)OPENAI_API_KEYenv var (usesgpt-5.6-lunaby default)- Interactive first-run wizard (requires a TTY; saves result to
~/.murmur/config.yaml)
mur newreads and writes the global file only — it does not consult or write the project-level<cwd>/.murmur/config.yamlfile described in Configuration files. - The generator's own artifacts must be installed, each at the newest version that fits thismurin the same index (in the latest release: the version shown): - the driver for your chosen provider —murmur/driver-anthropic@0.19.0ormurmur/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.yamlwritten 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.spawnblock 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(requiresnameandversionfields) - Scans
murmur.yamlfor literal secret patterns and emits warnings - Packages
murmur.yamlplus exactly the files listed inrequires_files:— the rest of the source directory (src/,Cargo.toml,README.md, editor files, build output) is not packaged.mur buildnever compiles anything, so a.wasmpayload must already exist on disk and be declared. A native or static artifact that declares norequires_files:builds to a manifest-only archive; a wasm artifact does not — with no root*.wasmto pack it fails withE-BLD-003. - Validates the manifest
name:, therequires_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 withE-BLD-001, because the scope comes from where the artifact is installed from — see Artifact names - Output written inside the source directory unless
--outputis specified - Appends
murmur-build.jsonto the archive, recording the artifact's built-at WIT level: the newest level thismurknows that the packed component'smurmur:*interfaces fit. A payload with no component fits every level. Arequires_files:entry packing asmurmur-build.jsonis refused withE-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:
- Strip trailing
/sofoo/resolves tofoo - Strip
.zipextension (case-insensitive) - Lowercase
- Replace any non-ASCII-alphanumeric, non-hyphen character with
_ - Collapse consecutive underscores to one
- 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, anddescriptionwhen one is set - Present (only with
--skill) — packed unchanged, and the front block is not read;--summarysetsdescriptionin the packed copy. Theruntimefield must beskillor the build fails withE-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_pathis omitted, CLI infers<name>-<version>.mur.zipfrom localmurmur.yaml --registryis refused with exit status 2 and a hint naming the global store--platformoverrides the platform tag (format:os-arch, e.g.darwin-aarch64). When omitted for a native artifact (implementation: nativein the zip'smurmur.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:
lateststableedge
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:
- Resolve a
name@versionartifact from the global store~/.murmur/artifacts/ - 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
- Store into the project-local store (or global store with
-g) -
Pin the name, resolved version, SHA-256 and, for a payload a source served, the scope's owner id in
murmur.lockasorigin: operator— project installs only, since-ghas no project to pin. An entry a capsule pinned throughmanage.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...) -
Compile each WASM tool, driver and hook for this machine into
~/.murmur/compiled/, unless--no-precompileis given (see the table above) -
Keep the host facts of each
capabilities.shell.allowbinary in~/.murmur/host_facts/, worked out as amur runof the project without--workdirwould 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 binariesA 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 thanmurmur.yamldeclares✗ name@version <platform> — artifact integrity check failed for name@version(withexpected 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:
- 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. - The
Rosterblock. -
The tally:
Roster Tally Exit code Admitted, no warning All checks passed.0Admitted, with warnings 0 checks passed, 0 errors found, N warning(s)., a blank line, and oneFix: <remedy>per warning0Refused 0 checks passed, 1 error found., a blank line, andFix: <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 withmurmur.lockif one is present, carries no binary built for another platform, and speaks only interface versions thismurserves; and theEnvironment requirementsblock found no unset variable and no declarationmur-roostwill refuse. Warnings do not change this1— one or more declared artifacts missing, disagree withmurmur.lock, have a runtime-pulled pinmur runwould refuse withE-RUN-043, hold a native binary this host cannot run, or speak an interface version thismurdoes not serve (checklist printed to stdout first); or theEnvironment requirementsblock 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
.envfrom nearest workspace containingmurmur.yaml, unless--no-env-fileis passed - Creates/uses
murmur.lockin 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, whenHOMEis unset, relative, or names a directory in which~/.murmurcannot 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 runaccepts all four artifact runtimes:tool,driver,hook, andskilltoolartifacts are exposed as model-callable tools;driver,hook, andskillartifacts are staged for runtime use but are hidden from the model's tool inventorytoolartifacts are offered to the model under their bare name, the part after the/:murmur/tool-gitis the tooltool-git. Two declared artifacts with one bare name are refused withE-CAP-020skillartifacts installskill.mdtotools/<name>/skill.mdin 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.artifactinmurmur.yamlon both transports: an http driver undertransport: http, a process driver undertransport: process. A missing driver exits witherror[E-RUN-005]; atransport: processharness binary that cannot be resolved exits witherror[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
--taskortask.mdgives the launch, under every lifecycle; - an A2A task, when the capsule ends after it (
lifecycle.after_task: exit, ortask_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:
- Every live task is cancelled, as
murmur.StopSessioncancels them, and no new task is started. - Each cancelled task's
task_endis written, after itson-task-endhooks. - The session teardown runs:
on-session-end, waiting for asynchronous hooks to finish,shell_abandonedrecords 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:
- The formation line, printed once every peer's door answers and before the entry member starts.
- 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 printed1—~/.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
SIGSTOPor 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 |
- The
FORMATIONcolumn appears betweenUPTIMEandURL. - 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.
- 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_dirof a listed member, or of a member whose process is gone — the record plainmur psprunes and--jsonkeeps; - 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 answered1— 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 gone1— 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 acapsule-closedframe1— 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 in1— 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 request1— 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:
- Validate
--manifest,--workdir,--mur-binaryand every--enventry (no network calls) - Resolve every declared artifact for
--deploy-platform, and themurbinary - Wait up to 30s for SSH to become available on the VM
- Upload the
murbinary viascpto/usr/local/bin/mur - Upload the manifest, the files it references and the optional workdir to
/root/mur-<id>/ - Upload every artifact into the VM's global store,
/root/.murmur/artifacts/<scope>/<name>/<version>/. A declaredlocal/artifact missing from the local stores is refused without asking a source - Write the environment variables to
/root/mur-<id>/.env, mode600, when there are any - Run
mur precompile --json --workdir /root/mur-<id>on the VM over every uploaded artifact, with the.envexported; skipped under--no-precompileor when the capsule declares no artifacts - Run
mur run --manifest <path> --workdir /root/mur-<id> --jsonon the VM with the same.envexported; wait up to 120s for the JSON line - Parse
localhost:PORTfrom the JSON output; open the port and construct the public URL -
Say who may call the published door:
Manifest Output No network.authenticationW-SEC-032on stderr, naming the public URLnetwork.authenticationdoor: bearer — read a token on the target: ssh <user>@<host> /usr/local/bin/mur token <session_id>on stdout. No token leaves the VM -
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 bymur deploy run(also listed bymur 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:
- 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.
- 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 → leadrow. - 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
--taskhas nothing above it. - 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:
- the turn;
- the attempt that failed, out of the most the call may make (
max_retries+ 1); - the provider's HTTP status, or, when there was none, the reason:
connectionordriver; - the wait before the next attempt;
- 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 rateis 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.jsonlholds no events are skipped, and anote: 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
--workdirfolder - 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 rmremoves 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:
- Stages the session with
case_idanddataset_idinjected into the hook environment. Its work folder is<manifest-dir>/workdir/<session-id>, fresh and empty. - 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 asstart_mismatch, and that run is not launched. - Copies
task_pathtotask.mdin the session's runtime directory. If the file does not exist, a warning is printed and the session runs without it. - Launches the session.
- With
check, once the launch has returned and every process the session started is dead: copiescheck.filesinto the work folder, keeping every entry's permission bits as forstart, and runs the command there, then writes its output tologs/check.login the session's records directory. See The check. - Reads the session's trace for its turns, tokens, agent time and ending, and
hooks/eval.jsonlfrom its records directory, where a scoring hook declaredcapabilities.filesystem.records: truewrites it. A run with no such file reportsno_scoresand a warning naming that grant. - Prints a result line,
result: pass|fail|no_scores session: <id>, andcheck: <result>for a case with a check. With--variantor--repeat, each run's lines start witharm: baseline|variant repeat: <n>. - 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 runreadsmurmur.yamlfrom<capsule-dir>/murmur.yaml. The capsule must declaremurmur/hook-evalin itsartifacts:block for aresult:other thanno_scores— the CLI does not inject the hook automatically.- Each manifest reads its own
murmur.lockand.envfrom its own directory, and resolves artifacts from its own.murmur/artifactsbefore 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..envvalues 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_failedand does not count towardpass; the run goes on with the next. MURMUR_DATASET_IDis taken fromobservability.eval.dataset_idin 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.sessionspan — 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.
mur search
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.