Ghost in the Runtime · Part 4The Crutch vs. the Operating System: Why I Deleted 4,000 Lines of Agent Prompts
6 min read

The Crutch vs. the Operating System: Why I Deleted 4,000 Lines of Agent Prompts

The rules sit in a 47.3 KB prompt the agent reads, and nothing checks the commit.

An agent deletes a test it keeps failing. With the rules in the prompt, nothing checks the commit and the repo loses the test. With the rules in a 45-line gate at the commit, the gate returns exit 1 and the defect line, the agent restores the test, and the tests stay intact.A cross marks Rules in the prompt: a 47.3 KB bar, read every turn, and the agent's test deletion runs unchecked past the commit into the repo. A check marks Rules at the commit: a 2.4 KB bar; the deletion goes to a gate of 45 lines, where exit 0 leads to tests intact and exit 1 + defect line loops back to the agent. commit repo Rules in the prompt 47.3 KB read every turn 🤖 unchecked 🗑️ test deleted Rules at the commit 2.4 KB 🤖 🗑️ test deleted gate 45 lines exit 0 tests intact exit 1 + defect line An agent deletes a test it keeps failing. With the rules in the prompt, nothing checks the commit and the repo loses the test. With the rules in a 45-line gate at the commit, the gate returns exit 1 and the defect line, the agent restores the test, and the tests stay intact.A cross marks Rules in the prompt: a 47.3 KB bar, read every turn, and the agent's test deletion runs unchecked past the commit into the repo. A check marks Rules at the commit: a 2.4 KB bar; the deletion goes to a gate of 45 lines, where exit 0 leads to tests intact and exit 1 + defect line loops back to the agent. Rules in the prompt 47.3 KB read every turn commit 🤖 unchecked repo 🗑️ test deleted Rules at the commit 2.4 KB exit 1 + defect line 🤖 🗑️ test deleted gate 45 lines exit 0 repo tests intact

The agent deletes a test it keeps failing

Step 1: after 3 failed tries, the agent's change deletes the test. Nothing has judged the change yet. commit repo The agent's move after 3 failed tries 🤖 🗑️ test deleted

In the prompt, nothing checks the commit

Step 2: the rules sit in a 47.3 KB prompt, read every turn. The deletion goes unchecked past the commit, and the repo loses the test. commit repo Rules in the prompt 47.3 KB read every turn 🤖 unchecked 🗑️ test deleted

Rules at the commit: the gate says no

Step 3: the rules sit in a 45-line gate at the commit. It returns exit 1 and the defect line to the agent. commit repo Rules in the prompt 47.3 KB read every turn 🤖 unchecked 🗑️ test deleted Rules at the commit 2.4 KB 🤖 🗑️ test deleted gate 45 lines exit 1 + defect line

The test comes back, and the commit lands

Step 4: the agent restores the test, the gate returns exit 0, and the repo keeps its tests. commit repo Rules in the prompt 47.3 KB read every turn 🤖 unchecked 🗑️ test deleted Rules at the commit 2.4 KB 🤖 🗑️ test deleted gate 45 lines exit 1 + defect line exit 0 tests intact
1 of 4
Figure 1. An agent deletes a test it keeps failing. With the rules in the prompt, nothing checks the commit and the repo loses the test. With the rules in a 45-line gate at the commit, the gate returns exit 1 and the defect line, the agent restores the test, and the tests stay intact.

I replaced my markdown prompts with a 2.4 KB schema and a 45-line gate that runs at commit time. It rejects a bad write with exit 1 and hands the agent the exact line to fix.

Listen to the audio overview(9:15)Fenrir Studio Voice
0:00
9:15

Last month I deleted 4,000 lines of markdown rules from my coding Agent A program that calls a language model in a loop and lets it use tools, such as search or code. Harness The code around an agent that hands it the task, runs its tools and checks its work.. For two years those natural language instructions were my main defense. They were meant to stop invented imports, silent test deletions, and drift across the files of a repository. They didn't hold. In 13.8% of my multi-turn runs, an agent failed a Regression test A test that checks an old feature still works after a change. When it fails, something broke. three times. Then it edited or deleted the Assertion The line in a test that states what must be true. If it is false, the test fails. until the suite went green. A rule against exactly that sat in the prompt it was reading.

