zeno-mobile-runner 0.2.14 → 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,29 @@ 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
+
19
+ ## 0.2.15 (2026-06-24)
20
+
21
+ ### Fixed
22
+
23
+ - Generated iOS shim commands now remove app-local ZMR-owned derived data paths
24
+ ending in `ZMRDerivedData` before `build-for-testing` refreshes. This avoids
25
+ reusing stale Xcode absolute paths when app checkouts are copied, while
26
+ refusing to delete arbitrary shared DerivedData locations.
27
+ - Release gates now verify that `ZMR_VERSION`, `package.json`, `src/version.zig`,
28
+ archive names, and release-smoked binaries agree before publishing.
29
+
7
30
  ## 0.2.14 (2026-06-23)
8
31
 
9
32
  ### Added
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.14`, 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.14` 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.14.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.14"
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.14.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.14"
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.14"
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.14",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "types": "index.d.ts",
@@ -101,6 +101,10 @@ with `--ios-shim`. It caches `build-for-testing` output and uses
101
101
  `test-without-building` for selector commands through `.zmr/ios-shim-state/`.
102
102
  Set `ZMR_IOS_SHIM_FORCE_REBUILD=1` after app-side target changes, or
103
103
  `ZMR_IOS_SHIM_ONESHOT=1` when you need to debug the slower cold-start path.
104
+ When `--derived-data-path` points at a ZMR-owned path ending in
105
+ `ZMRDerivedData`, the generated shim removes that directory before each
106
+ `build-for-testing` refresh so copied app checkouts do not reuse stale absolute
107
+ Xcode paths. It refuses to delete arbitrary shared DerivedData locations.
104
108
 
105
109
  ## Recommended App Repo Layout
106
110
 
package/docs/npm.md CHANGED
@@ -333,7 +333,10 @@ The generated command caches `build-for-testing` output under
333
333
  prints the last Xcode log lines when XCTest fails. Set
334
334
  `ZMR_IOS_SHIM_FORCE_REBUILD=1` after app-side target changes, or
335
335
  `ZMR_IOS_SHIM_ONESHOT=1` for a cold-start fallback while debugging app-side Xcode
336
- wiring.
336
+ wiring. When `--derived-data-path` points at a ZMR-owned path ending in
337
+ `ZMRDerivedData`, the generated command removes that directory before each
338
+ `build-for-testing` refresh so copied app checkouts do not reuse stale absolute
339
+ Xcode paths. It refuses to delete arbitrary shared DerivedData locations.
337
340
 
338
341
  ## Native Binary Resolution
339
342
 
@@ -1,4 +1,4 @@
1
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.14","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.14`.
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.14","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.14","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.14","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.14","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.14","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.14","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.14","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.14","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.14","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`.
@@ -208,8 +208,10 @@ This avoids failing the whole demo when one local simulator cannot start
208
208
  `launchd_sim`, while still surfacing a setup error if every available simulator
209
209
  fails to boot.
210
210
 
211
- If a previous Xcode build was interrupted, remove only the app-local ZMR derived
212
- data path configured for the shim, then rerun the shim once to prewarm it:
211
+ The generated shim now removes app-local ZMR derived data paths ending in
212
+ `ZMRDerivedData` before each `build-for-testing` refresh. If you intentionally
213
+ configured a different shared DerivedData path, clean only the app-local ZMR
214
+ cache you own, then rerun the shim once to prewarm it:
213
215
 
214
216
  ```bash
215
217
  rm -rf ios/build/ZMRDerivedData
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeno-mobile-runner",
3
- "version": "0.2.14",
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": {
@@ -82,6 +82,7 @@
82
82
  "!scripts/release-candidate.sh",
83
83
  "!scripts/release-gate.sh",
84
84
  "!scripts/release-smoke.sh",
85
+ "!scripts/verify-release-version.sh",
85
86
  "!scripts/sign-macos-release.sh",
86
87
  "!scripts/verify-release-artifacts.sh",
87
88
  "!scripts/__pycache__/",
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,7 +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"
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"
226
252
  STDIN_FILE="\$(mktemp)"
227
253
  trap 'rm -f "\$STDIN_FILE"' EXIT
228
254
 
@@ -332,6 +358,40 @@ destination_spec() {
332
358
  printf 'platform=%s,id=%s' "\$platform_name" "\$destination_id"
333
359
  }
334
360
 
361
+ clean_zmr_derived_data() {
362
+ if [[ -z "\$DERIVED_DATA_PATH_VALUE" ]]; then
363
+ return 0
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
368
+
369
+ local derived_data_abs
370
+ if [[ "\$DERIVED_DATA_PATH_VALUE" == /* ]]; then
371
+ derived_data_abs="\$DERIVED_DATA_PATH_VALUE"
372
+ else
373
+ derived_data_abs="$APP_ROOT/\$DERIVED_DATA_PATH_VALUE"
374
+ fi
375
+ while [[ "\$derived_data_abs" == */ && "\$derived_data_abs" != "/" ]]; do
376
+ derived_data_abs="\${derived_data_abs%/}"
377
+ done
378
+
379
+ case "\$derived_data_abs" in
380
+ "$APP_ROOT/ZMRDerivedData"|"$APP_ROOT"/*/ZMRDerivedData)
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"
388
+ ;;
389
+ *)
390
+ echo "warning: refusing to delete non-ZMR derived data path: \$DERIVED_DATA_PATH_VALUE" >&2
391
+ ;;
392
+ esac
393
+ }
394
+
335
395
  is_server_running() {
336
396
  if [[ ! -f "\$PID_FILE" ]]; then
337
397
  return 1
@@ -392,13 +452,14 @@ wait_for_ready() {
392
452
  }
393
453
 
394
454
  build_for_testing() {
395
- 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
396
456
  return 0
397
457
  fi
398
458
 
399
459
  local destination_id build_log
400
460
  destination_id="\$(destination_spec)"
401
461
  build_log="\$STATE_DIR/xcodebuild.build.log"
462
+ clean_zmr_derived_data
402
463
 
403
464
  run_xcodebuild_with_timeout "iOS shim build-for-testing" "\${ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS:-5400}" "\$build_log" \\
404
465
  xcodebuild build-for-testing \\
@@ -410,6 +471,7 @@ build_for_testing() {
410
471
  ZMR_SHIM_SERVER_DIR="\$SERVER_DIR" \\
411
472
  ZMR_APP_BUNDLE_ID="$BUNDLE_ID"
412
473
 
474
+ printf '%s\\n' "\$INSTALL_FINGERPRINT_VALUE" > "\$BUILD_FINGERPRINT_FILE"
413
475
  touch "\$BUILD_READY_FILE"
414
476
  }
415
477
 
@@ -19,7 +19,11 @@ Current status:
19
19
  selector commands through `test-without-building`, exchanging per-command
20
20
  files under `.zmr/ios-shim-state/`. Set `ZMR_IOS_SHIM_FORCE_REBUILD=1` to
21
21
  refresh the cached test bundle, or `ZMR_IOS_SHIM_ONESHOT=1` to force the
22
- slower one-command XCTest fallback for debugging.
22
+ slower one-command XCTest fallback for debugging. When configured with a
23
+ ZMR-owned derived data path ending in `ZMRDerivedData`, the command removes
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.
23
27
  - The iOS adapter still uses `xcrun simctl` for simulator install, launch,
24
28
  terminate, open link, screenshots, and logs. It uses `xcrun devicectl` for
25
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.14";
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";