Halfway through a debugging session I ran lsof -i :3000 out of habit and got six PIDs back. None of them were mine. They belonged to three different agent runs from earlier that day, each convinced it had shut the server down cleanly after the verification step.

This is a quieter failure than a broken test, and that makes it worse. The agent did what you asked. It ran the command, saw the page load, took the screenshot, and declared the task done. It just didn't clean up the processes it had spawned to get there.

The teardown gap

Most agent loops treat startup as part of the task and shutdown as an afterthought. The prompt says "start the dev server, hit the endpoint, confirm the response." It does not say "and then make sure the server is dead before you move on." The agent, reasonably, closes the loop on the verification evidence and moves to the next thing.

The result is a slow accretion: one orphaned Vite process from the UI task, a docker-compose stack left up from the integration test, a watcher holding the SQLite file lock, a tunnel still forwarding to a port that no longer serves anything. By the end of the day, your machine is not just slow. It is stateful in ways you have not audited.

Why the next task pays for it

The orphaned processes do not just waste RAM. They produce false failures in the next agent run.

You ask the agent to start the server. It picks port 3000 because that is what the README says. Port 3000 is already held by a zombie from the morning run. The agent gets EADDRINUSE, concludes the port is blocked, spends three turns killing PIDs it did not start, and eventually starts on 3001 — which is now the port your tests and your docs disagree about. The task that should have taken two minutes took twenty, and the new default port was never recorded anywhere.

The reverse happens too: the orphaned process is still serving the old build, the agent hits it expecting the new behavior, gets the old response, and "verifies" the fix against stale state. You get a green checkmark on a change that was never actually exercised.

Make teardown part of acceptance

I stopped treating "did the test pass" as the end of the loop and started requiring a teardown artifact before the agent can close a task. Concretely:

  • Record what you started. The agent has to list every background process, container, and port it opened during the task before it can say done. A short manifest in the task output is enough.
  • Verify teardown, not just startup. Before marking complete, the agent re-runs the port check and the container list and reports zero of its own PIDs remaining. If something is still up, that is not a cosmetic issue; it means the next run inherits contaminated state.
  • Use a per-task working directory and a per-task port. Instead of letting the agent fight over 3000, hand it a free port at task start and tell it to bind to that. Orphaned processes on random high ports are easier to spot and kill than ones squatting on the canonical port everyone expects.
  • Close the tunnel last. If the agent opened a tunnel or a forwarded port to verify against a live environment, that is the first thing to tear down. It is also the easiest to forget because it looks like infrastructure rather than a local process.

None of this is clever. It is just extending the verification loop from "did the thing work" to "did the working state get cleaned up." Once we made that explicit, the morning lsof audit went from six PIDs to zero, and the next task stopped tripping over the last one.

The local-first angle

This is one of those failures that scales with how much of your tooling lives on the developer's machine. When the API studio, the test runner, and the agent all share localhost, an orphaned process in one tool is a landmine for the others. We keep Powerduck local specifically so the spec and the live response sit in the same process tree and there is no stale service to disagree with each other — but that only works if the agent actually kills the dev server when it is done. Teardown is not a housekeeping chore. It is part of correctness.