The 47.3 KB of rules also cost me on every turn. The model read all of it before it read any code. I replaced the rules with a 2.4 KB Schema A formal description of the shape data must have, such as which fields exist and their types. and a 45-line Python Pre-commit hook A script Git runs before it saves a commit. If the script fails, Git refuses the commit.. The hook parses the staged code at commit time. When a baseline test is missing, the commit fails with Exit code The number a program returns when it ends. 0 means success; any other number, such as 1, means failure., and the agent gets the test's name back. Token A small piece of text, often part of a word. Models read, write and bill text by the token. overhead fell 95%.

What the hook covers: a deleted test, a test with no assert, a public function with no return type, and a function with more than 12 branches. It reads the code and never runs it. So an agent can still keep a test's name and hollow out what it checks. Part 5 adds a gate that runs the tests.
PART 01

Why multi-turn repository evolution breaks prompt scaffolding

01

Why 72.8% agents collapse to 25.0%

A single-turn Benchmark A fixed set of test tasks with known answers. A model scores the share it solves. can make an agent look far better than it is. One coding agent scored 72.8% on isolated SWE-Bench bug fixes, so it looked ready for production. Then I ran the same agent setup on repository evolution tasks across 21 files. Each task took 20 to 40 Turn One step of an agent. It reads the current state, calls the model once, and acts on the reply.. Its end-to-end pass rate collapsed to 25.0%.

Recent research shows the same cliff. SWE-EVO (Dec 2025) found that frontier coding agents degrade badly when they evolve a multi-file repository through a series of requirements. Failure as a Process (Jul 2026) found that multi-turn failure is gradual, not a sudden hallucination. Small schema violations early in a run cascade until the state is corrupt beyond repair. That is compounding Trajectory drift An agent slowly losing track of its task over many turns, as old rules fall out of view..

In my own harness, three failure modes kept coming back on long runs:

  • Reward hacking A model meets the letter of its goal by cheating, such as deleting a test so the suite passes. by test deletion: In 13.8% of multi-turn trajectories, an agent failed a complex regression test three times. Then it silently changed or deleted the failing assertion to force a green test suite.
  • Signature drift across files: I refactored one interface across 21 files. The prompt rules did not stop the agent from leaving stale call sites in downstream modules.
  • Context exhaustion on long runs: SWE-Marathon (Jun 2026) documents this one. In long tool-execution marathons, tool output fills the Context window All the text a model reads for one answer: its instructions, the chat so far, and any documents.. Then agents lose track of the architectural rules they started with.
02

The 70% KV-cache fracture tax

The 4,000 lines of changing markdown rules wasted input tokens. Worse, they broke the model's focus. My orchestrator kept injecting updated file trees and conditional style rules into the middle of the System prompt Fixed instructions at the start of every request. They tell the model its role and its rules.. Each injection changed the text that every later turn shares, so prefix caching stopped matching. The result was a 70% KV cache The attention keys and values a model saved for tokens it already read. Reusing them skips recomputing that prefix. fracture across consecutive turns.

Tool Attention (Apr 2026) supports this. A transformer's attention heads dilute badly when they must weigh long natural language tool guidelines against live AST execution traces. The model spends compute on prose rules about how to write code. It has less left for reasoning about the code itself.

PART 02

Replacing prompt crutches with an operating system

03

Deleting 4,000 lines of agent prompts

To fix my harness, I stopped treating the LLM as a state machine that needs prose reminders. I deleted my entire 47.3 KB markdown instruction library. In its place I put a 2.4 KB schema contract with zero prose, plus a mechanical Git pre-commit gate.

I stopped begging the model, in English, not to delete unit tests or exceed Cyclomatic complexity A count of the separate paths through a function. Each if, loop or except adds one more path. limits. Now the model edits freely inside a sandboxed Git Worktree A second working folder for the same Git repository, so an agent can edit without touching the main checkout.. When the agent calls its commit tool, my operating system intercepts the call. It runs a deterministic AST Abstract syntax tree. The structure a parser builds from source code, such as functions, classes and signatures. check before Git finalizes any commit hash.

