zeno-mobile-runner 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to Zeno Mobile Runner are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.2.16 (2026-06-25)
8
+
9
+ ### Fixed
10
+
11
+ - iOS Expo dev-client custom-link recovery now reopens matched project entries
12
+ from observed launcher state instead of reporting blind coordinate taps as
13
+ accepted. Visible but non-hittable launcher rows are tapped by their matched
14
+ frame, keeping recovery selector/state based.
15
+ - Generated iOS shim commands now fingerprint shim inputs and preserve reusable
16
+ app/Pods build outputs. Reinstalls reuse unchanged `build-for-testing`
17
+ products, and stale cleanup removes only ZMR shim products/intermediates.
18
+
7
19
  ## 0.2.15 (2026-06-24)
8
20
 
9
21
  ### Fixed
package/FEATURES.md CHANGED
@@ -142,7 +142,7 @@ state, and writes deterministic traces. It does not embed an LLM.
142
142
 
143
143
  ## Current Limitations
144
144
 
145
- - Current release status is `0.2.15`, a public developer preview rather than
145
+ - Current release status is `0.2.16`, a public developer preview rather than
146
146
  a production-stable `1.0.0`.
147
147
  - Physical iOS log capture is still simulator-first. Physical iOS screenshots
148
148
  are available when the XCTest/XCUIAutomation shim is configured.
package/README.md CHANGED
@@ -199,7 +199,7 @@ comparisons against your current E2E tool, and multi-device matrices, see
199
199
  Slow CI hardware can extend the generated iOS shim build timeout with
200
200
  `ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS`; `ZMR_IOS_SHIM_RESPONSE_TIMEOUT_SECONDS`
201
201
  bounds each in-flight request, and `ZMR_IOS_SHIM_TIMEOUT_MS` remains the outer
202
- process ceiling. Current release: `0.2.15` developer preview.
202
+ process ceiling. Current release: `0.2.16` developer preview.
203
203
  Protocol version: `2026-04-28`.
204
204
 
205
205
  ## Optional protocol clients
@@ -27,7 +27,7 @@ gradle -p clients/kotlin runFakeSession \
27
27
  ```
28
28
 
29
29
  ```kotlin
30
- implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.15.jar"))
30
+ implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.16.jar"))
31
31
  ```
32
32
 
33
33
  ```kotlin
@@ -4,7 +4,7 @@ plugins {
4
4
  }
5
5
 
6
6
  group = "dev.zmr"
7
- version = "0.2.15"
7
+ version = "0.2.16"
8
8
 
