* perf(ios): derive runner demand for Simulator opens and stop observation from awaiting the runner Local Simulator opens now decide how much the XCTest runner is needed from the runtime operations declared by the steps still ahead in the same batch: an observation-only plan starts no runner, an unknown plan keeps the speculative prewarm without ever awaiting it, and a plan with an interaction prepares readiness for that step. open --relaunch no longer waits for runner readiness on a Simulator and resets the runner target only when a session is already alive. The Apple find ports report not-proven instead of starting a runner on a Simulator without a live session, so wait and read-only find observe through the canonical AX-bridge tree. Physical devices keep their lifecycle unchanged. The plan travels through the server-private internal request channel, never the wire; the Apple owner maps declared operations to a runner demand through a record complete over the runtime operation union. Refs #2198 * test(fixtures): share one inert audio-probe host across the platform runtime fixtures The Apple and Android runtime fixtures carried identical audio-probe doubles; host-kit now owns the one copy and both fixtures import it. Also folds the two Apple native-find ports onto one admission helper and lifts the Simulator runner prewarm policy out of the open sequence, keeping both under the complexity gate. * fix(ios): answer runner liveness through the runner provider seam The find ports and the relaunch target reset asked the local session registry whether a runner was alive, which misreads scripted and request-scoped runner providers as absent. Liveness is now a provider question: the local provider consults its session registry, a provider without startup cost counts as live, and an awaited prewarm proves liveness without asking. * perf(ios): select plan uses from step input and give young Simulator targets a bounded bridge grace A snapshot, diff, or find step now selects the runtime uses its structured input reaches, the way its handler does, so a plain snapshot no longer counts the custom-actions alternative and an observation-only batch resolves runner demand none. The descriptor declares the selector next to its alternatives; the daemon plan derivation honors it and keeps the union for every other command. Without the runner wait, the first snapshot after an open reached the AX bridge while the app was still becoming the primary foreground owner or registering its accessibility server, and the typed fallback then started the runner the plan had just avoided. A target younger than ten seconds is re-read for a bounded grace measured from the first such failure: five seconds for a missing AX server, one second for an ownership miss so a launch-time system dialog still reaches the fallback quickly. Established targets get no grace. * fix(ios): a registered runner session counts as live only once it has answered A session record exists while xcodebuild is still connecting, so an alive child pid is not a runner that can answer. Treating it as live sent the relaunch target reset into a starting runner, queued behind its connection retries, and the failed reset invalidated the very session the prewarm was building. Liveness now also requires the session's readiness flag, which the first successful runner response sets. * refactor(ios): lift the bridge launch grace out of the snapshot route capture * test(descriptors): pin the snapshot, diff, and find step-use selectors * test: stub runner operations in the replay test-runner suite and keep runner-session tests within the size ratchet A Simulator open schedules a best-effort runner prewarm that outlives its request. The replay test-runner suite opened a Simulator with the real Apple tools, so the prewarm's deferred import resolved after the file finished and spawned into whichever file the worker ran next, where the hermetic signal guard failed an unrelated test. * fix(plan): count only required operations and read find and snapshot steps the way their handlers do Runner demand now counts a command's required operations only: a preferred or conditional operation is a measured fast path the command succeeds without, so get, wait, and read-only find stay observation-only. The step selectors for snapshot, diff, and find live next to the registry and read the daemon step exactly as the handlers do: the daemon flag for custom actions, and find's positionals through the same parser, where a missing action is a click and an unparseable step keeps every declared alternative. The handler and the selector share one action-to-intent map. The batch runner hands each step its remaining steps in handler shape, and the derived operations reach the platform as a typed list on the lifecycle execution instead of an untyped plan on every open. * perf(ios): let open wait for the launched app to become observable, and make runner liveness explicit The snapshot route no longer infers a launch from process start text and retries inside its own capture. Open owns launch timing instead: a local Simulator open asks the AX bridge whether the launched app is observable, bounded by per-code windows measured from the first typed launch-transition failure and never extended, so an ownership miss seen after an AX-server miss shrinks the deadline to the ownership window and a launch-time system dialog still reaches the typed fallback quickly. Any other device, or a bridge that cannot answer, keeps the fixed settle. The open response reports what it learned. Every runner provider now states whether it can answer without a startup wait; a bare executor answers directly by construction and scripted providers say so. The runner prewarm policy and the observation settle move out of the open sequence into their own module, and the native find admission is named for what it admits. * docs(context): keep the runner-demand vocabulary within the guidance budget The enumeration and the no-public-flag rule live on the contract type that owns them; CONTEXT.md keeps the term itself, and two neighbouring entries lose words that carried no meaning. * refactor(contracts): name the runtime operation vocabulary below the operations union The lifecycle execution carries the operations a plan requires, but typing that list with the operations union closed a 36-file type cycle: the operations types depend on the lifecycle types. The vocabulary now lives as a const list below both, proven equal to the union by a type test, so the plan is typed end to end, the Apple host table indexes it without casts, and the daemon narrows descriptor names through a guard instead of a cast. * fix(apple): reach runner liveness through the memoized operations loader Every Apple tool port loads the runner operations through the one memoized loader (#2314): a port that opens its own dynamic import can resolve the unmocked module while a test's mock factory is still loading and let a real local runner escape. The liveness port now uses the loader like its siblings; the facade members consumed only through the loader are declared to fallow, and the plan resolver reads one step per helper to stay under the complexity threshold. * fix(ios): keep bridge-only behavior to iOS Simulators The launch observation, the runner-free find admission, and the relaunch policy apply only where the host AX bridge exists: iOS Simulators. A tvOS Simulator keeps its awaited prewarm and asks for no observation, which the tvOS provider scenario now pins. * bench(ios): add a first-interaction cell to the snapshot convergence harness An open that defers runner readiness moves its cost to the first runner-dependent command. The cell starts each sample like cold, opens the fixture untimed, then times the first press that follows (the deep-link confirmation when the launch URL raises it, otherwise the screen anchor). * bench(ios): read the deep-link confirmation from a snapshot and by node type The open response carries no tree and regular snapshots publish the node type, so the confirmation iOS raises for a launch URL was never seen on this runtime and every deep-linked cell failed its anchor check. * refactor(plan): keep the step-use selectors inside the registry The eager-closure ratchet counts every module the registry loads; the selectors need nothing the registry does not already import, so they live beside find's recording-effect reader instead of adding a module to every entry that loads the registry. * feat(apple): release a speculative runner when the plan is proven observation-only #2198 requires a `none` runner demand to retain no runner, not only to start none. A runner a prewarm started that no command has used yet is speculative: the session records that mark at creation, the first command that is not a readiness probe clears it, and a Simulator open whose plan is proven observation-only asks the runner owner to release a speculative session in the background, so the observation path never waits for a runner to stop either. A runner that has served a command is the session's working runner and stays under the existing idle-stop policy, so a mixed workload does not pay a cold runner start at every observation-only open. The release goes through the runner provider seam: the local provider stops its own speculative session; a provider that never starts speculative work omits the operation and releases nothing. * bench(ios): press an unambiguous target on the catalog and Settings screens The first-interaction cell pressed the screen's anchor text, which on the catalog and iOS Settings screens names two actionable elements (the native tab and the screen title); the CLI refuses that as AMBIGUOUS_MATCH by design, so those two cells could never measure anything. Each such screen now names the element the cell presses. * fix(ios): keep observation on the bridge while app discovery is pending and no runner is live #2331 bounds one capture's wait for the Simulator app discovery and takes the XCTest fallback past it; #2198 stops a Simulator open from awaiting the runner. Together, a `wait` right after a relaunch on a loaded host fell back to XCTest while the runner was still starting, spent its poll budget on that start, and timed out (the iOS smoke lane after the main merge). A capture with no live runner now stays on the single-flight discovery, one wait slice at a time, until the discovery's own deadline or the request signal ends it; a runner that is already live still takes the fallback at once, the cheaper route #2331 chose. * fix(apple): queue a speculative-runner release behind a start that is still in flight A `possible` open's prewarm registers its session only when the start completes, so a `none` open that released in that window found nothing and the runner it meant to release survived as a retained speculative session. The release now takes the runner session lock: it queues behind the in-flight start, sees the registered speculative session, and stops it; a start a command asked for is left alone. Two deferred-start regressions pin both outcomes.
12 KiB
Agent Device Domain Language
Canonical vocabulary for the automation domain. Use these names in code, tests, issues, and architecture notes; implementation decisions and procedures belong in ADRs and task guidance.
Language
Sessions, targets, and devices
Platform family: An internal ownership group for related automation platforms: Apple, Android, HarmonyOS, Vega, Linux, web.
Platform leaf: A concrete OS and device shape within a platform family, classified independently: iOS simulator, physical iOS, tvOS, or macOS.
Platform module: A private package owning one platform family's device mechanics, metadata, and runtime bindings.
Device inventory gateway: The platform-neutral composition of local-family and provider inventory sources.
Device runtime gateway: The platform-neutral boundary that reports runtime facts and binds an admitted device to its runtime owner.
Runtime owner: The one local platform module, managed local owner, or provider runtime selected to execute behavior for an ownership-qualified device.
Managed local owner: The exact-only runtime owner for an allocator-managed local device; it delegates automation to the device's platform module while lifecycle stays with the allocator. Avoid: Managed provider, provider runtime
Request binding: A request-lived attachment of cancellation, diagnostics, progress, and admitted context to a runtime owner.
Bound device runtime: The view returned once a request binding proves the required runtime operations.
Runtime facet: A capability-cohesive interface on a bound device runtime with semantic inputs and typed outcomes.
Runtime fact: A typed claim about behavior available for one exact platform leaf, device or backend, and provider mode.
Narrowed bound runtime: A projection exposing required facets, optional preferred facets, and no undeclared facets.
Host capability: Narrow authority given to a platform module for host execution, diagnostics, progress, or native assets.
Target: The selected automation destination, such as mobile, TV, or desktop.
Session: Daemon-owned state for one selected target and its opened app or surface.
Device key: A stable provider-scoped identity for device ownership and contention.
Device lease: Logical remote ownership of a selected device for a tenant, run, or client.
Lease provider: The remote connection source that routes and owns a device lease.
Runner lease: A mutual-exclusion guard for a platform helper process. It is not remote client ownership. Avoid: Device lease, process lease
Device claim: Host-global exclusive ownership of one local device by an open session, a sessionless mutating command, or an allocator-held claim for a managed identity.
Allocator-held claim: A device claim whose principal is an installation and an allocator identity incarnation rather than a process; sessions and commands execute under it, and only the allocator's removal proof clears it. Avoid: Stale claim, session claim, synthetic session
Device-claim policy: A command's observation, ownership, or exclusive-mutation rule.
Device-claim rule: The per-owner-kind decision at the claim gate: ordinary, allocator-held, or none. Avoid: Claim policy, device-claim policy
Managed binding fence: The ownership fence of one managed binding: requester and identity incarnation as its token, request generation as its generation.
Request generation: The per-requester monotonic number of one allocation attempt on a lane; never shared across requesters.
Identity incarnation: The allocator-issued id of one creation of a managed identity, stable for its pool lifetime and preserved across Android clean-baseline reuse; a fresh iOS identity is a new device with a new incarnation, and a different incarnation on a claimed device is a conflict. Avoid: Request generation
Human-control hold: A device-scoped pause on agent mutations during human operation.
Commands and routing
Command surface: The catalog of public command identity, exposure, adapter policy, and metadata across entrypoints.
Runtime use: A command's platform-neutral declaration of required operations and preferred fast paths.
Inventory use: An inventory command's platform-neutral declaration for composing device sources unbound.
Daemon command registry: The daemon-side truth for route ownership and request-policy traits.
Runner command traits: Per-command classes steering Apple runner lifecycle and recovery, independent of the public surface.
Runner demand: What a Simulator open prepares, or releases unused, of the XCTest runner for remaining steps.
Daemon RPC protocol version: The integer that detects breaking compatibility across the remote daemon boundary.
Version-skew invariant: Local client and daemon versions must match; only remote daemons, separately versioned helpers, persisted artifacts, and released API consumers get compatibility handling.
Interactions, selectors, and refs
Interactor: The legacy monolithic interface between dispatch and platform behavior, kept only for unmigrated commands. Avoid: New or migrated command behavior
Interaction dispatch path: One route an interaction command takes from a resolved target to device execution.
Coordinate-first resolved element activation: An Apple interaction that resolves a semantic element and activates its resolved center point without a second lookup.
Parent-owned touch point: A point that keeps the selected parent's identity while avoiding independently interactive descendants at its center.
Guarantee cell: One dispatch-path-by-guarantee classification: enforced, delegated, inapplicable, or waived.
Owned waiver: A guarantee gap with a tracking issue and explicit owner.
Delegation-on-error: A fast path that returns semantic failures to the shared path; it establishes failure-side handling, not success-path parity.
Parity table: A golden rule table consumed by both TypeScript and native tests.
Coverage manifest: A contract test's declaration of the guarantee cells it proves.
Ref frame:
The session's authorization namespace for mutating @ref targets: a frozen observation epoch and
issuance scope.
Frame expiry seam: The point just before a mutating device operation where the active ref frame becomes invalid.
Mutation admission: The decision that an active ref frame's epoch and issuance scope authorize a ref mutation.
Ref generation pin:
An optional ~s<n> suffix carrying the snapshot generation an @ref was minted from.
Deferred interaction outcome: Post-response state recording whether a mutation still needs outcome retry, stabilization, or snapshot freshness recovery.
Settled observation: An optional post-action observation that waits for a quiet UI and diffs against the pre-action tree.
Resolution disclosure: Bounded response evidence describing how an interaction target resolved, issuing no new actionable refs.
Gestures and touch
Gesture plan: Typed, platform-neutral normalization of one- or two-contact gesture intent into bounded pointer trajectories.
Android planned-touch executor: The Android boundary selecting a provider-native or instrumentation-backed executor for a normalized touch plan.
Multi-touch geometry: The centroid, span, angle, translation, scale, and rotation that construct two-contact motion.
Snapshots and capture
Raw AX node: A backend-owned accessibility value before snapshot presentation.
Snapshot acquisition: One backend attempt's raw AX nodes and attempt-level capture facts.
Snapshot producer: The acquisition component that produced a snapshot's raw tree; presentation, scope, and geometry key on the producer, never on the platform channel alone.
Presentation options: The policy input turning one snapshot acquisition into a public projection.
Snapshot policy facet: The host-side owner of neutral snapshot policy (presentation, freshness, timeout, overlay); platform acquisition supplies raw facts and a fold policy, and runner-side Swift presentation stays separate across the process boundary.
Capture hint: The acquisition-facing view of a snapshot request: the projection a backend must serve, raw traversal depth kept apart from regular presented depth, and narrowing only where the backend can prove it complete.
Regular presented-depth frontier: The acquisition boundary for an unscoped regular snapshot, measured against regular presented depth after structural wrappers collapse.
Snapshot eligibility: Membership in a presented snapshot projection, independent of current hittability.
Clip fold: The regular projection's single visibility interpreter, run inside presentation for every backend: viewport and scroll-container clipping, ancestor projection, scroll hints, collapsed depth. Platform differences enter as a fold policy, never as a backend exception.
Presented node: A wire-facing snapshot value produced at the presentation boundary.
Snapshot capture plan: An ordered set of capture backends under one shared wall-clock budget.
Snapshot quality verdict: A structured statement of capture state, backend, degradation reason, effective depth, and collapsed content.
Snapshot projection: A view of one acquired tree. Interactive is a subset of regular, and regular is a subset of raw.
Declared capture residue: A fidelity limit in acquired evidence that presentation cannot repair and must disclose.
AX-unavailable target invalidation: The Apple behavior that discards a suspect cached application target after a root AX failure.
Recording and replay
Script recording:
Session mode that captures portable actions and target evidence into a .ad script.
Avoid: Screen recording
Recorded input parameterization:
An explicit fill contract that sends literal text to the live app while storing a caller-chosen
${VAR} placeholder durably.
Open-to-destination script:
A self-contained .ad script that opens an app, reaches and verifies a destination, and leaves the
session active.
Destination guard: A selector-targeted wait near the end of an open-to-destination script verifying its ready state.
Replay script source bundle: The complete caller-resolved set of script paths and contents for one replay or test run.
Screen-recording facet: A runtime facet that starts video capture and returns a live handle and durable descriptor.
Live resource handle: Process-local authority to finish or forcibly dispose active logging, recording, or profiling.
Durable resource descriptor: Bounded, versioned identity and recovery state from which the same runtime owner can reattach.
Reattachment: A fenced recovery attempt by the descriptor's exact runtime owner returning a live handle, completed result, missing state, or typed refusal.
Maestro compatibility
Maestro program: A source-preserving typed representation of the supported Maestro Flow syntax and behavior.
Maestro observation generation: Compatibility-engine evidence since the most recent mutation; mutation invalidates it.
Providers and tests
Provider: An external adapter that owns a device runtime or contributes transport to a platform module.
Managed device allocator port: The daemon-owned interface to a managed-device allocator: obtain, hold, and give back a managed device. Avoid: Simlock client, lease provider
Cloud WebDriver runtime: A provider runtime mapping a cloud-owned Appium or WebDriver session into agent-device inventory, leases, runtime behavior, artifacts, and release.
Cloud artifact: Provider-hosted session output: video, automation logs, device logs, or dashboard links.
Daemon artifact type: An optional semantic category from the owner of a daemon-managed downloadable artifact.