For a few weeks I ran a proper multi-agent orchestrator. The concept was right, and I still think the people building those tools are pointed the correct way. But the bill was absurd, and it took me a while to work out why.
It wasn't the coding. It was the talking about the coding.
Every turn, agents re-read the whole board, reconstructed project context, decided what to claim, checked dependencies, posted summaries, and polled for changes. Manager agents planned the work of other planners. The system spent most of its tokens deciding who should do the work rather than doing it.
So I built a replacement in an afternoon. Directories are columns. Tickets are markdown files. Moving a ticket is mv. Git is the history.
.agent-board/
todo/ 007-fix-login-redirect.md
doing/ 003-add-auth.md
review/ 005-refactor-api.md
blocked/
done/
That part is unremarkable — you could write it yourself this evening. The part worth writing down is what I kept refusing.
The inventory of refusals
Each of these looked like an obvious improvement at the moment I turned it down. Several I had already built before, in the thing I was replacing.
An agent registry. Roster state goes stale. An agent crashes and stays registered forever, and everything else waits on a ghost. You never need to know who exists — only what is unclaimed.
A scheduler. The most expensive mistake on the menu. Scheduling, claiming, dependency checks and timeouts should be deterministic code. Agents should spend tokens solving tasks, not repeatedly deciding who ought to solve them.
Leases and expiry. These only earn their complexity once nobody is watching. A human in the loop notices a stuck ticket for free.
JSON tickets. The comment list is the field several agents write at once. JSON arrays conflict in git every single time, and appending means parse, mutate, re-serialise, rewrite. Markdown just appends.
SQLite. A binary blob has no diff, no merge, no git log for a single ticket. Where several agents mutate shared state, the audit trail is worth more than query power.
A status field. The directory already says which column a ticket is in. A second copy of that fact is a thing that can drift, plus a reconcile step to keep it honest.
Presence indicators. Stale presence is worse than none, because it gets trusted. "Which agents are online" needs heartbeats, and heartbeats lie.
Custom columns. Sounds free. Isn't.
Every one of these is a step toward the board knowing which agents exist. That is the line. Cross it and you have rebuilt the expensive thing.
What actually carried the weight
Two ideas did most of the work, and neither is code you would notice in a diff.
Assignment is the column
I spent an embarrassing amount of time on "how do I assign a ticket to a specific agent" before noticing I already could. Moving a ticket to review/ assigns it to whoever watches review. You never need to know whether that is Claude or Codex or a person today — swap them tomorrow and nothing changes.
There is an owner field, and you can set it from the UI. It routes nothing. It is a sticky note, and a different agent picking the ticket up anyway is fine.
Discovery beats notification
My old setup had a perfectly good message bus sitting in a project. It had been there a month. And I was still typing "check your inbox" at agents several times a day.
When I finally looked, that project's AGENTS.md mentioned the message bus exactly zero times. Every session started ignorant of its own inbox. I had been solving a delivery problem that was really a discovery problem.
So board init writes a short block into the project's AGENTS.md and CLAUDE.md. That is the whole mechanism. Those files are read by a terminal agent and a VS Code extension alike — instructions are transport-independent in a way notifications never are.
Then I opened an agent in a project it had never seen and typed six words: Take ticket 1 from the board. It ran board show 1, claimed it, pushed a branch, opened a PR, and left a comment noting the one failing test was pre-existing — verified against main in a clean worktree.
I had not explained the board. I had not explained the commands.
What it cost to learn
Three defects shipped past a full green test suite and were caught only by review afterwards. All three were in the design, not the implementation.
The id allocator was wrong twice. First it reserved NNN-slug.md exclusively — so exclusivity depended on the ticket's title, and two agents with different titles both won id 001. I fixed it to reserve an id-only path inside the column, which still let two columns win the same id. It now reserves at the board root before renaming into place.
The comment function loaded the whole ticket, appended in memory, and rewrote the file — so two agents commenting at once silently lost one. That directly contradicted the reason I had chosen markdown over JSON in the first place. It is a real append now.
And a newline in a ticket title terminated the frontmatter early, which permanently broke every listing and every page, because both load every file. No malice required; an agent writing a two-line title would do it.
Passing tests told me nothing about any of these. A reviewer reverting the fix and watching comments actually disappear told me everything.
If you take one thing
It probably is not the board. It is this: before adding machinery so your agents can coordinate, check whether they know the coordination exists at all. Mine did not, for a month, while I typed reminders at them.
And when you do add machinery, write down what you refused. That list is the design. Without it, the next person — or the next agent reading your repo — will helpfully add the registry back.
agent-board is MIT licensed and lives at github.com/jharjadi/agent-board. The refusals, with their reasons, are in docs/decisions.md.
You sit down Friday with one messy GitHub issue. You want a usable plan before Monday morning. You paste the text into a chat agent.
The reply looks polished, complete, and very sure. It adds Kafka, Redis, and a new auth service. The issue never named those systems.
You do not have a brownfield bu
Il problema: SQL non e uno standard unico
Sulla carta, SQL e uno standard. Nella pratica, ogni database parla il suo dialetto. MySQL usa i backtick per quotare gli identificatori, PostgreSQL le virgolette doppie, SQLite le accetta entrambe ma preferisce le virgolette. MySQL ha AUTO_INCREMENT, Post
I’m building a small Go HTTP service with PostgreSQL. Most requests are synchronous, but I’m starting to add work that doesn’t need to finish before returning a response: sending notifications, processing uploads, and similar tasks. My current thought is to run those jobs in a small in-process worke