Skip to content

Releases

v475 — The Browser pane stops lying, the node card stops leaking, and the bump stays together

Choose a tag to compare

@MBHudson MBHudson released this 15 Nov 09:00

The Browser pane stops lying, the node card stops leaking, and the bump stays together

The Browser pane. Its "Browser not running — Press Launch above" panel is
.bs-empty { display: flex }, and an author display outranks the user agent's
[hidden] { display: none } — so once the pane had shown that panel it painted
over a live page for good. It has its own guard now and computes its
visibility from the pane's real state before _paintStatus returns early. The
pane also fills the screen properly on a phone (100dvh, safe-area padding,
chrome as a column, 62dvh sheet), the loader has a watchdog instead of spinning
forever, and there is a full-screen button: real requestFullscreen, a
bs-immersive fallback where that is refused, Escape peeling one layer at a
time, and closing the pane leaving full screen behind it.

The same trap, found twice. Auditing every hidden attribute against every
CSS rule that sets a display found the notification badge wearing an empty red
dot at zero notifications. tests/php/hidden.test.php keeps both from returning:
it joins the two sets (reading markup too, because .bs-empty is toggled as
empty.hidden far from the tag that made it, and bridging an id to the classes
on the same tag) and fails when an element the JS hides has no [hidden] guard.

The node card. It was never translucent. The floating floor (150vmax,
mix-blend-mode: screen) is a sibling of .topology-container, which isolates
the map, so it painted over the whole map subtree — the card included; its
background measured an opaque rgb(7,11,19) throughout. The floor now lives
inside the map's stacking context, above the canvas and below the card, and the
Desktop 2026 card states its solid background explicitly.