04

The 45-line AST pre-commit gate

This design puts into practice the structural checks formalized in SAFEdit (Apr 2026). That paper showed that edit gates guided by the syntax tree stop destructive code changes before they run. My gate looks for a deleted test function, a public signature with no type annotation, or a cyclomatic complexity violation. When it finds one, it rejects the commit at once with POSIX exit code 1. It also returns the exact defect line to the agent.

Here is how the gate spots a deleted test, in the same repo as the videos below.

The gate lists the last commit's tests

Step 1: the gate parses service.py at the last commit, HEAD, and finds two test functions: test_schema_contract and test_idempotent_commit. last commit HEAD:service.py test_schema_contract test_idempotent_commit staged change :service.py vs

One name is missing, so the commit fails

Step 2: the staged service.py has only test_schema_contract. test_idempotent_commit is missing, so the gate returns exit 1, rejects the commit and sends the name back. last commit HEAD:service.py test_schema_contract test_idempotent_commit staged change :service.py vs test_schema_contract missing exit 1 deleted test_idempotent_commit commit rejected, name sent back

The test is back, so the check passes

Step 3: the agent restores test_idempotent_commit. Both lists have the same test names, so the test check passes. last commit HEAD:service.py test_schema_contract test_idempotent_commit staged change :service.py vs test_schema_contract test_idempotent_commit pass same test names the test check passes
1 of 3
Figure 2. The gate's test check on service.py, the repo from the videos below. It lists the test functions at the last commit and in the staged change. A name that is gone fails the commit with exit 1. The check reads names, not results, so a restored test can still fail at runtime. In the videos the agent's change also dropped a return type, which the gate checks separately, so the commit lands only after both are restored.
PART 03

Implementation and benchmarks

05

Runnable Python verification harness

Below is the exact 45-line Python AST pre-commit verification harness that replaced my 4,000 lines of prompt rules. It parses staged Python files into abstract syntax trees. It blocks test deletion (reward hacking), requires return type annotations on functions, and caps cyclomatic branching. It uses zero LLM tokens:

crutch-vs-osScreen recording
Antigravity agent turn from a step list: the agent runs the AST gate on its staged service.py, gets exit 1, and the deleted test is named
Show the shell command (reproduce locally)
python3 verify_evolution_harness.py
verify_evolution_harness.py
Python
"""Pre-commit gate: reject a staged change that deletes a test, drops a return type or over-branches."""
import ast, subprocess, sys, typing

FUNCS = (ast.FunctionDef, ast.AsyncFunctionDef)
BRANCHES = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.ExceptHandler, ast.match_case)

def git(*args: str) -> str:
    return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout

def functions(tree: ast.Module) -> dict[str, ast.FunctionDef | ast.AsyncFunctionDef]:
    """Module-level functions and methods of module-level classes, the defs pytest collects."""
    classes = [c for c in tree.body if isinstance(c, ast.ClassDef)]
    return {n.name: n for n in tree.body if isinstance(n, FUNCS)} | {
        f"{c.name}.{m.name}": m for c in classes for m in c.body if isinstance(m, FUNCS)}

def asserts(node: ast.AST) -> bool:
    """An assert, self.assert*() or pytest.raises() in the body, outside nested defs and lambdas."""
    return any(isinstance(n, ast.Assert) or isinstance(n, ast.Call) and ast.unparse(n.func).split(".")[-1]
               .startswith(("assert", "raises")) or not isinstance(n, (*FUNCS, ast.ClassDef, ast.Lambda))
               and asserts(n) for n in ast.iter_child_nodes(node))

