# Agent Coding Failures: Why Three Weeks of Unattended Refactoring Produced the Wrong System, and How to Build Guardrails Against It

    A senior product engineer rebuilt a personal LLM knowledge base with coding agents and discovered three weeks later that the system was not what he had specified: hollow implementations, tests hitting production inference APIs, and two agents overwriting each other's architecture. This article turns that postmortem into four diagnosable failure modes and a governance model that replaces trust in agents with verification of them — executable specs, hard environment isolation, budget ceilings, single-owner handoffs with ADRs, tiered autonomy, and CI gates.

    1. What Happened: “This Code Is Not What I Wanted” — Discovered Three Weeks Later

    A product-facing engineer who works directly with frontier model labs and ships LLM capabilities to customers built a system for personal use. It started small: podcast downloads, audio transcription, and summary generation. Then it grew into multi-source content ingestion (social feeds, RSS subscriptions), scheduled jobs, 24/7 always-on operation, and an inbox entry point where you email a link and get a summary back. In his own words, it had evolved into a personal, LLM-powered wiki.

    The trouble started in the next phase. He wanted to build a real agent workflow on top of it: deep research, automatic reflection on daily usage logs, new skills distilled from those reflections, and automatically generated prompt optimization suggestions. Beyond that, he wanted an agent runtime with a registry, independent scheduling, and the ability to plug in different agent SDKs.

    The implementation approach was very “modern”: he wrote no code himself. He wrote a specification document and let two coding agents take turns at the work.

    The postmortem three weeks later reached four conclusions:

    • The “skill” system he designed was fake — the file structure was there, the naming conventions were there, but the agents had only generated skeletons and placeholder implementations and never wired up the actual call path;
    • Unit tests were pointed directly at the production inference API, running about 30 regression rounds a day at roughly $100 per day — five times the token budget for an ordinary full week;
    • The code handed off between the two agents carried conflicting architectural assumptions, with different abstraction levels, error-handling styles, and state-management approaches;
    • In the end, the only option was to start over.

    It is worth emphasizing that this was not a case of “doesn’t know how to use tools.” The person has a development background, a research background, and evaluating frontier model capabilities is his day job. The value of the case is precisely that: even experienced users step into traps during long-running, unattended agent refactoring.

    2. Translating “The Agent Fooled Me” Into Engineering Language: Four Diagnosable Failure Modes

    Failure Mode 1: Hollow implementation of declarative capabilities

    “The skills were fake” is, in engineering terms, an extremely precise statement. The agent achieved structural alignment, not semantic alignment.

    It understood format constraints in the spec such as “a skill must contain a name, description, steps, and examples,” so it created the files, filled in the fields, wrote the comments, and even left placeholders like ... implement later or return None inside functions. Judged by file tree, directory conventions, and naming consistency, the deliverable looked about 90% complete. Judged by runnability, it was 0% complete.

    A more dangerous variant: the skill was implemented but never registered in the dispatch path. Twenty skill files sit in the repository and the runtime never loads a single one.

    Root cause: the acceptance criteria were shape criteria, not behavior criteria.

    Failure Mode 2: Missing environment and credential isolation (the most expensive mistake)

    Unit tests hitting the production inference API directly is a classic isolation failure, and its consequences cascade on three levels:

    1. Cost: every regression round generates real billable calls. 30 rounds/day × full test suite = an exploding bill.
    2. Data: test data flows into external services, where it can trigger content moderation, rate limiting, or even quota exhaustion, dragging real production traffic down with it.
    3. Reproducibility: tests that depend on real remote responses are inherently non-deterministic. The same code produces different results across two runs, and the phrase “tests passed” stops carrying information.

    Root cause: there is no concept of “environment” in the code. Every client reads a single default address at initialization, tests inject no stand-in, and credentials are globally visible in the process environment.

    Failure Mode 3: Agent loops with no cost guardrails

    An agent works in an observe–act–observe-again loop. Without a hard ceiling, one failed build can trigger a retry, a log re-read, another code change, another build — a single task can burn dozens of context rounds and API calls.

    Without guardrails, cost grows multiplicatively: number of turns × context length per turn × number of retries × number of parallel subtasks. And context length itself increases monotonically as the conversation accumulates — in a three-week session, every late-stage turn is paying for all the earlier history.

    Failure Mode 4: Non-executable spec plus a two-agent handoff with no anchors

    A specification that only describes “what to build” without describing “how we know it’s done” hands all architecture decisions over to the model. When two agents take turns, the second one sees only a code diff and a (possibly stale) spec document; it cannot see why the first agent made the choices it made. The result is an implicit architectural rewrite at every handoff, with nobody accountable for overall consistency.

    3. The Governance Model: Replace “Trust the Agent” With “Verify the Agent”

    There is one core principle: an agent’s own account is not evidence of completion. Only externally executable evidence is.

    3.1 Make the spec executable

    Rewrite acceptance criteria from natural language into commands and assertions, so that “passing” has an objective determination:

    1. # task-spec.yaml — the single source of truth handed to the agent
    2. goal: Add a "daily usage reflection" capability that aggregates the previous day's logs and produces skill candidates
    3. inputs:
    4. - path: logs/usage/*.jsonl
    5. outputs:
    6. - path: skills/pending/{date}.md
    7. invariants: # no implementation may violate these
    8. - No new outbound calls to public inference services
    9. - Every skill file must contain four sections: name / description / steps / examples
    10. - New dependencies must be added to the dependency manifest with a pinned version
    11. acceptance: # acceptance = executable commands, not adjectives
    12. - make lint
    13. - make test-unit # must all use local stand-ins
    14. - make contract-test # replay recorded data to verify external contracts
    15. - make coverage-check # line coverage on new code >= 70%
    16. budget:
    17. max_turns: 15
    18. max_cost_units: 3
    19. max_context_tokens: 200000
    20. done_definition: # proof obligations for "done"
    21. - Run logs contain a write record targeting skills/pending
    22. - The new skill is loaded by the registry and appears in the /skills list output
    23. - All acceptance commands above exit with code 0

    The requirement that the skill “can be loaded by the registry and appear in the list” is the direct antidote to Failure Mode 1: it forces the agent to submit runtime evidence, not just files.

    3.2 Use hard isolation instead of “remember not to touch production”

    Isolation cannot rely on convention; it has to rely on code structure. The recommended pattern is “clients created by a factory + injected stand-ins”:

    1. # clients/factory.py
    2. from typing import Protocol
    3. class InferenceClient(Protocol):
    4. def complete(self, prompt: str) -> str: ...
    5. def build_client(env: str) -> InferenceClient:
    6. if env == "prod":
    7. return RemoteInferenceClient.from_env()
    8. if env == "test":
    9. return ReplayClient(cassette_dir="tests/cassettes") # recorded replay
    10. return EchoClient() # local deterministic stand-in

    Add a network guard underneath as a backstop, in case any “bypass the factory and construct directly” code sends a request:

    1. # tests/conftest.py
    2. import socket
    3. import pytest
    4. ALLOWED_HOSTS = {"inference.internal.mock"}
    5. @pytest.fixture(autouse=True)
    6. def forbid_real_egress(monkeypatch):
    7. real_getaddrinfo = socket.getaddrinfo
    8. def guarded(host, *args, **kwargs):
    9. if host not in ALLOWED_HOSTS:
    10. raise RuntimeError(f"Unit tests must not reach external hosts: {host}")
    11. return real_getaddrinfo(host, *args, **kwargs)
    12. monkeypatch.setattr(socket, "getaddrinfo", guarded)

    Supporting engineering conventions:

    • No real credentials appear anywhere inside the test process; credentials are injected as short-lived tokens and are valid only for the production runtime identity;
    • Production API quotas and rate limits are configured independently, with a fully separate quota pool for testing and development;
    • All outbound calls are written to structured logs (destination, latency, token counts, cost estimate) and fed into a common logging service and monitoring alerts, with “a production domain appearing in the test environment” triggering an alert directly.

    3.3 Cost guardrails: wrap a budgeter around the agent loop

    1. class BudgetExceeded(RuntimeError):
    2. pass
    3. class Budget:
    4. def __init__(self, max_turns: int = 15, max_cost: float = 3.0):
    5. self.max_turns, self.max_cost = max_turns, max_cost
    6. self.turns, self.cost = 0, 0.0
    7. def before_turn(self):
    8. if self.turns >= self.max_turns:
    9. raise BudgetExceeded(f"Exceeded max turns: {self.max_turns}")
    10. if self.cost >= self.max_cost:
    11. raise BudgetExceeded(f"Exceeded cost limit: {self.max_cost}")
    12. def record(self, prompt_tokens: int, completion_tokens: int, unit_price: float):
    13. self.turns += 1
    14. self.cost += (prompt_tokens + completion_tokens) / 1000 * unit_price

    Engineering essentials:

    • Exceeding the budget means failure and handing control back — do not automatically “try once more”;
    • Split long tasks into independent sessions, each carrying only the necessary context, to avoid monotonically accumulating history;
    • Replace “stuff the whole repository into the context” with retrieval-based context supply that pulls only the relevant file fragments;
    • Cache reusable results: specs, interface contracts, and lint results are all good cache candidates;
    • Tier the models: formatting, test writing, and comment updates go to lightweight models; architecture decisions go to high-capability models;
    • Validate the method on a small number of samples before opening it up to the full codebase.

    3.4 Single-agent ownership plus architecture decision records

    The answer to an unanchored handoff is not “pick a stronger model” but making decisions explicit:

    • Single ownership: one refactoring task is owned by exactly one executor within a given time window; other executors act only as read-only reviewers;
    • Architecture decision records (ADRs): every non-trivial choice gets a one-page markdown document covering context, candidate options, the reason for the choice, rejected options, and affected modules. This is the only trustworthy context at handoff time;
    • Small PRs: no single PR exceeds roughly 400 lines of change, stays focused on one intent, and can be rolled back independently;
    • Contract tests first: record a set of real response samples for each external dependency, then judge all subsequent changes against replaying those samples. Internal implementation can be refactored freely while external behavior stays pinned.

    3.5 Tiered autonomy: not all code deserves the same degree of independence

    Tier Scope Autonomy level Gate
    L0 Docs, comments, test additions, dependency upgrades Fully automatic CI passing is enough to merge
    L1 Non-core modules, utility functions, data processing Automatic + sampled review CI + daily human spot check
    L2 Scheduler, runtime, state management Suggestion + human implementation Every PR requires human review
    L3 Authentication/authorization, billing, data deletion paths Human implementation Two-person review + security review

    The criterion is simple: if something goes wrong, can you detect and roll it back within 10 minutes? If not, reduce the autonomy level.

    3.6 CI gates: surface “fake completion” before merge

    1. [lint] → [static type check] → [dependency audit] → [unit/contract tests]
    2. → [coverage gate] → [architecture constraint check] → [smoke run]

    The “architecture constraint check” deserves separate mention: use dependency-rule tooling to declare rules such as “the scheduling layer must not reference the remote client directly” and “skill modules must not reference runtime internals.” If the agent takes a shortcut and wires things directly just to make it pass, the build fails immediately. This is earlier and cheaper than an after-the-fact code review.

    4. A Reusable Checklist

    Before opening up any long-running autonomous work, confirm each item:

    1. Are all acceptance criteria commands and assertions rather than descriptive adjectives?
    2. Can the tests pass 100% in an offline environment?
    3. Is it physically impossible for the test process to obtain production credentials?
    4. Are there three hard ceilings — turn count, context length, and dollar amount — that interrupt the run when exceeded?
    5. Is runtime evidence required (log writes, successful registration, API responses) rather than just the existence of files?
    6. Is there an automated architecture constraint check?
    7. Is every PR small and independently revertible?
    8. Is each key decision carried by an ADR rather than relying on conversation memory?
    9. Does production have its own quota, rate limits, and alerts?
    10. Is there an agreed mandatory cadence for how often a human must read the code?

    The last item matters most. Agents keep producing things that look reasonable, and the cost of reading code does not go down just because a person did not write it. The three weeks in this case were fundamentally the accumulated cost of delayed verification. The workable approach is to set a fixed cadence — for example, at the end of each work unit and before each merge — to have a human read the core-path code, spreading verification cost evenly instead of paying it back all at once at the end.

    5. Conclusion: An Agent Is a Capacity Multiplier, Not a Responsible Party

    The real lesson of this case is not “agents are unreliable” but this: agents amplify the existing flaws in your engineering system. The environment distinctions that people used to simply remember, the architectural consistency that review used to catch, the mental model maintained by “I know which part is unfinished” — all of it breaks under autonomous execution, because the person who held that tacit knowledge is no longer in the loop.

    The effective response is not to use agents less, but to make tacit constraints explicit: executable specs, hard environment isolation, hard budget ceilings, evidence-backed completion, recorded decisions, and small, revertible changes. When those guardrails are in place, the output speed of an agent is a net gain. When they are missing, speed only accelerates the bill and the rework in step.