Skip to content

feat: unify gesture planning and multi-touch execution#1212

Open
thymikee wants to merge 22 commits into
mainfrom
agent/unified-gesture-architecture
Open

feat: unify gesture planning and multi-touch execution#1212
thymikee wants to merge 22 commits into
mainfrom
agent/unified-gesture-architecture

Conversation

@thymikee

@thymikee thymikee commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

This replaces parallel gesture implementations with one compositional architecture shared by CLI, Node.js, and MCP:

  • gesture-normalization.ts is the explicit public compatibility boundary; aliases normalize once into semantic intent.
  • gesture-plan.ts separates contact topology from motion and creates complete synchronized trajectories from a fresh active-app viewport.
  • Android, Apple, and WebDriver adapters consume the canonical performGesture(plan) primitive and no longer derive their own geometry.
  • Pinch, rotate, two-finger pan, and transform share the two-contact planner while retaining intent. Translation, scale, and rotation progress atomically.
  • Every sample is validated before injection. Impossible trajectories return structured GESTURE_TRAJECTORY_OUT_OF_BOUNDS details rather than being clamped.
  • Scroll remains separate because it owns edge traversal, content verification, and fallback policy.

ADR 0013 records the architecture and platform-specific cadence/geometry profiles.

Removed legacy paths

  • Deleted the public alias -> positional strings -> reparsing route.
  • Kept .ad positional syntax behind one named replay compatibility codec.
  • Replaced per-gesture Interactor methods with performGesture(plan) composition.
  • Deleted scalar Apple/Android multi-touch geometry and request layers while retaining the proven XCTest and instrumentation injectors.
  • Removed scalar runner gesture fields, the dead dragSemantics wire route, per-alias runtime wrappers, dead dispatch resolution, unused wire/type aliases, and the ignored Android helper intent field. The proven iOS fast-swipe pointer profile remains a private runner primitive selected from canonical plan intent.
  • Repaired gesture recording after generic gesture became the command identity; pan/fling/pinch/rotate/transform now produce canonical visualization events, and multi-touch centroid travel cannot be mislabeled as a one-finger back swipe.
  • Centralized Apple multi-touch admission and adapter defense in one shared policy.
  • Restored bounded repeated-swipe inputs and added a 60-second aggregate schedule limit at metadata and daemon trust boundaries.
  • Android two-contact gestures never fall back to adb input swipe; Drop automatic adb input swipe fallback from Android gesture swipes #690 continues to own removal of the existing one-contact fallback.

Public compatibility

  • One-finger pan remains the default; pointerCount: 2 / --pointer-count 2 opts into two fingers.
  • Pinch, rotate, and full-form transform remain available.
  • Swipe is a fixed quick fling. Historical swipe/fling duration inputs normalize to timed pan and emit a deprecation.
  • Pinch fixes translation/rotation at zero; rotate fixes translation at zero and scale at one.
  • .ad recordings keep their positional syntax and command identity.
  • Accidental alias response-shape differences are unified as an accepted breaking change. Session, stabilization, recording/replay, and diagnostic semantics remain unchanged.
  • Unsupported Apple families/device kinds remain capability-gated.

Reconciliation with #1207

Rebased after #1207 merged and retained its intended behavior:

  • authored Maestro coordinate start/end swipes still take precedence over directional presets;
  • Android helper install identity, ownership checks, and snapshot-helper shutdown ordering are preserved;
  • percentage Maestro swipes resolve against a fresh active-app frame, while authored coordinates remain unchanged;
  • the unified gesture planner remains the only gesture geometry source of truth.

Size and tightening

  • Source diff: 5,999 additions / 4,520 deletions (net +1,479), including native schedule tests, public contracts, docs, and the deterministic example surface.
  • Built file count remains 130.
  • Final-head CI size report: JS +5.3 kB raw / +2.4 kB gzip; npm tarball +2.4 kB; npm unpacked -107 B. CLI-help startup is +2.1 ms on 7-run medians; an earlier +12.3 ms outlier did not reproduce.
  • The internal daemon chunk is 4.2 kB smaller raw / 1.3 kB smaller gzip than main.
  • GesturePlan is not accidentally exported from the public package declaration surface.

