~/beer-and-code
▪ next event AI Engineering Lab 4ª Edição — Harness Engineering para Agentes Inteligentes · 14—15 Nov · 14 e 15 de novembro, das 09h às 13h — ao vivo via Google Meet save your seat ›
~ / tutorials / self-improving-harness-regression-gate $
Tutorials

Self-improving harness: how to let the agent rewrite its own rules without breaking what works

LS Lucas Souza · · 12 min read
Self-improving harness: how to let the agent rewrite its own rules without breaking what works

A self-improving harness is an agent harness that reads its own failures, proposes changes to its own rules, prompts and tools, and only keeps what demonstrably got better. The model weights don't change. What evolves is the code and text around the model.

Sounds like the dream of anyone who has spent a Sunday reading agent traces.

But there's a number that should be taped to the monitor of anyone building one.

In a September 2026 paper, the validation gate of one of these harnesses rejected 383 changes proposed by the agent itself. 211 of them, 55%, fixed the failure that triggered them and broke a case that used to work. More than half of the rejected "improvements" were bug swaps.

In this tutorial you'll build, in PHP, the full loop of a self-improving harness with the piece that makes it engineering instead of roulette: the regression gate. Rules versioned as files, failure mining, minimal fix proposals, validation against protected cases, promotion with provisional authority, and pruning of whatever stopped helping.

TL;DR

  • What it is: a loop where the agent edits its own harness (rules, prompt, policy) based on failures, with a gate that blocks any change that regresses a case that used to work.
  • Stack/Models: PHP 8.3 + Laravel + Claude Messages API (any model with structured output works), git to version the rules.
  • Cost/Access: paid API key. Every proposal requires running the validation suite, so the loop's cost is dominated by reruns, not by the proposal.
  • Useful link: Self-Healing Harness for Runtime Oversight of Agent Self-Modification.

Context: why self-improving harnesses became a trend (and why the gate is what matters)

Until last year, improving an agent was manual. You read the trace, eyeballed that a rule was missing, edited the prompt, ran it again and hoped.

In 2026 this became an automated loop, and research converged on the same design. The main examples:

  • Self-Harness, from Shanghai AI Lab, runs three stages: it mines weaknesses from failed traces, proposes minimal edits tied to a failure mechanism, and only promotes an edit if it improves without measurable degradation on held-out tasks. One rule the agent learned was a "loop breaker" after 50 tool calls. The reported relative gain on Terminal-Bench-2.0 was 33% to 60%.
  • Meta-Harness has a proposer agent read, through the filesystem, the code, scores and traces of every previous candidate. On online text classification, it beat the state of the art by 7.7 points using 4x fewer context tokens.
  • RRSI, from Google Research, added what was missing: a pruner that removes changes that are "too small, too expensive, or no longer useful". The final harness came out with 30% fewer policy tokens.
  • Self-Healing Harness is where the 55% number comes from. A proposed rule gets provisional authority and only persists across episodes if it improves the failure that triggered it "without regression beyond a fixed margin on protected cases". Result: the version with the harness beat the version without it in 16 out of 16 pairs on AppWorld, Terminal-Bench and τ²-Bench.

Notice what they all have in common. Nobody trusts the agent's self-assessment. Every change goes through an external check before it becomes part of the harness.

That's what separates a harness that evolves from one that rots, and it's the kind of piece we'll build and deliberately break, live, at AI Engineering Lab 4th Edition, November 14 and 15, 9am to 1pm (Brasília time): the agent proposes the rule, the gate decides in front of everyone.

Prerequisites

  • [ ] An agent already running a tool-calling loop. If you don't have one yet, see how to build your first harness in Laravel (Portuguese).
  • [ ] A suite of cases with deterministic verification: tests that pass or fail, checkable final state, comparable output. Without it, there is no gate.
  • [ ] Traces saved per run (tool calls, errors, final answer).
  • [ ] The harness rules directory inside a git repository.

