Skip to content

Architecture

The plugin is organized into three layers. Each one has a different job and gets used at a different point in the loop.

LayerJobExamples
WorkflowDecides what to do; gates quality/rn-feature-dev 8-phase pipeline; the rn-tester / rn-debugger / rn-code-architect agent protocols
DiscoveryLooks at the running app to know what’s thereCDP tools (cdp_component_tree, cdp_store_state, cdp_navigation_state, cdp_evaluate); device tools (device_press, device_fill, device_snapshot, device_screenshot)
Reproducible actionsReplays known flows in seconds; self-repairs on UI drift.rn-agent/actions/<name>.yaml; cdp_run_action orchestrator; cdp_repair_action selector patcher; actions guide
┌──────────────────────────────────────┐
│ Workflow (rn-feature-dev, agents) │
│ Decides WHAT, gates quality │
└────────┬───────────────────┬─────────┘
│ "verify this" │ "produce/replay flow"
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Discovery │ │ Actions │
│ Empirical reality │ │ Replay in seconds │
│ CDP + device tools │ │ Self-repair on drift│
│ Macro-Asserts │◄─┤ │
└─────────────────────┘ └──────────────────────┘
▲ │
│ Discovery emits │
└─── new actions ───┘
Actions repair via Discovery

Why this matters: replay vs. interactive walks

Section titled “Why this matters: replay vs. interactive walks”

On a known 3-step task-creation wizard, an interactive agent walk took 13 minutes 55 seconds. The same wizard, replayed as a saved action, finished in ~4 seconds — a ~210× speedup. That’s the load-bearing data point behind the architecture: discovery tools are how the agent finds new ground; actions are how it replays known ground without paying that cost again.

The agent doesn’t choose all-or-nothing. If you’re on the login screen and need to do something on home, the agent runs the saved login action as a prologue (4 seconds), then discovers the new work interactively. See actions for the full hybrid composition pattern.


Implementation layers — how it’s built

Section titled “Implementation layers — how it’s built”

The product layers above are organized; underneath, three implementation layers do the work.