Validation

Head 769b69a43 is rebased on e61edf753 and closes the final Linux swipe, iOS execution-profile, response-shape, rotate-velocity, multi-touch recording-semantic, and Apple runner lifecycle findings. An executable iOS XCTest forces private fling synthesis to fail, then proves the structured gesture command performs the real XCTest coordinate-drag fallback with canonical diagnostics. Missing-appBundleId dispatch also exercises the complete four-field target-cache invalidation path. These focused iOS regressions now execute in CI instead of relying on source scanning.

Local validation:

  • complete unit projects: 421 files / 3,827 assertions passed;
  • full coverage: 466 files / 4,036 tests passed, 86.18% statements / 76.50% branches;
  • provider integration: 33 files / 121 tests passed;
  • cross-platform Node integration: 15 passed, 6 expected live-target skips;
  • Android helper packaging and Java pointer event schedule tests;
  • iOS and macOS XCTest runner builds;
  • CI-equivalent Swift runner unit compile, including target-process cache refresh coverage;
  • focused CLI, Node.js, MCP, daemon, runtime, recording, geometry, capability, and compatibility contracts.

Live device evidence

Target Backend proof Result
Android API 36, Pixel 9 Pro XL emulator android-helper 0.19.3, freshly built; androidSnapshot.backend=android-helper Exact one-vs-two pointer recognition, fling left/right, pinch in/out, rotation +/-35 degrees, translation-only transform, zero-translation scale+rotate, and combined positive/negative transforms at 120-1200 ms passed with rendered metrics and screenshots. A stronger 0.5 scale-in was used after 0.75 did not cross the app recognizer threshold.
Android API 37 emulator android-helper 0.19.3 Full gesture-lab-android.ad replay passed earlier on this PR head lineage.
iOS 26.2, iPhone 17 Pro simulator XCTest exact-pointer executor Current full gesture-lab.ad replay passed in 31.6s. Expanded matrix covered pointer count, both fling directions, pinch 1.5/0.6, rotation +/-45 degrees, translation-only, zero-translation, and combined positive/negative transforms at 100/2000 ms with rendered metrics and screenshots. Upstream pager basic passed in 17.6s; a duration-less fast-swipe canary passed in 4.98s; landscape-left fast swipe changed observable state from page 0 to page 1.
Physical iPhone, iOS XCTest single-pointer executor One-finger pan returned pointerCount: 1 and moved Settings to Screen Time; fling returned pointerCount: 1 and moved onward to Apps. Physical single-pointer capability admission is confirmed.

Apple runner relaunch canary: sequential pager_basic_example.yaml and tab_view_inside_scroll_view_example.yaml flows passed 2/2 on iPhone 16 / iOS 26.2 in 45.3s. The second launchApp completed in 2.2s without the stale-process 120-second snapshot penalty; all sessions were closed.

Representative observed values:

  • Android pinch-out: x 0, y 0, scale 1.21, rotate 0; rotate -35: rotate -24.
  • Android combined positive: x 15, y 10, scale 1.10, rotate 13; strong negative: x -18, y -12, scale 0.67, rotate -24.
  • iOS pinch-out: scale 1.39; rotate -45: rotate -37.
  • iOS combined positive: x 20, y -20, scale 1.15, rotate 15; negative long: x -8, y 8, scale 0.90, rotate -10.

The Gesture Lab's main transform pan recognizer intentionally requires exactly two pointers. Therefore live one-finger topology is asserted by proving it does not activate the exact-two-pointer recognizer, while the planner/contract suite asserts its one-pointer trajectory. On iOS, a one-finger pan over the main transform target can satisfy the adjacent fling recognizer; this is recorded as a fixture limitation rather than mislabeled as pan-metric evidence.

The current Android expanded matrix was executed manually at the replay-defined coordinates because a test-suite relaunch returned the development client to its launcher/top-scroll state. Every canonical assertion and the additional negative/short/long cases passed individually. Earlier uninterrupted API 36 and API 37 replays remain recorded above; current iOS replay passed uninterrupted.

Limrun live verification is not claimed: the WIP cloud path requires uploading the freshly built helper APK, and that artifact upload was unavailable in the verification environment. The installable-helper route was inspected structurally, but that is not presented as device evidence.

