agent-device 0.7.6 → 0.7.10

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.
@@ -310,7 +310,7 @@ extension RunnerTests {
310
310
  compact: command.compact ?? false,
311
311
  depth: command.depth,
312
312
  scope: command.scope,
313
- raw: command.raw ?? false,
313
+ raw: command.raw ?? false
314
314
  )
315
315
  if options.raw {
316
316
  needsPostSnapshotInteractionDelay = true
@@ -68,14 +68,14 @@ extension RunnerTests {
68
68
  let outputSettings: [String: Any] = [
69
69
  AVVideoCodecKey: AVVideoCodecType.h264,
70
70
  AVVideoWidthKey: Int(dimensions.width),
71
- AVVideoHeightKey: Int(dimensions.height),
71
+ AVVideoHeightKey: Int(dimensions.height)
72
72
  ]
73
73
  let input = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
74
74
  input.expectsMediaDataInRealTime = true
75
75
  let attributes: [String: Any] = [
76
76
  kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
77
77
  kCVPixelBufferWidthKey as String: Int(dimensions.width),
78
- kCVPixelBufferHeightKey as String: Int(dimensions.height),
78
+ kCVPixelBufferHeightKey as String: Int(dimensions.height)
79
79
  ]
80
80
  let adaptor = AVAssetWriterInputPixelBufferAdaptor(
81
81
  assetWriterInput: input,
@@ -82,11 +82,11 @@ extension RunnerTests {
82
82
  x: Double(rootSnapshot.frame.origin.x),
83
83
  y: Double(rootSnapshot.frame.origin.y),
84
84
  width: Double(rootSnapshot.frame.size.width),
85
- height: Double(rootSnapshot.frame.size.height),
85
+ height: Double(rootSnapshot.frame.size.height)
86
86
  ),
87
87
  enabled: rootSnapshot.isEnabled,
88
88
  hittable: rootHittable,
89
- depth: 0,
89
+ depth: 0
90
90
  )
91
91
  )
92
92
 
@@ -149,11 +149,11 @@ extension RunnerTests {
149
149
  x: Double(snapshot.frame.origin.x),
150
150
  y: Double(snapshot.frame.origin.y),
151
151
  width: Double(snapshot.frame.size.width),
152
- height: Double(snapshot.frame.size.height),
152
+ height: Double(snapshot.frame.size.height)
153
153
  ),
154
154
  enabled: snapshot.isEnabled,
155
155
  hittable: hittable,
156
- depth: min(maxDepth, visibleDepth),
156
+ depth: min(maxDepth, visibleDepth)
157
157
  )
158
158
  )
159
159
 
@@ -216,7 +216,7 @@ extension RunnerTests {
216
216
  rect: snapshotRect(from: snapshot.frame),
217
217
  enabled: snapshot.isEnabled,
218
218
  hittable: hittable,
219
- depth: depth,
219
+ depth: depth
220
220
  )
221
221
  )
222
222
  }
