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.
Show the numbers
| Route | Headline macro-F1 | 95% CI | Split | Other numbers | Labels | Cost |
|---|---|---|---|---|---|---|
| Write (v0.3 B, revised transformer) | 0.908 | [0.905, 0.912] | sealed OOD | 0.957 [0.953, 0.961] sealed test; zero labels (v0.3 A): 0.744 sealed test | 200 windows, one revision | US$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 logic | 0, then 200 windows | US$16.70, then US$40.75 model usage (API-equivalent) |
| Train (v0.3 C-all, trained transformer) | 0.488 | [0.478, 0.497] | sealed OOD | 0.923 [0.918, 0.929] sealed test; false alarms 6.1% sealed test, 34.5% sealed OOD | 223,930 windows | about 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 measured | 200 windows | US$62.39 model usage (API-equivalent), both rounds |
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- Sealed test
- Sealed OOD
Show the numbers
| Version | Exp | Labels | Sealed test | Sealed OOD | Val, quick look |
|---|---|---|---|---|---|
| Written, no labels | 2 | 0 | 0.744 [0.734, 0.754] | 0.815 [0.806, 0.823] | n/a |
| Written, revised with 200 labels | 3 | 200 | 0.957 [0.953, 0.961] | 0.908 [0.905, 0.912] | n/a |
| No-tools draft | 4 | 0 | 0.326 [0.319, 0.333] | 0.292 [0.283, 0.300] | n/a |
| Draft repaired with tools and 200 labels | 5 | 200 | 0.959 [0.956, 0.961] | 0.765 [0.761, 0.769] | n/a |
| Base Qwen, unedited | n/a | 0 | not scored | not scored | 0.223 [0.202, 0.243] |
| Qwen, edited with no labels | 9 | 0 | not scored | not scored | 0.330 [0.309, 0.350] |
| Qwen, edit revised with 200 labels | 10 | 200 | not scored | not scored | 0.419 [0.397, 0.441] |
| Trained, 590 labels (mean of 3 seeds) | 8 | 590 | 0.347 | 0.241 | n/a |
| Trained, 2,216 labels (mean of 3 seeds) | 8 | 2,216 | 0.450 | 0.287 | n/a |
| Trained, 9,114 labels (mean of 3 seeds) | 8 | 9,114 | 0.707 | 0.305 | n/a |
| Trained, 37,285 labels | 8 | 37,285 | 0.755 | 0.400 | n/a |
| Trained, 223,930 labels | 8 | 223,930 | 0.923 | 0.488 | n/a |
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- Written by Fable
- Written with no tools, then repaired
- Trained, 590 to 223,930 labels
- Qwen hand-edited (val, quick look)
Show the numbers
| Version | This step | Time so far | Step cost | Cost so far | Sealed test | Sealed OOD |
|---|---|---|---|---|---|---|
| Written, no labels | 172 min | 2.9 h | US$42.73 | US$42.73 | 0.744 | 0.815 |
| Written, revised with 200 labels | 59 min | 3.8 h | US$21.89 | US$64.62 | 0.957 | 0.908 |
| No-tools draft | 58 min | 58 min | US$16.70 | US$16.70 | 0.326 | 0.292 |
| Draft repaired with tools and 200 labels | 74 min | 2.2 h | US$40.75 | US$57.44 | 0.959 | 0.765 |
| Base Qwen, unedited | n/a | 0 | n/a | US$0.00 | 0.223 (val, quick look) | not scored |
| Qwen, edited with no labels | 111 min | 1.9 h | US$32.58 | US$32.58 | 0.330 (val, quick look) | not scored |
| Qwen, edit revised with 200 labels | 61 min | 2.9 h | US$29.81 | US$62.39 | 0.419 (val, quick look) | not scored |
| Trained, 590 labels | 21 min | 21 min | at most US$2.49 | at most US$2.49 | 0.347 | 0.241 |
| Trained, 2,216 labels | 25 min | 25 min | at most US$2.96 | at most US$2.96 | 0.450 | 0.287 |
| Trained, 9,114 labels | 43 min | 43 min | at most US$4.98 | at most US$4.98 | 0.707 | 0.305 |
| Trained, 37,285 labels | 63 min | 1.1 h | at most US$7.38 | at most US$7.38 | 0.755 | 0.400 |
| Trained, 223,930 labels | 225 min | 3.8 h | at most US$26.22 | at most US$26.22 | 0.923 | 0.488 |
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 @ 4ec8cfaWays 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:
- 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.
- Train. The same architecture, randomly initialised and trained by gradient descent on 590 to 223,930 labelled examples.
- 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.
Show the numbers
| Session, operation | Tool and target | Status | Tests passed or file hash | Hidden effect | Label of the window ending here |
|---|---|---|---|---|---|
| Flips webhook.go between two versions, op 0 | search src | ok | none | — | |
| Flips webhook.go between two versions, op 1 | read_file internal/identity/slugify.go | ok | none | — | |
| Flips webhook.go between two versions, op 2 | edit_file internal/identity/webhook.go | ok | hb1c6547a05 > h3900d57125 | none | — |
| Flips webhook.go between two versions, op 3 | run_tests ./internal/...:unit | FAILED | 11/16 | new fact learned | — |
| Flips webhook.go between two versions, op 4 | edit_file internal/identity/webhook.go | ok | h3900d57125 > h982d5fe6e3 | none | — |
| Flips webhook.go between two versions, op 5 | run_tests ./internal/...:unit | FAILED | 11/16 | none | — |
| Flips webhook.go between two versions, op 6 | edit_file internal/identity/webhook.go | ok | h982d5fe6e3 > h3900d57125 | undoes an earlier edit | — |
| Flips webhook.go between two versions, op 7 | run_tests ./internal/...:unit | FAILED | 11/16 | none | ambiguous (not scored) |
| Flips webhook.go between two versions, op 8 | edit_file internal/identity/webhook.go | ok | h3900d57125 > h982d5fe6e3 | undoes an earlier edit | OSCILLATING |
| Flips webhook.go between two versions, op 9 | run_tests ./internal/...:unit | FAILED | 11/16 | none | OSCILLATING |
| Flips webhook.go between two versions, op 10 | edit_file internal/identity/webhook.go | ok | h982d5fe6e3 > h3900d57125 | undoes an earlier edit | OSCILLATING |
| Flips webhook.go between two versions, op 11 | run_tests ./internal/...:unit | FAILED | 11/16 | none | OSCILLATING |
| Flips webhook.go between two versions, op 12 | edit_file internal/identity/webhook.go | ok | h3900d57125 > h982d5fe6e3 | undoes an earlier edit | OSCILLATING |
| Flips webhook.go between two versions, op 13 | run_tests ./internal/...:unit | FAILED | 11/16 | none | OSCILLATING |
| Flips webhook.go between two versions, op 14 | edit_file internal/identity/webhook.go | ok | h982d5fe6e3 > h43abbdfa4c | progress (fixes something) | PRODUCTIVE |
| Flips webhook.go between two versions, op 15 | run_tests ./internal/...:unit | FAILED | 13/16 | none | PRODUCTIVE |
| Fixes something on each pass, op 0 | read_file lib/orders/scheduler.rb | ok | none | — | |
| Fixes something on each pass, op 1 | read_file lib/orders/migrations.rb | ok | none | — | |
| Fixes something on each pass, op 2 | search docs | ok | none | — | |
| Fixes something on each pass, op 3 | list_dir . | ok | none | — | |
| Fixes something on each pass, op 4 | read_file spec/orders/migrations_request_spec.rb | ok | none | — | |
| Fixes something on each pass, op 5 | list_dir spec | ok | none | — | |
| Fixes something on each pass, op 6 | edit_file lib/orders/timezone.rb | ok | h299630c5f0 > hd57cb1154a | none | — |
| Fixes something on each pass, op 7 | run_tests spec:request | FAILED | 3/14 | new fact learned | ambiguous (not scored) |
| Fixes something on each pass, op 8 | edit_file lib/orders/locale.rb | ok | h15aeac96e1 > h6061660a46 | progress (fixes something) | PRODUCTIVE |
| Fixes something on each pass, op 9 | run_tests spec:request | FAILED | 5/14 | new fact learned | PRODUCTIVE |
| Fixes something on each pass, op 10 | edit_file lib/orders/notifier.rb | ok | h614ce37a11 > h8fcbc4e82c | progress (fixes something) | PRODUCTIVE |
| Fixes something on each pass, op 11 | run_tests spec:request | FAILED | 8/14 | new fact learned | PRODUCTIVE |
| Fixes something on each pass, op 12 | edit_file lib/orders/notifier.rb | ok | h8bfc68a6d0 > h7c4a473a46 | progress (fixes something) | PRODUCTIVE |
| Fixes something on each pass, op 13 | run_tests spec:request | FAILED | 11/14 | new fact learned | PRODUCTIVE |
| Fixes something on each pass, op 14 | edit_file lib/orders/timezone.rb | ok | hd57cb1154a > h0413cb5346 | progress (fixes something) | PRODUCTIVE |
| Fixes something on each pass, op 15 | run_tests spec:request | ok | 14/14 | none | PRODUCTIVE |
| Fixes something on each pass, op 16 | run_tests spec:feature | ok | 7/7 | none | PRODUCTIVE |
| Retries the same failing suite, op 0 | search src | ok | new fact learned | — | |
| Retries the same failing suite, op 1 | read_file src/cli/rateLimiter.ts | ok | new fact learned | — | |
| Retries the same failing suite, op 2 | plan | ok | none | — | |
| Retries the same failing suite, op 3 | list_dir tests/fixtures | ok | none | — | |
| Retries the same failing suite, op 4 | read_file src/cli/__tests__/rateLimiter.integration.test.ts | ok | none | — | |
| Retries the same failing suite, op 5 | list_dir .github/workflows | ok | none | — | |
| Retries the same failing suite, op 6 | run_tests contract | FAILED | 7/9 | none | — |
| Retries the same failing suite, op 7 | run_tests contract | FAILED | 7/9 | bounded retry | RECOVERY |
| Retries the same failing suite, op 8 | read_file src/cli/__tests__/rateLimiter.contract.test.ts | ok | none | RECOVERY | |
| Retries the same failing suite, op 9 | run_tests contract | FAILED | 7/9 | bounded retry | RECOVERY |
| Retries the same failing suite, op 10 | run_tests contract | FAILED | 7/9 | bounded retry | RECOVERY |
| Retries the same failing suite, op 11 | read_file src/cli/rateLimiter.ts | ok | repeats work already done | RECOVERY | |
| Retries the same failing suite, op 12 | run_tests contract | FAILED | 7/9 | repeats work already done | ambiguous (not scored) |
| Retries the same failing suite, op 13 | search src | ok | repeats work already done | ambiguous (not scored) | |
| Retries the same failing suite, op 14 | read_file src/cli/__tests__/rateLimiter.contract.test.ts | ok | repeats work already done | ambiguous (not scored) | |
| Retries the same failing suite, op 15 | run_tests contract | FAILED | 7/9 | repeats work already done | STALLED |
| Retries the same failing suite, op 16 | read_file src/cli/rateLimiter.ts | ok | repeats work already done | STALLED | |
| Retries the same failing suite, op 17 | run_tests contract | FAILED | 7/9 | repeats work already done | STALLED |
| Retries the same failing suite, op 18 | read_file src/cli/__tests__/rateLimiter.contract.test.ts | ok | repeats work already done | STALLED | |
| Retries the same failing suite, op 19 | run_tests contract | FAILED | 7/9 | repeats work already done | STALLED |
| Retries the same failing suite, op 20 | search src | ok | repeats work already done | STALLED | |
| Retries the same failing suite, op 21 | run_tests contract | FAILED | 7/9 | repeats work already done | STALLED |
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 @ d08f05fThe 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.
Show the numbers
| Route | The spec | Labelled windows | Tools to run code | Its own network’s outputs | Validation scores | Sealed test and OOD |
|---|---|---|---|---|---|---|
| Write (v0.3) | yes | yes (0, then 200) | yes | yes | no | no |
| Train (v0.3) | no | yes (223,930) | no | yes | yes (early stopping) | no |
| Modify (v0.4) | yes | yes (0, then 200) | yes | yes (metered) | no | no |
| No tools (v0.5 r1) | yes | no | no | no | no | no |
| Revise (v0.5 B, third round) | yes | yes (200 + 200) | yes | yes | no | no |
| Route | Given | Out of reach | Sealed 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 class | repo, web, subagents, dev data, val scores, sealed sets | scored 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 stopping | the spec, sealed sets | scored 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 states | repo, web, subagents, dev data, val scores, sealed sets | not opened in v0.4; val only |
| Write without tools, v0.5 r1 | One 73,542-character prompt, then only "Continue." (plus one fixed Claude Code output-limit notice); no tools | tools, files, repo, web, all data | scored once (v0.5) |
| Revise with tools, v0.5 B and the third round | Tools; 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 sets | B: sealed sets once (v0.5); third round: new held-out sets (2,000 OOD, 4,000 in-distribution), hashed 7 Nov, opened once |
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 @ 67fc2b1Agents 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 asrepeated_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.
Show the numbers
| Item | Value |
|---|---|
| B: non-zero parameters | 295 of 3,256 (274 weights, 21 biases) |
| B: smallest and largest |weight| | 1/32 to 10,000 |
| B: macro-F1, sealed test | 0.936 [0.932, 0.940] |
| B: macro-F1, sealed OOD | 0.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 evidence | r1 = 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 episode | resA4 = 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 evidence | RT_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 bound | VM1 = 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, polling | POLL = 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: logits | PRODUCTIVE: 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 parameters | 233 of 3,256 (218 weights, 15 biases) |
| A: smallest and largest |weight| | 1/8 to 10,000 |
| A: macro-F1, sealed test | 0.667 [0.659, 0.675] |
| A: macro-F1, sealed OOD | 0.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 evidence | ine0 = 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, reversions | Vm1 = 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 progress | PROG_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 episode | ee4 = 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 episode | rs4 = 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 poll | POLL = 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: logits | PRODUCTIVE: 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) |
| Window | Events |
|---|---|
| Steady progress | read 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 worked | read 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 times | edit 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, undo | edit 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 |
weight_program/phase-b-revision/artifact.dlw and CIRCUITS.md @ 0fc235d; zero-label network weight_program/phase-a-selfcheck/ @ f8e0aaeCaveats. 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.
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)09:14:09.880 write_file path=zork/zeta/widget.qx -> ok in=2214 out=310 @ zork/zeta/widget.qx h3f9a0c1d2e > h7b21e9c04acontent idsShow the numbers
| Measure | Value |
|---|---|
| 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 text | 0 in 343,290 events |
| Leak | What it gave away | Now | Where |
|---|---|---|---|
| Code text in the diff | a fix adds + 1 and a guard line, a wrong variant a 1000+n constant, a cosmetic edit cleanup pass N, a data fix -ok | one pair of opaque content ids per changed file | spec v0.3 E9 |
| bytes= on write_file | the byte size separated progress from non-progress edits | removed | spec v0.3 r2 data audit (dev) |
| Hunk count per file | the number of hunks separated progress from non-progress edits | removed: one line per changed file | spec v0.3 r2 data audit (dev) |
agent_doom_loop_detector_spec_v0_3.md (E9, r2 data audit) @ e05f371; artifacts/reports/v03_dev_audits.json (marker_audit) @ d08f05fWith 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.
Show the numbers
| Token | Surface | Id | Marked by |
|---|---|---|---|
| An edit (edit_file), offset 7 | edit_file | 10 | H0.2: copies the tool name at offset 7 to every token of its block |
| An edit (edit_file), offset 13 | ok | 38 | H0.3: reads the block's last status word |
| An edit (edit_file), offset 21 | NL@ | 4 | the @ marker that H0.0 and H0.1 count from |
| An edit (edit_file), offset 23 | h3f9a0c1d2e | 2227 | H0.0: marks the old content id, the token 2 after @ (HOLD) |
| An edit (edit_file), offset 25 | h7b21e9c04a | 4182 | H0.1: marks the new content id, the token 4 after @ (HNEW) |
| The revert that undoes it (revert), offset 7 | revert | 12 | H0.2: copies the tool name at offset 7 to every token of its block |
| The revert that undoes it (revert), offset 13 | ok | 38 | H0.3: reads the block's last status word |
| The revert that undoes it (revert), offset 21 | NL@ | 4 | the @ marker that H0.0 and H0.1 count from |
| The revert that undoes it (revert), offset 23 | h7b21e9c04a | 4182 | H0.0: marks the old content id, the token 2 after @ (HOLD) |
| The revert that undoes it (revert), offset 25 | h3f9a0c1d2e | 2227 | H0.1: marks the new content id, the token 4 after @ (HNEW); H1.0: equals the old id of the edit, offset 23 (undo) |
| Head | Role | What it does |
|---|---|---|
| H0.2 | tool | copies the tool name at offset 7 to every token of its block |
| H0.0 | old id | marks the old content id, the token 2 after @ (HOLD) |
| H0.1 | new id | marks the new content id, the token 4 after @ (HNEW) |
| H0.3 | status | reads the block's last status word |
| H0.4 | count | finds the passed count, 3 tokens before the last passed |
| H1.0 | undo | layer 1: a new id equal to an earlier old id is an undo (REVMATCH) |
experiments/v0_3/packet_handed_over/GRAMMAR_SHEET.md @ e842bc3; weight_program/v0_3/phase-a/CIRCUITS.md §1, §5 @ eb38b33Show the numbers
| Matching failed blocks | Start token 1/(1+C) | Matches C/(1+C) | 3.5 × start | Matches − 3.5 × start | Unit: 40 × that, clipped | C ≥ 4? |
|---|---|---|---|---|---|---|
| C = 0 | 1.000 | 0.000 | 3.500 | −3.500 | −140.0 → 0 | off |
| C = 1 | 0.500 | 0.500 | 1.750 | −1.250 | −50.0 → 0 | off |
| C = 2 | 0.333 | 0.667 | 1.167 | −0.500 | −20.0 → 0 | off |
| C = 3 | 0.250 | 0.750 | 0.875 | −0.125 | −5.0 → 0 | off |
| C = 4 | 0.200 | 0.800 | 0.700 | 0.100 | +4.0 → 1 | fires |
| C = 5 | 0.167 | 0.833 | 0.583 | 0.250 | +10.0 → 1 | fires |
| C = 6 | 0.143 | 0.857 | 0.500 | 0.357 | +14.3 → 1 | fires |
| C = 7 | 0.125 | 0.875 | 0.438 | 0.438 | +17.5 → 1 | fires |
weight_program/v0_3/phase-a/CIRCUITS.md @ eb38b33; H1.4 after the revision in weight_program/v0_3/phase-b/CIRCUITS.md @ a64a4f3Caveats. 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.
Show the numbers
| True class | Zero-label transformer | Revised transformer |
|---|---|---|
| PRODUCTIVE (50 windows) | 48 of 50 right; 1 called RECOVERY, 1 called OSCILLATING | 49 of 50 right; 1 called OSCILLATING |
| RECOVERY (50 windows) | 34 of 50 right; 16 called PRODUCTIVE | 50 of 50 right |
| STALLED (50 windows) | 15 of 50 right; 34 called PRODUCTIVE, 1 called RECOVERY | 50 of 50 right |
| OSCILLATING (50 windows) | 44 of 50 right; 6 called PRODUCTIVE | 50 of 50 right |
| All 200 | 141 right | 199 right |
| Network disagrees with policy | 0 of 200 windows | 0 of 4,484 prefixes |
| Class, split | Zero-label | Revised |
|---|---|---|
| PRODUCTIVE, full val, excluding the 200 disclosed sessions (3,804 sessions) | 0.956 | 0.950 |
| RECOVERY, full val, excluding the 200 disclosed sessions (3,804 sessions) | 0.676 | 0.956 |
| STALLED, full val, excluding the 200 disclosed sessions (3,804 sessions) | 0.369 | 0.953 |
| OSCILLATING, full val, excluding the 200 disclosed sessions (3,804 sessions) | 0.928 | 0.973 |
| PRODUCTIVE, sealed test (3,996 sessions) | 0.952 | 0.952 |
| RECOVERY, sealed test (3,996 sessions) | 0.695 | 0.963 |
| STALLED, sealed test (3,996 sessions) | 0.356 | 0.956 |
| OSCILLATING, sealed test (3,996 sessions) | 0.918 | 0.968 |
| PRODUCTIVE, sealed OOD (2,000 sessions) | 0.933 | 0.903 |
| RECOVERY, sealed OOD (2,000 sessions) | 0.913 | 0.917 |
| STALLED, sealed OOD (2,000 sessions) | 0.364 | 0.951 |
| OSCILLATING, sealed OOD (2,000 sessions) | 0.999 | 0.980 |
experiments/v0_3/disclosure/DISCLOSURE.json @ eb38b33; weight_program/v0_3/phase-b/REVISION.md @ a64a4f3; artifacts/reports/v03_sealed.json @ 67fc2b1How 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.
Show the numbers
| Network | Parameters | Sealed test | Sealed OOD |
|---|---|---|---|
| Revised transformer (200 labelled windows) | 2,128,932 | 0.957 | 0.908 |
| Zero-label transformer (zero labels) | 2,128,932 | 0.744 | 0.815 |
| Network · block | Role | Reads | Writes | Feeds |
|---|---|---|---|---|
| Revised transformer · Token embedding | token flags, id bits | BOS, EOE, NLAT, HASHED, GT, NONRES, DIGIT, NUM, BITS, ISSTAT, SOK, SFAIL, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLL | OSCILLATING, STALLED, RECOVERY | |
| Revised transformer · Offset embedding | position in block | CODE, OFFLIN, OFF7, OFFGE10 | OSCILLATING, STALLED, RECOVERY | |
| Revised transformer · head H0.0 | tool of this block | OFF7, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLL | BTINSP..BTPOLL | STALLED, RECOVERY |
| Revised transformer · head H0.1 | latest status word | OFFLIN, ISSTAT, SOK, SFAIL | BSOK, BSFAIL | STALLED, RECOVERY |
| Revised transformer · head H0.2 | previous token | CODE, BITS, HASHED, GT, ISSTAT | PBITS, PHASHED, PGT, PISSTAT | OSCILLATING, STALLED, RECOVERY |
| Revised transformer · head H0.3 | has a hunk line | NLAT | HASHUNK | STALLED, RECOVERY |
| Revised transformer · head H0.4 | intent token | HASHED, OFFLIN, NONRES, OFFGE10, BITS | IBITS | STALLED, RECOVERY |
| Revised transformer · MLP 0 → VM | validation or hunk | BTINSP..BTPOLL, HASHUNK | VM | STALLED, RECOVERY |
| Revised transformer · head H1.0 | undo match | BITS, PBITS, PHASHED, GT | REVMATCH | OSCILLATING, STALLED, RECOVERY |
| Revised transformer · head H1.1 | passed count | OFFLIN, DIGIT, PISSTAT, NUM | PV | STALLED, RECOVERY |
| Revised transformer · head H1.2 | nearest same key | IBITS, BTINSP..BTPOLL, EOE, BSOK, BSFAIL | EXISTS, PSOK, PSFAIL | STALLED, RECOVERY |
| Revised transformer · head H1.3 | same key within 7 | IBITS, BTINSP..BTPOLL, EOE | EXISTS7 | STALLED, RECOVERY |
| Revised transformer · head H1.4 | counts failures | IBITS, BTINSP..BTPOLL, EOE, BSFAIL, BOS | OUTFAIL, ANCH | STALLED, RECOVERY |
| Revised transformer · head H1.5 | last check or edit | EOE, VM, BTINSP..BTPOLL, HASHUNK | NEAREST | STALLED, RECOVERY |
| Revised transformer · MLP 1 → NOTIDST | status changed | BSOK, PSOK, BSFAIL, PSFAIL | NOTIDST | STALLED |
| Revised transformer · MLP 1 → CNT3 | failures ≤ 3 | ANCH, OUTFAIL | CNT3 | STALLED, RECOVERY |
| Revised transformer · MLP 1 → CNT5 | failures ≤ 5 | ANCH, OUTFAIL | CNT5 | STALLED, RECOVERY |
| Revised transformer · MLP 1 → REVTOK | reverting token | REVMATCH, PGT, HASHED | REVTOK | OSCILLATING, STALLED, RECOVERY |
| Revised transformer · MLP 1 → NOOP | re-applied edit | BTINSP..BTPOLL, HASHUNK, BSFAIL | NOOP | STALLED |
| Revised transformer · MLP 1 → REDINSP | repeated read | BTINSP..BTPOLL, BSOK, EXISTS | REDINSP | STALLED |
| Revised transformer · head H2.0 | prev same-key block | IBITS, BTINSP..BTPOLL, EOE, PV, EXISTS7, PSFAIL, NEAREST | PPV, PEX7, PPSFAIL, PNEAR | STALLED, RECOVERY |
| Revised transformer · head H2.1 | best count so far | IBITS, PV, EOE | MAXPV | STALLED |
| Revised transformer · head H2.2 | nearest validation | EOE, BTINSP..BTPOLL, EXISTS, NOTIDST | NVEX, NVNOTST | STALLED |
| Revised transformer · head H2.3 | block reverts | REVTOK | REVBLK | OSCILLATING, STALLED, RECOVERY |
| Revised transformer · MLP 2 → IMPROVED | new best pass count | PV, MAXPV, BSFAIL, EXISTS, BSOK, PSFAIL | IMPROVED | |
| Revised transformer · MLP 2 → SI | ineffective block | REVBLK, BTINSP..BTPOLL, NOOP, REDINSP, HASHUNK, BSFAIL, NVEX, NVNOTST, MAXPV, PV, EXISTS, NOTIDST, PPV, EXISTS7, PSFAIL, NEAREST, CNT3, CNT5 | SI | STALLED |
| Revised transformer · MLP 2 → INPOL | retry within policy | PV, PPV, BTINSP..BTPOLL, BSFAIL, EXISTS7, PSFAIL, NEAREST, CNT3, REVBLK, CNT5 | INPOL | RECOVERY |
| Revised transformer · MLP 2 → RESOLV | failure resolved | BTINSP..BTPOLL, BSOK, PSFAIL, PEX7, PPSFAIL, PNEAR | RESOLV | RECOVERY |
| Revised transformer · head H3.0 | window averages | EOE, SI, REVBLK, IMPROVED, BOS | AVGSI, AVGREV, AVGIMP, ANCH8 | OSCILLATING, STALLED |
| Revised transformer · head H3.1 | active retry | EOE, INPOL | ACT | RECOVERY |
| Revised transformer · head H3.2 | resolved in last 4 | EOE, RESOLV | REC4 | RECOVERY |
| Revised transformer · MLP 3 → V2 | ≥ 2 reversions | AVGREV, ANCH8 | V2 | OSCILLATING |
| Revised transformer · MLP 3 → I5 | ≥ 5 ineffective | AVGSI, ANCH8 | I5 | STALLED |
| Revised transformer · MLP 3 → PROG | progress in window | AVGIMP, ANCH8 | PROG | |
| Revised transformer · MLP 3 → RECB | recovering | ACT, REC4 | RECB | RECOVERY |
| Zero-label transformer · Token embedding | token flags, id bits | BOS, EOE, NLAT, HASHED, H0, PASSEDW, DIGIT, NUM, BITS, ISSTAT, SOK, SFAIL, SFAILED, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLL, TSEARCH | OSCILLATING, STALLED, RECOVERY | |
| Zero-label transformer · Offset embedding | position in block | CODE, OFFLIN, OFF7, OFF10, OFF13, OFF14 | OSCILLATING, STALLED, RECOVERY | |
| Zero-label transformer · head H0.0 | old id slot | CODE, NLAT | HOLD | OSCILLATING, STALLED |
| Zero-label transformer · head H0.1 | new id slot | CODE, NLAT | HNEW | OSCILLATING, STALLED |
| Zero-label transformer · head H0.2 | tool of this block | OFF7, TINSP, TVAL, TMUT, TREVERT, THTTP, TPOLL, TSEARCH | BTINSP..BTSEARCH | STALLED, RECOVERY |
| Zero-label transformer · head H0.3 | latest status word | OFFLIN, ISSTAT, SOK, SFAIL, SFAILED | BSOK, BSFAIL, BSFAILED | STALLED, RECOVERY |
| Zero-label transformer · head H0.4 | where the count is | OFFLIN, PASSEDW, CODE | CODEP, HASPASSED | |
| Zero-label transformer · head H0.5 | has a hunk line | NLAT | HASHUNK | STALLED |
| Zero-label transformer · MLP 0 → SLOT | intent slot | OFF10, BTINSP..BTSEARCH, OFF13, OFF14 | SLOT | STALLED, RECOVERY |
| Zero-label transformer · MLP 0 → VM | validation or hunk | BTINSP..BTSEARCH, HASHUNK | VM | STALLED |
| Zero-label transformer · head H1.0 | undo match | BITS, HOLD, HASHED, H0 | REVMATCH | OSCILLATING, STALLED |
| Zero-label transformer · head H1.1 | intent bits to end | SLOT, BITS | BITS | STALLED, RECOVERY |
| Zero-label transformer · head H1.2 | passed count | CODEP, CODE, DIGIT, NUM | PV | |
| Zero-label transformer · head H1.3 | last check or edit | EOE, VM, BTINSP..BTSEARCH, HASHUNK | NEAREST | STALLED |
| Zero-label transformer · MLP 1 → REVTOK | reverting token | REVMATCH, HNEW, HASHED | REVTOK | OSCILLATING, STALLED |
| Zero-label transformer · MLP 1 → REDVAL | redundant validation | BTINSP..BTSEARCH, NEAREST | REDVAL | STALLED |
| Zero-label transformer · head H2.0 | counts same intent | BITS, BTINSP..BTSEARCH, EOE, BSFAIL, BOS | OUTALL, OUTFAIL, ANCH2 | STALLED, RECOVERY |
| Zero-label transformer · head H2.1 | block reverts | REVTOK | REVBLK | OSCILLATING, STALLED |
| Zero-label transformer · MLP 2 → SI | ineffective block | REVBLK, BTINSP..BTSEARCH, HASHUNK, BSFAIL, REDVAL, OUTALL, OUTFAIL, ANCH2 | SI | STALLED |
| Zero-label transformer · MLP 2 → KILL | retry beyond policy | OUTFAIL, ANCH2, BSFAIL, BTINSP..BTSEARCH | KILL | RECOVERY |
| Zero-label transformer · MLP 2 → INPOL | failure in policy | ANCH2, OUTFAIL, BSFAIL, BTINSP..BTSEARCH | INPOL | RECOVERY |
| Zero-label transformer · MLP 2 → RESOLV | failure resolved | OUTFAIL, ANCH2, BSOK | RESOLV | RECOVERY |
| Zero-label transformer · head H3.0 | window averages | EOE, SI, REVBLK, BOS | AVGSI, AVGREV, ANCH8 | OSCILLATING, STALLED |
| Zero-label transformer · head H3.1 | recovery sign | EOE, INPOL, RESOLV, KILL | NEAR4 | RECOVERY |
| Zero-label transformer · head H3.2 | latest validation | EOE, HASPASSED, PV, BSFAILED, BSOK | LPV, LFAILED, LOK | |
| Zero-label transformer · head H3.3 | earliest validation | EOE, HASPASSED, PV, BSFAILED, BSOK | FPV, FFAILED, FOK | |
| Zero-label transformer · MLP 3 → V2 | ≥ 2 reversions | AVGREV, ANCH8 | V2 | OSCILLATING |
| Zero-label transformer · MLP 3 → I5 | ≥ 5 ineffective | AVGSI, ANCH8 | I5 | STALLED |
| Zero-label transformer · MLP 3 → A4 | recovering | NEAR4 | A4 | RECOVERY |
| Zero-label transformer · MLP 3 → PROGN | passed count rose | LPV, FPV | PROGN | |
| Zero-label transformer · MLP 3 → PROGF | FAILED then OK | FFAILED, LOK | PROGF | |
| Zero-label transformer · MLP 3 → SIT | ineffective now | SI | SIT | STALLED |
weight_program/v0_3/phase-b/CIRCUITS.md (§3, §5, §6) @ a64a4f3; weight_program/v0_3/phase-a/CIRCUITS.md @ eb38b33Caveats. 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.
Show the numbers
| Word id | Path (prompt grammar sheet) | Offset from 2,048 | Code A residues (mod 4, 5, 7) | Code B residues (mod 3, 13) | Cells set to 1 |
|---|---|---|---|---|---|
| 6414 | zork/zeta/widget.qx | 4,366 | 2, 1, 5 | 1, 11 | d34, d37, d46, d49, d62 |
| 7212 | zork/zeta/gizmo_plinth.qx | 5,164 | 0, 4, 5 | 1, 3 | d32, d40, d46, d49, d54 |
| Query id | Key id | Shared cells | Cells matched | Score | Sink | Result |
|---|---|---|---|---|---|---|
| 6414 | 6414 | d34, d37, d46 | 3 of 3 | 24 | 20 | repeat |
| 6414 | 7212 | d46 | 1 of 3 | 8 | 20 | no repeat |
| 7212 | 6414 | d46 | 1 of 3 | 8 | 20 | no repeat |
| 7212 | 7212 | d32, d40, d46 | 3 of 3 | 24 | 20 | repeat |
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 @ b1f9e3bCaveats. 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.
layers.0.mlp.fc1.bias[32:48] = -2layers.0.mlp.fc1.bias[48:64] = -2layers.0.mlp.fc1.bias[32:64] = -3Show the numbers
| Bias | Key cells 1 to 16 (code A) | Cells firing | Sum | Match by construction |
|---|---|---|---|---|
| Before (b = −2) | 1 1 2 1 1 2 1 1 1 1 1 1 1 1 2 1 | 16 | 19 | 6.33 |
| After (b = −3) | 0 0 1 0 0 1 0 0 0 0 0 0 0 0 1 0 | 3 | 3 | 1 |
| Bias, labeller | PRODUCTIVE | RECOVERY | STALLED | OSCILLATING |
|---|---|---|---|---|
| Before, PRODUCTIVE | 3 | 0 | 47 | 0 |
| Before, RECOVERY | 4 | 0 | 46 | 0 |
| Before, STALLED | 14 | 0 | 36 | 0 |
| Before, OSCILLATING | 0 | 0 | 12 | 38 |
| After, PRODUCTIVE | 35 | 14 | 0 | 1 |
| After, RECOVERY | 6 | 42 | 2 | 0 |
| After, STALLED | 28 | 4 | 18 | 0 |
| After, OSCILLATING | 0 | 0 | 0 | 50 |
experiments/v0_5/runs/r1/replies/003.md @ 5b214c6; weight_program/v0_5/phase-b/ITERATIONS.md and REVISION.md @ b1f9e3bFable 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 test | False alarms, sealed test | Macro-F1, sealed OOD | False alarms, sealed OOD | |
|---|---|---|---|---|
| No-tools transformer (v0.5 r1, zero labels) | 0.326 | 95.7% | 0.292 | 81.5% |
| No-tools revision (v0.5 B) | 0.959 | 3.2% | 0.765 | 31.4% |
| Zero-label transformer (v0.3 A) | 0.744 | 1.9% | 0.815 | 4.0% |
| Revised transformer (v0.3 B) | 0.957 | 2.5% | 0.908 | 6.8% |
| Trained transformer, 223,930 labels (v0.3 C-all; Experiment 8) | 0.923 | 6.1% | 0.488 | 34.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).
- No-tools revision
- Revised transformer
- Trained transformer
- Shaded: trained has fewer false alarms
Show the numbers
| OOD family | Macro-F1, v0.5 B | Macro-F1, v0.3 B | Macro-F1, v0.3 C-all | False alarms, v0.5 B | False alarms, v0.3 B | False alarms, v0.3 C-all | Scored windows |
|---|---|---|---|---|---|---|---|
| index_watermark | 0.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_reconnect | 0.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_duel | 0.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_gaps | 0.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_spiral | 0.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_pong | 0.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_archaeology | 0.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_migration | 0.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_churn | 0.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_validation | 0.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 |
artifacts/reports/v05_sealed.json (splits.ood.populations.all.per_family) @ cba5a80; protocol experiments/v0_5/sealed/README.md @ abe8b83How 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>andhandoff: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.
Show the numbers
| Event (log line) | Error location | Tests passing | No-tools revision (rule) | Labeller |
|---|---|---|---|---|
| write_file contract_5 → ok | wasted (EDR) | progress | ||
| run_tests → FAILED 2/14 | contract_0.clj:354 | 2 of 14 | wasted (g_SAME) | no effect |
| write_file contract_10 → ok | wasted (EDR) | progress | ||
| run_tests → FAILED 3/14 | contract_0.clj:354 | 3 of 14 | wasted (g_SAME) | no effect |
| write_file contract_13 → ok | wasted (EDR) | progress | ||
| run_tests → FAILED 4/14 | contract_0.clj:354 | 4 of 14 | wasted (g_SAME) | no effect |
| write_file contract_11 → ok | wasted (EDR) | progress | ||
| run_tests → FAILED 5/14 | contract_0.clj:354 | 5 of 14 | wasted (g_SAME) | no effect |
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.
Show the numbers
| Rule | Kind | Flags | In distribution | OOD |
|---|---|---|---|---|
| Undo | Structural | a hunk that returns a file to an earlier state (kept from the no-tools draft) | 99.5% of 1,270 | 96% of 3,747 |
| Edit after a repeat | Behavioural | a file change within 3 operations after a repeated test result | 84% of 681 | 28% of 5,519 |
| Repeated target | Behavioural | a repeated read, search, plan or build target | 79% of 1,389 | 36% of 5,294 |
| Repeated error line | Behavioural | a failure whose error line was seen in the last 15 operations | 62% of 2,261 | 44% of 12,689 |
| Batch | Network | Labeller |
|---|---|---|
| Fresh in-distribution batch | 47% | 38% |
| Fresh OOD batch | 57% | 29% |
experiments/v0_5/ood_analysis/FINDINGS.md §2–§3 @ 82cc5ec; experiments/v0_5/ood_analysis/analyse.py @ 369beaeCaveats. 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 families | 0.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 families | 0.955, false alarms 4.2% | 0.766, false alarms 31.2% | 0.906, false alarms 6.9% | 0.494, false alarms 34.7% |
| Original simulator | 0.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.
- Third round
- No-tools revision
- Revised transformer
- Trained transformer
Show the numbers
| Family, network | Third round saw labels | Macro-F1 [95% CI] | False alarms [95% CI] | Scored windows |
|---|---|---|---|---|
| index_watermark, Third round (v0.5 Phase C) | no | 0.997 [0.995, 0.999] | 0.0% [0.0%, 0.0%] | 7,096 |
| index_watermark, No-tools revision (v0.5 B) | no | 0.935 [0.929, 0.941] | 11.1% [10.1%, 12.2%] | 7,096 |
| index_watermark, Revised transformer (v0.3 B) | no | 1.000 [1.000, 1.000] | 0.0% [0.0%, 0.0%] | 7,096 |
| index_watermark, Trained transformer (v0.3 C-all) | no | 0.712 [0.689, 0.735] | 3.2% [2.7%, 3.7%] | 7,096 |
| lease_reconnect, Third round (v0.5 Phase C) | no | 0.996 [0.995, 0.997] | 0.7% [0.4%, 0.9%] | 6,755 |
| lease_reconnect, No-tools revision (v0.5 B) | no | 0.925 [0.918, 0.932] | 13.2% [12.1%, 14.4%] | 6,755 |
| lease_reconnect, Revised transformer (v0.3 B) | no | 1.000 [1.000, 1.000] | 0.0% [0.0%, 0.0%] | 6,755 |
| lease_reconnect, Trained transformer (v0.3 C-all) | no | 0.644 [0.624, 0.662] | 12.7% [11.3%, 14.1%] | 6,755 |
| serializer_duel, Third round (v0.5 Phase C) | no | 0.950 [0.945, 0.954] | 9.4% [8.4%, 10.6%] | 7,713 |
| serializer_duel, No-tools revision (v0.5 B) | no | 0.830 [0.818, 0.841] | 41.8% [39.0%, 44.8%] | 7,713 |
| serializer_duel, Revised transformer (v0.3 B) | no | 0.933 [0.928, 0.938] | 7.3% [7.0%, 7.7%] | 7,713 |
| serializer_duel, Trained transformer (v0.3 C-all) | no | 0.329 [0.304, 0.353] | 42.8% [36.2%, 49.5%] | 7,713 |
| lockfile_churn, Third round (v0.5 Phase C) | no | 0.667 [0.658, 0.676] | 5.4% [5.1%, 5.8%] | 8,710 |
| lockfile_churn, No-tools revision (v0.5 B) | no | 0.569 [0.564, 0.573] | 22.3% [21.3%, 23.4%] | 8,710 |
| lockfile_churn, Revised transformer (v0.3 B) | no | 0.592 [0.587, 0.596] | 12.6% [12.1%, 13.0%] | 8,710 |
| lockfile_churn, Trained transformer (v0.3 C-all) | no | 0.257 [0.254, 0.259] | 3.8% [3.4%, 4.3%] | 8,710 |
| merge_ping_pong, Third round (v0.5 Phase C) | yes | 0.912 [0.906, 0.918] | 15.0% [14.1%, 16.1%] | 8,760 |
| merge_ping_pong, No-tools revision (v0.5 B) | yes | 0.783 [0.772, 0.793] | 41.2% [39.3%, 43.1%] | 8,760 |
| merge_ping_pong, Revised transformer (v0.3 B) | yes | 0.883 [0.877, 0.889] | 5.7% [5.4%, 5.9%] | 8,760 |
| merge_ping_pong, Trained transformer (v0.3 C-all) | yes | 0.611 [0.584, 0.637] | 59.4% [55.3%, 63.7%] | 8,760 |
| telemetry_gaps, Third round (v0.5 Phase C) | yes | 0.922 [0.910, 0.933] | 8.8% [7.6%, 10.0%] | 7,165 |
| telemetry_gaps, No-tools revision (v0.5 B) | yes | 0.781 [0.764, 0.795] | 29.9% [27.6%, 32.3%] | 7,165 |
| telemetry_gaps, Revised transformer (v0.3 B) | yes | 0.919 [0.910, 0.928] | 9.5% [8.1%, 11.0%] | 7,165 |
| telemetry_gaps, Trained transformer (v0.3 C-all) | yes | 0.578 [0.555, 0.600] | 21.9% [19.0%, 25.1%] | 7,165 |
| annotation_spiral, Third round (v0.5 Phase C) | yes | 0.990 [0.988, 0.992] | 1.6% [1.3%, 1.9%] | 7,699 |
| annotation_spiral, No-tools revision (v0.5 B) | yes | 0.775 [0.766, 0.784] | 41.4% [39.3%, 43.5%] | 7,699 |
| annotation_spiral, Revised transformer (v0.3 B) | yes | 0.910 [0.903, 0.917] | 16.0% [14.5%, 17.4%] | 7,699 |
| annotation_spiral, Trained transformer (v0.3 C-all) | yes | 0.449 [0.438, 0.461] | 65.6% [62.5%, 68.7%] | 7,699 |
| fixture_archaeology, Third round (v0.5 Phase C) | yes | 0.959 [0.953, 0.964] | 4.6% [3.9%, 5.2%] | 7,935 |
| fixture_archaeology, No-tools revision (v0.5 B) | yes | 0.628 [0.615, 0.643] | 41.2% [39.2%, 43.0%] | 7,935 |
| fixture_archaeology, Revised transformer (v0.3 B) | yes | 0.870 [0.862, 0.878] | 9.0% [8.3%, 9.7%] | 7,935 |
| fixture_archaeology, Trained transformer (v0.3 C-all) | yes | 0.457 [0.442, 0.472] | 59.5% [56.7%, 62.1%] | 7,935 |
| shard_migration, Third round (v0.5 Phase C) | yes | 0.974 [0.966, 0.981] | 0.6% [0.4%, 0.8%] | 7,727 |
| shard_migration, No-tools revision (v0.5 B) | yes | 0.603 [0.593, 0.613] | 34.8% [33.6%, 36.1%] | 7,727 |
| shard_migration, Revised transformer (v0.3 B) | yes | 0.876 [0.868, 0.883] | 5.1% [4.3%, 5.8%] | 7,727 |
| shard_migration, Trained transformer (v0.3 C-all) | yes | 0.318 [0.301, 0.335] | 29.0% [28.3%, 29.8%] | 7,727 |
| pinned_validation, Third round (v0.5 Phase C) | yes | 0.971 [0.966, 0.977] | 2.4% [2.0%, 2.9%] | 7,710 |
| pinned_validation, No-tools revision (v0.5 B) | yes | 0.517 [0.498, 0.536] | 48.7% [46.7%, 50.6%] | 7,710 |
| pinned_validation, Revised transformer (v0.3 B) | yes | 0.628 [0.619, 0.637] | 8.0% [7.5%, 8.6%] | 7,710 |
| pinned_validation, Trained transformer (v0.3 C-all) | yes | 0.420 [0.402, 0.437] | 69.5% [67.9%, 71.0%] | 7,710 |
artifacts/reports/v05c_heldout.json (sets.heldout_ood.populations.all.per_family) @ cefd087; experiments/v0_5/phase_c/heldout/README.md @ d23acd3How 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).
Show the numbers
| Session | Labels | Cost | Time | Wrote | Macro-F1 (set; false alarms) |
|---|---|---|---|---|---|
| No-tools draft (v0.5 r1), Experiment 4 | 0 | US$16.70 | about 58 min, 4 turns | 617; 66,014 non-zero weights | 0.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 5 | 200 | US$40.75 | about 53 + 21 min (2 launches) | 1,262; 2,813 weight entries changed | 0.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 7 | 400 | US$56.58 | about 66 min, 114 turns | 430; 999 weight entries changed | 0.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%) |
| Network | Made | Cost | Macro-F1 (set; false alarms) |
|---|---|---|---|
| Zero-label transformer (v0.3 A) | written with code, no labels | US$42.73 | 0.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 windows | US$21.89 | 0.957 (sealed test; false alarms 2.5%); 0.908 (sealed OOD; false alarms 6.8%) |
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 @ cefd087Caveats. 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 test | False alarms, sealed test | Macro-F1, sealed OOD | False 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).
Show the numbers
| Network | Sealed test macro-F1 | Sealed OOD macro-F1 | Sealed test false alarms | Sealed 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 seeds | 0.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 seeds | 0.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 seeds | 0.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 seed | 0.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 seed | 0.923 [0.918, 0.929] | 0.488 [0.478, 0.497] | 6.1% [5.6%, 6.6%] | 34.5% [33.2%, 36.0%] |
| Network | Sealed test macro-F1 | Sealed OOD macro-F1 | Sealed OOD false alarms |
|---|---|---|---|
| v0.2 A | 0.667 | 0.810 | 1.3% |
| v0.2 B | 0.936 | 0.870 | 8.5% |
| v0.2 C-32 (mean of 3 seeds) | 0.618 | 0.517 | 30.6% |
| v0.2 C-128 (mean of 3 seeds) | 0.932 | 0.644 | 39.1% |
| v0.2 C-512 (mean of 3 seeds) | 0.966 | 0.727 | 28.6% |
| v0.2 C-2048 (mean of 3 seeds) | 0.985 | 0.768 | 19.9% |
| v0.2 C-all (mean of 3 seeds) | 0.992 | 0.843 | 11.6% |
artifacts/reports/v03_sealed.json @ 67fc2b1; artifacts/reports/m4_sealed_v02.json @ 48985d7; blog_figures/F6.json @ bf164d1Training 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.
Show the numbers
| Model | Labelled windows | Seeds | Sealed test | Sealed OOD |
|---|---|---|---|---|
| Zero-label transformer (written by Fable) | 0 | n/a | 0.744 [0.734, 0.754] | 0.815 [0.806, 0.823] |
| Revised transformer (written by Fable) | 200 | n/a | 0.957 [0.953, 0.961] | 0.908 [0.905, 0.912] |
| Trained, C-32 (32 sessions) | 590 | 3 | 0.347 (0.341, 0.317, 0.383) | 0.241 (0.297, 0.188, 0.239) |
| Trained, C-128 (126 sessions) | 2,216 | 3 | 0.450 (0.421, 0.441, 0.489) | 0.287 (0.274, 0.262, 0.327) |
| Trained, C-512 (509 sessions) | 9,114 | 3 | 0.707 (0.716, 0.700, 0.705) | 0.305 (0.297, 0.342, 0.275) |
| Trained, C-2048 (2,027 sessions) | 37,285 | 1 | 0.755 | 0.400 |
| Trained, C-all (11,864 sessions) | 223,930 | 1 | 0.923 | 0.488 |
| v0.2 C-32 (trained, earlier dataset) | 563 | 3 | 0.618 | 0.517 |
| v0.2 C-128 (trained, earlier dataset) | 2,207 | 3 | 0.932 | 0.644 |
| v0.2 C-512 (trained, earlier dataset) | 9,216 | 3 | 0.966 | 0.727 |
| v0.2 C-2048 (trained, earlier dataset) | 38,120 | 3 | 0.985 | 0.768 |
| v0.2 C-all (trained, earlier dataset) | 223,930 | 3 | 0.992 | 0.843 |
| Epoch | Val subset macro-F1 |
|---|---|
| 1 | 0.830 |
| 2 | 0.912 |
| 3 (cap) | 0.934 |
artifacts/reports/v03_sealed.json @ 67fc2b1; experiments/phase-c-trained/v0_3/*/seed*/training_log.json @ a934164; blog_figures/F7.json @ bf164d1Show the numbers
| Network, true class | Called PRODUCTIVE | Called RECOVERY | Called STALLED | Called OSCILLATING | Windows | Recall |
|---|---|---|---|---|---|---|
| Revised transformer, true PRODUCTIVE | 26,576 | 402 | 302 | 623 | 27,903 | 0.952 |
| Revised transformer, true RECOVERY | 385 | 11,540 | 55 | 0 | 11,980 | 0.963 |
| Revised transformer, true STALLED | 341 | 563 | 19,913 | 6 | 20,823 | 0.956 |
| Revised transformer, true OSCILLATING | 434 | 0 | 31 | 13,851 | 14,316 | 0.968 |
| Trained transformer, true PRODUCTIVE | 24,642 | 1,156 | 599 | 1,506 | 27,903 | 0.883 |
| Trained transformer, true RECOVERY | 852 | 10,815 | 183 | 130 | 11,980 | 0.903 |
| Trained transformer, true STALLED | 341 | 149 | 19,895 | 438 | 20,823 | 0.955 |
| Trained transformer, true OSCILLATING | 205 | 17 | 0 | 14,094 | 14,316 | 0.984 |
| Network, true class | Called PRODUCTIVE | Called RECOVERY | Called STALLED | Called OSCILLATING | Windows | Recall |
|---|---|---|---|---|---|---|
| Revised transformer, true PRODUCTIVE | 39,741 | 964 | 2,041 | 1,256 | 44,002 | 0.903 |
| Revised transformer, true RECOVERY | 524 | 11,811 | 180 | 366 | 12,881 | 0.917 |
| Revised transformer, true STALLED | 106 | 8 | 7,626 | 283 | 8,023 | 0.951 |
| Revised transformer, true OSCILLATING | 254 | 0 | 0 | 12,378 | 12,632 | 0.980 |
| Trained transformer, true PRODUCTIVE | 16,562 | 8,997 | 13,828 | 4,615 | 44,002 | 0.376 |
| Trained transformer, true RECOVERY | 254 | 11,419 | 1,008 | 200 | 12,881 | 0.886 |
| Trained transformer, true STALLED | 1,174 | 473 | 6,196 | 180 | 8,023 | 0.772 |
| Trained transformer, true OSCILLATING | 4,235 | 229 | 3,738 | 4,430 | 12,632 | 0.351 |
artifacts/reports/v03_sealed.json (splits.{test,ood}.populations.all.overall.columns[*].confusion_matrix) @ 67fc2b1So 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
./fwdcommand (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).
Show the numbers
| Window and model | “productive” | “recovering” | “stalled” | “looping” | Answer |
|---|---|---|---|---|---|
| Window 1 (true PRODUCTIVE), Base Qwen | 19.26 | 19.17 | 17.78 | 17.93 | “productive” (right) |
| Window 1 (true PRODUCTIVE), WSP-A | 24.78 | 18.74 | 17.98 | 12.77 | “productive” (right) |
| Window 1 (true PRODUCTIVE), WSP-B | 15.17 | 15.08 | 14.18 | 59.10 | “looping” (wrong) |
| Window 2 (true PRODUCTIVE), Base Qwen | 15.52 | 19.19 | 18.43 | 17.95 | “recovering” (wrong) |
| Window 2 (true PRODUCTIVE), WSP-A | 14.64 | 20.90 | 18.14 | 15.09 | “recovering” (wrong) |
| Window 2 (true PRODUCTIVE), WSP-B | 18.91 | 19.99 | 18.37 | 17.93 | “recovering” (wrong) |
| Window 3 (true PRODUCTIVE), Base Qwen | 13.28 | 18.10 | 18.93 | 18.87 | “stalled” (wrong) |
| Window 3 (true PRODUCTIVE), WSP-A | 6.99 | 16.36 | 21.00 | 20.37 | “stalled” (wrong) |
| Window 3 (true PRODUCTIVE), WSP-B | 17.12 | 19.77 | 19.91 | 20.75 | “looping” (wrong) |
| Window 4 (true OSCILLATING), Base Qwen | 13.98 | 18.49 | 18.85 | 19.01 | “looping” (right) |
| Window 4 (true OSCILLATING), WSP-A | 8.33 | 16.61 | 18.31 | 36.96 | “looping” (right) |
| Window 4 (true OSCILLATING), WSP-B | 17.45 | 21.76 | 18.72 | 19.48 | “recovering” (wrong) |
| Window 5 (true PRODUCTIVE), Base Qwen | 14.25 | 19.01 | 18.88 | 18.57 | “recovering” (wrong) |
| Window 5 (true PRODUCTIVE), WSP-A | 10.40 | 19.11 | 18.89 | 19.67 | “looping” (wrong) |
| Window 5 (true PRODUCTIVE), WSP-B | 17.87 | 21.10 | 18.77 | 21.03 | “recovering” (wrong) |
| Window 6 (true RECOVERY), Base Qwen | 13.48 | 18.37 | 19.01 | 18.21 | “stalled” (wrong) |
| Window 6 (true RECOVERY), WSP-A | 7.66 | 17.12 | 20.78 | 18.00 | “stalled” (wrong) |
| Window 6 (true RECOVERY), WSP-B | 17.83 | 20.32 | 18.64 | 18.67 | “recovering” (right) |
| Window | Picked because | True class | Val session | Last block | Blocks read |
|---|---|---|---|---|---|
| Window 1 | Base Qwen answers productive | PRODUCTIVE | s-91230293fca6b6f1 | 19 | 20 |
| Window 2 | Base Qwen answers recovering | PRODUCTIVE | s-d6a050c9dc9f7a62 | 7 | 8 |
| Window 3 | Base Qwen answers stalled | PRODUCTIVE | s-dce7349f802ecac6 | 13 | 14 |
| Window 4 | Base Qwen answers looping | OSCILLATING | s-3f38dd2d5d7b0b06 | 12 | 13 |
| Window 5 | WSP-B's answer differs from WSP-A's | PRODUCTIVE | s-4b48186b6f02c2d7 | 13 | 14 |
| Window 6 | WSP-B's answer differs from WSP-A's | RECOVERY | s-7ceff85fc201fa8c | 17 | 18 |
| Block | Log text |
|---|---|
| Window 1, block 12 | 21:49:33.978 poll_status resource=ci/pipeline/1627 backoff=60 -> pending in=365 out=45 |
| Window 1, block 13 | 21:51:02.404 poll_status resource=ci/pipeline/1627 backoff=70 -> ready in=104 out=33 |
| Window 1, block 14 | 21:51:04.485 search scope=src query="decodeDiscount" -> ok in=293 out=35 |
| Window 1, block 15 | 21: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 16 | 21: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 17 | 21: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 18 | 21:51:30.429 plan action=summarise_findings -> ok in=1115 out=464 |
| Window 1, block 19 | 21:51:32.565 plan action=review_diff -> ok in=907 out=121 |
| Window 2, block 0 | 21:39:18.325 plan action=decompose -> ok in=2974 out=475 |
| Window 2, block 1 | 21: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 2 | 21:39:36.181 read_file path=lib/notify/checksum.rb lines=5-16 -> ok in=252 out=74 |
| Window 2, block 3 | 21:39:37.407 http_call GET https://honeycomb.io/1/query_results/{dataset}/{id} req=cb79f974 -> ok in=1851 out=41 |
| Window 2, block 4 | 21:39:38.287 read_file path=logs/ci-280.log lines=1-2 -> ok in=494 out=191 |
| Window 2, block 5 | 21:40:02.113 lint target=rubocop -> FAILED in=1962 out=167 ! Lint/UselessAssignment at lib/notify/checksum.rb:327 (0x93b755bf) |
| Window 2, block 6 | 21:40:03.115 search scope=src query="TODO" -> ok in=2016 out=138 |
| Window 2, block 7 | 21:40:13.526 edit_file path=lib/notify/checksum.rb -> ok in=1277 out=403 @ lib/notify/checksum.rb hdf24405e32 > h7fff8e7f3f |
| Window 3, block 6 | 00: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 7 | 00: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 8 | 00: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 9 | 00: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 10 | 00:59:00.463 search scope=lib query="convert_amount" -> ok in=1607 out=152 |
| Window 3, block 11 | 00:59:06.439 read_file path=lib/pipeline/graph_walk.rb lines=15-22 -> ok in=3090 out=191 |
| Window 3, block 12 | 00: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 13 | 00: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 5 | 10:58:18.869 list_dir path=deploy -> ok in=309 out=102 |
| Window 4, block 6 | 10: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 7 | 10: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 8 | 10: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 9 | 10: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 10 | 10: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 11 | 10: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 12 | 10: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 6 | 20: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 7 | 20: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 8 | 20: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 9 | 20:36:43.492 search scope=src query="encodeQuota" -> ok in=499 out=68 |
| Window 5, block 10 | 20: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 11 | 20: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 12 | 20: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 13 | 20: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 10 | 19:22:15.887 read_file path=lib/media/search_index.rb -> ok in=1906 out=84 |
| Window 6, block 11 | 19:22:16.901 list_dir path=tests -> ok in=562 out=140 |
| Window 6, block 12 | 19:22:17.968 search scope=src query="copyright" -> ok in=1935 out=79 |
| Window 6, block 13 | 19:22:19.099 read_file path=docs/onboarding.md -> ok in=1478 out=133 |
| Window 6, block 14 | 19:22:20.417 search scope=. query="unsafe" -> ok in=431 out=91 |
| Window 6, block 15 | 19:22:21.234 list_dir path=lib/web -> ok in=204 out=81 |
| Window 6, block 16 | 19: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 17 | 19: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) |
envs/qwen/hypaqwen/interface.py @ a616b93; experiments/v0_4/columns/ (val quick-look logits) @ 48acfc6Show the numbers
| Prompt | Label words | Median mass | Variety | Score | Predicted P / R / S / O |
|---|---|---|---|---|---|
| w1-state-full (used) | productive / recovering / stalled / looping | 0.816 | 0.910 | 0.742 | 7.2% / 27.5% / 39.6% / 25.6% |
| w1-answer-full | productive / recovering / stalled / looping | 0.863 | 0.610 | 0.526 | 2.4% / 2.1% / 36.0% / 59.6% |
| w1-state-short | productive / recovering / stalled / looping | 0.756 | 0.684 | 0.517 | 11.8% / 14.9% / 68.2% / 5.1% |
| w1-answer-short | productive / recovering / stalled / looping | 0.795 | 0.610 | 0.485 | 11.1% / 1.3% / 70.7% / 16.9% |
| w2-state-short | productive / recovering / stuck / looping | 0.905 | 0.284 | 0.257 | 10.0% / 1.3% / 88.7% / 0.0% |
| w2-answer-short | productive / recovering / stuck / looping | 0.916 | 0.234 | 0.214 | 9.6% / 0.1% / 90.3% / 0.0% |
| w2-state-full | productive / recovering / stuck / looping | 0.947 | 0.163 | 0.154 | 3.7% / 1.3% / 95.0% / 0.0% |
| w2-answer-full | productive / recovering / stuck / looping | 0.952 | 0.011 | 0.011 | 0.1% / 0.0% / 99.8% / 0.1% |
| w3-state-full (first rule's pick) | progress / retry / stuck / loop | 0.986 | 0.000 | 0.000 | 0.0% / 0.0% / 100.0% / 0.0% |
| w3-answer-full | progress / retry / stuck / loop | 0.983 | 0.000 | 0.000 | 0.0% / 0.0% / 100.0% / 0.0% |
| w3-answer-short | progress / retry / stuck / loop | 0.975 | 0.000 | 0.000 | 0.0% / 0.0% / 100.0% / 0.0% |
| w3-state-short | progress / retry / stuck / loop | 0.975 | 0.000 | 0.000 | 0.0% / 0.0% / 100.0% / 0.0% |
artifacts/reports/v04_prompt_choice.json and artifacts/reports/v04_prompt_choice_revision1.json @ a616b93Unedited, 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).
Show the numbers
| Tensor | Shape | WSP-A: weights changed | WSP-B: weights changed |
|---|---|---|---|
| embed_tokens | 248,320 × 1,024 | 1,024 (rank 1) | 2,048 (rank 2) |
| layers.21.mlp.down_proj | 1,024 × 3,584 | 1,024 (rank 1) | unchanged |
| layers.21.mlp.gate_proj | 3,584 × 1,024 | 1,024 (rank 1) | unchanged |
| layers.21.mlp.up_proj | 3,584 × 1,024 | 1,024 (rank 1) | unchanged |
| layers.22.mlp.down_proj | 1,024 × 3,584 | 1,024 (rank 1) | 1,024 (rank 1) |
| layers.22.mlp.gate_proj | 3,584 × 1,024 | 1,024 (rank 1) | 1,024 (rank 1) |
| layers.22.mlp.up_proj | 3,584 × 1,024 | 1,024 (rank 1) | 1,024 (rank 1) |
| layers.23.mlp.down_proj | 1,024 × 3,584 | 1,024 (rank 1) | 1,024 (rank 1) |
| layers.23.mlp.gate_proj | 3,584 × 1,024 | 1,024 (rank 1) | 1,024 (rank 1) |
| layers.23.mlp.up_proj | 3,584 × 1,024 | 1,024 (rank 1) | 1,024 (rank 1) |
| Total | 752,393,024 language-model weights | 10,240 in 10 tensors (0.00136%) | 8,192 in 7 tensors (0.00109%) |
weight_program/v0_4/wsp-a/BUILD_RECORD.json @ 5f58e4a; weight_program/v0_4/wsp-b/BUILD_RECORD.json @ d177712Show the numbers
| Unit | Gate | Up | Wire |
|---|---|---|---|
| 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 row | none (linear) | +1.6 PROG +1.6 RES −1.6 REP | added 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 row | none (linear) | +4.2 +1.5 NFR −REPF −REPH −REPP | added to ‘ productive’ |
| WSP-B: ‘ recovering’ label row | none (linear) | +2 PEND +2 REPH +2 REPP −1.5 REPF +0.5 NFR +0.5 RESH | added to ‘ recovering’ |
weight_program/v0_4/wsp-a/inputs/EDIT.md @ 5f58e4a; weight_program/v0_4/wsp-b/inputs/REVISION.md @ d177712Experiment 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).
Show the numbers
| Edit state | Edit hash | 200 disclosed windows (accuracy, in-sample) | Set R (Fable's own, 64 sessions) |
|---|---|---|---|
| WSP-A | 74576466 | 31.5% (first test, calls 1–13) | not run |
| Offline designs b1, b2, b3, b4 | none | not run (rejected) | not run |
| Revision | ac483550 | 42.5% (second test, calls 26–38); 45 fixed, 23 broken | 42% (calls 24–25) |
| Final WSP-B (STALLED wire × 0.2764) | 0ea2eb77 | 44.0% offline estimate; not run | 44% (calls 39–40) |
| Unedited Qwen (reference) | original | 24.0% | 19% |
weight_program/v0_4/wsp-b/inputs/REVISION.md and experiments/v0_4/logs/wsp-b.ledger.jsonl @ d177712All three routes:
| No labels | 200 labelled windows | 223,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, unedited | 0.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.
Show the numbers
| Model | Median gap (nats) | Top vs runner-up | Middle half (p25 to p75) | p5 to p95 | Accuracy [95% CI] |
|---|---|---|---|---|---|
| Base Qwen (unedited) | 0.34 | 1.4× | 0.15 to 0.60 | 0.03 to 1.11 | 0.24 [0.22, 0.27] |
| WSP-A (edited, no labels) | 3.40 | 30× | 1.36 to 8.36 | 0.28 to 18.73 | 0.35 [0.33, 0.38] |
| WSP-B (edited, 200 windows) | 2.14 | 8.5× | 0.86 to 6.74 | 0.16 to 30.11 | 0.44 [0.41, 0.46] |
experiments/v0_4/columns/*.val_quick.npz and artifacts/reports/v04_val_quick_wspb.json (readout_diagnostics) @ 48acfc6; blog_figures/F13.json @ bf164d1You 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.
| Exp | Network | Labels | In distribution | Off distribution | False alarms (in / off) | Cost |
|---|---|---|---|---|---|---|
| 1 | Warm-up MLP, written and revised (v0.2 B) | 200 | 0.936 sealed test | 0.870 sealed OOD | 3.1% / 8.5% | US$40.05 (all of v0.2) |
| 1 | Warm-up MLP, trained (v0.2 C-all, 3 seeds) | 223,930 | 0.992 sealed test | 0.843 sealed OOD | 0.8% / 11.6% | CPU |
| — | Rules engine from the spec (Codex) | 0 | 0.417 sealed test | 0.415 sealed OOD | 1.1% / 2.4% | — |
| 2 | Zero-label transformer (v0.3 A) | 0 | 0.744 sealed test | 0.815 sealed OOD | 1.9% / 4.0% | US$42.73 |
| 3 | Revised transformer (v0.3 B) | 200 | 0.957 sealed test | 0.908 sealed OOD | 2.5% / 6.8% | US$64.62 (A+B) |
| 4 | No-tools draft (v0.5 r1) | 0 | 0.326 sealed test | 0.292 sealed OOD | 95.7% / 81.5% | US$16.70 |
| 5 | No-tools revision (v0.5 B) | 200 | 0.959 sealed test | 0.765 sealed OOD | 3.2% / 31.4% | US$40.75 |
| 6 | Why it didn't transfer (diagnosis) | — | — | 32.8% of productive windows called STALLED (fresh OOD batch); 32.6% (sealed OOD) | — | CPU |
| 7 | Third round (v0.5 Phase C) † | 200 + 200 from Codex's generator | 0.949 held-out in-distribution | 0.750 held-out OOD, 4 unseen families (revised transformer 0.718) | 2.1% / 3.0% | US$56.58 |
| 8 | Trained transformer (v0.3 C-all) | 223,930 | 0.923 sealed test | 0.488 sealed OOD | 6.1% / 34.5% | ~3.75 h CPU |
| 9 | Qwen, zero-label edit (WSP-A) | 0 | 0.330 val, quick look | not measured | 55.3% / — | US$32.58 |
| 10 | Qwen, revised edit (WSP-B) | 200 | 0.419 val, quick look | not measured | 47.6% / — | US$29.81 |
| — | Base Qwen, unedited | 0 | 0.212 full val | not measured | 70.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 distribution | 0.957 (sealed test) | 0.923 (sealed test) | 0.419 (val, quick look) |
| Out of distribution | 0.908 (sealed OOD) | 0.488 (sealed OOD) | not measured |
| False alarms | 2.5% sealed test, 6.8% sealed OOD | 6.1% sealed test, 34.5% sealed OOD | 47.6% (val, quick look) |
| Learned from | spec + 200 labelled windows | 223,930 labelled windows | spec + 200 labelled windows, on top of pretraining |
| Cost to produce | US$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 price | US$62.39 of model usage (API-equivalent) for both rounds; about 3 h, plus local readouts |
| Without tools | no-tools revision: 0.959 (sealed test), 0.765 (sealed OOD); US$57.44 for two rounds | n/a | not tried |
| Parameters | 2,128,932; 156,501 non-zero | 2,128,932; dense | 752,393,024; 8,192 changed |
| Ships as | 8.5 MB weights file, 422-line builder | 8.5 MB weights file | base model (1.7 GB checkpoint) plus a 102 KB sparse diff |
| What it takes to run | Intel Mac CPU: median 0.23 s CPU time per whole session (2 threads), about 2,700 tokens per CPU-second | Intel Mac CPU: median 0.25 s CPU time per session, about 2,700 tokens per CPU-second | M4 Pro GPU (MPS), about 4 GB per fp32 process; 424 to 440 tokens/s wall, about 5 h per 4,004-session val pass |
| Reproducibility | builder reruns to a byte-identical file | bit-for-bit only on the same thread count, torch build and CPU | edit 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 rule | not inspected | each 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 failed | judgement: a weak first policy; without tools, behavioural rules fitted to one generator | false alarms off distribution | routed 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.
Background and Related Work
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.