Building AI Models Without Gradient Descent

Models are usually trained or adapted. Here we use a frontier AI, Fable 5.1, to write every weight of a 2.1M-parameter transformer from a spec, with no training, and we compare that with training the same network and with hand-editing a pretrained one (Qwen3.5-0.8B).

November 8, 2026 · 56 min read

TL;DR

A network Fable wrote from a spec came out ahead of the same network trained on 223,930 labels (sealed test), and held up far better on logs from a different generator (sealed OOD). Hand-editing a pre-trained model helped it, but not nearly enough to compete with our generated models.

  • What I tried. Anthropic's Fable (claude-fable-5-1) wrote every weight of a 2,128,932-parameter transformer from a written spec, starting from zeros, with no optimiser or training set. I compared that with training the same architecture by gradient descent, and with Fable hand-editing a small pretrained language model, Qwen3.5-0.8B.
  • The task. Detection from agent session traces of 'doom loops' and other degraded agent performance. The model should label what a coding agent is doing right now (making progress, recovering, stalled or looping) from synthetic logs. Scores are macro-F1, from 0 to 1 with all four classes weighted equally. Each score names its split; "sealed" sets were hashed in advance and scored once, and OOD (out of distribution) logs come from an independently written generator.
  • Written: 0.744 (sealed test) with zero labels. After one revision informed by 200 labelled windows: 0.957 (sealed test) and 0.908 (sealed OOD).
  • Trained on 223,930 labelled windows: 0.923 (sealed test) but 0.488 (sealed OOD), flagging 34.5% of healthy OOD windows as stuck, against 6.8% for the written network.
  • Written with no tools: 0.326 (sealed test). A second session, with tools, fixed one bias and rebuilt the decision logic: 0.959 (sealed test), 0.765 (sealed OOD). A third, given labels from the second generator, scored 0.750 on four families it never saw (held-out OOD), ahead of every earlier network, though not like for like.
  • Hand-edited Qwen, with the same 200 labels: 0.419 (val, quick look), +0.196 over base Qwen on the same sessions. The revised written network scored 0.954 (full val; cited, not paired).
  • Cost. Writing the transformer cost US$64.62 of model usage (API-equivalent) and about 3.8 hours of Fable sessions, about as long as the largest training run took. Priced as a rented Azure H100, that run would cost at most US$26.22, but it needed 223,930 labels. There was one authoring run per phase, and the edited Qwen was scored on validation only due to time / compute limits.

Four routes side by side, each with its macro-F1 on sealed test and on sealed OOD, at the same size. Write: Fable writes every weight of a transformer from a spec, 200 labelled windows; 0.957 sealed test, 0.908 sealed OOD (0.744 sealed test with no labels). Write without tools, then repair with tools: 0.959 sealed test, 0.765 sealed OOD after the repair; the draft scored 0.326 sealed test. Train: the same architecture trained on 223,930 labelled windows; 0.923 sealed test, 0.488 sealed OOD. Modify: Fable hand-edits Qwen3.5-0.8B with 200 labelled windows; 0.419 on a validation quick look, no sealed or OOD score.
Writing held up off distribution; training fell and editing lagged
0.957
sealed test
0.908
sealed OOD
With no labels: 0.744 sealed test
Fable writes every weight from a spec
200 labelled windows
0.959
sealed test
0.765
sealed OOD
Draft before the repair: 0.326 sealed test
Fable types every weight, then repairs it with tools
0 labelled windows, then 200
0.923
sealed test
0.488
sealed OOD
Same architecture, trained by gradient descent
223,930 labelled windows
0.419
val, quick look
Not measured
no sealed or OOD score
Fable hand-edits Qwen3.5-0.8B
200 labelled windows
Figure 1. Macro-F1 (0 to 1) of each route's best version on sealed test and sealed OOD, each set scored once; synthetic agent logs. Modify was scored only on a validation quick look.
Show the numbers
RouteHeadline macro-F195% CISplitOther numbersLabelsCost
Write (v0.3 B, revised transformer)0.908[0.905, 0.912]sealed OOD0.957 [0.953, 0.961] sealed test; zero labels (v0.3 A): 0.744 sealed test200 windows, one revisionUS$64.62 model usage (API-equivalent)
Write without tools (v0.5 r1, then v0.5 B)0.765[0.761, 0.769]sealed OOD (v0.5 B, after the repair)0.959 [0.956, 0.961] sealed test (v0.5 B); the no-tools draft (v0.5 r1, zero labels): 0.326 [0.319, 0.333] sealed test. The repair session had tools: it fixed one bias and rebuilt the decision logic0, then 200 windowsUS$16.70, then US$40.75 model usage (API-equivalent)
Train (v0.3 C-all, trained transformer)0.488[0.478, 0.497]sealed OOD0.923 [0.918, 0.929] sealed test; false alarms 6.1% sealed test, 34.5% sealed OOD223,930 windowsabout 3.75 h on 2 CPU threads (Intel Mac)
Modify (v0.4 WSP-B, Qwen3.5-0.8B hand-edited)0.419[0.397, 0.441]val, quick look (1,000 sessions)+0.196 [+0.165, +0.226] over base Qwen (val, quick look, paired; base 0.223 [0.202, 0.243] on the same 1,000 sessions); base 0.212 (full val, 3,804 sessions); 8,192 of 752,393,024 weights changed; no OOD measured200 windowsUS$62.39 model usage (API-equivalent), both rounds
Synthetic agent logs. Write, Write without tools and Train: sealed sets, opened once per version. Modify: validation only; no OOD measured. Sealed test: 3,996 sessions; sealed OOD: 2,000 sessions (Codex's independent generator); each scored once per version. Val quick look: 1,000 val sessions; full val: 3,804 sessions; both exclude the 200 disclosed sessions. 95% session-bootstrap CIs. Costs are model usage (API-equivalent).
Source: artifacts/reports/v03_sealed.json @ 67fc2b1; artifacts/reports/v04_val_quick_wspb.json @ 48acfc6; artifacts/reports/v05_sealed.json @ cba5a80; weight_program/v0_4/wsp-b/BUILD_RECORD.json @ d177712
Bar chart of macro-F1 for every version. Written by Fable: no labels 0.744 sealed test and 0.815 sealed OOD; revised with 200 labels 0.957 and 0.908. Written with no tools: draft 0.326 and 0.292; repaired 0.959 and 0.765. Trained on 223,930 labels: 0.923 sealed test and 0.488 sealed OOD. Qwen on a val quick look: unedited 0.223, edited 0.330, revised 0.419.
Revised written networks scored above training on both sealed sets
  • Sealed test
  • Sealed OOD
Macro-F1 of every version
Figure 2. Macro-F1 (0 to 1) of each version: the transformers on sealed test and sealed OOD, each set scored once, and Qwen on a val quick look, since it has no sealed scores. The trained bar is the largest run; the warm-up MLPs (Experiment 1) and the third round (held-out sets) are in Results at a Glance.
Show the numbers
VersionExpLabelsSealed testSealed OODVal, quick look
Written, no labels200.744 [0.734, 0.754]0.815 [0.806, 0.823]n/a
Written, revised with 200 labels32000.957 [0.953, 0.961]0.908 [0.905, 0.912]n/a
No-tools draft400.326 [0.319, 0.333]0.292 [0.283, 0.300]n/a
Draft repaired with tools and 200 labels52000.959 [0.956, 0.961]0.765 [0.761, 0.769]n/a
Base Qwen, uneditedn/a0not scorednot scored0.223 [0.202, 0.243]
Qwen, edited with no labels90not scorednot scored0.330 [0.309, 0.350]
Qwen, edit revised with 200 labels10200not scorednot scored0.419 [0.397, 0.441]
Trained, 590 labels (mean of 3 seeds)85900.3470.241n/a
Trained, 2,216 labels (mean of 3 seeds)82,2160.4500.287n/a
Trained, 9,114 labels (mean of 3 seeds)89,1140.7070.305n/a
Trained, 37,285 labels837,2850.7550.400n/a
Trained, 223,930 labels8223,9300.9230.488n/a
Macro-F1 with 95% session-bootstrap CIs where one exists; trained rows with several seeds are seed means (their CIs are in artifacts/reports/v03_sealed.json). Each sealed set was scored once. The val quick look is 1,000 val sessions, excluding the 200 disclosed ones; the Qwen versions were never scored on a sealed set. The chart shows only the largest trained run.
Source: artifacts/reports/v03_sealed.json, artifacts/reports/v05_sealed.json, artifacts/reports/v04_val_quick_wspb.json, experiments/v0_3/logs/v03-phase-a.stream.jsonl, experiments/v0_3/logs/v03-phase-a.c1.stream.jsonl, experiments/v0_3/logs/v03-phase-b.stream.jsonl, experiments/v0_5/logs/r1.t001.stream.jsonl, experiments/v0_5/logs/r1.t002.stream.jsonl, experiments/v0_5/logs/r1.t003.stream.jsonl, experiments/v0_5/logs/r1.t004.stream.jsonl, experiments/v0_5/phase_b/logs/v05b.stream.jsonl, experiments/v0_5/phase_b/logs/v05b.c1.stream.jsonl, experiments/v0_4/logs/wsp-a.stream.jsonl, experiments/v0_4/logs/wsp-b.stream.jsonl, experiments/phase-c-trained/v0_3/32/seed0/training_log.json, experiments/phase-c-trained/v0_3/32/seed1/training_log.json, experiments/phase-c-trained/v0_3/32/seed2/training_log.json, experiments/phase-c-trained/v0_3/128/seed0/training_log.json, experiments/phase-c-trained/v0_3/128/seed1/training_log.json, experiments/phase-c-trained/v0_3/128/seed2/training_log.json, experiments/phase-c-trained/v0_3/512/seed0/training_log.json, experiments/phase-c-trained/v0_3/512/seed1/training_log.json, experiments/phase-c-trained/v0_3/512/seed2/training_log.json, experiments/phase-c-trained/v0_3/2048/seed0/training_log.json, experiments/phase-c-trained/v0_3/all/seed0/training_log.json @ 4ec8cfa
Two scatter plots of macro-F1 against the hours and the model cost each version took. The revised written network took 3.8 h of Fable sessions and US$64.62; the largest trained network took 3.8 h of training, at most US$26.22 on a rented Azure H100, but 223,930 labels. The no-tools route took 2.2 h and US$57.44; the Qwen edits 2.9 h and US$62.39. A control switches the scores between sealed test and sealed OOD.
Training took as long as writing and cost less, but needed the labels
Score on
  • Written by Fable
  • Written with no tools, then repaired
  • Trained, 590 to 223,930 labels
  • Qwen hand-edited (val, quick look)
Macro-F1
Figure 3. Each version's macro-F1 against the hours it took and against its cost; a revision's totals include its draft's. Fable's cost is model usage (API-equivalent). Training's is a ceiling: its run time on two CPU threads, priced as renting an Azure H100 VM at US$6.98 an hour. Neither includes the spec, the harness or the labels. Switch between sealed test and sealed OOD; Qwen has only a val quick look.
Show the numbers
VersionThis stepTime so farStep costCost so farSealed testSealed OOD
Written, no labels172 min2.9 hUS$42.73US$42.730.7440.815
Written, revised with 200 labels59 min3.8 hUS$21.89US$64.620.9570.908
No-tools draft58 min58 minUS$16.70US$16.700.3260.292
Draft repaired with tools and 200 labels74 min2.2 hUS$40.75US$57.440.9590.765
Base Qwen, uneditedn/a0n/aUS$0.000.223 (val, quick look)not scored
Qwen, edited with no labels111 min1.9 hUS$32.58US$32.580.330 (val, quick look)not scored
Qwen, edit revised with 200 labels61 min2.9 hUS$29.81US$62.390.419 (val, quick look)not scored
Trained, 590 labels21 min21 minat most US$2.49at most US$2.490.3470.241
Trained, 2,216 labels25 min25 minat most US$2.96at most US$2.960.4500.287
Trained, 9,114 labels43 min43 minat most US$4.98at most US$4.980.7070.305
Trained, 37,285 labels63 min1.1 hat most US$7.38at most US$7.380.7550.400
Trained, 223,930 labels225 min3.8 hat most US$26.22at most US$26.220.9230.488
Times are wall clock. Fable's are its authoring sessions, every launch included (the v0.3 first launch hit the plan's usage limit before it wrote anything; its cost is in the total too). A revision's time and cost include its draft's. Training's is the training run alone, on two CPU threads of an Intel Mac, mean over seeds. None include writing the spec, building the harness, generating labels (free here, from the simulator) or the Qwen readouts (about 5 h per val pass). Fable's cost is model usage (API-equivalent) as Claude Code reported it. Training's is a ceiling: its measured run time at the pay-as-you-go price of the smallest Azure H100 VM (Standard_NC40ads_H100_v5 (1x H100 NVL), Linux, East US, US$6.98/h, Azure retail price list, retrieved 2026-11-08), which would run the same job no slower; on the GPU it would likely take minutes.
Source: artifacts/reports/v03_sealed.json, artifacts/reports/v05_sealed.json, artifacts/reports/v04_val_quick_wspb.json, experiments/v0_3/logs/v03-phase-a.stream.jsonl, experiments/v0_3/logs/v03-phase-a.c1.stream.jsonl, experiments/v0_3/logs/v03-phase-b.stream.jsonl, experiments/v0_5/logs/r1.t001.stream.jsonl, experiments/v0_5/logs/r1.t002.stream.jsonl, experiments/v0_5/logs/r1.t003.stream.jsonl, experiments/v0_5/logs/r1.t004.stream.jsonl, experiments/v0_5/phase_b/logs/v05b.stream.jsonl, experiments/v0_5/phase_b/logs/v05b.c1.stream.jsonl, experiments/v0_4/logs/wsp-a.stream.jsonl, experiments/v0_4/logs/wsp-b.stream.jsonl, experiments/phase-c-trained/v0_3/32/seed0/training_log.json, experiments/phase-c-trained/v0_3/32/seed1/training_log.json, experiments/phase-c-trained/v0_3/32/seed2/training_log.json, experiments/phase-c-trained/v0_3/128/seed0/training_log.json, experiments/phase-c-trained/v0_3/128/seed1/training_log.json, experiments/phase-c-trained/v0_3/128/seed2/training_log.json, experiments/phase-c-trained/v0_3/512/seed0/training_log.json, experiments/phase-c-trained/v0_3/512/seed1/training_log.json, experiments/phase-c-trained/v0_3/512/seed2/training_log.json, experiments/phase-c-trained/v0_3/2048/seed0/training_log.json, experiments/phase-c-trained/v0_3/all/seed0/training_log.json @ 4ec8cfa

Ways to build a Model

When you need a model for a narrow job, there are two usual routes: collect labelled examples and train one, or take a pretrained model and fine-tune it on your dataset. Either way an optimiser sets the weights, and you steer it only indirectly, through data and fine-tuning. Conventional software isn't like that: you define the inputs, the algorithm in the middle and the output. With models, you train one or call one someone else trained, then measure its output against an eval.

Frontier models make a third route plausible. If you can write down what you want, you can ask a model to write the network's weights directly, the way you'd ask it to write a piece of software. You get back a program that sets every parameter, a document that says what each part does, and a weights file built from that program.

I wanted to know whether a frontier model can build a working network from scratch with zero training, how that network compares with a trained one, and what it costs next to the two normal routes.

Around the time I started, a team at MIT CSAIL published the same idea in another domain, "Sparks of Weight-Space Programming" (Takehi, Teoh et al., 2 November 2026). Like theirs, this post is a series of experiments, each asking one question. I tried three routes on the same task, data, labels and scoring:

  1. Write. Fable got a small transformer with all 2,128,932 weights set to zero, plus a written spec, and wrote the weights by hand. It then made one revision informed by 200 labelled windows. We also try this with zero-tools, where Fable has to 'hand' write every weight.
  2. Train. The same architecture, randomly initialised and trained by gradient descent on 590 to 223,930 labelled examples.
  3. Modify. Fable hand-edited Qwen3.5-0.8B, again with no optimiser.

The Benchmark

The task is classifying coding-agent session trajectories. I picked it because it is easy to state, hard to get right, and a simulator can generate as much labelled data as I need; observability of agent traces has also been a recent focus of mine (Trajectory) because I think it's an important part of safe deployment of agent based systems.

After every tool call, the classifier labels the latest window, the agent's last eight operations, as PRODUCTIVE (real work), RECOVERY (a bounded retry or repair), STALLED (repeating work already done) or OSCILLATING (undoing and redoing). The label describes what is happening now, not whether the session will fail.

Healthy work and loops look very similar on the surface. Two sessions can have exactly the same rhythm, an edit that succeeds and then a test that fails, four times over. The difference is that one is flipping webhook.go between the same two versions, and the other is just making edits to the same file.

Three event timelines, one tile per tool call, with a coloured band underneath giving the label of the 8-operation window ending at each call. In ops 6 to 13 the first two sessions have the same sequence, an edit that succeeds then a failing test, four times. The first flips webhook.go between two versions and its tests stay at 11/16; it is labelled OSCILLATING. The second climbs 3, 5, 8, 11 of 14 and is labelled PRODUCTIVE. The third fails 7/9 nine times and moves from RECOVERY to STALLED.
The same rhythm gets opposite labels
What only the labeller sees
Flips webhook.go between two versions
Tests stay at 11/16 on every pass
Fixes something on each pass
Passing tests climb 3, 5, 8, 11, then 14 of 14
Retries the same failing suite
The same 7/9 fails 9 times: a bounded retry, then a stall
Figure 4. One tile per tool call (E edit, T test, R read, S search, L list, P plan); a light tile is a failed test, with the tests passed beneath it. The band under each row is the label of the 8-operation window ending at that call. The words are what each call did, which only the labeller sees: fact (new information), fix (progress), undo (a file reverted), retry, repeat. Standard simulator, dev split; synthetic.
Show the numbers
Session, operationTool and targetStatusTests passed or file hashHidden effectLabel of the window ending here
Flips webhook.go between two versions, op 0search srcoknone—
Flips webhook.go between two versions, op 1read_file internal/identity/slugify.gooknone—
Flips webhook.go between two versions, op 2edit_file internal/identity/webhook.gookhb1c6547a05 > h3900d57125none—
Flips webhook.go between two versions, op 3run_tests ./internal/...:unitFAILED11/16new fact learned—
Flips webhook.go between two versions, op 4edit_file internal/identity/webhook.gookh3900d57125 > h982d5fe6e3none—
Flips webhook.go between two versions, op 5run_tests ./internal/...:unitFAILED11/16none—
Flips webhook.go between two versions, op 6edit_file internal/identity/webhook.gookh982d5fe6e3 > h3900d57125undoes an earlier edit—
Flips webhook.go between two versions, op 7run_tests ./internal/...:unitFAILED11/16noneambiguous (not scored)
Flips webhook.go between two versions, op 8edit_file internal/identity/webhook.gookh3900d57125 > h982d5fe6e3undoes an earlier editOSCILLATING
Flips webhook.go between two versions, op 9run_tests ./internal/...:unitFAILED11/16noneOSCILLATING
Flips webhook.go between two versions, op 10edit_file internal/identity/webhook.gookh982d5fe6e3 > h3900d57125undoes an earlier editOSCILLATING
Flips webhook.go between two versions, op 11run_tests ./internal/...:unitFAILED11/16noneOSCILLATING
Flips webhook.go between two versions, op 12edit_file internal/identity/webhook.gookh3900d57125 > h982d5fe6e3undoes an earlier editOSCILLATING
Flips webhook.go between two versions, op 13run_tests ./internal/...:unitFAILED11/16noneOSCILLATING
Flips webhook.go between two versions, op 14edit_file internal/identity/webhook.gookh982d5fe6e3 > h43abbdfa4cprogress (fixes something)PRODUCTIVE
Flips webhook.go between two versions, op 15run_tests ./internal/...:unitFAILED13/16nonePRODUCTIVE
Fixes something on each pass, op 0read_file lib/orders/scheduler.rboknone—
Fixes something on each pass, op 1read_file lib/orders/migrations.rboknone—
Fixes something on each pass, op 2search docsoknone—
Fixes something on each pass, op 3list_dir .oknone—
Fixes something on each pass, op 4read_file spec/orders/migrations_request_spec.rboknone—
Fixes something on each pass, op 5list_dir specoknone—
Fixes something on each pass, op 6edit_file lib/orders/timezone.rbokh299630c5f0 > hd57cb1154anone—
Fixes something on each pass, op 7run_tests spec:requestFAILED3/14new fact learnedambiguous (not scored)
Fixes something on each pass, op 8edit_file lib/orders/locale.rbokh15aeac96e1 > h6061660a46progress (fixes something)PRODUCTIVE
Fixes something on each pass, op 9run_tests spec:requestFAILED5/14new fact learnedPRODUCTIVE
Fixes something on each pass, op 10edit_file lib/orders/notifier.rbokh614ce37a11 > h8fcbc4e82cprogress (fixes something)PRODUCTIVE
Fixes something on each pass, op 11run_tests spec:requestFAILED8/14new fact learnedPRODUCTIVE
Fixes something on each pass, op 12edit_file lib/orders/notifier.rbokh8bfc68a6d0 > h7c4a473a46progress (fixes something)PRODUCTIVE
Fixes something on each pass, op 13run_tests spec:requestFAILED11/14new fact learnedPRODUCTIVE
Fixes something on each pass, op 14edit_file lib/orders/timezone.rbokhd57cb1154a > h0413cb5346progress (fixes something)PRODUCTIVE
Fixes something on each pass, op 15run_tests spec:requestok14/14nonePRODUCTIVE
Fixes something on each pass, op 16run_tests spec:featureok7/7nonePRODUCTIVE
Retries the same failing suite, op 0search srcoknew fact learned—
Retries the same failing suite, op 1read_file src/cli/rateLimiter.tsoknew fact learned—
Retries the same failing suite, op 2planoknone—
Retries the same failing suite, op 3list_dir tests/fixturesoknone—
Retries the same failing suite, op 4read_file src/cli/__tests__/rateLimiter.integration.test.tsoknone—
Retries the same failing suite, op 5list_dir .github/workflowsoknone—
Retries the same failing suite, op 6run_tests contractFAILED7/9none—
Retries the same failing suite, op 7run_tests contractFAILED7/9bounded retryRECOVERY
Retries the same failing suite, op 8read_file src/cli/__tests__/rateLimiter.contract.test.tsoknoneRECOVERY
Retries the same failing suite, op 9run_tests contractFAILED7/9bounded retryRECOVERY
Retries the same failing suite, op 10run_tests contractFAILED7/9bounded retryRECOVERY
Retries the same failing suite, op 11read_file src/cli/rateLimiter.tsokrepeats work already doneRECOVERY
Retries the same failing suite, op 12run_tests contractFAILED7/9repeats work already doneambiguous (not scored)
Retries the same failing suite, op 13search srcokrepeats work already doneambiguous (not scored)
Retries the same failing suite, op 14read_file src/cli/__tests__/rateLimiter.contract.test.tsokrepeats work already doneambiguous (not scored)
Retries the same failing suite, op 15run_tests contractFAILED7/9repeats work already doneSTALLED
Retries the same failing suite, op 16read_file src/cli/rateLimiter.tsokrepeats work already doneSTALLED
Retries the same failing suite, op 17run_tests contractFAILED7/9repeats work already doneSTALLED
Retries the same failing suite, op 18read_file src/cli/__tests__/rateLimiter.contract.test.tsokrepeats work already doneSTALLED
Retries the same failing suite, op 19run_tests contractFAILED7/9repeats work already doneSTALLED
Retries the same failing suite, op 20search srcokrepeats work already doneSTALLED
Retries the same failing suite, op 21run_tests contractFAILED7/9repeats work already doneSTALLED
Dev split, standard simulator; synthetic. Sessions s-8c4e2bce5b5ce097 (abab_oscillation), s-35f046b90b6b6352 (improving_tests), s-b85d987d455fc994 (repeated_test_failure). A window is the last 8 operations; the first label is at op 7. The highlighted window is ops 6–13. Ambiguous windows are not scored.
Source: regenerated from datasets/v0_2/MANIFEST.json @ 51c9fa8 and datasets/v0_3/MANIFEST_v0_3.json @ d08f05f (sessions s-8c4e2bce5b5ce097, s-35f046b90b6b6352, s-b85d987d455fc994; data files gitignored, rebuild byte-identically, pinned by hash); rendered with src/agent_loop_lab/v03/renderer.py @ d08f05f

