Cortex · the lab

Does it actually work?

4 independent runs + 1 control · Cortex frozen at v1.0-eval · Claude Haiku 4.5

Cortex claims it can learn a codebase's house rules from your corrections, and keep only what measurably helps. So it was run for real: a working repository, real Claude Code sessions, the documented commands. Then it sat an exam it had never seen.

22%  →  77%

Tasks solved on held-out work (scenarios never shown to Cortex while it learned), without what Cortex evolved and with it: +55.0 points, 95% CI +34.1 to +74.1, pooled over the four task families that have a house rule. The control family, which has none, moved by −0.8.

4
evaluation runs
56
scenarios, 30 held out
6,462
rollouts recorded
18
items kept
9
candidates buried
$1,196
programme cost
What these numbers are. The evaluation ran Cortex from scratch four times (R1 to R4) on a frozen version, plus one control run with the same sessions and /evolve switched off. Every headline number is Claude Haiku 4.5, with the optional judge (Jev) off. The intervals are bootstraps over runs and tasks, not over single rollouts.

What the worked examples are. Steps 2 to 7 follow real files from an earlier development run, D0. Cortex was still being fixed while D0 ran, so its numbers are shown beside the evaluation and never pooled with it.
the whole pipeline, step by step↓
1 the bench

A repository with rules a model cannot guess

To measure whether a skill helps, you need work where being right and being house-style are two different things, and a machine that can tell them apart.

shopkit, a small invoicing toolkit

70 unit tests, no dependencies, eight commits by two fictional teammates. Its house rules are written in CONTRIBUTING.md, so a careful agent can find them, and they are enforced by make lint. Nothing in the repository reveals the answers. Left alone, Claude opened that file in 0.5% of 595 exam rollouts: the rules can be discovered, and they are not.

The linter is the judge. Not a human, not Claude. Every number on this page comes from a script returning pass or fail.

The work comes as 56 scenarios in five families: 26 for learning and 30 held out for the exam, six per family. Each card shows how often Claude passes that family's exam with no help.

A
Changelog
Every user-visible change gets a CHANGELOG.md line.
builds it, forgets the entry
alone: 0% pass
B
Money
Integer cents via the billing helpers. Never float, round(), Decimal.
fixes rounding with float()
alone: 26% pass
C
Exporters
A new exporter needs a registry entry, a docs row and a golden file.
writes the plugin, stops
alone: 2% pass
D
Control
No house rule at all. Plain bugs that need plain fixes.
◆ the deliberate trap
alone: 100% pass
E
Time
Read the clock only through shop.clock.
calls date.today()
alone: 55% pass
why family D exists

D is a trap for Cortex itself. There is no rule to learn, so the right behaviour is to produce nothing. A system that invents skills to look busy would create one here. Across the four evaluation runs Cortex kept zero items for D. It proposed one once (a rule about a text utility, in R3) and the gates killed it: it won no run and lost four.

2 a session

Everything starts with you correcting Claude

