zeno-mobile-runner 0.2.15 → 0.2.17

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 (77) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +20 -7
  3. package/FEATURES.md +29 -20
  4. package/README.md +73 -57
  5. package/SECURITY.md +11 -6
  6. package/clients/README.md +8 -7
  7. package/clients/go/README.md +2 -2
  8. package/clients/kotlin/README.md +2 -2
  9. package/clients/kotlin/build.gradle.kts +1 -1
  10. package/clients/python/README.md +2 -1
  11. package/clients/python/pyproject.toml +1 -1
  12. package/clients/rust/Cargo.lock +1 -1
  13. package/clients/rust/Cargo.toml +1 -1
  14. package/clients/rust/README.md +2 -2
  15. package/clients/swift/README.md +2 -2
  16. package/clients/typescript/README.md +2 -1
  17. package/clients/typescript/package.json +1 -1
  18. package/docs/adr/0001-agent-native-runner-boundary.md +1 -1
  19. package/docs/adr/README.md +7 -5
  20. package/docs/agent-discovery.md +15 -15
  21. package/docs/ai-agents.md +30 -20
  22. package/docs/app-integration.md +59 -27
  23. package/docs/benchmarking.md +16 -8
  24. package/docs/benchmarks/README.md +3 -1
  25. package/docs/benchmarks/benchmark-lab-v1.md +1 -1
  26. package/docs/client-installation.md +18 -9
  27. package/docs/clients.md +7 -6
  28. package/docs/config.md +29 -15
  29. package/docs/demo.md +14 -9
  30. package/docs/expo-smoke.md +12 -18
  31. package/docs/frameworks.md +30 -21
  32. package/docs/install.md +63 -13
  33. package/docs/npm.md +45 -27
  34. package/docs/production-readiness.md +32 -17
  35. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  36. package/docs/protocol-versioning.md +5 -3
  37. package/docs/protocol.md +33 -18
  38. package/docs/scenario-authoring.md +15 -8
  39. package/docs/support-matrix.md +38 -0
  40. package/docs/trace-privacy.md +5 -3
  41. package/docs/troubleshooting.md +17 -14
  42. package/npm/app-config.mjs +2 -0
  43. package/npm/commands.mjs +4 -4
  44. package/npm/scaffold.mjs +2 -2
  45. package/package.json +2 -2
  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/schemas/README.md +6 -3
  51. package/schemas/import-output.schema.json +1 -1
  52. package/schemas/scenario.schema.json +2 -0
  53. package/schemas/zmr-config.schema.json +2 -1
  54. package/scripts/install-ios-shim.sh +39 -4
  55. package/scripts/public-metadata-guard.sh +101 -0
  56. package/shims/android/README.md +4 -3
  57. package/shims/android/protocol.md +3 -2
  58. package/shims/ios/README.md +8 -7
  59. package/shims/ios/ZMRShimUITestCase.swift +58 -17
  60. package/shims/ios/protocol.md +2 -1
  61. package/skills/zmr-mobile-testing/SKILL.md +9 -8
  62. package/src/android_emulator.zig +54 -5
  63. package/src/cli_import.zig +15 -2
  64. package/src/cli_output.zig +2 -0
  65. package/src/cli_run.zig +8 -0
  66. package/src/config.zig +3 -0
  67. package/src/errors.zig +3 -0
  68. package/src/ios_devices.zig +100 -0
  69. package/src/main.zig +1 -1
  70. package/src/mcp_protocol.zig +12 -9
  71. package/src/run_options.zig +4 -0
  72. package/src/scaffold.zig +10 -8
  73. package/src/scenario.zig +43 -0
  74. package/src/selector.zig +53 -9
  75. package/src/trace_json.zig +4 -0
  76. package/src/validation.zig +5 -0
  77. package/src/version.zig +1 -1
@@ -1,13 +1,14 @@
1
1
  # Production Readiness
2
2
 
3
- ZMR is a public developer preview. The npm package is live, release artifacts
4
- are signed by GitHub release attestations, and local app teams can collect
5
- repeatable Android, iOS simulator, and physical iOS evidence. ZMR should not be
6
- called production-stable until the gates below are met and kept passing.
3
+ ZMR is currently a public developer preview. The npm package is live, release
4
+ artifacts are signed by GitHub release attestations, and app teams can collect
5
+ repeatable Android, iPhone simulator, iPad simulator, and physical iOS/iPadOS
6
+ evidence today. The product should not be described as production-stable until
7
+ the gates below are passing and remain part of the release routine.
7
8
 
8
9
  ## Current Release Standard
9
10
 
10
- Every public release should satisfy:
11
+ Every public release should satisfy these checks before publishing artifacts:
11
12
 
12
13
  - `bash tests/docs-readiness-test.sh`
13
14
  - `bash tests/public-safety-test.sh`
