Back to Blog
AI Agents Can Write Code. Nobody Is Managing Them. I Built the Missing Layer.

AI Agents Can Write Code. Nobody Is Managing Them. I Built the Missing Layer.

Two of my AI agents opened conflicting PRs on the same repo. I caught it at 11 PM. That's when I realized the control plane was already sitting on my screen: the kanban board.

AIDevOpsOpen SourceAI AgentsAutomation
April 5, 2026
6 min read

Two of my AI agents opened conflicting PRs on the same repository. Same file. Different changes. Neither knew the other existed.

I found out during code review at 11 PM on a Tuesday, staring at merge conflicts between two pieces of code that were both individually correct and collectively impossible. Three agents running across different projects, no coordination, no approval flow, no visibility into what each one was touching. I was their manager, and I was managing them the way you'd manage interns if you hated interns: assign work, walk away, hope for the best.

That Tuesday night, something clicked. I already had a system for managing work. Everyone does. It was open in the browser tab next to the merge conflict: the kanban board.

A kanban board is a state machine

Not metaphorically. Literally.

Cards are work items. Columns are states. Moving a card between columns is a state transition. Assigning a card means "you own this." The board is a visual state machine, and the human dragging cards across columns is the orchestrator.

Replace "the human" with an automation engine. Replace some of the assignees with AI agents.

That's Ouija. No new UI. No new workflow to learn. Your existing board becomes the control plane. Agents show up as board members. You assign a card to an agent the same way you'd assign it to a junior engineer. The card moves through columns the same way it always has. The difference is that between "In Progress" and "Review," an AI agent cloned the repo, wrote the code, and opened a PR.

Two trigger modes handle different situations. Auto mode: assign a card, the agent starts immediately. Manual mode: assign first, then drag to "In Progress" when you're ready. Manual gives you a staging area, a queue of work the agent will pick up when you say so. I use auto mode for small tasks and manual mode for anything I want to review the description on before an agent burns tokens on it.

The design decision that made everything else easy

Ouija is a 14-package TypeScript monorepo. Hundreds of decisions went into it. One mattered more than all the others combined.

typescript
transition(state, trigger, config)  { nextState, sideEffects, events }

A pure function. No I/O. No side effects. The state machine doesn't know about databases, APIs, or job queues. Give it a state and a trigger, it returns the next state and a list of things that should happen. The orchestrator handles the messy parts: loading state from Postgres, running side effects (move the card, send a Telegram notification, dispatch the agent), persisting results back.

Why does this matter?

Because state machines are where bugs go to hide. A card stuck in "In Progress" forever. A dispatch that fires twice. A notification that goes out before the PR is actually open. These bugs are brutal to diagnose when the state logic is tangled up with HTTP calls and database writes. You can't tell if the state transitioned wrong or if the API call failed or if the database write was the one that timed out.

With a pure transition function, every state path is testable in milliseconds without mocks. No database. No network. Just the function, a state, and a trigger. When something goes wrong in production (and it will), you can replay the exact sequence of states and triggers that caused it. The I/O layer gets tested separately with integration tests.

604 tests. Every edge case I could think of, plus a bunch the e2e run surfaced. The state machine has never been the thing that breaks.

The thing that breaks

At 2 AM on a Thursday, three hours into debugging why the second stage of the pipeline wouldn't fire, I found it.

Plane CE (the self-hosted kanban board I was using) doesn't fire webhooks on API-triggered changes. Only on UI actions.

Let that sink in. A human drags a card in the browser, Plane sends a webhook. Ouija moves the same card via the API (which it does after every completed task), Plane sends nothing. The feedback loop was broken by design.

The flow was supposed to be: agent finishes work, Ouija moves card to "Review" via API, Plane fires webhook, Ouija picks it up and triggers the next stage. Instead: agent finishes, card moves, silence. The pipeline stopped mid-cycle because the board never told Ouija the card had moved.

The documentation didn't mention this. The webhook settings page didn't hint at it. Three hours of my life, burned on an assumption that "webhooks fire on state changes" means all state changes, not just the ones triggered by mouse clicks.

This is the warning nobody gives you about building automation on products designed for human interaction. The webhook contract is almost always thinner than the docs suggest. Not "webhook fires when I click the button" testing. "Webhook fires when my code moves the card at 3 AM" testing. Test the full round-trip before you build anything on top of it.

The immediate fix was a polling fallback. The longer-term fix was adding Fizzy (Basecamp's kanban tool) as a second backend. Fizzy treats columns as first-class entities, fires webhooks on API changes (not just UI), signs payloads with HMAC-SHA256, and includes built-in delivery tracking. The architectural fit is what Plane should have been.

Ouija supports both. Environment variables select which backend. The plugin system (kanban, git, agent, notification) means swapping any component never touches the core engine.

What a card's life looks like

You create a card. "Add rate limiting to the /api/users endpoint." You assign it to Rex Coder, an AI agent who shows up as a board member.

Ouija receives the webhook. Guards run: is the description long enough to be actionable? Right labels? Already have an open PR for this card? Guards pass.

A BullMQ job fires. The agent worker clones the repo (or creates a git worktree if it already has a local copy), checks out a feature branch named after the card, builds a prompt from the card description and repo context, and hands it to Claude Code.

Claude writes the code. Commits. Pushes. Opens a PR. Ouija receives the GitHub webhook, moves the card to "Review," pings your Telegram with the PR link.

You review it like any other PR. Merge. Ouija catches the merge event, moves the card to "Done."

Card in. PR out. No babysitting. No Slack messages asking the agent how it's going. No checking at 11 PM whether two agents stepped on each other's code. The board is the single source of truth, same as it's always been. The agents are just new members of the team.

Try it

Ouija is open source under Apache 2.0 at github.com/muhammadkh4n/ouija.

If you're coordinating AI coding agents through Slack messages and copy-pasted prompts, you already have the dispatch system. It's the kanban board you look at every morning. Ouija just connects the wires.

Share

Get new posts in your inbox

Architecture, performance, security. No spam.

Keep reading