@@ -19,7 +19,7 @@ extension RunnerTests {
19
19
  if buffer.count + data.count > self.maxRequestBytes {
20
20
  let response = self.jsonResponse(
21
21
  status: 413,
22
- response: Response(ok: false, error: ErrorPayload(message: "request too large")),
22
+ response: Response(ok: false, error: ErrorPayload(message: "request too large"))
23
23
  )
24
24
  connection.send(content: response, completion: .contentProcessed { [weak self] _ in
25
25
  connection.cancel()
@@ -111,7 +111,7 @@ extension RunnerTests {
111
111
  "Content-Length: \(body.utf8.count)",
112
112
  "Connection: close",
113
113
  "",
114
- body,
114
+ body
115
115
  ].joined(separator: "\r\n")
116
116
  return Data(headers.utf8)
117
117
  }
@@ -59,7 +59,7 @@ final class RunnerTests: XCTestCase {
59
59
  .tabBar,
60
60
  .textField,
61
61
  .secureTextField,
62
- .textView,
62
+ .textView
63
63
  ]
64
64
  // Keep blocker actions narrow to avoid false positives from generic hittable containers.
65
65
  let actionableTypes: Set<XCUIElement.ElementType> = [
@@ -68,7 +68,7 @@ final class RunnerTests: XCTestCase {
68
68
  .link,
69
69
  .menuItem,
70
70
  .checkBox,
71
- .switch,
71
+ .switch
72
72
  ]
73
73
 
74
74
  // MARK: - XCTest Entry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.7.6",
3
+ "version": "0.7.10",
4
4
  "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Callstack",
@@ -25,7 +25,7 @@ Use this skill as a router, not a full manual.
25
25
  - Normal UI task: `open` -> `snapshot -i` -> `press/fill` -> `diff snapshot -i` -> `close`
26
26
  - Debug/crash: `open <app>` -> `logs clear --restart` -> reproduce -> `network dump` -> `logs path` -> targeted `grep`
27
27
  - Replay drift: `replay -u <path>` -> verify updated selectors
28
- - Remote multi-tenant run: allocate lease -> run commands with tenant isolation flags -> heartbeat/release lease
28
+ - Remote multi-tenant run: allocate lease -> point client at remote daemon base URL -> run commands with tenant isolation flags -> heartbeat/release lease
29
29
  - Device-scope isolation run: set iOS simulator set / Android allowlist -> run selectors within scope only
30
30
 
31
31
  ## Canonical Flows
@@ -62,14 +62,18 @@ agent-device replay -u ./session.ad
62
62
  ### 4) Remote Tenant Lease Flow (HTTP JSON-RPC)
63
63
 
64
64
  ```bash
65
+ # Client points directly at the remote daemon HTTP base URL.
66
+ export AGENT_DEVICE_DAEMON_BASE_URL=http://mac-host.example:4310
67
+ export AGENT_DEVICE_DAEMON_AUTH_TOKEN=<token>
68
+
65
69
  # Allocate lease
66
- curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
70
+ curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
67
71
  -H "content-type: application/json" \
68
72
  -H "Authorization: Bearer <token>" \
69
73
  -d '{"jsonrpc":"2.0","id":"alloc-1","method":"agent_device.lease.allocate","params":{"runId":"run-123","tenantId":"acme","ttlMs":60000}}'
70
74
 
71
75
  # Use lease in tenant-isolated command execution
72
- agent-device --daemon-transport http \
76
+ agent-device \
73
77
  --tenant acme \
74
78
  --session-isolation tenant \
75
79
  --run-id run-123 \
@@ -77,16 +81,21 @@ agent-device --daemon-transport http \
77
81
  session list --json
78
82
 
79
83
  # Heartbeat and release
80
- curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
84
+ curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
81
85
  -H "content-type: application/json" \
82
86
  -H "Authorization: Bearer <token>" \
83
87
  -d '{"jsonrpc":"2.0","id":"hb-1","method":"agent_device.lease.heartbeat","params":{"leaseId":"<lease-id>","ttlMs":60000}}'
84
- curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
88
+ curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
85
89
  -H "content-type: application/json" \
86
90
  -H "Authorization: Bearer <token>" \
87
91
  -d '{"jsonrpc":"2.0","id":"rel-1","method":"agent_device.lease.release","params":{"leaseId":"<lease-id>"}}'
88
92
  ```
89
93
 
94
+ Notes:
95
+ - `AGENT_DEVICE_DAEMON_BASE_URL` makes the CLI skip local daemon discovery/startup and call the remote HTTP daemon directly.
96
+ - `AGENT_DEVICE_DAEMON_AUTH_TOKEN` is sent in both the JSON-RPC request token and HTTP auth headers.
97
+ - In remote daemon mode, `--debug` does not tail a local `daemon.log`; inspect logs on the remote host instead.
98
+
90
99
  ## Command Skeleton (Minimal)
91
100
 
92
101
  ### Session and navigation
@@ -95,6 +104,8 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
95
104
  agent-device devices
96
105
  agent-device devices --platform ios --ios-simulator-device-set /tmp/tenant-a/simulators
97
106
  agent-device devices --platform android --android-device-allowlist emulator-5554,device-1234
107
+ agent-device ensure-simulator --device "iPhone 16" --ios-simulator-device-set /tmp/tenant-a/simulators
108
+ agent-device ensure-simulator --device "iPhone 16" --runtime com.apple.CoreSimulator.SimRuntime.iOS-18-4 --ios-simulator-device-set /tmp/tenant-a/simulators --boot
98
109
  agent-device open [app|url] [url]
99
110
  agent-device open [app] --relaunch
100
111
  agent-device close [app]
@@ -114,6 +125,12 @@ Isolation scoping quick reference:
114
125
  - Scope is applied before selectors (`--device`, `--udid`, `--serial`); out-of-scope selectors fail with `DEVICE_NOT_FOUND`.
115
126
  - With iOS simulator-set scope enabled, iOS physical devices are not enumerated.
116
127
 
128
+ Simulator provisioning quick reference:
129
+ - Use `ensure-simulator` to create or reuse a named iOS simulator inside a device set before starting a session.
130
+ - `--device <name>` is required (e.g. `"iPhone 16 Pro"`). `--runtime <id>` pins the runtime; omit to use the newest compatible one.
131
+ - `--boot` boots it immediately. Returns `udid`, `device`, `runtime`, `ios_simulator_device_set`, `created`, `booted`.
132
+ - Idempotent: safe to call repeatedly; reuses an existing matching simulator by default.
133
+
117
134
  TV quick reference:
118
135
  - AndroidTV: `open`/`apps` use TV launcher discovery automatically.
119
136
  - TV target selection works on emulators/simulators and connected physical devices (AndroidTV + AppleTV).
@@ -170,13 +187,15 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
170
187
  - Re-snapshot after UI mutations (navigation/modal/list changes).
171
188
  - Prefer `snapshot -i`; scope/depth only when needed.
172
189
  - Use refs for discovery, selectors for replay/assertions.
190
+ - `find "<query>" click --json` returns `{ ref, locator, query, x, y }` — all derived from the matched snapshot node. Do not rely on these fields from raw `press`/`click` responses for observability; use `find` instead.
173
191
  - Use `fill` for clear-then-type semantics; use `type` for focused append typing.
174
192
  - Use `install` for in-place app upgrades (keep app data when platform permits), and `reinstall` for deterministic fresh-state runs.
175
193
  - App binary format support for `install`/`reinstall`: Android `.apk`/`.aab`, iOS `.app`/`.ipa`.
176
194
  - Android `.aab` requires `bundletool` in `PATH`, or `AGENT_DEVICE_BUNDLETOOL_JAR=<path-to-bundletool-all.jar>` with `java` in `PATH`.
177
195
  - Android `.aab` optional: set `AGENT_DEVICE_ANDROID_BUNDLETOOL_MODE=<mode>` to control bundletool `build-apks --mode` (default: `universal`).
178
196
  - iOS `.ipa`: extract/install from `Payload/*.app`; when multiple app bundles are present, `<app>` is used as a bundle id/name hint.
179
- - iOS `appstate` is session-scoped; Android `appstate` is live foreground state.
197
+ - iOS `appstate` is session-scoped; Android `appstate` is live foreground state. iOS responses include `device_udid` and `ios_simulator_device_set` for isolation verification.
198
+ - iOS `open` responses include `device_udid` and `ios_simulator_device_set` to confirm which simulator handled the session.
180
199
  - Clipboard helpers: `clipboard read` / `clipboard write <text>` are supported on Android and iOS simulators; iOS physical devices are not supported yet.
181
200
  - Android keyboard helpers: `keyboard status|get|dismiss` report keyboard visibility/type and dismiss via keyevent when visible.
182
201
  - `network dump` is best-effort and parses HTTP(s) entries from the session app log file.
@@ -190,6 +209,7 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
190
209
  - Canonical trigger behavior and caveats are documented in [`website/docs/docs/commands.md`](../../website/docs/docs/commands.md) under **App event triggers**.
191
210
  - Permission settings are app-scoped and require an active session app:
192
211
  `settings permission <grant|deny|reset> <camera|microphone|photos|contacts|notifications> [full|limited]`
212
+ - iOS simulator permission alerts: use `alert wait` then `alert accept/dismiss` — `accept`/`dismiss` retry internally for up to 2 s so you do not need manual sleeps. See [references/permissions.md](references/permissions.md).
193
213
  - `full|limited` mode applies only to iOS `photos`; other targets reject mode.
194
214
  - On Android, non-ASCII `fill/type` may require an ADB keyboard IME on some system images; only install IME APKs from trusted sources and verify checksum/signature.
195
215
  - If using `--save-script`, prefer explicit path syntax (`--save-script=flow.ad` or `./flow.ad`).
@@ -197,6 +217,7 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
197
217
  - Use short lease TTLs and heartbeat only while work is active; release leases immediately after run completion/failure.
198
218
  - Env equivalents for scoped runs: `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET` (compat `IOS_SIMULATOR_DEVICE_SET`) and
199
219
  `AGENT_DEVICE_ANDROID_DEVICE_ALLOWLIST` (compat `ANDROID_DEVICE_ALLOWLIST`).
220
+ - For explicit remote client mode, prefer `AGENT_DEVICE_DAEMON_BASE_URL` / `--daemon-base-url` instead of relying on local daemon metadata or loopback-only ports.
200
221
 
201
222
  ## Security and Trust Notes
202
223
 
@@ -204,7 +225,7 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
204
225
  - If install is required, pin an exact version (for example: `npx --yes agent-device@<exact-version> --help`).
205
226
  - Signing/provisioning environment variables are optional, sensitive, and only for iOS physical-device setup.
206
227
  - Logs/artifacts are written under `~/.agent-device`; replay scripts write to explicit paths you provide.
207
- - For remote daemon mode, prefer `AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual` with `AGENT_DEVICE_HTTP_AUTH_HOOK` and tenant-scoped lease admission.
228
+ - For remote daemon mode, prefer `AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual` on the host plus client-side `AGENT_DEVICE_DAEMON_BASE_URL`, with `AGENT_DEVICE_HTTP_AUTH_HOOK` and tenant-scoped lease admission where needed.
208
229
  - Keep logging off unless debugging and use least-privilege/isolated environments for autonomous runs.
209
230
 
210
231
  ## Common Mistakes
@@ -34,6 +34,33 @@ If daemon startup fails with stale metadata hints, clean stale files and retry:
34
34
  - `~/.agent-device/daemon.json`
35
35
  - `~/.agent-device/daemon.lock`
36
36
 
37
+ ## iOS permission alerts (simulator only)
38
+
39
+ iOS apps trigger system permission dialogs (camera, location, notifications, etc.) on first use.
40
+ Use `alert` to handle them without tapping coordinates:
41
+
42
+ ```bash
43
+ agent-device alert wait # block until an alert appears (default 10 s timeout)
44
+ agent-device alert accept # accept the frontmost alert
45
+ agent-device alert dismiss # dismiss the frontmost alert
46
+ agent-device alert get # read alert title/message without acting
47
+ ```
48
+
49
+ **Timing note:** `alert accept` and `alert dismiss` include a built-in 2 s retry window.
50
+ If the alert is present in the UI hierarchy but not yet interactive, the command retries every 300 ms
51
+ rather than failing immediately. You do not need to add manual sleeps between triggering the alert
52
+ and accepting it.
53
+
54
+ **Preferred pattern for clean simulator sessions:**
55
+
56
+ ```bash
57
+ agent-device open MyApp --platform ios
58
+ agent-device alert wait 5000 # wait up to 5 s for the permission prompt
59
+ agent-device alert accept # accept; retries internally if not yet actionable
60
+ ```
61
+
62
+ `alert` is only supported on iOS simulators; iOS physical devices are not supported.
63
+
37
64
  ## iOS: "Allow Paste" dialog
38
65
 
39
66
  iOS 16+ shows an "Allow Paste" prompt when an app reads the system pasteboard. Under XCUITest (which `agent-device` uses), this prompt is suppressed by the testing runtime. Use `xcrun simctl pbcopy booted` to set clipboard content directly on the simulator instead.
@@ -6,7 +6,12 @@ tenant/run admission control.
6
6
  ## Transport prerequisites
7
7
 
8
8
  - Start daemon in HTTP mode (`AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual`).
9
- - Use a token from daemon metadata or `Authorization: Bearer <token>`.
9
+ - Point remote clients at the host with `AGENT_DEVICE_DAEMON_BASE_URL=http(s)://host:port[/base-path]`
10
+ or `--daemon-base-url <url>` so the CLI skips local daemon discovery/startup.
11
+ - Use `AGENT_DEVICE_DAEMON_AUTH_TOKEN` / `--daemon-auth-token` when the client should send the
12
+ shared daemon token automatically.
13
+ - Direct JSON-RPC callers can use a token in params, `Authorization: Bearer <token>`, or
14
+ `x-agent-device-token`.
10
15
  - Prefer an auth hook (`AGENT_DEVICE_HTTP_AUTH_HOOK`) for caller validation and
11
16
  tenant injection.
12
17
 
@@ -21,7 +26,7 @@ Use `POST /rpc` with JSON-RPC 2.0 methods:
21
26
  Example allocate:
22
27
 
23
28
  ```bash
24
- curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
29
+ curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
25
30
  -H "content-type: application/json" \
26
31
  -H "Authorization: Bearer <token>" \
27
32
  -d '{"jsonrpc":"2.0","id":"alloc-1","method":"agent_device.lease.allocate","params":{"tenantId":"acme","runId":"run-123","ttlMs":60000}}'
@@ -30,7 +35,7 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
30
35
  Example heartbeat:
31
36
 
32
37
  ```bash
33
- curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
38
+ curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
34
39
  -H "content-type: application/json" \
35
40
  -H "Authorization: Bearer <token>" \
36
41
  -d '{"jsonrpc":"2.0","id":"hb-1","method":"agent_device.lease.heartbeat","params":{"leaseId":"<lease-id>","ttlMs":60000}}'
@@ -39,7 +44,7 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
39
44
  Example release:
40
45
 
41
46
  ```bash
42
- curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
47
+ curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
43
48
  -H "content-type: application/json" \
44
49
  -H "Authorization: Bearer <token>" \
45
50
  -d '{"jsonrpc":"2.0","id":"rel-1","method":"agent_device.lease.release","params":{"leaseId":"<lease-id>"}}'
@@ -50,7 +55,7 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
50
55
  For tenant-isolated command execution, pass all four flags:
51
56
 
52
57
  ```bash
53
- agent-device --daemon-transport http \
58
+ agent-device \
54
59
  --tenant acme \
55
60
  --session-isolation tenant \
56
61
  --run-id run-123 \
@@ -60,6 +65,9 @@ agent-device --daemon-transport http \
60
65
 
61
66
  Admission checks require tenant/run/lease scope alignment.
62
67
 
68
+ The CLI sends `AGENT_DEVICE_DAEMON_AUTH_TOKEN` in both the JSON-RPC request token field and HTTP
69
+ auth headers so existing daemon auth paths continue to work.
70
+
63
71
  ## Failure semantics
64
72
 
65
73
  - Missing tenant/run/lease fields in tenant isolation mode: `INVALID_ARGS`
@@ -70,6 +78,8 @@ Admission checks require tenant/run/lease scope alignment.
70
78
 
71
79
  - Keep TTL short and heartbeat only while a run is active.
72
80
  - Release lease immediately on run completion/error paths.
81
+ - For remote debug sessions, inspect logs on the remote host; client-side `--debug` no longer tails
82
+ a local daemon log when `AGENT_DEVICE_DAEMON_BASE_URL` is set.
73
83
  - For bounded hosts, configure:
74
84
  - `AGENT_DEVICE_MAX_SIMULATOR_LEASES`
75
85
  - `AGENT_DEVICE_LEASE_TTL_MS`
@@ -21,6 +21,7 @@ Sessions isolate device context. A device can only be held by one session at a t
21
21
  - On iOS, `appstate` is session-scoped and requires a matching active session on the target device.
22
22
  - For dev loops where runtime state can persist (for example React Native Fast Refresh), use `open <app> --relaunch` to restart the app process in the same session.
23
23
  - Use `--save-script [path]` to record replay scripts on `close`; path is a file path and parent directories are created automatically.
24
+ - Use `close --shutdown` (iOS simulator only) to shut down the simulator as part of session teardown, preventing resource leakage in multi-tenant or CI workloads.
24
25
  - For ambiguous bare `--save-script` values, prefer `--save-script=workflow.ad` or `./workflow.ad`.
25
26
  - For deterministic replay scripts, prefer selector-based actions and assertions.
26
27
  - Use `replay -u` to update selector drift during maintenance.
@@ -36,6 +37,7 @@ agent-device devices --platform android --android-device-allowlist emulator-5554
36
37
 
37
38
  - Scope is applied before selectors (`--device`, `--udid`, `--serial`).
38
39
  - If selector target is outside scope, resolution fails with `DEVICE_NOT_FOUND`.
40
+ - If the scoped iOS simulator set is empty (first-run), the error includes the set path and a suggested `xcrun simctl --set <path> create ...` command.
39
41
  - With iOS simulator-set scope enabled, iOS physical devices are not enumerated.
40
42
  - Environment equivalents:
41
43
  - `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET` (compat: `IOS_SIMULATOR_DEVICE_SET`)
@@ -47,6 +49,8 @@ agent-device devices --platform android --android-device-allowlist emulator-5554
47
49
  agent-device session list
48
50
  ```
49
51
 
52
+ iOS session entries include `device_udid` and `ios_simulator_device_set` (null when using the default set). Use these fields to confirm device routing in concurrent multi-session runs without additional `simctl` calls.
53
+
50
54
  ## Replay within sessions
51
55
 
52
56
  ```bash
@@ -76,6 +76,16 @@ Efficient pattern:
76
76
 
77
77
  - If refs are unstable after UI transitions, switch to selector-based targeting and stop investing in ref-only flows.
78
78
 
79
+ ## find click response
80
+
81
+ `find "<query>" click --json` returns deterministic matched-target metadata:
82
+
83
+ ```json
84
+ { "ref": "@e3", "locator": "any", "query": "Increment", "x": 195, "y": 422 }
85
+ ```
86
+
87
+ Fields come from the matched snapshot node, not the platform runner. Use these for observability and replay quality — they are stable across runs for the same UI state.
88
+
79
89
  ## Replay note
80
90
 
81
91
  - Prefer selector-based actions in recorded `.ad` replays.