README
¶
MAV
Mobile Agent Verifier (mav) is a deterministic CLI for validating iOS Bazel
apps from coding agents.
MAV gives agents a compact API to build, launch, observe, navigate, interact, capture evidence, read logs, and check crashes. It is intentionally not an autonomous testing agent: it runs concrete commands and returns small, parseable results that another agent can act on.
What MAV Is For
- Verifying iOS app changes from an agent without opening Xcode.
- Driving simulator UI through accessibility trees before screenshots.
- Building repeatable flows that combine UI actions, waits, screenshots, video, logs, crash checks, and HTML evidence.
- Maintaining an app map that lets
mav go <screen>navigate from app launch to known screens. - Testing SwiftUI previews or isolated screens through a Bazel preview host.
MAV currently targets iOS Bazel projects. Simulator support is the main path. Real-device support is expected to use idb-backed capabilities where available, but the simulator path is the one exercised most heavily today.
Status
MAV is early and evolving. The current stable pieces are:
- Bazel discovery for iOS app targets.
- Simulator selection, boot, install, launch, screenshot, and video.
- AXe-first accessibility tree inspection and semantic interactions.
- idb coordinate taps and fallback capabilities.
- Native MAV YAML flows through
mav run. - JSON app map storage in
.mav/map/**. - HTML evidence reports in
/tmp/mav/<run-id>/report.html. - Filtered unified log capture for explicit MAV probes.
Requirements
- macOS.
- Xcode command line tools.
- Go, for development builds.
- Bazelisk, for Bazel app builds.
- AXe, for accessibility tree and semantic UI actions.
- idb, for coordinate taps and device/simulator fallback operations.
Check the local environment:
mav doctor
Install supported helper tools:
mav setup --install axe idb
mav setup currently uses Homebrew for supported tools.
Install
Build from source:
git clone https://github.com/bitomule/mav.git
cd mav
make build
Run the development binary:
.build/mav help
Or put it on your PATH:
ln -sf "$PWD/.build/mav" /usr/local/bin/mav
Release binaries are built by the GitHub release workflow for tagged releases.
Homebrew packaging lives in packaging/homebrew/mav.rb; checksums must be
filled from a published release before publishing a tap formula.
Quick Start
Run from the root of an iOS Bazel app repo:
mav discover
mav sim list
mav sim select --device "iPhone 17 Pro Max" --ios 26
mav open
mav ui tree
mav discover creates .mav/config.yaml and an initial app map. It caches the
Bazel app target, bundle id, selected simulator, locale/language, and available
tools.
mav open builds, installs, and launches the app. It creates a run directory
under /tmp/mav/<run-id>/ and starts logs.txt for MAV probes.
Example compact output:
ok cmd=discover bundle=com.example.app config=/repo/.mav/config.yaml target=//App:App
ok cmd=open run=7fd logs=/tmp/mav/7fd/logs.txt target="iPhone 17 Pro Max"
ok cmd=ui.tree driver=axe nodes=42 screen=start
Use --json when a caller needs structured output:
mav --json ui tree
Use --raw only when the underlying tool output is needed:
mav --raw ui tree
Help
mav help
mav help ui
mav ui --help
The top-level commands are:
doctor
setup
discover
sim
open
ui
capture
preview
run
go
logs
stop
crashes
evidence
Output Contract
Default output is one compact line:
ok cmd=<command> key=value key=value
fail code=<error_code> key=value key=value
Examples:
ok cmd=capture file=/tmp/mav/7fd/screen.png run=7fd
ok cmd=logs file=/tmp/mav/7fd/logs.txt matches=1 run=7fd
fail code=screen_not_found next="explore with mav ui tree/tap; map updates when the next screen is observed" screen=settings
fail code=ui_tree_empty driver=axe reason=simulator_accessibility_unavailable recovered=false
The goal is to give agents the minimum useful fields: what happened, where the artifact is, and what to do next when the command failed.
Project And Run State
Project state:
.mav/config.yaml
.mav/map/index.json
.mav/map/screens/*.json
.mav/map/current.json
.mav/map/pending.json
Run state:
/tmp/mav/<run-id>/logs.txt
/tmp/mav/<run-id>/commands.jsonl
/tmp/mav/<run-id>/evidence.jsonl
/tmp/mav/<run-id>/steps/*.png
/tmp/mav/<run-id>/trees/*.json
/tmp/mav/<run-id>/video.mov
/tmp/mav/<run-id>/crashes/
/tmp/mav/<run-id>/report.html
/tmp may resolve to a macOS per-user temporary directory such as
/var/folders/.../T.
App Map
The app map is JSON. It is updated by normal MAV commands:
mav openresets the current screen to the configured start screen.mav ui treerecords the current accessibility tree and screen elements.mav ui tap ...records a pending action from the current screen.- The next
mav ui treeobserves the next screen and writes the route edge.
Basic mapping loop:
mav open
mav ui tree
mav ui tap --id home_settings_button
mav ui tree
Prefer target selectors in this order:
- Accessibility id:
mav ui tap --id home_settings_button - Coordinates:
mav ui tap --x 398 --y 84 - Text:
mav ui tap --text Settings
Coordinates should be used only when the accessibility tree is insufficient and a screenshot makes the target unambiguous. Text is the last fallback because labels change with localization and copy edits.
Review .mav/map/** diffs before relying on a route. The map is source-level
project state, not temporary run output.
Navigation
mav go <screen-id> starts from app launch and follows a known route in the app
map.
mav go settings
It will:
- Build, install, and launch the app.
- Wait for a usable accessibility tree.
- Start video recording.
- Capture the start screen.
- Execute each mapped route edge with MAV UI primitives.
- Validate that each edge changes the tree.
- Validate target screen assertions when the map has them.
- Capture the target screen.
- Stop video.
- Generate
report.html. - Stop run-owned streams.
If the screen or route is unknown, MAV fails and does not explore:
fail code=screen_not_found screen=settings
fail code=route_not_found screen=settings
The caller should then explore manually with mav ui tree, mav ui tap,
mav ui scrollUntil, and mav capture.
UI Commands
mav ui tree
mav ui tap --id element_id
mav ui tap --x 120 --y 400
mav ui tap --text "Daily Reminder"
mav ui type "hello"
mav ui swipe --direction up
mav ui wait --id element_id --timeout 5s
mav ui scrollUntil --id privacy_policy_button --direction up --max-swipes 4
AXe is the default driver for accessibility tree inspection, semantic taps, typing, swipes, waits, and assertions. idb is used when it provides a concrete better capability, such as coordinate taps or fallback simulator/device operations.
Observation priority:
mav ui treemav capture- Video through
mav evidence start/stopor flows
Screenshots are for visual layout, custom rendering, media/canvas UI, or user-facing proof. The accessibility tree is cheaper and more useful for most agent decisions.
If AXe/idb return a single empty AXApplication tree, MAV treats simulator
accessibility as unavailable. It attempts a simulator reboot, app relaunch, and
tree retry before returning ui_tree_empty.
Native MAV Flows
mav run <flow.yaml> executes a native MAV YAML flow.
Use flows for repeatable feature validation:
version: 1
name: verify_daily_reminder
steps:
- open: {}
- evidence.start: {}
- go: { screen: settings }
- wait: { text: Daily Reminder, timeout: 5s }
- evidence.step: { name: before-toggle, note: Daily Reminder before tap }
- tap: { text: Daily Reminder }
- waitUntil:
any:
- text: "Don't Allow"
- text: "Allow"
- changedFrom: before-toggle
timeout: 5s
- evidence.step: { name: after-toggle, note: Result after tapping reminder }
- logs: { key: SettingsReached }
- crashes: {}
- evidence.stop: {}
- report: {}
Supported step types:
open
go
tree
tap
type
swipe
wait
waitUntil
assert
capture
scrollUntil
delay
logs
exec
crashes
evidence.start
evidence.step
evidence.stop
report
On failure, MAV stops run-owned processes, tries to capture failure evidence, writes report data, and returns a compact failure line.
Evidence
Evidence is explicit. Use it when a user needs proof of verification.
For ad-hoc navigation to a mapped screen:
mav go settings
For feature behavior, use a flow with named evidence points:
- open: {}
- evidence.start: {}
- go: { screen: settings }
- evidence.step: { name: before-toggle, note: Before tapping Daily Reminder }
- tap: { text: Daily Reminder }
- evidence.step: { name: after-toggle, note: After tapping Daily Reminder }
- evidence.stop: {}
- report: {}
The video should cover the path from launch/navigation through the tested behavior. Screenshots should prove the behavior itself, not only that the app opened.
MAV does not open HTML automatically. Inspect the reported file:
/tmp/mav/<run-id>/report.html
Logs
mav open, mav go, and mav run capture a filtered unified log stream for
MAV probes into logs.txt.
Use OSLog.Logger probes to prove code execution:
import OSLog
private let mavLog = Logger(
subsystem: "mav.com.example.app",
category: "probe"
)
mavLog.notice("MAV_LOG key=SettingsReached")
Then read logs from the current run:
mav logs --key SettingsReached
mav logs --contains SettingsReached
mav --raw logs --key SettingsReached
Do not use Swift print for MAV validation probes. MAV is designed around
filtered unified logs so the same probe pattern applies to simulator and device.
For trusted project-local shell assertions, opt in through .mav/config.yaml:
allow_shell: true
Then use an exec step:
- exec: { cmd: "grep -F 'MAV_LOG key=SettingsReached' $MAV_LOGS", contains: SettingsReached, timeout: 5s }
exec runs in the project root with MAV_ROOT, MAV_RUN_ID, MAV_RUN_DIR,
and MAV_LOGS set. This is an opt-in guard for trusted project checks, not a
security sandbox for untrusted commands.
Simulators
mav sim list
mav sim select --device "iPhone 17 Pro Max" --ios 26 --locale es_ES --language es
mav sim select --udid <simulator-udid>
mav sim boot
You can also pass simulator selection flags to mav open:
mav open --device "iPhone 17 Pro Max" --ios 26 --locale es_ES --language es
Previews
Use previews for isolated SwiftUI screens when launching the full app is too slow or the target screen is deep in a flow:
mav preview init
mav preview settings
mav ui tree
mav capture
mav preview init creates a Bazel preview host. Wire the real view and any
lightweight mocks into the generated host, then launch a preview by id.
Cleanup
Ad-hoc mav open and mav preview sessions keep log capture running for the
current run. Stop them when done:
mav stop
mav go and mav run stop run-owned streams automatically.
Command Reference
mav doctor
mav setup --install axe idb
mav discover
mav sim list
mav sim select --device NAME --ios VERSION [--locale LOCALE] [--language LANG]
mav sim select --udid UDID
mav sim boot
mav open [--device NAME] [--ios VERSION] [--udid UDID] [--locale LOCALE] [--language LANG]
mav ui tree
mav ui tap --id ID
mav ui tap --x X --y Y
mav ui tap --text TEXT
mav ui type TEXT
mav ui swipe [--direction up|down|left|right]
mav ui wait --id ID [--timeout 5s]
mav ui scrollUntil --id ID [--direction up] [--max-swipes 5]
mav capture [--run RUN_ID]
mav preview init [--dir MAVPreview] [--bundle-id BUNDLE_ID] [--force]
mav preview <view-id>
mav run flow.yaml
mav go <screen-id>
mav logs [--run RUN_ID] [--key KEY] [--contains TEXT] [--level LEVEL]
mav stop [--run RUN_ID]
mav crashes [--raw]
mav evidence start [--run RUN_ID]
mav evidence step --name NAME [--note NOTE] [--run RUN_ID]
mav evidence stop [--note NOTE] [--no-capture] [--run RUN_ID]
mav evidence report [--run RUN_ID]
Troubleshooting
fail code=config_not_found
Run:
mav discover
fail code=screen_not_found or fail code=route_not_found
The map does not know that screen or route yet. Explore manually:
mav open
mav ui tree
mav ui tap --id some_button
mav ui tree
Then inspect .mav/map/**.
fail code=ui_tree_empty
The simulator accessibility service did not recover after MAV retried. Re-run
mav open or select another simulator with mav sim select.
mav logs --key ... returns no matches
Make sure the app logs with OSLog.Logger using the configured MAV subsystem
and category, and make sure the behavior happened after MAV started the run.
Development
make test
make build
make check
make check runs gofmt, tests, and a local build.
Contributing
Issues and pull requests are welcome. Keep changes deterministic and preserve compact output: commands should report the minimum information an agent needs to continue, parse, or present evidence.
See CONTRIBUTING.md.
License
MIT. See LICENSE.