---
name: android-ui-analyser
description: >-
  Drive and inspect an Android app's UI on a device/emulator with the `aua`
  (android-ui-analyser) CLI. Returns the screen as a list of elements with stable integer IDs +
  bounding boxes, then acts BY ID — tap/input/swipe/key — so you never guess pixel coordinates.
  Use whenever the task involves an Android device/emulator: "test the Android app", "what's on
  screen", "tap/type/swipe the X", "is <text> visible", "drive the emulator", automating or
  debugging an Android UI flow, checking a screen after a change, or inspecting/seeding a
  debuggable app's SQLite database. Hierarchy-first (tens of ms); falls back to
  OCR/detection/grounding vision on Compose/Flutter/WebView/canvas/game screens the
  accessibility tree can't see.
---

<!-- Generated by `aua guide --emit-skill`. Edit guide.py (the single source), not this file. -->
# android-ui-analyser (`aua`) — agent operating manual

`aua` reports **what's on an Android screen and where**, so you act on **integer element IDs, not pixels**. It reads the accessibility/view hierarchy first (fast, exact) and falls back to image vision (detection + OCR, optional grounding VLM) on screens the hierarchy can't see. It remembers each app's layout so you start each session already knowing the map. For debuggable builds, `aua db` also provides structured private-SQLite inspection, guarded mutation, and rollback without agent-authored adb pipelines.

## Session protocol
1. **Ensure a device is available (headless is fine).** If `aua devices` is empty and you need to **verify a change without bothering the user** (no emulator window on their desktop), boot one quietly: `aua emulator start --headless` (uses `-no-window` + **host GPU** on Mac — not SwiftShader; pick `--avd <name>` when several AVDs exist — `aua emulator list`). Prefer an already running device when one is attached; don't kill the user's headed emulator unless they asked. For **HTTPS proxy / mock record** you need a *rootable* Google APIs AVD (Play Store images refuse `adb root`): `aua emulator recommend-proxy` / `ensure-proxy`, then `aua emulator start --avd aua_proxy --headless`. **Parallel agents on one host:** each boots with `aua emulator start --headless --parallel` (unique `-port` + `-read-only` + owner tag); pin every later command with the returned `serial` (`aua --serial …` / `AUA_SERIAL`); tear down only yours with `aua emulator stop --serial <yours>` or `AUA_OWNER=… aua emulator stop --mine`.
2. **ALWAYS stop headless emulators YOU started.** **Hard requirement before you end the session** (not optional, not 'if convenient'): `aua emulator stop --serial <yours>` (safest with parallel agents) or `AUA_OWNER=… aua emulator stop --mine` / `--avd <name>`. Orphaned headless AVDs burn CPU and battery. Do this even if the test failed. Safety nets (do not rely on them alone): idle watchdog auto-stops after `--idle-stop` seconds of no aua activity (default 900); MCP `emulator_start` tracks serials and stops them when the MCP process exits. `aua emulator status` shows what aua started (including `owner` / `port`).
3. **Start the warm daemon.** `aua daemon start` — holds the device connection + loaded models warm so each later call is ~tens of ms instead of paying Python/connect startup. Optional; every command still works without it. For even lower host latency on hot commands once the daemon is up, build `native/aua-fast` (`make -C native/aua-fast install`) and use `aua-fast analyze|tap|has|…` — a tiny C client that speaks the daemon socket (falls back to `aua` if the daemon is down). See `docs/NATIVE_ROADMAP.md`. Unchanged screens short-circuit via `meta.via=hierarchy-unchanged` / `--format delta`; wait on any tree change with `aua wait-and-analyze --changed` (or MCP `wait_changed`); multi-device with `aua fanout`.
4. **Start from the app playbook.** `aua about` prints what the tool already learned about THIS app — a one-line description, login **recipes** (e.g. how to log in as a test/full user), useful **deeplinks**, and **notes** (quirks, e.g. a dialog to dismiss after login, or that the 'Apps' tab is really Tools). Read it first and follow it — it saves you the discovery the last run already did. As you learn things, teach it back with `aua remember --about "…" | --note "…" | --recipe NAME --note "…" | --deeplink URI --note "…"` so the next run starts even more informed.
5. **Use what memory already knows.** `aua map` (or `aua map --brief`) prints the app's known screens + routes — but you usually don't need to call it: every `analyze` already returns `meta.known_screen` plus inline `meta.known_routes` / `meta.suggested_gotos` / `meta.map_hint`; unresolved map questions arrive in `meta.research_tasks`. Act on those instead of re-exploring. `aua map --find "<goal>"` gives just the route to a target. Feature-flag sets are separate contexts; use `--all-contexts` to compare variants and `--audit` to persist ambiguous names/routes as concrete research tasks.
6. **Take shortcuts with deeplinks.** `aua open-and-analyze "<uri>"` fires a deeplink — jump straight to a screen or trigger an app action (e.g. set a feature flag) instead of tapping through the UI. Far faster than navigating. When an app has known deeplinks, every `analyze` offers the best ones inline in `meta.suggested_deeplinks` (e.g. `open myapp://home`) — so to reach the screen under test, open the shortcut instead of navigating to it. Some deeplinks need an app restart to take effect (`aua app stop <pkg>` + `aua app launch <pkg>`). Don't know the app's deeplinks? `aua explore mine <repo> --app <pkg>` harvests them from the source once (then they ride inline + show in `aua about`).
7. **Inspect or seed app state through AUA, not hand-written adb.** For a debuggable build, `aua db list <pkg>` discovers private SQLite files, `aua db schema <pkg> <db>` describes them, and `aua db query <pkg> <db> "SELECT …"` returns bounded JSON rows. Android images often have no `sqlite3`, so AUA stops the app, snapshots the database plus WAL/SHM through `run-as`, queries with host SQLite, and relaunches by default (`--no-restart` leaves it stopped). Data mutation is `aua db execute <pkg> <db> "UPDATE …" --yes`: it creates a restore point, runs one transaction, rejects schema/PRAGMA/ATTACH changes, checks foreign keys and integrity, then replaces the database without stale sidecars. Use `db backups` / `db restore … --yes` to roll back. Query/execute results can contain user data; request only the columns and rows the task needs. For human inspection, the single-device `aua dashboard` detail view has a database workspace backed by the same service and typed mutation/restore confirmations.
8. **Index an app you don't know yet.** `aua explore plan` returns a prioritized worklist — mined deeplinks to probe, dead-end screens to expand. Run the tasks with normal `aua` commands; the results auto-record into the map + playbook, and re-running the plan shows what's left. This is how you (the agent) index an app for aua to remember.
9. **Feed research back and correct the map.** `aua knowledge add` stores an experience with source/agent/session/evidence so future runs inherit it. AUA also creates research tasks automatically when a new map entry is ambiguous or a route is provisional/unreplayable. Research `meta.research_tasks` (or run `aua reconcile plan`) in source/runtime, then submit the canonical JSON report. AUA does not spawn the research agent. `verdict=apply` commits every operation transactionally and returns a rollback id; `review` queues it and `reject` retains the feedback.
10. **Jump to a known screen in one call.** `aua goto "<goal>"` replays the remembered steps of each route edge — by resource-id first, then label — verifying every hop, including cross-app auth legs (Google sign-in through Chrome/GMS is folded into one edge). Prefer it whenever `suggested_gotos` lists your target. Known in-app hops skip OCR and retry it only when hierarchy cannot match a selector or verify arrival; transit screens keep automatic OCR. `--plan` prints the annotated route (steps, replayable, destructive) without acting. Steps matching `memory.destructive_labels` (delete/sign out/pay/…) are refused without `--allow-destructive`. On divergence it hands back the failing step, the remaining steps, and the current elements — finish that one step manually, then re-run `aua goto "…" --from-here` to resume mid-edge (skips steps that already match the current screen; also covers mid-auth). Plain `aua goto` still starts from the current *screen* on the map; `--from-here` is for mid-*edge* (you already tapped some of the recorded steps yourself).
11. **Replay whole journeys in one call (flows).** A flow is a Maestro-style YAML journey you can AUTHOR directly (no walking needed) or record: `aua flow save <name> --last N` materializes your recent actions (typed values become required `${PARAM_n}` placeholders — fill them in the file). `aua flow run <name> --param K=V` drives the whole journey — launch, taps, waits, asserts, cross-app auth, even `goto:` steps — and on divergence returns the failing step index + remaining steps; fix and resume with `--from-step N`. Flows live under `<memory.dir>/flows/*.yaml` (`aua flow list|show|delete`); `--dry-run` previews. Use a flow for any setup you repeat (reset account, log in, reach the screen under test) — one call instead of a dozen.
12. **Optional: let a fast model recover or explore (opt-in).** If `planner.enabled` is set (+ an API key like GEMINI_API_KEY), you get two extras. (1) On a `goto`/`flow` divergence, add `--assist` and a fast planner LLM tries to recover in the same call (dismiss a popup, find the moved element) before handing off — the divergence hint tells you when it's worth trying. (2) `aua navigate "<goal>"` drives to a goal with no prior map AND records the path, so the next `aua goto` is a free deterministic replay. It's OFF by default and never touches the fast path; destructive taps still need `--allow-destructive`.
13. **Drive by element ID.** `aua --format compact analyze` → a list of elements each with an integer `id` + bounds. Act on the id: `aua tap-and-analyze <id>`, `aua input-and-analyze <id> "text"`, `aua swipe-and-analyze up`, `aua key-and-analyze back`. Use `aua has "<text>"` (exit 0/1) to branch cheaply without parsing JSON.
14. **Ask for the columns you want — never post-process JSON.** **`aua --format tsv analyze` is the default way to look at a screen**: one element per line, tab-separated, `#`-commented summary on top, and status-bar/unlabelled noise already dropped (`--all` keeps everything). Narrow it in the same call instead of piping into a filter: `--fields id,text,rid,clickable` (`rid` = the short tail; `resource_id` = the full selector), `--where-text <substr>`, `--where-rid <substr>`, `--clickable`, `--region x1,y1,x2,y2` (header = `--region 0,0,1080,300 --clickable`), `--limit N`, `--nonempty`, `--no-system`, `--no-ime`, `--meta <csv>` / `--no-meta` (the routes and deeplink suggestions are worth reading once, not on every call). On View-based apps add `--no-wrappers` to drop the app's own id'd layout scaffolding (`app_bar`, `content_frame`) — inert, unlabelled boxes that wrap something; leaves and addressable containers stay. Filters of different kinds AND together, repeats of one kind OR together, and **ids are never renumbered** — the id in a filtered row is the id `aua tap-and-analyze` takes. The same flags work on `--format json|compact` when you want machine-readable output.
15. **Read interaction state, don't screenshot it.** Every element carries `checkable`/`checked`/`selected`/`scrollable`/`long_clickable`/`password` alongside `clickable`/`enabled`/`focused`. So *is this switch on?* is `aua --format tsv analyze --where-rid settingsSwitch --fields id,checkable,checked` — not a screenshot you have to look at. `selected` tells you which tab is active; `scrollable` tells you which container actually scrolls. These are **tri-state**: `true`/`false` when the accessibility node reported it, **empty/null when genuinely unknown** (a vision-derived element has no a11y attributes), so off never masquerades as unknown.
16. **Verify by resource-id, not just text.** `aua has --rid <id>` (or `aua has "<id>" --by id`) checks a resource-id (a bare tail like "containerDetail" works) — and it finds non-interactive **container** ids that `analyze` prunes from the element list, so it's the reliable way to assert you reached a screen (Maestro-style `assertVisible: id:`). Guard an action with the same selector you act with: `aua has --rid saveButton && aua tap-and-analyze --rid saveButton`. `wait --for <id> --by id` and `scroll-to <id> --by id` take `--by id` too. If a screen is WebView/Compose-backed and its result text isn't in the tree at all, read it with `analyze --source vision`.
17. **Act, then read the screen the action gives back.** IDs are only valid until the screen changes. By default every state-changing action (tap/input/swipe/scroll-to/key) returns the next screen inline in `observation` (elements with fresh ids). This is the default agent contract: action + `observation` covers the normal readback path, so you should skip `analyze` unless you need another filtered view. Every action response now also includes `observation_present`, `known_screen`, `stable_elements`, `action_diff_summary`, and `note`, so callers can branch on that single payload. `type → tap send` is two calls, not three, and `goto` returns the destination's `elements` too. The MCP surface makes that contract visible in the method name: `tap_and_analyze`, `input_and_analyze`, `scroll_and_analyze`, and the corresponding names for every observed action. The ambiguous short MCP names are not exposed. On the CLI, prefer the matching `tap-and-analyze` / `input-and-analyze` names; these explicit forms force the observation even if `--no-observe` is supplied. Pass `--no-observe` to skip it on action-only sequences. Action `observation` waits for a pixel change + idle (animation-aware) before dumping the tree, and a screen whose content is still streaming in has to hold still for one confirming sample — so you get the *next* screen, not a mid-transition snapshot with the list body missing. The observation is **compact by default** (`id,text,rid,clickable`, app nodes only); widen it with `--observe-fields all` or any field list. You therefore never need the `--no-observe` + `analyze` pair to get a cheap read — that pair costs two round trips for one screen. **That settle can only wait ~1.1s (max 1.6s).** A slower screen makes the action report `nothing changed` for a tap that did land, and `stale_risk` appears in `detail`. That is *not* evidence the tap missed: it cannot tell "no effect" from "not yet", so **never re-tap on it** — a second tap means a second submit. When you know what should come next, say so and the wait becomes evidence-based with your budget instead of the settle timer: `--until "rid:introCard"`, `--until "text:Chats"`, `--until "!text:Loading"` (with `--until-timeout MS`). The response then carries `await_outcome`: `satisfied` / `screen-changed` / `timeout`, and `await_terms` says which term is missing. Prefer `wait --for "<text>"` for known targets; reserve `wait --for-stable` for generation / loading / video.
18. **Wait on state, never sleep.** `aua wait-and-analyze --for "<text>"` waits for text to appear; `aua wait-and-analyze --for-stable` returns once the screen stops visually changing (grid pixel-hash; looping spinners/video are auto-masked so they don't block). Prefer goal waits over `--for-stable` after tabs/taps; never fixed sleeps.
19. **Wait on the backend when the screen cannot tell you.** `await` / `--until` terms are not limited to what is drawn. `net:<[METHOD ]PATH[=STATUS]>` waits for a completed HTTP exchange (`net:POST /v1/chat`, `net:/v1/chat=200`) — mitmproxy's response hook fires at *stream completion*, so it is the honest signal for a streamed chat turn; it needs `aua proxy start` running. `log:<substring>` matches logcat since the wait began and needs no proxy, but is only as good as what the app logs. Terms are ANDed, so `--until "net:POST /v1/chat,text:x ="` reads as *the backend replied and the screen shows it* — which matters because a streamed LaTeX answer reaches the hierarchy as U+FFFD, so no `text:` term alone can confirm it arrived. Both take a baseline when the wait starts, so the previous turn's response can never satisfy this one. Still not network idle: this app never is.
20. **One agent, one emulator — leases are automatic.** With several agents running at once, every one of them otherwise resolves to "the only/first device" and they drive each other's screens; nothing errors, the results are just wrong. So each command **claims a lease** on the device it uses and keeps you on the same one (element ids, app state and the learned map are all per-device). You need do nothing. Identify yourself with `--owner <agent>` or `$AUA_OWNER` if you want readable output; otherwise it is derived and stable for your process. Ask for what the device must support with `--needs root,play,proxy` and you get a capable one or a refusal — never a device that silently cannot do it. **Exit 9 (`device_leased`) means another agent holds it — that is routable, not fatal:** drop `--serial` and one will be picked, or take a different emulator; the hint lists which are free. `aua lease list` shows who holds what, `aua lease release` hands one back early. Leases expire on their own, so a crashed agent blocks nobody and there is nothing to clean up.
21. **Stop the daemon when done.** `aua daemon stop` releases the warm connection.

## Flag placement (this bites people)
**Global** flags go BEFORE the subcommand; **command** flags after.
✅ `aua --format compact analyze --source vision`  ·  ❌ `aua analyze --format compact` ("No such option").
- _global, BEFORE the subcommand_: `--format json|pretty|compact|tsv|delta|msgpack` (`tsv`/`delta`/`msgpack` = analyze; `delta` omits elements when unchanged; `msgpack` is AUA1 binary/base64), `--serial`, `--config`, `--profile`, `--timeout`, `--log-level`, `--no-cache`, `--with-image` (session default: attach raw screenshots on analyze/actions — prefer off; use only when you must SEE pixels)
- _analyze_: `--source auto|hierarchy|vision`, `--query "<nl>"`, `--deep`, `--cheap`, `--strategy <tier>`, `--annotate [path]`, `--with-image [path]` (also save the raw screenshot; path lands in `meta.raw_image`), `--with-ocr/--no-ocr`
- _analyze — views (use these instead of post-processing JSON)_: `--fields <csv>` (`id,text,rid,desc,bounds,center,type,clickable,enabled,focused,checkable,checked,selected,scrollable,long_clickable,password,resource_id,source,confidence`), `--nonempty`, `--no-system`, `--no-ime`, `--no-wrappers`, `--all`, `--where-text <substr>`, `--where-rid <substr>`, `--clickable`, `--region x1,y1,x2,y2`, `--limit N`, `--meta <csv>`, `--no-meta` — repeatable where it makes sense, and free of a device round-trip when the flags are wrong (bad name → exit 2 listing the valid ones)
- _screenshot_: `[PATH] | --out PATH`, `--region x1,y1,x2,y2` (crop before writing), `--scale <factor>`, `--max-width <px>`, `--annotate` (full-screen only) — crop/downscale when you must LOOK at something, so one header icon doesn't cost a 1080x2400 PNG in image tokens
- _daemon / orient_: `daemon start --quiet` skips the app-orientation blob (48 screens, mined deeplinks, notes); read it deliberately with `aua orient` instead — useful once per session, noise on every restart
- _emulator_: `emulator list|status|recommend-proxy|ensure-proxy [--name aua_proxy] [--api 30] [--force] [--start]|start [--avd NAME] [--headless|--windowed] [--gpu host|…] [--parallel] [--port N] [--read-only] [--owner TAG] [--idle-stop 900] [--wait N]|stop [--serial emulator-5554|--avd NAME|--owner TAG|--mine|--all]` — boot headless for unattended verify (Mac defaults to `-gpu host`); **`--parallel` for multi-agent** (unique port + read-only + owner); **always stop yours** (`--serial` / `AUA_OWNER=… --mine`); idle watchdog auto-stops after `--idle-stop` as backup; `ensure-proxy` creates a small rootable google_apis AVD (HTTPS proxy system CA — Play Store AVDs refuse `adb root`)
- _has_: `--by text|id|desc` (id finds pruned containers), `--match exact|contains|regex`, `--ignore-case`, `--ocr-fallback/--no-ocr-fallback`, `--timeout <ms>`
- _wait_: `--for "<text>"` (`--by id`, `--absent` = wait until it disappears), `--idle`, `--for-stable`, `--changed` (any hierarchy fingerprint change), `--interval`, `--settle`, `--timeout`, `--observe` (fresh ids, even on a miss). On timeout exit 3 with detail naming `--match` mode, fields searched, closest candidates — and a hint if the pattern looks like regex under `--match contains`
- _fanout_: `aua fanout [--serials a,b] [--parallel] <cmd…>` — run one subcommand on many devices (each gets `daemon.sock.<serial>`); gathers JSON per serial
- _open_: `<uri> [--package <pkg>]` — **pins the foreground package by default** so prod+dev installs never hit 'Open with…'; `--no-package-pin` to test the chooser; if a chooser still appears, errors naming the competing apps
- _resolve_: `<id|stable_key>` — remap a previous-frame id (or `rid:…` key) onto the current screen after IDs churn
- _app_: `launch <pkg> [--activity .Entry] [--clear --yes]`, `stop|kill|clear|grant`. `clear` / `launch --clear` wipe ALL app data (typically flags + login) — **requires `--yes` / `--yes-wipe-flags`**; re-apply flags afterwards
- _db_: `list <pkg>`, `schema <pkg> <db> [--table NAME]`, `query <pkg> <db> "SELECT …" [--params JSON --limit N]`, `execute <pkg> <db> "UPDATE …" --yes`, `backup|backups|restore`. All coherent reads stop/relaunch the app; execute backs up first and accepts data mutations only; restore preserves the current state as a new safety backup
- _clipboard / paste / copy_: `clipboard set|get`, `paste`, `copy --rid/--text/--desc` (Maestro copyTextFrom / pasteText / setClipboard)
- _erase_: `erase [ID] --chars N` (Maestro eraseText; omit ``--chars`` to clear the whole field)
- _location / orientation / airplane / media / record / clock_: `location set LAT,LON`, `orientation set|get`, `airplane on|off|toggle`, `media add PATH`, `record start|stop PATH`, `clock set --ms <unix-ms>` / `clock restore` (time travel invalidates auth — always restore)
- _logcat_: `logcat mark [NAME]`, `logcat --grep PAT [--since mark|last-action] [--tag T] [--json]` — bracket API/analytics verification around an action
- _suite_: `suite run PATH.yaml [--continue]` — AC checklist (has/expect/wait_for) with per-item pass/fail + summary; exit 8 if any fail
- _capture_: `capture status|last [--seconds N|--since last-action] [--region center]|export PATH.gif|explain [--llm]|on|off|prune|sidecar start|stop` — always-on rolling screencap with the daemon (deduped frames + diff summary / GIF); see `meta.capture_hint` / action `capture_hint` after fast transitions; suite failures attach `capture last --since last-action`. Sneak-peek a headless agent live: `aua dashboard` (separate process — enables capture via daemon or sidecar, opens http://127.0.0.1:8765; **grid** when multiple agents/emulators are online)
- _dashboard_: `dashboard [--serial …] [--grid] [--port 8765] [--no-open] [--poll-ms 500]` — localhost sneak-peek; auto **grid of live screens** when several emulators are online (click a tile for journal/map); the detail view browses debuggable app databases, schema, bounded queries, restore points, and guarded writes; enables capture; does not stop the agent (Ctrl-C closes the dashboard only)
- _dev_: `dev show`, `dev anim off|restore`, `dev crashes on|off`, `dev profile ac|default` (AC: anim off + crashes on; always restore)
- _a11y_: `a11y scroll <id|--rid …> [--forward|--backward]`, `a11y action <id> CLICK|LONG_CLICK|SCROLL_FORWARD|…`; analyze `--no-ime`
- _flags_: `flags set <pkg> KEY=VAL…`, `flags apply file.yaml` — needs a `flags.templates` entry for the package (set-flags schemes are app-specific). Writes, then **verifies** against the app's shared_prefs (`applied`/`ignored`; a dropped key exits 8, `--no-verify` to skip) and **restarts** the app (`--no-restart` to skip) because flags read at cold start ignore a live-process override. A successful restart activates a deterministic map context carrying the verified flags. When `flags.prefs_files` or `flags.context_keys` is configured, ordinary `analyze` also discovers already-active experiment/treatment/variant/flag values and switches map context automatically.
- _proxy / mock_: `proxy start|stop`, `mock map METHOD PATH [--status N --body '{…}']`, `mock record start|stop NAME`, `mock replay NAME` (optional `[proxy]` extra)
- _map_: `--app <pkg>`, `--brief`, `--screen <name>`, `--depth N`, `--find "<goal>"`, `--context <id>`, `--all-contexts`, `--audit`, `--json`
- _goto_: `<goal>` (fuzzy), `--plan` (annotated route, no taps), `--max-steps N`, `--allow-destructive`, `--assist` (opt-in planner recovery), `--from-here` (resume mid-edge after a manual hop / divergence)
- _flow_: `run <name> [--param K=V] [--file PATH] [--dry-run] [--from-step N] [--no-allow-destructive] [--assist]`, `save <name> [--last N] [--force]`, `list|show|delete`. Steps incl. `launch_app`/`stop_app`/`open_link`/`goto`/`flow` / `dev_profile` / `a11y_scroll` / `flags_apply` / `proxy_start`/`stop` / `mock_replay` (a `flow:` step runs a saved flow inline — reuse a shared `login` recipe).
- _open / about / remember_: `open <uri>` deeplink; `about` app playbook; `remember …` teach it
- _knowledge / reconcile_: `knowledge list|show|add|stale`; `reconcile plan|submit|status|apply|rollback` (external-agent JSON contract, transactional correction)
- _explore_: `mine <repo> --app <pkg>` harvests deeplink shortcuts from source into the playbook; `plan` returns a prioritized crawl worklist (probe deeplinks, expand dead-end screens) whose results auto-record
- _navigate (opt-in planner)_: `<goal>` (natural language), `--until <text>`, `--max-steps N`, `--allow-destructive`, `--save-flow <name>` — needs `planner.enabled`
- _observed actions (`*_and_analyze` MCP / `*-and-analyze` CLI)_: return the post-action screen inline (`observation`, fresh ids), and the explicit names cannot disable that readback. Prefer `hide-keyboard-and-analyze` over `key-and-analyze back` when the IME is covering the tree
- _logcat_: `aua logcat mark [NAME]` (default `default`; also auto-marks `last-action` immediately BEFORE every state-changing action, so `--since last-action` covers what the app logged in RESPONSE to it — that is the `act → what did it do` loop), `aua logcat [--grep REGEX] [--since MARK|last-action|30s] [--tag TAG] [--lines N] [--json]` — dump since the mark (default: last-action, else 30s). Windows are in DEVICE time (logcat lines are device-stamped and emulator clocks drift from the host by seconds); `mark` reports `clock`, `host_unix_ms` and the measured `skew_ms` so drift is visible rather than silently eating your window
- _suite_: `aua suite run PATH|-- [--continue] [--json]` — YAML AC checklist of `has` / `expect` / `wait_for` checks; exit 0 all pass, 8 any fail (stop on first fail unless `--continue`)

## The loop
```bash
aua --format tsv analyze         # READ the screen: one element per line, no noise
aua --format compact analyze     # same screen as JSON, when you need it machine-readable
aua ask "describe this screen top-to-bottom"  # screenshot + element graph via VLM
aua tap-and-analyze 4            # act by id and receive the resulting screen
aua input-and-analyze 2 "hello@example.com"  # type and receive the resulting screen
aua --format tsv analyze         # only if you need a narrower/fresher filtered view
```
Cheap presence check to branch on: `aua has "Sign in"` (exit 0 found / 1 not). `aua wait-and-analyze --for "Welcome"` polls until present; `aua wait-and-analyze --for-stable` returns once the screen settles (no OCR/hierarchy — just screenshots).

## Self-routing — the escalation ladder
No LLM decides the route; the engine starts at the cheapest tier that could answer and escalates only on a miss, bounded by config (`routing.max_tier`, default `vision`).
| Tier | Method | Answers |
|---|---|---|
| T0 text | hierarchy text match (`has`) | is this text/element present? |
| T1 selector | hierarchy selector locate | give me THIS known element to act on |
| T2 hierarchy | full hierarchy parse → element list | what's on screen? (`analyze`) |
| T3 vision | detection + OCR (local) | Compose/canvas/game (and weak WebView) trees |
| T4 grounding | grounding VLM (local or paid) | fuzzy/visual targets not resolvable above |

`analyze --query "the gear icon"` resolves from the hierarchy first (free) and only escalates. The default ceiling is **local vision**; reaching the (paid) grounding VLM requires `--deep`. `--cheap` forbids escalation; `--strategy <tier>` pins one. `meta.tier_used` reports which rung actually ran.

## Hard screens (Compose / Flutter / canvas / games)
Compose-without-semantics, Flutter, canvas, and games need vision — the gate escalates automatically. **WebView pages (Google sign-in, web content) usually expose a rich tree and stay on the fast hierarchy path**; only weak/hollow WebView trees escalate. If `analyze` visibly misses content, force it:
```bash
aua --format compact analyze --source vision --annotate
```
`meta.annotated_image` is a PNG with numbered boxes you can open.

## App memory (auto-recorded)
The tool maintains a persistent, **local-only** map per app under `memory.dir` (default `~/.android-ui-analyser`). Every `analyze` records the current screen and every state-changing action records a route edge — no extra calls, and the daemon path records too. Read it back with `aua map` / `aua map --find "<goal>"`. On a revisit, `meta.known_screen` names the recognised screen; a changed signature or app version flags it `stale` so you re-verify. Only the **durable skeleton** is stored (screens, routes, stable elements); dynamic lists are stored as a *shape*, and `EditText` values / secrets / PII are redacted (`<filled>` / `<redacted>`). The map is pushed to you inline on every `analyze` (`meta.known_routes` / `meta.suggested_gotos` / `meta.map_hint` / `meta.research_tasks`), ranked by your recent navigation so the screens you use most surface first; `aua goto "<goal>"` drives a remembered route in one call. **Cross-app auth legs (Google sign-in via Chrome/GMS, permission dialogs) fold into the origin app's route** and replay step by step — a redacted account row hands off for one manual tap, then re-running `goto` resumes. Replay refuses destructive steps (delete/sign out/…) without `--allow-destructive`; an auto-recorded route is provisional until observed twice (or landed on as a known post-action screen), and selectorless routes are rejected from `goto`. Schema v4 scopes screens/routes to automatically discovered deterministic feature-flag contexts; exact-context routes outrank trusted `legacy-default` fallbacks. Stable resource namespaces produce locale-independent destination names, while logical destinations group flag variants and loading/error/empty/ready states instead of acquiring numeric suffixes. `aua map --audit` persists source/runtime research tasks. Store agent feedback with `aua knowledge add`; exchange corrections through `aua reconcile plan|submit`, where `verdict=apply` is validated, snapshotted, committed atomically, and rollbackable. Manage with `aua memory show|path|update|forget` (`memory update --screen <name>` renames a badly-auto-named screen so `goto <name>` reads naturally).

## Headless / unattended verify
When you shipped a change and just need confidence it works — and the user should **not** see an emulator window pop up — prefer a headless AVD:
```bash
aua devices                          # already have a device? use it
aua emulator list                    # marks Play Store vs rootable
aua emulator start --headless        # -no-window; waits until adb is ready
aua daemon start --quiet
aua --format compact analyze         # same analyze path as a headed emulator
# … drive the flow under test …
aua emulator stop --serial <serial>  # REQUIRED — or: stop --mine / --avd <name>
```
**Parallel agents on one host** (each owns one emulator):
```bash
aua emulator start --headless --parallel --avd Pixel_7
# → {serial, port, owner}; pin: export AUA_SERIAL=<serial> AUA_OWNER=<owner>
aua --format compact analyze
# …
aua emulator stop --serial "$AUA_SERIAL"   # only yours — never bare stop --all
```
Sneak-peek all of them: `aua dashboard` (auto **grid** when multiple are online).
Headless on Mac uses **host GPU** (Metal). **Always stop AVDs you started** before ending — idle `--idle-stop` (default 900s) and MCP exit cleanup are backups only.
For **proxy / mock HTTPS** (apps that only trust system CAs), Play Store AVDs will not work — create a small rootable one:
```bash
aua emulator recommend-proxy         # package + why (no download)
aua emulator ensure-proxy --start    # download google_apis image + boot aua_proxy
aua --serial <serial> proxy start
aua emulator stop --mine             # cleanup when done
```
Analyze/tap/wait work identically; hierarchy + screenshots do not need a visible window. Never wipe or stop an emulator the user already had open unless they asked.

## Agent best practices (do / don't)

Other agents: treat this as the operating contract. Most wall-clock on a real run is **your** round trips, sleeps, and pixel guessing — not aua. Follow the **Do** column; the **Don't** column is how agents usually make aua look slow.

### Perception & action
| Don't | Do | Why |
|---|---|---|
| Drive with raw `adb` + screenshots + pixel taps | Drive with `aua` (`analyze` / `--rid` / `tap-and-analyze` / `input-and-analyze` / `has` / `wait-and-analyze`) | Pixels break on density/layout; ids and resource-ids do not. Discovery via screenshots is minutes; aua discovery is seconds. |
| Hand-roll `adb exec-out run-as` + DB/WAL copies + host sqlite + push-back | Use `aua db list|schema|query|execute|backup|restore` | AUA targets the leased device/package, snapshots sidecars coherently, returns JSON, and makes every confirmed mutation recoverable. |
| `analyze` after every `tap`/`input` (or always `--no-observe` then re-analyze) | Use the explicit `*-and-analyze` action and consume its compact `observation`; narrow it with `--observe-fields`, and add `--until "<predicate>"` when the next screen is slow | Post-action observation already has fresh ids. Extra analyzes double round trips. Measured on a 5-scenario run: 37 taps became 73 `analyze` + 37 `wait` calls this way, roughly 60% of wall-clock spent on avoidable round trips. |
| Re-tap when an action reports `nothing changed` / `stale_risk` | Re-read (or pass `--until`), never re-tap | The settle gives up after ~1.1s, so a slow screen reports `unchanged` for a tap that landed. Re-tapping means a second submit / second purchase — the one failure this contract exists to prevent. |
| Force `--source vision` / `--with-ocr` / `--no-ocr` on every screen | Leave `analyze` defaults alone; escalate vision only when the tree misses content (Compose/canvas/game, or `meta.lossy_text`) | Parallel OCR is cheap (~100ms) and prevents empty/broken reads. The map skips OCR automatically once a screen has enough hierarchy-only evidence — do not second-guess it. |
| Guess coordinates or scrape `uiautomator dump` yourself | Act with integer `id` from analyze, or stable `--rid <tail>` / `has --rid` | That is the whole point of aua — selectors, not geometry. |
| Hand-roll `clipboard set` + `paste` (or IME keys) for typing speed | Just `aua input-and-analyze <id|--rid …> "…"` — prefers one-shot `set_text`, then clipboard paste (clipboard restored), then IME keys | `input-and-analyze` owns the fast path and readback; agents should not reinvent it. |
| Use `key back` to dismiss the keyboard | `aua hide-keyboard-and-analyze` | `key back` often navigates away from the screen instead of only closing the IME. |

### Memory, map, and shortcuts
| Don't | Do | Why |
|---|---|---|
| Re-explore an app from scratch every session | Read `aua about`, follow `meta.suggested_gotos` / `suggested_deeplinks`, use `aua goto "<goal>"` / `aua map --find` | The map is the previous agent's gift. Ignoring it re-pays discovery cost. |
| Tap through 5 screens to reach a known destination | `aua open-and-analyze "<deeplink>"` or `aua goto "<goal>"` / a saved `aua flow run` | One call beats a hand-rolled path. Flows collapse whole journeys. |
| After a goto handoff / mid-path manual hop, re-walk from the start or invent the rest | `aua goto "<goal>" --from-here` | Skips remembered steps that already match the current screen (mid-edge resume). Plain `goto` starts from the map's current *screen*; `--from-here` is for mid-*edge*. |
| Keep discoveries only in the chat transcript | `aua remember` / `aua knowledge add` (and fix bad names with `memory update`) | The next agent (or you tomorrow) will not see this chat — write it into the playbook. |
| Start timed work before the daemon is warm | `aua daemon start` (optional: `aua-fast` for hot commands) | Cold Python/connect startup dwarfs hierarchy dump cost on short commands. |

### Waiting & speed (measured)
Measured on a real 6-scenario lane: 1348s wall clock, 239 aua calls, and only ~33s (2.5%) inside aua. Agent turns were ~48%; blind `sleep` burned 251s (19%). Optimise round trips, not aua internals.

| Don't | Do | Why |
|---|---|---|
| `tap --rid x --no-observe` then `analyze` | `tap-and-analyze --rid x` | A tap RETURNS the resulting screen by default. The two-call habit doubled a lane's round trips: 66 taps followed by 52 analyzes. |
| `sleep 8` after an action | `wait --for <text|id> --observe` | Returns the moment it appears, and hands you the screen. A sleep is slower when short and wrong when the screen is not ready. |
| Extra `wait --for-stable` / sleeps between hops of `goto` / `flow run` | Just run `goto` / `flow` — route replay already settles on the next known selector (`has` on rid/label) before falling back to pixel settle | Smarter settle is built in; agent-side waits only add turns. |
| `wait --for-stable` after tapping send | `wait --after-change` | Nothing has changed yet, so the screen is already 'stable': --for-stable returned in 1.6s with NO answer on screen; --after-change returned in 4.9s WITH it. Measured. |
| `sleep 30` for image generation | `wait --after-change` (or `--for-stable --settle 1500`) | Same trap, bigger waste. Wait on the condition, never the clock. |
| One shell call per assertion | Group independent checks in one call | Each extra call is another agent turn, ~6s of wall clock. |
| Screenshot every step | Screenshot what you will cite | 68 screenshots in one lane; most were never referenced. |
| Trust hierarchy text containing `?` | Re-read with `analyze --source vision` | U+FFFD means the glyph never reached you. `meta.lossy_text` now flags it. A formula answer read as 'solve for <?>: <?>' in hierarchy; OCR read '2x = 8' correctly. |
| `--parallel` to prepare an AVD you will install into | `--parallel --no-read-only` | --parallel implies -read-only, so an `adb install` lands in a discarded overlay and reports Success. The app is simply gone after stop. |
| Block the suite on an LLM/chat reply you do not assert on | Assert the UI affordance you need (`has --rid sendButton` / favorite), or mock the API | Model latency is app time, not tool time — waiting for a full answer inflates every run. |

## Worked examples
```bash
# No device attached? Boot headless so you don't bother the user:
aua emulator start --headless       # or: --avd pixel7

# Optional: warm daemon so every later call is ~tens of ms.
aua daemon start --quiet            # `aua orient` prints the app playbook on demand

# See the screen. When the app is mapped the response already carries
# meta.known_screen + meta.known_routes + meta.suggested_gotos — act on those.
aua --format tsv analyze

# Just the header, just the tappable things (no JSON post-processing):
aua --format tsv analyze --region 0,0,1080,300 --clickable --fields id,desc,rid

# Is that switch on? Read the boolean instead of looking at a screenshot:
aua --format tsv analyze --where-rid settingsSwitch --fields id,checkable,checked

# Must you actually SEE something? Crop it — a full 1080x2400 PNG is expensive:
aua screenshot --region 0,0,1080,300 --out /tmp/header.png   # then read that file

# Jump straight to a remembered screen (drives + verifies each hop,
# including cross-app auth legs):
aua goto "image creator"
aua goto "settings" --plan          # just print the route, take no action
aua goto "settings" --from-here     # resume mid-edge after a manual step

# Replay a whole journey (authored or recorded) in ONE call:
aua flow run reset_account_google_login --param ACCOUNT="Engineering Team"
aua flow save reach_checkout --last 8   # materialize what you just did

# Starter journey: open → tap → input → tap → wait → has → tap. Every action
# returns the post-action screen, so each id below comes from the previous call:
aua open-and-analyze "myapp://chat"       # response carries observation + fresh ids
aua tap-and-analyze 24                    # id from the open response
aua input-and-analyze 25 "How much?"      # id from the tap response
aua tap-and-analyze 26                    # send-button id from that same response
aua wait-and-analyze --for "How much?"     # confirm and receive the settled screen
aua has --rid resultBubble && echo present  # cheap branch, exit 0 present / 1 absent
aua tap-and-analyze 31                    # continue on another read-back id
```

An action response carries its own state, so a follow-up `analyze` is usually
unnecessary — reach for one when you need a *different* view (another region, OCR,
or a filtered projection), or when content was still streaming in on the first read.
```json
{
  "ok": true,
  "action": "tap",
  "observation_present": true,
  "known_screen": "chat",
  "stable_elements": [{"id": 25, "stable_key": "compose_input"}, {"id": 26, "stable_key": "send"}],
  "action_diff_summary": {"added": 0, "removed": 0, "changed": 2, "prev_count": 17, "curr_count": 17},
  "note": "No separate analyze needed; state is in observation."
}
```

## Output schema (read these fields)
```json
{ "schema_version": 1,
  "screen":   { "width", "height", "package", "activity", "source": "hierarchy|vision|mixed" },
  "elements": [ { "id", "type", "text", "resource_id", "content_desc",
                  "bounds": [x1,y1,x2,y2], "center": [x,y],
                  "clickable", "enabled", "focused",
                  "checkable", "checked", "selected",      // tri-state:
                  "scrollable", "long_clickable", "password",  // null = unknown
                  "source": "hierarchy|detection|ocr|grounding", "confidence" } ],
  "meta":     { "duration_ms", "tier_used", "path", "providers_used",
                "known_screen", "known_routes", "suggested_gotos", "research_tasks",
                "map_hint",
                "annotated_image", "raw_image", "device_serial" } }
```
`compact` drops null/default fields for the smallest token footprint — except `checked` on a `checkable` node, where *off* is the answer you asked for.
Don't post-process this by hand. `--format tsv` plus `--fields`/`--where-*`/`--region`/`--limit` gives you exactly the rows and columns you want in the same call (see the flag table above); `--all` turns tsv's implicit noise filtering off.

Action command responses always include a small contract wrapper so `analyze` is usually not needed:
```json
{"ok": true,
  "action": "tap",
  "observation_present": true,
  "known_screen": "chat",
  "stable_elements": [
    {"id": 25, "stable_key": "compose_input"},
    {"id": 26, "stable_key": "send"}
  ],
  "action_diff_summary": {"added": 0, "removed": 0, "changed": 2, "prev_count": 17, "curr_count": 17},
  "note": "No separate analyze needed; state is in observation.",
  "observation": { "screen": {...}, "elements": [...], "meta": {...} }
}
```
If `observation_present` is false, the action did not request a post-action read (`--no-observe` or unsupported action), so run `analyze` explicitly.
Need the actual pixels too? `--with-image [path]` on `analyze` AND on every action (tap/input/swipe/scroll-to/key/open) saves the raw screenshot to a timestamped file and returns its path in `meta.raw_image` (on actions: inside `observation.meta`) — Read that file when you must SEE the screen (visual fidelity, images, charts) instead of just addressing it. Over MCP the image comes back inline as an image content block. **Default off.** Do not pass `--with-image` on every step — hierarchy/TSV is faster and cheaper; images erase the token advantage of acting by id.

## Exit codes
| Code | Meaning |
|---|---|
| 0 | success (`has`: text present) |
| 1 | `has`: text not present · OR unexpected internal error (structured `internal_error` on stderr) |
| 2 | usage error |
| 3 | no device / device error / `wait --for-stable` timeout |
| 4 | provider error (fallback chain exhausted) |
| 5 | config error |
| 6 | selector matched nothing (`--rid`/`--text`/`--desc`) |
| 7 | selector matched several candidates (disambiguate with `--index`/`--first`) |
| 8 | `aua expect-and-analyze` / `aua suite run` assertion failed |

Errors print `{"error":{"code","message","hint"}}` to **stderr**; JSON results go to **stdout** (pipe-clean).

## Config & providers (only if asked to change perception)
Config is the nearest `.android-ui-analyser.yaml` (project) → user config; inspect with `aua config show` / `aua config path`, scaffold with `aua config init`. Swap a model with one line (e.g. `ocr.chain: [apple_vision, rapidocr]`). **Secrets are env-var names only** (`api_key_env: OPENAI_API_KEY`); set the env var — never paste keys. Check readiness with `aua doctor` (it never prints secret values).
`aua ask` is provider-neutral: configure `grounding.chain: [gemini, openai]`. The factory tries that order and skips providers whose API-key env var is absent, so one config works with either key. Reverse the list to prefer OpenAI when both exist. On macOS, Apple Vision OCR and hierarchy capture fuse into one observation (parallel when OCR is forced; auto mode consults the map first). Screens the map has seen hierarchy-only enough times (and never needed OCR) skip OCR on later visits — cheaper analyze without risking unknown screens. Web content inside a Custom Tab stays visible to a plain `analyze`. Readings that only repeat text the tree already reports are withheld (`ocr.drop_redundant`); pixel-only text always survives. OCR works on a 720px preview and maps boxes back to original screen coordinates. Route replay settles on the next step's known selector when possible instead of a full pixel `wait_stable`.