@@ -18,14 +19,20 @@ Every public release should satisfy:
18
19
  - `./scripts/verify-release-artifacts.sh --dist dist`
19
20
  - at least one trace or benchmark report rendered with `zmr report --junit`,
20
21
  or a pilot wrapper run that produced both `report.html` and `junit.xml`
21
- - a fresh npm install smoke:
22
+ - a fresh curl installer smoke:
23
+
24
+ ```bash
25
+ ./install.sh --version 0.2.17 --dry-run
26
+ ```
27
+
28
+ - a fresh npm convenience smoke:
22
29
 
23
30
  ```bash
24
31
  npm install --save-dev zeno-mobile-runner
25
32
  npx zmr version --json
26
33
  ```
27
34
 
28
- Tagged releases are expected to build release archives, generate
35
+ Tagged releases should build release archives, generate
29
36
  `RELEASE_MANIFEST.json`, publish GitHub artifact attestations, upload release
30
37
  assets, and publish the npm tarball through trusted publishing after the npm
31
38
  package is configured with the `release.yml` trusted publisher.
@@ -36,12 +43,17 @@ workflow artifact for 30 days in addition to GitHub release assets.
36
43
 
37
44
  ## Product Gates Before 1.0
38
45
 
46
+ These gates separate "usable preview" from "production-stable product claim."
47
+
39
48
  | Area | Required evidence | Current status |
40
49
  | --- | --- | --- |
41
50
  | Android emulator | 20-run pilot gate with zero failures and trace/report artifacts | Supported by `zmr-pilot-gate` and demo app |
42
51
  | Android physical device | 20-run pilot gate on a real connected device | Supported by ADB flow; app teams must collect evidence |
43
- | iOS simulator | 20-run pilot gate with XCTest shim selectors, screenshots, and reports | Supported by iOS demo and app-local shim |
44
- | iOS physical device | 20-run pilot gate on a real trusted device | Supported for lifecycle and shim screenshots; needs repeated public evidence |
52
+ | iPhone simulator | 20-run pilot gate with XCTest shim selectors, screenshots, and reports | Supported by iOS demo and app-local shim |
53
+ | iPad simulator | 20-run pilot gate on an iPad simulator with tablet layout coverage | Same iOS simulator path; needs repeated public evidence before production-stable claims |
54
+ | iPhone physical device | 20-run pilot gate on a real trusted device | Supported for lifecycle and shim screenshots; needs repeated public evidence |
55
+ | iPad physical device | 20-run pilot gate on a real trusted iPad | Same iOS/iPadOS physical path; app teams must collect separate tablet evidence |
56
+ | tvOS / watchOS | Platform-specific lifecycle, shim, trace, and pilot evidence | Not supported in this preview |
45
57
  | React Native | Public setup guidance plus selector-grade app evidence using stable labels or ids | Guidance exists; repeated public demo evidence is still needed |
46
58
  | Expo | Public smoke, dev-client scaffold, and iOS/Android run evidence | Basic iOS smoke is documented; repeated matrix evidence is still needed |
47
59
  | Flutter | Platform-level Android/iOS smoke using semantics, deep links, and screenshots | Supported at platform level; widget-tree claims are intentionally out of scope |
@@ -52,7 +64,8 @@ workflow artifact for 30 days in addition to GitHub release assets.
52
64
 
53
65
  ## Reliability Evidence
54
66
 
55
- Use repeated app-local pilots before making app or device claims:
67
+ Use repeated app-local pilots before making app or device claims. A single green
68
+ demo proves wiring; a 20-run pilot proves reliability for a target class:
56
69
 
57
70
  ```bash
58
71
  zmr-pilot-gate \
@@ -81,13 +94,13 @@ zmr-release-readiness \
81
94
  --json
82
95
  ```
83
96
 
84
- Keep the generated evidence in the app repository unless it is fully redacted
85
- and safe to publish.
97
+ Keep generated evidence in the app repository unless it is fully redacted and
98
+ safe to publish.
86
99
 
87
100
  ## Agentic Standard
88
101
 
89
- ZMR is agentic when an external agent can work from structured state instead of
90
- screenscraping or guessing:
102
+ ZMR meets the agent-first bar when an external agent can work from structured
103
+ state instead of screenshots, terminal prose, or guessed coordinates:
91
104
 
92
105
  - `zmr doctor --json` explains setup state and remediation.
93
106
  - `zmr schemas --json` exposes machine-readable contracts.
@@ -105,7 +118,7 @@ screenscraping or guessing:
105
118
  trace events, trace explanation, trace discovery/exploration, scenario
106
119
  validation, and redacted export.
107
120
 
108
- The safe discovery pattern is still external-agent-first: observe with
121
+ The safe discovery pattern remains external-agent-first: observe with
109
122
  `semantic_snapshot`, choose one typed action, record successful steps into a
