A MeshWeaver coding model from verified tokens

Decision (2026-11-06): do not fine-tune a model on the documentation. Use the Mac Studio fleet to generate tokens, and keep only the ones the platform's own gates verify. Every NodeType that ships Test/*.cs is a task with an executable answer key, and core's run-node-tests.py is already the judge the PR gate uses. Verified samples become the training set. The scorer is also the eval harness, so the same files measure today's models and any model trained later.

The prototype lives in devtools/coding-model/ (tasks.py, generate.py, score.py, stdlib-only).

1. Why verified tokens and not the docs

2. Tasks — mined, never written by hand

tasks.py walks every NodeType through core's canonical compile-check.py, using the same discover_nodetypes / resolve_sources the gate uses, so a task's unit is the unit the mesh compiles. Only types with a Test/ folder are mined: a task without an executable check has a guessed reward.

Kind Masked Reward
file one Source/*.cs (300–24,000 chars) the type builds and its tests pass
tests one Test/*.cs that declares a *Tests class it builds, adds cases of its own over the emptied file, and they pass

Prompt context: the rest of the unit, up to 60,000 characters, plus two things calibration showed are required:

Split by SOURCE-SHARING GROUP, never by file. Files of one module share helpers, so a file-level split leaks answers into training. Splitting by the owning module is not enough either: a NodeType may compile another module's sources (Governance/Standard pulls Store/Core/Source/…), which would put a held-out Store file verbatim into a training prompt. Modules are therefore joined whenever one's NodeType compiles the other's sources, and the split is a hash of the group. --eval-modules holds out every group containing a named module. Measured: one large group (Approvals, Developer, Essentials, Games, Governance, Hosting, MyAi, Providers, Store) holds about two thirds of the tasks and hashes to train. The eval set therefore comes from the self-contained modules today. Holding out part of the big group would require excluding the training tasks whose context contains an eval target, which is a refinement for later.

Measured on this repo (2026-11-06): 918 tasks from 45 modules (569 file + 305 tests train, 31 + 13 eval), mined in 20 s. The first count, 1,091, also included *TestsArea.cs wrappers, which declare no cases; calibration rejected them, so they are no longer mined. Satellite repositories have the same layout and mine with --root.

3. Reward — the platform's gates around untrusted code

score.py builds a sandbox per candidate. Its top-level entries are symlinks into a BUILT checkout, except the candidate's module, which is cloned with APFS cp -c (copy-on-write, nearly free) and has the candidate written over the target file. Core's run-node-tests.py then runs on that one NodeType with MW_REPO_ROOT set to the sandbox.

The candidate is untrusted code, and it is executed. A model trained against this reward is optimised to raise it, so the scorer assumes the candidate is hostile (all from the review of #5048):

4. The fleet — Macs make tokens, control places the work

Each Mac Studio runs an OpenAI-compatible model server (Ollama /v1, mlx_lm.server or llama.cpp) and scores its own candidates, because .NET runs natively on Apple silicon. A generation job is (task shard × model × k samples). The prototype's --endpoint list is phase 0. In the fleet the same body runs as a job placed by the control instance's queue on machines enrolled through the back-lease, which already models placement with per-model memory reservations. This is deliberately not a second distributor: every activity is enqueued on control.

Measured (2026-11-06, qwen3-coder:30b on Ollama, one M5 Max laptop, 2 concurrent requests): prompts of about 12k tokens arrived whole (no silent context truncation), the 2nd sample of a task reused the cached prompt (cached_tokens ≈ the whole prompt), and output ran at 38–40 completion tokens/s. The Studios' rates are measured the same way, from the rows generate.py writes. Order the work so a task's k samples land on the same server, because the cached prompt is most of the cost.

5. From tokens to a model

  1. Rejection sampling / expert iteration (start here). Keep candidates with reward 1.0, de-duplicate them, and fine-tune an open coder with LoRA (MLX on a Studio is enough at 30B-class with LoRA). Re-sample with the new model, re-score and repeat. This is offline RL with a binary reward and needs no GPU cluster.
  2. Repair trajectories. A failed candidate plus its compiler diagnostics is a prompt, and a passing fix is its answer. This teaches the loop an agent actually runs: write, compile, read the error, fix. The diagnostics are already in every score row.
  3. Online RL (GRPO) only if 1–2 plateau. It needs a rollout-and-update loop that rented GPUs suit better than the Macs. The reward function is unchanged.

Teachers: use open-weight models for training data (Qwen, GLM open weights). A closed model's outputs can be used for evaluation, but check that provider's terms before training on them.

6. Staleness is a re-run, not a retrain

Every row records the commit it was mined at. When a new platform set seals, the stored verified candidates are re-scored against it. Rows that no longer compile are dropped, which tells you exactly which APIs moved, and the next expert-iteration round re-learns them. Do this at least on every MAJOR.

7. Confidentiality

The model memorises its training code, so it is as private as its most private source. Mine only repositories that pass the confidential-term gate. Keep the model and its datasets internal, and never serve it from the public instance.

8. First baseline (2026-11-06)

qwen3-coder:30b, no retrieval, prompt as in §2:

Round Tasks Samples Reward 1.0 What failed
1: smallest-first context, no type list 3 Chess file 6 0 invented APIs (StackControl.WithChildren, Controls.DataGrid<T>, RenderingContext.Hub); a property dropped because its reader had been cut
2: users-first context same 3 6 2 (GameHistory.cs, 38/38 tests) re-declared types from files it could not see (CS0101)
3: + existing-types list (scorer before the hardening above) all 14 Chess file 28 2 samples; 2/14 tasks at pass@2 (ChessNativeBoard.cs, GameHistory.cs) 26 did not build. First diagnostics: CS0246 ×10, CS0118/CS1061/CS0117 ×2 each, 5 singletons

Round 3 drew 30,626 completion tokens in 1,049 s (29 tok/s aggregate, 2 concurrent requests; these prompts were longer than in round 2).

What the failures say, read from the diagnostics in the score rows:

9. Next steps

  1. Calibrate all 1,091 tasks on one built checkout and drop those that fail.
  2. Freeze the eval split. Measure each arm on it (local Qwen, Qwen + API signatures, GLM, a frontier model with the MCP) and record the table here.
  3. Wire generate.py + score.py as a back-lease job kind on control, then run the first train shard on the Studios.
  4. First expert-iteration round (MLX LoRA), scored on the frozen eval split.