Suggested review order

  1. Public compatibility rules in gesture-normalization.ts.
  2. Topology, geometry, cadence, and bounds invariants in gesture-plan.ts.
  3. Runtime composition and fresh viewport resolution.
  4. Android, Apple, and WebDriver adapters consuming exact planned samples.
  5. Public-surface contracts and deterministic example-app recognizers.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.7 MB 1.7 MB +5.3 kB
JS gzip 536.7 kB 539.1 kB +2.4 kB
npm tarball 646.3 kB 649.6 kB +3.3 kB
npm unpacked 2.3 MB 2.3 MB +4.9 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.3 ms 26.4 ms +0.1 ms
CLI --help 56.6 ms 57.2 ms +0.6 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/interaction.js +19.7 kB +6.2 kB
dist/src/runtime.js +6.7 kB +2.2 kB
dist/src/tv-remote.js +5.4 kB +1.3 kB
dist/src/internal/daemon.js -4.0 kB -1.2 kB
dist/src/gestures.js -3.2 kB -822 B

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-1212/

Built to branch gh-pages at 2026-07-12 14:54 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@thymikee thymikee force-pushed the agent/unified-gesture-architecture branch 2 times, most recently from d8657ed to 4d7321b Compare July 11, 2026 17:14
@thymikee

thymikee commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Two blockers remain on 4d7321ba after the routing/device-evidence re-review:

  1. Public swipe response compatibility regressed. dispatchSwipeViaRuntime now returns canonical kind/durationMs/pointerCount/from/to fields but drops the established x1/y1/x2/y2/effectiveDurationMs/timingMode fields. For ping-pong repetitions, it also reports the final reversed leg as from/to because runSwipeRepetitions overwrites the result each iteration. Keep the requested endpoints and established fields (canonical fields can be additive), and add a regression covering the old keys plus --pattern ping-pong --count 2.

  2. Generic gesture recording loses touch overlays. dispatchGestureViaRuntime finalizes with actionCommand: 'gesture', but recording-gestures.ts has no gesture event builder, so gesture pan / gesture pinch produce no visualization events. Route recording through the canonical subcommand (or add a delegating gesture builder) and add recording regressions for both one- and two-contact gestures.

The prior Android geometry/pinch-axis/capability/Android-TV findings are resolved, and the production adapters consume the canonical plan as ADR 0013 requires. No fixer was started.

@thymikee

thymikee commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

I traced the unified production route and found several regressions that are not Maestro-specific:

1. iOS plan execution drops the established swipe profile and synthesis policy

plannedGesture sends every iOS plan through generic synthesizeGesture. For one-pointer swipes this bypasses the existing dedicated synthesizeSwipe profile (100 ms movement plus endpoint hold, explicitly required for nested UIKit recognizers), as well as the screenshot-based synthesized coordinate context, keyboard policy, and XCTest fallback policy used by executeDragGesture.

This is observable outside the compat parser: any public coordinate swipe now takes the changed executor. On iOS 26.2, the react-native-pager-view suite passes only 1/9 flows on this head; the first 90% -> 10% swipe reports success but does not produce the expected page transition. The same suite passes 9/9 on Android.

The unified plan should retain backend execution profiles: fling/swipe should use the proven swipe timing, pan should use continuous sampled movement, and both should retain the existing synthesized-coordinate context/policy. Two-pointer plans can continue consuming synchronized trajectories.

Relevant paths:

  • apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift:1236
  • apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m:130
  • src/platforms/apple/interactions.ts

2. Explicit coordinate gestures now require accessibility snapshots

gestureCommand always resolves a viewport. Local Android/iOS interactors do not provide gestureViewport, so even a gesture with complete absolute endpoints falls through to captureInteractionSnapshot. This makes the previously AX-free coordinate path fail when accessibility serialization is unhealthy or cannot yield a usable frame.

It also adds a fresh snapshot before every repeated swipe. On Android this introduces snapshot-helper/gesture-helper churn; on iOS it adds runner work. A platform viewport should come from an AX-independent source such as the screenshot/device frame, and repetitions should reuse the validated plan/frame where the viewport cannot change within the command.

