Two agents were emailing each other through their humans. So we removed ourselves as the bottleneck. What followed was a week of multi-agent autonomous development that changed how I think about software teams.
Last week, a colleague and I were co-authoring an academic paper on document extraction techniques. We’re both heavy Claude Code users — not co-pilot style, but full agent delegation. My agent runs experiments, analyzes results, drafts sections. His does the same.
The workflow was: my agent would draft a section and output it. I’d copy-paste it into an email. Send it to Andrei. He’d paste it into his agent’s context. His agent would revise, output a new version. He’d copy-paste it into a reply. I’d paste it back into my agent.
Reading the emails, it was obvious. The writing was clearly AI-generated — on both sides. We were two humans acting as copy-paste relays between two AIs. The emails were a serialization format for inter-agent communication, and we were the transport layer.
This is a terrible architecture. We were the bottleneck. Every exchange waited for a human to context-switch from whatever they were doing, open Gmail, copy text, paste it, and hit send. The agents could produce a draft in 30 seconds. The human relay added 20 minutes of latency.
I had what I thought was a stupid idea: what if we just… removed ourselves?
The question was simple. Is there an agent communication tool? Something where my agent could send a structured message directly to Andrei’s agent, without either human touching it?
I searched. There wasn’t much. A few theoretical proposals. One Rust-based relay with 700 GitHub stars and no users. Nothing you could install and use in five minutes.
So I told my agent to build one.
The system I asked for was deliberately simple. A message relay. Agent A sends a structured message to Agent B. Messages have types (question, decision, handoff, review, update, ack), delivery states (pending → delivered → processed → replied), and thread IDs for conversation continuity. Nothing more.
The stack:
trunk_send, trunk_inbox, trunk_reply — native to its tool-calling interface.Four hours from “build this” to a deployed relay with MCP integration, agent registration, pairing codes, and message delivery.
Two more hours for real-time WebSocket push. One hour for the Cloudflare Worker that brokers the connections.
By midnight, two agents were exchanging structured messages without any human involvement.
Agents discover each other through pairing codes — 8-character alphanumeric strings, like a phone number for AI. Share your code with another agent (or its human), they call trunk_pair with it, and both sides can now exchange messages.
Agent A: trunk_register → gets secret + pairing code "U4Z6AE54"
Agent B: trunk_pair(code: "U4Z6AE54") → paired
Agent A: trunk_send(to: B, type: "question", content: "Should we use TypeScript or Rust for the CLI?")
Agent B: trunk_inbox → sees the question
Agent B: trunk_reply(message_id: ..., type: "decision", content: "TypeScript — we need MCP SDK support")
Messages carry structured metadata beyond the content: urgency (sync/async), finality (proposed/decided/fyi), context (background the recipient might need). This isn’t chat. It’s a typed communication protocol designed for machines that process structured data better than prose.
Every send requires an idempotency key. Agents retry. Networks fail. Without idempotency, you get duplicate messages, duplicate task claims, duplicate work. The relay deduplicates on the idempotency key — same key, same sender means “this is a retry, return the original response.”
Here’s where it gets strange.
Andrei connected his agent (Vesper) to the relay. We created a shared workspace. And then the agents started using Trunk to coordinate building Trunk.
Vesper sent my agent a message: “I’ll take the message lifecycle, idempotency, rate limiting, and reply linkage. You take the dashboard and bridge adapters.”
My agent acknowledged. They split the work. Each agent would claim files, announce what they were editing, and broadcast when they pushed. When Vesper accidentally deleted a file my agent was working on, they resolved the merge conflict through a Trunk message exchange in under two minutes.
Over two days of bootstrapped development:
They negotiated the task split through structured messages. They used handoff type messages to transfer ownership. ack messages to confirm receipt. update messages to broadcast progress. The protocol they were building was the protocol they were using.
This is, as far as I know, the first instance of AI agents using an agent communication protocol to build that protocol.
Point-to-point messaging between pairs isn’t enough when you have more than two agents. We added rooms — project-scoped spaces where multiple agents coordinate around a shared task board.
A room is attached to a repository via a .trunk config file committed to the repo root:
{
"project": "Superkey",
"room_id": "85256ec5-4095-4283-95f5-8cc70c458d3b",
"join_code": "3ZGXPD2Q"
}
Agents join the room with the join code and immediately see the shared task list. Tasks have the fields you’d expect — title, description, status, priority, owner — plus fields designed for multi-agent coordination:
group: Module grouping. “auth”, “payments”, “security”. Agents can filter by group to stay in their lane.depends_on: Array of task IDs. A task with unmet dependencies is automatically blocked. When a dependency completes, downstream tasks auto-unblock to open.sequence: Ordering within a group for prioritization.The dependency resolution is server-side. When an agent marks a task done, the relay checks every task in the same scope that lists it as a dependency. If all dependencies are now met, the blocked task transitions to open. An agent polling for available work immediately sees it.
This means you can express complex project plans: “build the auth module first, then payments (which depends on auth), then billing (which depends on both).” Seed the tasks with dependencies, and agents will execute them in the correct order without being told the order explicitly.
Agents working on the same project need shared state beyond tasks. Trunk provides two mechanisms:
Shared facts — key-value pairs scoped to a room. Version-controlled. An agent writes deployment_url = "https://staging.example.com" and every other agent in the room can read it. Useful for configuration, decisions, environment state.
Shared documents — versioned markdown blobs. Meeting notes, design decisions, runbooks. Agents can read the current version and see the edit history.
Both are designed for the context constraint that dominates LLM agent design: every byte the agent reads is context budget it can’t spend on work. Shared facts are tiny (key-value lookups). Shared documents are opt-in (agent reads them only when relevant). Neither floods the agent’s context the way a message inbox does.
Above rooms sits the workspace — a team-level container. Agents in a workspace can message each other without explicit pairing. This matters when you scale beyond 2-3 agents. You don’t want to manually pair every combination.
A workspace has a join code. An agent joins once and can immediately communicate with every other member. Fan-out messaging lets an agent broadcast to the entire workspace: “I just deployed v2.3” or “the staging database is down.”
Not every participant in a conversation is an AI agent. Humans use email, Slack, Intercom. Bridge adapters translate between human communication channels and Trunk’s structured message format.
The email bridge receives inbound mail via webhook, parses the sender and thread, and injects the message into Trunk. When a Trunk agent replies, the bridge sends the response as an email. The human sees a normal email thread. The agent sees structured Trunk messages. Neither knows the other is using a different interface.
Every outbound bridge message includes a machine-readable agent hint — a hidden div with the sender’s Trunk pairing code and a nudge: “This agent uses Trunk for coordination. Connect directly for faster communication.” It’s the viral loop: the bridge is trying to make itself obsolete by converting every human↔agent email thread into a native Trunk connection.
Having a communication relay is one thing. Running multiple agents simultaneously is another.
I built a harness — a process manager that spawns Claude Code instances from a config file. Each agent gets its own tab in a zellij terminal multiplexer session. The harness handles spawning, respawning (with a 30-second delay on exit), credit error detection (stops respawning if the account runs dry), and lifecycle management.
trunk harness start # spawns all agents from ~/.trunk/agents.json
trunk harness attach # opens the zellij session to watch them
trunk harness stop-all # kills everything
The first version used claude -p (pipe mode) with --bare — fast startup (~1 second), but it routes through the API, burning credits. Running 7 agents on API credits would cost hundreds of dollars per day.
The fix: drop --bare, unset ANTHROPIC_API_KEY in the wrapper script. Claude falls back to OAuth authentication, which uses the Max subscription — flat rate. Startup is ~7 seconds instead of ~1, but the agents run in parallel so it doesn’t matter.
Running 7 autonomous agents costs $200/month on a Max subscription. That’s the entire team.
An annoying lesson: running claude -p piped through tee (to capture logs) caused full stdout buffering. The agent appeared to hang for minutes — it was producing output, but the buffer wasn’t flushing because tee converts the TTY to a pipe. Line buffering becomes full buffering. The agent looks dead.
Fix: never pipe LLM agent stdout. Let it go directly to the terminal. The zellij pane is the log.
Multiple agents sharing one git checkout is a disaster. One agent’s uncommitted changes break another’s build. Branches collide. Half-written files get read by the wrong agent.
Each agent gets its own git worktree:
~/dev/superkey/superkey # main checkout (planner reads from here)
~/dev/superkey/worktrees/sk-build/ # builder's isolated working tree
~/dev/superkey/worktrees/sk-qa/ # QA's isolated working tree
~/dev/superkey/worktrees/sk-docs/ # docs agent's working tree
~/dev/superkey/worktrees/sk-review/ # reviewer's working tree
~/dev/superkey/worktrees/sk-merge/ # merge agent's working tree
Worktrees share the same .git directory, so they’re lightweight. But each has an independent working tree and can be on a different branch simultaneously. The builder creates a feature branch, does its work, pushes a PR. Meanwhile the QA agent runs tests on main. No collision.
The pattern on each loop: fetch latest main → hard reset → create feature branch → work → push → PR. The worktree is disposable. The PR is the durable artifact.
Here’s what’s running on my machine right now:
| Agent | Role | Cadence |
|---|---|---|
| Planner | Reads specs, decomposes into tasks, assigns priorities | Runs once, creates many tasks, idles |
| Builder | Claims a task, creates a branch, implements with tests, opens PR | Continuous, one task at a time |
| Reviewer | Reviews open PRs via gh pr list, approves or requests changes | Reactive, watches the PR queue |
| Merger | Checks CI status, resolves merge conflicts, squash-merges | Reactive, watches CI + approvals |
| QA | Runs full test suite, files bugs for failures | Periodic |
| Docs | Writes user guides, maintains changelog, audits coverage gaps | Follows the builder |
| Trunk Builder | Builds Trunk itself — dogfooding | Continuous |
They coordinate through Trunk room tasks. Each agent checks the room, finds work appropriate to its role, does it, and marks it done. The loop:
Builder opens PR
→ Reviewer approves (or requests changes)
→ CI runs
→ Merger checks CI + approval
→ If green: squash merge
→ If conflicts: Merger resolves, pushes fix
→ If CI fails: Merger files a bug task → Builder/QA picks it up
No human in the loop for execution. I seed the room with high-level tasks, and the agents decompose and execute.
The agents can’t make every decision autonomously. Ambiguous specs, architectural choices, priority calls — these need a human. The convention: agents create a task with group=human and priority=critical when they’re stuck. I check the mission control dashboard periodically and handle escalations.
The next step is wiring this to a Slack webhook — when a group=human task appears, it pings a Slack channel so I don’t have to poll.
The Trunk Builder agent ran out of assigned tasks. There were about 30 tasks in the project room. It completed them all — features, adapters, a CLI, bridge integrations.
Then, instead of stopping, it read the codebase and decided it needed security hardening.
Over two days, it produced 207 commits. It found and fixed:
It also wrote comprehensive test suites — cross-agent authorization tests, webhook signature verification, adapter test suites for Slack, Intercom, and email.
Nobody asked for any of this. The agent decided the codebase needed it.
Is all of it correct? I haven’t audited every commit. The tests pass. The types check. The patterns are consistent with the human-written code. But the honest answer is: I don’t know yet. This is one of the open questions — what’s the quality ceiling for autonomous code, and how do you verify 207 commits you didn’t write?
Not compute. Not model capability. Context.
Every piece of information an agent reads is budget it can’t spend on work. Early on, agents would check their Trunk inbox, find 200 messages from workspace broadcasts, and burn their entire context window reading them. They’d never get to the actual task.
The fix was blunt: “Do NOT check your inbox. List tasks in room X with group Y.” Skip discovery. Skip context building. Go directly to the work.
Agents with scoped prompts start producing useful output in 30 seconds. Agents with broad prompts (“check everything, understand the landscape”) waste 2-3 minutes on discovery and sometimes exhaust their context before writing a single line of code.
This has a deep implication for agent system design: the optimal agent has the smallest possible context scope. Not the most informed agent. The most focused one.
I tried using --continue to resume Claude sessions across respawns. The idea: the agent carries context from its previous loop, so it doesn’t have to re-discover what it was working on.
Bad idea. Context grows with each loop. Old context compresses. After a few cycles, the agent is carrying around a blob of compressed irrelevant information from three tasks ago. It degrades performance.
The better model: fresh session every loop. The agent starts clean, reads the current task state from the Trunk room, finds its in-progress work, and picks up where it left off. The task board is the durable memory. The agent is stateless and disposable.
This is the actor model applied to LLM agents. State lives in the coordination layer, not in the actor. An agent that crashes and restarts is indistinguishable from a fresh agent — because it reads the same task state and makes the same decisions.
I started with one agent per project. One generalist that does everything — plans, builds, tests, reviews, deploys.
It doesn’t scale. The agent context-switches between planning and coding. It reviews its own work (worthless). It can’t parallelize.
Decomposing into specialized roles — planner, builder, reviewer, merger, QA, docs — mirrors a human engineering team. But it’s not just organizational mimicry. Each role has a fundamentally different cadence:
Matching agents to cadences matters more than matching them to features. One builder per feature sounds parallel but creates coordination overhead. One builder that works serially through a prioritized queue is simpler and often faster.
With 7 agents running, I stopped writing code entirely. My role became:
group=human tasks.The productivity multiplier isn’t the agents — it’s the human freed from implementation. I spent 30 minutes seeding 17 tasks across three groups (bugs, security, tests). The agents have been executing for days.
Most people using AI agents today are in a 1:1 workflow. One human, one agent, one conversation. The human is the driver. The agent is the passenger.
The first shift is 1:N. One human, multiple agents. The human delegates different tasks to different agents. This is what the harness enables — I’m running 7 agents, each with a specific role. But I’m still the orchestrator. The agents report to me, not to each other.
The second shift is N:N. Agents coordinate with each other, not through the human. The planner creates tasks. The builder claims them. The reviewer checks the builder’s work. The merger ships it. The human is involved only at the boundaries — seeding work and handling escalations.
This is where Trunk operates. It’s the coordination layer that enables N:N. Without it, every agent-to-agent interaction routes through a human. With it, agents can self-organize around shared work.
The human doesn’t disappear. The human writes specs. Deep, precise specifications that capture business requirements, state machines, data models, API contracts. The kind of document that takes a day to write and saves a month of implementation.
In the 1:1 world, specs are optional. You can just tell the agent what to build and iterate. In the N:N world, specs are mandatory. You can’t iterate with 7 agents simultaneously — they need a shared source of truth that they can independently consult without asking you.
This is the docs-first workflow I wrote about previously, but scaled up. One human produces specifications. N agents consume them and produce software. The specifications are the interface between human intent and agent execution.
The human also makes judgment calls. When two agents disagree. When a spec is ambiguous. When the right answer depends on business context the agents don’t have. These decisions can’t be automated because they require understanding the customer, the market, and the strategy — things that live in the human’s head, not in the codebase.
Today, a software team is 5-10 humans who coordinate through Slack, Linear, GitHub, and meetings. Each human is both a decision-maker and an implementer.
In the N:N world, the team is 2-3 humans who make decisions, and 20-30 agents who implement them. The humans don’t need to coordinate implementation — the agents do that through Trunk. The humans coordinate strategy through whatever medium they prefer.
The team meeting changes. Instead of “who’s working on what” (the agents know), it’s “what should we build next” (only humans can decide). Sprint planning becomes task seeding. Standup becomes mission control.
I don’t know if this is good. I know it’s coming.
Trunk is a hackathon output, not a product. It works for my setup — a few agents on one machine, coordinating through a hosted relay. Scaling it to teams of humans, each running their own agent fleets, requires work I haven’t done: proper auth, billing, access control, audit logging for compliance, multi-tenancy.
But the pattern is interesting regardless of whether Trunk specifically becomes a product. The observations generalize:
Agents need a coordination layer. Without one, the human is the switchboard. With one, agents can self-organize. The specific implementation (Trunk, a competitor, a built-in platform feature) matters less than the capability.
Tasks-as-state is the right model. Agents are ephemeral. Work state must be durable and shared. A task board that agents can read and write is sufficient for sophisticated coordination.
Context scoping is the primary design constraint. Not model capability. Not cost. Context budget. Every system design decision for multi-agent should be evaluated against: “does this add or remove context from the agent’s window?”
Role specialization works. Not because LLMs need to be specialized, but because different roles have different cadences and concerns. A reviewer should not also be a builder. A merger should not also be a planner.
Humans add value at the boundaries. Specifications, architectural decisions, priority calls, quality judgment. Not implementation.
Some things I want to explore:
The thing that keeps tripping me up: the agents built the system they use to coordinate. The relay, the rooms, the task management, the dependency resolution, the mission control dashboard where I watch them work — all built by agents coordinating through the very system they were building.
There’s a bootstrap paradox here. The first two agents coordinated through Trunk to build Trunk. Then more agents joined. Now 7 agents use it daily to manage their own development. The system is its own best customer.
I started this project because two humans were acting as copy-paste relays between two AIs. The solution was to remove the humans from the loop. What I didn’t expect was that removing the humans from the communication loop would change the humans’ role entirely — from implementer to architect, from programmer to product manager.
I don’t think this is the future of software development. I think it’s already here. The models are capable. The tooling exists. The missing piece was coordination — a way for agents to talk to each other, share work, and self-organize without routing everything through a human.
That missing piece took four hours to build. The agents built it themselves.
Trunk is open source at github.com/usetrunk/trunk.