Repository navigation
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y line stays the dev command, the relayed refusal keeps its data, and the old simulator reader goes (llp/0031 §How `dev` uses it) A Mac without an Android SDK reaches `absent` again, because the iOS inspection answered. A NO_BOUND_DEVICE refusal keeps `dev` as its Try line; --print-url is an Or: door. The detached child's code row carries `data`, and only the Try line next to it counts. smoke keeps the How line in its boot reason and prints the acquire line. The EAS fallback asks the service only when the dotenv names a session. The booted-only simulator reader is deleted. The named tests produce the child's log through logCmdError, forward a --device the pin replaces, and pin the acquire line before the report. 0031 says the order built and the decisions taken; 0030 gains the boot-failed row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y line stays the dev command, the relayed refusal keeps its data, and the old simulator reader goes (llp/0031 §How `dev` uses it) A Mac without an Android SDK reaches `absent` again, because the iOS inspection answered. A NO_BOUND_DEVICE refusal keeps `dev` as its Try line; --print-url is an Or: door. The detached child's code row carries `data`, and only the Try line next to it counts. smoke keeps the How line in its boot reason and prints the acquire line. The EAS fallback asks the service only when the dotenv names a session. The booted-only simulator reader is deleted. The named tests produce the child's log through logCmdError, forward a --device the pin replaces, and pin the acquire line before the report. 0031 says the order built and the decisions taken; 0030 gains the boot-failed row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
89a1f71 to
1363cd1
Compare
The prerequisite outside the llp/0030 stack: `apps/eas-example`, which the live tier and the device-binding PRs dogfood, moves from SDK 57 to SDK 58. Independent of #132 to #134. ## Why now - `@expo/cli` 58 matches `run:android --device` by serial. The Android instances of llp/0032 are read-only copies of one AVD that share a name, so only the serial tells them apart. - SDK 58 embeds the fingerprint that `status --explain` reads through `expo-constants`; on 57 the installed app cannot be checked. - The CLI targets SDK 58 and the 57 fallbacks are to be removed, so the dogfood app should not keep exercising them. ## What changed `expo install --fix` against SDK 58, with two versions held one patch back by the repo's two-day minimum release age in `bunfig.toml`: `expo` 58.0.5 instead of 58.0.6 and `@expo/metro-runtime` 58.0.12 instead of 58.0.13. `expo install --check` reports only those two. A follow-up bumps them once they clear the policy. | Package | Before | After | |---|---|---| | expo | 57.0.19 | 58.0.5 | | @expo/metro-runtime | 57.0.15 | 58.0.12 | | expo-dev-client | ~57.0.18 | ~58.0.11 | | expo-status-bar | ~57.0.1 | ~58.0.3 | | react, react-dom | 19.2.3 | 19.3.0 | | react-native | 0.86.3 | 0.88.0-rc.3 | `expo config` resolves to `sdkVersion` 58.0.0. The lockfile follows. ## What to expect - The fingerprint changes, so the local `live-eas` suite needs a re-seeded finished build on EAS; the cloud-e2e workflow on this PR builds its own dev client and proves the app on 58. - No CLI code changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y line stays the dev command, the relayed refusal keeps its data, and the old simulator reader goes (llp/0031 §How `dev` uses it) A Mac without an Android SDK reaches `absent` again, because the iOS inspection answered. A NO_BOUND_DEVICE refusal keeps `dev` as its Try line; --print-url is an Or: door. The detached child's code row carries `data`, and only the Try line next to it counts. smoke keeps the How line in its boot reason and prints the acquire line. The EAS fallback asks the service only when the dotenv names a session. The booted-only simulator reader is deleted. The named tests produce the child's log through logCmdError, forward a --device the pin replaces, and pin the acquire line before the report. 0031 says the order built and the decisions taken; 0030 gains the boot-failed row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
81c0096 to
c060cd7
Compare
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y line stays the dev command, the relayed refusal keeps its data, and the old simulator reader goes (llp/0031 §How `dev` uses it) A Mac without an Android SDK reaches `absent` again, because the iOS inspection answered. A NO_BOUND_DEVICE refusal keeps `dev` as its Try line; --print-url is an Or: door. The detached child's code row carries `data`, and only the Try line next to it counts. smoke keeps the How line in its boot reason and prints the acquire line. The EAS fallback asks the service only when the dotenv names a session. The booted-only simulator reader is deleted. The named tests produce the child's log through logCmdError, forward a --device the pin replaces, and pin the acquire line before the report. 0031 says the order built and the decisions taken; 0030 gains the boot-failed row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
c060cd7 to
96718db
Compare
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y line stays the dev command, the relayed refusal keeps its data, and the old simulator reader goes (llp/0031 §How `dev` uses it) A Mac without an Android SDK reaches `absent` again, because the iOS inspection answered. A NO_BOUND_DEVICE refusal keeps `dev` as its Try line; --print-url is an Or: door. The detached child's code row carries `data`, and only the Try line next to it counts. smoke keeps the How line in its boot reason and prints the acquire line. The EAS fallback asks the service only when the dotenv names a session. The booted-only simulator reader is deleted. The named tests produce the child's log through logCmdError, forward a --device the pin replaces, and pin the acquire line before the report. 0031 says the order built and the decisions taken; 0030 gains the boot-failed row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
f0555ae to
019b827
Compare
| throw deviceUnavailableError('unreadable', { platform: 'ios', path: file }); | ||
| } | ||
| const own = read.kind === 'binding' ? read.binding : null; | ||
| const choice = chooseIosDevice({ own, inventory, reuseOnly, digest: digestForRoot(projectRoot) }); |
There was a problem hiding this comment.
[🚨 critical] Two concurrent acquisitions for the same unbound worktree both snapshot an empty inventory, so the second removes the first run's new binding and creates another simulator; both boot, but subsequent verbs target only the second one. Revalidate the inventory when the binding changed while waiting for the lock before declaring its device gone.
| if (running) { | ||
| if ( | ||
| reuseBoundDevice && | ||
| options.platform === 'ios' && |
There was a problem hiding this comment.
[🚨 critical] On Linux or Windows, repeating dev --ios --detach against a running server now calls xcrun and fails, although fresh runs intentionally skip local iOS acquisition on those hosts. Apply the same macOS guard used by bindsDevice.
| options.platform === 'ios' && | |
| process.platform === 'darwin' && | |
| options.platform === 'ios' && |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
e01db94 to
0ef7cfd
Compare
ccefad8 to
c4f7f32
Compare
Kudo
left a comment
There was a problem hiding this comment.
there're a couple places to have ${projectRoot}.expo-home. we should use path.join for it
| const root = canonicalizeExistingPath(projectRoot); | ||
| const now = Date.now(); | ||
| const file = path.join( | ||
| `${projectRoot}.expo-home`, |
There was a problem hiding this comment.
should use path.join(projectRoot, '.expo-home', ...)
aaa36f0 to
2219ccb
Compare
…d under a lock and read by every verb (llp/0031 §The module) src/deviceBinding/ holds the records, the lock, the lease, the choice, the inspect table and the rung loop of llp/0030, with the iOS boot through simctl bootstatus -b. No caller yet. The reap and the Android half arrive with llp/0032 and llp/0033. The five design files land with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ads the binding (llp/0031 §How `dev` uses it, §Readers, §smoke) The resolver binds for a serving draft before its presence probe; a building draft is bound after the refusals; every run and install step carries --device <udid>, and the open takes the bound device. dev --detach on a running server reuses the parked simulator or refuses not-reusable, and the child's DEVICE_* and NO_BOUND_DEVICE refusals reach the caller unchanged through the verdict's code row. navigate, runtime:*, status and the installed readers walk the rung loop; the EAS fallback accepts the dotenv id only. smoke's iOS boot is the registry's, left bound. The first-booted probe, pickSimulator, the iOS boot and NO_DEVICE go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…or, two worktrees get two simulators, and the docs name the bound device (llp/0031 §Tests, §Docs) The stub xcrun binds the booted simulator it lists; the smoke boot cases assert the registry file and bootstatus -b; a two-worktree dev case pins distinct E2E-CREATED-* udids on each run:ios --device. The first-booted sentences of llp/0004, 0005, 0022, 0025 and 0026 read the bound device. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y line stays the dev command, the relayed refusal keeps its data, and the old simulator reader goes (llp/0031 §How `dev` uses it) A Mac without an Android SDK reaches `absent` again, because the iOS inspection answered. A NO_BOUND_DEVICE refusal keeps `dev` as its Try line; --print-url is an Or: door. The detached child's code row carries `data`, and only the Try line next to it counts. smoke keeps the How line in its boot reason and prints the acquire line. The EAS fallback asks the service only when the dotenv names a session. The booted-only simulator reader is deleted. The named tests produce the child's log through logCmdError, forward a --device the pin replaces, and pin the acquire line before the report. 0031 says the order built and the decisions taken; 0030 gains the boot-failed row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…runtime that lists one, not the newest runtime's only iPhone (llp/0030 §Choice) iOS 27.1 lists iPhone Duo alone, and no app rendered on it. The choice now walks the available runtimes newest first and takes the first with an `iPhone <n> Pro`, else an `iPhone <n>`; only when no runtime has one does the newest runtime's first iPhone apply. The RFC's §Choice sentence says so, and §Risks gains the observation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… pass on Windows (llp/0031 §How `dev` uses it) The live-cloud suite runs dev --ios --no-open on a Linux worker to serve for an EAS session it opens itself; the acquire ran xcrun there and failed. iOS simulators exist on macOS only, so the callback binds nothing elsewhere, as on main. On Windows the function-length guard split on LF and never saw a lone closing brace in a CRLF checkout, and the binding's project root is the canonical path, which is a drive path there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o dev creates on a runtime that works (llp/0030 §Choice) simctl keeps listing such a simulator with isAvailable false. The reuse took it, bootstatus failed, the failed boot kept the binding, and every retry picked it again. The choice and the listing now read an unavailable simulator as missing: the binding is removed and a new simulator is created, and read verbs report gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…031 §Tests) Review: #134 (review 5505059377). Every e2e reader of the per-project Expo home rebuilt `${projectRoot}.expo-home` by string concatenation. One helper, `expoHomeOf(projectRoot)` in e2e/utils.ts, now returns `path.join(projectRoot, '.expo-home')`; `spawnAgentCli` and every reader use it, so the home sits inside the fixture project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2219ccb to
308d897
Compare
| if (read.kind !== 'binding') { | ||
| return null; | ||
| } | ||
| if (listing.kind === 'missing') { |
There was a problem hiding this comment.
[bug] After a failed boot, releaseWorktreeDevicesAsync snapshots the udid and origin, lists that simulator, then under the lock only checks that a binding file still exists. It never checks that the file still names that udid. A missing listing deletes whatever binding is there now, and any other listing expires that binding and may simctl shutdown the snapshotted udid. A concurrent acquire that replaced the binding therefore loses its file or its lease, and the shutdown can hit the simulator this release listed rather than the one the file names. The same section also ignores a rewrite of the same udid: a second acquire that already renewed the lease and booted still gets that lease expired and the simulator shut down.
Suggestion: Under the lock, return without writing when the device id differs from the snapshot, or when boundAt or expiresAt changed. Only then delete, expire, or shut down.
| if (outcome === 'lost') { | ||
| throw noBoundDeviceError('gone', { platform, cause: 'expired', path: inspection.path }); | ||
| } | ||
| return binding.device; |
There was a problem hiding this comment.
[suggestion] useBoundDeviceAsync treats any unexpired binding as extended and then returns the device from the earlier inspection. extendLeaseAsync does not compare device ids, so a rebind to a different simulator between the inspect and the lock still counts as success. findBoundDeviceAsync then deep-links that old udid (navigate, runtime:reload, runtime:stop) while the file names the new one. The lock-timeout warn-and-continue path is the documented one; this success path is not.
Suggestion: If the binding under the lock has a different device id, treat the outcome as lost instead of extending it and returning the inspected device.
|
|
||
| /** An `iPhone <n> Pro`, else an `iPhone <n>`, by the name `simctl` lists the type under. */ | ||
| function mainstreamIphone(types: { identifier: string; name?: string }[]): string | null { | ||
| for (const pattern of [/^iPhone \d+ Pro$/, /^iPhone \d+$/]) { |
There was a problem hiding this comment.
[suggestion] mainstreamIphone only accepts iPhone <n> Pro and iPhone <n>. iPhone <n> Pro Max matches neither, and find keeps the first Pro in simctl order rather than the newest model. parseNewestIosRuntime then returns on the first runtime with a match, so a newer runtime whose only pro phone is a Pro Max loses to an older runtime's Pro.
Suggestion: Treat iPhone <n> Pro Max as mainstream and prefer the newest matching model on the newest runtime that has one, before falling through to an older Pro.
| @@ -0,0 +1,53 @@ | |||
| # 0032: `dev` binds its own Android emulator instance | |||
|
|
|||
| **Type:** Plan | |||
There was a problem hiding this comment.
if these llps are all for "one-device-per-worktree", it would be good to merge these into single llp rather then having multiple. that would be easier to find correct llp in the future
after merge the llp, you may need to ask your agent to review all implementation code in this pr or upstack pr to link to correct llp number and section
PR 3a of llp/0030 (one device per worktree), based on main after the prep in #133 merged. Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. The five design files
llp/0030tollp/0034land in the first commit.The question
How does a worktree get an iOS simulator nobody else has, and how do the other verbs find it?
What changes for an agent
dev --iosbinds a simulator this CLI created for the worktree, namedagent-cli <digest>, boots it withsimctl bootstatus -b, and runs everyexpo run:iosand install step with--device <udid>. The binding is a JSON file under<expo home>/agent-cli/bindings/, held by a 60 min lease that use renews; every write runs under one machine-wide lock. Two worktrees get two simulators.navigate,runtime:reload,runtime:stop,statusand the installed-app readers read the binding and never list devices to choose one. Without a platform flag, anyupbinding wins. A worktree with no usable binding is refused withNO_BOUND_DEVICEand the onedevcommand that fixes it. Main'sNO_DEVICEandNO_IOS_DEVICEare gone.dev --detachon a running server boots the worktree's own parked simulator and refusesnot-reusableotherwise. A device refusal in the detached child reaches the agent with its own code, exit and How line, through the child verdict.smoke --iostakes its simulator from the binding and leaves it bound; it no longer shuts down the simulator it booted.adb-device rung is injected into the rung loop.--easaccepts the.env.eas-simulatorid only, until PR 5 binds sessions.Commits
src/deviceBinding/with its unit tests and the five LLPs. No caller.withDevice, the detach relay, the rung loop in every reader, smoke, the deletions.Decisions taken while implementing (to fold back into llp/0031)
Error code <CODE> exit <N>), written only when__EXPO_AGENT_CLI_DETACHED=1.DEVICE_UNAVAILABLEreasonboot-failed, exit 20, "run the command again".withDeviceruns after the--planreturn and the refusals; main's port block stays before--plan, because the plan prints the port.installNeededfrom the boot result: a created simulator's disk holds nothing.status's next action still names the plan'sdev --ioscommand, not the--detach --wait-readyform; changing it touches every status snapshot.src/deviceBinding/hasios.ts,errors.ts,events.tsandtools.tsbeyond the RFC's file list, all within the 250-line budget.Verification
bun run format:check.AGENT_CLI_NO_DEVICE=0on macOS.navigatewall time, a coldsimctl create).🤖 Generated with Claude Code