Relevant paths:

  • src/commands/interaction/runtime/gesture-command.ts:47
  • src/commands/interaction/runtime/gesture-command.ts:90
  • src/daemon/handlers/interaction-gesture.ts:218

3. A duration-less public swipe is now unsupported on Linux

normalizePublicSwipeMotion converts omitted-duration swipe to fling, then requireGestureSupported rejects fling on Linux. Before this PR, coordinate swipe called swipeLinux with the default duration. Normal swipe from -> to therefore regresses unless callers add a duration.

Relevant paths:

  • src/contracts/gesture-normalization.ts:161
  • src/core/capabilities.ts:153

4. Repeated-swipe safety bounds were removed

The old implementation enforced count <= 200 and pauseMs <= 10_000. The replacement validates only minimums, allowing an arbitrarily large sequential request to pin the daemon. Please restore the existing bounds at the daemon trust boundary.

Relevant path: src/daemon/handlers/interaction-gesture.ts:179

5. Legacy rotate velocity compatibility changed incorrectly

Historical rotate velocity accepted finite non-zero values, including negative/fractional input. The new compatibility parser requires a non-negative integer, rejecting previously valid values while accepting zero, which used to be invalid. Even if velocity is deprecated and ignored by planning, the compatibility boundary should accept the historical domain and report the deprecation.

Relevant path: src/contracts/gesture-input.ts:107

I am keeping the separate Maestro native-wait false-positive in the stacked compat work because that predates and is specific to the compat assertion strategy.

Copy link
Copy Markdown
Member Author

Architecture review — solution quality + legacy cleanup

Reviewed the whole unified-gesture path against ADR 0013, focusing on the design rather than line-nits. Short version: the architecture is the right one and the refactor is unusually clean — but there's one structural gap that is the common root of the regressions already flagged, plus a small amount of dead machinery left behind.

The design is good

The normalize → plan → performGesture(plan) pipeline is a genuine improvement:

  • gesture-normalization.ts as the single compat boundary is the correct place to isolate deprecated/alias handling, and it keeps the private daemon wire free to evolve.
  • Separating contact topology from motion in gesture-plan.ts (single vs. two-contact, intent carried on the plan) is the right abstraction, and validating every sample against a freshly-resolved viewport with a structured GESTURE_TRAJECTORY_OUT_OF_BOUNDS instead of silently clamping is a real correctness win.
  • Collapsing the per-alias Interactor/backend methods into one performGesture(plan) primitive, with adapters injecting exact planned samples (Android helper serializes samples, Apple forwards gesturePlan), removes a lot of duplicated geometry. I verified the deletions the ADR claims — the scalar multi-touch executors, the alias→positionals→reparse route, the capability/plugin wiring, the drag-series predicate, and the stale Maestro reference-frame cache are all actually gone. That's a lot of surface removed cleanly.

The one structural concern: the plan is geometry-complete but execution-profile-lossy

The plan faithfully captures where the pointers go, but performGesture treats every one-pointer plan as a generic gesture and drops the per-intent backend execution profile that the old specialized paths carried. On iOS, performGestureApple routes all single-pointer plans (fling/swipe/pan alike) through command: 'gesture' / gesturePlan — bypassing the dedicated synthesizeSwipe timing profile (movement + endpoint hold that nested UIKit recognizers rely on), the synthesized-coordinate context, and the keyboard/XCTest fallback policy.

This is exactly the class of regression already documented in the two review comments above (iOS pager-view 1/9, coordinate swipe now requiring AX snapshots, duration-less swipe unsupported on Linux, removed repeat-swipe bounds, swipe response-shape drop, lost recording overlays). They aren't independent bugs — they're all the unified executor discarding behavior the specialized executors used to own. The ADR itself says "fling/swipe should use the proven swipe timing, pan should use continuous sampled movement" — the plan should carry that execution intent to the adapter (it already carries intent), and each adapter should branch on it, rather than lowering everything to one generic path.

