zeno-mobile-runner 0.1.8 → 0.2.1

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +175 -238
  4. package/clients/kotlin/README.md +1 -1
  5. package/clients/kotlin/build.gradle.kts +1 -1
  6. package/clients/python/pyproject.toml +1 -1
  7. package/clients/rust/Cargo.lock +1 -1
  8. package/clients/rust/Cargo.toml +1 -1
  9. package/clients/typescript/package.json +1 -1
  10. package/docs/agent-discovery.md +10 -0
  11. package/docs/ai-agents.md +18 -0
  12. package/docs/benchmarking.md +39 -0
  13. package/docs/benchmarks/2026-06-09-android-workflow.md +73 -0
  14. package/docs/benchmarks/2026-06-09-android-workflow.results.jsonl +20 -0
  15. package/docs/benchmarks/2026-06-09-framework-baseline-status.md +32 -0
  16. package/docs/benchmarks/2026-06-09-ios-appium-comparison.md +115 -0
  17. package/docs/benchmarks/2026-06-09-ios-appium-comparison.results.jsonl +40 -0
  18. package/docs/benchmarks/2026-06-09-ios-demo.md +90 -0
  19. package/docs/benchmarks/2026-06-09-ios-demo.results.jsonl +20 -0
  20. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.md +128 -0
  21. package/docs/benchmarks/2026-06-09-ios-maestro-comparison.results.jsonl +40 -0
  22. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.md +143 -0
  23. package/docs/benchmarks/2026-06-09-ios-workflow-comparison.results.jsonl +40 -0
  24. package/docs/benchmarks/2026-06-09-ios-xctest-floor.md +106 -0
  25. package/docs/benchmarks/2026-06-09-ios-xctest-floor.results.jsonl +40 -0
  26. package/docs/benchmarks/README.md +36 -0
  27. package/docs/benchmarks/benchmark-lab-v1.json +155 -0
  28. package/docs/benchmarks/benchmark-lab-v1.md +95 -0
  29. package/docs/clients.md +16 -0
  30. package/docs/demo.md +36 -1
  31. package/docs/frameworks.md +10 -0
  32. package/docs/npm.md +44 -2
  33. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  34. package/docs/protocol.md +10 -10
  35. package/docs/scenario-authoring.md +15 -0
  36. package/docs/trace-privacy.md +9 -0
  37. package/docs/troubleshooting.md +6 -0
  38. package/examples/android-workflow.json +79 -0
  39. package/examples/ios-dev-client-open-link.json +24 -13
  40. package/examples/ios-dev-client-route-snapshot.json +33 -8
  41. package/examples/ios-shim-workflow.json +79 -0
  42. package/examples/react-native-expo-workflow.json +75 -0
  43. package/npm/scenarios.mjs +15 -8
  44. package/npm/wizard.mjs +1 -1
  45. package/package.json +6 -1
  46. package/prebuilds/darwin-arm64/zmr +0 -0
  47. package/prebuilds/darwin-x64/zmr +0 -0
  48. package/prebuilds/linux-arm64/zmr +0 -0
  49. package/prebuilds/linux-x64/zmr +0 -0
  50. package/scripts/benchmark-lab.py +253 -0
  51. package/scripts/create-android-demo-app.sh +324 -29
  52. package/scripts/create-ios-demo-app.sh +174 -7
  53. package/scripts/create-react-native-expo-demo-app.sh +727 -0
  54. package/scripts/demo.sh +3 -0
  55. package/scripts/install-ios-shim.sh +2 -2
  56. package/shims/ios/ZMRShim.swift +10 -0
  57. package/shims/ios/ZMRShimUITestCase.swift +49 -1
  58. package/shims/ios/protocol.md +1 -0
  59. package/src/cli_import.zig +31 -15
  60. package/src/cli_trace.zig +38 -16
  61. package/src/cli_validate.zig +12 -6
  62. package/src/ios.zig +44 -11
  63. package/src/ios_shim.zig +36 -2
  64. package/src/main.zig +6 -0
  65. package/src/version.zig +1 -1
  66. package/viewer/app.js +23 -3
