openwong2kim / verify-wmux
Launch and drive the real wmux desktop app (Electron main window) to prove a change works the way a user sees it: start an isolated instance, check it with doctor, click/press by data-testid or ARIA label over CDP, capture ARIA snapshots and screenshots, then stop only what was launched. Use whenever you need to run, launch, screenshot or verify wmux UI behavior, reproduce a UI bug, or say a wmux feature works; read references/features/ for the per-feature recipes.
<!--
Generated by following .claude/skills/create-verification-skill (adapted from
pstack, https://github.com/cursor/plugins, MIT, Copyright (c) 2026 Lauren Tan,
and pstack-claude, https://github.com/anishekkamal/pstack-claude, MIT,
Copyright (c) 2026 Lauren Tan, Copyright (c) 2026 Michael Denyer). The proof
standards and verdict wording are adapted from pstack-claude
plugins/pstack/skills/verify-web/references/proof-standards.md. See THIRDPARTYNOTICES.md.
-->
verify-wmux
Status: proven on macOS against the installed /Applications/wmux.app 4.3.0: launch, doctor, drive (Settings first, then one sweep over all ten feature files), ARIA snapshots and CDP screenshots, stop --purge, evidence kept. Not yet run on Windows or Linux, nor against a dev build; the PC switcher was proven only in its absent state.
wmux is an Electron terminal multiplexer. The surface a user touches is the main window (sidebar, panes with xterm terminals, rail pages, settings, dialogs). Secondary surfaces (the wmux CLI, the MCP server, the phone web client) are not driven by this skill; prove those with a terminal transcript or the repo's own harnesses (e2e/, rig/).
Everything runs through one driver, scripts/control-wmux.mjs (Node 22+, uses the repo's playwright-core dependency; run npm ci once in a fresh checkout). Every command prints one JSON object and exits non-zero on failure. Read [`references/features/README.md`](references/features/README.md) before driving, then the feature file that matches the request.
Launch
node scripts/control-wmux.mjs launch # /Applications/wmux.app, suffix -verify-<random>
node scripts/control-wmux.mjs launch --app <path/to/wmux> # any other packaged build's executable
node scripts/control-wmux.mjs launch --suffix -verify-mytest --timeout 90000What it does: spawns the app executable directly (its PID is recorded) with WMUX_DATA_SUFFIX=<suffix>, which isolates the socket, MCP socket, auth token, TCP port file, data dir ~/.wmux<suffix>/, the daemon and Electron userData (src/shared/constants.ts dataSuffix(), src/main/index.ts appInit). The launching environment is scrubbed of WMUX_* pane identity and CLAUDE* session variables first, because an agent usually runs inside the owner's wmux. ELECTRON_ENABLE_LOGGING=1 sends the renderer console into app.log.
Ready means: the CDP port is found and the main window has rendered #root. wmux picks its own CDP port in 18800-18899 at startup (src/main/browser-session/cdpPort.ts claimCdpPort); a --remote-debugging-port flag is not honored. The driver finds the port from the claim file ~/.cache/wmux-cdp/wmux-cdp-<port>.lock whose content is the launched PID, and cross-checks the [WinMux] CDP requested on port N / CDP listening on port N log lines.
Refusals (by design): an empty suffix (the owner's real instance), -dev (every dev build's default), -portable (any packaged copy outside /Applications), a suffix whose socket or daemon is already live, and a second launch while the recorded instance is still running. Every other command reads .claude/skills/verify-wmux/.run/state.json and refuses without it, and re-checks before each attach that the CDP claim still names the launched PID. The driver can only ever touch an instance it started.
Doctor
node scripts/control-wmux.mjs doctorRead-only. Reports healthy plus problems[]: app PID alive; app version vs checkout version (skew means the feature map's handles may not exist in the running build); CDP disabled (WMUX_DISABLE_CDP=true or ~/.wmux<suffix>/config.json browser.cdp.enabled:false, both stop all driving); CDP port claimed by our PID and answering /json/version; CDP listening logged (vs unverified); daemon PID; whether the first-run wizard or settings is open; window visibilityState/focus; renderer console error count. Run it before the first drive, after anything surprising, and after every failed drive. A failing doctor means fix or relaunch, not retry the drive. Output is also saved as doctor.json in the evidence dir.
Drive
node scripts/control-wmux.mjs click --testid first-run-wizard-skip
node scripts/control-wmux.mjs click --label Fleet # aria-label, exact
node scripts/control-wmux.mjs click --role menuitem --name "Settings ⌘,"
node scripts/control-wmux.mjs click --text "Skip for now" [--nth 0]
node scripts/control-wmux.mjs press --key Meta+Comma # Playwright key syntax
node scripts/control-wmux.mjs press --key Enter --label "Terminal input" # focus target first
node scripts/control-wmux.mjs type --text 'echo hi' --label "Terminal input" # keyboard.type into the target (appends)
node scripts/control-wmux.mjs hover --selector '[data-sidebar-row]' --nth 0 # reveal hover-only controls
node scripts/control-wmux.mjs wait-settle --testid settings-page-title [--state visible|hidden] [--timeout 10000]
node scripts/control-wmux.mjs eval --js "document.querySelector('[data-testid=settings-page-title]')?.textContent"
node scripts/control-wmux.mjs inspect --js "..." # alias of eval- Targets, best first:
--testid(data-testid),--label(aria-label),--role+--name,--text,--selector(CSS, last resort; never a generated class). Never screen coordinates. - A target matching several elements fails and says how many; scope it or pass
--nth. - A click blocked by a modal fails with the element that intercepts pointer events (for example
onboarding-backdrop); close that dialog through its own control. wait-settlewaits for the target state, then for 400 ms of DOM quiet (MutationObserver, which keeps working when a hidden or locked window pauses rAF). Use it after every action instead of sleeps.eval/inspectis for read-only inspection (a value, a count, a store read). Never use it to put the app into the state you were asked to reach; that skips the code the user runs.
Evidence
node scripts/control-wmux.mjs snapshot [--testid ID | --selector CSS | --role R --name N] --as settings-general
node scripts/control-wmux.mjs screenshot --as settings-general
node scripts/control-wmux.mjs console [--errors] [--tail 50]Artifacts go to .claude/skills/verify-wmux/evidence/<run-id>/ (git-ignored): numbered NNN-<as>.aria.yml (accessibility tree with the instance suffix and window title in the header) and NNN-<as>.png, plus app.log (main + renderer console), doctor.json and timeline.jsonl (every command and outcome). Screenshots use CDP Page.captureScreenshot; when the screen is locked or the window occluded it may time out (screenshot: unavailable) or come back near-empty (suspectBlank: true): then the ARIA snapshot plus an eval read-back is the evidence, and say so.
Proof standards:
- Drive the real user path (button, shortcut, menu). Internal setters, test-only hooks and
evalthat writes state do not count. Arranging a precondition is fine; injecting the symptom is not. - Capture the action and the resulting state: a snapshot or screenshot right after the action, of the region that changed. A final screenshot alone does not show causation.
- Confirm side effects from a second, read-only view: reopen the panel, read the value back with
eval, read the file under~/.wmux<suffix>/. - Keep identity visible: the snapshot header carries the suffix; doctor output records app, version and port.
- Every mapped entry point, or an explicit skip: if a feature has a button and a shortcut, proving one does not prove the other. Report
skipped: <entry>, reason: <why>. - Report exactly one verdict with the claim restated so it can fail:
VERIFIED | NOT VERIFIED | INCONCLUSIVE
Claim: <falsifiable claim>
Evidence: <artifact paths, read-back values>
Skipped: <entry points not driven, with reason> (or none)
Confidence: high | medium | low, and whyA clean NOT VERIFIED is a useful result. Do not upgrade INCONCLUSIVE because the code looks right.
Cleanup
node scripts/control-wmux.mjs stop # app + its daemon; data dir kept for inspection
node scripts/control-wmux.mjs stop --purge # also delete the suffixed data this run createdstop SIGTERMs the launched app PID (SIGKILL after 10 s), then stops the daemon that instance spawned (a normal quit only detaches from it): first wmux daemon stop over the instance's own suffixed socket, then, only if still alive, SIGTERM to the PID in ~/.wmux<suffix>/daemon.pid. It boots out finished launchd jobs labelled com.wmux.daemon<suffix>.* (macOS). --purge removes only suffixed paths that did not exist before launch. Never pkill, killall, or kill by name. Evidence is never removed; stop lists what survived. Run stop after every attempt, failed ones included; the state moves to .run/stopped-<run-id>.json.
Record the verdict (verifying a PR)
When the claim is a pull request, end with the verdict line above in your report. A pane has no tool that records a verdict: the agent factory's verifier bot records its own (pr_verdict in bot mode), and the factory's merge lane merges a PR only on a pass at exactly the head being merged.
Helpers
| Command | Purpose |
|---|---|
launch | start an isolated instance, wait for CDP + rendered root |
doctor | read-only health and identity check |
snapshot | ARIA tree of body or one target, saved to evidence |
screenshot | CDP PNG of the main window, saved to evidence |
click / hover / press / type | act by handle / key / characters |
eval (inspect) | read-only JS expression in the main window |
wait-settle | wait for a target state and DOM quiet |
console | renderer console lines from app.log |
stop | tear down what launch started |
Pure rules (suffix refusal, CDP claim parsing, env scrub, console parsing) live in scripts/lib/control-wmux-lib.mjs and are tested by scripts/__tests__/controlWmux.test.mjs.
Gotchas
- A fresh suffix is a fresh profile: the first-run wizard ("Welcome to wmux") covers the window, an onboarding tour ("Every agent, one board", Step 1 / 4) can follow, and a "Keyboard shortcuts" card appears. Reset to the baseline in [`references/features/README.md`](references/features/README.md) before driving.
- Isolation covers wmux's own paths only. Things outside them are shared with the owner:
~/.claude/settings.json,~/.claude.json,~/.wmux/hooks/(Claude Code integration), the~/.local/bin/wmuxCLI shim, Keychain, the network. The feature files list which actions write there; do not drive them. - A packaged build outside
/Applications(for exampleout/fromnpm run package) launched with our suffix still runs the macOS CLI-shim repair about 3 s after boot and may repoint~/.local/bin/wmuxat itself (src/main/index.ts,PORTABLE_DATA_SUFFIXguard). Prefer/Applications/wmux.app; if you must use a copy, check the shim afterwards. - Version skew: handles come from this checkout's
src/renderer; the installed app may be older. Doctor reports it. A missing handle under skew is not drift. - macOS "wmux is not responding"/memory dialogs or a locked screen can stall the window; CDP DOM reads keep working, screenshots may not.
- The quick-launch window (
launcher.html) is a secondpagetarget; the driver always picksindex.html.
Supersedes (follow-up, not done here)
These existing scripts drive the UI over CDP against whatever wmux is already running (several scan the 18800-18899 range and attach to the first answer, which can be the owner's instance; some write evidence to hard-coded Windows paths), and each re-implements attach and evidence. Their scenarios should move into feature files driven by control-wmux; they are left untouched for now:
scripts/channel-dock-dogfood.mjs,scripts/channel-members-dogfood.mjs,scripts/channels-company-dogfood.mjsscripts/s-c1-fleet-dogfood.mjs,scripts/s-c2-fleet-deepening-dogfood.mjsscripts/x3-dogfood-main.mjs,scripts/x3-dogfood-linkclick.mjs,scripts/x3-dogfood-restore.mjs,scripts/x3-dogfood-cleanup.mjsscripts/pane-stacking-dogfood.mjs,scripts/v281-gui-probe.mjs,scripts/homescreen-launch-probe.mjsscripts/a2a-state-query-dogfood.mjs,scripts/b1-plugin-host-dogfood.mjs,scripts/lanlink-pr5-cdp-dogfood.mjs,scripts/managed-chat-fault-probe.mjs,scripts/phase3-retention-probe.mjs
Keep the map honest with /maintain-verification-skill.