Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/phase6-foreign-flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"rn-dev-agent-cdp": minor
"rn-dev-agent-plugin": minor
---

#202 Phase 6 / #186 — foreign Maestro sessions become arbiter refusals; plugin maestro_run is the canonical surface.

While a foreign Maestro/XCUITest session drives the target simulator (UDID-scoped detection, 5 s TTL, fail-open), local `device_*` and flow tools refuse fast with `BUSY_FOREIGN_FLOW` (~50 ms measured) — pointing at the safe L1 reads — instead of colliding into the ~44 s runner-leak cascade. L1 introspection stays free; `device_screenshot` serves pixels via its simctl fallback; a ~10 s teardown grace after the plugin's own flows prevents self-false-positives while WDA dies. The two historical reasons to leave the plugin surface are live-gate-verified closed and #201 is closed — including a new fix: the clearState `--app-file` resolution is snapshotted outside the device container (the installed-container path used to be deleted by clearState itself before the reinstall could read it). `RN_IOS_FOREIGN_GUARD=0` disables both the warning and the refusal (`RN_IOS_FOREIGN_WARN=0` remains a deprecated alias). The foreign-runner `ps` scan now uses `-ww` (command-column truncation could silently drop the UDID → false negatives).
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Repo-local troubleshooting memory (replaces the Experience Engine):
- **`RnFastRunner` / `RnFastRunnerUITests-Runner` icons appear on the simulator** → Expected, not clutter. iOS device control is an XCUITest rig (D1219), so running it installs two apps: `RnFastRunner` (the minimal host app, bundle `dev.lykhoyda.rndevagent.fastrunner`) and `RnFastRunnerUITests-Runner` (the XCUITest harness — same pattern as WebDriverAgent's `WebDriverAgentRunner`). The Runner hosts the `POST /command` HTTP server on port 22088 and drives YOUR app via `XCUIApplication(bundleIdentifier:)` — it never drives itself. It stays installed/running on purpose so subsequent `device_*` calls are fast; leave it. (Contrast the legacy `AgentDeviceRunner` above, which IS unwanted.)
- **"Disconnected due to opening a second DevTools window" / React Native DevTools keeps getting kicked** → RN allows exactly one debugger frontend per app, and the bridge auto-reconnects by default (agent-first). To let the visual DevTools hold the seat, set `RN_CDP_AUTOCONNECT=0` (or `.rn-agent/config.json` → `{ "cdp": { "autoConnect": false } }`). The bridge then reconnects only when a CDP tool actually runs, and yields again once you reopen DevTools. Note: **any** CDP tool call — including `cdp_status` — reclaims the seat while it runs; passive mode only stops *background* re-grabs. Check the resolved mode in `cdp_status` → `autoConnect`.
- **MCP server died when Metro was restarted (all tools gone until session restart)** → Fixed since #202 Phase 5 (#264): the stdio supervisor holds no network sockets, so port-based kills (`lsof -ti tcp:8081 | xargs kill -9`) only take the worker, which respawns automatically (`cdp_status` → `bridge.workerRestarts`). If tools error with "worker is crash-looping", check the bridge log (`LOG_LEVEL=info` writes it) and restart the session. `RN_BRIDGE_SUPERVISOR=0` opts back into the legacy single-process bridge.
- **`BUSY_FOREIGN_FLOW` on device_*/maestro_run** → A foreign Maestro/XCUITest session (e.g. standalone maestro-mcp) is driving the same simulator. By design (#186): wait for it to finish (the guard clears within ~5 s of the foreign run ending), use L1 reads (`cdp_component_tree`, `cdp_store_state`) and `device_screenshot` meanwhile, or disable the guard with `RN_IOS_FOREIGN_GUARD=0` (`RN_IOS_FOREIGN_WARN=0` is a deprecated alias with the same effect). The first tap within ~10 s after your OWN `maestro_run` is exempt by design (WDA teardown grace).
- **"No booted simulator"** → Open Simulator.app or boot one via Xcode
- **iOS 26.x beta issues** → Use iOS 18 stable runtime (Xcode > Settings > Platforms)
- **Node.js odd version (v25)** → Switch to Node 22 LTS: `nvm install 22 && nvm use 22`
Expand Down Expand Up @@ -157,7 +158,7 @@ One mechanism per capability tier. The device-session honors this contract (the
| **L2 INTERACTION** | iOS `RnFastRunner` / Android `agent-device`; `cdp_interact` | primitive taps / types / scrolls | **shared** | re-attach, don't evict (Tier-0 reacquire + CDP re-pin, #188) |
| **L3 FLOW-REPLAY** | `maestro-runner` (Go + WDA) | whole-`.yaml` E2E flows | **exclusive** | owns the device for the flow's duration |

**Coexistence rule:** L1 reads never conflict with a foreign runner; L2 re-attaches rather than evicts; L3 owns the device. On `device_snapshot action=open`, if a foreign maestro session is detected (UDID-scoped) AND no local flow lease is held, the open result carries an informational `meta.foreignRunner` + `FOREIGN_RUNNER_ACTIVE` warning (`runners/external-runner-detect.ts`; opt out with `RN_IOS_FOREIGN_WARN=0`). See `docs-site` → "Using rn-dev-agent with maestro-mcp".
**Coexistence rule:** L1 reads never conflict with a foreign runner; L2 re-attaches rather than evicts; L3 owns the device. Since #202 Phase 6 (#186), a detected foreign Maestro session is an **arbiter input**, not just a warning: while it is live (UDID-scoped, 5 s-TTL `ps axww` scan via `lifecycle/foreign-flow-gate.ts`, fail-open), local L2 `device_*` and L3 flow tools refuse fast with `BUSY_FOREIGN_FLOW` (~50 ms measured, vs the ~44 s runner-leak cascade) while L1 reads stay free and `device_screenshot` serves pixels via its simctl fallback. A ~10 s teardown grace after the plugin's OWN flows prevents self-false-positives while WDA dies. The plugin's `maestro_run` is the **canonical** Maestro surface (it participates in the arbiter, parks the L2 runner, marks CDP stale, auto-repairs actions); the standalone maestro-mcp coexists for ad-hoc use and is refused against rather than collided with mid-flow. The device-open `FOREIGN_RUNNER_ACTIVE` warning remains. `RN_IOS_FOREIGN_GUARD=0` disables BOTH the warning and the refusal; the older `RN_IOS_FOREIGN_WARN=0` is a deprecated alias with the same (full) effect — if you set it to quiet the warning, know it now also drops the refusal. See `docs-site` → "Using rn-dev-agent with maestro-mcp".

**Device-session visibility + self-healing (#210, D1249).** The L2 `rn-fast-runner` is THE iOS `device_*` backend; Maestro/WDA is the L3 flow engine — **serialized, not competing** (there is no shared WDA session to "ride": maestro-runner spawns WDA per-flow and tears it down). Three reuse-first behaviors make the single iOS path coherent: (1) `cdp_status.deviceSession` reports `{ sessionOpen, rnFastRunner: 'alive'|'stale'|'dead', foreignRunner? }` (iOS-gated) so the runner's state is visible before any `device_*`; (2) `device_find/press/fill` **auto-spawn** the runner from the dispatch choke point when it's down and the XCUITest rig is **prebuilt** — a missing rig returns an actionable `RN_FAST_RUNNER_DOWN` (never a silent multi-minute `xcodebuild`); (3) `device_screenshot` **falls back to `xcrun simctl io screenshot`** whenever the runner can't serve it — including while a Maestro flow owns the device (it runs unleased via the arbiter's `FLOW_FALLBACK_TOOLS` allowlist; simctl is OS-level and can't conflict with WDA). Mid-flow needs map to non-conflicting mechanisms: **pixels → simctl**, **tree/state → `cdp_component_tree`/`cdp_store_state`** (CDP introspection coexists with a flow by design), **taps → not mid-flow** (the arbiter refuses them on purpose). A WDA W3C client was rejected — it would add a *second* XCUITest backend, the opposite of unifying.

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ One mechanism per capability tier — **L1 + L2 coexist** (drive with XCTest, as
| **L2 INTERACTION** | iOS `RnFastRunner` / Android `agent-device`; `cdp_interact` | primitive taps / types / scrolls | shared |
| **L3 FLOW-REPLAY** | `maestro-runner` (Go + WDA) | whole-`.yaml` E2E flows | exclusive |

**Foreign runners** (e.g. the standalone `maestro-mcp`): L1 reads are always safe; on an L2 leak the device-session re-attaches rather than evicts (#188); `device_snapshot action=open` surfaces an informational `FOREIGN_RUNNER_ACTIVE` warning when a foreign session is present and no local flow is running.
**Foreign runners** (e.g. the standalone `maestro-mcp`): L1 reads are always safe; on an L2 leak the device-session re-attaches rather than evicts (#188); `device_snapshot action=open` surfaces an informational `FOREIGN_RUNNER_ACTIVE` warning. Since #186, a LIVE foreign session (UDID-scoped, 5 s-TTL `ps` scan, fail-open) also makes local `device_*` and flow tools refuse fast with `BUSY_FOREIGN_FLOW` (~50 ms, vs the ~44 s runner-leak cascade) while CDP reads stay free and `device_screenshot` falls back to simctl; the plugin's `maestro_run` is the canonical Maestro surface. Opt out with `RN_IOS_FOREIGN_GUARD=0`.

## MCP server (CDP bridge)

Expand Down
8 changes: 6 additions & 2 deletions docs-site/src/content/docs/guides/maestro-interop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ When an L2 call sees the runner-leak sentinel after a maestro run, the device-se

## The `FOREIGN_RUNNER_ACTIVE` warning

When you `device_snapshot action=open` while a foreign maestro automation session is driving the same simulator (and rn-dev-agent is not itself running a flow), the open result carries an informational `meta.foreignRunner` and a `FOREIGN_RUNNER_ACTIVE` warning. It is a **heads-up only** — it does not block or change the open. It tells you the simulator is contended so you can expect a re-foreground if you interleave `device_*`.
When you `device_snapshot action=open` while a foreign maestro automation session is driving the same simulator (and rn-dev-agent is not itself running a flow), the open result carries an informational `meta.foreignRunner` and a `FOREIGN_RUNNER_ACTIVE` warning. The open itself is never blocked.

The detection is UDID-scoped (it ignores maestro flows on other simulators and the idle maestro-mcp server). Opt out with `RN_IOS_FOREIGN_WARN=0`. Note: immediately after your *own* `maestro_run`, you may briefly see the warning while maestro's driver is still exiting — that's expected.
Since #186 shipped, the contention handling goes further: while a foreign session is live (UDID-scoped detection, ~5 s cache), rn-dev-agent's `device_*` taps and `maestro_run` flows **refuse fast with `BUSY_FOREIGN_FLOW`** (~50 ms) instead of colliding into the multi-second runner-leak recovery cascade. L1 reads (`cdp_component_tree`, `cdp_store_state`, `cdp_navigation_state`) keep working throughout, and `device_screenshot` serves pixels via its OS-level simctl fallback. The guard clears within ~5 s of the foreign run ending; the first tap within ~10 s after rn-dev-agent's **own** flows is exempt (WDA teardown grace). Disable with `RN_IOS_FOREIGN_GUARD=0` (`RN_IOS_FOREIGN_WARN=0` is a deprecated alias — it disables the refusal too, not just this warning).

**Division of labor:** rn-dev-agent's `maestro_run` is the canonical Maestro surface — it participates in the device arbiter, parks the L2 interaction runner for the flow, marks CDP stale afterwards, and auto-repairs saved actions. Use the standalone maestro-mcp for ad-hoc exploration; when both target one simulator, rn-dev-agent now steps back cleanly instead of fighting.

The detection is UDID-scoped (it ignores maestro flows on other simulators and the idle maestro-mcp server). Opt out with `RN_IOS_FOREIGN_GUARD=0` (disables the refusal too). Note: immediately after your *own* `maestro_run`, you may briefly see the warning while maestro's driver is still exiting — that's expected, and the refusal guard explicitly exempts that window.

## L3 flows own the device

Expand Down
1 change: 1 addition & 0 deletions docs-site/src/content/docs/tools/cdp/cdp_run_action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Replay a learned action by id with end-to-end auto-repair. Loads the action from
| `timeoutMs` | `number` | No | | | Maestro execution timeout per attempt (ms). Default 120_000. |
| `trigger` | `enum: agent | ci | human` | No | | | RunRecord trigger annotation. Default "agent". CI calls should pass "ci". |
| `forceReload` | `boolean` | No | | | GH #173: when true (default), acknowledge any human edit to the YAML as the new baseline before running so downstream repair does not abort with STALE_TARGET. Pass false for the strict Phase 129 "respect external edits" behavior (useful for CI replays of fixed baselines). |
| `params` | `Record<string, unknown>` | No | | | Parameter bindings for the action\\'s $\{VAR\} placeholders, forwarded to maestro as -e KEY=VALUE on the first attempt AND the post-repair retry (GH #116). Keys must match /^[A-Z_][A-Z0-9_]*$/ (validated in maestro_run). |

## Usage

Expand Down
1 change: 1 addition & 0 deletions docs-site/src/content/docs/tools/testing/maestro_run.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Execute a Maestro flow via maestro-runner. Pass flowPath for an existing .yaml f
| `appId` | `string` | No | | | App bundle ID (auto-detected from app.json) |
| `appFile` | `string` | No | | | iOS only — path to a built .app/.ipa for maestro-runner to reinstall on clearState. Auto-resolved from the flow appId when omitted (GH#201). |
| `timeoutMs` | `number` | No | `120000` | min: 5000, max: 300000, integer | Execution timeout in ms |
| `params` | `Record<string, unknown>` | No | | | GH #116: parameter bindings forwarded as -e KEY=VALUE for $\{KEY\} placeholders in the flow. Keys must match /^[A-Z_][A-Z0-9_]*$/ (validated in the handler). |

## Usage

Expand Down
Loading
Loading