Free during the betaVersion 0.6.0macOS & Windowsproprietary licenceRead what to expect
All posts
20 min readCourtney Allen

Prompt to Page 0.6.0: a check that cannot pass should not run

MLX models could not start at all on Apple Silicon from 0.4.2 through 0.5.2 — the app looked for a filename the installer had stopped producing. A form page used to be generated twice, because the quality check flagged the back link the app itself adds. Both looked like your problem. Neither was.

MLX models could not start at all on Apple Silicon from 0.4.2 through 0.5.2 — the app looked for a filename the installer had stopped producing. A form page used to be generated twice, because the quality check flagged the back link the app itself adds. Both looked like your problem. Neither was.

Version 0.6.0 of Prompt to Page is out. It is a speed and correctness release for the first thing you ask of a page. On Apple Silicon, it is also the release that makes MLX models work again. Both the macOS DMG and the Windows MSI are on the v0.6.0 tag. Installed copies update in place.

If you tried an MLX model in the last three weeks and concluded something was wrong with your Mac, start there. Nothing was wrong with your Mac. If you did not, the sentence that changes your week is this: a form page that needs no corrective pass now arrives in about half the time — because the quality check was failing a back link the app itself puts on every page, and a score that cannot pass is a second generation you always pay for.

Those are the same defect in two costumes. A check the app made unsatisfiable, reported as your fault.

It wasn't your Mac

From 0.4.2 through 0.5.2, choosing any MLX model on Apple Silicon failed with "the bundled MLX Python runtime is missing." The message told you to reinstall. Reinstalling could never have helped. Every installer since 0.4.2 had the same layout. Nothing was missing.

The app checked for the bundled Python runtime under one filename — Python, the layout up to 0.4.1. The build had been producing another since 0.4.2 — libpython3.<N>.dylib. So it refused to launch a sidecar that was present and working. The 0.4.2 notes report a different MLX failure as fixed, and that is still true as far as it goes: 0.4.2 stopped the macOS 14/15 metallib crash. The same change introduced this one. I have not rewritten the 0.4.2 entry. This one names the range.

The check now accepts either filename, derived from the same rule the installer uses. The release build and CI both now ask the app itself whether it would start the bundled sidecar — a question that would have caught this the day it landed.

The llama.cpp backend, which is the default and the only one Windows uses, was never affected.

When an MLX model does start, it now receives the instructions every model in the catalog was measured against. It did not, before. Page generations routed to the managed MLX engine were grouped with the external backends — Ollama, LM Studio — and sent as a bare request. No system prompt. Every MLX catalog entry had its prompt profile scored on a shape production never sent. Both managed engines now get the curated instructions; Ollama and LM Studio keep the messages they expect. Qwen3-family MLX models also now get the same thinking suppression as their llama.cpp twins — on the MLX wire, thinking arrives as ordinary content and would otherwise eat the output budget.

Measured on the same page and model, Gemma 4 E4B Instruct MLX 4-bit, three runs: quality went from failing at 80 to passing at 85, and the prompt grew from 1,073 tokens to 2,033. That buys quality, not speed. On the packaged app, the same kind of form page on that MLX model took 102.5 seconds and still took a corrective pass. The observable that the instructions are on the wire is the size of the prompt — thousands of tokens, not the ~70 of a bare user message.

The check that could never pass

The quality rule is "no placeholder primary actions." It is a good rule. A Continue button that goes to #, a form whose action is empty, a stray formaction that goes nowhere — those should fail, and they still do.

What it also flagged was the back link the app itself adds to every page. On GOV.UK that class is govuk-back-link. Its href is # on purpose. The linker owns the destination — previous page, or the placeholder on a standalone page. The system prompt mandates that exact markup. So a compliant page could never pass, and a corrective pass ran on almost every run, paying for a second model round trip that had nothing to fix.

The pack's own back-link class is now exempt from that scan. Packs that declare no back-link class keep the unfiltered scan. Dead form actions still flag.

With that out of the way, the remaining trigger showed itself. The page guidance never said that inputs and a submit button belong inside a <form>, so the first turn emitted them loose and the corrective pass had to wrap them. Form-page contexts now state that contract up front.

Measured across two sittings on the bundled default model — Gemma 4 E4B Instruct Q4_K_M, managed llama.cpp, GOV.UK, a page asking for a name, a date of birth and a National Insurance number, five runs each sitting:

No corrective pass: 29.1 s, 30.8 s, then 30.5 s.

With one: 52.3–58.1 s.

About half the time, when the first turn is accepted. Pages are also now accepted on the first turn with the back link present, which the old rule made impossible. I saw that on sitting 1, runs 3 and 5, and on sitting 2 every page carried exactly one govuk-back-link.

The corrective pass is not gone, and it is not rare. Sitting 1 repaired 3 of 5. Sitting 2 repaired 4 of 5. This release removes a cause that could never be satisfied. It does not make the second generation unusual.

A score that prefers the empty page

