Every AI system accumulates bugs over time. Runtime errors. Edge-case regressions. Memory entries that drift from what is actually true. A human engineer usually catches these in code review or incident postmortems — then the cycle repeats. We got tired of the cycle. So we built a loop that closes it automatically, and we open-sourced the result at github.com/muninnravenbot-ux/agent-self-improvement.
This post walks through every stage of that loop, explains why each stage exists, and names the specific failure modes each one prevents. It is not a research paper — this runs in production on the same agent system we build client automations on.
Stage 1: gathering signals that actually matter
Most self-improvement proposals start with external feedback — user ratings, thumbs-down buttons, helpfulness scores. We start with the agent's own error log, because that is the only signal that never lies. A runtime exception is a verified fact. A user rating is an opinion. When you are deciding whether to automatically apply a patch, you want facts.
The signal gatherer reads from four sources in parallel: the structured error log (exception type, stack frame, frequency), the agent's reasoning output where it flagged its own uncertainty, the diff of recently merged changes, and the memory store's own staleness markers. Each source produces a structured entry — not raw log lines, but typed records with a severity, a location, and a reproducibility count. Anything seen fewer than twice gets held; single-occurrence noise is not worth an automated patch.
The reproducibility filter is not a nice-to-have. Without it the loop will burn compute chasing one-off environment quirks and produce patches for conditions that will never recur.
Stage 2: applicability review before touching a single file
The gathered signals go to an LLM review pass. The question is not "write a fix" — it is "is this signal actually caused by code we own, and is an automated fix appropriate here?" That distinction matters enormously in practice.
Some errors are upstream library regressions. Some are environment-specific (a missing env var in staging that is set in production). Some are policy failures — the agent did something it should not do, and the fix is a behavior change, not a code change. Pushing an automated code patch for any of these would make things worse.
The reviewer returns one of three verdicts: patch (owned code, clear root cause, automatable), escalate (needs a human or a policy decision), or defer (worth watching but not acting on yet). Only patch verdicts proceed. The other two go into a human-readable queue. This is intentional: the loop is meant to handle the mechanical majority and surface the hard remainder cleanly, not to pretend every problem is solvable without judgment.
Stage 3: drafting the fix in an isolated worktree
Once a signal is cleared for patching, the loop checks out an isolated git worktree — a fresh directory pointing at the same repository but in its own branch. The main working tree is never touched during this phase. This is not just hygiene; it is a hard correctness requirement.
If the patch drafting process crashes, runs too long, or produces garbage, the main tree is completely unaffected. There is no partial-write state to clean up. The worktree branch simply gets deleted. Running the patch in-place would mean that any failure leaves the agent in an inconsistent state — and an AI agent in an inconsistent state is significantly worse than one that failed cleanly.
The drafter is given the signal record, the relevant source files, and a strict constraint: touch only the files implicated by the signal. No opportunistic cleanups. No "while I'm here" refactors. The diff must be minimal and traceable to the specific error. This constraint is enforced structurally — the drafter only has write access to files the signal analysis identified.
Stage 4: smoke-testing with the real build tool
This is the stage that most self-healing proposals skip or paper over, and it is the stage that makes everything else trustworthy.
The test that never runs in production is a false guarantee. We learned this directly: an earlier version of the loop ran tsc --noEmit as its smoke test, declared the patch valid when it passed, and merged. The type-checker passed. The runtime exploded. TypeScript's type system does not execute code; it proves types. A patch can be type-correct and functionally broken.
The smoke test now runs the actual build command — bun build in our case, whatever produces the artifact that actually runs in production in yours. Then it launches the agent process itself under a test fixture that replays the exact message sequence that originally triggered the error. The test passes only when the agent completes that sequence without the error. Anything less is not a test; it is a hope.
If the smoke test fails, the worktree branch is deleted, the signal is escalated to the human queue with the failed test output attached, and the loop moves on. No merge happens. No state is corrupted.
Stage 5: gated merge, not auto-merge
A patch that passes the smoke test still does not merge automatically. It opens a pull request and pings a guardian — in our setup, a human approver for anything touching core agent logic, and an automated guardian for peripheral tooling. The distinction matters: core logic changes have blast radius across the whole agent. A bug in the tool that formats output is contained.
The guardian receives a structured summary: signal record, root cause analysis, diff, smoke test output, and the reproduction fixture result. That is enough context to approve or reject in under two minutes. The goal is not to eliminate human judgment — it is to make every human decision cost two minutes rather than two hours of archaeology.
For the automated guardian path, the merge gate enforces two hard rules: the diff must touch fewer than five files, and the patch must not modify any file that was also modified in the last 24 hours by a human commit. Both rules exist to prevent the loop from merging into an active development area and creating conflicts that waste more time than the loop saves.
Stage 6: memory consolidation that never drops critical entries
After a successful merge, the loop updates the agent's memory. This is not just logging "patch applied." It is a structured consolidation step that answers three questions: what was the error pattern, what was the fix, and what invariant does this establish going forward?
The third question is the one that pays compounding returns. Once the loop has established that "the build tool for this project is bun build, not tsc", every future patch attempt for this project will use the right tool. The loop does not re-learn the same lesson twice.
Memory consolidation has its own protection rule: critical entries — anything tagged as a safety constraint, a gating rule, or a verified production fact — can only be modified by the consolidation step, never silently overwritten. The loop logs a warning and escalates if a patch's consolidation would contradict a critical entry. This prevents a subtle class of failure where the loop "improves" itself into a state where it bypasses its own safety gates.
What the loop does not do
Worth being explicit: this loop does not replace code review for features. It does not apply speculative improvements. It does not change agent policy or behavior rules without human approval. Those are the three categories where "the agent made this change automatically" is not a defensible answer.
What it handles is the mechanical majority: reproducible runtime errors with clear root causes in owned code. That category is surprisingly large in a production agent. And once it is handled automatically, engineering attention concentrates on the cases that actually need judgment.
The design principle underneath all of it
Every stage of this loop is designed around one principle: nothing gets promoted to production without a verified artifact of correctness. Not "the LLM thinks this is right." Not "the types check out." A real build. A real replay. A real diff in a real pull request. The moment you accept a symbolic check — a type-check, a lint pass, an LLM confidence score — as a substitute for the actual artifact, you have reintroduced the original problem: a guarantee that exists only in theory.
That principle also explains why the isolated worktree, the gated merge, and the critical-entry protection exist. Each one is a concrete enforcement of the same idea: the loop is only as trustworthy as its weakest verification step.