Building with AI

The Handoff Is Part of the System

July 20, 2026

Chatterbox became three repositories before it became one coherent experience.

The API owns tenants, authentication, knowledge ingestion, conversations, leads, and billing rules. The platform gives a business the controls to configure its chatbot. The client is the small script that runs on someone else’s website and talks to their visitors.

I worked on all three at once using separate AI coding sessions. Each session had a clear boundary. The backend published an OpenAPI schema. The platform generated types from it. The client consumed the public configuration and chat endpoints.

That sounded clean. It still produced bugs that no single repository could explain.

An agency could switch between clients in the dashboard, yet the install page kept showing the same tenant key. White-label branding saved correctly in the backend, but the embedded widget still needed to fetch and render it. A lead form could complete successfully while the widget kept accepting messages on the conversation that was supposed to be closed.

Every component was locally reasonable. The product was wrong.

A schema carries shape, not intent

OpenAPI helped a lot. It gave the platform a shared vocabulary and caught ordinary mismatches early. It could say that capture_mode exists, that an account may be null during onboarding, or that a response includes lead rules.

It could not explain the full meaning of those fields.

When a visitor submits their contact details, the backend marks a lead as captured. The platform may let the business choose how aggressively that capture happens. The widget has to block new input and offer a fresh conversation. If the last step is missing, the visitor sees a bot that accepted their details and then carried on as if nothing happened.

The type was correct. The state transition was incomplete.

Cross-repository features need a small state machine written in plain language:

  1. What event starts the transition?
  2. Which service records it?
  3. Which clients must react?
  4. What can the user do next?
  5. What happens when one side is running an older version?

Without those answers, a field in a schema can create false confidence.

A publication mailbox for agents

I did not want the backend session editing platform code or the platform session patching Django when an endpoint was missing. Clear ownership kept the diffs easier to review. It also created a practical problem: the agent that found a gap often could not fix it.

I first looked for an existing way to let independent coding agents cooperate. The options I found mostly assumed an orchestrated Claude workflow: one parent session starts the agents, knows who they are, and collects their replies. That works well while one orchestration run remains alive.

Chatterbox did not fit that shape. The first mailbox connected two independently started participants: the backend session and the frontend session working in the platform repository. The embedded client had its own repository and coding session, but it was not a third publisher in that mailbox. No parent agent stayed around to route messages after a session ended.

I ended up making a small publication mailbox from two text files in the shared Chatterbox directory: backend-says.txt and frontend-says.txt. Each side published to its own file and read the other side's publication. There was no private agent inbox or direct-delivery step. One writer per file kept the exchange from becoming another merge conflict.

The open-source version does not preserve that early layout literally. I cleaned the skill up for multiple repositories and more than two agents. It now resolves a shared .agent-mailbox/ directory through a four-step project lookup, then gives each participating agent its own publication file. Related working trees can use the same mailbox without leaving loose files in their shared parent directory.

I am releasing that version as agent-mailbox. It remains deliberately small: a durable publication mailbox for agents that share a project without sharing an orchestrator or even the same working session.

An agent read the other participant's publication before starting related work and added a note to its own file when it changed a contract, needed work from the other side, or reached a state the other side could consume. Each note carried a timestamp, sender, status, the changed contract, what had been checked, and the remaining action. With two participants the addressee was obvious and optional. The cleaned-up skill requires a → @peer addressee when three or more agents share the mailbox.

The files were primitive. The protocol mattered more.

“The endpoint is done” was too vague. A useful handoff looked closer to this:

  • schema regenerated;
  • field available under this response;
  • migration required;
  • deployed on the development host;
  • platform codegen can run now;
  • widget behavior still pending.

That level of detail stopped both sides from guessing. It also made restarts less painful. A new session could read the last exchange and recover the real state of the work.

Done belongs to the consumer

The hardest coordination bugs came from competing definitions of done.

The backend considered white-label configuration complete once it could save and return the agency name. The widget considered it complete once a visitor could see the correct name. The business owner only cared that switching tenants did not leak one client’s key or branding into another client’s installation.

The consumer has the strictest useful definition.

This changed how I close cross-repo work. A backend test proves the backend behavior. It does not prove the feature. The feature is done after the next consumer regenerates its contract, handles the state, and verifies the real flow.

That can mean a browser check. It can mean loading the embed script on a plain test page. It can mean opening two tabs to make sure they do not share a conversation incorrectly. The check should follow the user’s path across the boundary.

More agents create more edges

Parallel coding sessions can move quickly when the work separates cleanly. Chatterbox made that speed visible. The API, dashboard, and client all advanced in the same day.

The same setup multiplies handoffs. Every handoff can lose a decision, a deployment detail, or one half of a state transition. Adding another agent increases coordination work in the same way adding another service does.

I now treat the coordination layer as part of the architecture. It needs ownership, a durable message format, a source of truth for contracts, and an end-to-end definition of done.

Building with AI: I assigned repository boundaries and reviewed the product flow; separate agents wrote most of the API, platform, and widget changes. Their local tests could all pass while the feature still failed at a handoff. The publication mailbox, generated OpenAPI contract, and consumer-side browser check became the response to that mismatch.

When the mailbox caught real bugs

OpenAPI remained the machine-readable authority for field shapes. The mailbox carried everything the schema could not: whether a migration was needed, whether the endpoint was available locally or deployed, what the consumer still had to change, and which user flow had been checked.

In one handoff, the platform pulled a new schema and rendered detected facts grouped under their source. The next pass showed that detected_from had been described as a string even though the real response was the nullable object {id, name, type, url}. The mailbox made the consumer-facing mistake visible before either side quietly built around a false type.

The same protocol caught a production build bug that neither side could own alone. NEXT_PUBLIC_USE_MOCK existed at runtime, but Next.js had already inlined public variables during pnpm build. The backend deployment moved it into Compose build arguments and verified the rendered plan with docker compose config; the platform changed mock mode to explicit opt-in so an unset variable chose the real API.

The result was fewer locally complete, globally broken features. Contract changes arrived with the consumer work still attached. One writer per publication file kept the history stable, OpenAPI carried shape, and the browser or embed page decided when a cross-repository feature was genuinely done.