Skip to content

Commit aa221aa

Browse files
Lykhoydaclaude
andauthored
feat(rn-device): in-tree Android UIAutomator runner (MVP) (#165)
* docs(rn-device): reflect iOS-MVP runtime split — rn-fast-runner (iOS) + agent-device (Android) Six docs updated to match the iOS-MVP architecture from PR #164 / D1219: 1. **skills/rn-setup/SKILL.md** (source of truth for /doctor + /setup Phase 1) — split check #3 into "3. rn-fast-runner (iOS — in-tree XCTest rig)" and "3b. agent-device CLI (Android — optional on iOS-only setups)". New check verifies the xcodeproj + build artifacts, surfaces the one-time `xcodebuild build-for-testing` command on NEEDS_BUILD. Updated 14-row output table, common rationalizations (banner WARNING no longer critical on iOS), verification checklist. RN_DEVICE_KILL_LEGACY=1 documented as the recommended iOS-only setting for stale daemons. 2. **commands/doctor.md** — frontmatter + body now mention both rn-fast-runner (iOS) and agent-device (Android). Doctor delegates to the rn-setup skill so most of the surface is inherited; doctor.md just keeps the frontmatter / overview accurate. 3. **commands/setup.md** — Phase 1 abort-thresholds list split: "CRITICAL" no longer hardcodes `agent-device` — instead lists the platform-specific device-control row (rn-fast-runner on macOS+iOS targets OR agent-device on Android targets). N/A rows for the off-platform runtime are documented as OPTIONAL. 4. **CLAUDE.md** (plugin) — Prerequisites, Troubleshooting, Architecture, Device tools, Key Technical Decisions sections all updated. Architecture table now has separate rows for iOS (in-tree rn-fast-runner /command HTTP) and Android (agent-device CLI). Troubleshooting added rows for "no .xctestrun at expected path" + "legacy AgentDeviceRunner re-appears". iOS-only quirks list under Device tools documents the type-timeout shim and the TS orchestrator routing for find/scrollintoview. 5. **README.md** — Setup prerequisites table now lists rn-fast-runner (iOS) + agent-device (Android) as separate platform-conditional rows. /doctor description bumped from 12-row to 14-row. Architecture ASCII diagram split iOS/Android device-interaction paths. Troubleshooting added three new rows: stale AgentDeviceRunner respawn, rn-fast-runner build artifacts missing, iOS device_fill timeout shim. 6. **CLAUDE-MD-TEMPLATE.md** (user-injected) — new "iOS Device Runtime — In-tree rn-fast-runner" subsection between Multi-Device Setups and Required Dev Setup. Documents the runtime split so user agents working in projects with the template injected know to ignore agent-device warnings on iOS, what RN_DEVICE_KILL_LEGACY does, and how to interpret device_fill's runnerTimeoutShim meta marker. No code changes — pure documentation alignment with the runtime already shipped in 8345f4b / 7f76411. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): scaffold in-tree Android UIAutomator runner Greenfield Gradle Android instrumentation project at scripts/rn-android-runner/ mirroring the iOS rn-fast-runner architecture (PR #164). Long-lived @LargeTest mainLoop() runs under `am instrument`, embeds NanoHTTPD on port 22089, dispatches the same POST /command JSON contract as the iOS runner. Kotlin 2.0.21 / AGP 8.7.3 / UIAutomator 2.3.0 / NanoHTTPD 2.3.1 / minSdk 23 / Java 17. Configurator.setWaitForIdleTimeout(0) intentionally disables UIAutomator's global idle wait — RN main thread never reports quiescence when Reanimated/RAF worklets are active. Mirrors iOS's withTemporaryScrollIdleTimeoutIfSupported shim. CommandDispatcher covers all MVP verbs: snapshot, tap/press, type/fill, drag/swipe/scroll, screenshot, back, dismissKeyboard, longPress, pinch, findText. `find` is intentionally NOT dispatched here — TS-side device_find is a snapshot-based orchestrator on Android too (mirrors iOS, D1217 symmetry). This is greenfield code (no MIT upstream vendoring) — see plan §File Structure for the rationale vs iOS's rn-fast-runner import. Task 1 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(rn-device): Task 1 post-review hardening Apply 6 code-quality review findings from the rn-android-runner Task 1 implementation: 1. (security MUST_FIX) Remove unsafe shell-fallback from type(). When no text input has keyboard focus, throw NoFocusedInputException → structured {code: NO_FOCUSED_INPUT} response instead of executing `input text $escaped` via UiAutomation.executeShellCommand. The 2-line escaper only neutralized spaces + single-quotes; injection vectors `;` `&` `|` backtick `$(…)` `>` `<` `\n` were all unguarded. 3. Wrap snapshot XML parser in try/catch; throw SnapshotParseException → {code: SNAPSHOT_PARSE_FAILED} instead of opaque 500. 6. Catch InterruptedException in mainLoop for clean shutdown; add @after stopServer() so NanoHTTPD's socket releases without TIME_WAIT lingering across test reruns. 7. Wrap uiObjectToJson in findText with StaleObjectException catch; return {found: false, stale: true} when the UiObject2 was recycled between query and serialization (common on RN's re-render-heavy screens). 8. Correct findByTextOrId regex anchoring: `.*(:id/)?$safe$` matched any id ending in the query (e.g. `submit` matched `cancel_submit`). Now `^[^:]+:id/$safe$` requires the `:id/` separator and anchors the query as the complete id-name suffix. 9. Add MIT SPDX license header to every Kotlin file under app/src/androidTest/, per the plan's File Structure note. Deferred to a later task (out of scope for this commit): - [2] foreground() launcher-after-back-press edge case - [4] pinch minimum ratio threshold - [5] error message stack/cause chain enrichment - [10]-[14] NIT-level refactors (magic constants, version catalog, etc.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(rn-device): pin Android foreground snapshot regression SnapshotForegroundRegressionTest is the Android equivalent of iOS's B155 regression test (rn-fast-runner/.../SnapshotForegroundRegressionTest.swift). It activates com.rndevagent.testapp via the launcher intent, waits up to 10s for the package to foreground, then calls CommandDispatcher.snapshot() and asserts: - The flat JSON snapshot contains "tab-home" (a stable testID in test-app's bottom tab bar). - The snapshot does NOT contain "rn-dev-agent Android runner" (the runner's own app label), proving foreground() correctly activated the target package and the dispatcher read its hierarchy, not the runner's own UI. This pins the same invariant on Android that B155 fixed on iOS: the runner must always read the target app's accessibility tree per request, regardless of which app foregrounded before the call. Task 2 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): add TypeScript client for Android runner Mirrors the iOS rn-fast-runner-client.ts pattern (PR #164): - runAndroid() — single POST /command entry point speaking to the in-tree UIAutomator runner from Task 1 - startAndroidRunner() — spawns `adb shell am instrument` with the RN_ANDROID_RUNNER_PORT arg, sets up `adb forward tcp:22089`, parses logcat for the RN_ANDROID_RUNNER_LISTENER_READY + port handshake - stopAndroidRunner() — SIGTERM the instrument + logcat processes, remove the `adb forward` rule - postCommand() — POSTs to http://127.0.0.1:<port>/command - _setFetchForTest / _setAndroidRunnerStateForTest — test seams mirroring iOS test-seam pattern State file at /tmp/rn-android-runner-state.json kept distinct from iOS so both can run simultaneously. DEFAULT_PORT = 22089 (iOS uses 22088). Includes the iOS runner-timeout shim ported for Android: when UIAutomator returns "Could not detect idle state" / "Idle timeout exceeded" on .type, treat it as success with meta.runnerTimeoutShim: true. RN's main thread never reports quiescence when Reanimated/RAF worklets are active, so this particular timeout shape is benign — the text was appended before the timeout fired. Snapshot post-processing reuses the shared fast-runner-ref-map: mapRunnerNodesToFlat builds @en refs from the runner's index; updateRefMapFromFlat populates the same ref-map iOS uses. Test coverage: 4 unit tests verifying snapshot dispatch + ref-map population, tap coordinates, STALE_REF early-return, and runner error code surfacing. All passing. Also adds 'SCREENSHOT_FAILED' to the ToolErrorCode union in src/types.ts — the runner-client uses that literal as a failure code, mirroring how 'SNAPSHOT_FAILED' is defined. Task 3 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): gate Android runner short-circuit + add buildRunAndroidArgs Combines Tasks 4 + 5 of the Android MVP plan because Task 4's short-circuit calls buildRunAndroidArgs (defined in Task 5) — splitting would break the build mid-task-sequence. Task 4 — Android short-circuit in runAgentDevice(): Adds an Android short-circuit immediately after the existing iOS block in agent-device-wrapper.ts. Gated behind `RN_ANDROID_RUNNER=1` so the legacy agent-device daemon/CLI path stays the default until Task 11's default-flip. RN_ANDROID_RUNNER_COMMANDS covers all 13 MVP verbs: snapshot, tap, press, fill, type, back, screenshot, keyboard, swipe, scroll, drag, longpress, pinch. `find` is deliberately NOT included so device_find on Android routes through the same TS-side findInLatestSnapshot orchestrator iOS uses (D1217 symmetry — UIAutomator's regex-match By.text() would diverge from iOS's exact-or-substring semantics). Task 5 — buildRunAndroidArgs() + helpers: Translates runAgentDevice CLI argv shape into RunAndroidArgs that the TS client (Task 3) understands. Mirrors buildRunIOSArgs structurally. New helpers: optionValue() (parses --foo bar), androidPositionals() (filters flag args out of the positional list). Stale-ref handling injects the sentinel into press/tap/type/longpress paths so the runner returns STALE_REF without an HTTP round-trip. Test coverage: 4 new unit tests verifying the short-circuit is env-gated and platform-scoped, the command set covers MVP verbs, the case-block fragments are present in buildRunAndroidArgs, and stale-ref/screenshot out-path sentinels are wired correctly. Tasks 4 + 5 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): Android flatten helper + stale-ref Android coverage Tasks 6 + 7 of the Android MVP plan — both small and touch the same fast-runner-ref-map module, so combining the commit keeps history tight. Task 6 — flattenAndroidAccessibilityTree: Adds an exported pass-through helper for Android because the Kotlin runner's snapshot endpoint already returns flat `nodes` with `@eN` refs (unlike iOS's XCUITree which needs a depth-first walk). The helper mirrors the iOS flattenXCUITree shape but is a no-op over the node list itself; the only work is building a sliced ref-map keyed without the leading '@' for refCenter() lookups. Task 7 — stale-ref detection for Android: Adds one regression test to test/unit/stale-ref-detection.test.js proving the existing isRefStale + findNewRefByMetadata helpers handle Android flat-node shapes without any platform branching. Same ref identity rules (testID, label, rect) carry over. Verifies that: 1. Identical refs are not stale. 2. Refs with a changed identifier are stale. 3. Metadata-based lookup finds the new ref after a re-render. Together these two tasks confirm the iOS-built fast-runner-ref-map is genuinely generic (D1217 — testID is the durable anchor). No new isAndroidRefStale or per-platform code needed. Tasks 6 + 7 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): runner-backed find + scrollintoview orchestration on Android Task 8 of the Android MVP plan. iOS already routes device_find (non-exact) and device_scrollintoview through TS-side orchestrators that compose runAgentDevice('snapshot') + findInLatestSnapshot + a swipe loop. This commit extends the same path to Android behind RN_ANDROID_RUNNER=1. Changes: - Rename scrollIntoViewIOS -> scrollIntoViewWithRunner (platform-neutral name now that both platforms use it). - Entry gate becomes: session?.platform === 'ios' || (session?.platform === 'android' && process.env.RN_ANDROID_RUNNER === '1') so Android opts in once the env flag is set. - Inside scrollIntoViewWithRunner, replace direct fastSwipe() calls with runAgentDevice(['swipe', x1, y1, x2, y2, duration]). On iOS this still hits the iOS short-circuit -> runIOS('drag'); on Android with the env flag set it hits the new Android short-circuit -> runAndroid('drag'). - Mirror device_find's existing iOS branch for Android — non-exact text finds route through fetchFindCandidates (snapshot-based) instead of the legacy agent-device find CLI, so the Android dispatcher never re-spawns the upstream daemon during a find. Removed the previous runAgentDevice(['scrollintoview', ...]) fallback for Android-without-env (replaced with structured IN_TREE_RUNNER_REQUIRED error). Task 11 flips the env default, so the legacy delegate is removed early rather than late. Test coverage: 2 new tests; full suite 1461/1461 passing. Task 8 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): warn on competing Android UIAutomator runners Task 9 of the Android MVP plan. The iOS-side external-runner detector (PR #164) warned at session-open about a stale ~/.agent-device daemon. This commit adds the Android counterpart: detectAndroidExternalRunner runs `adb shell ps -A` and filters for processes matching /uiautomator|agent-device|AgentDevice/i while excluding our own `dev.lykhoyda.rndevagent.androidrunner` package. When `device_snapshot action=open` runs against an Android session AND RN_ANDROID_RUNNER=1 is set, the diagnostic fires and pushes an ANDROID_UIAUTOMATOR_COMPETITOR warning into the session's warnings list. Users see this in the session-open response so they can stop the competing process before our runner contends for focus. Test coverage: 2 new unit tests cover (1) detection of a competing upstream `uiautomator runtest` process and (2) correct exclusion of our own runner package from the warning. Task 9 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(rn-device): enable Android in-tree runner by default Task 11 of the Android MVP plan. Flip the RN_ANDROID_RUNNER env gate from opt-in (=== '1') to opt-out (!== '0'). Android device automation now uses the in-tree scripts/rn-android-runner/ UIAutomator runner by default. Set RN_ANDROID_RUNNER=0 to revert to the legacy agent-device daemon/CLI path during rollback or diagnosis. Changes: - agent-device-wrapper.ts - Android short-circuit gate becomes default-on - device-interact.ts - find + scrollintoview Android-branch gates flip - device-session.ts - Android external-runner diagnostic gate flips - Unit tests updated to match the new env semantics - BUGS.md (plugin) - record deferred Task 10 live smoke-test - ROADMAP.md (plugin) - Phase 138 entry with task-by-task summary Live smoke-test (Task 10) deferred - host had insufficient disk space to boot the Pixel_9_Pro AVD. Unit + Gradle layers all green (1464/1464 tests pass). See BUGS.md + ROADMAP Phase 138 for re-run instructions. Task 11 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(rn-device): grant INTERNET to runner target manifest (Android smoke-test finding) Live Task 10 smoke-test surfaced this real plan defect: java.net.SocketException: socket failed: EPERM (Operation not permitted) on NanoHTTPD's ServerSocket.bind() at runner startup. Root cause: am instrument injects test code into the TARGET app's process (not the test APK's UID). ServerSocket.bind() checks the target UID's permissions, not the test APK's. The androidTest manifest had android.permission.INTERNET; the target app manifest did not. The runner's main AndroidManifest.xml now declares it too. Verified live: after the fix, instrumentation logs RN_ANDROID_RUNNER_LISTENER_READY + RN_ANDROID_RUNNER_PORT=22089 within seconds of am instrument start, curl /health returns {"ok":true}, and a /command snapshot of com.rndevagent.testapp returns the expected tab-home / tab-tasks / tab-notifications / tab-profile identifiers. Plan reference: docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md Task 10 (live smoke-test) - now passes end-to-end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(rn-device): Android runner emulator smoke test passed Live verification on Pixel_9_Pro AVD (emulator-5554): - ./gradlew :app:assembleDebugAndroidTest -> BUILD SUCCESSFUL - am instrument readiness handshake: RN_ANDROID_RUNNER_LISTENER_READY + RN_ANDROID_RUNNER_PORT=22089 within ~3s of spawn - curl http://127.0.0.1:22089/health -> {"ok":true} - curl /command snapshot of com.rndevagent.testapp -> 148 nodes including tab-home, tab-tasks, tab-notifications, tab-profile identifiers from the React Native bottom tab bar - ps -A | grep agent-device|AgentDevice|uiautomator (excluding our androidrunner package) -> empty (no upstream contention) Smoke-test surfaced one real plan defect (INTERNET permission missing from target app manifest); fixed in the preceding commit and verified post-fix. Closes Task 10 of 11 per docs/superpowers/plans/2026-05-16-rn-android-runner-mvp-plan.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(rn-device): Task 10 smoke-test passed live - clear deferred notes BUGS.md: move the Task 10 "deferred" entry to a Fixed section with the live verification details (handshake, /health, snapshot returning tab-* identifiers, zero upstream agent-device processes). Document the one plan defect surfaced + fixed during the run (target-app INTERNET permission - commit f5d4e0a). ROADMAP.md: update Phase 138 Task 10 row from "(deferred)" to "d0367da (+ f5d4e0a plan-defect fix)" with passing criteria summary. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(rn-device): use relative paths in Android test source-grep helpers (CI fix) CI exposed a real plan defect that local runs missed: three Android tests loaded their source files via hardcoded absolute paths (/Users/anton_personal/GitHub/...) inherited verbatim from the plan's test snippets. CI runs at /home/runner/work/... so readFileSync threw ENOENT and the test files failed at module load. Switch all three to import.meta.url + path.resolve to derive the plugin-root-relative path at runtime: - test/unit/android-runner-short-circuit.test.js (2 read sites) - test/unit/build-run-android-args.test.js (1 read site) - test/unit/tools/scrollintoview-orchestration.test.js (1 read site) Also dedupe the inner readFileSync inside the third android-runner-short-circuit test — it was re-reading the same file with the same hardcoded path; the top-level `source` constant is the right reference. No production code changes; tests-only patch. Full suite remains 1464/1464 locally and should now match on CI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d137eea commit aa221aa

44 files changed

Lines changed: 2338 additions & 79 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BUGS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# BUGS
2+
3+
Plugin-side bug log. Workspace-side scaffolding bugs live in
4+
`../rn-dev-agent-workspace/docs/BUGS.md`.
5+
6+
## Open
7+
8+
_(none currently — Task 10 live smoke-test ran successfully on 2026-05-18; see Fixed section below)_
9+
10+
## Fixed
11+
12+
### ~~Live Android emulator smoke test deferred (Task 10, plan 2026-05-16)~~ (PASSED — 2026-05-18)
13+
14+
Task 10 ran live on Pixel_9_Pro AVD (emulator-5554) after host disk space was freed. Acceptance criteria green:
15+
16+
- `./gradlew :app:assembleDebugAndroidTest` → BUILD SUCCESSFUL
17+
- `am instrument` readiness handshake: `RN_ANDROID_RUNNER_LISTENER_READY` + `RN_ANDROID_RUNNER_PORT=22089` within ~3s of spawn
18+
- `curl http://127.0.0.1:22089/health``{"ok":true}`
19+
- `curl /command snapshot` of `com.rndevagent.testapp` → 148 nodes including `tab-home`, `tab-tasks`, `tab-notifications`, `tab-profile` identifiers
20+
- `ps -A | grep agent-device|AgentDevice|uiautomator` (excluding our androidrunner package) → empty (no upstream contention)
21+
22+
Smoke-test surfaced one real plan defect along the way: `java.net.SocketException: EPERM` on `NanoHTTPD.start()` because the target app's manifest was missing `INTERNET` permission. `am instrument` injects test code into the target app's process, and `ServerSocket.bind()` checks the target UID's permissions, not the test APK's. Fixed by adding the permission to `scripts/rn-android-runner/app/src/main/AndroidManifest.xml` (commit `f5d4e0a`). Smoke-test ran clean post-fix.
23+
24+
**Operator notes for re-running**: Metro must be reachable from the emulator. Set up `adb -s emulator-5554 reverse tcp:8081 tcp:8081` before launching the test-app. Existing iOS Metro on host port 8081 is reused — no need for a separate `pnpm android` if iOS Metro is already running.

CLAUDE-MD-TEMPLATE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,29 @@ If `device_list` shows more than one booted device (e.g., both an iOS simulator
369369

370370
This is a known plugin issue — see [Lykhoyda/rn-dev-agent#60](https://github.com/Lykhoyda/rn-dev-agent/issues/60) for tracking and escape-hatch patterns.
371371

372+
### iOS Device Runtime — In-tree `rn-fast-runner`
373+
374+
After PR #164 (D1219), iOS device automation is owned by the **in-tree `rn-fast-runner`** XCTest project that ships with the plugin. iOS no longer requires `agent-device` to be globally installed. The user-facing tool surface (`device_press`, `device_fill`, `device_swipe`, …) is unchanged — only the underlying transport.
375+
376+
What this means in practice:
377+
378+
- **iOS-only projects** can ignore any "agent-device not installed" warning from the SessionStart banner — it's informational. Only flag the warning as actionable if you're targeting Android.
379+
- **First-time iOS setup** needs a one-time pre-build of the runner so `xcodebuild test-without-building` has artifacts to launch:
380+
```bash
381+
cd ${CLAUDE_PLUGIN_ROOT}/scripts/rn-fast-runner/RnFastRunner && \
382+
xcodebuild build-for-testing \
383+
-project RnFastRunner.xcodeproj \
384+
-scheme RnFastRunner \
385+
-destination "platform=iOS Simulator,id=<UDID>" \
386+
-derivedDataPath ../build/DerivedData
387+
```
388+
After that, the runner spawns lazily on the first `device_snapshot action=open`.
389+
- **Stale upstream `AgentDeviceRunner`** may still be alive on the simulator from a previous install — it will fight `rn-fast-runner` for foreground focus and cause "main thread execution timed out" or RUNNER_LEAK shapes. Set `RN_DEVICE_KILL_LEGACY=1` (plugin terminates the daemon at session-open) or one-time-clean: `pkill -f AgentDeviceRunner && rm -f ~/.agent-device/daemon.{json,lock}`.
390+
- **`device_fill` may report "main thread execution timed out" on iOS even though the text lands in the field.** This is a known XCTest-internal quiescence behavior (`XCUIElement.typeText()` bypasses the runner's skip-quiescence shim). The TS client treats this specific error shape as success on `.type` and surfaces `meta.runnerTimeoutShim: true`. If you see this, the side-effect succeeded — proceed; do not retry, because a retry would double-type.
391+
- **`device_find` non-exact + `device_scrollintoview`** are TS-side orchestrators on iOS (snapshot → fuzzy-match / viewport-check → fastSwipe loop). They never touch the legacy `agent-device find/scrollintoview` CLI, so they don't respawn the upstream runner.
392+
393+
Android dispatch is unchanged — still 3-tier `agent-device` (daemon socket → fast-runner → CLI). Multi-device routing rules in the section above still apply.
394+
372395
### Required Dev Setup for Full Tool Coverage
373396

374397
Most projects need **zero source mutation** — the plugin's CDP-injected helpers walk the React fiber tree to find `<NavigationContainer>`'s ref and the React Navigation hooks chain automatically. The table below lists what each tool needs to work; rows marked "auto-discovered" require no user code.

CLAUDE.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ Development scaffolding lives in the **sibling workspace repo**:
3737
- **Node.js >= 22 LTS** (even-numbered release — NOT v25)
3838
- **iOS Simulator** booted with your app OR **Android Emulator** running
3939
- **Metro dev server** running (`npx expo start` or `npx react-native start`)
40-
- The following are auto-installed by the plugin but may need manual install if they fail:
41-
- `agent-device``npm install -g agent-device`
42-
- `maestro-runner` — auto-installed to `~/.maestro-runner/`
40+
- Platform-specific device-control runtime (one of):
41+
- **iOS** — in-tree `rn-fast-runner` XCTest project ships with the plugin (`scripts/rn-fast-runner/`). Pre-build it once with a booted simulator: `cd ${CLAUDE_PLUGIN_ROOT}/scripts/rn-fast-runner/RnFastRunner && xcodebuild build-for-testing -project RnFastRunner.xcodeproj -scheme RnFastRunner -destination "platform=iOS Simulator,id=<UDID>" -derivedDataPath ../build/DerivedData`. After that, the runner spawns lazily on the first `device_snapshot action=open`. iOS no longer requires `agent-device` (PR #164 / D1219).
42+
- **Android**`agent-device` CLI: `npm install -g agent-device` (auto-installed by the plugin; may need manual install if the auto-install fails).
43+
- `maestro-runner` — auto-installed to `~/.maestro-runner/`
4344

4445
### Essential commands
4546
```
@@ -65,7 +66,9 @@ Development scaffolding lives in the **sibling workspace repo**:
6566

6667
### Troubleshooting
6768
- **"CDP connection failed"** → Is Metro running? Is the app loaded on the simulator?
68-
- **"agent-device not installed"** → Run `npm install -g agent-device`
69+
- **"agent-device not installed"** → Only required for Android. If targeting Android, run `npm install -g agent-device`. iOS uses the in-tree `rn-fast-runner` and does not need it.
70+
- **"rn-fast-runner did not become ready" / no `.xctestrun` at the expected path** → Pre-build the runner once with `xcodebuild build-for-testing` (see Prerequisites). The build artifacts live at `scripts/rn-fast-runner/build/DerivedData/`.
71+
- **Legacy `AgentDeviceRunner` re-appears on the simulator** → A stale `~/.agent-device/daemon.json` is respawning the upstream runner. Either run with `RN_DEVICE_KILL_LEGACY=1` (the plugin terminates the daemon at session-open) or `pkill -f AgentDeviceRunner && rm -f ~/.agent-device/daemon.json ~/.agent-device/daemon.lock` one-time.
6972
- **"No booted simulator"** → Open Simulator.app or boot one via Xcode
7073
- **iOS 26.x beta issues** → Use iOS 18 stable runtime (Xcode > Settings > Platforms)
7174
- **Node.js odd version (v25)** → Switch to Node 22 LTS: `nvm install 22 && nvm use 22`
@@ -89,11 +92,16 @@ Three layers working together:
8992

9093
| Layer | Tool | Role |
9194
|-------|------|------|
92-
| Device interaction | agent-device CLI (auto-installed) | Cross-platform native device control: tap, swipe, fill, find, snapshot, screenshot |
95+
| Device interaction (iOS) | In-tree `rn-fast-runner` XCTest rig (`scripts/rn-fast-runner/`) — single `POST /command` HTTP endpoint | Native iOS device control via XCTest. Always calls `XCUIApplication.activate()` per request so the target app is foregrounded (B155 / D1219). |
96+
| Device interaction (Android) | `agent-device` CLI (auto-installed) | Native Android device control: tap, swipe, fill, find, snapshot, screenshot |
9397
| App introspection | Custom MCP server → Hermes CDP via WebSocket | Persistent WebSocket — reads React fiber tree, store state, network, console, errors |
9498
| E2E testing | maestro-runner (preferred) / Maestro (fallback) | YAML-based persistent test files for CI |
9599

96-
Fallback: `xcrun simctl` (iOS) + `adb` (Android) for device lifecycle when agent-device is unavailable.
100+
iOS dispatch: every iOS `device_*` call short-circuits through `runIOS()` (TS client at `scripts/cdp-bridge/src/runners/rn-fast-runner-client.ts`) to the in-tree runner's `/command` endpoint. Coordinate-based gestures map to `.drag`; direction-based swipes/scrolls are pre-computed to coords by `device-interact.ts` before dispatch. `device_find` non-exact + `device_scrollintoview` are TS-side orchestrators over `runIOS('snapshot')` (no Swift `.findText` round-trip for fuzzy match — too coarse, returns bool only).
101+
102+
Android dispatch unchanged: 3-tier `agent-device` (daemon socket → fast-runner → CLI). The legacy daemon is detected at session-open on iOS too and warned about (`RN_DEVICE_KILL_LEGACY=1` opts into termination) — a stale daemon respawns the upstream `AgentDeviceRunner` and fights our `RnFastRunner` for focus.
103+
104+
Fallback: `xcrun simctl` (iOS) + `adb` (Android) for device lifecycle (boot / install / launch / terminate) — the runner doesn't manage device state, only interaction.
97105

98106
### MCP Server (cdp-bridge)
99107

@@ -118,12 +126,16 @@ Fallback: `xcrun simctl` (iOS) + `adb` (Android) for device lifecycle when agent
118126
- `cdp_set_shared_value` — set Reanimated SharedValue by testID for proof captures
119127
- `collect_logs` — parallel multi-source log collection
120128

121-
**Device tools** (14 — native interaction via agent-device CLI):
129+
**Device tools** (14 — iOS: in-tree `rn-fast-runner` `/command` endpoint; Android: `agent-device` CLI):
122130
- `device_list` / `device_screenshot` / `device_snapshot`
123131
- `device_find` / `device_press` / `device_fill` / `device_swipe` / `device_scroll`
124132
- `device_scrollintoview` / `device_back` / `device_longpress` / `device_pinch`
125133
- `device_permission` / `device_batch`
126134

135+
iOS-only quirks worth knowing:
136+
- `device_fill` may surface a Swift-internal `XCUIElement.typeText` quiescence-timeout from XCTest's main-thread sync. The TS client treats this specific error as success on `.type` (`meta.runnerTimeoutShim: true`) because the side-effect (text appended to the field) demonstrably succeeds — observed across the iOS-MVP smoke-tests.
137+
- `device_find` non-exact + `device_scrollintoview` ALWAYS route through the TS orchestrators on iOS (never the legacy `agent-device find/scrollintoview` CLI), so they don't respawn the upstream `AgentDeviceRunner`.
138+
127139
**Testing & composite tools** (13):
128140
- `proof_step` / `cross_platform_verify` / `maestro_run` / `maestro_generate` / `maestro_test_all`
129141
- `cdp_auto_login` + device helpers (deeplink, accept/dismiss dialog, focus_next, pick_date, pick_value)
@@ -136,7 +148,8 @@ Fallback: `xcrun simctl` (iOS) + `adb` (Android) for device lifecycle when agent
136148
- 3-tier interaction model: cdp_interact (JS) > device_press (XCTest) > Maestro (E2E) — D497
137149
- Hook-mode fallbacks for network body + CPU profile on RN < 0.83 — D597
138150
- Proactive __RN_AGENT freshness check before every tool call — D502
139-
- agent-device CLI wrapped via `agent-device-wrapper.ts` — 3-tier dispatch: fast-runner → daemon → CLI
151+
- iOS device verbs route through `rn-fast-runner-client.ts` (`runIOS()``/command`); Android keeps 3-tier `agent-device` dispatch (fast-runner → daemon → CLI) via `agent-device-wrapper.ts` — D1219, PR #164
152+
- `cdp_repair_action` self-bootstraps the iOS fast-runner on auto-repair (no pre-opened device session required) — D1220
140153

141154
## Conventions
142155

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ cd /path/to/your-rn-app
2626
/rn-dev-agent:setup
2727
```
2828

29-
This checks **9 prerequisites** and fixes what it can automatically:
29+
This checks **10 prerequisites** and fixes what it can automatically:
3030

3131
| Check | Required | Auto-install |
3232
|-------|----------|-------------|
3333
| Node.js >= 22 LTS | Yes | No |
3434
| CDP bridge deps | Yes | Yes |
35-
| [agent-device](https://github.com/nicklama/agent-device) | Yes | Yes |
35+
| rn-fast-runner (iOS) | iOS targets only — ships in-tree; one-time `xcodebuild build-for-testing` | No |
36+
| [agent-device](https://github.com/nicklama/agent-device) | Android targets only — iOS no longer needs it (PR #164) | Yes |
3637
| [maestro-runner](https://github.com/devicelab-dev/maestro-runner) | Yes | Yes |
3738
| iOS Simulator / Android Emulator | One platform | No |
3839
| Metro dev server | Yes | No |
@@ -93,7 +94,7 @@ A saved, replayable flow through your app — login, navigate to settings, reach
9394
| Command | Purpose |
9495
|---------|---------|
9596
| `/rn-dev-agent:setup` | Inject CLAUDE.md tool-routing rules + nav-ref + Zustand exposure |
96-
| `/rn-dev-agent:doctor` | 12-row diagnostic table — Node, CDP, agent-device, maestro-runner, simulators, Metro, helpers freshness, plugin version |
97+
| `/rn-dev-agent:doctor` | 14-row diagnostic table — Node, CDP, rn-fast-runner (iOS), agent-device (Android), maestro-runner, simulators, Metro, helpers freshness, plugin version |
9798
| `/rn-dev-agent:check-env` | Quick environment-readiness check |
9899
| `/rn-dev-agent:nav-graph` | Extract and inspect the app navigation graph |
99100
| `/rn-dev-agent:send-feedback` | Open a GitHub issue with sanitized environment context |
@@ -160,10 +161,14 @@ Claude Code
160161
├── MCP Server (CDP Bridge) ─── WebSocket → Metro → Hermes CDP
161162
│ 74 tools: component tree, store state, profiling, network, interaction, recording, self-healing
162163
163-
└── Bash (device lifecycle)
164-
xcrun simctl / adb / maestro-runner / agent-device
164+
└── Device interaction
165+
├── iOS → in-tree rn-fast-runner (XCTest /command HTTP) ← D1219, PR #164
166+
└── Android → agent-device CLI (daemon socket → fast-runner → CLI)
165167
│ │
166168
iOS Simulator Android Emulator
169+
170+
Device lifecycle (boot / install / launch): xcrun simctl + adb
171+
E2E test execution: maestro-runner (preferred) / Maestro (fallback)
167172
```
168173

169174
[Architecture details](https://lykhoyda.github.io/rn-dev-agent/architecture/)
@@ -194,6 +199,9 @@ Claude Code
194199
| Spawned subagent says "MCP tools unavailable" | Never spawn `rn-tester` / `rn-debugger` via Task tool — MCP stdio doesn't propagate to subprocesses (GH #31). Use `/rn-dev-agent:test-feature` or `/rn-dev-agent:debug-screen` instead; protocols run inline in the parent session. |
195200
| Blank white screen after many reloads | NativeWind stylesheet corruption after 5+ `cdp_reload` cycles. Kill Metro, restart it, relaunch the app. `cdp_status` warns when reload count is high. |
196201
| `device_scroll` times out on Reanimated screens | agent-device daemon `waitForIdle` deadlocks with Reanimated worklets. Fixed in v0.22.0 — scroll routes through fast-runner HID synthesis. Ensure fast-runner is healthy via device session. |
202+
| Legacy `AgentDeviceRunner` re-appears on iOS sim | Stale `~/.agent-device/daemon.json` respawns the upstream runner alongside our in-tree `rn-fast-runner`. Run with `RN_DEVICE_KILL_LEGACY=1` (plugin terminates the daemon at session-open) or one-time: `pkill -f AgentDeviceRunner && rm -f ~/.agent-device/daemon.{json,lock}`. |
203+
| iOS `device_*` calls fail with "rn-fast-runner did not become ready" | Build artifacts missing. Pre-build once: `cd ${CLAUDE_PLUGIN_ROOT}/scripts/rn-fast-runner/RnFastRunner && xcodebuild build-for-testing -project RnFastRunner.xcodeproj -scheme RnFastRunner -destination "platform=iOS Simulator,id=<UDID>" -derivedDataPath ../build/DerivedData`. After that, the runner spawns lazily on `device_snapshot action=open`. |
204+
| iOS `device_fill` returns "main thread execution timed out" but text appears in the field | Known XCTest-internal quiescence behavior; the TS client treats this specific error as success on `.type` (`meta.runnerTimeoutShim: true`). The side-effect succeeded — proceed. |
197205

198206
[Full troubleshooting guide](https://lykhoyda.github.io/rn-dev-agent/troubleshooting/)
199207

ROADMAP.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ROADMAP
2+
3+
Plugin-side roadmap. Workspace-side scaffolding roadmap lives in
4+
`../rn-dev-agent-workspace/docs/ROADMAP.md`.
5+
6+
## Phase 138: rn-android-runner MVP — in-tree UIAutomator runner for Android (2026-05-16)
7+
8+
**Why:** Android device automation was the last surface still routed through
9+
upstream `agent-device` after PR #164's iOS-MVP migration. Phase 138 vendors
10+
an in-tree Gradle Android instrumentation runner mirroring the iOS pattern
11+
(NanoHTTPD on port 22089, single `POST /command` JSON contract, UIAutomator
12+
under `am instrument`).
13+
14+
**What landed** (PR #?, branch `feat/rn-android-runner-mvp`):
15+
16+
| Task | Commit | What |
17+
|---|---|---|
18+
| 1 | ac22b74 | Gradle scaffold + Kotlin runner skeleton (~340 LOC). Configurator.setWaitForIdleTimeout(0) for RN/Reanimated. |
19+
| 2 | 5620d8a | SnapshotForegroundRegressionTest.kt (Android equivalent of iOS B155). |
20+
| 3 | 16cf3fa | TS HTTP client `rn-android-runner-client.ts` + 4 unit tests. runner-timeout shim for UIAutomator typeText on RN. |
21+
| 4+5 | fcb8870 | Short-circuit in runAgentDevice gated by RN_ANDROID_RUNNER + buildRunAndroidArgs + 4 unit tests. |
22+
| 6+7 | 658fe2d | flattenAndroidAccessibilityTree helper + Android stale-ref test coverage. |
23+
| 8 | 6227536 | device_find/device_scrollintoview Android branches reuse the iOS orchestrators (D1217 symmetry). |
24+
| 9 | ccd82e8 | detectAndroidExternalRunner warns about competing UIAutomator/agent-device processes at session-open. |
25+
| 10 | `d0367da` (+ `f5d4e0a` plan-defect fix) | Live Android emulator smoke-test PASSED on Pixel_9_Pro AVD. Acceptance criteria green: handshake, /health, /command snapshot returning tab-* identifiers, zero upstream agent-device processes. Surfaced + fixed one real plan defect — target-app manifest was missing INTERNET permission (am instrument runs test code under target UID). |
26+
| 11 | (this commit) | Flip env default ON. `RN_ANDROID_RUNNER=0` is the new escape hatch. Docs. |
27+
28+
**Acceptance criteria met:**
29+
- Unit suite: 1464+/1464+ passing (was 1449 pre-Android, +15 new Android tests)
30+
- Gradle `:app:assembleDebugAndroidTest` builds clean
31+
- D1217 cross-platform symmetry: device_find / device_scrollintoview use the same TS orchestrators on both platforms
32+
- D1219-equivalent for Android: iOS-side `agent-device` dependency dropped for the same reasons
33+
34+
**Out of scope (carried to a later phase):**
35+
- Physical-device hardening beyond ADB
36+
- Android TV / Wear OS / multi-display input
37+
- Recording, replay, video capture
38+
- AccessibilityService-based automation
39+
- Removing the upstream `agent-device` Android fallback. Currently kept as the `RN_ANDROID_RUNNER=0` escape hatch.

0 commit comments

Comments
 (0)