@@ -0,0 +1,155 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "Benchmark Lab v1",
4
+ "purpose": "Define reproducible mobile benchmark fixtures, modes, adapters, and claim boundaries for ZMR against framework-relevant local runners.",
5
+ "claimPolicy": {
6
+ "minimumRuns": 20,
7
+ "candidatePassRate": 100,
8
+ "candidateFailures": 0,
9
+ "requiresSameContext": true,
10
+ "requiresCommittedRows": true,
11
+ "requiresSanitizedCommands": true,
12
+ "forbiddenClaim": "Do not present fixture results as universal speed or reliability claims."
13
+ },
14
+ "modes": [
15
+ {
16
+ "id": "cold-command",
17
+ "label": "Cold command",
18
+ "description": "Each row measures the command surface a user would run from a shell, including runner startup."
19
+ },
20
+ {
21
+ "id": "warm-suite",
22
+ "label": "Warm suite",
23
+ "description": "The app, device, and runner bridge are prepared before timed rows so the benchmark isolates repeated scenario execution."
24
+ },
25
+ {
26
+ "id": "native-floor",
27
+ "label": "Native floor",
28
+ "description": "A lower-bound timing for the platform shim path. It is diagnostic evidence, not a product comparison."
29
+ }
30
+ ],
31
+ "fixtures": [
32
+ {
33
+ "id": "native-ios-workflow",
34
+ "label": "Generated native iOS workflow",
35
+ "framework": "native-ios",
36
+ "platforms": ["ios"],
37
+ "status": "evidence-committed",
38
+ "scenario": "examples/ios-shim-workflow.json",
39
+ "appGenerator": "scripts/create-ios-demo-app.sh",
40
+ "workflow": [
41
+ "launch",
42
+ "profile-entry",
43
+ "catalog-scroll",
44
+ "item-detail",
45
+ "save-item",
46
+ "review-assertion"
47
+ ],
48
+ "evidence": "docs/benchmarks/2026-06-09-ios-workflow-comparison.md"
49
+ },
50
+ {
51
+ "id": "native-android-workflow",
52
+ "label": "Generated native Android workflow",
53
+ "framework": "native-android",
54
+ "platforms": ["android"],
55
+ "status": "evidence-committed",
56
+ "scenario": "examples/android-workflow.json",
57
+ "appGenerator": "scripts/create-android-demo-app.sh",
58
+ "workflow": [
59
+ "launch",
60
+ "profile-entry",
61
+ "catalog-scroll",
62
+ "item-detail",
63
+ "save-item",
64
+ "review-assertion"
65
+ ],
66
+ "evidence": "docs/benchmarks/2026-06-09-android-workflow.md"
67
+ },
68
+ {
69
+ "id": "react-native-expo-workflow",
70
+ "label": "React Native and Expo workflow",
71
+ "framework": "react-native-expo",
72
+ "platforms": ["android", "ios"],
73
+ "status": "fixture-available",
74
+ "scenario": "examples/react-native-expo-workflow.json",
75
+ "appGenerator": "scripts/create-react-native-expo-demo-app.sh",
76
+ "workflow": [
77
+ "deep-link-open",
78
+ "profile-entry",
79
+ "list-scroll",
80
+ "stateful-save",
81
+ "review-assertion"
82
+ ],
83
+ "notes": "Generated fixture uses testID values, accessibility labels, and deep links. Public ZMR rows and baseline runner rows are not collected yet."
84
+ },
85
+ {
86
+ "id": "flutter-semantics-workflow",
87
+ "label": "Flutter semantics workflow",
88
+ "framework": "flutter",
89
+ "platforms": ["android", "ios"],
90
+ "status": "planned",
91
+ "workflow": [
92
+ "deep-link-open",
93
+ "semantics-label-selection",
94
+ "scroll",
95
+ "stateful-save",
96
+ "review-assertion"
97
+ ],
98
+ "notes": "Measure app-level Android/iOS automation through semantics labels and deep links. Do not claim widget-tree driver support."
99
+ }
100
+ ],
101
+ "runnerAdapters": [
102
+ {
103
+ "id": "zmr",
104
+ "label": "ZMR",
105
+ "status": "available",
106
+ "collector": "scripts/benchmark.sh",
107
+ "modes": ["cold-command", "warm-suite"]
108
+ },
109
+ {
110
+ "id": "maestro",
111
+ "label": "Maestro",
112
+ "status": "evidence-committed",
113
+ "collector": "scripts/benchmark-command.sh",
114
+ "modes": ["cold-command"]
115
+ },
116
+ {
117
+ "id": "appium",
118
+ "label": "Appium",
119
+ "status": "partial",
120
+ "collector": "scripts/benchmark-command.sh",
121
+ "modes": ["cold-command"],
122
+ "notes": "The current public iOS workflow attempt failed while starting WebDriverAgent, so fixture-specific setup hardening is required before timing rows."
123
+ },
124
+ {
125
+ "id": "detox",
126
+ "label": "Detox",
127
+ "status": "planned",
128
+ "collector": "scripts/benchmark-command.sh",
129
+ "modes": ["warm-suite"],
130
+ "notes": "Requires a React Native fixture with project-local native build targets and a test harness."
131
+ }
132
+ ],
133
+ "nextSlices": [
134
+ {
135
+ "id": "android-native-rows",
136
+ "status": "done",
137
+ "description": "Collect 20-run Android rows for the generated native workflow on a clean emulator."
138
+ },
139
+ {
140
+ "id": "react-native-expo-fixture",
141
+ "status": "done",
142
+ "description": "Add a generated React Native/Expo fixture with stable test IDs, deep links, and matching ZMR workflow files."
143
+ },
144
+ {
145
+ "id": "flutter-semantics-fixture",
146
+ "status": "next",
147
+ "description": "Add a Flutter fixture that proves app-level semantics support without widget-tree claims."
148
+ },
149
+ {
150
+ "id": "warm-suite-runner",
151
+ "status": "later",
152
+ "description": "Add suite-level benchmark collection so bridge prewarm and repeated scenario execution are measured separately from cold command startup."
153
+ }
154
+ ]
155
+ }
@@ -0,0 +1,95 @@
1
+ # Benchmark Lab v1
2
+
3
+ Benchmark Lab v1 is the public evidence plan for ZMR. It keeps framework
4
+ fixtures, runner adapters, timing modes, and claim boundaries explicit so speed
5
+ or reliability statements are reproducible instead of anecdotal.
6
+ It is the framework-level evidence map for React Native, Expo, Flutter, native
7
+ Android, and native iOS fixtures.
8
+
9
+ The machine-readable source is
10
+ [benchmark-lab-v1.json](benchmark-lab-v1.json). Render or validate it with:
11
+
12
+ ```bash
13
+ zmr-benchmark-lab --manifest docs/benchmarks/benchmark-lab-v1.json --format markdown
14
+ zmr-benchmark-lab --manifest docs/benchmarks/benchmark-lab-v1.json --format json
15
+ ```
16
+
17
+ ## Direction
18
+
19
+ ZMR should compete first where mobile teams already make framework choices:
20
+ React Native, Expo, Flutter, native Android, and native iOS. The lab is not a
21
+ generic benchmark scoreboard. Each fixture must represent an app workflow a
22
+ developer can inspect, build, run, and adapt.
23
+
24
+ The near-term wedge is agent-native mobile testing: structured observation,
25
+ selector-grade actions, trace-first debugging, and reviewable scenario
26
+ generation. Benchmarks should prove the local runner path is fast and reliable
27
+ without overstating what one fixture demonstrates.
28
+
29
+ ## Fixtures
30
+
31
+ | Fixture | Framework | Platforms | Status | Scenario |
32
+ | --- | --- | --- | --- | --- |
33
+ | Generated native iOS workflow | native-ios | iOS | evidence committed | `examples/ios-shim-workflow.json` |
34
+ | Generated native Android workflow | native-android | Android | evidence committed | `examples/android-workflow.json` |
35
+ | React Native and Expo workflow | react-native-expo | Android, iOS | fixture available | `examples/react-native-expo-workflow.json` |
36
+ | Flutter semantics workflow | flutter | Android, iOS | planned | pending |
37
+
38
+ The first richer iOS evidence pack is
39
+ [2026-06-09 iOS simulator workflow comparison](2026-06-09-ios-workflow-comparison.md).
40
+ It covers launch, profile entry, catalog scroll, item detail, save, review, and
41
+ final-state assertion on the generated native iOS demo app.
42
+
43
+ The first Android workflow evidence pack is
44
+ [2026-06-09 Android emulator workflow](2026-06-09-android-workflow.md). It
45
+ records 20 repeated ZMR runs through the platform UIAutomator path.
46
+
47
+ The React Native/Expo fixture is available through
48
+ `scripts/create-react-native-expo-demo-app.sh`. It generates an Expo app with
49
+ stable `testID` values, accessibility labels, a deep-link scheme, and matching
50
+ Android/iOS ZMR workflow scenarios. Public timing rows are still pending.
51
+
52
+ ## Runner Adapters
53
+
54
+ | Adapter | Status | Collector | Notes |
55
+ | --- | --- | --- | --- |
56
+ | ZMR | available | `scripts/benchmark.sh` | Candidate runner for all fixtures. |
57
+ | Maestro | evidence committed | `scripts/benchmark-command.sh` | Use YAML flows that match the same visible app state. |
58
+ | Appium | partial | `scripts/benchmark-command.sh` | The current public iOS workflow attempt failed while starting WebDriverAgent, so setup needs hardening before timing rows. |
59
+ | Detox | planned | `scripts/benchmark-command.sh` | Requires a React Native fixture with native build targets and a project-local test harness. |
60
+
61
+ Other local runner rows can be collected with the same generic command wrapper,
62
+ but public docs should only name tools when a fixture-specific evidence pack is
63
+ available or when a status row explains why evidence is missing.
64
+
65
+ ## Modes
66
+
67
+ | Mode | Meaning |
68
+ | --- | --- |
69
+ | Cold command | Measures the shell command a user runs, including runner startup. |
70
+ | Warm suite | Prepares the app, device, and runner bridge before timed rows, isolating repeated scenario execution. |
71
+ | Native floor | Measures a direct platform shim path as diagnostic lower-bound evidence, not a product comparison. |
72
+
73
+ Cold-command rows are the best default for user-facing claims. Warm-suite rows
74
+ are the best way to prove the core execution path can become faster without
75
+ hiding setup work. Native-floor rows show where remaining overhead lives.
76
+
77
+ ## Claim Rules
78
+
79
+ - Use at least 20 candidate rows and 20 baseline rows for public comparison
80
+ evidence.
81
+ - Require 100% candidate pass rate and zero candidate failures for any public
82
+ speed claim.
83
+ - Compare only rows with the same host class, OS/toolchain, device state, app
84
+ id, app build, scenario, and timing mode.
85
+ - Commit sanitized result rows and commands. Do not commit raw trace logs when
86
+ they contain local absolute paths or app data.
87
+ - Phrase every result as fixture-specific evidence. Do not describe one lab run
88
+ as a universal product claim.
89
+
90
+ ## Next Slices
91
+
92
+ 1. Add a Flutter semantics fixture that proves app-level Android/iOS support
93
+ without claiming widget-tree automation.
94
+ 2. Add warm-suite collection so bridge prewarm and repeated execution can be
95
+ measured separately from command startup.
package/docs/clients.md CHANGED
@@ -3,6 +3,22 @@
3
3
  ZMR clients are reference implementations for the JSON-RPC protocol used by
4
4
  `zmr serve`. They are intentionally small and dependency-light.
5
5
 
6
+ TypeScript and Python are the most common starting points for app teams and
7
+ agent harnesses. Go, Rust, Swift, and Kotlin clients are reference integrations
8
+ for teams that want to embed the protocol from those ecosystems. Go and Rust
9
+ include typed trace discovery and scenario validation helpers for host-side
10
+ agent loops; Swift and Kotlin include lightweight discovery and validation
11
+ helpers for host-side automation.
12
+
13
+ | Language | Entry point | Example |
14
+ | --- | --- | --- |
15
+ | TypeScript | `clients/typescript/index.mjs` + `index.d.ts` | `node clients/typescript/examples/fake-session.mjs` |
16
+ | Python | `clients/python/zmr_client.py` + `pyproject.toml` | `python3 clients/python/examples/fake_session.py` |
17
+ | Go | `clients/go/zmr/client.go` | `go run ./clients/go/examples/fake-session` |
18
+ | Rust | `clients/rust/src/lib.rs` | `cargo run --manifest-path clients/rust/Cargo.toml --example fake_session` |
19
+ | Swift | `clients/swift/Sources/ZMRClient` | `swift build --package-path clients/swift` |
20
+ | Kotlin | `clients/kotlin/src/main/kotlin/dev/zmr` | `gradle -p clients/kotlin build` |
21
+
6
22
  ## What Clients Mean