The bump stays together. tests/php/version.test.php reads VERSION and
APP_VERSION through the app's own Update::versionPairIn, then checks both
appVersion badges, both <html data-build="…"> attributes, every ui/ page
that carries a build number, and every ?v= under ui/** and both
index.html files — and fails if VERSION falls behind. Running it the first
time surfaced <html data-build="143"> in both pages: langs.js appends that
attribute to the Prism vendor scripts it injects, so every Prism file had been
fetched as ?v=143 through four releases. It reads 474 now and is the fourth
place RELEASING.md names.

VERSION 470 → 475. APP_VERSION 470 → 474 — no cache-buster bump in this one,
because every browser file it ships is already at 474. JS 42 files / 1,413
assertions, PHP 37 files / 37 passed (3 skipped), docs 14/14.

Updating: Config → Updates → DOWNLOAD UPDATE… — nothing installs itself.

v470 — The drill leads with this pass's file

Choose a tag to compare

@MBHudson MBHudson released this 14 Nov 23:46

What changed

The pass file is the first thing the drill says. A model kept rebuilding its todo list from the previous pass and copying the old path along with it, so pass 2 wrote pass 1's file. The path was stated midway down the drill's rules, after the step list, where a model following its own remembered plan never looked.

trainingBlockFor now opens with it, before the steps:

[TRAINING MODE — paced drill, ~50% request rate. …]
YOUR FILE FOR THIS PASS: training/advanced-agent-…-p1.txt — every file step reads and
writes exactly that path. A path from an earlier pass, or one copied out of your own
todo list, is the WRONG file.

The per-turn reminder also leads now: instead of appending [Training drill file for you: …] behind the operator's message, it opens with the path — it is the last thing a turn reads before acting. Desktop, browser and custom drills get no file line, because they are not file drills.

Checks

From the request the model actually received: the new leading line appeared in 12 of 12 requests of a live Advanced run, and pass 2 wrote …-p2.txt on its own first attempt with zero refusals in the log — no correction needed, unlike the run before this change.

JS 42 files / 1,413 assertions, PHP 35 files / 35 passed (3 skipped on this host), docs 14/14.

VERSION 469 → 470, APP_VERSION 469 → 470, every ?v= under ui/**, and both index.html files.

v469 — Every pass writes its own file

Choose a tag to compare

@MBHudson MBHudson released this 14 Nov 23:39

What changed

Each pass writes its own file, and can no longer borrow the last one. A drill graded the file name loosely: the write step accepted any pass's file, so a second pass that rebuilt its todo list from the first — path and all — wrote …-p1.txt again, overwrote pass 1's evidence, and still counted as clean. v467 made the report tell the truth about that; this release stops it happening.

Grading follows the pass counter. trainingFileCheckFor maps a pass number to that pass's file while the history is walked, so a call in pass 2 must name the pass-2 file. The old matcher that accepted any generation is gone, and a multi-pass history still grades correctly — pass 1 against p1, pass 2 against p2, rather than the whole history against the newest name.

The write is refused before the tool runs. Grading alone only notices after the file has been overwritten. drillWriteRefusal() sits in the tool loop for write_file, edit_file and append_file while training is on: a path inside this run's own drill family that is not the current pass's file comes back as an ordinary tool error naming the right file, so the model corrects itself next step and nothing is written. Reading an earlier pass's file is still allowed — that is how evidence is inspected — and paths outside the run's drill files are untouched.

Checks

Verified on a live Advanced run, not just the suites. The writes came back in this order:

…-p1.txt = ok
…-p1.txt = error  "Error: drill pass 2 writes training/…-p2.txt, not training/…-p1.txt …"
…-p2.txt = ok

and on disk pass 1's file kept its original timestamp (382 bytes, unchanged) while pass 2's created its own — the evidence each pass produced survived.

JS 42 files / 1,409 assertions, PHP 35 files / 35 passed (3 skipped on this host), docs 14/14.

VERSION 467 → 469, APP_VERSION 467 → 469, every ?v= under ui/**, and both index.html files.

v467 — The report names the file each pass wrote

Choose a tag to compare

@MBHudson MBHudson released this 14 Nov 23:29

What changed

Each pass is reported against the file it actually wrote. A live Advanced run finished 8/8 clean and its report listed pass 2 against …-p2.txt — a file nothing wrote. The tally was right: pass 2 had really run, but it rebuilt its todo list from pass 1's (path and all), so it wrote …-p1.txt a second time. The write step matches any pass file on purpose — a history spanning several passes would otherwise re-judge pass 1 against pass 2's name — so the pass was graded on its records, and only the name in the report was wrong: it came from the pass counter, which had already moved on. drillPassFile() now reads the last write_file path out of the run's records, so every pass is filed under the file an operator can open. Drills with no write step (Frontier, Browser, Custom) keep the expected-path fallback.

The report lists every pass it counts. The verdict signal that ends a run at its clean-pass target also builds the final report, and it fired before the finishing pass had been logged — so the modal counted two passes and listed one line. That signal is now announced last in the publish, after the pass is logged and the counter has moved.

Checks

Verified against a live Advanced run on a real model (nemotron-3-ultra via OpenRouter), not just the suites: the completion report now reads 2 passes · 2 failed attempts · 1 clean with two pass lines, both naming …-p1.txt — the file each pass wrote — and pass 2's failed step 7 quotes the real error.

JS 42 files / 1,400 assertions, PHP 35 files / 35 passed (3 skipped on this host), docs 14/14.

VERSION 466 → 467, APP_VERSION 463 → 467, every ?v= under ui/**, and both index.html files.

v466 — The Advanced drill owns its page

Choose a tag to compare

@MBHudson MBHudson released this 14 Nov 23:02

What changed

v466 — the Advanced drill owns its page. The Advanced steps used to fetch https://example.com/, and step 4 required the literal phrase "Example Domain" from it. example.com reworded its page, so a model that fetched it and saved the text correctly was graded 3/8 · 4 failed at step 4 and rewrote a good file until it gave up. Step 3 now fetches http://drill.local/advanced.html, served from disk by the app (ui/drill/advanced.html): no DNS, no socket, so it always answers — and it has to work that way, because fetching this app's own origin would deadlock under the single-threaded PHP server while it is answering the request that asked for it. assertPublicUrl is untouched: only a flat *.html name in ui/drill/ is answered, and loopback, LAN and metadata addresses are still refused. The page carries DRILL-PAGE-v1, which the write step and the read-back grade, and tests/php/drillPage.test.php fails if the page and the grader ever disagree about it.

The other drills were audited the same way. Frontier's typing step no longer pins PHP's exact sentence (it accepts any read-back that is not reported as unverified), and Hard, Easy and Browser grade the drill's own file, the strings the drill itself teaches, or the app's own local page — nothing whose wording lives outside this repo.

v465 — Advanced tracks the page it grades. Before the fixture moved in-house, the checks accepted every wording example.com has served and graded the saved content itself (at least 40 characters of page text) rather than a phrase in the arguments, while still refusing a placeholder save.

v464 — the two-pass check from a terminal. php tools/drill-selftest.php runs the self-test without a browser, and a failed pass now reports the first error it hit, at the step it happened on, instead of only counting it.

v463 — RUN TWO-PASS SELF-TEST in Config → Permissions. The run report lists each pass with the file it wrote.

v462 — every training pass writes its own drill file (…-p1.txt, …-p2.txt), so a second pass never starts on the first pass's bytes. The old training/ folder is renamed aside at Start rather than deleted, so the evidence of an earlier run survives.

Checks

JS 42 files / 1,395 assertions, PHP 35 files / 35 passed (3 skipped on this host: no symlink, no git), docs 14/14.

VERSION 461 → 466, APP_VERSION 461 → 463, every ?v= under ui/**, and both index.html files.

v461 — the Browser drill opens a window you can actually watch

Choose a tag to compare

@MBHudson MBHudson released this 14 Nov 04:09

The Browser drill opens a window you can actually watch

browser_open now shows a visible Chrome/Edge window by default; "headless":true is the opt-in exception. The Browser drill's step 3 refuses a headless result and tells the model to retry with "headless":false; the Browser screen's checkbox follows the running mode, and switching to visible restarts a headless instance cleanly.

window_open now brings the named program to the front, and window_type reads the window back through Windows UI Automation so its result says whether the text actually landed. The Frontier drill requires that read-back, and the live strip names the call to retry when it is unverified.

Verified live with NEMOTRON-3-ULTRA: Browser 9/9, one clean pass, zero failed attempts, in an unobstructed Edge window. The earlier headless run was not counted.

The head-to-head teaser is in the README and the Pages site.

Tests: JS 42 files / 1,360 assertions · PHP 33/33 (3 skipped) · docs 14/14.

VERSION 460 → 461, APP_VERSION 457 → 458.

v460 — read/edit calls keep their arguments, and the drill names its tools

Choose a tag to compare

@MBHudson MBHudson released this 13 Nov 07:59

Foreign read and edit calls used to lose the arguments that make them work. read_file and show_file now accept the path spellings Claude, Cursor and OpenAI-compatible tools use (file_path, filepath, filename, target_file, file), keep a separate range, and turn offset/limit into the sandbox's start-end range. Edit resolves to edit_file — never write_file — and edit_file accepts file_path plus old_str/old_text/new_str/new_text. Native read_file now advertises the separate range field, and nativeToolArgs keeps it; stringified JSON arguments are parsed before path normalization, which is the shape a native or relay call can send. The Hard drill now names the exact tool in every read and edit step (read_file, never show_file; edit_file, never write_file or str_replace_editor), while Easy intentionally keeps its original diagnostic wording. Verified live with nemotron-lightning: two Hard passes, each 17/17 clean with 0 failed attempts and 0 retries; a native ranged read returned line 1; and the stringified read/edit path ran through the live tool registry. JS 41 files / 1,229 assertions, PHP 33/33 (3 skipped), docs 14/14. VERSION 459 → 460, APP_VERSION 448 → 449 in ui/js/store.js, every ?v= under ui/**, and both index.html files.

v459 — Easy or Hard, and every Start begins clean

Choose a tag to compare

@MBHudson MBHudson released this 13 Nov 05:30

Training mode now lets you choose the drill, and every run starts from a clean page.

Easy / Hard in the Training popup

Easy is the original 12-step sequence from 53b05a9, including the old “Append a second line” step 7 and the old rules. Hard is the current 17-step drill with the ranged read, anchored edits and display tools. Both keep per-agent files and the Continue loop; Easy’s terminal “and stop” became “this completes the 12-step pass,” because the harness now owns the ending.

The choice is saved as settings.trainingMode.difficulty (default Hard), locked while a run is active, and drives the step list, prompt block, kickoff, verdict total and strip (easy drill). An Easy run grades as 12/12, never 12/17.

A fresh session per Start

Start creates Training drill — <date/time>, aborts an in-flight turn directly without setting the one-shot STOP flag, switches to the empty session, then sends the kickoff. If session creation fails, it falls back to the current session.

Live comparison, one model

Mode Result Time Failed tool calls Continue resumes
Easy 12/12, clean ~62s 0 —
Hard 17/17, clean ~176s 0 4

Both runs used nemotron-lightning and ended with the same 37-byte training / drill 2 passed (exact match) file. The model reasoned around the old append wording, so this comparison did not produce a failure step: the visible difference was time and resume count, with Hard showing uncertain reasoning at step 16. Running a model that previously failed the old drill is the next useful test.

Verification

JS 41 files / 1,214 assertions, PHP 33/33 (3 platform skips), docs 14/14, update 132. VERSION 458 → 459, APP_VERSION 445 → 446.

v458 — two agents drill side by side

Choose a tag to compare

@MBHudson MBHudson released this 13 Nov 04:50

The live two-agent check on v457 found two shared-message defects that unit tests could not see.

The kickoff still showed the literal training/training-<agent>.txt, so models stopped to work out what <agent> meant instead of using the exact path in their own TRAINING MODE block. It now says "your drill file" and leaves the path to the block.

The automatic Continue listed every agent's position, and the second model read the first model's "pass complete (17/17)" as its own and tried to start at step 17. Each agent's recorded position now lives in its own block, and Continue is generic. Older transcripts that mention training/training.txt are explicitly marked obsolete.

Verified live in a fresh session with two Groq agents: both passed steps 5–9 concurrently, ending training/training-agent-…-ucy5su.txt and training/training-agent-drill-probe.txt at the same 37-byte training / drill 2 passed (exact match) content, written five seconds apart, with the old shared file untouched. Both read 14–15/17 with zero failed attempts when the check ended.

The PHP drill sequence now replays the reported three-line collision against real bytes and pins both the shared file's ambiguity and the per-agent files' success.

JS 41 files / 1,178 assertions, PHP 33/33 (sandbox 155), docs 14/14. VERSION 457 → 458, APP_VERSION 443 → 444.

v457 — every agent drills in its own file

Choose a tag to compare

@MBHudson MBHudson released this 13 Nov 04:17

Training mode drills every active agent in the same sandbox, and all of them wrote training/training.txt. Two passes therefore shared one piece of drill state: an agent that resumed at step 7 read another agent's completed output as its own step 6 result, its step 8 appended a second drill 2 passed line, and its step 9 — old_string: "drill 2 passed" — then matched both that line and the prefix of the (exact match) line another pass had written, so every retry failed with str_replace: old string appears 2 times.

Each agent now gets training/training-<agent id>.txt: the TRAINING MODE block names that exact path, the step labels are filled from it, and drillVerdict(calls, file) grades the pass against the same file, so another agent's work can neither satisfy a step nor corrupt one. A turn that was already in flight when Start was pressed gets the path appended to its steered kickoff. training/training.txt remains the default for callers with no agent.

The cache-buster bump exposed one test that had hardcoded ?v=442; contextWindow.test.mjs now reads APP_VERSION like every other suite.

JS 41 files / 1,173 assertions, PHP 33/33, docs 14/14. VERSION 456 → 457, APP_VERSION 442 → 443.