When the app decides a generated page needs one corrective pass, that pass is sent the page plus the list of things to fix. On a long page in a dense design system, that can leave the model almost no room to reply inside its context window. It then returns a fragment, and the fragment used to replace the complete page, presented as a success.

I watched this happen. A Scottish Government Blue Badge page, 6,172 bytes, replaced by a 150-byte breadcrumb stump — one empty list item — on a normal success card. The repair had "succeeded." It had 38 tokens of room.

Deciding this on the quality score would have made it worse. The score is 100 minus per-defect penalties, with no completeness term. A near-empty page has almost nothing left to be wrong with, so it scores higher than the full page it replaced. An error-count comparison fails the same way: a genuine repair that adds a form, a label and a group raises the error count because there is finally markup to check.

The guard is a size floor. A corrective pass that shrinks the page below a quarter of what the first attempt produced is rejected, and the original is kept. It cannot be satisfied by emitting less, and it cannot block a repair that actually repairs.

I did not see that rejection fire on the sitting that published. I saw the collapse once, on the build that still accepted it. Seven later attempts on the published build never produced the stump. The guard is in the code and in the tests. It is not a packaged-app observation from the sitting that shipped.

Checks that wait their turn

Accessibility checks had two scheduling bugs and one timing one.

Pressing Check while the automatic check was still waiting out its delay ran two scans of the same page. Separately, the order in which page switching cleared results and armed the next scan meant the new page's automatic scan was cancelled the instant it was scheduled, so switching page never produced a check at all. Both are fixed. Navigating away mid-scan now abandons the request in a few milliseconds instead of leaving it waiting out a fifteen-second timeout.

The scan engine holds the main thread for as long as it runs. An automatic check beginning inside a burst of typing added the rest of its run to that keystroke. Measured in the Chrome harness, not the packaged WebView: 43–47 ms to paint on a 40-field page, 96–107 ms on a 100-field page, against about 17 ms when idle. Automatic checks now wait for an idle moment after their existing delay, with a two-second ceiling, so a scan is deferred but never dropped. Checks you ask for yourself still run immediately — the wait is the result you are waiting for. A keystroke that lands in the middle of a scan already running still waits it out. This removes scans starting during input, not blocking by one in flight.

Where this does not hold yet

Hardest first, because two stronger sentences were written and then killed by the sittings.

I drafted that corrective passes went from three of three runs to none. That was a harness result on a faster host, after the form contract landed: 0 of 3, 11.2–14.7 s. It does not describe the packaged app. The packaged sittings repaired 3 of 5 and 4 of 5. Quote the ~30 s / 52–58 s split. Do not quote the harness times as what you will see, and do not say the second generation is gone.

I then drafted that a planned next route is what buys first-turn acceptance. Sitting 2 killed that. All five of its runs had no planned route. One was clean anyway. The literal /next-step never appeared. Repairs fired on 4 of 5. A planned route is not a switch for this.

The collapse guard is in 0.6.0 and was not exercised on the sitting that published. Absence of a stump is not proof the rejection runs.

The setup wizard can offer an MLX model as a recommendation on a Mac. It does not always pre-select one. Honest catalog scoring can put a GGUF first at equal quality. "Choosing any MLX model failed" is the claim. "The wizard always picked one" is not.

There is no Windows packaged sitting for 0.6.0. The MSI is published; the measurements above are macOS. The MLX bugs never applied to Windows. The form-page timings were not re-taken there.

A bare <form> with no action still passes the checks. I saw that on both sittings. The back-link exemption does not make a missing destination fail.

The smaller print

An NHS.UK class that does not exist is no longer offered to the model. The pack's allowlist carried nhsuk-header_logo, a single-underscore typo of nhsuk-header__logo. It is not a class in NHS.UK Frontend. It had been removed once and came back through an additive extractor merge. It is now removed and denylisted, so neither a merge nor a harvest can re-admit it.

Accessibility checks keep their engine loaded on demand rather than pulling it into startup JavaScript when displaying its version. The engine and scan rules are unchanged.

Class-name correction skips candidates whose lengths make a match impossible. Correction thresholds, Unicode handling and deterministic tie-breaking are unchanged.

Generation, preview rendering, accessibility scan lifecycle and model discovery now have committed benchmark harnesses and recorded artifacts, so the figures above are reproducible rather than remembered. No behaviour depends on this.

The thread

0.5.0 was about artefacts that were tidier than the evidence behind them. 0.6.0 is about checks that were stricter than the world the app had made. A runtime file the build no longer produced. A back link the app requires and then fails. A quality score with no completeness term, used as a replacement rule. A prompt profile measured on a wire shape production never sent.

The first page should not fail a check the app made impossible. It should not cost two generations to satisfy a rule the first generation could never pass. And if you tried an MLX model since 0.4.2 and decided your setup was the problem: it was not.

Prompt to Page 0.6.0 is live for macOS (Apple Silicon) and Windows; installed copies update in place. Free closed beta. If you want in, you know where to find me.

Try Prompt to Page.

Free during the closed beta. Generation runs on your machine.