110
123
  candidate scenario, validate it, rerun it deterministically, and require human
111
124
  review before committing generated tests.
@@ -114,10 +127,12 @@ review before committing generated tests.
114
127
 
115
128
  - Claim Android and iOS app-level support only for flows that pass local pilot
116
129
  evidence on the target device class.
130
+ - Claim iPad support separately from iPhone support. The runner path is shared,
131
+ but tablet layouts and size classes can change UI trees and selector outcomes.
117
132
  - Claim React Native and Expo support through app-level lifecycle, deep links,
118
133
  accessibility labels, selectors, screenshots, traces, and reports.
119
134
  - Claim Flutter support at the Android/iOS app level when the app exposes stable
120
135
  semantics, labels, ids, or deep links.
121
136
  - Do not claim Flutter widget-tree inspection, Dart state inspection, managed
122
- device-farm coverage, or a built-in autonomous test writer until those
123
- features exist and have public evidence.
137
+ device-farm coverage, tvOS, watchOS, or a built-in autonomous test writer until
138
+ those features exist and have public evidence.
@@ -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.17","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}
@@ -1,12 +1,14 @@
1
1
  # Protocol Versioning
2
2
 
3
- ZMR exposes two public automation surfaces:
3
+ Use this page when changing scenario JSON, JSON-RPC, MCP-visible schemas, trace
4
+ schemas, or stable error codes. ZMR exposes two public automation surfaces:
4
5
 
5
6
  - scenario JSON files consumed by `zmr run`
6
7
  - JSON-RPC methods exposed by `zmr serve`
7
8
 
8
- The current protocol version is a date string. Before `v1.0.0`, breaking changes
9
- are allowed only when the protocol version and changelog are updated together.
9
+ The current protocol version is a date string. Before `v1.0.0`, breaking
10
+ changes are allowed only when the protocol version and changelog are updated
11
+ together.
10
12
  `runner.capabilities` exposes this policy in machine-readable form:
11
13
 
12
14
  ```json
package/docs/protocol.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # ZMR JSON-RPC Protocol
2
2
 
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`.
3
+ ZMR exposes newline-delimited JSON-RPC 2.0 over stdio or localhost TCP. This is
4
+ the contract used by reference clients, agent harnesses, and the MCP server
5
+ internals. Each request is one JSON object followed by `\n`; each response is
6
+ one JSON object followed by `\n`.
4
7
 
5
- Current runner version: `0.2.15`.
8
+ Current runner version: `0.2.17`.
6
9
 
7
10
  Current protocol version: `2026-04-28`.
8
11
 
@@ -47,7 +50,7 @@ and protocol versions. The response is covered by
47
50
  `schemas/inspect-output.schema.json`:
48
51
 
49
52
  ```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"]}
53
+ {"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.17","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
54
  ```
52
55
 
53
56
  `zmr discover --from-trace <trace-dir> --out <scenario.json> --validate --json`
@@ -60,7 +63,7 @@ invent credentials, or commit files. The response is covered by
60
63
  `schemas/discover-output.schema.json`:
61
64
 
62
65
  ```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"]}
66
+ {"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.17","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
67
  ```
65
68
 
66
69
  `zmr explore --from-trace <trace-dir> --out <scenario.json> --goal <goal>
@@ -71,7 +74,7 @@ launch devices, crawl the app, invent missing actions, discover credentials, or
71
74
  commit files. The response is covered by `schemas/explore-output.schema.json`:
72
75
 
73
76
  ```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"]}