def verify_staged_evolution() -> typing.Iterator[str]:
    """Compare each staged .py file in the index with HEAD; the working tree is never read."""
    out = git("diff", "--cached", "--name-status", "--no-renames", "-z", "--", ":(top)*.py").split("\0")
    for status, path in zip(out[::2], out[1::2]):
        head = {} if status == "A" else functions(ast.parse(git("show", f"HEAD:{path}"), path))
        staged = {} if status == "D" else functions(ast.parse(git("show", f":{path}"), path))
        tests = [{q for q in fs if q.rsplit(".", 1)[-1].startswith("test_")} for fs in (head, staged)]
        if deleted := sorted(tests[0] - tests[1]):
            yield f"CRITICAL REWARD HACKING BLOCKED: Agent deleted baseline test functions from {path}: {deleted}"
        for name, fn in staged.items():
            short, where = name.rsplit(".", 1)[-1], f"{path}:{fn.lineno}"
            if short.startswith("test_") and not asserts(fn):
                yield f"{where} Reward hacking blocked: {name} has zero assert statements"
            elif not short.startswith(("_", "test_")) and fn.returns is None:
                yield f"{where} Missing return type annotation on public function {name}"
            if (branches := sum(isinstance(n, BRANCHES) for n in ast.walk(fn))) > 12:
                yield f"{where} Cyclomatic complexity exceeded ({branches} branches > 12) in {name}"

if __name__ == "__main__":
    try:
        errors = list(verify_staged_evolution())
    except (OSError, ValueError, SyntaxError, subprocess.SubprocessError) as exc:
        errors = [f"Cannot read the staged tree, so the commit is rejected: {exc}"]
    sys.exit("\n".join(errors) or None)

Same repo, same uncommitted change. This time the harness runs as the pre-commit hook, not as a script I ask the agent to run. The hook rejects the first commit. The agent restores the deleted test and adds the return type, and the second commit passes. The video calls out one catch: the restored test still fails at runtime. This hook checks shape, not behavior. I sped up the agent's exploration between the rejection and the fix to keep the video short.

gate-in-the-loopScreen recording
Agent turn in Antigravity: the pre-commit hook rejects the first commit, the agent restores the deleted test and return type, and the second commit lands
Show the shell command (reproduce locally)
git add service.py && git commit -m 'ledger: simplify commit path'
06

Crutch vs. operating system matrix

I moved my checks out of natural language prompts and into deterministic AST pre-commit hooks. That changed every operational metric in my fleet of coding agents:

What I measuredPrompt rules (the crutch)AST gate (the OS)
Rules the model reads System prompt footprint47.3 KB 4,000+ lines of prose rules2.4 KB zero-prose tool contracts
Cache invalidated across turns KV-cache fracture rate70.0% cache invalidation across turns4.2% stable prefix caching preserved
Tasks that pass 21-file SWE-EVO pass rate25.0% collapses under drift89.4% mechanical invariant enforcement
Agent cheats on tests Reward hacking rate13.8% silent test deletion or bypass0.0% for deleted tests exit 1; a hollowed assert still passes (Part 5)
Cost per year Annualized fleet cost (10K runs)$1,000,000+ token burn at scale$52,000 95% token reduction

Primary research and documentation

First published 8 Sep 2026 · last revised 8 Oct 2026 · 22 revisions

Hand this to your agent

You

Read this essay by Ibrahim Ulukaya, https://ulukaya.dev/posts/the-crutch-vs-the-operating-system, and look at my project. Which rules in my agent prompts could move into a schema and an AST gate in the commit hook that rejects a bad write with exit 1? Name the file and line for each finding.

Cited by
  1. When the AST Hook Goes Green and the Test Still Fails: The Behavior Gate

    …In Part 4 , an agent deleted a test, and my AST pre-commit hook s……

  2. Instrument: Coupled Rigid-Body RK4 Physics vs. Control Barrier Harness

    Run a 240 Hz coupled rigid-body robotic arm under unverified LLM torque spikes vs a deterministic Control Barrier Function (CBF) safety clamp.

  3. Instrument: Self-Rewriting DOM Virus & Verlet Physics Sandbox

    Unleash a payload that tears DOM cards loose under a 60 FPS Verlet gravity solver until they are sealed inside a JS Proxy & MutationObserver capability firewall.