What someone picking up Ubuntu or Windows needs to know, and what is already true so they do not re-derive it.
| platform | backend | parity driver | receipts |
|---|---|---|---|
| macOS (Aqua) | src/backends/darwin.mjs + darwin-accessibility.m |
scripts/lib/desktop-darwin.mjs |
27 tasks × 5 repeats, 24 demonstrated — parity/results/darwin-aqua-2026-09-07.json |
| Linux (X11) | src/backends/linux.mjs |
scripts/lib/desktop-x11.mjs |
27 tasks × 5 repeats, 25 demonstrated + 2 documented skips on the isolated surface — parity/results/linux-xvfb-isolated-2026-09-16.json; the 2026-09-07 receipts predate the engine/driver split and are stale |
| Windows | src/backends/win32.mjs |
scripts/lib/desktop-win32.mjs |
none yet — driver exists but needs a real Windows desktop run (see below) |
| Wayland (sway) | src/backends/linux-wayland.mjs + wayland-input.py |
scripts/lib/desktop-wayland.mjs (--wayland, --isolated starts a headless sway) |
27 tasks × 5 repeats on headless sway, 21 demonstrated — parity/results/linux-wayland-isolated-sway-2026-10-11.json |
You do not need an Ubuntu machine to start. docker/run.sh parity builds
a headless Linux desktop — Xvfb, a window manager, the X11 tools, an AT-SPI bus,
Chromium and Tk — and runs the isolated route in it from anywhere Docker runs.
What that does and does not qualify is in docker/README.md;
the short version is that it exercises the real X11 code paths but says nothing
about Wayland or about a real login session's window manager.
First job on Ubuntu: run the existing suite on a real login session.
npm run parity on a desktop X11 session should reproduce the container
result — 25 demonstrated, 2 documented skips, native.modal_dialog failing
under the toolkit-modal limitation. The isolated-surface receipt is fresh
(parity/results/linux-xvfb-isolated-2026-09-16.json); the older
linux-x11-2026-09-06.json shared-session receipt still predates the
engine/driver split. If a row moves, look at WM behavior first — the
container runs openbox, real desktops differ.
First job on Windows: the driver exists (scripts/lib/desktop-win32.mjs)
but has no receipts. It needs a real interactive Windows desktop — a Windows
VM (Parallels/UTM/VMware) or a cloud Windows host — with Node.js, Chrome and
Python 3 + Tk (the python.org installer bundles tcl/tk). There is no
--isolated route: Windows containers have no interactive desktop, and a
second session is a different user's desktop, so the shared console session
is the only surface and interference is measured, not engineered away.
A CI windows-latest run is not a user's desktop; say plainly which produced
any receipt. The suite file is parity/tasks.win32.json: held-input rows and
control.permission_denied carry committed skip reasons, and browser.upload
drives the common "Open" dialog (typed path + Return commits; no
window_title click needed).
scripts/parity-run.mjs is platform-neutral: CLI, the MCP client, the task
DSL, the oracle comparison, the step executor, interference bookkeeping and the
summary. It knows nothing about any windowing system.
A driver is one module exporting createDesktop({ parityDir, tasksDoc, isolated })
which returns:
| member | contract |
|---|---|
sessionType() |
short surface name; goes in the receipt and the matrix (x11, xvfb, aqua, …) |
start() / stop() |
set up and tear down anything the run needs; stop() may return a cleanup summary for run.json |
baseEnv() |
environment shared by fixtures and the spawned MCP server |
serverEnv(ctx) |
extra environment for the MCP server only (Linux forces CODEWHALE_CU_APP=off and a scratch state dir; macOS must not, or requests stop routing through the app that holds the OS permissions) |
hostProbe() |
{ pointer: {x, y}, activeWindow } sampled independently of the backend under test |
launchFixture(kind, repCtx) |
start a disposable fixture, set repCtx.pid, return the process |
focusFixture(repCtx) |
give it input focus, if that is how the platform works (a no-op on macOS, where input is process-bound) |
killFixture(proc, repCtx) |
stop it and remove its scratch dirs |
prelude(task, repCtx) |
DSL steps to run before the task's own (macOS binds input here); their tool calls are counted separately |
clientOrigin(fixtureKey, { window, repCtx }) |
the fixture's content origin in screen points |
windowGeometry(title, repCtx) |
optional — origin and size of a non-fixture window (a native dialog or chooser) found by title on the work display; backs the {window_title, at} target. A driver without it fails that target with "window not found". |
oracleState(task, repCtx) |
the fixture's state, read outside the tool surface |
meta() |
environment facts for run.json |
Register it in the DRIVERS map at the top of scripts/parity-run.mjs.
- The oracle never goes through the tool surface. X11 reads the fixture's
window title with
xdotool; macOS serves the page from a loopback server and reads its beacons, and reads a state file the native fixture writes. If the thing under test also reports the result, the run proves nothing. - Interference is sampled by something that shares no code with the backend.
X11 uses
xdotool; macOS usesparity/darwin-probe.m, a standalone CoreGraphics/AppKit binary; Windows usesparity/win32-probe.ps1, a standaloneGetCursorPos+GetForegroundWindowscript. - Fixtures report their own geometry where they can. Measuring a window
from outside means guessing where the title bar and decorations end.
parity/fixtures/native.pyandnative-macos.mboth publish their content origin;browser.htmlpublisheswindow.screenX/Yand its inner/outer size when it is served over http. - A task the platform cannot express gets
skipwith a reason in the per-platform suite, and appears in the matrix as skipped. Never drop a row. - Per-platform suites are
parity/tasks.<platform>.json, picked up automatically, withparity/tasks.jsonas the shared default. Keep the task ids identical so the columns stay comparable, and put the platform's reason inknown_limitations["<platform>-<session>"].
These were surprises here; the equivalent question is worth asking on each OS.
- Can pointer events be delivered to a chosen process? On macOS, no —
keyboard yes, pointer and scroll no, by every API. That is what forced the
accessibility-first pointer path and the
strategyparameter onleft_click. X11 and Windows deliver input to the focused window instead, so the answer there is different but the receipt should still be honest about what it cost. - Does the receipt tell the truth about cost?
pointer_moved,foreground_taken,foreground_before/afterexist on macOS receipts. A Windows backend that moves the cursor should say so the same way. strategy: "a11y"must fail closed on backends without an accessibility press for coordinates.linux.mjs,win32.mjsandharmonyos.mjsdo this today (assertEventStrategy); keep it that way rather than silently degrading to a raw click.- Does the app/agent route prepare arguments the same as the in-process
route? It did not, until recently: coordinate targets were sent to the
desktop app and the ssh agent as raster pixels instead of screen points,
which is wrong on any scaled display and skipped the raster's own refusals.
tests/server-wire-targets.test.mjscovers that route now (CODEWHALE_CU_TEST_REMOTE=1); it is platform-neutral and should stay green.
list_appsreports nowindowCounton any backend, so a caller cannot tell which applications have windows without a second call.scripts/smoke.mjsworks around it by falling back to the frontmost app.- There is no Wayland driver; the X11 driver's
xdotool/xwininfocalls will not work there. - No Codex baseline exists on any platform, so no parity claim is published —
only the measured matrix.
docs/PARITY.mdsays how to record one.