Skip to content

Architecture

Three layers, separated so that two of them never touch a platform.

flowchart TD
bin["bin/verikun.ts<br/>(exit-code shim)"] --> cli["cli.ts<br/>(dispatch + handlers)"]
cli --> sel["ui/selector.ts<br/>@id / text: / desc: / class:"]
cli --> fmt["ui/format.ts<br/>inline / compact / tree / json"]
cli --> vp["ui/viewport.ts<br/>reachability geometry"]
cli --> ds["device/settings.ts<br/>capability table"]
cli --> drv["drivers/index.ts<br/>getDriver(platform, device)"]
drv --> adb["drivers/adb.ts<br/>adb"]
drv --> ios["drivers/ios.ts<br/>idb + simctl"]
adb --> ap["ui/android-parse.ts<br/>uiautomator XML"]
ios --> ip["ui/ios-parse.ts<br/>idb describe-all JSON"]
ap --> el["Element[]<br/>(normalized)"]
ip --> el
el --> sel
el --> fmt
el --> vp
cli.ts and the ui/ layer see only normalized Elements; drivers/ is the only platform-specific code

The Driver interface is what every platform backend implements, and Element is the normalized node model every other layer consumes.

cli.ts, ui/selector.ts and ui/format.ts operate only on Element[] and Driver — they contain zero adb or xcrun knowledge. Keep it that way: platform quirks belong in drivers/.

getDriver(platform, device) in drivers/index.ts is the only place a concrete driver is constructed. The one exception is commands/devices.ts (vk devices), which probes both backends directly so it can list everything attached.

cli.ts is the dispatcher:

  • run(argv) parses arguments and builds a Ctx { driver, platform, device, positionals, flags, record? } (commands/context.ts).
  • executeCommand(command, ctx) is the switch to each cmdX(ctx) handler.

Adding a command means adding a case (plus any aliases) and a line in usageText() — and, if it should appear in a test run, an entry in RECORDABLE in run.ts.

Helpers and handlers that outgrow the dispatcher live under commands/: context.ts holds the Ctx every handler takes, auto-wait.ts the selector auto-wait, batch.ts the batch line grammar and devices.ts the device listing. They never import cli.ts, so there is no cycle.

run.ts and report.ts sit beside cli.ts. They may use the driver, like cli.ts does — they are not part of the platform-agnostic core. report.ts is pure: RunState → JUnit/HTML strings, no filesystem, no driver.

Every command re-captures the hierarchy fresh — driver.getElements() → uiautomator dump.

This is intentional: the screen may animate or auto-advance between calls. Do not add a snapshot cache expecting staleness to be acceptable.

Capability Mechanism
Hierarchy uiautomator dump
Screenshots screencap -p
Interaction input tap / text / swipe / keyevent
Screen size wm size
Logs logcat

launch() resolves the app’s default LAUNCHER activity with cmd package resolve-activity --brief and starts it with am start -n <component>. Keep that path: monkey -c LAUNCHER hangs indefinitely on some OEM skins (MIUI/HyperOS).

Shells to xcrun simctl / devicectl for discovery and — on a simulator — screenshots, launch, stop and logs; and to Facebook’s idb for the accessibility hierarchy, all interaction, screen size, and lifecycle on physical devices.

A private isSimulator() — is the resolved UDID in simctl list? — picks simctl-versus-idb for the branchable methods. resolvedSerial() must return a concrete UDID, because idb cannot address the simctl-only booted alias.

Two methods have no clean iOS equivalent and degrade honestly rather than half-implement: clearApp throws exit 3 (no per-app data reset) and currentApp returns "(unknown)" (no foreground query). Keep that shape for any similar gap.

It declares every vk device set key: its value domain, its per-platform support (supported / unsupported / noop), and — where unsupported — the manual equivalent.

One table drives four consumers: argument validation, the driver switch, vk device caps, and the vk ai plan validator. Adding a setting is a table row plus a case in each driver, so a platform gap can never be documented in one place and forgotten in another.

