Building a Software Development Workflow Around Agentic AI + TDD
Brian Kotos
September 3, 2026
I've been experimenting with a more structured approach to AI-assisted software development while building Listello, an open-source task management application.
I recorded a short video walking through the workflow:
The idea is less about asking AI to “build a feature” and more about creating a repeatable process where AI works within architecture, patterns, and tests that I define.
For this example, I added the ability to mark a completed item as incomplete. I started close to the domain and progressively worked outward through each layer.
The uncomplete-item slice cuts through the whole stack like this:
Click to enlarge
The application service, for example, follows the same pattern as the existing operations:
func (s *itemService) UncompleteItem(itemID string) (domain.Item, error) {
item, err := s.itemRepository.GetByID(itemID)
if err != nil {
return domain.Item{}, err
}
event, err := (&item).Uncomplete()
if err != nil {
return domain.Item{}, err
}
if err := s.itemRepository.Save(item); err != nil {
return domain.Item{}, err
}
if err := s.eventPublisher.Publish(event); err != nil {
return domain.Item{}, err
}
return item, nil
}
From there I worked outward to the HTTP adapter:
func UncompleteItem(itemService application.ItemService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
item, err := itemService.UncompleteItem(id)
if err != nil {
response.WriteError(w, http.StatusBadRequest, err.Error())
return
}
response.WriteJSON(w, http.StatusOK, viewdto.ItemFromDomain(item))
}
}
Then through the frontend client and UI until the full vertical slice was connected.
The key part of the workflow is that I'm doing this with a red → green TDD loop at each step. AI first generates the failing specification, I review it, and only then do I have it implement enough code to make the test pass.
That became especially useful when I hit a real bug near the end of the session.
Completing an item and then immediately uncompleting it didn't correctly update the UI unless the page had refreshed first. Rather than simply telling AI to fix it, I asked it to first reproduce the problem as a failing test:
it("immediately unchecks the checkbox when clicked after completing", () => {
// Arrange
const onComplete = vi.fn();
const onUncomplete = vi.fn();
render(
createElement(ItemRow, {
item: baseItem,
onComplete,
onUncomplete,
}),
);
// Act
fireEvent.click(
screen.getByRole("button", { name: "Mark complete" }),
);
fireEvent.click(
screen.getByRole("button", { name: "Mark incomplete" }),
);
// Assert
expect(onUncomplete).toHaveBeenCalledWith("IT_1");
const toggle = screen.getByRole("button", {
name: "Mark complete",
});
expect(toggle).not.toHaveClass("is-checked");
expect(toggle.querySelector("svg")).not.toBeInTheDocument();
});
Once the failure was reproduced, the eventual implementation change was tiny:
if (!completed) {
setOptimisticallyComplete(true);
onComplete(item.ID);
return;
}
setOptimisticallyComplete(false); // the missing line
onUncomplete(item.ID);
That small example captures a big part of what I'm trying to accomplish.
I don't want AI to just generate large amounts of code and hope that it works. I want to give it a narrow set of architectural patterns, examples, and automated specifications, then let it handle more of the repetitive implementation work inside those boundaries.
The commits from the session followed that progression: application service → API handler → frontend client → UI wiring → refactoring → regression fix. That's increasingly how I think about agentic software development:
I define the architecture, patterns, tests, and boundaries. AI increasingly handles the repeatable work of moving through them.