From 671e2f71a2773af6b6b0ce27b49ed15bfe7963f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 12 Feb 2026 15:02:36 +0100 Subject: [PATCH 1/5] Refine reinstall flow and add agent-focused coverage --- README.md | 2 ++ src/daemon/handlers/session.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index b83677228..63a91fbae 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ Sessions: - All interaction commands require an open session. - If a session is already open, `open ` switches the active app and updates the session app bundle. - `close` stops the session and releases device resources. Pass an app to close it explicitly, or omit to just close the session. +- `reinstall ` uninstalls and installs the app binary in one command (Android + iOS simulator in v1). +- `reinstall` accepts package/bundle id style app names and supports `~` in paths. - Use `--session ` to manage multiple sessions. - Session scripts are written to `~/.agent-device/sessions/-.ad` when recording is enabled with `--save-script`. - Deterministic replay is `.ad`-based; use `replay --update` (`-u`) to update selector drift and rewrite the replay file in place. diff --git a/src/daemon/handlers/session.ts b/src/daemon/handlers/session.ts index 098a8e99b..e5c7a3284 100644 --- a/src/daemon/handlers/session.ts +++ b/src/daemon/handlers/session.ts @@ -321,6 +321,10 @@ export async function handleSessionCommands(params: { return { ok: true, data: { session: sessionName, appName, appBundleId } }; } const device = await resolveTargetDevice(req.flags ?? {}); +<<<<<<< HEAD +======= + await ensureReady(device); +>>>>>>> 6519908 (Refine reinstall flow and add agent-focused coverage) const inUse = sessionStore.toArray().find((s) => s.device.id === device.id); if (inUse) { return { From 9b115d7438c046870962220bbe24fb62b41e63d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 12 Feb 2026 15:46:35 +0100 Subject: [PATCH 2/5] Avoid duplicate readiness preflight in open flow --- src/daemon/handlers/session.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/daemon/handlers/session.ts b/src/daemon/handlers/session.ts index e5c7a3284..098a8e99b 100644 --- a/src/daemon/handlers/session.ts +++ b/src/daemon/handlers/session.ts @@ -321,10 +321,6 @@ export async function handleSessionCommands(params: { return { ok: true, data: { session: sessionName, appName, appBundleId } }; } const device = await resolveTargetDevice(req.flags ?? {}); -<<<<<<< HEAD -======= - await ensureReady(device); ->>>>>>> 6519908 (Refine reinstall flow and add agent-focused coverage) const inUse = sessionStore.toArray().find((s) => s.device.id === device.id); if (inUse) { return { From 29955ab8481eeb9f6849b52879704a27dff02395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 12 Feb 2026 16:30:22 +0100 Subject: [PATCH 3/5] Document boot and reinstall in website docs and skill --- skills/agent-device/SKILL.md | 2 ++ skills/agent-device/references/session-management.md | 4 ++++ website/docs/docs/quick-start.md | 6 ++++++ website/docs/docs/sessions.md | 3 +++ 4 files changed, 15 insertions(+) diff --git a/skills/agent-device/SKILL.md b/skills/agent-device/SKILL.md index 2b12dd52d..159765f5b 100644 --- a/skills/agent-device/SKILL.md +++ b/skills/agent-device/SKILL.md @@ -169,8 +169,10 @@ agent-device apps --platform android --user-installed - On iOS, `xctest` is the default and does not require Accessibility permission. - If XCTest returns 0 nodes (foreground app changed), agent-device falls back to AX when available. - `open ` can be used within an existing session to switch apps and update the session bundle id. +- `reinstall ` supports Android devices/emulators and iOS simulators in v1. - If AX returns the Simulator window or empty tree, restart Simulator or use `--backend xctest`. - Use `--session ` for parallel sessions; avoid device contention. +- Use `boot --platform ios|android` as explicit preflight in CI before `open`. - Use `--activity ` on Android to launch a specific activity (e.g. TV apps with LEANBACK). - Use `fill` when you want clear-then-type semantics. - Use `type` when you want to append/enter text without clearing. diff --git a/skills/agent-device/references/session-management.md b/skills/agent-device/references/session-management.md index f40084eb9..07bc340e6 100644 --- a/skills/agent-device/references/session-management.md +++ b/skills/agent-device/references/session-management.md @@ -3,7 +3,9 @@ ## Named sessions ```bash +agent-device boot --platform ios agent-device --session auth open Settings --platform ios +agent-device --session auth reinstall com.example.app ./build/MyApp.app agent-device --session auth snapshot -i ``` @@ -14,6 +16,8 @@ Sessions isolate device context. A device can only be held by one session at a t - Name sessions semantically. - Close sessions when done. - Use separate sessions for parallel work. +- Use `boot --platform ios|android` as an explicit readiness check in CI. +- Use `reinstall ` for fresh app state instead of manual logout flows. - For deterministic replay scripts, prefer selector-based actions and assertions. - Use `replay -u` to update selector drift during maintenance. diff --git a/website/docs/docs/quick-start.md b/website/docs/docs/quick-start.md index 9700be9c1..402625c33 100644 --- a/website/docs/docs/quick-start.md +++ b/website/docs/docs/quick-start.md @@ -7,6 +7,9 @@ title: Quick Start Every device automation follows this pattern: ```bash +# 0. Optional CI/device preflight +agent-device boot --platform ios # or android + # 1. Navigate agent-device open SampleApp --platform ios # or android @@ -32,10 +35,13 @@ agent-device boot --platform ios # or android ## Common commands ```bash +agent-device boot --platform ios # Ensure simulator/device is ready agent-device open SampleApp agent-device snapshot -i # Get interactive elements with refs agent-device click @e2 # Click by ref agent-device fill @e3 "test@example.com" # Clear then type (Android verifies and retries once if needed) +agent-device reinstall com.example.app ./build/MyApp.app --platform ios # Fresh app state (iOS simulator) +agent-device reinstall com.example.app ./build/app.apk --platform android # Fresh app state (Android) agent-device get text @e1 # Get text content agent-device screenshot page.png # Save to specific path agent-device close diff --git a/website/docs/docs/sessions.md b/website/docs/docs/sessions.md index 609173403..8b5417bbb 100644 --- a/website/docs/docs/sessions.md +++ b/website/docs/docs/sessions.md @@ -7,6 +7,7 @@ title: Sessions Sessions keep device state and snapshots consistent across commands. ```bash +agent-device boot --platform ios agent-device open Settings --platform ios agent-device session list agent-device open Contacts # change app while reusing the default session @@ -25,5 +26,7 @@ Notes: - `open ` within an existing session switches the active app and updates the session bundle id. - Use `--session ` to run multiple sessions in parallel. +- Use `boot --platform ios|android` as an explicit readiness preflight in CI. +- Use `reinstall ` when you need a fresh app state (for example login flows) without manual logout. For replay scripts and deterministic E2E guidance, see [Replay & E2E (Experimental)](/docs/replay-e2e). From 2c0cc2aaa383cea6764d6d2c71e17c3bb3117e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 12 Feb 2026 16:58:05 +0100 Subject: [PATCH 4/5] Refine boot docs: troubleshooting-only and non-CI wording --- README.md | 2 -- skills/agent-device/SKILL.md | 2 -- skills/agent-device/references/session-management.md | 4 ---- website/docs/docs/quick-start.md | 6 ------ website/docs/docs/sessions.md | 3 --- 5 files changed, 17 deletions(-) diff --git a/README.md b/README.md index 63a91fbae..b83677228 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,6 @@ Sessions: - All interaction commands require an open session. - If a session is already open, `open ` switches the active app and updates the session app bundle. - `close` stops the session and releases device resources. Pass an app to close it explicitly, or omit to just close the session. -- `reinstall ` uninstalls and installs the app binary in one command (Android + iOS simulator in v1). -- `reinstall` accepts package/bundle id style app names and supports `~` in paths. - Use `--session ` to manage multiple sessions. - Session scripts are written to `~/.agent-device/sessions/-.ad` when recording is enabled with `--save-script`. - Deterministic replay is `.ad`-based; use `replay --update` (`-u`) to update selector drift and rewrite the replay file in place. diff --git a/skills/agent-device/SKILL.md b/skills/agent-device/SKILL.md index 159765f5b..2b12dd52d 100644 --- a/skills/agent-device/SKILL.md +++ b/skills/agent-device/SKILL.md @@ -169,10 +169,8 @@ agent-device apps --platform android --user-installed - On iOS, `xctest` is the default and does not require Accessibility permission. - If XCTest returns 0 nodes (foreground app changed), agent-device falls back to AX when available. - `open ` can be used within an existing session to switch apps and update the session bundle id. -- `reinstall ` supports Android devices/emulators and iOS simulators in v1. - If AX returns the Simulator window or empty tree, restart Simulator or use `--backend xctest`. - Use `--session ` for parallel sessions; avoid device contention. -- Use `boot --platform ios|android` as explicit preflight in CI before `open`. - Use `--activity ` on Android to launch a specific activity (e.g. TV apps with LEANBACK). - Use `fill` when you want clear-then-type semantics. - Use `type` when you want to append/enter text without clearing. diff --git a/skills/agent-device/references/session-management.md b/skills/agent-device/references/session-management.md index 07bc340e6..f40084eb9 100644 --- a/skills/agent-device/references/session-management.md +++ b/skills/agent-device/references/session-management.md @@ -3,9 +3,7 @@ ## Named sessions ```bash -agent-device boot --platform ios agent-device --session auth open Settings --platform ios -agent-device --session auth reinstall com.example.app ./build/MyApp.app agent-device --session auth snapshot -i ``` @@ -16,8 +14,6 @@ Sessions isolate device context. A device can only be held by one session at a t - Name sessions semantically. - Close sessions when done. - Use separate sessions for parallel work. -- Use `boot --platform ios|android` as an explicit readiness check in CI. -- Use `reinstall ` for fresh app state instead of manual logout flows. - For deterministic replay scripts, prefer selector-based actions and assertions. - Use `replay -u` to update selector drift during maintenance. diff --git a/website/docs/docs/quick-start.md b/website/docs/docs/quick-start.md index 402625c33..9700be9c1 100644 --- a/website/docs/docs/quick-start.md +++ b/website/docs/docs/quick-start.md @@ -7,9 +7,6 @@ title: Quick Start Every device automation follows this pattern: ```bash -# 0. Optional CI/device preflight -agent-device boot --platform ios # or android - # 1. Navigate agent-device open SampleApp --platform ios # or android @@ -35,13 +32,10 @@ agent-device boot --platform ios # or android ## Common commands ```bash -agent-device boot --platform ios # Ensure simulator/device is ready agent-device open SampleApp agent-device snapshot -i # Get interactive elements with refs agent-device click @e2 # Click by ref agent-device fill @e3 "test@example.com" # Clear then type (Android verifies and retries once if needed) -agent-device reinstall com.example.app ./build/MyApp.app --platform ios # Fresh app state (iOS simulator) -agent-device reinstall com.example.app ./build/app.apk --platform android # Fresh app state (Android) agent-device get text @e1 # Get text content agent-device screenshot page.png # Save to specific path agent-device close diff --git a/website/docs/docs/sessions.md b/website/docs/docs/sessions.md index 8b5417bbb..609173403 100644 --- a/website/docs/docs/sessions.md +++ b/website/docs/docs/sessions.md @@ -7,7 +7,6 @@ title: Sessions Sessions keep device state and snapshots consistent across commands. ```bash -agent-device boot --platform ios agent-device open Settings --platform ios agent-device session list agent-device open Contacts # change app while reusing the default session @@ -26,7 +25,5 @@ Notes: - `open ` within an existing session switches the active app and updates the session bundle id. - Use `--session ` to run multiple sessions in parallel. -- Use `boot --platform ios|android` as an explicit readiness preflight in CI. -- Use `reinstall ` when you need a fresh app state (for example login flows) without manual logout. For replay scripts and deterministic E2E guidance, see [Replay & E2E (Experimental)](/docs/replay-e2e). From 2aa1bd3e025e68ceffd36c5bcd8a9f83c676ce03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 12 Feb 2026 17:23:38 +0100 Subject: [PATCH 5/5] Fix duplicate find matching, add IOS_TOOL_MISSING reason, DRY retry log env check - Remove redundant findNodeByLocator call in find handler; reuse bestMatches from the ambiguity check instead of scanning nodes twice. - Add IOS_TOOL_MISSING boot failure reason so xcrun/simctl absence gets a specific diagnostic hint instead of generic BOOT_COMMAND_FAILED. - Extract isEnvTruthy helper into retry.ts and replace duplicated RETRY_LOGS_ENABLED parsing in ios/index.ts and android/devices.ts. Co-authored-by: Cursor --- src/daemon/handlers/find.ts | 6 +++--- src/platforms/__tests__/boot-diagnostics.test.ts | 10 +++++++++- src/platforms/android/devices.ts | 6 ++---- src/platforms/boot-diagnostics.ts | 5 ++++- src/platforms/ios/index.ts | 6 ++---- src/utils/retry.ts | 4 ++++ 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/daemon/handlers/find.ts b/src/daemon/handlers/find.ts index c55698bfa..b2f6461a8 100644 --- a/src/daemon/handlers/find.ts +++ b/src/daemon/handlers/find.ts @@ -1,5 +1,5 @@ import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts'; -import { findBestMatchesByLocator, findNodeByLocator, type FindLocator } from '../../utils/finders.ts'; +import { findBestMatchesByLocator, type FindLocator } from '../../utils/finders.ts'; import { attachRefs, centerOfRect, type RawSnapshotNode, type SnapshotState } from '../../utils/snapshot.ts'; import { AppError } from '../../utils/errors.ts'; import type { DaemonRequest, DaemonResponse } from '../types.ts'; @@ -97,7 +97,7 @@ export async function handleFindCommands(params: { const start = Date.now(); while (Date.now() - start < timeout) { const { nodes } = await fetchNodes(); - const match = findNodeByLocator(nodes, locator, query, { requireRect: false }); + const match = findBestMatchesByLocator(nodes, locator, query, { requireRect: false }).matches[0]; if (match) { if (session) { sessionStore.recordAction(session, { @@ -134,7 +134,7 @@ export async function handleFindCommands(params: { }, }; } - const node = findNodeByLocator(nodes, locator, query, { requireRect: requiresRect }); + const node = bestMatches.matches[0] ?? null; if (!node) { return { ok: false, error: { code: 'COMMAND_FAILED', message: 'find did not match any element' } }; } diff --git a/src/platforms/__tests__/boot-diagnostics.test.ts b/src/platforms/__tests__/boot-diagnostics.test.ts index 518668d55..97a931678 100644 --- a/src/platforms/__tests__/boot-diagnostics.test.ts +++ b/src/platforms/__tests__/boot-diagnostics.test.ts @@ -19,7 +19,7 @@ test('classifyBootFailure maps adb offline errors', () => { assert.equal(reason, 'ADB_TRANSPORT_UNAVAILABLE'); }); -test('classifyBootFailure maps tool missing from AppError code', () => { +test('classifyBootFailure maps tool missing from AppError code (android)', () => { const reason = classifyBootFailure({ error: new AppError('TOOL_MISSING', 'adb not found in PATH'), context: { platform: 'android', phase: 'transport' }, @@ -27,6 +27,14 @@ test('classifyBootFailure maps tool missing from AppError code', () => { assert.equal(reason, 'ADB_TRANSPORT_UNAVAILABLE'); }); +test('classifyBootFailure maps tool missing from AppError code (ios)', () => { + const reason = classifyBootFailure({ + error: new AppError('TOOL_MISSING', 'xcrun not found in PATH'), + context: { platform: 'ios', phase: 'boot' }, + }); + assert.equal(reason, 'IOS_TOOL_MISSING'); +}); + test('classifyBootFailure reads stderr from AppError details', () => { const reason = classifyBootFailure({ error: new AppError('COMMAND_FAILED', 'adb failed', { diff --git a/src/platforms/android/devices.ts b/src/platforms/android/devices.ts index 0e8bd1a9d..7f97e59a9 100644 --- a/src/platforms/android/devices.ts +++ b/src/platforms/android/devices.ts @@ -2,14 +2,12 @@ import { runCmd, whichCmd } from '../../utils/exec.ts'; import type { ExecResult } from '../../utils/exec.ts'; import { AppError, asAppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; -import { Deadline, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; +import { Deadline, isEnvTruthy, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts'; const EMULATOR_SERIAL_PREFIX = 'emulator-'; const ANDROID_BOOT_POLL_MS = 1000; -const RETRY_LOGS_ENABLED = ['1', 'true', 'yes', 'on'].includes( - (process.env.AGENT_DEVICE_RETRY_LOGS ?? '').toLowerCase(), -); +const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS); function adbArgs(serial: string, args: string[]): string[] { return ['-s', serial, ...args]; diff --git a/src/platforms/boot-diagnostics.ts b/src/platforms/boot-diagnostics.ts index c18140c5e..6fc116886 100644 --- a/src/platforms/boot-diagnostics.ts +++ b/src/platforms/boot-diagnostics.ts @@ -3,6 +3,7 @@ import { asAppError } from '../utils/errors.ts'; export type BootFailureReason = | 'IOS_BOOT_TIMEOUT' | 'IOS_RUNNER_CONNECT_TIMEOUT' + | 'IOS_TOOL_MISSING' | 'ANDROID_BOOT_TIMEOUT' | 'ADB_TRANSPORT_UNAVAILABLE' | 'CI_RESOURCE_STARVATION_SUSPECTED' @@ -25,7 +26,7 @@ export function classifyBootFailure(input: { const platform = input.context?.platform; const phase = input.context?.phase; if (appErr?.code === 'TOOL_MISSING') { - return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'BOOT_COMMAND_FAILED'; + return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'IOS_TOOL_MISSING'; } const details = (appErr?.details ?? {}) as Record; const detailMessage = typeof details.message === 'string' ? details.message : undefined; @@ -117,6 +118,8 @@ export function bootFailureHint(reason: BootFailureReason): string { return 'Check adb server/device transport (adb devices -l), restart adb, and ensure the target device is online and authorized.'; case 'CI_RESOURCE_STARVATION_SUSPECTED': return 'CI machine may be resource constrained; reduce parallel jobs or use a larger runner.'; + case 'IOS_TOOL_MISSING': + return 'Xcode command-line tools are missing or not in PATH; run xcode-select --install and verify xcrun works.'; case 'BOOT_COMMAND_FAILED': return 'Inspect command stderr/stdout for the failing boot phase and retry after environment validation.'; default: diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index a43c9c8f4..9dbf8fd13 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -2,7 +2,7 @@ import { runCmd } from '../../utils/exec.ts'; import type { ExecResult } from '../../utils/exec.ts'; import { AppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; -import { Deadline, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; +import { Deadline, isEnvTruthy, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts'; const ALIASES: Record = { @@ -14,9 +14,7 @@ const IOS_BOOT_TIMEOUT_MS = resolveTimeoutMs( TIMEOUT_PROFILES.ios_boot.totalMs, 5_000, ); -const RETRY_LOGS_ENABLED = ['1', 'true', 'yes', 'on'].includes( - (process.env.AGENT_DEVICE_RETRY_LOGS ?? '').toLowerCase(), -); +const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS); export async function resolveIosApp(device: DeviceInfo, app: string): Promise { const trimmed = app.trim(); diff --git a/src/utils/retry.ts b/src/utils/retry.ts index fb8262485..d07b61cc1 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -39,6 +39,10 @@ export type RetryTelemetryEvent = { reason?: string; }; +export function isEnvTruthy(value: string | undefined): boolean { + return ['1', 'true', 'yes', 'on'].includes((value ?? '').toLowerCase()); +} + export const TIMEOUT_PROFILES: Record = { ios_boot: { startupMs: 120_000, operationMs: 20_000, totalMs: 120_000 }, ios_runner_connect: { startupMs: 120_000, operationMs: 15_000, totalMs: 120_000 },