Skip to content

fix(core): correct the dev-client login path refusals and auth-route matching - #997

Open
Lykhoyda wants to merge 12 commits into
mainfrom
fm/rn-dev-agent-issue-993-fix
Open

fix(core): correct the dev-client login path refusals and auth-route matching#997
Lykhoyda wants to merge 12 commits into
mainfrom
fm/rn-dev-agent-issue-993-fix

Conversation

@Lykhoyda

@Lykhoyda Lykhoyda commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Intent

Captain (2026-09-07): "take next" on the architect -> dev -> QA workflow, then on the one open policy question: "A".

Substance of what those refer to. Two user-filed issues describe a login path that does not work:

An architect stage investigated both and its accepted report's verdict: these are FOUR INDEPENDENT defects that one QA journey happened to walk through in sequence, not a chain; only the LINK_FOREIGN one is pooled-worktree specific; and issue 990 overlaps issue 993 on exactly two of them, so both issues are one piece of work. The report's implementation plan, which the captain's "take next" dispatches, is: (1) in the linked-worktree inheritance planner, make the LINK_FOREIGN remediation depend on the regime — when the primary worktree has no corpus, say the only accepted target is the primary worktree's .rn-agent/actions, that it does not exist, and that the supported shapes are a real actions directory in this worktree or creating the corpus in the primary; when the primary has a corpus, keep the re-point wording but name the target — without changing the destination classifier, the allowlist, or the single corpus resolver; (2) in cdp_auto_login, replace leaf-only route matching with a walk over the nested route chain root-to-leaf (routeName at each level and params.screen where present), matching if any level matches the existing auth patterns, without adding "intro" or any app-specific name to the patterns, and make the negative reason carry the observed route chain; (3) in the replay compatibility preflight, evaluate the regex-selector fence before the enginePin header refusal (runtime pin still first) so an unpinned regex action gets the terminal reason instead of being told to run migrate-actions, which would refuse it; (4) when a flow contains a launchApp stage and the subsequent origin claim or completion fails with METRO_ORIGIN_MISMATCH, annotate the failure so the result names the flow's own launchApp (with clearState if present) relaunch as the cause and points to device_reset_state and the EG_DEV_CLIENT_CLEARSTATE rule, keeping the original code and axis — message and metadata only, no change to the probe, its order, or its result; (5) the pre-mutation clearState refusal decided below; (6) host-neutral skills and the rn-agent template README state the single accepted inheritance target and that login learned actions on a dev client must be warm (start attached, no relaunch) and use id or literal selectors. The report's boundary: no change to axis A or B probing, claimOrigin, relaunchSessionRuntime, connectExactSessionTarget, pinExactDevClient, or tool profiles; no relaxation of the runner pin, the exact-pin refusal, the engine-pin refusal, or the GH #750 regex fence; no foreign-corpus allow path, confirmation flag, or second corpus resolver; no app-specific names in the auth route patterns; no product-shipped login flow for any specific app; the project-side corpus rewrite belongs to the app's own lane, not to rn-dev-agent; the issue 990 secondary findings (one-launch-per-Metro bundle binding, native logs gated on axis B, cdp_connect relaunches, fingerprint cache persistence, the dev-client picker RECENTLY OPENED row, device_screenshot default path, the postflight envelope hint, any readiness-timeout change) are deliberately omitted. Ship as one PR, core source only, with a changeset bumping rn-dev-agent-plugin and rn-dev-agent-core; each of the four defects gets a test that fails on the current code and passes after the change.

The captain's answer "A" settles the one question the report escalated: managed dev-client replay through cdp_run_action and cdp_login_prologue must REFUSE flows containing clearState true BEFORE executing anything, fail-closed and side-effect-free, matching the position cdp_auto_login already takes. The captain accepted the stated consequence: flows that currently limp through will fail outright instead.

What Changed

  • cdp_run_action (and cdp_login_prologue through it) now refuses a flow containing clearState on a managed dev-client session with the new DEV_CLIENT_CLEARSTATE_REFUSED code, before the compat preflight and before any runner call, origin claim, or park; the refusal reads the signed buildKind === 'expo' install binding and decides on parsed commands via a single containsClearState predicate now shared with cdp_auto_login. The replay compat preflight also evaluates the regex-selector fence ahead of the enginePin header refusal, so an unpinned regex action gets the terminal reason instead of being pointed at migrate-actions.
  • cdp_auto_login walks the nested route chain root-to-leaf (routeName per level plus params.screen on unmounted navigators) instead of matching only the leaf, treating a level with a mounted child as a container that must match a whole pattern, and the negative reason now carries the observed chain.
  • maestro_run tracks flow-owned launchApp relaunches at runtime across partitioned iOS segments and the deferred completion; when a METRO_ORIGIN_MISMATCH follows an unclaimed relaunch, the error is re-issued with the same code, holder, and axis plus a cause message, details.nextAction, and meta.flowRelaunch. Linked-worktree LINK_FOREIGN remediation now names the single accepted target and whether it is missing or not a directory, and the skills, rn-agent template README, AGENTS.md, and docs site record the warm-login and dev-client constraints; a changeset patches rn-dev-agent-core and rn-dev-agent-plugin, with regression tests per defect.

Risk Assessment

✅ Low: All six intent items are implemented within the stated boundary with behavioral (not source-grepping) regression tests, the new refusal is fail-closed and pre-mutation, the relaunch attribution only decorates error messages while preserving code/holder/details/meta and never alters control flow, and the only surviving findings are an unavoidable consequence of a decision the user already made plus one dead export.

Testing

I ran the seven targeted test files covering the four defects plus the clearState refusal and the GH #705/auto-login neighbours (149/149 green), and proved they are genuine regressions by rebuilding from the base source, where five of them fail. Beyond unit tests I captured the actual end-user surfaces: the worktree-inheritance plan CLI output in all three source regimes, the cdp_auto_login result for the exact nav state reported in issue #993, the cdp_run_action and cdp_login_prologue JSON envelopes (with runner/origin-claim/relaunch counters proving the dev-client clearState refusal is side-effect-free), and the maestro_run METRO_ORIGIN_MISMATCH message and metadata after a flow-owned launchApp. The same script run against the base build gives a line-by-line before/after diff of that operator-visible text. This change ships no rendered UI — the product surface is an MCP tool envelope and a CLI — so the evidence is CLI transcripts and JSON envelopes rather than screenshots. The doc-only part of the intent (skills / rn-agent template README wording) is not independently exercisable and was not separately tested. One incidental issue: five unrelated migrateLearnedActions tests fail on a default macOS TMPDIR because their fixtures do not realpath the temp root; setting TMPDIR to its realpath makes the whole file pass. Worktree left clean; all evidence written to the run's evidence directory.

Evidence: Before/after summary of the five user-visible surfaces
# GH #993 / #990 login path — user-visible before/after