7
23
 
8
24
  The runner is still the Zig binary. A client starts or connects to:
package/docs/demo.md CHANGED
@@ -17,10 +17,13 @@ The script builds `zig-out/bin/zmr`, then runs:
17
17
  - `zmr validate examples/android-app-referral-deep-link.json`
18
18
  - `zmr validate examples/android-app-error-state.json`
19
19
  - `zmr validate examples/android-shim-smoke.json`
20
+ - `zmr validate examples/android-workflow.json`
21
+ - `zmr validate examples/react-native-expo-workflow.json`
20
22
  - `zmr validate examples/ios-smoke.json`
21
23
  - `zmr validate examples/ios-dev-client-open-link.json`
22
24
  - `zmr validate examples/ios-dev-client-route-snapshot.json`
23
25
  - `zmr validate examples/ios-shim-smoke.json`
26
+ - `zmr validate examples/ios-shim-workflow.json`
24
27
  - expected-failing `zmr validate --json` output that shows `fieldPath`, `line`,
25
28
  and `column` for invalid scenarios covered by `schemas/validate-output.schema.json`
26
29
  - `zmr doctor --adb ./tests/fake-adb.sh --xcrun ./tests/fake-xcrun.sh --ios-shim ./tests/fake-ios-shim.sh`
@@ -81,6 +84,8 @@ The Swift and Kotlin reference client flows verify host-side native-language
81
84
  agent/test-harness integration for iOS and Android teams.