File Purpose
bin/verikun.ts The #!/usr/bin/env node shim. Calls run() and exits with its code, or prints Fatal: and exits 3.
cli.ts Every command handler except vk devices (commands/devices.ts), usageText(), the dispatch switches, executeOutcome / executeParsed / executeForServer, mapError, provider selection, and the ai / suite / install orchestration.
commands/context.ts The Ctx every command handler takes: driver, platform, device, positionals, flags, and the recorder when the command is part of a test run.
commands/auto-wait.ts Selector auto-wait: the wait window and poll interval, no-window tolerance while polling, and the waiting resolveOneWaiting / matchWaiting every selector-resolving handler goes through.
commands/batch.ts The batch line grammar: shell-like quoting without a shell, and the globals a batch call passes down to every line.
commands/devices.ts vk devices: the listing and its aligned table — the one place a concrete Driver is constructed outside getDriver(), because listing needs both backends at once.
args.ts Minimal dependency-free argv parser
errors.ts CliError plus the exit-code contract, isEnvError, probeFailure, and the two heal-trigger selector subclasses
version.ts Generated from package.json by scripts/gen-version.mjs. Never hand-edit.
File Purpose
exec.ts All external process spawning, shell-free (args array). Plus commandExists.
output.ts Output discipline: results → stdout, diagnostics → stderr, --json everywhere. The quiet toggle vk ai uses.
types.ts Shared types; the platform-normalized Element model
image.ts Hand-rolled dependency-free PNG downscaler over node:zlib
File Purpose
drivers/index.ts getDriver(platform, device) factory and re-exports
drivers/adb.ts The Android driver, plus adbTransport / severanceRisk (the wireless-adb airplane guard)
drivers/ios.ts The iOS driver over idb plus simctl
File Purpose
ui/android-parse.ts Hand-rolled scanner: uiautomator XML → Element[]
ui/ios-parse.ts idb ui describe-all --json → the same Element[]. Exports IOS_UNREPORTED_STATE.
ui/selector.ts The selector grammar and the tiered auto-healing matcher
ui/state-support.ts Refuses a state modifier the platform cannot answer for
ui/format.ts Compact one-line element rendering
ui/viewport.ts Pure viewport geometry — the basis of auto-scroll and the offscreen marker
File Purpose
run.ts The Recorder: disk-persisted run state, implicit start and rollover, step capture, device-override snapshots, archive
report.ts Pure rendering: RunState → JUnit/HTML, SuiteRun → index.json/index.html
suite.ts vk suite: enumerate, run the queue across one or more lanes, reset between tests, retries, manifest, the CI-gate exit code. Dependency-injected; imports nothing from cli.ts.
server.ts vk server: HTTP+JSON with mandatory bearer auth, the validateNode gate, leases and failover — every piece of policy stays on the main thread
server-http.ts The transport helpers every handler shares: an error that knows its HTTP status and client exit code, capped body reads, JSON replies, wire-flag and artifact encoding
server-lifecycle.ts The start / restart / stop / list seam buildServer is injected with, and its production implementation over drivers/lifecycle.ts
server-pool.ts One worker thread per device, replies correlated, one command at a time per device. Mechanism only.
server-worker.ts The device call at the end of that pipeline, on its own thread
rpc.ts The pure wire contract, including the error codec that preserves thrown-error subclass identity
File Purpose
device/settings.ts The device-settings capability table
device/claims.ts The host-global claim store — which attached device another job is already driving
device/grant.ts DeviceGrant: the one run-scoped acquire / touch / release contract, over a host claim or a vk server lease
device/pool.ts --devices parsing and resolution, shared by vk server and vk suite
device/failover.ts Pure classifiers deciding whether a failure is the artifact’s or the device’s
device/prep.ts vk device prep: explicit, undoable test-device setup
File Purpose
agent/ir.ts The plan IR: typed node union, validateNode, parsePlan, and the non-recursive JSON schemas
agent/engine.ts The deterministic interpreter. Imports no cli.ts, so no cycle.
agent/grammar.ts GRAMMAR + REPAIR_GRAMMAR, the cache-controlled stable prompt prefix
agent/cache.ts The plan cache, keyed and fingerprinted
agent/lint.ts Compile-fidelity lint — catches directives silently dropped from the plan
agent/cost.ts Token accounting, the price table, providerFor(model), the budget abort — see Cost & budget
agent/provider.ts The AgentProvider seam
agent/claude.ts Anthropic Messages API over built-in fetch, no SDK
agent/openai.ts OpenAI Chat Completions; exports toStrictSchema
agent/cli-provider.ts The spec-parameterized provider that shells to codex / cursor-agent
agent/remote.ts The client-side remote exec backend