9
9
  kotlin {
10
10
  jvmToolchain(17)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zmr-client"
7
- version = "0.2.15.dev1"
7
+ version = "0.2.16.dev1"
8
8
  description = "Python JSON-RPC client for Zeno Mobile Runner."
9
9
  requires-python = ">=3.9"
10
10
  license = { text = "MIT" }
@@ -100,7 +100,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
100
100
 
101
101
  [[package]]
102
102
  name = "zmr-client"
103
- version = "0.2.15"
103
+ version = "0.2.16"
104
104
  dependencies = [
105
105
  "serde",
106
106
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "zmr-client"
3
- version = "0.2.15"
3
+ version = "0.2.16"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  description = "Rust JSON-RPC client for Zeno Mobile Runner."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmr/client",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "types": "index.d.ts",
@@ -1,4 +1,4 @@
1
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.15","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
1
+ {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
2
2
  {"jsonrpc":"2.0","id":2,"result":[{"serial":"fake-device-1","state":"device","ready":true}]}
3
3
  {"jsonrpc":"2.0","id":3,"result":{"sessionId":"default"}}
4
4
  {"jsonrpc":"2.0","id":4,"result":true}
package/docs/protocol.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ZMR exposes newline-delimited JSON-RPC 2.0 over stdio or localhost TCP in v1. Each request is one JSON object followed by `\n`. Each response is one JSON object followed by `\n`.
4
4
 
5
- Current runner version: `0.2.15`.
5
+ Current runner version: `0.2.16`.
6
6
 
7
7
  Current protocol version: `2026-04-28`.
8
8
 
@@ -47,7 +47,7 @@ and protocol versions. The response is covered by
47
47
  `schemas/inspect-output.schema.json`:
48
48
 
49
49
  ```json
50
- {"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.15","protocolVersion":"2026-04-28","dir":".","configPath":".zmr/config.json","configExists":true,"agentInstructionsPath":".zmr/AGENTS.md","agentInstructionsExists":true,"platforms":[{"name":"android","enabled":true,"defaultDevice":"emulator-5554","smokeScenario":".zmr/android-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-android"},{"name":"ios","enabled":true,"defaultDevice":"booted","smokeScenario":".zmr/ios-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-ios"}],"recommendedCommands":["zmr doctor --strict --json --config .zmr/config.json","zmr schemas --json","zmr validate --json .zmr/android-smoke.json","zmr validate --json .zmr/ios-smoke.json","zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent","zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent"],"claimsPolicy":["verify runs with trace evidence before making readiness claims","do not claim Flutter widget-tree inspection"],"limitations":["inspect is read-only and does not launch devices","autonomous crawling is not shipped; generate or edit scenarios for human review"]}
50
+ {"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","dir":".","configPath":".zmr/config.json","configExists":true,"agentInstructionsPath":".zmr/AGENTS.md","agentInstructionsExists":true,"platforms":[{"name":"android","enabled":true,"defaultDevice":"emulator-5554","smokeScenario":".zmr/android-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-android"},{"name":"ios","enabled":true,"defaultDevice":"booted","smokeScenario":".zmr/ios-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-ios"}],"recommendedCommands":["zmr doctor --strict --json --config .zmr/config.json","zmr schemas --json","zmr validate --json .zmr/android-smoke.json","zmr validate --json .zmr/ios-smoke.json","zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent","zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent"],"claimsPolicy":["verify runs with trace evidence before making readiness claims","do not claim Flutter widget-tree inspection"],"limitations":["inspect is read-only and does not launch devices","autonomous crawling is not shipped; generate or edit scenarios for human review"]}
51
51
  ```
52
52
 
53
53
  `zmr discover --from-trace <trace-dir> --out <scenario.json> --validate --json`
@@ -60,7 +60,7 @@ invent credentials, or commit files. The response is covered by
60
60
  `schemas/discover-output.schema.json`:
61
61
 
62
62
  ```json
63
- {"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.15","protocolVersion":"2026-04-28","out":".zmr/discovered/replay-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/replay-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/replay-smoke.json","zmr run .zmr/discovered/replay-smoke.json --json --trace-dir traces/zmr-agent"]}
63
+ {"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/replay-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/replay-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/replay-smoke.json","zmr run .zmr/discovered/replay-smoke.json --json --trace-dir traces/zmr-agent"]}
64
64
  ```
65
65
 
66
66
  `zmr explore --from-trace <trace-dir> --out <scenario.json> --goal <goal>
@@ -71,7 +71,7 @@ launch devices, crawl the app, invent missing actions, discover credentials, or
71
71
  commit files. The response is covered by `schemas/explore-output.schema.json`:
72
72
 
73
73
  ```json
74
- {"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.15","protocolVersion":"2026-04-28","goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"],"out":".zmr/discovered/login-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/login-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/login-smoke.json","zmr run .zmr/discovered/login-smoke.json --json --trace-dir traces/zmr-agent"]}
74
+ {"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"],"out":".zmr/discovered/login-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/login-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/login-smoke.json","zmr run .zmr/discovered/login-smoke.json --json --trace-dir traces/zmr-agent"]}
75
75
  ```
76
76
 
77
77
  `zmr draft --from-trace <trace-dir> --out <scenario.json> --json` is the lower
@@ -84,7 +84,7 @@ into fields, or commit files. The response is covered by
84
84
  `schemas/draft-output.schema.json`:
85
85
 
86
86
  ```json