82
85
  The fake Android shim flow exercises shim-backed hierarchy, wait, tap, type,
83
86
  hide-keyboard, and snapshot handling.
87
+ The React Native/Expo workflow example validates the framework fixture scenario
88
+ that uses deep links, accessibility labels, and stable `testID` values.
84
89
  The fake iOS flow exercises simulator lifecycle, deep-link opening, screenshot
85
90
  artifact capture, log capture, and snapshot trace writing. The fake iOS shim
86
91
  flow exercises shim-backed hierarchy, wait, tap, type, hide-keyboard, and
@@ -88,7 +93,11 @@ snapshot handling.
88
93
 
89
94
  Load any generated `.zmrtrace` in `viewer/index.html` to inspect the replay
90
95
  timeline, payloads, screenshot, UI tree, selected node details, and raw
91
- artifacts side-by-side.
96
+ artifacts side-by-side. When the viewer and the bundle are served over HTTP,
97
+ link straight to a loaded trace with `viewer/index.html?bundle=<url>` — useful
98
+ for CI artifact links and shared triage.
99
+
100
+ ![ZMR trace viewer with a loaded Android demo trace showing the timeline, device screenshot, and UI tree](assets/viewer-android.png)
92
101
 
93
102
  ## Real Android Pilot Demo
94
103
 
@@ -185,6 +194,32 @@ zmr run /tmp/zmr-android-demo/.zmr/android-smoke.json \
185
194
  The scenario launches the app, waits for visible text, taps a button, types
186
195
  text into a field, and captures a trace-backed snapshot.
187
196
 
197
+ ## React Native And Expo Fixture
198
+
199
+ To generate a public React Native and Expo app with stable test IDs,
200
+ accessibility labels, deep-link routing, and matching Android/iOS ZMR workflow
201
+ scenarios:
202
+
203
+ ```bash
204
+ npx zmr-create-react-native-expo-demo-app --out /tmp/zmr-rn-expo-demo
205
+ cd /tmp/zmr-rn-expo-demo
206
+ bun install
207
+ bunx expo start
208
+ ```
209
+
210
+ After installing a development build on the target device, run the generated
211
+ scenario for the platform you are measuring:
212
+
213
+ ```bash
214
+ zmr run .zmr/react-native-expo-android-workflow.json \
215
+ --device emulator-5554 \
216
+ --app-id com.example.mobiletest \
217
+ --trace-dir traces/zmr-rn-expo-android
218
+ ```
219
+
220
+ The fixture includes `expo-dev-client` and is available for benchmark
221
+ collection, but it does not have public timing rows yet.
222
+
188
223
  ## Real iOS Simulator Demo
189
224
 
190
225
  To generate a small public demo app with the ZMR XCTest shim already installed:
@@ -20,6 +20,16 @@ Keep generated ZMR files under `.zmr/` and run the wizard from the app repo:
20
20
  npx zmr-wizard --app-id com.example.mobiletest --package-json
21
21
  ```
22
22
 
23
+ To inspect a generated public fixture with a longer workflow:
24
+
25
+ ```bash
26
+ npx zmr-create-react-native-expo-demo-app --out /tmp/zmr-rn-expo-demo
27
+ ```
28
+
29
+ That fixture includes `expo-dev-client`, deep-link setup, stable `testID`
30
+ values, accessibility labels, and Android/iOS ZMR workflow scenarios under
31
+ `.zmr/`.
32
+
23
33
  ## Expo
24
34
 
25
35
  Expo development builds work like React Native apps once they are installed on a
package/docs/npm.md CHANGED
@@ -33,6 +33,9 @@ The package exposes:
33
33
  matching `.zmr/` smoke scenario for public demos and emulator pilots.
34
34
  - `zmr-create-ios-demo-app`: creates a generic SwiftUI simulator app with
35
35
  `.zmr/` scenarios and the iOS shim already installed for public demos.
36
+ - `zmr-create-react-native-expo-demo-app`: creates a generic React Native and
37
+ Expo app with stable `testID` values, accessibility labels, deep-link config,
38
+ and Android/iOS `.zmr/` workflow scenarios.
36
39
  - `zmr-demo-android`: creates, installs, and runs the generated Android demo
37
40
  through a real emulator/device.
38
41
  - `zmr-demo-ios`: creates, builds, and runs the generated iOS simulator demo
@@ -216,6 +219,24 @@ zmr run /tmp/zmr-android-demo/.zmr/android-smoke.json \
216
219
  --trace-dir /tmp/zmr-android-demo/traces/android-demo
217
220
  ```
218
221
 
222
+ ## React Native And Expo Demo Fixture
223
+
224
+ Generate a public React Native and Expo app when you need a framework-level
225
+ benchmark fixture before collecting timing rows:
226
+
227
+ ```bash
228
+ npx zmr-create-react-native-expo-demo-app --out /tmp/zmr-rn-expo-demo
229
+ cd /tmp/zmr-rn-expo-demo
230
+ bun install
231
+ bunx expo start
232
+ ```
233
+
234
+ The generated app includes `expo-dev-client`, stable `testID` values,
235
+ accessibility labels, an Expo deep-link scheme, and platform-specific ZMR
236
+ workflow scenarios under `.zmr/`. After installing a development build on the
237
+ target device, run the generated Android or iOS workflow scenario from the app
238
+ directory.
239
+
219
240
  ## iOS Demo App
220
241
 
221
242
  For a clean public iOS demo that does not depend on a private app:
@@ -349,8 +370,12 @@ on the tag workflow:
349
370
 
350
371
  - Package: `zeno-mobile-runner`
351
372
  - Provider: GitHub Actions
352
- - Repository: `johnmikel/zeno-mobile-runner`
353
- - Workflow file: `release.yml`
373
+ - Organization or user: `johnmikel`
374
+ - Repository: `zeno-mobile-runner`
375
+ - Workflow filename: `release.yml`
376
+ - Environment name: leave blank unless the release job also declares a GitHub
377
+ deployment environment.
378
+ - Allowed actions: `npm publish`
354
379
 
355
380
  The release workflow already requests `id-token: write`, builds the npm tarball
356
381
  from the tag, attests the generated release artifacts, uploads the GitHub
@@ -361,6 +386,23 @@ Trusted publishing requires a current npm runtime. The tag workflow uses Node
361
386
  24 so the npm CLI can exchange the GitHub Actions OIDC identity for publish
362
387
  authorization.
363
388
 
389
+ With `npm@11.10.0` or newer, maintainers can also configure the same trust
390
+ relationship from an authenticated local shell:
391
+
392
+ ```bash
393
+ npm trust list zeno-mobile-runner
394
+ npm trust github zeno-mobile-runner \
395
+ --repo johnmikel/zeno-mobile-runner \
396
+ --file release.yml \
397
+ --allow-publish
398
+ ```
399
+
400
+ If `npm trust` is not available, update npm or use the package settings page on
401
+ npmjs.com. A failed publish with `E404` for an existing package usually means
402
+ the trusted-publisher configuration is missing, points at a different GitHub
403
+ owner/repository/workflow filename, names an environment that the workflow does
404
+ not use, or does not allow `npm publish`.
405
+
364
406
  ### Manual publish with passkey or 2FA
365
407
 
366
408
  Use trusted publishing for normal tagged releases. If you need to publish a
@@ -1,4 +1,4 @@
1
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.1.8","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.1","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.1.8`.
5
+ Current runner version: `0.2.1`.
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.1.8","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.1","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.1.8","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.1","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.1.8","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.1","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.1.8","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.1","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.1.8","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
217
+ {"name":"zmr","version":"0.2.1","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.1.8","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.1","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
@@ -432,7 +432,7 @@ Request:
432
432
  Response:
433
433
 
434
434
  ```json