Smoking gun tying this to leftover code: runner-contract.ts still declares SynthesizedDragSemantics = 'swipe' | 'pan' | 'fling' and two dragSemantics? wire fields (lines 12, 88, 115) — this was literally the field that preserved a public gesture's timing semantics after lowering to runner drag. This PR removed its only producer (iosDragCommand) and never replaced it, so the mechanism for "keep the swipe profile" is now dead code rather than wired through. git grep dragSemantics: returns zero producers; the Swift decode branch is unreachable. That orphan is both a clear legacy leftover to delete and a signal that the profile-preservation the ADR promises isn't actually implemented on the new path.

Legacy leftovers

  1. SynthesizedDragSemantics + dragSemantics fields (runner-contract.ts:12,88,115, plus the matching Swift model fields) — dead, no TS producer or consumer. Delete, or (preferably) re-wire it to fix the profile-loss above. This is the same retired-synthesized-drag machinery that runner-command-traits.ts:30 notes was removed for tapSeries/dragSeries; dragSemantics was just missed.

  2. Per-alias runtime command wrappers (gesture-command.ts:68-88 panCommand/flingCommand/rotateGestureCommand/transformGestureCommand, pinchCommand, + their registrations/types in runtime/index.ts). Production now only reaches interactions.gesture(...); these thin wrappers have no non-test callers via the bound runtime. They're only justified if commands.interactions.{pan,fling,…} is a supported embedded programmatic API — worth a deliberate keep/remove decision, since carrying five per-alias command objects sits in tension with the ADR's "one primitive, no method per alias" goal.