87
- {"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.15","protocolVersion":"2026-04-28","out":".zmr/discovered/surface-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":4,"replay":{"enabled":false,"eventCount":0,"stepCount":0,"skippedEventCount":0},"warnings":["draft requires human review before commit"],"nextCommands":["zmr validate --json .zmr/discovered/surface-smoke.json","zmr run .zmr/discovered/surface-smoke.json --json --trace-dir traces/zmr-agent"]}
87
+ {"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/surface-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":4,"replay":{"enabled":false,"eventCount":0,"stepCount":0,"skippedEventCount":0},"warnings":["draft requires human review before commit"],"nextCommands":["zmr validate --json .zmr/discovered/surface-smoke.json","zmr run .zmr/discovered/surface-smoke.json --json --trace-dir traces/zmr-agent"]}
88
88
  ```
89
89
 
90
90
  `zmr draft --include-actions` additionally parses `events.jsonl` and prepends
@@ -214,7 +214,7 @@ installers, setup scripts, and generated clients. The response is covered by
214
214
  `schemas/version-output.schema.json`:
215
215
 
216
216
  ```json
217
- {"name":"zmr","version":"0.2.15","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
217
+ {"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
218
218
  ```
219
219
 
220
220
  ## Capabilities Output Contract
@@ -226,7 +226,7 @@ and method inventory for JSON-RPC clients. The result object is covered by
226
226
  iOS simulator, or physical iOS workflows are available.
227
227
 
228
228
  ```json
229
- {"name":"zmr","version":"0.2.15","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}
229
+ {"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}
230
230
  ```
231
231
 
232
232
  ## Doctor Output Contract
@@ -433,7 +433,7 @@ Request:
433
433
  Response:
434
434
 
435
435
  ```json
436
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.15","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
436
+ {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.16","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
437
437
  ```
438
438
 
439
439
  ### `trace.events`
@@ -515,7 +515,7 @@ Request:
515
515
  Response:
516
516
 
517
517
  ```json
518
- {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.15","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-smoke.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-smoke.json","name":"agent smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-smoke.json","zmr run .zmr/discovered/agent-smoke.json --json --trace-dir traces/agent-session"]}}
518
+ {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-smoke.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-smoke.json","name":"agent smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-smoke.json","zmr run .zmr/discovered/agent-smoke.json --json --trace-dir traces/agent-session"]}}
519
519
  ```
520
520
 
521
521
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
@@ -538,7 +538,7 @@ Request:
538
538
  Response:
539
539
 
540
540
  ```json
541
- {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.15","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-goal.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent goal smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-goal.json","name":"agent goal smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-goal.json","zmr run .zmr/discovered/agent-goal.json --json --trace-dir traces/agent-session"],"goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"]}}
541
+ {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.16","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-goal.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent goal smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-goal.json","name":"agent goal smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-goal.json","zmr run .zmr/discovered/agent-goal.json --json --trace-dir traces/agent-session"],"goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"]}}
542
542
  ```
543
543
 
544
544
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeno-mobile-runner",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "Agent-native mobile app test runner for React Native, Expo, Flutter, and native Android/iOS.",
5
5
  "main": "npm/index.mjs",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
@@ -165,8 +165,28 @@ fi
165
165
 
166
166
  mkdir -p "$APP_ROOT"
167
167
  APP_ROOT="$(cd "$APP_ROOT" && pwd)"
168
- mkdir -p "$APP_ROOT/.zmr" "$APP_ROOT/.zmr/shims/ios"
169
- rm -f "$APP_ROOT/.zmr/ios-shim-state/build-for-testing.ready"
168
+ SHIM_INSTALL_FINGERPRINT="$(
169
+ {
170
+ printf '%s\n' \
171
+ "$SCHEME" \
172
+ "$TEST_TARGET" \
173
+ "$TEST_BUNDLE_ID" \
174
+ "$WORKSPACE" \
175
+ "$PROJECT" \
176
+ "$APP_TARGET" \
177
+ "$BUNDLE_ID" \
178
+ "$DERIVED_DATA_PATH" \
179
+ "$DEVICE_TYPE" \
180
+ "$CONFIGURATION" \
181
+ "$DEPLOYMENT_TARGET"
182
+ shasum -a 256 \
183
+ "$ROOT/scripts/install-ios-shim.sh" \
184
+ "$ROOT/scripts/ensure-ios-shim-target.rb" \
185
+ "$ROOT/shims/ios/ZMRShim.swift" \
186
+ "$ROOT/shims/ios/ZMRShimUITestCase.swift"
187
+ } | shasum -a 256 | awk '{print $1}'
188
+ )"
189
+ mkdir -p "$APP_ROOT/.zmr" "$APP_ROOT/.zmr/shims/ios" "$APP_ROOT/.zmr/ios-shim-state"
170
190
  rm -f "$APP_ROOT/.zmr/ios-shim-state/destination.id"
171
191
  rm -f "$APP_ROOT/.zmr/ios-shim-state/xcodebuild.pid"
172
192
  rm -f "$APP_ROOT/.zmr/ios-shim-state/xcodebuild.log"
@@ -222,8 +242,13 @@ PID_FILE="\$STATE_DIR/xcodebuild.pid"
222
242
  READY_FILE="\$SERVER_DIR/ready"
223
243
  DESTINATION_ID_FILE="\$STATE_DIR/destination.id"
224
244
  BUILD_READY_FILE="\$STATE_DIR/build-for-testing.ready"
245
+ BUILD_FINGERPRINT_FILE="\$STATE_DIR/build-for-testing.fingerprint"
246
+ DERIVED_DATA_CLEAN_FINGERPRINT_FILE="\$STATE_DIR/derived-data-clean.fingerprint"
225
247
  LOG_FILE="\$STATE_DIR/xcodebuild.log"
226
248
  DERIVED_DATA_PATH_VALUE="$DERIVED_DATA_PATH"
249
+ INSTALL_FINGERPRINT_VALUE="$SHIM_INSTALL_FINGERPRINT"
250
+ ZMR_TEST_TARGET_NAME="$TEST_TARGET"
251
+ ZMR_SCHEME_NAME="$SCHEME"
227
252
  STDIN_FILE="\$(mktemp)"
228
253
  trap 'rm -f "\$STDIN_FILE"' EXIT
229
254
 
@@ -337,6 +362,9 @@ clean_zmr_derived_data() {
337
362
  if [[ -z "\$DERIVED_DATA_PATH_VALUE" ]]; then
338
363
  return 0
339
364
  fi
365
+ if [[ "\${ZMR_IOS_SHIM_FORCE_REBUILD:-}" != "1" && -f "\$DERIVED_DATA_CLEAN_FINGERPRINT_FILE" ]] && [[ "\$(cat "\$DERIVED_DATA_CLEAN_FINGERPRINT_FILE" 2>/dev/null || true)" == "\$INSTALL_FINGERPRINT_VALUE" ]]; then
366
+ return 0
367
+ fi
340
368
 
341
369
  local derived_data_abs
342
370
  if [[ "\$DERIVED_DATA_PATH_VALUE" == /* ]]; then
@@ -350,7 +378,13 @@ clean_zmr_derived_data() {
350
378
 
351
379
  case "\$derived_data_abs" in
352
380
  "$APP_ROOT/ZMRDerivedData"|"$APP_ROOT"/*/ZMRDerivedData)
353
- rm -rf "\$derived_data_abs"
381
+ if [[ -d "\$derived_data_abs/Build/Products" ]]; then
382
+ find "\$derived_data_abs/Build/Products" -depth \\( -name "\$ZMR_TEST_TARGET_NAME*" -o -name "\$ZMR_SCHEME_NAME*" \\) -exec rm -rf {} +
383
+ fi
384
+ if [[ -d "\$derived_data_abs/Build/Intermediates.noindex" ]]; then
385
+ find "\$derived_data_abs/Build/Intermediates.noindex" -depth \\( -name "\$ZMR_TEST_TARGET_NAME.build" -o -name "\$ZMR_SCHEME_NAME.build" \\) -exec rm -rf {} +
386
+ fi
387
+ printf '%s\\n' "\$INSTALL_FINGERPRINT_VALUE" > "\$DERIVED_DATA_CLEAN_FINGERPRINT_FILE"
354
388
  ;;
355
389
  *)
356
390
  echo "warning: refusing to delete non-ZMR derived data path: \$DERIVED_DATA_PATH_VALUE" >&2
@@ -418,7 +452,7 @@ wait_for_ready() {
418
452
  }
419
453
 
420
454
  build_for_testing() {
421
- if [[ "\${ZMR_IOS_SHIM_FORCE_REBUILD:-}" != "1" && -f "\$BUILD_READY_FILE" ]]; then
455
+ if [[ "\${ZMR_IOS_SHIM_FORCE_REBUILD:-}" != "1" && -f "\$BUILD_READY_FILE" && -f "\$BUILD_FINGERPRINT_FILE" ]] && [[ "\$(cat "\$BUILD_FINGERPRINT_FILE" 2>/dev/null || true)" == "\$INSTALL_FINGERPRINT_VALUE" ]]; then
422
456
  return 0
423
457
  fi
424
458
 
@@ -437,6 +471,7 @@ build_for_testing() {
437
471
  ZMR_SHIM_SERVER_DIR="\$SERVER_DIR" \\
438
472
  ZMR_APP_BUNDLE_ID="$BUNDLE_ID"
439
473
 
474
+ printf '%s\\n' "\$INSTALL_FINGERPRINT_VALUE" > "\$BUILD_FINGERPRINT_FILE"
440
475
  touch "\$BUILD_READY_FILE"
441
476
  }
442
477
 
@@ -21,8 +21,9 @@ Current status:
21
21
  refresh the cached test bundle, or `ZMR_IOS_SHIM_ONESHOT=1` to force the
22
22
  slower one-command XCTest fallback for debugging. When configured with a
23
23
  ZMR-owned derived data path ending in `ZMRDerivedData`, the command removes
24
- that directory before each `build-for-testing` refresh and refuses to delete
25
- arbitrary shared DerivedData paths.
24
+ only stale shim target products and intermediates before a needed
25
+ `build-for-testing` refresh, preserves app and Pods build outputs, and
26
+ refuses to delete arbitrary shared DerivedData paths.
26
27
  - The iOS adapter still uses `xcrun simctl` for simulator install, launch,
27
28
  terminate, open link, screenshots, and logs. It uses `xcrun devicectl` for
28
29
  physical-device lifecycle where Apple exposes a supported local command, and
@@ -2,6 +2,8 @@ import Foundation
2
2
  import XCTest
3
3
 
4
4
  final class ZMRShimUITestCase: XCTestCase {
5
+ private let expoDevClientRecoveryTimeout: TimeInterval = 10
6
+
5
7
  func testRunZMRCommand() throws {
6
8
  let environment = ProcessInfo.processInfo.environment
7
9
  let app = makeApplication(bundleIdentifier: shimRuntimeValue("ZMR_APP_BUNDLE_ID", environment: environment))
@@ -318,6 +320,7 @@ final class ZMRShimUITestCase: XCTestCase {
318
320
 
319
321
  if app.staticTexts["Deep link received:"].waitForExistence(timeout: 1) {
320
322
  if tapFirstMatchingExpoCandidate(
323
+ app: app,
321
324
  queries: [app.buttons, app.cells, app.staticTexts],
322
325
  predicate: predicate
323
326
  ) {
@@ -328,12 +331,29 @@ final class ZMRShimUITestCase: XCTestCase {
328
331
  if expoDevClientFallback,
329
332
  isCustomSchemeURL(openedURL),
330
333
  !isExpoDevClientURL(openedURL) {
331
- if tapExpoDevClientDeepLinkCoordinateFallback(app: app) {
332
- return (true, "expo-dev-client-deep-link-coordinate")
333
- }
334
- if tapExpoDevClientDeepLinkCandidateFallback(app: app, predicate: predicate) {
334
+ return waitForExpoDevClientRecovery(app: app, deepLinkPredicate: predicate)
335
+ }
336
+
337
+ return (false, "")
338
+ }
339
+
340
+ private func waitForExpoDevClientRecovery(
341
+ app: XCUIApplication,
342
+ deepLinkPredicate: NSPredicate
343
+ ) -> (accepted: Bool, label: String) {
344
+ let deadline = Date().addingTimeInterval(expoDevClientRecoveryTimeout)
345
+ while Date() < deadline {
346
+ if app.staticTexts["Deep link received:"].exists,
347
+ tapExpoDevClientDeepLinkCandidateFallback(app: app, predicate: deepLinkPredicate) {
335
348
  return (true, "expo-dev-client-deep-link-candidate")
336
349
  }
350
+
351
+ let homeSelection = resumeExpoDevClientHome(app: app)
352
+ if homeSelection.accepted {
353
+ return homeSelection
354
+ }
355
+
356
+ Thread.sleep(forTimeInterval: 0.2)
337
357
  }
338
358
 
339
359
  return (false, "")
@@ -367,6 +387,7 @@ final class ZMRShimUITestCase: XCTestCase {
367
387
 
368
388
  let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR label CONTAINS[c] %@", " http://", " https://")
369
389
  if tapFirstMatchingExpoCandidate(
390
+ app: app,
370
391
  queries: [app.buttons, app.cells, app.staticTexts],
371
392
  predicate: predicate
372
393
  ) {
@@ -399,6 +420,7 @@ final class ZMRShimUITestCase: XCTestCase {
399
420
  }
400
421
 
401
422
  private func tapFirstMatchingExpoCandidate(
423
+ app: XCUIApplication,
402
424
  queries: [XCUIElementQuery],
403
425
  predicate: NSPredicate
404
426
  ) -> Bool {
@@ -410,19 +432,44 @@ final class ZMRShimUITestCase: XCTestCase {
410
432
  break
411
433
  }
412
434
 
413
- guard element.isHittable else {
414
- continue
435
+ if tapMatchedExpoCandidate(element: element, app: app) {
436
+ return true
415
437
  }
416
-
417
- element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
418
- Thread.sleep(forTimeInterval: 1.0)
419
- return true
420
438
  }
421
439
  }
422
440
 
423
441
  return false
424
442
  }
425
443
 
444
+ private func tapMatchedExpoCandidate(element: XCUIElement, app: XCUIApplication) -> Bool {
445
+ if element.isHittable {
446
+ element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
447
+ Thread.sleep(forTimeInterval: 1.0)
448
+ return true
449
+ }
450
+
451
+ let visibleFrame = element.frame.intersection(app.frame)
452
+ guard !visibleFrame.isNull,
453
+ !visibleFrame.isEmpty,
454
+ app.frame.width > 0,
455
+ app.frame.height > 0 else {
456
+ return false
457
+ }
458
+
459
+ let normalizedX = (visibleFrame.midX - app.frame.minX) / app.frame.width
460
+ let normalizedY = (visibleFrame.midY - app.frame.minY) / app.frame.height
461
+ guard normalizedX >= 0,
462
+ normalizedX <= 1,
463
+ normalizedY >= 0,
464
+ normalizedY <= 1 else {
465
+ return false
466
+ }
467
+
468
+ app.coordinate(withNormalizedOffset: CGVector(dx: normalizedX, dy: normalizedY)).tap()
469
+ Thread.sleep(forTimeInterval: 1.0)
470
+ return true
471
+ }
472
+
426
473
  private func isCustomSchemeURL(_ value: String?) -> Bool {
427
474
  guard let value else {
428
475
  return false
@@ -437,15 +484,9 @@ final class ZMRShimUITestCase: XCTestCase {
437
484
  return value.hasPrefix("exp+") && value.contains("://expo-development-client/")
438
485
  }
439
486
 
440
- private func tapExpoDevClientDeepLinkCoordinateFallback(app: XCUIApplication) -> Bool {
441
- Thread.sleep(forTimeInterval: 1.5)
442
- app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6)).tap()
443
- Thread.sleep(forTimeInterval: 1.0)
444
- return true
445
- }
446
-
447
487
  private func tapExpoDevClientDeepLinkCandidateFallback(app: XCUIApplication, predicate: NSPredicate) -> Bool {
448
488
  tapFirstMatchingExpoCandidate(
489
+ app: app,
449
490
  queries: [app.buttons, app.cells, app.staticTexts],
450
491
  predicate: predicate
451
492
  )
package/src/version.zig CHANGED
@@ -1,4 +1,4 @@
1
- pub const runner_version = "0.2.15";
1
+ pub const runner_version = "0.2.16";
2
2
  pub const protocol_version = "2026-04-28";
3
3
  pub const protocol_min_compatible_version = "2026-04-28";
4
4
  pub const protocol_stability = "dev-preview";