435
- {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.1.8","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"]}}
435
+ {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.1","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
436
  ```
437
437
 
438
438
  ### `trace.events`
@@ -514,7 +514,7 @@ Request:
514
514
  Response:
515
515
 
516
516
  ```json
517
- {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.1.8","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"]}}
517
+ {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.1","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
518
  ```
519
519
 
520
520
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
@@ -537,7 +537,7 @@ Request:
537
537
  Response:
538
538
 
539
539
  ```json
540
- {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.1.8","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"]}}
540
+ {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.1","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
541
  ```
542
542
 
543
543
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
@@ -4,6 +4,19 @@ ZMR scenarios are JSON so agents can generate and mutate them without a second
4
4
  DSL. JSON is strict, schema-validatable, and easy for agents and code generators
5
5
  to emit. Keep scenarios explicit, short, and biased toward stable selectors.
6
6
 
7
+ Scenarios can be written by hand, or generated review-first from the trace of
8
+ a live session:
9
+
10
+ ```mermaid
11
+ flowchart LR
12
+ SESSION["Live agent session<br/>or zmr run"] --> TRACE["Trace directory"]
13
+ TRACE --> DISCOVER["zmr discover / draft / explore<br/>--from-trace"]
14
+ DISCOVER --> CANDIDATE["Scenario candidate<br/>.zmr/discovered/*.json"]
15
+ CANDIDATE --> REVIEW["Human / agent review"]
16
+ REVIEW --> VALIDATE["zmr validate --json"]
17
+ VALIDATE --> CI["zmr run in CI<br/>report.html · junit.xml"]
18
+ ```
19
+
7
20
  ## Selector Strategy
8
21
 
9
22
  Prefer selectors in this order:
@@ -83,8 +96,10 @@ The example directory includes templates for common app flows:
83
96
  - `examples/android-app-onboarding.json`
84
97
  - `examples/android-app-referral-deep-link.json`
85
98
  - `examples/android-app-error-state.json`
99
+ - `examples/android-workflow.json`
86
100
  - `examples/ios-dev-client-open-link.json`
87
101
  - `examples/ios-dev-client-route-snapshot.json`
102
+ - `examples/ios-shim-workflow.json`
88
103
 
89
104
  Run `zmr validate --json <scenario.json>` before touching a device. Invalid
90
105
  scenarios report `fieldPath`, `line`, and `column` when ZMR can identify the
@@ -3,6 +3,15 @@
3
3
  ZMR traces are debugging artifacts. They can contain sensitive app state even
4
4
  when scenario files are generic.
5
5
 
6
+ ```mermaid
7
+ flowchart LR
8
+ RUN["zmr run / live session"] --> DIR["Trace directory<br/>events.jsonl · screenshots ·<br/>UI trees · logs · timings"]
9
+ DIR --> REPORT["zmr report<br/>report.html · junit.xml"]
10
+ DIR --> EXPLAIN["zmr explain<br/>failure diagnosis"]
11
+ DIR --> EXPORT["zmr export --redact<br/>.zmrtrace bundle"]
12
+ EXPORT --> VIEWER["Static trace viewer<br/>or shared evidence"]
13
+ ```
14
+
6
15
  Raw trace directories may include:
7
16
 
8
17
  - screenshots
@@ -188,6 +188,12 @@ app targets. Pass `--project` explicitly for still-ambiguous multi-project
188
188
  workspaces. Run with `--ios-shim ./.zmr/ios-shim` or set
189
189
  `tools.iosShimPath` in `.zmr/config.json`.
190
190
 
191
+ A clean prebuild can push the shim's first `build-for-testing` through a full
192
+ native dependency compile. ZMR waits up to 90 minutes by default; on slower CI
193
+ hardware, raise the ceiling with the `ZMR_IOS_SHIM_TIMEOUT_MS` environment
194
+ variable (milliseconds), for example `ZMR_IOS_SHIM_TIMEOUT_MS=10800000` for
195
+ three hours.
196
+
191
197
  If a real iOS run fails with CoreSimulator or Xcode cache errors such as
192
198
  `Operation not permitted`, `CoreSimulatorService connection became invalid`, or
193
199
  an unexpected workspace/build database error, rerun from a normal terminal or CI