`defect-surfaces.mjs` drives the real public surfaces (the `worktree-inheritance plan`
CLI subprocess, `cdp_auto_login`, `cdp_run_action`, `cdp_login_prologue`, and the
`maestro_run` authority-stage executor) and prints exactly what the operator sees.
The same script was run against the base build (`before-fix-transcript.txt`) and the
branch build (`after-fix-transcript.txt`).

| # | Surface | Before | After |
|---|---------|--------|-------|
| 1 | `worktree-inheritance plan` on a pooled worktree with a foreign actions link | one generic line in all three regimes: "symlink to something else; setup can re-point it" | names the single accepted target `<primary worktree>/.rn-agent/actions` and its state (missing / not-a-directory / available) with a followable remedy |
| 2 | `cdp_auto_login` on the reported nav state `__root › auth › intro` | `"App is not on an auth screen"` while `cdp_navigation_state` showed the auth stack | detects the auth screen; negative answers now name the chain, e.g. `(route: AuthenticatedStack › Home)` |
| 3 | `cdp_run_action` on an unpinned action with regex text selectors | "Run … migrate-actions before replay" — a remedy that then refuses the action | terminal regex reason: "Rewrite as id or literal text selectors before replay" |
| 4 | `maestro_run` origin failure after the flow's own `launchApp {clearState: true}` | `METRO_ORIGIN_MISMATCH`, `nextAction: repair the named authority axis` | same code/axis, but the message and `meta.flowRelaunch` blame the flow's `launchApp` and point at `device_reset_state` / `EG_DEV_CLIENT_CLEARSTATE`. A warm `launchApp {stopApp:false}` is still unattributed. |
| 5 | `cdp_run_action` / `cdp_login_prologue` replaying a clearState flow on a managed dev client | ran the flow (`maestroRuns=2`) | `DEV_CLIENT_CLEARSTATE_REFUSED` with `maestroRuns=0 originClaims=0 relaunches=0`; bare React Native sessions and warm actions still run |

Reproduce: `TMPDIR=$(node -p "require('fs').realpathSync(require('os').tmpdir())") node defect-surfaces.mjs`
(the repo's action-corpus realpath checks reject macOS's `/var` → `/private/var` symlinked TMPDIR).
Evidence: Branch build: product transcript of all five surfaces

==============================================================================
DEFECT 1 - pooled worktree, foreign actions link (cdp corpus LINK_FOREIGN)
==============================================================================

$ rn-dev-agent worktree-inheritance plan   [primary has NO corpus]
------------------------------------------------------------------
.rn-agent/actions: LINK_FOREIGN — Destination is a symlink, but the only accepted target <primary worktree>/.rn-agent/actions does not exist, so there is nothing to re-point it to. Supported shapes: replace the link with a real actions directory in this worktree, or create the corpus at <primary worktree>/.rn-agent/actions and re-run /rn-dev-agent:setup.

$ rn-dev-agent worktree-inheritance plan   [primary path occupied by a file]
----------------------------------------------------------------------------
.rn-agent/actions: LINK_FOREIGN — Destination is a symlink, but the only accepted target <primary worktree>/.rn-agent/actions exists but is not a directory, so there is nothing to re-point it to. Supported shapes: replace the link with a real actions directory in this worktree, or replace <primary worktree>/.rn-agent/actions with a real actions directory and re-run /rn-dev-agent:setup.

$ rn-dev-agent worktree-inheritance plan   [primary owns a corpus]
------------------------------------------------------------------
.rn-agent/actions: LINK_FOREIGN — Destination is a symlink to something other than the only accepted target <primary worktree>/.rn-agent/actions; /rn-dev-agent:setup can re-point it there after explicit confirmation.

==============================================================================
DEFECT 2 - cdp_auto_login reads the nested route chain
==============================================================================

cdp_navigation_state: {"routeName":"__root","nested":{"routeName":"auth","params":{"screen":"intro"},"nested":{"routeName":"intro"}}}
-------------------------------------------------------------------------------------------------------------------------------------
route chain compared : __root › auth › intro
on an auth screen    : true
cdp_auto_login says  : {
  "loggedIn": false,
  "reason": "App is on an auth screen but no explicitly authorized legacy login subflow exists. Recovery cannot proceed; use a compatible owned learned action for durable authentication or proof."
}
(reported #993 state  __root > auth(params.screen=intro) > intro)

cdp_navigation_state: {"routeName":"AuthStack","params":{"screen":"Intro"}}
---------------------------------------------------------------------------
route chain compared : AuthStack › Intro
on an auth screen    : true
cdp_auto_login says  : {
  "loggedIn": false,
  "reason": "App is on an auth screen but no explicitly authorized legacy login subflow exists. Recovery cannot proceed; use a compatible owned learned action for durable authentication or proof."
}
(nested navigator whose child has not mounted  AuthStack + params.screen)

cdp_navigation_state: {"routeName":"AuthenticatedStack","nested":{"routeName":"Home"}}
--------------------------------------------------------------------------------------
route chain compared : AuthenticatedStack › Home
on an auth screen    : false
cdp_auto_login says  : {
  "loggedIn": false,
  "reason": "App is not on an auth screen (route: AuthenticatedStack › Home)"
}
(logged-in app under an Authenticated navigator)

==============================================================================
DEFECT 3 - unpinned regex action gets the terminal reason, not the dead-end remedy
==============================================================================

cdp_run_action { actionId: "user-login" }
-----------------------------------------
{
  "ok": false,
  "error": "Action uses regex text selectors (.*Getsafe.*http://.*) which are not a validated maestro-runner 1.1.24 capability (GH #750 CONTAINS mistranslation). Rewrite as id or literal text selectors before replay. No UI mutation will run.",
  "code": "ENGINE_PIN_MISMATCH",
  "meta": {
    "actionId": "user-login",
    "fallback": "none",
    "pin": {
      "pinned": "1.1.24",
      "status": "pinned-ok"
    },
    "selectedPath": "/test/pin-cache/1.1.24/bin/maestro-runner",
    "provenance": "pin-cache",
    "writes": {
      "actionYaml": {
        "written": false,
        "reason": "repair-not-applied"
      },
      "runtimeState": "sidecar",
      "runtimeStatePath": "/private/var/folders/wy/khrzvmhd0ss969ydn32ghccm0000gn/T/rn-agent-test-kxwVpx/.rn-agent/state/user-login.state.json",
      "databaseMirror": "best-effort"
    }
  }
}

runner invocations: 0

==============================================================================
DEFECT 5 - managed dev-client replay refuses a clearState flow before touching anything
==============================================================================

cdp_run_action { actionId: "user-login" }  [dev client]
-------------------------------------------------------
{
  "ok": false,
  "error": "Refusing to replay a flow containing clearState on a managed dev-client session. The clearState relaunch uninstalls the app and strands the dev client at its picker, so the relaunched app cannot re-attach to the authority-bound Metro (EG_DEV_CLIENT_CLEARSTATE). No runner was invoked and the app was not touched. Remove launchApp clearState from the action so it starts from the attached app; when a state reset is needed, run device_reset_state before cdp_run_action or cdp_login_prologue.",
  "code": "DEV_CLIENT_CLEARSTATE_REFUSED",
  "meta": {
    "actionId": "user-login",
    "fallback": "none",
    "launchShape": "dev-client",
    "nextAction": "Rewrite the action without launchApp clearState (start from the attached app) and, if a reset is needed, run device_reset_state first.",
    "writes": {
      "actionYaml": {
        "written": false,
        "reason": "repair-not-applied"
      },
      "runtimeState": "sidecar",
      "runtimeStatePath": "/private/var/folders/wy/khrzvmhd0ss969ydn32ghccm0000gn/T/rn-agent-test-v8R9LX/.rn-agent/state/user-login.state.json",
      "databaseMirror": "best-effort"
    }
  }
}

cdp_login_prologue { }  [same session, inherits the refusal]
------------------------------------------------------------
{
  "ok": false,
  "error": "Refusing to replay a flow containing clearState on a managed dev-client session. The clearState relaunch uninstalls the app and strands the dev client at its picker, so the relaunched app cannot re-attach to the authority-bound Metro (EG_DEV_CLIENT_CLEARSTATE). No runner was invoked and the app was not touched. Remove launchApp clearState from the action so it starts from the attached app; when a state reset is needed, run device_reset_state before cdp_run_action or cdp_login_prologue.",
  "code": "DEV_CLIENT_CLEARSTATE_REFUSED",
  "meta": {
    "actionId": "user-login",
    "fallback": "none",
    "launchShape": "dev-client",
    "nextAction": "Rewrite the action without launchApp clearState (start from the attached app) and, if a reset is needed, run device_reset_state first."
  }
}

side effects: maestroRuns=0 originClaims=0 relaunches=0

cdp_run_action, same flow on a BARE React Native session (GH #705 behaviour kept)
---------------------------------------------------------------------------------
ok=true code=- maestroRuns=1

cdp_run_action, WARM action on the same dev-client session (unaffected)
-----------------------------------------------------------------------
ok=true code=- maestroRuns=1

==============================================================================
DEFECT 4 - origin failure after a flow-owned launchApp names the flow, not the axis
==============================================================================

maestro_run failure envelope for a flow whose own launchApp {clearState: true} preceded it
------------------------------------------------------------------------------------------
code   : METRO_ORIGIN_MISMATCH
message: METRO_ORIGIN_MISMATCH: the claimed device app is not attached to the authority-bound Metro The flow's own launchApp (clearState: true) relaunched the app and it did not re-register on the authority-bound Metro within the readiness window; the axis is reporting that relaunch, not a broken binding. Do not relaunch a dev-client app from inside a learned action (EG_DEV_CLIENT_CLEARSTATE): start the action from the attached app, and reset state with device_reset_state before cdp_run_action or cdp_login_prologue instead of launchApp clearState.
meta   : {
  "flowRelaunch": {
    "command": "launchApp",
    "clearState": true,
    "stopApp": true
  },
  "axis": "A",
  "nextAction": "Do not relaunch a dev-client app from inside a learned action (EG_DEV_CLIENT_CLEARSTATE): start the action from the attached app, and reset state with device_reset_state before cdp_run_action or cdp_login_prologue instead of launchApp clearState."
}

same failure after a WARM launchApp {stopApp: false} - nothing was relaunched, so no blame
------------------------------------------------------------------------------------------
code       : METRO_ORIGIN_MISMATCH
message    : METRO_ORIGIN_MISMATCH: the claimed device app is not attached to the authority-bound Metro
flowRelaunch attributed: false
nextAction : Run rn_session with action "status" and repair the named authority axis.

[transcript complete]
(node:7294) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Evidence: Base build: same script, same fixtures (the reported failures)

==============================================================================
DEFECT 1 - pooled worktree, foreign actions link (cdp corpus LINK_FOREIGN)
==============================================================================

$ rn-dev-agent worktree-inheritance plan   [primary has NO corpus]
------------------------------------------------------------------
.rn-agent/actions: LINK_FOREIGN — Destination is a symlink to something else; /rn-dev-agent:setup can re-point it after explicit confirmation.

$ rn-dev-agent worktree-inheritance plan   [primary path occupied by a file]
----------------------------------------------------------------------------
.rn-agent/actions: LINK_FOREIGN — Destination is a symlink to something else; /rn-dev-agent:setup can re-point it after explicit confirmation.

$ rn-dev-agent worktree-inheritance plan   [primary owns a corpus]
------------------------------------------------------------------
.rn-agent/actions: LINK_FOREIGN — Destination is a symlink to something else; /rn-dev-agent:setup can re-point it after explicit confirmation.

==============================================================================
DEFECT 2 - cdp_auto_login reads the nested route chain
==============================================================================

cdp_navigation_state: {"routeName":"__root","nested":{"routeName":"auth","params":{"screen":"intro"},"nested":{"routeName":"intro"}}}
-------------------------------------------------------------------------------------------------------------------------------------
route chain compared : (not exported at this commit)
on an auth screen    : false
cdp_auto_login says  : {
  "loggedIn": false,
  "reason": "App is not on an auth screen"
}
(reported #993 state  __root > auth(params.screen=intro) > intro)

cdp_navigation_state: {"routeName":"AuthStack","params":{"screen":"Intro"}}
---------------------------------------------------------------------------
route chain compared : (not exported at this commit)
on an auth screen    : true
cdp_auto_login says  : {
  "loggedIn": false,
  "reason": "App is on an auth screen but no explicitly authorized legacy login subflow exists. Recovery cannot proceed; use a compatible owned learned action for durable authentication or proof."
}
(nested navigator whose child has not mounted  AuthStack + params.screen)

cdp_navigation_state: {"routeName":"AuthenticatedStack","nested":{"routeName":"Home"}}
--------------------------------------------------------------------------------------
route chain compared : (not exported at this commit)
on an auth screen    : false
cdp_auto_login says  : {
  "loggedIn": false,
  "reason": "App is not on an auth screen"
}
(logged-in app under an Authenticated navigator)

==============================================================================
DEFECT 3 - unpinned regex action gets the terminal reason, not the dead-end remedy
==============================================================================

cdp_run_action { actionId: "user-login" }
-----------------------------------------
{
  "ok": false,
  "error": "Action is not migrated to maestro-runner@1.1.24 or newer. Run node <plugin-root>/rn-dev-agent-core/dist/maestro-runner-pin.js migrate-actions --root <app> before replay. Incompatible actions are terminal — no manual fallback.",
  "code": "ENGINE_PIN_MISMATCH",
  "meta": {
    "actionId": "user-login",
    "fallback": "none",
    "pin": {
      "pinned": "1.1.24",
      "status": "pinned-ok"
    },
    "selectedPath": "/test/pin-cache/1.1.24/bin/maestro-runner",
    "provenance": "pin-cache",
    "writes": {
      "actionYaml": {
        "written": false,
        "reason": "repair-not-applied"
      },
      "runtimeState": "sidecar",
      "runtimeStatePath": "/private/var/folders/wy/khrzvmhd0ss969ydn32ghccm0000gn/T/rn-agent-test-looTRS/.rn-agent/state/user-login.state.json",
      "databaseMirror": "best-effort"
    }
  }
}

runner invocations: 0

==============================================================================
DEFECT 5 - managed dev-client replay refuses a clearState flow before touching anything
==============================================================================

cdp_run_action { actionId: "user-login" }  [dev client]
-------------------------------------------------------
{
  "ok": true,
  "data": {
    "passed": true,
    "actionId": "user-login",
    "transport": "unproven",
    "transportVersion": null,
    "fallback": "unproven",
    "proofDomain": "xctest-native",
    "perStepReadback": {
      "source": "maestro-runner-step-report",
      "complete": false,
      "steps": []
    },
    "repair": {
      "attempted": false,
      "outcome": "skipped",
      "phases": {
        "firstAttemptMs": 1
      }
    },
    "autoRepair": {
      "attempted": false,
      "outcome": "skipped",
      "phases": {
        "firstAttemptMs": 1
      }
    },
    "writes": {
      "actionYaml": {
        "written": false,
        "reason": "repair-not-applied"
      },
      "runtimeState": "sidecar",
      "runtimeStatePath": "/private/var/folders/wy/khrzvmhd0ss969ydn32ghccm0000gn/T/rn-agent-test-j77hSB/.rn-agent/state/user-login.state.json",
      "databaseMirror": "best-effort"
    },
    "durationMs": 209,
    "flowFile": "/private/var/folders/wy/khrzvmhd0ss969ydn32ghccm0000gn/T/rn-agent-test-j77hSB/.rn-agent/actions/user-login.yaml",
    "firstAttemptOutput": "Flow passed"
  }
}

cdp_login_prologue { }  [same session, inherits the refusal]
------------------------------------------------------------
{
  "ok": true,
  "data": {
    "schemaVersion": 1,
    "role": "ACTION_LOGIN_HELPER",
    "alias": "user-login",
    "actionId": "user-login",
    "startedAt": "2026-09-07T22:41:11.316Z",
    "endedAt": "2026-09-07T22:41:12.417Z",
    "elapsedMs": 1101,
    "steps": [
      {
        "name": "inventory",
        "startedAt": "2026-09-07T22:41:11.316Z",
        "endedAt": "2026-09-07T22:41:11.520Z",
        "elapsedMs": 204
      },
      {
        "name": "resolve",
        "startedAt": "2026-09-07T22:41:11.520Z",
        "endedAt": "2026-09-07T22:41:11.742Z",
        "elapsedMs": 222
      },
      {
        "name": "replay",
        "startedAt": "2026-09-07T22:41:11.742Z",
        "endedAt": "2026-09-07T22:41:12.194Z",
        "elapsedMs": 452
      },
      {
        "name": "verify-run-record",
        "startedAt": "2026-09-07T22:41:12.194Z",
        "endedAt": "2026-09-07T22:41:12.417Z",
        "elapsedMs": 223
      }
    ],
    "inventory": {
      "count": 1,
      "actionIds": [
        "user-login"
      ]
    },
    "runRecord": {
      "timestamp": "2026-09-07T22:41:11.962Z",
      "durationMs": 0,
      "status": "pass",
      "trigger": "agent",
      "autoRepair": {
        "attempted": false,
        "outcome": "skipped",
        "phases": {
          "firstAttemptMs": 0
        }
      },
      "runId": "46a722bc-adef-4a6d-9db6-55a9e3ddfd79",
      "timing": {
        "startedAt": "2026-09-07T22:41:11.962Z",
        "endedAt": "2026-09-07T22:41:11.962Z",
        "elapsedMs": 0,
        "steps": [
          {
            "name": "maestro-first-attempt",
            "startedAt": "2026-09-07T22:41:11.962Z",
            "endedAt": "2026-09-07T22:41:11.962Z",
            "elapsedMs": 0
          }
        ]
      },
      "deviceId": "5C10B45B-2065-458B-B885-0F83F49747C8"
    },
    "writes": {
      "actionYaml": {
        "written": false,
        "reason": "repair-not-applied"
      },
      "runtimeState": "sidecar",
      "runtimeStatePath": "/private/var/folders/wy/khrzvmhd0ss969ydn32ghccm0000gn/T/rn-agent-test-j77hSB/.rn-agent/state/user-login.state.json",
      "databaseMirror": "best-effort"
    },
    "actionResult": {
      "transport": "unproven",
      "transportVersion": null,
      "fallback": "unproven",
      "perStepReadback": {
        "source": "maestro-runner-step-report",
        "complete": false,
        "steps": []
      }
    }
  }
}

side effects: maestroRuns=2 originClaims=0 relaunches=0

cdp_run_action, same flow on a BARE React Native session (GH #705 behaviour kept)
---------------------------------------------------------------------------------
ok=true code=- maestroRuns=1

cdp_run_action, WARM action on the same dev-client session (unaffected)
-----------------------------------------------------------------------
ok=true code=- maestroRuns=1

==============================================================================
DEFECT 4 - origin failure after a flow-owned launchApp names the flow, not the axis
==============================================================================

maestro_run failure envelope for a flow whose own launchApp {clearState: true} preceded it
------------------------------------------------------------------------------------------
code   : METRO_ORIGIN_MISMATCH
message: METRO_ORIGIN_MISMATCH: the claimed device app is not attached to the authority-bound Metro
meta   : {
  "axis": "A",
  "nextAction": "Run rn_session with action \"status\" and repair the named authority axis."
}

same failure after a WARM launchApp {stopApp: false} - nothing was relaunched, so no blame
------------------------------------------------------------------------------------------
code       : METRO_ORIGIN_MISMATCH
message    : METRO_ORIGIN_MISMATCH: the claimed device app is not attached to the authority-bound Metro
flowRelaunch attributed: false
nextAction : Run rn_session with action "status" and repair the named authority axis.

[transcript complete]
(node:62534) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
  • Evidence: Evidence driver (real CLI subprocess + tool handlers, no assertions) (local file: ~/.no-mistakes/evidence/01M1YNGNSR3E9QZVV2BR8JXYZG/defect-surfaces.mjs)
Evidence: Key before → after deltas
D1 worktree-inheritance plan (pooled worktree, foreign actions link)
before: .rn-agent/actions: LINK_FOREIGN - Destination is a symlink to something else; /rn-dev-agent:setup can re-point it after explicit confirmation. [identical in all 3 regimes]
after : .rn-agent/actions: LINK_FOREIGN - Destination is a symlink, but the only accepted target <primary worktree>/.rn-agent/actions does not exist, so there is nothing to re-point it to. Supported shapes: replace the link with a real actions directory in this worktree, or create the corpus at <primary worktree>/.rn-agent/actions and re-run /rn-dev-agent:setup.

D2 cdp_auto_login, nav state __root > auth(params.screen=intro) > intro
before: on an auth screen: false reason: "App is not on an auth screen"
after : on an auth screen: true (chain compared: __root > auth > intro)
logged-in AuthenticatedStack > Home still false, reason now: "App is not on an auth screen (route: AuthenticatedStack > Home)"

D3 cdp_run_action, unpinned action with regex text selectors
before: "Action is not migrated to maestro-runner@1.1.24 or newer. Run ... migrate-actions --root <app> before replay."
after : "Action uses regex text selectors (.*Getsafe.*http://.*) ... Rewrite as id or literal text selectors before replay. No UI mutation will run."

D4 maestro_run origin failure after the flow's own launchApp {clearState: true}
before: nextAction: "Run rn_session with action status and repair the named authority axis."
after : code METRO_ORIGIN_MISMATCH / axis A unchanged; message adds "The flow's own launchApp (clearState: true) relaunched the app and it did not re-register ...";
meta.flowRelaunch {command: launchApp, clearState: true, stopApp: true}; nextAction points at device_reset_state / EG_DEV_CLIENT_CLEARSTATE.
Warm launchApp {stopApp:false}: flowRelaunch attributed = false, axis remedy retained.

D5 cdp_run_action + cdp_login_prologue, clearState flow on a managed dev client
before: side effects: maestroRuns=2 originClaims=0 relaunches=0 (the flow ran)
after : code DEV_CLIENT_CLEARSTATE_REFUSED, side effects: maestroRuns=0 originClaims=0 relaunches=0
bare React Native session: ok=true maestroRuns=1 warm action on dev client: ok=true maestroRuns=1
- Outcome: ⚠️ 1 info across 1 run (17m5s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ packages/rn-dev-agent-core/src/tools/run-action.ts:166 - flowCommandsClearState only matches the bare string command &#39;clearState&#39; or a nested key whose value is exactly true, so the documented Maestro spelling - clearState: &lt;appId&gt; slips through the new fail-closed refusal. Concrete path: an action containing - launchApp: {stopApp: false} / - clearState: com.test.app parses cleanly (clearState is in ALLOWED_COMMANDS at maestro-validator.ts:107, and validateValue accepts the scalar), so preflightCommands holds {clearState: &#39;com.test.app&#39;}; optionsClearState sees key clearState but nested !== true, then recurses into the string and returns false. On a buildKind: &#39;expo&#39; session the refusal at run-action.ts:719 does not fire, the flow runs, the dev client's stored Metro URL is wiped, and the caller gets the METRO_ORIGIN_MISMATCH that Option A exists to prevent — the exact bug: no working login path on a pooled worktree - corpus LINK_FOREIGN, prologue tears down axis A, auto-login misreads auth route, warm action engine-incompatible #993 symptom, fail-open. cdp_auto_login's pre-existing containsClearState (auto-login.ts:172) already catches this spelling, so the two owners now disagree, and AGENTS.md claims the refusal covers "a bare clearState". Smallest remedy inside the existing helper: treat a command-root clearState key with any value as clearing state, while keeping the nested === true rule so launchApp: {clearState: false} stays allowed.
  • ℹ️ .changeset/login-path-corrections.md:6 - The changeset body is four sentences covering all four defects. The standing convention for changesets is a single-sentence summary, because the body is published verbatim into the package CHANGELOG for external consumers; the per-defect detail belongs in the PR description. Recommend collapsing to one sentence (e.g. the dev-client clearState refusal plus the relaunch attribution) and moving the rest to the PR body. Flagged as ask-user because it is the author's published release note, not a mechanical defect.

🔧 Fix: dedupe clearState predicate, close appId-spelling bypass
2 warnings still open:

  • ⚠️ packages/rn-dev-agent-core/src/domain/maestro-validator.ts:141 - containsClearState matches an object key named clearState regardless of its value, so - launchApp:\n clearState: false — a valid, explicit Maestro spelling that clears nothing — is treated as a clearState flow. Concrete path: on an EXPO_INSTALL dev-client session, run-action.ts:703 sees containsClearState(preflightCommands) === true and returns DEV_CLIENT_CLEARSTATE_REFUSED with a message asserting "The clearState relaunch uninstalls the app and strands the dev client at its picker", which is false for that flow; the action becomes unreplayable until the author deletes a key that was already safe. The same PR uses two narrower spellings of the same concept — flowRelaunchFacts requires launch.clearState === true (maestro-run.ts:307) and the GH bug: launchApp clearState reinstall breaks strict install identity - every corpus auth flow unrunnable #705 helper requires /clearState:\s*true\b/ (resolve-ios-app-file.ts:14) — so the three owners now disagree on clearState: false. This is a side effect of the round-1 instruction to widen the predicate to catch - clearState: &lt;appId&gt;; that widening is still needed for the bare-command spelling. Narrower form that keeps the appId fix: key === &#39;clearState&#39; &amp;&amp; nested !== false. Flagged ask-user because the broad form was the author's explicit instruction and because narrowing it also relaxes cdp_auto_login, which shares the predicate.
  • ⚠️ packages/rn-dev-agent-core/src/tools/run-action.ts:150 - Simplification: isDevClientLaunchShape accepts two spellings of "this is a dev client" — buildKind === &#39;expo&#39; and typeof devClientUrl === &#39;string&#39;. The intent requires the refusal for "managed dev-client replay"; every Expo dev-client session carries buildKind: &#39;expo&#39; (set at prepare-build, rn-session.ts:787/860, and preserved by reissueInstallBinding), so the first spelling alone satisfies the requirement. The second widens the refusal to a bare-react-native session that merely declared a devClientUrl (the session schema accepts one independently, index.ts:1467) — a case no intent requirement names, and the only case the devClientUrl branch adds. It is fail-closed, so the risk is a false refusal rather than a bypass. Recommended remedy is removing the devClientUrl disjunct and keeping the exact buildKind === &#39;expo&#39; test (and dropping the corresponding assertion in bug: no working login path on a pooled worktree - corpus LINK_FOREIGN, prologue tears down axis A, auto-login misreads auth route, warm action engine-incompatible #993-dev-client-clearstate-refusal.test.ts), not hardening it.

🔧 Fix: narrow clearState predicate and dev-client launch shape
2 infos still open:

  • ℹ️ packages/rn-dev-agent-core/src/domain/maestro-validator.ts:138 - containsClearState returns true for the bare string &#39;clearState&#39; in ANY position, not just as a command. Because the walk recurses into every object value, - tapOn:\n id: &#34;clearState&#34; reaches containsClearState(&#39;clearState&#39;) via the id value and matches. Concrete path: on an EXPO_INSTALL dev-client session, a learned action that taps a debug element whose testID is literally clearState hits run-action.ts:702 and returns DEV_CLIENT_CLEARSTATE_REFUSED with a message asserting "The clearState relaunch uninstalls the app and strands the dev client at its picker" — for a flow that contains no clearState command at all, and the action becomes unreplayable. This is the third over-match found in the same predicate (after clearState: &lt;appId&gt; and clearState: false); the string branch exists only to catch the bare - clearState command form, which the validator does accept (ALLOWED_COMMANDS, maestro-validator.ts:180) and which always appears as an ARRAY element. Narrower form that keeps every currently-required match: test === &#39;clearState&#39; only for array elements (value.some((c) =&gt; c === &#39;clearState&#39; || containsClearState(c))) and drop the top-level string check. Flagged ask-user because the predicate is shared with cdp_auto_login, so narrowing it also relaxes that tool's legacy .maestro/ refusal — the same coupling the author decided on explicitly in the previous round.
  • ℹ️ packages/rn-dev-agent-core/src/tools/run-action.ts:125 - RunActionInstallReceipt.devClientUrl is declared and documented ("Present when the managed launch opens the app through a dev-client URL") but never read anywhere in the file — it is leftover from the typeof devClientUrl === &#39;string&#39; disjunct that the previous fix round removed from isDevClientLaunchShape (now exactly buildKind === &#39;expo&#39;). Structural typing means no call site depends on it either. Dead declaration; remove the field and its comment. (platform, deviceId, appId, buildKind are all genuinely read.)

🔧 Fix: make clearState match positional, drop dead receipt field
2 infos still open:

  • ℹ️ packages/rn-dev-agent-core/src/tools/auto-login.ts:126 - isAuthRouteChain matches AUTH_ROUTE_PATTERNS against every level of the chain, and the same predicate is reused as the post-login exit condition at auto-login.ts:373 (stillOnAuth = await isOnAuthScreen(client)). Because the patterns are substring matches, an ancestor navigator whose name merely contains one (e.g. an Authenticated / AuthenticatedStack root that stays mounted after login) now makes a logged-in app read as on-auth at both ends: handleAutoLogin proceeds to replay the login flow, and after a successful replay the 5s loop reports loggedIn: false, &#34;still on an auth screen&#34;. Before the change only the leaf was compared, so an ancestor name could not do this. Both outcomes fail loudly (the login flow's assertVisible steps fail; the verification returns a failure), so nothing is silently wrong. Flagged only because the intent mandates the widening — "matching if any level matches the existing auth patterns" and "no app-specific names in the auth route patterns" — so narrowing (e.g. keeping the leaf-only test for the post-replay exit check, or requiring an exact pattern match rather than substring) is the author's product call, not a repair. No change required if the author accepts the tradeoff.
  • ℹ️ packages/rn-dev-agent-core/src/tools/maestro-run.ts:296 - flowRelaunchFacts (line 296), relaunchesApp (line 320) and attributeOriginFailureToFlowRelaunch (line 365) are exported but consumed only inside maestro-run.ts — no other src module and no test imports them (tests import createFlowRelaunchTracker and FLOW_RELAUNCH_NEXT_ACTION only). They widen the module's public surface with no requirement. Drop the export on those three functions. FlowRelaunchFacts and FlowRelaunchTracker must stay exported (they appear in the exported createFlowRelaunchTracker return type and MaestroRunArgs.flowRelaunches).

🔧 Fix: match auth ancestors exactly, unexport relaunch helpers
2 warnings still open:

  • ⚠️ packages/rn-dev-agent-core/src/tools/auto-login.ts:105 - Appending params.screen to the chain (line 91) moves the routeName that carried it out of the leaf position, where isAuthRouteChain (line 105) then applies exact-segment matching instead of substring matching. Concrete state, exactly the window params.screen exists to serve (a nested navigator that has not yet mounted its child, so nested is absent): {routeName:&#39;AuthStack&#39;, params:{screen:&#39;Intro&#39;}} -> chain [&#39;AuthStack&#39;,&#39;Intro&#39;] -> leaf intro matches nothing, ancestor authstack is not exactly any AUTH_ROUTE_PATTERN -> isAuthRouteChain false. Before this branch, getDeepestRouteName returned AuthStack and matchesAuthPattern substring-matched auth -> true. Same for onboardingStack/loginStack/SignUpStack + params.screen. handleAutoLogin then returns {loggedIn:false, reason:&#39;App is not on an auth screen (route: AuthStack › Intro)&#39;}, which autoLoginToolResult (line 401) wraps in okResult, so the caller proceeds as if no login were needed while the app sits on the login screen - a wrong result with no error, in the exact defect (2) this change fixes. This is a false negative, materially different from the Authenticated false positive settled in the previous round, and the exact-ancestor rule itself is the recorded decision, so the narrowing is the author's call: substring-match the deepest observed routeName and any params.screen appended after it (leaf-equivalent), reserving exact matching for levels that have a mounted child below them. Empty chain and the __root › auth › intro case stay as they are.
  • ⚠️ packages/rn-dev-agent-core/src/tools/maestro-run.ts:476 - Simplification: throw relaunches.attribute(error) in the relaunchManagedApp catch is a third attribution site beyond the intent, which scopes the annotation to 'the subsequent origin claim or completion fails with METRO_ORIGIN_MISMATCH' - the claim site (line 460) and the completion site (line 498) already satisfy that. It is also unreachable through the wired gate: the real 'relaunched but did not re-register' failure comes out of connectExactSessionTarget (src/session/connect-exact-session-target.ts:221-465) as a plain Error, which attribute leaves untouched and which the GH bug: maestro_run aborts a passing flow at mid-flow launchApp - re-register check kills relaunch-in-flow patterns #708 deferral path already owns; and authority.relaunch (src/session/authority-gate.ts:2007-2024) raises a METRO_ORIGIN_MISMATCH SessionAuthorityError only when dependencies.relaunchBoundRuntime is unwired, which production wires at src/index.ts:626 (the runtime's own unavailable codes are AUTHORITY_STORE_UNAVAILABLE / SESSION_NOT_INITIALIZED / SESSION_OWNER_LOST, never METRO_ORIGIN_MISMATCH). Were it reachable, it would assert a runtime fact that did not happen - 'relaunched the app and it did not re-register on the authority-bound Metro' for a relaunch the gate refused before performing - and would replace the correct axis-repair nextAction. Remedy is removal, not hardening: restore throw error at that catch and keep attribution on the claim and completion sites only. Test GH#993 D2.e in test/unit/bug: maestro_run aborts a passing flow at mid-flow launchApp - re-register check kills relaunch-in-flow patterns #708-mid-flow-relaunch.test.ts asserts this branch and would need to drop the attribution assertion (its GH bug: maestro_run aborts a passing flow at mid-flow launchApp - re-register check kills relaunch-in-flow patterns #708 abort-semantics assertions stand).

🔧 Fix: judge unmounted navigators as screens, drop relaunch attribution
2 infos still open:

  • ℹ️ packages/rn-dev-agent-core/src/tools/auto-login.ts:115 - Accepted tradeoff, no action needed. The round-5 rule judges any level with no mounted child by substring (matchesAuthPattern), so {routeName:&#39;AuthenticatedStack&#39;, params:{screen:&#39;Home&#39;}} with no nested (the window right after login, before the authenticated navigator's state lands on the parent route) matches auth and reads as on-auth; under the round-4 rule that state was an ancestor and read false. This is not fixable in a narrower form: the recorded decision requires {routeName:&#39;AuthStack&#39;, params:{screen:&#39;Intro&#39;}} to be on-auth, and AuthStack / AuthenticatedStack are indistinguishable under substring matching, so any rule satisfying the required case admits this one. The mounted-child cases the decision enumerated (AuthenticatedStack › Home, Root(params.screen=Login) › Home) are correctly false, and the reported __root › auth › intro is correctly true. Recording it so the boundary is explicit rather than rediscovered later.
  • ℹ️ packages/rn-dev-agent-core/src/tools/run-action.ts:150 - DEV_CLIENT_CLEARSTATE_REFUSAL is exported but consumed only at its single call site inside run-action.ts (line 701) — no other src module and no test imports it (the refusal tests assert on the returned envelope's error text via regex, and import only isDevClientLaunchShape). It widens the module's public surface with no requirement, the same class of finding the round-4 fix applied to flowRelaunchFacts / relaunchesApp / attributeOriginFailureToFlowRelaunch in maestro-run.ts. Drop the export keyword; keep RunActionInstallReceipt and isDevClientLaunchShape exported (the former appears in the exported RunActionDeps.installReceipt return type, the latter is exercised directly by a test).
⚠️ **Test** - 1 info
  • ℹ️ packages/rn-dev-agent-core/test/unit/action-engine-compat.test.ts:564 - Pre-existing, unrelated to this change: five migrateLearnedActions tests in packages/rn-dev-agent-core/test/unit/action-engine-compat.test.ts fail on a default macOS TMPDIR because their fixtures use mkdtempSync(join(tmpdir(), ...)) while the corpus guards compare realpathSync paths (/var/folders/... vs /private/var/folders/...). They pass with TMPDIR set to the realpath. Wrapping the fixture roots in realpathSync(...), as the worktree-inheritance fixtures already do, would remove the host dependency.
  • node --test test/unit/gh-993-auth-route-chain.test.ts test/unit/gh-993-dev-client-clearstate-refusal.test.ts test/unit/gh-708-mid-flow-relaunch.test.ts test/unit/action-engine-compat.test.ts test/unit/session/worktree-inheritance.test.ts test/unit/gh-705-clearstate-appfile-forwarding.test.ts test/unit/auto-login-authority.test.ts — 149/149 pass on the branch build
  • Fail-before check: git checkout 7f33f73b -- packages/rn-dev-agent-core/src &amp;&amp; corepack yarn build:core, then the same defect tests — 5 fail (GH#993: LINK_FOREIGN remediation names the accepted target per regime, GH#993: unpinned regex action gets the terminal regex refusal, not the migrate remedy, plus gh-993-auth-route-chain / gh-993-dev-client-clearstate-refusal / gh-708-mid-flow-relaunch failing wholesale on the not-yet-existing exports); source restored and rebuilt, worktree left clean
  • Manual product transcript node defect-surfaces.mjs run against both the base and branch builds, driving the real dist/worktree-inheritance.js plan CLI as a subprocess over a git-worktree fixture in all three source regimes (D1); handleAutoLogin / isOnAuthScreen over the exact __root › auth › intro nav state from issue #993, an unmounted AuthStack + params.screen, and a logged-in AuthenticatedStack › Home (D2); cdp_run_action on an unpinned regex warm-login action (D3); executeMaestroAuthorityStages + authorityErrorMeta for a METRO_ORIGIN_MISMATCH after a flow-owned launchApp {clearState:true} and after a warm launchApp {stopApp:false} (D4); cdp_run_action and cdp_login_prologue envelopes plus runner/claim/relaunch side-effect counters for a clearState flow on a dev-client session, the same flow on a bare React Native session, and a warm action on the dev client (D5)
  • corepack yarn build:core — rebuilt dist from HEAD source before and after the fail-before experiment
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Four independent defects one QA journey walked through in sequence, plus the
captain-approved pre-mutation clearState refusal (Option A). None of them
changes which authority axes are probed, when, or what proves them; the runner
pin, exact-pin refusal, engine-pin refusal and regex fence are unchanged.

1. worktree-inheritance: the LINK_FOREIGN remediation now depends on the
   regime. With no primary corpus it names the single accepted target
   (<primary worktree>/.rn-agent/actions), says it does not exist, and lists
   the two supported shapes; with a primary corpus it names the target it can
   be re-pointed to. classifyDestination, the allowlist and
   resolveReadableActionCorpus are untouched.
2. auto-login: isOnAuthScreen compares the whole route chain root->leaf
   (routeName and params.screen at each level) instead of the leaf only, so
   `__root > auth > intro` is an auth screen without adding any app-specific
   name; the negative reason carries the observed chain.
3. action-engine-compat: the regex-selector fence is evaluated before the
   enginePin header refusal, so an unpinned regex action gets its terminal
   reason instead of being sent to migrate-actions, which refuses it; the
   header refusal now says what an `incompatible` migration result means.
4. maestro-run: a METRO_ORIGIN_MISMATCH raised after a flow-owned launchApp
   relaunch is attributed to that relaunch (message + meta.flowRelaunch);
   the SessionAuthorityError instance, code, axis, and the GH #708
   abort-vs-defer control flow are unchanged.
5. run-action (Option A): on a managed dev-client session (install binding
   buildKind expo or devClientUrl) a flow containing clearState is refused
   with DEV_CLIENT_CLEARSTATE_REFUSED before any runner call, origin claim or
   park. cdp_login_prologue inherits it. Bare React Native sessions and
   unmanaged replay keep GH #705 behaviour.
6. creating-actions skill + rn-agent README: single accepted inheritance
   target; dev-client login actions must be warm with id/literal selectors.

Each defect has a test that fails on the previous dist and passes now:
gh-993-auth-route-chain, gh-993-dev-client-clearstate-refusal (zero runner
invocations, unchanged sidecar), gh-708-mid-flow-relaunch (D2.b/D2.e),
action-engine-compat (unpinned regex ordering), worktree-inheritance (S1/S2).
…stall-binding read

GH #993 reads the install binding for every clearState flow to decide the
dev-client refusal, so the GH #705 test now pins the invariant it guards —
explicit appFile wins and auto-resolution never runs — and the new suite pins
that an explicit appFile does not bypass DEV_CLIENT_CLEARSTATE_REFUSED.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-07T23:04:13.557855Z 656e9a7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Preserve both intents in the troubleshooting catalog: #996's
readiness-deadline cleanup probe tallies on METRO_PORT_CLAIM_* and
#997's flowRelaunch annotation on METRO_ORIGIN_MISMATCH. Regenerated
host runtimes from the merged sources.
@Lykhoyda

Lykhoyda commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

QA Report — PR #997 / GH #993

Date: 2026-09-09
Head: 1a6a44da (fm/rn-dev-agent-issue-993-fix, merge of origin/main b3520436 / #996)
App: sibling rn-dev-agent-workspace/test-app (Expo SDK 56, RN 0.85.3, NativeWind, com.rndevagent.testapp)
Device: iOS Simulator WALLETPOA-203 iPhone 17 Pro (F15E0DEC-24B8-45C8-84C5-3C98F39FFD6D)
Host: Darwin 25.6.0, Node v26.8.1
Method: spawned this PR’s host supervisor.js with cwd = test-app and drove rn_session / device_* / cdp_* over JSON-RPC (this repo’s MCP session cannot pin a sibling worktree).

Conflicts

Resolved against main after #996 landed. Only content conflict: apps/docs-site/src/content/docs/troubleshooting.mdx. Kept both intents — #996’s readiness-deadline probe-tally wording on METRO_PORT_CLAIM_* / METRO_START_CLEANUP_UNPROVEN, and this PR’s flowRelaunch / device_reset_state annotation on METRO_ORIGIN_MISMATCH. Regenerated host runtimes with corepack yarn build:host-runtimes (did not keep the auto-merged minified dist). Merge commit pushed to fm/rn-dev-agent-issue-993-fix.

Verdict

Pass — merge as-is for GH #993.

All four independent defects hold on the product path. No foreign-corpus allow path. Nested auth › intro is not a route in test-app; the chain walk is live-proven on this app’s real tree (Tabs › HomeTab › HomeMain) and unit-pinned for the reported auth › intro shape.

What was tested

# Scenario Result
1 Conflict merge + host runtime rebuild troubleshooting.mdx keeps both #996 and #997 rows. Host dist regenerated, not hand-edited.
2 LINK_FOREIGN CLI — scratch linked worktree, RN package.json, symlink to a foreign corpus NO_SOURCE: remediation names <primary worktree>/.rn-agent/actions does not exist; supported shapes are a real local actions dir or creating the primary corpus. No can re-point it. PRIVATE_SOURCE_AVAILABLE: can re-point it there after explicit confirmation. CLI JSON never leaked the absolute fixture path.
3 Managed sessionbind_deviceapply_integrationpnpm ios (Simulator.app open) → pin_dev_client pnpm ios exit 0 in 46 s, signed install receipt (not pasted). Session ready, Metro 8188, installBound + metroBound + bundleBound.
4 cdp_run_action clearState probe (temp YAML, deleted after) DEV_CLIENT_CLEARSTATE_REFUSED. EG_DEV_CLIENT_CLEARSTATE. launchShape: "dev-client". No runner was invoked and the app was not touched. App pid 40995 unchanged.
5 Unpinned regex probe (temp YAML, no enginePin) ENGINE_PIN_MISMATCH with regex text selectors (.*Getsafe.*http://.*) / Rewrite as id or literal text selectors / No UI mutation will run. No migrate-actions.
6 cdp_auto_login on Onboarding Treated as an auth screen (pattern onboarding). Did not say “not on an auth screen”. Stopped at “no explicitly authorized legacy login subflow” (test-app has no .maestro/login.yaml — expected).
7 cdp_auto_login on Home after dismissing Expo Continue + onboarding-done App is not on an auth screen (route: Tabs › HomeTab › HomeMain). Negative reason carries the observed root→leaf chain.
8 cdp_auto_login on Login (home-login-btn) Nav routeName: "Login". Treated as an auth screen. Same “no legacy login subflow” stop — not a false “not on an auth screen”.

Live evidence (quoted)

LINK_FOREIGN, primary has no corpus:

"regime": "NO_SOURCE",
"state": "LINK_FOREIGN",
"remediation": "Destination is a symlink, but the only accepted target <primary worktree>/.rn-agent/actions does not exist, so there is nothing to re-point it to. Supported shapes: replace the link with a real actions directory in this worktree, or create the corpus at <primary worktree>/.rn-agent/actions and re-run /rn-dev-agent:setup."

LINK_FOREIGN, primary owns a corpus:

"regime": "PRIVATE_SOURCE_AVAILABLE",
"state": "LINK_FOREIGN",
"remediation": "Destination is a symlink to something other than the only accepted target <primary worktree>/.rn-agent/actions; /rn-dev-agent:setup can re-point it there after explicit confirmation."

clearState Option A (managed Expo receipt):

DEV_CLIENT_CLEARSTATE_REFUSED: Refusing to replay a flow containing clearState on a managed dev-client session. … (EG_DEV_CLIENT_CLEARSTATE). No runner was invoked and the app was not touched.

Regex before migrate-actions:

ENGINE_PIN_MISMATCH: Action uses regex text selectors (.*Getsafe.*http://.*) which are not a validated maestro-runner 1.1.24 capability (GH #750 CONTAINS mistranslation). Rewrite as id or literal text selectors before replay. No UI mutation will run.

Auth-route chain (live, this app’s tree — not the reported auth › intro leaf):

Onboarding → “on an auth screen but no … login subflow”
Home      → App is not on an auth screen (route: Tabs › HomeTab › HomeMain)
Login     → “on an auth screen but no … login subflow”

cdp_login_prologue independently refused with no exact user-login learned action was found. Auth-tag or intent inference is not permitted. — consistent with “login on a managed dev client is a warm owned action, not a relaunch.”

Out of scope / environment

  1. test-app has no nested auth(params.screen=intro) navigator. That exact reported chain stays on test/unit/gh-993-auth-route-chain.test.ts. Live QA proved the same walk on Tabs › HomeTab › HomeMain and leaf Login / Onboarding.
  2. A second app on this simulator held a foreign XCTest/WDA session (BUSY_FOREIGN_FLOW) and produced one RECONNECT_TIMEOUT on the first cdp_auto_login. After simctl launch + pin_dev_client, CDP recovered and the table above ran. Not a GH bug: no working login path on a pooled worktree - corpus LINK_FOREIGN, prologue tears down axis A, auto-login misreads auth route, warm action engine-incompatible #993 defect.
  3. First attach showed Expo’s native “This is the developer menu / Continue” sheet over onboarding. cdp_interact cannot tap that (not in the RN tree). device_find + device_press ref=@e55 dismissed it. First-open chrome, not this PR.
  4. pnpm ios -- --device … is correctly SESSION_BUILD_IDENTITY_CONFLICT once bind_device already pinned the UDID. Plain pnpm ios is the managed path.
  5. Did not run Android. macOS action-engine-compat /var vs /private/var migrate tests still fail the same way AGENTS.md documents on main — not treated as a merge regression.

Residual risk

Low. Axis A/B probing, claimOrigin, pinExactDevClient, runner pin, and the regex fence itself are unchanged; this PR only reorders the regex-vs-migrate terminal reason and adds the dev-client clearState fail-closed. A clearState login YAML on a managed Expo session now refuses instead of stranding the picker — that is the intended behavior. I did not ship or run a login flow.

Cleanup

Temp probe YAML deleted. device_snapshot closestop_metrorestore_integrationrelease. test-app scripts.ios = expo run:ios; .rn-agent/integration removed; port 8188 free. Accidental pnpm-lock.yaml drift from a flagged pnpm ios -- --device reverted. QA worktree left at /Volumes/SSD-project/Github/rn-dev-agent-qa997 (qa/997-conflict-fix tracking origin/fm/rn-dev-agent-issue-993-fix @ 1a6a44da).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant