diff --git a/README.md b/README.md index 9c056a81cb..7820b485f3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ agent-device doctor agent-device help workflow ``` -Run `doctor` yourself before handing the CLI to an agent; `help workflow` links to the guides for debugging, replay, and profiling, and the installed help always matches the installed version. +Run `doctor` yourself before you hand the CLI to an agent. `help workflow` links to the guides for debugging, replay, and profiling. ### Drive an app from the CLI @@ -61,7 +61,7 @@ agent-device screenshot ./contact-form.png agent-device close ``` -Refs are only valid from the latest output: after a `--settle` command, use the refs in its diff, and take a new snapshot only if the diff omits what you need. Snapshots come from the app's accessibility tree, so clear labels, roles, and test IDs make agent runs more reliable; use screenshots and video as evidence or when accessibility data is poor. +Refs are valid only in the latest output: after a `--settle` command, use the refs in its diff, and take a new snapshot only if the diff omits what you need. Snapshots come from the app's accessibility tree, so clear labels, roles, and test IDs make agent runs more reliable; use screenshots and video as evidence or when accessibility data is poor. ![agent-device demo showing Codex using agent-device to create a new contact in the iOS Contacts app from a simple prompt](./website/docs/public/agent-device-contacts.gif) @@ -126,7 +126,7 @@ With the CLI installed, prompts like these work end to end: ## Next steps - [AI Agent Setup](https://oss.callstack.com/agent-device/docs/agent-setup): skills, project rules, and per-client setup for Cursor, Codex, Claude Code, Windsurf, and others. -- [Quick Start](https://oss.callstack.com/agent-device/docs/quick-start): a guided run on the bundled Expo test app with screenshots, replay, and performance data. +- [Quick Start](https://oss.callstack.com/agent-device/docs/quick-start): open an app, read its UI, and interact with it from the command line. - [Replay & E2E](https://oss.callstack.com/agent-device/docs/replay-e2e) and [Debugging & Profiling](https://oss.callstack.com/agent-device/docs/debugging-profiling): repeatable tests and bug hunting. ## Where to run agent-device @@ -143,9 +143,9 @@ The same session and evidence model works at every step: the agent explores the `agent-device` keeps device state in sessions. It uses a local accessibility bridge for iOS Simulator snapshots and XCTest for iOS interactions, physical iOS, and tvOS; ADB and the snapshot helper on Android; HDC and ArkUI `uitest` on HarmonyOS; Vega CLI/VDA on the Vega Virtual Device; a local helper on macOS; and AT-SPI on Linux. -Support depth varies by target. Newer backends such as HarmonyOS and Vega OS cover a subset of commands; run `agent-device capabilities --platform ` to see what a target supports. +Support depth varies by target. Some backends, such as HarmonyOS and Vega OS, cover a subset of commands; run `agent-device capabilities --platform ` to see what a target supports. -Sessions are scoped to the caller's git worktree, and host-local device claims stop parallel agents from taking over each other's simulators and emulators. Inspect ownership without a daemon via `agent-device device status`, and settle provably dead owners with `agent-device device release --stale`. The same commands drive hosted devices on [BrowserStack, AWS Device Farm, TestMu AI, and Limrun](https://oss.callstack.com/agent-device/docs/device-clouds). +Sessions are scoped to the caller's git worktree, and host-local device claims stop parallel agents from taking over each other's simulators and emulators. Run `agent-device device status` to inspect ownership without a daemon, and `agent-device device release --stale` to release claims whose owner is provably no longer running. The same commands drive hosted devices on [BrowserStack, AWS Device Farm, TestMu AI, and Limrun](https://oss.callstack.com/agent-device/docs/device-clouds). `agent-device` uses the inspect-act-verify process from Vercel's [agent-browser](https://github.com/vercel-labs/agent-browser) for mobile, TV, and desktop apps. Basic `--platform web` support runs `agent-browser` in the same session and replay system. diff --git a/docs/adr/0013-unified-gesture-plans.md b/docs/adr/0013-unified-gesture-plans.md index ce5077504f..590521a7d2 100644 --- a/docs/adr/0013-unified-gesture-plans.md +++ b/docs/adr/0013-unified-gesture-plans.md @@ -156,7 +156,7 @@ resolve after planning, and interpolation never splits a token, so the count is values are not. The removal process these inputs follow — announce, warn for one minor release, publish the migration guide, prove the repository is clean, then remove the branch and its tests — is documented in the public -[gesture migration guide](https://agent-device.dev/docs/migrating-gestures). +[gesture migration guide](https://oss.callstack.com/agent-device/docs/migrating-gestures). Repeated coordinate swipes are bounded at the public command contract and daemon trust boundary. Individual count and pause limits prevent pathological fields, while the combined planned gesture diff --git a/examples/sdk/tsconfig.json b/examples/sdk/tsconfig.json index 068bb60320..39717d5a35 100644 --- a/examples/sdk/tsconfig.json +++ b/examples/sdk/tsconfig.json @@ -15,6 +15,7 @@ "agent-device/android-adb": ["../../src/sdk/android-adb.ts"], "agent-device/contracts": ["../../src/sdk/contracts.ts"], "agent-device/finders": ["../../src/sdk/finders.ts"], + "agent-device/selectors": ["../../src/sdk/selectors.ts"], "agent-device/ai-sdk": ["../../src/ai-sdk/index.ts"] } }, diff --git a/packages/provider-testmu/README.md b/packages/provider-testmu/README.md index 2cf34a621c..31d6a82757 100644 --- a/packages/provider-testmu/README.md +++ b/packages/provider-testmu/README.md @@ -11,7 +11,7 @@ export LT_ACCESS_KEY=your-access-key agent-device connect testmu --platform ios --device "iPhone 16" --provider-os-version 18.0 --provider-app ./MyApp.zip ``` -See the [TestMu AI guide](https://agent-device.dev/docs/testmu) for setup and supported operations. +See the [TestMu AI guide](https://oss.callstack.com/agent-device/docs/testmu) for setup and supported operations. To update the plugin, run `agent-device plugins update @agent-device/testmu`. -After adding or updating a plugin, close your sessions and run `agent-device daemon stop` before reconnecting. +After you add or update the plugin, close your sessions and run `agent-device daemon stop` before reconnecting. diff --git a/packages/provider-testmu/package.json b/packages/provider-testmu/package.json index 2dd95897f9..2aca86c0d4 100644 --- a/packages/provider-testmu/package.json +++ b/packages/provider-testmu/package.json @@ -59,5 +59,5 @@ "url": "git+https://github.com/callstack/agent-device.git", "directory": "packages/provider-testmu" }, - "homepage": "https://agent-device.dev/docs/testmu" + "homepage": "https://oss.callstack.com/agent-device/docs/testmu" } diff --git a/website/docs/docs/agent-setup.md b/website/docs/docs/agent-setup.md index 22c68a02d6..92696b372a 100644 --- a/website/docs/docs/agent-setup.md +++ b/website/docs/docs/agent-setup.md @@ -5,46 +5,31 @@ description: Configure Cursor, Codex, Claude Code, Windsurf, Cline, Goose, iOS S # AI Agent Setup -`agent-device` is built for AI agents, but humans usually install it, grant device permissions, and decide which agent client should use it. +Set up Cursor, Codex, Claude Code, Windsurf, Cline, Goose, or another coding agent to drive mobile, TV, desktop, and web apps with `agent-device`, through skills, project rules, or MCP. -Use this page to wire Cursor, Codex, Claude Code, Windsurf, Cline, Goose, or another coding agent into mobile, TV, desktop, and web app verification. It covers skills, project rules, and MCP setup for React Native QA, Expo app verification, iOS Simulator automation, Android Emulator automation, tvOS checks, Android TV checks, Vega OS VVD control, web browser sessions, debugging, profiling, and exploratory QA. +In short: install the CLI, let the agent start with the requested app, and have it read the installed CLI help only for specialized work or when a command is unclear. MCP tools use the same daemon client as the CLI. -The short version: install the CLI, let the agent start normal work with the requested app, and use version-matched help only for specialized work or an unclear command shape. MCP tools use command contracts backed by the same `AgentDeviceClient` execution path as the CLI adapters. - -## Prerequisite: install the CLI +## Install the CLI ```bash npm install -g agent-device@latest -agent-device --version -agent-device help workflow -``` - -For one-off human use without a global install: - -```bash -npx agent-device --version -npx agent-device help workflow ``` -Global install is better for normal agent workflows because repeated commands and terminal sessions resolve to one stable version. Project-local installs are also good when you want a lockfile-pinned agent-device version. - -Avoid telling agents to choose an npm version or run `npx -y agent-device@latest` autonomously: it fetches and executes a mutable npm package without a human prompt. For unattended agent use, prefer a trusted installed binary, a project-local install, or a version supplied by the user or project config. - -For Node, Xcode, Android SDK, macOS, and iOS device prerequisites, see [Installation](/docs/installation). +Use a global or project-local install so every agent command runs the same version. For Node, Xcode, Android SDK, macOS, and iOS device prerequisites, and for when one-off `npx` use is safe, see [Installation](/docs/installation). ## Install the skills -Install the CLI first, then install the repository skills when your agent runtime supports them: +If your agent client supports skills, install them after the CLI: ```bash npx skills add callstack/agent-device ``` -Skills are distributed from the GitHub repository rather than the npm package. The [agent-device skill](https://github.com/callstack/agent-device/blob/main/skills/agent-device/SKILL.md) is the canonical router for skill-aware clients. For focused simulator work, use the [iOS Simulator skill](https://github.com/callstack/agent-device/blob/main/skills/ios-simulator/SKILL.md) or [Android Emulator skill](https://github.com/callstack/agent-device/blob/main/skills/android-emulator/SKILL.md). They start normal work directly and route agents to the separately installed, version-matched CLI help only when the task is specialized or a command shape is unclear. +Skills come from the GitHub repository, not the npm package. The [agent-device skill](https://github.com/callstack/agent-device/blob/main/skills/agent-device/SKILL.md) is the main entry point. For simulator-only work, use the [iOS Simulator skill](https://github.com/callstack/agent-device/blob/main/skills/ios-simulator/SKILL.md) or [Android Emulator skill](https://github.com/callstack/agent-device/blob/main/skills/android-emulator/SKILL.md). Skills tell the agent to start work directly and to read the installed CLI help only when the task is specialized or a command is unclear. Because the help ships with the CLI, it always matches your installed version. ## Recommended agent rule -Add this as a project rule, custom instruction, or skill equivalent when your agent client supports it: +Add this as a project rule or custom instruction if your agent client supports one: ```text Use agent-device only for app/device automation tasks. For a normal app-driving task, start immediately. Do not probe first with `--help`, `--version`, `devices`, `appstate`, `snapshot`, or `screenshot`; open the requested app in the foreground and continue from its initial interactive snapshot. For TV, Fire TV, or Vega OS tasks, read `agent-device help tv`. For exploratory QA, read `agent-device help dogfood`. For logs, network, audio, traces, or runtime failures, read `agent-device help debugging`. For React Native component trees, props/state/hooks, slow renders, or rerenders, read `agent-device help react-devtools`. For React Native JavaScript heap growth, heap snapshots, allocation hotspots, or retained-object leaks, read `agent-device help cdp`. For React Native apps, overlays, Metro/Fast Refresh blockers, and routing to React DevTools or debugging evidence, read `agent-device help react-native`. @@ -54,17 +39,17 @@ Use MCP tools or the CLI in the integrated terminal. If `agent-device` is not on ## MCP server -`agent-device mcp` starts the official stdio MCP server. It exposes direct structured tools for installed CLI commands. Tools run through command contracts and `AgentDeviceClient`; local-only workflows stay CLI-only rather than subprocess fallbacks. +`agent-device mcp` starts the stdio MCP server. It exposes installed CLI commands as structured tools. Some local workflows are CLI-only and have no MCP tool. -For web automation, MCP tools can target `platform: "web"` after the managed backend is available, but `agent-device web setup` and `agent-device web doctor` are CLI-only. Run setup from a terminal in the same effective state directory before asking an MCP client to drive a browser session. +For web automation, MCP tools can target `platform: "web"` once the managed browser backend is set up, but `agent-device web setup` and `agent-device web doctor` are CLI-only. Run setup from a terminal that uses the same state directory as the MCP server before you ask an MCP client to drive a browser session. -The server also ships its own guidance, so MCP-only clients do not depend on a separately installed skill: the handshake (`initialize` and `server/discover`) returns compact `instructions` with the workflow card (start with `open`, act with `settle: true`, verify, `close`, ref fidelity, sparse-AX recovery), and an MCP-only `help` tool returns the same guides as `agent-device help ` on demand. `help` is not a startup step; agents call it for specialized work (gestures, scripting, TV, macOS, web, remote, debugging) or an unclear command shape. +MCP-only clients don't need a separately installed skill. When a client connects (`initialize` and `server/discover`), the server returns short `instructions` with the core workflow: start with `open`, act with `settle: true`, verify, `close`, how to keep refs accurate, and how to recover when the accessibility tree is sparse. An MCP-only `help` tool returns the same guides as `agent-device help `. Agents don't call `help` at startup; they call it for specialized work (gestures, scripting, TV, macOS, web, remote, debugging) or when a command is unclear. -Tool execution failures are returned as MCP tool results with `isError: true`; clients and agents should inspect the tool result, not only the successful JSON-RPC envelope. +When a tool fails, the server returns an MCP tool result with `isError: true`. Check the tool result, not only whether the JSON-RPC call succeeded. -MCP clients must not use this server as a generic shell runner. If the CLI is missing, agents should ask a human before installing or updating packages, reconnect the server after setup, and retry the intended app-driving command without adding version/help probes. +The server does not run arbitrary shell commands. If the CLI is missing, the agent should ask you before installing or updating packages, reconnect the server after setup, and retry the original app-driving command without adding version or help probes. -Global install configuration: +Configuration for a global install: ```json { @@ -77,7 +62,7 @@ Global install configuration: } ``` -No global install variant. Pin a user- or project-selected package version for unattended agent use: +Without a global install, pin a version you've reviewed for unattended agent use: ```json { @@ -90,13 +75,17 @@ No global install variant. Pin a user- or project-selected package version for u } ``` -Registry metadata uses MCP name `io.github.callstack/agent-device`, npm package `agent-device`, stdio transport, `mcpName` package verification, `server.json`, `glama.json`, and `smithery.yaml`. Glama lists the server at [callstack/agent-device](https://glama.ai/mcp/servers/callstack/agent-device). +In MCP registries, the server is listed as `io.github.callstack/agent-device` (npm package `agent-device`, stdio transport). Glama lists it at [callstack/agent-device](https://glama.ai/mcp/servers/callstack/agent-device). + +## Fix a missing agent-device command + +Some agent clients run commands with a different `PATH` than your normal shell, so a global install that works in your terminal can be missing in the agent's terminal or MCP server. Run `command -v agent-device` in your own terminal and give the agent that absolute path, or use it as the MCP server `command`. If the path is under a version manager or a package-manager global bin directory, check your shell startup files for how that directory gets on `PATH`; the agent client may not load them. ## Cursor -Cursor works well with either the plain CLI or MCP tools. Use the CLI path when you want the most auditable setup and terminal-visible commands. Add MCP when you want Cursor Agent to discover structured `agent-device` tools directly from chat. +Cursor can use the CLI or MCP tools. Use the CLI when you want every command visible in the terminal. Add MCP when you want Cursor Agent to discover `agent-device` tools directly from chat. -### Cursor path A: CLI only +### Cursor: use the CLI Create a project rule: @@ -130,9 +119,9 @@ Then ask Cursor Agent to run: agent-device open --platform ios --foreground ``` -### Cursor path B: MCP tools +### Cursor: use MCP tools -Create project MCP config: +Create a project MCP config: ```bash mkdir -p .cursor @@ -148,13 +137,13 @@ cat > .cursor/mcp.json <<'JSON' JSON ``` -Restart Cursor or reconnect MCP from Cursor settings, then ask Cursor Agent: +Restart Cursor or reconnect MCP in Cursor settings, then ask Cursor Agent: ```text Use the agent-device MCP tools to inspect the iOS app. Open the app, take an interactive snapshot, act on visible refs/selectors, verify with another snapshot, and close the session. ``` -If the MCP server fails because Cursor cannot find the global binary, use the absolute binary path in `.cursor/mcp.json`: +If the MCP server fails to start because Cursor can't find the global binary, use the absolute binary path in `.cursor/mcp.json`: ```json { @@ -169,21 +158,21 @@ If the MCP server fails because Cursor cannot find the global binary, use the ab ## Codex -Put the recommended rule in `AGENTS.md` or the project instructions. Let Codex run `agent-device` in the terminal: +Put the [recommended rule](#recommended-agent-rule) in `AGENTS.md` or the project instructions, then let Codex run `agent-device` in the terminal: ```bash agent-device open --platform ios --foreground ``` -Some agent clients run commands in an environment that differs from the user's normal install shell. If the user installed `agent-device` globally but the agent cannot find it, resolve the command the same way the user would from a normal terminal session, then use the absolute binary path for the intended `open` and subsequent commands. This may require inspecting shell startup behavior or package-manager/global bin locations; do not assume the agent process `PATH` is the user's `PATH`. +If Codex can't find `agent-device`, see [Fix a missing agent-device command](#fix-a-missing-agent-device-command). -For reviews or planning-only tasks, tell the agent not to run devices unless explicitly requested. +For reviews or planning-only tasks, tell the agent not to touch devices unless you ask. ## Claude Code -Claude Code works through the terminal CLI and through the VS Code extension panel. The VS Code extension can use MCP servers configured by the Claude CLI and managed with `/mcp`. +Claude Code can use `agent-device` from the terminal or from the VS Code extension. The VS Code extension uses MCP servers you add with the `claude` CLI and manage with `/mcp`. -### Claude path A: CLI only +### Claude Code: use the CLI Put this in `CLAUDE.md`: @@ -213,7 +202,7 @@ Then ask Claude Code to run: agent-device open --platform android --foreground ``` -### Claude path B: MCP tools +### Claude Code: use MCP tools Add a user-scoped server: @@ -240,32 +229,30 @@ Confirm `agent-device` is connected, then ask: Use the agent-device MCP tools to verify the app. Open the app, take an interactive snapshot, use refs/selectors for actions, verify with another snapshot, and close the session. ``` -If Claude cannot start the MCP server because the extension process cannot find the global binary, remove and re-add it with an absolute path: +If Claude Code can't start the MCP server because the extension can't find the global binary, remove the server and add it again with an absolute path: ```bash claude mcp remove agent-device claude mcp add --transport stdio --scope user agent-device -- /absolute/path/to/agent-device mcp ``` -The same CLI commands remain available in the integrated terminal for long-running or manual workflows. +You can still run CLI commands in the integrated terminal for long-running or manual workflows. ## Windsurf, Cline, Goose, and other MCP clients -Use the [MCP server](#mcp-server) configuration when the client supports `mcpServers`, then tell the agent to use MCP tools or terminal CLI commands for device workflows. - -If the client has project rules or custom instructions, add the recommended agent rule above. If it does not, ask the agent to open the requested app and continue from the initial interactive snapshot; introduce a help topic only when the task is specialized or a command shape is unclear. +If the client supports `mcpServers`, use the [MCP server](#mcp-server) configuration, then tell the agent to use MCP tools or terminal CLI commands for device work. -## Why this setup works +If the client supports project rules or custom instructions, add the [recommended agent rule](#recommended-agent-rule). If it doesn't, ask the agent to open the requested app and continue from the initial interactive snapshot; point it to a help topic only when the task is specialized or a command is unclear. -The CLI stays the auditable automation surface, installed help stays version-matched with the commands, skills and rules route agents toward the right help topics, and MCP gives compatible clients direct structured tools backed by the same daemon/client implementation. +## Related pages -For the broader positioning, supported targets, observability features, and how `agent-device` differs from scripted test frameworks, see [Introduction](/docs/introduction). For exact command groups and platform behavior, see [Commands](/docs/commands). +For supported targets, evidence features, and how `agent-device` differs from scripted test frameworks, see [Introduction](/docs/introduction). For command groups and platform behavior, see [Commands](/docs/commands). For the local execution model, permissions, artifacts, and sensitive data guidance, see [Security & Trust](/docs/security-trust). ## Agent-readable docs -Use [llms-full.txt](https://oss.callstack.com/agent-device/llms-full.txt) when an agent needs a single text bundle of the current docs. The installed CLI remains authoritative for exact command syntax: +Use [llms-full.txt](https://oss.callstack.com/agent-device/llms-full.txt) when an agent needs the whole documentation as one text file. For exact command syntax, the installed CLI help is authoritative: ```bash agent-device help diff --git a/website/docs/docs/ai-sdk.md b/website/docs/docs/ai-sdk.md index a340778f48..aa2d2716d5 100644 --- a/website/docs/docs/ai-sdk.md +++ b/website/docs/docs/ai-sdk.md @@ -4,7 +4,7 @@ title: AI SDK # AI SDK -Use `agent-device/ai-sdk` to give an [AI SDK](https://ai-sdk.dev/) agent a typed set of tools for navigating and inspecting an app. The tools run in-process, share one named session, and default to the focused perceive-and-act surface most agents need. +Use `agent-device/ai-sdk` to give an [AI SDK](https://ai-sdk.dev/) agent a typed set of tools for navigating and inspecting an app. The tools run in-process, share one named session, and by default cover the core loop of reading the screen and acting on it. ```bash pnpm add agent-device ai @@ -48,13 +48,13 @@ try { } ``` -Set `AI_MODEL` to a model available through your configured AI SDK provider. The agent sees the available device tools and chooses the calls needed to complete the prompt. The returned `client` targets the same session; keep cleanup in `finally` so the device is released even if generation fails. +Set `AI_MODEL` to a model available through your configured AI SDK provider. The agent decides which device tools to call to complete the prompt. The returned `client` targets the same session; close it in `finally` so the device is released even if generation fails. -## Options +## Choose tools and approvals `set: 'core'` is the default. It exposes the perceive-and-act loop: open, close, snapshot, click, press, fill, type, get, is, find, wait, back, scroll, swipe, alert, and screenshot. - Pass `set: 'all'` when the agent also needs device-management or observability commands. -- Pass `approval: { close: 'user-approval' }` to require approval for a command. `createAgentDeviceTools()` returns the map as `toolApproval`, ready to pass to [`ToolLoopAgent`](https://ai-sdk.dev/docs/agents/tool-approvals). +- Pass `approval: { close: 'user-approval' }` to require user approval before a command runs. `createAgentDeviceTools()` returns the map as `toolApproval`, ready to pass to [`ToolLoopAgent`](https://ai-sdk.dev/docs/agents/tool-approvals). -See the [Node.js API](/docs/client-api) when the host application needs deterministic setup or other direct device control outside the agent loop. See the AI SDK reference for [`ToolLoopAgent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent). +Use the [Node.js API](/docs/client-api) when your application needs deterministic setup or other direct device control outside the agent loop. See the AI SDK reference for [`ToolLoopAgent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent). diff --git a/website/docs/docs/aws-device-farm.md b/website/docs/docs/aws-device-farm.md index bb92be698c..ddbfc37e9a 100644 --- a/website/docs/docs/aws-device-farm.md +++ b/website/docs/docs/aws-device-farm.md @@ -5,15 +5,15 @@ description: Drive AWS Device Farm remote-access sessions with agent-device. # AWS Device Farm -Use AWS Device Farm for hosted Android and iOS remote-access WebDriver sessions. The adapter does not route Vega OS or accept a Vega Fire TV ARN. The initial Vega workflow uses a local VVD. +Use AWS Device Farm to run agent-device on hosted Android and iOS devices through remote-access WebDriver sessions. You need an AWS account with a Device Farm project and the AWS CLI installed. AWS Device Farm does not cover Vega OS: agent-device rejects a Vega Fire TV ARN. For Vega, use a local Vega Virtual Device (VVD). -## Credentials and connection +## Set credentials and connect -AWS Device Farm uses the AWS CLI credential provider chain. `agent-device` runs `aws devicefarm ...`, so it works with any non-interactive AWS CLI credential source available in CI. It does not require `aws login`. See the [AWS CLI environment variable reference](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) for supported credential sources. +agent-device runs `aws devicefarm ...`, so it uses the AWS CLI credential provider chain and works with any non-interactive AWS CLI credential source in CI. You do not need `aws login`. See the [AWS CLI environment variable reference](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) for supported credential sources. -Use short-lived CI credentials instead of long-lived IAM user keys. In GitHub Actions, use OIDC to assume an IAM role and let the action export standard AWS environment variables. AWS documents [IAM OIDC providers](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html), and the official [`configure-aws-credentials` action](https://github.com/aws-actions/configure-aws-credentials) documents the GitHub Actions setup. +Use short-lived CI credentials instead of long-lived IAM user keys. In GitHub Actions, use OIDC to assume an IAM role and let the action export the standard AWS environment variables. AWS documents [IAM OIDC providers](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html), and the official [`configure-aws-credentials` action](https://github.com/aws-actions/configure-aws-credentials) documents the GitHub Actions setup. -For example, a CI job might set: +For example, a CI job can set: ```bash export AWS_REGION=us-west-2 @@ -30,7 +30,7 @@ export AWS_WEB_IDENTITY_TOKEN_FILE=/path/to/token export AWS_REGION=us-west-2 ``` -Connect with the Device Farm project, device, and optional app upload: +Connect with the Device Farm project, the device, and an optional app upload: ```bash agent-device connect aws-device-farm \ @@ -40,7 +40,7 @@ agent-device connect aws-device-farm \ --aws-app-arn arn:aws:devicefarm:us-west-2::upload: ``` -`--aws-app-arn` is optional when the remote-access session does not need an uploaded app. You can also provide the ARNs through environment variables: +Omit `--aws-app-arn` when the remote-access session does not need an uploaded app. You can also pass the ARNs through environment variables: ```bash export AWS_DEVICE_FARM_PROJECT_ARN=... @@ -48,13 +48,13 @@ export AWS_DEVICE_FARM_DEVICE_ARN=... export AWS_DEVICE_FARM_APP_ARN=... ``` -`AGENT_DEVICE_AWS_DEVICE_FARM_PROJECT_ARN`, `AGENT_DEVICE_AWS_DEVICE_FARM_DEVICE_ARN`, and `AGENT_DEVICE_AWS_DEVICE_FARM_APP_ARN` are accepted as agent-device-specific aliases. +agent-device also accepts `AGENT_DEVICE_AWS_DEVICE_FARM_PROJECT_ARN`, `AGENT_DEVICE_AWS_DEVICE_FARM_DEVICE_ARN`, and `AGENT_DEVICE_AWS_DEVICE_FARM_APP_ARN` as aliases. -`connect` makes read-only `get-project`, `get-device`, and, when supplied, `get-upload` calls. It rejects a device or app for the wrong platform, and an app upload that is not ready. AWS Device Farm does not support app installation after remote-access session allocation. When an app is required, run the printed reconnect command, including `--session --force`, before `open`. +`connect` makes read-only `get-project`, `get-device`, and, when you pass an app, `get-upload` calls. It rejects a device or app for the wrong platform, and an app upload that is not ready. You cannot install an app once the remote-access session is allocated. When an app is required, run the printed reconnect command, including `--session --force`, before `open`. -## CLI workflow +## Run a session from the CLI -Every unscoped `connect` creates a fresh connection. The printed next steps include its generated `--session`. Keep that flag on every command when multiple processes or CI jobs share a host. The active connection is only safe for one sequential workflow. To replace a named connection, run `connect ... --session --force`. An unscoped `--force` creates a new connection and leaves existing sessions untouched. +Every `connect` without `--session` creates a new connection, and the printed next steps include its generated `--session`. Keep that flag on every command when several processes or CI jobs share a host; the active connection is safe only for one sequential workflow. To replace a named connection, run `connect ... --session --force`. `--force` without `--session` creates a new connection and leaves existing sessions untouched. ```bash export AWS_REGION=us-west-2 @@ -76,11 +76,11 @@ agent-device artifacts --json agent-device disconnect ``` -For MCP-only use, run `connect` in the same effective state directory before starting `agent-device mcp`. MCP exposes operational tools but not provider `connect` commands. +To use AWS Device Farm only through MCP, run `connect` in the same effective state directory before you start `agent-device mcp`. MCP exposes device commands such as `open`, `snapshot`, `close`, and `artifacts`, but not provider `connect` commands. -## Node.js client +## Use the Node.js client -Use direct client configuration when the Node process manages AWS credentials and selectors: +Configure the client directly when your Node.js process manages the AWS credentials and selectors: ```ts import { createAgentDeviceClient } from 'agent-device'; @@ -98,16 +98,16 @@ await client.apps.open({ app: 'com.example.app' }); const closed = await client.sessions.close(); ``` -## Artifacts and troubleshooting +## Get artifacts and troubleshoot -After `close`, AWS Device Farm can return remote-access video and log artifacts after the provider finalizes them. Run `agent-device artifacts --json`, or look up a previous session explicitly: +After `close`, AWS Device Farm can return remote-access video and log artifacts once it finalizes them. Run `agent-device artifacts --json`, or look up an earlier session by its ARN: ```bash agent-device artifacts --provider aws-device-farm --json ``` -If `connect` fails, use the reported `aws devicefarm get-*` error to check the credential chain, ARN, region, resource platform, or upload readiness. The provider has not allocated a device yet. If artifacts are pending immediately after `close`, retry the lookup. +If `connect` fails, use the reported `aws devicefarm get-*` error to check the credential chain, ARN, region, resource platform, or upload readiness. A failed `connect` has not allocated a device yet. If artifacts are still pending right after `close`, retry the lookup. -On hosted WebDriver sessions, `fill` checks that the field received focus before it sends keys. If it cannot confirm focus, it fails without typing. Use `snapshot -i` to confirm the target. If the driver cannot expose focus at all, use `press ` followed by `type `. That sends text without confirming the destination. +On hosted WebDriver sessions, `fill` checks that the field received focus before it sends keys. If it cannot confirm focus, it fails without typing. Use `snapshot -i` to confirm the target. If the driver cannot report focus at all, use `press ` followed by `type `, which sends text without confirming where it lands. -A screen that never goes still — a looping video, a live ticker, continuous animation — gives the provider's driver no quiet moment to read the UI tree, so `snapshot -i` can run out of its read budget on a rented device while `screenshot` of the same screen still returns. Snapshots covers what that failure means and what to do instead; on a metered device the difference matters, because every second of the walk is billed. Take the screenshot, drive from `@refs` an earlier snapshot captured, and remember that `--depth` trims a tree after it arrives, so it cannot shorten a read that never returned. +On a screen that never goes still, such as a looping video, a live ticker, or continuous animation, `snapshot -i` can time out while `screenshot` of the same screen still returns. On a metered device every second of that read is billed, so do not retry the snapshot in a loop. Take a screenshot instead and drive from `@refs` an earlier snapshot captured. `--depth` trims the tree after it arrives, so it cannot shorten a read that never finishes. See [Snapshots](/docs/snapshots) for details. diff --git a/website/docs/docs/batching.md b/website/docs/docs/batching.md index 682dd8948d..99bfcf4d49 100644 --- a/website/docs/docs/batching.md +++ b/website/docs/docs/batching.md @@ -4,11 +4,10 @@ title: Batching # Batching -Use `batch` to run multiple commands in a single daemon request. +Use `batch` to run several commands in one daemon request. It saves round trips when you already know +the next sequence of actions. -This is useful for agent workflows that already know the next sequence of actions and want to reduce orchestration overhead. - -## CLI examples +## Run a batch from the CLI From a file: @@ -21,7 +20,7 @@ agent-device batch \ --json ``` -Inline for small payloads: +Inline, for small payloads: ```bash agent-device batch --steps '[{"command":"open","input":{"app":"settings"}},{"command":"wait","input":{"kind":"duration","durationMs":100}}]' @@ -29,7 +28,7 @@ agent-device batch --steps '[{"command":"open","input":{"app":"settings"}},{"com ## Step payload format -`batch` accepts a JSON array of steps: +Pass `batch` a JSON array of steps: ```json [ @@ -51,11 +50,14 @@ agent-device batch --steps '[{"command":"open","input":{"app":"settings"}},{"com Notes: -- `input` is required and uses the same fields as the matching MCP/Node command. -- Unknown top-level step fields are rejected. Supported keys are `command`, `input`, and `runtime`. -- The legacy `positionals`/`flags` step shape was removed in 0.21. Migrate each step to structured input, for example `{"command":"open","input":{"app":"settings","platform":"ios"}}`. -- nested `batch` and `replay` steps are rejected. -- `--on-error stop` is the supported behavior. +- `input` is required and takes the same fields as the matching MCP or Node client command. +- A step can only have `command`, `input`, and `runtime` keys; any other key is rejected. +- Steps written as `positionals`/`flags` are rejected (that shape was removed in 0.21). Rewrite each step with structured input, for example `{"command":"open","input":{"app":"settings","platform":"ios"}}`. +- `batch` and `replay` steps can't be nested inside a batch. +- `--on-error stop` is the only error mode: the batch stops at the first failing step. +- A batch runs at most 100 steps by default. Pass `--max-steps ` to raise or lower the limit, up to 1000. +- A step without `platform` uses the batch's `--platform`. Session lock defaults don't override it. +- Steps use the same session binding and [session lock mode](/docs/sessions#lock-a-named-session-to-a-device) as the `batch` command itself. ## Response shape @@ -78,7 +80,7 @@ Success: } ``` -In non-JSON mode, `batch` also prints a short per-step summary after the overall completion line. +Without `--json`, `batch` prints the overall completion line followed by a short summary of each step. Failure: @@ -102,16 +104,16 @@ Failure: } ``` -## Agent best practices +## Tips for agents -- Batch only one related screen flow at a time. -- After mutating steps (`open`, `click`, `fill`, `swipe`), add a sync guard (`wait`, `is exists`) before critical reads. -- Treat prior refs/snapshots as stale after UI changes. +- Keep each batch to one screen flow. +- After steps that change the UI (`open`, `click`, `fill`, `swipe`), add a `wait` or `is exists` step before reads that matter. +- Treat earlier refs and snapshots as stale after the UI changes. - Prefer `--steps-file` over inline JSON. -- Keep batches moderate (about 5-20 steps). -- Replan from the failing step using `details.step` and `details.partialResults`. +- Keep batches to about 5-20 steps. +- When a batch fails, use `details.step` and `details.partialResults` to replan from the failing step. -## Canonical recipes +## Example recipes Open app -> open thread -> type -> send @@ -150,9 +152,10 @@ Open app -> open action menu -> choose option -> verify ] ``` -## Stale accessibility tree risk +## Avoid acting on a stale accessibility tree -Rapid UI changes can outpace accessibility tree updates. Mitigate by inserting explicit waits and splitting long workflows into phases: +The accessibility tree can lag behind fast UI changes. Add explicit waits, and split long workflows +into separate batches by phase: 1. navigate 2. verify/extract diff --git a/website/docs/docs/browserstack.md b/website/docs/docs/browserstack.md index 681ba53d8a..12ba4825fb 100644 --- a/website/docs/docs/browserstack.md +++ b/website/docs/docs/browserstack.md @@ -5,11 +5,11 @@ description: Drive BrowserStack App Automate sessions with agent-device. # BrowserStack -Use BrowserStack App Automate for hosted Android and iOS WebDriver sessions. +Use BrowserStack App Automate to run agent-device on hosted Android and iOS devices over WebDriver. You need a BrowserStack account with App Automate access. -## Credentials and connection +## Set credentials and connect -Set BrowserStack credentials in a non-interactive environment: +Export your BrowserStack credentials, for example from CI secrets: ```bash export BROWSERSTACK_USERNAME=... @@ -26,11 +26,11 @@ agent-device connect browserstack \ --provider-app bs://app-id ``` -`--provider-app` accepts a BrowserStack app reference such as `bs://...`, an HTTP(S) app URL, or an existing local app path. BrowserStack uploads a local path when it creates the hosted session. +`--provider-app` accepts a BrowserStack app reference such as `bs://...`, an HTTP(S) app URL, or an existing local app path. agent-device uploads a local path when it creates the hosted session. -During `connect`, agent-device verifies the BrowserStack credentials and the exact device/OS pair. It checks a `bs://` reference against recent uploads and confirms that a local artifact exists before saving its absolute path. A public URL stays configured and BrowserStack validates it when the session starts. `open` still needs the app's installed package or bundle identifier, not its upload name. +`connect` verifies the BrowserStack credentials and the exact device/OS pair. It checks a `bs://` reference against recent uploads, and confirms that a local app file exists before saving its absolute path. A public URL is saved as-is; BrowserStack validates it when the session starts. `open` takes the app's installed package or bundle identifier, not its upload name. -A running daemon keeps the BrowserStack credentials it started with. If your shell holds different ones, the first command that allocates a lease, such as `open`, refuses before it creates a session; run `agent-device daemon stop` (with the same `--state-dir`) and rerun the command. A shell that sets neither variable uses the daemon's. +A running daemon keeps the BrowserStack credentials it started with. If your shell holds different ones, the first command that allocates a lease, such as `open`, refuses before it creates a session. Run `agent-device daemon stop` (with the same `--state-dir`) and rerun the command. A shell that sets neither variable uses the daemon's credentials. Optional labels: @@ -54,14 +54,14 @@ Optional device features: --provider-no-resign-app # iOS only ``` -BrowserStack receives these values in `bstack:options` when it creates the hosted session. +agent-device sends these values to BrowserStack as `bstack:options` capabilities when it creates the hosted session. -- The orientation applies when the session starts. An activity without a fixed orientation, such as a Chrome Custom Tab hosting OAuth, can still open in landscape. Run `agent-device orientation portrait` after launching it when needed. -- `--provider-appium-version` pins the Appium server BrowserStack runs for the session. Unset, BrowserStack falls back to its default Appium 1.x; `mobile:` commands such as `deepLink` and `pressButton` need a 2.x or newer server. +- The orientation applies when the session starts. An activity without a fixed orientation, such as a Chrome Custom Tab hosting OAuth, can still open in landscape. Run `agent-device orientation portrait` after it opens if you need portrait. +- `--provider-appium-version` pins the Appium server BrowserStack runs for the session. Without it, BrowserStack runs its default Appium 1.x, and `mobile:` commands such as `deepLink` and `pressButton` need Appium 2.x or newer. - `--provider-network-profile` and `--provider-custom-network` are mutually exclusive. -- `--provider-no-resign-app` applies to iOS only. BrowserStack re-signs uploaded iOS apps with its provisioning profile, which strips entitlements; opt out when testing entitlement-dependent features such as push notifications. +- `--provider-no-resign-app` applies to iOS only. BrowserStack re-signs uploaded iOS apps with its provisioning profile, which strips entitlements. Pass this flag when you test features that need entitlements, such as push notifications. -## CLI workflow +## Run a session from the CLI ```bash export BROWSERSTACK_USERNAME=... @@ -83,11 +83,11 @@ agent-device artifacts --json agent-device disconnect ``` -For MCP-only use, run `connect` in the same effective state directory before starting `agent-device mcp`. MCP exposes `open`, `snapshot`, `click`, `close`, and `artifacts`, but not provider `connect` commands. +To use BrowserStack only through MCP, run `connect` in the same effective state directory before you start `agent-device mcp`. MCP exposes device commands such as `open`, `snapshot`, `close`, and `artifacts`, but not provider `connect` commands. -## Node.js client +## Use the Node.js client -Use direct client configuration when the Node process manages BrowserStack credentials and selectors rather than a saved CLI connection profile: +Configure the client directly when your Node.js process manages the BrowserStack credentials and selectors instead of a saved CLI connection profile: ```ts import { createAgentDeviceClient } from 'agent-device'; @@ -118,14 +118,14 @@ if (providerSessionId) { } ``` -## Artifacts and troubleshooting +## Get artifacts and troubleshoot -After `close`, BrowserStack can return session video, Appium logs, device logs, dashboard URLs, and public URLs. Run `agent-device artifacts --json`, or look up a previous session explicitly: +After `close`, BrowserStack can return session video, Appium logs, device logs, dashboard URLs, and public URLs. Run `agent-device artifacts --json`, or look up an earlier session by its ID: ```bash agent-device artifacts --provider browserstack --json ``` -BrowserStack errors distinguish rejected credentials, an unavailable device/OS pair, a missing `bs://` upload, and a missing local artifact. If artifact lookup is pending immediately after `close`, retry it. BrowserStack may still be finalizing video and log URLs. +BrowserStack errors distinguish rejected credentials, an unavailable device/OS pair, a missing `bs://` upload, and a missing local app file. If artifacts are still pending right after `close`, retry the lookup; BrowserStack may still be finalizing video and log URLs. -On hosted WebDriver sessions, `fill` checks that the field received focus before it sends keys. If it cannot confirm focus, it fails without typing. Use `snapshot -i` to confirm the target. If the driver cannot expose focus at all, use `press ` followed by `type `. That sends text without confirming the destination. +On hosted WebDriver sessions, `fill` checks that the field received focus before it sends keys. If it cannot confirm focus, it fails without typing. Use `snapshot -i` to confirm the target. If the driver cannot report focus at all, use `press ` followed by `type `, which sends text without confirming where it lands. diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index dde7455752..9b4ea68bd6 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -10,7 +10,7 @@ Building an agent? Start with the dedicated [AI SDK](/docs/ai-sdk) or [Eve](/doc ## Runnable examples -The repository includes [runnable, typechecked Node.js examples](https://github.com/callstack/agent-device/tree/main/examples/sdk) that import the same published `agent-device/*` entry points used by consumers: +The repository includes [runnable, typechecked Node.js examples](https://github.com/callstack/agent-device/tree/main/examples/sdk). They import the same published `agent-device/*` entry points your code uses: | Example | Demonstrates | | --- | --- | @@ -19,7 +19,7 @@ The repository includes [runnable, typechecked Node.js examples](https://github. | [`batch-orchestration.ts`](https://github.com/callstack/agent-device/blob/main/examples/sdk/batch-orchestration.ts) | Run a batch through a custom transport | | [`metro-runtime.ts`](https://github.com/callstack/agent-device/blob/main/examples/sdk/metro-runtime.ts) | Normalize a Metro URL and resolve runtime transport hints | -The examples are checked against the source SDK using their dedicated [`tsconfig.json`](https://github.com/callstack/agent-device/blob/main/examples/sdk/tsconfig.json). After building the package with `pnpm build`, run an example directly with Node: +Each example typechecks against the SDK through its own [`tsconfig.json`](https://github.com/callstack/agent-device/blob/main/examples/sdk/tsconfig.json). Build the package with `pnpm build`, then run an example with Node: ```bash node --experimental-strip-types examples/sdk/client-session.ts @@ -27,7 +27,7 @@ node --experimental-strip-types examples/sdk/client-session.ts ## API reference -Supported public entry points for Node consumers: +Public entry points: - `agent-device` - `createAgentDeviceClient(options?)` @@ -108,21 +108,21 @@ Supported public entry points for Node consumers: ## Basic usage -The canonical client example is embedded below. It is also runnable from [`examples/sdk/client-session.ts`](https://github.com/callstack/agent-device/blob/main/examples/sdk/client-session.ts). +This example opens an app, takes a snapshot, interacts, handles typed errors, and always closes the session. Run it from [`examples/sdk/client-session.ts`](https://github.com/callstack/agent-device/blob/main/examples/sdk/client-session.ts). ```ts file="/../examples/sdk/client-session.ts" ``` -`client.devices.capabilities()` returns `{ device, availableCommands }`, using the same capability matrix as the CLI. Use it when a dynamic integration needs to decide which command names are valid for the selected target. +`client.devices.capabilities()` returns `{ device, availableCommands }`, using the same capability matrix as the CLI. Use it to check which command names the selected target supports. For direct iOS simulator app launches, `client.apps.open({ app, platform: 'ios', launchConsole: './artifacts/app.console.log' })` captures launch-time stdout/stderr. The option mirrors `open --launch-console` and is not valid for URL opens or non-simulator targets. -`client.sessions.stateDir()` mirrors `session state-dir` and returns the resolved daemon state directory as a pure local resolution — it never starts -or contacts the daemon. Pass `{ stateDir }` to resolve an explicit override the same way the CLI resolves `--state-dir`. +`client.sessions.stateDir()` mirrors `session state-dir` and returns the resolved daemon state directory without starting +or contacting the daemon. Pass `{ stateDir }` to resolve an explicit override the same way the CLI resolves `--state-dir`. `client.sessions.artifacts({ provider, providerSessionId })` mirrors `artifacts --provider ... --provider-session ...` and returns provider-hosted `cloudArtifacts`. -Use it for BrowserStack, AWS Device Farm, or TestMu AI session videos/logs after a cloud session has stopped, or omit `providerSessionId` when an embedding host has registered a provider runtime that can infer the active lease. Limrun does not currently expose provider artifacts through this command. +Use it for BrowserStack, AWS Device Farm, or TestMu AI session videos/logs after a cloud session has stopped; `client.sessions.close()` returns the ID as `closed.provider?.providerSessionId`. Omit `providerSessionId` when an embedding host has registered a provider runtime that can infer the active lease. Limrun does not expose provider artifacts through this command. ```ts const result = await client.sessions.artifacts({ @@ -139,7 +139,7 @@ if ('cloudArtifacts' in result) { ## Device cloud sessions -Limrun, BrowserStack, AWS Device Farm, and TestMu AI can be driven through the normal typed client methods. Use the corresponding CLI `connect` flow when you want persisted local connection state. Use direct client config when a Node integration already owns credentials and provider selectors. +You drive Limrun, BrowserStack, AWS Device Farm, and TestMu AI devices with the same typed client methods. Use the CLI `connect` flow to persist connection state locally. Pass provider settings in the client config when your Node.js integration already owns credentials and provider selectors. ```ts import { createAgentDeviceClient } from 'agent-device'; @@ -163,14 +163,14 @@ from an explicit selector, an existing session, one local booted/bootable candid simulator with the app installed, or one provider-owned candidate. Ambiguous requests fail with structured retry selectors instead of silently retargeting. -Use `client.sessions.artifacts({ provider, providerSessionId })` with `closed.provider?.providerSessionId` to fetch provider-hosted video and log URLs after close. See the [BrowserStack](/docs/browserstack), [AWS Device Farm](/docs/aws-device-farm), [TestMu AI](/docs/testmu), and [Limrun](/docs/limrun) guides for provider-specific setup. +To fetch provider-hosted video and log URLs after close, pass `closed.provider?.providerSessionId` to [`client.sessions.artifacts()`](#basic-usage). See the [BrowserStack](/docs/browserstack), [AWS Device Farm](/docs/aws-device-farm), [TestMu AI](/docs/testmu), and [Limrun](/docs/limrun) guides for provider-specific setup. ## Web sessions -Typed client commands can target browser sessions with the same command methods by passing -`platform: 'web'`. The managed web backend is set up through the CLI, not through a typed client -method, so run `agent-device web setup` before first use in the same effective state directory. Use -`agent-device web doctor` when you need to verify backend health. +Pass `platform: 'web'` to drive a browser session with the same command methods. Web automation +requires Node 24+. No client method sets up the managed web backend: run `agent-device web setup` +before first use, in the same state directory the client uses. Run `agent-device web doctor` to check +backend health. ```ts await client.apps.open({ url: 'https://example.com', platform: 'web' }); @@ -188,24 +188,22 @@ await client.observability.audio({ await client.sessions.close(); ``` -Web automation requires Node 24+. MCP tools use the same command contracts, so they can target -`platform: 'web'` after setup, but local setup/doctor remains a CLI-only workflow. Web network -inspection adapts managed `agent-browser` request history to the existing network result shape; -request and response bodies are not exposed by that backend path. Web audio probes sample HTML -media elements and return compact dBFS buckets. +MCP tools use the same command contracts, so they can also target `platform: 'web'` after setup; +setup and doctor stay CLI-only. Web network inspection returns the standard network result shape +without request or response bodies. ## Android ADB providers -Use `agent-device/android-adb` when a bridge owns Android device access but wants upstream command -behavior for ADB-shaped operations. Executors receive arguments after `adb`, so remote bridges can -route the same argument arrays through an ADB tunnel, websocket API, or another remote transport. +Use `agent-device/android-adb` when your bridge owns Android device access and you want +agent-device's behavior for ADB operations. Executors receive the arguments that follow `adb`, so a +remote bridge can route the same argument arrays through an ADB tunnel, websocket API, or another +remote transport. -The public helpers accept an executor directly and do not expose the daemon's scoped adb -interception internals. Use `captureAndroidLogcatWithAdb(executor, options?)` when a bridge needs a +The helpers take an executor directly. Use `captureAndroidLogcatWithAdb(executor, options?)` for a bounded logcat capture. -Providers can also expose `reverse` for first-class port reverse ownership. Plain executors do not -advertise reverse support automatically; call `createAndroidPortReverseManager(providerOrExecutor)` +A provider can also expose `reverse` to own port reversal. Plain executors do not advertise reverse +support; call `createAndroidPortReverseManager(providerOrExecutor)` only when the provider supports `adb reverse` argument semantics. The manager makes duplicate setup idempotent for the same owner and rejects conflicting owners for the same local endpoint. For a device that other adb clients also drive, pass an executor with `{ noRebind: true }`: the manager @@ -239,11 +237,11 @@ const foreground = await getAndroidAppStateWithAdb(provider.exec); ## Command methods -Use `client.command.()` for command-level device actions. It uses the same daemon transport path as the higher-level client methods, including session metadata, tenant/run/lease fields, normalized daemon errors, and remote artifact handling. +Use `client.command.()` for command-level device actions. These calls go through the same daemon transport as the higher-level client methods, so they carry session metadata and tenant/run/lease fields, return normalized daemon errors, and handle remote artifacts. -Results are daemon-shaped objects with typed known fields, so command semantics stay aligned with the CLI. +Results are daemon-shaped objects with typed known fields and follow the CLI's command semantics. -A failed interaction rejects with the same error the CLI prints. Read `error.details.dispatched` before you retry; [Commands](./commands.md) explains the two values. +A failed interaction rejects with the same error the CLI prints. Read `error.details.dispatched` before you retry; [Retry after a failed command](/docs/commands#retry-after-a-failed-command) explains the values. Every client call that dispatches to the daemon accepts `signal?: AbortSignal` to cancel that one call: @@ -252,7 +250,7 @@ const controller = new AbortController(); await client.interactions.press({ ref: '@e12', signal: controller.signal }); ``` -With the built-in transport, a signal that is already aborted rejects the call without sending anything (`error.details.dispatched: 'no'`). Aborting while the request is in flight closes that request's connection, the daemon marks the request canceled, and the promise rejects with the typed canceled-request error (`error.details.reason: 'request_canceled'`, `error.details.dispatched: 'unknown'`). A custom transport receives the signal on its context and may cancel differently. The daemon and the session stay alive for other requests, and an abort is never a timeout: it never triggers the timeout path's runner cleanup or daemon reset. Cancellation covers the daemon request itself, so a response-artifact download already underway is not stopped, and a canceled one-shot replay still runs the existing cleanup that can tear down a daemon this client started. +With the built-in transport, a signal that is already aborted rejects the call without sending anything (`error.details.dispatched: 'no'`). Aborting while the request is in flight closes that request's connection, the daemon marks the request canceled, and the promise rejects with the typed canceled-request error (`error.details.reason: 'request_canceled'`, `error.details.dispatched: 'unknown'`). A custom transport receives the signal on its context and may cancel differently. The daemon and the session stay alive for other requests. An abort is not a timeout: it never triggers the runner cleanup or daemon reset that a timeout does. Cancellation covers only the daemon request, so a response-artifact download already underway is not stopped, and a canceled one-shot replay still runs the existing cleanup that can tear down a daemon this client started. ```ts await client.command.wait({ @@ -308,7 +306,7 @@ await client.command.fold({ `fold` accepts either `pose` or `keyframes`. Keyframes use linear interpolation at roughly 60 updates per second; repeat an angle to hold it. Timestamps must start at zero and increase strictly, with 2–64 frames and a final timestamp no greater than 60,000ms. Angles must be finite and between 0° and 180°. The final timestamp bounds motion, excluding helper preparation and final hinge verification. A custom final angle is verified within 0.5°; interior angles must also settle. Cancellation stops the motion at its current angle. Re-snapshot afterwards, including after interrupted motion. -`press`, `click`, and `longpress` take `readinessTimeoutMs`. With it, the command waits up to that many milliseconds for a target that is not on screen yet, then performs the requested interaction. Without it, the command looks once and fails at once, which is the right choice for an agent that most often misses because the selector is wrong. Use it in scripted flows, where a step can land a render early: +`press`, `click`, and `longpress` take `readinessTimeoutMs`. With it, the command waits up to that many milliseconds for a target that is not on screen yet, then performs the requested interaction. Without it, the command looks once and fails at once, which is the right choice for an agent that most often misses because the selector is wrong. Use it in scripted flows, where a step can run before the screen finishes rendering: ```ts await client.interactions.press({ @@ -319,7 +317,7 @@ await client.interactions.press({ The wait is capped at 2 seconds and covers only a target that has not appeared. When the target is still missing after the wait, the error carries `error.details.readiness` with `waitedMs`, `polls`, and `end` (`expired` or `stalled`). A capture that shows an empty accessibility tree ends the wait at once with `capture_sparse` and `readiness.end: sparse`. When the command had to wait and then succeeded, the result carries `data.readiness` with `polls` and `waitedMs`. A command that found its target on the first look has no `readiness` field. A covered, off-screen, or ambiguous target fails at once, and a screen that stays unreadable for the whole wait fails with its own error; neither carries `readiness`. `readinessTimeoutMs` is not an MCP tool argument and has no CLI flag. -Vega OS client support is currently VVD-only and covers device discovery, app open/close, `back`, `home`, and `tvRemote`. Physical Fire TV, capture, selector, install, logging, and performance methods report unsupported for Vega targets. +Vega OS client support is VVD-only and covers device discovery, app open/close, `back`, `home`, and `tvRemote`. Physical Fire TV, capture, selector, install, logging, and performance methods report unsupported for Vega targets. Supported command methods: @@ -340,9 +338,9 @@ Supported command methods: - `prepare` - `viewport` -The deprecated `rotate()` alias remains available for compatibility; use `orientation()` in new integrations. +`rotate()` is a deprecated alias for `orientation()`; use `orientation()`. -The complete domain-client method map is: +Domain client methods: - `client.devices.list()`, `capabilities()`, `boot()`, `shutdown()` - `client.sessions.list()`, `stateDir()`, `close()`, `saveScript()`, `artifacts()` @@ -361,10 +359,10 @@ The complete domain-client method map is: `client.devices.list()` returns `AgentDeviceDevice` entries. Their optional `model` and `osVersion` fields describe the hardware and OS when discovery reports them; see [Device discovery](/docs/commands#device-discovery) for the sources. -`client.capture.snapshot()` carries an optional `viewport: { width, height }` beside `nodes`: the box those rects are measured in, in the same coordinate space and orientation, so a consumer scales and clips against the screen it was shown instead of inferring one from the largest rect on screen. It is absent when the producer measured no box and never reported as a zero; see [`snapshot`](/docs/commands) for what each producer answers with. +`client.capture.snapshot()` carries an optional `viewport: { width, height }` beside `nodes`: the box those rects are measured in, in the same coordinate space and orientation. Scale and clip against it instead of inferring the screen from the largest rect. When no box was measured, the field is absent rather than zero; see [Coordinates and viewport](/docs/snapshots#coordinates-and-viewport) for what each producer answers with. `client.observability.events({ cursor, limit })` reads the session event timeline as paged JSON entries. Use `nextCursor` from the previous page to continue from the daemon-owned `events.ndjson` file without replaying already uploaded/displayed events. Cursors are absolute and survive the file's size rotation; a cursor older than the retained window rejects with `COMMAND_FAILED`, `details.reason: "EVENT_LOG_CURSOR_EXPIRED"`, and `details.earliestCursor` to resume from. -The event timeline keeps operational context such as command/status/timing, paths, session/device/app identifiers, refs/selectors, and coordinates. Typed text, clipboard writes, push/event payloads, raw unknown command arguments, and matching raw message fragments are replaced with length-only placeholders. +The timeline leaves out user-entered content such as typed text, selector values, and payloads; see [Sessions](/docs/sessions#find-a-sessions-logs-and-artifacts) for what it keeps. `client.observability.audio()` mirrors `audio probe start|status|stop`. Use it to collect compact RMS/peak dBFS buckets while other session actions continue: @@ -385,11 +383,18 @@ const audio = await client.observability.audio({ await client.observability.audio({ platform: 'web', action: 'probe', probeAction: 'stop' }); ``` -Web probes sample HTML media elements. Host-system probes use `platform: 'macos'`, `platform: 'ios'` for iOS simulators, or `platform: 'android'` for Android emulators on macOS hosts. They sample host system audio through ScreenCaptureKit and require Screen Recording permission. Physical iOS and Android app audio are not exposed by this command. +Pass `platform: 'web'`, `'macos'`, `'ios'` (simulators), or `'android'` (emulators on macOS hosts). See [Audio probes](/docs/debugging-profiling#audio-probes) for what each platform samples, permissions, and unsupported targets. -Pass an explicit area to `client.observability.perf()` so each request stays focused; options and `area` are required. The removed optionless call and `area: 'metrics'` aggregate shape fail with replacements in 0.21. Pass `{ area: 'frames' }` for a bounded frame/jank-health payload or `{ area: 'memory', action: 'sample' }` for a compact memory-only sample. Use `{ area: 'memory', action: 'snapshot', kind: 'android-hprof', out: 'app.hprof' }` on Android or `{ area: 'memory', action: 'snapshot', kind: 'memgraph', out: 'app.memgraph' }` on supported Apple simulator/macOS app sessions to write large memory artifacts to disk. Android native artifacts use `{ area: 'cpu', subject: 'profile', action: 'start' | 'stop' | 'report', kind: 'simpleperf', out }` and `{ area: 'trace', action: 'start' | 'stop', kind: 'perfetto', out }`; CPU reports return at most ten top functions in data and print five, while trace/profile contents remain on disk. Physical iOS device memgraph capture reports unavailable with a reason/hint. On Android and supported Apple targets, `data.metrics.fps.droppedFramePercent` is the primary frame-smoothness value. Android derives it from the current `adb shell dumpsys gfxinfo framestats` window; connected iOS devices derive it from `xcrun xctrace` Animation Hitches for the active app process. Frame samples include `windowStartedAt`, `windowEndedAt`, and `worstWindows` so agents can correlate dropped-frame clusters with logs, network entries, and their own session actions. A successful Android read resets Android frame stats; `open ` resets the Android frame window too, so agents can call `perf({ area: 'frames' })`, perform a transition or gesture, then call it again to inspect that focused window. iOS simulator and macOS app sessions report frame health as unavailable rather than inventing FPS or dropped-frame values. +`client.observability.perf()` requires an options object with an explicit `area`; a call without one fails with an error that names the replacement. The option shapes mirror the `perf` CLI forms: -For Apple native profiling, call `perf({ area: 'cpu', subject: 'profile', action: 'start', kind: 'xctrace', template: 'Time Profiler', out: 'app.trace' })`, then stop with the same trace path and write a compact report with `action: 'report'`. The CPU report includes a bounded weighted top-function summary; the raw trace remains an artifact. `area: 'trace'` supports xctrace templates such as `Animation Hitches`. +- Frame health: `{ area: 'frames' }` +- Memory: `{ area: 'memory', action: 'sample' }`, or `{ area: 'memory', action: 'snapshot', kind: 'android-hprof' | 'memgraph', out }` to write an artifact to disk +- CPU profile: `{ area: 'cpu', subject: 'profile', action: 'start' | 'stop' | 'report', kind: 'xctrace' | 'simpleperf', out }` +- Trace: `{ area: 'trace', action: 'start' | 'stop', kind: 'perfetto' | 'xctrace', out }` + +To start an xctrace capture, also pass a `template` such as `'Time Profiler'` or `'Animation Hitches'`. + +On Android, each successful `perf({ area: 'frames' })` read and each `open` of an app resets the frame window, so call it, perform a transition or gesture, then call it again to inspect that window. See [Performance snapshots](/docs/debugging-profiling#performance-snapshots) for result fields and platform support. `client.recording.record({ action: 'start', path, quality: 'medium' })` starts a recording with medium output quality. @@ -397,14 +402,14 @@ For Apple native profiling, call `perf({ area: 'cpu', subject: 'profile', action `client.batch.run({ steps })` accepts structured steps: `{ command: 'open', input: { app: 'settings' } }`. Step `input` uses the same fields as the -matching client command; daemon-shaped `positionals`/`flags` steps are internal to the daemon batch -executor. +matching client command. Daemon-shaped `positionals`/`flags` steps are internal to the daemon and not +part of the client API. ## Batch orchestration for custom transports -Use `agent-device/batch` when a bridge or in-process runner receives daemon-shaped requests but owns command dispatch itself. The helper keeps validation, inherited flags, serial execution, partial results, and error envelopes aligned with the daemon batch command. +Use `agent-device/batch` when a bridge or in-process runner receives daemon-shaped requests but owns command dispatch itself. The helper applies the same validation, inherited flags, serial execution, partial results, and error envelopes as the daemon `batch` command. -The standalone custom-transport example is embedded below from [`examples/sdk/batch-orchestration.ts`](https://github.com/callstack/agent-device/blob/main/examples/sdk/batch-orchestration.ts). +Full example from [`examples/sdk/batch-orchestration.ts`](https://github.com/callstack/agent-device/blob/main/examples/sdk/batch-orchestration.ts): ```ts file="/../examples/sdk/batch-orchestration.ts" ``` @@ -439,22 +444,22 @@ if (installed.materializationId) { await androidClient.sessions.close(); ``` -On Android, a successful `installFromSource()` response returns enough app identity to relaunch the installed app: +On Android, a successful `installFromSource()` response includes the identity you need to relaunch the app: - `packageName` - `launchTarget` -If the daemon cannot determine installed app identity, the request fails instead of returning an empty success payload. +If the daemon cannot determine the installed app's identity, the request fails instead of returning an empty success payload. ## URL source rules -`installFromSource()` URL sources are intentionally limited: +`installFromSource()` limits URL sources: - Private and loopback hosts are blocked by default. - URL sources from any public host may point directly to an installable, including a bare iOS `.ipa`, or to a `.zip`, `.tar`, `.tar.gz`, or `.tgz` archive containing exactly one. - For existing reachable artifact URLs, use `source: { kind: 'url', url: ... }`. - For local artifacts, use `source: { kind: 'path', path: ... }` or the CLI `install`/`reinstall` commands. -- For compatible remote daemons that resolve CI artifacts server-side, pass a GitHub Actions artifact source: +- When a remote daemon resolves CI artifacts server-side, pass a GitHub Actions artifact source: ```ts await client.apps.installFromSource({ @@ -468,7 +473,7 @@ await client.apps.installFromSource({ }); ``` -Remote daemons may also support `{ kind: 'github-actions-artifact', owner, repo, artifactName }` or `{ kind: 'github-actions-artifact', owner, repo, runId, artifactName }`. The local client preserves these payloads and does not perform GitHub authentication or artifact download. +Remote daemons may also support `{ kind: 'github-actions-artifact', owner, repo, artifactName }` or `{ kind: 'github-actions-artifact', owner, repo, runId, artifactName }`. The local client passes these payloads through unchanged; it does not authenticate with GitHub or download the artifact. Android `.apk` and `.aab` URL sources resolve package identity from the downloaded install artifact. Archive URLs may contain one installable `.apk`, `.aab`, `.ipa`, or iOS `.app`, including inside nested archives. @@ -512,9 +517,9 @@ Use `agent-device/remote-config` for profile loading and path resolution, `agent ## Selector helpers -Use `agent-device/selectors` when a remote daemon or bridge needs to parse and match selector expressions without deep-importing daemon internals. The `role=` term matches the platform-neutral `kind` vocabulary that `snapshot --json` publishes (see [Snapshots](./snapshots.md#structured-node-fields---json)): pass the nodes as captured, and a node whose `kind` is `text` always matches `role=text`. Separately, pre-reconciliation leaf spellings (`statictext`, `edittext`, `textview`, …) still match during a deprecation window, each on the nodes that actually carried that class. A legacy spelling matches beside such a node's `kind`, not in place of it — `role=webarea` resolves a macOS-helper `AXWebArea` node whose `kind` is `axwebarea`, for example. Matching stays platform-aware because editability checks differ by backend. +Use `agent-device/selectors` to parse and match selector expressions in a remote daemon or bridge. The `role=` term matches the platform-neutral `kind` vocabulary that `snapshot --json` publishes (see [Snapshots](./snapshots.md#structured-node-fields---json)): pass the nodes as captured, and a node whose `kind` is `text` always matches `role=text`. Separately, pre-reconciliation leaf spellings (`statictext`, `edittext`, `textview`, …) still match during a deprecation window, each on the nodes that actually carried that class. A legacy spelling matches beside such a node's `kind`, not in place of it — `role=webarea` resolves a macOS-helper `AXWebArea` node whose `kind` is `axwebarea`, for example. Matching stays platform-aware because editability checks differ by backend. -`listSelectorChainMatches(nodes, chain, options)` returns every node the winning selector alternative matches, in snapshot order, plus that alternative and its index — the same first-match domain `findSelectorChainMatch` uses, without uniqueness refusal, so a runner applies its own strictness to the same nodes the CLI matched. `resolveSelectorChain` can name a LATER alternative: by default it refuses an ambiguous one and keeps walking, so the indices agree when it passes `requireUnique: false`, when the first matching alternative is unique, or when `disambiguateAmbiguous: true` resolves that alternative in place. It returns `null` when no alternative matches. `options` is `{ platform, requireRect? }`; the matched `SnapshotNode` objects are the ones passed in. +`listSelectorChainMatches(nodes, chain, options)` returns every node the winning selector alternative matches, in snapshot order, plus that alternative and its index — the same first-match domain `findSelectorChainMatch` uses, without uniqueness refusal, so a runner applies its own strictness to the same nodes the CLI matched. `resolveSelectorChain` can name a later alternative: by default it refuses an ambiguous one and keeps walking, so the indices agree when it passes `requireUnique: false`, when the first matching alternative is unique, or when `disambiguateAmbiguous: true` resolves that alternative in place. It returns `null` when no alternative matches. `options` is `{ platform, requireRect? }`; the matched `SnapshotNode` objects are the ones passed in. ```ts import { findSelectorChainMatch, parseSelectorChain } from 'agent-device/selectors'; @@ -528,3 +533,5 @@ const match = findSelectorChainMatch(snapshot.nodes, chain, { if (!match) { // Build a daemon-shaped error with formatSelectorFailure(...) if needed. +} +``` diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index b5be1a80ca..c3b6bd1398 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -4,26 +4,36 @@ title: Commands # Commands -This page summarizes the primary command groups. +Find the command for a task, the flags it takes, and how it behaves on each platform. For persistent defaults and project-scoped CLI settings, see [Configuration](/docs/configuration). -For agent workflow guidance that is matched to the installed CLI, run: +For agent workflow guidance that matches your installed CLI version, run: ```bash agent-device help +agent-device help commands agent-device help workflow +agent-device help manual-qa +agent-device help dogfood +agent-device help validate agent-device help debugging +agent-device help scripting +agent-device help gestures agent-device help react-native agent-device help react-devtools -agent-device help remote +agent-device help cdp +agent-device help tv agent-device help web agent-device help macos -agent-device help dogfood +agent-device help remote +agent-device help physical-device agent-device help ios-system-ui +agent-device help maestro +agent-device help foldable ``` -Skills are recommended for auto-routing when your agent runtime supports them, but they are not required. The CLI help topics are the version-matched operating contract. +If your agent runtime supports skills, install them to route agents automatically; they are optional. The CLI help topics are the reference for your installed version. For MCP-aware clients that support direct tools, run: @@ -31,7 +41,15 @@ For MCP-aware clients that support direct tools, run: agent-device mcp ``` -The MCP server exposes direct structured tools for installed commands. Tools use structured input contracts through `AgentDeviceClient`; local-only workflows stay CLI-only rather than subprocess fallbacks. It does not expose generic shell execution over MCP. MCP tools can target `platform: "web"` after `agent-device web setup`, but setup and doctor stay CLI-only. +For MCP client setup and what MCP does not expose, see [AI Agent Setup](/docs/agent-setup#mcp-server). + +## Output and warnings + +In text mode the CLI prints every response warning as a `Warning:` line after the command's own +output, for every command. Four commands declare their stdout to be the value a caller pipes (`get`, +`find`, `clipboard`, `record`) and print those lines on **stderr** instead, so +`value=$(agent-device get text …)` still captures exactly the value. `--json` keeps them in +`data.warnings` and writes neither line. ## Navigation @@ -68,14 +86,13 @@ agent-device fold open - `device release --stale` settles a provably dead owner's durable resources through the same exact-owner reconciliation `open` uses and clears its claim last, all without a daemon. Live, uncertain, PID-reused, and corrupt claims always fail closed and are reported with the reason; a live owner is released by closing its session from its own workspace or with `daemon stop --state-dir `. One claim condition is settled on the device instead: a device that rebooted after its claim was taken destroyed the app, runner, and accessibility session that claim described, so `open` reconciles that owner's resources, takes the claim, and reports the release in `warnings`. `device release --stale` never probes a device for its boot, so it keeps refusing such a claim. - `--platform apple` is an alias for the Apple automation backend (`ios`, `tvOS`, `macOS` selection). - Use `--target mobile|tv|desktop` with `--platform` (required) to select phone/tablet vs TV-class vs desktop-class targets. -- `boot` is mainly needed when starting a new session and `open` fails because no booted simulator/emulator is available. -- `boot --timeout ` is a startup budget for the Simulator or Android emulator boot, same as `open`'s. A never-booted iOS Simulator runs Apple's first-boot migration, which can take several minutes; without the flag the command's 90-second request envelope ends the boot first, before the wait's own 120-second cap ever applies. When the budget runs out the command fails with `error.details.reason: boot_timeout` and the Simulator or emulator keeps booting, so a retry finds it further along. +- You need `boot` mainly when you start a new session and `open` fails because no simulator or emulator is booted. +- `--timeout ` on `boot`, `open `, and `prepare ios-runner` is a startup budget for the iOS Simulator or Android emulator boot (for `prepare ios-runner`, one budget shared with runner preparation). A never-booted iOS Simulator runs Apple's first-boot migration, which can take several minutes; without the flag the command's 90-second request envelope ends the boot first, before the wait's own 120-second cap ever applies. When the budget runs out the command fails with `error.details.reason: boot_timeout` and the Simulator or emulator keeps booting, so a retry finds it further along. See [Sessions](/docs/sessions#wait-for-a-slow-or-busy-device). - Android: `boot --platform android --device ` launches that emulator in GUI mode when needed. - Android: add `--headless` to launch without opening a GUI window. - Android: `shutdown --platform android --device ` stops a running emulator. -- `open [app|url] [url]` already boots/activates the selected target when needed. -- `open --timeout ` is a startup budget for that boot, on iOS Simulators and Android emulators. A never-booted iOS Simulator runs Apple's first-boot migration, which can take several minutes; without the flag the 90-second request envelope ends the boot first, before the wait's own 120-second cap ever applies. When the budget runs out the command fails with `error.details.reason: boot_timeout` and the Simulator keeps booting, so a retry finds it further along. -- `open --wait ` waits up to that budget for a device another session is holding instead of failing at once. The open reports each poll, then either opens the device or fails with `DEVICE_IN_USE` naming the owning session and saying the budget was spent. A wait that finds the device taken again keeps waiting for the rest of its budget, so several opens can queue on one device and none of them is refused before its budget is spent. Only session contention is waited for: a device claim held by another workspace's daemon is never retriable and returns its recovery command immediately. The wait extends the command's timeout envelope, so a long budget does not need a longer `--timeout`. +- `open [app|url] [url]` boots and activates the selected target when needed. +- `open --wait ` waits up to that budget for a device another session is holding instead of failing at once, then fails with `DEVICE_IN_USE` if the budget runs out. Only session contention is waited for: a device claim held by another workspace's daemon is never retriable and returns its recovery command immediately. The wait extends the command's timeout envelope, so a long budget does not need a longer `--timeout`. See [Sessions](/docs/sessions#wait-for-a-slow-or-busy-device) for queuing behavior. - `open ` deep links are supported on Android and iOS. - On an iOS Simulator, handing the URL to the device is one bounded `simctl openurl`: when CoreSimulator never answers it, the open fails within 20 seconds with `details.reason: ios-simulator-openurl-timeout` and kills the child, instead of the request being cancelled around a still-running `openurl`. The Simulator may still have received the URL, so retry the open or answer the prompt with `alert accept`. - `open ` opens a deep link on iOS. @@ -90,8 +107,8 @@ agent-device fold open non-simulator targets. - `open --platform macos --surface app|frontmost-app|desktop|menubar` selects the macOS session surface explicitly. `app` is the default when an app argument is provided. - A surface given to `open`, as a flag or from a config file, is saved in `--save-script` scripts, so `replay` reopens the same surface. When no surface is given, the script has no `--surface`, and replay opens an app session. -- `back` now defaults to app-owned back navigation. On Apple targets that means visible in-app back UI only. On Android this currently maps to the same back keyevent because Android routes in-app back through that platform event. -- `back --in-app` is an explicit alias for the default app-owned behavior. +- `back` defaults to app-owned back navigation. On Apple targets that means visible in-app back UI only. On Android it sends the back keyevent, because Android routes in-app back through that platform event. +- `back --in-app` names the default app-owned behavior explicitly. - `back --system` asks for system back input explicitly. On Android this is the normal back keyevent. On iOS and tvOS it uses the platform back gesture or Siri Remote menu action. On macOS, where there is no generic system back input, `back --system` reports unavailable instead of falling back to app-owned navigation. - `orientation ` forces a mobile device into `portrait`, `portrait-upside-down`, `landscape-left`, or `landscape-right`. - `orientation` is supported on iOS and Android mobile targets. macOS and tvOS do not expose it. @@ -99,28 +116,22 @@ agent-device fold open - `action-button` is an iPhone and iPad command. Android, web, Linux, HarmonyOS, and Vega refuse it, and so do tvOS, macOS, and visionOS leaves. - `action-button` asks the device whether it has the button before pressing it. A target whose model has none — an iPhone SE beside an iPhone 15, or most iPad simulators — fails with `UNSUPPORTED_OPERATION` rather than reporting a press that never happened. - `action-button` does not activate or relaunch the session's app, and it takes no `--settle`: pressing a hardware button is not a navigation, so the app stays where it was. -- `action-button` reports that the press was dispatched, not what the system did with it. Simulators run no Shortcuts and no App Intents, so what a press triggers can only be verified on a physical iPhone; on a Simulator the command proves the press was accepted and that the session app was not brought forward. -- `fold ` puts a foldable iPhone simulator (iPhone Duo) into a hinge pose. The command sends a private HID hinge event inside the selected simulator (ADR 0025), then reads the hinge angle back with `devicectl device motion hinge-angle` and reports the pose only when that reading agrees: `closed` is 0°, `open` is 180°, and `half-open` is any angle between them (requested at 130°). An angle inside that interval only proves the category, so `half-open` is reported once two consecutive readings both fall inside it and agree within 0.5°. The response names the panel the device now lights and its native panel point size, marked `coordinateSpace: "native-panel"`; that size is the panel's own geometry, not the next snapshot's viewport (a 669x951 inner panel can host a 951x669 app window), so it cannot place a tap. Re-snapshot afterwards, and never carry refs or coordinates across a `fold`. After that snapshot, taps, long presses, and scrolling follow the app window on the active panel in closed, half-open, and open poses. +- `action-button` reports that the press was dispatched, not what the system did with it. Simulators run no Shortcuts and no App Intents, so you can verify what a press triggers only on a physical iPhone; on a Simulator the command proves the press was accepted and that the session app was not brought forward. +- `action-button` is slow to loop. On an iPhone 17 Pro Simulator the press itself spent about five seconds inside XCUITest, while `home` and `app-switcher` on the same session took under two seconds each. +- `fold ` puts a foldable iPhone simulator (iPhone Duo) into a hinge pose. The command sends a hinge event inside the selected simulator, then reads the hinge angle back with `devicectl device motion hinge-angle` and reports the pose only when that reading agrees: `closed` is 0°, `open` is 180°, and `half-open` is any angle between them (requested at 130°). An angle inside that interval only proves the category, so `half-open` is reported once two consecutive readings both fall inside it and agree within 0.5°. The response names the panel the device now lights and its native panel point size, marked `coordinateSpace: "native-panel"`; that size is the panel's own geometry, not the next snapshot's viewport (a 669x951 inner panel can host a 951x669 app window), so it cannot place a tap. Re-snapshot afterwards, and never carry refs or coordinates across a `fold`. After that snapshot, taps, long presses, and scrolling follow the app window on the active panel in closed, half-open, and open poses. - For timed motion, use `fold --keyframes '[{"atMs":0,"angle":0},{"atMs":1667,"angle":160},{"atMs":3333,"angle":100},{"atMs":5000,"angle":180}]'`. This runs the opening/reversal/reopening sequence over five seconds. Supply either a preset or keyframes, never both. Use 2–64 keyframes starting at 0ms with strictly increasing integer timestamps up to 60,000ms and finite angles in 0–180°. Linear interpolation runs at roughly 60Hz; equal consecutive angles hold the hinge. Motion duration excludes preparation and final-angle verification. Cancellation stops at the current angle; re-snapshot even after an interrupted trajectory. - `fold` is simulator-only and requires an Xcode toolchain with the iOS simulator SDK and foldable HID support (verified on Xcode 27.1). It runs a helper against the session UDID; the helper is built once per `Fold.m` source hash and Xcode toolchain, cached under `~/.agent-device/fold-helper`, and rebuilt only when the source or the toolchain changes. Device Hub and host Accessibility permission are not required. Build failures report `fold-helper-build-failed`; dispatch failures report `fold-hid-dispatch-failed`. There is no UI fallback. Single-panel simulators and physical devices are refused. -- A simulator scoped to a non-default set with `--ios-simulator-device-set` is refused before any hinge is touched with `UNSUPPORTED_OPERATION` and `details.reason: "unsupported-device-scope"`. The HID send accepts `--set`, but `devicectl device info displays` and `devicectl device motion hinge-angle` accept only `--device` and resolve a scoped simulator as not found, so the pose could not be read back (ADR 0025). Run `fold` against a simulator in the default set. -- `fold` costs one bounded hinge stream per read, and devicectl's smallest stream is five seconds: `closed` and `open` take about ten seconds, `half-open` about sixteen, because the hinge animates and the command waits for it to stop. A hinge whose last reading is some other pose fails with `COMMAND_FAILED` and `reason: fold-pose-unverified`, naming the angle CoreDevice still reports. A hinge seen `half-open` but never at rest fails with `reason: fold-pose-unsettled`, naming the observed and previous angles: the requested category was observed, and what is missing is a pose the hinge holds (#2730). -- `action-button` is not a cheap command to loop. On an iPhone 17 Pro Simulator the press itself spent about five seconds inside XCUITest, while `home` and `app-switcher` on the same session took under two seconds each. +- A simulator scoped to a non-default set with `--ios-simulator-device-set` is refused before any hinge is touched with `UNSUPPORTED_OPERATION` and `details.reason: "unsupported-device-scope"`, because `devicectl` cannot read the hinge pose of a simulator outside the default set. Run `fold` against a simulator in the default set. +- `fold` costs one bounded hinge stream per read, and devicectl's smallest stream is five seconds: `closed` and `open` take about ten seconds, `half-open` about sixteen, because the hinge animates and the command waits for it to stop. A hinge whose last reading is some other pose fails with `COMMAND_FAILED` and `reason: fold-pose-unverified`, naming the angle CoreDevice still reports. A hinge seen `half-open` but never at rest fails with `reason: fold-pose-unsettled`, naming the observed and previous angles: the requested category was observed, and what is missing is a pose the hinge holds. - On iOS devices, `http(s)://` URLs open in Safari when no app is active. Custom scheme URLs require an active app in the session. -- Commands that need one concrete device refuse to guess: if no `--device`/`--udid`/`--serial` is given and several candidates are equally preferred (for example two booted emulators), the command fails with `AMBIGUOUS_MATCH` and lists them, rather than picking one and returning a successful answer about a device you did not select. Preferences still apply first — virtual over physical, booted over offline — so one booted emulator beside offline ones resolves normally, as does any command running inside an existing session. `devices` lists everything as before. -- Commands that omit `--session` use an implicit `default` session scoped to the caller's current git worktree or working directory. This keeps independent local agents from accidentally attaching to each other's default session. -- `--session ` or `AGENT_DEVICE_SESSION` opt into an explicitly named session when a script intentionally wants to share or reuse that session name. -- A configured `AGENT_DEVICE_SESSION` implies bound-session lock mode by default. The CLI forwards that policy to the daemon, which enforces the same conflict handling for CLI, typed client, and direct RPC requests. -- `--session-lock reject|strip` and `AGENT_DEVICE_SESSION_LOCK=reject|strip` remain available for explicit named-session automation. `strip` resolves conflicts by dropping platform and scope selectors (`--platform`, `--target`, `--ios-simulator-device-set`, `--android-device-allowlist`) only. A selector that names a *different device* than the lock — `--udid`, `--serial`, `--device` — is never dropped: the request fails with `INVALID_ARGS` naming both the requested and the bound device, because continuing would run the command against a device the caller did not select. Recover by closing the bound session if the requested device is the one you want, or by removing the selector if the bound device is. -- Direct RPC callers can pass `meta.lockPolicy` and optional `meta.lockPlatform` on `agent_device.command` requests for the same daemon-enforced behavior. -- In `batch`, steps that omit `platform` still inherit the parent batch `--platform`; lock-mode defaults do not override that parent setting. +- Commands that need one concrete device refuse to guess: if no `--device`/`--udid`/`--serial` is given and several candidates are equally preferred (for example two booted emulators), the command fails with `AMBIGUOUS_MATCH` and lists them, rather than picking one and returning a successful answer about a device you did not select. Preferences still apply first — virtual over physical, booted over offline — so one booted emulator beside offline ones resolves normally, as does any command running inside an existing session. `devices` still lists every device. +- Commands that omit `--session` use an implicit session scoped to the caller's git worktree or working directory and to the selected platform (`ios`, `android`; the platform-less `default` session only without `--platform`), so independent local agents do not attach to each other's sessions. Use `--session ` or `AGENT_DEVICE_SESSION` to share a named session, and `--session-lock reject|strip` (or `AGENT_DEVICE_SESSION_LOCK`) to set its lock mode. See [Sessions](/docs/sessions#lock-a-named-session-to-a-device). - Tenant-scoped daemon runs can pass `--tenant`, `--session-isolation tenant`, `--run-id`, and `--lease-id` to enforce lease admission. - Remote daemon clients can pass `--daemon-base-url http(s)://host:port[/base-path]` to skip local daemon discovery/startup and call a remote HTTP daemon directly. - Use `--daemon-auth-token ` (or `AGENT_DEVICE_DAEMON_AUTH_TOKEN`) for explicit service/API-token automation against non-loopback remote daemon URLs; the client sends it in both the JSON-RPC request token and HTTP auth headers. - Use [Remote Proxy](/docs/remote-proxy) when you need to run `agent-device proxy` on a Mac with simulator/device access and drive it from another machine through cloudflared, ngrok, or another HTTP tunnel. - Use [BrowserStack](/docs/browserstack), [AWS Device Farm](/docs/aws-device-farm), or [TestMu AI](/docs/testmu) when a CI agent needs a hosted device session without interactive login. -- For human cloud access, `connect` can discover a cloud connection profile, while `connect --remote-config ...` uses a local profile. Both refresh a stored CLI session into a short-lived `adc_agent_...` token when needed. If no CLI session exists, interactive shells start login automatically; CI and non-interactive shells fail with API-token setup instructions. Use `--no-login` to disable implicit login. `AGENT_DEVICE_CLOUD_BASE_URL` is the bridge/control-plane API origin; its `/api-keys` route may redirect to the dashboard for token creation. -- For remote `connect` and `connect --remote-config` flows, see [Remote Metro workflow](#remote-metro-workflow). +- For human cloud access with `connect` or `connect --remote-config`, see [Remote Metro workflow](#remote-metro-workflow). - Android React Native relaunch flows require an installed package name for `open --relaunch`; install/reinstall the APK first, then relaunch by package. `open --relaunch` is rejected because runtime hints are written through the installed app sandbox. - For Metro-backed React Native JS changes, use `metro reload` before `open --relaunch`; it mirrors pressing `r` in the Metro terminal and keeps the native process alive. - Remote daemon screenshots and recordings are downloaded back to the caller path, so `screenshot page.png` and `record start session.mp4` remain usable when the daemon runs on another host. @@ -146,30 +157,20 @@ agent-device takeover status --session remote-session agent-device takeover release --session remote-session ``` -The command uses the device from the admitted remote lease, installs a short-lived hold, keeps it -alive in the foreground, and releases it on Ctrl+C. Activation waits for admitted mutations to finish -before reporting active. While held, state-changing commands fail with -`DEVICE_IN_USE` and `details.reason: "human_control_active"`, explaining that agent interactions are -temporarily disabled. Snapshots, screenshots, selector reads, logs, and other read-only diagnostics -remain available. The hold also -protects an existing remote device lease from inactivity expiry so the human does not accidentally -hand the simulator to a different agent. - -A foreground hold expires automatically if its process disappears. Releasing or expiring the final -hold refreshes the existing lease's inactivity window. Tenant commands can modify only holds owned -by their admitted lease, not provider-host administrative holds. - -Holds do not survive daemon restart; reconnect and re-establish them before continuing human -interaction. Local takeover without a device-scoped remote lease is not supported in this version. -See [remote takeover and host administration](./remote-proxy.md#human-takeover) for the VM-side API. +`takeover` holds the leased device in the foreground until you press Ctrl+C. While held, +state-changing commands fail with `DEVICE_IN_USE` and `details.reason: "human_control_active"`; +read-only diagnostics remain available. Takeover requires a device-scoped remote lease; local +sessions without one are not supported. Holds do not survive a daemon restart; reconnect and +re-establish them before continuing human interaction. See +[Remote Proxy](./remote-proxy.md#human-takeover) for hold lifetime, lease behavior, and the host API. ## Web Automation -Minimal `--platform web` support reuses [agent-browser](https://github.com/vercel-labs/agent-browser). `agent-device` owns command/session/replay integration, refs/selectors, and artifact routing; `agent-browser` owns browser launch, page control, screenshots, and browser-specific mechanics. +`--platform web` drives a browser through [agent-browser](https://github.com/vercel-labs/agent-browser). Web automation requires Node 24+. `agent-device` owns command/session/replay integration, refs/selectors, and artifact routing; `agent-browser` owns browser launch, page control, screenshots, and browser-specific mechanics. -Use `--platform web` when a browser step belongs inside an `agent-device` session, replay, batch, MCP, or typed-client flow. Use `agent-browser` directly for standalone web automation. +Use `--platform web` when a browser step belongs inside an `agent-device` session, replay, batch, MCP, or Node.js client flow. Use `agent-browser` directly for standalone web automation. -Set up and verify the managed web backend before relying on web sessions: +Set up and verify the managed web backend before you open a web session: ```bash agent-device web setup @@ -193,17 +194,15 @@ agent-device viewport 1280 900 --platform web agent-device close --platform web ``` -- Web automation uses a managed, pinned `agent-browser` backend as an implementation detail. +- `agent-device` installs and manages its own pinned `agent-browser` backend. - Run `web setup` before first use and in CI bootstrap steps. Normal `--platform web` commands do not install the backend implicitly. - Runtime web commands resolve the backend only from the managed install in the effective agent-device state dir. - `web setup` is idempotent and reuses the pinned backend when it is already installed. - `web doctor` verifies the managed backend after setup. - The managed install respects `--state-dir` and `AGENT_DEVICE_STATE_DIR`. -- Web automation requires Node 24+. - Supported through `agent-device`: URL open, snapshot refs, `get text/attrs`, `is visible/hidden/exists/absent/focused/text`, `find text/selector`, click/press, hover, fill/type, wait, `network dump`, `audio probe`, screenshot, close, and replay scripts composed from those commands. - `hover <@ref|selector|x y>` moves the pointer without pressing so hover-gated UI (row toolbars, menus) appears. Add `--settle` to read what it revealed instead of taking another snapshot. `hover @ref` hovers the browser's own element handle; like `click @ref --settle`, the `--settle` diff needs a selector or coordinate target on web because web refs carry no geometry. -- `audio probe start [durationSeconds] [bucketMs]` samples HTML media elements into compact RMS/peak dBFS buckets while the page keeps running. The first timing positional is seconds; the second is milliseconds. -- URL-backed web media may be routed through the probe `AudioContext` while observed. Use `audio probe status` to poll partial buckets and `audio probe stop` to end the probe early. +- `audio probe start [durationSeconds] [bucketMs]` samples HTML media elements into compact RMS/peak dBFS buckets; see [Debugging & Profiling](/docs/debugging-profiling#audio-probes). - Out of scope for `agent-device` web support: tab/window/devtools control, network routing/interception/HAR, cookies/storage, downloads/uploads, arbitrary page scripting, multi-page orchestration, and raw browser diagnostics. Use `agent-browser` directly for those browser-specific workflows. ## Device isolation scopes @@ -214,7 +213,7 @@ agent-device devices --platform android --android-device-allowlist emulator-5554 ``` - `--ios-simulator-device-set ` constrains simulator discovery and simulator command execution via `xcrun simctl --set ...`. -- The XCTest runner's `xcodebuild` phases resolve a scoped simulator in the same set through `-DVTSimulatorSetLocation=`; `~/Library/Developer/XCTestDevices` is never redirected. If the selected Xcode no longer resolves the simulator that way, the runner start fails with `details.reason: "simulator_set_destination_not_found"`, and the error names the set, and the selected Xcode version when this daemon has already read it. +- The Apple runner's `xcodebuild` phases resolve a scoped simulator in the same set through `-DVTSimulatorSetLocation=`; `~/Library/Developer/XCTestDevices` is never redirected. If the selected Xcode no longer resolves the simulator that way, the runner start fails with `details.reason: "simulator_set_destination_not_found"`, and the error names the set, and the selected Xcode version when this daemon has already read it. - On macOS, daemon startup puts back a `~/Library/Developer/XCTestDevices` that an older agent-device left redirected into a scoped set, before the daemon accepts requests: it removes any symlink at that path, restores `XCTestDevices.agent-device-backup` when it exists, and records each step in `daemon.log`. A symlinked `XCTestDevices` is not supported: daemon startup removes a symlink you made yourself too, for example one that moves `XCTestDevices` to another volume. Only the link is removed, never the directory it points to, and the `daemon.log` entry names that directory. A failed restore is recorded there too and does not stop the daemon or a runner start. - `--android-device-allowlist ` constrains Android discovery/selection to comma or space separated serials. - Scope is applied before selectors (`--device`, `--udid`, `--serial`), so out-of-scope selectors fail with `DEVICE_NOT_FOUND`. @@ -245,10 +244,10 @@ agent-device capabilities --session checkout --json ### HarmonyOS command boundary -HarmonyOS support uses HDC and ArkUI `uitest`. On current API 24 devices it supports lifecycle and HAP deployment, ArkUI snapshot/screenshot and selector reads, one-pointer touch and text actions, keyboard `enter`/`dismiss`, app logs, foreground app state, process RSS samples, and `settings clear-app-state`. Run `agent-device capabilities --platform harmonyos` for the authoritative command list for a selected device. +HarmonyOS support uses HDC and ArkUI `uitest`. On API 24 devices it supports lifecycle and HAP deployment, ArkUI snapshot/screenshot and selector reads, one-pointer touch and text actions, keyboard `enter`/`dismiss`, app logs, foreground app state, process RSS samples, and `settings clear-app-state`. Run `agent-device capabilities --platform harmonyos` for the authoritative command list for a selected device. - `gesture pan|fling|swipe` and `swipe` use HDC's single-pointer input primitives. Multi-touch gestures and target-authored `gesture drag` return `UNSUPPORTED_OPERATION` rather than approximating the interaction. -- Physical HarmonyOS devices support whole-screen recording through `record start --scope device` or `--scope system`. This uses the device ScreenRecorder service, not HDC `screenrecord`; emulator recording is explicitly rejected. HarmonyOS recording does not support `--fps`, `--quality`, or `--hide-touches`. +- Physical HarmonyOS devices support whole-screen recording through `record start --scope device` or `--scope system`. This uses the device ScreenRecorder service, not HDC `screenrecord`; emulator recording is rejected. HarmonyOS recording does not support `--fps`, `--quality`, or `--hide-touches`. - Orientation control, clipboard, alert automation, network/audio capture, push and app-event delivery, React Native helpers, and trace capture are not advertised for HarmonyOS. The public API 24 HDC surface has no usable `pasteboard`, notification, or `aa send` command on the supported emulator and physical device. - `settings` intentionally supports only `clear-app-state`; other system settings are not changed through undocumented parameter writes. `perf memory sample` provides process RSS; frame health and memory-snapshot artifacts remain unavailable. @@ -264,7 +263,7 @@ agent-device doctor --remote --json - `doctor` diagnoses device, app, Metro, and React Native setup/readiness issues for the selected target. - Use `--platform ios|android|vega|macos|linux|web|apple` to scope the checks to one backend; without it, `doctor` reports across the discoverable targets. - `--app ` focuses app-specific checks (install state, Metro/React Native wiring) on a single bundle id or app name. -- `--remote` runs the environment-only checks that do not require a booted device, which is what CI bootstrap and the packaged-CLI smoke use. +- `--remote` runs only the environment checks that do not need a booted device; use it in CI bootstrap. - `doctor` is read-only: it never boots, installs, or mutates the session device. ## Prepare Apple runner @@ -273,18 +272,18 @@ agent-device doctor --remote --json agent-device prepare ios-runner --platform ios --timeout 240000 ``` -- `prepare ios-runner` is intended for Apple-platform CI setup before `snapshot`, `replay`, or `test`. +- Use `prepare ios-runner` in Apple-platform CI setup before `snapshot`, `replay`, or `test`. - Run it after the simulator/device is booted and the app is installed, but before the first snapshot, replay, or test command. -- `--timeout ` is one budget shared by the Simulator boot (when the target is not booted yet) and the runner preparation; a never-booted Simulator's first-boot migration is bounded by it, not by the 120-second default boot wait. -- It builds or reuses the local XCTest runner, starts a runner session, and verifies that the runner can answer a lightweight health command. +- `--timeout ` is one budget shared by the Simulator boot and runner preparation; see [Navigation](#navigation). +- It builds or reuses the local Apple runner, starts a runner session, and verifies that the runner can answer a lightweight health command. - In JSON output, top-level `buildMs`, `connectMs`, and `healthCheckMs` are diagnostic fields and may overlap; use `timing.additiveParts` for additive wall-clock phase totals. `connectMs` contains `buildMs` when a runner artifact is built or rebuilt. -- If health checking exposes a bad restored runner artifact, Agent Device marks that artifact bad and rebuilds once. -- If a fresh runner launch gets stuck before accepting connections, Agent Device invalidates that runner session and launches it once more without forcing a rebuild. -- CI may cache `~/.agent-device/apple-runner/derived` when the cache key includes the exact Agent Device package contents and selected Xcode version. -- After a successful build, Agent Device removes the build scratch from the new cache key (intermediates, precompiled and module caches, logs) and keeps `Build/Products` and the metadata file, which is all reuse and launch read; one iOS simulator key measured 165.7 MB before the trim and 5.9 MB after. A key that fails certification is rebuilt from scratch either way, and a runner source or Xcode change mints a new key, so the scratch is never used again. Only the key this build gets with no path override is trimmed, compared by resolved real path; a `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` pointing anywhere else, including inside the managed runner cache, is never trimmed, because a fixed path rebuilds into the same tree incrementally. -- Agent Device versions from before the Apple runner rename kept their cache in `~/.agent-device/ios-runner`. Current versions never read it, and it is not removed automatically because an older version installed on the same machine may still use it. Delete it with `rm -rf ~/.agent-device/ios-runner` once no old version runs. +- If health checking exposes a bad restored runner artifact, agent-device marks that artifact bad and rebuilds once. +- If a fresh runner launch gets stuck before accepting connections, agent-device invalidates that runner session and launches it once more without forcing a rebuild. +- CI may cache `~/.agent-device/apple-runner/derived` when the cache key includes the exact agent-device package contents and selected Xcode version. +- After a successful build, agent-device removes the build scratch from the new cache key (intermediates, precompiled and module caches, logs) and keeps `Build/Products` and the metadata file, which is all reuse and launch read; one iOS simulator key measured 165.7 MB before the trim and 5.9 MB after. A key that fails certification is rebuilt from scratch either way, and a runner source or Xcode change mints a new key, so the scratch is never used again. Only the key this build gets with no path override is trimmed, compared by resolved real path; a `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` pointing anywhere else, including inside the managed runner cache, is never trimmed, because a fixed path rebuilds into the same tree incrementally. +- agent-device versions from before the Apple runner rename kept their cache in `~/.agent-device/ios-runner`. Current versions never read it, and it is not removed automatically because an older version installed on the same machine may still use it. Delete it with `rm -rf ~/.agent-device/ios-runner` once no old version runs. - Runner reuse is authorized only by the cache metadata's content manifest: a restored tree whose files no longer match the recorded digests, modes, or symlink targets is discarded and rebuilt. A cache key must stay exact — the runtime never falls back to a broader cache. -- Every runner source or Xcode change creates a new cache key under `~/.agent-device/apple-runner/derived//`, where the folder names the platform and device kind (`ios-simulator`, `ios-device`, `tvos-simulator`, `tvos-device`, `macos`, `visionos-simulator`, `visionos-device`). After a build, Agent Device deletes, on a best-effort basis and without delaying the start, keys beside the new one in the same folder that are neither among the 3 most recently used (the new key included) nor used in the last day, and never one a runner lease not proven dead points at or a build is holding. A key that cannot be deleted is left in place and reported as a `runner_xctestrun_cache_eviction_failed` warning diagnostic. `AGENT_DEVICE_IOS_RUNNER_CACHE_KEEP=` changes that count and `0` keeps every key. A set `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` is never swept. +- Every runner source or Xcode change creates a new cache key under `~/.agent-device/apple-runner/derived//`, where the folder names the platform and device kind (`ios-simulator`, `ios-device`, `tvos-simulator`, `tvos-device`, `macos`, `visionos-simulator`, `visionos-device`). After a build, agent-device deletes, on a best-effort basis and without delaying the start, keys beside the new one in the same folder that are neither among the 3 most recently used (the new key included) nor used in the last day, and never one a runner lease not proven dead points at or a build is holding. A key that cannot be deleted is left in place and reported as a `runner_xctestrun_cache_eviction_failed` warning diagnostic. `AGENT_DEVICE_IOS_RUNNER_CACHE_KEEP=` changes that count and `0` keeps every key. A set `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` is never swept. - Certification is fail-closed: when a product tree cannot be certified at all — a product escaping the derived-data root, an unreadable subtree, a file over 128 MB, or a non-regular entry such as a socket — the build fails with `runner_cache_uncertifiable` naming the path instead of launching uncertified bytes. Point `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` at a plain directory the current user owns; replacing the tree (the error's hint says how) clears a refusal. - Runner build/start output is written to the session's `runner.log`. The top-level `daemon.log` is reserved for daemon lifecycle/startup issues. @@ -311,15 +310,15 @@ vega virtual-device stop ``` - AndroidTV app launch and app listing resolve TV launchable activities via `LEANBACK_LAUNCHER`. -- TV target selection supports Apple TV and Android TV simulators/emulators and connected devices. Initial Vega OS support is limited to the Vega Virtual Device. +- TV target selection supports Apple TV and Android TV simulators/emulators and connected devices. Vega OS support is limited to the Vega Virtual Device (VVD). - TV targets are focus-first. Use `tv-remote` to move D-pad/remote focus before selecting a control; avoid raw `adb shell input keyevent` in command plans. - On Android TV, `tv-remote` maps to ADB keyevents. `tv-remote longpress