Skip to content

Commit e8da1e1

Browse files
thymikeeclaude
andauthored
feat(fold): pose a foldable iPhone simulator through Device Hub and verify it with CoreDevice (#2717)
* feat(fold): pose a foldable iPhone simulator through Device Hub and verify it with CoreDevice Add `agent-device fold <closed|half-open|open>` and `client.command.fold({ pose })`. ADR 0025 recorded that no official host API sets the hinge pose and left it to the operator. The operator's own control is the pose action bar in the Xcode Device Hub window, whose controls are ordinary accessibility buttons; the macOS helper gains a `device-hub pose` subcommand that finds Device Hub in the process table (LaunchServices registers the trampolined app with pid -1, which is why System Events saw no nodes), reopens its window with a reopen event when it shows none, selects the simulator through the sidebar row keyed `TableRow.Device.<udid>`, and presses the control. The press is not the evidence. The Apple owner reads the hinge angle back through `devicectl device motion hinge-angle` until it agrees with the request: closed is 0°, open is 180°, and half-open is any angle between them reported only once two consecutive reads agree the hinge stopped moving, because a hinge sweeps through half-open angles on its way to either stop (the live run read 175.1° one stream after pressing Book, 130° after it settled). The stream never ends on its own, so each read is bounded by devicectl's smallest `--timeout` of five seconds. The response carries the verified pose, the angle, and the lit panel's point size, so an agent sees its refs are stale without another capture. `setFoldPose` is a host-side device-runtime operation rather than an interactor method: it needs no XCUITest runner, and the leaf fact admits iPhone/iPad simulators while the operation itself refuses a single-panel simulator from CoreDevice's display table. Every other platform and provider states its own refusal cell. Verified through the built CLI on an iOS 27.1 iPhone Duo simulator: open → 180° on LCD-1 (669x951pt, screenshot 951x669), half-open → 130°, closed → 0° on LCD (466x678pt, screenshot 466x678), from a Device Hub that showed no window for the device; an iPhone 17 refuses with UNSUPPORTED_OPERATION before anything is pressed. Co-Authored-By: Claude Fable 5.1 <[email protected]> * chore(gates): register fold in the exports snapshot, coverage tables, and conformance tables The contracts export snapshot names the new `fold-runtime` subpath, the six-platform coverage declaration and its Android evidence row classify the command, the runtime-binding conformance table resolves it, and the provider output guard drives it. Co-Authored-By: Claude Fable 5.1 <[email protected]> --------- Co-authored-by: Claude Fable 5.1 <[email protected]>
1 parent e3edc9e commit e8da1e1

84 files changed

Lines changed: 1878 additions & 67 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{"type":"replay_test_attempt_start","ts":"2026-09-19T14:32:46.818Z","replayPath":"/tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-pzCEKx/01-flow.yaml","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","requestId":"req-maestro-test-wire-true:test:1:01-flow.yaml:attempt:1"}
2-
{"type":"replay_test_attempt_stop","ts":"2026-09-19T14:32:46.819Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","ok":true,"timedOut":false,"durationMs":2}
3-
{"type":"replay_test_finalize_start","ts":"2026-09-19T14:32:46.819Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1"}
4-
{"type":"replay_test_finalize_stop","ts":"2026-09-19T14:32:46.820Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","ok":true,"durationMs":1}
5-
{"type":"replay_test_cleanup_start","ts":"2026-09-19T14:32:46.820Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1"}
6-
{"type":"replay_test_cleanup_stop","ts":"2026-09-19T14:32:46.820Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","ok":true,"durationMs":0}
1+
{"type":"replay_test_attempt_start","ts":"2026-09-20T20:37:45.965Z","replayPath":"/tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-IAQdpt/01-flow.yaml","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","requestId":"req-maestro-test-wire-true:test:1:01-flow.yaml:attempt:1"}
2+
{"type":"replay_test_attempt_stop","ts":"2026-09-20T20:37:45.967Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","ok":true,"timedOut":false,"durationMs":2}
3+
{"type":"replay_test_finalize_start","ts":"2026-09-20T20:37:45.967Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1"}
4+
{"type":"replay_test_finalize_stop","ts":"2026-09-20T20:37:45.967Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","ok":true,"durationMs":0}
5+
{"type":"replay_test_cleanup_start","ts":"2026-09-20T20:37:45.967Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1"}
6+
{"type":"replay_test_cleanup_stop","ts":"2026-09-20T20:37:45.967Z","session":"default:test:req-maestro-test-wire-true:1-01-flow:attempt-1","ok":true,"durationMs":0}

‎.agent-device/test-artifacts/req-maestro-test-wire-true/01-flow.yaml/attempt-1/result.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
file: /tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-pzCEKx/01-flow.yaml
1+
file: /tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-IAQdpt/01-flow.yaml
22
session: default:test:req-maestro-test-wire-true:1-01-flow:attempt-1
33
attempt: 1/1
44
status: passed
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{"type":"replay_test_attempt_start","ts":"2026-09-19T14:32:46.820Z","replayPath":"/tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-pzCEKx/02-flow.yaml","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","requestId":"req-maestro-test-wire-true:test:2:02-flow.yaml:attempt:1"}
2-
{"type":"replay_test_attempt_stop","ts":"2026-09-19T14:32:46.821Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","ok":true,"timedOut":false,"durationMs":1}
3-
{"type":"replay_test_finalize_start","ts":"2026-09-19T14:32:46.821Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1"}
4-
{"type":"replay_test_finalize_stop","ts":"2026-09-19T14:32:46.821Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","ok":true,"durationMs":0}
5-
{"type":"replay_test_cleanup_start","ts":"2026-09-19T14:32:46.821Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1"}
6-
{"type":"replay_test_cleanup_stop","ts":"2026-09-19T14:32:46.821Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","ok":true,"durationMs":0}
1+
{"type":"replay_test_attempt_start","ts":"2026-09-20T20:37:45.968Z","replayPath":"/tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-IAQdpt/02-flow.yaml","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","requestId":"req-maestro-test-wire-true:test:2:02-flow.yaml:attempt:1"}
2+
{"type":"replay_test_attempt_stop","ts":"2026-09-20T20:37:45.968Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","ok":true,"timedOut":false,"durationMs":0}
3+
{"type":"replay_test_finalize_start","ts":"2026-09-20T20:37:45.969Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1"}
4+
{"type":"replay_test_finalize_stop","ts":"2026-09-20T20:37:45.969Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","ok":true,"durationMs":0}
5+
{"type":"replay_test_cleanup_start","ts":"2026-09-20T20:37:45.969Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1"}
6+
{"type":"replay_test_cleanup_stop","ts":"2026-09-20T20:37:45.969Z","session":"default:test:req-maestro-test-wire-true:2-02-flow:attempt-1","ok":true,"durationMs":0}

‎.agent-device/test-artifacts/req-maestro-test-wire-true/02-flow.yaml/attempt-1/result.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
file: /tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-pzCEKx/02-flow.yaml
1+
file: /tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-IAQdpt/02-flow.yaml
22
session: default:test:req-maestro-test-wire-true:2-02-flow:attempt-1
33
attempt: 1/1
44
status: passed
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{"type":"replay_test_attempt_start","ts":"2026-09-19T14:32:46.823Z","replayPath":"/tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-Wnbhbp/01-flow.yaml","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","requestId":"req-maestro-test-wire-undefined:test:1:01-flow.yaml:attempt:1"}
2-
{"type":"replay_test_attempt_stop","ts":"2026-09-19T14:32:46.823Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","ok":true,"timedOut":false,"durationMs":0}
3-
{"type":"replay_test_finalize_start","ts":"2026-09-19T14:32:46.823Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1"}
4-
{"type":"replay_test_finalize_stop","ts":"2026-09-19T14:32:46.823Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","ok":true,"durationMs":0}
5-
{"type":"replay_test_cleanup_start","ts":"2026-09-19T14:32:46.823Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1"}
6-
{"type":"replay_test_cleanup_stop","ts":"2026-09-19T14:32:46.823Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","ok":true,"durationMs":0}
1+
{"type":"replay_test_attempt_start","ts":"2026-09-20T20:37:45.971Z","replayPath":"/tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-XmcOFG/01-flow.yaml","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","requestId":"req-maestro-test-wire-undefined:test:1:01-flow.yaml:attempt:1"}
2+
{"type":"replay_test_attempt_stop","ts":"2026-09-20T20:37:45.971Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","ok":true,"timedOut":false,"durationMs":0}
3+
{"type":"replay_test_finalize_start","ts":"2026-09-20T20:37:45.971Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1"}
4+
{"type":"replay_test_finalize_stop","ts":"2026-09-20T20:37:45.971Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","ok":true,"durationMs":0}
5+
{"type":"replay_test_cleanup_start","ts":"2026-09-20T20:37:45.971Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1"}
6+
{"type":"replay_test_cleanup_stop","ts":"2026-09-20T20:37:45.971Z","session":"default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1","ok":true,"durationMs":0}

‎.agent-device/test-artifacts/req-maestro-test-wire-undefined/01-flow.yaml/attempt-1/result.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
file: /tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-Wnbhbp/01-flow.yaml
1+
file: /tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-XmcOFG/01-flow.yaml
22
session: default:test:req-maestro-test-wire-undefined:1-01-flow:attempt-1
33
attempt: 1/1
44
status: passed
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{"type":"replay_test_attempt_start","ts":"2026-09-19T14:32:46.824Z","replayPath":"/tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-Wnbhbp/02-flow.yaml","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","requestId":"req-maestro-test-wire-undefined:test:2:02-flow.yaml:attempt:1"}
2-
{"type":"replay_test_attempt_stop","ts":"2026-09-19T14:32:46.824Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","ok":true,"timedOut":false,"durationMs":0}
3-
{"type":"replay_test_finalize_start","ts":"2026-09-19T14:32:46.824Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1"}
4-
{"type":"replay_test_finalize_stop","ts":"2026-09-19T14:32:46.824Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","ok":true,"durationMs":0}
5-
{"type":"replay_test_cleanup_start","ts":"2026-09-19T14:32:46.824Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1"}
6-
{"type":"replay_test_cleanup_stop","ts":"2026-09-19T14:32:46.824Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","ok":true,"durationMs":0}
1+
{"type":"replay_test_attempt_start","ts":"2026-09-20T20:37:45.972Z","replayPath":"/tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-XmcOFG/02-flow.yaml","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","requestId":"req-maestro-test-wire-undefined:test:2:02-flow.yaml:attempt:1"}
2+
{"type":"replay_test_attempt_stop","ts":"2026-09-20T20:37:45.972Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","ok":true,"timedOut":false,"durationMs":0}
3+
{"type":"replay_test_finalize_start","ts":"2026-09-20T20:37:45.972Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1"}
4+
{"type":"replay_test_finalize_stop","ts":"2026-09-20T20:37:45.973Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","ok":true,"durationMs":1}
5+
{"type":"replay_test_cleanup_start","ts":"2026-09-20T20:37:45.973Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1"}
6+
{"type":"replay_test_cleanup_stop","ts":"2026-09-20T20:37:45.973Z","session":"default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1","ok":true,"durationMs":0}

‎.agent-device/test-artifacts/req-maestro-test-wire-undefined/02-flow.yaml/attempt-1/result.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
file: /tmp/agent-device-test-run-75313-0YH0p7/agent-device-maestro-remote-test-Wnbhbp/02-flow.yaml
1+
file: /tmp/agent-device-test-run-94227-XxlCsl/agent-device-maestro-remote-test-XmcOFG/02-flow.yaml
22
session: default:test:req-maestro-test-wire-undefined:2-02-flow:attempt-1
33
attempt: 1/1
44
status: passed

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@
1010
and every stop the host applies mid-hold — a cancelled request, a dropped client, the deadline —
1111
reaches the helper as SIGTERM first so it releases the button before it exits; SIGKILL only
1212
follows a helper that has not exited a second later.
13+
- Added (ios): `fold <closed|half-open|open>` and `client.command.fold({ pose })` put a foldable
14+
iPhone simulator (iPhone Duo) into a hinge pose. ADR 0025 recorded that no official host API sets
15+
the pose and left it to the operator; the pose is now set the way the operator did it, by pressing
16+
the pose control in the Xcode Device Hub window through macOS accessibility, and it is confirmed
17+
the way ADR 0025 asked for, by reading the hinge angle back from CoreDevice
18+
(`devicectl device motion hinge-angle`) until it agrees with the request. The response reports
19+
the verified pose, the hinge angle, and the panel the device now lights with its point size, so an
20+
agent can see that its refs and coordinates are stale without another capture. The macOS helper
21+
gained a `device-hub pose` subcommand that finds Device Hub in the process table (LaunchServices
22+
registers the trampolined app with no pid), reopens its window when it shows none, and selects the
23+
simulator through the sidebar row keyed by its UDID so two simulators sharing a name cannot be
24+
confused. Simulator-only; a single-panel simulator refuses with `UNSUPPORTED_OPERATION`, and every
25+
other platform states its own refusal cell.
1326
- Added (diff): `diff screenshot` accepts a JPEG baseline or current image. Both inputs had to be PNG,
1427
so a capture exported by another tool had to be converted first and a HarmonyOS capture — which the
1528
platform serves as JPEG under whatever name the command was given — could never be compared. Each

‎apple/macos-helper/Package.swift‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,20 @@ let package = Package(
1414
.target(
1515
name: "AgentDeviceMacOSInput"
1616
),
17+
.target(
18+
name: "AgentDeviceMacOSDeviceHub"
19+
),
1720
.executableTarget(
1821
name: "AgentDeviceMacOSHelper",
19-
dependencies: ["AgentDeviceMacOSInput"]
22+
dependencies: ["AgentDeviceMacOSInput", "AgentDeviceMacOSDeviceHub"]
2023
),
2124
.testTarget(
2225
name: "AgentDeviceMacOSInputTests",
2326
dependencies: ["AgentDeviceMacOSInput"]
2427
),
28+
.testTarget(
29+
name: "AgentDeviceMacOSDeviceHubTests",
30+
dependencies: ["AgentDeviceMacOSDeviceHub"]
31+
),
2532
]
2633
)

0 commit comments

Comments
 (0)