Spec-Driven Development: How to Stop AI Agents from Rewriting Everything
Somewhere around the third time an AI coding assistant helpfully refactored something you didn’t ask it to refactor, introduced an abstraction you didn’t need, and deleted a function it had decided was redundant, you start thinking about governance.
Not as a joke. Actual governance. A methodology. Rules the AI cannot deviate from without explicit instruction.
That’s Spec-Driven Development, and it’s now the mandatory workflow for everything built in this lab.
The Problem
AI coding assistants are extraordinarily good at implementing things. They are less good at stopping when the thing is implemented. The natural tendency is to look at surrounding code and improve it, rename things for consistency, extract repeated patterns into abstractions, and generally demonstrate initiative in ways that create git diffs you didn’t review and bugs you didn’t introduce.
In a home lab context, this manifests as: you ask an AI agent to add a function to a script, and it rewrites the entire script in a different style, updates the imports, reformats the comments, and changes a variable name that was referenced in three other files. The function you wanted works. Everything else might not.
The solution is not to use less capable AI agents. The solution is to constrain them.
The SDD Methodology
Spec-Driven Development is a three-document system applied before any code is written:
spec.md — What are we building and why? User stories, functional requirements, explicit out-of-scope items. The out-of-scope section is as important as the requirements — it tells the implementation agent what it is not allowed to touch.
plan.md — How will we build it? Architecture decisions, technology choices, data models, security approach. Must comply with the lab constitution (more on that below).
tasks.md — What specifically needs to be done? A granular checklist ordered by dependency. Each task is atomic, verifiable, and checkable. The agent checks off tasks as it completes them and stops at the end of the list.
The workflow: architect (Gemini CLI, using the sdd-architect skill) generates all three documents from a one-paragraph description. Implementation agent (Claude Code, Aider, or Cursor) executes the tasks.md checklist without deviation. Nothing gets implemented that isn’t in the checklist. Nothing gets refactored because it looked like it could use improvement.
The Constitution
F:\Projects\homelab\.specify\memory\constitution.md is the document that every AI agent in the lab reads before making architectural decisions. It encodes infrastructure realities, technology preferences, and security requirements:
- Identity & Secrets: Credentials live in VaultWarden. No plaintext in config files.
- Service Accounts: Named, scoped accounts only. No generic
agentorclaudeaccounts. - Networking: Assume multiple VLANs at 172.16.x.x. Internal TLS via Step-CA.
- Containerisation: New persistent services run in Docker. Traefik handles TLS termination.
- MCP Transport: stdio only. No SSE.
- Agent Framework: LangGraph. Always.
That last point deserves its own section.
The LangGraph Decision
After evaluating several agent frameworks, LangGraph won. The reasons:
State machine routing is the right mental model for infrastructure agents. An agent investigating a network anomaly follows a path: discover hosts → query firewall rules → check switch port state → correlate results → report. This is a directed graph, not a free-form conversation. LangGraph’s StateGraph models this explicitly.
LangChain MCP adapters (langchain-mcp-adapters) provide a clean integration path to the existing stdio MCP servers. The pattern is:
from langchain_mcp_adapters.client import MultiServerMCPClient
from mcp_config import MCP_SERVERS
client = MultiServerMCPClient({"juniper": MCP_SERVERS["juniper"]})
tools = await client.get_tools()
The MCP servers built for Claude Code integration work identically for LangGraph agents. Build once, use everywhere.
Proxy routing via ChatOpenAI pointed at litellm.sdx.local means LangGraph agents use the same unified endpoint as every other AI tool in the lab:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
base_url="https://litellm.sdx.local/v1",
api_key=os.environ["LITELLM_MASTER_KEY"],
model="claude-sonnet-4-6",
http_client=httpx.Client(verify=os.environ["REQUESTS_CA_BUNDLE"])
)
Model selection is a parameter. Switching from Claude to Gemini or local Gemma4 is one line.
The Agent Workspace
The F:\Projects\homelab\agents\ directory is the designated workspace for LangGraph agents. The environment:
- Python 3.12.3 in WSL2
- venv at
agents/venv/with LangGraph 1.1.10, langchain-mcp-adapters 0.2.2 mcp_config.py— shared registry for all four MCP servers.env—LITELLM_MASTER_KEYandREQUESTS_CA_BUNDLE(gitignored)
The mcp_config.py registry solves a practical problem: each agent would otherwise need to hardcode the launch commands for each MCP server. Instead:
# Any agent
from mcp_config import MCP_SERVERS
client = MultiServerMCPClient({
"juniper": MCP_SERVERS["juniper"],
"pfsense": MCP_SERVERS["pfsense"]
})
The servers are spawned with /usr/bin/python3 explicitly to prevent the agents’ venv Python from shadowing system Python (which holds the MCP server dependencies). This was a bug found during initial testing: the agents venv modifies PATH, subprocesses inherit PATH, MCP server startup fails with ModuleNotFoundError. Explicit path, fixed.
The Use Case: AI That Does What It’s Asked
The practical outcome of SDD: a new feature request goes from idea to implemented, tested code without the implementation touching anything outside the defined scope.
An example from recent work: adding the langchain-mcp-adapters integration to the agents workspace. The SDD process produced a tasks.md with eight explicit steps. The implementation agent checked off each step, stopped at the end, and produced no additional “improvements.” The git diff contained exactly what was intended.
This sounds like a low bar. In practice, it represents a significant shift from the alternative — where an AI agent with good intentions and broad access will make many small changes that are individually defensible and collectively difficult to review.
Constrained agents are more useful than unconstrained ones. The constraint is the point.
What’s Next
The agent framework is built. The MCP servers are running. The proxy is handling model routing. The next thing to build is the first actual autonomous agent — something that does a meaningful infrastructure task without human step-through. The network topology agent is the likely candidate: query the Juniper switch, cross-reference pfSense rules, identify anomalies, report.
That’s a LangGraph spec waiting to be written. Probably by Gemini CLI, using the sdd-architect skill, generating a tasks.md that Claude Code will execute without embellishment.
The circle is almost complete.