# Setup

Wire murmur after `npm install -g @mu-crew/murmur`. Selling and surface
overview: [README](../README.md). Hard ssh cases: [SSH.md](../SSH.md).

## Per-node init

On every node that runs agents:

```bash
murmur init                      # this node's identity
pi install npm:@mu-crew/murmur   # the agent-side extension, as a pi package
```

pi loads the extension from the package's `pi` manifest. The CLI and the
extension are two installs of the same package, so upgrade them together:

```bash
npm i -g @mu-crew/murmur@latest
pi update npm:@mu-crew/murmur    # then /reload running agents
```

Both share one store. When the schema changes, the newer side rebuilds the store
and the older side refuses it rather than rebuilding it back: a CLI verb prints
`written by a newer murmur` and exits 4 (`notify` and `clear`, which run from
hooks, stay silent), the dash shows the message, and an older extension records
nothing until it is upgraded. An extension that already had the store open when a newer
CLI rebuilt it goes stale (its agent drops out of the view) until it reloads.
CLIs from 1.4.0 and earlier lack the refusal, so upgrade the CLI first.

### Coming from `murmur link pi`

The old `link pi` wrote a shim at `~/.pi/agent/extensions/murmur.ts`. Left in
place next to the package, pi loads the extension twice and every event is
reported twice. In this order:

```bash
pi install npm:@mu-crew/murmur   # once: registers the package
murmur pi migrate                # report only
murmur pi migrate --apply        # move the shim aside; nothing is deleted
```

Only a file byte for byte equal to a shim `link pi` generated (its two generated
paths aside) is moved, into `~/.pi/agent/disabled/`, which pi does not read. A
`link pi --copy` install, the inlined copy v0.1 `link pi` wrote, or an edited
file is reported and left for you: move it aside yourself.
`MURMUR_PI_HOME` changes the home directory it starts from.

Running agents keep the old extension code until they restart. Order matters:
without `murmur init` the extension loads and records nothing. Agents must run
**inside tmux**. A pane is the address; without one there is nothing to jump to.

## tmux focus hooks

Looking at a finished agent clears its badge. On every node, in `.tmux.conf`:

```tmux
# Run murmur only when the pane, its window or its session shows a badge.
%hidden MURMUR_ATTN="#{||:#{m/r:^(done|blocked|error|crashed)$,#{@murmur_pane_state}},#{||:#{m/r:^(done|blocked|error|crashed)$,#{@murmur_window_state}},#{m/r:^(done|blocked|error|crashed)$,#{@murmur_session_state}}}}"
%hidden MURMUR_CLEAR="run-shell -b 'murmur clear --pane #{pane_id}'"
set-hook -g after-select-pane      "if -F '$MURMUR_ATTN' \"$MURMUR_CLEAR\""
set-hook -g after-select-window    "if -F '$MURMUR_ATTN' \"$MURMUR_CLEAR\""
set-hook -g client-session-changed "if -F '$MURMUR_ATTN' \"$MURMUR_CLEAR\""
```

The gate skips a Node start on switches with nothing to acknowledge. Window and
session states count too, so orphan badges (from a pane that is gone) still
clear.

These are per node and not optional. `murmur clear` is what acknowledges an
attention request. A node's own snapshot is what every peer reads — without
these hooks, finished agents stay `done` in every peer's list.

Verify with `tmux show-hooks -g`. `set-hook` accepts a name your tmux does not
have and exits 0, so a wrong name fails silently.

Pass the pane id explicitly: hooks run in the tmux server, where `$TMUX_PANE` is
unset. Without it, a window holding an agent and a shell clears when you focus
the shell.

Focus can only cancel attention. It cannot stop a running agent or alter what
the agent reported about itself.

## Side panel

Bind the side panel in `.tmux.conf`:

```tmux
bind C-m run-shell "murmur sidepanel"
```

Then press `prefix + C-m` to toggle it for the current window. This key is free
in tmux's default prefix table. `prefix + m` and `prefix + M` are not free: tmux
uses them to set and clear a pane mark.

The panel opens on the left at 25 percent of the window width clamped to
25–40 columns, then shrinks to fit its widest line (never below 16 columns). It
spans the full viewport even when the existing layout is nested, and takes focus. Toggling it again removes only that window's panel.

Press `j` or `Down` to select the next agent, and `k` or `Up` to select the
previous agent. Press `g` or `Home` to select the first agent, and `G` or `End`
to select the last agent. Press `?` to show all panel keys. Press `?` or
`Escape` to close the help panel. Press `c` to toggle compact one-line rows. With
the mouse, click a row to select it, double-click it to jump to that agent, and
scroll the wheel to move the selection. The side panel and dashboard share this compact setting, along with the saved sort
and visibility settings.

## Harnesses other than pi

pi reports in-process through the extension. Codex, opencode, and the Cursor CLI
have no such hook — they run a command when something happens — so they use
`murmur notify`, which records an attention request for the pane it runs in.

This path is attention only: no ownership, no `running`/`idle`, no crash
detection. Unrecognised events become `blocked`, so a new harness still puts a
row in front of you.

A request counts as crew, not yours, when the hook's environment has
`MU_MANAGED_AGENT=1` or `MU_AGENT_NAME`, which mu sets on the agents it places.
The hook inherits them from the harness, so a wrapper that clears the
environment (`env -i`) makes a mu worker count as yours.

### Codex

```toml
# ~/.codex/config.toml
notify = ["murmur", "notify", "--source", "codex"]
```

Do not wrap with `sh -lc` alone. Codex appends the event JSON as one more
argument, and `sh -lc '<script>' <arg>` puts that argument in `$0` rather than
`$1` — the wrapper swallows the payload. Exec murmur directly, or forward
explicitly:

```toml
notify = ["/bin/sh", "-lc", "exec murmur notify --source codex \"$@\"", "codex-notify"]
```

Codex fires `agent-turn-complete` (turn ended, waiting for you) — murmur records
`done`. Drop `--title Codex` if you have it; the message falls back to the
payload's `last-assistant-message`.

