mav

module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 3, 2026 License: MIT

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:

  1. mav open resets the current screen to the configured start screen.
  2. mav ui tree records the current accessibility tree and screen elements.
  3. mav ui tap ... records a pending action from the current screen.
  4. The next mav ui tree observes 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:

  1. Accessibility id: mav ui tap --id home_settings_button
  2. Coordinates: mav ui tap --x 398 --y 84
  3. 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:

  1. Build, install, and launch the app.
  2. Wait for a usable accessibility tree.
  3. Start video recording.
  4. Capture the start screen.
  5. Execute each mapped route edge with MAV UI primitives.
  6. Validate that each edge changes the tree.
  7. Validate target screen assertions when the map has them.
  8. Capture the target screen.
  9. Stop video.
  10. Generate report.html.
  11. 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:

  1. mav ui tree
  2. mav capture
  3. Video through mav evidence start/stop or 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.

Directories

Path Synopsis
cmd
mav command
internal
mav

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL