Blackbird is a durable, local-first coordination service for human and AI agent
work. The program is the Rust binary in rust/. It is one database
and eight MCP tools. See ADR-0004.
Agents get project-scoped identity, durable mail, and advisory path leases:
blackbird_join, blackbird_claim, blackbird_release, blackbird_status,
blackbird_say, blackbird_read, blackbird_ack, and blackbird_wait.
Registration tokens are stored as a SHA-256 hash. A conflicting claim is
ok: false. name@host, spend, and tracker reads are refused.
The database file is coordination-v1.sqlite under $XDG_STATE_HOME/blackbird/
(or $BLACKBIRD_DB). A Go database is refused and left unchanged. There is no
migration.
blackbird daemon listens on 127.0.0.1:8081. POST / is stateless JSON-RPC.
GET /health is the probe. blackbird mcp speaks the same JSON-RPC on stdin.
phux can launch that stdio server as a plugin (rust/phux-plugin.toml). The
wrapper unsets PHUX_SOCKET and does not attach to a phux server.
cargo test --manifest-path rust/Cargo.toml
cargo build --manifest-path rust/Cargo.toml --bin blackbird
rust/target/debug/blackbird doctorblackbird install
blackbird statusinstall copies this binary to ~/.local/libexec/blackbird/blackbird and
writes a new unit (io.phux.blackbird-rust on macOS, blackbird-rust.service
on Linux) plus the MCP client entries for Claude, Codex, and OpenCode. If
port 8081 is already taken, install stops and leaves that process running.
It does not replace a Go launchd job named com.phall1.blackbird.
uninstall removes that unit, those client entries, and the copied binary.
The database stays.
The Homebrew formula tracks the last published release. A checkout of this tree builds the Rust binary with Cargo, above.
blackbird daemon [--sqlite-path PATH] [--http-listen 127.0.0.1:8081]
blackbird mcp
blackbird doctor
blackbird status
blackbird overview
blackbird agents
blackbird inbox
blackbird reservations
blackbird install
blackbird uninstall
blackbird versionblackbird --sqlite-path=PATH with no subcommand starts the daemon, which is
what an existing service definition already runs. --http-listen must be
loopback. Tests bind 127.0.0.1:0.
Start with blackbird_join, passing an absolute repository path as
project_key and a stable agent_name. Retain the returned
registration_token to resume the same identity after process or machine
restarts.
The MCP surface is exactly eight tools: blackbird_join, blackbird_claim,
blackbird_release, blackbird_status, blackbird_say, blackbird_read,
blackbird_ack, and blackbird_wait. Spend, cost, and tracker fields on status are refused.
All tools except initial join authenticate with the returned agent_token.
Claim the narrowest relevant paths before editing, use one conversation per
work item, acknowledge required handoffs, and release exact selector sets when
work completes.
A refused claim is a normal ok:false result, not a retry-loop error. Its
blocked_by and options identify the holder and the useful next actions;
blackbird_status answers the same question at any time, and blackbird_wait
parks until the path frees or mail arrives. It returns path_free,
mail_arrived, or deadline, so a caller that ran out of budget still learns
what happened.
This binary does not serve peer mail. A name@host recipient is refused.
The listener accepts loopback only.
"Push" says only that an adapter moved a message; it does not say what the host does to the model. Blackbird names that behavior with three verbs:
- notify adds durable context without starting a model turn. OpenCode uses
this mode with
noReply: true; a future Codex adapter can usethread/inject_itemsfor the same contract. - steer admits a message into the active model loop. Claude Code MCP Channels provide this mid-turn path.
- queue schedules an ordered follow-up that runs when the host can accept another turn. Pi uses this mode while a session is busy.
These are host behaviors, not Blackbird mailbox facts. Adapter delivery in any mode never marks a message read or acknowledged.
blackbird hook is one fail-open Go adapter for the command-hook contracts that
can add stdout to model context. It supports Claude Code, Cursor, and GitHub
Copilot CLI with host-specific JSON emitted by the same binary. Hooks only run at
host lifecycle boundaries, so this is queue delivery rather than externally
triggerable push. Codex's outbound-only notify callback and Devin's hosted
automations cannot consume this contract and are explicitly unsupported. See
command-hook delivery for exact config and limits.
The plugin in packages/opencode-plugin includes a
native /blackbird (/mail) browser: select an agent mailbox, filter unread
mail, and open full, paginated threads. It opens beside a session or as a page
from home, with keyboard and mouse navigation and narrow-terminal support.
Browsing leaves agent read receipts and acknowledgements unchanged.
The server uses the V2 Effect SDK and the local daemon's authenticated admin API; the terminal uses typed RPC. See the plugin setup and development guide for loading the checkout, optional notify-mode delivery, and isolated UI tests.
The blackbird-opencode package appends each durable message.available event
to an OpenCode session transcript without spending an agent turn — the message
is persisted and visible, and no agent loop is scheduled to answer it. It uses
SSE only as a low-latency wake signal, catches up from the SQLite event journal
after every reconnect, resolves each message through a privacy-checked endpoint,
and does not mark or acknowledge mail on the agent's behalf.
Add it to OpenCode's plugins configuration with an absolute repository path:
Every version pinned in a delivery example on this page is the one this repository shipped when the example was written; release-please bumps each package on its own tag, so read the current set rather than those lines:
jq -r '"\(.name)@\(.version)"' packages/*/package.jsonOpenCode installs the package and its production dependencies in its isolated
plugin cache. Blackbird stores each adapter's delivery cursor server-side;
registration tokens, conversation-to-session bindings, and host-specific
quarantine state remain under $XDG_STATE_HOME/blackbird with private directory
and file permissions.
blackbird-pi is a Pi-native extension. It runs only inside an active Pi
session and queues durable messages as ordered follow-ups that trigger a turn
when Pi can accept one:
pi install npm:blackbird-pi@0.1.1The blackbird Claude Code plugin uses MCP Channels to steer messages into the
active model loop. Claude owns its stdio channel server for the active session;
there is no Blackbird adapter service.
claude plugin marketplace add phall1/blackbird
claude plugin install blackbird@blackbird
claude --channels plugin:blackbird@blackbirdAdapter delivery never marks a Blackbird message read or acknowledged.
Gemini CLI push delivery is not on the roadmap. Its hook events
run only from the CLI's own lifecycle, so an external Blackbird message cannot
wake an active session. Its A2A server also advertises
pushNotifications: false, while the HTTP API still has an open
authentication defect. Reconsider this only if Gemini ships
an externally triggerable, authenticated push primitive; MCP remains the pull
half and cannot substitute for one.
Microsoft's Agent Host Protocol represents a host's sessions, chats, and subagents to clients; it explicitly does not define peer identity, agent-to-agent messaging, task assignment, or coordination. Blackbird therefore does not adopt AHP as its core protocol: its repository-scoped agents, durable mail, and advisory path claims are complementary. If a supported host exposes a stable AHP endpoint, integrate it through a thin adapter rather than changing Blackbird's storage or delivery semantics.
Blackbird does not ship an Agent Client Protocol agent. ACP clients spawn a coding agent that owns its model, tools, and prompt turns; Blackbird is a coordinator, not a second coding-agent runtime. A Blackbird ACP process would therefore open a separate coordination-only chat, while wrapping an existing ACP agent would duplicate that agent's session, authentication, and tool lifecycle just to proxy JSON-RPC.
The useful integration already exists one layer lower. JetBrains and Zed both forward configured MCP servers to the selected external agent, so install Blackbird's MCP server for the real coding agent rather than replacing it with a Blackbird-branded ACP shell. Reconsider an ACP adapter only if the protocol gains a client-side facility for injecting context into an already-running external-agent thread; stable ACP v1 exposes no such method.
cargo fmt --manifest-path rust/Cargo.toml -- --check
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path rust/Cargo.tomlBLACKBIRD_VERSION, BLACKBIRD_COMMIT, and BLACKBIRD_BUILT_AT are read at
compile time. --version prints
blackbird version=<v> commit=<c> built_at=<t>. Unset fields are the crate
version, unknown, and unknown.
Releases are cut by release-please from Conventional Commit subjects. feat:
takes the minor, fix: and perf: take the patch, and every other type is
recorded without appearing in the changelog.
Landing a commit on main updates a single chore: release main pull request,
rebased onto main each run. That pull request is the release: merging it
writes the changelog and manifest, tags vX.Y.Z, and triggers the release
workflow, which builds each target on its own native runner, rebuilds and
compares the binary to prove the build is reproducible, asserts --version
against the tag, publishes the archives with checksums, and dispatches the
formula update to phall1/homebrew-tap.
The release branch is generated. Never commit to it or merge into it: the next run rebases it away. Anything that belongs to a human — upgrade notes, guidance, rationale — belongs in this README or in the commit subject that earns the changelog line.
Pull requests squash, and the repository allows nothing else. One pull request
becomes one commit whose subject is the pull request title, so a change earns
exactly one changelog line. A merge commit that repeats the branch's own
feat: subject earns two, which is how 0.4.0 came to list its feature twice.
Two settings are load-bearing and easy to break. The root package declares no
component: release-please parses the component back out of the merged release
pull request's title, an aggregate title carries none, and a configured
component it cannot find makes it refuse to tag with PR component: undefined does not match configured component. The release then sits merged and
untagged, which blocks every later release too. Branch auto-deletion is also
off, so a merged release branch survives long enough for its release to be
built. If a release ever lands merged but untagged, read the Release Please run
log before touching anything: the abort line names the cause.
The pinned Go toolchain is deliberate and appears in go.mod and both
workflows. Keep them in step, and note that the release build omits
-buildid=: with it, Go emits a macOS binary without an LC_UUID load command
that the dynamic loader refuses to run. Builds stay reproducible without it,
which the workflow verifies on every release.
The released blackbird binary manages its per-user service without requiring
root access:
blackbird install
blackbird status
blackbird update
blackbird uninstallinstall creates XDG config, data, and state directories and one launchd agent
or systemd user unit for the daemon. During upgrades it stops and removes
definitions left by the retired blackbird-claude and blackbird-pi services
while retaining their private databases and transcripts for migration.
OpenCode, Pi, and Claude Code delivery is owned by their native plugin systems.
Installation also installs an unattended Homebrew updater that runs every six
hours: a non-KeepAlive launchd job on macOS, or a systemd user timer and
oneshot service on Linux. Updater failures are retained in Blackbird's state
logs on macOS and the user journal on Linux. The updater never restarts itself;
the daemon restarts only after the installed formula version changes.
The updater is scheduled only when Homebrew is present, because it upgrades the
Homebrew formula and has nothing to do without it. Detection searches the PATH
the updater itself runs with rather than your shell's, since a Homebrew under a
custom prefix is on one and not the other. On a machine without it — a source
build, typically — install schedules no updater and removes one an earlier
install left behind, status and doctor report updater=unsupported rather
than a fault, and update refuses with that reason instead of a missing-brew
error. Nothing else about the installation changes, and such a machine is
updated by whatever installed it.
Installation also adds one blackbird HTTP MCP entry to detected OpenCode,
Claude Code, and Codex configurations while preserving unrelated settings.
When user-managed OpenCode JSONC exists, Blackbird leaves it untouched rather
than creating a competing JSON file. Repeated installs converge the remaining
daemon, updater, and client definitions.
update runs brew update followed by
brew upgrade phall1/tap/blackbird; the service is restarted only when the
installed formula version changes, and it fails before running either command
when Homebrew is absent. status reports both the daemon and updater.
uninstall stops the daemon and updater, cleans legacy adapter definitions, and
retains databases, transcripts, logs, XDG directories, and MCP client settings.
{ "plugins": [ { "package": "blackbird-opencode@0.1.3", "options": { "baseUrl": "http://127.0.0.1:8080", "projectKey": "~/workspace/project", "agentName": "OpenCode", "routing": { "mode": "conversation" } } } ] }