If you already built the agent improvement loop (Portuguese), this post is its evolution. There, the check accepted a change when the aggregate score went up. Here you'll see why that isn't enough.

▪ Clã Beer and Code

A tutorial shows you the way — in the Clã you build alongside us. A live class every week, real AI Engineering projects, next to people already in production.

Join the Clã

Hands-on: the self-improving harness loop with a regression gate

The loop has six steps:

rodar suíte → minerar falhas → propor 1 regra mínima → validar (gate) → provisória → promover ou descartar
                                                                         ↑
                                                         poda periódica remove regra inútil

(run suite → mine failures → propose 1 minimal rule → validate (gate) → provisional → promote or discard; periodic pruning removes useless rules.)

Step 1: harness as versioned files

The first decision is the most important: the agent never edits the harness code directly. It edits a directory of text rules, which the harness loads into the system prompt. That limits the blast radius and makes everything auditable by diff, the same idea Google's Harness2 and Anthropic's Dreaming follow.

harness/
├── base-prompt.md          # escrito por humano, o agente não toca
├── rules/
│   ├── active/             # regras promovidas
│   │   └── 0007-nao-repetir-comando-identico.md
│   └── provisional/        # regras em período de teste
└── suites/
    ├── protected.json      # casos que passam hoje e NÃO podem regredir
    └── holdout.json        # casos que o proposer nunca vê

base-prompt.md is written by a human and off-limits to the agent; active/ holds promoted rules; provisional/ holds rules on probation; protected.json holds cases that pass today and must not regress; holdout.json holds cases the proposer never sees.

The loader is trivial, on purpose:

class HarnessLoader
{
    public function systemPrompt(bool $includeProvisional = true): string
    {
        $parts = [file_get_contents(base_path('harness/base-prompt.md'))];

        $dirs = ['harness/rules/active'];
        if ($includeProvisional) {
            $dirs[] = 'harness/rules/provisional';
        }

        foreach ($dirs as $dir) {
            foreach (glob(base_path("$dir/*.md")) as $file) {
                $parts[] = file_get_contents($file);
            }
        }

        return implode("\n\n---\n\n", $parts);
    }
}

Step 2: mine failures from traces

Run the suite, collect the failed cases and group them by failure mechanism, not by case. Ten cases that failed because the agent repeated the same command are one failure.

class FailureMiner
{
    public function mine(array $results): array
    {
        $failures = array_filter($results, fn ($r) => ! $r['passed']);

        // Cheap classification first: deterministic signals in the trace
        return collect($failures)->groupBy(function ($r) {
            $calls = collect($r['trace'])->where('type', 'tool_call');

            return match (true) {
                $calls->count() > 50 => 'loop_sem_progresso',
                $calls->duplicates(fn ($c) => json_encode($c['input']))->isNotEmpty() => 'comando_repetido',
                str_contains($r['error'] ?? '', 'timeout') => 'timeout_de_ferramenta',
                default => 'nao_classificado',
            };
        })->map(fn ($group, $mechanism) => [
            'mechanism' => $mechanism,
            'cases' => $group->pluck('id')->all(),
            'sample_trace' => $group->first()['trace'],
        ])->values()->all();
    }
}

The nao_classificado (unclassified) bucket goes to an LLM for classification later. Start with what's cheap.

Step 3: propose one minimal rule per failure

The proposer receives one failure mechanism and returns one short rule. One change per proposal is what lets the gate know what caused the improvement or the regression.

