* fix: prevent auto-selection of physical iOS devices when simulators are available
When running with --platform ios and a physical iPhone connected but no
simulators booted, the tool would connect to the physical device instead
of booting a simulator. Add a safeguard in resolveTargetDevice that
rejects auto-selected physical devices and falls back to
findBootableIosSimulator, which queries simctl without the isAvailable
filter to find any bootable simulator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle empty device list and filter unavailable simulators in fallback
- Extract resolveIosDevice() so the iOS fallback logic is testable via
dependency injection without module mocking
- Catch DEVICE_NOT_FOUND from selectDevice when no explicit selector was
used, so the simulator fallback runs even when listIosDevices returns
an empty list (all simulators filtered by isAvailable)
- Restore isAvailable check in findBootableIosSimulator to avoid
returning simulators with missing runtimes that cannot boot
- Add 8 tests covering: physical-device rejection, empty-list recovery,
explicit-selector passthrough, and normal simulator-present path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Physical iOS devices always report booted=true, so the "prefer booted"
tiebreaker would pick them over unbooted simulators. This caused sessions
to bind to a connected iPhone instead of a simulator when no explicit
device selector (--udid/--device/--serial) was provided.
Now, when multiple candidates exist and no explicit device was requested,
physical devices are filtered out if virtual alternatives are available.
The simulator is then booted on demand by the existing ensureBootedSimulator
flow.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The 1.5s per-command timeout in resolveAndroidEmulatorAvdName was too
aggressive for emulators over remote adb daemon transport, causing
open --relaunch to fail during device discovery.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
adb shell concatenates all arguments with spaces into a single string
for the remote shell. Without quoting, the && in the mkdir/cat script
was interpreted as a command separator, causing mkdir to run with no
arguments ("mkdir: Needs 1 argument").
Wrapping the script in single quotes ensures the remote device shell
passes the entire command as one argument to sh -c.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When `xcrun simctl launch` fails with FBSOpenApplicationServiceErrorDomain
code=4 persistently (e.g. architecture mismatch), all 30 retries would fail
identically, wasting 30 seconds before surfacing a generic error. Now we bail
after 3 consecutive identical FBS failures, probe the simulator for root cause
(app not installed, device binary on simulator, etc.), and surface an actionable
hint. Also reduces maxAttempts from 30 to 10 as a secondary safety net.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
resolveAndroidApp treated any input containing a dot as a package name,
which caused APK file paths (e.g. /path/to/app-debug.apk) to flow through
as appId into runtime hints, failing with a misleading "app not debuggable"
diagnosis. Android package names never contain slashes, so exclude paths
with '/' from the dot-based package heuristic.
Also differentiate probe failures in writeAndroidDevPrefs: check
isAndroidRunAsDeniedOutput on the probe result so connectivity/transport
errors are not misattributed to debuggability.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The artifact download endpoint was at GET /artifacts/{id}, but proxies
in the sandbox → proxy → daemon architecture only forward known paths
(/rpc, /health, /upload). Move downloads to GET /upload/{id} so they
share the already-forwarded /upload path (POST for uploads, GET for
downloads).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add remote runtime hints and artifact retrieval
* fix: apply session runtime transport hints on open
* fix: secure and time out artifact downloads
* fix: propagate AbortSignal on request cancellation to stop iOS runner promptly
When a client disconnects (socket close or HTTP connection drop), in-flight
iOS runner operations now receive an AbortSignal that immediately interrupts
pending HTTP fetches, retry delays, and retry loops—ensuring detached
xcodebuild/runner processes are not driven after the parent workflow is canceled.
Closes#193
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: ensure HTTP disconnect cancellation always propagates
* fix: rethrow canceled runner waits before fallback
* fix: harden cancellation tracking and propagation
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: support installing local artifacts over remote daemon transport
When the CLI runs on a different machine than the daemon (via
AGENT_DEVICE_DAEMON_BASE_URL), install/reinstall commands now
transparently upload local app binaries (.apk/.ipa/.aab) and app
bundles (.app dirs) to the daemon before installing. No new flags
required — same CLI ergonomics.
- Add POST /upload endpoint to daemon HTTP server with auth and
streaming file/tar reception
- Add client-side upload helper with timeout and tar packing for
.app directories
- Intercept install/reinstall in sendToDaemon for remote daemons,
upload artifact, rewrite positional path
- Clean up uploaded temp files after install completes (or fails)
with 5-minute safety-net auto-cleanup for abandoned uploads
- Sanitize artifact filenames to prevent path traversal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden remote artifact upload flow
* refactor: split upload registry from receiver
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: strip pre-PNG garbage from Android screenshots on multi-display devices
On Android emulators with multiple displays (e.g. Galaxy Z Fold), `adb
exec-out screencap -p` writes a warning to stdout before the PNG data,
producing corrupt files. Locate the PNG signature in the buffer and
discard everything before it.
https://claude.ai/code/session_01Lfb3WKqXCSqYtCg8dbS8JW
* refactor: simplify PNG stripping and test scripts
- Remove unnecessary ternary; subarray(0) is a no-op
- Replace hex-encoded printf mock scripts with file-based payloads
https://claude.ai/code/session_01Lfb3WKqXCSqYtCg8dbS8JW
* fix: trim Android screenshot output to complete PNG payload
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add strict key-set assertions to find click handler tests to prevent
accidental schema drift in the machine-consumed JSON response.
Closes#182
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add --shutdown option to close command for iOS simulator teardown (#172)
When --shutdown is passed, close ends the session then runs xcrun simctl
shutdown on the associated simulator. Shutdown result (success, exitCode,
stdout, stderr) is included in the JSON response data.
The option is silently ignored for non-simulator targets (physical iOS
devices, Android). A shutdownSimulator override is injected into
handleSessionCommands for testability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: document close --shutdown option for iOS simulator teardown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: keep close --shutdown successful when simulator shutdown fails
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add ensure-simulator command for scoped iOS device sets (#169)
Adds a new first-class `ensure-simulator` command that ensures an iOS
simulator exists (and optionally boots it) inside a scoped device set,
without requiring custom simctl scripting outside agent-device.
Usage:
agent-device ensure-simulator --device "iPhone 16" [--runtime <id>] [--boot] [--ios-simulator-device-set <path>]
JSON output includes udid, device, runtime, ios_simulator_device_set,
and whether the simulator was created vs reused.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: document ensure-simulator in commands reference and skill
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return deterministic matched-target metadata for find click responses (#170)
On success, build the response from the matched snapshot node rather than
passing through whatever the underlying click/press handler returns.
Response now includes: ref, locator, query, and x/y derived from the
matched element's rect — all stable across runs.
Also makes `dispatch` injectable in `handleFindCommands` (matching the
pattern in `handleInteractionCommands`) and adds tests that assert the
deterministic shape and verify that non-deterministic platform runner
data does not bleed into the response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: document find click response shape in selectors and skill references
Add response shape documentation for `find "<query>" click --json` to:
- website/docs/docs/selectors.md (new "Response shape (click)" section)
- skills/agent-device/SKILL.md (guardrail note)
- skills/agent-device/references/snapshot-refs.md (new "find click response" section)
Follows the fix in #178 that made the response deterministic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: retry alert accept/dismiss on transient 'alert not found' errors
Adds a 2 s internal retry window for `alert accept` and `alert dismiss`
when the runner returns "alert not found" or "no alert". Errors from
other causes break the loop immediately and are re-thrown unchanged.
Also injects `runnerCommand` as an optional param to enable unit testing
without spawning a real runner.
Closes#171
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: document alert accept/dismiss retry behaviour in skill and permissions reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: enrich DEVICE_NOT_FOUND error for empty scoped iOS simulator sets (#168)
When selectDevice finds no candidates and a simulatorSetPath context is
provided for iOS, throw a detailed error including the set path and a
suggested xcrun simctl --set ... create command to provision the first
simulator.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: note enriched DEVICE_NOT_FOUND guidance for empty scoped iOS simulator sets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add device identity fields to open, appstate, and session_list responses
for iOS targets, enabling isolation verification in concurrent multi-session
runs without extra simctl calls.
Closes#173
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract resolveTargetDevice, series/jitter helpers, and push payload
parsing into dedicated modules to reduce dispatch.ts from ~706 to ~564 LOC.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>