The data is synthetic. Claude Opus 5.5 built the simulator that generates the standard data; Codex Sol 6.1 independently wrote a second generator for the out-of-distribution data. The model that writes the weights wrote neither. Each operation produces two things: a public log line, which the classifier sees, and a private record of what it did, which is the only source of labels. The spec's rule of thumb: "Novel but fruitless exploration is never labelled STALLED. Repetition of already-done work is what defines a stall."

That is an obvious limitation of this experiment; at some point I might rerun it with a public agent-trace dataset.

The simulator has 12 behaviour families, several of them deliberate lookalikes, and produced 20,000 sessions in three splits:

  • dev, 12,000 sessions: training and audits. No weight author saw it.
  • val, 4,004 sessions: checking during development.
  • test, 3,996 sessions: sealed, meaning hashed in advance and scored once.

Codex's generator, written without seeing the simulator's code, produced the sealed out-of-distribution (OOD) set: 2,000 sessions in 10 new families. It uses the same labeller and log format but different behaviour, and some surface details differ too: shorter gaps, zero token counts, its own vocabulary and a clock that starts at midnight.

Macro-F1 averages F1 (the harmonic mean of precision and recall) over the four classes, so ignoring a rare class is punished. Always answering PRODUCTIVE scores 0.136 (sealed test) and 0.181 (sealed OOD). The false-alarm rate is the share of healthy windows (PRODUCTIVE or RECOVERY) flagged as STALLED or OSCILLATING. Windows the labeller marks ambiguous (about 11% of val) or under-instrumented (about 3%) are not scored.

How the Experiments Were Run

First, the concession: Fable got the labelling rules as text, and the trained models got them only through examples. This I believe is an acceptable method since having the rules isn't the same as having the answers, the labels depend on hidden state. A rules engine based system Codex wrote from the same spec scored 0.417 (sealed test) and 0.415 (sealed OOD) on the warm-up benchmark.

  • Weights start at exactly zero. The builder may not use autograd, optimisers or random numbers, run the model, or solve for weights numerically.
  • No literal is longer than 256 elements, so a trained matrix can't be pasted in.
  • Every build runs twice and must produce identical output.
  • No automated search of any kind.
  • The author works in an isolated session, denied the repo, the web and subagents, and every transcript is audited. The v0.2 and v0.3 isolation was tightened for v0.5 after a review found a macOS path-alias bypass; a search of the transcripts for that path found no sign it was used.

Test and OOD hashes were committed before any network existed (51c9fa8), each evaluator refuses to run twice, and protocols were pre-registered. The sealed sets were opened once per version, three times in all. Fable never saw a sealed result, but Claude and I knew earlier results when designing later versions, so the last round was scored on fresh held-out sets.

Access matrix: six things an author could have while building, against five routes. Write (v0.3): the spec, 0 then 200 labelled windows, tools, its own network's outputs; no validation scores. Train (v0.3): no spec, 223,930 labelled windows, its own outputs and validation scores for early stopping; no tools. Modify (v0.4): the spec, 0 then 200 labelled windows, tools, metered outputs; no validation scores. No tools (v0.5 r1): the spec only. Revise (v0.5 B and the third round): the spec, 200 plus 200 labelled windows, tools and its own outputs; no validation scores. No route had the sealed test or sealed OOD sets.
No author saw the sealed sets; each route saw only this
Figure 5. What each route's author (for Train, the optimiser) had while building; a filled square means it had it. The sealed test and sealed OOD sets were hashed before any network existed.
Show the numbers
Access by route
RouteThe specLabelled windowsTools to run codeIts own network’s outputsValidation scoresSealed test and OOD
Write (v0.3)yesyes (0, then 200)yesyesnono
Train (v0.3)noyes (223,930)noyesyes (early stopping)no
Modify (v0.4)yesyes (0, then 200)yesyes (metered)nono
No tools (v0.5 r1)yesnonononono
Revise (v0.5 B, third round)yesyes (200 + 200)yesyesnono
Train has no author; its row is what the optimiser saw. Revise: 200 val windows in v0.5 B; the third round added 200 from Codex's generator.
What each route was given
RouteGivenOut of reachSealed sets
Write, v0.3 (Phase A, then Phase B with 200 windows)Packet (spec, renderer, tokeniser, model.py, zeros model); Phase B: 200 val windows with true label, network class, policy classrepo, web, subagents, dev data, val scores, sealed setsscored once after the archive (v0.3)
Train, v0.3 Phase C (gradient descent)590 to 223,930 labelled dev windows; 500 val sessions for early stoppingthe spec, sealed setsscored once (v0.3)
Modify, v0.4 (Qwen3.5-0.8B, WSP-A then WSP-B)./py loads and saves tensors (at most half of the layer weights), no forward pass; ./fwd metered, 2,000,000 tokens per phase; WSP-B: 200 val windows, label logits for at most 2 edit statesrepo, web, subagents, dev data, val scores, sealed setsnot opened in v0.4; val only
Write without tools, v0.5 r1One 73,542-character prompt, then only "Continue." (plus one fixed Claude Code output-limit notice); no toolstools, files, repo, web, all datascored once (v0.5)
Revise with tools, v0.5 B and the third roundTools; r1's network; 200 val windows; third round also 200 windows from Codex's generator (6 of 10 families, re-timed)repo, web, subagents, dev data, val scores, sealed setsB: sealed sets once (v0.5); third round: new held-out sets (2,000 OOD, 4,000 in-distribution), hashed 7 Nov, opened once
Data sets: dev 12,000, val 4,004, sealed test 3,996 and sealed OOD 2,000 sessions; synthetic. Sealed test and OOD hashes committed in 51c9fa8, before any network; opened once per version (v0.2, v0.3, v0.5).
Source: experiments/v0_3/sealed/README.md @ 2fce67f; experiments/v0_3/packet_handed_over/AUTHOR_INSTRUCTIONS.md @ e842bc3; docs/V04_WSP_KERNEL.md @ 0612987; experiments/v0_5/PROMPT_NOTES.md @ f23bcd2; experiments/v0_5/sealed/README.md @ abe8b83; experiments/v0_5/phase_c/heldout/README.md @ d23acd3; artifacts/reports/v03_sealed.json @ 67fc2b1

Agents ran experiment process:

  • Claude (Opus 5.5, via Claude Code) built and audited the harness, simulator and evaluators.
  • Fable wrote every weight and never touched the harness.
  • Codex (Sol 6.1), built the adversarial pieces, so a same-vendor model wasn't marking its own work.

My job was mostly deciding what to test and what processes to accept.

Notes: below I use plain names, with the repo's version codes in captions. In the repo, v0.2 is the warm-up, v0.3 the transformer, v0.4 the Qwen edits and v0.5 the no-tools rounds. Numbers in square brackets are 95% intervals from bootstrapping over sessions; a paired difference (two networks on the same sessions) gets its own interval. Quotes from Fable's thinking are Claude Code's summaries, not raw reasoning.

Writing a Network From a Spec

Experiment 1: Can Fable compile a policy into weights exactly?

The bet, as I wrote it in the first spec: "An LLM can directly construct sparse weights for a fixed small MLP which correctly identifies these patterns on held-out trace windows, without updating weights from examples by an optimizer." That is actually two questions in reality; Can the model write a good policy (design)? Can it put that policy into weights (compilation)?

Setup

  • Network: a warm-up MLP, 120→24→12→4: 3,256 parameter slots, all zero, reading 15 engineered features (such as repeated_intent) for each of 8 operations. Each layer is a weighted sum followed by a ReLU, which clips negatives to zero.
  • What Fable received: the spec and the model source; no data.
  • What it returned: a policy in plain Python, a weight builder and CIRCUITS.md, which says what each unit does.
  • Phases, all in one session: A-strict, with no code execution; A-selfcheck, which could run on inputs it wrote; B, one revision after seeing 200 disclosed windows.
  • Disclosed windows: 200 val windows, 50 per class, chosen by hash, labels shown once. Scores on them are in-sample.
  • Not allowed: the standing guards.
  • Scoring: sealed test and sealed OOD, once.
  • Cost: US$40.05 of model usage (API-equivalent), including a US$13.95 first response that hit the output cap without writing a file.

Written blind, with zero labels, it scored 0.667 [0.659, 0.675] (sealed test) and 0.810 [0.803, 0.817] (sealed OOD). A-strict and A-selfcheck were byte-identical: running it found nothing to fix. Compilation was exact: Fable checked the network against its own policy on about 1.9 million enumerated feature windows and found zero disagreements.

After one revision it scored 0.936 [0.932, 0.940] (sealed test) and 0.870 [0.867, 0.873] (sealed OOD), and got 195 of 200 disclosed windows right, up from 139 (in-sample). Trained by gradient descent, the same MLP matched it at about 2,200 labelled windows and reached 0.992 with 223,930 (sealed test, three-seed mean). Off distribution the trained MLP scored 0.843 (sealed OOD, three-seed mean), below the written 0.870.

How it did it. Fable wrote the policy first, "as the exact arithmetic of the circuit", then transcribed it layer by layer. Each yes/no condition became a pair of ReLUs with the threshold in the bias. Units 0 to 7, for instance, score each operation as relu(RI + SR + RF − NEU): a point each for a repeated intent, a revert and a repeated failure. With no way to run it, only a shape check on the build, it wrote: "Sparsity counts match my hand tally exactly (156 first-layer weights, 49 second-layer, 13 output)." The revision lowered the stall threshold and reordered priorities, citing window ids for each change, and left 5 windows wrong on purpose. The result sets 295 of 3,256 slots, and every unit has a name.

Node-link diagram of the warm-up network Fable wrote: an 8 by 15 input grid feeding 24 units, then 12, then 4 logits, with every non-zero weight drawn as a line whose width grows with its size. The first-layer units sit in five named circuits: redundancy evidence, resolution of an episode, recent retry and fresh-failure evidence, reversions, progress and retry bound, and polling; three second-layer units are unused.
Every one of the 295 non-zero parameters belongs to a named circuit
Run a window
Figure 6. Every non-zero weight of the revised warm-up MLP (v0.2 B) as a line whose width grows with log |w| (sign is in the tooltips); the other 2,961 of 3,256 slots are exactly zero. Pick one of four invented windows to watch it run.
Show the numbers
The revised network (B) and the zero-label network it revised (A)
ItemValue
B: non-zero parameters295 of 3,256 (274 weights, 21 biases)
B: smallest and largest |weight|1/32 to 10,000
B: macro-F1, sealed test0.936 [0.932, 0.940]
B: macro-F1, sealed OOD0.870 [0.867, 0.873]
B on the invented window "Steady progress"PRODUCTIVE (logits 0, 0, 0, 0)
B on the invented window "A retry that worked"RECOVERY (logits 0, 82.5, −163, 0)
B on the invented window "Same failure, five times"STALLED (logits 0, 0, 20, 0)
B on the invented window "Undo, redo, undo"OSCILLATING (logits 0, −20,000, −2,000, 40)
B: layer-1 circuit A, redundancy evidencer1 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats); r2 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats); r3 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats); r4 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats); r5 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats); r6 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats); r7 = r_s = relu(RI_s + RF_s + SR_s - NEU_s - 3*EXT_s) for slot s = unit + 1 (slots 1–7; slot 0 never repeats)
B: layer-1 circuit B, resolution of an episoderesA4 = resA_s = relu(RI_s + SUC_s + EXT_s + VAL_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j + NEU_j)) for s = 4, 5, 6, 7 (unit 7 = slot 4 … unit 10 = slot 7), bias −3; resA5 = resA_s = relu(RI_s + SUC_s + EXT_s + VAL_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j + NEU_j)) for s = 4, 5, 6, 7 (unit 7 = slot 4 … unit 10 = slot 7), bias −3; resA6 = resA_s = relu(RI_s + SUC_s + EXT_s + VAL_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j + NEU_j)) for s = 4, 5, 6, 7 (unit 7 = slot 4 … unit 10 = slot 7), bias −3; resA7 = resA_s = relu(RI_s + SUC_s + EXT_s + VAL_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j + NEU_j)) for s = 4, 5, 6, 7 (unit 7 = slot 4 … unit 10 = slot 7), bias −3; resB4 = resB_s = relu(RI_s + SUC_s + VC_s - 3 + (1/8) * sum_{j<s} RF_j) for s = 4, 5, 6, 7, bias −3; resB5 = resB_s = relu(RI_s + SUC_s + VC_s - 3 + (1/8) * sum_{j<s} RF_j) for s = 4, 5, 6, 7, bias −3; resB6 = resB_s = relu(RI_s + SUC_s + VC_s - 3 + (1/8) * sum_{j<s} RF_j) for s = 4, 5, 6, 7, bias −3; resB7 = resB_s = relu(RI_s + SUC_s + VC_s - 3 + (1/8) * sum_{j<s} RF_j) for s = 4, 5, 6, 7, bias −3
B: layer-1 circuit C, recent retry and fresh-failure evidenceRT_a = RT_a = relu(sum_{s=4..7} RF_s); RT_b = RT_b = relu(sum_{s=4..7} RF_s - 1), bias −1; NF = NF = relu(sum_s (ERR_s - VC_s) - 10 * sum_{s=4..7} VD_s)
B: layer-1 circuit D, reversions, progress and retry boundVM1 = VM1 = relu(sum_s SR_s - 1), weight 1 on every SR, bias −1; PROG = PROG = relu(100 * sum_s VD_s - 1), weight 100 on every VD, bias −1; NZ = NZ = relu(-20 * sum_s VD_s - 1), weight −20 on every VD, bias −1; OOP = OOP = relu(sum_s RF_s - 3), weight 1 on every RF, bias −3
B: layer-1 circuit E, pollingPOLL = POLL = relu(RI_7 + NEU_7 + EXT_7 - 2), bias −2; OVER = OVER = relu(sum_s NEU_s + 3 * NEU_0 - 6), weight 1 on every NEU (4 on NEU_0), bias −6
B: layer 2 (class conditions)OSC = relu(VM1); SA_a = relu(R + 2*OOP - 1 - veto), R = u0 + … + u6 (weights 1 on units 0–6, 2 on unit 21, −40 on units 18, 19, 20, bias −1); SA_b = relu(R + 2*OOP - 2 - veto), same weights, bias −2; SB_a = relu(2*POLL + OVER - 2 - veto) (weights 2 on unit 22, 1 on unit 23, −40 on units 18, 19, 20, bias −2); SB_b = relu(2*POLL + OVER - 3 - veto), same weights, bias −3; ACT = relu(2*(RT_a - RT_b) + NF/32 - 2 - 10*OOP) (weights 2, −2 on units 15, 16; 1/32 on unit 17; −10 on unit 21; bias −2); RES_a = relu(8 * (u7 + … + u14)); RES_b = relu(8 * (u7 + … + u14) - 1), bias −1; POLLf = relu(POLL - OVER) (weights 1 on unit 22, −1 on unit 23)
B: logitsPRODUCTIVE: all zero; RECOVERY: 2000*ACT + 20*RES + 20*POLLf - 10000*OSC (unit 5: +2000, 6: +20, 7: −20, 8: +20, 0: −10000); STALLED: 20*(STALL_A + STALL_B) - 2000*ACT - 100*RES - 100*POLLf - 1000*OSC (unit 1: +20, 2: −20, 3: +20, 4: −20, 5: −2000, 6: −100, 7: +100, 8: −100, 0: −1000); OSCILLATING: 20*OSC (unit 0: +20)
A: non-zero parameters233 of 3,256 (218 weights, 15 biases)
A: smallest and largest |weight|1/8 to 10,000
A: macro-F1, sealed test0.667 [0.659, 0.675]
A: macro-F1, sealed OOD0.810 [0.803, 0.817]
A on the invented window "Steady progress"PRODUCTIVE (logits 0, 0, 0, 0)
A on the invented window "A retry that worked"RECOVERY (logits 0, 220, −60, 0)
A on the invented window "Same failure, five times"STALLED (logits 0, −100, 20, 0)
A on the invented window "Undo, redo, undo"OSCILLATING (logits 0, −1,000, 0, 20)
A: layer-1 circuit A, stall-ineffective evidenceine0 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine1 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine2 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine3 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine4 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine5 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine6 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; ine7 = ine_s = relu(RI_s + SR_s + RF_s - NEU_s), unit s reads slot s; end2 = end2 = relu(RI_7 + SR_7 + RF_7 - NEU_7 - 1), bias −1
A: layer-1 circuit B, reversionsVm1 = Vm1 = relu(sum_s SR_s - 1), weight 1 on every SR, bias −1; Vm2 = Vm2 = relu(sum_s SR_s - 2), bias −2
A: layer-1 circuit C, net validation progressPROG_a = PROG_a = relu(1e4 * sum_s VD_s - 174), weight 10000 on every VD, bias −174; PROG_b = PROG_b = relu(1e4 * sum_s VD_s - 175), bias −175
A: layer-1 circuit D, active in-policy episodeee4 = ee_s = relu(ERR_s - VC_s) for s = 4, 5, 6, 7 (unit 9 = slot 4 … unit 12 = slot 7); ee5 = ee_s = relu(ERR_s - VC_s) for s = 4, 5, 6, 7 (unit 9 = slot 4 … unit 12 = slot 7); ee6 = ee_s = relu(ERR_s - VC_s) for s = 4, 5, 6, 7 (unit 9 = slot 4 … unit 12 = slot 7); ee7 = ee_s = relu(ERR_s - VC_s) for s = 4, 5, 6, 7 (unit 9 = slot 4 … unit 12 = slot 7); OOP = OOP = relu(sum_s RF_s - 2 * EXT_7 - 3), weight 1 on every RF, −2 on EXT_7, bias −3
A: layer-1 circuit E, recently resolved episoders4 = rs_s = relu(RI_s + SUC_s + MUT_s + VAL_s + EXT_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j - VC_j)) for s = 4, 5, 6, 7 (unit 13 = slot 4 … unit 16 = slot 7), bias −3; rs5 = rs_s = relu(RI_s + SUC_s + MUT_s + VAL_s + EXT_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j - VC_j)) for s = 4, 5, 6, 7 (unit 13 = slot 4 … unit 16 = slot 7), bias −3; rs6 = rs_s = relu(RI_s + SUC_s + MUT_s + VAL_s + EXT_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j - VC_j)) for s = 4, 5, 6, 7 (unit 13 = slot 4 … unit 16 = slot 7), bias −3; rs7 = rs_s = relu(RI_s + SUC_s + MUT_s + VAL_s + EXT_s - VC_s - 3 + (1/8) * sum_{j<s} (ERR_j - VC_j)) for s = 4, 5, 6, 7 (unit 13 = slot 4 … unit 16 = slot 7), bias −3
A: layer-1 circuit F, endpoint pollPOLL = POLL = relu(RI_7 + NEU_7 + EXT_7 - 2), bias −2
A: layer 2 (class conditions)OSC = relu((u17 - u18) - (u19 - u20)) = relu(V2 - PROG); STALL_a = relu(pre), pre = sum_{s} ine_s + 30*(ine_7 - end2) - 34 - 30*PROG - 30*V2 (weights: 1 on units 0–6, 31 on unit 7, −30 on unit 8, −30/+30 on units 19/20, −30/+30 on units 17/18, bias −34); STALL_b = relu(pre - 1), same weights, bias −35; ACT_a = relu(E - 10*OOP), E = u9 + u10 + u11 + u12; ACT_b = relu(E - 10*OOP - 1), bias −1; RES_a = relu(8 * (u13 + u14 + u15 + u16)); RES_b = relu(8 * (…) - 1), bias −1; POLL = relu(u22)
A: logitsPRODUCTIVE: all zero; RECOVERY: 20*RES + 20*POLL + 200*ACT - 100*STALL' - 1000*OSC (unit 5: +20, 6: −20, 7: +20, 3: +200, 4: −200, 1: −100, 2: +100, 0: −1000); STALLED: 20*STALL' - 60*ACT (unit 1: +20, 2: −20, 3: −60, 4: +60); OSCILLATING: 20*OSC (unit 0: +20)
Weights read from the committed artefacts (weight_program/phase-b-revision/artifact.dlw @ 0fc235d; weight_program/phase-a-selfcheck/artifact.dlw @ f8e0aae); circuits and formulas from each CIRCUITS.md. Sealed scores: 95% session-bootstrap CIs, each split scored once (artifacts/reports/m4_sealed_v02.json). The drawing shows B; a wire's width grows with log |w| from 1/32 to 10,000, and the sign of each weight and the 21 non-zero biases are in the tooltips and formulas.
The four invented windows, oldest event first
WindowEvents
Steady progressread a file; edit a file; run tests for the first time: 6 of 9 pass; read another file; edit it; re-run the tests: 8 of 9 pass; edit again; re-run: all 9 pass
A retry that workedread a file; edit it; run tests for the first time: all pass; call an API: error; same call, same error; same call again: success; edit a file; re-run the tests: still all pass
Same failure, five timesedit a file; re-run the tests: 7 of 9 pass, as before; same run, same failure; same run, same failure; same run, same failure; same run, same failure; read the same file again; same run, same failure
Undo, redo, undoedit a file; re-run the tests: 7 of 9 pass, as before; edit it another way; put the first version back; re-run: same failure; switch to the other version again; re-run: same failure; back to the first version
Invented for this figure, not benchmark data. The activations are a forward pass of each network on them, and each network's prediction matches its own reference_policy.py.
Source: weight_program/phase-b-revision/artifact.dlw and CIRCUITS.md @ 0fc235d; zero-label network weight_program/phase-a-selfcheck/ @ f8e0aae

Caveats. The features are already partial detectors, so this tests compilation far more than design; the transformer in Experiment 2 removes them and is a truer test. The zero-label network's higher OOD score is partly down to the OOD set's class mix.

Experiment 2: Can Fable write a transformer with no labels?

This version of the experiment removes the warm-up's crutch. It reads the raw log text token by token, so Fable has to work out a policy from the spec alone and then compile it into 2.1 million weights.

Setup

  • Network: a causal transformer with 4 layers, 8 attention heads, no normalisation and a residual stream (the shared vector each layer reads from and adds to) 128 numbers wide: 2,128,932 parameters all initially set to zero.
  • Input: the session so far. Each tool call is a block: a header (run_tests suite=alpha … -> FAILED 7/9 passed), an error line, and diffs as opaque content ids (@ zork/zeta/widget.qx h3f9a0c1d2e > h7b21e9c04a).
  • Output: 4 logits after every block, one raw score per class; the highest wins.
  • What Fable received: the spec, a grammar sheet of invented example blocks, and the frozen model, tokeniser and renderer source. No labels, no data.
  • Not allowed: the standing guards; it could run its network only on logs it wrote itself.
  • Scoring: sealed test and sealed OOD.
  • Cost: US$42.73 of model usage (API-equivalent). Total run took about 94 minutes.
One invented edit block before and after the label leaks were closed. Before: a write_file header with a bytes= field, a hunk count and four lines of code diff, each marked as a leak. After: the same header without bytes= and one line with two opaque content ids. Below, two equal bars: the probe and the majority guess both score 0.378 macro-F1 for progress on dev edits.
With the leaks removed, a probe did no better than guessing
Before
09:14:09.880 write_file path=zork/zeta/widget.qx bytes=1284 -> ok in=2214 out=310byte size @ zork/zeta/widget.qx (1 hunk)hunk count- acc = clamp(acc, 418)diff text+ if item is None:+ return acc+ acc = clamp(acc, 418 + 1)
After, as Fable saw it
09:14:09.880 write_file path=zork/zeta/widget.qx -> ok in=2214 out=310 @ zork/zeta/widget.qx h3f9a0c1d2e > h7b21e9c04acontent ids
Macro-F1 for progress, 56,417 dev edits
Figure 7. One invented edit before and after the leaks were closed, and a probe's macro-F1 for progress on what remained against the majority guess (dev, 56,417 edit events). Its AUC was 0.562 (0.5 is chance), all of it from the tool name.
Show the numbers
Probe on what remained (dev, 56,417 edits)
MeasureValue
Probe macro-F1, threshold 0.5 (dev)0.378
Majority-guess macro-F1 (dev)0.378
Probe AUC (dev; 0.5 is chance)0.562
Probe AUC, tool name only (dev)0.562
Probe AUC, without the tool name (dev)0.485
Probe macro-F1, threshold at the positive rate (dev)0.416
Probe macro-F1 without the tool name, threshold at the positive rate (dev)0.493
edit_file events that change behaviour (dev)42,441 events, 17,992 progress
write_file events that change behaviour (dev)9,427 events, 4,124 progress
revert events that change behaviour (dev)4,549 events, 0 progress
Generator marker strings in rendered dev text0 in 343,290 events
Dev split only (12,000 sessions), harness side; Fable never saw labels. Edits are MUTATE events that change behaviour; progress = PROGRESS or RESOLVES_EPISODE (39.2% of them), against NONE or REVERSION. Probe features: hunk_lines, changed_paths, tool=edit_file, tool=revert, tool=write_file, trunc_present; logistic regression, 5-fold CV grouped by session. The only signal left is the tool name: 0 of 4,549 reverts are progress. After the fix every changed file is one hunk line, so the hunk-line feature is constant. At a positive-rate threshold a random score gets about 0.50 macro-F1, so 0.493 without the tool name is chance; AUC is the fair comparison (0.562 with the tool name, 0.485 without).
The three leaks
LeakWhat it gave awayNowWhere
Code text in the diffa fix adds + 1 and a guard line, a wrong variant a 1000+n constant, a cosmetic edit cleanup pass N, a data fix -okone pair of opaque content ids per changed filespec v0.3 E9
bytes= on write_filethe byte size separated progress from non-progress editsremovedspec v0.3 r2 data audit (dev)
Hunk count per filethe number of hunks separated progress from non-progress editsremoved: one line per changed filespec v0.3 r2 data audit (dev)
Other edit kinds marked themselves in the diff text too: wrong variant "1007" (acc = clamp(acc, 1007)); cosmetic edit "cleanup pass 2" (# cleanup pass 2); data fix "-ok" (value = "v412-ok"). The example block is invented: its "after" lines follow the v0.3 grammar sheet and its "before" diff is built with the generator's own code templates.
Source: agent_doom_loop_detector_spec_v0_3.md (E9, r2 data audit) @ e05f371; artifacts/reports/v03_dev_audits.json (marker_audit) @ d08f05f

With zero labels, reading raw text, it scored 0.744 [0.734, 0.754] (sealed test) and 0.815 [0.806, 0.823] (sealed OOD), with false alarms of 1.9% and 4.0%. That is above the warm-up, which was handed features (0.667, sealed test; not paired). Its blind spot was stalls: it called 13,116 of 20,823 STALLED windows PRODUCTIVE (sealed test). Compilation held again: network and policy disagreed on 3 of 84,584 windows (sealed test). A 380-line builder sets 121,126 non-zero parameters and rebuilds byte for byte.

How it did it. The policy was built first and then the circuit:

  • Clean switches. An attention head lets a token look up others by matching a query against their keys. Every head here gives tokens it shouldn't read a −10,000 score, and when it has nothing to read it attends to the start token instead, so no stray signal leaks in.
  • Layer 0 parses by position (Figure 8). Layer 1 compares: each hashed id carries 13 sign bits, so a match scores 13 and anything else at most 11. That is enough to notice a file's new hash equalling an earlier old one, which is an undo.
  • Layer 2 counts (Figure 9). Attention's softmax turns scores into weights that sum to one. Give the start token and every matching earlier block the same score, and the start token gets exactly 1/(1+C), where C is the number of matches, so "failed at least K times" becomes a linear test. A transformer has no counter; this one gets one from the arithmetic of attention. It matters again in Experiment 10.
  • The output layer encodes rule priority as integer logits: OSCILLATING 55, STALLED 20, RECOVERY 10, PRODUCTIVE 0.
Two invented log blocks laid out as token tiles: an edit_file block and the revert that undoes it. In each, three tokens are highlighted: the tool name at offset 7, and the old and new content ids 2 and 4 tokens after the @ marker. In the revert the ids are swapped, and a line joins the revert's new id to the same id in the edit, labelled as an undo.
The network reads fields by position; a revert swaps the ids back
Figure 8. Two invented grammar-sheet blocks as tokens, an edit and the revert that undoes it, with the three tokens the zero-label network's layer-0 heads read (v0.3 Phase A). Layer 1 matches the revert's new id against the earlier old id.
Show the numbers
Marked tokens
TokenSurfaceIdMarked by
An edit (edit_file), offset 7edit_file10H0.2: copies the tool name at offset 7 to every token of its block
An edit (edit_file), offset 13ok38H0.3: reads the block's last status word
An edit (edit_file), offset 21NL@4the @ marker that H0.0 and H0.1 count from
An edit (edit_file), offset 23h3f9a0c1d2e2227H0.0: marks the old content id, the token 2 after @ (HOLD)
An edit (edit_file), offset 25h7b21e9c04a4182H0.1: marks the new content id, the token 4 after @ (HNEW)
The revert that undoes it (revert), offset 7revert12H0.2: copies the tool name at offset 7 to every token of its block
The revert that undoes it (revert), offset 13ok38H0.3: reads the block's last status word
The revert that undoes it (revert), offset 21NL@4the @ marker that H0.0 and H0.1 count from
The revert that undoes it (revert), offset 23h7b21e9c04a4182H0.0: marks the old content id, the token 2 after @ (HOLD)
The revert that undoes it (revert), offset 25h3f9a0c1d2e2227H0.1: marks the new content id, the token 4 after @ (HNEW); H1.0: equals the old id of the edit, offset 23 (undo)
Blocks and token ids from the v0.3 grammar sheet handed to Fable; every value in them is invented. Rendered text: 09:14:09.880 edit_file path=zork/zeta/widget.qx -> ok in=2214 out=310 / @ zork/zeta/widget.qx h3f9a0c1d2e > h7b21e9c04a | 09:14:15.762 revert paths=zork/zeta/widget.qx -> ok in=2470 out=12 / @ zork/zeta/widget.qx h7b21e9c04a > h3f9a0c1d2e
Heads of the zero-label network (v0.3 Phase A (zero-label))
HeadRoleWhat it does
H0.2toolcopies the tool name at offset 7 to every token of its block
H0.0old idmarks the old content id, the token 2 after @ (HOLD)
H0.1new idmarks the new content id, the token 4 after @ (HNEW)
H0.3statusreads the block's last status word
H0.4countfinds the passed count, 3 tokens before the last passed
H1.0undolayer 1: a new id equal to an earlier old id is an undo (REVMATCH)
From CIRCUITS.md, sections 1 and 5. H0.3 and H0.4 (status word, passed count) read other fields of the same blocks; the figure draws only the three reads that find structure by position.
Source: experiments/v0_3/packet_handed_over/GRAMMAR_SHEET.md @ e842bc3; weight_program/v0_3/phase-a/CIRCUITS.md §1, §5 @ eb38b33
A slider sets C, the number of matching failed blocks, from 0 to 7. Above, the start token and each match get the same attention weight, 1/(1+C), and other blocks get 0. Below, the matches' total weight is a bar with a threshold mark at 3.5 times the start token's weight; the bar passes the mark from C = 4, where the rule fires.
Equal scores turn counting into a linear test
Matching failed blocks
C = 3
Attention weights
The linear test for C ≥ 4
Figure 9. Attention weights of the zero-label network's counting head (H2.0, v0.3 Phase A) when the start token and C matching failed blocks share one score, and the retry rule's linear test for K = 4. Drag C. Analytic, not measured on any split.
Show the numbers
Matching failed blocksStart token 1/(1+C)Matches C/(1+C)3.5 × startMatches − 3.5 × startUnit: 40 × that, clippedC ≥ 4?
C = 01.0000.0003.500−3.500−140.0 → 0off
C = 10.5000.5001.750−1.250−50.0 → 0off
C = 20.3330.6671.167−0.500−20.0 → 0off
C = 30.2500.7500.875−0.125−5.0 → 0off
C = 40.2000.8000.7000.100+4.0 → 1fires
C = 50.1670.8330.5830.250+10.0 → 1fires
C = 60.1430.8570.5000.357+14.3 → 1fires
C = 70.1250.8750.4380.438+17.5 → 1fires
Analytic: closed-form weights of head H2.0 in layer 2 of the zero-label network (v0.3 Phase A), not measurements. The head looks back up to 7 blocks; every matching block here failed, so C counts the failed retries. The start token and every matching earlier block score exactly 300; a block that does not match scores at most 260, so its weight is below 10⁻¹⁷. The unit is MLP 2 units 7–8 (retry beyond policy, 40*(OUTFAIL - 3.5*ANCH2), clipped to 0..1). The revised network (v0.3 Phase B) moves the counter to head H1.4 in layer 1, with a shared score of 12,000 and a reach of 63 blocks.
Source: head H2.0 in weight_program/v0_3/phase-a/CIRCUITS.md @ eb38b33; H1.4 after the revision in weight_program/v0_3/phase-b/CIRCUITS.md @ a64a4f3

Caveats. The architecture was chosen to be hand-writable: no normalisation, and a start-token attention column added in a design review that included a hand-programmability lens.

Experiment 3: Can 200 labelled windows improve the network?

Fable knew where it was weak: "The remaining uncertainty is in the oracle itself: which failures start episodes and whether test failures count. Phase B's 200 windows will settle that." Were its errors bad compilation or bad judgement by the authoring model?

Setup

  • Network: the archived zero-label transformer, revised in the same session.
  • What Fable received: the 200 disclosed windows, each with its rendered prefix (the session up to that point), its true label, and the network's and policy's answers, to rerun freely.
  • Not allowed: the standing guards; one revision.
  • Scoring: sealed test and sealed OOD, once, plus full val: val minus the 200 disclosed sessions, 3,804 sessions.
  • Cost: US$21.89 of model usage (API-equivalent), 59 minutes.

On the sealed sets it went from 0.744 to 0.957 [0.953, 0.961] and from 0.815 to 0.908 [0.905, 0.912] (sealed OOD). False alarms rose from 1.9% to 2.5% (sealed test) and from 4.0% to 6.8% (sealed OOD). STALLED recall went from 0.37 to 0.95, and the fix carried over to the other generator, a good indicator that its fix was generalisalble.

Before the revision, network and policy agreed on all 200 disclosed windows: the network computed exactly what Fable's policy said, but the policy was subtly wrong. After it, agreement fell slightly 99.95% to 100% across val, test and OOD.

Two waffles of the 200 disclosed windows, 50 per true class; a cell in its class colour is right, a grey cell wrong. The zero-label transformer gets 141 right (PRODUCTIVE 48, RECOVERY 34, STALLED 15, OSCILLATING 44); the revised transformer gets 199, missing one PRODUCTIVE window. Network and policy agreed on every window in both. Below them, STALLED recall rises from 0.369 to 0.953 on full val, 0.356 to 0.956 on sealed test and 0.364 to 0.951 on sealed OOD.
Every error was the policy's, and fixing the policy fixed the network
STALLED recall, zero-label → revised
Figure 10. Each cell is one of the 200 disclosed windows, 50 per true class, as the zero-label transformer and its one revision called it (disclosed, in-sample). Below, STALLED recall before and after on full val, sealed test and sealed OOD.
Show the numbers
The 200 disclosed windows (in-sample)
True classZero-label transformerRevised transformer
PRODUCTIVE (50 windows)48 of 50 right; 1 called RECOVERY, 1 called OSCILLATING49 of 50 right; 1 called OSCILLATING
RECOVERY (50 windows)34 of 50 right; 16 called PRODUCTIVE50 of 50 right
STALLED (50 windows)15 of 50 right; 34 called PRODUCTIVE, 1 called RECOVERY50 of 50 right
OSCILLATING (50 windows)44 of 50 right; 6 called PRODUCTIVE50 of 50 right
All 200141 right199 right
Network disagrees with policy0 of 200 windows0 of 4,484 prefixes
Disclosed val windows, in-sample: Fable saw them for the revision. Revised-network classes follow REVISION.md: 199 of 200 right, the one miss V053 (PRODUCTIVE called OSCILLATING), and agreement with the revised policy on 4,484 of 4,484 prefixes.
Recall by class
Class, splitZero-labelRevised
PRODUCTIVE, full val, excluding the 200 disclosed sessions (3,804 sessions)0.9560.950
RECOVERY, full val, excluding the 200 disclosed sessions (3,804 sessions)0.6760.956
STALLED, full val, excluding the 200 disclosed sessions (3,804 sessions)0.3690.953
OSCILLATING, full val, excluding the 200 disclosed sessions (3,804 sessions)0.9280.973
PRODUCTIVE, sealed test (3,996 sessions)0.9520.952
RECOVERY, sealed test (3,996 sessions)0.6950.963
STALLED, sealed test (3,996 sessions)0.3560.956
OSCILLATING, sealed test (3,996 sessions)0.9180.968
PRODUCTIVE, sealed OOD (2,000 sessions)0.9330.903
RECOVERY, sealed OOD (2,000 sessions)0.9130.917
STALLED, sealed OOD (2,000 sessions)0.3640.951
OSCILLATING, sealed OOD (2,000 sessions)0.9990.980
Full val excludes the 200 disclosed sessions (3,804 sessions). Sealed test (3,996 sessions) and sealed OOD (2,000) were each scored once.
Source: experiments/v0_3/disclosure/DISCLOSURE.json @ eb38b33; weight_program/v0_3/phase-b/REVISION.md @ a64a4f3; artifacts/reports/v03_sealed.json @ 67fc2b1

How it did it. It rewrote the policy, giving a reason and window ids for each change. One change made progress mean a new high in passed tests; that check matters again in Experiment 6. Midway through recompiling, network and policy disagreed on 68 of the 200 windows, and Fable traced why: "The traces show that layer 1 gathers the passed count, but the heads that read it also sit in layer 1, so they see zeros and IMPROVED fires on every failing run. I'll move those heads to layer 2…" That is debugging a neural network by reading activations and reasoning about layer order, with no gradient anywhere. The final network agreed with the policy on all 4,484 disclosed prefixes. Here is the output layer, verbatim from the revised build_weights.py:

W, b = m.head.weight, m.head.bias
W[3, V2] = 60.0
W[3, PROG] = -100.0
b[3] = -5.0
W[2, I5] = 50.0
W[2, SI] = 50.0
W[2, ACT] = -100.0
W[2, PROG] = -100.0
b[2] = -80.0
W[1, RECB] = 15.0
b[1] = -5.0

Row 3 is OSCILLATING: at least two reversions in the window (V2), unless there is progress (PROG). Row 2 is STALLED: at least five ineffective repeats (I5), the latest included (SI), unless an in-policy retry is active (ACT) or there is progress. Row 1 is RECOVERY; PRODUCTIVE is the default at zero. The network has 156,501 non-zero parameters, about 7.4% of the model.

Wiring chart of the revised transformer: embeddings, then layers 0 to 3, each a column of named attention heads (H0.0 to H3.2) and MLP unit groups named by the band they write, such as REVTOK, SI, V2 and PROG, ending in output logits OSCILLATING 55, STALLED 20, RECOVERY 10 and PRODUCTIVE 0. Picking a class fills the blocks that feed its rule; the OSCILLATING path runs through the undo-match head H1.0 to V2, and its rule is 60 V2 − 100 PROG − 5.
Every block has a name, and each class traces to one rule
Trace the rule for
OSCILLATING = 60 V2 − 100 PROG − 5, so 55 when it fires; the grey block vetoes it
STALLED = 50 I5 + 50 SI − 100 ACT − 100 PROG − 80, so 20 when it fires; the grey blocks veto it
RECOVERY = 15 RECB − 5, so 10 when it fires
PRODUCTIVE = 0: it wins when no other rule fires
Figure 11. Every attention head and MLP unit group of the revised transformer (v0.3 B), by layer, under its name in CIRCUITS.md. Pick a class: the filled blocks feed its logit, and the line under the chart is its rule.
Show the numbers
The two written networks
NetworkParametersSealed testSealed OOD
Revised transformer (200 labelled windows)2,128,9320.9570.908
Zero-label transformer (zero labels)2,128,9320.7440.815
Macro-F1, each sealed split scored once. The drawing shows the revised network; the zero-label network's blocks are listed below.
Every block, what it reads and writes
Network · blockRoleReadsWritesFeeds
Revised transformer · Token embeddingtoken flags, id bitsBOS, EOE, NLAT, HASHED, GT, NONRES, DIGIT, NUM, BITS, ISSTAT, SOK, SFAIL, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLLOSCILLATING, STALLED, RECOVERY
Revised transformer · Offset embeddingposition in blockCODE, OFFLIN, OFF7, OFFGE10OSCILLATING, STALLED, RECOVERY
Revised transformer · head H0.0tool of this blockOFF7, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLLBTINSP..BTPOLLSTALLED, RECOVERY
Revised transformer · head H0.1latest status wordOFFLIN, ISSTAT, SOK, SFAILBSOK, BSFAILSTALLED, RECOVERY
Revised transformer · head H0.2previous tokenCODE, BITS, HASHED, GT, ISSTATPBITS, PHASHED, PGT, PISSTATOSCILLATING, STALLED, RECOVERY
Revised transformer · head H0.3has a hunk lineNLATHASHUNKSTALLED, RECOVERY
Revised transformer · head H0.4intent tokenHASHED, OFFLIN, NONRES, OFFGE10, BITSIBITSSTALLED, RECOVERY
Revised transformer · MLP 0 → VMvalidation or hunkBTINSP..BTPOLL, HASHUNKVMSTALLED, RECOVERY
Revised transformer · head H1.0undo matchBITS, PBITS, PHASHED, GTREVMATCHOSCILLATING, STALLED, RECOVERY
Revised transformer · head H1.1passed countOFFLIN, DIGIT, PISSTAT, NUMPVSTALLED, RECOVERY
Revised transformer · head H1.2nearest same keyIBITS, BTINSP..BTPOLL, EOE, BSOK, BSFAILEXISTS, PSOK, PSFAILSTALLED, RECOVERY
Revised transformer · head H1.3same key within 7IBITS, BTINSP..BTPOLL, EOEEXISTS7STALLED, RECOVERY
Revised transformer · head H1.4counts failuresIBITS, BTINSP..BTPOLL, EOE, BSFAIL, BOSOUTFAIL, ANCHSTALLED, RECOVERY
Revised transformer · head H1.5last check or editEOE, VM, BTINSP..BTPOLL, HASHUNKNEARESTSTALLED, RECOVERY
Revised transformer · MLP 1 → NOTIDSTstatus changedBSOK, PSOK, BSFAIL, PSFAILNOTIDSTSTALLED
Revised transformer · MLP 1 → CNT3failures ≤ 3ANCH, OUTFAILCNT3STALLED, RECOVERY
Revised transformer · MLP 1 → CNT5failures ≤ 5ANCH, OUTFAILCNT5STALLED, RECOVERY
Revised transformer · MLP 1 → REVTOKreverting tokenREVMATCH, PGT, HASHEDREVTOKOSCILLATING, STALLED, RECOVERY
Revised transformer · MLP 1 → NOOPre-applied editBTINSP..BTPOLL, HASHUNK, BSFAILNOOPSTALLED
Revised transformer · MLP 1 → REDINSPrepeated readBTINSP..BTPOLL, BSOK, EXISTSREDINSPSTALLED
Revised transformer · head H2.0prev same-key blockIBITS, BTINSP..BTPOLL, EOE, PV, EXISTS7, PSFAIL, NEARESTPPV, PEX7, PPSFAIL, PNEARSTALLED, RECOVERY
Revised transformer · head H2.1best count so farIBITS, PV, EOEMAXPVSTALLED
Revised transformer · head H2.2nearest validationEOE, BTINSP..BTPOLL, EXISTS, NOTIDSTNVEX, NVNOTSTSTALLED
Revised transformer · head H2.3block revertsREVTOKREVBLKOSCILLATING, STALLED, RECOVERY
Revised transformer · MLP 2 → IMPROVEDnew best pass countPV, MAXPV, BSFAIL, EXISTS, BSOK, PSFAILIMPROVED
Revised transformer · MLP 2 → SIineffective blockREVBLK, BTINSP..BTPOLL, NOOP, REDINSP, HASHUNK, BSFAIL, NVEX, NVNOTST, MAXPV, PV, EXISTS, NOTIDST, PPV, EXISTS7, PSFAIL, NEAREST, CNT3, CNT5SISTALLED
Revised transformer · MLP 2 → INPOLretry within policyPV, PPV, BTINSP..BTPOLL, BSFAIL, EXISTS7, PSFAIL, NEAREST, CNT3, REVBLK, CNT5INPOLRECOVERY
Revised transformer · MLP 2 → RESOLVfailure resolvedBTINSP..BTPOLL, BSOK, PSFAIL, PEX7, PPSFAIL, PNEARRESOLVRECOVERY
Revised transformer · head H3.0window averagesEOE, SI, REVBLK, IMPROVED, BOSAVGSI, AVGREV, AVGIMP, ANCH8OSCILLATING, STALLED
Revised transformer · head H3.1active retryEOE, INPOLACTRECOVERY
Revised transformer · head H3.2resolved in last 4EOE, RESOLVREC4RECOVERY
Revised transformer · MLP 3 → V2≥ 2 reversionsAVGREV, ANCH8V2OSCILLATING
Revised transformer · MLP 3 → I5≥ 5 ineffectiveAVGSI, ANCH8I5STALLED
Revised transformer · MLP 3 → PROGprogress in windowAVGIMP, ANCH8PROG
Revised transformer · MLP 3 → RECBrecoveringACT, REC4RECBRECOVERY
Zero-label transformer · Token embeddingtoken flags, id bitsBOS, EOE, NLAT, HASHED, H0, PASSEDW, DIGIT, NUM, BITS, ISSTAT, SOK, SFAIL, SFAILED, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLL, TSEARCHOSCILLATING, STALLED, RECOVERY
Zero-label transformer · Offset embeddingposition in blockCODE, OFFLIN, OFF7, OFF10, OFF13, OFF14OSCILLATING, STALLED, RECOVERY
Zero-label transformer · head H0.0old id slotCODE, NLATHOLDOSCILLATING, STALLED
Zero-label transformer · head H0.1new id slotCODE, NLATHNEWOSCILLATING, STALLED
Zero-label transformer · head H0.2tool of this blockOFF7, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLL, TSEARCHBTINSP..BTSEARCHSTALLED, RECOVERY
Zero-label transformer · head H0.3latest status wordOFFLIN, ISSTAT, SOK, SFAIL, SFAILEDBSOK, BSFAIL, BSFAILEDSTALLED, RECOVERY
Zero-label transformer · head H0.4where the count isOFFLIN, PASSEDW, CODECODEP, HASPASSED
Zero-label transformer · head H0.5has a hunk lineNLATHASHUNKSTALLED
Zero-label transformer · MLP 0 → SLOTintent slotOFF10, BTINSP..BTSEARCH, OFF13, OFF14SLOTSTALLED, RECOVERY
Zero-label transformer · MLP 0 → VMvalidation or hunkBTINSP..BTSEARCH, HASHUNKVMSTALLED
Zero-label transformer · head H1.0undo matchBITS, HOLD, HASHED, H0REVMATCHOSCILLATING, STALLED
Zero-label transformer · head H1.1intent bits to endSLOT, BITSBITSSTALLED, RECOVERY
Zero-label transformer · head H1.2passed countCODEP, CODE, DIGIT, NUMPV
Zero-label transformer · head H1.3last check or editEOE, VM, BTINSP..BTSEARCH, HASHUNKNEARESTSTALLED
Zero-label transformer · MLP 1 → REVTOKreverting tokenREVMATCH, HNEW, HASHEDREVTOKOSCILLATING, STALLED
Zero-label transformer · MLP 1 → REDVALredundant validationBTINSP..BTSEARCH, NEARESTREDVALSTALLED
Zero-label transformer · head H2.0counts same intentBITS, BTINSP..BTSEARCH, EOE, BSFAIL, BOSOUTALL, OUTFAIL, ANCH2STALLED, RECOVERY
Zero-label transformer · head H2.1block revertsREVTOKREVBLKOSCILLATING, STALLED
Zero-label transformer · MLP 2 → SIineffective blockREVBLK, BTINSP..BTSEARCH, HASHUNK, BSFAIL, REDVAL, OUTALL, OUTFAIL, ANCH2SISTALLED
Zero-label transformer · MLP 2 → KILLretry beyond policyOUTFAIL, ANCH2, BSFAIL, BTINSP..BTSEARCHKILLRECOVERY
Zero-label transformer · MLP 2 → INPOLfailure in policyANCH2, OUTFAIL, BSFAIL, BTINSP..BTSEARCHINPOLRECOVERY
Zero-label transformer · MLP 2 → RESOLVfailure resolvedOUTFAIL, ANCH2, BSOKRESOLVRECOVERY
Zero-label transformer · head H3.0window averagesEOE, SI, REVBLK, BOSAVGSI, AVGREV, ANCH8OSCILLATING, STALLED
Zero-label transformer · head H3.1recovery signEOE, INPOL, RESOLV, KILLNEAR4RECOVERY
Zero-label transformer · head H3.2latest validationEOE, HASPASSED, PV, BSFAILED, BSOKLPV, LFAILED, LOK
Zero-label transformer · head H3.3earliest validationEOE, HASPASSED, PV, BSFAILED, BSOKFPV, FFAILED, FOK
Zero-label transformer · MLP 3 → V2≥ 2 reversionsAVGREV, ANCH8V2OSCILLATING
Zero-label transformer · MLP 3 → I5≥ 5 ineffectiveAVGSI, ANCH8I5STALLED
Zero-label transformer · MLP 3 → A4recoveringNEAR4A4RECOVERY
Zero-label transformer · MLP 3 → PROGNpassed count roseLPV, FPVPROGN
Zero-label transformer · MLP 3 → PROGFFAILED then OKFFAILED, LOKPROGF
Zero-label transformer · MLP 3 → SITineffective nowSISITSTALLED
From each network's CIRCUITS.md: section 3 (residual bands), section 5 (heads and MLP units) and section 6 (logits). Revised: PRODUCTIVE = 0; RECOVERY = 15 RECB − 5; STALLED = 50 I5 + 50 SI − 100 ACT − 100 PROG − 80; OSCILLATING = 60 V2 − 100 PROG − 5. Zero-label: PRODUCTIVE = 0; RECOVERY = 15 A4 − 5; STALLED = 50 I5 + 50 SIT − 100 INPOL − 100 PROGN − 100 PROGF − 80; OSCILLATING = 60 V2 − 100 PROGN − 100 PROGF − 5. Paths follow each logit's positive terms upstream, band by band.
Source: weight_program/v0_3/phase-b/CIRCUITS.md (§3, §5, §6) @ a64a4f3; weight_program/v0_3/phase-a/CIRCUITS.md @ eb38b33

Caveats. 141 → 199 is in-sample, and one window was left wrong on purpose. The revision shared the zero-label session's memory and could rerun freely, which the Qwen revision (Experiment 10) couldn't. The 25 of 84,679 val windows where network and policy disagree are all in one family Fable's self-check never reached.

Writing With No Tools at All

Everything above let Fable write and run code. In the MIT post's no-tools experiment, GPT-6 Astra wrote policy networks for three MinAtar games in one reply to a single prompt, with no code execution, no calculator and nothing run before scoring. I wanted the same premise here, and then to see what it took to improve the result (if needed). Each round was a separate Fable session, handed the previous weights and replies but not the previous session's memory. I wanted to prove that the model itself could reason over the weights directly and not just use previous context to help.

Experiment 4: Can Fable write the network with no tools?

Setup

  • Network: the same transformer, every weight starting at 0.
  • What Fable received: one 73 KB prompt built only from a context pack (spec, rules, grammar sheet, renderer, tokeniser and model source). Every reply from the model was answered only with "Continue." until the model decided it had finished.
  • Not allowed: tools, code, a calculator, files, data, labels or feedback.
  • Deviations from the MIT protocol, both mine: any number of replies, and no stated output limit.
  • What else reached it: Claude Code's own system prompt; environment, date and account details (the account e-mail names the organisation); and, once, a fixed "output limit hit" notice inside a reply. None carries task information.
  • Scoring: full val, then sealed test and sealed OOD, once.
  • Cost: US$16.70 of model usage (API-equivalent), about 58 minutes.

It thought for about 55 minutes before writing anything: about 236,000 tokens of reasoning, hitting the 128,000-token ceiling on a single response once. Then it wrote a design and the weights in four replies: 617 statements, 66,014 non-zero weights.

It scored 0.320 [0.313, 0.328] (full val) and 0.326 (sealed test), with false alarms on 95.8% of healthy windows (full val). The average hides a split. Its OSCILLATING detector works: F1 0.83 at 98% precision (full val). STALLED fired on almost everything, including 25,851 of 27,049 productive windows (full val).

This is obviously a very poor initial performance from Fable.

How it did it. The hard part without code is the 8,192 hashed word ids. To notice that a path repeats, every id needs a recognisable vector, and the code-written builder made those with a hash function. Fable used modular arithmetic instead. Each id gets a one-hot code (a single 1 among zeros) of its value mod 4, 5 and 7, which gives 140 distinct codes by the Chinese remainder theorem, plus a second code mod 3 and 13. All of it is a few dozen strided fills (Figure 12). On top came previous-token heads (it typed out all 192 squares of a quadratic position score by hand), a 1/(1+C) counter, an undo matcher and window sums. The undo path worked. With no way to run the network, nothing could show why STALLED fired everywhere.

A 16-cell strip, code A for word id 6414 (widget.qx), with one cell lit in each of the mod 4, 5 and 7 groups, and the strided statement tok_emb.weight[2050:10240:4, 34] = 1 that sets the mod 4 cell, one of 48 such fills. Below, two attention-score bars: the same path read again matches 3 of 3 cells and scores 24, above the start-token sink of 20; a different path matches 1 of 3 and scores 8, below it.
A residue code lets a repeat match 3 of 3 cells
Code A for widget.qx, word id 6414
Attention score when a later path is read
Figure 12. Code A for one word id: one cell lit per residue of its offset from 2,048 mod 4, 5 and 7 (code B, mod 3 and 13, is in the table). Below, the attention score a later path earns; it must beat the start-token sink to count as a repeat. Ids from the prompt's grammar sheet, not benchmark data.
Show the numbers
Word ids and the cells they light
Word idPath (prompt grammar sheet)Offset from 2,048Code A residues (mod 4, 5, 7)Code B residues (mod 3, 13)Cells set to 1
6414zork/zeta/widget.qx4,3662, 1, 51, 11d34, d37, d46, d49, d62
7212zork/zeta/gizmo_plinth.qx5,1640, 4, 51, 3d32, d40, d46, d49, d54
Word ids 2,048–10,239 are hashed words; these two are example tokens from the prompt's grammar sheet, not benchmark data. Each cell is set by one strided fill such as tok_emb.weight[2050:10240:4, 34] = 1. 32 such fills cover every word id; 16 more give number tokens code A, 48 in all.
Matching a repeat (layer 1, head 2, code A only)
Query idKey idShared cellsCells matchedScoreSinkResult
64146414d34, d37, d463 of 32420repeat
64147212d461 of 3820no repeat
72126414d461 of 3820no repeat
72127212d32, d40, d463 of 32420repeat
Each shared cell adds 32 × 0.25 = 8 to the attention score; the start-token sink scores 20, so only a 3-of-3 match draws the attention. Code A has 4 × 5 × 7 = 140 values, so 58 or 59 of the 8,192 word ids share each one: a chance match is about 1 in 140 per key. Codes A and B together have 5,460 values. The scores are as r1 designed them; in round 1 one wrong bias let the key light every cell (next figure).
Source: experiments/v0_5/runs/r1/replies/001.md and 003.md @ 5b214c6; experiments/v0_5/PROMPT.txt @ f23bcd2; weight_program/v0_5/phase-b/REVISION.md @ b1f9e3b

Caveats. The prompt holds every task fact the code-writing author had, presented differently (no tool protocol; a few added scoring definitions and a token table) and it was only given one run.

Experiment 5: Can Fable repair a model with no prior knowledge?

The no-tools run got no feedback, so one wrong number could sink it without it ever knowing or having the opportunity to correct itself. We run a second round against that model, as in Experiment 3, a second round got the 200 labelled windows and tools. Unlike Experiment 3, it was a new session.

Setup

  • Network: the no-tools model.
  • What Fable received: the draft's replies and weights, the same packet, the 200 disclosed windows with the draft's answers, and tools to run the network and read activations at any layer.
  • Not allowed: automated search or fitting.
  • Scoring: full val, paired against the revised transformer. Then one pre-registered sealed run, which also re-scored every earlier transformer as a check; they reproduced, bar one window of one trained network.
  • Cost: US$40.75 of model usage (API-equivalent); it hit my plan's usage limit once and was resumed with a fixed "continue, same rules" message.

The initial model failed because of one bias. Layer-0 units 32 to 63 had bias −2 where −3 was needed, so a "target code" fired on every file path and everything looked like a repeat. The single statement layers.0.mlp.fc1.bias[32:64] = -3 took the disclosed windows from 0.331 to 0.714 (in-sample self-check). If the original authoring model had been able to test it, then it's likely this would have been caught and fixed.

Two columns. Round 1 as written, with bias −2 on layer-0 units 32 to 63: all 16 key cells at a read path fire, so STALLED logits reached 60 to 90; macro-F1 0.331. Round 2's first change, layers.0.mlp.fc1.bias[32:64] = -3: 3 of 16 cells fire, so only a true repeat matches; macro-F1 0.714. Both on the 200 disclosed windows, in-sample.
One wrong bias made every path look like a repeat
Round 1, as written
layers.0.mlp.fc1.bias[32:48] = -2layers.0.mlp.fc1.bias[48:64] = -2
16 of 16 cells fire, so STALLED logits reached 60 to 90
Macro-F1 0.331 (disclosed windows, in-sample)
Round 2, the first change
layers.0.mlp.fc1.bias[32:64] = -3
3 of 16 cells fire, so only a true repeat matches
Macro-F1 0.714 (disclosed windows, in-sample)
Figure 13. The 16 code-A key cells at one read path (widget.qx) under each bias, and macro-F1 before and after the change. Self-check on the 200 disclosed windows (in-sample), not an evaluation.
Show the numbers
Key at a read path, zork/zeta/widget.qx (id 6414)
BiasKey cells 1 to 16 (code A)Cells firingSumMatch by construction
Before (b = −2)1 1 2 1 1 2 1 1 1 1 1 1 1 1 2 116196.33
After (b = −3)0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0331
Layer 0 units 32 to 47: key = relu(code + hashed + offset 10 + read event + b). At a read path all 3 gates are 1, so b = −2 gives code + 1 (every cell fires) and b = −3 gives the code itself (3 cells). The matching head copies key ÷ 3. Read by Fable on disclosed window V001 before the fix: match ≈ 6.33 on every coded token (should be at most 1), re-read flag ≈ 8.7, wasted-effort score ≈ 10 per event; STALLED logits 60 to 90 on 141 of 200 disclosed windows. Round 1 statements: layers.0.mlp.fc1.bias[32:48] = -2; layers.0.mlp.fc1.bias[48:64] = -2. Round 2's first change: layers.0.mlp.fc1.bias[32:64] = -3, no other weight changed.
Self-check on the 200 disclosed windows (in-sample): labeller class by row, network's call by column
Bias, labellerPRODUCTIVERECOVERYSTALLEDOSCILLATING
Before, PRODUCTIVE30470
Before, RECOVERY40460
Before, STALLED140360
Before, OSCILLATING001238
After, PRODUCTIVE351401
After, RECOVERY64220
After, STALLED284180
After, OSCILLATING00050
50 windows per class; a self-check, not an evaluation. Before is b = −2, after is b = −3. Before: macro-F1 0.331, false alarms 93%. After: macro-F1 0.714, false alarms 3%.
Source: experiments/v0_5/runs/r1/replies/003.md @ 5b214c6; weight_program/v0_5/phase-b/ITERATIONS.md and REVISION.md @ b1f9e3b

Fable rebuilt the decision logic (1,262 statements, 2,813 weights changed). The result scored 0.957 [0.954, 0.960] (full val), level with the revised transformer's 0.954 (paired +0.003 [−0.001, +0.007]), with slightly more false alarms (3.2% against 2.4%).

Macro-F1, sealed testFalse alarms, sealed testMacro-F1, sealed OODFalse alarms, sealed OOD
No-tools transformer (v0.5 r1, zero labels)0.32695.7%0.29281.5%
No-tools revision (v0.5 B)0.9593.2%0.76531.4%
Zero-label transformer (v0.3 A)0.7441.9%0.8154.0%
Revised transformer (v0.3 B)0.9572.5%0.9086.8%
Trained transformer, 223,930 labels (v0.3 C-all; Experiment 8)0.9236.1%0.48834.5%

The two revised written networks are level on sealed test (paired +0.0015 [−0.0020, +0.0052]) but not on sealed OOD: −0.143 [−0.147, −0.139]. The no-tools revision's false alarms on sealed OOD, 31.4%, are close to the 34.5% of the same transformer trained on 223,930 labels (Experiment 8). It is behind the revised transformer in all ten OOD families, and ahead of the trained network in all ten by macro-F1, though not on false alarms in four of them (Figure 14). Nearly all the loss is one error: it called 32.6% of PRODUCTIVE windows STALLED (sealed OOD).

Dot plot of the ten sealed OOD families, macro-F1 beside false-alarm rate, for the no-tools revision, the revised transformer and the trained transformer. In every family the no-tools revision's macro-F1 is below the revised transformer's and above the trained transformer's. Four shaded rows (telemetry_gaps, shard_migration, lockfile_churn and index_watermark) mark the families where the trained transformer has fewer false alarms than the no-tools revision.
Off distribution, the no-tools revision sits between the other two
  • No-tools revision
  • Revised transformer
  • Trained transformer
  • Shaded: trained has fewer false alarms
Figure 14. Macro-F1 and false-alarm rate per family on the 10 sealed OOD families (one-time run), ordered by the no-tools revision's macro-F1. Shaded rows are the four families where the trained transformer has fewer false alarms than the no-tools revision.
Show the numbers
OOD familyMacro-F1, v0.5 BMacro-F1, v0.3 BMacro-F1, v0.3 C-allFalse alarms, v0.5 BFalse alarms, v0.3 BFalse alarms, v0.3 C-allScored windows
index_watermark0.934 [0.928, 0.940]1.000 [1.000, 1.000]0.732 [0.710, 0.752]11.7% [10.6%, 12.8%]0.0% [0.0%, 0.0%]2.3% [1.9%, 2.8%]7,462
lease_reconnect0.926 [0.919, 0.933]1.000 [1.000, 1.000]0.666 [0.650, 0.682]12.9% [11.8%, 14.1%]0.0% [0.0%, 0.0%]17.0% [15.2%, 18.8%]6,892
serializer_duel0.832 [0.821, 0.843]0.931 [0.925, 0.936]0.306 [0.281, 0.330]40.3% [37.7%, 43.0%]7.1% [6.8%, 7.5%]42.1% [35.8%, 48.8%]7,844
telemetry_gaps0.784 [0.769, 0.797]0.917 [0.906, 0.927]0.563 [0.539, 0.585]31.0% [28.7%, 33.4%]10.1% [8.6%, 11.6%]18.4% [15.7%, 21.2%]7,221
annotation_spiral0.783 [0.774, 0.792]0.916 [0.908, 0.923]0.447 [0.436, 0.458]39.6% [37.5%, 41.8%]14.9% [13.3%, 16.5%]64.5% [61.4%, 67.7%]7,694
merge_ping_pong0.771 [0.759, 0.783]0.883 [0.876, 0.890]0.617 [0.592, 0.641]43.5% [41.5%, 45.4%]5.4% [5.1%, 5.6%]60.6% [55.9%, 65.2%]8,506
fixture_archaeology0.617 [0.603, 0.631]0.866 [0.858, 0.874]0.442 [0.426, 0.458]42.5% [40.7%, 44.2%]9.1% [8.4%, 9.8%]60.7% [57.6%, 63.7%]7,843
shard_migration0.597 [0.587, 0.607]0.876 [0.866, 0.884]0.294 [0.279, 0.308]36.9% [35.6%, 38.2%]5.1% [4.4%, 5.9%]28.0% [27.2%, 28.8%]7,628
lockfile_churn0.573 [0.569, 0.577]0.596 [0.592, 0.600]0.259 [0.257, 0.262]21.9% [21.0%, 22.8%]12.2% [11.7%, 12.7%]2.1% [1.7%, 2.4%]8,706
pinned_validation0.524 [0.504, 0.543]0.620 [0.613, 0.628]0.451 [0.434, 0.468]48.1% [46.2%, 49.9%]8.6% [8.1%, 9.2%]71.7% [70.5%, 72.9%]7,742
Sealed OOD, one-time pre-registered run; 95% session-bootstrap CIs; 200 sessions per family; rows ordered by the no-tools revision's macro-F1. No-tools revision = v0.5 B, revised transformer = v0.3 B, trained transformer = v0.3 C-all. False alarms: share of PRODUCTIVE or RECOVERY windows called STALLED or OSCILLATING. The trained transformer has fewer false alarms than the no-tools revision in 4 families (index_watermark, telemetry_gaps, shard_migration, lockfile_churn). Overall, sealed test (3,996 sessions): v0.5 B 0.959, false alarms 3.2%; v0.3 B 0.957, false alarms 2.5%; v0.3 C-all 0.923, false alarms 6.1%. sealed OOD (2,000 sessions): v0.5 B 0.765, false alarms 31.4%; v0.3 B 0.908, false alarms 6.8%; v0.3 C-all 0.488, false alarms 34.5%.
Source: artifacts/reports/v05_sealed.json (splits.ood.populations.all.per_family) @ cba5a80; protocol experiments/v0_5/sealed/README.md @ abe8b83

How it did it. It read activations. On a disclosed window it saw the match signal near 6.3 on every coded token, where it should be at most 1, and traced that to the bias. Then it read the windows to learn the labeller, kept the draft's sound parts and rebuilt the decision logic around observable rules: a failure is identified by its error line, and a retry is the same failure soon after with no edit between. One thing it chose not to build: "No progress estimate (a decreasing failure count) was added." Only two of its 200 windows needed one; it left one wrong and got the other right by accident.

So running code mattered twice. Without it, one wrong number broke the model. With it and 200 labels, Fable found that number in one pass and matched the revised transformer, the best transformer in the project, in distribution (sealed test). What it built didn't transfer as well across to OOD traces.

Caveats. The rule against search and fitting covers automated search only: Fable compared hand-written rule variants on the 200 windows and chose after reading their scores. A new session isn't like for like with the transformer's same-session revision. The audit found two read-only ls commands outside the shell rule.

Experiment 6: Why didn't the repair transfer to off distribution?

Both revised networks saw the same 200 windows and are level in distribution, yet the no-tools revision is 0.143 behind off distribution (sealed OOD). I want to understand why.

Setup

  • Network: the no-tools revision, unchanged.
  • Data: a fresh OOD batch, new sessions drawn for diagnosis: 1,000 sessions (38,832 windows) from Codex's generator with a new seed, plus a fresh in-distribution batch. No sealed data.
  • Method: read the network's internal "this operation was wasted effort" signal at every operation, using Fable's own dimension map, and compare it with the labeller's hidden record.
  • Not done: no patched network was built or scored, and nothing was shown to Fable. Post-hoc analysis by me, with Claude.
  • Cost: CPU only.

The fresh batch reproduced the sealed failure almost exactly: 32.8% of productive windows called STALLED (fresh OOD batch), against 32.6% (sealed OOD). The revised transformer's rate was 4.6% on both.

The labeller calls a window STALLED only when at least five of eight operations repeat work, nothing improved and nothing new was learned. On the 7,175 windows wrongly called STALLED (fresh OOD batch), the labeller saw real progress in 92% and counted no repeated work at all in 61%; the network counted five to eight wasted operations in every one. The revision had written in what wasted effort looks like in 200 windows from one generator, and three of its assumptions failed:

  • Same error line means same failure. A typical miss: edit, 2/14; edit, 3/14; edit, 4/14; edit, 5/14, at the same error location every time, counted as eight wasted operations out of eight (Figure 15).
  • Plans matched on their first word. Codex's generator writes plans like isolate:<path> and handoff:42:0. Keyed on the verb alone, 88% of the plans flagged as repeats were different actions (fresh OOD batch).
  • No check for progress. The revised transformer blocks STALLED when the passing count reaches a new high, the check from Experiment 3. The no-tools lineage never built it.
An eight-row event table alternating file edits and test runs. Tests passing rise from 2 to 3, 4 and 5 of 14 while the error stays at contract_0.clj line 354, a file never edited. The no-tools revision marks every event as wasted and calls the window STALLED. The labeller marks every edit as progress and every run as no effect, and calls it PRODUCTIVE, as does the revised transformer.
The count climbs, but the no-tools revision calls it stuck
No-tools revision: STALLEDLabeller: PRODUCTIVERevised transformer: PRODUCTIVE
Figure 15. One window from the fresh OOD batch (seed 20261107, shard_migration family), not the sealed set: its eight events, the tests passing after each run, and how the no-tools revision and the labeller scored each event.
Show the numbers
Event (log line)Error locationTests passingNo-tools revision (rule)Labeller
write_file contract_5 → okwasted (EDR)progress
run_tests → FAILED 2/14contract_0.clj:3542 of 14wasted (g_SAME)no effect
write_file contract_10 → okwasted (EDR)progress
run_tests → FAILED 3/14contract_0.clj:3543 of 14wasted (g_SAME)no effect
write_file contract_13 → okwasted (EDR)progress
run_tests → FAILED 4/14contract_0.clj:3544 of 14wasted (g_SAME)no effect
write_file contract_11 → okwasted (EDR)progress
run_tests → FAILED 5/14contract_0.clj:3545 of 14wasted (g_SAME)no effect
One window from the shard_migration family, fresh OOD batch, seed 20261107; not the sealed set. Rules that fired in the no-tools revision: EDR, a file change within 3 events after a repeated test result; g_SAME, a failure whose error line was seen in the last 15 events. Verdicts: no-tools revision (v0.5 B) 8 of 8 events wasted, STALLED; labeller progress +0.29, 0 wasted, PRODUCTIVE; revised transformer (v0.3 B) sees new highs 2, 3, 4, 5 and calls it PRODUCTIVE.
Source: experiments/v0_5/ood_analysis/FINDINGS.md §4 @ 82cc5ec; per-window rows regenerated by experiments/v0_5/ood_analysis/analyse.py @ 369beae (gitignored)

The checks that held up were structural: an edit that returns a file to an earlier state is an undo whichever generator wrote it. The checks that broke were behavioural (Figure 16). Rules that describe the log's structure transfer; rules that describe one generator's habits don't.

Slope chart of how often the labeller agreed with four of the no-tools revision's wasted-effort rules, fresh in-distribution batch to fresh OOD batch. The structural undo rule stays near the top, 99.5% to 96%. The three behavioural rules drop: edit after a repeat 84% to 28%, repeated target 79% to 36%, repeated error line 62% to 44%.
Structural rules travel; behavioural ones don't
Labeller agrees with the rule's flag, fresh batches
Figure 16. Share of the no-tools revision's 'wasted effort' flags the labeller agreed with, by rule, on a fresh in-distribution batch (473 sessions) and a fresh OOD batch (1,000 sessions from Codex's generator, seed 20261107). Fresh batches, not sealed sets.
Show the numbers
Labeller agreement, by rule
RuleKindFlagsIn distributionOOD
UndoStructurala hunk that returns a file to an earlier state (kept from the no-tools draft)99.5% of 1,27096% of 3,747
Edit after a repeatBehaviourala file change within 3 operations after a repeated test result84% of 68128% of 5,519
Repeated targetBehaviourala repeated read, search, plan or build target79% of 1,38936% of 5,294
Repeated error lineBehaviourala failure whose error line was seen in the last 15 operations62% of 2,26144% of 12,689
Agreement: of the operations where the no-tools revision (v0.5 B) raised its "wasted effort" flag and this rule fired, the share the labeller also marks wasted (redundant, or a reversion). Each operation is counted once across overlapping windows; "edit after a repeat" and "repeated target" are counted where they fire alone, as in FINDINGS.md §3. A further structural rule, an edit that changes nothing, agreed on all 59 of its in-distribution operations and never fired in the fresh OOD batch. Fresh in-distribution batch: the original simulator, 473 sessions, 12,825 operations. Fresh OOD batch: Codex's generator, master seed 20261107, 1,000 sessions, 47,847 operations. Neither batch is a sealed set.
Share of all operations flagged as wasted effort
BatchNetworkLabeller
Fresh in-distribution batch47%38%
Fresh OOD batch57%29%
7,175 of 21,879 productive windows in the fresh OOD batch were called STALLED. On those, the labeller saw real progress in 92% and no wasted operation in 61%; the network counted 5 to 8 wasted operations in every one.
Source: experiments/v0_5/ood_analysis/FINDINGS.md §2–§3 @ 82cc5ec; experiments/v0_5/ood_analysis/analyse.py @ 369beae

