Shared 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.
This is a fork.
FORK.mdlists every capabilityplexinc/Maestroadds on top of upstream — fork-only flow commands (readFile,@aliaspath references), platforms (tvOS, Vega/Fire TV, Roku), web-driver enhancements, bundled Studio, and distribution. Read it before writing flows or tests so you don't treat fork features as missing or reinvent them. Any new fork-only feature must be documented there in the same commit.
Top-level Gradle modules. Code lives under each module's src/main/.
| Module | Role |
|---|---|
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. |
maestro-ios-driver/ |
Host side of iOS driver wrapper (Kotlin). The actual XCTest runner lives in maestro-ios-xctest-runner/. |
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/. |
maestro-ios/ |
iOS host-side glue (small — most iOS host code lives in maestro-client/). |
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, VegaDriver.kt, RokuDriver.kt. This is where most "auto-grant", "auto-dismiss", system-dialog handling and platform-specific quirks belong. |
maestro-orchestra/ |
Command execution layer. Orchestra.kt interprets each Maestro command, applies retries, manages the command lifecycle. Sub-packages: error/, filter/, workspace/, yaml/. |
maestro-orchestra-models/ |
Shared command/data models (used by maestro-orchestra/ and consumers). |
maestro-cli/ |
CLI entry point + MCP server. Mixed Kotlin (~100 files) + Swift (~56 files for iOS-related CLI bits). |
maestro-utils/ |
Shared utilities. |
maestro-web/ |
Web (browser) driver pieces. |
maestro-proto/ |
Protobuf definitions shared across modules. |
maestro-test/ |
Cross-module tests that doesn't require devices. |
Shipped 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).
| Path | Role |
|---|---|
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. |
e2e/demo_app/.maestro/ |
Maestro flow YAMLs that drive the demo app. |
e2e/workspaces/ |
Additional app workspaces (e.g. simple_web_view, wikipedia). |
e2e/run_tests |
Test driver invoked by the workflow. |
Tag-based filters inside the YAML flows split test runs into two suites at execution time:
passing/— flows taggedpassing. Expected to pass. Any failure here is a real regression. This is the only suite the diagnose agent reads.failing/— flows taggedfailing. 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 treatfailing/artifacts as regressions.
Artifacts land at <artifact_root>/tests/<app>/<suite>/:
.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:
workflow_dispatchinputs —android_version(choice enum),app(string, defaultdemo_app),flow(string, optional single-flow). Thevalidate-inputsjob rejectsandroid_version <= android-29, missingappworkspace, or ambiguousflow. (See PR #3226.)pull_requesttriggers are byte-identical to the prior behaviour; manual dispatches use the new narrowing knobs.test-androidjob boots an emulator onsystem-images;${android_version};google_apis;x86_64and runse2e/run_tests android.
Skills that bump platform versions (Android API levels, iOS versions) drive this workflow via gh workflow run test-e2e.yaml --ref <branch> -f android_version=<...>.
Three 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.
Standard 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() }.
./gradlew :maestro-orchestra:test # one module
./gradlew test # all modulesCross-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:
IntegrationTest.kt— fullMaestroorchestration against an in-processFakeDriver(defined inmaestro-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.GraalJsEngineTest.kt/ sharedJsEngineTest.kt— Maestro's JS extension points (evalScript, JS-evaluated assertions/conditions). Exercisesorg.graalvm.polyglotdirectly.FlowControllerTest.kt,DeepestMatchingElementTest.kt— orchestration and view-hierarchy logic.
Stack: 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.
./gradlew :maestro-test:testSmoke-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.
Stack: 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.
cd e2e && ./run_tests <android|ios|web> # local
gh workflow run test-e2e.yaml --ref <branch> -f android_version=android-<N> # CITwo roles for the same E2E setup. The same suite serves both purposes — treat them identically:
- Regression smoke — every PR that touches Maestro source runs the suite on the current platform versions, catching behaviour breakage on existing platforms.
- 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 plannedbump-ios-version) drives.
A 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").
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 under e2e/workspaces/<app>/ with its own .maestro/ flow YAMLs and a binary under e2e/apps/. Existing examples: simple_web_view (WebView coverage), wikipedia (real-world third-party app). The workflow's app input narrows a manual dispatch to one workspace: ... -f app=simple_web_view.
LLM-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.
./run_mcp_tool_tests.sh ios # tool-functionality (fast)
./run_mcp_evals.sh ios # LLM behaviour (slower)- Kotlin 1.9 / JVM 17. Gradle. No DI framework — services are constructed manually.
- Protobuf for the on-device wire format (
maestro-proto/). - Coroutines with explicit dispatchers;
runBlockingonly at entry points. - Exposed exceptions classify failures (retryable vs terminal) — see
maestro-orchestra/src/main/java/maestro/orchestra/error/. - Temp files and directories go through
maestro.utils.TempFileHandler, notjava.nio.file.Files.createTempFile/createTempDirectorydirectly.TempFileHandleris aCloseablethat recursively cleans up everything it allocated onclose(). DirectFiles.createTempFile(...)skips that lifecycle and leaks/tmpcontent (especially painful on long-lived JVMs like the cloud worker). Construct aTempFileHandlernear the lifecycle owner, call itscreateTempFile/createTempDirectory, andclose()it in afinally.
.claude/skills/*— skills (workflows). Each skill'sSKILL.mdreferences this file for module roles..claude/agents/*.md— subagents (e.g.diagnose-maestro-failure.md). Their input/output contracts are documented in each file.
- Don't fix driver-behaviour gaps by patching
.github/workflows/test-e2e.yaml(e.g. extraadb shell settings put …, command-line tweaks, AVD pre-config). Workflow band-aids hide the regression from users running Maestro outside our CI. Fixmaestro-android/,maestro-client/, ore2e/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. - 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. - Don't commit local changes to the iOS driver zips. A local build regenerates two zips —
maestro-driver-ios.zipandmaestro-driver-iosUITests-Runner.zip— undermaestro-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. - Don't modify existing flows in
failing/to make them pass — that's the negative-path suite by design.