How to drive the improvement plan with an AI coding model and know what to run,
what to report, and when a decision is yours. Read alongside PLAN.md, which
holds the technical rationale.
CURRENT STATE (2026-07-08): Releases 1.5.0 / 1.6.0 / 1.7.0 / 🎉 1.8.0 RELEASED 2026-07-08 (Phases 5+6+7 bundled), bench 12,661,251. 1.8.0 ≈ +93 Elo over 1.7.0 at 3+0.03, and an LTC field gauntlet (colosseum 10+0.1) confirmed it holds at long TC — ~+40 Elo head-to-head vs 1.7.0 (gains compress at LTC, as expected for eval refits). Phase breakdown: Phase 5 (TM) clock-at-go +2.95; Phase 6 (search) 6.1 TT-bound eval +7.18 → 6.10 bundle +9.14; Phase 7 (eval) 7.1 SF@60k distill +6.75 + five on-policy self-play cycles (+21.02, +19.51, +18.29, +15.32) ≈ +80.9. 7.4 histshape SPSA was SKIPPED (low EV — family SPSA 0-for-2 at maturity, LTC-compresses to ~+2); **7.5 = the LTC gauntlet
- release.**
⚠️ Key lesson from Phase 7: holdout-MSE-delta badly under-predicts Elo for on-policy self-play refits — the linear eval barely moves each cycle yet gains ~+15-20 (the king-safety funnel keeps re-shaping); trust the SPRT, and expect ~half the fast-TC gain to survive at LTC. ▶ NEXT (post-1.8.0): Phase 8 (feature menu, EV≈0, skip unless evidence) or Phase 9 (NNUE, the ceiling-breaker); another on-policy self-play cycle is still mildly positive (~+7 LTC) if a 1.9.0 is wanted. The bullets that follow are the Phase-1 historical record.
- Phase 0 harness is complete.
- SPSA and SPRT now share the representative clock TC:
tc=3+0.03. Use.\tools\sprt.ps1 -TC "10+0.1"for LTC confirmation and-MoveTime 0.1only for the optional old fixed-movetime sanity check. - Phase 1 search-parameter plumbing is complete.
SearchParamsexists and is exposed as tune-gated UCI options with-DTUNE=ON.- Default-equivalence has been recorded:
bench 13 = 4,972,548 nodes, full CTest passed. - Phase 1 pruning SPSA was SPRT-accepted:
+18.87 +/- 8.81 Elo, 2930 games, H1 accepted. - Phase 1 LMR SPSA completed: 4034 iterations, 129,088 games.
- Phase 1 LMR SPRT was accepted:
+15.63 +/- 8.02 Elo, 3714 games, H1 accepted. - Narrowed combined Phase 1 polish was rejected:
-0.40 +/- 3.20 Elo, 23,210 games, H0 accepted. - Harness-corrected retry audit: the combined-polish retry folds into the relocated Step 7.4 histshape SPSA (after the eval refit); the post-LMR deeper re-search became 6.4 (done, exposure-only).
- Phase 1 validation completed:
- vs Basilisk 1.4.9/defaults: 2000 games, 53.90%, approx +27.16 Elo.
- Phase 1 is complete. The accepted search-constant head is
tools\test_engines\basilisk-phase1-final-pext-pgo.exe(shipped as 1.5.0). - A 2026-06 analysis re-baselined the plan: the depth deficit vs Stockfish is eval accuracy + search selectivity, not nps and not time management. PLAN.md Phases 2-7 were rewritten accordingly (Texel pipeline, search efficiency wave, eval features, time management, feature menu, NNUE last).
- Step 2.0 is complete.
src/EvalParams.hcreated:struct EvalParamsholds all ~900 tunable eval weights with defaults identical to the old inline constants;EVAL_PARAM_LISTX-macro enumerates every group.eval.cppupdated to read fromg_eval_params;init_eval_tablestakesconst EvalParams&. Bench fingerprint onrelease-pext(non-PGO) on this machine: 4,283,684 nodes (the recorded 4,972,548 was the PGO build; 4,283,684 is the correct non-PGO baseline for refactor verification). 8/8 CTest passed. - Step 2.1 is complete. Under
BASILISK_TUNE:load_eval_file_if_set()readsBASILISK_EVAL_FILEenv-var path on startup;run_dumpeval()prints all 900 parameters inname index valueformat.dumpevalconsole command added to UciLoop. Round-trip verified byte-identical (dump → load → dump). Release build ignores both; bench fingerprint unchanged: 4,283,684 nodes, 8/8 CTest passed. - Step 2.2 is complete. CMake
TEXEL=ONoption added;basilisk-texeltarget built fromsrc/eval.cpp(TEXEL_TRACE+BASILISK_TUNE) +tools/texel/tuner.cpp.EvalTracestruct holdsint16_t mg[900]andeg[900]flat count arrays;reconstruct()computes tapered dot product. All 900 linear eval parameters instrumented withTR_MG/TR_EG/TR_BOTHmacros; pawn cache bypassed underTEXEL_TRACE; king safety traced as one-hotSafetyTablelookup. Acceptance test:basilisk-texel --verifyreconstructs exactly on all positions (OCB/50-move/mate-drive absorbed intorest). Release bench 4,283,684 nodes, 8/8 CTest passed. - Step 2.3 is complete.
tools/datagen.ps1andtools/texel/extract.pycreated. datagen.ps1 runs fastchess self-play (nodes=8000, 30k rounds = 60k games) writingtools/texel/data/selfplay.pgn. extract.py uses python-chess to skip first 16 plies + last 6 plies, drops in-check and capture/promotion moves, samples ≤12 positions per game, deduplicates by FEN (4 fields), splits by game (5% holdout), writestrain.csv/holdout.csv. Round-trip pipeline verified: extracted positions passbasilisk-texel --verify. - Where the plan stands (restructured 2026-06-18, PLAN.md §0.5): Phase 2 texel infra (2.0–2.3) and the cheap, structure-independent scalar fits are done/accepted (material +29, mobility +8.8, passers +16.6, pawn-structure +30.7, hanging baked; rooks rejected). The plan was reordered so eval structure is built before the heavy eval tuning and the search SPSA. The remaining old 2.4 tuning (king safety 2.4c, PSTs 2.4d, polish 2.4e, plus the untuned pawn-threats) is deferred into the new Phase 4.
1.6.0shipped (commitVersion 1.6.0); the repo is on branchv1.7.0.- Phase 3 (eval structure build-out, Steps 3.0–3.11) is ✅ COMPLETE
(2026-06-24…27). The whole enlarged HCE structure is in place, seeded inert
and Texel-traceable; bench fingerprint is now
4,168,590(re-baselined by the two behaviour-changing steps). Per-step gates were green throughout (bench identity for the inert steps, 9/9 CTest incl.test_endgames18/18, texel--verifyexact 8,598/8,598,test_evalsymmetry).- 3.0–3.4, 3.6–3.9 behaviour-identical (seeded-inert structure; bench unchanged at each step): attack maps + pins, threats, king-safety v2 danger model, per-count mobility tables, pawn/passer refinement, space + winnable coupling, small positional terms, material imbalance, HCE survey adds.
- 3.5 (endgame knowledge) behaviour-changing — exact KPK bitbase, KBNK
corner mate, KNNK/no-pawn-minor/OCB draw scaling. Bench →
4,377,437. SPRT −4.17 ± 5.20 (KEPT) = the per-node NPS tax (EPD-proven correct), not a chess bug; recovered by 3.10 + 3.11 below. - 3.10
apply_endgameguard (behaviour-identical): skip the endgame census in the opening/middlegame — removes the cost behind 3.5's −4. - 3.11 lazy eval (behaviour-changing): skip the heavy positional block when
the material/PST margin is decisive (
LAZY_MARGIN=700;apply_endgameruns on both paths so KBNK/KPK survive). Bench →4,168,590. SPRT +16.64 ± 7.03 Elo, LOS 100%, H1 (LazyOn vs LazyOff) — a clear net gain that recovers the whole Phase-3 NPS tax and then some.
- The next implementation work is Phase 4 — the eval data-fit campaign
(+80–160 Elo), starting at Step 4.0 (build the finite-difference
--tune-kingsafetyre-eval path — the biggest lever — plus the feature-support / bucketed-holdout tuner-readiness gate). None of the Phase-3 structure is tuned yet; Phase 4 is where it activates. Ships as release 1.7.0.
Read PLAN.md §0.5 for why this order: build all eval structure first (Phase 3, no games), fit the eval once (Phase 4), harden time management (Phase 5), then run the search SPSA once last (Phase 6). Texel fits are cheap; SPSA/SPRT games are the conserved resource.
| Phase | What | Status | Model(s) | Elo |
|---|---|---|---|---|
| 1–3 | search SPSA, cheap eval scalars, robustness, eval structure | done (1.5.0/1.6.0) | (historical) | +27 / +54 / +16.6 |
| 4 Eval data-fit campaign | staged Texel refit over the full structure | done (1.7.0) | Sonnet 4.6 medium (driving) | +280.74 vs phase1-final |
| 5 Time-management hardening | clock-at-go fix; 5.8 TM SPSA washed → reverted |
done | Opus 4.8 medium / Sonnet 4.6 medium | ~+3 |
| 6 Search-efficiency wave | 6.1 TT-bound eval; 6.2–6.8 mostly washed/inert; SPSA → 7.4 | ✅ COMPLETE (6.10 +9.14 H1) | Sonnet 5 med; interaction-risk ports Fable 5 med-high (alt Opus 4.8) | +9.14 (bundle) |
| 7 Eval-refresh cycles + one search tune | SF distill +6.75, ×5 self-play cycles (+21.02/+19.51/+18.29/+15.32); 7.4 SPSA skipped; 7.5 LTC gauntlet | ✅ DONE — 1.8.0 released 2026-07-08 | Fable 5 med (alt Opus 4.8) + Sonnet 5 med | ~+93 fast / ~+40 LTC vs 1.7.0 |
| 8 Feature menu | demoted — audit found nothing to add | skip unless evidence | — | ~0 |
| 9 NNUE | the ceiling-breaker | after Phase 7 flattens | Fable 5 high (alt Opus 4.8 high) | +200…+400 |
Per-step models + Elo are on each PLAN step header and in the Phase-7 EV table. SPRT is the only verdict.
Gauntlet validation (2026-06-19, 35k games @
tc=3+0.03): Basilisk 1.5.1 (dev, partial scalar tuning only) was 2nd of 9, +54 over 1.5.0, tied with "SF-capped-2700" — the eval lever is confirmed, with KS/PST/structure still to come. Two cautions: (1) SFUCI_Elois calibrated for 120s+1s/CCRL-40/4, so it is not a true anchor at this fast TC — for a real CCRL number run a slower-TC gauntlet that includes Critter 1.6a (it forfeited at 3+0.03). (2) 1.5.1 lost 65 games on time (vs 1.5.0's 18) — add a hard time-safety floor tocompute_time_limitnow; forfeits contaminate every gauntlet.
So if you say "implement the next step," the model should know (2026-07-04):
1.7.0 has shipped (Phase 4 eval, +280), the dev head is 1.8.0 (bench
13,495,492), and Phases 5 and 6 are both COMPLETE (Phase 6 closed at 6.10 =
+9.14 ± 4.62, H1). Phase 7 is the active phase and its on-policy self-play
cycles keep paying ~+20 each: 7.1 +6.75 (b8c469b), 7.2 +21.02 (d113471), 7.3
+19.51 (9741070) — all H1, cumulative ≈ +47, curve not flattening. The
next step is another self-play cycle from the 7.3 head, same pipeline each
time: datagen.ps1 with -Book tools\texel\data\beast_seed.epd -BookFormat epd → extract_parallel.py --balance-phase 1.5 → --tune all + sequential
--tune-kingsafety (bake tune-all → rebuild basilisk-texel → re-fit KS → bake
KS) → build_test → SPRT vs the prior head. Stop when a cycle yields <~+8
(trust the SPRT — the holdout MSE stalls long before the Elo does).
Most work is a short ping-pong:
You -> "Implement next step of the plan."
Model -> Reads PLAN.md, inspects current state, makes only needed edits, and
tells you exactly what to run.
You -> Run the command and paste the short result.
Model -> Acts on the result: keep, revert, rerun, or move to the next gate.
For SPSA and SPRT, the model cannot honestly guess the result. Your report from the long-running command is the decision input.
Phase 2.9.2 — done (2026-06-19). tools/gauntlet.ps1 now has a -TC/
-MoveTime/-TimeMargin parameter set mirroring sprt.ps1: clock tc=10+0.1
by default (the phase-boundary condition from §10), fixed st=… only when
-MoveTime is given, and the console banner reports the real TC used. Pure
tooling, no engine change.
Phase 5 Step 5.1 / 5.1b (TM foundation; old labels 6.1/6.1b) — done (2026-06-20). A first LittleBlitzer probe
(521 games) of the original Step 2.9.1 patch (a reserve clamp bolted onto the
old tiered-percentage formula) showed an oh1/oh10 tpm gap and a lone forfeit on
the aggressive Move Overhead=1 variant, traced to the old formula being
tighter to the margin than Rarog's already-rewritten SF-style one. Rather than
re-tune that formula's margins, compute_time_limit's clock path is now a
direct port of Rarog's Phase 2.2 Stockfish-style rewrite
(D:\code\rarog\src\time_manager.rs) — logT-based optConst/maxConst,
ply-aware sudden-death and explicit-movestogo branches, same constants, no new
BASILISK_TUNE knobs (matching Rarog, which exposes none either). The 2.9.1
reserve is folded in using Rarog's exact mechanics: hard_ceiling = raw_time - 2*overhead (Basilisk's original patch used 3x effective margin from double
overhead subtraction; now it's 2x, identical to Rarog). compute_time_limit
also gained a game_ply parameter, threaded from both call sites
(Searcher::search and the ponderhit branch of check_stop).
bench 13 unchanged (4,033,379 nodes, non-PGO release-pext — TM doesn't
touch fixed-depth bench), 8/8 CTest passed. Manually verified wtime from
60000 down to 0 (winc=30): smooth degradation, no hangs; at the gauntlet TC
(wtime=3000 winc=30) budgets land ~68-78ms, in the same range as the whole
opponent field's tpm (65.6-70.0) from the prior LittleBlitzer snapshot.
LittleBlitzer validation — PASSED (2026-06-20). Full 35,000-game overnight
pool at tc=3+0.03, default Move Overhead: t=0 for Basilisk 1.5.1-sftm
(and every other engine in the pool — confirms the harness itself is sound, not
just this engine). Rating 2696.8, 62.7% score, 2nd of 5 — essentially tied with
Stockfish-2700-capped, well clear of Rarog 2.1.0 (2620.0) and SF-2600/2500.
This conclusively answers the time-safety question; no remaining doubt that the
Step 6.1/6.1b fix works.
Skippable, not recommended: a second-harness forfeit check. Step 6.1/6.1b
is a forfeit fix, not a strength change — its true Elo delta vs phase1-final
is expected to sit near 0, which is the worst case for an SPRT (elo0=0 elo1=3 converges slowest exactly when the true value sits on the H0 boundary;
it can run tens of thousands of games without crossing either bound — do
not use sprt.ps1 here). If you still want a second-harness sanity check
per Rarog finding #3 (LB can mismeasure some engines), use gauntlet.ps1
(fixed games, no stopping rule) and read the t= count only — ignore the
score:
.\tools\gauntlet.ps1 `
-Engine tools\test_engines\basilisk-phase6-sf-tm-pext-pgo.exe `
-Opponents tools\test_engines\basilisk-phase1-final-pext-pgo.exe `
-Name Phase6SfTm -TC "10+0.1" -Games 1000Given the LB result already showed t=0 for every engine in the pool
(not just Basilisk, so it's not a Basilisk-specific harness blind spot), this
adds little. Skip straight to the release unless you want extra peace of mind.
Then 2.9.3 (release 1.6.0). Then the eval work
begins with Phase 3.0 — the attack-map substrate (PLAN.md §6): a
behaviour-identical refactor that computes attacked_by[color][pt],
attacked[color], attacked2[color] once per evaluate() and routes
mobility, king safety, and hanging through them. No games — the gate is the
bench fingerprint.
Ask the model:
"Build a PGO test binary for the time-safety fix and run a clock-TC gauntlet vs phase1-final to confirm time forfeits drop to ~0."
Then verify nothing changed:
# Rebuild and check the refactor is behaviour-preserving.
cmake --build build\release-pext --target basilisk
.\build\release-pext\basilisk.exe # then: bench 13 (expect the recorded baseline)
# And keep --verify exact:
cmake --build build\release-pext --target basilisk-texel
.\build\release-pext\basilisk-texel.exe --verify tools\texel\data\holdout.csvMost of Phase 3 is the same shape: implement a structural step with new
sub-terms seeded inert → confirm bench 13 unchanged + --verify exact +
CTest → move on. The games (SPRT) only start in Phase 4, when the staged
Texel campaign activates the new terms.
The texel toolchain and datasets already exist (Phase 2): tools/datagen.ps1,
tools/texel/{sample_fens,extract,import_beast}.py, the basilisk-texel
target, and train.csv/holdout.csv (self-play) plus beast_sf_*.csv
(Stockfish-labelled). Reuse them in Phase 4 — no new data needed unless holdout
loss stalls.
This creates about 1.52M train / 80k holdout Stockfish-target positions in
FEN;target format. The importer treats the raw Stockfish WDL target as
side-to-move perspective and converts black-to-move rows to white perspective,
which is what basilisk-texel trains against. Keep it separate from the
self-play data when reporting results.
After the material tune finishes, inspect the printed material deltas before
baking them into EvalParams.h; plausible values still need SPRT before they
are accepted.
For the full Beast import (beast_sf_all_train.csv /
beast_sf_all_holdout.csv), add --max-positions 0 to the tune command.
Without it, basilisk-texel intentionally loads only 5M positions from each
split to protect later, wider tune groups from accidental huge memory use.
After the failed SF-supervised material SPRT, use Beast FENs as start positions only and let Basilisk self-play generate the labels:
python tools\texel\sample_fens.py A:\Chess\Beast\data\txt\positions.txt `
--out tools\texel\data\beast_seed.epd --count 100000
.\tools\datagen.ps1 -Suffix phase2-base `
-Book tools\texel\data\beast_seed.epd -BookFormat epd `
-OutputPgn tools\texel\data\selfplay_beast_seed.pgn `
-Rounds 75000 -Nodes 8000
python tools\texel\extract.py tools\texel\data\selfplay_beast_seed.pgn `
--train train_beast_seed.csv --holdout holdout_beast_seed.csv `
--skip-start 0 --max-per-game 12The Phase 2 scalar tuning is complete (§0.5): material, mobility, passers,
pawn-structure, and hanging are baked into EvalParams.h; rooks was rejected.
Do not continue the old 2.4 subgroups (king safety, PSTs, threats, minors) —
they are deferred into the Phase 4 campaign that runs after the Phase 3
structure exists.
For a fresh clone, the historical Phase 1 commands are:
# One-time setup on a fresh clone
.\tools\setup_tools.ps1
# Build the original default tune baseline, only needed for pruning from scratch
.\tools\build_test.ps1 -Suffix phase1-defaults
# Configure pruning SPSA
.\tools\setup_spsa.ps1 -ConfigGroup pruning -Iterations 5000
# Run the tuner
cd tools\weather-factory
python main.pyStop SPSA with Ctrl-C when values look stable or the planned run is complete.
State is saved under tools\weather-factory\tuner\state.json, so rerunning
python main.py resumes. Running setup_spsa.ps1 again starts a fresh run and
archives old tuner state unless you pass -Resume.
After pruning is accepted, build the current accepted head as the LMR parent and run LMR SPSA from that engine:
cd ..\..
.\tools\build_test.ps1 -Suffix phase1-lmr-baseline
.\tools\setup_spsa.ps1 -ConfigGroup lmr -EngineSuffix phase1-lmr-baseline -Iterations 5000
cd tools\weather-factory
python main.pyThe narrowed combined polish already ran and failed under the old split harness. Do not run it as a Phase 1 continuation. Its one justified retry folds into the relocated Step 7.4 histshape SPSA, after the eval refit, using the unified clock harness:
cd ..\..
.\tools\setup_spsa.ps1 -ConfigGroup combined -EngineSuffix phase1-lmr -Iterations 2000
cd tools\weather-factory
python main.pyTreat that command as a historical reference; the current search-tune config is
config_histshape.json (Step 7.4) — see tools/spsa_configs/README.md.
Minimal:
"Pruning SPSA stopped at 5,000 iterations. Final values: RfpCoeff=128, RazorCoeff=276, NullEvalDiv=180, AspirationDelta=21, ..."
Helpful extras:
"HistPruneCoeff sat at the max for the last 1,000 iterations."
The model will decide whether to bake the values, rerun with adjusted ranges, or discard the noisy group.
Minimal:
"SPRT: H1 accepted after 1,840 games."
or:
"SPRT: H0 accepted after 2,210 games."
Helpful extras:
"Score 53.1%, LLR crossed +2.94."
H1 usually means keep the candidate. H0 usually means revert or retry once if the SPSA run was obviously flawed.
For pure refactors:
"bench 13 = 4,972,548 nodes."
For tuned candidates:
"bench 13 = 4,812,903 nodes."
A changed bench fingerprint is expected after tuning. It is a behavior fingerprint, not an Elo score.
Minimal:
"Threats fit: train loss 0.0981 → 0.0972, holdout 0.0986 → 0.0979."
Helpful extras (these are what the model acts on — loss alone is never the verdict, SPRT is):
"Bucket losses: all held or improved except king-attack (0.094 → 0.097); feature-support flagged
threat_by_minor[Q]with only 31 observations; the new safe-check unit weights fit with plausible signs."
A regressing bucket, a sparse-feature warning, or an implausible sign/shape is a stop-and-investigate signal before spending SPRT games, not after.
Paste the important error line:
"fastchess exited with: engine option RfpCoeff not found."
or:
"bench 13 returned 0 nodes; engine crashed on startup."
The model can diagnose from that.
| Situation | Usual decision |
|---|---|
| SPSA values are stable and plausible | Bake candidate values, build, test, SPRT |
| SPSA values are noisy | Run longer or reduce the group/ranges |
| One plausible value hits a boundary | Widen that range once and rerun |
| Many values hit boundaries | Treat the run as suspect; narrow the problem |
| SPRT accepts H1 | Keep, record, move to next group |
| SPRT accepts H0 | Revert; retry once only if the run was flawed |
| End of Phase 1 | Run fixed-game validation before release work |
| Phase 2 asks for Texel data | Build dataset and holdout before tuning |
Do not keep running repeated SPRTs against tiny changes until one passes. That is just statistical fishing wearing a little hat.
Update this when work completes.
-
tools/setup_tools.ps1exists for fastchess/weather-factory setup. -
tools/sprt.ps1exists and uses repo-local book/default paths. -
tools/gauntlet.ps1exists for fixed-game phase-boundary validation. -
tools/build_test.ps1builds pext PGO tune binaries intotools\test_engines. -
tools/setup_spsa.ps1writes pruning/LMR weather-factory configs. - Baseline
bench 13fingerprint recorded: 4,972,548 nodes. - Calibration self-vs-self SPRT skipped intentionally.
-
SearchParamsstruct exists. - Search params are exposed as UCI spin options under
-DTUNE=ON. - Engine passes tuned params into search.
- Default-equivalence verified: bench 13 = 4,972,548 nodes, 8/8 CTest passed.
- Pruning SPSA run completed.
- Pruning candidate SPRT-confirmed: +18.87 +/- 8.81 Elo, 2930 games, H1 accepted.
- LMR SPSA run completed: 4034 iterations, 129,088 games.
- LMR candidate SPRT-confirmed: +15.63 +/- 8.02 Elo, 3714 games, H1 accepted.
- Optional narrowed combined polish SPSA completed: 2863 iterations, 91,616 games.
- Optional narrowed combined polish SPRT-confirmed rejected: -0.40 +/- 3.20 Elo, 23,210 games, H0 accepted; reverted.
- Phase 1 validation completed: - vs Basilisk 1.4.9/defaults: 2000 games, 638 wins, 482 losses, 880 draws, 53.90%, approx +27.16 Elo.
Phase 1 is complete. Keep phase1-final as the accepted head.
Full per-candidate record (commands, intermediate holdout losses, build fingerprints, PGN paths) lives in PLAN.md §4 — this is the outcome summary.
- 2.0–2.3 infra:
EvalParams(bench-identical, 4,283,684 non-PGO baseline); tune-only loader +dumpeval(900 params, round-trip exact, hidden in release);basilisk-texel(TEXEL=ON, 900-param trace,--verifyexact incl. frozen-term edges);datagen.ps1+extract.py; Beast/SF import verified (1,520,109 train / 79,891 holdout). - 2.4a material — SF-WDL labels rejected (
-16.56 ± 9.56, 2,982 games), so pivoted to Basilisk-labeled Beast-seed self-play; that candidate accepted+29.05 ± 11.36(1,930 games). Bakedmg_val={85,323,363,514,1085},eg_val={96,310,319,557,996}. - 2.4b — tuner gained scalar subgroups with sign/shape clamps + best-epoch
restore (the broad all-scalar pass was not baked: implausible
signs/shapes). Per-subgroup SPRT outcomes:
Subgroup SPRT verdict (games) Baked values mobility accepted +8.77 ± 5.68(7,288)mob_mg={0,0,5,5,1,2,0},mob_eg={0,0,5,7,7,12,0}passers accepted +16.57 ± 8.28(3,462)passed_mg[1..6]={8,8,8,25,90,90},passed_eg[1..6]={10,10,53,84,106,124}, support/cand/free-EG ≈0,pass_safe_eg=16,prox_base=11pawnstruct accepted +30.74 ± 11.76(1,836)doubled=(-2,-8),isolated=(-9,-17),connected=(18,1),backward=(0,-12)hanging baked, SPRT pending hang_pen={0,0,22,39,33,36,0}rooks rejected +3.13 ± 4.74(10,666; inconclusive, awkward shape) — reverted— - 2.4b cheap structure-independent scalars done (mobility/passers/pawnstruct/hanging accepted; rooks rejected). Stop 2.4 here (§0.5).
- 2.4b
threats/minorspawn-threat scalars — DEFERRED to Phase 4.2/4.5 (Phase 3 rewrites the threats term). - 2.4c King safety block — DEFERRED to Phase 4.3 (Phase 3.2 rewrites king safety first; do not tune the toy model).
- 2.4d PSTs (+material refit) — DEFERRED to Phase 4.7.
- 2.4e Global polish — DEFERRED to Phase 4.8.
- 2.5 2000-game validation vs
phase1-final— moved to the Phase 4 boundary.
- 2.9.2 Add
-TC/-MoveTime/-TimeMargintotools/gauntlet.ps1(clocktc=10+0.1default, mirrorssprt.ps1; banner fixed). Pure tooling, no engine change, parse-verified. - 2.9.1 (superseded by Phase 5 Step 5.1/5.1b, old labels 6.1/6.1b, 2026-06-20 — see below) — original standalone reserve patch on the old formula; replaced rather than re-tuned after a LittleBlitzer probe showed it binding more than Rarog's equivalent.
- 2.9.3 Release
1.6.0prep done (2026-06-20) — version bumped insrc/Constants.h+CMakeLists.txt,CHANGELOG.mdentry added, clean non-tune release build verified (options hidden,bench 13matches, 8/8 CTest), dist binaries built, committed. Pending (user-only): create tagv1.6.0and push — the model never tags or pushes. - (calibration, anytime) Slower-TC gauntlet with a CCRL-rated anchor (Critter 1.6a / Fruit 2.1), pin with
ordo -A "<name>" -a <ccrl>(PLAN.md §10).
- 3.0 Attack-map substrate — DONE 2026-06-24.
evaluate()now buildsattacked_by[c][pt],attacked[c](==is_attacked_byunion),attacked2[c],king_zone[c]once; mobility + king-attacker pressure folded into ONE knight/slider sweep (was two); hanging-piece detection readsattacked[]instead ofis_attacked_by().attacked_by/attacked2seeded for 3.1/3.2. blockers/pin masks deferred to 3.2/3.3 (no consumer in 3.0 → dead NPS now; add in the same sweep when consumed). Gates:bench 13 = 4,033,379(identical), 8/8 CTest,--verifyexact (8598/8598). — Opus 4.8 medium - 3.1 Threats package — DONE 2026-06-25. SF-style threats added to
evaluate()on the Step-3.0 maps, all weights seeded 0 (bench unchanged):threat_by_minor/rook[7](by attacked type), threat-by-king, hanging refinement (threat_hanging+weak_queen_prot),restricted,threat_push. Uses the SFstrongly_protected/weakdecomposition; existing flat pawn-threat +hang_penstay active until the Phase-4.2 swap; overloaded-defender deferred. Gates:bench 13 = 4,033,379(identical), 8/8 CTest,--verifyexact (8598/8598). — Opus 4.8 high - 3.2 King safety v2 — DONE 2026-06-25. Rebuilt the KS finalization as a full danger model feeding the
attack_units → safety_tablefunnel, all inputs seeded 0 (no-queen relief re-expressed asks_noqueen_num/den=2/3, byte-identical):ks_safe_check[7],ks_weak_ring,ks_ring_pressure,ks_flank_attack/defense,ks_pawnless_flank,ks_king_blockers,ks_central_king,ks_shelter_storm(open-files-near-king proxy; existing linear shelter/storm stays active in parallel). No linear trace (index-shaping → Phase-4.3 finite-difference tuner). Also computedblockers_for_king[](resolves the deferred 3.0 pin masks; 3.3 reuses them). Gates:bench 13 = 4,033,379(identical), 8/8 CTest,--verifyexact (8598/8598). — Opus 4.8 high - 3.3 Per-count mobility tables — DONE 2026-06-25. Replaced linear
mob*wwith one-hot tablesmob_n[9]/mob_b[14]/mob_r[15]/mob_q[28](mg/eg), seededtable[i]=i*old_w(byte-identical); oldmob_mg/eg[7]removed; tunermobilitygroup + clamps updated. Mobility area left exactly as today — the SF-style area refinement (exclude own K/Q, blocked pawns, pinned) is a behaviour change deferred to Phase 4.4 (Rarog precedent), where it's A/B'd with the table fit. Gates:bench 13 = 4,033,379(identical), 8/8 CTest,--verifyexact (8598/8598). — Opus 4.8 high - 3.4 Pawn-structure refinement — DONE 2026-06-25. All seeded 0 (existing flat pawn/passer terms stay active). Pawn-cache-safe in
eval_pawns:connected_rank[8],weak_unopposed,pawn_lever,blocked_pawn[2](rel rank 5/6),pawn_majority(per flank). Piece-dependent inevaluate()(attack maps / kings):passed_path_safe_eg,passed_block_defended_eg,passed_king_block_eg,blockader_knight_eg. Gates:bench 13 = 4,033,379(identical), 8/8 CTest,--verifyexact (8598/8598). — Opus 4.8 medium - 3.5 Scale-factor framework + endgame knowledge — DONE 2026-06-25 (behaviour-changing). Added a
ScaleFactorframework (0–64,SCALE_NORMAL=64,SCALE_DRAW=0) +apply_endgame()run before the 50-move damping: exact KPK bitbase (retrograde fixed-point, lazy-built once), KBNK corner mop-up driving the bare king to the bishop-coloured corner ({a1,h8} dark / {a8,h1} light), KNNK draw, no-pawn ≤ minor draw-scaling, and the generalised OCB rule folded into the framework (behaviour-identical when it applies). Generic mate-drive mop-up kept for the general KXK case. All terms frozen (non-traced) →--verifyexact by construction. Unlike 3.0–3.4 this is NOT bench-identical: KPK/KRKR-scaling material is reachable at the leaves of the bench's pawn/rook endings, so the score of bench position 3 changes. New fingerprintbench 13 = 4,377,437(was 4,033,379). Gated by the newtests/endgames.epdsuite (18/18) +test_endgamesCTest. SPRT (2026-06-25,tc=3+0.03, simplify[-5,0]): −4.17 ± 5.20 Elo, LOS 5.79%, 7,586 games, stopped manually. KEPT (not reverted). Read as NPS tax, not a chess bug: EPD passing rules out a scaling regression; the small negative isapply_endgame()'s per-node census on every node (worst TC for endgame knowledge). Same NPS-tax shape as the whole seeded-inert Phase-3 structure → recovered by theapply_endgameguard in 3.10 + the Phase-4-boundary verdict (lazy eval 3.11 can't skip endgame scaling). Gates: bench 4,377,437, 9/9 CTest, endgame suite 18/18,--verifyexact. — Opus 4.8 high - 3.6 Space + winnable coupling — DONE 2026-06-25 (bench-identical). Space refinement (traced, seeded 0): base
SpaceMgkept active +SpaceBehindMg(safe central squares behind own pawns),SpacePieceMg(per-side safe-space × own non-pawn piece count),SpaceBlockedMg(× own blocked-pawn count) — tuned withmiscin Phase 4.5. Winnable/complexity (frozen, not traced, seeded 0): complexity over king outflanking / both-flanks pawns / king infiltration / pawn-only endgame / passed count / total pawns / bias, applied toegwith a sign-preserving clampeg += sign(eg)·max(complexity,−|eg|)(never flips eg sign); nonlinear → finite-difference tuned in Phase 4.5 like the 3.2 KS funnel; new tunerwinnablegroup registers the 7 knobs. Gates:bench 13 = 4,377,437(identical), 9/9 CTest,--verifyexact. — Sonnet 4.6 medium - 3.7 Small positional terms — DONE 2026-06-25. Seeded-0 batch in
evaluate()(attack maps + king_zone): bishop_outpost, reachable_outpost, bad_bishop, minor/rook_king_ring, rook_closed, rook_queen_file, connected_rooks, weak_queen, bishop_pair_pawns (flat bp stays). Deferred (optional/cheap-scope): long-diagonal, bishop x-ray, uncontested outpost, Ethereal closedness/central-king. Gates: bench 4,377,437 (identical), 9/9 CTest,--verifyexact (8598/8598). — Sonnet 4.6 medium - 3.8 Material imbalance — DONE 2026-06-25. SF-style symmetric quadratic imbalance, all coeffs seeded 0:
imb_linear[6],imb_our[21],imb_their[21](lower-triangular count-product coefficients). Linear in its coefficients → traced normally (TR_BOTH, mg=eg) and fit by the ordinary linear tuner (no finite-diff needed);--verifyexact proves the bookkeeping. Addedimbalancetuner group. Gates: bench 4,377,437 (identical), 9/9 CTest,--verifyexact (8598/8598). — Opus 4.8 high - 3.9 HCE survey additions — DONE 2026-06-25. Seeded-0: unstoppable_passer (rule of the square, enemy-pieceless + clear path), minor_behind_pawn, king_protector (minor→king distance), queen_infiltration, pawn_islands (eval_pawns). Optional trio (bishop x-ray, R+Q battery, slider-on-queen) skipped. Gates: bench 4,377,437 (identical), 9/9 CTest,
--verifyexact (8598/8598). — Opus 4.8 medium / Sonnet 4.6 medium - 3.10 Eval hot-loop cleanup — DONE 2026-06-25 (apply_endgame guard). Restructured
apply_endgame()so the 12-popcount census + the five known-endgame rules run only when a side is a lone king or pawnless (the necessary condition); the opening/middlegame skips the whole block, OCB stays outside and always runs. Behaviour-identical (guarded rules can't fire when the condition is false) → bench unchanged. Removes the per-node census behind 3.5's −4 SPRT → a re-SPRT should recover it. No speculative micro-opt (per the plan). Gates: bench 4,377,437 (identical), 9/9 CTest,--verifyexact (8598/8598). — Opus 4.8 medium - 3.11 Lazy eval — DONE 2026-06-25 (single checkpoint; SPRT owed). Checkpoint before the attack-map substrate: if
|tapered(material/PST/imbalance/pawns/minor-bonuses)| > LAZY_MARGIN(=700, conservative, tune under SPRT) skip the whole attack-map block and finish withapply_endgame(so KBNK/KPK survive —test_endgamespasses). Early return#ifndef TEXEL_TRACE→--verifyexact. Bench re-baselined 4,377,437 → 4,168,590. Single checkpoint (2nd checkpoint = later refinement). Gates: 9/9 CTest,--verifyexact. SPRT ✅ ACCEPTED H1 (2026-06-27,[-3,0]): +16.64 ± 7.03 Elo, LOS 100%, 4,828 games (LazyOn vs LazyOff/d4cc5cf) — a clear net gain, far above Rarog's +4.4; recovers the Phase-3 NPS tax (incl. 3.5's −4). LAZY_MARGIN=700 kept (lowering it = a Phase-5 SPSA option). This closes Phase 3. — Opus 4.8 high - Phase 3 gate — MET. Every step:
--verifyexact (8,598/8,598), CTest 9/9 incl.test_endgames(endgame EPD suite 18/18).benchidentical for the inert steps (3.0–3.4, 3.6–3.10); the two behaviour-changing steps re-baselined per their SPRT gates (3.5 →4,377,437, 3.11 →4,168,590). - Phase 3 trace/eval regression — DONE (the Phase-3-scoped checks). (a) reconstruction == eval delta:
--verifyexact (8,598/8,598) every step. (b) eval symmetry:test_evalmirror test (5 positions incl. piece-rich middlegames + the endgame-scaling rook ending), passing. (c) seeded-zero changes trace but not eval/bench: bench identity per inert step + theTR_traces fire (exercised by--verify). (d) per-term activation counts — CLOSED 2026-06-27 bybasilisk-texel --feature-supporton the full 1.7M train set: every linearly-traced term fires; the only 61 zero-activation traced params are all structurally impossible (pawn PSTs ranks 1/8, passer/connected-rank 0/7, theImbTheirdiagonal, piece-type none/king slots), and the 33 KS/winnable funnel knobs are expected-zero. (PLAN.md §6) - Phase 3 eval-cost budget — MEASURED (record-only, not a gate). Best-of-5
benchNPS, pext-PGO: Phase-3 head 2,988,236 vsphase1-final3,333,606 = −10.4% — better than Rarog's −22%, because 3.10'sapply_endgameguard + 3.11 lazy eval recovered most of the seeded-inert cost. Per the plan this is not gated (terms are overhead until Phase 4 activates them); the real strength verdict is the Phase-4 boundary. (The lazy-on-vs-off SPRT already showed +16.6 Elo net, so the raw −10.4% does not cost Elo.) - HCE source checklist — DONE at planning (2026-06-20). The term list was cross-checked against SF11/classical (SF16 ≈ SF11 frozen — faithfulness check only) + Ethereal (source of
Closedness) + RubiChess + Lambergar; the non-SF terms that pass surfaced (closedness, central-king danger, overloaded defender) were folded into 3.1/3.7. (PLAN.md §3.9)
- 4.0 Tuner/data readiness gate — DONE 2026-06-27. All built/resolved:
--feature-support(closes box 2d; full-train run = every traced term fires, 61 zeros all structurally impossible),--tune-kingsafetyfinite-difference path (snapshot+reused Board, coord descent over 43 KS funnel knobs,safety_tablenon-decreasing; smoke 60k: holdout MSE 0.09691→0.09619, activatesks_safe_check12/4/8/4 etc.),--l2L2-to-prior, bucketed holdout (phase split in--tune),extract.py --balance-phase(phase-faithful mix + downsample). Blended labels = capability already in place; binary cache deprioritized; targeted-data = process.--verifystill exact. (PLAN.md §6.5 Step 4.0). (Original spec retained below.) — BUILD the finite-difference re-eval path (perturb weight → re-evaluate()→ ΔMSE, coordinate descent + shape clamps) for theattack_units→safety_tablefunnel knobs; the linear trace is structurally blind to them (Rarog: all 11 danger inputs = 0 activations, and fitting them was its biggest stage, +42.5 Elo). Trace linearly whatever 3.2 can express as a direct mg/eg contribution so only the funnel knobs need the re-eval path. Then: feature-support diagnostics (run first — it's what reveals the dead KS inputs; freeze/merge sparse params; this also performs the per-term activation-count check that closes Phase-3 trace-regression item (d) — i.e. confirm every linearly-traced term fires nonzero, and that the zero-activation ones are exactly the finite-diff/frozen funnel knobs, not bugs), bucketed holdout (phase / material class / king-attack / passers / quiet-threat — a bucket regressing while global loss drops is a stop-and-investigate signal), targeted-data policy (regen only the offending bucket), phase-balanced sampling quotas, blended (result+teacher) labels, binary feature cache, regularization/shape constraints (PLAN.md §6.5 Step 4.0). Dataset: ~2–3M on-policy self-play from the post-Phase-3 head; raw count isn't the constraint (PSTs saturate by ~1M) — per-term support is, so run feature-support first and targeted-top-up only sparse buckets. Big refresh = Phase 7, not now. - 4.1 = KING SAFETY — ACCEPTED +65.48 ± 13.58 Elo (H1, LOS 100%, 1514 games, 2026-06-27). Ran KS first (biggest lever) via
--tune-kingsafetyon the 1.7Mbeast_seeddata: holdout MSE 0.09870→0.09780, activatedks_safe_check7/6/8/6 +ks_king_blockers4, sharper monotonicsafety_table(tail→296). Baked; bench 4,168,590→4,123,914, 9/9 CTest. Bigger than Rarog's +42.5. New head =phase41-ks. (Material re-confirm low-value — folded into 4.7.) - 4.2 Threats — ACCEPTED +79.13 ± 14.82 Elo (H1, LOS 100%, 1264 games, 2026-06-27). Linear
--tune threatsfrom the KS base; new SF threats activated,hang_pendropped to 0 (absorbed by the package). Baked; bench→3,929,330, 9/9 CTest. Bigger than Rarog's +45.2. New head =phase42-threats. Phase-4 cumulative ≈ +144 (KS +65, threats +79). - 4.3 King safety v2 group — DONE (this IS the king-safety fit, executed FIRST as "Stage 4.1" above; +65.48 Elo accepted). Numbering note: the plan table reserves 4.3 for king safety, but I promoted it to the first fit ("biggest lever first"), so it carries the execution label 4.1 / binary
phase41-ks. Same work — not a separate outstanding stage. - 4.3b King-safety SPSA polish (optional, not yet done) — a small game-based SPSA over a few KS knobs on top of the data fit; decide if worth it after the campaign. — Sonnet 4.6 medium
- 4.4 Mobility — DONE 2026-06-28, no clean win (kept threats head). Investigated thoroughly: (1) per-count table fit on the existing area — no true headroom over the Phase-2 linear seed; unregularized fit over-valued mobility and failed CTest (startpos depth-5 +131), L2 confirmed the "gain" was overfitting (collapses to seed). (2) SF-style mobility-area refinement (exclude own K/Q, blocked/back-rank pawns, pinned
blockers_for_king; include own minor/rook squares) — implemented + re-fit tables (monotonic clamp): improved holdout −0.00064, but still over-valued (startpos +131, failed CTest); L2 collapsed the gain; area-change with seed tables landed startpos at exactly 100 (borderline) with noise-level holdout. Reverted both. Conclusion: Phase-2 linear mobility is already well-tuned; mobility has no calibrated, sanity-passing improvement on 4.2-head data. The area refinement is structurally correct, so the re-try is scheduled as its own step, 4.6b, on fresh data (the 4.7 boundary). (The CTest startpos sanity bound earned its keep twice.) - 4.5 = POSITIONAL DATA-FIT — ACCEPTED +57.21 ± 15.48 Elo (H1, LOS 100%, 956 games, 2026-06-28). One joint Texel group
phase45(120 active params) over the full positional structure: pawn refinement (3.4), small positional terms (3.7), HCE survey adds (3.9), minors, rooks, space (3.6), winnable (3.6) — excludes material/PST (4.7), imbalance (4.6), mobility/threats/KS (done). Fit with L2-to-prior from the start (mobility lesson). λ-sweep on 1.73Mbeast_seed: λ=1e-6 holdout −0.000608 (48 moved) / 2e-6 −0.000450 / 5e-6 −0.000287. Baked λ=1e-6 — biggest gain and it survives regularization (the test mobility failed). 45 members moved, all small & sensibly-signed (bad_bishop_eg −1,blocked_pawn −2,space_mg→0,tempo +3,passed_king_block_eg +7). Gate:--verifyexact (10000/10000), 9/9 CTest, startpos depth-12 +52cp (mobility hit +131), NPS unchanged. New head =phase45-positional(bench 3,987,976). New tooling:tools/texel/bake.py(dump→header, self-limiting to moved params) + tuner groupphase45. — Opus 4.8 high - 4.6 = MATERIAL IMBALANCE — ACCEPTED +26.94 ± 8.38 Elo (H1, LOS 100%, 3218 games, 2026-06-28). SF-style quadratic imbalance (3.8) fit linearly on 1.73M
beast_seed, on top of the 4.5 head, with L2-to-prior. λ-sweep: 1e-6 holdout −0.000344 (25 moved) / 2e-6 −0.000239 / 5e-6 −0.000134. Baked λ=1e-6. The 6 structurally-deadimb_theirdiagonal coeffs (when i==j the featuretheir_cnt = ic_W·ic_B − ic_B·ic_W ≡ 0; flat t∈{0,2,5,9,14,20}, flagged by 4.0 feature-support) were excluded from the active set and verified to stay 0. 3 array members moved, all small (max coeff 8):imb_linear→[0,0,0,1,1,1], plus modestimb_our/imb_theirquadratics. Gate:--verifyexact (10000/10000), 9/9 CTest, startpos depth-12 +48cp, NPS fine. New head =phase46-imbalance. Baked viabake.py. — Sonnet 4.6 medium - 4.6b Mobility-area refinement — DONE 2026-06-28, sane on fresh data (kept). Re-applied the SF mobility area in
eval.cpp(exclude own K/Q, blocked/low-rank pawns, pinnedblockers_for_king— moved before the sweep; include own minor/rook squares) + a monotonic non-decreasing clamp on the mobility tables in the tuner; re-fit on the 3.35M v17 set. The 4.4 over-valuation is GONE: startpos depth-12 = +32cp (4.4 had +131 and failed the CTest bound) — fresh diverse labels calibrated the magnitude, exactly as the 4.4 note predicted. Holdout gain is marginal (area itself −0.00003; the refit moved tables only ±1, +noise), so rather than spend a separate likely-inconclusive SPRT, the SF area is kept as the more-correct model and validated folded into the 4.7 combined SPRT vsphase46. Gates:--verifyexact (10000/10000 fresh holdout), 9/9 CTest, bench re-baselined 4,150,316. Standalone binaryphase46b-mobareaexists to isolate if 4.7 regresses. — Opus 4.8 high - 4.7 = PST + MATERIAL REFIT — ACCEPTED +6.45 ± 4.60 Elo (H1, LOS 99.7%, 10402 games, 2026-06-28; combined with 4.6b vs
phase46). Definitive refit of all 782 PST+material params on the fresh 3.35M v17 set, L2=1e-6 anchored toward the PeSTO seeds. λ-sweep: 1e-6 holdout −0.000149 (42/778 moved, converged epoch 9) / 5e-6 −0.000059. Baked 1e-6. Conservative as expected (PSTs were already PeSTO-quality, material already tuned): material barely moved (pawn mg 85→83, eg 96→94, rest ±1), PSTs adjusted 4/6 piece tables each. Gate:--verifyexact (10000/10000 fresh holdout; the regenerated PST block kept the structurally-dead pawn-rank squares at 0), startpos depth-12 +50cp, 9/9 CTest, bench 3,763,657. Baked viabake.py --allow-pst(first use of the 2-D path). New head =phase47-pst. The modest gain validates 4.6b too (one combined SPRT). — Sonnet 4.6 medium - 4.8 = GLOBAL POLISH — ACCEPTED +33.25 ± 9.40 Elo (H1, LOS 100%, 2610 games, 2026-06-28). Joint refit of all 1179 params (material + PST + all positional + threats + mobility + imbalance) on the fresh 3.35M v17 set, lr 0.15, L2=1e-6. λ-sweep: 1e-6 holdout −0.000695 (117 moved, epoch 55) / 3e-6 −0.000365 (34). Baked 1e-6. Far bigger than a typical "polish" because the 4.5 positional terms were fit on the old beast_seed — reconciling them (+ everything else) to fresh v17 recovered real headroom; all three phases improved (opening 0.1460→0.1450, mg 0.0950→0.0940, eg 0.0625→0.0618), not endgame over-tuning. Deltas small/sensible (material ±1). Gate:
--verifyexact, startpos +60cp, 9/9 CTest, bench 3,764,539. New head =phase48-polish. KS re-fit deferred to Phase 7 (v17 is endgame-heavy → wrong data to re-fit king safety). Then: 4.8a prune + 2000-game validation vsphase1-final+ gauntlet. (The post-Phase-5 eval-refresh grind is its own Phase 7 — see below.) - 4.8a Dead-feature prune — DONE 2026-06-29 (behaviour-identical). The 4.8 global-polish fit on fresh v17 confirmed every flagged term stayed exactly 0 even on endgame-heavy data → genuinely redundant, removed. Pruned 14 params / 8 features:
pass_supp(×3),cand(mg+eg),pawn_lever(mg+eg),blockader_knight_eg,bishop_outpost(mg+eg),weak_queen(mg+eg),unstoppable_passer_eg,space_behind_mg— acrosseval.cpp(compute + trace blocks),EvalParams.h(struct members +EVAL_PARAM_LISTX-macros), andtuner.cpp(thephase45/miscgroup ranges + clamp lines). Gated on byte-identical bench (3,764,539 unchanged) +--verify10000/10000 exact (trace layout shrank consistently) + 9/9 CTest; builds clean with no orphaned variables (verifiedpath/q_them/outpost_sqs/wp/bpare all still used by live terms). Confirmed by a simplification SPRT ([-5,0], 3+0.03): +2.49 ± 4.19 Elo, H1 accepted, 11294 games — i.e. not a regression, hair-positive from the leaner-eval NPS edge, exactly as expected. The KS re-fit (the other audit flag) is deferred to Phase 7 — v17 is endgame-heavy (59%), the wrong data to re-fit king safety. — Opus 4.8 high
Phase 5 - Time Management hardening + tuning (PLAN.md §7) — promoted to Phase 5, executed BEFORE the Phase 6 search wave (2026-06-29)
The 5.1/5.1b/5.2 foundation (Rarog-port formula + reserve + the 2026-06-20 LB validation) is done and stays. Reopened (steps 5.3–5.9) because LB still shows time-losses post-Phase-3/4 (heavier eval per node) and the TM constants were never tuned for Basilisk. Root cause (2026-06-29): (1) the clock starts inside the worker, missing
go-receipt→search dispatch latency the GUI charges (likely the main LB forfeit); (2) fixed 2048-node poll overshoots the tiny bullet budget now that nodes are heavier; (3) thin static2*overheadreserve
- 10 ms default Move Overhead; (4) zero TM-constant tuning. Aim: generally strong across bullet→slow, not a TC specialist.
PHASE 5 STATUS (2026-06-29). Diagnosis (5.3) overturned the premise: the engine's TM is sound — no overshoot (≤1 ms / 12k bullet moves), correct allocation; the LB forfeits are GUI-pipe latency, not an engine bug. So the robustness "repairs" were unnecessary: 5.5 dropped (no overshoot), 5.6 dropped (no real forfeit), 5.7 deferred (existing adaptive logic already extends on instability; a mid-iteration version needs an SPRT). Code-complete and committed: 5.3 (TM_Debug instrument), 5.4 (clock-at-
go, bench-identical, SPRT-validated +2.95 ± 6.74, kept), 5.8 (9 TM knobs exposed for SPSA, behaviour-identical at defaults). One maintainer validation remains to close Phase 5:
5.4 non-regression SPRT— DONE (+2.95 ± 6.74 at 4,468 games, stopped early as a confirmed non-regression).- 5.8 TM-constant SPSA (the Elo lever, +8–25):
at./tools/setup_spsa.ps1 -ConfigGroup tm -EngineSuffix phase5tm -Iterations 5000 cd tools\weather-factory ; python main.pytc=10+0.1; then re-validate the tuned set at1+0.01and60+0.6, bake intoSearchParams.hdefaults, and SPRT the baked set vsphase5tm.After 5.8 bakes + 5.9 cross-TC check → Phase 5 ships and Phase 6 (search wave) opens. Until then the Phase-5 head is behaviour-identical to 1.7.0 except 5.4.
- 5.1 Increment-aware budget formula implemented — direct port of Rarog's
Phase 2.2 SF-style rewrite (logT-based optConst/maxConst, ply-aware
sudden-death + explicit-movestogo branches);
compute_time_limitgained agame_plyparameter.bench 13unchanged (4,033,379nodes), 8/8 CTest passed. (implemented 2026-06-20 under its old label Step 6.1.) - 5.1b Time-safety reserve matched to Rarog's exact mechanics (
raw_time - 2*overheadhard ceiling, not the original 2.9.1 patch's 3x-effective double subtraction). (old label 6.1b.) - 5.2 Harness support exists:
sprt.ps1 -TCand-MoveTime, plusgauntlet.ps1 -TC/-MoveTime/-TimeMargin(Step 2.9.2). (old label 6.2.) - 5.2b LittleBlitzer validation leg — PASSED (2026-06-20). Full 35,000-game
overnight pool at
tc=3+0.03, default Move Overhead:t=0for Basilisk 1.5.1-sftm (and every other engine in the pool). Rating 2696.8, 62.7%, 2nd of 5 — essentially tied with SF-2700-capped, far ahead of Rarog/SF-2600/SF-2500. (The optional fastchess second-harness leg and the old "6.3 optional SPSA" are absorbed into 5.9 / 5.8 below.) - 5.3 Diagnose & instrument the LB time-loss — DONE 2026-06-29 (instrumented + measured). Findings (1+0.01 fastchess, 12,471 moves + the LB forfeit log): the engine's TM is sound — overshoot is negligible (14/12471 moves over
hardby ≤ 1 ms, pure poll-rounding) and allocation is correct (elapsed < hardalways). The only harness-independent imperfection is dispatch ≤ 20 ms (go-receipt→clock-start, under bullet+concurrency) — what 5.4 reclaims. The LB forfeits are GUI/pipe latency, not a budget bug: the LB log caught a forfeit withelapsed_ms=112but LB charging 959 ms (dispatch_ms=0) — ~847 ms in the LB↔engine pipe (LB is a fragile 2012 single-threaded GUI choking on verbose bullet output). fastchess (efficient I/O,timemargin=40) never forfeits. Consequences: 5.5 is unnecessary (no real overshoot); the LB issue is not a TM-budget repair. — Opus 4.8 mediumTM_DebugUCI check (default off) — advertised only in tune/dev builds (#ifdef BASILISK_TUNE) so a harness/GUI actually sends thesetoption(fastchess/LB silently skip unadvertised options — the first attempt loggedWarning; doesn't have option TM_Debugand produced no data); release builds keep a clean 9-option list. When on,Searcher::searchemits oneinfo string tm soft_ms=.. hard_ms=.. elapsed_ms=.. dispatch_ms=..per move.dispatch_ms=go-receipt (captured inUciProtocol::cmdGo) →start_time_, the latency the GUI charges butelapsed_seconds()doesn't yet count (defect #1). Timing unchanged; bench 3,764,539, 9/9 CTest, debug-off play-identical. Diagnostic binary:tools/test_engines/basilisk-phase53-tmdebug-pext-pgo.exe. Pending (maintainer): run it under LB / a1+0.01fastchess gauntlet withoption.TM_Debug=true+ engine logging, capture theinfo string tmlines, and confirm which defect dominates (expectdispatch_ms> 0 under LB andelapsed_ms>hard_msovershoot at bullet) → sizes 5.4/5.5. — Opus 4.8 medium - 5.4 Start the clock at
go-receipt — DONE + VALIDATED 2026-06-29. Non-regression SPRT[-3,0]at 3+0.03 (phase5vsv1.7.0): +2.95 ± 6.74 Elo at 4,468 games (LLR 0.66, point estimate solidly positive) — stopped early as a confirmed non-regression (formally crossing H1 would take ~15k more games for a low-stakes safety change measuring positive). Kept.Searcher::searchnow setsstart_time_ = limits.go_recv_time(thecmdGotimestamp threaded in 5.3) instead of the in-workernow(), falling back tonow()when unset (internal/bench). The engine now accounts for the ~20 ms dispatch latency 5.3 measured, tightening it against the GUI clock.dispatch_msreads 0 by construction now (confirms it). Bench 3,764,539 unchanged, 9/9 CTest. Validated via thephase5tmbinary (5.4 + 5.8-exposure, the latter behaviour-identical at defaults). — Opus 4.8 medium - [SKIPPED] 5.5 Anti-overshoot poll granularity — LIKELY SKIP (5.3 data). Premise was that heavier eval per node would overshoot the tiny bullet budget; measured overshoot is ≤ 1 ms over 12,471 bullet moves, so the fixed
(nodes_ & 2047)poll is already fine. Revisit only if a future change (or a real harness) shows actual overshoot. — Sonnet 4.6 medium - [DROPPED] 5.6 GUI-robust reserve + Move Overhead default — DROPPED (5.3 data, 2026-06-29). Premise was forfeits from a too-thin reserve; the engine never forfeits in robust harnesses (fastchess
1+0.01, 12k moves, 0 losses) and the LB forfeit is GUI-pipe latency (~847 ms) a reserve can't sanely absorb. The current2*overheadreserve is adequate. Revisit only if a real (non-LB) forfeit appears. - [DEFERRED] 5.7 Root fail-low / instability time extension — DEFERRED (2026-06-29). The adaptive-stop block already implements instability extension (best-move stability scale → more time when the move just changed; score-drop scale → more time on a crash; effort scale), and 5.8 just made all of those SPSA-tunable. A mid-iteration fail-low extension is a behaviour-changing search heuristic with real regression risk that needs an SPRT to land — out of scope for the games-less autonomous pass. Revisit after the 5.8 SPSA (or fold into the Phase-6 search wave) if there's measured headroom. — Opus 4.8 medium
- 5.8 Expose TM constants under
BASILISK_TUNE— EXPOSURE DONE 2026-06-29 (SPSA pending). Added 9 SPSA-tunable knobs toSearchParams(defaults == baked → behaviour-identical):TmOptMult/TmMaxMult(overall budget ×100),TmStability(×1000),TmScoreDropThr/TmScoreDropDiv,TmEffortHi/Lo/HiMult/LoMult.compute_time_limitapplies the multipliers;Searcher::searchreads the adaptive-stop knobs. Exposed only under#ifdef BASILISK_TUNE(release stays 9 options, verified), parsed inParameters.cpp, wired intosetup_spsa.ps1as grouptm(tools/spsa_configs/config_tm.json). bench 3,764,539, 9/9 CTest, tune build lists + accepts them. SPSA binary:basilisk-phase5tm-pext-pgo.exe. SPSA RUN + BAKED 2026-06-30 (1147 iters @ 3+0.03, weather-factory): converged onTmOptMult 105/TmMaxMult 85(the dominant levers — more on the typical move, tighter max cap) + the secondary knobs (stability 69, scoredrop 35/104, effort 74/20/82/126). Baked intoSearchParams.h; bench 3,764,539 unchanged, 9/9 CTest; verified the budget moves (soft 258→271, hard 1774→1508). Candidatebasilisk-phase58-tmtuned-pext-pgo.exe. REVERTED 2026-07-01 after the 5.9 validation (below): the bake was a wash, soSearchParams.hwas restored to the hand-tuned defaults (100/100/60/30/100/80/25/80/120); the 9 knobs stay exposed underBASILISK_TUNEfor future re-tuning, only the baked values were dropped. — Sonnet 4.6 medium - 5.9 Validation + Phase 5 finalize — DONE 2026-07-01. PHASE 5 COMPLETE. The gain SPRT
elo1=3at 3+0.03 (phase58-tmtunedvsphase5tm) was a wash:+0.88 ± 4.03 Eloover 12,262 games, LLR flat (~−0.05) inside[0, 3], LOS 66%. The SPSA found no gain even at its own tuning TC — a clean confirmation of the Phase 5.3 "TM is already sound" finding. Decision: revert the 5.8 bake (defaults restored — see 5.8) and skip the cross-TC legs (not worth the compute for a sub-Elo, plausibly TC-overfit tweak). Phase 5 ships on 5.4 (clock-at-go) alone, already validated+2.95 ± 6.74non-regression — and the reverted defaults reproduce exactly thephase5tmbinary that carried that result, so no re-validation is needed. Bundled into the 1.8.0 dev line (not released standalone; ships alongside the Phase 6 search wave). — Opus 4.8 medium
Phase 6 - Search Efficiency Wave (PLAN.md §5; renumbered to Phase 6 on 2026-06-29 — executed AFTER Phase 5 time management. SPSA last — driving Sonnet 5 medium, dense/interaction-risk ports Fable 5 medium-high with an Opus 4.8 alternative — Fable 5 is available for a limited time)
- 6.1 TT-bound eval refinement — ACCEPTED +7.18 ± 4.05 Elo (H1, LOS 99.97%, 13,162 games, 2026-07-01). In
negamax, a separateevalfor pruning decisions only (RFP/razoring/NMP/futility) prefers the TT score when its bound proves it tighter than the corrected static eval (exact / fail-high above / fail-low below);ss->eval/static_evalstay raw soimproving+ correction-history are unaffected. Mirrored in qsearch stand-pat. Gain SPRTelo1=3at3+0.03(phase61-ttboundvsphase5final) crossed H1 (LLR 2.96; Ptnml [318,1562,2639,1654,408]). Merged todevelopment(commit00370b8, the validatedb600571). New head fingerprint 13,503,085 (was 13,466,664; the pruning cuts the hardest bench position hard — top-pos share 19.6% → 7.2%). Early SPRT trend was negative → recovered to +7.18: a reminder not to react to sub-3k-game noise. Follow-up: a mate-score guard on the refinement (branch commit75650d6, house-styleabs(tt_score) < MATE_SCORE - MAX_PLY, matching the existing ProbCut guard) is NOT in the validated head — bundle it into 6.2's SPRT as a non-regression rider, or apply as a rare-trigger correctness bugfix. — Sonnet 4.6 medium - [REJECTED] 6.2 History: 6-ply continuation table — REGRESSED −7.70 ± 4.77 Elo (H0, LOS 0.08%, 9,204 games, 2026-07-01). Added
cont_hist6_(ply-6 continuation,/3weight) mirroringcont_hist4_, on branchphase6-6.2-history(commit0f4d08d), bundling the 6.1 mate guard. Bench dropped 13,503,085 → 11,266,326 (~−16% nodes) — but the node cut came largely from the extra history magnitude feeding LMR/history-pruning (tuned without it) → over-pruning → clean regression. Dropped; NOT merged. Development stays at the validated 6.1 head. Key lesson (PLAN §0.5): cont_hist6 is a Rarog feature that gains for Rarog but hurts Basilisk — a direct confirmation that we must benchmark against SF and let SPRT decide, not copy the sibling. The bundled mate guard can't explain −7.7 (rare trigger); it survives onphase6-6.1-ttbound(75650d6) for a future non-regression rider. If ply-6 is ever revisited, do it via the wave2 SPSA (jointly with the divisors it disturbs), not as a hand-add. — Sonnet 4.6 medium
Remaining steps REWORKED 2026-07-01 from a line-by-line audit of SF master (snapshot 2026-06-30) + Ethereal 14.40 + Weiss 2025 (the HCE anti-bias check — PLAN §5 has the full audit + formulas). Basilisk's search base is audit-confirmed modern (singular w/ multicut+double+negative ext, IIR, ProbCut, LMP, history pruning, corr-hist, etc.); the steps below are the proven remaining refinements, ordered by evidence strength. Rules unchanged: one at a time, SPRT
elo1=3@ 3+0.03, knobs exposed at introduction, SPSA only in 6.9. Models: Fable 5 is limited-time — Opus 4.8 at the same thinking level is the standing alternative.Pattern established by 6.3 + 6.4 (both landed as exposure-only, 2026-07-01): literature constants (SF's, Weiss's) chaotically break the KBNK/KQK mate-resolution CTests because Basilisk's other search constants weren't tuned for that scale. For 6.5–6.8: implement the mechanism, expose it, verify CTest passes at a provably-inert default (mathematically argued, not just empirically observed to pass), and defer the real value to 6.9 — do not spend cycles hand-picking a canary-passing constant.
- 6.3 History formula — DONE 2026-07-01 as behaviour-identical EXPOSURE (commit
18c5db5; constants deferred to 6.9). The planned Weiss-constant transplant (and SF's, and blends) chaotically destabilised the mate-resolution CTests (KBNK playout + KQK mate-in-5 flipped non-monotonically under consumer knobs) — any linear form is ~30× larger at shallow depth than ourd²and the consumers (hist_prune_coeff,lmr_hist_div) were tuned for the small scale. Shipped instead:bonus = min(quad·d²/64 + lin·d, max)+ mirrored malus knobs +hist_ttmove_bonus(SF-only, default 0) — 7 knobs, defaults reproduce the legacy formula EXACTLY (bench identical, 9/9 CTest, no SPRT owed). The asymmetric-linear shape both references prove is reachable by the 6.9 SPSA, tuned jointly with the consumers (HistPruneCoeffrange widened [1000, 28000]). 6.1 mate guard merged alongside (correctness fix): dev-head bench fingerprint now 12,736,941 (was 13,503,085 — the guard's footprint concentrates in the suite's mate positions); guard validation rides in the 6.10 cumulative SPRT. — Fable 5 medium - 6.4 Post-LMR deeper re-search + conthist + double-ext cap — DONE 2026-07-01 as EXPOSURE ONLY (commit
945ec8c; real values deferred to 6.9). Hit the SAME canary fragility as 6.3, twice: (1) thedo_deeper/do_shallowerdepth adjustment — swept flat margins 40/80/150/300/600/1200 against the KBNK CTest, non-monotonic (only 300 passed) with no natural "off" point in a sane range → dropped entirely; (2) the post-LMR conthist nudge (Weiss form) at full weight broke the KQK mate-in-5 CTest → shipped behind a newPostLmrHistScaleknob (percent, default 0, provably inert —hist_updatewith bonus=0 is a no-op); (3) the double-ext cap — Weiss's own seed (≤5) also independently broke KBNK → shipped as real infra (ss->double_exts, propagated viaSearchStack) defaulting to 200, mathematically unreachable (double_exts ≤ MAX_PLY=128). Bench 12,736,941 unchanged (provable), 9/9 CTest (incl. both canaries). Extractedhistory_bonus_value/malus_value/update_cont_for_moveas shared helpers (pure refactor). No SPRT owed. RealPostLmrHistScale/DoubleExtMaxvalues are 6.9 SPSA material, jointly withLmrHistDiv/HistPruneCoeff. — Sonnet 5 medium - [REJECTED] 6.5 SEE-quiet + capture futility — DONE 2026-07-01, both EXPOSED-BUT-INERT (
c3f1adc→3d14867). Capture futility (eval + 200 + 200·lmr_depth + PieceValue[captured] + capHist/32 ≤ alpha) was canary-clean so it shipped active (bench 12,736,941 → 11,806,504) and got a real SPRT vs the 6.4 head: −2.78 ± 7.50 Elo, LOS 23%, LLR → H0 over 3.6k games — wash-to-tiny-loss → REVERTED to inert (CapFutDepth=0). SEE-quiet never went active: naive base-table-lmr_depthbroke KBNK completely (no mate in 250 plies, every coeff 25–120 failed; SF'slmr_depthhere includes the history term that protects precise endgame quiets) → inert (QuietSeeDepth=0). Net: no Elo, but both wired + exposed for 6.9 SPSA (cap_fut_depth~7 + margins;quiet_see_depth~8 after the history-awarelmr_depthfix). Dev stays at validated 6.4 head (12,736,941, 9/9 CTest). Lesson: canary-clean ≠ Elo gain — the games are the arbiter. — Opus 4.8 high - [SKIPPED] 6.6 Fail-low prior countermove bonus — skipped 2026-07-02 (user decision). Weakest evidence in the wave (SF-only) and, after 6.2–6.5's run of rejections/washes/inert-landings, decided to go straight to 6.8 rather than spend another implement/CTest/SPRT cycle on it. Revisit only if 6.9 shows spare headroom. — Sonnet 5 medium
- 6.7 Fractional LMR (1024ths) +
lmr_tt_capture— DONE 2026-07-02, behaviour-identical infra (commit5daae1c).lmr_table_→ 1024ths (int(1024·(base+ln·ln/div)), floor identity keeps base reduction identical); the 4lmr_*_adjknobs → 1024ths (defaults ×1024, ranges 0..3072); reduction accumulated in 1024ths,>>10at the end; newlmr_tt_captureseeded 0 (inert). History kept integer-quantised so it's behaviour-identical (no SPRT) rather than the spec's fractional-history rounding step (which the 6.2–6.5 run showed would just wash). Bench 12,736,941 unchanged, 9/9 CTest; verified live in a TUNE build (LmrNonPvAdj=1536gives a node count distinct from 1024 and 2048 — sub-ply control). Gives 6.9 five fractional LMR dims +lmr_tt_capture. — Opus 4.8 medium - 6.8 Qsearch quiet checks — DONE 2026-07-02, exposed-but-inert (commit
62648ca). Atqply==0, after captures fail to cut off: try quiet checking moves (gen_quiet_checks) filtered by SEE≥0, capped atQsearchCheckCap. The seeded default (6, "4–6" per spec) broke the KBNK mate CTest — the sixth mechanism in a row (6.2–6.5) to trip this canary at its literature seed. Gated behindQsearchCheckCapdefaulting to 0, provably inert (loop skipped whencap ≤ 0). Bench 12,736,941 unchanged, 9/9 CTest; confirmed live in a TUNE build. No SPRT owed. Real value → 6.9. Phase 6's feature list is now complete (6.1–6.8 resolved: one gain, one rejection, six exposure-only). — Sonnet 5 medium - [MOVED → 7.4] 6.9 wave2 joint SPSA — infrastructure built (commit
74bf5f0), the RUN relocated by the 2026-07-02 EV review (triggered by a 30h negative-gain Rarog SPSA). Why: (1) family SPSA base rate at maturity = 0-for-2 (5.8 TM wash; Rarog negative); (2)config_wave2.json's start point activates known-negative mechanisms (CapFutDepth=7= the exact −2.78 config;QuietSeeDepth=6= the KBNK-breaking mechanism) — a local optimizer starting 5–15 Elo down a hole in 24 noisy dims; (3) §0.5's own rule: search constants are cp-denominated → tune once, at the FINAL eval scale, and Phase 7 is about to refit the eval. Replacement =config_histshape.json(11 always-live dims from current defaults) run as Step 7.4 after the eval cycles. wave2 config +phase69-wave2basebinary remain valid for a possible evidence-based wave3. — Sonnet 5 medium - 6.10 Phase-6 boundary validation — ✅ DONE 2026-07-04, +9.14 ± 4.62 Elo (H1). Cumulative SPRT
phase69-wave2basevsphase5final(elo1=3, 3+0.03): +9.14 ± 4.62, LOS 99.99%, 9,810 games, Ptnml [223,1149,1953,1307,273]. Validated the whole Phase-6 bundle (6.1 +7.18 + the un-SPRT'd mate guard + the inert 6.3–6.8 exposures). +9.14 ≥ 6.1's +7.18 → the mate guard is vindicated (its only behaviour change beyond 6.1; neutral-to-slightly-positive, no bisect needed). PHASE 6 COMPLETE. Cross-TC riders skipped (TC over-fit implausible; re-check only if 7.5's gauntlet flags it). Release deferred to 7.5 (1.8.0 = Phases 5+6+7). — Sonnet 5 low - [SUPERSEDED — old pre-rework acceptance stubs, no open work] These are the original Phase-6 acceptance-criteria stubs from before the 2026-07-02 EV-review renumbering; every one is covered by the reworked 6.3–6.10 entries above, so they are resolved / not separately necessary:
- old "6.5 Post-LMR deeper retry" + old "6.7 Double-extension cap" → folded into reworked 6.4 (exposure-only:
do_deeper/do_shallowerdropped as canary-non-monotonic;DoubleExtMax=200shipped provably inert). - old "6.6 Qsearch quiet checks" → reworked 6.8 (exposed-but-inert,
QsearchCheckCap=0). - old "6.8 Razoring restriction experiment" → not pursued — it wasn't on the SF/Ethereal/Weiss audit shortlist and its EV was ≈0; revisit only on new evidence.
- old "6.9 Second-wave SPSA" → relocated to Step 7.4 (the
config_histshape.jsonrun) by the EV review; see reworked 6.9[MOVED → 7.4]above.
- old "6.5 Post-LMR deeper retry" + old "6.7 Double-extension cap" → folded into reworked 6.4 (exposure-only:
Phase 7 - Non-NNUE ceiling: eval-refresh cycles + the one search tune (PLAN.md §7.5; THE ACTIVE PHASE after 6.10 — reworked 2026-07-02 EV review, absorbs the relocated 6.9 SPSA as 7.4)
The EV table (decision aid — every step individually skippable; gains overlap):
Step Expected Elo (range / median) Cost Wash risk Model 7.1 SF-distill refit ✅ DELIVERED +6.75 ± 4.07 (est. +10…+35/~+20) fit = minutes; 1 SPRT 2h48 scale drift did NOT materialize Fable 5 med (alt Opus 4.8 med) pipeline; Sonnet 5 med fit 7.2 self-play refresh ✅ DELIVERED +21.02 ± 7.73 (est. +5…+25/~+12) datagen 45 min + fit + SPRT 49 min overperformed ~2× — 7.3 now high-EV Sonnet 5 med 7.3 cycle 2 ✅ DELIVERED +19.51 ± 7.04 (est. 0…+10/~+4 — under-called ~5×) same (~2h/cycle) curve not flat; iterate on (MSE-stall ≠ Elo-stall) Sonnet 5 med 7.4 histshape SPSA 0…+12 / ~+4 ~12–18h, abort @600 iters ~50% (5.8 wash + Rarog −30h precedent) Sonnet 5 med 7.5 boundary + 1.8.0 0 (banks the above) ~4–8h — Sonnet 5 low (Phase 8 menu) ~0 — demoted, audit found nothing — — skip (Phase 9 NNUE) +200…+400 months low Fable 5 high (alt Opus 4.8 high)
- 7.0 Non-NNUE ceiling analysis read & stop-point understood. SF11 (~3440 CCRL) is the clean classical proof a complete HCE reaches the high-3000s; the 2026-07-01 SF/HCE audit (vs Ethereal 14.40 + Weiss 2025 + SF-classical) confirmed Basilisk's eval is feature-complete pre-NNUE — so Phase 7 is a label/data-quality grind, not new terms. Don't size off NNUE-era "HCE" ratings (Berserk/Rubi/Stash 3300+ are their NNUE builds, classical ~3000–3150). King-bucketed PSTs = NNUE input shape → do Phase 9 instead, not more HCE. — read-first
- 7.1 SF-distillation bootstrap — ✅ DONE 2026-07-04, ACCEPTED +6.75 ± 4.07 Elo (H1, LOS 99.94%, 16,066 games, Ptnml [623,1865,2834,1999,712]; SPRT
7.1-sfdistillvs6.10-head,elo1=3@ 3+0.03). Merged to development (b8c469b), bench 12,736,941 → 13,544,942. Re-centered the HCE against Stockfish @60k nodes (Hydra'sannotate_sf.pyon the quiet-filtered beast set), convertingFEN;cp→FEN;wdlvia1/(1+10^(-cp/400)). Fitbasilisk-texel --tune allon 2.0M/100k, L2=1e-6, K=1.372: holdout MSE 0.02226→0.02134, data phase-balanced 47/30/22 (vs v17's 59% endgame). The eval-scale risk did NOT materialize — the L2-to-prior anchor pinned material (Pawn 83/96, Knight 322/310, Rook 514/561, Queen 1085/998) and kept startpos at +50cp@d12; 55 eval lines nudged toward SF (tempo 15→20, rook_open 25→27, PSTs, imb/mob/threats ±1-2). 9/9 CTest — KBNK/KQK canary clean; one shallow-depth sanity bound widened 100→150cp (distilled eval reads +122@d5). KS funnel re-fit (--tune-kingsafety) DEFERRED as a follow-up rider (finite-diff too slow to finish inline; isolating the distillation gave a cleaner verdict) — rolls into 7.2's--tune-kingsafetyon fresh data or runs standalone first. The10+0.1boundary gauntlet rolls into 7.5. — Fable 5 medium (alt: Opus 4.8 medium) pipeline; Sonnet 5 medium fit - 7.2 On-policy self-play refresh cycle — ✅ DONE 2026-07-06, ACCEPTED +21.02 ± 7.73 Elo (H1, LOS 100%, 4,618 games, Ptnml [165,495,795,604,250]; SPRT
7.2-selfplayvs7.1-head,elo1=3@ 3+0.03). Merged to development (d113471), bench 13,544,942 → 14,095,183. Biggest single-step gain since the Phase-4 eval campaign — ~2× the ~+12 EV midpoint; proves the HCE eval was not near its ceiling and the self-play curve is still climbing (→ 7.3 is high-EV). Joint refit on 968,771 unique on-policy positions (200k games from the 7.1 head seeded bybeast_seed.epd;--balance-phase 1.5→ 25/37/37 from natural 9/34/55).--tune all --l2 1e-6(K=2.5) + the KS funnel re-fit deferred from 7.1 (sequential joint bake on the tune-all baseline). Eval moved substantively (queen_infiltration 5→8, minor_king_ring 1→5, safety_table tail 296→342/351, ks_central_king 0→3; material pinned). 9/9 CTest — KQK/KBNK canaries clean. Historical plan/pipeline notes retained below. Must follow 7.1: self-play WDL optimizes Basilisk winning from positions its own search reaches, correcting distillation's off-policy + representability bias. Regen self-play with the strongest head (datagen.ps1→extract_parallel.py), one joint low-lr refit (--tune all+--tune-kingsafety), SPRT + gauntlet — NOT a re-stage. Fold in: phase-rebalance (v17 was 59% endgame), quiet-filter (extract.py already skips in-check/capture plies), optional blended labelsλ·WDL+(1−λ)·σ(Basilisk-own score); ride-along re-fits: deferred nonlinear KS re-fit (now on correct data), sigmoid-K, tempo, lazy-margin, shelter/storm→danger fold. Expected ~+10–40 Elo.⚠️ DATAGEN NEEDS THE DIVERSE BOOK. A first attempt (2026-07-05) withdatagen.ps1's defaultSuperGM_4mvs.pgnbook collapsed 200k games → 31,880 unique positions (fixed-node self-play from a tiny book replays identical games). The proven fix (PLAN.md §637–664, anddatagen.ps1's own book-size warning) is to seed fromtools/texel/data/beast_seed.epd(100k diverse openings from the Beast corpus) — Phase-2 got 1.73M unique from 200k games that way. Aside (2026-07-05): reusing Hydra's readybeast_train.csv(WDL on the same 2M positions 7.1 already distilled) was tried as a shortcut and abandoned — off-policy, the eval barely moved, and it slightly degraded KQK (mate 5→6); the null result confirmed on-policy self-play is the only productive 7.2. Concrete pipeline (this cycle): (A)datagen.ps1 -Suffix phase71-sfdistill -Book tools\texel\data\beast_seed.epd -BookFormat epd -Rounds 100000 -Nodes 8000 -OutputPgn ...\selfplay_72_beastseed.pgn; (B)extract_parallel.py ... --balance-phase 1.5(float R = cap each bucket to R× the smallest; the parallel extractor now matchesextract.py's flag exactly, commit 4b399b4); (C)--tune all --l2 1e-6; (D)--tune-kingsafety --max-positions 100000(~8 min — the finite-diff path times out at 600k, calibrated 2026-07-05); (E) bake both (--allow-pstfor the eval dump) →build_test.ps1 -Suffix phase72-selfplay; (F) SPRTelo1=3vsbasilisk-phase71-sfdistill. — Sonnet 5 medium - [~] 7.3 Iterate (further self-play cycles) + stop — ▶ ITERATING, curve NOT flat. Cycle 1 ACCEPTED +19.51 ± 7.04 Elo (H1, LOS 100%, 4,440 games, Ptnml [91,505,847,618,159]; SPRT
7.3-selfplayvs7.2-head), merged9741070, bench 14,095,183 → 13,495,492 — ~as much as 7.2's +21, so cycle-4 datagen is running. Repeat 7.2 with the now-stronger head; stop when a cycle yields<~+8 Elo.⚠️ The stop signal is the SPRT, NOT the holdout-MSE stall — cycle 1's linear eval barely moved (holdout 0.0797→0.0793, all movers ±1/±2) yet gained +19.5; the KS funnel keeps re-shaping each cycle (safety_table tail 351→373). Each accepted cycle is releasable. Sustained flattening = pure-HCE tuning spent → Phase 9 (NNUE). — Sonnet 5 medium - 7.4 histshape SPSA — the ONE search tune, at the final eval scale (relocated 6.9). Only after 7.1–7.3 settle.
config_histshape.json= 11 always-live dims (6.3 history-shape 7 +LmrHistDiv+HistPruneCoeff+LmrCutNodeAdj+LmrTtCapture) starting from current defaults (iteration 1 = the validated head — unlike the superseded wave2, whose start activated known-negative mechanisms). Pre-registered rules: 1000–1500 iters (~12–18h) not 5000; abort at ~600 if the trend ≤ 0 and keep defaults; converged candidate → bake → PGO build → SPRT vs pre-tune head AND fullctest; a wash → keep defaults, done tuning. Gated 6.4/6.5/6.8 mechanisms stay inert unless an evidence-based wave3 case appears../tools/build_test.ps1 -Suffix phase74-histbase→./tools/setup_spsa.ps1 -ConfigGroup histshape -EngineSuffix phase74-histbase -Iterations 1500→cd tools/weather-factory && python main.py. — Sonnet 5 medium - 7.5 Boundary validation + release 1.8.0 — ✅ DONE 2026-07-08. Validation was an LTC field gauntlet in colosseum (
10+0.1, 12-engine field) + an independent LittleBlitzer cross-check, not the originally-planned cumulative SPRT + cross-TC riders (superseded by the direct gauntlet). 1.8.0-rc beats 1.7.0 head-to-head ~+40 Elo at LTC (the +93 fast-TC gain compresses to ~+40 at long TC — normal for eval refits; both tools agreed directionally). The absolute colosseum field-estimate (2939, Δ−111) is not a regression — it's anchored on strong fixed opponent ratings; the within-gauntlet 1.7.0 head-to-head is the trustworthy number. 7.4 histshape SPSA skipped (low EV). 1.8.0 = Phases 5+6+7 bundled, released from the cycle-5 head (bench 12,661,251). — Sonnet 5 low
- Phase 8 feature menu — demoted 2026-07-02 (EV ≈ 0; the SF/Ethereal/Weiss audit found no missing features pre-NNUE). Skip to Phase 9 (NNUE, +200–400, Fable 5 high / alt Opus 4.8 high) unless a specific gauntlet weakness points at a listed item.
- Phase 9 NNUE remains the terminal option; eval boundary stays clean.
After each phase — especially after Phase 4 (eval), which over-fits self-play
most — run tools/gauntlet.ps1 at tc=10+0.1. Recommended opponents (pick by
measured score): Basilisk 1.5.0 (phase1-final) + Rarog; Critter 1.6a
(~3150–3200, a target to clear); Stockfish capped via UCI_LimitStrength,
start 2900 → 3100 → 3300 (Basilisk is already ~3100, so start high) at the
level where Basilisk scores ~30–70%; one independent mid/high HCE
(Lambergar / Peacekeeper / Igel). Time budget on the 5950X: datagen/SPSA at
-Concurrency 24; Texel fits are CPU-minutes — run them freely.
Last released: 1.6.0 (Phase 2 eval scalars + time-forfeit fix). 1.7.0
prepared 2026-06-29 (Phase 4 eval data-fit campaign; version/CHANGELOG/README
done, awaiting the maintainer's tag). When to tag the next ones (full rationale +
gates in PLAN.md §10):
| Tag after | What ships (cumulative since last tag) | Version |
|---|---|---|
| Phase 2.9 | Phase 2 eval scalars (+54 vs 1.5.0) + time-forfeit fix | 1.6.0 |
| Phase 3 | bench-identical only (except 3.5 endgame knowledge — behaviour-changing, SPRT-gated) — nothing to release | — |
| Phase 4 | full eval data-fit campaign (+280.74 vs phase1-final) | 1.7.0 (shipped) |
| Phases 5+6+7 | TM ( |
1.8.0 (unreleased; ships at Step 7.5) |
| Phase 8 | demoted (EV ≈ 0) — only if a gauntlet weakness surfaces | patch per batch |
| Phase 9 | NNUE (+200–400) | 2.0.0 |
Rules: minor bump (1.Y.0) for each phase that banks SPRT+gauntlet-validated
strength; patch (1.y.Z) for fix-only releases; major (2.0.0) for the
NNUE architecture swap. The version tracks everything new since the last tag,
not the last phase — that's why the post-2.9 release is 1.6.0 (it ships all of
Phase 2's eval gains), even though Step 2.9 itself is just a fix.
Released as
1.6.0, not1.5.1.1.5.1(a patch) would have undersold the biggest eval gain so far. The release was gated on the time-safety fix (Phase 5 Step 5.1/5.1b, old labels 6.1/6.1b) clearingt=≈0 and the +54 holding vsphase1-final— the forfeit fix is what made the eval gains releasable.
To prepare a release, say: "Release 1.6.0." The model runs the checklist
in PLAN.md §10 — bumps the version in both src/Constants.h and
CMakeLists.txt, updates CHANGELOG.md (README no longer carries per-version
content), verifies the non-tune release build hides the tuning options +
passes CTest, builds the dist binaries (the full platform matrix comes from
.github/workflows/release.yml on tag push), and commits. It will not tag
and will not push — those are yours. Create the tag and push whenever you're
actually ready to cut the release:
git tag -a v1.6.0 -m "Version 1.6.0"
git push origin <branch>
git push origin v1.6.0It will refuse to even commit if the version's pre-release gate (SPRT/gauntlet) hasn't passed — so settle that first.
# Build a named pext-PGO+TUNE binary
.\tools\build_test.ps1 -Suffix <name>
# SPRT a gain candidate (default tc=3+0.03 clock)
.\tools\sprt.ps1 `
-EngineA tools\test_engines\basilisk-<candidate>-pext-pgo.exe `
-EngineB tools\test_engines\basilisk-<baseline>-pext-pgo.exe `
-NameA "Candidate" -NameB "Baseline"
# SPRT a small/tighter candidate
.\tools\sprt.ps1 `
-EngineA tools\test_engines\basilisk-<candidate>-pext-pgo.exe `
-EngineB tools\test_engines\basilisk-<baseline>-pext-pgo.exe `
-NameA "Candidate" -NameB "Baseline" -Elo1 3
# LTC confirmation / TC-suspect candidate
.\tools\sprt.ps1 `
-EngineA tools\test_engines\basilisk-<candidate>-pext-pgo.exe `
-EngineB tools\test_engines\basilisk-<baseline>-pext-pgo.exe `
-NameA "Candidate" -NameB "Baseline" -TC "10+0.1" -Elo1 3
# Optional old fixed-movetime sanity gauntlet
.\tools\sprt.ps1 `
-EngineA tools\test_engines\basilisk-<candidate>-pext-pgo.exe `
-EngineB tools\test_engines\basilisk-<baseline>-pext-pgo.exe `
-NameA "Candidate" -NameB "Baseline" -MoveTime 0.1
# Refactor/default-equivalence SPRT, only when needed
.\tools\sprt.ps1 `
-EngineA tools\test_engines\basilisk-refactor-pext-pgo.exe `
-EngineB tools\test_engines\basilisk-baseline-pext-pgo.exe `
-NameA "Refactor" -NameB "Baseline" -Elo0 -3 -Elo1 3
# Phase-boundary fixed-game validation
.\tools\gauntlet.ps1 `
-Engine tools\test_engines\basilisk-phase1-final-pext-pgo.exe `
-Opponents tools\test_engines\basilisk-phase1-defaults-pext-pgo.exe `
-Name Phase1Final `
-Games 2000
# Configure SPSA
.\tools\setup_spsa.ps1 -ConfigGroup pruning -Iterations 5000
.\tools\setup_spsa.ps1 -ConfigGroup lmr -Iterations 5000
.\tools\setup_spsa.ps1 -ConfigGroup combined -EngineSuffix phase1-lmr -Iterations 2000
# Run/resume SPSA
cd tools\weather-factory
python main.pyBench is best run interactively:
.\build\release-pext\basilisk.exe
bench 13
quit
weather-factory can only perturb the engine through UCI. For example, it sends:
setoption name RfpCoeff value 128
That is why search constants are UCI options in tune builds. They are hidden in
normal release builds through the BASILISK_TUNE compile definition.
Before any public release, verify a non-tune build does not expose the tuning option list.
- Do not accept a tuned value set without SPRT.
- Do not interpret lower or higher node count as strength.
- Do not bundle feature work with tuning defaults.
- Do not skip fixed-game validation at phase boundaries.
- Do not start Phase 3 features while Phase 1 or Phase 2 still has obvious tuning work left.
- Keep
Evaluator::evaluate(const Board&)as the boundary between search and evaluation.
The process is the strength engine here: tune, test, keep only what survives.