┌─────────────────────────────────────────────────────┐
│ Claude Code │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Skills │ │ Agents │ │ Commands │ │
│ │ (knowledge) │ │ (protocols) │ │ (entry pts)│ │
│ └──────┬───┬──┘ └──────┬───────┘ └─────┬──────┘ │
│ │ │ │ │ │
│ ┌──────▼───▼────────────▼─────────────────▼──────┐ │
│ │ MCP Server (CDP Bridge) │ │
│ │ WebSocket → Metro → Hermes CDP │ │
│ │ 74 tools across 5 families │ │
│ └─────────┬───────────────────────────┬───────────┘ │
│ │ │ │
│ ┌─────────▼──────────┐ ┌─────────▼──────────┐ │
│ │ rn-fast-runner │ │ agent-device CLI │ │
│ │ (iOS, in-tree) │ │ (Android, 3-tier) │ │
│ │ XCTest /command │ │ daemon → CLI │ │
│ └────────────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────┘
│ │
┌────▼────┐ ┌─────▼─────┐
│ iOS Sim │ │ Android │
│ │ │ Emulator │
└─────────┘ └───────────┘
Implementation tierToolRole
Device interaction (iOS)In-tree rn-fast-runner XCTest rig (scripts/rn-fast-runner/) — POST /command HTTPNative iOS device control. Always calls XCUIApplication.activate() per request (D1219, PR #164). iOS no longer requires agent-device.
Device interaction (Android)agent-device CLI (auto-installed)3-tier dispatch: daemon socket → fast-runner → CLI fallback
App introspectionCustom MCP server → Hermes CDP via WebSocketPersistent WebSocket — reads React fiber tree, store state, network, console, errors
E2E testingmaestro-runner (preferred) / Maestro (fallback)YAML-based persistent test files; underlying format for actions in .rn-agent/actions/

Fallback: xcrun simctl (iOS) + adb (Android) for device lifecycle (boot / install / launch / terminate) — the runner doesn’t manage device state, only interaction.

The MCP server is a Node.js process that maintains a persistent WebSocket connection to the React Native app’s Hermes engine through Metro’s CDP endpoint.

74 tools across five families:

  • CDP — React internals via Chrome DevTools Protocol (component tree, store state, navigation, profiling, network)
  • Device — Native interaction (iOS: rn-fast-runner, Android: agent-device)
  • Actions — Record / replay / self-repair (cdp_run_action, cdp_repair_action, cdp_record_test_save_as_action, cdp_record_test_*)
  • Testing — Proof capture, auto-login, cross-platform verify, Maestro orchestration
  • Macro-Asserts — State-assertive replays (expect_redux, expect_route, expect_visible_by_testid, expect_text)

All tools are registered through a single trackedTool() wrapper that adds telemetry via the Experience Engine — that’s the same mechanism that feeds the auto-action capture loop.

On first CDP connect, ~2KB of JavaScript is injected into the Hermes runtime via Runtime.evaluate. This creates globalThis.__RN_AGENT with methods:

  • getTree(filter, depth) — Walk the React fiber tree
  • getNavState() — Read React Navigation / Expo Router state
  • getStoreState(path, type) — Read Redux / Zustand / React Query
  • getComponentState(testID) — Inspect hooks by testID
  • navigateTo(screen, params) — Navigate via fiber tree traversal
  • getErrors() / clearErrors() — Error tracking

Since MCP is pull-based (tools are called on demand), events that fire between calls are buffered:

BufferSizeContent
Console200 entriesconsole.log/warn/error output
Network100 entriesHTTP request/response metadata
Errors50 entriesUnhandled exceptions and promise rejections
DecisionRationale
Inject helpers ONCE on connect~2KB JS, then call __RN_AGENT.* — small payloads per call
5-second timeout on ALL CDP callsPrevents hanging promises
RedBox detection before tree returnCheck fiber root for LogBox/ErrorWindow, warn instead of returning error overlay
Debugger.paused auto-resumePrevents silent JS thread freeze from debugger; statements
Network fallback for RN < 0.83Try Network.enable, if fails → inject fetch/XHR monkey-patches
Filter mandatory on component treeFull dumps waste 10K+ tokens — always scope to testID or component

Since PR #164 / D1219 the two platforms use different dispatch paths:

iOS — single-endpoint rn-fast-runner. Every iOS device_* call short-circuits through runIOS() (TS client at scripts/cdp-bridge/src/runners/rn-fast-runner-client.ts) to a POST /command HTTP endpoint exposed by an in-tree XCTest rig. 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) and device_scrollintoview are TS-side orchestrators over runIOS('snapshot') — no Swift .findText round-trip for fuzzy matching. iOS no longer requires agent-device.

Android — 3-tier agent-device fallback. The Android path retains the original tiered dispatch via agent-device-wrapper.ts:

  1. fast-runner (XCTest-style HTTP server bundled with agent-device) — lowest latency
  2. agent-device daemon — persistent process, medium latency
  3. agent-device CLI — direct invocation, highest latency

Measured: iOS rn-fast-runner delivers ~216ms tap, ~5ms snapshot, ~74ms screenshot — the fast-runner path is ~13× faster than CLI fallbacks on either platform.

A stale ~/.agent-device/daemon.json can respawn the upstream AgentDeviceRunner and fight the in-tree rn-fast-runner for focus on iOS. The plugin detects the legacy daemon at session-open; set RN_DEVICE_KILL_LEGACY=1 to opt into termination automatically.

ToolWhy not
AppiumToo heavy, latency overhead, black-box (no RN sync)
FlipperDeprecated for debugging in RN 0.76+
DetoxGreat for JS tests but not AI-agent-friendly (JS files, not YAML)
Facebook idbPython + pip + companion daemon = too much setup friction