{"owner":"mobile-dev-inc","repo":"Maestro","hasSkills":true,"totalSkillsCount":49,"totalTokensCount":20288,"categories":["subagent-persona","anthropic-skill","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":[".claude/agents/diagnose-maestro-failure.md",".claude/skills/bump-android-version/SKILL.md","e2e/demo_app/.maestro/commands/addMedia.yaml","e2e/demo_app/.maestro/commands/assertNotVisible.yaml","AGENTS.md","e2e/demo_app/.maestro/commands/assertScreenshotCroppedHEIC.yaml","e2e/demo_app/.maestro/commands/assertVisible.yaml","e2e/demo_app/.maestro/commands/assertScreenshotCroppedJPG.yaml","e2e/demo_app/.maestro/commands/assertScreenshotCropped.yaml","e2e/demo_app/.maestro/commands/assertTrue.yaml","e2e/demo_app/.maestro/commands/back.yaml","e2e/demo_app/.maestro/commands/clearState.yaml","e2e/demo_app/.maestro/commands/copyTextFrom.yaml","e2e/demo_app/.maestro/commands/doubleTapOn.yaml","e2e/demo_app/.maestro/commands/eraseText.yaml","e2e/demo_app/.maestro/commands/evalScript.yaml","e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml","e2e/demo_app/.maestro/commands/hideKeyboard.yaml","e2e/demo_app/.maestro/commands/inputRandomCityName.yaml","e2e/demo_app/.maestro/commands/inputRandomColorName.yaml","e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml","e2e/demo_app/.maestro/commands/inputRandomEmail.yaml","e2e/demo_app/.maestro/commands/inputRandomNumber.yaml","e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml","e2e/demo_app/.maestro/commands/inputRandomText.yaml","e2e/demo_app/.maestro/commands/inputText.yaml","e2e/demo_app/.maestro/commands/killApp.yaml","e2e/demo_app/.maestro/commands/launchApp.yaml","e2e/demo_app/.maestro/commands/longPressOn.yaml","e2e/demo_app/.maestro/commands/openLink.yaml","e2e/demo_app/.maestro/commands/pasteText.yaml","e2e/demo_app/.maestro/commands/pressKey.yaml","e2e/demo_app/.maestro/commands/repeat.yaml","e2e/demo_app/.maestro/commands/retry.yaml","e2e/demo_app/.maestro/commands/runFlow.yaml","e2e/demo_app/.maestro/commands/runScript.yaml","e2e/demo_app/.maestro/commands/scroll.yaml","e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml","e2e/demo_app/.maestro/commands/setAirplaneMode.yaml","e2e/demo_app/.maestro/commands/setLocation.yaml","e2e/demo_app/.maestro/commands/setOrientation.yaml","e2e/demo_app/.maestro/commands/stopApp.yaml","e2e/demo_app/.maestro/commands/swipe.yaml","e2e/demo_app/.maestro/commands/swipe_from_point.yaml","e2e/demo_app/.maestro/commands/takeScreenshot.yaml","e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml","e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml","e2e/demo_app/.maestro/commands/travel.yaml","e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml"],"skills":{".claude/agents/diagnose-maestro-failure.md":"---\nname: diagnose-maestro-failure\ndescription: Use when a Maestro test-android run has failed and a structured per-flow diagnosis is needed without bloating the caller's context. Accepts either a local artifact directory or a GitHub Actions run/job URL. Reads commands JSON, screenshots, and maestro.log; classifies each newly-failing passing/ flow; identifies cascade groups; and returns a structured report with proposed fixes. Does NOT edit code or commit — the caller is responsible for consent and application.\ntools: Bash, Read, Agent\n---\n\n# Diagnose Maestro Failure (subagent)\n\nYou diagnose Maestro `test-android` failures from CI artifacts and return a structured report. You do not modify code, commit, or push — the caller handles consent and application.\n\n## Input\n\nThe caller passes one of:\n\n- **Local artifact directory** — e.g. `/Users/.../maestro-root-dir-android` or `/tmp/maestro-android-<run_id>`. Must contain a `tests/` subtree.\n- **GitHub Actions URL** — `https://github.com/mobile-dev-inc/Maestro/actions/runs/<run_id>` or `.../runs/<run_id>/job/<job_id>`.\n\nIf a URL, extract the `run_id` and download:\n\n```bash\nRUN_ID=<extracted>\nDEST=/tmp/maestro-android-$RUN_ID\ngh run download \"$RUN_ID\" --repo mobile-dev-inc/Maestro \\\n  --name maestro-root-dir-android -D \"$DEST\"\n```\n\nIf the directory has no `tests/demo_app/passing/` subtree (e.g. iOS artifact, or the run failed before tests), report that and stop.\n\n## Where to Look\n\nOnly `<artifact_root>/tests/demo_app/passing/` is signal. `failing/` is the negative-path suite — its commands are *supposed* to fail. Ignore it entirely.\n\n```bash\ncd <artifact_root>/tests/demo_app/passing\ngrep -l '\"FAILED\"' commands-*.json\n```\n\nThe status field lives at `.[].metadata.status`. Possible values: `COMPLETED` (success) and `FAILED` (terminal failure). `commands-*.json` is the **only** authoritative source — `screenshot-❌-*.png` is not.\n\n**`commands-(retry).json` is the canonical example.** This flow exercises the `retryCommand` directive: a tap fails, Maestro retries it, the retry succeeds, and the flow passes. In the artifact:\n\n- All entries (including the recovered `tapOnElement`) have `metadata.status = \"COMPLETED\"` — Maestro writes only the final outcome of a recovered command, not the failed attempt.\n- A `screenshot-❌-<ts>-(retry).png` still exists, captured at the moment of the failed attempt before the retry succeeded.\n- The grep above does **not** match this file (no `\"FAILED\"` string anywhere), so the agent never investigates it. This is correct behaviour.\n\nThe risk is treating the `❌` screenshot as evidence of failure on its own. Don't. Trust the candidates from grep — they are the flows whose JSON contains a terminal `FAILED`. Any other `❌` screenshot in `passing/` is from a recovered retry and is irrelevant.\n\n## Per-Flow Investigation\n\nFor each candidate flow returned by the grep above:\n\n1. **Open `commands-(<flow>).json`.** Locate the entry whose `metadata.status == \"FAILED\"` — that's the terminal failure. Record: command type (the single key under `command`), error message, `metadata.timestamp` (ms epoch), `metadata.sequenceNumber`, `metadata.duration` (≈17000 ms = default-wait timeout, indicating UI-blocked, not crash).\n2. **Open the screenshot `screenshot-❌-<timestamp>-(<flow>).png` whose timestamp matches the FAILED entry.** This is the highest-signal artifact — JSON and logs alone misclassify when a system dialog is invisible to them. Always read it. Ignore `❌` screenshots from other timestamps in the same file — those are recovered retries.\n3. **Grep `maestro.log`** for the flow name and the timestamp window for surrounding driver activity, gRPC errors, and stack traces.\n\n## Classification\n\n| Screen at failure | Class | Where the fix lives |\n|---|---|---|\n| Expected app screen, but selector wouldn't match | Test flow issue | `e2e/demo_app/.maestro/**/*.yaml` |\n| Android / GMS system dialog blocking the app | Maestro driver gap (auto-grant / auto-dismiss missing) | `maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt` or `.github/workflows/test-e2e.yaml` (emulator pre-config) |\n| Pixel launcher visible behind a system dialog | Same — dialog stole focus across `launchApp` / `clearState` / `stopApp` | Same as above |\n| App crash dialog / blank screen | App-side issue | Out of scope — report only |\n| Stack trace from `maestro.*` in `maestro.log` | Maestro framework bug | `maestro-client/` / `maestro-orchestra/` / `maestro-android/` |\n\nFor anything classified as **driver gap** or **framework bug**, dispatch an `Explore` subagent into the relevant module(s) to find the offending code path before proposing line numbers. Never guess.\n\n## Cascade Detection (critical)\n\nA single system dialog can fail many subsequent flows because `clearState` / `launchApp` / `stopApp` don't dismiss system overlays. List FAILED entries by ascending timestamp; if downstream screenshots show the *same* overlay, group them under the **root trigger** (the earliest flow that raised the dialog). Propose **one** fix per root, not N fixes for the cascade.\n\n## Required Output Format\n\nReturn this structure verbatim — the caller parses it:\n\n````markdown\n## Summary\n<one-line: how many flows failed, how many root causes, how many cascade groups>\n\n## Root causes (caller acts on each)\n\n### 1. <flow-or-group-label> — <classification>\n- **Trigger:** <what raised this>\n- **Affected flows:** <flow1>, <flow2>, ... (N flows)\n- **Cascade?** yes/no\n- **Proposed fix:**\n  - File: `<exact path>:<line range>`\n  - What it does: <one sentence>\n  - Diff (unified or pseudo):\n    ```\n    - old line\n    + new line\n    ```\n  - Side effects: <if any, else \"none\">\n  - Verified via Explore? <yes — line numbers confirmed | no — fix is in CI yaml or test flow yaml, line numbers from direct read>\n\n### 2. ...\n\n## Out of scope\n- `<flow>` — <reason, e.g. app crash, environmental>\n\n## Notes\n<anything the caller should know but isn't a fix proposal>\n````\n\n## Constraints\n\n- Do NOT edit, write, or commit any file. Diagnosis only.\n- Do NOT propose a separate fix for each member of a cascade group.\n- Do NOT guess at Kotlin line numbers — verify via Explore subagent first.\n- Do NOT report `failing/` flows as failures.\n- Do NOT trust `❌` screenshots on their own. The grep filter (`grep -l '\"FAILED\"' commands-*.json`) is the source of truth for which flows had a terminal failure. A `❌` screenshot inside a flow whose JSON has zero `FAILED` entries is from a recovered retry and is not evidence of regression. `commands-(retry).json` is the canonical example — see \"Where to Look\" for details.\n- Always read the screenshot, even when JSON+log seem to point clearly at a cause — the screenshot is what reveals system overlays.\n\n## When to Stop and Hand Back\n\nHand back as soon as every passing-suite FAILED command has been either (a) classified with a proposed fix, or (b) classified as out-of-scope with a reason. Do not loop, retry, or attempt to apply fixes.\n",".claude/skills/bump-android-version/SKILL.md":"---\nname: bump-android-version\ndescription: Use when bumping Maestro's Android compileSdk/targetSdk to a new API level and validating end-to-end against the test-e2e GHA workflow until the test-android job is green.\n---\n\n# Bump Android Version\n\n## Overview\n\nDrive an Android-version bump in Maestro commit-by-commit on a single feature branch: edit gradle, rebuild the on-device driver APKs, push a draft PR, dispatch `test-e2e.yaml` against the new system image, watch the `test-android` job, and diagnose+fix any newly-failing `passing/` flows. Loop until green.\n\n## When to Use\n\n- User asks to bump Maestro's Android `compileSdk` / `targetSdk` (e.g. \"bump to API 36\").\n- User wants to validate Maestro against a new Android system image.\n\nOne commit per logical step. Don't bundle the gradle bump and the APK rebuild — they need to be separately revertible. After a failing run, fixes are committed and the workflow re-dispatched. Loop until green; do not stop on the first re-dispatch.\n\n**The loop only acts on `passing/` failures.** `tests/demo_app/passing/` is the regression-detection suite — its flows are expected to pass, so a failure there is a real signal. `tests/demo_app/failing/` is the negative-path suite (its flows are *expected* to fail) and is ignored end-to-end. The diagnose agent rejects `failing/` artifacts and rejects retry-recovered flows in `passing/` (see [`.claude/agents/diagnose-maestro-failure.md`](../../agents/diagnose-maestro-failure.md)). If the run is otherwise green except for `failing/`, that's a green run.\n\n## Pre-flight\n\n- Working tree clean (`git status`).\n- `main` up to date: `git fetch origin && git checkout main && git pull --ff-only origin main`.\n- The workflow's `workflow_dispatch` inputs `android_version` (and optional `app` / `flow`) must exist on the branch. \n- `android_version` is a `choice` input — its options live in `.github/workflows/test-e2e.yaml`. If `<new>` isn't already listed (e.g. bumping to API 37), add it to the `options:` list in a separate commit before dispatching, otherwise the workflow rejects the input.\n- The `validate-inputs` job rejects `android_version <= android-29`. Bumps below API 29 aren't supported.\n- Branch name: `bump-android-api-<old>-<new>` (e.g. `bump-android-api-34-36`).\n\n```bash\ngit checkout -b bump-android-api-<old>-<new> origin/main\n```\n\n## Commit 1: bump compile/target SDK\n\nRead current values from `maestro-android/build.gradle.kts` (`compileSdk` and `targetSdk`). Tell the user the current values verbatim, then ask for the new target:\n\n> Current `compileSdk` / `targetSdk` = `<N>`. What's the new API level?\n\nEdit both lines. Also `rg -n \"compileSdk\\s*=|targetSdk\\s*=\" --type kotlin --type-add 'kotlin:*.kts'` to catch consistent occurrences in other modules — only update those that intentionally track the same SDK. Commit:\n\n```bash\ngit add maestro-android/build.gradle.kts <other touched files>\ngit commit -m \"chore(android): bump compile/target SDK from <old> to <new>\"\n```\n\n## Commit 2: rebuild driver APKs\n\n```bash\n./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest\n```\n\nThe build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update three checked-in files. Stage and commit *only* those:\n\n```bash\ngit add maestro-client/src/main/resources/maestro-app.apk \\\n        maestro-client/src/main/resources/maestro-server.apk \\\n        maestro-client/src/main/resources/maestro-android-source.sha256\ngit commit -m \"chore(android): rebuild driver APKs against API <new>\"\n```\n\nIf `assemble` fails, **stop and surface the failure to the user** with the gradle error and a suggested fix. Do not proceed without rebuilt driver APKs. Once we have assemble fixed and new drivers are commited, we can proceed to the next step.\n\n## Step 3: push draft PR + dispatch\n\n```bash\ngit push -u origin bump-android-api-<old>-<new>\ngh pr create --draft \\\n  --title \"chore(android): bump compile/target SDK to <new>\" \\\n  --body \"Validates Maestro against API <new>. Driver APKs rebuilt. Test-e2e dispatched with system-images;android-<new>;google_apis;x86_64.\"\n\ngh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n  -f android_version=\"android-<new>\"\n\n# capture the run id\nsleep 3\ngh run list --workflow test-e2e.yaml --branch bump-android-api-<old>-<new> --limit 1 --json databaseId,status\n```\n\nAlways pass `-f android_version=android-<new>` — the default is `android-32` and would silently re-test the old API. The workflow constructs the full `system-images;...` string internally. `app` and `flow` are optional narrowing knobs (defaults: `demo_app`, all flows); leave them off for the full bump-validation run.\n\n## Step 4: watch the test-android job\n\n```bash\ngh run watch <run_id>\n```\n\nBlock in the foreground; the run is the bottleneck. **Green here means every `passing/` flow passed (terminally — retry-recovered counts as passed). `failing/` outcomes do not affect the verdict.** On green: update PR from draft → ready, done.\n\nOn red, download artifacts:\n\n```bash\ngh run download <run_id> --name maestro-root-dir-android -D /tmp/maestro-android-<run_id>\n```\n\n## Step 5: diagnose (delegated to subagent)\n\nDispatch the **`diagnose-maestro-failure`** subagent with the artifact directory `/tmp/maestro-android-<run_id>` (or the GHA run URL — the subagent handles both). It diagnoses each passing-suite failure in isolation and returns a structured report grouped by root cause (cascades collapsed). It does **not** apply fixes — that's your job here.\n\nWhen the subagent returns:\n\n**Fixes go in Maestro source — not in `.github/workflows/test-e2e.yaml`.** A failing flow on a new API level reflects a behaviour Maestro users will also hit on their own machines/CI, not just ours. Patching the GHA workflow (e.g. extra `adb shell settings put …`, command-line tweaks, AVD setup steps) hides the regression from real users and ships a Maestro that's only green inside our CI.\n\nAcceptable fix targets and their roles are documented in [`AGENTS.md`](../../../AGENTS.md) (modules `maestro-android/`, `maestro-client/`, `maestro-orchestra/`, `e2e/demo_app/`). Driver-behaviour fixes belong in `maestro-android/` or `maestro-client/`; fixture-only issues belong in `e2e/demo_app/`. Editing the driver source means rebuilding the driver APKs (`./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest`) before re-dispatching.\n\nValid edits to `test-e2e.yaml` during this loop are workflow-shape changes (matrix, retention, dispatch inputs) — and the narrow exception below. Anything else (e.g. extra `adb shell settings put …` to mask a Maestro driver gap) is off-limits. If the subagent proposes a workflow patch, push back: ask it (or yourself) where the same fix would live in `maestro-android/` or `maestro-client/` so users on the new API level inherit it automatically.\n\n**Exception — third-party app first-run UI not triggered by a Maestro API.** A pre-installed app's own onboarding (Chrome Welcome / \"Make Chrome your own\", browser default-app picker, Play Protect prompts that fire at app launch) is environment-harness state, not driver behaviour. Maestro doesn't expose a public API that triggers them — they're side effects of the OS image we picked. These may be pre-disabled in the AVD setup step (typical knobs: `setprop debug.chrome.command_line`, `settings put global …` flags). Test: *if Maestro called nothing related to this dialog, would it still appear?* If yes → CI workaround is fair game. If no (e.g. `setLocation` triggers GMS Location Accuracy via `FusedLocationProviderClient` inside the driver) → fix the driver instead.\n\n1. For each root cause in its report, present the proposed diff to the user (file path + one-line summary + diff + side effects). Reject any proposal that targets `test-e2e.yaml` for driver-behaviour reasons before showing it — re-scope to Maestro source first.\n2. **Ask consent explicitly per fix:**\n   > Proposed fix for `<root cause>`: `<one-liner>`. Apply?\n3. On approval: edit, commit with a focused message — don't bundle unrelated fixes.\n   ```bash\n   git commit -m \"fix(android-<new>): <summary>\"\n   ```\n4. On rejection: skip that fix; record what was rejected so you don't re-propose it next iteration.\n\nAfter all approved fixes for this iteration are committed:\n\n- **If any fix was applied:** push and re-dispatch, then loop back to **Step 4** with the new `run_id`.\n\n  ```bash\n  git push\n  gh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n    -f android_version=\"android-<new>\"\n  ```\n\n- **If no fixes were applied** (everything was out-of-scope, or the user rejected every proposal): pause and ask the user how to proceed. Do not keep dispatching.\n\n## Termination\n\nStop when `test-android` is green on the latest run. Then:\n\n```bash\ngh pr ready <pr_number>\n```\n\nIf the loop has gone three iterations without progress (same flow keeps failing for different reasons after fixes), pause and ask the user how to proceed — don't spin indefinitely.\n\n## Anti-patterns\n\n- **Bundling gradle + APK rebuild into one commit** — kills bisectability. Always two commits.\n- **Dispatching with default inputs** — `android_version` defaults to `android-32` and hits the old API. Always pass `-f android_version=android-<new>`.\n- **Forgetting to extend the `android_version` choice enum** — the workflow rejects API levels not listed in `options:`. Add `<new>` to the enum in a separate commit before the first dispatch.\n- **Auto-applying Maestro source fixes without consent** — every Kotlin patch needs explicit user approval first.\n- **Patching `.github/workflows/test-e2e.yaml` to mask a driver behaviour gap** — workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/` or `e2e/demo_app/` instead so the fix ships with the driver APKs.\n- **Treating `failing/` artifacts as regressions** — that suite is expected to fail.\n- **Treating `❌` screenshots in `passing/` as regressions on their own** — when `retryCommand` recovers a failed attempt, Maestro writes only the final `COMPLETED` entry to `commands-(<flow>).json` but leaves the `❌` screenshot from the failed attempt on disk (e.g. `screenshot-❌-<ts>-(retry).png` for the `retry` flow itself). The diagnose agent uses `commands-*.json` containing a terminal `FAILED` entry as the source of truth — orphan `❌` screenshots without a matching FAILED entry are noise.\n- **Committing on `main`** — always work on the bump branch. Verify with `git status` before every commit.\n- **Skipping the screenshot read** — the screenshot tells you what `maestro.log` and the JSON cannot. It's the highest-signal artifact. Always read it.\n","e2e/demo_app/.maestro/commands/addMedia.yaml":"appId: com.google.android.documentsui\ntags:\n  - passing\n  - android # TODO: How do we prove this works on iOS?\n---\n\n- addMedia:\n  - ../assets/image.png\n\n- launchApp: com.google.android.documentsui # Files app\n\n- tapOn: Show roots\n- tapOn: Images\n\n# Locate the image via Search instead of opening the \"Pictures\" album. On a\n# loaded emulator a tap on the album can be misread as a long-press, which\n# selects the album instead of opening it and leaves the test stranded on the\n# album list. The search icon is a button, so a long-press on it is harmless.\n\n- tapOn:\n    id: \"com.google.android.documentsui:id/option_menu_search\"\n- inputText: \"image\" # Search a partial term (\"image\") so the query text differs from the file name\n- assertVisible: image.png\n\n- longPressOn: image.png\n- tapOn:\n    id: \"com.google.android.documentsui:id/action_menu_delete\"\n- tapOn: OK\n- assertNotVisible: image.png\n","e2e/demo_app/.maestro/commands/assertNotVisible.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertNotVisible: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'Form Test'\n    enabled: false","AGENTS.md":"# AGENTS.md — Maestro\n\nShared context for any Claude Code skill or subagent operating in this repo. Skills (`.claude/skills/*`) reference this file rather than restating module roles; if a description here drifts from reality, fix it here once and every skill follows.\n\n## Commits, pull requests, and anything else you publish\n\nThis is a public repo. Anything pushed or posted is published permanently: commits, branch names, code and comments, test fixtures and flows, PR and issue text, review comments. Never:\n\n- **Publish secrets**, tokens, credentials, or sensitive `.env` contents.\n- **Add AI-attribution trailers** (`Co-authored-by:` for a tool, \"Generated with…\", session links), or `Signed-off-by`.\n\n**Staff contributors only**: if you know which customer, org, or deployment a change is for — from a tracker, Slack, or Maestro Cloud — that's staff context and must not reach the repo. Don't name them, and don't include what identifies them: their app's package or bundle ID, flows or screenshots from their app, Cloud IDs or dashboard URLs, or an internal tracker's contents. Reference trackers by ID only (`MA-1234`, `#3504`). Reproduce against the demo app, not their app.\n\nFull rules — PR title and body, sensitive info: [`docs/pull-requests.md`](docs/pull-requests.md); issues: [`docs/issues.md`](docs/issues.md). Opening a PR or issue with `--body` (`gh pr create`, `gh issue create`) skips the templates in `.github/`, so read the matching doc first and use its headings. Open a PR as ready for review when it is; open it as a draft only when there's a reason not to ask for a human yet, such as checking CI passes.\n\n## Module map\n\nTop-level Gradle modules. Code lives under each module's `src/main/`.\n\n| Module                       | Role                                                                                                                                                                                                                                                                                     |\n|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `maestro-android/`           | On-device Android driver. Kotlin sources compile to two checked-in APKs (`maestro-app.apk`, `maestro-server.apk`) consumed by `maestro-client/`. The build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update those APKs plus a `maestro-android-source.sha256` checksum.    |\n| `maestro-ios-driver/`        | Host side of iOS driver wrapper (Kotlin). The actual XCTest runner lives in `maestro-ios-xctest-runner/`.                                                                                                                                                                                |\n| `maestro-ios-xctest-runner/` | Swift XCTest runner that runs on the iOS device/simulator. The compiled artifacts (`maestro-driver-ios*.zip`) are checked in under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`.                                                                |\n| `maestro-ios/`               | iOS host-side glue (small — most iOS host code lives in `maestro-client/`).                                                                                                                                                                                                              |\n| `maestro-client/`            | Host-side Kotlin SDK that drives devices. Platform drivers live in `src/main/java/maestro/drivers/`: `AndroidDriver.kt`, `IOSDriver.kt`, `WebDriver.kt`, `CdpWebDriver.kt`. This is where most \"auto-grant\", \"auto-dismiss\", system-dialog handling and platform-specific quirks belong. |\n| `maestro-orchestra/`         | Command execution layer. `Orchestra.kt` interprets each Maestro command, applies retries, manages the command lifecycle. Sub-packages: `error/`, `filter/`, `workspace/`, `yaml/`.                                                                                                       |\n| `maestro-orchestra-models/`  | Shared command/data models (used by `maestro-orchestra/` and consumers).                                                                                                                                                                                                                 |\n| `maestro-cli/`               | CLI entry point + MCP server. Mixed Kotlin (~100 files) + Swift (~56 files for iOS-related CLI bits).                                                                                                                                                                                    |\n| `maestro-utils/`             | Shared utilities.                                                                                                                                                                                                                                                                        |\n| `maestro-web/`               | Web (browser) driver pieces.                                                                                                                                                                                                                                                             |\n| `maestro-proto/`             | Protobuf definitions shared across modules.                                                                                                                                                                                                                                              |\n| `maestro-test/`              | Cross-module tests that doesn't require devices.                                                                                                                                                                                                                                         |\n\n## E2E test fixtures (`e2e/`)\n\nShipped fixtures used by `.github/workflows/test-e2e.yaml`. Run via `e2e/run_tests <android|ios|web>` (see `e2e/run_tests` for env-var inputs `MAESTRO_APP`, `MAESTRO_FLOW_PATH`, `FIXTURES_PORT`).\n\n**Web flows need a fixtures server.** They fetch their pages from `http://127.0.0.1:7357`, which `run_tests web` starts and stops for itself. Running one web flow directly does not, and the flow then fails at `launchApp` with a connection error (`net::ERR_CONNECTION_REFUSED`). Run `e2e/ensure_fixtures` first — it is idempotent and waits until the server answers.\n\n| Path                     | Role                                                                                                                                                                    |\n|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `e2e/demo_app/`          | Flutter demo app whose only purpose is to exercise Maestro features. Contains its own `CLAUDE.md`. Built binaries are uploaded to a GCS bucket and re-downloaded by CI. |\n| `e2e/demo_app/.maestro/` | Maestro flow YAMLs that drive the demo app.                                                                                                                             |\n| `e2e/workspaces/`        | Further workspaces, each holding its flow YAMLs directly (no `.maestro/` subdirectory): `simple_web_view`, `wikipedia`, and `web` — which is browser flows with no app behind it. |\n| `e2e/workspaces.txt`     | The workspaces the suite knows about, and for each one its path, the platforms it runs on, and whether it carries `failing` flows. `run_tests` validates it before running anything. |\n| `e2e/run_tests`          | Test driver invoked by the workflow.                                                                                                                                    |\n| `e2e/ensure_fixtures`    | Starts that server unless one is already up, and waits until it answers. Call before running a web flow by hand.                                                         |\n| `e2e/serve_fixtures`     | The static server itself, in the foreground, for `workspaces/web/fixtures/`.                                                                                             |\n| `e2e/list_workspaces`    | Prints the workspace names from the manifest, so callers need not parse it.                                                                                             |\n\n### `passing/` vs `failing/` suites\n\nTag-based filters inside the YAML flows split test runs into two suites at execution time:\n\n- `passing/` — flows tagged `passing`. **Expected to pass.** Any failure here is a real regression. This is the only suite the diagnose agent reads.\n- `failing/` — flows tagged `failing`. **Expected to fail** (negative-path coverage: assertions that should not match, commands that should error). The workflow inverts the success check on this suite. Do not treat `failing/` artifacts as regressions.\n\nArtifacts land at `<artifact_root>/tests/<app>/<suite>/`:\n\n## `test-e2e.yaml` workflow contract\n\n`.github/workflows/test-e2e.yaml` is the validation harness for both PR triggers and manual `workflow_dispatch` (e.g. validating a new Android API level or iOS version). Contract:\n\n- **`workflow_dispatch` inputs** — `android_version` (choice enum), `app` (string, default `demo_app`), `flow` (string, optional single-flow). The `validate-inputs` job rejects `android_version <= android-29`, missing `app` workspace, or ambiguous `flow`. (See PR #3226.)\n- **`pull_request` triggers** are byte-identical to the prior behaviour; manual dispatches use the new narrowing knobs.\n- **`test-android` job** boots an emulator on `system-images;${android_version};google_apis;x86_64` and runs `e2e/run_tests android`.\n\nSkills that bump platform versions (Android API levels, iOS versions) drive this workflow via `gh workflow run test-e2e.yaml --ref <branch> -f android_version=<...>`.\n\n## Testing\n\nThree layers — unit, integration, E2E — plus MCP-specific evals. Each layer has a different cost/coverage trade-off; default to the lowest layer that can express the test.\n\n### Unit tests (per module, `src/test/kotlin/`)\n\nStandard per-class tests. Stack: **JUnit 5** (`junit-jupiter-api` + `-params` + `-engine`), **Google Truth** for assertions, **MockK** for mocks. Each module's `build.gradle.kts` enables the platform via `tasks.named<Test>(\"test\") { useJUnitPlatform() }`.\n\n```bash\n./gradlew :maestro-orchestra:test          # one module\n./gradlew test                              # all modules\n```\n\n### Integration tests (`maestro-test/`)\n\nCross-module tests for behaviour that does **not** require a device or simulator — JS engine integration points, command orchestration end-to-end, cancellation / coroutine semantics. Notable suites:\n\n- `IntegrationTest.kt` — full `Maestro` orchestration against an in-process `FakeDriver` (defined in `maestro-test/src/main/kotlin/maestro/test/drivers/`: `FakeDriver`, `FakeLayoutElement`, `FakeTimer`). Covers test-run cancellation (`CancellationException`, `withTimeout`, supervisor scopes) and the full command lifecycle without a real device.\n- `GraalJsEngineTest.kt` / shared `JsEngineTest.kt` — Maestro's JS extension points (`evalScript`, JS-evaluated assertions/conditions). Exercises `org.graalvm.polyglot` directly.\n- `FlowControllerTest.kt`, `DeepestMatchingElementTest.kt` — orchestration and view-hierarchy logic.\n\nStack: **JUnit 5**, **Google Truth**, **WireMock JRE8** (HTTP fakes), plus the in-house `FakeDriver` fixtures listed above. No mocks of Maestro's own classes — tests run real `Maestro` against the fakes.\n\n```bash\n./gradlew :maestro-test:test\n```\n\n### Simulator tests (opt-in, inside a module's unit tests)\n\nFor driver code whose input is produced by the platform itself — a line in the simulator's log, a file macOS writes — and which a hand-written fixture therefore cannot keep honest. The test runs the real class against a booted simulator, with no CLI, flow or XCTest runner. It is skipped unless `MAESTRO_TEST_SIMULATOR_UDID` is set, so `./gradlew test` behaves the same everywhere else; the `test-ios` job of `test-e2e.yaml` sets it. Example: `IOSDriverCrashSimulatorTest`.\n\n```bash\nMAESTRO_TEST_SIMULATOR_UDID=<booted-udid> ./gradlew :maestro-client:test --tests '*SimulatorTest'\n```\n\n### E2E tests (`e2e/`)\n\nSmoke-test every Maestro command across Android, iOS, and Web on real fixture apps. Maestro is its own dogfood harness: the CLI executes Maestro flow YAMLs against the fixtures, asserting both the framework's commands and the platform drivers behave correctly.\n\nStack: **Maestro CLI itself** (dogfood) + `e2e/run_tests` shell driver + GHA workflow (`.github/workflows/test-e2e.yaml`). Fixture and suite layout is in \"E2E test fixtures (`e2e/`)\" above.\n\n```bash\ncd e2e && ./run_tests <android|ios|web>     # local\ngh workflow run test-e2e.yaml --ref <branch> -f android_version=android-<N>   # CI\n```\n\n**Two roles for the same E2E setup.** The same suite serves both purposes — treat them identically:\n\n1. **Regression smoke** — every PR that touches Maestro source runs the suite on the current platform versions, catching behaviour breakage on existing platforms.\n2. **New-OS validation** — when launching a new Android API level or iOS version, the same flows are dispatched against the new system image to confirm Maestro still works. This is what `bump-android-version` (and the planned `bump-ios-version`) drives.\n\nA flow breaking for either reason is a real regression — fix in `maestro-android/`, `maestro-client/`, or `e2e/demo_app/`, not in `test-e2e.yaml` (see \"What NOT to do\").\n\n**Multiple apps for framework-specific coverage.** `demo_app/` (Flutter) is the default fixture and exercises every Maestro command. When a target is **framework-specific** (SwiftUI, React Native, Jetpack Compose specifics, WebView quirks, etc.), add a separate workspace: flow YAMLs directly under `e2e/workspaces/<name>/`, a binary under `e2e/apps/` if there is an app to install, **and a row in `e2e/workspaces.txt`** — `validate_workspaces` fails a workspace that has flows but no row, since without one `run_tests` would never find it. Existing examples: `simple_web_view` (WebView coverage), `wikipedia` (real-world third-party app), `web` (browser flows, no app). The workflow's `app` input narrows a manual dispatch to one workspace: `... -f app=simple_web_view`.\n\n### MCP server evals (`maestro-cli/src/test/mcp/`)\n\nLLM-behaviour evaluations and tool-functionality tests for the MCP server inside `maestro-cli`. Stack: **`mcp-server-tester`** (npm package, run via `npx`) consuming YAML definitions (`full-evals.yaml`, `inspect-screen-evals.yaml`, `tool-tests-{with,without}-device.yaml`) plus per-platform setup scripts under `setup/`. See `maestro-cli/src/test/mcp/README.md` for the model list, scorers, and how to run.\n\n```bash\n./run_mcp_tool_tests.sh ios     # tool-functionality (fast)\n./run_mcp_evals.sh ios          # LLM behaviour (slower)\n```\n\n## Conventions\n\n- Kotlin 1.9 / JVM 17. Gradle. No DI framework — services are constructed manually.\n- Protobuf for the on-device wire format (`maestro-proto/`).\n- Coroutines with explicit dispatchers; `runBlocking` only at entry points.\n- Exposed exceptions classify failures (retryable vs terminal) — see `maestro-orchestra/src/main/java/maestro/orchestra/error/`.\n- **A run's files fall into three buckets.** *Inputs* (flow YAML, scripts, `assertScreenshot` references, `addMedia` files) are read from the workspace and never written to. *Outputs* — anything a user may want after the run — are allocated through `ArtifactsGenerator`, which places them in the artifacts bundle and lists them in `manifest.json`. *Scratch* files go through `TempFileHandler` (below). Callers decide what happens to those folders after the run and may keep only what the manifest lists, so an output written anywhere else — beside an input especially — can be silently lost. `RunOutputStaysInBundleTest` (`maestro-test/`) enforces this and makes every new command declare whether it creates files.\n- **Temp files and directories go through `maestro.utils.TempFileHandler`**, not `java.nio.file.Files.createTempFile/createTempDirectory` directly. `TempFileHandler` is a `Closeable` that recursively cleans up everything it allocated on `close()`. Direct `Files.createTempFile(...)` skips that lifecycle and leaks `/tmp` content (especially painful on long-lived JVMs like the cloud worker). Construct a `TempFileHandler` near the lifecycle owner, call its `createTempFile` / `createTempDirectory`, and `close()` it in a `finally`.\n\n## Where Claude Code resources live\n\n- `.claude/skills/*` — skills (workflows). Each skill's `SKILL.md` references this file for module roles.\n- `.claude/agents/*.md` — subagents (e.g. `diagnose-maestro-failure.md`). Their input/output contracts are documented in each file.\n\n## What NOT to do\n\n- Don't fix driver-behaviour gaps by patching `.github/workflows/test-e2e.yaml` (e.g. extra `adb shell settings put …`, command-line tweaks, AVD pre-config). Workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/`, `maestro-client/`, or `e2e/demo_app/` instead so the fix ships with the driver APKs. Workflow edits are valid for shape-changes (matrix, retention, dispatch inputs) and the narrow third-party-FRE exception documented in skill files.\n- Don't edit checked-in driver artifacts (`maestro-app.apk`, `maestro-server.apk`, `maestro-android-source.sha256`, `maestro-driver-ios*.zip`) by hand — they are gradle finalizer outputs.\n- Don't commit local changes to the iOS driver zips. A local build regenerates two zips — `maestro-driver-ios.zip` and `maestro-driver-iosUITests-Runner.zip` — under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`. This is normal: they're used by local builds. But although they're checked into the repo, they're managed exclusively by CI, so leave any local modifications to them out of your commits.\n- Don't modify existing flows in `failing/` to make them pass — that's the negative-path suite by design.\n","e2e/demo_app/.maestro/commands/assertScreenshotCroppedHEIC.yaml":"appId: com.example.example\ntags:\n  - failing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedHEIC.heic\n    cropOn:\n      id: \"testContainer\"\n","e2e/demo_app/.maestro/commands/assertVisible.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertVisible: 'Form Test'\n\n- assertVisible:\n    text: 'Form Test'\n\n- assertVisible:\n    id: 'fabAddIcon'","e2e/demo_app/.maestro/commands/assertScreenshotCroppedJPG.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedJPG.jpg\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n","e2e/demo_app/.maestro/commands/assertScreenshotCropped.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCropped.png\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n","e2e/demo_app/.maestro/commands/assertTrue.yaml":"appId: com.example.example\ntags:\n  - passing\nenv:\n  THING: \"five\"\n---\n\n- launchApp # For idempotence of sections\n\n- assertTrue: ${\"test\" == \"test\"}\n\n- assertTrue:\n    condition: ${12 < 20}\n\n- assertTrue:\n    condition: ${THING == \"five\"} # Using the env at the top of the file","e2e/demo_app/.maestro/commands/back.yaml":"appId: com.example.example\ntags:\n  - passing\n  - android\n---\n- launchApp # For idempotence of sections\n\n- tapOn: 'Form Test'\n- assertVisible: 'Login'\n- back\n- assertVisible: 'Form Test'","e2e/demo_app/.maestro/commands/clearState.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/clearState.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/clearState.yaml)","e2e/demo_app/.maestro/commands/copyTextFrom.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/copyTextFrom.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/copyTextFrom.yaml)","e2e/demo_app/.maestro/commands/doubleTapOn.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/doubleTapOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/doubleTapOn.yaml)","e2e/demo_app/.maestro/commands/eraseText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/eraseText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/eraseText.yaml)","e2e/demo_app/.maestro/commands/evalScript.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/evalScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/evalScript.yaml)","e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml)","e2e/demo_app/.maestro/commands/hideKeyboard.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/hideKeyboard.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/hideKeyboard.yaml)","e2e/demo_app/.maestro/commands/inputRandomCityName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCityName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCityName.yaml)","e2e/demo_app/.maestro/commands/inputRandomColorName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomColorName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomColorName.yaml)","e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml)","e2e/demo_app/.maestro/commands/inputRandomEmail.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomEmail.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomEmail.yaml)","e2e/demo_app/.maestro/commands/inputRandomNumber.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomNumber.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomNumber.yaml)","e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml)","e2e/demo_app/.maestro/commands/inputRandomText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomText.yaml)","e2e/demo_app/.maestro/commands/inputText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputText.yaml)","e2e/demo_app/.maestro/commands/killApp.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/killApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/killApp.yaml)","e2e/demo_app/.maestro/commands/launchApp.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/launchApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/launchApp.yaml)","e2e/demo_app/.maestro/commands/longPressOn.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/longPressOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/longPressOn.yaml)","e2e/demo_app/.maestro/commands/openLink.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/openLink.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/openLink.yaml)","e2e/demo_app/.maestro/commands/pasteText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pasteText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pasteText.yaml)","e2e/demo_app/.maestro/commands/pressKey.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pressKey.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pressKey.yaml)","e2e/demo_app/.maestro/commands/repeat.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/repeat.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/repeat.yaml)","e2e/demo_app/.maestro/commands/retry.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/retry.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/retry.yaml)","e2e/demo_app/.maestro/commands/runFlow.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/runFlow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runFlow.yaml)","e2e/demo_app/.maestro/commands/runScript.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/runScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runScript.yaml)","e2e/demo_app/.maestro/commands/scroll.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/scroll.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scroll.yaml)","e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml)","e2e/demo_app/.maestro/commands/setAirplaneMode.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setAirplaneMode.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setAirplaneMode.yaml)","e2e/demo_app/.maestro/commands/setLocation.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setLocation.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setLocation.yaml)","e2e/demo_app/.maestro/commands/setOrientation.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setOrientation.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setOrientation.yaml)","e2e/demo_app/.maestro/commands/stopApp.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/stopApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/stopApp.yaml)","e2e/demo_app/.maestro/commands/swipe.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/swipe.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe.yaml)","e2e/demo_app/.maestro/commands/swipe_from_point.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/swipe_from_point.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe_from_point.yaml)","e2e/demo_app/.maestro/commands/takeScreenshot.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/takeScreenshot.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshot.yaml)","e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml)","e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml)","e2e/demo_app/.maestro/commands/travel.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/travel.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/travel.yaml)","e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml)"},"files":{".claude/agents/diagnose-maestro-failure.md":"---\nname: diagnose-maestro-failure\ndescription: Use when a Maestro test-android run has failed and a structured per-flow diagnosis is needed without bloating the caller's context. Accepts either a local artifact directory or a GitHub Actions run/job URL. Reads commands JSON, screenshots, and maestro.log; classifies each newly-failing passing/ flow; identifies cascade groups; and returns a structured report with proposed fixes. Does NOT edit code or commit — the caller is responsible for consent and application.\ntools: Bash, Read, Agent\n---\n\n# Diagnose Maestro Failure (subagent)\n\nYou diagnose Maestro `test-android` failures from CI artifacts and return a structured report. You do not modify code, commit, or push — the caller handles consent and application.\n\n## Input\n\nThe caller passes one of:\n\n- **Local artifact directory** — e.g. `/Users/.../maestro-root-dir-android` or `/tmp/maestro-android-<run_id>`. Must contain a `tests/` subtree.\n- **GitHub Actions URL** — `https://github.com/mobile-dev-inc/Maestro/actions/runs/<run_id>` or `.../runs/<run_id>/job/<job_id>`.\n\nIf a URL, extract the `run_id` and download:\n\n```bash\nRUN_ID=<extracted>\nDEST=/tmp/maestro-android-$RUN_ID\ngh run download \"$RUN_ID\" --repo mobile-dev-inc/Maestro \\\n  --name maestro-root-dir-android -D \"$DEST\"\n```\n\nIf the directory has no `tests/demo_app/passing/` subtree (e.g. iOS artifact, or the run failed before tests), report that and stop.\n\n## Where to Look\n\nOnly `<artifact_root>/tests/demo_app/passing/` is signal. `failing/` is the negative-path suite — its commands are *supposed* to fail. Ignore it entirely.\n\n```bash\ncd <artifact_root>/tests/demo_app/passing\ngrep -l '\"FAILED\"' commands-*.json\n```\n\nThe status field lives at `.[].metadata.status`. Possible values: `COMPLETED` (success) and `FAILED` (terminal failure). `commands-*.json` is the **only** authoritative source — `screenshot-❌-*.png` is not.\n\n**`commands-(retry).json` is the canonical example.** This flow exercises the `retryCommand` directive: a tap fails, Maestro retries it, the retry succeeds, and the flow passes. In the artifact:\n\n- All entries (including the recovered `tapOnElement`) have `metadata.status = \"COMPLETED\"` — Maestro writes only the final outcome of a recovered command, not the failed attempt.\n- A `screenshot-❌-<ts>-(retry).png` still exists, captured at the moment of the failed attempt before the retry succeeded.\n- The grep above does **not** match this file (no `\"FAILED\"` string anywhere), so the agent never investigates it. This is correct behaviour.\n\nThe risk is treating the `❌` screenshot as evidence of failure on its own. Don't. Trust the candidates from grep — they are the flows whose JSON contains a terminal `FAILED`. Any other `❌` screenshot in `passing/` is from a recovered retry and is irrelevant.\n\n## Per-Flow Investigation\n\nFor each candidate flow returned by the grep above:\n\n1. **Open `commands-(<flow>).json`.** Locate the entry whose `metadata.status == \"FAILED\"` — that's the terminal failure. Record: command type (the single key under `command`), error message, `metadata.timestamp` (ms epoch), `metadata.sequenceNumber`, `metadata.duration` (≈17000 ms = default-wait timeout, indicating UI-blocked, not crash).\n2. **Open the screenshot `screenshot-❌-<timestamp>-(<flow>).png` whose timestamp matches the FAILED entry.** This is the highest-signal artifact — JSON and logs alone misclassify when a system dialog is invisible to them. Always read it. Ignore `❌` screenshots from other timestamps in the same file — those are recovered retries.\n3. **Grep `maestro.log`** for the flow name and the timestamp window for surrounding driver activity, gRPC errors, and stack traces.\n\n## Classification\n\n| Screen at failure | Class | Where the fix lives |\n|---|---|---|\n| Expected app screen, but selector wouldn't match | Test flow issue | `e2e/demo_app/.maestro/**/*.yaml` |\n| Android / GMS system dialog blocking the app | Maestro driver gap (auto-grant / auto-dismiss missing) | `maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt` or `.github/workflows/test-e2e.yaml` (emulator pre-config) |\n| Pixel launcher visible behind a system dialog | Same — dialog stole focus across `launchApp` / `clearState` / `stopApp` | Same as above |\n| App crash dialog / blank screen | App-side issue | Out of scope — report only |\n| Stack trace from `maestro.*` in `maestro.log` | Maestro framework bug | `maestro-client/` / `maestro-orchestra/` / `maestro-android/` |\n\nFor anything classified as **driver gap** or **framework bug**, dispatch an `Explore` subagent into the relevant module(s) to find the offending code path before proposing line numbers. Never guess.\n\n## Cascade Detection (critical)\n\nA single system dialog can fail many subsequent flows because `clearState` / `launchApp` / `stopApp` don't dismiss system overlays. List FAILED entries by ascending timestamp; if downstream screenshots show the *same* overlay, group them under the **root trigger** (the earliest flow that raised the dialog). Propose **one** fix per root, not N fixes for the cascade.\n\n## Required Output Format\n\nReturn this structure verbatim — the caller parses it:\n\n````markdown\n## Summary\n<one-line: how many flows failed, how many root causes, how many cascade groups>\n\n## Root causes (caller acts on each)\n\n### 1. <flow-or-group-label> — <classification>\n- **Trigger:** <what raised this>\n- **Affected flows:** <flow1>, <flow2>, ... (N flows)\n- **Cascade?** yes/no\n- **Proposed fix:**\n  - File: `<exact path>:<line range>`\n  - What it does: <one sentence>\n  - Diff (unified or pseudo):\n    ```\n    - old line\n    + new line\n    ```\n  - Side effects: <if any, else \"none\">\n  - Verified via Explore? <yes — line numbers confirmed | no — fix is in CI yaml or test flow yaml, line numbers from direct read>\n\n### 2. ...\n\n## Out of scope\n- `<flow>` — <reason, e.g. app crash, environmental>\n\n## Notes\n<anything the caller should know but isn't a fix proposal>\n````\n\n## Constraints\n\n- Do NOT edit, write, or commit any file. Diagnosis only.\n- Do NOT propose a separate fix for each member of a cascade group.\n- Do NOT guess at Kotlin line numbers — verify via Explore subagent first.\n- Do NOT report `failing/` flows as failures.\n- Do NOT trust `❌` screenshots on their own. The grep filter (`grep -l '\"FAILED\"' commands-*.json`) is the source of truth for which flows had a terminal failure. A `❌` screenshot inside a flow whose JSON has zero `FAILED` entries is from a recovered retry and is not evidence of regression. `commands-(retry).json` is the canonical example — see \"Where to Look\" for details.\n- Always read the screenshot, even when JSON+log seem to point clearly at a cause — the screenshot is what reveals system overlays.\n\n## When to Stop and Hand Back\n\nHand back as soon as every passing-suite FAILED command has been either (a) classified with a proposed fix, or (b) classified as out-of-scope with a reason. Do not loop, retry, or attempt to apply fixes.\n",".claude/skills/bump-android-version/SKILL.md":"---\nname: bump-android-version\ndescription: Use when bumping Maestro's Android compileSdk/targetSdk to a new API level and validating end-to-end against the test-e2e GHA workflow until the test-android job is green.\n---\n\n# Bump Android Version\n\n## Overview\n\nDrive an Android-version bump in Maestro commit-by-commit on a single feature branch: edit gradle, rebuild the on-device driver APKs, push a draft PR, dispatch `test-e2e.yaml` against the new system image, watch the `test-android` job, and diagnose+fix any newly-failing `passing/` flows. Loop until green.\n\n## When to Use\n\n- User asks to bump Maestro's Android `compileSdk` / `targetSdk` (e.g. \"bump to API 36\").\n- User wants to validate Maestro against a new Android system image.\n\nOne commit per logical step. Don't bundle the gradle bump and the APK rebuild — they need to be separately revertible. After a failing run, fixes are committed and the workflow re-dispatched. Loop until green; do not stop on the first re-dispatch.\n\n**The loop only acts on `passing/` failures.** `tests/demo_app/passing/` is the regression-detection suite — its flows are expected to pass, so a failure there is a real signal. `tests/demo_app/failing/` is the negative-path suite (its flows are *expected* to fail) and is ignored end-to-end. The diagnose agent rejects `failing/` artifacts and rejects retry-recovered flows in `passing/` (see [`.claude/agents/diagnose-maestro-failure.md`](../../agents/diagnose-maestro-failure.md)). If the run is otherwise green except for `failing/`, that's a green run.\n\n## Pre-flight\n\n- Working tree clean (`git status`).\n- `main` up to date: `git fetch origin && git checkout main && git pull --ff-only origin main`.\n- The workflow's `workflow_dispatch` inputs `android_version` (and optional `app` / `flow`) must exist on the branch. \n- `android_version` is a `choice` input — its options live in `.github/workflows/test-e2e.yaml`. If `<new>` isn't already listed (e.g. bumping to API 37), add it to the `options:` list in a separate commit before dispatching, otherwise the workflow rejects the input.\n- The `validate-inputs` job rejects `android_version <= android-29`. Bumps below API 29 aren't supported.\n- Branch name: `bump-android-api-<old>-<new>` (e.g. `bump-android-api-34-36`).\n\n```bash\ngit checkout -b bump-android-api-<old>-<new> origin/main\n```\n\n## Commit 1: bump compile/target SDK\n\nRead current values from `maestro-android/build.gradle.kts` (`compileSdk` and `targetSdk`). Tell the user the current values verbatim, then ask for the new target:\n\n> Current `compileSdk` / `targetSdk` = `<N>`. What's the new API level?\n\nEdit both lines. Also `rg -n \"compileSdk\\s*=|targetSdk\\s*=\" --type kotlin --type-add 'kotlin:*.kts'` to catch consistent occurrences in other modules — only update those that intentionally track the same SDK. Commit:\n\n```bash\ngit add maestro-android/build.gradle.kts <other touched files>\ngit commit -m \"chore(android): bump compile/target SDK from <old> to <new>\"\n```\n\n## Commit 2: rebuild driver APKs\n\n```bash\n./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest\n```\n\nThe build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update three checked-in files. Stage and commit *only* those:\n\n```bash\ngit add maestro-client/src/main/resources/maestro-app.apk \\\n        maestro-client/src/main/resources/maestro-server.apk \\\n        maestro-client/src/main/resources/maestro-android-source.sha256\ngit commit -m \"chore(android): rebuild driver APKs against API <new>\"\n```\n\nIf `assemble` fails, **stop and surface the failure to the user** with the gradle error and a suggested fix. Do not proceed without rebuilt driver APKs. Once we have assemble fixed and new drivers are commited, we can proceed to the next step.\n\n## Step 3: push draft PR + dispatch\n\n```bash\ngit push -u origin bump-android-api-<old>-<new>\ngh pr create --draft \\\n  --title \"chore(android): bump compile/target SDK to <new>\" \\\n  --body \"Validates Maestro against API <new>. Driver APKs rebuilt. Test-e2e dispatched with system-images;android-<new>;google_apis;x86_64.\"\n\ngh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n  -f android_version=\"android-<new>\"\n\n# capture the run id\nsleep 3\ngh run list --workflow test-e2e.yaml --branch bump-android-api-<old>-<new> --limit 1 --json databaseId,status\n```\n\nAlways pass `-f android_version=android-<new>` — the default is `android-32` and would silently re-test the old API. The workflow constructs the full `system-images;...` string internally. `app` and `flow` are optional narrowing knobs (defaults: `demo_app`, all flows); leave them off for the full bump-validation run.\n\n## Step 4: watch the test-android job\n\n```bash\ngh run watch <run_id>\n```\n\nBlock in the foreground; the run is the bottleneck. **Green here means every `passing/` flow passed (terminally — retry-recovered counts as passed). `failing/` outcomes do not affect the verdict.** On green: update PR from draft → ready, done.\n\nOn red, download artifacts:\n\n```bash\ngh run download <run_id> --name maestro-root-dir-android -D /tmp/maestro-android-<run_id>\n```\n\n## Step 5: diagnose (delegated to subagent)\n\nDispatch the **`diagnose-maestro-failure`** subagent with the artifact directory `/tmp/maestro-android-<run_id>` (or the GHA run URL — the subagent handles both). It diagnoses each passing-suite failure in isolation and returns a structured report grouped by root cause (cascades collapsed). It does **not** apply fixes — that's your job here.\n\nWhen the subagent returns:\n\n**Fixes go in Maestro source — not in `.github/workflows/test-e2e.yaml`.** A failing flow on a new API level reflects a behaviour Maestro users will also hit on their own machines/CI, not just ours. Patching the GHA workflow (e.g. extra `adb shell settings put …`, command-line tweaks, AVD setup steps) hides the regression from real users and ships a Maestro that's only green inside our CI.\n\nAcceptable fix targets and their roles are documented in [`AGENTS.md`](../../../AGENTS.md) (modules `maestro-android/`, `maestro-client/`, `maestro-orchestra/`, `e2e/demo_app/`). Driver-behaviour fixes belong in `maestro-android/` or `maestro-client/`; fixture-only issues belong in `e2e/demo_app/`. Editing the driver source means rebuilding the driver APKs (`./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest`) before re-dispatching.\n\nValid edits to `test-e2e.yaml` during this loop are workflow-shape changes (matrix, retention, dispatch inputs) — and the narrow exception below. Anything else (e.g. extra `adb shell settings put …` to mask a Maestro driver gap) is off-limits. If the subagent proposes a workflow patch, push back: ask it (or yourself) where the same fix would live in `maestro-android/` or `maestro-client/` so users on the new API level inherit it automatically.\n\n**Exception — third-party app first-run UI not triggered by a Maestro API.** A pre-installed app's own onboarding (Chrome Welcome / \"Make Chrome your own\", browser default-app picker, Play Protect prompts that fire at app launch) is environment-harness state, not driver behaviour. Maestro doesn't expose a public API that triggers them — they're side effects of the OS image we picked. These may be pre-disabled in the AVD setup step (typical knobs: `setprop debug.chrome.command_line`, `settings put global …` flags). Test: *if Maestro called nothing related to this dialog, would it still appear?* If yes → CI workaround is fair game. If no (e.g. `setLocation` triggers GMS Location Accuracy via `FusedLocationProviderClient` inside the driver) → fix the driver instead.\n\n1. For each root cause in its report, present the proposed diff to the user (file path + one-line summary + diff + side effects). Reject any proposal that targets `test-e2e.yaml` for driver-behaviour reasons before showing it — re-scope to Maestro source first.\n2. **Ask consent explicitly per fix:**\n   > Proposed fix for `<root cause>`: `<one-liner>`. Apply?\n3. On approval: edit, commit with a focused message — don't bundle unrelated fixes.\n   ```bash\n   git commit -m \"fix(android-<new>): <summary>\"\n   ```\n4. On rejection: skip that fix; record what was rejected so you don't re-propose it next iteration.\n\nAfter all approved fixes for this iteration are committed:\n\n- **If any fix was applied:** push and re-dispatch, then loop back to **Step 4** with the new `run_id`.\n\n  ```bash\n  git push\n  gh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n    -f android_version=\"android-<new>\"\n  ```\n\n- **If no fixes were applied** (everything was out-of-scope, or the user rejected every proposal): pause and ask the user how to proceed. Do not keep dispatching.\n\n## Termination\n\nStop when `test-android` is green on the latest run. Then:\n\n```bash\ngh pr ready <pr_number>\n```\n\nIf the loop has gone three iterations without progress (same flow keeps failing for different reasons after fixes), pause and ask the user how to proceed — don't spin indefinitely.\n\n## Anti-patterns\n\n- **Bundling gradle + APK rebuild into one commit** — kills bisectability. Always two commits.\n- **Dispatching with default inputs** — `android_version` defaults to `android-32` and hits the old API. Always pass `-f android_version=android-<new>`.\n- **Forgetting to extend the `android_version` choice enum** — the workflow rejects API levels not listed in `options:`. Add `<new>` to the enum in a separate commit before the first dispatch.\n- **Auto-applying Maestro source fixes without consent** — every Kotlin patch needs explicit user approval first.\n- **Patching `.github/workflows/test-e2e.yaml` to mask a driver behaviour gap** — workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/` or `e2e/demo_app/` instead so the fix ships with the driver APKs.\n- **Treating `failing/` artifacts as regressions** — that suite is expected to fail.\n- **Treating `❌` screenshots in `passing/` as regressions on their own** — when `retryCommand` recovers a failed attempt, Maestro writes only the final `COMPLETED` entry to `commands-(<flow>).json` but leaves the `❌` screenshot from the failed attempt on disk (e.g. `screenshot-❌-<ts>-(retry).png` for the `retry` flow itself). The diagnose agent uses `commands-*.json` containing a terminal `FAILED` entry as the source of truth — orphan `❌` screenshots without a matching FAILED entry are noise.\n- **Committing on `main`** — always work on the bump branch. Verify with `git status` before every commit.\n- **Skipping the screenshot read** — the screenshot tells you what `maestro.log` and the JSON cannot. It's the highest-signal artifact. Always read it.\n","e2e/demo_app/.maestro/commands/addMedia.yaml":"appId: com.google.android.documentsui\ntags:\n  - passing\n  - android # TODO: How do we prove this works on iOS?\n---\n\n- addMedia:\n  - ../assets/image.png\n\n- launchApp: com.google.android.documentsui # Files app\n\n- tapOn: Show roots\n- tapOn: Images\n\n# Locate the image via Search instead of opening the \"Pictures\" album. On a\n# loaded emulator a tap on the album can be misread as a long-press, which\n# selects the album instead of opening it and leaves the test stranded on the\n# album list. The search icon is a button, so a long-press on it is harmless.\n\n- tapOn:\n    id: \"com.google.android.documentsui:id/option_menu_search\"\n- inputText: \"image\" # Search a partial term (\"image\") so the query text differs from the file name\n- assertVisible: image.png\n\n- longPressOn: image.png\n- tapOn:\n    id: \"com.google.android.documentsui:id/action_menu_delete\"\n- tapOn: OK\n- assertNotVisible: image.png\n","e2e/demo_app/.maestro/commands/assertNotVisible.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertNotVisible: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'Form Test'\n    enabled: false","AGENTS.md":"# AGENTS.md — Maestro\n\nShared context for any Claude Code skill or subagent operating in this repo. Skills (`.claude/skills/*`) reference this file rather than restating module roles; if a description here drifts from reality, fix it here once and every skill follows.\n\n## Commits, pull requests, and anything else you publish\n\nThis is a public repo. Anything pushed or posted is published permanently: commits, branch names, code and comments, test fixtures and flows, PR and issue text, review comments. Never:\n\n- **Publish secrets**, tokens, credentials, or sensitive `.env` contents.\n- **Add AI-attribution trailers** (`Co-authored-by:` for a tool, \"Generated with…\", session links), or `Signed-off-by`.\n\n**Staff contributors only**: if you know which customer, org, or deployment a change is for — from a tracker, Slack, or Maestro Cloud — that's staff context and must not reach the repo. Don't name them, and don't include what identifies them: their app's package or bundle ID, flows or screenshots from their app, Cloud IDs or dashboard URLs, or an internal tracker's contents. Reference trackers by ID only (`MA-1234`, `#3504`). Reproduce against the demo app, not their app.\n\nFull rules — PR title and body, sensitive info: [`docs/pull-requests.md`](docs/pull-requests.md); issues: [`docs/issues.md`](docs/issues.md). Opening a PR or issue with `--body` (`gh pr create`, `gh issue create`) skips the templates in `.github/`, so read the matching doc first and use its headings. Open a PR as ready for review when it is; open it as a draft only when there's a reason not to ask for a human yet, such as checking CI passes.\n\n## Module map\n\nTop-level Gradle modules. Code lives under each module's `src/main/`.\n\n| Module                       | Role                                                                                                                                                                                                                                                                                     |\n|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `maestro-android/`           | On-device Android driver. Kotlin sources compile to two checked-in APKs (`maestro-app.apk`, `maestro-server.apk`) consumed by `maestro-client/`. The build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update those APKs plus a `maestro-android-source.sha256` checksum.    |\n| `maestro-ios-driver/`        | Host side of iOS driver wrapper (Kotlin). The actual XCTest runner lives in `maestro-ios-xctest-runner/`.                                                                                                                                                                                |\n| `maestro-ios-xctest-runner/` | Swift XCTest runner that runs on the iOS device/simulator. The compiled artifacts (`maestro-driver-ios*.zip`) are checked in under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`.                                                                |\n| `maestro-ios/`               | iOS host-side glue (small — most iOS host code lives in `maestro-client/`).                                                                                                                                                                                                              |\n| `maestro-client/`            | Host-side Kotlin SDK that drives devices. Platform drivers live in `src/main/java/maestro/drivers/`: `AndroidDriver.kt`, `IOSDriver.kt`, `WebDriver.kt`, `CdpWebDriver.kt`. This is where most \"auto-grant\", \"auto-dismiss\", system-dialog handling and platform-specific quirks belong. |\n| `maestro-orchestra/`         | Command execution layer. `Orchestra.kt` interprets each Maestro command, applies retries, manages the command lifecycle. Sub-packages: `error/`, `filter/`, `workspace/`, `yaml/`.                                                                                                       |\n| `maestro-orchestra-models/`  | Shared command/data models (used by `maestro-orchestra/` and consumers).                                                                                                                                                                                                                 |\n| `maestro-cli/`               | CLI entry point + MCP server. Mixed Kotlin (~100 files) + Swift (~56 files for iOS-related CLI bits).                                                                                                                                                                                    |\n| `maestro-utils/`             | Shared utilities.                                                                                                                                                                                                                                                                        |\n| `maestro-web/`               | Web (browser) driver pieces.                                                                                                                                                                                                                                                             |\n| `maestro-proto/`             | Protobuf definitions shared across modules.                                                                                                                                                                                                                                              |\n| `maestro-test/`              | Cross-module tests that doesn't require devices.                                                                                                                                                                                                                                         |\n\n## E2E test fixtures (`e2e/`)\n\nShipped fixtures used by `.github/workflows/test-e2e.yaml`. Run via `e2e/run_tests <android|ios|web>` (see `e2e/run_tests` for env-var inputs `MAESTRO_APP`, `MAESTRO_FLOW_PATH`, `FIXTURES_PORT`).\n\n**Web flows need a fixtures server.** They fetch their pages from `http://127.0.0.1:7357`, which `run_tests web` starts and stops for itself. Running one web flow directly does not, and the flow then fails at `launchApp` with a connection error (`net::ERR_CONNECTION_REFUSED`). Run `e2e/ensure_fixtures` first — it is idempotent and waits until the server answers.\n\n| Path                     | Role                                                                                                                                                                    |\n|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `e2e/demo_app/`          | Flutter demo app whose only purpose is to exercise Maestro features. Contains its own `CLAUDE.md`. Built binaries are uploaded to a GCS bucket and re-downloaded by CI. |\n| `e2e/demo_app/.maestro/` | Maestro flow YAMLs that drive the demo app.                                                                                                                             |\n| `e2e/workspaces/`        | Further workspaces, each holding its flow YAMLs directly (no `.maestro/` subdirectory): `simple_web_view`, `wikipedia`, and `web` — which is browser flows with no app behind it. |\n| `e2e/workspaces.txt`     | The workspaces the suite knows about, and for each one its path, the platforms it runs on, and whether it carries `failing` flows. `run_tests` validates it before running anything. |\n| `e2e/run_tests`          | Test driver invoked by the workflow.                                                                                                                                    |\n| `e2e/ensure_fixtures`    | Starts that server unless one is already up, and waits until it answers. Call before running a web flow by hand.                                                         |\n| `e2e/serve_fixtures`     | The static server itself, in the foreground, for `workspaces/web/fixtures/`.                                                                                             |\n| `e2e/list_workspaces`    | Prints the workspace names from the manifest, so callers need not parse it.                                                                                             |\n\n### `passing/` vs `failing/` suites\n\nTag-based filters inside the YAML flows split test runs into two suites at execution time:\n\n- `passing/` — flows tagged `passing`. **Expected to pass.** Any failure here is a real regression. This is the only suite the diagnose agent reads.\n- `failing/` — flows tagged `failing`. **Expected to fail** (negative-path coverage: assertions that should not match, commands that should error). The workflow inverts the success check on this suite. Do not treat `failing/` artifacts as regressions.\n\nArtifacts land at `<artifact_root>/tests/<app>/<suite>/`:\n\n## `test-e2e.yaml` workflow contract\n\n`.github/workflows/test-e2e.yaml` is the validation harness for both PR triggers and manual `workflow_dispatch` (e.g. validating a new Android API level or iOS version). Contract:\n\n- **`workflow_dispatch` inputs** — `android_version` (choice enum), `app` (string, default `demo_app`), `flow` (string, optional single-flow). The `validate-inputs` job rejects `android_version <= android-29`, missing `app` workspace, or ambiguous `flow`. (See PR #3226.)\n- **`pull_request` triggers** are byte-identical to the prior behaviour; manual dispatches use the new narrowing knobs.\n- **`test-android` job** boots an emulator on `system-images;${android_version};google_apis;x86_64` and runs `e2e/run_tests android`.\n\nSkills that bump platform versions (Android API levels, iOS versions) drive this workflow via `gh workflow run test-e2e.yaml --ref <branch> -f android_version=<...>`.\n\n## Testing\n\nThree layers — unit, integration, E2E — plus MCP-specific evals. Each layer has a different cost/coverage trade-off; default to the lowest layer that can express the test.\n\n### Unit tests (per module, `src/test/kotlin/`)\n\nStandard per-class tests. Stack: **JUnit 5** (`junit-jupiter-api` + `-params` + `-engine`), **Google Truth** for assertions, **MockK** for mocks. Each module's `build.gradle.kts` enables the platform via `tasks.named<Test>(\"test\") { useJUnitPlatform() }`.\n\n```bash\n./gradlew :maestro-orchestra:test          # one module\n./gradlew test                              # all modules\n```\n\n### Integration tests (`maestro-test/`)\n\nCross-module tests for behaviour that does **not** require a device or simulator — JS engine integration points, command orchestration end-to-end, cancellation / coroutine semantics. Notable suites:\n\n- `IntegrationTest.kt` — full `Maestro` orchestration against an in-process `FakeDriver` (defined in `maestro-test/src/main/kotlin/maestro/test/drivers/`: `FakeDriver`, `FakeLayoutElement`, `FakeTimer`). Covers test-run cancellation (`CancellationException`, `withTimeout`, supervisor scopes) and the full command lifecycle without a real device.\n- `GraalJsEngineTest.kt` / shared `JsEngineTest.kt` — Maestro's JS extension points (`evalScript`, JS-evaluated assertions/conditions). Exercises `org.graalvm.polyglot` directly.\n- `FlowControllerTest.kt`, `DeepestMatchingElementTest.kt` — orchestration and view-hierarchy logic.\n\nStack: **JUnit 5**, **Google Truth**, **WireMock JRE8** (HTTP fakes), plus the in-house `FakeDriver` fixtures listed above. No mocks of Maestro's own classes — tests run real `Maestro` against the fakes.\n\n```bash\n./gradlew :maestro-test:test\n```\n\n### Simulator tests (opt-in, inside a module's unit tests)\n\nFor driver code whose input is produced by the platform itself — a line in the simulator's log, a file macOS writes — and which a hand-written fixture therefore cannot keep honest. The test runs the real class against a booted simulator, with no CLI, flow or XCTest runner. It is skipped unless `MAESTRO_TEST_SIMULATOR_UDID` is set, so `./gradlew test` behaves the same everywhere else; the `test-ios` job of `test-e2e.yaml` sets it. Example: `IOSDriverCrashSimulatorTest`.\n\n```bash\nMAESTRO_TEST_SIMULATOR_UDID=<booted-udid> ./gradlew :maestro-client:test --tests '*SimulatorTest'\n```\n\n### E2E tests (`e2e/`)\n\nSmoke-test every Maestro command across Android, iOS, and Web on real fixture apps. Maestro is its own dogfood harness: the CLI executes Maestro flow YAMLs against the fixtures, asserting both the framework's commands and the platform drivers behave correctly.\n\nStack: **Maestro CLI itself** (dogfood) + `e2e/run_tests` shell driver + GHA workflow (`.github/workflows/test-e2e.yaml`). Fixture and suite layout is in \"E2E test fixtures (`e2e/`)\" above.\n\n```bash\ncd e2e && ./run_tests <android|ios|web>     # local\ngh workflow run test-e2e.yaml --ref <branch> -f android_version=android-<N>   # CI\n```\n\n**Two roles for the same E2E setup.** The same suite serves both purposes — treat them identically:\n\n1. **Regression smoke** — every PR that touches Maestro source runs the suite on the current platform versions, catching behaviour breakage on existing platforms.\n2. **New-OS validation** — when launching a new Android API level or iOS version, the same flows are dispatched against the new system image to confirm Maestro still works. This is what `bump-android-version` (and the planned `bump-ios-version`) drives.\n\nA flow breaking for either reason is a real regression — fix in `maestro-android/`, `maestro-client/`, or `e2e/demo_app/`, not in `test-e2e.yaml` (see \"What NOT to do\").\n\n**Multiple apps for framework-specific coverage.** `demo_app/` (Flutter) is the default fixture and exercises every Maestro command. When a target is **framework-specific** (SwiftUI, React Native, Jetpack Compose specifics, WebView quirks, etc.), add a separate workspace: flow YAMLs directly under `e2e/workspaces/<name>/`, a binary under `e2e/apps/` if there is an app to install, **and a row in `e2e/workspaces.txt`** — `validate_workspaces` fails a workspace that has flows but no row, since without one `run_tests` would never find it. Existing examples: `simple_web_view` (WebView coverage), `wikipedia` (real-world third-party app), `web` (browser flows, no app). The workflow's `app` input narrows a manual dispatch to one workspace: `... -f app=simple_web_view`.\n\n### MCP server evals (`maestro-cli/src/test/mcp/`)\n\nLLM-behaviour evaluations and tool-functionality tests for the MCP server inside `maestro-cli`. Stack: **`mcp-server-tester`** (npm package, run via `npx`) consuming YAML definitions (`full-evals.yaml`, `inspect-screen-evals.yaml`, `tool-tests-{with,without}-device.yaml`) plus per-platform setup scripts under `setup/`. See `maestro-cli/src/test/mcp/README.md` for the model list, scorers, and how to run.\n\n```bash\n./run_mcp_tool_tests.sh ios     # tool-functionality (fast)\n./run_mcp_evals.sh ios          # LLM behaviour (slower)\n```\n\n## Conventions\n\n- Kotlin 1.9 / JVM 17. Gradle. No DI framework — services are constructed manually.\n- Protobuf for the on-device wire format (`maestro-proto/`).\n- Coroutines with explicit dispatchers; `runBlocking` only at entry points.\n- Exposed exceptions classify failures (retryable vs terminal) — see `maestro-orchestra/src/main/java/maestro/orchestra/error/`.\n- **A run's files fall into three buckets.** *Inputs* (flow YAML, scripts, `assertScreenshot` references, `addMedia` files) are read from the workspace and never written to. *Outputs* — anything a user may want after the run — are allocated through `ArtifactsGenerator`, which places them in the artifacts bundle and lists them in `manifest.json`. *Scratch* files go through `TempFileHandler` (below). Callers decide what happens to those folders after the run and may keep only what the manifest lists, so an output written anywhere else — beside an input especially — can be silently lost. `RunOutputStaysInBundleTest` (`maestro-test/`) enforces this and makes every new command declare whether it creates files.\n- **Temp files and directories go through `maestro.utils.TempFileHandler`**, not `java.nio.file.Files.createTempFile/createTempDirectory` directly. `TempFileHandler` is a `Closeable` that recursively cleans up everything it allocated on `close()`. Direct `Files.createTempFile(...)` skips that lifecycle and leaks `/tmp` content (especially painful on long-lived JVMs like the cloud worker). Construct a `TempFileHandler` near the lifecycle owner, call its `createTempFile` / `createTempDirectory`, and `close()` it in a `finally`.\n\n## Where Claude Code resources live\n\n- `.claude/skills/*` — skills (workflows). Each skill's `SKILL.md` references this file for module roles.\n- `.claude/agents/*.md` — subagents (e.g. `diagnose-maestro-failure.md`). Their input/output contracts are documented in each file.\n\n## What NOT to do\n\n- Don't fix driver-behaviour gaps by patching `.github/workflows/test-e2e.yaml` (e.g. extra `adb shell settings put …`, command-line tweaks, AVD pre-config). Workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/`, `maestro-client/`, or `e2e/demo_app/` instead so the fix ships with the driver APKs. Workflow edits are valid for shape-changes (matrix, retention, dispatch inputs) and the narrow third-party-FRE exception documented in skill files.\n- Don't edit checked-in driver artifacts (`maestro-app.apk`, `maestro-server.apk`, `maestro-android-source.sha256`, `maestro-driver-ios*.zip`) by hand — they are gradle finalizer outputs.\n- Don't commit local changes to the iOS driver zips. A local build regenerates two zips — `maestro-driver-ios.zip` and `maestro-driver-iosUITests-Runner.zip` — under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`. This is normal: they're used by local builds. But although they're checked into the repo, they're managed exclusively by CI, so leave any local modifications to them out of your commits.\n- Don't modify existing flows in `failing/` to make them pass — that's the negative-path suite by design.\n","e2e/demo_app/.maestro/commands/assertScreenshotCroppedHEIC.yaml":"appId: com.example.example\ntags:\n  - failing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedHEIC.heic\n    cropOn:\n      id: \"testContainer\"\n","e2e/demo_app/.maestro/commands/assertVisible.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertVisible: 'Form Test'\n\n- assertVisible:\n    text: 'Form Test'\n\n- assertVisible:\n    id: 'fabAddIcon'","e2e/demo_app/.maestro/commands/assertScreenshotCroppedJPG.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedJPG.jpg\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n","e2e/demo_app/.maestro/commands/assertScreenshotCropped.yaml":"appId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCropped.png\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n","e2e/demo_app/.maestro/commands/assertTrue.yaml":"appId: com.example.example\ntags:\n  - passing\nenv:\n  THING: \"five\"\n---\n\n- launchApp # For idempotence of sections\n\n- assertTrue: ${\"test\" == \"test\"}\n\n- assertTrue:\n    condition: ${12 < 20}\n\n- assertTrue:\n    condition: ${THING == \"five\"} # Using the env at the top of the file","e2e/demo_app/.maestro/commands/back.yaml":"appId: com.example.example\ntags:\n  - passing\n  - android\n---\n- launchApp # For idempotence of sections\n\n- tapOn: 'Form Test'\n- assertVisible: 'Login'\n- back\n- assertVisible: 'Form Test'","e2e/demo_app/.maestro/commands/clearState.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/clearState.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/clearState.yaml)","e2e/demo_app/.maestro/commands/copyTextFrom.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/copyTextFrom.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/copyTextFrom.yaml)","e2e/demo_app/.maestro/commands/doubleTapOn.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/doubleTapOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/doubleTapOn.yaml)","e2e/demo_app/.maestro/commands/eraseText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/eraseText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/eraseText.yaml)","e2e/demo_app/.maestro/commands/evalScript.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/evalScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/evalScript.yaml)","e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml)","e2e/demo_app/.maestro/commands/hideKeyboard.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/hideKeyboard.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/hideKeyboard.yaml)","e2e/demo_app/.maestro/commands/inputRandomCityName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCityName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCityName.yaml)","e2e/demo_app/.maestro/commands/inputRandomColorName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomColorName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomColorName.yaml)","e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml)","e2e/demo_app/.maestro/commands/inputRandomEmail.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomEmail.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomEmail.yaml)","e2e/demo_app/.maestro/commands/inputRandomNumber.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomNumber.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomNumber.yaml)","e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml)","e2e/demo_app/.maestro/commands/inputRandomText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomText.yaml)","e2e/demo_app/.maestro/commands/inputText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputText.yaml)","e2e/demo_app/.maestro/commands/killApp.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/killApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/killApp.yaml)","e2e/demo_app/.maestro/commands/launchApp.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/launchApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/launchApp.yaml)","e2e/demo_app/.maestro/commands/longPressOn.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/longPressOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/longPressOn.yaml)","e2e/demo_app/.maestro/commands/openLink.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/openLink.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/openLink.yaml)","e2e/demo_app/.maestro/commands/pasteText.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pasteText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pasteText.yaml)","e2e/demo_app/.maestro/commands/pressKey.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pressKey.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pressKey.yaml)","e2e/demo_app/.maestro/commands/repeat.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/repeat.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/repeat.yaml)","e2e/demo_app/.maestro/commands/retry.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/retry.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/retry.yaml)","e2e/demo_app/.maestro/commands/runFlow.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/runFlow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runFlow.yaml)","e2e/demo_app/.maestro/commands/runScript.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/runScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runScript.yaml)","e2e/demo_app/.maestro/commands/scroll.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/scroll.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scroll.yaml)","e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml)","e2e/demo_app/.maestro/commands/setAirplaneMode.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setAirplaneMode.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setAirplaneMode.yaml)","e2e/demo_app/.maestro/commands/setLocation.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setLocation.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setLocation.yaml)","e2e/demo_app/.maestro/commands/setOrientation.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setOrientation.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setOrientation.yaml)","e2e/demo_app/.maestro/commands/stopApp.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/stopApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/stopApp.yaml)","e2e/demo_app/.maestro/commands/swipe.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/swipe.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe.yaml)","e2e/demo_app/.maestro/commands/swipe_from_point.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/swipe_from_point.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe_from_point.yaml)","e2e/demo_app/.maestro/commands/takeScreenshot.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/takeScreenshot.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshot.yaml)","e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml)","e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml)","e2e/demo_app/.maestro/commands/travel.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/travel.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/travel.yaml)","e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md — Maestro\n\nShared context for any Claude Code skill or subagent operating in this repo. Skills (`.claude/skills/*`) reference this file rather than restating module roles; if a description here drifts from reality, fix it here once and every skill follows.\n\n## Commits, pull requests, and anything else you publish\n\nThis is a public repo. Anything pushed or posted is published permanently: commits, branch names, code and comments, test fixtures and flows, PR and issue text, review comments. Never:\n\n- **Publish secrets**, tokens, credentials, or sensitive `.env` contents.\n- **Add AI-attribution trailers** (`Co-authored-by:` for a tool, \"Generated with…\", session links), or `Signed-off-by`.\n\n**Staff contributors only**: if you know which customer, org, or deployment a change is for — from a tracker, Slack, or Maestro Cloud — that's staff context and must not reach the repo. Don't name them, and don't include what identifies them: their app's package or bundle ID, flows or screenshots from their app, Cloud IDs or dashboard URLs, or an internal tracker's contents. Reference trackers by ID only (`MA-1234`, `#3504`). Reproduce against the demo app, not their app.\n\nFull rules — PR title and body, sensitive info: [`docs/pull-requests.md`](docs/pull-requests.md); issues: [`docs/issues.md`](docs/issues.md). Opening a PR or issue with `--body` (`gh pr create`, `gh issue create`) skips the templates in `.github/`, so read the matching doc first and use its headings. Open a PR as ready for review when it is; open it as a draft only when there's a reason not to ask for a human yet, such as checking CI passes.\n\n## Module map\n\nTop-level Gradle modules. Code lives under each module's `src/main/`.\n\n| Module                       | Role                                                                                                                                                                                                                                                                                     |\n|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `maestro-android/`           | On-device Android driver. Kotlin sources compile to two checked-in APKs (`maestro-app.apk`, `maestro-server.apk`) consumed by `maestro-client/`. The build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update those APKs plus a `maestro-android-source.sha256` checksum.    |\n| `maestro-ios-driver/`        | Host side of iOS driver wrapper (Kotlin). The actual XCTest runner lives in `maestro-ios-xctest-runner/`.                                                                                                                                                                                |\n| `maestro-ios-xctest-runner/` | Swift XCTest runner that runs on the iOS device/simulator. The compiled artifacts (`maestro-driver-ios*.zip`) are checked in under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`.                                                                |\n| `maestro-ios/`               | iOS host-side glue (small — most iOS host code lives in `maestro-client/`).                                                                                                                                                                                                              |\n| `maestro-client/`            | Host-side Kotlin SDK that drives devices. Platform drivers live in `src/main/java/maestro/drivers/`: `AndroidDriver.kt`, `IOSDriver.kt`, `WebDriver.kt`, `CdpWebDriver.kt`. This is where most \"auto-grant\", \"auto-dismiss\", system-dialog handling and platform-specific quirks belong. |\n| `maestro-orchestra/`         | Command execution layer. `Orchestra.kt` interprets each Maestro command, applies retries, manages the command lifecycle. Sub-packages: `error/`, `filter/`, `workspace/`, `yaml/`.                                                                                                       |\n| `maestro-orchestra-models/`  | Shared command/data models (used by `maestro-orchestra/` and consumers).                                                                                                                                                                                                                 |\n| `maestro-cli/`               | CLI entry point + MCP server. Mixed Kotlin (~100 files) + Swift (~56 files for iOS-related CLI bits).                                                                                                                                                                                    |\n| `maestro-utils/`             | Shared utilities.                                                                                                                                                                                                                                                                        |\n| `maestro-web/`               | Web (browser) driver pieces.                                                                                                                                                                                                                                                             |\n| `maestro-proto/`             | Protobuf definitions shared across modules.                                                                                                                                                                                                                                              |\n| `maestro-test/`              | Cross-module tests that doesn't require devices.                                                                                                                                                                                                                                         |\n\n## E2E test fixtures (`e2e/`)\n\nShipped fixtures used by `.github/workflows/test-e2e.yaml`. Run via `e2e/run_tests <android|ios|web>` (see `e2e/run_tests` for env-var inputs `MAESTRO_APP`, `MAESTRO_FLOW_PATH`, `FIXTURES_PORT`).\n\n**Web flows need a fixtures server.** They fetch their pages from `http://127.0.0.1:7357`, which `run_tests web` starts and stops for itself. Running one web flow directly does not, and the flow then fails at `launchApp` with a connection error (`net::ERR_CONNECTION_REFUSED`). Run `e2e/ensure_fixtures` first — it is idempotent and waits until the server answers.\n\n| Path                     | Role                                                                                                                                                                    |\n|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `e2e/demo_app/`          | Flutter demo app whose only purpose is to exercise Maestro features. Contains its own `CLAUDE.md`. Built binaries are uploaded to a GCS bucket and re-downloaded by CI. |\n| `e2e/demo_app/.maestro/` | Maestro flow YAMLs that drive the demo app.                                                                                                                             |\n| `e2e/workspaces/`        | Further workspaces, each holding its flow YAMLs directly (no `.maestro/` subdirectory): `simple_web_view`, `wikipedia`, and `web` — which is browser flows with no app behind it. |\n| `e2e/workspaces.txt`     | The workspaces the suite knows about, and for each one its path, the platforms it runs on, and whether it carries `failing` flows. `run_tests` validates it before running anything. |\n| `e2e/run_tests`          | Test driver invoked by the workflow.                                                                                                                                    |\n| `e2e/ensure_fixtures`    | Starts that server unless one is already up, and waits until it answers. Call before running a web flow by hand.                                                         |\n| `e2e/serve_fixtures`     | The static server itself, in the foreground, for `workspaces/web/fixtures/`.                                                                                             |\n| `e2e/list_workspaces`    | Prints the workspace names from the manifest, so callers need not parse it.                                                                                             |\n\n### `passing/` vs `failing/` suites\n\nTag-based filters inside the YAML flows split test runs into two suites at execution time:\n\n- `passing/` — flows tagged `passing`. **Expected to pass.** Any failure here is a real regression. This is the only suite the diagnose agent reads.\n- `failing/` — flows tagged `failing`. **Expected to fail** (negative-path coverage: assertions that should not match, commands that should error). The workflow inverts the success check on this suite. Do not treat `failing/` artifacts as regressions.\n\nArtifacts land at `<artifact_root>/tests/<app>/<suite>/`:\n\n## `test-e2e.yaml` workflow contract\n\n`.github/workflows/test-e2e.yaml` is the validation harness for both PR triggers and manual `workflow_dispatch` (e.g. validating a new Android API level or iOS version). Contract:\n\n- **`workflow_dispatch` inputs** — `android_version` (choice enum), `app` (string, default `demo_app`), `flow` (string, optional single-flow). The `validate-inputs` job rejects `android_version <= android-29`, missing `app` workspace, or ambiguous `flow`. (See PR #3226.)\n- **`pull_request` triggers** are byte-identical to the prior behaviour; manual dispatches use the new narrowing knobs.\n- **`test-android` job** boots an emulator on `system-images;${android_version};google_apis;x86_64` and runs `e2e/run_tests android`.\n\nSkills that bump platform versions (Android API levels, iOS versions) drive this workflow via `gh workflow run test-e2e.yaml --ref <branch> -f android_version=<...>`.\n\n## Testing\n\nThree layers — unit, integration, E2E — plus MCP-specific evals. Each layer has a different cost/coverage trade-off; default to the lowest layer that can express the test.\n\n### Unit tests (per module, `src/test/kotlin/`)\n\nStandard per-class tests. Stack: **JUnit 5** (`junit-jupiter-api` + `-params` + `-engine`), **Google Truth** for assertions, **MockK** for mocks. Each module's `build.gradle.kts` enables the platform via `tasks.named<Test>(\"test\") { useJUnitPlatform() }`.\n\n```bash\n./gradlew :maestro-orchestra:test          # one module\n./gradlew test                              # all modules\n```\n\n### Integration tests (`maestro-test/`)\n\nCross-module tests for behaviour that does **not** require a device or simulator — JS engine integration points, command orchestration end-to-end, cancellation / coroutine semantics. Notable suites:\n\n- `IntegrationTest.kt` — full `Maestro` orchestration against an in-process `FakeDriver` (defined in `maestro-test/src/main/kotlin/maestro/test/drivers/`: `FakeDriver`, `FakeLayoutElement`, `FakeTimer`). Covers test-run cancellation (`CancellationException`, `withTimeout`, supervisor scopes) and the full command lifecycle without a real device.\n- `GraalJsEngineTest.kt` / shared `JsEngineTest.kt` — Maestro's JS extension points (`evalScript`, JS-evaluated assertions/conditions). Exercises `org.graalvm.polyglot` directly.\n- `FlowControllerTest.kt`, `DeepestMatchingElementTest.kt` — orchestration and view-hierarchy logic.\n\nStack: **JUnit 5**, **Google Truth**, **WireMock JRE8** (HTTP fakes), plus the in-house `FakeDriver` fixtures listed above. No mocks of Maestro's own classes — tests run real `Maestro` against the fakes.\n\n```bash\n./gradlew :maestro-test:test\n```\n\n### Simulator tests (opt-in, inside a module's unit tests)\n\nFor driver code whose input is produced by the platform itself — a line in the simulator's log, a file macOS writes — and which a hand-written fixture therefore cannot keep honest. The test runs the real class against a booted simulator, with no CLI, flow or XCTest runner. It is skipped unless `MAESTRO_TEST_SIMULATOR_UDID` is set, so `./gradlew test` behaves the same everywhere else; the `test-ios` job of `test-e2e.yaml` sets it. Example: `IOSDriverCrashSimulatorTest`.\n\n```bash\nMAESTRO_TEST_SIMULATOR_UDID=<booted-udid> ./gradlew :maestro-client:test --tests '*SimulatorTest'\n```\n\n### E2E tests (`e2e/`)\n\nSmoke-test every Maestro command across Android, iOS, and Web on real fixture apps. Maestro is its own dogfood harness: the CLI executes Maestro flow YAMLs against the fixtures, asserting both the framework's commands and the platform drivers behave correctly.\n\nStack: **Maestro CLI itself** (dogfood) + `e2e/run_tests` shell driver + GHA workflow (`.github/workflows/test-e2e.yaml`). Fixture and suite layout is in \"E2E test fixtures (`e2e/`)\" above.\n\n```bash\ncd e2e && ./run_tests <android|ios|web>     # local\ngh workflow run test-e2e.yaml --ref <branch> -f android_version=android-<N>   # CI\n```\n\n**Two roles for the same E2E setup.** The same suite serves both purposes — treat them identically:\n\n1. **Regression smoke** — every PR that touches Maestro source runs the suite on the current platform versions, catching behaviour breakage on existing platforms.\n2. **New-OS validation** — when launching a new Android API level or iOS version, the same flows are dispatched against the new system image to confirm Maestro still works. This is what `bump-android-version` (and the planned `bump-ios-version`) drives.\n\nA flow breaking for either reason is a real regression — fix in `maestro-android/`, `maestro-client/`, or `e2e/demo_app/`, not in `test-e2e.yaml` (see \"What NOT to do\").\n\n**Multiple apps for framework-specific coverage.** `demo_app/` (Flutter) is the default fixture and exercises every Maestro command. When a target is **framework-specific** (SwiftUI, React Native, Jetpack Compose specifics, WebView quirks, etc.), add a separate workspace: flow YAMLs directly under `e2e/workspaces/<name>/`, a binary under `e2e/apps/` if there is an app to install, **and a row in `e2e/workspaces.txt`** — `validate_workspaces` fails a workspace that has flows but no row, since without one `run_tests` would never find it. Existing examples: `simple_web_view` (WebView coverage), `wikipedia` (real-world third-party app), `web` (browser flows, no app). The workflow's `app` input narrows a manual dispatch to one workspace: `... -f app=simple_web_view`.\n\n### MCP server evals (`maestro-cli/src/test/mcp/`)\n\nLLM-behaviour evaluations and tool-functionality tests for the MCP server inside `maestro-cli`. Stack: **`mcp-server-tester`** (npm package, run via `npx`) consuming YAML definitions (`full-evals.yaml`, `inspect-screen-evals.yaml`, `tool-tests-{with,without}-device.yaml`) plus per-platform setup scripts under `setup/`. See `maestro-cli/src/test/mcp/README.md` for the model list, scorers, and how to run.\n\n```bash\n./run_mcp_tool_tests.sh ios     # tool-functionality (fast)\n./run_mcp_evals.sh ios          # LLM behaviour (slower)\n```\n\n## Conventions\n\n- Kotlin 1.9 / JVM 17. Gradle. No DI framework — services are constructed manually.\n- Protobuf for the on-device wire format (`maestro-proto/`).\n- Coroutines with explicit dispatchers; `runBlocking` only at entry points.\n- Exposed exceptions classify failures (retryable vs terminal) — see `maestro-orchestra/src/main/java/maestro/orchestra/error/`.\n- **A run's files fall into three buckets.** *Inputs* (flow YAML, scripts, `assertScreenshot` references, `addMedia` files) are read from the workspace and never written to. *Outputs* — anything a user may want after the run — are allocated through `ArtifactsGenerator`, which places them in the artifacts bundle and lists them in `manifest.json`. *Scratch* files go through `TempFileHandler` (below). Callers decide what happens to those folders after the run and may keep only what the manifest lists, so an output written anywhere else — beside an input especially — can be silently lost. `RunOutputStaysInBundleTest` (`maestro-test/`) enforces this and makes every new command declare whether it creates files.\n- **Temp files and directories go through `maestro.utils.TempFileHandler`**, not `java.nio.file.Files.createTempFile/createTempDirectory` directly. `TempFileHandler` is a `Closeable` that recursively cleans up everything it allocated on `close()`. Direct `Files.createTempFile(...)` skips that lifecycle and leaks `/tmp` content (especially painful on long-lived JVMs like the cloud worker). Construct a `TempFileHandler` near the lifecycle owner, call its `createTempFile` / `createTempDirectory`, and `close()` it in a `finally`.\n\n## Where Claude Code resources live\n\n- `.claude/skills/*` — skills (workflows). Each skill's `SKILL.md` references this file for module roles.\n- `.claude/agents/*.md` — subagents (e.g. `diagnose-maestro-failure.md`). Their input/output contracts are documented in each file.\n\n## What NOT to do\n\n- Don't fix driver-behaviour gaps by patching `.github/workflows/test-e2e.yaml` (e.g. extra `adb shell settings put …`, command-line tweaks, AVD pre-config). Workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/`, `maestro-client/`, or `e2e/demo_app/` instead so the fix ships with the driver APKs. Workflow edits are valid for shape-changes (matrix, retention, dispatch inputs) and the narrow third-party-FRE exception documented in skill files.\n- Don't edit checked-in driver artifacts (`maestro-app.apk`, `maestro-server.apk`, `maestro-android-source.sha256`, `maestro-driver-ios*.zip`) by hand — they are gradle finalizer outputs.\n- Don't commit local changes to the iOS driver zips. A local build regenerates two zips — `maestro-driver-ios.zip` and `maestro-driver-iosUITests-Runner.zip` — under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`. This is normal: they're used by local builds. But although they're checked into the repo, they're managed exclusively by CI, so leave any local modifications to them out of your commits.\n- Don't modify existing flows in `failing/` to make them pass — that's the negative-path suite by design.\n","isInternal":false,"tokens":3819,"sizeBytes":18533},{"name":"SKILL.md","path":".claude/skills/bump-android-version/SKILL.md","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/.claude/skills/bump-android-version/SKILL.md","title":"bump-android-version","category":"anthropic-skill","format":"markdown","content":"---\nname: bump-android-version\ndescription: Use when bumping Maestro's Android compileSdk/targetSdk to a new API level and validating end-to-end against the test-e2e GHA workflow until the test-android job is green.\n---\n\n# Bump Android Version\n\n## Overview\n\nDrive an Android-version bump in Maestro commit-by-commit on a single feature branch: edit gradle, rebuild the on-device driver APKs, push a draft PR, dispatch `test-e2e.yaml` against the new system image, watch the `test-android` job, and diagnose+fix any newly-failing `passing/` flows. Loop until green.\n\n## When to Use\n\n- User asks to bump Maestro's Android `compileSdk` / `targetSdk` (e.g. \"bump to API 36\").\n- User wants to validate Maestro against a new Android system image.\n\nOne commit per logical step. Don't bundle the gradle bump and the APK rebuild — they need to be separately revertible. After a failing run, fixes are committed and the workflow re-dispatched. Loop until green; do not stop on the first re-dispatch.\n\n**The loop only acts on `passing/` failures.** `tests/demo_app/passing/` is the regression-detection suite — its flows are expected to pass, so a failure there is a real signal. `tests/demo_app/failing/` is the negative-path suite (its flows are *expected* to fail) and is ignored end-to-end. The diagnose agent rejects `failing/` artifacts and rejects retry-recovered flows in `passing/` (see [`.claude/agents/diagnose-maestro-failure.md`](../../agents/diagnose-maestro-failure.md)). If the run is otherwise green except for `failing/`, that's a green run.\n\n## Pre-flight\n\n- Working tree clean (`git status`).\n- `main` up to date: `git fetch origin && git checkout main && git pull --ff-only origin main`.\n- The workflow's `workflow_dispatch` inputs `android_version` (and optional `app` / `flow`) must exist on the branch. \n- `android_version` is a `choice` input — its options live in `.github/workflows/test-e2e.yaml`. If `<new>` isn't already listed (e.g. bumping to API 37), add it to the `options:` list in a separate commit before dispatching, otherwise the workflow rejects the input.\n- The `validate-inputs` job rejects `android_version <= android-29`. Bumps below API 29 aren't supported.\n- Branch name: `bump-android-api-<old>-<new>` (e.g. `bump-android-api-34-36`).\n\n```bash\ngit checkout -b bump-android-api-<old>-<new> origin/main\n```\n\n## Commit 1: bump compile/target SDK\n\nRead current values from `maestro-android/build.gradle.kts` (`compileSdk` and `targetSdk`). Tell the user the current values verbatim, then ask for the new target:\n\n> Current `compileSdk` / `targetSdk` = `<N>`. What's the new API level?\n\nEdit both lines. Also `rg -n \"compileSdk\\s*=|targetSdk\\s*=\" --type kotlin --type-add 'kotlin:*.kts'` to catch consistent occurrences in other modules — only update those that intentionally track the same SDK. Commit:\n\n```bash\ngit add maestro-android/build.gradle.kts <other touched files>\ngit commit -m \"chore(android): bump compile/target SDK from <old> to <new>\"\n```\n\n## Commit 2: rebuild driver APKs\n\n```bash\n./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest\n```\n\nThe build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update three checked-in files. Stage and commit *only* those:\n\n```bash\ngit add maestro-client/src/main/resources/maestro-app.apk \\\n        maestro-client/src/main/resources/maestro-server.apk \\\n        maestro-client/src/main/resources/maestro-android-source.sha256\ngit commit -m \"chore(android): rebuild driver APKs against API <new>\"\n```\n\nIf `assemble` fails, **stop and surface the failure to the user** with the gradle error and a suggested fix. Do not proceed without rebuilt driver APKs. Once we have assemble fixed and new drivers are commited, we can proceed to the next step.\n\n## Step 3: push draft PR + dispatch\n\n```bash\ngit push -u origin bump-android-api-<old>-<new>\ngh pr create --draft \\\n  --title \"chore(android): bump compile/target SDK to <new>\" \\\n  --body \"Validates Maestro against API <new>. Driver APKs rebuilt. Test-e2e dispatched with system-images;android-<new>;google_apis;x86_64.\"\n\ngh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n  -f android_version=\"android-<new>\"\n\n# capture the run id\nsleep 3\ngh run list --workflow test-e2e.yaml --branch bump-android-api-<old>-<new> --limit 1 --json databaseId,status\n```\n\nAlways pass `-f android_version=android-<new>` — the default is `android-32` and would silently re-test the old API. The workflow constructs the full `system-images;...` string internally. `app` and `flow` are optional narrowing knobs (defaults: `demo_app`, all flows); leave them off for the full bump-validation run.\n\n## Step 4: watch the test-android job\n\n```bash\ngh run watch <run_id>\n```\n\nBlock in the foreground; the run is the bottleneck. **Green here means every `passing/` flow passed (terminally — retry-recovered counts as passed). `failing/` outcomes do not affect the verdict.** On green: update PR from draft → ready, done.\n\nOn red, download artifacts:\n\n```bash\ngh run download <run_id> --name maestro-root-dir-android -D /tmp/maestro-android-<run_id>\n```\n\n## Step 5: diagnose (delegated to subagent)\n\nDispatch the **`diagnose-maestro-failure`** subagent with the artifact directory `/tmp/maestro-android-<run_id>` (or the GHA run URL — the subagent handles both). It diagnoses each passing-suite failure in isolation and returns a structured report grouped by root cause (cascades collapsed). It does **not** apply fixes — that's your job here.\n\nWhen the subagent returns:\n\n**Fixes go in Maestro source — not in `.github/workflows/test-e2e.yaml`.** A failing flow on a new API level reflects a behaviour Maestro users will also hit on their own machines/CI, not just ours. Patching the GHA workflow (e.g. extra `adb shell settings put …`, command-line tweaks, AVD setup steps) hides the regression from real users and ships a Maestro that's only green inside our CI.\n\nAcceptable fix targets and their roles are documented in [`AGENTS.md`](../../../AGENTS.md) (modules `maestro-android/`, `maestro-client/`, `maestro-orchestra/`, `e2e/demo_app/`). Driver-behaviour fixes belong in `maestro-android/` or `maestro-client/`; fixture-only issues belong in `e2e/demo_app/`. Editing the driver source means rebuilding the driver APKs (`./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest`) before re-dispatching.\n\nValid edits to `test-e2e.yaml` during this loop are workflow-shape changes (matrix, retention, dispatch inputs) — and the narrow exception below. Anything else (e.g. extra `adb shell settings put …` to mask a Maestro driver gap) is off-limits. If the subagent proposes a workflow patch, push back: ask it (or yourself) where the same fix would live in `maestro-android/` or `maestro-client/` so users on the new API level inherit it automatically.\n\n**Exception — third-party app first-run UI not triggered by a Maestro API.** A pre-installed app's own onboarding (Chrome Welcome / \"Make Chrome your own\", browser default-app picker, Play Protect prompts that fire at app launch) is environment-harness state, not driver behaviour. Maestro doesn't expose a public API that triggers them — they're side effects of the OS image we picked. These may be pre-disabled in the AVD setup step (typical knobs: `setprop debug.chrome.command_line`, `settings put global …` flags). Test: *if Maestro called nothing related to this dialog, would it still appear?* If yes → CI workaround is fair game. If no (e.g. `setLocation` triggers GMS Location Accuracy via `FusedLocationProviderClient` inside the driver) → fix the driver instead.\n\n1. For each root cause in its report, present the proposed diff to the user (file path + one-line summary + diff + side effects). Reject any proposal that targets `test-e2e.yaml` for driver-behaviour reasons before showing it — re-scope to Maestro source first.\n2. **Ask consent explicitly per fix:**\n   > Proposed fix for `<root cause>`: `<one-liner>`. Apply?\n3. On approval: edit, commit with a focused message — don't bundle unrelated fixes.\n   ```bash\n   git commit -m \"fix(android-<new>): <summary>\"\n   ```\n4. On rejection: skip that fix; record what was rejected so you don't re-propose it next iteration.\n\nAfter all approved fixes for this iteration are committed:\n\n- **If any fix was applied:** push and re-dispatch, then loop back to **Step 4** with the new `run_id`.\n\n  ```bash\n  git push\n  gh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n    -f android_version=\"android-<new>\"\n  ```\n\n- **If no fixes were applied** (everything was out-of-scope, or the user rejected every proposal): pause and ask the user how to proceed. Do not keep dispatching.\n\n## Termination\n\nStop when `test-android` is green on the latest run. Then:\n\n```bash\ngh pr ready <pr_number>\n```\n\nIf the loop has gone three iterations without progress (same flow keeps failing for different reasons after fixes), pause and ask the user how to proceed — don't spin indefinitely.\n\n## Anti-patterns\n\n- **Bundling gradle + APK rebuild into one commit** — kills bisectability. Always two commits.\n- **Dispatching with default inputs** — `android_version` defaults to `android-32` and hits the old API. Always pass `-f android_version=android-<new>`.\n- **Forgetting to extend the `android_version` choice enum** — the workflow rejects API levels not listed in `options:`. Add `<new>` to the enum in a separate commit before the first dispatch.\n- **Auto-applying Maestro source fixes without consent** — every Kotlin patch needs explicit user approval first.\n- **Patching `.github/workflows/test-e2e.yaml` to mask a driver behaviour gap** — workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/` or `e2e/demo_app/` instead so the fix ships with the driver APKs.\n- **Treating `failing/` artifacts as regressions** — that suite is expected to fail.\n- **Treating `❌` screenshots in `passing/` as regressions on their own** — when `retryCommand` recovers a failed attempt, Maestro writes only the final `COMPLETED` entry to `commands-(<flow>).json` but leaves the `❌` screenshot from the failed attempt on disk (e.g. `screenshot-❌-<ts>-(retry).png` for the `retry` flow itself). The diagnose agent uses `commands-*.json` containing a terminal `FAILED` entry as the source of truth — orphan `❌` screenshots without a matching FAILED entry are noise.\n- **Committing on `main`** — always work on the bump branch. Verify with `git status` before every commit.\n- **Skipping the screenshot read** — the screenshot tells you what `maestro.log` and the JSON cannot. It's the highest-signal artifact. Always read it.\n","frontmatter":{"name":"bump-android-version","description":"Use when bumping Maestro's Android compileSdk/targetSdk to a new API level and validating end-to-end against the test-e2e GHA workflow until the test-android job is green."},"isInternal":false,"tokens":2676,"sizeBytes":10708},{"name":"addMedia.yaml","path":"e2e/demo_app/.maestro/commands/addMedia.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/addMedia.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.google.android.documentsui\ntags:\n  - passing\n  - android # TODO: How do we prove this works on iOS?\n---\n\n- addMedia:\n  - ../assets/image.png\n\n- launchApp: com.google.android.documentsui # Files app\n\n- tapOn: Show roots\n- tapOn: Images\n\n# Locate the image via Search instead of opening the \"Pictures\" album. On a\n# loaded emulator a tap on the album can be misread as a long-press, which\n# selects the album instead of opening it and leaves the test stranded on the\n# album list. The search icon is a button, so a long-press on it is harmless.\n\n- tapOn:\n    id: \"com.google.android.documentsui:id/option_menu_search\"\n- inputText: \"image\" # Search a partial term (\"image\") so the query text differs from the file name\n- assertVisible: image.png\n\n- longPressOn: image.png\n- tapOn:\n    id: \"com.google.android.documentsui:id/action_menu_delete\"\n- tapOn: OK\n- assertNotVisible: image.png\n","isInternal":false,"tokens":233,"sizeBytes":894},{"name":"assertNotVisible.yaml","path":"e2e/demo_app/.maestro/commands/assertNotVisible.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/assertNotVisible.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertNotVisible: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'Form Test'\n    enabled: false","isInternal":false,"tokens":65,"sizeBytes":225},{"name":"assertScreenshotCropped.yaml","path":"e2e/demo_app/.maestro/commands/assertScreenshotCropped.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/assertScreenshotCropped.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCropped.png\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n","isInternal":false,"tokens":69,"sizeBytes":262},{"name":"assertScreenshotCroppedHEIC.yaml","path":"e2e/demo_app/.maestro/commands/assertScreenshotCroppedHEIC.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/assertScreenshotCroppedHEIC.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - failing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedHEIC.heic\n    cropOn:\n      id: \"testContainer\"\n","isInternal":false,"tokens":63,"sizeBytes":237},{"name":"assertScreenshotCroppedJPG.yaml","path":"e2e/demo_app/.maestro/commands/assertScreenshotCroppedJPG.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/assertScreenshotCroppedJPG.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedJPG.jpg\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n","isInternal":false,"tokens":71,"sizeBytes":265},{"name":"assertTrue.yaml","path":"e2e/demo_app/.maestro/commands/assertTrue.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/assertTrue.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - passing\nenv:\n  THING: \"five\"\n---\n\n- launchApp # For idempotence of sections\n\n- assertTrue: ${\"test\" == \"test\"}\n\n- assertTrue:\n    condition: ${12 < 20}\n\n- assertTrue:\n    condition: ${THING == \"five\"} # Using the env at the top of the file","isInternal":false,"tokens":78,"sizeBytes":276},{"name":"assertVisible.yaml","path":"e2e/demo_app/.maestro/commands/assertVisible.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/assertVisible.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertVisible: 'Form Test'\n\n- assertVisible:\n    text: 'Form Test'\n\n- assertVisible:\n    id: 'fabAddIcon'","isInternal":false,"tokens":55,"sizeBytes":200},{"name":"back.yaml","path":"e2e/demo_app/.maestro/commands/back.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/back.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"appId: com.example.example\ntags:\n  - passing\n  - android\n---\n- launchApp # For idempotence of sections\n\n- tapOn: 'Form Test'\n- assertVisible: 'Login'\n- back\n- assertVisible: 'Form Test'","isInternal":false,"tokens":54,"sizeBytes":185},{"name":"clearState.yaml","path":"e2e/demo_app/.maestro/commands/clearState.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/clearState.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/clearState.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/clearState.yaml)","tokens":300,"sizeBytes":212},{"name":"copyTextFrom.yaml","path":"e2e/demo_app/.maestro/commands/copyTextFrom.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/copyTextFrom.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/copyTextFrom.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/copyTextFrom.yaml)","tokens":300,"sizeBytes":216},{"name":"doubleTapOn.yaml","path":"e2e/demo_app/.maestro/commands/doubleTapOn.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/doubleTapOn.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/doubleTapOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/doubleTapOn.yaml)","tokens":300,"sizeBytes":214},{"name":"eraseText.yaml","path":"e2e/demo_app/.maestro/commands/eraseText.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/eraseText.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/eraseText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/eraseText.yaml)","tokens":300,"sizeBytes":210},{"name":"evalScript.yaml","path":"e2e/demo_app/.maestro/commands/evalScript.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/evalScript.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/evalScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/evalScript.yaml)","tokens":300,"sizeBytes":212},{"name":"extendedWaitUntil.yaml","path":"e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml)","tokens":300,"sizeBytes":226},{"name":"hideKeyboard.yaml","path":"e2e/demo_app/.maestro/commands/hideKeyboard.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/hideKeyboard.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/hideKeyboard.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/hideKeyboard.yaml)","tokens":300,"sizeBytes":216},{"name":"inputRandomCityName.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomCityName.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCityName.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCityName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCityName.yaml)","tokens":300,"sizeBytes":230},{"name":"inputRandomColorName.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomColorName.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomColorName.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomColorName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomColorName.yaml)","tokens":300,"sizeBytes":232},{"name":"inputRandomCountryName.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml)","tokens":300,"sizeBytes":236},{"name":"inputRandomEmail.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomEmail.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomEmail.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomEmail.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomEmail.yaml)","tokens":300,"sizeBytes":224},{"name":"inputRandomNumber.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomNumber.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomNumber.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomNumber.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomNumber.yaml)","tokens":300,"sizeBytes":226},{"name":"inputRandomPersonName.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml)","tokens":300,"sizeBytes":234},{"name":"inputRandomText.yaml","path":"e2e/demo_app/.maestro/commands/inputRandomText.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomText.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomText.yaml)","tokens":300,"sizeBytes":222},{"name":"inputText.yaml","path":"e2e/demo_app/.maestro/commands/inputText.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputText.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputText.yaml)","tokens":300,"sizeBytes":210},{"name":"killApp.yaml","path":"e2e/demo_app/.maestro/commands/killApp.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/killApp.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/killApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/killApp.yaml)","tokens":300,"sizeBytes":206},{"name":"launchApp.yaml","path":"e2e/demo_app/.maestro/commands/launchApp.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/launchApp.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/launchApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/launchApp.yaml)","tokens":300,"sizeBytes":210},{"name":"longPressOn.yaml","path":"e2e/demo_app/.maestro/commands/longPressOn.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/longPressOn.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/longPressOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/longPressOn.yaml)","tokens":300,"sizeBytes":214},{"name":"openLink.yaml","path":"e2e/demo_app/.maestro/commands/openLink.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/openLink.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/openLink.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/openLink.yaml)","tokens":300,"sizeBytes":208},{"name":"pasteText.yaml","path":"e2e/demo_app/.maestro/commands/pasteText.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pasteText.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pasteText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pasteText.yaml)","tokens":300,"sizeBytes":210},{"name":"pressKey.yaml","path":"e2e/demo_app/.maestro/commands/pressKey.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pressKey.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pressKey.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pressKey.yaml)","tokens":300,"sizeBytes":208},{"name":"repeat.yaml","path":"e2e/demo_app/.maestro/commands/repeat.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/repeat.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/repeat.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/repeat.yaml)","tokens":300,"sizeBytes":204},{"name":"retry.yaml","path":"e2e/demo_app/.maestro/commands/retry.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/retry.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/retry.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/retry.yaml)","tokens":300,"sizeBytes":202},{"name":"runFlow.yaml","path":"e2e/demo_app/.maestro/commands/runFlow.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runFlow.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/runFlow.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runFlow.yaml)","tokens":300,"sizeBytes":206},{"name":"runScript.yaml","path":"e2e/demo_app/.maestro/commands/runScript.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runScript.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/runScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/runScript.yaml)","tokens":300,"sizeBytes":210},{"name":"scroll.yaml","path":"e2e/demo_app/.maestro/commands/scroll.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scroll.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/scroll.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scroll.yaml)","tokens":300,"sizeBytes":204},{"name":"scrollUntilVisible.yaml","path":"e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/scrollUntilVisible.yaml)","tokens":300,"sizeBytes":228},{"name":"setAirplaneMode.yaml","path":"e2e/demo_app/.maestro/commands/setAirplaneMode.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setAirplaneMode.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setAirplaneMode.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setAirplaneMode.yaml)","tokens":300,"sizeBytes":222},{"name":"setLocation.yaml","path":"e2e/demo_app/.maestro/commands/setLocation.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setLocation.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setLocation.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setLocation.yaml)","tokens":300,"sizeBytes":214},{"name":"setOrientation.yaml","path":"e2e/demo_app/.maestro/commands/setOrientation.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setOrientation.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/setOrientation.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/setOrientation.yaml)","tokens":300,"sizeBytes":220},{"name":"stopApp.yaml","path":"e2e/demo_app/.maestro/commands/stopApp.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/stopApp.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/stopApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/stopApp.yaml)","tokens":300,"sizeBytes":206},{"name":"swipe_from_point.yaml","path":"e2e/demo_app/.maestro/commands/swipe_from_point.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe_from_point.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/swipe_from_point.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe_from_point.yaml)","tokens":300,"sizeBytes":224},{"name":"swipe.yaml","path":"e2e/demo_app/.maestro/commands/swipe.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/swipe.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/swipe.yaml)","tokens":300,"sizeBytes":202},{"name":"takeScreenshot.yaml","path":"e2e/demo_app/.maestro/commands/takeScreenshot.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshot.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/takeScreenshot.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshot.yaml)","tokens":300,"sizeBytes":220},{"name":"takeScreenshotFullScreen.yaml","path":"e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/takeScreenshotFullScreen.yaml)","tokens":300,"sizeBytes":240},{"name":"toggleAirplaneMode.yaml","path":"e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/toggleAirplaneMode.yaml)","tokens":300,"sizeBytes":228},{"name":"travel.yaml","path":"e2e/demo_app/.maestro/commands/travel.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/travel.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/travel.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/travel.yaml)","tokens":300,"sizeBytes":204},{"name":"waitForAnimationToEnd.yaml","path":"e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml","title":"Commands Skill","category":"anthropic-skill","format":"yaml","content":"# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/waitForAnimationToEnd.yaml)","tokens":300,"sizeBytes":234},{"name":"diagnose-maestro-failure.md","path":".claude/agents/diagnose-maestro-failure.md","rawUrl":"https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/.claude/agents/diagnose-maestro-failure.md","title":"diagnose-maestro-failure","category":"subagent-persona","format":"markdown","content":"---\nname: diagnose-maestro-failure\ndescription: Use when a Maestro test-android run has failed and a structured per-flow diagnosis is needed without bloating the caller's context. Accepts either a local artifact directory or a GitHub Actions run/job URL. Reads commands JSON, screenshots, and maestro.log; classifies each newly-failing passing/ flow; identifies cascade groups; and returns a structured report with proposed fixes. Does NOT edit code or commit — the caller is responsible for consent and application.\ntools: Bash, Read, Agent\n---\n\n# Diagnose Maestro Failure (subagent)\n\nYou diagnose Maestro `test-android` failures from CI artifacts and return a structured report. You do not modify code, commit, or push — the caller handles consent and application.\n\n## Input\n\nThe caller passes one of:\n\n- **Local artifact directory** — e.g. `/Users/.../maestro-root-dir-android` or `/tmp/maestro-android-<run_id>`. Must contain a `tests/` subtree.\n- **GitHub Actions URL** — `https://github.com/mobile-dev-inc/Maestro/actions/runs/<run_id>` or `.../runs/<run_id>/job/<job_id>`.\n\nIf a URL, extract the `run_id` and download:\n\n```bash\nRUN_ID=<extracted>\nDEST=/tmp/maestro-android-$RUN_ID\ngh run download \"$RUN_ID\" --repo mobile-dev-inc/Maestro \\\n  --name maestro-root-dir-android -D \"$DEST\"\n```\n\nIf the directory has no `tests/demo_app/passing/` subtree (e.g. iOS artifact, or the run failed before tests), report that and stop.\n\n## Where to Look\n\nOnly `<artifact_root>/tests/demo_app/passing/` is signal. `failing/` is the negative-path suite — its commands are *supposed* to fail. Ignore it entirely.\n\n```bash\ncd <artifact_root>/tests/demo_app/passing\ngrep -l '\"FAILED\"' commands-*.json\n```\n\nThe status field lives at `.[].metadata.status`. Possible values: `COMPLETED` (success) and `FAILED` (terminal failure). `commands-*.json` is the **only** authoritative source — `screenshot-❌-*.png` is not.\n\n**`commands-(retry).json` is the canonical example.** This flow exercises the `retryCommand` directive: a tap fails, Maestro retries it, the retry succeeds, and the flow passes. In the artifact:\n\n- All entries (including the recovered `tapOnElement`) have `metadata.status = \"COMPLETED\"` — Maestro writes only the final outcome of a recovered command, not the failed attempt.\n- A `screenshot-❌-<ts>-(retry).png` still exists, captured at the moment of the failed attempt before the retry succeeded.\n- The grep above does **not** match this file (no `\"FAILED\"` string anywhere), so the agent never investigates it. This is correct behaviour.\n\nThe risk is treating the `❌` screenshot as evidence of failure on its own. Don't. Trust the candidates from grep — they are the flows whose JSON contains a terminal `FAILED`. Any other `❌` screenshot in `passing/` is from a recovered retry and is irrelevant.\n\n## Per-Flow Investigation\n\nFor each candidate flow returned by the grep above:\n\n1. **Open `commands-(<flow>).json`.** Locate the entry whose `metadata.status == \"FAILED\"` — that's the terminal failure. Record: command type (the single key under `command`), error message, `metadata.timestamp` (ms epoch), `metadata.sequenceNumber`, `metadata.duration` (≈17000 ms = default-wait timeout, indicating UI-blocked, not crash).\n2. **Open the screenshot `screenshot-❌-<timestamp>-(<flow>).png` whose timestamp matches the FAILED entry.** This is the highest-signal artifact — JSON and logs alone misclassify when a system dialog is invisible to them. Always read it. Ignore `❌` screenshots from other timestamps in the same file — those are recovered retries.\n3. **Grep `maestro.log`** for the flow name and the timestamp window for surrounding driver activity, gRPC errors, and stack traces.\n\n## Classification\n\n| Screen at failure | Class | Where the fix lives |\n|---|---|---|\n| Expected app screen, but selector wouldn't match | Test flow issue | `e2e/demo_app/.maestro/**/*.yaml` |\n| Android / GMS system dialog blocking the app | Maestro driver gap (auto-grant / auto-dismiss missing) | `maestro-client/src/main/java/maestro/drivers/AndroidDriver.kt` or `.github/workflows/test-e2e.yaml` (emulator pre-config) |\n| Pixel launcher visible behind a system dialog | Same — dialog stole focus across `launchApp` / `clearState` / `stopApp` | Same as above |\n| App crash dialog / blank screen | App-side issue | Out of scope — report only |\n| Stack trace from `maestro.*` in `maestro.log` | Maestro framework bug | `maestro-client/` / `maestro-orchestra/` / `maestro-android/` |\n\nFor anything classified as **driver gap** or **framework bug**, dispatch an `Explore` subagent into the relevant module(s) to find the offending code path before proposing line numbers. Never guess.\n\n## Cascade Detection (critical)\n\nA single system dialog can fail many subsequent flows because `clearState` / `launchApp` / `stopApp` don't dismiss system overlays. List FAILED entries by ascending timestamp; if downstream screenshots show the *same* overlay, group them under the **root trigger** (the earliest flow that raised the dialog). Propose **one** fix per root, not N fixes for the cascade.\n\n## Required Output Format\n\nReturn this structure verbatim — the caller parses it:\n\n````markdown\n## Summary\n<one-line: how many flows failed, how many root causes, how many cascade groups>\n\n## Root causes (caller acts on each)\n\n### 1. <flow-or-group-label> — <classification>\n- **Trigger:** <what raised this>\n- **Affected flows:** <flow1>, <flow2>, ... (N flows)\n- **Cascade?** yes/no\n- **Proposed fix:**\n  - File: `<exact path>:<line range>`\n  - What it does: <one sentence>\n  - Diff (unified or pseudo):\n    ```\n    - old line\n    + new line\n    ```\n  - Side effects: <if any, else \"none\">\n  - Verified via Explore? <yes — line numbers confirmed | no — fix is in CI yaml or test flow yaml, line numbers from direct read>\n\n### 2. ...\n\n## Out of scope\n- `<flow>` — <reason, e.g. app crash, environmental>\n\n## Notes\n<anything the caller should know but isn't a fix proposal>\n````\n\n## Constraints\n\n- Do NOT edit, write, or commit any file. Diagnosis only.\n- Do NOT propose a separate fix for each member of a cascade group.\n- Do NOT guess at Kotlin line numbers — verify via Explore subagent first.\n- Do NOT report `failing/` flows as failures.\n- Do NOT trust `❌` screenshots on their own. The grep filter (`grep -l '\"FAILED\"' commands-*.json`) is the source of truth for which flows had a terminal failure. A `❌` screenshot inside a flow whose JSON has zero `FAILED` entries is from a recovered retry and is not evidence of regression. `commands-(retry).json` is the canonical example — see \"Where to Look\" for details.\n- Always read the screenshot, even when JSON+log seem to point clearly at a cause — the screenshot is what reveals system overlays.\n\n## When to Stop and Hand Back\n\nHand back as soon as every passing-suite FAILED command has been either (a) classified with a proposed fix, or (b) classified as out-of-scope with a reason. Do not loop, retry, or attempt to apply fixes.\n","frontmatter":{"name":"diagnose-maestro-failure","description":"Use when a Maestro test-android run has failed and a structured per-flow diagnosis is needed without bloating the caller's context. Accepts either a local artifact directory or a GitHub Actions run/job URL. Reads commands JSON, screenshots, and maestro.log; classifies each newly-failing passing/ flow; identifies cascade groups; and returns a structured report with proposed fixes. Does NOT edit code or commit — the caller is responsible for consent and application.","tools":"Bash, Read, Agent"},"isInternal":false,"tokens":1705,"sizeBytes":7024}],"systemPromptSnippet":"<yakaai_skills repo=\"mobile-dev-inc/Maestro\">\n<!-- File: AGENTS.md (Tokens: ~3819 | Category: root-instruction) -->\n# AGENTS.md — Maestro\n\nShared context for any Claude Code skill or subagent operating in this repo. Skills (`.claude/skills/*`) reference this file rather than restating module roles; if a description here drifts from reality, fix it here once and every skill follows.\n\n## Commits, pull requests, and anything else you publish\n\nThis is a public repo. Anything pushed or posted is published permanently: commits, branch names, code and comments, test fixtures and flows, PR and issue text, review comments. Never:\n\n- **Publish secrets**, tokens, credentials, or sensitive `.env` contents.\n- **Add AI-attribution trailers** (`Co-authored-by:` for a tool, \"Generated with…\", session links), or `Signed-off-by`.\n\n**Staff contributors only**: if you know which customer, org, or deployment a change is for — from a tracker, Slack, or Maestro Cloud — that's staff context and must not reach the repo. Don't name them, and don't include what identifies them: their app's package or bundle ID, flows or screenshots from their app, Cloud IDs or dashboard URLs, or an internal tracker's contents. Reference trackers by ID only (`MA-1234`, `#3504`). Reproduce against the demo app, not their app.\n\nFull rules — PR title and body, sensitive info: [`docs/pull-requests.md`](docs/pull-requests.md); issues: [`docs/issues.md`](docs/issues.md). Opening a PR or issue with `--body` (`gh pr create`, `gh issue create`) skips the templates in `.github/`, so read the matching doc first and use its headings. Open a PR as ready for review when it is; open it as a draft only when there's a reason not to ask for a human yet, such as checking CI passes.\n\n## Module map\n\nTop-level Gradle modules. Code lives under each module's `src/main/`.\n\n| Module                       | Role                                                                                                                                                                                                                                                                                     |\n|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `maestro-android/`           | On-device Android driver. Kotlin sources compile to two checked-in APKs (`maestro-app.apk`, `maestro-server.apk`) consumed by `maestro-client/`. The build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update those APKs plus a `maestro-android-source.sha256` checksum.    |\n| `maestro-ios-driver/`        | Host side of iOS driver wrapper (Kotlin). The actual XCTest runner lives in `maestro-ios-xctest-runner/`.                                                                                                                                                                                |\n| `maestro-ios-xctest-runner/` | Swift XCTest runner that runs on the iOS device/simulator. The compiled artifacts (`maestro-driver-ios*.zip`) are checked in under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`.                                                                |\n| `maestro-ios/`               | iOS host-side glue (small — most iOS host code lives in `maestro-client/`).                                                                                                                                                                                                              |\n| `maestro-client/`            | Host-side Kotlin SDK that drives devices. Platform drivers live in `src/main/java/maestro/drivers/`: `AndroidDriver.kt`, `IOSDriver.kt`, `WebDriver.kt`, `CdpWebDriver.kt`. This is where most \"auto-grant\", \"auto-dismiss\", system-dialog handling and platform-specific quirks belong. |\n| `maestro-orchestra/`         | Command execution layer. `Orchestra.kt` interprets each Maestro command, applies retries, manages the command lifecycle. Sub-packages: `error/`, `filter/`, `workspace/`, `yaml/`.                                                                                                       |\n| `maestro-orchestra-models/`  | Shared command/data models (used by `maestro-orchestra/` and consumers).                                                                                                                                                                                                                 |\n| `maestro-cli/`               | CLI entry point + MCP server. Mixed Kotlin (~100 files) + Swift (~56 files for iOS-related CLI bits).                                                                                                                                                                                    |\n| `maestro-utils/`             | Shared utilities.                                                                                                                                                                                                                                                                        |\n| `maestro-web/`               | Web (browser) driver pieces.                                                                                                                                                                                                                                                             |\n| `maestro-proto/`             | Protobuf definitions shared across modules.                                                                                                                                                                                                                                              |\n| `maestro-test/`              | Cross-module tests that doesn't require devices.                                                                                                                                                                                                                                         |\n\n## E2E test fixtures (`e2e/`)\n\nShipped fixtures used by `.github/workflows/test-e2e.yaml`. Run via `e2e/run_tests <android|ios|web>` (see `e2e/run_tests` for env-var inputs `MAESTRO_APP`, `MAESTRO_FLOW_PATH`, `FIXTURES_PORT`).\n\n**Web flows need a fixtures server.** They fetch their pages from `http://127.0.0.1:7357`, which `run_tests web` starts and stops for itself. Running one web flow directly does not, and the flow then fails at `launchApp` with a connection error (`net::ERR_CONNECTION_REFUSED`). Run `e2e/ensure_fixtures` first — it is idempotent and waits until the server answers.\n\n| Path                     | Role                                                                                                                                                                    |\n|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `e2e/demo_app/`          | Flutter demo app whose only purpose is to exercise Maestro features. Contains its own `CLAUDE.md`. Built binaries are uploaded to a GCS bucket and re-downloaded by CI. |\n| `e2e/demo_app/.maestro/` | Maestro flow YAMLs that drive the demo app.                                                                                                                             |\n| `e2e/workspaces/`        | Further workspaces, each holding its flow YAMLs directly (no `.maestro/` subdirectory): `simple_web_view`, `wikipedia`, and `web` — which is browser flows with no app behind it. |\n| `e2e/workspaces.txt`     | The workspaces the suite knows about, and for each one its path, the platforms it runs on, and whether it carries `failing` flows. `run_tests` validates it before running anything. |\n| `e2e/run_tests`          | Test driver invoked by the workflow.                                                                                                                                    |\n| `e2e/ensure_fixtures`    | Starts that server unless one is already up, and waits until it answers. Call before running a web flow by hand.                                                         |\n| `e2e/serve_fixtures`     | The static server itself, in the foreground, for `workspaces/web/fixtures/`.                                                                                             |\n| `e2e/list_workspaces`    | Prints the workspace names from the manifest, so callers need not parse it.                                                                                             |\n\n### `passing/` vs `failing/` suites\n\nTag-based filters inside the YAML flows split test runs into two suites at execution time:\n\n- `passing/` — flows tagged `passing`. **Expected to pass.** Any failure here is a real regression. This is the only suite the diagnose agent reads.\n- `failing/` — flows tagged `failing`. **Expected to fail** (negative-path coverage: assertions that should not match, commands that should error). The workflow inverts the success check on this suite. Do not treat `failing/` artifacts as regressions.\n\nArtifacts land at `<artifact_root>/tests/<app>/<suite>/`:\n\n## `test-e2e.yaml` workflow contract\n\n`.github/workflows/test-e2e.yaml` is the validation harness for both PR triggers and manual `workflow_dispatch` (e.g. validating a new Android API level or iOS version). Contract:\n\n- **`workflow_dispatch` inputs** — `android_version` (choice enum), `app` (string, default `demo_app`), `flow` (string, optional single-flow). The `validate-inputs` job rejects `android_version <= android-29`, missing `app` workspace, or ambiguous `flow`. (See PR #3226.)\n- **`pull_request` triggers** are byte-identical to the prior behaviour; manual dispatches use the new narrowing knobs.\n- **`test-android` job** boots an emulator on `system-images;${android_version};google_apis;x86_64` and runs `e2e/run_tests android`.\n\nSkills that bump platform versions (Android API levels, iOS versions) drive this workflow via `gh workflow run test-e2e.yaml --ref <branch> -f android_version=<...>`.\n\n## Testing\n\nThree layers — unit, integration, E2E — plus MCP-specific evals. Each layer has a different cost/coverage trade-off; default to the lowest layer that can express the test.\n\n### Unit tests (per module, `src/test/kotlin/`)\n\nStandard per-class tests. Stack: **JUnit 5** (`junit-jupiter-api` + `-params` + `-engine`), **Google Truth** for assertions, **MockK** for mocks. Each module's `build.gradle.kts` enables the platform via `tasks.named<Test>(\"test\") { useJUnitPlatform() }`.\n\n```bash\n./gradlew :maestro-orchestra:test          # one module\n./gradlew test                              # all modules\n```\n\n### Integration tests (`maestro-test/`)\n\nCross-module tests for behaviour that does **not** require a device or simulator — JS engine integration points, command orchestration end-to-end, cancellation / coroutine semantics. Notable suites:\n\n- `IntegrationTest.kt` — full `Maestro` orchestration against an in-process `FakeDriver` (defined in `maestro-test/src/main/kotlin/maestro/test/drivers/`: `FakeDriver`, `FakeLayoutElement`, `FakeTimer`). Covers test-run cancellation (`CancellationException`, `withTimeout`, supervisor scopes) and the full command lifecycle without a real device.\n- `GraalJsEngineTest.kt` / shared `JsEngineTest.kt` — Maestro's JS extension points (`evalScript`, JS-evaluated assertions/conditions). Exercises `org.graalvm.polyglot` directly.\n- `FlowControllerTest.kt`, `DeepestMatchingElementTest.kt` — orchestration and view-hierarchy logic.\n\nStack: **JUnit 5**, **Google Truth**, **WireMock JRE8** (HTTP fakes), plus the in-house `FakeDriver` fixtures listed above. No mocks of Maestro's own classes — tests run real `Maestro` against the fakes.\n\n```bash\n./gradlew :maestro-test:test\n```\n\n### Simulator tests (opt-in, inside a module's unit tests)\n\nFor driver code whose input is produced by the platform itself — a line in the simulator's log, a file macOS writes — and which a hand-written fixture therefore cannot keep honest. The test runs the real class against a booted simulator, with no CLI, flow or XCTest runner. It is skipped unless `MAESTRO_TEST_SIMULATOR_UDID` is set, so `./gradlew test` behaves the same everywhere else; the `test-ios` job of `test-e2e.yaml` sets it. Example: `IOSDriverCrashSimulatorTest`.\n\n```bash\nMAESTRO_TEST_SIMULATOR_UDID=<booted-udid> ./gradlew :maestro-client:test --tests '*SimulatorTest'\n```\n\n### E2E tests (`e2e/`)\n\nSmoke-test every Maestro command across Android, iOS, and Web on real fixture apps. Maestro is its own dogfood harness: the CLI executes Maestro flow YAMLs against the fixtures, asserting both the framework's commands and the platform drivers behave correctly.\n\nStack: **Maestro CLI itself** (dogfood) + `e2e/run_tests` shell driver + GHA workflow (`.github/workflows/test-e2e.yaml`). Fixture and suite layout is in \"E2E test fixtures (`e2e/`)\" above.\n\n```bash\ncd e2e && ./run_tests <android|ios|web>     # local\ngh workflow run test-e2e.yaml --ref <branch> -f android_version=android-<N>   # CI\n```\n\n**Two roles for the same E2E setup.** The same suite serves both purposes — treat them identically:\n\n1. **Regression smoke** — every PR that touches Maestro source runs the suite on the current platform versions, catching behaviour breakage on existing platforms.\n2. **New-OS validation** — when launching a new Android API level or iOS version, the same flows are dispatched against the new system image to confirm Maestro still works. This is what `bump-android-version` (and the planned `bump-ios-version`) drives.\n\nA flow breaking for either reason is a real regression — fix in `maestro-android/`, `maestro-client/`, or `e2e/demo_app/`, not in `test-e2e.yaml` (see \"What NOT to do\").\n\n**Multiple apps for framework-specific coverage.** `demo_app/` (Flutter) is the default fixture and exercises every Maestro command. When a target is **framework-specific** (SwiftUI, React Native, Jetpack Compose specifics, WebView quirks, etc.), add a separate workspace: flow YAMLs directly under `e2e/workspaces/<name>/`, a binary under `e2e/apps/` if there is an app to install, **and a row in `e2e/workspaces.txt`** — `validate_workspaces` fails a workspace that has flows but no row, since without one `run_tests` would never find it. Existing examples: `simple_web_view` (WebView coverage), `wikipedia` (real-world third-party app), `web` (browser flows, no app). The workflow's `app` input narrows a manual dispatch to one workspace: `... -f app=simple_web_view`.\n\n### MCP server evals (`maestro-cli/src/test/mcp/`)\n\nLLM-behaviour evaluations and tool-functionality tests for the MCP server inside `maestro-cli`. Stack: **`mcp-server-tester`** (npm package, run via `npx`) consuming YAML definitions (`full-evals.yaml`, `inspect-screen-evals.yaml`, `tool-tests-{with,without}-device.yaml`) plus per-platform setup scripts under `setup/`. See `maestro-cli/src/test/mcp/README.md` for the model list, scorers, and how to run.\n\n```bash\n./run_mcp_tool_tests.sh ios     # tool-functionality (fast)\n./run_mcp_evals.sh ios          # LLM behaviour (slower)\n```\n\n## Conventions\n\n- Kotlin 1.9 / JVM 17. Gradle. No DI framework — services are constructed manually.\n- Protobuf for the on-device wire format (`maestro-proto/`).\n- Coroutines with explicit dispatchers; `runBlocking` only at entry points.\n- Exposed exceptions classify failures (retryable vs terminal) — see `maestro-orchestra/src/main/java/maestro/orchestra/error/`.\n- **A run's files fall into three buckets.** *Inputs* (flow YAML, scripts, `assertScreenshot` references, `addMedia` files) are read from the workspace and never written to. *Outputs* — anything a user may want after the run — are allocated through `ArtifactsGenerator`, which places them in the artifacts bundle and lists them in `manifest.json`. *Scratch* files go through `TempFileHandler` (below). Callers decide what happens to those folders after the run and may keep only what the manifest lists, so an output written anywhere else — beside an input especially — can be silently lost. `RunOutputStaysInBundleTest` (`maestro-test/`) enforces this and makes every new command declare whether it creates files.\n- **Temp files and directories go through `maestro.utils.TempFileHandler`**, not `java.nio.file.Files.createTempFile/createTempDirectory` directly. `TempFileHandler` is a `Closeable` that recursively cleans up everything it allocated on `close()`. Direct `Files.createTempFile(...)` skips that lifecycle and leaks `/tmp` content (especially painful on long-lived JVMs like the cloud worker). Construct a `TempFileHandler` near the lifecycle owner, call its `createTempFile` / `createTempDirectory`, and `close()` it in a `finally`.\n\n## Where Claude Code resources live\n\n- `.claude/skills/*` — skills (workflows). Each skill's `SKILL.md` references this file for module roles.\n- `.claude/agents/*.md` — subagents (e.g. `diagnose-maestro-failure.md`). Their input/output contracts are documented in each file.\n\n## What NOT to do\n\n- Don't fix driver-behaviour gaps by patching `.github/workflows/test-e2e.yaml` (e.g. extra `adb shell settings put …`, command-line tweaks, AVD pre-config). Workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/`, `maestro-client/`, or `e2e/demo_app/` instead so the fix ships with the driver APKs. Workflow edits are valid for shape-changes (matrix, retention, dispatch inputs) and the narrow third-party-FRE exception documented in skill files.\n- Don't edit checked-in driver artifacts (`maestro-app.apk`, `maestro-server.apk`, `maestro-android-source.sha256`, `maestro-driver-ios*.zip`) by hand — they are gradle finalizer outputs.\n- Don't commit local changes to the iOS driver zips. A local build regenerates two zips — `maestro-driver-ios.zip` and `maestro-driver-iosUITests-Runner.zip` — under `maestro-ios-driver/src/main/resources/driver-iPhoneSimulator/Debug-iphonesimulator/`. This is normal: they're used by local builds. But although they're checked into the repo, they're managed exclusively by CI, so leave any local modifications to them out of your commits.\n- Don't modify existing flows in `failing/` to make them pass — that's the negative-path suite by design.\n\n<!-- File: .claude/skills/bump-android-version/SKILL.md (Tokens: ~2676 | Category: anthropic-skill) -->\n---\nname: bump-android-version\ndescription: Use when bumping Maestro's Android compileSdk/targetSdk to a new API level and validating end-to-end against the test-e2e GHA workflow until the test-android job is green.\n---\n\n# Bump Android Version\n\n## Overview\n\nDrive an Android-version bump in Maestro commit-by-commit on a single feature branch: edit gradle, rebuild the on-device driver APKs, push a draft PR, dispatch `test-e2e.yaml` against the new system image, watch the `test-android` job, and diagnose+fix any newly-failing `passing/` flows. Loop until green.\n\n## When to Use\n\n- User asks to bump Maestro's Android `compileSdk` / `targetSdk` (e.g. \"bump to API 36\").\n- User wants to validate Maestro against a new Android system image.\n\nOne commit per logical step. Don't bundle the gradle bump and the APK rebuild — they need to be separately revertible. After a failing run, fixes are committed and the workflow re-dispatched. Loop until green; do not stop on the first re-dispatch.\n\n**The loop only acts on `passing/` failures.** `tests/demo_app/passing/` is the regression-detection suite — its flows are expected to pass, so a failure there is a real signal. `tests/demo_app/failing/` is the negative-path suite (its flows are *expected* to fail) and is ignored end-to-end. The diagnose agent rejects `failing/` artifacts and rejects retry-recovered flows in `passing/` (see [`.claude/agents/diagnose-maestro-failure.md`](../../agents/diagnose-maestro-failure.md)). If the run is otherwise green except for `failing/`, that's a green run.\n\n## Pre-flight\n\n- Working tree clean (`git status`).\n- `main` up to date: `git fetch origin && git checkout main && git pull --ff-only origin main`.\n- The workflow's `workflow_dispatch` inputs `android_version` (and optional `app` / `flow`) must exist on the branch. \n- `android_version` is a `choice` input — its options live in `.github/workflows/test-e2e.yaml`. If `<new>` isn't already listed (e.g. bumping to API 37), add it to the `options:` list in a separate commit before dispatching, otherwise the workflow rejects the input.\n- The `validate-inputs` job rejects `android_version <= android-29`. Bumps below API 29 aren't supported.\n- Branch name: `bump-android-api-<old>-<new>` (e.g. `bump-android-api-34-36`).\n\n```bash\ngit checkout -b bump-android-api-<old>-<new> origin/main\n```\n\n## Commit 1: bump compile/target SDK\n\nRead current values from `maestro-android/build.gradle.kts` (`compileSdk` and `targetSdk`). Tell the user the current values verbatim, then ask for the new target:\n\n> Current `compileSdk` / `targetSdk` = `<N>`. What's the new API level?\n\nEdit both lines. Also `rg -n \"compileSdk\\s*=|targetSdk\\s*=\" --type kotlin --type-add 'kotlin:*.kts'` to catch consistent occurrences in other modules — only update those that intentionally track the same SDK. Commit:\n\n```bash\ngit add maestro-android/build.gradle.kts <other touched files>\ngit commit -m \"chore(android): bump compile/target SDK from <old> to <new>\"\n```\n\n## Commit 2: rebuild driver APKs\n\n```bash\n./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest\n```\n\nThe build's `copyMaestroAndroid` / `copyMaestroServer` finalizers update three checked-in files. Stage and commit *only* those:\n\n```bash\ngit add maestro-client/src/main/resources/maestro-app.apk \\\n        maestro-client/src/main/resources/maestro-server.apk \\\n        maestro-client/src/main/resources/maestro-android-source.sha256\ngit commit -m \"chore(android): rebuild driver APKs against API <new>\"\n```\n\nIf `assemble` fails, **stop and surface the failure to the user** with the gradle error and a suggested fix. Do not proceed without rebuilt driver APKs. Once we have assemble fixed and new drivers are commited, we can proceed to the next step.\n\n## Step 3: push draft PR + dispatch\n\n```bash\ngit push -u origin bump-android-api-<old>-<new>\ngh pr create --draft \\\n  --title \"chore(android): bump compile/target SDK to <new>\" \\\n  --body \"Validates Maestro against API <new>. Driver APKs rebuilt. Test-e2e dispatched with system-images;android-<new>;google_apis;x86_64.\"\n\ngh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n  -f android_version=\"android-<new>\"\n\n# capture the run id\nsleep 3\ngh run list --workflow test-e2e.yaml --branch bump-android-api-<old>-<new> --limit 1 --json databaseId,status\n```\n\nAlways pass `-f android_version=android-<new>` — the default is `android-32` and would silently re-test the old API. The workflow constructs the full `system-images;...` string internally. `app` and `flow` are optional narrowing knobs (defaults: `demo_app`, all flows); leave them off for the full bump-validation run.\n\n## Step 4: watch the test-android job\n\n```bash\ngh run watch <run_id>\n```\n\nBlock in the foreground; the run is the bottleneck. **Green here means every `passing/` flow passed (terminally — retry-recovered counts as passed). `failing/` outcomes do not affect the verdict.** On green: update PR from draft → ready, done.\n\nOn red, download artifacts:\n\n```bash\ngh run download <run_id> --name maestro-root-dir-android -D /tmp/maestro-android-<run_id>\n```\n\n## Step 5: diagnose (delegated to subagent)\n\nDispatch the **`diagnose-maestro-failure`** subagent with the artifact directory `/tmp/maestro-android-<run_id>` (or the GHA run URL — the subagent handles both). It diagnoses each passing-suite failure in isolation and returns a structured report grouped by root cause (cascades collapsed). It does **not** apply fixes — that's your job here.\n\nWhen the subagent returns:\n\n**Fixes go in Maestro source — not in `.github/workflows/test-e2e.yaml`.** A failing flow on a new API level reflects a behaviour Maestro users will also hit on their own machines/CI, not just ours. Patching the GHA workflow (e.g. extra `adb shell settings put …`, command-line tweaks, AVD setup steps) hides the regression from real users and ships a Maestro that's only green inside our CI.\n\nAcceptable fix targets and their roles are documented in [`AGENTS.md`](../../../AGENTS.md) (modules `maestro-android/`, `maestro-client/`, `maestro-orchestra/`, `e2e/demo_app/`). Driver-behaviour fixes belong in `maestro-android/` or `maestro-client/`; fixture-only issues belong in `e2e/demo_app/`. Editing the driver source means rebuilding the driver APKs (`./gradlew :maestro-android:assemble :maestro-android:assembleAndroidTest`) before re-dispatching.\n\nValid edits to `test-e2e.yaml` during this loop are workflow-shape changes (matrix, retention, dispatch inputs) — and the narrow exception below. Anything else (e.g. extra `adb shell settings put …` to mask a Maestro driver gap) is off-limits. If the subagent proposes a workflow patch, push back: ask it (or yourself) where the same fix would live in `maestro-android/` or `maestro-client/` so users on the new API level inherit it automatically.\n\n**Exception — third-party app first-run UI not triggered by a Maestro API.** A pre-installed app's own onboarding (Chrome Welcome / \"Make Chrome your own\", browser default-app picker, Play Protect prompts that fire at app launch) is environment-harness state, not driver behaviour. Maestro doesn't expose a public API that triggers them — they're side effects of the OS image we picked. These may be pre-disabled in the AVD setup step (typical knobs: `setprop debug.chrome.command_line`, `settings put global …` flags). Test: *if Maestro called nothing related to this dialog, would it still appear?* If yes → CI workaround is fair game. If no (e.g. `setLocation` triggers GMS Location Accuracy via `FusedLocationProviderClient` inside the driver) → fix the driver instead.\n\n1. For each root cause in its report, present the proposed diff to the user (file path + one-line summary + diff + side effects). Reject any proposal that targets `test-e2e.yaml` for driver-behaviour reasons before showing it — re-scope to Maestro source first.\n2. **Ask consent explicitly per fix:**\n   > Proposed fix for `<root cause>`: `<one-liner>`. Apply?\n3. On approval: edit, commit with a focused message — don't bundle unrelated fixes.\n   ```bash\n   git commit -m \"fix(android-<new>): <summary>\"\n   ```\n4. On rejection: skip that fix; record what was rejected so you don't re-propose it next iteration.\n\nAfter all approved fixes for this iteration are committed:\n\n- **If any fix was applied:** push and re-dispatch, then loop back to **Step 4** with the new `run_id`.\n\n  ```bash\n  git push\n  gh workflow run test-e2e.yaml --ref bump-android-api-<old>-<new> \\\n    -f android_version=\"android-<new>\"\n  ```\n\n- **If no fixes were applied** (everything was out-of-scope, or the user rejected every proposal): pause and ask the user how to proceed. Do not keep dispatching.\n\n## Termination\n\nStop when `test-android` is green on the latest run. Then:\n\n```bash\ngh pr ready <pr_number>\n```\n\nIf the loop has gone three iterations without progress (same flow keeps failing for different reasons after fixes), pause and ask the user how to proceed — don't spin indefinitely.\n\n## Anti-patterns\n\n- **Bundling gradle + APK rebuild into one commit** — kills bisectability. Always two commits.\n- **Dispatching with default inputs** — `android_version` defaults to `android-32` and hits the old API. Always pass `-f android_version=android-<new>`.\n- **Forgetting to extend the `android_version` choice enum** — the workflow rejects API levels not listed in `options:`. Add `<new>` to the enum in a separate commit before the first dispatch.\n- **Auto-applying Maestro source fixes without consent** — every Kotlin patch needs explicit user approval first.\n- **Patching `.github/workflows/test-e2e.yaml` to mask a driver behaviour gap** — workflow band-aids hide the regression from users running Maestro outside our CI. Fix `maestro-android/` or `e2e/demo_app/` instead so the fix ships with the driver APKs.\n- **Treating `failing/` artifacts as regressions** — that suite is expected to fail.\n- **Treating `❌` screenshots in `passing/` as regressions on their own** — when `retryCommand` recovers a failed attempt, Maestro writes only the final `COMPLETED` entry to `commands-(<flow>).json` but leaves the `❌` screenshot from the failed attempt on disk (e.g. `screenshot-❌-<ts>-(retry).png` for the `retry` flow itself). The diagnose agent uses `commands-*.json` containing a terminal `FAILED` entry as the source of truth — orphan `❌` screenshots without a matching FAILED entry are noise.\n- **Committing on `main`** — always work on the bump branch. Verify with `git status` before every commit.\n- **Skipping the screenshot read** — the screenshot tells you what `maestro.log` and the JSON cannot. It's the highest-signal artifact. Always read it.\n\n<!-- File: e2e/demo_app/.maestro/commands/addMedia.yaml (Tokens: ~233 | Category: anthropic-skill) -->\nappId: com.google.android.documentsui\ntags:\n  - passing\n  - android # TODO: How do we prove this works on iOS?\n---\n\n- addMedia:\n  - ../assets/image.png\n\n- launchApp: com.google.android.documentsui # Files app\n\n- tapOn: Show roots\n- tapOn: Images\n\n# Locate the image via Search instead of opening the \"Pictures\" album. On a\n# loaded emulator a tap on the album can be misread as a long-press, which\n# selects the album instead of opening it and leaves the test stranded on the\n# album list. The search icon is a button, so a long-press on it is harmless.\n\n- tapOn:\n    id: \"com.google.android.documentsui:id/option_menu_search\"\n- inputText: \"image\" # Search a partial term (\"image\") so the query text differs from the file name\n- assertVisible: image.png\n\n- longPressOn: image.png\n- tapOn:\n    id: \"com.google.android.documentsui:id/action_menu_delete\"\n- tapOn: OK\n- assertNotVisible: image.png\n\n<!-- File: e2e/demo_app/.maestro/commands/assertNotVisible.yaml (Tokens: ~65 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertNotVisible: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'kwyjibo'\n\n- assertNotVisible:\n    text: 'Form Test'\n    enabled: false\n\n<!-- File: e2e/demo_app/.maestro/commands/assertScreenshotCropped.yaml (Tokens: ~69 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCropped.png\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n\n<!-- File: e2e/demo_app/.maestro/commands/assertScreenshotCroppedHEIC.yaml (Tokens: ~63 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - failing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedHEIC.heic\n    cropOn:\n      id: \"testContainer\"\n\n<!-- File: e2e/demo_app/.maestro/commands/assertScreenshotCroppedJPG.yaml (Tokens: ~71 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - passing\n---\n- launchApp\n- tapOn: \"Cropped Screenshot Test\"\n- waitForAnimationToEnd\n\n- assertScreenshot:\n    path: ../screenshots/assertScreenshotCroppedJPG.jpg\n    thresholdPercentage: 99.9\n    cropOn:\n      id: \"testContainer\"\n\n<!-- File: e2e/demo_app/.maestro/commands/assertTrue.yaml (Tokens: ~78 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - passing\nenv:\n  THING: \"five\"\n---\n\n- launchApp # For idempotence of sections\n\n- assertTrue: ${\"test\" == \"test\"}\n\n- assertTrue:\n    condition: ${12 < 20}\n\n- assertTrue:\n    condition: ${THING == \"five\"} # Using the env at the top of the file\n\n<!-- File: e2e/demo_app/.maestro/commands/assertVisible.yaml (Tokens: ~55 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - passing\n---\n\n- launchApp # For idempotence of sections\n\n- assertVisible: 'Form Test'\n\n- assertVisible:\n    text: 'Form Test'\n\n- assertVisible:\n    id: 'fabAddIcon'\n\n<!-- File: e2e/demo_app/.maestro/commands/back.yaml (Tokens: ~54 | Category: anthropic-skill) -->\nappId: com.example.example\ntags:\n  - passing\n  - android\n---\n- launchApp # For idempotence of sections\n\n- tapOn: 'Form Test'\n- assertVisible: 'Login'\n- back\n- assertVisible: 'Form Test'\n\n<!-- File: e2e/demo_app/.maestro/commands/clearState.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/clearState.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/clearState.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/copyTextFrom.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/copyTextFrom.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/copyTextFrom.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/doubleTapOn.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/doubleTapOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/doubleTapOn.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/eraseText.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/eraseText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/eraseText.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/evalScript.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/evalScript.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/evalScript.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/extendedWaitUntil.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/hideKeyboard.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/hideKeyboard.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/hideKeyboard.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomCityName.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCityName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCityName.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomColorName.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomColorName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomColorName.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomCountryName.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomEmail.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomEmail.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomEmail.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomNumber.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomNumber.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomNumber.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomPersonName.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputRandomText.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputRandomText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputRandomText.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/inputText.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/inputText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/inputText.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/killApp.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/killApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/killApp.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/launchApp.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/launchApp.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/launchApp.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/longPressOn.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/longPressOn.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/longPressOn.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/openLink.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/openLink.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/openLink.yaml)\n\n<!-- File: e2e/demo_app/.maestro/commands/pasteText.yaml (Tokens: ~300 | Category: anthropic-skill) -->\n# Commands Skill\n\nPath: `e2e/demo_app/.maestro/commands/pasteText.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/mobile-dev-inc/Maestro/HEAD/e2e/demo_app/.maestro/commands/pasteText.yaml)\n\n<!-- ... +19 more modular skills available via /api/skills/mobile-dev-inc/Maestro?path={path} -->\n</yakaai_skills>"}