# Installing Agentbus ## macOS: Homebrew ```sh brew install ericfitz/tap/agentbus ``` The formula installs a signed, notarized universal binary from the matching [GitHub release](https://github.com/ericfitz/agentbus/releases). ## Linux: install script ```sh curl -fsSL https://github.com/ericfitz/agentbus/releases/latest/download/install.sh | sh ``` Installs `agentbus` into `~/.local/bin` without root, for x86_64 and aarch64. The script downloads the release tarball with `SHA256SUMS` and `SHA256SUMS.sig`, checks the Ed25519 signature with the public key built into the script (OpenSSL 3 is required: `openssl` or `openssl3` on `PATH`), checks the tarball's checksum, then replaces the binary atomically. Rerun it to upgrade; restart harness sessions and the TUI afterwards. | Variable | Meaning | |---|---| | `AGENTBUS_VERSION` | Install this tag (`vX.Y.Z`) instead of the latest release. | | `AGENTBUS_INSTALL_DIR` | Absolute target directory (default `$HOME/.local/bin`). For a system-wide install: `curl -fsSL \| sudo env AGENTBUS_INSTALL_DIR=/usr/local/bin sh`. The script never runs `sudo` itself. | | `AGENTBUS_BASE_URL` | A mirror of `https://github.com/ericfitz/agentbus`. | | `AGENTBUS_SKIP_SIGNATURE` | `1` skips only the signature check (for hosts with OpenSSL 1.1); the checksum is still verified. Any other value is rejected. | The script stops with a message instead of guessing when it is given positional arguments, an `AGENTBUS_VERSION` that is not exactly `vX.Y.Z`, a relative `AGENTBUS_INSTALL_DIR`, no `HOME` and no `AGENTBUS_INSTALL_DIR`, a copy without an embedded release key, or when `/agentbus` is a directory. Verifying a download by hand, with the public key from [`release/agentbus-release-ed25519.pub`](../release/agentbus-release-ed25519.pub): ```sh openssl pkeyutl -verify -rawin -pubin -inkey agentbus-release-ed25519.pub -in SHA256SUMS -sigfile SHA256SUMS.sig grep agentbus-vX.Y.Z-linux-amd64.tar.gz SHA256SUMS | sha256sum -c ``` The Linux and Windows archives are built in GitHub Actions on runners of their own OS and architecture and carry a build provenance attestation. With the GitHub CLI you can check that an archive was built from this repository by that workflow, in addition to the signature check: ```sh gh attestation verify agentbus-vX.Y.Z-linux-amd64.tar.gz --repo ericfitz/agentbus \ --signer-workflow ericfitz/agentbus/.github/workflows/release-build.yml ``` Homebrew on Linux works too: `brew install ericfitz/tap/agentbus` installs the static amd64 or arm64 binary. ## Windows: install script, Scoop, winget ```powershell irm https://github.com/ericfitz/agentbus/releases/latest/download/install.ps1 | iex ``` Installs `agentbus.exe` into `%LocalAppData%\Programs\agentbus` (amd64 or arm64) and adds that directory to your user `PATH`; open a new terminal afterwards. Windows PowerShell 5.1 and PowerShell 7 both work. The script verifies the release's Ed25519-signed `SHA256SUMS` with OpenSSL 3 and then the zip's hash. It looks for OpenSSL 3 on `PATH` first, then in Git for Windows (`usr\bin\openssl.exe`, `mingw64\bin\openssl.exe`, then, on ARM64, `clangarm64\bin\openssl.exe`); `winget install --id Git.Git -e` provides one, and without OpenSSL 3 the script refuses. Rerunning upgrades: a running `agentbus.exe` is renamed, not overwritten, so restart harness sessions and the TUI afterwards. The script never runs `agentbus init`; it prints the next steps. The release zips also carry a `README.txt`, but unzipping one does not install agentbus: the harnesses start `agentbus.exe` by name. On Windows, `agentbus init` (any form, including `--dry-run`) refuses to run unless the `agentbus` that `PATH` resolves is the binary you ran, and its message gives the install script command. | Variable | Meaning | |---|---| | `AGENTBUS_VERSION` | Install this tag (`vX.Y.Z`) instead of the latest release. | | `AGENTBUS_INSTALL_DIR` | Absolute target directory (default `%LocalAppData%\Programs\agentbus`). | | `AGENTBUS_BASE_URL` | A mirror of `https://github.com/ericfitz/agentbus`. | | `AGENTBUS_SKIP_SIGNATURE` | `1` skips only the signature check; the hash is still verified. Unset is the default; any other value is rejected. | As on Linux, the script stops with a message instead of guessing when it is given positional arguments, an `AGENTBUS_VERSION` that is not exactly `vX.Y.Z`, a relative `AGENTBUS_INSTALL_DIR`, no `LocalAppData` and no `AGENTBUS_INSTALL_DIR`, a copy without an embedded release key, or when `\agentbus.exe` is a directory. It also stops when the target directory is not writable, and it fails after installing if the installed binary does not run `agentbus version`. A refusal throws, so a piped `iex` leaves your session open. Package managers: ```powershell scoop bucket add ericfitz https://github.com/ericfitz/scoop-bucket scoop install agentbus winget install ericfitz.agentbus ``` The winget command works once the winget package is published. Stop agentbus sessions (MCP servers, `agentbus wait`, `agentbus watch`, the TUI) before `scoop update agentbus` or `winget upgrade ericfitz.agentbus`: neither can replace the files of a running program. To tell whether a Windows binary is signed, run `Get-AuthenticodeSignature .\agentbus.exe`: a signed binary reports `Status` `Valid` and the expected signer (the maintainer, through Microsoft Artifact Signing). A release whose notes say its Windows binaries are not Authenticode-signed, and every release made before signing existed (which has no such notes line), is unsigned; it is still covered by the signed `SHA256SUMS` and the build attestations. See [release-signing.md](release-signing.md). Windows paths: the configuration file is `%AppData%\agentbus\config.json` and the data directory `%LocalAppData%\agentbus` (Local, so the database does not roam with a domain profile). `AGENTBUS_CONFIG`, `AGENTBUS_DATA_DIR` and `data_directory` override them as on other platforms. `%AppData%` must be set (it is in a normal session); in a stripped environment pass `--config` or set `AGENTBUS_CONFIG`. ## WSL Inside WSL use the Linux installer above. Windows and WSL each run their own bus: an agent in a Windows terminal and an agent in a WSL shell do not see each other. agentbus refuses a data directory that crosses the boundary (a `/mnt/c/...` path under WSL, or a `\\wsl$\...`, `\\wsl.localhost\...` or a drive mapped to one of those on Windows), because SQLite's file locks do not work between Windows and WSL processes on one file and concurrent use could corrupt the database. Keep each side's database on its own disk. ## Or, build from source ```sh CGO_ENABLED=0 go install -trimpath github.com/ericfitz/agentbus@latest ``` This installs the latest tagged source into `$(go env GOBIN)` (default `~/go/bin`); `agentbus version` prints the version compiled into the source, which may lag the tag. From a clone, `CGO_ENABLED=0 go build -o agentbus .` at the repository root does the same. Put the binary on your `PATH` so harnesses can find it by name. Maintainers cut a release with `release/release.sh --windows-signing=on|off` (a stable `vX.Y.Z` tag that is already pushed; the flag is required, see [release-signing.md](release-signing.md)): macOS is built and notarized locally, the other archives come from the `release-build` workflow, and the release is a draft until `SHA256SUMS` is signed. The script needs an authenticated `gh` that can verify attestations, and `.github/workflows/release-build.yml` must be on the default branch and in the tag, because `gh workflow run --ref ` only dispatches a workflow the default branch knows. The workflow builds and runs each Windows zip's binary on a native Windows runner. `--no-publish` stops with the signed draft for a dry run. `--skip-windows-packages` still ships the Windows zips but leaves the Scoop bucket and winget at the previous version (no `komac` or komac token needed). Every release carries `SHA256SUMS` over all archives, `install.sh` and `install.ps1`, signed with the Ed25519 release key. ## Before first run: bootstrap with `agentbus init` Once per machine, from any directory outside a git repository (or with `--global` from inside one): ```sh agentbus init --global ``` For each harness it finds (`~/.claude`, `~/.codex`, or `~/.grok` exists), it registers the MCP server through the harness's own CLI (`claude mcp add -s user`, `codex mcp add`, `grok mcp add --scope user`), merges an `agentbus identity` SessionStart hook, an `agentbus stop-hook` Stop hook, an `agentbus unpark` UserPromptSubmit hook (ADR 0025), and (Claude Code only) an `agentbus subagent-hook` SubagentStart hook (backing the file up to `.bak` first), and installs the `using-agentbus` skill. Hook and skill paths: | Harness | Hooks | Skill | |---------|-------|-------| | Claude Code | `~/.claude/settings.json` (SessionStart, Stop, UserPromptSubmit, SubagentStart) | `~/.claude/skills/using-agentbus/SKILL.md` | | Codex | `~/.codex/hooks.json` (SessionStart, Stop, UserPromptSubmit) | `~/.agents/skills/using-agentbus/SKILL.md` | | Grok Build | `~/.grok/hooks/agentbus.json` (Stop, UserPromptSubmit; see below) | `~/.grok/skills/using-agentbus/SKILL.md` | The Stop hook runs when the agent finishes a turn. If messages it has not been handed yet are waiting, it keeps the agent working with an instruction to call `receive`; otherwise the agent stops as usual. It allows one such continuation per turn and lets the agent stop on any error (ADR 0012). The Claude Code SubagentStart hook (ADR 0016) puts a subagent on the bus when its dispatcher asks. Claude Code does not give the hook the subagent's prompt, so when its session is registered the hook tells every subagent: if your prompt asks you to use agentbus ("use agentbus" or "register with agentbus"), `register` with `parent` set to that identity and a distinct name (or the `parent=`/`name=` your prompt gives). It prints nothing on any error or when the parent is not registered. Codex also gets `tool_timeout_sec = 300` and `~/.codex/prompts/agentbus.md`. Grok Build also gets `~/.grok/commands/agentbus.md` (the `/agentbus` slash command) and `~/.grok/rules/agentbus.md`. Grok ignores SessionStart hook stdout, so `init` installs no SessionStart hook; that rule is what tells a session to run `agentbus identity` and follow it. Grok's default `tool_timeout_sec` is 6000, already above `receive_max_wait_seconds`, so `init` does not set one. The skill ships inside the binary, so rerun `init --global` after upgrading to refresh it. `--harness claude`, `--harness codex`, or `--harness grok` configures only that harness, even if it is not detected. `--dry-run` prints what would change without writing. Restart the harness afterwards. Then, inside each repository: ```sh agentbus init ``` This writes the repository's identity file (`.local/agentbus.json`, from the repository directory's name), creates the repository's own channels on the bus (`general/`, `memory/`) and adds them to the persistent channel list, adds `.local/` to `.gitignore` if it is not already ignored, and prints the registration line. A stale `tasks` or `tasks/...` entry already in the file (task lists are per effort now, not per repository) is removed, with a note naming what was removed. From inside a session the same thing is one command: `/agentbus:init` in Claude Code (an MCP prompt the server advertises, so nothing is installed for it), `/prompts:agentbus init` in Codex (from the custom prompt file above; Codex does not surface MCP prompts yet), or `/agentbus init` in Grok Build (from `~/.grok/commands/agentbus.md`). Either way the agent runs `agentbus init` and then calls `register` in the current session. ## Removing agentbus from a harness: `agentbus uninstall` ```sh agentbus uninstall [--harness claude|codex|grok] [--dry-run] ``` It undoes `agentbus init --global` for each harness it finds, or only the one named by `--harness`, even if that harness is not detected. `--dry-run` prints what would be removed without changing anything. There is no confirmation prompt. A second run still calls each harness's `mcp remove`: Claude and Grok then print `... failed (agentbus may not have been registered)` and Codex exits 0. Everything else is reported "not present", and nothing on disk changes. For each harness it removes: - the MCP entry, through the harness's own CLI (`claude mcp remove -s user agentbus`, `codex mcp remove agentbus`, which also removes `tool_timeout_sec`, and `grok mcp remove --scope user agentbus`). Without that CLI on PATH it prints the entry to delete by hand; - the hook commands that contain `agentbus identity`, `agentbus stop-hook`, `agentbus unpark` or `agentbus subagent-hook` (a substring match, so a hook whose command merely contains one of these, such as a wrapper script line, is removed too), from `~/.claude/settings.json`, `~/.codex/hooks.json` and `~/.grok/hooks/agentbus.json`, after backing each edited file up to `.bak`. Grok's hook file is deleted only once nothing else is left in it (no hooks and no other keys); - the skill, prompt, command and rule files that `init --global` wrote; - the directories those deletions leave empty, but never a directory directly under your home directory. It leaves alone the bus database and key, the config file, every repository's `.local/` and running processes. It never deletes a `.bak` file; each hook file it edits is backed up to `.bak` first, which replaces any older backup. Restart the harness afterwards. The sections below describe what `init` sets up, for doing it by hand. ## Configuration (optional) Default path: `~/.config/agentbus/config.json` (`%AppData%\agentbus\config.json` on Windows, where `%AppData%` must be set or you pass `--config` or `AGENTBUS_CONFIG`). No file means defaults. Override the path with `--config ` or the `AGENTBUS_CONFIG` environment variable; override the data directory alone with `AGENTBUS_DATA_DIR`. Example enabling semantic memory search through a local Ollama: ```json { "embedding_endpoint": "http://localhost:11434/v1/embeddings", "embedding_model": "nomic-embed-text" } ``` Embeddings are optional and off by default. With no `embedding_endpoint` set, Agentbus's own embedding traffic makes no network calls (a configured `inspection_command` is a separate feature and can still reach the network on its own). For a remote provider, set `embedding_endpoint` and `embedding_model` to that provider's values and add `"embedding_api_key_file": "~/.keys/VOYAGE_API_KEY"`. The file may be a bare key or an `export NAME=value` line (value bare, 'single-quoted' or "double-quoted"). Blank lines and `#` comments (whole-line, or after the value) are ignored; plain `NAME=value` without `export` is not supported. Any other content, such as a second line or a value with whitespace, makes startup fail with an error naming the file (as a missing or unreadable file does). Its contents are never logged. `"embedding_api_key_env": "OPENAI_API_KEY"` names an environment variable to read the key from instead; it wins when set and non-empty, and the key file is the fallback for processes started without it (a harness-spawned MCP server only sees the variable if the harness was launched from a shell that had it). `embedding_query_timeout_seconds` (default 10, range 0.1-120) bounds the query embedding during a search; past it, `semantic` and `both` searches fall back to text results and set `semantic_unavailable`. `task_idle_hours` (default 168, 7 days), `task_expiry_hours` (default 336, 14 days), and `memory_expiry_hours` (default 8760, 1 year) are the ADR 0013 inactivity periods: the TUI marks a task channel idle in its rail after `task_idle_hours` with no task activity; the maintenance tick deletes a task channel, with all its tasks, after `task_expiry_hours` with none; and it tombstones a memory that no `get_memory` call or `search` hit has touched for `memory_expiry_hours`. 0 disables the corresponding rule; negative values are rejected. `status_history_count` (default 20, range 1-200) is how many statuses `set_status` keeps per identity; older ones are dropped, and the TUI's status history view (`enter` on a board row) and `status_history` show what is kept. `status_offline_hours` (default 24, range 0-720) is how long an identity with no live session stays on the status board as `offline`, counted from its last status (an identity that never set one is not shown); 0 shows live sessions only. `status_stale_minutes` (default 30, range 0-1440) is how old a planning, implementing or reviewing status may get before the Stop hook reminds the session to update it; 0 turns the reminder off, including the "you have no status" nudge. `tui_rail_width` (default 0, range 0-1000) is the channel rail's width in columns. The TUI writes it when you press `<` or `>`, rewriting the config file with its keys sorted; 0 means a fifth of the screen. Each press steps the saved value (not the width shown), so a narrow terminal never overwrites a wide setting; what is drawn is clamped to the terminal. `tui_name` (default: your OS user name) is the identity `agentbus tui` registers under; `agentbus tui --as ` overrides it for one run. It follows the same rule as agent names: 1-128 bytes, no `/`, no control characters. Keep `receive_max_wait_seconds` below your harness's MCP tool call timeout: Claude Code's default is 300 seconds (the `MCP_TOOL_TIMEOUT` environment variable, or a per-server `timeout` field in the MCP config), Codex exposes `tool_timeout_sec` (init sets it to 300), and Grok Build's default `tool_timeout_sec` is 6000. Agentbus itself bounds `receive_max_wait_seconds` to a maximum of 240 seconds (default 60). ## Per-repo identity and channels (what `agentbus init` writes) `.local/agentbus.json` in the repository (git-ignored): ```json { "identity": "Sam", "channels": ["general", "memory", "general/Sam", "memory/Sam"] } ``` `identity` is the name the agent registers with. `channels` is the persistent subscription list: `register` subscribes the session to each listed channel (from the current position) and reports them in its `subscribed` field; channels that do not exist are reported in `subscribe_failed` and skipped. Without a `channels` key the list is `general` and `memory`; an empty list means no automatic subscriptions. A memory channel in the list is never subscribed: since ADR 0008 `register` returns it in `memory_channels` for `search`, and drops any memory subscription the identity still holds. A `tasks` or `tasks/...` entry left over from before ADR 0013 is ignored (not subscribed) and reported in `register`'s `ignored_channels` field instead; `init` removes such entries from the file, or delete them by hand. `init` writes the two machine-wide channels plus the repository's own `general/` and `memory/`, creating them on the bus if missing. A prefix implies the kind, so `create_channel` needs none for such names. Rerunning `init` recreates them for whatever identity the file names; a project channel from before v1.5.0 (``, `-memory`) is renamed to its prefixed name with its history. Task lists are separate and per effort, not per repository: create `tasks/` with `create_channel` when you start one, or subscribe to one you find with `list_channels`. It is removed, with all its tasks, after `task_expiry_hours` of inactivity (see "Configuration" above). Edit the list from the repository root with `agentbus subscribe ` and `agentbus unsubscribe ` (the file is created if missing), or from inside a session by passing `persistent: true` to the `subscribe` or `unsubscribe` tool. Changes apply at the next `register`. A persistent `subscribe` from a subdirectory creates the file at the nearest git root (not the subdirectory) when none exists yet. `agentbus identity` first checks the bus: if this harness process already holds a live top-level session (for example after a rename, resume or compaction), it prints that session's name (ADR 0025 decision 10; opening the bus creates the data directory and database if absent, as `wait` does). If the bus cannot be opened or the harness holds none, it falls back to the name below. It looks for this file by walking up from the current directory, stopping at the nearest `.git`, so a nested repository reports its own name rather than an enclosing one. Without a matching file it suggests the repository directory's basename; when that directory is your home directory (no repository, or a dotfiles repository there) the name is `-home`, where `` is the TUI's default name, so it cannot collide with the human's own identity. An identity in the file is never rewritten, and MCP `register` refuses the configured `tui_name` and any name a running `agentbus tui` holds. A session that already holds a name cannot register a second top-level name unless it passes `rename: true`, which ends the old one; a subagent passes `parent` instead. Its output is the session protocol every agent is told to follow: register, receive, keep a background `agentbus wait` running, post progress to the chat channel, search and post memories, discover. Every bus has two channels from the start, `general` (ordinary) and `memory` (memory), recreated after `agentbus reset`. ## Claude Code (manual setup) The harness's MCP server entry must be named `agentbus`, since the harness prefixes that name onto every tool: tools appear as `mcp__agentbus__` (`mcp__agentbus__send`, `mcp__agentbus__receive`, and so on). `.mcp.json` in the repository, or the user-level MCP config: ```json { "mcpServers": { "agentbus": { "command": "agentbus", "args": ["mcp"] } } } ``` `.claude/settings.json` hooks: SessionStart so every session (startup, `/clear`, resume) is told to register, and Stop so the session checks the bus at the end of every turn (ADR 0012): ```json { "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "agentbus identity" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "agentbus stop-hook" } ] } ], "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "agentbus unpark" } ] } ], "SubagentStart": [ { "hooks": [ { "type": "command", "command": "agentbus subagent-hook" } ] } ] } } ``` Claude Code keeps one `agentbus mcp` connection per conversation and shares it with subagents. Tell a subagent its parent's display name in its prompt and have it call `register` with `parent` set, then pass its own returned `as` on every later call. ## Codex (manual setup) `~/.codex/config.toml`: ```toml [mcp_servers.agentbus] command = "agentbus" args = ["mcp"] tool_timeout_sec = 300 ``` `tool_timeout_sec` must stay above `receive_max_wait_seconds` (see [Configuration](#configuration-optional)) or a long `receive` wait gets cut off by Codex before Agentbus itself would have returned. Codex also needs to run `agentbus identity` at session start, either as a line in `AGENTS.md` or as a `$CODEX_HOME/hooks.json` SessionStart hook. The Stop hook checks the bus at the end of every turn: ```json { "hooks": { "SessionStart": [ { "matcher": "startup|resume|clear", "hooks": [ { "type": "command", "command": "agentbus identity" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "agentbus stop-hook" } ] } ], "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "agentbus unpark" } ] } ] } } ``` Codex does not wake an idle session when a background command exits, so a Codex session sees messages that arrive while it is idle at the end of its next turn, not sooner. Codex asks you to trust a hook the first time it would run one; accept that prompt, or start Codex with `--dangerously-bypass-hook-trust`, or the hook never fires. Codex spawns a separate `agentbus mcp` process per thread, including subagent threads; each registers on its own, so there's no single shared connection to inherit an `as` from the way Claude Code subagents do. Each thread must still call `register` itself and pass its own returned `as` on every later call; if you want a subagent thread's display name to show its parent, its prompt must tell it the parent's name to pass as `parent` on `register`, since a separate process has no other way to learn it. ## Grok Build (manual setup) `~/.grok/config.toml` (what `grok mcp add --scope user agentbus -- agentbus mcp` writes): ```toml [mcp_servers.agentbus] command = "agentbus" args = ["mcp"] ``` Leave `tool_timeout_sec` unset unless you have lowered the default of 6000 below `receive_max_wait_seconds`. `~/.grok/hooks/agentbus.json` (files there are trusted without a prompt) checks the bus at the end of every turn: ```json { "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "agentbus stop-hook" } ] } ], "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "agentbus unpark" } ] } ] } } ``` Grok ignores SessionStart hook stdout, so a SessionStart hook cannot deliver the registration block. Install `~/.grok/rules/agentbus.md` instead: ```markdown # Agentbus At the start of a session, run `agentbus identity` and follow its output. ``` `init --global` writes that rule with the register instructions included. The skill goes to `~/.grok/skills/using-agentbus/SKILL.md`. Inside a session, `/agentbus init` comes from `~/.grok/commands/agentbus.md`. ## Operating - `agentbus init` bootstraps a repository; `agentbus init --global` bootstraps the harnesses on this machine (see above). - `agentbus version` prints the version. Release builds set it with `-ldflags "-X github.com/ericfitz/agentbus/internal/mcpserver.Version="`. - `agentbus stop-hook` is the Stop hook `init --global` installs. It reads the hook's JSON from stdin (`cwd`, `stop_hook_active`/`stopHookActive`) and prints `{"decision":"block","reason":...}` when unseen messages are waiting for its session. Always exits 0. - `agentbus park` / `agentbus unpark` (ADR 0025): `park` puts a session in a state where only its direct messages wake it (`wait`, `watch`, the Stop hook); `unpark` ends it and is the UserPromptSubmit hook `init --global` installs in every harness. `-as` names the session; without it the session is found as for the hooks below. `unpark` prints nothing and always exits 0; `park` exits 2 when no session can be found. `register`, any MCP tool call except `receive`, and `agentbus reset` also end a park. - The hooks and `agentbus wait` or `watch` without `-as` find their session by harness process (ADR 0017): the `agentbus mcp` server records the harness (its first non-shell ancestor) on each session it registers, and they use the live top-level session registered from their own harness. Without a match (or on a platform other than macOS, Linux and Windows) they use the working directory's identity, what `agentbus identity` prints. So a second session in one repository, registered as `2`, gets its own hooks and wait. - `agentbus subagent-hook` is the Claude Code SubagentStart hook. It reads `cwd` and `agent_type` from stdin and, when its session is registered, prints `hookSpecificOutput.additionalContext` telling the subagent to register only if its prompt asks it to. Always exits 0. - `agentbus wait` exits 0 when a message arrives, 1 on `-timeout`, 2 on an error, 3 when a newer `agentbus wait` for the same identity replaced it, and 4 when the harness that started it exited (both silent, no output; ADR 0014). The harness is the first ancestor that is not a shell, checked every 2 seconds with its start time to guard against pid reuse; `-no-harness-watch` turns this off. Only one wait runs per identity, and a wait for a top-level identity also replaces the other top-level waits from the same harness; its pid, harness and how the identity was chosen are recorded in `/wait/.pid`, and a stale pidfile never blocks a new wait. Shared-harness caveat: any two top-level waits or watches from one harness replace each other regardless of `-as` (panes of tmux or screen share the tmux server as their harness), and a `-no-harness-watch` wait still records its harness. - `agentbus watch` is deprecated but still available (ADR 0025 stops recommending it; agents run a background `wait` on every harness except Codex). It is `wait` for Claude Code's Monitor tool (ADR 0021). It takes `wait`'s flags except `-timeout`, runs until Monitor kills it (at most 30 minutes, so run it with `timeout_ms` 1800000 and re-arm it when the monitor expires), and prints one line per wake: `agentbus: new message(s) for on ; call receive`, with a `+` after `` when the batch hit `receive_max_count`. The line carries no message content. A message is announced once, even if `receive` has not run yet, and lines are at least 5 seconds apart; messages arriving in the gap share the next line. Nothing but wake lines goes to stdout. It never exits 0: 2 on an error (message on stderr), 3 when a newer `watch` or `wait` for the same identity replaced it, and 4 when its harness exited (both silent). `wait` and `watch` share one lock and pidfile, so either replaces the other. Codex has no idle wake. - `agentbus identity` prints the registration block (the register sentence plus the session protocol) for the current directory, naming the live session this harness already holds if there is one (else the name derived from the directory). It takes `-config` (or `AGENTBUS_CONFIG`) to find the bus. It's also what the SessionStart hooks above run. - `agentbus subscribe ` / `agentbus unsubscribe ` edit the persistent channel list in `.local/agentbus.json`. Run from the repository root; takes effect at the next register. - The MCP server logs to `agentbus.log` in the data directory as JSON lines (one object per line with `time` in UTC RFC3339 milliseconds, `level`, `msg`, `pid`), rotated across four 16 MiB files. Background failures (embedding, maintenance steps) are logged there as warnings with diagnostic fields; each process logs an identical failure once, then summarizes repeats (`suppressed=N`) at most once per ten minutes and when it exits. - `agentbus status` shows live identities, channels, usage against budget, and any capacity notice. - `agentbus tui` opens a live dashboard: channels with unread counts on the left with live sessions under them, the selected channel's stream in the center, a compose line, and a status bar. It registers as `tui_name` from the config (default: your OS user name; `--as ` overrides) and is an ordinary bus participant, so agents see your messages like any other. Press `esc` for the command keys and `?` for the full keymap. `tab` and `shift+tab` move between the rail (its five sections), the message list of the highlighted channel or session, and the compose line. The rail has five sections, each under a heading: channels, memories, tasks, tags and sessions. `↑` and `↓` walk the headings and their rows in turn; a `general/`, `memory/` or `tasks/` prefix is left off a row's name in its own section (rows whose short names collide keep their full names). With a heading selected, `←` collapses its section, `→` expands it and `enter` toggles it; a collapsed heading reads `▶ memories (19, 3 unread)` (the item count, plus unread messages when there are any), and the stream keeps showing the last channel or session you selected. `s` and `d` do nothing on a heading. Collapsed sections are not saved: every start shows all of them expanded. On a short terminal the rail drops the blank rows between sections, then shows at least one item of each section with a combined `▲N ▼M more` row. `home` selects the channels heading. Selecting a session shows its direct-message inbox (`dm/`): the count beside a session is its unviewed direct messages, and the compose line there sends that identity a direct message. Your own row is your inbox; agents reach you at `dm/`. `/` searches, `h` opens health and config, `q` quits. Arrow keys never change pane: `↑`/`↓` move within the focused one, `→` opens the selected message's body if it's closed and has one, else shows its direct replies; `←` hides shown replies (the whole subtree), else closes the body. On a memory, `.` shows the next older version and `,` the next newer one, labeled `r · kept`; any other key returns it to the latest. A memory past its first revision shows `r` the same way. The label sits on the message's header line, after the sender (and the `→` recipient, when shown) and before any tag chips. `<` and `>` narrow and widen the channel rail two columns at a time; the width is saved as `tui_rail_width` in the config file (0 or absent is a fifth of the screen), stepping from the saved width; the width drawn is clamped to the terminal, keeping the rail at least 16 columns and the messages pane at least 40. `enter` replies to the selected message, or opens compose from the rail. Colors come from the `theme` and `themes` config settings; see below. The first TUI launch after upgrading subscribes to every existing inbox from its oldest retained message, so retained direct messages show as unread once. A `tasks/` channel shows its task tree: subtasks start hidden under their parent (`N subtasks`), `→` opens a task's details, then its subtasks, `←` hides its subtasks, then its details; ❎ pending, ⏱️ in progress (with the owner's name beside it), ✅ completed (dimmed); a blocked task keeps ❎ with `blocked by #n`. You can change tasks that are unassigned or yours (the TUI's identity): `space` cycles the selected task's state (not started, in progress, completed) as a draft shown in the `unsaved` color, `enter` saves it (a move into in progress claims the task), `esc` cancels it; moving the cursor or leaving the pane discards it. `t` takes an unassigned task, `u` unassigns your not-started one, and `a` opens a picker of you and the live sessions and assigns the task to the one you choose (`esc` closes it without a change). A task owned by an agent cannot be changed here. From the channel or session list, `t` follows a tag set (a `tags` section lists yours; select one to see every chat message carrying all of its tags; `s` or `d` on it unfollows, no confirmation). - `agentbus reset` deletes all bus data (messages, memories, channels, identities, cursors) after you type `yes` to confirm; configuration is kept. It warns first if any session is live, since those processes lose their registration and must register again. Message sequence numbers keep counting up across a reset rather than restarting at 1. - Logs never go to stdout or stderr. The default data directory is `~/.local/share/agentbus` on macOS and Linux, and `%LocalAppData%\agentbus` on Windows. ## TUI theme Colors live in named themes. `themes` is an array of them and `theme` names the one the TUI applies (default `default`). The built-in `default` theme is always available unless the file defines its own entry of that name: ```json { "theme": "night", "themes": [ { "name": "night", "background": "black", "agent": "brightcyan", "user": "brightyellow" } ] } ``` A value is one of the sixteen ANSI color names (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, or a `bright` form such as `brightblack`), an index `0`-`15`, or `default` for the terminal's own color. Matching is case-insensitive. Nothing here fails config load: a `theme` name that is not in `themes`, a key a theme leaves out, and a value that is not a color (including `#RRGGBB`) all fall back to the `default` theme's value; bad values and unknown theme names print one line on stderr before the TUI starts. | Theme key | Used for | Default | |-----------|----------|---------| | `background` | screen background | `default` | | `text` | message content | `default` | | `dim` | dividers, help, summaries | `brightblack` | | `timestamp` | message timestamps (unselected rows) | `brightblack` | | `tag` | tag chip background (`default` falls back to dim `#tag` words) | `brightblack` | | `agent` | agent names, selected channel, key hints | `cyan` | | `user` | your own name | `yellow` | | `memory` | memory channels and memory versions | `magenta` | | `tasks` | task-list channels | `yellow` | | `health` | live heartbeat dot, ok states | `green` | | `warn` | warnings such as the text-only search badge | `yellow` | | `error` | errors and the delete confirmation | `red` | | `selection` | selected row background (focused pane) | `blue` | | `selection_inactive` | selected rail row background while the messages or compose pane has focus | `brightblack` | | `selection_text` | text of the selected rail row while its pane has focus | `black` | | `unsaved` | a task row whose drafted state is not saved yet (`space` in a task list; `enter` saves) | `yellow` | On the selected message row, dim text (timestamp, thread summary) switches to the `text` color so it stays readable on `selection`. Chip text is black on light `tag` colors (yellow, cyan, white, and bright green/yellow/cyan/white) and bright white on the rest. The health overlay (`h`) shows the log file path, the theme in use with each resolved value, each agent session's harness name and version (from the MCP `clientInfo` its harness sends; `unknown` if it sends none), and the loaded config. `o` opens the config file in `$VISUAL`, else `$EDITOR`, else `vi`, run through the shell so a value with arguments or spaces works. A GUI editor in `$VISUAL` (for example `code --wait`) opens in the background and the TUI stays live; the config check shows when the editor exits (with `--wait`, when you close the tab). A terminal editor (`vi`, `vim`, `nvim`, `nano`, `emacs`, `micro`, `hx`, and similar), or one set only in `$EDITOR`, takes over the terminal until you quit it. ## TUI icons `icons` picks the icon set the TUI draws for channels, senders, task status, and the other markers below; the default, `emoji`, needs no font setup and is what every screenshot in this doc shows. ```json { "icons": "nerdfont", "icon_map": { "chat": "\uf27a" } } ``` | Value | What it does | |-------|--------------| | `emoji` | default; color emoji, no font setup | | `nerdfont` | Nerd Font glyphs at the codepoints below | | `custom` | `emoji`, overridden per-name by `icon_map` | `icon_map` applies on top of whichever `icons` set is chosen (`emoji`, `nerdfont`, or `custom`, itself just `emoji`); it maps any of the names below to a glyph (a literal character, or a `\uXXXX` JSON escape -- JSON has no `\U` escape, so a supplementary-plane glyph like `idle`'s needs a UTF-16 surrogate pair, e.g. `"\udb81\udcb2"` for U+F04B2, or the literal character pasted directly). An icon name `icon_map` doesn't set keeps its base-set glyph. An unknown `icons` value, an unknown `icon_map` name, or an empty `icon_map` value falls back to the base set for that icon and prints one line on stderr before the TUI starts, the same way a bad theme value does. | Icon name | Used for | emoji | nerdfont (Nerd Fonts 3.x codepoint) | |-----------|----------|-------|--------------------------------------| | `chat` | chat channels | speech balloon | U+F27A fa-message | | `memory` | memory channels | floppy disk | U+F0C7 fa-floppy-disk | | `tasks` | task-list channels | clipboard | U+F0AE fa-list-check | | `agent` | other senders and sessions | gear | U+EE0D fa-robot | | `user` | your own name | adult | U+F007 fa-user | | `idle` | an ended session, or a task channel past `task_idle_hours`, in the rail | sleeping symbol | U+F04B2 md-sleep | | `task_pending` | a pending or blocked task | ❎ | U+F096 fa-square | | `task_in_progress` | an in-progress task | stopwatch | U+F152 fa-square-caret-right | | `task_completed` | a completed task | ✅ | U+F14A fa-square-check | | `collapsed` | selected list item; a thread that can expand | ▶ | U+F0DA fa-caret-right | | `expanded` | a thread whose replies are shown | ▼ | U+F0D7 fa-caret-down | | `error` | the toast and overlay error prefix | ✗ | U+F06A fa-circle-exclamation | | `arrow` | sender → recipient, a pending task's owner | → | U+F061 fa-arrow-right | The `agent` and `user` icons are drawn in the sender's color (the `user` theme color for your own name, `agent` for everyone else) wherever they appear: message headers, the sessions rail, in-progress task owners, and a DM's compose prompt. The focused rail row drops all color, as it does for every row. This applies to every set, including glyphs set with `icon_map`; color emoji ignore the text color. The health overlay (`h`) names the active set on an `icons ·` line. ### Using a Nerd Font `nerdfont` needs a Nerd Font installed and selected as the terminal's font for the codepoints above (agentbus does not install or download any font). Install a **Mono** variant — the non-Mono build advances two columns for these glyphs where agentbus's rail and task-row alignment assumes one, and they'll look off by a column: ``` brew install --cask font-sauce-code-pro-nerd-font ``` Then point the terminal at it: - **iTerm2**: Preferences → Profiles → Text → set "Non-ASCII Font" to the Nerd Font Mono (the main font can stay whatever you use for code). - **kitty**: add a `symbol_map` line in `kitty.conf` covering the Private Use Area ranges above, pointing at the Nerd Font Mono. - **WezTerm**: add the Nerd Font Mono to `font_fallback` in `wezterm.lua` (or set it as the whole `font` if you want it for code too). - **Terminal.app**: its font fallback for these codepoints is unreliable; set the Nerd Font Mono as the profile's own font rather than relying on fallback. ### Font Awesome Pro Three Font Awesome glyphs — `fa-face-sleeping` (idle), `fa-microchip-ai` and `fa-user-robot` (agent alternatives) — are Pro-only and not in Nerd Fonts. They're reachable only through `icon_map`, mapped to the codepoints of your own licensed Pro font; agentbus ships no fonts, Free or Pro. Nerd Fonts and Font Awesome Pro share some Private Use Area codepoints with different glyphs at them (for example U+F46D is `oct-home` in Nerd Fonts, not the Pro glyph some icon lists give it there). If your terminal's primary font is a Nerd Font, a Pro codepoint that collides with one of its assignments renders the Nerd Font's glyph instead of the Pro one, so check each `custom` codepoint against the Nerd Font cmap before relying on it. Do not redistribute a patched Pro font; that violates its license. ## Limits worth knowing - `register`'s `context` is capped at 1 KiB and `parent` at 512 bytes. - An idle subscription is reaped, and reported as expired, the next time its owner calls `receive`. Re-registering also reaps any idle subscription for that name, but drops it silently instead of reporting it. The periodic background maintenance tick does not reap subscriptions itself. - A single record larger than `result_default_kib` is still delivered on its own rather than dropped; only the fixed 4 MiB hard ceiling is never exceeded.