Caveats. This explains the gap; it doesn't show a progress check would close it, and no patched network should be scored on the sealed OOD set. It covers Codex's generator only.

Experiment 7: Can Fable find what broke without being told?

A fix written from my diagnosis would just be me programming through Fable. So a third session got labelled windows from Codex's generator and nothing from the diagnosis. Would it find the faults itself, and would its fixes carry over to families of trajectories it had never seen?

Setup

  • Network: the no-tools revision as the base, in a new session.
  • What Fable received: the same tools and rules, the same 200 val windows, and 200 new labelled windows from Codex's generator, drawn from 6 of its 10 families. Nothing from the diagnosis.
  • Not allowed: the standing guards.
  • Scoring: two held-out sets, hashed before launch and scored once: 2,000 sessions from Codex's generator across all 10 families, 4 of which Fable never saw, and 4,000 from the original simulator. The sealed sets stayed closed.
  • Cost: US$56.58 of model usage (API-equivalent), 66 minutes, US$250 cap.

On the four families it never saw, the third round scored 0.750 [0.743, 0.757] (held-out OOD), with false alarms of 3.0%, the fewest of any network. Paired, that is +0.071 [+0.064, +0.079] over the no-tools revision and +0.031 [+0.024, +0.039] over the revised transformer. On the original simulator it gave a little back: −0.011 [−0.013, −0.009] and −0.006 [−0.010, −0.001] against the same two (held-out in-distribution), with fewer false alarms than either.

Held-out set (scored once)Third round (v0.5 Phase C)No-tools revision (v0.5 B)Revised transformer (v0.3 B)Trained transformer (v0.3 C-all)
Codex's generator, 4 unseen families0.750, false alarms 3.0%0.679, false alarms 19.4%0.718, false alarms 4.3%0.349, false alarms 12.5%
Codex's generator, all 10 families0.955, false alarms 4.2%0.766, false alarms 31.2%0.906, false alarms 6.9%0.494, false alarms 34.7%
Original simulator0.949, false alarms 2.1%0.960, false alarms 3.2%0.954, false alarms 2.4%0.923, false alarms 6.4%

Every network scores low on the unseen families: they hold only 305 STALLED windows (about 1%), all from one family, and every network scores close to zero on them.

Dot plot of the ten held-out OOD families, with macro-F1 and false-alarm rate for four networks; the four families no network had labels from are listed first. In every family the third round has a higher macro-F1 and fewer false alarms than the no-tools revision. In the unseen families it is ahead of the revised transformer in serializer_duel and lockfile_churn, and behind by at most 0.004 in the other two. The trained transformer is lowest by macro-F1 in every family.
The third round is ahead of the no-tools revision in all ten families
  • Third round
  • No-tools revision
  • Revised transformer
  • Trained transformer