Not with a prompt, a config or a framework. Here is the first session of the development run, as its records show it.

  1. a teammate

    A failing test arrives

    QA commits a test: tax at a fractional rate like 8.25% comes out wrong. The bug is real: the code throws away everything after the decimal point.

    shop/billing/tax.py the bug
    def tax_for(amount: Money, rate: str) -> Money:
        whole = int(rate.split(".")[0])   # "8.25" becomes 8
        return Money(amount.cents * whole // 100)
  2. you

    You ask Claude to fix it

    your request “CI has been red since QA added tests/test_tax_rates.py: tax_for() gives the wrong tax for fractional rates like 8.25%, and it doesn't round the way the tests expect. Can you fix it?”

    An ordinary request. No mention of house rules: you would not think to mention them, and that is the point.

  3. Claude · Haiku 4.5

    It fixes the bug and breaks the house style

    The arithmetic is now correct and the new test passes. It is still wrong for this codebase: the fix reaches for Decimal, and money here is integer cents.

  4. CI

    The linter catches it

    make lint message shortened
    $ make lint
    shop/billing/tax.py: money: Decimal()
      amounts are integer cents; use the helpers in shop/billing/rates.py
    CI is red, although the new test passes
  5. you

    You correct it. This is the only teaching signal in the system

    your reply, shortened “The test passes now, but CI is still red: make lint rejects float arithmetic in shop/billing/. […] Please redo it with the helpers in shop/billing/rates.py.”

    Claude redoes it with the house helper. Session over: the kind of exchange you have every day and forget by Friday.

    the corrected fix house style
    + from .rates import percent_of
    +     return percent_of(amount, rate)
the raw material

Cortex never invents a rule. It only amplifies a correction you actually made. In the development run, 16 of the 26 sessions contained one (17 corrections in all), and those are the entire input to everything that follows.

3 /harvest · the task

Freeze that moment into something replayable

A session is a story, and you cannot measure a story. /harvest turns it into a task: a checkpoint you can drop any agent back into, as often as you like.

.evolve/tasks/01/  the real one, from the session above
task.yaml
base_sha: 2b5173a, the commit before Claude touched anything. Rewind here and the bug is alive again.
prompt.txt
What you asked, word for word. Replayed identically every time, so runs are comparable.
check.sh
The judge. It runs the test that was failing and the project's own linter for the rule you had to correct. Exit 0 means solved, and it says nothing else.
fix.patch
The reference fix: proof the task can be solved at all.
notes.md
One line on what was actually wrong, and the area it belongs to.
the detail that makes it work

check.sh tests the correction too. A check that ran only the unit test would have passed the Decimal version, and would be blind to the exact thing we are trying to teach. The correction is what turns a test into a house-rule test.

The development run ended with 27 tasks and 17 lessons. Now there is a ruler.↓
4 /evolve · the candidate

Write one item aimed at what keeps recurring

Cortex reads the lessons, finds something that happened more than once, and writes a proposal. It is staged, not installed: nothing is live yet.

Two billing corrections → one rule

Round 1 left two lessons about the same thing: money arithmetic in shop/billing/. Cortex wrote a rule scoped to exactly those files. Its name, its wording and its scope were all chosen by the model, unattended. This is the file, word for word:

.evolve/candidate/use-billing-helpers/RULE.md
---
paths:
  - "shop/billing/**"
---

# Billing arithmetic

- NEVER parse string rates or percentages with
  `int(rate.split(".")[0])` or similar patterns that
  truncate decimals. Use `percent_of(amount, rate)` for
  percentage calculations and `times(amount, rate)` for
  currency conversions from the `.rates` module instead.
staged, not live

This file sits in a holding area. Nothing has been proved yet, and Cortex is not allowed to install it because it sounds reasonable, which it does.

5 the measurement

The same jobs, twice: with it and without it

This is the heart of Cortex, and it is the oldest idea in science: a controlled comparison, where exactly one thing differs between the two worlds.

First, what a rollout is

One rollout = one complete headless Claude session, alone in a throwaway clone of the repository, rewound to a task's broken commit, handed that task's prompt, with no human present. When it stops, check.sh scores it 1 or 0.

Your working copy is never touched. The lab's records hold 6,462 of them.

base arm cand arm your skills as they are today the same + the candidate repo rewound to base_sha repo rewound to base_sha same prompt · same tasks same prompt · same tasks × 3 runs each · check.sh scores × 3 runs each · check.sh scores difference = the candidate
Only one file differs between the two sides. So a change in the score comes from that file or from run-to-run luck, never from a different prompt or a different day.

Why every task is run three times

Claude is not deterministic: the same prompt can pass once and fail the next time. So each task is run several times per arm and its score is the share that passed: 0, 0.33, 0.67 or 1. One run proves nothing. Three runs begin to separate an effect from a coin flip.

1 · the screen
a cheap first filter
12 rolloutsabout $1about a minute
  • only the tasks the candidate was written for
  • 2 runs per task, per arm
  • may say “worth confirming” or “dead”
  • may never approve anything
→
2 · the confirm
the verdict that counts
138 rolloutsabout $15about 10 min
  • every task in the suite
  • 3 runs per task, per arm
  • this is where damage elsewhere shows up
  • says KEEP, KILL or RECHECK

Typical sizes late in the development run: a screen on 3 target tasks, a confirm on 23 tasks, 12 rollouts at a time.

why two stages

A screen looks only at the tasks the candidate targets, so it cannot see what the candidate breaks. That is why it may kill but never approve. It is also optimistic: for one candidate the screen measured a gain of 1.5 and the confirm 0.67. The screen throws away losers for a dollar; the confirm decides.

6 the gates

Five questions. One “no” and it is buried.

The confirm produces numbers; the gates turn numbers into a verdict. cortex score applies them, and it contains no model of any kind. Each red box names a real candidate that gate stopped, and the run it came from.

1
Did it help at all?
At least one task has to pass more often. “It feels cleaner” is not a result.
enforce-shop-clock · D0
gain 0: it helped nothing
2
Did any single task get much worse?
A drop of more than one run in three on any task kills it, even if the total improved.
text-utilities-correctness · R3
one task lost two of its three runs
3
Did it break work that already passed?
The protected set: every task that passed every run without it must still pass every run. This is the gate that does the real work.
shop-clock-access · R3
net +8 runs, but a task that always passed lost one, twice
4
Is the gain bigger than noise?
A win of one lucky run is not a win. The net has to reach two runs.
exporter-checklist (a skill) · R3
net gain of 1 run, under the floor of 2
5
Did the model even load it?
A candidate that was never loaded cannot have caused the difference, so the difference was noise.
changelog-updates · D0
offered in 10 rollouts, opened in 0
and two more answers

RECHECK. At three runs per task, one unlucky run on a task that always passed looks exactly like a broken task. So when the only failure is a per-task drop, those tasks are measured again before anything is buried. In R1 a clock rule lost one run on a protected task, the re-run did not repeat it, and the rule was kept. In R3 the same thing happened to another clock rule, the drop repeated, and it was killed.

RERUN. “The measurement itself was not trustworthy, so decide nothing”: a sweep that was cut short, too many rollouts lost to infrastructure, or no task in the sweep that could have shown a gain. It stops a tooling failure from being recorded as “this idea is bad”.

7 two twins

You cannot tell by reading

These two files came out of the development run, for the same house rule, with the same name and the same scope. One is buried. One was kept. Read them and try to tell which is which.

✗ buried.evolve/graveyard/
---
name: check-changelog-on-shop-edits
description: When editing shop code, verify
  CHANGELOG.md is updated for user-visible
  changes
paths:
  - "shop/**"
---

When you modify files in the shop directory,
verify that the change is user-visible. If it
introduces a new feature, bug fix, or
user-facing behavior change:

1. Check whether CHANGELOG.md has been updated
   under `[Unreleased]`
2. If the change is user-visible but
   CHANGELOG.md has not been updated, update it
   by adding a brief entry under `[Unreleased]`
3. Add the CHANGELOG.md update in the same
   commit as the feature implementation
On its screen it was offered in 6 of 6 rollouts and opened in 0. No task improved.
✓ kept.claude/skills/
---
name: check-changelog-on-shop-edits
description: Before finishing changes to shop/
  code, verify CHANGELOG.md was updated
paths:
  - "shop/**"
---

1. After modifying shop/ code, check if
   CHANGELOG.md needs an entry.
2. If you added a user-visible feature or fix
   to shop/ code, ensure CHANGELOG.md has an
   [Unreleased] entry describing it.
3. If the [Unreleased] section is missing, add
   it at the top of the file above the latest
   version header.
4. Do not skip CHANGELOG updates even for
   internal refactoring — if users would notice
   the change, it needs documenting.
Kept. On D0's own exam its family went from 0% to 67%.

Same name. Same paths. Similar instructions. Reading them, nobody could say which one helps. The sweep could: with the first, nothing changed; with the second, the changelog tasks started passing.

Whether a skill helps is not visible in its text.
It is visible in a measurement.

Two reasonable drafts, one verdict each, and the verdicts differ. That is the whole case for measuring instead of reviewing.

why did the second one work?

The development run's guess was the description, the one line Claude always sees. Even the kept version was opened in only 7 of the 123 exam rollouts where it was offered, yet its family improved. The evaluation tested that idea directly, by replacing skill bodies with filler, and the answer was inconclusive. What did repeat: in all four evaluation runs the loop ended up encoding this duty as a rule, not as a skill.

why the dead one is kept

The graveyard is not a bin of old drafts. Nothing in it loads, and it costs nothing. It exists so that the same experiment is not run twice, and so that after a model upgrade you can ask the question again with the old result to compare against. Today the sweep also refuses a candidate whose name is already buried, so a retry takes a new name: these two would now be a v1 and a v2.

8 the ledger

Four runs, started from nothing

Each evaluation run began with an empty harness and worked through the same sessions. Each one learned all four house rules, and none kept anything for the control family.

What each run kept, house rule by house rule

House ruleR1R2R3R4D0 (development)
A changelog ruleshop/**ruleshop/** 2 rulesshop/** and 3 named filesruleshop/** gated skillshop/**
B money ruleshop/billing/**ruleshop/billing/** 2 rules5 named filesruleshop/billing/** ruleshop/billing/**
C exporters gated skillshop/plugins/**ruleshop/plugins/** ruleshop/plugins/*_export.pygated skillshop/plugins/** gated skillshop/plugins/**
E time ruleshop/**ruleshop/** rule3 named filesruleshop/** nothing kept
D control nothingnothingnothingnothing nothing
cycles · kept · buried 9 · 4 · 09 · 4 · 111 · 6 · 410 · 4 · 4 17 · 3 · 7

Across the four runs: 39 cycles, 18 items kept, 9 candidates buried. In 14 of those cycles Cortex found nothing recurring and proposed nothing, which costs no rollout. A cycle that changes nothing is a correct outcome.

The runs agree on what to learn and less on how wide to make it. Three runs chose broad globs; R3 named individual files, and its narrow money rules then loaded in none of its 30 exam rollouts on money. The gates judge a scope only on the tasks already harvested: they cannot see that it is too narrow for work they have not met.

the hardest one to swallow

In the development run, shop-clock-usage scored the largest gain of that run (+4.33) and was buried anyway, because one unrelated task that always passed dropped from 100% to 67%, and the drop repeated when it was measured again. That run made four attempts at the clock rule and kept none. Each evaluation run did land one, which is why a single run is never the whole story.

9 the exam

Work it had never seen

Everything so far is training: Cortex optimised against those tasks. The exam uses 30 fresh scenarios, six per family, never shown to /harvest or /evolve. Each was run five times without the evolved items and five times with them, in each of the four runs.

Pass rate on held-out work

Grey: the bare repository. Blue: with what that run evolved. Pooled over the four evaluation runs; the line under each name is the 95% interval of the gain.

A changelog+91.0 to +100.0
0% → 97%
+97.0
B money+17.5 to +88.3
26% → 79%
+53.3
C exporters+25.0 to +73.3
2% → 52%
+50.0
E time+1.7 to +60.0
55% → 82%
+26.7
D control−5.0 to 0.0
100% → 99%
−0.8
A+B+C+E+34.1 to +74.1
22% → 77%
+55.0
without the evolved items with them
read the small rows too

D is flat because it must be: a control family with no house rule, left alone. E gains least, with the widest interval: without help Claude already passes 55% of the clock tasks, so there is less room. A benchmark where every row jumped would be the suspicious one.

Is the difference real?

The noise floor. The same bare repository was built twice and measured as if one copy were a treatment. The two differ by −3.3 points (interval −7.3 to 0.0). The gains above are far outside that.

The development run agrees. Its three items, measured again on the same 30 scenarios, gain +42.5 points on the rule families. On its own smaller exam of 15 scenarios, D0 had shown 36% → 76% over all five families.

10 the finding

A rule is loaded. A skill has to be chosen.

Every rollout records what was available to the model and what it actually used. That gap explains most of what the loop kept.

ItemRunOfferedUsed
the 16 kept rulesR1 to R4rulewhenever a matching file was readevery time
exporter-checklistR1gated skill4848 / 48
exporter-checklistR4gated skill4840 / 48
complete-exporter-setupD0gated skill2720 / 27
check-changelog-on-shop-editsD0gated skill1237 / 123

Counts are exam rollouts with the evolved items installed. A rule does not have to be noticed: when the agent reads a matching file it is simply there. A skill has to be chosen. It gets chosen when its description names the task at hand (“when implementing a new exporter”) and mostly ignored when it names a side duty (“verify the CHANGELOG was updated”).

So across the four evaluation runs the loop kept 16 rules and 2 skills. It is not expressing a taste. A candidate that never loads is stopped by gate 5, so the same advice survives as a rule and dies as a skill.

what to take from it

A side duty tied to an area (a changelog line, a docs row, a house helper to use) belongs in a rule on that area. A procedure for the task itself can be a skill, with a description that names the task.

11 honesty

What this shows, and what it does not

✓ What the lab shows

  • On unseen work, what Cortex evolved raised the pass rate on the four rule families from 22% to 77%, in four independent runs.
  • The control family was left alone: −0.8 points, and no item was kept for it.
  • Every run learned all four house rules, mostly as path-scoped rules that cost nothing until their area is touched.
  • It refused its own ideas when they failed: 9 candidates buried, and 14 cycles that proposed nothing.
  • None of 20 planted placebos and none of 5 planted harmful items was kept.
  • /prune never deleted an item that had not fired.
  • The whole programme cost $1,196, plus $155 for the development run. On Haiku 4.5 a rollout cost about $0.125.

△ What it does not show

  • One synthetic repository, four lintable rules. A machine can check them. Rules that need judgement were not tested at all.
  • The gain belongs to a weak model. See the card below.
  • The gates were not shown to be calibrated. The agent opened none of the 20 placebos, so only gate 5 was exercised, and of 4 known-good items the gates kept 2.
  • /prune matched 6 of 8 planted expectations.
  • On a real repository nobody here built (structlog), Claude was rarely corrected, nothing recurred, and the loop kept nothing. That shows when the loop has work to do, not whether what it learns transfers.
  • Scripted corrections. The lab's user never gets confused or changes their mind.
  • No check before a sweep predicted its verdict. Neither how many tasks a glob reaches nor a judge's relevance score separated what was kept from what was buried.
  • A sandbox is a git clone, so a rollout could in principle read a later fix from history. Measured: 2 of 1,809 loop rollouts did.

The same harness under a stronger model

R1's four items, measured again on the exam under claude-sonnet-5. Grey: the bare repository. Blue: with the items.

A changelog
94% → 100%
+6
B money
100% → 100%
+0
C exporters
78% → 89%
+11
E time
83% → 89%
+6
D control
100% → 100%
+0
A+B+C+E−2.8 to +13.9
89% → 94%
+5.6

Without any harness the stronger model already passes 89% of the rule-family tasks, so there is little left to gain: +5.6 points, where the same harness gained +66.7 under Haiku. One /prune pass after the upgrade found 2 of the 4 items removable. Read that with care: a single pass at three runs per task cannot tell a small effect from none.

where to run it

The loop is worth most where the model is weakest, and it should be asked again after every model upgrade. That is what /prune is for.