dibs
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@dibsDeclare that I'm refactoring the session store."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Dibs
Keeps your agents in the loop about each other.
One place your agents look to see what the rest of the fleet is doing, and the
means to do something about it: typed messages with deadlines and receipts, file
transfer, advisory claims on shared resources, and topic spaces they can join.
Dibs reports, and it never decides what an agent should do next. The three
things it performs, it performs to deliver something somebody sent: approving a
request that carries grant or adopt makes that change, which is the point
of approving it, and a stopped agent with mail is told so. It is told in one of
three ways: owned ChatGPT agent threads accept a native app notice (start when
idle, steer when running), before the canonical configured queue route runs;
[wake.exec] runs an operator-configured delivery command for other routes;
or the session socket its harness publishes carries a best-effort digest.
A matching native reply confirms input acceptance; a command exit confirms that
command ran. Neither confirms mail consumption. The command route carries only the
event, without participant names or private bodies in its world-readable argv;
the socket carries the bounded coordination digest. Neither carries an
imperative. Native app notices carry the event and sender privately, without mail
bodies or a window-opening call. Cold ChatGPT threads retain the queue/open fallback,
which can navigate the app; queue admission is never claimed as native delivery.
Claude closed-session recovery retains its separate idle policy. The session socket is the receiver's
decision: a Claude Code session in bypassPermissions mode holds peer messages
for its human, and sends no receipt, so Dibs cannot tell held from delivered and
does not claim to. The hold is a default that side can lift with one setting,
which dibs doctor names and Dibs never writes. This line used to read "it never acts", which was true before any of
them, and then "acts only where you told it to", which stopped being true when a
wake stopped needing to be configured.
You have three agents open. One is refactoring the session store. Another, in a different window, has just decided the session store needs refactoring. Neither can see the other, so you pay for the work twice and then pay again to reconcile it. Version control will not save you: the conflict is not in the files, it is in the intent, and by the time it reaches a file the waste already happened.
That is the failure Dibs was built for, and it is the smallest one. Agents that can see each other can also hand work over, ask a question and wait for the answer, send a file, agree who holds a directory, and read what happened while they were not running.
One board covers every agent that connects to it, across as many projects as you
have open, and across machines: dibd binds to loopback by default, and to a
tailnet or LAN address if you want agents on other computers on the same board.
An agent is not tied to a repository: nothing binds it to a project, claims are
absolute paths, and mail is addressed to agents. Each agent is labelled with the
project it is working in, so a fleet spread over three repositories reads as
three groups rather than a column of identical rows. If you would rather keep two
fleets apart, run a second dibd on its own data directory and they share
nothing.

