Skip to content

Commit 58c4886

Browse files
Lykhoydaclaude
andauthored
feat(cdp): debugger-seat opt-out + silent hook-mode network transport (#267)
* docs(spec): debugger-seat opt-out + silent hook-mode network transport External feedback: the CDP bridge evicts React Native DevTools from the single debugger seat (0ms-reconnect + background poll re-grab it), and hook-mode network capture spams __RN_NET__ console lines into Metro logs and the user's DevTools. Approved design: keep agent-first default, add autoConnect opt-out (RN_CDP_AUTOCONNECT env var > .rn-agent/config.json > default true) disabling only background reconnect paths; replace the __RN_NET__ console.log transport with an in-app ring buffer drained on demand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(plan): TDD implementation plan for debugger-seat opt-out + buffered net transport Amendments applied from the multi-LLM plan review (external providers failed — Gemini 429, Codex usage limit — findings are file-verified coordinator research): DeviceBufferManager import/constructor blocker, foreground-reclaim seat policy for cdp_status/recoverWedge documented, wait_for_network drain throttled to 500ms, status payload shape reconciled to top-level autoConnect, duplicate-id dedup added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cdp): resolve autoConnect from RN_CDP_AUTOCONNECT env + .rn-agent/config.json Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cdp): gate background reconnect paths behind autoConnect (passive mode) Add optional `isAutoConnectEnabled` to `ReconnectContext`; when present and false, `handleClose` sets state to disconnected and returns without launching the reconnect loop, and `startBackgroundPoll` skips arming the timer — yielding the single RN debugger seat to a human DevTools session. All foreground/on-demand connect paths remain ungated. Existing consumers without the callback keep today's behavior (backward-compatible). 5/5 new unit tests pass; gh-208-status-storm-preempt regression clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cdp): gate armed background-poll callback in passive mode; dedupe gate via isPassive Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cdp): surface autoConnect resolution in cdp_status and wire passive mode into CDPClient Adds CDPClient.autoConnectState (lazy-resolved once per process via resolveAutoConnect()), passes isAutoConnectEnabled into buildReconnectCtx() so passive-mode gating from Task 2 is live, and surfaces { enabled, source } as cdp_status.autoConnect. Carry-over fixes: load-bearing comment on the 'env' in deps test-seam guard in resolveAutoConnect, and a new test covering the real process.env.RN_CDP_AUTOCONNECT branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cdp): pin autoConnectState cache contract; report autoConnect on status failure paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(cdp): extract applyNetworkHookEntry with duplicate-id dedup Pulls the request/response application logic out of parseNetworkHookMessage into a new exported applyNetworkHookEntry function so Tasks 5/6 (buffered drain path) can share identical entry-application semantics with the legacy console-event path. Adds a getByKey dedup guard on 'request' entries to prevent double-push during the one-release back-compat window where both paths may report the same id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cdp): buffered in-app transport for hook-mode network capture (no console spam) Replaces the console.log __RN_NET__ transport with an in-app ring buffer (__RN_AGENT_NET_BUF__, capped at 100 entries, idempotent on reinject). Adds RN_FORCE_NETWORK_HOOK=1 test seam for live-verification on RN >= 0.83. Adds 6 new tests (2 hardening pin-tests + 4 NETWORK_CB_BUFFERED_SCRIPT contract tests); full suite: 1883/1883 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cdp): on-demand drain of the in-app network hook buffer Adds drainNetworkHookBuffer() (src/cdp/net-hook-drain.ts) — a fail-open swap-and-return drain of globalThis.__RN_AGENT_NET_BUF__ that applies entries via applyNetworkHookEntry into DeviceBufferManager; returns count of entries applied, never throws. Carry-over fixes from Task 5 review: - Hot-path guard in NETWORK_CB_BUFFERED_SCRIPT: try/catch + Array.isArray repair so a corrupted buffer can never break the user's fetch/XHR - Seam double-feed fix in setup.ts: RN_FORCE_NETWORK_HOOK seam now also calls Network.disable (best-effort) so CDP events don't double-feed the buffer - Post-seam log accuracy: seam moved above the logger.info line so the logged network mode reflects the post-seam value - Test-file carry-over: mid-file import moved to top alongside other imports 18/18 targeted tests pass; 1890/1890 full suite pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(cdp): drain hook-mode network buffer on demand in the network-reading tools Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cdp): cover network-body drain path, honest wait deadline, hook-mode granularity docs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: DevTools coexistence (autoConnect opt-out) + changeset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cdp): app-side timestamps for buffered hook entries; docs + status envelope polish - NETWORK_CB_BUFFERED_SCRIPT now pushes { t, d, ts: Date.now() } so each entry carries its fire-time epoch ms; drain path forwards ts as atMs to applyNetworkHookEntry, which uses new Date(atMs ?? Date.now()).toISOString() — fixes false-positive since-pinning in cdp_wait_for_network for batched drains (Fix 1, 8 new TDD tests in net-hook-buffered-transport.test.js) - docs: remove device_* from the passive-mode seat-reclaim example (they don't touch CDP); add known-limitation note for cdp_open_devtools proxy not auto-resuming in passive mode (Fix 2, maestro-interop.mdx) - status.ts: PICKER_BLOCKING failure envelope now includes autoConnect state consistent with the other two failure paths (Fix 3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7b61792 commit 58c4886

39 files changed

Lines changed: 2410 additions & 62 deletions

.changeset/debugger-seat-optout.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"rn-dev-agent-cdp": minor
3+
"rn-dev-agent-plugin": minor
4+
---
5+
6+
Debugger-seat coexistence with React Native DevTools + silent hook-mode network capture.
7+
8+
- New opt-out for background auto-reconnect: `RN_CDP_AUTOCONNECT=0` or `.rn-agent/config.json` `{ "cdp": { "autoConnect": false } }`. In passive mode the bridge yields the single RN debugger seat to the visual DevTools and reconnects only on explicit tool calls. Resolved mode is visible in `cdp_status``autoConnect` and `/doctor`.
9+
- Hook-mode network capture (RN < 0.83 fallback) no longer transports entries via `console.log("__RN_NET__:…")` — entries go to an in-app ring buffer drained on demand, so Metro logs and the user's DevTools console stay clean.

CLAUDE-MD-TEMPLATE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ Tool calls must follow this sequence to avoid race conditions:
493493
| `cdp_reload` reports `reconnected: false` | Wait 5-10s | New Hermes target not yet registered | `cdp_connect force: true`; if ambiguous target, pass `targetId:` |
494494
| `device_screenshot` captures the wrong platform || Multi-device routing bug | Pass `platform:` explicitly, or fall back to raw `adb screencap` / `simctl io` |
495495
| `cdp_interact accessibilityLabel="..."` fails (label matching is fuzzy) | Prefer testID-keyed calls: `cdp_interact(testID="...")` or `device_batch` with `testID=` field. Fall back to `device_snapshot` + `device_press(ref="@eN")` only when no testID exists. | Label matching unreliable; testID matching is exact and fiber-tree-resolved ||
496+
| "Disconnected due to opening a second DevTools window" / React Native DevTools keeps getting kicked | `cdp_status``autoConnect` field | RN allows one debugger frontend per app; bridge auto-reconnects by default (agent-first) | Set `RN_CDP_AUTOCONNECT=0` or `.rn-agent/config.json``{ "cdp": { "autoConnect": false } }`. Bridge then reconnects only on explicit tool calls and yields again when DevTools reopens. Note: **any** CDP tool call — including `cdp_status` — reclaims the seat while it runs; passive mode only stops *background* re-grabs. |
496497

497498
### Authentication & Permission Pre-flight
498499

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Development scaffolding lives in the **sibling workspace repo**:
6868
Authoring & lifecycle:
6969
```
7070
/rn-dev-agent:setup — Check & install all prerequisites; scaffolds .rn-agent/
71-
/rn-dev-agent:doctor — 14-row diagnostic table for the whole environment
71+
/rn-dev-agent:doctor — 15-row diagnostic table for the whole environment
7272
/rn-dev-agent:check-env — Quick environment-readiness check
7373
/rn-dev-agent:rn-feature-dev <desc> — Full 8-phase feature development pipeline
7474
/rn-dev-agent:test-feature <desc> — Test a feature end-to-end; auto-records an action on pass
@@ -105,6 +105,7 @@ Repo-local troubleshooting memory (replaces the Experience Engine):
105105
- **"rn-fast-runner did not become ready" / no `.xctestrun` at the expected path** → The runner self-builds on first use (cold `xcodebuild test`), so this now usually means the cold build itself timed out or failed — not a missing prebuild. The cold-build ready timeout is 360s; a slower machine or a build error (check Xcode/simulator state) can still trip it. To take the build out of the hot path, pre-build once with `xcodebuild build-for-testing` (see Prerequisites). The build artifacts live at `scripts/rn-fast-runner/build/DerivedData/`.
106106
- **Legacy `AgentDeviceRunner` re-appears on the simulator** → A stale `~/.agent-device/daemon.json` is respawning the upstream runner. Since #202 the plugin terminates stale `AgentDeviceRunner` processes at session-open by default (scoped to the target simulator UDID) and clears orphaned `~/.agent-device/daemon.{json,lock}`, so this should self-heal. If you've opted out via `RN_DEVICE_KILL_LEGACY=0`, either drop that override or clean up one-time: `pkill -f AgentDeviceRunner && rm -f ~/.agent-device/daemon.json ~/.agent-device/daemon.lock`.
107107
- **`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.)
108+
- **"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`.
108109
- **"No booted simulator"** → Open Simulator.app or boot one via Xcode
109110
- **iOS 26.x beta issues** → Use iOS 18 stable runtime (Xcode > Settings > Platforms)
110111
- **Node.js odd version (v25)** → Switch to Node 22 LTS: `nvm install 22 && nvm use 22`

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ An **action** is a saved Maestro flow the agent **emits** when `/test-feature` v
9595
| Command | Purpose |
9696
|---------|---------|
9797
| `/rn-dev-agent:setup` | Inject CLAUDE.md tool-routing rules + nav-ref + Zustand exposure |
98-
| `/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 |
98+
| `/rn-dev-agent:doctor` | 15-row diagnostic table — Node, CDP, rn-fast-runner (iOS), agent-device (Android), maestro-runner, simulators, Metro, helpers freshness, plugin version, CDP auto-reconnect mode |
9999
| `/rn-dev-agent:check-env` | Quick environment-readiness check |
100100
| `/rn-dev-agent:nav-graph` | Extract and inspect the app navigation graph |
101101
| `/rn-dev-agent:send-feedback` | Open a GitHub issue with sanitized environment context |

commands/doctor.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ description: Diagnose installation health. Check Node, CDP bridge, rn-fast-runne
44
argument-hint:
55
---
66

7-
Run the environment-diagnostic checklist from the `rn-setup` skill. Walk all 14 prerequisite checks (Node.js version, CDP bridge dependencies, **rn-fast-runner build (iOS)**, **agent-device CLI (Android)**, maestro-runner, iOS simulator, Android emulator, Metro dev server, CDP connection, **injected `__RN_AGENT` helpers**, ffmpeg, physical-device prerequisites, **plugin version freshness**, **Vercel rules sync freshness**) and surface install commands for any missing dependencies.
7+
Run the environment-diagnostic checklist from the `rn-setup` skill. Walk all 15 prerequisite checks (Node.js version, CDP bridge dependencies, **rn-fast-runner build (iOS)**, **agent-device CLI (Android)**, maestro-runner, iOS simulator, Android emulator, Metro dev server, CDP connection, **injected `__RN_AGENT` helpers**, ffmpeg, physical-device prerequisites, **plugin version freshness**, **Vercel rules sync freshness**, **CDP auto-reconnect mode**) and surface install commands for any missing dependencies.
88

99
iOS device automation is owned by the in-tree `rn-fast-runner` XCTest project (D1219, PR #164) — `agent-device` is Android-only after that change. Mark `agent-device` as N/A on iOS-only setups and `rn-fast-runner` as N/A on Android-only / non-macOS setups.
1010

1111
**This command is read-only.** It diagnoses the current environment and recommends fixes. It does NOT modify any files in the user's project, inject documentation, or instrument source code.
1212

13-
Present results as a 14-row table. For any RED rows, give the user the exact install command (with `nvm` / `sudo` / `brew` flag selection based on their environment). For the `rn-fast-runner` row: if NEEDS_BUILD, note that the runner self-builds on first use (a slow cold build) and OFFER to run the one-time `xcodebuild build-for-testing` now to move that cost out of the first interaction (the rn-setup skill section 3 has the exact form + the offer prompt). Building the runner writes only to plugin-internal `scripts/rn-fast-runner/build/` — it does not touch the user's project, so the offer is consistent with this command's read-only-on-your-project contract. For the helpers row specifically, do NOT suggest retrying `cdp_status` — the bridge already auto-retried injection. If MISSING, recommend `device_*` fallbacks or `cdp_reload`. For the **plugin version** row: if BEHIND, surface `/plugin update rn-dev-agent` and let the user decide whether to update before continuing. If OFFLINE (GitHub unreachable), skip without failing — plugin works fine without the upstream check. For the **Vercel rules sync** row: if STALE (>30 days since last sync) or MISSING (no `third_party/vercel-labs/agent-skills/UPSTREAM.lock.json`), surface the resync command — do NOT auto-run; the user runs `node ${CLAUDE_PLUGIN_ROOT}/scripts/sync-vercel-skills.mjs --ref <sha>` themselves.
13+
Present results as a 15-row table. For any RED rows, give the user the exact install command (with `nvm` / `sudo` / `brew` flag selection based on their environment). For the `rn-fast-runner` row: if NEEDS_BUILD, note that the runner self-builds on first use (a slow cold build) and OFFER to run the one-time `xcodebuild build-for-testing` now to move that cost out of the first interaction (the rn-setup skill section 3 has the exact form + the offer prompt). Building the runner writes only to plugin-internal `scripts/rn-fast-runner/build/` — it does not touch the user's project, so the offer is consistent with this command's read-only-on-your-project contract. For the helpers row specifically, do NOT suggest retrying `cdp_status` — the bridge already auto-retried injection. If MISSING, recommend `device_*` fallbacks or `cdp_reload`. For the **plugin version** row: if BEHIND, surface `/plugin update rn-dev-agent` and let the user decide whether to update before continuing. If OFFLINE (GitHub unreachable), skip without failing — plugin works fine without the upstream check. For the **Vercel rules sync** row: if STALE (>30 days since last sync) or MISSING (no `third_party/vercel-labs/agent-skills/UPSTREAM.lock.json`), surface the resync command — do NOT auto-run; the user runs `node ${CLAUDE_PLUGIN_ROOT}/scripts/sync-vercel-skills.mjs --ref <sha>` themselves. For the **CDP auto-reconnect** row: read `autoConnect` from `cdp_status` output; report `ON (default)`, `OFF (env)`, or `OFF (config)`. OFF is informational (YELLOW, not RED) with note "React Native DevTools can hold the debugger seat; the bridge reconnects only on tool calls".
1414

1515
If the user wants the plugin to also inject project instructions (CLAUDE.md template, nav-ref, store exposure) — point them at `/rn-dev-agent:setup` instead.

docs-site/src/content/docs/guides/maestro-interop.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,28 @@ A `maestro_run` / `maestro_test_all` flow (rn-dev-agent's own L3) takes the devi
2929
## Why there's no shared "WDA session" to ride (#210)
3030

3131
A natural question: since maestro drives the device fine via WebDriverAgent, can `device_*` "ride" that WDA session instead of rn-fast-runner? No — maestro-runner spawns WDA **per flow** and tears it down when the flow exits, so there's no persistent session to attach to, and a WDA client would add a *second* XCUITest backend (the opposite of unifying). The coherent model is: **rn-fast-runner is the `device_*` backend** (it auto-spawns when prebuilt — `cdp_status.deviceSession` shows its `alive`/`stale`/`dead` state), **maestro/WDA is the flow engine**, and they're serialized. Mid-flow, use simctl for pixels and CDP reads for state/tree.
32+
33+
## Using rn-dev-agent with React Native DevTools
34+
35+
React Native (Hermes) allows **exactly one debugger frontend** per app at a time. By default, rn-dev-agent is agent-first: its CDP bridge auto-reconnects in the background after each disconnect, which means opening the visual React Native DevTools window will kick the bridge out — and the bridge will immediately reclaim the seat.
36+
37+
**To let React Native DevTools hold the debugger seat**, disable background auto-reconnect with one of two opt-out surfaces:
38+
39+
- **Environment variable** (takes precedence over everything):
40+
```bash
41+
RN_CDP_AUTOCONNECT=0 # also accepts 'false'; '1'/'true' force it back on
42+
```
43+
- **Project config file** (`.rn-agent/config.json` in your project root, env wins):
44+
```json
45+
{ "cdp": { "autoConnect": false } }
46+
```
47+
48+
In passive mode the bridge **yields the seat** and reconnects only when a CDP tool actually runs (e.g. `cdp_status`, `cdp_component_tree`). Once you reopen React Native DevTools after a tool call, the bridge yields again automatically.
49+
50+
**Known limitation:** in passive mode, a previously-opened DevTools-sharing proxy (`cdp_open_devtools`) is not auto-resumed after a disconnect — re-run `cdp_open_devtools` if you use it.
51+
52+
**Important caveat:** *any* CDP tool call — including `cdp_status` — reclaims the seat while it runs. Passive mode stops only *background* re-grabs (the close-triggered reconnect loop and the background poll). If you need uninterrupted DevTools access, avoid running CDP tools while the window is open.
53+
54+
The resolved auto-connect mode is always visible in `cdp_status` → top-level `autoConnect` field: `{ enabled: true|false, source: 'default'|'env'|'config' }`. The `/rn-dev-agent:doctor` command also reports it as the **CDP auto-reconnect** row (YELLOW when OFF — informational, not an error).
55+
56+
**Hook-mode network capture note:** on React Native < 0.83 (or when `RN_FORCE_NETWORK_HOOK=1` is set), network entries are captured by in-app `fetch`/XHR wrappers. As of this release that capture **no longer writes `__RN_NET__:…` lines to the console** — entries go directly to an in-app ring buffer (`globalThis.__RN_AGENT_NET_BUF__`, capped at 100) that the network tools drain on demand. Metro logs and your React Native DevTools console will no longer contain hook-mode network noise. No user action is required; the change is transparent.

0 commit comments

Comments
 (0)