Skip to content
Agentic9 min readBy Sebastian Gieseler

One workspace per agent: why development environments should share nothing

When several coding agents work on one stack at the same time, crosstalk between them is the most expensive failure: a migration in the wrong database, a job in the wrong stack, a build that never arrives. What our days looked like before devstack, and why every worktree now gets a self-contained workspace.

An ordinary morning for us: one ticket is open in the editor, and in three terminals coding agents are working on three others, each on its own branch. This is no longer an experiment, it is the normal case. The question it raises has less to do with the models than with the environment: where does the code an agent just changed actually run, and what does the agent get to see of it?

This article describes how for a long time we could only answer that question with workarounds, and why devstack, our open-source tool for local development stacks, now enforces a clear rule: every agent works in a self-contained workspace. Anything that passes back and forth between two workspaces is not a feature, it is a bug.

Before: one stack, many writers

Our starting point was a stack that had grown over time: a .NET API with a database migrator, an Angular frontend, a few smaller helper services and around twenty containers, from SQL Server and Elasticsearch to a job scheduler and a webhook queue. Development happens on Windows 11. Everything was orchestrated by an AppHost running under a file watcher. For one person with an IDE, a setup like this works well. In our daily work, though, there was never just one person: the IDE, terminals and several agents were writing to the same working tree at the same time.

The result was a list of headaches, each of which cost real time:

  • "But the endpoint is there." A new endpoint answers 404 although the code is in place and the project builds. The running process holds on to its own DLLs, the new build overwrites them only partly, and nothing reports it. The agent then looks for the bug in code that is not even running.
  • The same service three times in one second. Several agents save at almost the same moment, and the watcher restarts on every change. Duplicate instances start their background workers before they lose the race for the port, and pick up jobs and queue messages on the way. In the end the application itself needed a lock against being started twice.
  • A half-finished edit takes everything down. The restart comes before anyone knows whether the new state compiles at all. One agent in the middle of a rename takes the working stack away from everyone else.
  • The migration lands in the wrong database. A stack started from a second worktree found the first one's containers, because they held the same fixed ports, and applied its migrations there.
  • The job lands in the wrong stack. A scheduler or pull queue shared by two stacks hands work to whichever asks first. That is not a resource question, it is a correctness bug.

Within three months the AppHost, the launch script and the runbook grew to around 3,300 lines, a good share of them workarounds. Each one made sense on its own. Together they described one underlying problem: the workspaces were not separated.

Crosstalk is the real anti-pattern

Working with several agents, one thinks of code conflicts first. Branches and pull requests handle those well. More dangerous is crosstalk at runtime: one agent changes something, and another one feels it without knowing why.

For an agent this is particularly expensive. A person eventually notices that something behaves oddly and asks. An agent takes the behaviour of its environment as the truth. If the API answers with the old state, it builds explanations for behaviour its code never had. If another agent changes the shared database, the test it just saw pass fails on the next run. The debugging is aimed at the wrong opponent, and in the end there are commits in review that solve a problem that never existed.

For us this leads to a simple rule: a workspace must be complete for the agent working in it, and nothing in it may depend on another workspace.

What a self-contained workspace needs

In devstack such a workspace is called a lane, and every git worktree is one. git worktree add, then devstack up in the new directory, and the same stack runs a second time beside the first:

  • Its own ports. A lane's services and its containers' published ports are shifted by a fixed offset. Two lanes never get in each other's way, and each knows where to find its own services.
  • Its own containers and its own data. Every lane starts its own Compose project from the Compose files of its own worktree, with its own volumes and networks. A branch that changes a Compose file runs against exactly that change, not against someone else's infrastructure.
  • No one else's work. Background workers do not run in a lane by default, since they may also talk to systems outside the stack. Whoever needs them turns them on for that lane, and then its scheduler and queue belong to it alone.
  • A build that touches nothing running. A change builds as a candidate beside the running instance and takes over only once it is healthy. A compiler error leaves the old build answering, and a newer change cancels the build of an older one.

For a long time we allowed one exception: containers without state of their own, such as a telemetry dashboard, could be shared between the lanes and the main tree. In practice that caused crosstalk too. A lane's containers could not reach the shared services by name, so telemetry went missing without a word. All lanes wrote into the same telemetry, and a lane on another branch got the main tree's definition. Since version 0.33, sharing is therefore deprecated. Where memory is tight, a profile leaves out what a lane does not need: visibly, rather than half separated.

The agent asks the environment instead of guessing

A self-contained workspace only helps if the agent can read it. For that, devstack comes with an MCP server and generated skills. An agent asks for status and gets a clear answer for its lane: is its change live yet, or does it say not yet active: building, failed or held? It reads the logs of its own services, restarts them and runs the commands the stack declares, such as a database reset. Every tool acts on the lane of the worktree the agent is working in. It does not restart its neighbour's stack by accident.

This shifts the debugging. The question "is my code even running?" is answered by the environment, not by the agent's guesses.

When the machine runs out of room

A separate set of containers per lane costs memory. That is the price of this rule, and we pay it deliberately. Before a lane starts, devstack checks whether there is enough memory, and otherwise names the number and the ways out. The most important way out is not a half-separated lane but a lane on another machine: devstack up --on <node> starts it on a server in the team's pool, and its ports are forwarded locally as if it ran here. The workspace stays complete, it just lives somewhere else.

What changed in our daily work

The morning looks much as it used to: a ticket in the editor, several agents on their own branches. The difference is that each of them has its own complete stack. A migration in one lane only touches that lane's database. A half-finished edit produces at most a failed candidate, and the lane's old build keeps answering. The launch scripts, locks and runbook pages of the past are no longer needed.

If you work with several agents on one stack yourself, it is worth looking at your own assumptions: where do your workspaces share something today, a database, a port, a queue, a build directory? Each of those is a possible path for crosstalk. devstack is open source, still before version 1.0 and in daily use with us. It runs natively on Windows and Linux, with macOS in preview. You can find it on GitHub.

Call