ADE Apple Development
SkillMediaUse this skill when you need to see an iOS or SwiftUI change actually running on a simulator, finding or creating the lane's device, booting and streaming it, driving it by element query, screenshotting, recording, capturing proof, or rendering a SwiftUI preview, via `ade apple`.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the ADE Apple Development skill
What this skill tells your AI
The instructions your AI receives, as published by arul28/ade in apps/desktop/resources/agent-skills/ade-apple/SKILL.md and read by ahel’s review.
This skill is for iOS and SwiftUI apps. For a macOS app, a dev Electron app or a web page, the ade-computer-use skill picks the right surface.
Drive the lane's Apple simulator with "$ADE_CLI_PATH" apple <command>.
$ADE_CLI_PATH is the CLI of the ADE that launched you and already targets
its brain, so your calls and the desktop's Apple Development tool share one
session. A bare ade goes through PATH and can reach a different ADE.
The pixels come from a vendored Swift helper (ade-sim-helper) reading the
device framebuffer. There is no Screen Recording grant, no Simulator window to
keep open, and no backend to choose. If you find advice about idb, window
capture, live-start or a --backend flag, it is describing a version of this
feature that no longer exists.
Four rules before your first command
- Always call
"$ADE_CLI_PATH", never a bareade. A bareadecan be an older install or another ADE's CLI. That shows up as "Unknown command 'apple'", "Domain 'apple' is unavailable", or a runtime version mismatch. Do not add--socket:"$ADE_CLI_PATH"already names the right one. - Never open or script the Simulator app. No
open -a Simulator, no AppleScript or System Events. ADE drives the device directly; the Simulator window is not needed and adds nothing. - Never capture proof with
xcrun simctl io … screenshotorrecordVideo.apple screenshotandapple record-start/record-stopfile proof to the lane automatically. See below for why the owner matters. - Check each step before you report it. A tap, swipe, button or type
that returns
okonly means ADE sent the input. It does not mean the app did what you wanted. After each step that matters, confirm it:apple foreground(which app is in front),apple assert-visibleorapple wait-for-element(what is on screen), orapple snapshot. Report only what you confirmed. If a step failed, say which step, and do not describe the result you meant to get. Beforerecord-stop, confirm the final state, so the video ends on it.
Common tasks
Use these directly; you do not need --help for them. A="$ADE_CLI_PATH".
| Task | Command |
|---|---|
| Open an installed app | "$A" apple relaunch --bundle-id com.apple.mobilesafari --text |
| Open a web page or deep link | "$A" apple open-url "https://www.google.com" --text |
| Tap a control by its label | "$A" apple tap-element --label "Address" --text |
| Type, then press Return | "$A" apple type "reddit" --submit --text |
| Press one key | "$A" apple key return --text (also tab) |
| Wait for something to appear | "$A" apple wait-for-element --label "Cancel" --timeout-ms 8000 --text |
| Which app is in front | "$A" apple foreground --text |
| What is on screen | "$A" apple snapshot --text |
| Go home / app switcher | "$A" apple button home --text / "$A" apple button app-switcher --text |
| Close an app (no gesture) | "$A" apple terminate --bundle-id com.apple.mobilesafari --text |
| Show the device to the user | "$A" apple show --text (tools pane) or --floating |
- Right after
relaunch,foregroundcan still saynullfor a moment. Wait for an element of that app instead of checking at once. - A simulator signed in to an Apple Account can show an "Apple Account
Verification" alert over every app. Dismiss it with
tap-element --label "Not Now", then continue.
Start here: ask what you can do
"$ADE_CLI_PATH" apple status --text
status is the gate and the map.
supported: falsemeans the runtime is not a Mac. Stop and say so.capabilitieslists every action you may call on this device, by name. Read it instead of guessing verbs or reading source.devicetells you whether this lane already owns a simulator, itsorigin(createdor the olderclonefor ADE's own,attachedfor the user's), and itslaneId.tools.helper(present,path,version) is the check when every device call fails at once.
"Start the app on a simulator" is one command
"$ADE_CLI_PATH" apple launch --follow --open-drawer --text
launch does the whole chain: find or make the lane's device, boot it,
resolve the target, build it with xcodebuild, install it, start it, and
claim the drawer session. Add --open-drawer when a human asked to watch.
--follow announces the wait up front for a cold build.
So you do not run xcodebuild by hand to put an app on a screen, and you
do not need to know the scheme. Run "$ADE_CLI_PATH" apple apps --text first only
when you must choose between several targets, then pass --target <id>.
Use --no-build when the app is already installed and you only want it in
front. Use relaunch or terminate for an app that is already there.
Use this lane's own device
Use this lane's own device. ade apple start (or device-create) gives the
lane its own; do it without asking.
start with no arguments does this for you: when the lane has no device it
makes a new, empty one from the newest installed runtime, boots it, and
streams it. Other simulators in apple devices belong to someone else:
another lane, an xcodebuild test run, or the user.
- You may make the lane's ONE device without asking. You may not make a
second one, and you never make a simulator with
xcrun simctl createorsimctl clone— ADE cannot see or delete a device made that way. - You may attach an installed simulator only when the user names it
(
device-attach --simulator <name>orstart --udid <udid>). ADE refuses a simulator another lane holds, withAPPLE_DEVICE_NOT_LANE_OWNED. --device <udid>on any other command must be this lane's device. With no device yet, the command tells you to runade apple start.
A busy device is not a blocker, and it is not a reason to ask a human.
Make the lane's own with start. It takes seconds (see "The lane's device").
Tests run on the lane's device
"$ADE_CLI_PATH" apple test --scheme <scheme> --text
"$ADE_CLI_PATH" apple test --target <id> --only ADETests/SyncTests --text
apple test runs xcodebuild test on this lane's device, with parallel
testing off and DerivedData in the lane's cache
(.ade/cache/ios-simulator/DerivedData), which ADE deletes with the lane. One
test run goes at a time on the Mac (APPLE_TEST_RUN_BUSY: wait, then run
again). A failing test comes back as passed: false with the failures and the
log path; it is not an error.
Do not run xcodebuild test by hand against a simulator you made. If you must
run xcodebuild yourself, pass -derivedDataPath .ade/cache/ios-simulator/DerivedData
from the lane worktree, never /tmp or ~/Library/Developer/Xcode/DerivedData.
This is worth saying plainly because the failure looks reasonable from the inside: you find a booted simulator, the guard tells you another chat owns it, and stopping to ask seems polite. It is not. It blocks the work for no reason and hands the human a decision they should never have been given.
And when the request was for simulator proof, simulator proof is what you owe. A passing unit test is not a substitute for a screen. If you cannot reach the screen, say exactly what stopped you rather than offering evidence of a different kind and calling it done.
The lane's device
One simulator per lane. ADE creates one on first ask, or binds one you already
have. ADE never downloads a runtime. With none installed you get
APPLE_NO_INSTALLED_SIMULATORS — tell the user to open Xcode ▸ Settings ▸
Components.
Two facts that decide what you should do:
- A runtime is the iOS image. It is the large download, one per iOS version, and ADE will not fetch one.
- A device is an instance made from a runtime. Creating one copies no iOS,
so it is cheap. A new lane device is empty: it copies nothing from any other
simulator and grows only with what the lane installs. A device that is
Shutdownis installed and ready — it needs a boot, which takes seconds.
So "no device free" is almost never true: one runtime serves any number of devices, and a new one is a folder of app data, not another copy of iOS. A new device has no sign-ins, so the app starts at its first-run state.
"$ADE_CLI_PATH" apple device-list --installed --text # what a picker shows
"$ADE_CLI_PATH" apple device-list --runtimes --text # installed runtimes and models
"$ADE_CLI_PATH" apple device-list --text # the one this lane owns ($ADE_LANE_ID)
"$ADE_CLI_PATH" apple start --text # make one if the lane has none, boot, stream
"$ADE_CLI_PATH" apple start --device-type "iPad Air 11-inch (M4)" --text # a specific model
"$ADE_CLI_PATH" apple stop --text # power the device OFF
startis the one-step bring-up: make a device when the lane has none, boot it if it is off, wait for the boot, then stream. Prefer it.start --udidworks only for the device this lane already holds.stoppowers the device off.shutdownonly ends this chat's session and leaves the simulator running — the two are not the same, and a verb namedstopthat left a booted device behind was a real bug.device-createanddevice-attachnever boot.device-deleteremoves the lane's ADE device and refuses an attached device unless--force, which only detaches. Likestop, it is refused while another chat is driving the device.- ADE deletes the lane's own device, with all its data and the lane's build cache, when the lane is archived or deleted. An attached device is powered off and loses only the apps ADE installed. ADE never deletes a simulator it did not create.
- An ADE device that nothing uses for 30 minutes is powered off.
startboots it again in seconds. - Deleting an installed simulator from the list is the user's call, made in the desktop picker. ADE refuses it from an agent; do not try.
Drive the app
Name elements, not pixels.
"$ADE_CLI_PATH" apple snapshot --text
"$ADE_CLI_PATH" apple tap-element --label "Sign in" --text
"$ADE_CLI_PATH" apple fill-element --identifier email-field --value ada@example.com --text
"$ADE_CLI_PATH" apple wait-for-element --label Welcome --timeout-ms 8000 --text
"$ADE_CLI_PATH" apple assert-visible --label Welcome --text
- Run
snapshotfirst. Query with--ref,--identifier,--label,--text-match,--roleand--index. - A
refnames its own tier.id:andcomponent:survive a re-render;pos:survives nothing, so ask for an accessibility identifier instead. wait-for-elementreplaces a sleep. Add--goneto wait for a disappearance.- Fall back to coordinates only when no query matches:
tap,drag,swipe,scroll,select. typeandkeyact on the focused field, so tap the field first.foregroundreports which app the device has in front, read from the device rather than from what ADE last launched.- Hardware buttons:
button home(alsolock,volume-up,volume-down,siri,app-switcher).shakeis refused withAPPLE_BUTTON_UNSUPPORTED, because neither the helper norsimctlhas it.app-switcheris Simulator's own App Switcher command, two home presses 150 ms apart; do not presshometwice yourself, the gap between two calls is too long. - Orientation:
rotate landscape-left(alsoportrait,portrait-upside-down,landscape-right). Read the answer.rotateturns the device and then reads the screen, soapplied: truemeans the framebuffer was seen on the requested axis — not that a request was sent. iOS always accepts the device orientation and then lets the foreground app decide, soapplied: falsewithreason: APPLE_ROTATE_NOT_ADOPTEDmeans that app kept its own orientation. The Home Screen and Settings are portrait-only on an iPhone, and no iPhone supports portrait upside down — put the app you are testing in front before you rotate, and do not treat a refusal as a broken simulator.verification: already-on-axismeans the screen was on that axis before you asked; turning within one axis leaves the pixel size unchanged, so the exact side is not confirmed.
Agent launches stay in the background; apple show brings the device up at
any time.
Close an app the way a person does
Bottom-edge swipes do not work: the helper sends them as ordinary touches, so iOS never sees the home-indicator gesture, and a swipe from the bottom edge opens nothing. Use the App Switcher instead.
"$ADE_CLI_PATH" apple button app-switcher --text
"$ADE_CLI_PATH" apple screenshot --out switcher.png --text # check it opened
"$ADE_CLI_PATH" apple swipe 201 480 201 80 --duration-ms 150 --text
"$ADE_CLI_PATH" apple button home --text
- Coordinates are points. Read the screen size from
apple stream-status(pointWidth×pointHeight); the example is an iPhone 16 Pro (402 × 874). - The front app's card sits in the middle of the switcher. Swipe it up from about (W/2, 0.55 H) to (W/2, 0.09 H), fast: 150 ms. A slow drag only lifts the card.
- Take a screenshot after
app-switcher. If the switcher is not showing, do not keep swiping: say so, and useapple terminate --bundle-id <id>to stop the app without showing it (Safari iscom.apple.mobilesafari). - While a recording runs, every failed try is in the video. Check the screen after each step instead of repeating a gesture.
Show the device to the user
When the user asks to see the device ("open the sim drawer", "show me"):
"$ADE_CLI_PATH" apple show --text # the Apple tool in the tools pane
"$ADE_CLI_PATH" apple show --floating --text # the floating player over the chat
"$ADE_CLI_PATH" ui show proof --text # this chat's proof drawer
It targets your own chat and reports what really happened. A shell with no
ADE chat identity (ADE_CHAT_SESSION_ID unset, e.g. an OpenCode agent shell)
cannot use apple show or ui show; ask the user to open the tool.
shown— it is on screen now.held— a desktop window has this project open but the user cannot see it yet (your chat is not in front, or the window is hidden). It opens when the user goes to your chat. Say so.no_desktop(exit 1) — no desktop window is open for this chat, so nothing was shown. Tell the user; do not claim you opened it.
A desktop on another machine that has your chat open answers too. You do not
need show just to be seen working: while you drive the device and the Apple
tool is not open, the desktop floats the device over your chat by itself,
unless the user closed that player (then only apple show or the user brings
it back).
Proof is automatic, and only on this path
You do not pin anything. Both of these file themselves in the proof drawer and
return a proofArtifactId:
- every recording, the moment it stops, whoever started it;
- every screenshot you take.
"$ADE_CLI_PATH" apple screenshot --out shot.png --text
"$ADE_CLI_PATH" apple frame --out shot.png --text
"$ADE_CLI_PATH" apple proof-bundle --caption "Settings row renders" --text
Capture through these commands, not through xcrun simctl io screenshot.
The difference is not the picture, it is the owner. An ade apple capture is
filed against the lane and the asking chat, so it appears in the drawer the
human is looking at. A picture you take with simctl and then hand to
ade proof attach is filed against whatever ADE can work out about your shell,
and a shell with no chat session — every OpenCode agent has one, because a
single opencode serve is shared across chats — used to produce a record owned
by nothing, invisible to everyone, while every command reported success. That
now fails loudly instead of lying, which is better and still not proof.
If you must attach a file from elsewhere, run ade proof attach from inside
the lane worktree so ADE can place it, and read the last line: it names the
lane and the chat the artifact landed on. lane none / chat none is a failure,
not a detail.
screenshotround-tripssimctland works with no stream running.framegrabs one decoded frame from the live stream and fails withAPPLE_STREAM_NOT_RUNNINGwhen there is none. Prefer it while streaming.proof-bundlefiles the same single drawer row asscreenshot, and also writeselements.json, the log andmetadata.jsonnext toscreen.pngin a directory under the build root. Use it when a reviewer wants the whole context; the row it returns is the picture.
Recording, and what starts one
"$ADE_CLI_PATH" apple record-start --overlays on --label "signup" --text
"$ADE_CLI_PATH" apple record-stop --keep --text
"$ADE_CLI_PATH" apple record-list --text
"$ADE_CLI_PATH" apple record-delete --id <id> --text
-
Your input starts a recording; the user's never does. Any input from the CLI, an agent action or a semantic action counts as evidence and starts one automatically, tagged
auto. Input from the desktop pane is stamped as the user's and starts nothing. -
It stops at the end of your turn, or after five minutes, whichever is first.
record-startduring an auto recording converts it to manual with no gap. A manual recording stops itself after five minutes of real time (stopReason: "cap"), or after two minutes with no input ("idle"), and files as proof;--max-seconds <n>sets a shorter limit. Still callrecord-stopwhen you are done.When it stops, the recording becomes a demo: still time is cut, waits play faster, tap rings are drawn, and the file ends under 10 MB. The camera does not zoom on a phone; pass
record-start --zoomonly when the user asks for zoom. Mark each step withade proof step "<what happens next>": a caption in the video and a chapter in ADE's player.record-stopreportsdurationMs(the demo),wallDurationMs(real time) andidleCutMs. Pass--plainonly when the user asks for the recording as it was recorded. -
It files itself as proof on stop. There is no auto-delete; recordings accumulate until the user clears them.
-
You may delete a recording you own. Anything else is
APPLE_RECORDING_PINNED. -
A second chat gets
APPLE_OWNED_BY_OTHER_SESSIONwith the owning chat, lane and age. -
If recording fails, say so. Never attach an older recording or a file you did not just record.
Overlays, meaning tap rings and typed-text badges, land in the saved file only and never on a live viewer. Secure text is never badged.
Live view
"$ADE_CLI_PATH" apple stream-start --fps 60 --text
"$ADE_CLI_PATH" apple stream-start --scale-factor 0.5 --bitrate-kbps 2500 --text
"$ADE_CLI_PATH" apple stream-status --text
"$ADE_CLI_PATH" apple stream-stop --text
stream-status reports the shape and never the address or the token. A status
of running: true means the capture is alive, not that a picture is arriving.
Device settings, log, previews
"$ADE_CLI_PATH" apple appearance dark --text
"$ADE_CLI_PATH" apple accessibility reduce-motion on --text
"$ADE_CLI_PATH" apple location 37.7749 -122.4194 --text
"$ADE_CLI_PATH" apple permission grant photos --bundle-id <id> --text
"$ADE_CLI_PATH" apple push --bundle-id <id> --title Hi --body "You have mail" --text
"$ADE_CLI_PATH" apple log-start --bundle-id <id> --text
"$ADE_CLI_PATH" apple preview-current --text
Also content-size, status-bar, open-url, relaunch, terminate,
uninstall, app-state, log, log-stop, preview-status, previews,
preview-match, preview-ensure, preview-render.
What the user sees in the desktop
Worth knowing, because it changes what is running under you.
- The tool is Apple Development, one pane inside the Work tools pane.
- Closing its tab powers the device off, behind a confirmation when the device is booted. Minimising the tools pane leaves it running and shows a floating preview.
- The rail carries Home, Rotate, Inspect, Screenshot, Record, the 3D/Flat view toggle, Tools and More. The drawer has four groups: Device, App, Capture and Preview Lab.
Ownership
One chat owns a simulator session at a time. A second launch fails with
APPLE_OWNED_BY_OTHER_SESSION.
- Ownership releases when the owning chat is deleted or archived.
- The guard is cooperative:
shutdown --force,launch --forceandclaim --ignore-ownershipget through. Ask before you evict another chat. claim --lane <lane-id>attaches a running session to a lane.
Gotchas
APPLE_NO_INSTALLED_SIMULATORS— no installed runtime. Point at Xcode ▸ Settings ▸ Components. Never try to download one.APPLE_HELPER_UNAVAILABLE— the helper binary is missing from this install.status→tools.helper.presentis the check.APPLE_STREAM_NOT_RUNNING—frameneeds a live stream; usescreenshot.APPLE_DEVICE_OFF— the lane's device is powered off, and watching never boots it.apple start(orstream-start) powers it on.APPLE_ROTATE_NOT_ADOPTED— the device turned and the app on screen did not. Launch a landscape-capable app first; it is not a fault to report.APPLE_ROTATE_UNMEASURABLE— the screen could not be read, so the rotation is unconfirmed either way. Check the device is still booted.APPLE_BUTTON_UNSUPPORTED— that button has no helper orsimctlequivalent. Today:shake.APPLE_DEVICE_ATTACHED_NOT_DELETABLE—device-deletewithout--forceon an attached simulator.--forcedetaches; it does not delete.APPLE_DEVICE_EXISTS— this lane already owns a device. Use it, or remove it first.APPLE_DEVICE_NOT_LANE_OWNED— you named a simulator this lane does not hold, or another lane holds it. Do not look for a way around it. Runade apple startto get the lane's own, or attach it first if the user named it.APPLE_RUNTIME_NOT_INSTALLED— the runtime or model you named is not on this Mac. The message lists what is; ADE never downloads one.APPLE_TEST_RUN_BUSY— anotherapple testis running. Wait and run again.IOS_SIMULATOR_TARGET_ROOT_MISMATCH— re-run"$ADE_CLI_PATH" apple apps --text.IOS_SIMULATOR_NO_BUILDABLE_TARGET— pass--target-idor--bundle-idonly when you deliberately want the installed app.screenshot --outandframe --outmust land inside the build root.--textreads two ways. A bare--textis ADE's output mode;--text-match <value>is the element query's substring match.
Signals
- GitHub stars
- 110
- Forks
- 13
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
ade-apple- Source
- github.com/arul28/ade