class RuleProposer
{
    public function propose(array $failure, string $currentPrompt): array
    {
        $response = Http::withHeaders([
            'x-api-key' => config('services.anthropic.key'),
            'anthropic-version' => '2023-06-01',
        ])->post('https://api.anthropic.com/v1/messages', [
            'model' => config('services.anthropic.model'),
            'max_tokens' => 1024,
            'system' => <<<'TXT'
                You improve an agent's harness. You receive ONE failure mechanism and the
                current prompt. Propose ONE short operational rule (max. 3 sentences) that
                addresses that mechanism. Do not rewrite existing rules. Do not mention
                specific cases or benchmark data: the rule must generalize.
                TXT,
            'tools' => [[
                'name' => 'submit_rule',
                'description' => 'Submits the proposed rule.',
                'input_schema' => [
                    'type' => 'object',
                    'properties' => [
                        'slug' => ['type' => 'string', 'description' => 'kebab-case'],
                        'rule' => ['type' => 'string'],
                        'rationale' => ['type' => 'string'],
                    ],
                    'required' => ['slug', 'rule', 'rationale'],
                ],
            ]],
            'tool_choice' => ['type' => 'tool', 'name' => 'submit_rule'],
            'messages' => [[
                'role' => 'user',
                'content' => json_encode([
                    'mechanism' => $failure['mechanism'],
                    'trace' => array_slice($failure['sample_trace'], -30),
                    'current_prompt' => $currentPrompt,
                ]),
            ]],
        ])->throw()->json();

        return collect($response['content'])->firstWhere('type', 'tool_use')['input'];
    }
}

Two guardrails are born right here. tool_choice forces structured output. And the instruction forbids citing benchmark cases, which is exactly the kind of proposal RRSI's critic exists to block: a rule that memorizes the test instead of generalizing.

Step 4: the regression gate

This is the piece that matters. The candidate rule only passes if it meets three conditions at the same time:

  1. The cases that triggered the failure improved.
  2. No protected case regressed. Not the average. Case by case.
  3. The holdout score, which the proposer never saw, didn't drop beyond a margin.
class RegressionGate
{
    public function __construct(
        private SuiteRunner $runner,
        private float $holdoutMargin = 0.02,
    ) {}

    public function evaluate(array $rule, array $failure): GateResult
    {
        $baseline = $this->runner->snapshot();             // current results, case by case
        $candidate = $this->runner->withExtraRule($rule);  // same suite, rule applied

        $triggerFixed = collect($failure['cases'])
            ->filter(fn ($id) => $candidate->passed($id))
            ->count();

        if ($triggerFixed === 0) {
            return GateResult::reject('did not fix the failure that motivated the rule');
        }

        $regressions = collect($baseline->passedIds('protected'))
            ->reject(fn ($id) => $candidate->passed($id))
            ->values();

        if ($regressions->isNotEmpty()) {
            return GateResult::reject('regressed protected cases: ' . $regressions->implode(', '));
        }

        if ($candidate->score('holdout') < $baseline->score('holdout') - $this->holdoutMargin) {
            return GateResult::reject('holdout dropped beyond the margin');
        }

        return GateResult::accept($triggerFixed, $candidate->score('holdout'));
    }
}

Why case by case and not the average? Because the average hides exactly the 55%. A rule that fixes 4 cases and breaks 2 raises the average and passes a "score went up" check. In a harness running on its own for weeks, those small breaks pile up until nobody knows why the agent behaves the way it does anymore.

Agents are non-deterministic. Run each protected case more than once (3 is a good start) and only treat consistent failures as regressions, otherwise the gate will reject everything because of noise.

Step 5: provisional authority, then promotion

Passed the gate? It's still not promoted. It goes to rules/provisional/ and runs in production, or in the next suite round, for a while. That's the Self-Healing Harness design: provisional authority first, persistence only with evidence.

class RulePromoter
{
    public function stage(array $rule): void
    {
        $path = "harness/rules/provisional/{$rule['slug']}.md";
        file_put_contents(base_path($path), $rule['rule']);

        Process::run(['git', 'add', $path]);
        Process::run(['git', 'commit', '-m', "harness: provisional rule {$rule['slug']}\n\n{$rule['rationale']}"]);
    }