Macro-F1, higher is better
False alarms, lower is better
Figure 17. Macro-F1 and false-alarm rate per family on the held-out OOD set (Codex's generator: 2,000 sessions, 800 in the four unseen families), scored once. Only the third round saw labels from this generator. lockfile_churn holds all the unseen families' STALLED windows, and every network's STALLED F1 there is near zero.
Show the numbers
Family, networkThird round saw labelsMacro-F1 [95% CI]False alarms [95% CI]Scored windows
index_watermark, Third round (v0.5 Phase C)no0.997 [0.995, 0.999]0.0% [0.0%, 0.0%]7,096
index_watermark, No-tools revision (v0.5 B)no0.935 [0.929, 0.941]11.1% [10.1%, 12.2%]7,096
index_watermark, Revised transformer (v0.3 B)no1.000 [1.000, 1.000]0.0% [0.0%, 0.0%]7,096
index_watermark, Trained transformer (v0.3 C-all)no0.712 [0.689, 0.735]3.2% [2.7%, 3.7%]7,096
lease_reconnect, Third round (v0.5 Phase C)no0.996 [0.995, 0.997]0.7% [0.4%, 0.9%]6,755
lease_reconnect, No-tools revision (v0.5 B)no0.925 [0.918, 0.932]13.2% [12.1%, 14.4%]6,755
lease_reconnect, Revised transformer (v0.3 B)no1.000 [1.000, 1.000]0.0% [0.0%, 0.0%]6,755
lease_reconnect, Trained transformer (v0.3 C-all)no0.644 [0.624, 0.662]12.7% [11.3%, 14.1%]6,755
serializer_duel, Third round (v0.5 Phase C)no0.950 [0.945, 0.954]9.4% [8.4%, 10.6%]7,713
serializer_duel, No-tools revision (v0.5 B)no0.830 [0.818, 0.841]41.8% [39.0%, 44.8%]7,713
serializer_duel, Revised transformer (v0.3 B)no0.933 [0.928, 0.938]7.3% [7.0%, 7.7%]7,713
serializer_duel, Trained transformer (v0.3 C-all)no0.329 [0.304, 0.353]42.8% [36.2%, 49.5%]7,713
lockfile_churn, Third round (v0.5 Phase C)no0.667 [0.658, 0.676]5.4% [5.1%, 5.8%]8,710
lockfile_churn, No-tools revision (v0.5 B)no0.569 [0.564, 0.573]22.3% [21.3%, 23.4%]8,710
lockfile_churn, Revised transformer (v0.3 B)no0.592 [0.587, 0.596]12.6% [12.1%, 13.0%]8,710
lockfile_churn, Trained transformer (v0.3 C-all)no0.257 [0.254, 0.259]3.8% [3.4%, 4.3%]8,710
merge_ping_pong, Third round (v0.5 Phase C)yes0.912 [0.906, 0.918]15.0% [14.1%, 16.1%]8,760
merge_ping_pong, No-tools revision (v0.5 B)yes0.783 [0.772, 0.793]41.2% [39.3%, 43.1%]8,760
merge_ping_pong, Revised transformer (v0.3 B)yes0.883 [0.877, 0.889]5.7% [5.4%, 5.9%]8,760
merge_ping_pong, Trained transformer (v0.3 C-all)yes0.611 [0.584, 0.637]59.4% [55.3%, 63.7%]8,760
telemetry_gaps, Third round (v0.5 Phase C)yes0.922 [0.910, 0.933]8.8% [7.6%, 10.0%]7,165
telemetry_gaps, No-tools revision (v0.5 B)yes0.781 [0.764, 0.795]29.9% [27.6%, 32.3%]7,165
telemetry_gaps, Revised transformer (v0.3 B)yes0.919 [0.910, 0.928]9.5% [8.1%, 11.0%]7,165
telemetry_gaps, Trained transformer (v0.3 C-all)yes0.578 [0.555, 0.600]21.9% [19.0%, 25.1%]7,165
annotation_spiral, Third round (v0.5 Phase C)yes0.990 [0.988, 0.992]1.6% [1.3%, 1.9%]7,699
annotation_spiral, No-tools revision (v0.5 B)yes0.775 [0.766, 0.784]41.4% [39.3%, 43.5%]7,699
annotation_spiral, Revised transformer (v0.3 B)yes0.910 [0.903, 0.917]16.0% [14.5%, 17.4%]7,699
annotation_spiral, Trained transformer (v0.3 C-all)yes0.449 [0.438, 0.461]65.6% [62.5%, 68.7%]7,699
fixture_archaeology, Third round (v0.5 Phase C)yes0.959 [0.953, 0.964]4.6% [3.9%, 5.2%]7,935
fixture_archaeology, No-tools revision (v0.5 B)yes0.628 [0.615, 0.643]41.2% [39.2%, 43.0%]7,935
fixture_archaeology, Revised transformer (v0.3 B)yes0.870 [0.862, 0.878]9.0% [8.3%, 9.7%]7,935
fixture_archaeology, Trained transformer (v0.3 C-all)yes0.457 [0.442, 0.472]59.5% [56.7%, 62.1%]7,935
shard_migration, Third round (v0.5 Phase C)yes0.974 [0.966, 0.981]0.6% [0.4%, 0.8%]7,727
shard_migration, No-tools revision (v0.5 B)yes0.603 [0.593, 0.613]34.8% [33.6%, 36.1%]7,727
shard_migration, Revised transformer (v0.3 B)yes0.876 [0.868, 0.883]5.1% [4.3%, 5.8%]7,727
shard_migration, Trained transformer (v0.3 C-all)yes0.318 [0.301, 0.335]29.0% [28.3%, 29.8%]7,727
pinned_validation, Third round (v0.5 Phase C)yes0.971 [0.966, 0.977]2.4% [2.0%, 2.9%]7,710
pinned_validation, No-tools revision (v0.5 B)yes0.517 [0.498, 0.536]48.7% [46.7%, 50.6%]7,710
pinned_validation, Revised transformer (v0.3 B)yes0.628 [0.619, 0.637]8.0% [7.5%, 8.6%]7,710
pinned_validation, Trained transformer (v0.3 C-all)yes0.420 [0.402, 0.437]69.5% [67.9%, 71.0%]7,710
Held-out OOD (Codex's generator), 2,000 sessions, 800 of them in the 4 unseen families, scored once; per family from sets.heldout_ood.populations.all.per_family. Only the third round saw labelled windows from this generator, drawn from the 6 seen families. False alarms: productive or recovery windows called STALLED or OSCILLATING, as a share of productive and recovery windows. 95% session-bootstrap CIs. Pooled over the unseen families: third round 0.750, no-tools revision 0.679, revised transformer 0.718, trained 0.349. The unseen families hold only 305 STALLED windows (1.0% of 30,274 scored), all in lockfile_churn, where every network's STALLED F1 is near zero (third round 0.079, the others 0.000).
Source: artifacts/reports/v05c_heldout.json (sets.heldout_ood.populations.all.per_family) @ cefd087; experiments/v0_5/phase_c/heldout/README.md @ d23acd3

How it did it. Without being told, it found two of the three faults: "The second generator reports one error line per session, so Phase B treated an improving repair (W008: 1/13, 2/13, 3/13, 4/13 at the same line) as a stall." A failure now counts as a repeat only if its error line and its passing count match, and a plan like isolate:<path> is keyed on its argument. It didn't add a progress check. Instead, having noted a difference between the generators, it made a fresh file read block STALLED, accepting two wrong answers on the original 200 windows to get six of the new ones right.

This isn't a like-for-like win: only the third round saw labelled windows from Codex's generator. What it shows is narrower. Given labels from a second source, Fable fixed two of three faults without being told what they were, and the gains transferred across to trajectories it hadn't seen. Those trajectories come from the same generator, so this says nothing yet about a third. The whole no-tools lineage cost US$114.02 of model usage (API-equivalent) over three sessions (Figure 18).

Three steps joined by arrows. No-tools draft, Experiment 4: written in one prompt with no tools and no labels, US$16.70; 0.326 sealed test, 0.292 sealed OOD. No-tools revision, Experiment 5: the draft repaired with tools and 200 labelled windows, US$40.75; 0.959 sealed test, 0.765 sealed OOD. Third round, Experiment 7: revised again with 200 labelled windows from Codex's generator, US$56.58; 0.949 held-out in-distribution, 0.750 held-out OOD, 4 unseen families. Scores are macro-F1.
Three sessions, each given a little more, for US$114.02 in all
No-tools draft, Experiment 4
Written in one prompt, with no tools and no labels
US$16.70
0.326sealed test
0.292sealed OOD
No-tools revision, Experiment 5
The draft, repaired with tools and 200 labelled windows
US$40.75
0.959sealed test
0.765sealed OOD
Third round, Experiment 7
Revised again, adding 200 labelled windows from Codex's generator
US$56.58
0.949held-out in-distribution
0.750held-out OOD, 4 unseen families
Figure 18. Each round was a new Fable session, handed the previous weights and written replies but not its memory. Scores are macro-F1 on the set named under each; cost is model usage (API-equivalent).
Show the numbers
The no-tools lineage
SessionLabelsCostTimeWroteMacro-F1 (set; false alarms)
No-tools draft (v0.5 r1), Experiment 40US$16.70about 58 min, 4 turns617; 66,014 non-zero weights0.320 (full val; false alarms 95.8%); 0.326 (sealed test; false alarms 95.7%); 0.292 (sealed OOD; false alarms 81.5%)
No-tools revision (v0.5 B), Experiment 5200US$40.75about 53 + 21 min (2 launches)1,262; 2,813 weight entries changed0.957 (full val; false alarms 3.2%); 0.959 (sealed test; false alarms 3.2%); 0.765 (sealed OOD; false alarms 31.4%)
Third round (v0.5 Phase C), Experiment 7400US$56.58about 66 min, 114 turns430; 999 weight entries changed0.750 (held-out OOD, 4 unseen families; false alarms 3.0%); 0.955 (held-out OOD, all 10 families; false alarms 4.2%); 0.949 (held-out in-distribution; false alarms 2.1%)
Each session was new: handed the previous weights and written replies, not the memory. Cost is model usage as Claude Code reported it (API-equivalent); the lineage total is the exact sum, US$114.02. The no-tools revision's two launches are one session, resumed after a plan usage limit with a fixed "continue, same rules" message. Full val excludes the 200 disclosed sessions. Sealed test and sealed OOD were scored once; the held-out sets (Codex's generator, 2,000 sessions; original simulator, 4,000) were scored once, and only the third round saw labelled windows from Codex's generator. The diagnosis (Experiment 6) ran between the second and third sessions and was not shown to Fable.
For comparison: the code-written transformer
NetworkMadeCostMacro-F1 (set; false alarms)
Zero-label transformer (v0.3 A)written with code, no labelsUS$42.730.744 (sealed test; false alarms 1.9%); 0.815 (sealed OOD; false alarms 4.0%)
Revised transformer (v0.3 B)revised in the same session after 200 labelled windowsUS$21.890.957 (sealed test; false alarms 2.5%); 0.908 (sealed OOD; false alarms 6.8%)
Both in one session. The v0.3 costs are the corrected figures from experiments/v0_4/STATUS.md.
Source: experiments/v0_5/runs/r1/run.json and apply.json @ 5b214c6; weight_program/v0_5/phase-b/BUILD_RECORD.json @ b1f9e3b; weight_program/v0_5/phase-c/BUILD_RECORD.json @ 6f6980e; artifacts/reports/v05_sealed.json @ cba5a80; artifacts/reports/v05c_heldout.json @ cefd087

Caveats. Three of its new rules are tuned to Codex's generator, so I treat the six families it saw as flattering and the four it didn't as the fairer test. An audit on fresh sessions found no sign it detects which generator wrote a log: changing the clock, gaps or token counts changed none of its decisions, and renaming paths and suites changed 10 of 15,824 windows, against 11 for a control rename. Fable tried about 31 hand-written rule variants against the 400 windows and chose after reading their scores; a few choices were numeric (a threshold of 5 rather than 4). Its thinking is redacted in the logs. Both held-out sets have now been used.

Writing v Training

Experiment 8: Does writing hold up against training?

Writing a network is only interesting if it competes or beats the normal route. So the same transformer was trained by gradient descent at five label budgets, and everything was scored once on both sealed sets.

Setup

  • Network: the same transformer, randomly initialised.
  • Training: cross-entropy loss (the negative log of the probability given to the right answer), the Adam optimiser at learning rate 3e-4, and gradient clipping.
  • Early stopping: after each epoch, score a fixed 500-session val subset and keep the best checkpoint. This makes trained val figures optimistic.
  • What the trainer received: labelled dev windows only: 590, 2,216, 9,114, 37,285 and 223,930. Three seeds for the three smallest budgets, one for the two largest.
  • Not allowed: the spec.
  • Scoring: sealed test (3,996 sessions) and sealed OOD (2,000), once.
  • Cost: eleven runs in about 9.3 hours on two CPU threads of an Intel Mac, deterministic algorithms on; the largest took about 3.75 hours for three epochs (full passes over the data). The labels were free, from the simulator.
Macro-F1, sealed testFalse alarms, sealed testMacro-F1, sealed OODFalse alarms, sealed OOD
Zero-label transformer (written, 0 labels)0.744 [0.734, 0.754]1.9%0.815 [0.806, 0.823]4.0%
Revised transformer (written, 200 labelled windows)0.957 [0.953, 0.961]2.5%0.908 [0.905, 0.912]6.8%
Trained transformer (223,930 labelled windows)0.923 [0.918, 0.929]6.1%0.488 [0.478, 0.497]34.5%

In distribution, the written network is marginally ahead. Off distribution, the trained one fails. The 0.034 lead on sealed test is outside both intervals (no paired interval exists for this pair), but training stopped while still improving, so I wouldn't lean on it. On sealed OOD every trained transformer scored below the zero-label written one; the 9,114-window runs averaged about 0.31, and one seed flagged 80% of healthy windows. The revised network's macro-F1 is above the trained one's in all 10 OOD families. A fresh draw reproduced the gap: 0.906 against 0.494 (held-out OOD, 2,000 new sessions).

Slope chart of macro-F1 from the sealed test split to the sealed OOD split. The written networks stay high: the zero-label one goes from 0.744 to 0.815 and the revised one, written with 200 labels, from 0.957 to 0.908, while every trained network falls on OOD, the one trained on 223,930 labels from 0.923 to 0.488. Below, false-alarm rates on a log scale from sealed test to sealed OOD: 4.0% and 6.8% on sealed OOD for the written networks, against 34.5% to 68.6% for the trained ones.
Off distribution, the written networks held up and the trained one fell
Macro-F1
False alarms
Figure 19. Macro-F1 from sealed test to sealed OOD for the written networks (coral), the trained ones by labelled windows (slate; small dots are seeds) and the always-PRODUCTIVE prior. Below, false alarms from sealed test (ring) to sealed OOD (dot, rate printed), log scale.
Show the numbers
v0.3, sealed splits
NetworkSealed test macro-F1Sealed OOD macro-F1Sealed test false alarmsSealed OOD false alarms
Prior (always PRODUCTIVE)0.136 [0.132, 0.139]0.181 [0.180, 0.182]0.0% [0.0%, 0.0%]0.0% [0.0%, 0.0%]
Zero-label transformer (written, no labels)0.744 [0.734, 0.754]0.815 [0.806, 0.823]1.9% [1.7%, 2.1%]4.0% [3.7%, 4.3%]
Revised transformer (written, 200 labels)0.957 [0.953, 0.961]0.908 [0.905, 0.912]2.5% [2.2%, 2.7%]6.8% [6.4%, 7.1%]
Trained transformer, 590 labels (C-32), mean of 3 seeds0.347 [0.337, 0.357]0.241 [0.235, 0.247]22.8% [21.6%, 23.9%]51.2% [49.5%, 52.9%]
Trained transformer, 2,216 labels (C-128), mean of 3 seeds0.450 [0.440, 0.459]0.287 [0.282, 0.292]12.2% [11.4%, 13.1%]37.8% [36.5%, 39.1%]
Trained transformer, 9,114 labels (C-512), mean of 3 seeds0.707 [0.696, 0.718]0.305 [0.297, 0.313]9.0% [8.4%, 9.6%]68.6% [67.4%, 69.9%]
Trained transformer, 37,285 labels (C-2048), 1 seed0.755 [0.743, 0.767]0.400 [0.391, 0.409]4.4% [3.9%, 4.9%]50.4% [48.8%, 52.0%]
Trained transformer, 223,930 labels (C-all), 1 seed0.923 [0.918, 0.929]0.488 [0.478, 0.497]6.1% [5.6%, 6.6%]34.5% [33.2%, 36.0%]
False alarms: share of PRODUCTIVE or RECOVERY windows flagged STALLED or OSCILLATING. 95% session-bootstrap CIs; for 3-seed means, the CI is over seed means. Sealed test: 3,996 sessions, 75,022 scored windows. Sealed OOD: 2,000 sessions, 77,538 scored windows. Each sealed split was scored once.
v0.2, the earlier and easier dataset
NetworkSealed test macro-F1Sealed OOD macro-F1Sealed OOD false alarms
v0.2 A0.6670.8101.3%
v0.2 B0.9360.8708.5%
v0.2 C-32 (mean of 3 seeds)0.6180.51730.6%
v0.2 C-128 (mean of 3 seeds)0.9320.64439.1%
v0.2 C-512 (mean of 3 seeds)0.9660.72728.6%
v0.2 C-2048 (mean of 3 seeds)0.9850.76819.9%
v0.2 C-all (mean of 3 seeds)0.9920.84311.6%
The same methods on the v0.2 dataset (the warm-up MLP's), for comparison only.
Source: artifacts/reports/v03_sealed.json @ 67fc2b1; artifacts/reports/m4_sealed_v02.json @ 48985d7; blog_figures/F6.json @ bf164d1

Training worked, and was still improving when stopped: macro-F1 rose with labels from 0.347 to 0.923 (sealed test), and the largest run was still improving at its cap (Figure 20): its early-stopping score rose 0.830, 0.912 and 0.934 over its three epochs (500-session val subset, not sealed). Off distribution, more labels helped much less: 0.241 to 0.488 (sealed OOD). The trained network mistook progress for stalls and lost the undo detector (Figure 21). I never inspected it, so its failure is described only.

Line chart of sealed test macro-F1 against labelled training windows on a log scale. The trained networks rise from 0.347 at 590 labelled windows to 0.923 at 223,930. Two coral stars mark the networks Fable wrote: the zero-label one at 0.744 with no labels, and the revised one at 0.957 after 200; their scores run across the chart as reference lines. The trained curve passes the zero-label line at 37,285 labels and ends just below the revised one.
Labels kept helping, but training stayed below the revised network
Sealed test
Figure 20. Trained macro-F1 on sealed test by labelled windows (log scale; small dots are seeds, one seed at the two largest budgets), with the two networks Fable wrote as coral stars at 0 and 200 labels.
Show the numbers
Macro-F1 by labelled windows
ModelLabelled windowsSeedsSealed testSealed OOD
Zero-label transformer (written by Fable)0n/a0.744 [0.734, 0.754]0.815 [0.806, 0.823]
Revised transformer (written by Fable)200n/a0.957 [0.953, 0.961]0.908 [0.905, 0.912]
Trained, C-32 (32 sessions)59030.347 (0.341, 0.317, 0.383)0.241 (0.297, 0.188, 0.239)
Trained, C-128 (126 sessions)2,21630.450 (0.421, 0.441, 0.489)0.287 (0.274, 0.262, 0.327)
Trained, C-512 (509 sessions)9,11430.707 (0.716, 0.700, 0.705)0.305 (0.297, 0.342, 0.275)
Trained, C-2048 (2,027 sessions)37,28510.7550.400
Trained, C-all (11,864 sessions)223,93010.9230.488
v0.2 C-32 (trained, earlier dataset)56330.6180.517
v0.2 C-128 (trained, earlier dataset)2,20730.9320.644
v0.2 C-512 (trained, earlier dataset)9,21630.9660.727
v0.2 C-2048 (trained, earlier dataset)38,12030.9850.768
v0.2 C-all (trained, earlier dataset)223,93030.9920.843
Written networks: 95% session-bootstrap CIs. Trained: mean over seeds, each seed in brackets; their CIs are in artifacts/reports/v03_sealed.json. Sealed test: 3,996 sessions, 75,022 scored windows. Sealed OOD: 2,000 sessions, 77,538 scored windows, from an independently written generator. Each sealed split was scored once. The revised network was revised after seeing 200 labelled val windows. C-all's budget was 12,000 sessions; 11,864 were available. v0.2 rows are a different, easier dataset.
The largest run (223,930 labelled windows), early-stopping score by epoch
EpochVal subset macro-F1
10.830
20.912
3 (cap)0.934
Scored after each epoch on a fixed 500-session val subset (9,350 windows; the 200 disclosed sessions removed) that also chose the checkpoint kept, so it is optimistic and not sealed. Training stopped at its registered 3-epoch cap, still rising. From experiments/phase-c-trained/v0_3/all/seed0/training_log.json.
Source: artifacts/reports/v03_sealed.json @ 67fc2b1; experiments/phase-c-trained/v0_3/*/seed*/training_log.json @ a934164; blog_figures/F7.json @ bf164d1
Four 4-by-4 confusion matrices: the written and trained transformers on sealed test (top) and sealed OOD (bottom). Three are strongly diagonal. On sealed OOD the trained network calls 13,828 of 44,002 PRODUCTIVE windows STALLED (31%, against 2,041 for the written network) and 4,235 of 12,632 OSCILLATING windows PRODUCTIVE.
Off distribution, the trained network calls healthy work stuck
Sealed test
Sealed OOD
Figure 21. Row-normalised confusion matrices for the revised written transformer and the trained transformer: rows are the true class, columns the class each network called. The upper pair is sealed test (75,022 windows), the lower pair sealed OOD (77,538).
Show the numbers
Sealed test: 3,996 sessions, 75,022 scored windows
Network, true classCalled PRODUCTIVECalled RECOVERYCalled STALLEDCalled OSCILLATINGWindowsRecall
Revised transformer, true PRODUCTIVE26,57640230262327,9030.952
Revised transformer, true RECOVERY38511,54055011,9800.963
Revised transformer, true STALLED34156319,913620,8230.956
Revised transformer, true OSCILLATING43403113,85114,3160.968
Trained transformer, true PRODUCTIVE24,6421,1565991,50627,9030.883
Trained transformer, true RECOVERY85210,81518313011,9800.903
Trained transformer, true STALLED34114919,89543820,8230.955
Trained transformer, true OSCILLATING20517014,09414,3160.984
Sealed OOD: 2,000 sessions, 77,538 scored windows
Network, true classCalled PRODUCTIVECalled RECOVERYCalled STALLEDCalled OSCILLATINGWindowsRecall
Revised transformer, true PRODUCTIVE39,7419642,0411,25644,0020.903
Revised transformer, true RECOVERY52411,81118036612,8810.917
Revised transformer, true STALLED10687,6262838,0230.951
Revised transformer, true OSCILLATING2540012,37812,6320.980
Trained transformer, true PRODUCTIVE16,5628,99713,8284,61544,0020.376
Trained transformer, true RECOVERY25411,4191,00820012,8810.886
Trained transformer, true STALLED1,1744736,1961808,0230.772
Trained transformer, true OSCILLATING4,2352293,7384,43012,6320.351
Counts of scored windows; rows are the oracle (true) label, columns the class each network called. Macro-F1: revised transformer 0.957 (sealed test), 0.908 (sealed OOD); trained transformer 0.923 (sealed test), 0.488 (sealed OOD). The sealed OOD sessions come from an independently written generator; each split was scored once. Revised transformer: written by Fable, revised after 200 labelled val windows. Trained transformer: the same architecture trained on 223,930 labelled windows, one seed.
Source: artifacts/reports/v03_sealed.json (splits.{test,ood}.populations.all.overall.columns[*].confusion_matrix) @ 67fc2b1

So the claim is narrow: on this benchmark, at the budgets I ran, a network compiled from a spec plus 200 labels was far more robust out of distribution than the same architecture trained on labels alone. How much of that gap is under-training is open. The trained warm-up MLP, run to convergence, also fell below its written counterpart (0.843, three-seed mean, against 0.870, sealed OOD), but it fell only from 0.992 (sealed test).

Caveats. The architecture was not neutral: no normalisation and a start-token column may make gradient descent harder. OOD labels come from the rules Fable was given, so a compiled network should transfer; what surprised me was how far the trained one fell. Nobody tested whether part of its drop is a reaction to the second generator's surface differences, though removing the midnight clock didn't help it: 0.494 on the re-timed held-out OOD set.

Modifying a Pretrained Model

Writing worked because Fable could state the rules and the weights were blank. Most of the time you don't start from blank weights; you start from a pre-trained model that already knows a lot. Can the same approach rewire one? Is a frontier model able to reason over the weights of another LLM and modify it correctly?

Experiment 9: Can Fable hand-edit Qwen into a classifier?

Qwen3.5-0.8B has far more built in knowledge than any spec could describe. With zero labels, can Fable find what it already represents and rewire it into a classifier?

Setup

  • Network: Qwen3.5-0.8B, in fp32 on the Mac's GPU: 752,393,024 language-model parameters (its vision encoder and multi-token-prediction head off).
  • How it classifies: a frozen prompt defines the classes and, after each block, ends in State:. The prediction is whichever of four single-token words ( productive, recovering, stalled, looping) gets the highest next-token logit (Figure 22).
  • What Fable received: the spec and a client that can read tensors and save edits; forward passes only on texts it wrote itself. No labels.
  • Not allowed: benchmark text, scores, gradients or fitting.
  • Enforcement: a model server owns the weights, and every forward pass is a visible, metered ./fwd command (2 million tokens per phase), so "try edits in a loop and keep the best" can't be scripted.
  • Scoring: the val quick look, a fixed 1,000-session subset chosen by hash in advance, paired against base Qwen.
  • Cost: US$32.58 of model usage (API-equivalent), 1 hour 51 minutes, on one M4 Pro.

That harness is cool piece of engineering in its own right. Even choosing the prompt was instructive. The pre-registered, label-free rule for choosing it picked a prompt under which Qwen answered "stuck" on every window, so the rule was revised, still without labels (Figure 23).

A two-line pipeline: a frozen 167-token prompt, the log so far and a suffix ending in 'State:' go into Qwen3.5-0.8B, where the edits are made, and the highest of four label-word logits is the answer. Below, for a chosen val window, three small bar charts (base Qwen, WSP-A, WSP-B) of the four label-word logits centred on each model's mean, each model's winning word in full colour, and whether it was right.
Qwen answers through its prompt: the highest of four words wins
Frozen prompt, 167 tokens+log so far+suffix ending “State:”
→Qwen3.5-0.8B, edited here→four label-word logits→highest wins
Window
Window 1 (val, quick look): true class PRODUCTIVE
Window 2 (val, quick look): true class PRODUCTIVE
Window 3 (val, quick look): true class PRODUCTIVE
Window 4 (val, quick look): true class OSCILLATING
Window 5 (val, quick look): true class PRODUCTIVE
Window 6 (val, quick look): true class RECOVERY
Figure 22. Each model's four label-word logits for one of six val windows (picked by a fixed rule that reads no labels), minus that model's mean of the four, in nats; 'looping' is the word for OSCILLATING. Val, quick look; bars past the scale print their values.
Show the numbers
Label-word logits
Window and model“productive”“recovering”“stalled”“looping”Answer
Window 1 (true PRODUCTIVE), Base Qwen19.2619.1717.7817.93“productive” (right)
Window 1 (true PRODUCTIVE), WSP-A24.7818.7417.9812.77“productive” (right)
Window 1 (true PRODUCTIVE), WSP-B15.1715.0814.1859.10“looping” (wrong)
Window 2 (true PRODUCTIVE), Base Qwen15.5219.1918.4317.95“recovering” (wrong)
Window 2 (true PRODUCTIVE), WSP-A14.6420.9018.1415.09“recovering” (wrong)
Window 2 (true PRODUCTIVE), WSP-B18.9119.9918.3717.93“recovering” (wrong)
Window 3 (true PRODUCTIVE), Base Qwen13.2818.1018.9318.87“stalled” (wrong)
Window 3 (true PRODUCTIVE), WSP-A6.9916.3621.0020.37“stalled” (wrong)
Window 3 (true PRODUCTIVE), WSP-B17.1219.7719.9120.75“looping” (wrong)
Window 4 (true OSCILLATING), Base Qwen13.9818.4918.8519.01“looping” (right)
Window 4 (true OSCILLATING), WSP-A8.3316.6118.3136.96“looping” (right)
Window 4 (true OSCILLATING), WSP-B17.4521.7618.7219.48“recovering” (wrong)
Window 5 (true PRODUCTIVE), Base Qwen14.2519.0118.8818.57“recovering” (wrong)
Window 5 (true PRODUCTIVE), WSP-A10.4019.1118.8919.67“looping” (wrong)
Window 5 (true PRODUCTIVE), WSP-B17.8721.1018.7721.03“recovering” (wrong)
Window 6 (true RECOVERY), Base Qwen13.4818.3719.0118.21“stalled” (wrong)
Window 6 (true RECOVERY), WSP-A7.6617.1220.7818.00“stalled” (wrong)
Window 6 (true RECOVERY), WSP-B17.8320.3218.6418.67“recovering” (right)
Raw next-token logits of the four label words, val quick look (1,000 sessions; 18,222 scored windows). The bars plot each logit minus that model's mean of the four (nats); two run off the scale and print their value. Prompt: 167 tokens before the log, 11 after it, ending in "State:"; of the 248,320 next-token logits only these four are read.
The six windows
WindowPicked becauseTrue classVal sessionLast blockBlocks read
Window 1Base Qwen answers productivePRODUCTIVEs-91230293fca6b6f11920
Window 2Base Qwen answers recoveringPRODUCTIVEs-d6a050c9dc9f7a6278
Window 3Base Qwen answers stalledPRODUCTIVEs-dce7349f802ecac61314
Window 4Base Qwen answers loopingOSCILLATINGs-3f38dd2d5d7b0b061213
Window 5WSP-B's answer differs from WSP-A'sPRODUCTIVEs-4b48186b6f02c2d71314
Window 6WSP-B's answer differs from WSP-A'sRECOVERYs-7ceff85fc201fa8c1718
Selection rule: key = sha256('hypanet-blog-f9:{session_id}:{window_end_block}'); candidates = scored windows of the 1,000 val quick-look sessions; slots 1-4: for each class (PRODUCTIVE, RECOVERY, STALLED, OSCILLATING) the smallest-key candidate that base Qwen answers with that class; slots 5-6: the two smallest-key remaining candidates where WSP-B's answer differs from WSP-A's. The rule reads predictions and hashes only; the true class is looked up after selection, for display.
The last eight blocks of each window, as Qwen reads them
BlockLog text
Window 1, block 1221:49:33.978 poll_status resource=ci/pipeline/1627 backoff=60 -> pending in=365 out=45
Window 1, block 1321:51:02.404 poll_status resource=ci/pipeline/1627 backoff=70 -> ready in=104 out=33
Window 1, block 1421:51:04.485 search scope=src query="decodeDiscount" -> ok in=293 out=35
Window 1, block 1521:51:05.420 read_file path=src/main/java/com/hooli/media/RetryPolicy.java lines=7-12 -> ok in=426 out=145
Window 1, block 1621:51:13.079 write_file path=src/main/java/com/hooli/media/RetryPolicy.java -> ok in=487 out=357 @ src/main/java/com/hooli/media/RetryPolicy.java h482bf76a96 > h596d22d281
Window 1, block 1721:51:20.235 edit_file path=src/main/java/com/hooli/media/RetryPolicy.java -> ok in=693 out=172 @ src/main/java/com/hooli/media/RetryPolicy.java ha44cb380b9 > h778c01a607
Window 1, block 1821:51:30.429 plan action=summarise_findings -> ok in=1115 out=464
Window 1, block 1921:51:32.565 plan action=review_diff -> ok in=907 out=121
Window 2, block 021:39:18.325 plan action=decompose -> ok in=2974 out=475
Window 2, block 121:39:34.591 run_tests suite=spec:unit -> FAILED 7/16 passed in=3625 out=77 ! KeyError at spec/notify/checksum_unit_spec.rb::checksum parse header large batch:466 (0xb5417530)
Window 2, block 221:39:36.181 read_file path=lib/notify/checksum.rb lines=5-16 -> ok in=252 out=74
Window 2, block 321:39:37.407 http_call GET https://honeycomb.io/1/query_results/{dataset}/{id} req=cb79f974 -> ok in=1851 out=41
Window 2, block 421:39:38.287 read_file path=logs/ci-280.log lines=1-2 -> ok in=494 out=191
Window 2, block 521:40:02.113 lint target=rubocop -> FAILED in=1962 out=167 ! Lint/UselessAssignment at lib/notify/checksum.rb:327 (0x93b755bf)
Window 2, block 621:40:03.115 search scope=src query="TODO" -> ok in=2016 out=138
Window 2, block 721:40:13.526 edit_file path=lib/notify/checksum.rb -> ok in=1277 out=403 @ lib/notify/checksum.rb hdf24405e32 > h7fff8e7f3f
Window 3, block 600:58:38.646 http_call GET https://ci.acme.dev/api/v4/jobs/{job_id}/trace req=5cb22f84 -> error in=1969 out=164 ! TimeoutError at ci.acme.dev/api/v4/jobs/{job_id}/trace:169 (0x3b4bf25d)
Window 3, block 700:58:49.950 http_call GET https://ci.acme.dev/api/v4/jobs/{job_id}/trace req=50868045 -> error in=856 out=64 ! TimeoutError at ci.acme.dev/api/v4/jobs/{job_id}/trace:169 (pid 21033)
Window 3, block 800:58:56.297 http_call GET https://ci.acme.dev/api/v4/jobs/{job_id}/trace req=b5b3b851 -> error in=1222 out=86 ! TimeoutError at ci.acme.dev/api/v4/jobs/{job_id}/trace:169 (0xeda3faa5)
Window 3, block 900:58:58.954 http_call GET https://ci.acme.dev/api/v4/jobs/{job_id}/trace req=db2b2ed2 -> ok in=635 out=131
Window 3, block 1000:59:00.463 search scope=lib query="convert_amount" -> ok in=1607 out=152
Window 3, block 1100:59:06.439 read_file path=lib/pipeline/graph_walk.rb lines=15-22 -> ok in=3090 out=191
Window 3, block 1200:59:10.627 edit_file path=lib/pipeline/graph_walk.rb -> ok in=1267 out=1345 @ lib/pipeline/graph_walk.rb h57d52e3380 > hb7451f029f
Window 3, block 1300:59:24.494 run_tests suite=spec:request -> FAILED 7/9 passed in=8606 out=171 ! NameError at spec/orders/parser_request_spec.rb::parser expand locale default config:147 (pid 29757)
Window 4, block 510:58:18.869 list_dir path=deploy -> ok in=309 out=102
Window 4, block 610:58:25.390 edit_file path=internal/orders/queue_worker.go -> ok in=881 out=158 @ internal/orders/queue_worker.go h36a1f4a4a6 > h7f1030e7bc
Window 4, block 710:58:36.698 run_tests suite=./internal/...:integration -> FAILED 4/7 passed in=1059 out=26 ! ErrUnexpectedEOF at internal/identity/feature_flags_integration_test.go::TestEstimateDigestConcurrentAccess:594 (0x2743ecdb)
Window 4, block 810:58:47.875 edit_file path=internal/orders/queue_worker.go -> ok in=309 out=185 @ internal/orders/queue_worker.go h7f1030e7bc > hef61741607
Window 4, block 910:58:56.057 run_tests suite=./internal/...:integration -> FAILED 4/7 passed in=933 out=92 ! ErrUnexpectedEOF at internal/identity/feature_flags_integration_test.go::TestEstimateDigestConcurrentAccess:594 (0xddb75ff3)
Window 4, block 1010:59:01.622 edit_file path=internal/orders/queue_worker.go -> ok in=574 out=116 @ internal/orders/queue_worker.go hef61741607 > h7f1030e7bc
Window 4, block 1110:59:07.954 run_tests suite=./internal/...:integration -> FAILED 4/7 passed in=1150 out=58 ! ErrUnexpectedEOF at internal/identity/feature_flags_integration_test.go::TestEstimateDigestConcurrentAccess:594 (0xb6e7e95d)
Window 4, block 1210:59:12.710 edit_file path=internal/orders/queue_worker.go -> ok in=588 out=211 @ internal/orders/queue_worker.go h7f1030e7bc > hef61741607
Window 5, block 620:36:11.948 run_tests suite=contractTests tests=src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleLeapYear,src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleRoundTrip,src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleRounding -> FAILED 0/3 passed in=2128 out=49 ! ClassCastException at src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleLeapYear:151 (0xeb409ece)
Window 5, block 720:36:15.977 write_file path=src/main/java/com/umbrella/sync/Webhook.java -> ok in=823 out=143 @ src/main/java/com/umbrella/sync/Webhook.java h76cb54341c > hc08a33f3b0
Window 5, block 820:36:41.059 run_tests suite=contractTests tests=src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleLeapYear,src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleRoundTrip,src/test/java/com/umbrella/sync/WebhookContractTest.java::scheduleLocaleRounding -> ok 3/3 passed in=782 out=57
Window 5, block 920:36:43.492 search scope=src query="encodeQuota" -> ok in=499 out=68
Window 5, block 1020:36:44.651 read_file path=src/main/java/com/umbrella/metrics/Money.java lines=7-24 -> ok in=695 out=118
Window 5, block 1120:36:50.684 http_call GET https://sentry.io/api/0/issues/{issue_id}/events/ req=6cbce982 -> ok in=435 out=38
Window 5, block 1220:36:52.279 edit_file path=src/main/java/com/umbrella/metrics/Money.java -> ok in=861 out=318 @ src/main/java/com/umbrella/metrics/Money.java h70799c87b1 > h0ed90831cd
Window 5, block 1320:37:07.399 run_tests suite=contractTests -> FAILED 11/16 passed in=2813 out=89 ! IllegalArgumentException at src/test/java/com/umbrella/metrics/CursorContractTest.java::splitAddressDefaultConfig:405 (0xb164a7e7)
Window 6, block 1019:22:15.887 read_file path=lib/media/search_index.rb -> ok in=1906 out=84
Window 6, block 1119:22:16.901 list_dir path=tests -> ok in=562 out=140
Window 6, block 1219:22:17.968 search scope=src query="copyright" -> ok in=1935 out=79
Window 6, block 1319:22:19.099 read_file path=docs/onboarding.md -> ok in=1478 out=133
Window 6, block 1419:22:20.417 search scope=. query="unsafe" -> ok in=431 out=91
Window 6, block 1519:22:21.234 list_dir path=lib/web -> ok in=204 out=81
Window 6, block 1619:22:46.017 run_tests suite=spec:request -> FAILED 3/11 passed in=1249 out=42 ! TypeError at spec/media/migrations_request_spec.rb::migrations estimate path boundary:306 (pid 41305)
Window 6, block 1719:23:02.230 run_tests suite=spec:request -> FAILED 3/11 passed in=5067 out=88 ! TypeError at spec/media/migrations_request_spec.rb::migrations estimate path boundary:306 (pid 54507)
Source: envs/qwen/hypaqwen/interface.py @ a616b93; experiments/v0_4/columns/ (val quick-look logits) @ 48acfc6
Top: bars of predicted-class shares on 2,069 dev windows. The first rule's pick answered STALLED 100% of the time; the prompt used gave PRODUCTIVE 7%, RECOVERY 28%, STALLED 40%, OSCILLATING 26%. Bottom: a scatter of twelve prompts by confidence and answer variety. Four prompts sit at variety 0 with the highest confidence, the first rule's pick among them; the prompt used sits highest, scoring 0.742.
Rewarding confidence alone picked a prompt that always said stuck
Answers on 2,069 dev windows
All twelve candidate prompts, dev
Figure 23. Share of the 2,069 dev windows base Qwen gave each answer under the first rule's pick and under the prompt used (top), and all twelve candidate prompts by confidence and answer variety (normalised entropy of the predicted classes). The revised rule's score is confidence × variety. 100 dev sessions; no labels used.
Show the numbers
PromptLabel wordsMedian massVarietyScorePredicted P / R / S / O
w1-state-full (used)productive / recovering / stalled / looping0.8160.9100.7427.2% / 27.5% / 39.6% / 25.6%
w1-answer-fullproductive / recovering / stalled / looping0.8630.6100.5262.4% / 2.1% / 36.0% / 59.6%
w1-state-shortproductive / recovering / stalled / looping0.7560.6840.51711.8% / 14.9% / 68.2% / 5.1%
w1-answer-shortproductive / recovering / stalled / looping0.7950.6100.48511.1% / 1.3% / 70.7% / 16.9%
w2-state-shortproductive / recovering / stuck / looping0.9050.2840.25710.0% / 1.3% / 88.7% / 0.0%
w2-answer-shortproductive / recovering / stuck / looping0.9160.2340.2149.6% / 0.1% / 90.3% / 0.0%
w2-state-fullproductive / recovering / stuck / looping0.9470.1630.1543.7% / 1.3% / 95.0% / 0.0%
w2-answer-fullproductive / recovering / stuck / looping0.9520.0110.0110.1% / 0.0% / 99.8% / 0.1%
w3-state-full (first rule's pick)progress / retry / stuck / loop0.9860.0000.0000.0% / 0.0% / 100.0% / 0.0%
w3-answer-fullprogress / retry / stuck / loop0.9830.0000.0000.0% / 0.0% / 100.0% / 0.0%
w3-answer-shortprogress / retry / stuck / loop0.9750.0000.0000.0% / 0.0% / 100.0% / 0.0%
w3-state-shortprogress / retry / stuck / loop0.9750.0000.0000.0% / 0.0% / 100.0% / 0.0%
Dev split: the 100 sessions with the smallest sha256 of "v04-prompt-choice:{session_id}", every window (2,069). No labels, gate results or families were read. Mass: the probability base Qwen puts on the four label words together, median over windows. Variety: entropy of the predicted-class shares divided by log 4 (0 = always one class, 1 = uniform). First rule: highest mass (w3-state-full). Revised rule: highest score = mass × variety (w1-state-full, 0.742; next best w1-answer-full, 0.526). P / R / S / O: share of windows answered PRODUCTIVE, RECOVERY, STALLED, OSCILLATING.
Source: artifacts/reports/v04_prompt_choice.json and artifacts/reports/v04_prompt_choice_revision1.json @ a616b93

Unedited, Qwen scores 0.212 [0.202, 0.222] (full val), +0.073 over the constant prior, with false alarms of 70.9%. It barely decides: the median gap between its top two label logits is 0.34 nats (natural-log units), so its top answer is only about 1.4 times as likely as the runner-up.

With zero labels, Fable's edit, WSP-A (for weight-space programming), gained +0.107 [+0.083, +0.130] over base Qwen, paired, reaching 0.330 (val, quick look). Its false alarms were 55.3%. The zero-label written transformer scored 0.750 (full val; cited, not paired). The edit touched 10 tensors and 10,240 of 752,393,024 entries (Figure 24). Every change is rank 1: an outer product of two vectors, the smallest structured change you can make to a matrix.

How it did it. It probed what the model represents, using synthetic sessions it wrote and labelled itself, and caught a confound on the way: "the top neurons mostly track session length because STALLED windows sit late and position is confounded with class. I'm switching to matched-pair directions, where prefixes are identical and only one property differs, so nuisance factors cancel." Then it found three MLP neurons silent at the readout position on its own sessions, and rewrote each as a hand-built AND gate, a shape Qwen's MLP unit (SwiGLU) already has. There is one each for OSCILLATING, STALLED and RECOVERY, in layers 23, 22 and 21, plus an embedding-row direction for productive (Figure 25). On its own sessions, the logit changes matched the design (correlation ≥ 0.98).

10,240 of 752,393,024 weights changed by WSP-A (0.00136%), every change rank 1. A strip of the model, the embedding then 24 layers, marks the edited 'productive' embedding row and one unit in each of layers 21, 22 and 23 as thin amber lines. Below, the 1,024 numbers of one edited row are drawn as a barcode. WSP-B changed 8,192 weights in 7 tensors.
The whole edit is one embedding row and three MLP units
10,240 of 752,393,024
weights changed by WSP-A: 0.00136%, every change rank 1
one row: “productive”one unit in each of layers 21–23
Embedding24 layers
One edited row: the 1,024 numbers Fable wrote
Figure 24. The weights WSP-A changed, as a strip of Qwen3.5-0.8B: the embedding (drawn to scale) and 24 layers, with each edited row or unit as an amber line. Below, one edited row in full. WSP-B, the revision, changed 8,192 weights in 7 tensors.
Show the numbers
TensorShapeWSP-A: weights changedWSP-B: weights changed
embed_tokens248,320 × 1,0241,024 (rank 1)2,048 (rank 2)
layers.21.mlp.down_proj1,024 × 3,5841,024 (rank 1)unchanged
layers.21.mlp.gate_proj3,584 × 1,0241,024 (rank 1)unchanged
layers.21.mlp.up_proj3,584 × 1,0241,024 (rank 1)unchanged
layers.22.mlp.down_proj1,024 × 3,5841,024 (rank 1)1,024 (rank 1)
layers.22.mlp.gate_proj3,584 × 1,0241,024 (rank 1)1,024 (rank 1)
layers.22.mlp.up_proj3,584 × 1,0241,024 (rank 1)1,024 (rank 1)
layers.23.mlp.down_proj1,024 × 3,5841,024 (rank 1)1,024 (rank 1)
layers.23.mlp.gate_proj3,584 × 1,0241,024 (rank 1)1,024 (rank 1)
layers.23.mlp.up_proj3,584 × 1,0241,024 (rank 1)1,024 (rank 1)
Total752,393,024 language-model weights10,240 in 10 tensors (0.00136%)8,192 in 7 tensors (0.00109%)
Counts and ranks from each edit's BUILD_RECORD.json. Units rewritten: layer 21 unit 1007 (recovering), layer 22 unit 1613 (stalled), layer 23 unit 1046 (looping); WSP-B reverts layer 21. Embedding rows are the label words (token ids productive 25,399, recovering 39,923, stalled 65,552, looping 60,477). 752,393,024 counts the language model only (vision encoder and multi-token-prediction head excluded). Every 4th layer uses full attention, the rest linear attention. In the strip the embedding is drawn to scale against the mean layer (254,279,680 weights), and each layer's amber line sits at its unit's position (unit over 3,584). The embedding row is drawn 40% down, not to scale: token 25,399 of 248,320 would sit at the top edge. The barcode is WSP-A's layer-23 gate_proj row 1046, the gate of the unit that writes to “looping” (edit_constants.safetensors, gate_row_L23; largest magnitude 0.361).
Source: weight_program/v0_4/wsp-a/BUILD_RECORD.json @ 5f58e4a; weight_program/v0_4/wsp-b/BUILD_RECORD.json @ d177712
Circuit diagram of the edit to Qwen3.5-0.8B: three neurons in layers 21, 22 and 23 each multiply a gate formula by an up formula over feature directions and drive one label word (recovering, stalled, looping) through a weighted wire; a label row adds a linear term to productive. Switching to WSP-B reverts the layer-21 neuron, gives recovering and productive label-row terms (productive with a 4.2 default) and rescales the STALLED wire, untested.
Three neurons became AND gates, each driving one label word
Figure 25. Each rewritten neuron multiplies a gate by an up term, both weighted sums of feature directions Fable measured at the readout, and its wire writes to one label word's logit. WSP-B, after the 200 disclosed windows, reverts the layer-21 neuron, adds label-row terms (a +4.2 default for productive) and scales the STALLED wire by 0.2764, untested.
Show the numbers
UnitGateUpWire
WSP-A: layer 21, unit 1007 (RECOVERY)silu(3 × (+ERR − 0))+RES +0.9× 0.4 → ‘ recovering’
WSP-A: layer 22, unit 1613 (STALLED)silu(3 × (+REP +2 CHAIN − 2.5))−RES −PROG +CHAIN −2 EDITS −2 EDITREPfree× 0.045 → ‘ stalled’ (shielded)
WSP-A: layer 23, unit 1046 (OSCILLATING)silu(3 × (+EDITS +EDITREPfree − 0.5))−PROG −RES +REP +CHAIN +1.5 EDITS +1.5 EDITREPfree +EDITCOUNT −2.5× 0.14 → ‘ looping’
WSP-A: ‘ productive’ label rownone (linear)+1.6 PROG +1.6 RES −1.6 REPadded to ‘ productive’
WSP-B: layer 21, unit 1007 (RECOVERY)reverted to original——
WSP-B: layer 22, unit 1613 (STALLED)silu(3 × (+REP +2 CHAIN +REPF −PEND −0.5 REPH − 3.478))−RES −PROG +CHAIN −2 EDITS −2 EDITREPfree −NFR −PEND −0.5 REPH −0.5 REPP +1.144× 0.1055 → ‘ stalled’ (shielded)
WSP-B: layer 23, unit 1046 (OSCILLATING)silu(3 × (+EDITS +EDITREPfree − 1.642))+1.5 EDITS +1.5 EDITREPfree +EDITCOUNT −NFR −2.433× 0.8142 → ‘ looping’
WSP-B: ‘ productive’ label rownone (linear)+4.2 +1.5 NFR −REPF −REPH −REPPadded to ‘ productive’
WSP-B: ‘ recovering’ label rownone (linear)+2 PEND +2 REPH +2 REPP −1.5 REPF +0.5 NFR +0.5 RESHadded to ‘ recovering’
Each neuron computes silu(3 × (gate − threshold)) × up, an AND gate; gate and up are weighted sums of standardised feature directions read at the ‘State:’ position. Feature directions: ERR, errors present and recent; RES, a success right after errors; PROG, improving validation, then a pass; REP, identical failing blocks repeat; EDITS, edits among repeated failures; EDITREPfree, edits repeat without errors; CHAIN, long poll or retry chains; EDITCOUNT, several edits, not one; NFR, reads or searches present; REPF, a failing test repeats; REPH, an http error repeats; REPP, a pending poll repeats; PEND, pending polls present; RESH, http retries, then ok (weak). The STALLED wire lies in the null space of layer 23's value projection, so that layer's attention can't carry it to the readout. WSP-B's STALLED wire scale is 0.382 × 0.2764 = 0.1055: the revision as run gave +18.1 logits (median) on the disclosed STALLED windows against a designed +5 (disclosed windows (200, 50 per class), revision ac483550 as run, in-sample). WSP-A's effects are on Fable's own sessions; WSP-B's on the 200 disclosed windows (in-sample).
Source: weight_program/v0_4/wsp-a/inputs/EDIT.md @ 5f58e4a; weight_program/v0_4/wsp-b/inputs/REVISION.md @ d177712

Experiment 10: Can 200 labelled windows close the gap?

With the same 200 windows, the written transformer's revision went from 0.750 to 0.954 (full val). WSP-B, the revised Qwen edit, gets those windows too: can benchmark data fix what Fable's own sessions couldn't?

Setup

  • Network: the same Qwen, starting from the WSP-A edit, in the same Fable session.
  • What Fable received: the 200 disclosed windows, each with its rendered prefix, true label and WSP-A's answer.
  • Not allowed: more than two edit states on the disclosed windows (WSP-A and one revision), logits only, which blocks fitting a probe (a small classifier trained on the model's activations) to the 200. The written revision could rerun freely, so this is not like for like.
  • Scoring: val quick look, paired against base Qwen.
  • Cost: US$29.81 of model usage (API-equivalent), about an hour; both rounds US$62.39, about the same as writing.

WSP-B reaches 0.419 [0.397, 0.441] (val, quick look), +0.196 [+0.165, +0.226] over base Qwen, paired, against 0.954 (full val; cited, not paired) for the written revision. Its false alarms are 47.6%. The final edit touches 7 tensors and 8,192 entries.

So no. Fable could route what Qwen already computes, but it didn't find anything that keeps counting past two repeats, and the stall rule needs five. Its WSP-A notes: "The model encodes presence and recency, never counts." It measured matched-pair directions (the mean hidden-state difference between texts that differ in one property) at the readout position:

  • The repetition signal saturates after two identical failing blocks. The written network's STALLED rule needs five (I5).
  • Two coarse many-against-few signals are encoded: four edits against one, and eleven polls against four.
  • Hunk-id reversal, the defining signal of OSCILLATING, had no separating power (d′ ≈ −0.3, where 0 means no separation).

Writing from zeros, Fable built a counter with the 1/(1+C) trick. Modifying Qwen, it didn't try to build one, so whether that can be done by hand is untested. Its explanation, that Qwen's linear-attention layers (which keep a running summary instead of every past token) overwrite repeated keys, is a hypothesis: the check wasn't run, and it covers only this 0.8B model at the readout, with these directions.

How it did it. The windows were a shock: WSP-A got 31.5% of them right, against 56% on its own synthetic sessions. "The real logs are long and busy, so the original model never says productive (its logit sits 4.2 below the others on every class)…" So "productive" became the default through a +4.2 term, RECOVERY moved into a linear term in the label rows, and the other two neurons were re-gated. Its second and last test showed the STALLED wire overshooting, +18.1 logits against a designed +5, so it rescaled that wire offline. (Rescaling an original tensor is refused, but this was its own added wire.) That fix was calculated, never tested (Figure 26). Its offline estimate, 0.44 mean per-class recall (disclosed windows, in-sample), came close to the 0.47 it scored on the same measure (val, quick look; plain accuracy 0.44, Figure 27).

Timeline of WSP-B's edit states on the 200 disclosed windows (in-sample): WSP-A at 31.5% and the revision at 42.5% (45 errors fixed, 23 broken) as filled points, four rejected designs as grey dots with no score, and the final state as a hollow point with an untested offline estimate of 44.0%.
Only two edit states were tested; the last fix was calculated
Figure 26. Accuracy of each edit state WSP-B ran, in order, on the 200 disclosed windows (50 per class, so class-balanced; in-sample). The final state rescaled the STALLED wire by 0.2764; it was estimated offline and never run.
Show the numbers
Edit stateEdit hash200 disclosed windows (accuracy, in-sample)Set R (Fable's own, 64 sessions)
WSP-A7457646631.5% (first test, calls 1–13)not run
Offline designs b1, b2, b3, b4nonenot run (rejected)not run
Revisionac48355042.5% (second test, calls 26–38); 45 fixed, 23 broken42% (calls 24–25)
Final WSP-B (STALLED wire × 0.2764)0ea2eb7744.0% offline estimate; not run44% (calls 39–40)
Unedited Qwen (reference)original24.0%19%
WSP-B could run at most 2 edit states on the disclosed windows. Accuracy there equals mean per-class recall, since there are 50 per class. The STALLED wire's median gain on the disclosed STALLED windows was +18.1 logits under the revision as run, against a designed +5; the × 0.2764 rescale was computed on those same windows. Set R is 64 sessions Fable wrote and labelled. Call numbers refer to WSP-B's forward-pass ledger.
Source: weight_program/v0_4/wsp-b/inputs/REVISION.md and experiments/v0_4/logs/wsp-b.ledger.jsonl @ d177712

All three routes:

No labels200 labelled windows223,930 labelled windows
Written (full val)0.750, false alarms 2.0%0.954, false alarms 2.4%
Trained (full val, optimistic)0.926, false alarms 5.8%
Modified (val, quick look)0.330, false alarms 55.3%0.419, false alarms 47.6%
Base Qwen, unedited0.212 (full val), false alarms 70.9%

The trained figure is optimistic because its early-stopping subset sits inside full val. Written against modified is cited, not paired, but base Qwen moved by only 0.011 between quick look (0.223) and full val (0.212), so gaps of about 0.42 and 0.53 are very unlikely to close. The edits made Qwen more certain faster than they made it right (Figure 27). WSP-B calls 43% of truly productive windows "looping", against 25% for base Qwen (val, quick look), plausibly because of the OSCILLATING neuron, though no ablation was run.

Three ridgelines of the gap between the top two label logits, shifting about a decade to the right from base Qwen (median 0.34 nats) to WSP-A (3.40) and WSP-B (2.14), beside accuracy dots that move only from 0.24 to 0.35 and 0.44; val, quick look.
The edits made Qwen more certain much faster than more right
Figure 27. The gap between the top two label logits over all 21,308 rows (log scale), beside accuracy over the 18,222 scored windows; val, quick look (1,000 sessions).
Show the numbers
ModelMedian gap (nats)Top vs runner-upMiddle half (p25 to p75)p5 to p95Accuracy [95% CI]
Base Qwen (unedited)0.341.4×0.15 to 0.600.03 to 1.110.24 [0.22, 0.27]
WSP-A (edited, no labels)3.4030×1.36 to 8.360.28 to 18.730.35 [0.33, 0.38]
WSP-B (edited, 200 windows)2.148.5×0.86 to 6.740.16 to 30.110.44 [0.41, 0.46]
All val, quick look (1,000 sessions chosen by hash in advance). The gap is the top-two label-logit margin; "top vs runner-up" is e raised to the median gap. Margins need no labels and cover all 21,308 rows, including gate-rejected and ambiguous ones; accuracy covers the 18,222 scored windows, with 95% session-bootstrap CIs from the committed reports. Rows below 0.001 nats (Base Qwen 32, WSP-A 4, WSP-B 9) and above 100 nats (Base Qwen 0, WSP-A 0, WSP-B 0) are counted but not drawn.
Source: experiments/v0_4/columns/*.val_quick.npz and artifacts/reports/v04_val_quick_wspb.json (readout_diagnostics) @ 48acfc6; blog_figures/F13.json @ bf164d1

You can rewire what a model notices. And a note for anyone who loads weights they didn't build: a 102 KB diff, written with no optimiser, measurably moved what the model says about a log. Verify their hashes.

Caveats. Quick look only. The pre-registered primary test (paired against the written network on full val) hasn't been run, and val isn't untouched: the written round's val results were known before this round was designed. A fresh sealed test set exists and stays sealed, by my choice; I wanted a quick answer, not 90 hours of readouts. The ×0.2764 rescale was computed on the same windows its 0.44 estimate is measured on. Fable copied its own files with shutil, and the run-time guard didn't refuse it. The audit flagged it, and Claude, acting for me and before any readout, assessed it as a rule breach with no effect on the evaluation. The audit and that assessment are published with the transcripts.

Results at a Glance

Every experiment's best number, each with its split and label budget. The rows are not a ranking.

ExpNetworkLabelsIn distributionOff distributionFalse alarms (in / off)Cost
1Warm-up MLP, written and revised (v0.2 B)2000.936 sealed test0.870 sealed OOD3.1% / 8.5%US$40.05 (all of v0.2)
1Warm-up MLP, trained (v0.2 C-all, 3 seeds)223,9300.992 sealed test0.843 sealed OOD0.8% / 11.6%CPU
—Rules engine from the spec (Codex)00.417 sealed test0.415 sealed OOD1.1% / 2.4%—
2Zero-label transformer (v0.3 A)00.744 sealed test0.815 sealed OOD1.9% / 4.0%US$42.73
3Revised transformer (v0.3 B)2000.957 sealed test0.908 sealed OOD2.5% / 6.8%US$64.62 (A+B)
4No-tools draft (v0.5 r1)00.326 sealed test0.292 sealed OOD95.7% / 81.5%US$16.70
5No-tools revision (v0.5 B)2000.959 sealed test0.765 sealed OOD3.2% / 31.4%US$40.75
6Why it didn't transfer (diagnosis)——32.8% of productive windows called STALLED (fresh OOD batch); 32.6% (sealed OOD)—CPU
7Third round (v0.5 Phase C) †200 + 200 from Codex's generator0.949 held-out in-distribution0.750 held-out OOD, 4 unseen families (revised transformer 0.718)2.1% / 3.0%US$56.58
8Trained transformer (v0.3 C-all)223,9300.923 sealed test0.488 sealed OOD6.1% / 34.5%~3.75 h CPU
9Qwen, zero-label edit (WSP-A)00.330 val, quick looknot measured55.3% / —US$32.58
10Qwen, revised edit (WSP-B)2000.419 val, quick looknot measured47.6% / —US$29.81
—Base Qwen, unedited00.212 full valnot measured70.9% / ——

Costs are model usage (API-equivalent). † Not like for like: it saw labels from the second generator.

Which Approach, When

The best version of each route side by side, on what I'd want to know before putting one into a system:

Write (revised transformer, v0.3 B)Train (trained transformer, v0.3 C-all)Modify (Qwen, WSP-B)
In distribution0.957 (sealed test)0.923 (sealed test)0.419 (val, quick look)
Out of distribution0.908 (sealed OOD)0.488 (sealed OOD)not measured
False alarms2.5% sealed test, 6.8% sealed OOD6.1% sealed test, 34.5% sealed OOD47.6% (val, quick look)
Learned fromspec + 200 labelled windows223,930 labelled windowsspec + 200 labelled windows, on top of pretraining
Cost to produceUS$64.62 of model usage (API-equivalent); about 3.8 h of Fable sessions (2.5 h after a first launch cut off by a usage limit)about 3.75 h on two CPU threads (9.3 h for all 11 runs), plus 223,930 labels; at most US$26.22 at Azure's H100 rental priceUS$62.39 of model usage (API-equivalent) for both rounds; about 3 h, plus local readouts
Without toolsno-tools revision: 0.959 (sealed test), 0.765 (sealed OOD); US$57.44 for two roundsn/anot tried
Parameters2,128,932; 156,501 non-zero2,128,932; dense752,393,024; 8,192 changed
Ships as8.5 MB weights file, 422-line builder8.5 MB weights filebase model (1.7 GB checkpoint) plus a 102 KB sparse diff
What it takes to runIntel Mac CPU: median 0.23 s CPU time per whole session (2 threads), about 2,700 tokens per CPU-secondIntel Mac CPU: median 0.25 s CPU time per session, about 2,700 tokens per CPU-secondM4 Pro GPU (MPS), about 4 GB per fp32 process; 424 to 440 tokens/s wall, about 5 h per 4,004-session val pass
Reproducibilitybuilder reruns to a byte-identical filebit-for-bit only on the same thread count, torch build and CPUedit rebuilds byte for byte; logits bit-identical across 1 to 4 MPS workers (48-session benchmark)
Readable?every non-zero block named in CIRCUITS.md; each logit maps to a rulenot inspectedeach MLP edit is a named rank-1 formula and the label-row edit is rank 2; the 752M weights they route through are not readable
How it failedjudgement: a weak first policy; without tools, behavioural rules fitted to one generatorfalse alarms off distributionrouted only existing features (its choice: every MLP edit rank 1); confident when wrong

Notes on the table:

  • Speed. CPU times are from an isolated latency measurement (Intel Mac, 2 threads, 100 val sessions). Both networks run the same dense forward pass, so the written network's sparsity is not exploited. The Qwen figure is wall time on an M4 Pro GPU, so the run rows are not like for like.
  • Cost. Costs exclude writing the spec and building the harness. The writing cost is as reported by Claude Code on a usage-limited plan, so it isn't a bill. The revision replaced about 200 of the builder's 380 lines, the nearest thing I have to the cost of a change; I haven't measured what a spec change costs.
  • Labels. The 223,930 labels were free here because a simulator generated them. Outside a simulator, the label budget is the dominant cost of the train route; the write route needed 200.

If the network is a compiled copy of a readable policy, why not ship the policy? For this task you could; it is 212 lines of standard-library Python. The network is the experiment, it shows that a frontier LLM can both write and reason over model weights, it's also a good indicator that this is a viable alternative to training a model from scratch.

Tracr (Lindner et al., 2023) compiles RASP (Weiss et al., 2021) programs into transformer weights, and the 1/(1+C) counting head is the construction it uses for SelectorWidth. Fable used that construction without being pointed to it, though it may well know Tracr from its own training. ROME (Meng et al., 2022) edits facts inside pretrained models, and activation steering (Turner et al., 2023) adds directions to change behaviour; the Qwen experiments sit closest to those. "Sparks of Weight-Space Programming" is the nearest neighbour: a frontier model reading, fixing and writing game-playing networks directly. What I haven't seen elsewhere is an LLM designing the policy from a spec and compiling it by hand, compared on one task with training (under sealed evaluation) and with modifying a pretrained model (on validation).

What This Doesn't Show

Each experiment lists its own caveats. These apply to all of them:

  • The data is synthetic, from one log grammar plus an independent OOD generator. Nothing here has met a real agent transcript, and "OOD" means a different behaviour generator, not different rules.
  • One authoring run per phase. Confidence intervals are over sessions, not over repeated attempts by Fable.
  • The sealed sets were opened once per version, three times in all, and later versions were designed knowing earlier sealed results. The held-out sets that replaced them have now been used too.
  • No LoRA, fine-tuning or probe baselines, and a 0.8B model. A larger model may well represent counts at the readout.
  • Segmentation was given: the models are told where each tool call starts and ends.

What Comes Next

Real agent transcripts, a LoRA baseline at matched label budgets, and opening the sealed test set held back from the Qwen round. I also want the operational number I don't have: what a change to the spec costs when the network has to follow it.

If you've read The Overstochastic Default, this is the same argument from another angle: for a component whose behaviour matters, I'd rather have one I can read and rebuild from source than one I have to trust. On synthetic logs, at least, writing the weights gave me that, and the better off-distribution score of the two routes I measured there.

Logs and Artefacts

The repo, the weights, every CIRCUITS.md and REVISION.md, the author transcripts with their audits and adjudications, the prompts and the reports are published with this post in the HypaNet repository.

Acknowledgements

Thanks to the MIT CSAIL authors of "Sparks of Weight-Space Programming", whose post prompted the no-tools rounds.

Citation

@misc{gribben2026withoutgradientdescent,
  author       = {Gribben, Matthew},
  title        = {Building AI Models Without Gradient Descent},
  year         = {2026},
  month        = nov,
  howpublished = {\url{https://matthewgribben.com/blog/building-ai-models-without-gradient-descent}},
  note         = {Code, weights and transcripts: \url{https://github.com/Hypabolic/HypaNet}}
}

Methods. Pre-registration: test and OOD hashes before the first authoring phase (51c9fa8); the v0.3 sealed protocol (2fce67f); the modification round's freeze and deviations (experiments/v0_4/PREREGISTRATION.md); the v0.5 sealed protocol (abe8b83) and the third round's held-out sets (d23acd3), each before the runs they score. Authoring cost (model usage, API-equivalent): warm-up US$40.05; writing US$64.62; modifying US$62.39; writing without tools US$114.02 over three rounds.

Next, a follow-on post: The utility of LLM written models in real systems.

Matthew Gribben is a Chief Technology Officer writing about AI systems, software architecture, cyber security, cryptography, and the practical realities of technology leadership.