Commit Graph

169 Commits

Author SHA1 Message Date
Michał Pierzchała 5162f7f662 feat: add Roku WebDriver provider 2026-07-08 18:30:00 +02:00
Michał Pierzchała b91eaad885 refactor: make iOS synthesized gesture policy explicit (#1152)
* refactor: make iOS synthesized gesture policy explicit

* test: harden settle observation under coverage

* fix: preserve first-command synthesized drag behavior

* refactor: simplify synthesized frame policy

* refactor: inline synthesized command policies

* refactor: simplify sequence synthesized context

* refactor: clarify synthesized drag fallback policy

* refactor: keep synthesized gesture policy runner-local
2026-07-08 17:15:42 +02:00
Michał Pierzchała f18d0b2e92 fix: improve settle observation guidance (#1154) 2026-07-08 17:12:19 +02:00
Michał Pierzchała 7f61df30ae feat: add TV remote command (#1147)
* feat: add TV remote command

* feat: improve TV remote ergonomics

* test: cover tv-remote provider scenario

* fix: preserve focused Android TV nodes

* docs: tighten PR description guidance

* fix: remove d-pad command alias

* docs: clarify tv-remote hold syntax

* feat: add tv-remote longpress CLI sugar
2026-07-08 10:59:48 +02:00
Michał Pierzchała 9dfebbe3be fix: omit interaction uptime from wire responses (#1142)
* fix: omit interaction uptime from wire responses

* fix: simplify interaction wire sanitizer
2026-07-07 16:53:44 +02:00
Michał Pierzchała 8f28c31c86 fix: recover completed Android recording from pending-only manifest (#1141)
* fix: recover completed Android recording from a pending-only manifest

When the daemon crashes in the brief window between writing the pending recovery
manifest (before screenrecord starts) and upgrading it to a `current` manifest, the
screenrecord process can still finish and leave a complete MP4 on the device. record
stop previously discarded it as stale because the pending-only recovery path never
checked for an on-device file, unlike the `current` path which already recovers a
finished recording. Extend the pending-only path to recover the completed file with the
same finished-recording warning, and skip the stop signal when the recovered recording
has no tracked pid (a pending chunk never records one, and probing an empty pid is
unsafe).

* fix: treat JSON arrays as invalid Android recovery manifests

isRecord accepted arrays (typeof [] === 'object'), so a stray `[]` recovery manifest
was classified as blocked rather than deleted, wedging every subsequent record stop.
Reject arrays and null so a non-object manifest is cleaned up like other malformed
metadata.
2026-07-07 12:04:58 +02:00
Michał Pierzchała 0dcc1aa553 fix: normalize interaction response wire shapes (#1114)
* fix: normalize interaction response wire shapes

* fix: reduce interaction response complexity
2026-07-07 11:29:01 +02:00
Michał Pierzchała 7e583c4136 fix: simplify Android recording recovery (#1135)
* fix: harden android recording recovery

* fix: reduce android recording recovery fallow complexity

* test: fix android recording recovery rebase

* fix: block uncertain android recording fallback

* fix: address android recording recovery review

* fix: address android recording recovery review followup

* fix: simplify Android recording recovery

* fix: address android recovery ownership review

* refactor: reuse android recovery manifest helpers

* refactor: split android pending recovery resolution

* fix: clarify scoped android recovery hint
2026-07-07 10:50:57 +02:00
Michał Pierzchała 5c5fa012f7 feat: --settle returns the settled diff in the interaction response (#1101) (#1106)
* feat: --settle returns the settled diff in the interaction response (#1101)

press/click/fill/longpress --settle executes the action, waits for the UI
to go quiet (wait stable's loop, shared via stable-capture.ts), and returns
the settled diff vs the pre-action tree in the same response — one round
trip instead of the interact -> observe pair.

- payload: changed lines only (bounded), summary counts, added-line refs,
  refsGeneration; best-effort (settled:false + hint on never-quiet content,
  never an action failure); --verify shares the settle captures
- ref issuance: the settled tree becomes the session snapshot; a
  diff-carrying settle response clears snapshotRefsStale and the MCP layer
  merge-only re-pins added-line refs at the settle generation
- grammar: --settle + --settle-quiet <ms> + --timeout <ms> (flag-sourced
  descriptor budget with new envelope:'widen' semantics mirroring wait)
- ADR 0011: new settleObservation guarantee classified on every path with
  contract scenarios per enforced/delegated cell

* test: give the two contention-flaky doctor scenarios explicit budgets

The doctor provider scenarios sit at ~5s of real daemon-harness work on a
loaded host and flake at vitest's 5s default during full-suite runs (the
known contention flake AGENTS.md documents). Same in-file precedent as the
Metro-probe scenario's 10s budget.

* fix: move SettleParams to contracts to satisfy the layering DAG

daemon/handlers/interaction-flags.ts imported the type across the
daemon -> commands boundary (R2 commands-floor). The tuning params are
part of the interaction contract like SettleObservation, so they live
in contracts/interaction.ts and both layers import from there.

* feat: keep settle diffs content-first — drop Key nodes, added lines win the cap

Bluesky dogfood: a fill that summons the iOS keyboard spent 49 of the 80
capped diff lines spelling out QWERTY keys, and a screen transition with
269 removals could starve out the added lines entirely. Key-type nodes
are now filtered from both diff sides (the [keyboard] container line
still signals presence), and under truncation added lines — the ones
carrying fresh refs — win slots over removals.

* docs: state the core loop in the top-level help starting point

Benchmarked with headless haiku/sonnet agents given only --help: both
models skipped the help-workflow pointer and started with plain
snapshot (38KB payloads they then had to re-read from files). One
core-loop line at the starting point is what teaches snapshot -i and
--settle to models that never read a second help page.

* fix: preserve settle digest refs for mcp

* fix: reduce settle fallow complexity

* fix: surface settle output in CLI text

* fix: complete settle handling for longpress

* refactor: localize daemon timeout envelopes

* refactor: deepen post-action observation

* refactor: centralize post-action observation planning

* refactor: derive settle capability from descriptors

* refactor: trim settle descriptor helpers
2026-07-06 20:18:44 +02:00
Michał Pierzchała db69124c00 feat: add capabilities command (#1133) 2026-07-06 19:35:46 +02:00
Michał Pierzchała 54f6d45b32 refactor: extract host process primitives (#1134) 2026-07-06 19:01:32 +02:00
Michał Pierzchała be4bd092b6 fix: recover Android recordings after daemon restart (#1129)
* fix: recover android recordings after daemon restart

* refactor: reduce recording recovery complexity

* fix: address android recording recovery feedback
2026-07-06 15:28:27 +02:00
Michał Pierzchała 5a362ec752 build: drop tsc entirely and fix sandbox-hostile tests (#1100)
Follow-ups from the bundler/CI speed work, re-validated against latest
main. The typescript package is gone from the toolchain:

- pnpm typecheck stays on tsgo; the typecheck:tsc escape hatch is
  removed along with the typescript devDependency.
- args.test.ts extracted cli.ts dispatch literals through the
  TypeScript compiler API - the only remaining consumer. It now walks
  the same AST via oxc-parser (matching the OXC lint/format/build
  stack); both implementations extract an identical 14-literal set
  from cli.ts, verified side by side before the swap. The
  substitution-free template case ts.isStringLiteralLike covered is
  preserved.
- dts bundling is unaffected: the tsdown build uses the tsgo backend
  and builds green with no typescript package installed.

Test fixes for containerized agent environments:

- The missing-binary doctor-guidance web provider test pins Node 24
  via the file's existing withNodeRuntimeVersion helper, so it asserts
  the setup hint instead of inheriting the host Node and failing on
  Node 22 (the supported engines floor).
- The clean-xcuitest cleanup-failure smoke test skips as root: chmod
  0o500 cannot force a removal failure when the process bypasses
  directory permissions.

AGENTS.md toolchain notes updated to match.


Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-04 21:02:36 +02:00
Michał Pierzchała 86736a5f87 feat: versioned snapshot refs with MCP auto-pinning (#1096)
* feat: versioned snapshot refs with MCP auto-pinning

Refs are positional indexes into the latest stored session tree; #1093's
coarse snapshotRefsStale marker warns honestly but cannot say WHICH tree
a ref came from. Give the session a monotonically increasing
snapshotGeneration, advanced wherever the stored tree is replaced: the
setSessionSnapshot choke point and the snapshot/diff command path that
bypasses it.

Token economy (non-negotiable): the snapshot tree output is unchanged —
plain e12 refs on every node. Ref-issuing responses (snapshot command,
find ref outputs) carry the generation ONCE as the additive
refsGeneration field. Ref-consuming commands (press/click/fill/longpress/
get/wait) accept both forms: plain @e12 keeps today's behavior including
the coarse #1093 warning; pinned @e12~s3 is clean when the generation
matches the stored tree, gets a precise warning naming both generations
when it does not, and a malformed suffix is INVALID_ARGS with a grammar
hint. Warn-only this release — tightening comes later per the compat
ladder.

The MCP layer auto-pins at zero token cost: it sees snapshot/find
responses before the model does, remembers the last refsGeneration per
session name, and rewrites plain @ref tool arguments to the pinned form
before forwarding. The model never sees or types suffixes; with no
remembered generation, refs pass through unpinned (never guess).

Replay parsing and script writing strip and IGNORE pins — generations
are meaningless outside the session that minted them.

Refs #1076

* docs: CONTEXT.md vocabulary for ref generation pins

Moved from #1097 per the review sequencing note: the term lands with
the behavior it describes.

* docs: teach the ref pin syntax in CLI help

MCP agents get pins transparently (auto-pinning), but CLI-driving
agents only ever met the coarse warning — refsGeneration arrived in
snapshot responses with nothing explaining it, making pins an
undiscoverable feature on the primary agent surface. One help line in
the agent loop guidance closes that; warnings stay short (they fire
repeatedly, teaching belongs in once-read surfaces).

* fix: per-ref MCP pin provenance and seeded generations

Review findings on the first cut:

1. The MCP layer kept ONE refsGeneration per session, so after
   snapshot(s12) -> find(s13) a plain @e37 from the pre-find snapshot got
   pinned ~s13 and read as current — recreating the find-blessing hole at
   the pinning layer. Replace it with per-ref provenance:
   Map<pinScope, Map<refBody, generation>>, scoped by state dir + session
   name (stateDir is a per-call MCP config field, so one server process
   can face multiple daemons). Merge-only updates: refs present in a
   ref-issuing response (snapshot nodes, digest refs, the find ref) move
   to its generation; absent refs KEEP their older pins — an old pin on a
   replaced tree is what makes the daemon warn. Never-issued refs pass
   through unpinned; an issuing response without refsGeneration clears
   the scope; memory bounded to the ~1000 most recently issued pins.

2. Generations were per-lifetime counters from 1, so a reopened
   session's ~s1 collided silently with the previous lifetime's. Seed
   the first bump at a random 6-digit base (crypto randomInt):
   cross-lifetime collisions become ~1e-6 — probabilistic (seeded), not
   identity-based, documented on the field. Pin format unchanged;
   within-lifetime comparisons stay exact.

Tests: the MCP blessing scenario (pre-find ref stays pinned to ITS
generation), the daemon half in the provider scenario (find must not
bless a pre-find pin), reopen/reseed at unit + handler level, state-dir
scope isolation, digest-ref merging; generation fixtures made
seed-agnostic (relative bumps, echo the observed seed).

Refs #1076
2026-07-04 21:02:10 +02:00
Michał Pierzchała 6ee483c33f feat: warn when @refs outlive the session snapshot they came from (#1093)
Refs are positional indexes into the latest stored session tree; any
selector-based command's resolution capture silently reshuffles them
(#1076). Track an honest marker on the daemon session
(snapshotRefsStale): set wherever the stored snapshot is replaced by a
response that does not hand the new refs to the client, cleared only
where the client demonstrably receives them (snapshot responses, find
ref outputs). Commands consuming @ref arguments while the marker is set
(press/click/longpress/fill/get/wait) attach a warning and still
execute — the geometric guards keep catching detectable drift.

Closes #1076
2026-07-04 17:11:22 +02:00
Michał Pierzchała ad754ac0a4 feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011) (#1091)
* feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011)

Runner ELEMENT_NOT_FOUND/AMBIGUOUS_MATCH on the direct iOS selector
dispatch now delegate to the tree-based runtime path, which supplies
runtime disambiguation, occlusion refusal, non-hittable
promotion/annotation, and rich selector diagnostics/hints.

Maestro replay dispatches (allowNonHittableCoordinateFallback) keep the
runner-native error shapes: the new delegateSemanticFailures option is
threaded from the dispatch flag, and the query path
(allowElementNotFound at selector-runtime) keeps its exact semantics.

Registry: direct-ios-selector errorTaxonomy/nonHittable/occlusion flip
from gap waivers to delegated cells; the pinned gap list shrinks 6 -> 3.
Success-path parity cells (disambiguation, responseIdentity)
intentionally remain gaps per ADR 0011.

Refs #1081

* test: contract coverage for the three newly-delegated direct-iOS cells

The coverage gate demanded scenarios the moment the rebase flipped
errorTaxonomy/nonHittable/occlusion to delegated — as designed. Three
transcript scenarios prove the delegation end to end: runner
ELEMENT_NOT_FOUND falls back to runtime no-match diagnostics, to the
covered refusal, and to an annotated coordinate tap (the transcript
asserts the fallback tap is coordinate-keyed).
2026-07-04 16:41:03 +02:00
Michał Pierzchała f3e07ff236 test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3) (#1092)
* fix: preserve the runner's non-hittable fallback marker through direct selector press

The direct iOS selector press handler spread successText('Tapped <selector>')
after the runner payload, clobbering the 'tapped via non-hittable coordinate
fallback' message that directIosSelectorFallbackDetails keys on — so
maestroNonHittableCoordinateFallbackUsed could never be true end-to-end (the
existing unit test passes because it mocks dispatchCommand above this layer).
Found by the ADR 0011 Layer-3 maestro-fallback contract scenario.

Share the marker string as MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE and keep it
as the success message when the runner reports fallback usage.

* test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3)

test/integration/interaction-contract/ holds one scenario file per dispatch
path, each with a sibling .coverage.ts manifest declaring which guarantee
matrix cells it proves (scenario strings double as the vitest test titles).
index.ts aggregates the manifests statically, and a new Layer-3 gate in
src/contracts/__tests__/interaction-contract-coverage.test.ts fails when any
enforced (runtime/runner/delegated) cell lacks a scenario or when a scenario
claims a waived/inapplicable cell — coverage of the matrix is by
construction in both directions.

Path forcing is natural (no test-only env switch needed): selector/ref
targets take the runtime path, a tapTarget backend takes the native-ref fast
path, x/y takes the coordinate path, and simple-selector clicks on an iOS
provider transcript take the direct runner path (the transcript itself
proves which path ran via assertComplete).

Fixtures are the permanent Bluesky shapes: closed drawer, drawer + visible
twin, edge-grazing container, covered button, non-hittable cell.

Refs #1081
2026-07-04 16:02:41 +02:00
Stanisław Chmiela 9407839f6e feat: tag daemon artifacts with semantic types (#1066)
* feat: tag daemon artifacts with semantic types

* Normalize artifact type threading through daemon tracking

* Add timeout error to http server artifact test helper

* test: restore artifact wait helper behavior

* test: fail artifact wait helper on timeout

* refactor: model artifactType as optional on wire shapes

Applies the design-review tweak: producer-owned APIs (reserveOutput,
trackDownloadableArtifact, finalization callbacks) keep the required
'DaemonArtifactType | undefined' form so artifact owners must explicitly
decide, while public/wire/result shapes (DaemonArtifact, both artifact
inventory entry types) become 'artifactType?:' — missing metadata is
valid, JSON drops undefined, and remote/older daemons may omit the
field. Construction sites now omit the key for untyped artifacts, and
the finalization test asserts key absence (toEqual cannot distinguish
absent from explicitly-undefined).

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-07-04 12:19:47 +02:00
Michał Pierzchała 664b713130 feat(interaction): opt-in --verify evidence for press/click/fill (#1064)
* feat(interaction): opt-in --verify evidence for press/click/fill (#1047)

Adds an opt-in --verify flag that returns cheap post-action evidence
(foregroundApp, nodeCount, interactiveNodeCount, digest,
changedFromBefore) instead of requiring a full follow-up snapshot to
confirm a mutating command had an effect. The digest hashes the
(type, label, identifier) multiset of an interactive-only capture,
order-independent so it doesn't flip on harmless re-ordering; the
node tree is never serialized back to the client, only the digest and
counts. Default behavior (no --verify) is byte-identical to today.

Implements the approved design from the #1047 issue comment:
- src/utils/ax-digest.ts: new standalone digest module.
- Pre-action digest reuses the snapshot the resolution path already
  captures for ref/selector targets (zero extra cost); point targets
  opt into one extra baseline capture only when --verify is set.
- Post-action: one interactive-only capture through the same capture
  helper, digested and discarded.
- --verify threaded through the CLI flag schema, MCP input schema,
  interactionResultExtra allowlist, and MCP output schemas, following
  the same plumbing as --double-tap and the #1040 targetHittable
  precedent.
- The native-ref/direct-iOS-selector fast paths are skipped when
  --verify is set, since they bypass the resolution/capture path
  evidence depends on.

* test: cover press --verify with a provider scenario; classify the flag

CI's architecture-progress gate failed on 1 unclassified public flag.
--verify drives real device captures, so it belongs in the
device-observable list backed by an actual provider scenario rather than
the intentionally-outside bucket: the new scenario asserts evidence
(changedFromBefore, digest, nodeCount) on press @ref --verify, that the
verify capture's tree is never serialized into the response, and that
the transcript completes (snapshot -> tap -> verify snapshot; the @ref
path reuses the session snapshot as its baseline, so no extra
resolution capture entry exists).

* fix: type the verify scenario transcript entries

* fix: include interaction extras in the fill @ref response branch

The ref branch of dispatchFillViaRuntime rebuilt responseData from
backendResult/coordinates, dropping interactionResultExtra(result) — so
fill @ref --verify returned no evidence even though the post-action
capture ran (live E2E gap found in PR #1064 review). Spreading the
extras also gives fill @ref the same ref/refLabel/selectorChain (and
conditional targetHittable/hint) fields press @ref already returns.

Adds daemon tests for fill @ref --verify evidence and for the no-verify
path staying evidence-free with no post-action capture.
2026-07-04 10:48:54 +02:00
Michał Pierzchała 4711bc434b fix: report expired daemon resources as expiry, not INVALID_ARGS (#1069)
* fix: report expired daemon resources as expiry, not INVALID_ARGS

TTL/GC'd registry entries (downloadable/uploaded artifacts, resumable
uploads, materialized paths) surfaced as INVALID_ARGS, so agents got
"Check command arguments and run --help" when the resource had simply
expired. Add a shared requireTenantOwnedEntry helper that reports these
as COMMAND_FAILED with reason RESOURCE_EXPIRED and a per-resource
recovery hint, and dedupes the paired wrong-tenant UNAUTHORIZED check.

Also: give request cancellation an explicit "canceled intentionally"
hint instead of the misleading retry default; replace the ad-hoc
INTERNAL_ERROR code (not in the KnownAppErrorCode union) with UNKNOWN
plus a report-a-bug hint; and fix materialized-path TTL expiry of
tenant-owned entries, which threw UNAUTHORIZED in a void-ed promise
(unhandled rejection) and leaked the entry and its temp dir.

* fix: report expired resumable upload tickets as expiry, not INVALID_ARGS

Route requireResumableUpload() through the shared requireTenantOwnedEntry
helper so a missing/expired upload ticket on PUT /upload/direct/:id and
POST /upload/finalize returns the COMMAND_FAILED + RESOURCE_EXPIRED shape
(with a recovery hint) instead of the misleading INVALID_ARGS/400.

Genuinely invalid input (malformed preflight args, bad content-range,
incomplete upload, hash mismatch) stays INVALID_ARGS.
2026-07-04 10:46:49 +02:00
Michał Pierzchała 50a6a3533a fix: surface stderr excerpts from hand-rolled exec failure wraps (#1072)
* fix: surface stderr excerpts from hand-rolled exec failure wraps

normalizeError only replaces a generic COMMAND_FAILED message with the
first meaningful stderr line when details.processExitError is true, but
only createExitError in utils/exec.ts set that flag. Hand-rolled wrap
sites (throw new AppError('COMMAND_FAILED', msg, {stdout, stderr,
exitCode}) after an allowFailure run) missed it, so users saw messages
like "xcrun exited with code 22" with a generic hint instead of the
actual stderr excerpt.

Add execFailureDetails(result, extra?) to utils/exec.ts returning the
stdout/stderr/exitCode spread plus processExitError: true (extras spread
after the base keys so call sites can keep truncation or String()
normalization), and convert ~42 exit-guarded wrap sites across the apple
platform (apps launch/terminate paths, devicectl, simulator boot, perf,
perf-xctrace, runner modules, dsym/symbolication, macOS host provider)
and android/web (app-helpers, logcat, devices, perf, settings,
snapshot helpers, app-lifecycle, device-input-state, multitouch-helper,
agent-browser provider).

Sites reachable at exit 0 (parse/stdout checks), failure aggregations,
timeout/forced-kill errors, and sites that already build richer messages
(macos helper JSON, runner-contract early-exit, maestro run-script) are
deliberately left unflagged so stderr enrichment never replaces a more
specific message.

* fix: append stderr excerpts to curated wrap messages instead of replacing them

Spreading processExitError to curated call sites made normalizeError
replace specific messages like 'uiautomator dump did not return XML'
with the bare stderr line, losing the operation context (caught by the
provider-failures integration test). Keep wholesale replacement only for
the information-free generic '<tool> exited with code N' wraps; curated
messages now keep their specific reason and gain the stderr excerpt as a
': <excerpt>' suffix (skipped when already present).

Separate commit rather than amend so the semantics change to
maybeEnrichCommandFailedMessage stays individually reviewable on the PR.
2026-07-04 10:25:03 +02:00
devin-ai-integration[bot] d18066ff2b fix(cloud-webdriver): stop artifacts command from allocating a new provider session (#1030)
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 17:44:21 +02:00
devin-ai-integration[bot] 7777fa549e fix(aws-device-farm): send requested platform in WebDriver capabilities (#1028)
The AWS Device Farm runtime is registered with a hardcoded platform of 'android', so baseSessionForLease builds the createSession capabilities with platformName 'Android' before prepareSession resolves the real platform. On an iOS device Device Farm rejects this with "reserved ... invalid values: ['platformName']". Rebuild webdriverCapabilities in the AWS prepareSession using the resolved platform and device name (mirroring the BrowserStack provider).

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 16:33:50 +02:00
Michał Pierzchała fcb7e32f1c fix: clarify clean-xcuitest output (#1018)
* fix: clarify clean-xcuitest output

* fix: simplify clean-xcuitest formatter

* fix: clarify clean-xcuitest failure output
2026-07-02 13:53:42 +02:00
Michał Pierzchała 996d93e979 perf: cut iOS open --relaunch from 11s to 2.85s (#1010)
* perf: keep iOS simulator runner hot across open --relaunch

open --relaunch tore down the XCUITest runner session before closing the
app, then paid a full xcodebuild test-without-building restart (~6s) after
reopening it. Simulator close/open go through simctl and never touch the
runner, so a healthy runner survives the relaunch; stale runners are
recovered by the readiness preflight + invalidate/restart path that landed
after #705 reverted #700's version of this.

Steady-state open --relaunch on iPhone 17 Pro sim: 11.0s -> 4.9s
(startup.durationMs 5929 -> 1574). Real devices keep the conservative
teardown.

* perf: drop redundant simctl inventory listings from the iOS open path

A single open --relaunch spawned `simctl list devices -j` (~0.7s each)
three times: session-device re-resolve, closeIosApp's booted check, and
launchIosSimulatorApp's booted check.

- ensureBootedSimulator now keeps a 5s recently-observed-Booted memo
  (mirroring DEVICE_READY_CACHE_TTL_MS at the daemon layer) so repeated
  boot checks inside one request cost nothing; shutdownSimulator
  invalidates it, boot transitions seed it.
- refreshSessionDeviceIfNeeded skips the re-resolve while the device's
  XCUITest runner session is alive - a live runner attached to the UDID
  already proves the simulator exists and is booted.

With the runner kept hot across relaunch, steady-state open --relaunch on
iPhone 17 Pro sim drops 4.9s -> 2.85s (startup.durationMs ~1574 -> ~760).
Combined with the previous commit: 11.0s -> 2.85s.

* test: expect single simctl listing in tvOS provider flow

The recently-observed-Booted memo removes the repeat state listings the
launch and terminate boot checks used to make; the transcript now records
one listing per flow.
2026-07-02 11:33:43 +02:00
Michał Pierzchała 36917157ab test: surface-wide guard that no command response emits internal apple platform (#1005)
Adds a provider-integration guard (apple-platform-output-guard.test.ts) that
stands up a fake-provider daemon for BOTH a macOS Apple session and an
iOS-simulator Apple session, drives EVERY public command off PUBLIC_COMMANDS,
and deep-scans each serialized response for the internal 'apple' platform token
(any string VALUE or object KEY that exactly equals 'apple').

The guard is catalog-driven: a partition test fails if a new public command is
neither in DRIVEN_COMMANDS nor SKIPPED_COMMANDS, so a new command can't silently
escape the check. All 50 public commands are driven; the skip-set is empty.

Caught + fixed a leak PR #1004 misses: doctor's data.platform (session-doctor.ts)
echoed the raw internal device.platform ('apple') when doctor ran against a bound
session with no --platform flag. Now projected via publicPlatformString(device).

doctor's byPlatform.apple KEY leak stays tracked to PR #1004 via a narrow,
documented allowlist entry (not duplicated here).
2026-07-02 07:18:56 +02:00
Michał Pierzchała cd1551bf42 refactor: collapse public Platform ios/macos into apple (#979) (#1002)
* refactor: collapse public Platform ios/macos into apple (#979)

Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a
single `apple` platform, with `appleOs` as the sole OS discriminant. Approach
(b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on
every read path and still EMITS the leaf `ios`/`macos` strings on every output,
so machine consumers see no change.

Kernel (src/kernel/device.ts):
- PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and
  PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases.
- New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the
  post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output
  projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform.
- isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs).

Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal
`device.platform === 'ios'|'macos'` branch sites migrated to the predicates,
behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse
to one `apple` row.

Output projection (approach b) emits the leaf via publicPlatformString at:
devices / session_list (session-inventory), boot / shutdown / appstate /
prepare-ios-runner (session-state, session), the selector/backend platform
(selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime),
proxy device key, request-lock backfill, runtime-set binding, click-button
validation, and both `.ad` context-line writers.

Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform,
REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy
leaves. Adds a parity test gate (platform-collapse-parity.test.ts).

Refs #979 (part of #972).

* fix: project platform to the public leaf at open/perf response sites (#979)

The Platform collapse left two daemon response builders emitting the raw
internal `device.platform` ('apple'), which the client normalizer rejects
(isPublicPlatform excludes 'apple') — dropping the resolved device from the
response:
- session-open-surface.ts: `open` result `platform`/device projection.
- session-perf.ts: the perf/frames/memory base response builders.
Both now go through `publicPlatformString(device)`, so output stays the leaf
`ios`/`macos` per approach (b). (The android/non-apple perf branches were
already leaf-safe.)

Also update macos-desktop provider test: the lifecycle mock observes the
INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the
recorded tag is `prepare:apple:desktop`.

Fixes the provider-integration assertions that blocked both the Integration
Tests and Coverage CI jobs (both run the provider-integration project).
Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/
fallow green.

* fix: project platform to the public leaf at nested output sites (#979)

The Platform collapse (approach b) projects device.platform through
publicPlatformString at emit sites so machine consumers keep seeing the
leaf ios/macos and never the internal `apple`. Several nested output
fields were missed. Project them and narrow their emitted types to
PublicPlatform:

- Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) —
  response.support.platform / artifact.support.platform, plus the
  sibling sampleAppleFramePerf error data.
- Apple xctrace perf capture/result platform surfaced in the perf
  cpu-profile started/stopped response data.
- snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in
  snapshot/test response data and the slow-snapshot warning string.
- doctor target-app evidence.platform + human summary, and doctor
  target-app-device evidence.booted[].platform.
- provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud
  Apple devices (reachable via deviceFieldsFromPublicPlatform).

Internal 'apple' emissions (selector-matching input, diagnostic
emitDiagnostic telemetry, session appLog state, replay .ad flags) are
left as-is. Adds focused tests pinning Apple perf memory support to the
leaf and a guard asserting no emitted platform field equals 'apple'.
2026-07-01 19:29:46 +02:00
Michał Pierzchała f0b926c21f fix: trim root type exports (#999) 2026-07-01 14:47:03 +02:00
Michał Pierzchała d21b8ce32e feat: add doctor command (#883)
* feat: add doctor command

* fix: reduce doctor command complexity

* fix: classify doctor integration flags

* fix: simplify doctor setup

* refactor: split doctor checks

* fix: simplify doctor check set

* fix: include stopped android avds in devices

* fix: report doctor device inventory

* refactor: reuse device inventory selectors

* fix: summarize doctor inventory by platform

* fix: show metro cwd in doctor

* refactor: simplify metro doctor lookup

* fix: update doctor imports after apple consolidation

* feat: make doctor Metro probe controllable and surface hidden toolchain failures

Two gaps found while verifying the doctor command on a real environment:

- Metro host/port were uncontrollable from the CLI: --metro-host/--metro-port
  were rejected by allowedFlags, and readDoctorOptions only read them from
  req.runtime (populated by remote/connection profiles, never a plain CLI
  flag). The Metro check's own hint told users to 'pass the correct
  --metro-host/--metro-port', which did not exist. Declare the flags and read
  them from req.flags (runtime kept as fallback) so the probe can target any
  endpoint, e.g. from outside an RN/Expo project directory.

- A broken per-platform toolchain was silently hidden: readDoctorDeviceInventory
  dropped inventory failures whenever any other platform returned devices, so a
  broken Xcode or Android SDK still reported a green 'pass'. Keep the failures
  and surface each as a warn (device-<platform>) when other platforms have
  devices; scoped --platform runs stay quiet.

* fix: align doctor CI expectations

* feat: extend doctor preflight checks

* fix: keep doctor checks within ci gates

* fix: simplify doctor metro surface

* refactor: trim doctor bundle impact

* fix: restore useful doctor diagnostics

* refactor: reuse doctor output helpers

* refactor: share device inventory grouping

* refactor: keep doctor focused on preflight checks

* refactor: simplify doctor toolchain probes

* fix: keep scoped simulator hint generic

* fix: clarify doctor Xcode selection context

* fix: recognize provider scope in remote doctor

* fix: address doctor review gaps

* fix: keep doctor metro checks inferred
2026-07-01 14:24:32 +02:00
Michał Pierzchała 56b41a53ab feat: add cross-platform audio probe (#880)
* feat: add web audio probe

* fix: stabilize web audio probe

* test: cover audio probe review gaps

* fix: address audio probe review feedback

* feat: support macOS audio probe

* docs: document audio probe help

* feat: support simulator audio probe

* test: account for host audio platform support

* refactor: deepen audio probe lifecycle

* perf: trim audio probe package size

* refactor: address audio probe review comments

* refactor: remove audio probe leftovers

* fix: encode audio probe eval options as data

* fix: document audio probe eval sanitization

* fix: sanitize audio probe eval options

* fix: use codeql-recognized eval option sanitizer

* fix: allowlist audio probe eval options

* fix: avoid json-stringified audio eval action

* refactor: trim audio probe input surface

* fix: align audio probe with apple helper paths

* test: update audio capability parity oracle

* refactor: isolate host audio probe backend

* fixup! refactor: isolate host audio probe backend

* fixup! refactor: isolate host audio probe backend

* fixup! test: update audio capability parity oracle
2026-07-01 13:27:45 +02:00
Michał Pierzchała a707348b9b feat: add hosted WebDriver provider support (#948)
* feat: add cloud webdriver artifacts

* fix: clean up local session after provider release failure

* fix: tag cloud webdriver provider requests

* feat: connect hosted webdriver providers

* docs: document hosted provider credentials

* refactor: tighten cloud webdriver provider internals

* refactor: consolidate cloud webdriver provider definitions

* refactor: collapse hosted webdriver runtime wrapper

* refactor: reduce cloud webdriver smell surface

* docs: clarify hosted provider interfaces

* fix: avoid regex slash trimming in webdriver urls

* docs: rename hosted providers to device clouds

* fix: align provider profile imports with remote modules

* fix: skip local android recovery for provider devices

* test: classify cloud provider integration flags

* fix: close active cloud connection session

* test: cover provider disconnect cli flow

* fix: make cloud webdriver sessions launchable

* fix: align cloud webdriver input gestures

* fix: avoid keyboard input during cloud scroll

* fix: constrain cloud webdriver scroll gestures

* refactor: isolate cloud webdriver scroll frame

* refactor: deduplicate cloud webdriver helpers

* refactor: tighten cloud webdriver action types

* fix: harden cloud webdriver release

* fix: polish provider disconnect diagnostics

* refactor: group connection profile helpers

* fix: repair rebased internal paths

* fix: satisfy cloud webdriver CI guards
2026-07-01 13:01:47 +02:00
Michał Pierzchała e229957602 refactor: split daemon server runtime into daemon/server/ — Phase 5 (#985)
Phase-5 §5.5 folder move (server side; the daemon/client/ split shipped in
#962). Extracts the process-bootstrap / server-runtime cluster into
src/daemon/server/ as a pure, behaviorless path codemod — no logic changes.

Moved (server bootstrap/runtime — the layer that spins up the daemon and
owns the platform graph; each imported only by the bootstrap layer + each
other):
  src/daemon-runtime.ts          -> src/daemon/server/daemon-runtime.ts
  src/daemon/http-server.ts      -> src/daemon/server/http-server.ts
  src/daemon/transport.ts        -> src/daemon/server/transport.ts
  src/daemon/server-lifecycle.ts -> src/daemon/server/server-lifecycle.ts
  src/daemon/server-shutdown.ts  -> src/daemon/server/server-shutdown.ts

Left in src/daemon/ root (request core / shared wire helpers, out of scope):
  request-router.ts, handlers/, session-store.ts, lease-registry.ts, context.ts
  (the daemon's request layer) and http-contract.ts / http-health.ts /
  http-errors.ts / config.ts (HTTP wire contract + daemon config shared across
  client, remote, and cli — not server-only).

Left: src/daemon.ts (the thin process entry) stays at src/ with the other
package entrypoints; it is coupled to its physical path by four non-import
string references (rslib entry, config dev-mode sentinel, process-identity
detection regex, daemon-client launch srcPath), so moving it is beyond a pure
import codemod.

Rewrote every from/import/import()/type-only specifier per importer
(resolve-based path.relative recompute) across src and test, and renamed the
fallow health-baseline key for http-server.ts. daemon-runtime's static
platforms/ import is now inside the daemon-server seam the layering lint
(#984 R3) allows.

Verification: tsc --noEmit 0; layering check (branch script) unchanged (3
pre-existing R3 violations, 0 new); oxfmt clean; oxlint --deny-warnings 0;
fallow audit --base origin/main clean (14 files); rslib build 0
(internal/daemon entry still emits); vitest 17 passed (daemon-entrypoint,
http-server-rpc-validation, server-shutdown + 3 provider-integration).
2026-07-01 10:02:59 +02:00
Michał Pierzchała 26ac865c63 refactor: consolidate Apple platform internals (#968) 2026-06-30 21:30:46 +02:00
Michał Pierzchała 1cbe446df5 chore: prune public package exports (#961)
* chore: prune public package exports

* chore: drop extra facade exports

* chore: remove dead export leftovers

* docs: align public API docs
2026-06-30 15:51:37 +02:00
Michał Pierzchała 189b062519 refactor: extract client/ folder — Phase 5 (#960)
Move the SDK client + companion-tunnel cluster out of the src/ root into a
dedicated src/client/ folder, per plans/perfect-shape.md §5.5 (~8k LOC
client/remote unfoldered at src root; remote/ already extracted in #951).

Files moved (9): client, client-types, client-shared, client-normalizers,
client-companion-tunnel{,-contract,-worker}, client-react-devtools-companion,
companion-tunnel.

- git renames; 68 importers repointed via the resolve-based codemod
  (intra-client stays ./, staying deps recomputed)
- companion-tunnel.ts keeps rslib key 'internal/companion-tunnel' so dist
  output stays dist/src/internal/companion-tunnel.js (public subpath
  byte-identical; verified by build)
- update non-src importers (3 integration tests, vitest coverage include),
  .fallowrc.json entrypoint, fallow-baselines/health.json keys

backend.ts and daemon-client*.ts are intentionally left for follow-up
(daemon/client split). Behaviorless path codemod; all gates green.
2026-06-30 14:36:59 +02:00
Michał Pierzchała 548bf0c47c refactor: extract remote/ folder — Phase 5 (#951)
Move the remote/proxy/upload subsystem out of the src/ root cluster into a
dedicated src/remote/ intent folder, per plans/perfect-shape.md §5.5:

  daemon-proxy · daemon-artifacts · upload-client(-artifact) · remote-config
  · remote-config-core · remote-config-schema · remote-connection-state

- 8 files moved (git renames); imports repointed via a resolve-based codemod
  (path.relative recomputation — correctly distinguishes the root remote-config
  from the unrelated src/utils/remote-config.ts)
- rslib entry keeps key 'remote-config' so dist output stays
  dist/src/remote-config.js; public 'agent-device/remote-config' byte-identical
- update .fallowrc.json entrypoint + fallow-baselines/health.json keys +
  vitest.config.ts coverage include + the integration test import paths

Behaviorless path codemod. typecheck/lint/build/fallow/tests all green.
Stacked on #950 (contracts→kernel).
2026-06-30 11:47:39 +02:00
Michał Pierzchała f67b72d7db refactor: delete the WaitCommandResult/AlertCommandResult mirror types — Phase 2c (#946)
BREAKING (intentional, approved): removes the last two hand-written result-type
mirrors from client-types.ts. wait and alert are genuinely dynamic — wait's
daemon data is toDaemonWaitData's Record, and alert's iOS path is a generic
runner Record — so per the typed-result doctrine they should be the untyped
CommandRequestResult, not an invented closed shape.

- delete WaitCommandResult and AlertCommandResult from client-types.ts; the
  client.command.wait/alert methods now return CommandRequestResult.
- drop their public exports from index.ts and the now-unused AlertInfo import.
- the android-lifecycle integration test that read the typed alert.alert.source
  now casts the untyped bag.

This completes the result-type half of the client-types.ts mirror deletion (the
13 closed commands already live in src/contracts/* via CommandResultMap). The
Options-type half (deriving from inputSchema) is a separate follow-up.

Verified: tsc, oxfmt + oxlint --deny-warnings, fallow audit clean, Layering
Guard empty, 476 client/contracts/mcp tests pass.
2026-06-30 08:21:47 +02:00
Michał Pierzchała 7a1640e53f refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3 (#940)
* refactor: move errors/redaction/device into src/kernel — Phase 5 slice 3

Relocates the foundational primitive trio from src/utils/ into the kernel/ layer
(joining snapshot.ts from slice 2), per the target folder DAG in
plans/perfect-shape.md §5.5. A pure path codemod, no behavior change.

They form a closed cluster — device -> errors -> redaction, with redaction a
leaf — so kernel/ takes no upward dependency, and every importer becomes a clean
downward import toward kernel. errors.ts is the most-imported module in the
tree; device.ts the §5.5-named headliner. Moving all three atomically avoids a
half-state where one would import another across the utils/kernel boundary.

Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the moved files, so the unrelated commands/management/device.ts and
other same-named files are untouched): 483 sites across 402 files. The two
platform-descriptor doc comments and the fallow health baseline key for
device.ts are updated to the new path; the contracts-schema-public guard that
asserts the error helpers pull no diagnostics/node: deps now reads kernel/.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, rslib build, full vitest
suite (2877 pass), fallow audit clean (411 changed files), Layering Guard empty;
kernel/ files import only within kernel.

* docs: update guidance references to kernel/{device,errors} after the move

AGENTS.md (Apple-family sync rule + normalizeError), ADR-0009, and
plans/apple-platform-consolidation.md still named the old src/utils/ paths.
Point them at src/kernel/. plans/perfect-shape.md's utils/device.ts mention is
left as-is — it describes the pre-move diagnosis.
2026-06-30 07:25:02 +02:00
Michał Pierzchała dae80c70c0 refactor: add provider device runtime foundation (#933) 2026-06-29 19:12:30 +02:00
Michał Pierzchała 41358fe281 feat: typed command results, batch 3 (clipboard, appstate) — Phase 2 (#937)
Wire two more commands into the CommandResultMap spine as closed shapes,
grounded in the handlers' literal returns:

- clipboard (src/core/dispatch.ts handleClipboardCommand) -> a discriminated
  union on `action`: read => { text }, write => { textLength, message }.
- appstate (src/daemon/handlers/session-state.ts handleAppStateCommand) -> a
  discriminated union on `platform`: Apple (ios/macos) session state — now
  including the iOS-only device_udid / ios_simulator_device_set locators the
  previous hand-written mirror OMITTED — or Android package/activity.

Both result types move from the open client-types.ts mirror (DaemonResponseData
& {…}) into new src/contracts/{clipboard,app-state}.ts and are wired through
CommandResult<'clipboard'> / CommandResult<'appstate'>. Public export names are
preserved (re-exported via client-types.ts -> index.ts), so no API break.

Tightening clipboard to a closed union surfaced an unguarded .text/.textLength
access in an Android integration test (previously masked by the Record index
signature); fixed with discriminant guards. The parity test now pins all 12
migrated commands; the public-root export test gains clipboard/appstate samples.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, fallow audit clean,
Layering Guard empty, 791 tests across core/contracts/client/commands pass.
2026-06-29 19:08:39 +02:00
Michał Pierzchała 0be806cc50 refactor: seed src/kernel with snapshot.ts type module — Phase 5 slice 2 (#932)
Establishes the kernel/ layer from the perfect-shape DAG (plans/perfect-shape.md
§5.5) by moving the foundational snapshot type module (Rect, SnapshotNode,
SnapshotQualityVerdict, centerOfRect — re-exported by contracts.ts and imported
by ~130 files) from src/utils/snapshot.ts to src/kernel/snapshot.ts.

Pure path codemod, no behavior change. snapshot.ts is a leaf (zero imports, after
the preceding cycle-break PR), so kernel/ takes no upward dependency — and every
one of its ~130 importers (utils, core, daemon, platforms, commands, tests)
becomes a clean downward import toward kernel. This also unblocks the future
snapshot/ AX-domain extraction: those domain files now import DOWN into kernel
rather than sideways within utils.

Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the old module, so the other snapshot.ts files under commands/platforms/
daemon are untouched). 138 sites across 128 src files + 2 integration worlds.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, full vitest suite
(308 files / 2863 tests), fallow audit clean (133 files), rslib build, Layering
Guard empty; kernel/snapshot.ts confirmed import-free.
2026-06-29 17:30:46 +02:00
Michał Pierzchała ed701f3d1f feat: support resumable proxy uploads (#919)
* feat: support resumable proxy uploads

* refactor: reduce upload proxy fallow findings

* refactor: localize proxy upload routing

* test: simplify resumable proxy upload coverage

* refactor: deepen upload route handling

* refactor: tighten upload http types

* test: avoid dynamic proxy route dispatch

* feat: show remote upload progress

* fix: show install phase after upload

* fix: isolate resumable upload attempts

* refactor: split upload client internals

* test: make install notice test independent of CI env
2026-06-29 16:21:00 +02:00
Michał Pierzchała 4eccfdde5c feat: infer install target from artifact (#923)
* feat: infer install target from artifact

* fix: allocate proxy lease for install
2026-06-29 14:33:40 +02:00
Michał Pierzchała 8f92572ae4 feat: expose web screen recording (#891) 2026-06-26 19:36:50 +02:00
Michał Pierzchała 2d6fc98222 refactor: tighten daemon output helpers (#892)
* refactor: tighten daemon screenshot typing

* refactor: share screenshot result typing

* refactor: inline record shape checks

* refactor: inline record readers

* refactor: type batch and network output

* refactor: type batch run response

* refactor: narrow batch and observability output types

* refactor: validate typed batch runtime hints

* refactor: restore record boundary guard

* refactor: split daemon output parsers

* refactor: narrow screenshot overlay parser

* refactor: type screenshot overlay data

* refactor: type shutdown result payload
2026-06-26 16:32:52 +02:00
Michał Pierzchała be1e1c9931 feat: add cdp command agent-cdp passthrough (#873)
* feat: add agent-cdp passthrough

* docs: narrow agent-cdp memory guidance

* chore: pin agent-cdp 1.6.0

* test: cover agent-cdp guidance

* fix: preserve agent-cdp passthrough flags

* fix: expose CDP wrapper as cdp

* docs: move CDP workflow to debugging guide

* docs: mention cdp in command reference
2026-06-25 21:44:47 +02:00
Michał Pierzchała 8c49d54ff1 fix: clarify proxy runner ownership (#882)
* fix: clarify proxy runner ownership

* fix: tighten proxy runner review follow-ups
2026-06-25 21:44:31 +02:00
Michał Pierzchała 98c0b1d3bf test: migrate test app to expo dev client (#881)
* test: migrate test app to expo dev client

* docs: align test app device targeting

* docs: clarify dev client setup tradeoffs

* docs: remove stale sdk reference
2026-06-25 21:43:14 +02:00
Michał Pierzchała 56d8e3902a test: migrate Android recording sizing coverage (#876) 2026-06-25 17:33:51 +02:00
Michał Pierzchała f253d50a63 fix: resolve web find locators (#870) 2026-06-25 10:12:29 +02:00