The same fields may arrive as JSON on stdin (opencode's plugin form). argv wins
when both are present; flags still beat the payload, so `--event-type` can pin
an event murmur does not know.

### opencode

OpenCode 2 has no notify hook, so a plugin pipes JSON to `murmur notify`. The
[mu-crew dotfiles](https://github.com/mu-crew/dotfiles) ship one at
`opencode/plugins/murmur/tui.ts`. Install it as
`~/.config/opencode/plugins/murmur/tui.ts`. opencode loads only a directory
with a `tui` entry, and skips a bare file in `plugins/`.

It must be a TUI plugin. A server plugin runs in opencode's shared background
service, which keeps the environment of the pane that first started it. Every
notification from every opencode window would then land on that one pane. A
TUI plugin runs in the terminal, so `murmur notify` gets the right
`$TMUX_PANE`. Every TUI sees every session's events, so the plugin only
reports sessions it has shown, either as the open session or as a tab.

A finished turn arrives as `session.execution.succeeded`. The plugin sends it
as `session.idle`, which murmur records as `done`. `permission.asked` is not in
murmur's table, so it becomes `blocked`.

### Cursor CLI

```json
{
  "version": 1,
  "hooks": {
    "stop": [
      {
        "command": "murmur notify --source cursor"
      }
    ]
  }
}
```

`~/.cursor/hooks.json` (user) or `.cursor/hooks.json` (project). `status:
completed` → `done`; `error` → `error`; `aborted` → `blocked`. The agent must run inside
tmux. Interactive `agent` sessions are the target; non-interactive `agent -p`
has been observed to omit `stop`.

### PATH in hooks

A notify hook inherits the PATH of whatever launched the harness. `/bin/sh -l`
does not fix a missing npm prefix. Verify from inside the harness:

```bash
murmur notify --source probe --message reachable && murmur status
murmur clear --pane "$TMUX_PANE"
```

If `murmur` is missing there, use the absolute path from `command -v murmur`.

The probe is a real attention request: no event name → `blocked`, so the window
badges. `murmur clear` (or focus, with the hooks above) takes it back.

`notify` outside tmux, or against a store from a newer murmur, records nothing
and exits 0, so it cannot break the caller's exit code. A pane reached only via notify is still a full jumpable
row.

## Notifications

murmur runs `~/.config/murmur/on-attention` (or `$MURMUR_CONFIG_DIR/on-attention`)
once for each new `done`, `blocked`, `error` or `crashed`, on any node. Make it
executable; nothing else is configured. It runs detached, with its output
discarded, and these variables set:

| Variable | Value |
| --- | --- |
| `MURMUR_KIND` | `done`, `blocked`, `error` or `crashed` |
| `MURMUR_AGENT` | the agent name, as the picker shows it |
| `MURMUR_HOST` | the peer name, or this node's display name |
| `MURMUR_LOCAL` | `1` for this node, `0` for a peer |
| `MURMUR_PANE` | the pane id on its node |
| `MURMUR_MESSAGE` | the request text; for `error`, pi's error message |
| `MURMUR_HOST_COLOR` | the host's accent hex, as the dash colors it |
| `MURMUR_GLYPH` | the kind's Nerd Font glyph, as every murmur surface shows it |
| `MURMUR_KIND_COLOR` | the kind's hex color, as every murmur surface shows it |
| `MURMUR_EVENT` | all of the above and more, as JSON |

```sh
#!/bin/sh
# ~/.config/murmur/on-attention
case "$MURMUR_KIND" in
  done) urgency=low ;;
  *) urgency=critical ;;
esac
notify-send -u "$urgency" "murmur: $MURMUR_AGENT $MURMUR_KIND" \
  "$MURMUR_HOST${MURMUR_MESSAGE:+ — $MURMUR_MESSAGE}"
```

A host's color is a crc32 of its short name over eight Catppuccin accents, the
same hash the mu-crew dotfiles publish as `@mu_crew_host_color`, so one host is one
color in the status bar, the dash, the side panel, the picker and your
notifications. For a peer the name
is the one given to `peer add`.

The [mu-crew dotfiles](https://github.com/mu-crew/dotfiles#notifications)
ship a ready hook that turns these into notifications with an icon: a frame in
the host color around the kind's glyph.

On macOS, use `osascript -e "display notification \"$MURMUR_MESSAGE\" with title \"$MURMUR_AGENT $MURMUR_KIND\""`.
For a phone, `curl -d "$MURMUR_AGENT $MURMUR_KIND" ntfy.sh/<topic>`.

The hook fires from `murmur collect`, which `murmur status` runs on each
status-bar tick, as do the dash and side panel. So an event arrives within one
tick, and only on a node that runs one of those. Run the hook on the machine
you sit at; peers need no hook. Each event fires once even when several
surfaces collect at the same time. Events older than 15 minutes do not fire, so
installing the hook does not replay the backlog. A crew agent's `done` fires
too; filter on `MURMUR_EVENT`'s `driver` if you do not want it.

## Peers and doctor

On the machine you watch from:

```bash
murmur peer list         # configured peers, last seen
murmur peer list --all   # also ssh hosts that could become peers
murmur peer add devbox
murmur doctor            # survey each peer over ssh
murmur doctor --topology # also probe who can reach whom
```

`peer list` is local. `doctor` dials out. Membership is per node: peering a
machine does not mean it peers you — if it does not peer back, its list cannot
see your agents. `doctor` writes nothing and repairs nothing; it prints the
commands. Exit 0 for observations, 1 only for a real problem. Suggestions name
this node as it calls itself; that name may not resolve from the peer's side.
`peer add` accepts a target that does not answer yet and discovers identity on
the first collect.

`--topology` costs one dial per ordered pair. Plain `doctor` answers "is my
fleet mutual?"; `--topology` answers "what shapes are possible here?". A
recommended hub is arithmetic on that matrix. Spokes see the hub and the hub
sees every spoke; **spokes do not see each other** — `export` publishes local
panes only. A pair that was not demonstrably up reads `unknown`, not
unreachable.

Asleep or powered-off nodes are normal. `status`, `pick`, `dash`, and
`sidepanel` stay silent about that. `murmur peer list` has LAST SEEN; `murmur collect` (run on purpose)
prints one line per peer it could not reach.

Bind a popup picker:

```tmux
bind -N "agent state picker" a display-popup -E -w 80% -h 60% "murmur pick"
```

Rows use the shared picker prefix `marker · 30-column agent · state · context`, matching the `tms` session picker while retaining Murmur's state word, stream, host, age, and flags.

Typing in pick matches agent name, workstream or tmux session, host, and the
state word as literal substrings — so `blocked` narrows without a dedicated
binding.

## One key to the dash and back

After a jump, getting back depends on where the agent lives: a local agent is
an ordinary tmux move, a remote one hands the keyboard to the remote server.
Bind one key that covers both:

```tmux
bind -N "toggle murmur dash" G run-shell -b "murmur dash --goto"
```

Add it on every machine you jump to, not just the one you sit at — on a remote
host it is the remote tmux that reads the key.

`murmur dash --goto` decides in this order:

1. **In a murmur-controlled remote session** it detaches. The wrapper's own
   restore command then returns the local client to where the jump started.
2. **In the dash** it goes back to the pane you pressed the key in last. If that
   pane is gone it goes to its window. If the window is gone too, it stays in
   the dash and says `nothing to go back to`.
3. **With a dash running** it records where you are and switches to the dash.
   This includes an ordinary `ssh` login to a machine that has its own dash.
4. **With no dash running** it records where you are, opens `murmur dash` in a
   window of the dedicated `murmur-dash` session, and switches to it. On a node
   without `murmur init` it refuses instead, because the dash would exit at once.

The side panel is an ordinary pane, so the key works there and the split is
left alone: back returns you to the panel as you left it.

A dash marks its pane for as long as it runs, and murmur marks the one client
its own jump attaches. The way back is one option per client,
`@murmur_return_<client>`, so two terminals toggling at once do not trade
places. All three are tmux options, so nothing is inferred from process
ancestry or history. A dash killed with `SIGKILL` cannot clear its marker, so
`--goto` checks the pane is still alive before switching and opens a new dash
if it is not.

The `murmur-dash` session has `detach-on-destroy off`: quitting the dash with `q`
moves your client to another session instead of detaching the terminal.

`run-shell -b` backgrounds the call, so the key never blocks the server. The
cost is that a failure message goes nowhere; run `murmur dash --goto` by hand
to see it.

The client mark applies to the next client that attaches to the remote tmux
server. A jump command override that does not itself attach a tmux client, or
attaches after some other client does, leaves the jump unmarked — then `--goto`
switches to the remote host's own dash instead of coming home.

## Jump command override

`target` is always ssh (collector). The jump command is for a human and need not
be ssh:

```bash
murmur peer set dev --jump-command 'et dev -c "tmux attach -t {pane}"'
```

murmur substitutes `{pane}` and runs the rest unparsed. Default is
`ssh -t <target> env LC_CTYPE=C.UTF-8 tmux attach`. The explicit UTF-8 locale
keeps Nerd Font glyphs intact when the remote SSH login has no locale. Override
the command when sshd sets `MaxSessions 1` — see [SSH.md](../SSH.md).

Remote jump from inside tmux opens a local wrapper session named after the peer
with a trailing `~`:

- `status off` — the remote's bar is the only one on screen
- `prefix None` — `^b` goes straight to the remote

Both are per-session. Leaving the remote returns you to the window you jumped
from and destroys the wrapper. Jumping to the same host twice reuses one
session. Outside tmux, pick/dash run ssh directly and you land back at your
shell on exit.

Inside the wrapper the local tmux has no prefix. Escape hatch (root table —
not `M-b` / other Alt letters that would eat picker's own filters):

```tmux
bind -n M-Escape if-shell -F '#{m:*~,#{session_name}}' detach-client
```

## Snapshot versions

All nodes must speak the same snapshot format. A mismatch is rejected rather
than guessed — the peer shows as reachable-but-broken with the reason.
`murmur peer list` shows each peer's version. Patch versions that share a
snapshot major interoperate.
