289 Commits

Author SHA1 Message Date
Michał Pierzchała d7beb52768 feat: expose web network dump through agent-browser (#838)
* feat: expose web network dump through agent-browser

* fix: reduce web network mapper complexity

* fix: gate web network headers by include mode

* test: assert compact web network summary

* refactor: simplify web network dump mapping

* refactor: trim web network coverage
2026-06-22 14:40:25 +02:00
Michał Pierzchała d3955d5143 chore: remove test-only dead exports (#836)
* chore: remove test-only dead exports

Remove 13 production exports that were consumed only by their own unit
tests, along with those tests. Found via a fallow production-vs-default
unused-export differential (exports unused by production but kept "live"
by a test), then each candidate was reachability-traced and the verdict
adversarially re-verified to confirm zero production use (internal,
re-export, registry, or dynamic dispatch).

Removed exports + their dedicated tests:
- backend.ts: hasBackendCapability, hasBackendEscapeHatch (+ orphaned
  BACKEND_CAPABILITY_ESCAPE_HATCH_METHODS) — cascade from runtime change
- runtime.ts: assertBackendCapabilityAllowed
- command-catalog/capabilities: (kept — see below)
- commands/cli-grammar/common.ts: commandNameSet (no test; pure dead)
- compat/maestro/support-matrix.ts: MAESTRO_COMPAT_UNSUPPORTED_CAPABILITIES
- core/dispatch-resolve.ts: resolveIosDevice
- daemon-client.ts: openApp (standalone fn; Backend.openApp untouched)
- daemon/daemon-command-registry.ts: listDaemonHandlerCommands
- daemon/post-gesture-stabilization.ts: capturePostGestureStabilizedSnapshot
- platforms/android/scroll-hints.ts: annotateAndroidScrollableContentHints
- platforms/ios/runner-session.ts: stopRunnerSession
- platforms/ios/runner-xctestrun-products.ts: xctestrunReferencesExistingProducts
- replay/script.ts: parseReplayScript
- utils/cli-option-schema.ts: getOptionSpecForToken
- utils/finders.ts: findNodeByLocator

Tests whose real subject is live code (handler routing, snapshot status,
writeReplayScript, maestro doc-sync) were rewritten to drop the dead
symbol while keeping coverage, not deleted.

typecheck, lint, and the unit suite (2568 tests) pass; fallow
production unused-exports drops 231 -> 218 with no new dead code.

* test: restore replay script parser coverage

* test: re-point replay parser cases to parseReplayScriptDetailed

Address review on #836: parseReplayScript was a thin wrapper, but the
parser it drove (parseReplayScriptDetailed) is live via
compat/replay-input.ts and compat/maestro/export-flow.ts. Restore the
record/screenshot/snapshot/gesture/type parsing assertions deleted with
the wrapper, re-pointed to parseReplayScriptDetailed(...).actions, so
.ad parser regressions (--hide-touches/--fullscreen/--force-full,
gesture positionals, --delay-ms) stay covered.

* Revert "test: re-point replay parser cases to parseReplayScriptDetailed"

This reverts commit 20a01e8641.
2026-06-21 13:04:47 +02:00
Michał Pierzchała d05cfc727b feat: manage web backend setup (#833) 2026-06-20 11:44:00 +02:00
Aldo Ryanda 73dc7f882d feat(recording): align quality and max-size controls (#816)
* feat(recording): make iOS export quality configurable

Wire the existing recording-export-quality enum through the record command
down to the Swift export preset. Adds a `--export-quality <medium|high>`
option for iOS recordings that controls the AVAssetExportSession preset used
when a recording is re-encoded.

`medium` stays the default and selects AVAssetExportPresetMediumQuality, which
preserves the fast simulator-friendly export. `high` opts into
AVAssetExportPresetHighestQuality for evidence-grade output. This is separate
from the existing integer `--quality <5-10>` capture flag that scales render
resolution.

Closes #568

* fix(recording): apply export quality to touch-overlay export path

The --export-quality flag was only wired into the resize export path. The
touch-overlay re-encode (finalizeRecordingOverlay -> overlayRecordingTouches ->
recording-overlay.swift) ignored it and always picked AVAssetExportPresetMediumQuality,
so record stop with --export-quality high had no effect when the stop path
re-encodes only to burn in touch overlays.

Thread the recording's exportQuality through finalizeRecordingOverlay and
overlayRecordingTouches, pass it as --export-quality to recording-overlay.swift,
and resolve the preset there via the same exportPresetName() helper used by
recording-resize.swift. Medium stays the default when the arg is absent, so
behavior is unchanged for callers that do not set it.

* feat: align recording quality and size flags

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-06-19 18:44:38 +02:00
Michał Pierzchała 87a6ac7c9a feat: bridge web provider to agent-browser (#826) 2026-06-19 16:47:01 +02:00
Michał Pierzchała 833479e01b feat: add semantic web provider seam (#825)
* feat: add semantic web provider seam

* fix: forward web provider through request router
2026-06-19 12:26:19 +02:00
Michał Pierzchała 188715b409 feat: add web platform vocabulary (#824)
* feat: add web platform vocabulary

* fix: tighten web platform admission
2026-06-19 11:56:45 +02:00
Michał Pierzchała afd413021a feat: improve maestro test reporter (#811) 2026-06-16 16:51:47 +01:00
Michał Pierzchała d29b86d7b8 fix: report maestro ios runner setup failures (#809) 2026-06-16 16:28:28 +01:00
Hanno J. Gödecke 350cd0c1cb feat(ios): support external xctest runner artifact (#806)
* feat(ios): support external xctest runner artifact

* fix(ios): pass derived data path to xctest runner

* fix(ios): reuse external xctest runner sessions
2026-06-16 15:44:41 +01:00
Michał Pierzchała a6bb86539f fix: stabilize Android Maestro replay interactions (#805)
* fix: stabilize Android Maestro replay interactions

* refactor: dedupe Maestro visible recovery

* test: relax Android Maestro snapshot count assertions

* refactor: unify Maestro recoverable interaction state
2026-06-14 15:46:26 +02:00
Michał Pierzchała 991f4c708b fix: match Maestro directory test order (#802)
* fix: match Maestro directory test order

* test: cover Maestro nested directory discovery
2026-06-13 20:48:34 +02:00
Michał Pierzchała 778349a991 fix: stabilize Android Maestro replay reliability (#799)
* docs: clarify Android snapshot helper idle waits

* fix: stabilize Android Maestro replay context
2026-06-13 19:08:57 +02:00
Michał Pierzchała 0034a7672a fix: align Maestro test discovery order (#801) 2026-06-13 13:29:24 +02:00
pupuking723 98376b93d5 fix: report snapshot timing diagnostics (#798)
* fix: report snapshot timing diagnostics

* fix: refine snapshot diagnostics reporting

* fix: preserve replay snapshot diagnostics on failure

* refactor: simplify snapshot diagnostics plumbing

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-06-13 12:17:25 +02:00
Michał Pierzchała 96534e860b fix: stream replay suite progress (#795)
* fix: stream replay suite progress

* fix: tighten replay progress cancellation
2026-06-12 20:51:40 +02:00
Michał Pierzchała 5c83fe4e50 fix: improve daemon diagnostics and remove compact snapshots (#786)
* fix: improve daemon startup diagnostics

* perf: relax query-sweep recovery budget

* fix: make compact snapshot flag a no-op

* fix: remove compact snapshot mode

* test: align tvOS remote provider expectation

* test: drop compact snapshot progress target
2026-06-12 17:32:56 +02:00
Michał Pierzchała c925563959 fix: avoid focusing booted iOS simulators (#790) 2026-06-12 14:57:58 +02:00
Michał Pierzchała fa1c1d55da refactor: localize command surface modules (#772)
* refactor: localize command surface modules

* refactor: localize command runtime modules

* refactor: tighten localized command exports

* refactor: address command localization review

* refactor: deepen batch command policy

* refactor: split provider progress model

* refactor: collapse command client facets

* refactor: run progress metrics as TypeScript

* refactor: remove obsolete command shims

* fix: update localized snapshot output import

* fix: preserve debug command localization
2026-06-12 14:02:04 +02:00
Michał Pierzchała f0d1674cdf feat: add Android native perf profiling (#757)
* feat: add Android native perf profiling

* fix: satisfy fallow for Android perf profiling

* fix: harden Android native perf lifecycle

* test: reduce native perf mock complexity

* docs: show compact native perf evidence example

* fix: align perf rebase validation

* fix: summarize Android perfetto artifacts

* fix: clean up Android native perf on session close
2026-06-12 13:12:41 +02:00
Michał Pierzchała 385c1dd655 fix(daemon): use snapshot quality for sparse handling (#779)
* fix(daemon): use snapshot quality for sparse handling

* fix(daemon): preserve sparse snapshot verdict state

* fix(daemon): route sparse verdicts through selector reads
2026-06-12 13:04:39 +02:00
Michał Pierzchała 0425df2be6 refactor: bundle snapshot capture annotations (#777)
* refactor: bundle snapshot capture annotations

* refactor: address snapshot annotations review

* test: cover snapshot annotation quality mapping
2026-06-12 12:43:13 +02:00
Michał Pierzchała fa8cce37d1 refactor(ios): snapshot capture plans with a structured quality verdict (#783)
* refactor(ios): snapshot capture plans with a structured quality verdict

Implements ADR 0004's explicit-strategies decision as architecture
(candidates 1+2 of the snapshot pipeline review):

- Snapshot backend seam: three adapters (recursive tree, query sweep,
  private AX) behind one captureWithBackend dispatch. Each strategy
  declares its chain as data (regular: tree→queries→private-ax,
  compact: queries→private-ax, raw: tree→private-ax) and one plan
  runner walks it under a 20s umbrella budget so chained recovery can
  never stack past the 30s main-thread watchdog. Terminal policy is
  per-plan: raw rethrows AX failures (diagnostics preserve errors),
  interactive fails closed with runnerFatal invalidation.
- Single quality classifier: one sparsePayloadReason predicate (with
  reason codes), one collapsed-leaf detector, replacing the three
  divergent sparse detectors (Swift structural, daemon count==1, CLI
  count<=3) that each patched a different failure shape.
- Structured snapshot quality verdict on the wire (state, backend,
  reason, reasonCode, effectiveDepth, collapsedLeafIndexes): the daemon
  and CLI render warnings from it instead of re-deriving degradation
  from node shapes; budget starvation is no longer blamed on the app's
  accessibility. Legacy runner messages and daemon-side detectors stay
  behind a verdict-absent gate for mixed-version compat.
- The verdict surfaces in --json (snapshotQuality) for agents; the
  generic sparse CLI hint is suppressed when a verdict explains it.

Threading the verdict exposed two more hand-copy field drops
(captureInteractionOutcomeAwareSnapshot, serializeSnapshotResult,
client response mapping) - now carried alongside warnings everywhere.

Verified live: Settings healthy (tree, no warnings), Settings compact
under load (recovered/private-ax/budget), production login (sparse
best-effort with honest warning), collapse fixture (healthy +
collapsedLeafIndexes -> @ref warning), Bluesky Home (recovered/
private-ax, 24 nodes in 2s). Full unit suite 2327 passed, fallow clean,
runner builds.

* fix(ios): correct recovered-snapshot viewport and private-AX scope semantics

Review follow-ups on the capture-plan refactor:

- The query-sweep synthetic root doubles as the daemon's viewport
  (find.ts prefers on-screen matches inside nodes[0].rect), but it was
  built from candidate bounds, so off-screen controls below the screen
  could inflate it and win duplicate-label resolution. The root now
  uses the real finite viewport, falling back to candidate bounds only
  when viewport capture failed.

- The private-AX backend applied --scope as a per-node text filter,
  hiding the matched container's children — diverging from regular
  snapshot scope semantics and contradicting the depth-cap hint that
  recommends scoped re-runs. Scope now selects the matched subtree:
  descendants inherit the match and only the normal option filters
  apply to them (in-bundle test covers a non-matching descendant).

Verified live on Bluesky Home: scope homeScreen returns the 52-node
subtree including non-matching descendants; compact root rect equals
the screen (0,0,402,874).

* fix(ios): fail closed on interactive AX failure, stamp fatal verdict, validate parser

Three review findings on the capture-plan terminal path:

- P1: the fail-closed guard required `best == nil`, but the query-sweep
  tier always returns a synthetic-root sparse payload that sets `best` —
  so an interactive recursive-tree AX serialization failure that no
  backend recovered returned a sparse snapshot instead of invalidating
  the cached target. Reaching the terminal already means no backend
  produced a usable tree, so the sparse `best` must not suppress the
  fail-closed path. Extracted the decision into a pure, unit-tested
  `resolveSnapshotPlanTerminal` (closes the terminal-ordering testability
  gap the architecture review flagged).
- P2: `snapshotAccessibilityUnavailable` returned a payload with no
  `snapshotQuality`, leaving one planned sparse result on the
  legacy-message path. It now carries a sparse/ax-rejected verdict like
  every other planned snapshot, so downstream sparse handling keys off
  the verdict.
- P2: `readSnapshotQualityVerdict` cast any string state/backend into the
  union, so a malformed object suppressed the legacy node-shape
  detectors. State and backend are now validated against their unions
  (unknown → verdict-absent → legacy detectors run); an unknown
  reasonCode is dropped rather than rejecting the whole verdict, so a
  forward-version runner still yields a usable verdict.

Unit-covered: Swift resolveSnapshotPlanTerminal matrix + fatal-verdict
assertion; TS parser accept/reject/forward-compat. Full suite 249 files
/ 2449 tests, fallow, lint, runner build green.
2026-06-12 12:25:40 +02:00
Michał Pierzchała cba020de21 fix: add iOS private AX snapshot fallback (#758)
* fix: add iOS private AX snapshot fallback

* fix: add public iOS snapshot query recovery

* fix(ios): make private AX snapshot fallback recover deep React Native trees

Four fixes that turn the #758 private AX fallback from
works-on-one-tree-shape into reliable on Bluesky Home:

- Depth ladder: the AX server rejects bulk snapshot requests outright
  (kAXErrorIllegalArgument) once requested depth crosses a
  tree-size-dependent limit that moves with live content. Retry at
  56/40/24/12 instead of giving up after one attempt at 64.
- Real attribute identifiers: the server silently ignored the raw
  keypath strings the bridge passed, so every node came back with a
  zero frame (breaking ref taps and the interactive/compact filters,
  which is why 'snapshot -i -c' stayed sparse). Map keypaths through
  XCElementSnapshot.axAttributesForElementSnapshotKeyPaths (it returns
  an NSSet) and drop the mapper's expensive extras (automation type,
  window display id, base type) that pushed deep requests past the 30s
  main-thread watchdog.
- Viewport from the private root frame when the public windows query
  degrades to an infinite viewport, so off-screen drawer content stops
  passing the visibility filter.
- Runner source fingerprint now includes .m/.h, so bridge edits stop
  reusing stale cached runner builds.

Also hardens the bridge per review: UInt(exactly:) for untrusted
element types, pid_t-sized objc_msgSend for process id matching, and
objCType-checked NSValue frame decoding.

* fix(ios): recover deadline-truncated near-empty compact snapshots

The all-structural sparse detector misses the common large-RN-tree case
where the typed-query sweep resolves one or two stray controls before
its 1s deadline: the payload has 'content', so recovery never fires,
yet 2 nodes is useless in practice. Treat deadline-truncated payloads
with <= 8 nodes as needing recovery, and only replace the original
payload when the recovered tree actually carries more nodes. Completed
sweeps on legitimately minimal screens stay untouched (not truncated).

* chore: fix CI for the AX snapshot fallback branch

- Sync the setup metadata script's fingerprint extension list with the
  runtime (.m/.h were added for the ObjC bridge), fixing the cache
  metadata parity test.
- Reduce find.ts complexity flagged by fallow: hoist the node fetcher
  into createFindNodeFetcher with a recoverSparseInteractiveSnapshot
  helper, split match disambiguation and resolution scoring into
  narrowMultipleMatches/resolvedTouchScore, extract rectsMatch.

* feat(ios): make accessibility fallbacks and collapsed containers visible in snapshot output

Two transparency gaps from #701's 'no silent fallback' requirement:

- Runner-attached snapshot messages now surface as snapshot warnings
  (readAppleSnapshotResult previously dropped them), so every recovery
  through the fallback accessibility backend or query tier is announced,
  states what it usually means (the app publishes an unhealthy
  accessibility tree - fixing the app is the real cure), and points to
  screenshot as visual truth.

- A leaf whose label merges many comma-joined segments is flagged as a
  collapsed accessible container: the app marks a container accessible,
  hiding every descendant from assistive tech and automation alike.
  Nothing can be recovered below it (VoiceOver sees the same merged
  element), so the warning names the node, estimates the merged label
  count, and gives the app-side fix plus the screenshot/coordinate-tap
  workaround.

Validated live on the lab stress fixture (adlab://stress?accessible=1):
the 6-node tree now carries '@e5 [Other] merges ~126 labels...'.

* fix(ios): detect sparse trees with labeled roots and surface warnings through the daemon

Validated against a real-world repro (a production React Native app's
login screen, simulator build provided privately by the reporter): a
full-screen accessibilityViewIsModal overlay leaves the public snapshot
with just Application+Window. Two gaps kept recovery off:

- The sparse detector counted the Application label (the app's display
  name) as content and the full-screen root as hittable, so the app
  name alone defeated recovery. Application/Window labels and root
  hittability say nothing about tree health and no longer count.
- Interactor-level snapshot warnings were dropped by the daemon capture
  chain (only the runtime/commands layer kept them); they now thread
  through CaptureSnapshotResult into BackendSnapshotResult.

With both fixes that login screen recovers through the public query
tier: 16 nodes with every control addressable (fill @ref + read-back
verified), and the output carries the recovery warning. Bluesky-class
trees still ladder into the private fallback unchanged.
2026-06-12 07:55:17 +02:00
Michał Pierzchała f8704f46ef feat: add Apple xctrace perf profiling (#755) 2026-06-11 16:45:29 +02:00
Michał Pierzchała a35c444df6 feat: add perf memory diagnostics (#759)
* feat: add perf memory diagnostics

* fix: address perf memory CI findings

* fix: address perf memory review feedback

* docs: explain perf memory diagnostics benefits
2026-06-11 14:58:09 +02:00
Michał Pierzchała 86941579a8 perf(ios): anchor recording gesture clock from runner response stamps (#762)
Every ok runner response now carries a transport-stamped currentUptimeMs
captured just before the HTTP write. Simulator recording start anchors
gesture overlay timing from the warm snapshot response it already makes,
skipping the standalone uptime request. The standalone uptime path stays
as fallback for older runner builds, and journal-stored responses remain
unstamped so recovered results never pair a stale uptime with a late
receipt time.

Closes #670
2026-06-11 14:23:55 +02:00
Michał Pierzchała 93f104aaaa fix: make Device Hub simulator launch opt-in (#766) 2026-06-11 13:00:53 +02:00
Michał Pierzchała ded6a167b5 fix: use full-screen reference frame for recording touch overlays (#765)
Coordinate presses during recording resolved their touch reference
frame from an interactive-only snapshot that was also captured in
compact mode. Compact pruning drops Application/Window containers, so
viewport inference fell back to a bounding box of leaf elements (e.g.
386x208 instead of the 440x956 screen) and the overlay burn-in scaled
tap markers with that garbage frame: x landed nearly right by
coincidence, y blew past the canvas and clamped to the bottom edge.

Capture the reference-frame snapshot with compact disabled so the
Application node survives and the frame is the real screen point size.

Verified on an iPhone 17 simulator (402x874pt screen, 220x480 video):
gesture telemetry now carries 402x874 and the burned-in marker lands at
(121, 329) vs expected (120.4, 329.5); previously it rendered clamped
at the bottom edge.
2026-06-11 11:42:59 +02:00
Thiago Brezinski b2e4ace12c fix: focus booted iOS simulators with Device Hub (#750)
* fix: focus booted iOS simulators with Xcode Device Hub

* fix: expose Device Hub opt-out

* test: cover Device Hub screenshot retry focus

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-06-10 20:07:08 +02:00
Michał Pierzchała c32249503b test: cover least-tested parsing modules + verify public exports reach npm; drop dead daemon barrels (#749)
* test: cover least-tested CLI grammar and config parsing modules

Add focused unit tests for the modules the coverage report flagged as
least covered, exercising real behavior rather than padding metrics:

- utils/source-value: env/config value parsing (booleans, enums, enum
  flags with setValue, int bounds, multiple) — 42% -> 100% lines
- commands/cli-grammar/gesture: CLI<->daemon gesture argument
  translation for every gesture kind incl. error paths — 41% -> 100%
- commands/cli-grammar/system: back/rotate/keyboard/clipboard/
  react-native readers and writers incl. validation — 57% -> 100%
- core/device-rotation: orientation parsing with aliases/errors — 100%
- core/dispatch-payload: push payload loading from inline JSON and
  files, with temp-file I/O and JSON/shape error handling

https://claude.ai/code/session_018i1mhcSe6sqM4mKLUKgYXe

* chore: gitignore generated coverage report directory

https://claude.ai/code/session_018i1mhcSe6sqM4mKLUKgYXe

* test: verify public exports reach the npm build; drop dead daemon barrels

Strengthen the package-exports test so it verifies the real
publish-time invariant: every package.json "exports" subpath maps to a
configured rslib build entry that points at an existing source module
which actually exposes named exports. This catches a subpath being added
to package.json without a matching build entry (which would ship a
broken import), and keeps import/types targets in lockstep.

Remove four internal re-export barrels under src/daemon that were pure
1:1 forwarders to their src/utils source, and repoint all importers at
the real module so it is obvious where the code lives:

- daemon/is-predicates.ts      -> utils/selector-is-predicates.ts
- daemon/selectors-build.ts    -> utils/selector-build.ts
- daemon/snapshot-diff.ts      -> utils/snapshot-diff.ts
- daemon/snapshot-processing.ts -> utils/snapshot-processing.ts

The curated daemon/selectors.ts facade is kept; only blind forwarders
were removed. No behavior change.

https://claude.ai/code/session_018i1mhcSe6sqM4mKLUKgYXe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 16:28:01 +02:00
Michał Pierzchała 963ffc259c refactor: move daemon-shared contracts out of commands (#741) 2026-06-10 14:28:24 +02:00
Michał Pierzchała b30292d867 refactor(types): deepen type consolidation (#743)
* refactor(types): deepen type consolidation

* fix: satisfy fallow type refactor audit
2026-06-10 14:05:39 +02:00
Michał Pierzchała b8172e3da3 perf(daemon): offload PNG decode/encode and screenshot diff to a worker thread (#734)
* perf(daemon): offload PNG decode/encode and screenshot pixel diff to a worker thread

PNG decode (inflateSync) and per-pixel screenshot diffing previously ran
synchronously on the daemon event loop, stalling all concurrent sessions
while multi-MB screenshots were processed.

- add src/utils/png-worker.ts worker_threads entry (rslib internal/png-worker)
  handling one decode, encode, or diff-pixels job per message
- add src/utils/png-worker-client.ts async wrappers (decodePngAsync,
  encodePngAsync, computeScreenshotDiffPixelsAsync) that lazily spawn the
  worker, resolve it next to the current module in dev (.ts) and dist (.js)
  like the companion tunnel entry, and fall back to the in-process
  synchronous path when the worker is unavailable
- extract the unchanged pixel-compare loop into
  src/utils/screenshot-diff-pixels.ts so both paths share identical logic
- route daemon call sites (screenshot-overlay annotate, compareScreenshots)
  through the async wrappers; results stay byte-identical

https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2

* refactor(daemon): apply review findings to PNG worker offload

- guard postMessage failures: a job-specific send error (e.g. DataCloneError)
  now cleans up its pending entry and falls back to sync for that call only
- resolve the worker entry via a shared src/utils/internal-entry.ts helper
  that returns null on any resolution failure (non-file import.meta.url)
- match repo precedent: spawn the worker with --experimental-strip-types
  when the resolved entry is a .ts module
- report permanent worker degradation once (scoped diagnostic + process
  warning with the failure reason) instead of silently going sync-only
- daemon lifecycle: pre-warm the worker at startup and terminate it during
  shutdown with a 1s best-effort timeout (daemon-only)
- collapse the three async wrappers onto one kind-typed job runner with a
  single unavailability channel (rejection, no null path) and drop the dead
  mismatched-result guards
- derive the diff-pixels contract types from screenshot-diff-pixels.ts and
  share toBuffer via the contract module
- serialize worker errors with normalizeError and reconstruct AppError
  (code/message/details) client-side; the worker reuses decodePng so decode
  failures are identical on both paths
- transfer result buffers back to the client when a view fully owns its
  ArrayBuffer; clone pooled buffers to protect Node's shared buffer pool
- decode baseline/current screenshots concurrently in compareScreenshots
- move resizePngFileToMaxSize to src/utils/png-resize.ts and route its
  decode/encode through the worker (daemon screenshot --max-size path)

https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2

* fix(fallow): declare png worker entry and simplify resultTransferList

The Fallow audit flagged src/utils/png-worker.ts as unreachable from any
entry point and resultTransferList as over the complexity threshold.

- Declare src/utils/png-worker.ts in .fallowrc.json's entry list: it is a
  worker_threads entry point loaded at runtime (mirrors src/daemon.ts /
  src/companion-tunnel.ts and the internal/png-worker rslib entry).
- Flatten resultTransferList into filter/map over an extracted
  ownsEntireArrayBuffer predicate, preserving the exact transfer rule
  (byteOffset === 0, byteLength === owner.byteLength, real ArrayBuffer).
- Add direct unit coverage asserting fully-owned buffers are transferred
  while pooled/offset views are not.

https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 12:27:44 +02:00
Michał Pierzchała 4f95ca8881 fix(daemon): timing-safe token comparison, daemon.json hardening, shell-quote CVE (#731)
* fix(daemon): timing-safe token comparison and daemon.json permission hardening

Use crypto.timingSafeEqual (via SHA-256 digests, length-independent) for the
three daemon token checks, and chmod daemon.json to 0600 after writes since
writeFileSync only applies mode on creation.

https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2

* fix(deps): clear CVE-2026-9277 by overriding shell-quote to >=1.8.4 in test-app

Override added to examples/test-app/pnpm-workspace.yaml (package.json-level
overrides are silently ignored for this nested app, see the comment there).
Lockfile change is limited to shell-quote 1.8.3 -> 1.8.4; pnpm audit is clean.

https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 11:00:19 +02:00
Michał Pierzchała ab2026816f refactor(types): tuple ownership, LogBackend rename, string-enum helpers + tail dedupes (#720)
* refactor(types): rename NetworkLogBackend -> LogBackend

The union is reused for both network-log and app-log backends, so the
network-specific name was misleading. Also renames the resolveNetworkLogBackend
helper to resolveLogBackend. No behavior change.

* refactor(types): add string-enum helpers (isStringMember/parseStringMember)

Small utils/string-enum.ts helpers for the now-common 'as const tuple' string
enums: a membership guard and a strict exact-match parser. Adopt at the strict
sites (parseSessionSurface, parseScrollDirection, parseSwipePreset, isPerfArea,
isPerfAction), replacing hand-rolled switch/includes boilerplate. Alias-accepting
parsers (device rotation) keep their custom logic. Error messages unchanged.

Chose focused helpers over a defineStringEnum factory: the modules are already
concise with tuple-as-source, and a factory would force export-name churn for no
gain.

* refactor(types): dedupe runner/interaction tail shapes

- RunnerXctestrunCacheKind / RunnerXctestrunArtifactState shared between
  RunnerXctestrunArtifact and AppleRunnerPrepareResult.
- Annotate buildInteractionSurfaceSignature with its existing
  InteractionSurfaceSignature alias.
- RunnerOpts / runnerOptionsFromContext use Pick<RunnerContext, ...>.

(Skipped daemon-invoke-fn: maestro already has its own MaestroRuntimeInvoke
alias, so forcing a shared DaemonInvoke creates naming inconsistency for ~2 lines.)

* refactor(types): dedupe remaining tail shapes

- ElementSelectorTarget (core/interactor-types.ts) shared by
  DirectIosSelectorTarget and ElementSelectorTapOptions (= Omit<...,'raw'>).
- Collapse client-types ClientCommandBaseOptions into the identical exported
  DeviceCommandBaseOptions; merge the two byte-identical snapshot-pick aliases.
- File-local SelectorRuntimeError and LineWriter for repeated inline shapes.
- Export mcp ToolResult and use it for the router's textToolResult return.

Pure refactor, type-only.

* address review: adopt helpers, fix alias direction, tidy tail dedupes

- Adopt parseStringMember/isStringMember at parseGestureDirection and the
  swipe-pattern guard in dispatch-interactions.ts (same module the PR touched;
  byte-identical error/behavior).
- Extract RunnerCallOptions = Pick<RunnerContext,...> in interactor-types.ts;
  reuse in dispatch-interactions + ios/interactions (was written twice).
- Invert the ElementSelector alias direction: ElementSelectorTapOptions stays the
  plain core type; DirectIosSelectorTarget = ElementSelectorTapOptions & { raw }
  (keeps the daemon-only raw field in the daemon module).
- Drop redundant cleanStaleBundles in AppleRunnerLifecycleOptions (already in
  AppleRunnerCommandOptions); drop the now-unnecessary cast in isStringMember.
- callTool returns Promise<ToolResult>; rename SelectorRuntimeError ->
  DirectIosSelectorErrorResult (matches the DirectIosSelector* family).
- Dedupe the log-backend device mapping: export resolveLogBackend and reuse it
  in resolveSessionLogBackendLabel.

Pure refactor; typecheck/lint green, affected tests pass.
2026-06-10 10:55:37 +02:00
Michał Pierzchała 0f7187f543 fix: scope source daemon state by worktree (#719)
* fix: scope source daemon state by worktree

* docs: clarify worktree daemon state tradeoffs

* ci: harden Apple runner cache

* chore: keep daemon state helper internal

* ci: validate Apple runner cache restores

* ci: simplify Apple runner cache setup
2026-06-10 10:32:58 +02:00
Michał Pierzchała 22fba8718c feat: add shutdown command (#718)
* feat: add shutdown command

* fix: address shutdown review feedback

* fix: reject active session shutdown targets

* fix: preserve shutdown failure details

* fix: satisfy shutdown fallow audit

* fix: simplify shutdown session handling
2026-06-10 10:24:42 +02:00
Michał Pierzchała 1de7e73e2a refactor(types): consolidate duplicated types across src/ into single sources of truth (#717)
* refactor(types): reuse canonical string-unions across the codebase

Replace inline duplicates of SessionSurface, ClickButton, DeviceRotation,
BackMode, ScrollDirection, Platform/PlatformSelector/DeviceTarget,
AlertAction, and LeaseBackend with imports of the existing canonical types.
Extract BackMode into a core/back-mode.ts leaf (matching click-button.ts /
session-surface.ts) to keep backend.ts dependency-clean. Add 'satisfies
readonly T[]' drift-guards to the runtime enum tuples consumed by enumField.

Pure refactor: single source of truth for these unions, no behavior change.

* refactor(types): consolidate gesture + daemon/network/metro mode unions

- Add SwipePattern to core/scroll-gesture.ts; reuse SwipePreset,
  ScrollInputDirection, ScrollDirection at remaining inline sites.
- Move DaemonServerMode / DaemonTransportPreference / SessionIsolationMode /
  NetworkIncludeMode to contracts.ts (the client<->daemon boundary); daemon/
  config.ts and daemon/network-log.ts re-export them. Drop client-types.ts's
  three private alias copies.
- Reuse MetroPrepareKind in metro.ts and remote-config-schema.ts.
- Add satisfies-guards to the runtime enum tuples.

Pure refactor, type-only changes.

* refactor(types): single-source remaining string-literal unions

- ElementSelectorKey (core/interactor-types.ts) for the 'id|label|text|value'
  selector-key subset across core/daemon/ios/maestro.
- GESTURE_KINDS + GestureKind (command-catalog.ts), dropping the duplicate
  GESTURE_KIND_VALUES.
- AndroidTextInputAction (android/adb-executor.ts); reuse NetworkLogBackend for
  the app-log backend union.
- Reuse contracts.ts JsonRpcId / JsonRpcRequestEnvelope in mcp router + server.
- New commands/log-command-contract.ts (LOG_ACTION_VALUES + LogAction),
  mirroring perf-command-contract.ts.
- Named Android snapshot helper metadata unions (transport/captureMode/install
  reason) shared between helper + backend metadata.

Pure refactor, type-only changes; full unit suite green.

* refactor(types): consolidate geometry shapes

- Reuse canonical Rect (utils/snapshot.ts) for inline {x,y,width,height}
  literals (parsing, output, screenshot-diff regions/ocr, atspi-bridge).
- GestureReferenceFrame (core/scroll-gesture.ts) as the single reference-frame
  type; TouchReferenceFrame becomes an alias; replace inline
  {referenceWidth,referenceHeight} across daemon + maestro + commands.
- New ImageDimensions (screenshot-geometry.ts) and MovementRange
  (screenshot-diff-ocr.ts) for repeated {width,height} / {min,max} shapes.
- File-local AndroidRecordingSize in record-trace-android.ts.

Pure refactor, type-only changes. Point literals with normalized-vs-absolute
semantics deliberately left untouched.

* refactor(types): consolidate result/options/resolver shapes (+oxfmt)

- Generic PlatformProviderResolver<T> collapses 6 near-identical resolver
  types in request-platform-providers.ts.
- Reuse canonical DaemonError (contracts.ts) / NormalizedError (utils/errors.ts)
  for the inline error DTOs in daemon/types.ts and utils/output.ts.
- Single DaemonFailureResponse: FailedDaemonResponse becomes an alias and the
  maestro-local redefinition is dropped.
- Extract one shared toBackendResult() into commands/runtime-types.ts, deleting
  5 duplicate copies.
- Normalize whitespace with oxfmt across the touched files.

Full unit suite green.

* refactor(types): share TransformGestureParams + RepeatedInput bundles

- TransformGestureParams (core/scroll-gesture.ts) replaces 4 identical inline
  transform-gesture param shapes (core interactor, dispatch, android multitouch,
  client options).
- Reuse RepeatedInput (commands/command-input.ts) for the tap-modifier bundle in
  PressCommandOptions and BackendTapOptions.

Pure refactor, type-only.

* refactor(types): extract BackendResultEnvelope mix-in

Replace the repeated inline { backendResult?: Record<string, unknown>;
message?: string } pair on ~17 single-object command result types with
'& BackendResultEnvelope' (commands/runtime-types.ts). Discriminated-union
variants and single-field result types are intentionally left inline.

Pure refactor, type-only changes; failing files re-verified green in isolation
(full-suite failures were flaky timeouts).

* refactor(types): dedupe runner/replay/selector/exec result shapes

- RunnerSessionOptions = AppleRunnerLifecycleOptions (field-identical).
- Reuse ReplayActionBlockInvoker for the maestro + daemon replay invoker types.
- SelectorSnapshotOptions aliases the canonical SelectorSnapshotInput; annotate
  selectorSnapshotOptionsFromFlags.
- Reuse ExecResult for the { stdout; stderr; exitCode } subset in
  record-trace-errors and app-log-process.

Pure refactor, type-only.

* refactor(types): make canonical tuples the single source for enum unions

Each string-enum module now exports an 'as const' literal tuple as the single
source and derives its union via (typeof TUPLE)[number]: SESSION_SURFACES,
CLICK_BUTTONS, DEVICE_ROTATIONS, BACK_MODES, SCROLL_DIRECTIONS/SWIPE_PRESETS/
SWIPE_PATTERNS, SCROLL_INPUT_DIRECTIONS, ALERT_ACTIONS, DEVICE_TARGETS. The
command metadata/input files import these tuples instead of redefining local
*_VALUES copies, which also retires 10 now-redundant 'satisfies' drift-guards.

Member order preserved everywhere (enumField error messages unchanged). Platform
tuples intentionally left as-is (command-input vs contracts use different orders).

Net -39 lines; tuple+type dual maintenance eliminated for these unions.
2026-06-09 19:13:55 +02:00
Michał Pierzchała 500f4f3045 refactor: deepen replay test attempt module (#715) 2026-06-09 15:07:30 +02:00
Michał Pierzchała 39e4682592 refactor: deepen runner disposal (#714)
* refactor: deepen runner disposal

* docs: clarify PR descriptions
2026-06-09 15:07:16 +02:00
Michał Pierzchała 49e59d651a feat: record replay test videos (#712)
* feat: record replay test videos

* fix: align replay test video timing

* fix: leave replay test video tail visible

* refactor: clarify replay test video lifecycle

* refactor: pass replay video attempt context directly

* fix: warn on replay video finalization failure
2026-06-09 14:17:32 +02:00
Michał Pierzchała c2b29d5600 fix: stabilize Maestro replay on iOS (#713)
* fix: stabilize Maestro replay on iOS

* fix: scope iOS runner cleanup to daemon owner

* fix: lease iOS runner ownership per device

* fix: release prepared iOS runner daemon in CI

* fix: inline runner lease release cleanup
2026-06-09 14:12:57 +02:00
Michał Pierzchała 35f54a863c fix: resolve Maestro taps from regular snapshots (#709)
* fix: resolve Maestro taps from regular snapshots

* fix: tighten Maestro snapshot compatibility

* test: align Maestro snapshot freshness fixtures
2026-06-08 18:12:09 +02:00
Michał Pierzchała 2014cb6873 fix: harden covered snapshot targets (#708)
* fix: block covered snapshot targets

* fix: harden covered snapshot targets
2026-06-08 14:08:52 +02:00
Michał Pierzchała c89719f7ff fix: decode escaped selector values (#711) 2026-06-08 12:58:18 +02:00
Michał Pierzchała 31ce5903a2 feat: add replay test sharding (#707)
* feat: add replay test sharding

* refactor: simplify sharding device resolution

* fix: address sharding review feedback
2026-06-08 10:56:42 +02:00
Michał Pierzchała 5c083eacc6 fix: harden iOS replay runner prewarm (#705)
* fix: harden iOS replay runner prewarm

* fix: avoid stale iOS runner during relaunch

* fix: stop stale iOS runner processes

* fix: clean stale iOS runners before startup
2026-06-07 20:37:23 +02:00
Michał Pierzchała 81448c8f97 feat: add perf metrics and frames commands (#703)
* feat: add perf metrics and frames commands

* fix: tighten perf command ergonomics

* test: move perf area coverage to provider integration
2026-06-07 20:36:57 +02:00
Michał Pierzchała 86971990cb fix: scope runner diagnostics to sessions (#704) 2026-06-07 11:35:38 +02:00