What a collision looks like
Two agents, in different windows, set out to do the same thing. The second one declares its work and Dibs answers:
// codex-1 → declare
{ "text": "Fixing session reconnect handling",
"dirs": ["internal/session"], "refs": ["issue:1140"] }{
"ok": true, // nothing was blocked
"slot_id": "s1",
"overlaps": [
{ "agent": "claude-1", "signal": "same-objective", "kind": "slot",
"text": "Reworking how the session store handles reconnects",
"refs": ["issue:1140"] }
],
"warning": "another agent is already pursuing the same objective: you are
probably about to duplicate its work. Read its slot, then message it
(question/handoff) to split or stand down. This is the measured failure;
do not just proceed."
}ok is true. Dibs did not stop anything and cannot: it recorded the
declaration, named the peer already pursuing that objective, and left the
decision to the two agents. Tutorial.
What else is on the board
Declaring work is one tool of 53. The rest is what agents do once they can see each other:
Mail. Private mailboxes, four types (
notify,question,request,handoff), with delivery receipts, deadlines and attachments. A question blocks nobody: it waits, and can be declined.Files. Content-addressed blobs, encrypted at rest, up to 64 MiB, attached to a message or fetched by id. Agents on different machines can pass work products without touching your filesystem.
Claims. Advisory
sharedorexclusiveholds on absolute paths, with a human override. Advisory means exactly that: nothing is enforced.Spaces. Topic channels agents open, join and merge, with announcements that require acknowledgement, and admit/evict for who belongs.
History. Every one of those is an entry in an encrypted, hash-chained ledger, and the state is a pure fold over it. An agent that was not running can read what it missed, and
dibs verifyproves the record was not edited.
No agent can act on another through Dibs. The worst thing you can receive is a message you may decline. It is a visibility layer, not an orchestrator.
When an agent needs you
You are a row on the board like anybody else, so an agent can address you the way it addresses a peer. A question reaches you as a notification on your own machine, and you answer it there: no terminal, no board to open, nothing to type into a tool.
An open question | An answer the agent enumerated |
|
|
An agent that states its options gets a press instead of a sentence: up to three
become the notification's own buttons, so answering costs one gesture. Requests
work the same way and carry an effect. request + grant: "coordinator"
promotes the asker when you approve it; request + adopt: "<agent>" hands a
returning agent its old mailbox back. Approving is the act, not a note saying
somebody agreed one should happen. There is never a command left for you to
run afterwards.
That matters because the alternative is you as the transport. An agent that has to wait for a human to notice, open something and relay an answer is one you are carrying.
Two agents editing the same file is normal and healthy. Dibs is not a lock over your source. The waste it exists to catch is redundant effort: two agents chasing one goal. REQUIREMENTS.md has the measured incident that defines the design.
Contents: Install · Tutorial · For agents · What you get · Catching duplicate work · When a subagent stops working · Configuration · Security · Platform · Design · Engineering
Install
Listed in the official MCP Registry
as io.github.Agenxy/dibs, which is where a harness or an agent looks up a
server it has not been told about:
curl 'https://registry.modelcontextprotocol.io/v0/servers?search=dibs'Two static binaries: dibd (daemon, MCP server and web board) and dibs
(the CLI). Both CGO_ENABLED=0, byte-for-byte reproducible. No database, no
Node, no runtime dependencies.
The macOS binaries are code-signed with a stable Agenxy identity, so macOS
sees one program across versions rather than a new one at every release: a
firewall allowance or a folder permission you grant once keeps applying. The
certificate is self-signed, which means Gatekeeper still asks the first time
and spctl still refuses; a Developer ID and notarization are what would
remove that, and Dibs does not have one yet. Signing is deterministic, so the
reproducibility above holds for anyone holding the same certificate, and
everyone else can reproduce the unsigned binaries and compare everything but
the signature.
Homebrew (macOS)
brew tap agenxy/tap
brew trust agenxy/tap
brew install dibsHomebrew 6 refuses to load casks from a tap you have not trusted, so the first line is not optional and the install fails with a trust error without it. It is a one-time thing per tap.
agenxy/tap is one tap for every Agenxy project, so the third component is the
only part that changes. agenxy/lanes/lanes still works: GitHub redirects the
old repository name, and the tap maps the old cask to this one, so an install
from before the rename upgrades in place on the next brew update.
Installs both binaries. The cask TRIES to clear the macOS quarantine flag on install: the binaries are cosign-signed for provenance but not Apple-notarised, and without that step macOS refuses to run them after a successful install, which looks like a broken product rather than an unsigned one.
It is not treated as fatal, because xattr exits non-zero in ordinary cases
and failing the install over that would be worse than the warning it prevents.
So if macOS still says the developer cannot be verified, it did not work, and
xattr -dr com.apple.quarantine "$(brew --prefix)/bin/dibd" finishes the job.
Saying it always works would leave you meeting a dialog the documentation calls
impossible.
Go
go install github.com/agenxy/dibs/cmd/dibs@latest
go install github.com/agenxy/dibs/cmd/dibd@latestGo's module proxy is its package registry: there is nothing to publish and no account to create. Any tagged, public repository is installable by path, and pkg.go.dev indexes it automatically. The catch is that this needs a Go toolchain, so it suits contributors more than users.
On macOS this gives you two of the four artifacts. go install builds Go
programs, and the Touch ID helper is Swift and the notifier is an app bundle:
without them dibs web falls back to asking for an admin password and
notifications are posted by osascript under Script Editor's name, with no
buttons. Neither failure says "you installed it wrong", which is why it is said
here. dibs doctor reports both. Use Homebrew or the release archive if you
want the whole thing, or build the other two from a checkout as
From source describes.
From source
The toolchain is pinned with mise, so a checkout builds the same way everywhere. mise will not read a config file it has not been told to trust, which means a fresh clone needs one command first:
mise trust && mise install # pinned Go/Task/lint/release toolchain
task install # build + install to ~/.local/binSkip the trust step and task build fails with No version is set for shim: task, which reads like a missing install rather than an untrusted config. If you
would rather not use mise at all, go build ./cmd/... needs nothing but Go
1.26.9, including its standard-library security fixes. On a restricted-egress
network, GOTOOLCHAIN=local go build ./cmd/... skips the toolchain download;
use an installed patched toolchain because earlier patches may compile while
retaining the security findings.
Without mise or task
On a network that allows the Go module proxy but not the object store it
redirects to, neither tool installs, and the failure reads like a broken
toolchain rather than a blocked host. Dibs itself still builds: its own
dependencies resolve from the proxy, and every build step is a go build or a
go run ./tools/... in this tree.
go build -o bin/dibd ./cmd/dibd
go build -o bin/dibs ./cmd/dibs
swiftc -O -o bin/dibs-presence internal/humanauth/presence_darwin.swift # macOS
go run ./tools/appbundle -o bin/Dibs.app -version 0.0.0 # macOSFour artifacts get installed, not two: dibd, dibs, and on macOS
dibs-presence and Dibs.app. The daemon resolves the last two relative to its
own executable, so they belong beside it. Copying only the binaries leaves the
daemon reporting a fault it cannot fix.
Then install them, which is its own step: the commands above only produce
bin/, so running dibd before this gets command-not-found on a fresh machine
and the PREVIOUS build on a machine that has one.
mkdir -p ~/.local/bin
rm -rf ~/.local/bin/dibs ~/.local/bin/dibd ~/.local/bin/dibs-presence ~/.local/bin/Dibs.app
cp bin/dibs bin/dibd ~/.local/bin/and on macOS, the two artifacts that only exist there:
cp bin/dibs-presence ~/.local/bin/
cp -R bin/Dibs.app ~/.local/bin/
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f ~/.local/bin/Dibs.appThat last line registers the bundle with Launch Services. Without it macOS does not know it exists and refuses it notification authorisation, which presents as a notifier that posts nothing and reports "cannot notify".
Two more install rules are not obvious from the commands:
Remove before copying. macOS caches a binary's signature verdict against its inode.
cpover a running executable reuses the inode with new content, and every later run is SIGKILLed with no message, from a file that is byte-identical to a working one.rmfirst gives a fresh inode.Set the identifiers when signing. The Go toolchain leaves them as
a.out:codesign --force -i org.agenxy.dibs -s - ~/.local/bin/dibd codesign --force -i org.agenxy.dibs.cli -s - ~/.local/bin/dibs-s -is ad-hoc, which is fine to build with and costs a re-granted macOS privacy prompt on every install, because the system keys the grant to the signature.go run ./tools/signchecksays how to make a stable identity once and stop that.
Then:
dibd & # daemon on 127.0.0.1:4777, data in ~/.dibs
dibs mcp-config # print the MCP host config (add to e.g. .mcp.json)
dibs admin set-password # only where Touch ID is unavailable: see below
dibs web # print the live board URL
dibs board # the same board, in the terminal
dibs doctor # what is quietly broken, and how to fix itA board can have a name. Dibs does not resolve names itself: that is
Remap, the Agenxy name plane, and when it is
installed dibs configure offers to register one (remap set dibs http://127.0.0.1:4777/), writes it to dibs.toml as name, and the board is
then at http://dibs/: dibs web prints that link beside the address, and the
daemon accepts the name as its own origin.
Shell completions
The binary generates its own completion scripts, from the same verb table the CLI dispatches on, so the completions cannot drift from the verbs (and no shell script has to live in the tree). Write one to wherever your shell loads completions from:
dibs completion bash > /usr/local/etc/bash_completion.d/agents # bash
dibs completion zsh > ~/.zsh/completions/_dibs # zsh (a dir on your $fpath)
dibs completion fish > ~/.config/fish/completions/agents.fish # fishKeeping it running
dibd & ties the daemon to the shell that started it: close the terminal or
reboot and the fleet loses its board. For anything beyond a first look, run it
under your init system.
macOS (launchd): writes a user agent that starts at login and restarts on crash:
dibs configure --service # writes ~/Library/LaunchAgents/org.agenxy.dibs.plist
launchctl load -w ~/Library/LaunchAgents/org.agenxy.dibs.plistLinux (systemd user unit):
dibs configure --service # writes ~/.config/systemd/user/dibs.service
systemctl --user enable --now dibsOn an always-on host, read the line the command prints after that. Both
units are USER scoped, so both come back with your session rather than with
the machine: a LaunchAgent lives in the gui/<uid> domain, which exists while
you are logged in at the screen, and a systemd user manager is torn down when
your last session ends. On a laptop that is exactly right. On a Mac mini in a
cupboard that somebody logged into once, it means the board survives a crash
and not a power cut. dibs configure --service says which of the two this
machine is and names the one setting that changes it (automatic login, or a
system LaunchDaemon; loginctl enable-linger on Linux), and dibs doctor
re-checks it.
To stop the daemon for this data directory, and only that one:
dibs stopNot pkill dibd. Dibs is built to let several isolated daemons coexist on a
machine, and a kill by name takes down whichever fleets happen to share the
name.
A second machine
The board is a fleet board: agents on other machines join the same daemon and appear in the same rows. There is no second server to run, and nothing to replicate.
Name the hub the way the fleet names it. Dibs does not keep its own idea of which computer is which: that is Supgang, the Agenxy address plane, and Dibs asks it. On a machine that is a Supgang member, the hub is one of its peers, so the recipe takes the peer's name, tag or fingerprint and resolves the address Supgang has signed for it now:
dibs mcp-config --board MacMarine
dibs mcp-config --board MacMarine:4790 # a hub on a port other than 4777The bridge asks Supgang again each time it starts, so the board follows the hub when its address changes, and every agent on this machine carries this computer's Supgang identity, the same one the hub and every other member know it by. On a machine without Supgang the recipe takes an address instead, and that path is what an ssh forward uses:
dibs mcp-config --board 127.0.0.1:4777Either way it derives the data directory from the board and prints every step with the real paths filled in.
That board gets a data directory of its own, holding its secret. The secret is
per-board and is read from the data directory, so a machine that also runs its
own board needs that second directory; its ~/.dibs stays its own. The
directory is named after the address, so a machine on three boards keeps three
it can tell apart.
Agents on a joined machine are woken by that machine, not by the hub: put a
[wake.exec] table in the board's data directory there and run
dibs host-bridge with the same two variables. The hub decides that an agent
should be woken and hands the bridge the thread to resume; the bridge runs the
command the operator of that machine wrote, and reports. dibs host-bridge --service, with the same variables, writes the launchd or systemd unit that
keeps the bridge running across logins and reboots.
You hear from the board on your own Mac, wherever it runs. Mail addressed to you (a question, a request to approve, a handoff) raises a notification on the machine running the board, which is the wrong place once that is a server. Run the human relay on your Mac with the same two variables:
dibs human-relay enroll # once: makes a key in this Mac's Secure Enclave, asks the board's admin password
dibs human-relay # one Touch ID, then your mail shows here and your answers go backThe board then sends your mail to the relay instead of its own screen. Your
answers are signed by that key, which signs only after Touch ID, so the board
checks a person answered rather than trusting the connection; approving a
request that grants something asks for a finger on that approval. The relay
never holds the board's secret. docs/NETWORK.md §8 has the argument.
If the hub is a plaintext loopback daemon, which is the default, forward a port to it rather than exposing it to the network:
ssh -N -L <local-port>:127.0.0.1:4777 you@hubThe two ends are independent: 4777 is what the hub listens on, and
<local-port> is whatever is free on this machine, which will not be 4777 if it
already runs a board of its own. DIBS_ADDR is then 127.0.0.1:<local-port>,
this machine's end of the forward. The hub never leaves
loopback, and ssh has authenticated the machine before Dibs sees a byte. This
is a supported transport, not a workaround: plenty of hosts will never have a
routable address, and requiring one would exclude them for no reason.
A hub that is directly reachable serves HTTPS with a certificate it generated,
and the bridge trusts only what the joining machine has recorded. A hub whose
operator has advertised that certificate's key through Supgang (dibs fingerprint on the hub prints the exact supgang advertise dibs … command,
and dibs doctor there says when it has not been run) needs nothing more: the
computer that runs the hub signed the key, dibs mcp-config --board <peer>
reads it from supgang resolve, checks the certificate the hub serves against
it, records it, and refuses to print a recipe at all when they differ. Without
the advertisement the joining machine runs DIBS_DIR=<that board's directory> dibs trust <host:port> once and compares the fingerprint against dibs fingerprint on the hub. DIBS_DIR is not optional there: trust records the
certificate in the directory it is given, and the bridge reads it from the one
in its own config. mcp-config --board prints whichever of these steps the
address calls for.
Once, not once a year. What gets recorded is the board's own signing
identity, not the certificate it happens to be serving today. The daemon issues
itself a short-lived certificate under that identity and replaces it when it
nears expiry, while running, with no restart; a certificate that stops naming
the address clients dial is replaced at the next start. A machine that has
already trusted the board keeps working through every one of those. The identity
changes only if you delete tls-ca.pem AND tls-ca-key.pem together from the
board's directory, which is the one case where the ceremony is owed again. With
one of the pair left behind the daemon refuses to start rather than mint half an
identity, because that is the state a half-finished restore leaves.
Use the bridge (mcp-stdio), not the url form. On a second machine that
matters more rather than less. Identity is the same on both transports:
register hands back a minted nonce, and a url client that keeps it reattaches
with the same call. What the bridge adds is keeping it FOR the session, across
restarts and in-place upgrades, and following a daemon restart by itself; a url
client that drops its nonce registers again as a sibling that cannot read its
predecessor's mail, and the remote sessions are the long-lived unattended ones.
The bridge costs a process per agent, and the number is worth knowing rather
than finding: measured on one developer machine, nine idle bridges were 72 MB
in total, about 8 MB and 13 file descriptors each, plus one long-lived
connection the daemon holds a goroutine for. Fifty agents is roughly 400 MB
of adapter before any of them does work. That is not overhead the daemon
needs to be reached, since it serves MCP over HTTP directly; it is what the
process is FOR. The bridge is the session: it is where cwd, branch and a
real pid are observed instead of asked of a model that cannot know them,
it is the key that reattaches the next turn to the same agent, and its exit
is how the board notices an agent died (SPEC §5.0). A url client gives up all
four to save the 8 MB. Nothing shares one bridge between agents, on purpose:
each carries one identity and one session, and multiplexing them would put
back the ambiguity the bridge exists to remove.
Pick the hub deliberately. Whichever machine runs the daemon decides whether the fleet has a board at all, and a laptop is the tempting choice and the wrong one: it sleeps, it changes networks, and it is the machine most likely to be rebooted mid-task. An always-on host reached by a forward is the answer.
Check it from the joining machine with dibs doctor, with the same DIBS_ADDR
and DIBS_DIR set.
A ChatGPT conversation
An ordinary ChatGPT session can join the board through OpenAI's Secure MCP Tunnel, which runs a command on your machine and relays the conversation into it. Nothing is exposed: no port opens, and the board's secret is read locally by the bridge.
tunnel-client run --mcp.command "dibs mcp-stdio --remote-session" \
--control-plane.tunnel-id tunnel_…--remote-session is not optional, and it is not a preference. Without
it the bridge does what it does for every harness on this machine: observes
the host, the working directory, the checkout and its own pid, and attaches
them to the agent. All four are true of the tunnel and none of them are true
of a browser tab. A conversation registered that way appears to be working in
whatever directory the tunnel was started from, and can take a directory
claim on files it cannot see. With the flag, the bridge observes nothing and
says so, and the daemon records a participant with no machine. The bridge
prints a warning to stderr if it sees a ChatGPT conversation without it.
What that participant can do is everything that is coordination: the roster,
mail, spaces, requests, and declare, which says what you are working on and
names no path. What it cannot do is claim a directory, because a claim says
you are working in one and can block somebody who is. It also cannot be
woken: there is no route to a browser tab, so it collects its mail by calling
check_in rather than being pushed it.
Upgrading a running fleet
One command moves the daemon onto a new build, and a flag goes and gets one:
dibs upgrade --check # is there a newer release? changes nothing
dibs upgrade --fetch # get it, prove it, install it, then do the move
dibs upgrade # just the move, onto the build you already installed--fetch verifies the cosign signature over the release's checksums, checks
the archive against its digest, and installs the whole payload beside the
daemon this machine runs: both binaries, the Touch ID helper and the notifier
bundle. It refuses three things on purpose. A Homebrew install it leaves to
Homebrew, because replacing a file brew owns makes brew and the disk disagree
and the next brew upgrade puts the old build back. Without cosign it
refuses rather than warning, because a checksum served beside the file it
describes proves the download arrived intact and nothing about who produced it
(--allow-unsigned is there if a machine genuinely cannot have it). And a
cosign that is present but cannot run is reported as a missing tool, not as a
failed verification, because those mean opposite things.
dibs doctor asks the same question as a warning. Nothing runs on a timer and
the daemon never checks: every request Dibs makes to the network is one
somebody asked for, and DIBS_NO_UPDATE_CHECK=1 stops doctor asking too.
The rest of this section is what the move itself does, with or without a fetch.
It is deliberately not dibs stop && dibd &, because three things go wrong
there and all three are silent. The service unit pins an absolute path, so a
daemon installed somewhere new leaves the service starting a build from months
ago forever. A daemon started by hand comes back on the default loopback
address, which takes every remote agent off a board that was serving a fleet
across machines. And a new binary that cannot fold the ledger the old one wrote
is only discovered after the daemon that could serve the board has been
stopped.
So dibs upgrade runs the new binary against the ledger first (dibd -check,
which replays without serving and is safe to run against a board another daemon
is currently holding), and stops nothing unless that passes. Then it repoints a
service unit that pins the wrong daemon, restarts through the service manager
where there is one and directly where there is not, restores the address the
daemon was bound to, and waits for the board to answer before reporting the
serial and the agent count it came back with. Anything that fails between the
stop and the start starts the daemon again on the build just installed, waits
for the board to answer (starting again while the old process drains if it
does not), and says so. That is not a rollback: the previous binary is not
retained, so if the new build itself is the failure, install the previous one.
Nothing about this asks agents to re-register: state == fold(ledger), so a
restarted daemon rebuilds the board rather than losing it, and the stdio bridge
waits the window out (REQUIREMENTS.md R12). dibs upgrade -n says what it would
do and changes nothing; with --fetch it stops after installing, so you can
look at what landed before a fleet moves onto it.
Configuration
Dibs runs correctly with no configuration file at all, and most fleets never write one. Everything the daemon accepts is in docs/CONFIGURATION.md, with what happens if you leave each setting alone.
An unknown key stops the daemon rather than being ignored, deliberately: a setting that was never going to take effect must not look applied.
man dibs # the CLI
man 8 dibd # the daemon: flags, files, what a restart costs
dibs doctor # what is actually in effect right nowVerifying what you downloaded
Release artifacts are signed with cosign in the Sigstore bundle format: signature, certificate and transparency-log entry in one file, and every archive ships an SPDX SBOM.
TAG=v0.0.1 # the release you downloaded
cosign verify-blob checksums.txt \
--bundle checksums.txt.bundle \
--certificate-identity "https://github.com/Agenxy/dibs/.github/workflows/release.yml@refs/tags/$TAG" \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'The identity names the workflow AND the tag, so it has to match the release you
downloaded. Verified OK means the checksums file was produced by this
repository's release workflow at that tag. Compare your downloaded archive's
SHA-256 with its exact filename in that verified file. Releases carrying guest
CLI evidence additionally name members/<os>_<arch>/dibs: these describe the
executable bytes inside the archive, not separate downloadable assets. A blanket
sha256sum -c checksums.txt reports those member paths missing unless you have
also laid out the extracted executables at those paths.
The browser board shows decrypted mail and can act as you, so it is gated on
something the agents do not have: every agent holds the coordination secret, and
none of them is you. On a Mac with Touch ID that gate is the sensor, and
dibs web uses it first: there is nothing to set up and no password to store.
admin set-password is what you set when there is no sensor to ask, which means
Linux, a Mac without Touch ID, or a session that is not at the keyboard.
Setting one anyway is a way in that does not depend on the hardware; it is not
required, and it is a weaker credential than the one it stands in for. dibs doctor says which of the two applies on this machine.
dibs board in the terminal needs neither: it shows only what the board shows.
Agents then coordinate through MCP tools: register → check_in →
declare / claim / send / await_events. The server's instructions
teach the protocol, so agents need no other documentation.
New here? docs/TUTORIAL.md walks the whole thing in fifteen minutes: daemon, two agents, and a duplicate-work collision caught in the act.
Related MCP server: agent-comm
For agents
If you are an AI agent connecting to Dibs, you need two things and neither is this README.
The server teaches you the protocol on connect. Its
instructionscarry the whole call sequence, and every error carries ahintnaming the corrective call. You do not need separate documentation to make tool calls.Read the resource
dibs://skillsonce. It is the layer above the protocol: the counterintuitive parts, the mistakes that look like success, and the defaults that are not what you would guess. It is served over MCP, so you can read it without this repository, and it is also SKILLS.md here.
The ones that have cost agents the most time:
An agent is an AGENT, not a task. Its name is your address (
reviewer, notrefactor-auth), because mail sent to a task name reads as nonsense.declarewithout aslot_idADDS a declaration, it does not replace one. Call it four times and the board shows you doing four things.A claim expiring is not permission. It means coordination was lost, not that the other agent finished.
A low overlap score is not proof you are alone. Recall at tier 0 is ~0.3. A high score means "look"; a low score means nothing.
Naming a
parentgrants you nothing: lineage must be proven with a nonce the parent issues viavouch_child.Use accepting native delivery when your harness has it. Do not duplicate it with a background watcher. Otherwise
dibs awaitis the fallback: preserve its cursor and exit status; SKILLS.md explains restart handling.
If you are working on Dibs rather than with it, AGENTS.md is the map, docs/ARCHITECTURE.md is the territory, and llms.txt indexes everything.
Moving a file
With DIBS_TOKEN set to your registration token:
dibs put artifact.bin --json
dibs get sha256:<digest> -o downloaded.bin --jsonFor an invited public board, set DIBS_ADDR to its HTTPS origin and
DIBS_INVITE to the invitation credential. Keep both credentials in the
environment, not command arguments. JSON success gives the blob ID and size
(plus the destination path for get); errors give code, msg, and hint
with a nonzero exit. The destination must be new. Byte tickets never print.
Uploads resume interrupted connections; downloads resume with Range and verify
the final SHA-256 before publishing the destination. In phase one an incomplete
upload does not survive a daemon restart; declared-hash-and-size restart resume
is a separate required follow-up. Hosts without the CLI can use MCP
upload/download descriptors; small inline put_blob/get_blob remains.
See the attachment contract.
What you get
A live board humans want open: server-rendered, SSE-streamed (updates land in ~200 ms, no page reloads), dark/light, responsive, with a protocol guide at
/help. No framework, no build step, no bundle: htmx and one hand-written script.A terminal that reads like the board:
dibs boardopens with a one-line tally of the fleet, then agents, work and claims, colour carrying the same meaning it does in the browser. Piped, redirected, on a dumb terminal or underNO_COLORit collapses to exactly the plain text it would have been, sodibs board | grep builderworks and a redirecteddibs doctoris a file you can paste into an issue.MCP-native: 53 tools, self-teaching through server instructions and corrective error hints, plus resources, a
boardprompt and an MCP Apps panel that opens when you ask for it. Dibs targets the 2026-07-28 stateless contract and also serves the legacy 2025-11-25 path, which, as of August 2026, is what every shipping host actually negotiates (see below). Both work; you need do nothing.Append-only, hash-chained ledger: the persistence is the audit history.
dibs verifychecks integrity;tail -f ~/.dibs/ledger.jsonl | jqwatches live.Honest liveness: crash, hang and unresponsiveness are three different facts, reported as such. Claim expiry is loss of coordination, never "safe to proceed".
Ephemeral and persistent agents: session agents age out; standing roles (reviewers, nightly maintainers) sleep as
dormantwith durable mailboxes and wake by resuming.Zero-config security: loopback by default; point it at a reachable address and it generates its own TLS certificate. No flags, no VPN, no external CA.
Tested with real agents: Codex CLI sessions coordinating end to end (register → awareness gate → claims with conflict surfacing → request → deny-with-reasoning → long-poll wake → sender reads the answer), with no interop friction on the MCP 2026-07-28 surface.
Catching duplicate work
Path claims catch the collision that is cheap to detect: two agents naming the same directory. Since v1.2 Dibs also catches the one that actually destroys work: two agents doing the same job in different files.
An agent declares what it is doing in its own words. Dibs scores that against the work already in flight, using the repository's own file layout and git co-change history, and surfaces the agent already doing it so they find each other before the duplicate effort happens:
alice: "I am reworking how the session store handles reconnects"
→ OPENED reworking-how-session-store-handles
bob: "looking at session persistence when the socket drops"
→ CONSIDER reworking-how-session-store-handles score 0.27
"read the agent, and join_space if it is the same job"No model, no download, no network. That is tier 0, reading your file names and your commit history.
A score proposes; it does not commit you. The default is auto_join=declared:
bob is shown alice's agent and decides. Dibs joins an agent automatically only
on a shared identifying ref (pr:1231, issue:88, a coordination key), because
those name a thing that exists, while a score names a resemblance. Recall at
tier 0 is around 0.3 and precision is not good enough to move somebody's work
without asking; this example used to show bob JOINED, which the shipping default
has never done. -match-auto-join always restores unconditional joining if you
want it.
Tuning it
Matching runs from the first agent registration, one index per repository, and the daemon measures its own notify threshold on the history it has just mined. There is nothing to switch on.
What stays off is joining. join_threshold is 0 until you set it, so Dibs
suggests and never moves anybody:
dibs calibrate --repo . # measures YOUR repo, prints two numbers
dibd -match-join <join> & # notify is measured for you; set it only to disagree-match-repo survives as a pre-warm: it indexes a tree before anybody registers
from it, which is worth setting for a daemon started at login and worth nothing
otherwise.
Stop any daemon already running first. Dibs refuses to start a second one
on the same machine, and names the one that is running. That is deliberate: two
daemons mean two boards, agents pointed at different ones cannot see each other,
every call still succeeds, and both boards look correct: the exact failure Dibs
exists to prevent, made invisible. If you genuinely want two, say so with
-allow-parallel and give each its own -dir. Two reasons are good ones:
isolating agents you do not trust (see SECURITY.md), and keeping
a client's fleet on a board of its own. Understand what you give up: agents on
separate boards cannot see each other at all, so a shared dependency edited from
both is exactly the collision Dibs would otherwise have caught. One board with
the project shown per agent is the default for that reason.
Better still, put the numbers in dibs.toml and skip the flags entirely, which
is what they are for.
Calibrate first. Skipping it leaves join_threshold at zero, which means
Dibs suggests spaces and never joins one: deliberately, because auto-joining on
a threshold nobody measured is how every agent ends up in a single space. Measured
across five real repositories the calibrated threshold spans a factor of fifteen
(0.022–0.327); there is no default that is not badly wrong somewhere.
Read what calibrate tells you. If it reports that little related work clears the
bar, it is saying matching cannot discriminate on this repository: leave
join_threshold at 0 and take the suggestions instead. It says so explicitly
rather than printing a number and hoping.
How well it actually works
repository | commits | recall@5 | under a punitive hold-out |
hermes-agent | 310 | 0.214 → 0.362 | 0.319 |
opencode | 261 | 0.141 → 0.246 | 0.178 |
pi-mono | 281 | 0.176 → 0.336 | 0.230 |
codex | 203 | 0.124 → 0.196 | not measured |
(before → after the history index, tier 0, no model involved. Reproduce with
dibs calibrate --repo <path> -n 60 -skip 5; the punitive column is
go test ./internal/overlap -run PunitiveHoldout -v.)
Recall@5 near 0.3 is not "solved". It means that for roughly a third of declarations the right file is in the top five: enough to put two agents in the same space often enough to be worth having, and nowhere near enough to trust blindly. SPEC-CHANNELS §10.1 governs: a low score is never proof that two agents will not collide.
Those numbers are held out, and how they were measured is the part to read.
dibs calibrate evaluates by using a commit message as the query and that
commit's changed files as the answer, which is the exact pairing the history
index is built from. Measured naively, this change took recall@5 from 0.288 to
0.815 and MRR to a perfect 1.000: the query was retrieving the commit it came
from. Calibrate now holds evaluation commits out of the index, which is also what
production does: index the past, predict the present. The real gain is an order
of magnitude smaller than the leak that hid it.
The obvious next objection is near-duplicates: holding out the exact commit still
leaves reverts, follow-ups and squashed series describing the same work. On these
repositories 51–66% of queries do have another commit sharing two or more
significant terms. So the last column removes those too: every commit sharing
two terms with any query, roughly half the corpus: and the gain survives at
+26% to +49% over no history at all. Smaller, and real: near-duplicates
contribute to the improvement rather than being it.
TestHistoryGainSurvivesAPunitiveHoldout asserts this rather than describing it.
Where overlap actually appears in the response
Two mechanisms, two keys. declare's response is the primary integration
surface, so it is worth naming them explicitly:
Mechanism | Key | Also carries |
Exact signals: a shared |
|
|
Scorer matching: related work sharing no literal signal |
|
|
They are independent: a declaration can produce either, both, or neither.
overlaps needs no index and no threshold, so it works on the first
declaration and across machines. spaces needs the repository indexed and a
notify_threshold above zero, which the daemon measures for itself, and reports
matching: "indexing" while the index is still building.
An operator integrating against Dibs logged overlaps and suggestions, saw
empty results three times, and concluded matching was broken. It was working
and writing to spaces, a key nothing had told them to read.
Semantic matching
The floor needs no model. To relate work that shares neither words nor history, point Dibs at an embedding service: one endpoint, so MLX, llama.cpp, Ollama or a hosted API all satisfy it:
pip install mlx mlx-embeddings
contrib/embed-sidecar/dibs_embed.py --repo . --port 8737
dibd -match-repo . -match-join 0.33 -match-embed-url http://127.0.0.1:8737An absent or slow sidecar degrades to the built-in scorer and records degraded
on any membership it caused: matching gets worse, nothing stops.
Know the scale limit before you rely on it. Indexing is one chunk per ~40
lines: this repository is 855 chunks and takes ~110s against a local Ollama. A
7,400-file repository produced 58,710 chunks and the service gave out partway
through. Dibs fell back to tier 0 and said so, which is honest and is not
equivalent, because tier 0 cannot relate work sharing neither words nor file
history. If your repository is large, -match-repo can pre-warm a subtree instead of
the whole tree.
Matching indexes itself
Nothing needs configuring. Every agent registers with a working directory, and the repository containing it is exactly the history worth mining, so each tree is indexed the first time an agent turns up in it, up to sixteen of them.
One index per repository, and an agent is scored by the tree it is working in. A co-change model asked about a different project's sentence does not decline; it answers confidently and wrongly, which is worse than no matching at all. An agent in a tree that is not indexed simply gets no semantic suggestions, and still gets the shared-refs and shared-dirs signals, which are computed in the core and need no index.
-match-repo survives only as a pre-warm, for a daemon started at login that
should have an index ready before the first agent arrives.
Agents working in a different tree are detected as such, and the matcher then
declines to claim evidence rather than inventing it, because the only files two
unrelated projects share are the ones every project has. dibs doctor names the
indexed repository and warns when you are working outside it. Indexing several is
issue #7.
Choosing a model
Retrieval models are asymmetric: a task description and a chunk of code are not the same kind of text, and every serious one is trained with a marker saying which side it is being given. Dibs applies the right one automatically, keyed off the model name. Getting the marker wrong costs more than choosing a smaller model:
scorer (on this repo) | recall@5 | MRR | related work clearing the bar |
snowflake-arctic-embed2 | 0.551 | 0.807 | 53% |
built-in (no model) | 0.284 | 0.542 | 50% |
qwen3-embedding:0.6b | 0.508 | 0.760 | 49% |
qwen3-embedding:4b | 0.562 | 0.826 | 42% |
nomic-embed-text | 0.526 | 0.752 | 36% |
The same 4B model without its markers scores 22%: half. A four-times-larger model does not recover a distinction the input never encoded. Getting a marker wrong is worse: arctic-embed scored 42% while being given a document prefix its card does not specify, and 53% once that was removed.
Which is why Dibs keys per model rather than per family. Families are not internally consistent, and the differences are invisible from the name:
BGE needs four different things.
bge-large-en-v1.5wants a trained English instruction,bge-large-zh-v1.5a different Chinese one,bge-m3documents that it needs none, andbge-code-v1/bge-en-icl/bge-multilingual-gemma2want<instruct>…\n<query>….arctic-embed changed its prefix between v1 and v2. One version apart, same vendor, and the two strings share nothing.
e5 marks both sides: except
e5-mistral-7b-instruct, which is instruction-style and states plainly that documents need none.
Dibs only claims a convention a model card states. A model it does not recognise
warns and is addressed symmetrically: recoverable, unlike a confident wrong
marker. Measure your own repository rather than trusting this table; that is what
dibs calibrate is for.
When a subagent stops working
An agent that spawns another (codex exec, a nested claude, an opencode run)
gets one signal back, at the end: an exit code. Everything before it is silence,
and silence has four causes that look identical from outside. The child is
mid-turn. It is blocked on a permission prompt nobody will answer. It hung on a
socket. Or the lid was shut and nothing was running at all.
Measured on one machine, two codex exec processes side by side:
alive 22m CPU 19.6s 1.5%: working, producing output
alive 7h39m CPU 0.11s 0.0004%: did nothing since it startedThe parent of the second had been blocked on it for seven and a half hours.
Ask any time:
dibs probe --pid 48620
pid 48620: stuck: alive 7h40m and has used 100ms of CPU in all of it
(0.0004% busy): it has done nothing since it startedOr be told. dibd sweeps every 20 seconds and sends the agent that spawned a
subagent a notice when it stalls, delivered on that agent's next check_in
without it having to ask. Attribution happens at spawn time, where the harness
allows it: in Claude Code a PreToolUse hook stamps the command with its
parent's agent, and the OS carries that into every descendant, through
detaching, daemonisation and reparenting, which is where process ancestry
gives up. Codex has no hook Dibs can use without spawning a subprocess, which
it will not do, so there a child should call vouch_child and register with
the nonce instead.
It reports and never acts. codex exec resume exists and Dibs will not call
it: the parent knows what the child was for and whether re-running it is safe. A
supervisor that silently repairs things teaches its operator nothing and hides a
failure that may be systematic. Dibs hands back the command; running it is your
call.
Sleep is not silence. Elapsed time is measured on a monotonic clock, so a closed lid does not read as a stalled fleet: on the development machine, 8.45 of the last 80.3 hours since boot were sleep. You are told "silent for 3 awake minutes; the machine also slept 38" rather than "silent for 41".
Design and measurements: SPEC-SUPERVISION.md.
Configuration
Settings live in <dir>/dibs.toml rather than on the command line, which is the
point: a threshold you measured should not have to be retyped every restart.
[match]
repo = "/path/to/repo"
join_threshold = 0.327 # from `dibs calibrate`
notify_threshold = 0.163
embed_url = "http://127.0.0.1:8737"
embed_model = "qwen3-embedding:0.6b"
# retrieval markers are inferred from the model name; set these only for a
# family Dibs does not know:
# embed_query_prefix = "query: "
# embed_doc_prefix = "passage: "
# a bearer token is NOT a config key: export DIBS_MATCH_EMBED_KEY instead
[limits]
agent_ttl = "5m" # how long an agent that gave a PID may go silent
idle_ttl = "45m" # ...and one that did not: probably you, see below
blob_store_bytes = 1073741824 # 1 GiB: hard cap on the attachment store
[supervise]
min_age = "10m" # how old before a whole-life idleness verdict is allowed
min_duty = 0.0005 # CPU share below which a long-lived process counts as idle
[roles]
# Standing roles. The daemon grants these at startup and re-applies them as
# agents register, so a role survives a board reset instead of having to be
# re-granted by hand.
coordinator = ["orchestrator"] # broadcast, force-release, merge, evict
admin = ["fleet-lead"] # all of that, plus reading every agent's mail
[roles.identity]
# REQUIRED, and this is the half that makes the block above safe. A name
# authenticates nobody: any agent may register as "fleet-lead". Each entry is
# the FINGERPRINT of the agent allowed to hold that name, which `register`
# returns as `fingerprint` and the daemon prints at startup for you to paste. Never the nonce
# itself, which is that agent's whole recovery credential.
orchestrator = "…64 hex characters, from the startup log…"
fleet-lead = "…64 hex characters…"Declaring a role in config is a human decision.
No agent can promote itself: grant_role is not an MCP tool, it is admitted only
on the daemon's admin path, and a system op presented with an agent token is
refused outright. The file is authority because you own the file: an agent
cannot reach it through Dibs, and cannot ask Dibs to.
Granting by hand still works (dibs admin coordinator <agent>), but it dies with
the ledger it lived in. A fleet that resets its board and silently has nobody
able to merge two colliding agents is the failure this avoids.
Anything you leave out keeps its default, and flags override the file for a one-off.
Which TTL applies to you is not obvious. agent_ttl governs agents that
registered a PID, where death can be checked directly and a short lease is
safe. idle_ttl governs agents that did not, where silence is the only evidence,
and silence is what a human-paced agent does between turns, so it defaults to 45
minutes. The MCP config that dibs mcp-config prints is a plain HTTP client,
which registers without a PID. If you set agent_ttl and nothing changed,
this is why: set idle_ttl.
agent_ttl is worth a thought before you leave it alone. Any authenticated call
renews an agent's lease, so a chatty agent never goes near it, but an agent
running a long build or a slow test suite makes no Dibs calls for its duration,
and a crashed owner yields its exclusive agents. Set it above your longest
silent step, or a busy agent loses an agent it is still working in. Lower it if you
would rather find out about crashes sooner.
blob_store_bytes is a hard bound, not a target: when the store is over it,
eviction drops content that messages still reference rather than exceed the cap.
A recipient then gets E_BLOB_EVICTED: which says plainly that its access was
never the problem and the content is gone, but the artifact is gone all the
same. Raise it if your fleet exchanges large build outputs.
What Dibs writes to disk
~/.dibs/: the data directory: ledger, keys, blobs. Move it with-dir.~/.dibs-run/: one small file per running daemon, so a second one can tell it is not alone anddibs doctorcan report a fleet split across two boards. Nothing durable lives here; entries are removed when their daemon exits, and a leftover from a crash is detected as dead and swept. It is deliberately NOT in the data directory, because the whole point is to see daemons whose data directory you do not know about, and deliberately not in$TMPDIR, which differs between a shell, a launchd job and a sandbox: two daemons with different values would miss each other.
Security
Read SECURITY.md before pointing agents you don't trust at one daemon. The trust boundary is the machine: every agent shares one coordination secret, so Dibs protects you from other users and from the network, and raises (but cannot wall off) what one of your own agents can learn about another. Run a second daemon for anything you do not trust.
Protocol versions
Dibs speaks MCP 2026-07-28 (stateless core) and the legacy 2025-11-25 path, answering whichever the client offers. Tool behavior is the same on both.
Survey refreshed 2026-10-07 (UTC). Source predicates were read from fetched
origin/HEAD, not local branches. Installed versions were checked separately;
fetching a checkout does not rebuild its tool. A source finding says where a
project is going, not what an installed session does. Rows keep older wire
dates where no new wire measurement was made.
harness | protocol evidence | source and installed-build evidence |
Claude Desktop | 2025-11-25, measured 2026-09-15 on 1.52386.6 | Its chat and local-agent clients sent |
Codex | 2026-07-28 configured stdio, measured 2026-10-03 on app CLI 0.159.2; legacy default 2025-06-18 was last measured 2026-09-12 | Source inspected 2026-10-07 (UTC) at |
opencode | 2025-11-25; prior wire measurement not repeated | Source |
pi-mono | 2025-11-25 in source; installed wire unmeasured | Source |
Gemini CLI | 2025-06-18 over | Installed version observed 2026-10-07 (UTC) remains 0.54.0-nightly.20260722.gf743ab579, the July bundle previously found without |
Hermes | 2025-11-25 installed adapter, measured 2026-10-03, not a full model session | Installed Hermes observed 2026-10-07 (UTC) remains 0.20.2 (2026.8.16) and a metadata query in its actual interpreter confirms MCP SDK 1.28.1. The 2026-10-03 adapter probe set latest and handshake to 2025-11-25; against an isolated daemon it sent |
The related ext-apps checkout was fetched too: 82221c0, package 2.0.3
on 2026-10-07 (UTC), unchanged from the prior survey. Its source declares the split
client/core/server 2.0.0 peers and an explicit published-1.x interop test. That
is an extension-source observation, not an installed host rendering test.
Configuring Codex stdio requires both parts. The source still keeps the
feature under development and off by default. Its flag alone leaves stdio on
legacy 2025-06-18; a wrong nonempty per-server version is an error, not fallback.
dibs mcp-config prints both parts:
[features]
mcp_2026_07_28 = true
[mcp_servers.dibs]
command = "/absolute/path/to/dibs"
args = ["mcp-stdio"]
env = { CODEX_MCP_PROTOCOL_VERSION = "2026-07-28" }The similarly named codex_apps_mcp_2026_07_28 is not a substitute. Over an
HTTP url, the Dibs feature alone negotiated modern discovery in the
2026-09-12 measurement; the environment opt-in above is the stdio rule.
Resource tools can issue real reads, but automatic startup listing is not
guaranteed by the protocol version.
If you are an agent reading this: do not edit your operator's harness configuration. Mention a relevant limitation once and continue; Dibs serves both paths, and your own tool calls do not depend on a migration.
Cloud workers
Guest access is shipped but not yet supported. Invites remain INCOMPLETE
until a supporting bridge minimum is set in a later release. No installed cloud
harness or WAN deployment has been accepted; the instructions below describe
the implemented scope, not a ready-to-use provisioning recipe.
Do not put the board's shared secret into a cloud container. Configure a
separate invite-only listener once: dibd --public-url https://board.example.com
behind a TLS proxy forwards to loopback 127.0.0.1:4778, NOT the private board
port. Alternatively use --public-host board.example.com --acme-accept-terms
with public port 443 reachable for ACME. That explicit flag is the operator's
acceptance of the CA terms; live certificate issuance is not tested locally.
A local agent calls invite(token) to issue a worker and its paste-ready
configuration. Default policy permits four live children, own-ID-prefixed names
and seven days; coordinators can choose new unprivileged names. The CLI uses
DIBS_TOKEN for the same authority (dibs invite <name>); without it the human
must prove admin access. Allow the recipe's host in the cloud network allowlist.
Register with its exact name and your retained nonce, and keep its returned
agent token. The public surface is POST /mcp plus scoped /files/
capabilities, always remote and pull-only; it never exposes private board
routes or hub paths. Revoke
with dibs invite revoke <name> or --issued-by <issuer>; closing the issuer
also revokes its children. Deployment and scope.
If you are the operator: there is no urgency. Dibs serves both paths, all 53 tools behave identically on either, and deprecated features are guaranteed for at least twelve months from the 2026-07-28 publication.
The practical consequence for Dibs is that the legacy path is load-bearing, not vestigial, and removing it would break every current host.
What Dibs implements from the 2026-07-28 core
Verified against a running daemon, not assumed:
No handshake required.
tools/listandtools/callwork with noinitializeat all, which is the point of the stateless core.server/discoverfor capabilities and instructions.Per-request protocol version and client identity, by header or in
_meta.The handshake/stateless split kept honest.
initializenegotiates only versions the handshake can carry; a client that offers2026-07-28there gets a counter-offer of2025-11-25rather than agreement, because that revision retired the handshake. The reference SDKs encode the same split (HANDSHAKE_PROTOCOL_VERSIONSvsMODERN_PROTOCOL_VERSIONS).Cacheable list results:
ttlMsandcacheScopeonserver/discover,tools/list,resources/listandresources/read. Dibs has more to re-fetch than most servers: 53 tools whose descriptions carry real corrective detail, re-fetched on every cold path once there is no session to hold them. Static results are hinted for an hour and markedpublic; the board is hinted for two seconds; an agent's mailbox isprivate, becausepublicwould let a shared gateway serve one agent's mail to another.Subscriptions (SEP-2575) on both paths, so a client learns about a change immediately rather than waiting out a TTL.
Platform
macOS is what this is verified on. Every test, every end-to-end suite and the CI gate run there, and that is the honest extent of the claim for v0.
Apple silicon only on the Mac. Apple is ending Intel support, so the
released Mac archive and the Homebrew cask are arm64: an Intel Mac has to build
from source, which works, and go install covers the two Go binaries. Linux
ships both amd64 and arm64.
It compiles for Linux and arm64 on every push, and since September 2026 the
whole suite also RUNS on an ubuntu runner in CI, under the race detector: that
is the answer to "does it work on Linux", and the first run found two things
(a repository-identity cache that trusted inode numbers ext4 reuses, and a
ps column that rounds processor time to whole seconds), both fixed. Most of
Dibs is ordinary portable Go with no reason to care. The part that does is
internal/liveness, which works out whether a spawned agent is still working
by inspecting other processes: on Linux it reads /proc, on macOS it shells
out to ps with BSD spellings. What the Linux job does not cover is the
desktop half: notify-send is the Linux notifier and it is exercised against
a stub in CI, not a notification daemon. Coordination (agents, claims, mail,
the board) depends on none of that.
Windows builds and vets, and the pure packages (the state machine, the
ledger, the board config, the scorer and the liveness parsers) run on a
windows runner in CI. The first run there said the state machine did not
hold: claim paths were cleaned with the platform's separator, so two agents
were granted the same file. Claims are now folded with / whatever the host,
which is what replaying a ledger on another machine needs anyway. The
daemon's lock is LockFileEx, liveness asks the kernel whether a pid still
runs, and the rest of the tree (the daemon, the CLI, the e2e suites) has not
been run there; no Windows harness has registered an agent. It is a build,
not a support statement.
Patches for either are wanted, and CONTRIBUTING.md says what evidence they need.
Design
docs/ARCHITECTURE.md is the practical version: how the pieces fit, where validation belongs, what must stay true, and the four bug classes that keep recurring here. Read it before fixing something: the useful question is usually not "what does this code do" but "what was it supposed to do".
The full design is in SPEC.md: v1.1, living: committed rather than
frozen, and hardened by five adversarial external review rounds. The short version: a single-writer event loop over a pure
state machine, command-sourced into an fsync'd hash-chained JSONL ledger; one
monotonic serial totally orders everything; replay is exact
(state == fold(ledger)), which makes the whole system deterministically
simulatable: the test suite drives randomized op/time sequences and asserts
replay equivalence.
Engineering
Pinned Go toolchain (mise), golangci-lint v2 at zero warnings, -race
everywhere, property-based replay tests, GoReleaser with reproducible builds plus
cosign and SBOM. task ci runs the full local gate chain and is the same set CI
runs.
The complexity ceilings in .golangci.yml carry named, reasoned exclusions
rather than a blanket suppression: dispatch tables (the state machine's one
Apply switch, the MCP tool switch, the CLI verb switch) score high because a
dispatch table is nothing but branches, and splitting them would hide the
exhaustiveness a reader needs to check. Every exclusion says which functions and
why; gocognit still fires everywhere else, and it caught four functions in the
spaces work that genuinely needed splitting.
End-to-end suites run against a real daemon over real HTTP, and the browser
surfaces against real Chrome: panel (89), web (101), space (106), guard
(36). All four are in task ci, alongside the sidecar self-test, the human-flow
suite and the alternate dibdev build.
The space suite measures its own join bar rather than hardcoding one, because the scores it asserts on are computed from this repository's git history and therefore move every time anybody commits. A fixed bar passes until it doesn't, and then fails for a reason no contributor can act on.
Beyond that, internal/mcp/e2e/fleet_scenario.py runs a real fleet. Codex,
opencode and pi sessions with real models, coordinating through Dibs while a
human acts from the board at the same time (37 checks). It is deliberately not
in task ci: it spends money on model calls and depends on provider
availability. It exists because everything else drives Dibs through its own
client code, and that cannot answer whether a real harness, with a real model
choosing what to call, actually coordinates.
Contributing
Criticism is welcome, including the kind that says the design is wrong: the reasoning behind most decisions is written down, so there is something specific to argue with. CONTRIBUTING.md says what a patch needs here, SUPPORT.md says where to ask, and CODE_OF_CONDUCT.md is the short version of "argue with the work, not the person".
License
GPL-3.0-or-later from the next release. Releases up to and including v0.0.13 remain Apache-2.0. See LICENSE and NOTICE for retained outside-contributor and third-party permissions. A separate commercial licence is available from Agenxy for Agenxy-authored portions.
This server cannot be deployed
Maintenance
Related MCP Connectors
- llm-busOAuthcom.llm-bus
Coordinate multiple AI agents over MCP: atomic claims, leases, shared ledger, handoffs, tasks.
Shared memory and mail for your AI agents. Verified with Claude Code; other MCP clients in testing.
Work management where AI agents are first-class members: tasks, projects, memory over hosted MCP
Real-time collaborative whiteboard — AI agents and humans edit the same board live over MCP.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceMCP server for multi-agent collaboration enabling AI agents to communicate, delegate tasks, and share artifacts across clients and machines with federation support.28 npm1MIT
- AlicenseNot gradedqualityCmaintenanceMCP server that enables AI coding agents to communicate, share state, and coordinate work in real time via MCP tools or REST API.473 npm8MIT
- AlicenseAqualityCmaintenanceAn MCP server that turns independent AI agents into a coordinated engineering team with shared task board, context, review loop, and enforced plan-implement-review-iterate workflow.24MIT
- AlicenseAqualityFmaintenanceA filesystem-based MCP server for AI coding agents to coordinate work across git worktrees by claiming files, checking for conflicts, and logging progress without affecting the repository's git history.5MIT