Everything else I spot-checked as intentionally retained (webdriver swipe used by scroll, the Android single-contact adb input swipe fallback owned by #690, .ad positional compat via the one adapter) is fine.

Bottom line

Architecturally this is a keeper — merge-worthy direction. The blocker before it ships is making the executor preserve per-intent profiles (fling/swipe vs. pan) on each adapter instead of flattening to one generic path; that single fix, plus restoring the AX-free coordinate path / Linux duration-less swipe / repeat-swipe bounds / response fields already noted, resolves the behavioral regressions. Then delete the dragSemantics orphan and settle the per-alias wrapper question.


Generated by Claude Code

@thymikee

Copy link
Copy Markdown
Member Author

Follow-up: I implemented and live-verified the cross-cutting fixes from the review above.

Cherry-pick: 7142eab45 from fix/pr1212-gesture-regressions

Commit: 7142eab45

What it fixes

  • Adds an explicit internal single-pointer executionProfile, so duration-bearing compatibility swipes retain the established fast swipe plus endpoint-hold executor while genuine timed pans remain continuous. No duration heuristic is used.
  • Apple still accepts one unified GesturePlan: single-pointer plans lower through the existing screenshot-frame, keyboard, and fallback-aware drag executor; two-pointer pan/pinch/rotate/transform continue using exact synchronized trajectories.
  • Android resolves current oriented display geometry through cmd display get-displays, avoiding accessibility snapshot/helper churn. Probe errors and unusable OEM output fall back to the existing snapshot viewport rather than failing gestures.
  • Restores count <= 200 and pauseMs <= 10_000 consistently in CLI metadata and the daemon trust boundary.
  • Updates ADR 0013 to match the actual executor split.

Validation

  • Focused Vitest: 167/167 passed.
  • pnpm check:quick, pnpm build, pnpm build:xcuitest, formatting, diff check, and direct fallow audit passed.
  • Android API 36 Pixel_9_Pro_XL: snapshot backend android-helper version 0.19.3; all 9/9 pager-view flows passed in 243.9s. pager_basic took 13.8s.
  • iOS 26.2: pager_basic passed independently on iPhone 16 and iPhone 17 Pro after the execution-profile fix.
  • Non-RTL iOS worker run: 5/7 passed. The two residual failures are reproduced by native Maestro on the same app/device: on_page_selected_example fails before any gesture because its initial alert never appears, and pager_vertical_basic_example fails after the next-page button.
  • Additional native Maestro A/B confirms nested_pagerView_example fails its second swipe and the RTL basic flow fails on the Android-specific Navigate up selector. Those current upstream flows cannot provide a 9/9 iOS oracle.

The separate iOS native-wait exact-verification change remains only in stacked Maestro PR #1217.

@thymikee

Copy link
Copy Markdown
Member Author

Four blockers remain on clean head 6053d87e; the final cleanup commits did not preserve all reviewed fixes:

  1. High: duration-less coordinate swipe is still rejected on Linux. normalizePublicSwipeMotion maps it to fling, then requireGestureSupported rejects flings on Linux before the Linux drag executor runs. The new Linux regression supplies durationMs: 16, so it misses the broken public default. Add a duration-less Linux swipe regression and preserve the established default swipe behavior.

  2. High: the final cleanup removed the repaired iOS swipe execution profile again. The remediation introduced explicit swipe semantics so one-pointer fling/swipe retained the proven swipe timing and fallback-aware coordinate executor. The final head deletes dragSemantics / synthesizeSwipe and .gesture again sends every one-pointer plan through generic plannedGesture -> synthesizeGesture, bypassing executeDragGesture's synthesized-coordinate policy, keyboard handling, and XCTest fallback. Restore intent-aware lowering for single-pointer fling/swipe versus timed pan, and add a runner regression that fails when the swipe profile/fallback route is removed.

  3. Medium: daemon repetition safety bounds are still absent. readSwipeInput validates only count >= 1 and pauseMs >= 0, then loops/sleeps directly. Restore the shipped count <= 200 and pauseMs <= 10_000 limits at this trust boundary and test both maxima.

  4. Medium: legacy rotate velocity compatibility remains inverted. gesture-input.ts still parses velocity as a non-negative integer, rejecting previously accepted finite negative/fractional non-zero values while accepting zero, which the legacy contract rejected. Since velocity is deprecated and ignored, the compatibility boundary should accept the historical finite non-zero domain and emit the deprecation; add regressions for negative, fractional, and zero values.

The generic gesture recording-overlay gap is fixed, provider viewport modeling is now covered, and all 23 checks are green. Those improvements do not cover the four behavioral contracts above. No fixer was started.

@thymikee

Copy link
Copy Markdown
Member Author

Re-review of clean head 030c34cf:

  • Resolved: daemon count / pauseMs maxima are restored, with an additional 60-second combined schedule cap and focused route/schema coverage.

Three blockers remain:

  1. High: duration-less coordinate swipe is still rejected on Linux. The handler normalizes omitted duration to fling, then capability admission rejects fling on Linux before the Linux drag executor runs. The Linux provider test still supplies durationMs: 16, so it passes under the broken default. Add a duration-less Linux swipe regression and preserve the established default.

  2. High: iOS single-pointer swipe still bypasses its established execution profile and fallback. The Apple adapter sends all iOS plans through generic gesture -> plannedGesture -> synthesizeGesture. That path linearly traverses the plan and returns private-synthesis failures as unsupported; it does not use executeDragGesture's swipe timing, synthesized-coordinate policy, keyboard handling, or XCTest coordinate fallback. Apple adapter tests cover two-pointer iOS and single-pointer macOS/tvOS, but not a single-pointer iOS plan. Restore intent-aware lowering for fling/swipe versus timed pan and add a regression that fails when the profile/fallback route is removed.

  3. Medium: legacy rotate velocity compatibility is unchanged. gesture-input.ts still requires a non-negative integer, rejecting historically valid finite negative/fractional non-zero values and accepting zero. Keep the historical finite non-zero parser domain while emitting the deprecation, with negative/fractional/zero regressions.

All 23 checks are green, but the current tests do not exercise the two high-severity paths above. No fixer was started.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed the review follow-ups in bd407c079:

  • Added canonical centroid overlay events for rotate and transform, with recorder-boundary regression tests. Existing pan/fling/pinch coverage remains green.
  • Aligned every duration-bearing structured gesture input with the planner range: integer 16..10000. Values such as durationMs: 0 now fail early with INVALID_ARGS and a range-specific message.
  • Documented the active-app viewport preference and visible-snapshot-union fallback near-edge limitation in versioned workflow help.
  • Documented that tvOS coordinate pan/fling preserve only dominant remote-swipe direction, not endpoints or duration.
  • Physically smoke-tested single-finger pan and fling on iPhone 00008150-001849640CF8401C: pan returned pointerCount: 1 and moved Settings to Screen Time; fling returned pointerCount: 1 and moved onward to Apps. Capability admission remains honest; no gate change needed. Session closed.

Validation: format, typecheck, lint, Fallow, focused 33 tests, and build passed. The complete unit projects ran 421 files / 3,824 assertions green; the command exit was tripped only by an unrelated screenshot retry test taking 5.27s against the 5s slow-test ceiling, and that test passed in isolation in 449ms.

@thymikee

Copy link
Copy Markdown
Member Author

Re-review at bd407c0: not ready. The delta adds duration bounds and recording telemetry but does not change the three blocked production paths:

  • High: duration-less Linux coordinate swipe still normalizes to fling and fails Linux capability gating; the Linux scenario still supplies a duration.
  • High: iOS single-pointer swipe still uses generic gesture synthesis without the established synthesized-drag fallback/profile parity; no single-pointer iOS failure regression exists.
  • Medium: rotate velocity still rejects formerly valid negative/fractional values and accepts formerly invalid zero.

New in this delta: Low: recording-gesture-events.ts sends two-finger transform centroid travel through the single-pointer swipe classifier, so an edge-origin transform can be mislabeled back-swipe; the new centered test does not cover it.

No fixer was dispatched.

@thymikee

Copy link
Copy Markdown
Member Author

Resolved the three remaining re-review blockers in cd9f74fc6:

  • Linux now admits duration-less coordinate and preset public swipe through its existing drag primitive, while explicit directional gesture fling remains unsupported because Linux cannot honor velocity semantics. Added capability and daemon-route regressions.
  • iOS single-pointer fling plans now select the proven fast XCTest swipe profile (100 ms movement + planned hold) inside the runner. The canonical GesturePlan remains the only payload; timed pan and all two-pointer plans still use exact sampled synthesis. The fast path reuses executeDragGesture, preserving keyboard avoidance, synthesized-coordinate policy, private synthesis, XCTest coordinate fallback, and fallback diagnostics. It suppresses drag-only visualization-frame fields so the unified public response shape remains unchanged. No dragSemantics wire compatibility was restored.
  • Deprecated rotate velocity again accepts the historical finite non-zero domain, including negative/fractional values, and still normalizes away with a deprecation. MCP metadata now matches.

Adversarial review found and prompted the response-shape guard above; a follow-up pass found no remaining blocker.

Validation:

  • format, typecheck, lint, Fallow, build, and build:xcuitest green
  • unit: 421 files / 3,827 assertions green
  • coverage: 466 files / 4,036 assertions green
  • provider integration: 33 files / 121 assertions green
  • Node integration: 15 passed, 6 intentional live skips
  • focused gesture regressions: 79/79 green

Live iOS 26.2 (iPhone 17 Pro):

  • complete repo Gesture Lab replay passed (pan 1/2 pointers, fling both directions, pinch, rotate, combined transform) in 31.6s
  • upstream pager-view pager_basic_example.yaml passed in 17.6s
  • duration-less pager fast-swipe canary passed in 4.98s
  • landscape-left duration-less fast swipe changed observable state from page number 0 / Page 1 / 10 to page number 1 / Page 2 / 10
  • all manual sessions closed; simulator restored to portrait

@thymikee

Copy link
Copy Markdown
Member Author

Addressed the additional recording-semantics finding in 121a15a80.

back-swipe is a one-contact navigation semantic, so canonical recording now classifies it only for one-pointer pan/fling travel. Two-finger pan, rotate, and transform always emit generic centroid-travel swipe overlays, including edge-origin trajectories. Added recorder-boundary regression coverage for a left-edge transform and right-edge two-finger pan; the old raw one-finger edge-swipe test remains green.

Validation: format, typecheck, lint, focused recording tests, Fallow, and diff check green.

@thymikee

Copy link
Copy Markdown
Member Author

Found one Apple runner lifecycle bug still present on the latest #1212 head (bd407c079), while validating the stacked Maestro rework. This is not gesture-specific.

Deterministic reproduction on iPhone 16 / iOS 26.2:

agent-device test ./.maestro/tests/pager_basic_example.yaml ./.maestro/tests/tab_view_inside_scroll_view_example.yaml --maestro --platform ios --device "iPhone 16" -e APP_ID=com.pagerviewexample --debug

Before the fix, the first flow passed and the second failed after launchApp replaced the app process. The retained runner kept the old XCUIApplication; the first XCTest snapshot warm-up timeout for the new process then recorded the existing 120-second bundle-wide snapshot penalty, so later captures deferred XCTest semantics to private AX. The runner trace showed the PID replacement followed by SNAPSHOT_XCTEST_CHANNEL_PENALIZED / ...DEFERRED.

I made the fix atomic in 226014986 (fix: refresh Apple runner state after app relaunch), ready to cherry-pick. It:

  • tracks the cached target PID and reacquires XCUIApplication when the process changes;
  • clears snapshot-channel penalty state from the previous process;
  • exempts only the first new-process snapshot from recording a sticky penalty (the next capture can still penalize a genuinely hostile screen);
  • resets the exemption at target activation/invalidation boundaries.

Validation:

  • focused XCTest lifecycle cases: 3 passed;
  • pnpm build:xcuitest: iOS and macOS passed;
  • live reduced repro after rebuilding/clearing retained runner: 2/2 passed in 40.5s;
  • trace confirms AGENT_DEVICE_RUNNER_TARGET_CACHE_REFRESH on the relaunch and no penalty/defer event.

The full pager-view iOS suite still has separate Maestro/test-fixture failures, so this commit deliberately fixes only the independently proven core runner lifecycle issue.

@thymikee

Copy link
Copy Markdown
Member Author

Re-review at 121a15a: the four prior production findings are fixed: Linux duration-less swipe, iOS fast-swipe and fallback implementation, rotate velocity compatibility, and multi-touch recording classification.

One merge-readiness gap remains: the iOS fix is not regression-locked through the command path. Current tests exercise the standalone execution selector and response filtering, but reverting the gesture branch to the old sampled executor would still leave them green. Add a test that executes a single-pointer fling or gesture command, forces synthesis failure, and proves the fast-swipe profile plus xctest-coordinate-drag fallback. Equivalent live runner evidence for that exact failure path would also close the evidence gap.

No fixer dispatched.

@thymikee thymikee force-pushed the agent/unified-gesture-architecture branch from 121a15a to e762801 Compare July 12, 2026 13:24
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto current main and addressed the remaining review gap on head e762801d2. The shipped Swift gesture-command branch is now guarded so replacing fling's fast-swipe route with sampled synthesis, removing the synthesized-drag policy, or dropping the XCTest coordinate-drag fallback fails the unit suite. Cherry-picked the Apple target-process refresh fix and live-verified the sequential relaunch canary: 2/2 flows passed in 45.3s; the second launch completed in 2.2s; sessions closed. Post-rebase local gates and all GitHub checks are green.

@thymikee

Copy link
Copy Markdown
Member Author

Re-review at e762801 found two remaining gaps:

  1. The new Apple fling contract test source-scans Swift and locks the fast-swipe routing text, but it does not execute synthesis failure. The effective iOS fallback is inside executeSynthesizedDragGesture; removing that branch and returning unsupported would leave this test green. Add an executable forced-failure test or equivalent live evidence.
  2. The new process-bound cache state is not cleared at every invalidation site. The no-appBundleId path clears currentApp and currentBundleId directly but leaves currentAppProcessIdentifier and snapshotXCTestPenaltyWarmupExemptionPending. A later no-bundle snapshot can consume a relaunch exemption for the wrong target. Route this through the complete invalidation helper and cover that production path.

Not ready yet. No fixer dispatched.

@thymikee

Copy link
Copy Markdown
Member Author

Confirmed and fixed both blockers in 769b69a43. The old source-scan guard is deleted. A focused iOS XCTest now runtime-replaces private swipe synthesis with a deterministic failure, executes the structured single-pointer fling command, performs the real XCTest coordinate drag, and asserts fallback diagnostics plus canonical response shape. The missing-appBundleId production branch now calls the complete target-cache invalidator; a production-path test seeds stale app/bundle/PID/warmup-exemption state and proves all four fields are cleared. All 3 focused iOS XCTest regressions executed locally: 3 passed, 0 failed. The iOS CI job now executes them before smoke replay.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant