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"],
"opnsense": MCP_SERVERS["opnsense"]
})
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 OPNsense 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.
Update — August 2026
The methodology held. Two of the constitution’s clauses did not, and the agent this post ends on was never built.
The network topology agent doesn’t exist, and shouldn’t. The plan was a LangGraph state machine: query the switch, cross-reference firewall rules, correlate, report. What actually happened is that Homer — the Hermes agent that replaced Clawdia in June — was wired to the same six MCP servers and does that work conversationally, on a schedule, as part of the morning digest.
The spec was never wrong about the task. It was wrong about needing a program. The steps I’d carefully drawn as a directed graph were a directed graph because that’s how I’d have written it in Python, not because the problem demanded one — and an agent with the right tools and a cron entry covers the same ground without a state machine to maintain. That’s an uncomfortable finding for a post arguing that constraint is the point, and I’d rather leave it standing than quietly delete the backlog item.
“Agent Framework: LangGraph. Always.” is therefore the clause that aged worst. A constitution is supposed to encode decisions that are expensive to revisit, and a framework mandate written before building anything with it wasn’t that — it was a preference in a costume. The clauses that earned their place were the boring, testable ones: stdio transport only, no plaintext credentials in config, named and scoped service accounts. Every one of those has since been vindicated by something breaking in a way it prevented.
The secrets clause changed targets. “Credentials live in VaultWarden” became “credentials live in the age-encrypted vault” when VaultWarden was decommissioned on 11 June 2026. The rule survived the implementation, which is roughly the test of whether a rule was written at the right altitude.
The architect side of the workflow got complicated. Google deprecated Gemini CLI for individual tiers on 18 June 2026, replacing it with Antigravity CLI — which is now the primary agent CLI on REX, but authenticates by interactive Google sign-in and supports no custom base URL, so it cannot route through the lab’s LiteLLM proxies at all. Gemini CLI is kept alive specifically as the proxy-routed fallback. Two CLIs where there was one, because the newer one can’t be pointed at your own infrastructure.
The three-document loop itself — spec, plan, tasks, then an implementation agent that executes the checklist and stops — is unchanged and still mandatory. This blog post, and the corrections appended to it, went through it.