Case study
DaVinci
A personal multi-agent orchestrator that takes a task and a repo and drives it through research, planning, execution, and integration testing, with human review at the gates that matter.
Most of the work in shipping a feature isn't typing the code. It's holding the whole thing in your head: what the change touches, what to build first, what breaks downstream, whether the tests actually prove anything or just pass. That coordination is the expensive part. DaVinci was my attempt to hand the coordination to a system and keep myself in the seat only where judgment earns its keep.
It's a personal project. It takes a task and a repository and drives it the way a small team would. Research the codebase, plan the work into dependent pieces, write and test each piece, then run the whole thing end to end. You can let it go start to finish, or you can make it stop and check with you at the points that matter. I built it solo over a few months to answer a question I kept arguing about: how much of the coding-agent problem is a model problem, and how much is plumbing. The answer turned out to be mostly plumbing.
The problem
A single coding agent on a single task is a solved demo. Point it at a repo, give it a prompt, watch it churn. The trouble starts the moment the task is big enough to need a plan, or you want more than one of them running, or you want to trust the result without reading every line. Then you're not building a smarter agent, you're building the thing around the agent: the queue that holds work, the state that survives a crash, the gate where a human says yes, the logic that stops two agents from stomping each other's commits. None of that is prompt engineering. It's the boring distributed-systems stuff that decides whether the clever part ever ships. DaVinci is less a code generator than a small operations layer for a fleet of processes that don't fully trust each other. The generating is the easy tenth of it.
The shape
A dashboard and an orchestration API sit in front. They drive a task through four phases, each backed by its own container-style task: Research, Planning, Execution, and an Integration test. The phases don't call each other directly. Each one reports back to the API over HTTP callbacks as it makes progress, the API writes that progress to Postgres, and completing one phase is what triggers the next. Artifacts and diffs land in object storage. Everything durable lives in Postgres so a phase can die and get re-triggered without losing the thread.
The phases talk through callbacks instead of a direct chain because the work is long and lossy. A research container might run for many minutes, and if the API held a connection open and waited, a single blip would take the whole task down. So each phase is fire-and-report: the API kicks it off, forgets about it, and reacts to callbacks. State is the only thing that connects one phase to the next, and state lives in the database, not in memory.
Four phases, two modes
Research breaks a task into three to five directions and explores them at once. A container clones the repo and runs a coding-agent CLI on each direction in parallel, and the findings stream back through the same callbacks. When the last direction reports in, that completion auto-triggers Planning. Planning aggregates the findings and produces a subtask hierarchy with explicit dependencies, so the executor later knows what can run now and what has to wait. On approval it cuts a dedicated working branch and queues the root subtasks, the ones with no dependencies.
Two modes. In handoff mode it runs end to end on its own, which is fine for well-scoped work. In review mode it pauses at two gates, after planning and after execution, and waits for me to approve or push back in a chat. It revises and resubmits until I'm happy. I ended up writing a whole post on why those gates are an architecture and not a checkbox. DaVinci is where that conviction came from.
The hard part: making the executor honest
The interesting part is how a single subtask actually gets done. The lazy version hands the whole thing to one model and hopes. Instead the executor runs a small loop with three separate roles, because "write code" and "check the code" are different jobs and one model wearing both hats grades its own homework.
A code-gen agent writes the change. A test-gen agent writes tests against it. A test-runner runs them and, when they fail, does the part everyone skips: it diagnoses why. Every failure gets a root cause out of five: is the code wrong (code_issue), is the test wrong (test_issue), are both wrong (both_issue), is it the environment (env_issue), or did it actually pass (test_passed)? That label decides where the feedback goes next round. code_issue routes back to code-gen, test_issue to test-gen, both_issue to both, env_issue into fixing the setup rather than the diff. Blaming the code when the test was garbage only burns rounds. The loop runs up to five times, and if it can't get to green honestly, it says so and flags the subtask instead of committing a passing lie.
The hard part: running a lot of these without lighting money on fire
One agent is easy. The problem is fifty. Each executor is a heavy container, and if every queued subtask spun one up the instant it was ready, both the bill and the account limits would start screaming. So execution doesn't run tasks directly. It puts them on an SQS FIFO queue, a Lambda router picks them up, and a per-subtask Step Function checks for capacity before it starts anything.
If there's room, it runs. If not, it waits sixty seconds and checks again, and the wait costs nothing, which is the entire trick. A queue that burns compute just to hold its place is a queue that taught you an expensive lesson. This one sleeps for free and wakes up when a slot opens. Concurrency stays capped, the account stays happy, and nothing gets dropped. FIFO ordering keeps a single subtask from being picked up twice on a retry.
The hard part: the subtasks fight over the same branch
Subtasks that share the same dependencies run in parallel, which means they race to push to the same working branch. The first one wins. The rest get rejected. So each executor, when its push bounces, fetches, rebases, and if there's a real conflict, hands both versions to a model to resolve, merging dependency additions and overlapping imports the way you would by hand. Then it tries the push again, up to five times, before it gives up and flags the subtask for me.
Nothing rewrites history. Every attempt, including the ones that failed and got retried, stays in the log where I can read it. The file changes for each subtask get pulled from the version-control API and stored, so the record of what landed doesn't depend on the branch staying pristine. When you're letting agents commit, the audit trail isn't a nice-to-have. It's the only reason you can trust the thing at all, a lesson that turned into another post.
When a subtask can't be saved, there are two ways out. Rerun starts a fresh attempt with an incremented number, so the failed one stays on the record. Force-bypass marks it done and unblocks its dependents, for when I've decided the dependency doesn't really matter. Both are explicit and logged.
The hard part: letting a human in without stopping the machine
In review mode the plan doesn't go straight to execution. It gets submitted to a chat gate, and nothing branches or queues until I approve. I can approve it, or I can send feedback in the chat, in which case the planner folds the feedback in, revises, and resubmits. Only on approval does DaVinci cut the working branch and queue the root subtasks.
The execution review gate mirrors this one phase later: once the subtasks are committed, the same submit-review-revise loop runs before the integration test. The point of doing it this way, rather than a blocking prompt, is that the review is just another durable state. The task sits in a pending-review status in Postgres until I act. I can walk away, come back an hour later, and the chat is still there. The human is a step in the state machine, not a thread someone has to keep alive.
That durability is also what makes pause, resume, and stop work. Pausing writes the current phase into the task's status and halts the active work. Resume reads that stored phase back and re-triggers whatever got killed, which only works because no phase depended on anything held in memory. Stop is terminal. All three are just status transitions, which is the whole reason they're reliable.
The data and state model
Everything runs off a task row in Postgres with a status that walks a fixed lifecycle: pending, researching, research_complete, planning, pending_plan_review, plan_complete, executing, pending_execution_review, integration_testing, and finally completed or failed. The pending-review statuses are where the human gates live. Any phase can drop the task into failed.
Hanging off the task are the tables that make it replayable. Research directions record what each parallel explorer found. Subtasks carry their dependencies, status, commit sha, the files they changed, and their test results. Test rounds store each pass through the executor loop with its root-cause label, so I can read back exactly why round three routed to test-gen. Integration-test runs are tracked separately because there can be more than one, and plan-review and execution-review messages are the chat transcripts of the gates. There's also a catalog of MCP tool servers a task can use and a record of which skills got invoked. It's a lot of tables for a personal project, and that's the point: the schema is the audit trail, and the audit trail is what lets me trust an agent's commit without reading it.
A few things ride on top. Model selection is per phase, so research can run a cheaper model than execution. The MCP tool servers are configured per task, so a run only gets the tools it should. And task creation can be driven from a Linear issue, which is how I used it day to day: file the issue, let DaVinci pick it up.
The integration test phase
The last phase catches the lies the unit tests missed. A container sets up a real environment, node or python plus whatever services the repo needs, including a Postgres instance, and runs the regression checks: lint, type-check, the existing suite. It's allowed up to three auto-fix commits to clear those, because a broken import isn't worth flagging a human over. Once the baseline is green, an agent does feature verification, actually exercising the thing the task was supposed to build rather than trusting that green tests mean a working feature. Multiple runs get tracked, because you almost never get it in one.
What I'd keep
DaVinci is shelved now. It was an experiment, and experiments earn their keep by teaching you something you carry into the next thing. Three things stuck.
Durable, resumable execution beats clever prompting. The moment a run can survive a crash and pick up where it left off, half the reliability problems stop being problems. The human belongs at specific gates, not sprinkled through every step, or you've just built a slower human. And the instant you run more than a couple of agents at once, the hard problems stop being about the model and start being about queues, capacity, conflict resolution, and who gets to push.
It stopped being a project about generating code and became a project about running a lot of processes that don't fully trust each other, safely. That second project is the one I'd build again.
Comments