    public function promote(string $slug): void
    {
        // Promotion becomes a PR, not a direct commit: a human approves what goes to active/
        Process::run(['git', 'checkout', '-b', "harness/promote-{$slug}"]);
        Process::run(['git', 'mv', "harness/rules/provisional/{$slug}.md", "harness/rules/active/{$slug}.md"]);
        Process::run(['git', 'commit', '-m', "harness: promote {$slug}"]);
        Process::run(['gh', 'pr', 'create', '--fill']);
    }
}

The commit carries the proposer's rationale. Three months from now, when someone asks why rule 0007 exists, the answer is in git log.

Step 6: pruning, because rules age too

A harness that only grows turns into the bloated harness (Portuguese) the next model no longer needs. Every so often, run an ablation: remove each active rule, one at a time, and run the suite.

foreach (glob(base_path('harness/rules/active/*.md')) as $file) {
    $without = $runner->withoutRule(basename($file));

    if ($without->score('protected') >= $baseline->score('protected')
        && $without->score('holdout') >= $baseline->score('holdout')) {
        $promoter->proposeRemoval(basename($file, '.md')); // also via PR
    }
}

If removing the rule doesn't hurt anything, it's dead weight. It's RRSI's pruner, pocket edition.

Limitations and gotchas

  • No deterministic verification, no gate. The Self-Harness authors themselves advise against the pattern in subjectively evaluated domains like medical, legal or critical infrastructure decisions. LLM-as-a-judge as the only yardstick is self-assessment under another name. The same problem shows up in the progress mirage of long-running agents (Portuguese).
  • The agent can learn what it shouldn't. The paper Practice Makes Unsafe tested 21 combinations of agent and self-evolution method: all of them wrote unsafe skills, and evolution increased harm on malicious tasks in 17 of them. Include safety cases in the protected suite and review what gets promoted.
  • Prompt injection becomes a permanent rule. A trace with malicious content can lead the proposer to write a bad rule. Sanitize traces before passing them to the proposer and keep base-prompt.md out of its reach.
  • Cost. Each proposal runs the suite twice, plus the repetitions for non-determinism. Propose in batches, validate in parallel and cap proposals per round.
  • Overfitting. Without a holdout the proposer never sees, the harness memorizes the suite.

Quick FAQ

Is a self-improving harness the same as fine-tuning?

No. Fine-tuning changes the model weights. A self-improving harness changes what's around the model: rules, prompt, tools, memory. It's cheaper, reversible with git revert and auditable by diff. Every system cited here keeps the model frozen.

What's the difference between self-improving and self-healing?

Self-healing usually refers to runtime recovery: retry, fallback, circuit breaker, checkpoint, breaking out of loops. Self-improving means learning between runs and changing the harness for the next one. The Self-Healing Harness paper mixes both: it detects, heals and validates the fix before persisting it.

Can I let the gate approve on its own, without a human?

For provisional rules, yes, as long as the protected suite is good. For permanent promotion, start with a PR and human approval. Anthropic made the same call with Dreaming: the job writes a new, separate memory store, and you decide whether to adopt it.

How many protected cases do I need?

Start with cases that pass today and would hurt if they broke: critical flows, safety cases, bugs already fixed. A few dozen well-chosen cases beat hundreds of generic ones. Every regression that slips through becomes a new protected case.

Conclusion

A self-improving harness isn't the agent programming itself. It's an engineering loop with four pieces: real failures as input, minimal changes, a case-by-case gate and pruning.

Remove the gate and those 211 changes, which fixed one thing and broke another, would have made it into the harness without anyone noticing.

The next step of this trend is already in the papers: models trained only to edit harnesses, like Harness-R1, and co-evolution of model and harness. What doesn't change is the gate. The more autonomy the loop gets, the more the gate becomes the piece you really need to write well.

Lucas Souza
Written by
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

▪ Clã Beer and Code

There is no shortage of content. What is missing is someone to untangle it: what matters now is how to implement it the right way. In the Clã you get that live, every week, with people who have already filtered out the noise.

Join the Clã
Meet the Clã Beer and Code
playing