77
+ {"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.17","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
78
  ```
76
79
 
77
80
  `zmr draft --from-trace <trace-dir> --out <scenario.json> --json` is the lower
@@ -84,7 +87,7 @@ into fields, or commit files. The response is covered by
84
87
  `schemas/draft-output.schema.json`:
85
88
 
86
89
  ```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"]}
90
+ {"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.17","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
91
  ```
89
92
 
90
93
  `zmr draft --include-actions` additionally parses `events.jsonl` and prepends
@@ -214,7 +217,7 @@ installers, setup scripts, and generated clients. The response is covered by
214
217
  `schemas/version-output.schema.json`:
215
218
 
216
219
  ```json
217
- {"name":"zmr","version":"0.2.15","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
220
+ {"name":"zmr","version":"0.2.17","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
218
221
  ```
219
222
 
220
223
  ## Capabilities Output Contract
@@ -226,7 +229,7 @@ and method inventory for JSON-RPC clients. The result object is covered by
226
229
  iOS simulator, or physical iOS workflows are available.
227
230
 
228
231
  ```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"]}
232
+ {"name":"zmr","version":"0.2.17","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
233
  ```
231
234
 
232
235
  ## Doctor Output Contract
@@ -278,7 +281,7 @@ generated app-local file, direct paths for the config/scenario/matrix/agent
278
281
  files, first-run commands, and the generated script names:
279
282
 
280
283
  ```json
281
- {"ok":true,"mode":"app","dir":".","appId":"com.example.mobiletest","created":["./.zmr/config.json","./.zmr/android-smoke.json","./.zmr/ios-smoke.json","./.zmr/device-matrix.json","./.zmr/AGENTS.md"],"configPath":"./.zmr/config.json","androidScenarioPath":"./.zmr/android-smoke.json","iosScenarioPath":"./.zmr/ios-smoke.json","deviceMatrixPath":"./.zmr/device-matrix.json","agentInstructionsPath":"./.zmr/AGENTS.md","next":"zmr doctor --strict --json --config ./.zmr/config.json","nextCommands":["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"],"smokeCommands":["zmr run ./.zmr/android-smoke.json --device emulator-5554 --trace-dir ./traces/zmr-android","zmr run ./.zmr/ios-smoke.json --platform ios --device booted --trace-dir ./traces/zmr-ios"],"scriptCount":16,"scriptNames":["doctor","schemas","validate","android","androidReport","androidReliability","ios","iosReport","iosReliability","matrix","pilotGate","readiness","serve","mcp","explain","exportTrace"]}
284
+ {"ok":true,"mode":"app","dir":".","appId":"com.example.mobiletest","created":["./.zmr/config.json","./.zmr/android-smoke.json","./.zmr/ios-smoke.json","./.zmr/device-matrix.json","./.zmr/AGENTS.md"],"configPath":"./.zmr/config.json","androidScenarioPath":"./.zmr/android-smoke.json","iosScenarioPath":"./.zmr/ios-smoke.json","deviceMatrixPath":"./.zmr/device-matrix.json","agentInstructionsPath":"./.zmr/AGENTS.md","next":"zmr doctor --strict --json --config ./.zmr/config.json","nextCommands":["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"],"smokeCommands":["zmr run ./.zmr/android-smoke.json --device emulator-5554 --trace-dir ./traces/zmr-android --ensure-device","zmr run ./.zmr/ios-smoke.json --platform ios --device booted --trace-dir ./traces/zmr-ios --ensure-device"],"scriptCount":16,"scriptNames":["doctor","schemas","validate","android","androidReport","androidReliability","ios","iosReport","iosReliability","matrix","pilotGate","readiness","serve","mcp","explain","exportTrace"]}
282
285
  ```
283
286
 
284
287
  Agents should read `agentInstructionsPath` for the app-local operating note and
@@ -299,8 +302,9 @@ Single-scenario mode reports the created scenario and next validation command:
299
302
  ## Import Output Contract
300
303
 
301
304
  `zmr import flow-yaml <flow.yaml> --out .zmr/imported.json --json` converts a
302
- supported subset of mobile-flow YAML into native ZMR scenario JSON. The
303
- response is covered by `schemas/import-output.schema.json`:
305
+ supported subset of mobile-flow YAML into native ZMR scenario JSON. Keep the
306
+ generated JSON as the reviewed, deterministic scenario that agents and CI run.
307
+ The response is covered by `schemas/import-output.schema.json`:
304
308
 
305
309
  ```json
306
310
  {"ok":true,"format":"flow-yaml","source":"flows/login.yaml","out":".zmr/login-smoke.json","name":"Imported login smoke","appId":"com.example.mobiletest","stepCount":10,"next":"zmr validate .zmr/login-smoke.json","nextCommands":["zmr validate --json .zmr/login-smoke.json","zmr run .zmr/login-smoke.json --json --trace-dir traces/zmr-run"]}
@@ -360,7 +364,7 @@ zmr serve --transport stdio --platform ios --ios-device-type physical --device <
360
364
  zmr mcp --config .zmr/config.json --trace-dir traces/mcp-agent-session
361
365
  ```
362
366
 
363
- `runner.capabilities` reports `platforms: ["android","ios"]`, `platformSupport.ios.status: "supported"`, and legacy `iosPreview: false`. Android supports emulators and connected devices. iOS simulators use `simctl` for discovery, install, launch, stop, clear-state-by-uninstall, deep links, screenshots, logs, and snapshots. Physical iOS devices use `devicectl` for discovery, install, launch, deep-link launch, clear-state-by-uninstall, and best-effort stop. Selector-grade `ui.*` methods on iOS require a configured XCTest/XCUIAutomation shim command; without one they return `IosXCTestShimRequired`. With the shim configured, single-field `ui.tap`, selector-scoped `ui.type`, selector-scoped `ui.eraseText`, `wait.*`, and `assert.*` can execute directly through XCTest. Compound selectors continue to use the portable snapshot-matching fallback. iOS snapshot responses are bounded to common XCTest element families so traces stay usable on large apps. Physical iOS screenshot artifacts use the XCTest shim; physical-device log capture is intentionally limited in this release.
367
+ `runner.capabilities` reports `platforms: ["android","ios"]`, `platformSupport.ios.status: "supported"`, and legacy `iosPreview: false`. Android supports emulators and connected devices. iOS and iPadOS simulators use `simctl` for discovery, install, launch, stop, clear-state-by-uninstall, deep links, screenshots, logs, and snapshots. Physical iPhone and iPad devices use `devicectl` for discovery, install, launch, deep-link launch, clear-state-by-uninstall, and best-effort stop. Selector-grade `ui.*` methods on iOS require a configured XCTest/XCUIAutomation shim command; without one they return `IosXCTestShimRequired`. With the shim configured, single-field `ui.tap`, selector-scoped `ui.type`, selector-scoped `ui.eraseText`, `wait.*`, and `assert.*` can execute directly through XCTest. Compound selectors continue to use the portable snapshot-matching fallback. iOS snapshot responses are bounded to common XCTest element families so traces stay usable on large apps. Physical iOS screenshot artifacts use the XCTest shim; physical-device log capture is intentionally limited in this release.
364
368
 
365
369
  ## Core Methods
366
370
 
@@ -433,7 +437,7 @@ Request:
433
437
  Response:
434
438
 
435
439
  ```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"]}}
440
+ {"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.17","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
441
  ```
438
442
 
439
443
  ### `trace.events`
@@ -515,7 +519,7 @@ Request:
515
519
  Response:
516
520
 
517
521
  ```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"]}}
522
+ {"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.17","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
523
  ```
520
524
 
521
525
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
@@ -538,7 +542,7 @@ Request:
538
542
  Response:
539
543
 
540
544
  ```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"]}}
545
+ {"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.17","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
546
  ```
543
547
 
544
548
  Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
@@ -719,12 +723,15 @@ Current stable public codes:
719
723
 
720
724
  ## Selectors
721
725
 
722
- Selectors can combine fields. All provided fields must match the same visible node.
726
+ Selectors must be non-empty and can combine fields. Unknown selector keys are
727
+ rejected by `zmr validate`, JSON-RPC, and MCP tool handling. All provided fields
728
+ must match the same visible node.
723
729
 
724
730
  ```json
725
731
  {
726
732
  "id": "email-login-submit-button",
727
733
  "resourceId": "email-login-submit-button",
734
+ "stableId": "rid:email-login-submit-button:4",
728
735
  "text": "Sign in",
729
736
  "textContains": "Sign",
730
737
  "contentDesc": "Account",
@@ -733,6 +740,11 @@ Selectors can combine fields. All provided fields must match the same visible no
733
740
  }
734
741
  ```
735
742
 
743
+ Prefer app-owned `id`/`resourceId` values, accessibility identifiers, content
744
+ descriptions, labels, and stable text for committed scenarios. Use `stableId`
745
+ only as a fallback copied from the current `semantic_snapshot` when an agent
746
+ needs to act immediately and no stronger selector exists.
747
+
736
748
  ## Example
737
749
 
738
750
  ```json
@@ -744,7 +756,8 @@ Selectors can combine fields. All provided fields must match the same visible no
744
756
 
745
757
  ## Scenario-Only Flow Primitives
746
758
 
747
- Scenario JSON supports additional orchestration primitives for agent-grade mobile flows:
759
+ Scenario JSON supports additional orchestration primitives for agent-grade
760
+ mobile flows:
748
761
 
749
762
  - `waitAny`
750
763
  - `waitNotVisible`
@@ -757,4 +770,6 @@ Scenario JSON supports additional orchestration primitives for agent-grade mobil
757
770
  - `hideKeyboard`
758
771
  - `"optional": true` on any step
759
772
 
760
- These are intentionally explicit JSON structures instead of YAML conditionals, so agents can generate, validate, and mutate flows without parsing a second language.
773
+ These are intentionally explicit JSON structures instead of YAML conditionals,
774
+ so agents can generate, validate, and mutate flows without parsing a second
775
+ language.
@@ -1,11 +1,12 @@
1
1
  # Scenario Authoring
2
2
 
3
- ZMR scenarios are JSON so agents can generate and mutate them without a second
4
- DSL. JSON is strict, schema-validatable, and easy for agents and code generators
5
- to emit. Keep scenarios explicit, short, and biased toward stable selectors.
3
+ ZMR scenarios are JSON so agents, code generators, and CI scripts can create
4
+ and mutate them without learning another DSL. The parser is strict: validate
5
+ before a device run, keep flows explicit, and bias selectors toward app-owned
6
+ identifiers.
6
7
 
7
- Scenarios can be written by hand, or generated review-first from the trace of
8
- a live session:
8
+ Scenarios can be written by hand or generated review-first from the trace of a
9
+ live session:
9
10
 
10
11
  ```mermaid
11
12
  flowchart LR
@@ -19,17 +20,22 @@ flowchart LR
19
20
 
20
21
  ## Selector Strategy
21
22
 
22
- Prefer selectors in this order:
23
+ Prefer selectors in this order for committed scenarios:
23
24
 
24
25
  1. `id` or `resourceId` for app-owned controls.
25
26
  2. `contentDesc` for intentional accessibility labels.
26
27
  3. Exact `text` for stable product copy.
27
28
  4. `textContains` only for headings, errors, or partial copy that is expected to
28
29
  vary.
30
+ 5. `stableId` only as a fallback copied from the current `semantic_snapshot`
31
+ when no app-owned selector exists.
29
32
 
30
33
  Avoid selecting by text that includes user data, timestamps, counts, prices, or
31
34
  network-provided content. Prefer app-owned resource ids or accessibility identifiers
32
35
  over widening a selector until it matches unrelated nodes.
36
+ Treat `stableId` as a live-session fallback: it can unblock immediate agent
37
+ actions, but committed CI scenarios should prefer app-owned selectors because UI
38
+ tree shape and fallback IDs can change as layouts evolve.
33
39
 
34
40
  ## Waits And Assertions
35
41
 
@@ -81,7 +87,7 @@ The importer supports the common subset needed for smoke scenarios:
81
87
  `hideKeyboard`, `assertVisible`, `assertNotVisible`, `assertHealthy`,
82
88
  `openLink`, `back`,
83
89
  `scrollUntilVisible`, `takeScreenshot`, and simple wait commands. Review the
84
- generated JSON before committing it; native `.zmr/*.json` scenarios remain the
90
+ generated JSON before committing it. Native `.zmr/*.json` scenarios remain the
85
91
  runtime contract for agents and CI.
86
92
 
87
93
  Use `setLocation` before location-dependent assertions to set simulator or
@@ -115,4 +121,5 @@ The example directory includes templates for common app flows:
115
121
 
116
122
  Run `zmr validate --json <scenario.json>` before touching a device. Invalid
117
123
  scenarios report `fieldPath`, `line`, and `column` when ZMR can identify the
118
- source location.
124
+ source location. Unknown root, step, and selector fields are rejected so typos do
125
+ not silently change test intent.
@@ -0,0 +1,38 @@
1
+ # Support Matrix
2
+
3
+ Use this page to decide what ZMR can claim today, what needs app-local proof,
4
+ and what is still outside the preview. ZMR is intended as an AI-agent-first
5
+ replacement path for mobile automation: agents observe UI state, choose typed
6
+ actions, validate generated scenarios, and export trace evidence that can replay
7
+ in CI without an LLM.
8
+
9
+ Support is claimed only when ZMR has lifecycle, observation, selector/action,
10
+ trace, and repeat-run evidence for the target class.
11
+
12
+ | Target | Status | Evidence standard | Notes |
13
+ | --- | --- | --- | --- |
14
+ | Android emulator | Supported | Public demo smoke plus 20-run pilot gate | ADB/UI Automator, optional Android shim, emulator lifecycle helpers |
15
+ | Android physical device | Supported, app-team evidence required | 20-run pilot gate on a connected device | Same ADB path; publish only redacted app-safe evidence |
16
+ | iPhone simulator | Supported | Public iOS simulator demo smoke plus shim traces | `simctl` lifecycle with app-local XCTest/XCUIAutomation shim for selector-grade actions |
17
+ | iPad simulator | Supported, evidence-needed | iPad simulator smoke plus 20-run pilot gate before production claims | Uses the same iOS simulator path; teams should verify tablet layouts and size-class branches |
18
+ | iPhone physical device | Supported, validate locally | 20-run pilot gate on a trusted device | `devicectl` lifecycle plus XCTest shim, subject to signing, Developer Mode, and Xcode availability |
19
+ | iPad physical device | Supported, evidence-needed | 20-run pilot gate on a trusted iPad before production claims | Uses the same physical iOS/iPadOS path; keep it a separate row because tablet UI can diverge |
20
+ | tvOS simulator/device | Not supported in this preview | Separate proof of concept, shim, destination, and trace evidence required | Reasonable Apple-platform research after iPad evidence, but not a 1.0 dependency |
21
+ | watchOS simulator/device | Not supported in this preview | Separate companion/pairing/lifecycle design and evidence required | Treat as customer-driven roadmap work, not a 1.0 dependency |
22
+ | Cloud device farms | Not included | Adapter and provider-specific evidence required | Current focus is local and self-managed devices |
23
+
24
+ ## Evidence Policy
25
+
26
+ - Production-stable support requires a 20-run pilot gate with zero failures and
27
+ redacted trace/report artifacts.
28
+ - iPad evidence must stay separate from iPhone evidence. The runner path is
29
+ shared, but layouts, split views, and size-class behavior can produce
30
+ different UI trees and selector outcomes.
31
+ - App-owned selectors should come first: `resourceId`/`id`, accessibility
32
+ identifiers, content descriptions, accessibility labels, then stable visible
33
+ text.
34
+ - Use `stableId` only as a fallback copied from a current semantic snapshot. It
35
+ is useful for immediate agent actions; committed CI scenarios should prefer
36
+ app-owned identifiers.
37
+ - tvOS and watchOS should stay out of public support claims until their platform
38
+ lifecycles, shim protocols, and trace evidence exist.
@@ -1,7 +1,8 @@
1
1
  # Trace Privacy
2
2
 
3
- ZMR traces are debugging artifacts. They can contain sensitive app state even
4
- when scenario files are generic.
3
+ ZMR traces are product-debugging artifacts. Treat them as sensitive by default:
4
+ even a generic scenario can capture private UI state, logs, identifiers, or
5
+ typed inputs from the app under test.
5
6
 
6
7
  ```mermaid
7
8
  flowchart LR
@@ -24,7 +25,8 @@ Raw trace directories may include:
24
25
 
25
26
  ## Sharing Rules
26
27
 
27
- - Disable unnecessary raw artifacts in `.zmr/config.json`:
28
+ - Disable unnecessary raw artifacts in `.zmr/config.json` before collecting
29
+ traces that may leave a trusted machine:
28
30
 
29
31
  ```json
30
32
  {
@@ -1,6 +1,7 @@
1
1
  # Troubleshooting
2
2
 
3
- Start with structured diagnostics instead of reading terminal output by hand:
3
+ Start with structured diagnostics. They give humans, agents, and CI scripts the
4
+ same setup state, error codes, field paths, and remediation hints:
4
5
 
5
6
  ```bash
6
7
  zmr doctor --json
@@ -13,25 +14,25 @@ zmr explain traces/zmr-android
13
14
  `zmr doctor --json` is the first command to run when setup is unclear. It
14
15
  reports Zig, ADB, Android device count, `xcrun`, iOS simulator state, physical
15
16
  iOS device state, and configured Android/iOS shim command paths in a
16
- machine-readable shape that scripts and agents can inspect. Device readiness
17
- checks report `warning` when ADB sees zero devices, `xcrun` sees zero booted
18
- iOS simulators, `devicectl` sees zero paired physical iOS devices, or all
19
- listed physical devices are disconnected/unavailable, with stable
17
+ machine-readable shape. Device readiness checks report `warning` when ADB sees
18
+ zero devices, `xcrun` sees zero booted iOS simulators, `devicectl` sees zero
19
+ paired physical iOS devices, or all listed physical devices are
20
+ disconnected/unavailable, with stable
20
21
  `setup.android.no_devices`, `setup.ios.no_booted_simulators`,
21
22
  `setup.ios.no_physical_devices`, and
22
23
  `setup.ios.no_ready_physical_devices` error codes.
23
24
  Missing tool and shim checks also include stable setup codes such as
24
25
  `setup.adb.not_found` and `setup.android_shim.not_found`.
25
26
  By default `doctor` exits zero after printing diagnostics so interactive setup
26
- can keep going; add `--strict` when CI or install scripts should exit non-zero
27
+ can keep going. Add `--strict` when CI or install scripts should exit non-zero
27
28
  for any warning or missing check.
28
- When run with `--config .zmr/config.json`, it
29
- first reports whether the config file itself loaded, then validates configured
30
- smoke scenario files from `android.smokeScenario` and
31
- `ios.smokeScenario` so app-local setup mistakes fail before device orchestration
32
- starts. Config files with wrong types, such as string values for boolean
33
- artifact controls, unknown fields, such as misspelled `smokeScenario`, or empty strings
34
- where paths/app ids/script commands are required are reported as `config` warnings.
29
+
30
+ When run with `--config .zmr/config.json`, `doctor` first reports whether the
31
+ config file loaded, then validates configured smoke scenario files from
32
+ `android.smokeScenario` and `ios.smokeScenario` so app-local setup mistakes fail
33
+ before device orchestration starts. Config files with wrong types, unknown
34
+ fields such as misspelled `smokeScenario`, or empty strings where paths, app
35
+ ids, or script commands are required are reported as `config` warnings.
35
36
  Those warnings include `fieldPath` in JSON mode when ZMR can identify the
36
37
  invalid `.zmr/config.json` key, plus stable `errorCode` values for setup
37
38
  automation.
@@ -109,6 +110,7 @@ zmr validate --json .zmr/android-smoke.json
109
110
  The JSON output includes `errorCode`, `fieldPath`, `line`, and `column` when ZMR
110
111
  can locate the source. Fix schema and selector mistakes there first; device
111
112
  state debugging is slower and less reliable when the scenario itself is invalid.
113
+ Unknown root, step, and selector fields are rejected to catch typos early.
112
114
 
113
115
  ## Android Device Issues
114
116
 
@@ -234,7 +236,8 @@ with the `appState` command above.
234
236
 
235
237
  ## Trace And Failure Issues
236
238
 
237
- When a run fails, do not rerun blindly. Inspect the recorded failure:
239
+ When a run fails, inspect the recorded failure before changing selectors or
240
+ rerunning:
238
241
 
239
242
  ```bash
240
243
  zmr explain traces/zmr-android
@@ -71,12 +71,14 @@ export function appConfig(appId, { android = true, ios = true, androidShim = "",
71
71
  defaultDevice: "emulator-5554",
72
72
  smokeScenario: ".zmr/android-smoke.json",
73
73
  traceDir: "traces/zmr-android",
74
+ ensureDevice: true,
74
75
  },
75
76
  ios: {
76
77
  enabled: ios,
77
78
  defaultDevice: "booted",
78
79
  smokeScenario: ".zmr/ios-smoke.json",
79
80
  traceDir: "traces/zmr-ios",
81
+ ensureDevice: true,
80
82
  },
81
83
  artifacts: {
82
84
  screenshots: true,
package/npm/commands.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  export function smokeRunCommand({ platform, androidShim = "", iosShim = "" }) {
2
2
  if (platform === "android") {
3
- const args = ["zmr", "run", ".zmr/android-smoke.json", "--device", "emulator-5554", "--trace-dir", "traces/zmr-android"];
3
+ const args = ["zmr", "run", ".zmr/android-smoke.json", "--device", "emulator-5554", "--trace-dir", "traces/zmr-android", "--ensure-device"];
4
4
  if (androidShim) args.push("--android-shim", androidShim);
5
5
  return shellJoin(args);
6
6
  }
7
7
  if (platform === "ios") {
8
- const args = ["zmr", "run", ".zmr/ios-smoke.json", "--platform", "ios", "--device", "booted", "--trace-dir", "traces/zmr-ios"];
8
+ const args = ["zmr", "run", ".zmr/ios-smoke.json", "--platform", "ios", "--device", "booted", "--trace-dir", "traces/zmr-ios", "--ensure-device"];
9
9
  if (iosShim) args.push("--ios-shim", iosShim);
10
10
  return shellJoin(args);
11
11
  }
@@ -76,10 +76,10 @@ export function reliabilityCommand({ scenario, platform = "", device, appId, xcr
76
76
 
77
77
  export function devClientRunCommand({ platform }) {
78
78
  if (platform === "android") {
79
- return "zmr run .zmr/android-dev-client-smoke.json --device emulator-5554 --trace-dir traces/zmr-android-dev-client";
79
+ return "zmr run .zmr/android-dev-client-smoke.json --device emulator-5554 --trace-dir traces/zmr-android-dev-client --ensure-device";
80
80
  }
81
81
  if (platform === "ios") {
82
- return "zmr run .zmr/ios-dev-client-open-link.json --platform ios --device booted --trace-dir traces/zmr-ios-dev-client";
82
+ return "zmr run .zmr/ios-dev-client-open-link.json --platform ios --device booted --trace-dir traces/zmr-ios-dev-client --ensure-device";
83
83
  }
84
84
  throw new Error(`unsupported dev-client platform: ${platform}`);
85
85
  }
package/npm/scaffold.mjs CHANGED
@@ -139,12 +139,12 @@ export function appInitOutput(appRoot, appId, plan, { packageScripts = false } =
139
139
  } else {
140
140
  if (androidScenarioPath) {
141
141
  smokeCommands.push(
142
- `zmr run ${shellQuote(androidScenarioPath)} --device emulator-5554 --trace-dir ${shellQuote(pathJoin(appRoot, "traces", "zmr-android"))}`,
142
+ `zmr run ${shellQuote(androidScenarioPath)} --device emulator-5554 --trace-dir ${shellQuote(pathJoin(appRoot, "traces", "zmr-android"))} --ensure-device`,
143
143
  );
144
144
  }
145
145
  if (iosScenarioPath) {
146
146
  smokeCommands.push(
147
- `zmr run ${shellQuote(iosScenarioPath)} --platform ios --device booted --trace-dir ${shellQuote(pathJoin(appRoot, "traces", "zmr-ios"))}`,
147
+ `zmr run ${shellQuote(iosScenarioPath)} --platform ios --device booted --trace-dir ${shellQuote(pathJoin(appRoot, "traces", "zmr-ios"))} --ensure-device`,
148
148
  );
149
149
  }
150
150
  }