Repository navigation
[agent-cli] dev --device binds a device the user names (llp/0035 §--device on dev) - #151
Conversation
fe2f781 to
10dae9f
Compare
10dae9f to
bd8e010
Compare
bd8e010 to
0edd118
Compare
2d11115 to
eeda9d5
Compare
eeda9d5 to
71e220f
Compare
71e220f to
a79fa7b
Compare
| ...withDevice(plan, deviceIdOf(device)), | ||
| explicitDevice: { refuses: false, summary }, | ||
| }; | ||
| } catch (error) { |
There was a problem hiding this comment.
[bug] dev --plan --device catches every preview failure, stores it as explicitDevice.refuses, and returns 0. That includes create-timeout (exit 22 on a real run), an unreadable binding, and tool failures such as ADB_NOT_RUNNABLE. Those are not device-choice refusals. refusal() and the tool errors leave suggestedCommand unset, so how is null: the Device line keeps only the first sentence, the How is dropped, and the dev follow-up is skipped because refuses is true.
Suggestion: Catch only explicit-not-found, explicit-bound, own-explicit-over-owned, and not-reusable. Rethrow tool, timeout, and unreadable errors so --plan fails the same way the run does.
| * What `dev --device` does to devices when this plan runs, or the refusal it will meet, with the | ||
| * refusal's How command. Read without writes or boots. Absent without `--device`. | ||
| */ | ||
| explicitDevice?: |
There was a problem hiding this comment.
[bug] explicitDevice is part of the plan, and dev --plan --json prints it because emitStartPlan spreads the plan object. The cli:start_plan event still sends only the fields declared in src/plan/events.ts (src/plan/emit.ts builds that payload and does not copy explicitDevice). An agent that reads the event stream sees a normal plan when this one will refuse or bind a named device.
Suggestion: Add explicitDevice to the cli:start_plan payload, null when the plan has none, the same way devServerPort is copied.
| if (!isCurrent()) return null; | ||
| return bindAndroidSectionAsync(projectRoot, { inventory, reuseOnly, tools }); | ||
| } | ||
| const candidates = inventory.runningSerials.map((serial) => ({ |
There was a problem hiding this comment.
[bug] Android --device candidates are only runningSerials. parseAndroidDevices keeps state device and drops unauthorized and offline. preview.ts builds the same list, and listedSerials is parsed but not used for the match. A serial adb lists in one of those states, including this worktree's own explicit USB phone, becomes explicit-not-found ("No device uniquely matches"). The How says to run adb devices, which shows that serial, and the same --device fails again. The owned spawned emulator is the only exception, and only while its pid is alive.
Suggestion: If the query equals a listed serial whose state is not device, refuse with that state and a How that says to authorize or reconnect. Do not call it a miss. Keep state device as the set that can actually be bound.
| platform, | ||
| own ? { bound: deviceNameOf(own.device), named: deviceNameOf(device) } : undefined | ||
| ); | ||
| if (own?.device.backend === 'local-android' && own.device.origin.kind === 'spawned') |
There was a problem hiding this comment.
[bug] own-explicit-over-owned applies to every own spawned binding, with no pid check. A dead emulator still rejects --device <other serial> with "This worktree runs the emulator instance …", which is false. Reap skips this worktree's own files, so the dead binding is not removed on the way out. Without --device, a dead pid is already treated as gone and replaced. Naming that same dead serial is also wrong: it is not added to the candidates, so it is reported as no match.
Suggestion: Throw own-explicit-over-owned only when tools.isPidAlive(emulatorPid). When the pid is dead, drop the binding and continue the explicit choice.
| ): Promise<StartPlan> { | ||
| const selectedPlatform = boundPlatform(options); | ||
| const explicit = explicitRequestOf(options.device, options.callerArgv); | ||
| if (selectedPlatform && explicit) { |
There was a problem hiding this comment.
[bug] Plan mode previews --device and returns before the implicit-EAS stop. On a host that can see a device but cannot build here, dev --android --device <serial> --plan (adb present, no toolchain) or the iOS equivalent can print "Will bind …" and pin run / install steps. The same command without --plan throws EAS_ROUTE_NOT_CHOSEN and never acquires. A choice refusal is also printed for a check the run never reaches. The new EAS follow-up strips --device and says the local device is not used, which contradicts the Device line.
Suggestion: When plan.buildLocation is an implicit EAS route, skip the device preview and leave explicitDevice unset. Keep the follow-up that drops --device.
|
|
||
| return { | ||
| mode: argv.includes('--plan') ? 'plan' : 'run', | ||
| ...(device === undefined ? {} : { device }), |
There was a problem hiding this comment.
[suggestion] --device is accepted for every platform. --eas is rejected, but --web, AGENT_CLI_NO_DEVICE, and a host that cannot bind (boundPlatform is null) keep the flag and then ignore it. There is no preview and no error. The plan follow-up still repeats --device, because explicitDevice was never set, so the suggested command names a device the run will not bind.
Suggestion: Reject --device with --web as BAD_ARGS, as with --eas. When this host will not bind the named platform, refuse with that reason instead of starting a server and dropping the selection.
…evice on dev) A person who wants the agent on their own simulator or a USB phone names it with `dev --device <name|udid|serial>`. The CLI binds that device as `explicit`: release and reap never shut it down, delete it or kill it. A device bound to another live worktree is refused, and `dev --plan` says so before anything runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or package (llp/0033 §--device on dev) `dev --device <serial>` read the AVD list with `emulator -list-avds` before it matched the serial, so a ready USB phone on a host with no emulator package failed with EMULATOR_NOT_RUNNABLE, in the plan preview and in acquisition (Kudo, review 5491842950). An explicit selection matches only `adb devices -l` serials, so it now reads only that list. The spawn path still reads the AVDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in its refusal (llp/0033 §--device on dev) With this worktree's dev server running and a live binding, `dev --device <other>` refused not-reusable with "this worktree has no ios device to reuse", which was false (dogfood session 1, F7). The refusal now names the device the running server uses and the one --device names. The code, exit 20 and the dev:stop How line stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efusal leads with its How (llp/0033 §--device on dev) A plan that would refuse --device put "Will refuse" as the last Why bullet and still suggested running the plan (dogfood session 1, F8). A plan that would bind said nothing about devices, so it hid the delete of the own created simulator and the takeover of another worktree's expired binding (F6). The plan now carries explicitDevice from the same readExplicitChoice result and prints it as a Device line under the steps: "Will bind <name> (<id>)", the worktree it takes over from, and the simulator it deletes; or "Will refuse: <reason>". A refusing plan offers the refusal's How command (now also its suggestedCommand) instead of `dev --<platform>`, and still exits 0. The preview writes nothing and boots nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… §--device on dev) The Device line read "Will refuse: <reason>", and a refusal whose How command equals another follow-up's command (status --json from project-context) listed that command twice. The line now reads "Will refuse — <reason>", matching the Build line's dash, and the plan follow-ups drop a later entry whose command repeats an earlier one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd, and a plan next to a running server refuses as the run does (llp/0033 §--device on dev)
Dogfood session 2 found three gaps. With this worktree's dev server running,
`dev --device X --plan` said "Will bind" while the run refused not-reusable,
and its follow-up `dev --ios` dropped --device and --port (N4). An ambiguous
--device sent the agent to `status --json`, which lists no simulators (N5).
The explicit-bound How dropped the caller's --port (F10).
The plan now reads the choice reuse-only when the dev-server lock answers,
the same test the detached run uses. A bind plan's dev follow-up keeps
--device and a named --port. acquireDeviceAsync and the preview take
explicit as { query, rerun }, where rerun is the caller's arguments without
--device and --plan: explicit-bound repeats that command, an ambiguous match
repeats it with --device <first match>, and no match points at
`xcrun simctl list devices available` or `adb devices`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…detach, --wait-ready and --json (llp/0033 §--device on dev) The detached child raises the refusal, and its own argv lacks the flags the parent strips. The parent now passes its argv in __EXPO_AGENT_CLI_CALLER_ARGV, and explicitRequestOf builds the How command from it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m one helper, runs detached, and reads the caller argv once (llp/0033 §--device on dev) Review M1: four builders repeated the caller's `dev` command with different results. The plan refusal and bind follow-up suggested a foreground `dev`, which blocks an agent, and the bind follow-up dropped --local. `rerunDevCommand` (src/plan/rerun.ts) is now the one builder: the platform first, --plan dropped, an optional --device added, and --detach --wait-ready added when the caller had no --detach. The explicit refusals, the plan's `dev` follow-up and PORT_TAKEN_AFTER_RETRY use it; their local copies are deleted. Review M2: `__EXPO_AGENT_CLI_CALLER_ARGV` was parsed unguarded in a helper and inherited by every subprocess. `resolveDevOptions` now reads it once into `callerArgv`, only under __EXPO_AGENT_CLI_DETACHED=1, falls back to the own argv when it is malformed, and removes it from the environment. The port-taken How in a detached child uses the same caller argv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refuse foreground device replacement while the worktree server runs. Reuse bound USB devices without the emulator package, rechecking the binding before acquisition. Remove local device arguments from suggested EAS runs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e, null without --device (llp/0035 §--device on dev) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and fails as the run does on a tool error, a timeout or an unreadable binding (llp/0035 §--device on dev) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…licit, because the run stops before it binds one (llp/0035 §--device on dev) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…local device, before the probe (llp/0035 §--device on dev) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is gone, and only a live own instance refuses own-explicit-over-owned (llp/0035 §Choice) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d refuses explicit-not-ready (exit 7) and names what readies it (llp/0035 §Choice) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…very, and the recheck under the lock tests the same listing (llp/0035 §Choice) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne state (llp/0035 §Choice) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
a79fa7b to
c9884e3
Compare
…b uses it (llp/0035 §iOS binding) (#134) PR 3a of llp/0035 (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/0035` to `llp/0035 §EAS session binding` land 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 --ios` binds a simulator this CLI created for the worktree, named `agent-cli <digest>`, boots it with `simctl bootstatus -b`, and runs every `expo run:ios` and 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`, `status` and the installed-app readers read the binding and never list devices to choose one. Without a platform flag, any `up` binding wins. A worktree with no usable binding is refused with `NO_BOUND_DEVICE` and the one `dev` command that fixes it. Main's `NO_DEVICE` and `NO_IOS_DEVICE` are gone. - `dev --detach` on a running server boots the worktree's own parked simulator and refuses `not-reusable` otherwise. A device refusal in the detached child reaches the agent with its own code, exit and How line, through the child verdict. - `smoke --ios` takes its simulator from the binding and leaves it bound; it no longer shuts down the simulator it booted. - Android is unchanged until 3b: its first-`adb`-device rung is injected into the rung loop. - The EAS fallback without `--eas` accepts the `.env.eas-simulator` id only, until PR 5 binds sessions. ## Commits 1. The module `src/deviceBinding/` with its unit tests and the five LLPs. No caller. 2. The wiring: the resolver callback, `withDevice`, the detach relay, the rung loop in every reader, smoke, the deletions. 3. The e2e suite, the LLP sentences that said "first booted", and the changelog. ## Decisions taken while implementing (to fold back into llp/0035 §iOS binding) - The child verdict carries the error code and exit (`Error code <CODE> exit <N>`), written only when `__EXPO_AGENT_CLI_DETACHED=1`. - A failed boot is `DEVICE_UNAVAILABLE` reason `boot-failed`, exit 20, "run the command again". - `withDevice` runs after the `--plan` return and the refusals; main's port block stays before `--plan`, because the plan prints the port. - Smoke's install phase now honours `installNeeded` from the boot result: a created simulator's disk holds nothing. - `status`'s next action still names the plan's `dev --ios` command, not the `--detach --wait-ready` form; changing it touches every status snapshot. - `src/deviceBinding/` has `ios.ts`, `errors.ts`, `events.ts` and `tools.ts` beyond the RFC's file list, all within the 250-line budget. ## Verification - Typecheck, lint (no new warnings), `bun run format:check`. - Unit: 229 files, 4842 tests passed. Every test named in llp/0035 §iOS tests exists under that name. - Build, then e2e: 41 files, 835 tests passed; the device cases run with `AGENT_CLI_NO_DEVICE=0` on macOS. - Not yet run: the live checks of llp/0035 §iOS tests (two real worktrees, `navigate` wall time, a cold `simctl create`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…137) Each worktree gets its own read-only Android emulator instance on a free console port. `dev --android` acquires it, build/install steps receive its serial, and device commands use the recorded binding instead of whichever emulator happens to be first. The registry reserves ports held by every listed emulator, including booting/offline instances. Cleanup checks whole `-avd` and `-ports` values before killing an owned process, and validates the binding identity so release or failed-boot cleanup cannot remove a replacement binding. Windows install-test coverage is restored. ## Stack Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. Based on #134. The four review-fix commits are included in this PR, ending at `95a1184a`. ## Validation The split was previously checked with typechecks, formatting, lint and unit tests on each layer, plus focused lifecycle/dev/stop/smoke integration tests and local-device dogfooding. This publication preserves the reviewed branch tip; tests were not rerun for the stack and PR-description changes. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Devices have a 60-minute lease, but a long-running Metro or native build may go longer without a device command. Renew the worktree's leases every five minutes while the dev-server or build runner is active, including bindings created after the runner starts. Renewal stops with the runner; a lost lease is reported once. The wrappers cover `dev`, detached `dev`, `start`, and build steps. `AGENT_CLI_NO_DEVICE=1` disables renewal. Timer and wiring tests cover lease renewal. The design documents describe lifecycle behavior and dependencies. ## Stack Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. Based on #137 (`vonovak/android-device-instance`). ## Validation - Unit: 5,006 pass, including timer tests and two wiring tests that fail if either wrapper is removed. - E2E: `dev-test` and `dev-detach-test`, 73 of 73. - Dogfooding: with Metro running and no verb in between, the binding's `expiresAt` moved forward at +5 and +10 minutes (read from the binding file). - Documentation update: repository formatting, document links and stale-reference checks pass. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Stopping Metro can leave the worktree's devices allocated. `dev:stop --release` shuts down and parks a created iOS simulator or ends a spawned Android emulator after confirming this worktree's server has stopped. Plain `dev:stop` keeps devices; explicitly selected devices are preserved. A remaining or replacement server prevents release. When stop refuses because ownership cannot be verified, JSON now reports the observed process liveness and port occupancy instead of falsely reporting that both are gone. Unmeasured facts are `null`. Missing lock evidence no longer claims the server was never started by the CLI. Signal authorization and device-release policy are unchanged. ## Stack Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. Based on #146. Binding-identity and failed-boot cleanup protections come from #137. ## Validation - 47 focused stop/release unit tests and 14 built-CLI stop tests passed; new regressions failed before the reporting fix. - At the integrated #152 tip: 5,138 unit tests across 241 files and 136 built-CLI dev-detach/status/device-lifecycle/stop tests passed. Build, typecheck, lint (existing warnings only), and formatting passed. The selected cold simulator creation/install/release smoke test passed in 127.55 seconds. The full smoke suite was not completed; partial bounded runs are not a full-suite pass. These fix regressions use subprocess/device stubs; native acceptance was not repeated for this source-fix pass. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An unattended smoke run should return the device resources it acquired. Release a device bound by smoke's started dev server through `dev:stop --release`, or release the boot phase's own binding when reusing an existing server. A device already bound and running before the run stays available; `--no-start` acquires and releases nothing. Name the bound device and its cleanup result in human output and `environment.boundDevice`. Follow-up commands account for whether the run stopped its server or released its device, and preserve the caller's port. Tests cover replacement bindings, reused servers, kept devices and iOS/Android restart guidance. ## Stack Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. Based on #147 (B, `vonovak/lifecycle-2-release`). ## Validation This layer is rebased over the stop-reporting correction in #147; its smoke implementation is unchanged. At the integrated #152 tip: 5,138 unit tests across 241 files and 136 built-CLI dev-detach/status/device-lifecycle/stop tests passed. Build, typecheck, lint (existing warnings only), and formatting passed. The selected cold simulator creation/install/release smoke test passed in 127.55 seconds. The full smoke suite was not completed; partial bounded runs are not a full-suite pass. These fix regressions use subprocess/device stubs; native acceptance was not repeated for this source-fix pass. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A deleted worktree cannot run `dev:stop --release`, and expired leases leave device resources behind. Device acquisition and `dev:stop` reap other worktrees' stale bindings: delete created simulators for deleted roots, shut down expired simulators when an iOS inventory is available, and end owned Android emulator instances. Explicit devices are preserved. Registry changes happen under the lock; device actions run afterward, including when acquisition refuses. Results appear under `reaped` in `dev:stop --json`. Cross-worktree results identify `projectRoot` and report `devServerCleanup: "not-checked"`; deleted-root human output also explains that CLI/Metro processes may remain. A late last-build metadata write skips an already deleted project root. ## Scope and limits Reaping uses lease expiry as its liveness signal. After a long host sleep it can park a simulator while Metro still runs. This PR cleans device resources; it does not terminate orphan CLI/Metro processes after worktree deletion. Safe server cleanup needs durable server ownership information and remains a separate follow-up. The metadata guard does not make the existing write sequence atomic against simultaneous directory deletion. ## Stack Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. Based on #148. The first commit extracts iOS acquisition; subsequent changes implement reaping and clarify its reporting. ## Validation - 152 device-binding/last-build/stop-lifecycle unit tests and two built-CLI device-lifecycle tests passed. Reporting and deleted-root write regressions failed before the fixes. - At the integrated #152 tip: 5,138 unit tests across 241 files and 136 built-CLI dev-detach/status/device-lifecycle/stop tests passed. Build, typecheck, lint (existing warnings only), and formatting passed. The selected cold simulator creation/install/release smoke test passed in 127.55 seconds. The full smoke suite was not completed; partial bounded runs are not a full-suite pass. These fix regressions use subprocess/device stubs; native acceptance was not repeated for this source-fix pass. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tatus sends an unbound worktree to dev (llp/0035 §Lifecycle output) (#152) `status` can recommend the wrong platform or lose a chosen Metro port when a startup has not published its lock or its server has stopped. It can also recommend device commands for a worktree whose device lease has expired. This PR reports the worktree's binding and produces recovery commands using the known development context. ## Behavior - `status --json` exposes each binding's platform, backend, identity, origin, state and expiry. Detached `dev` names its bound device in human output and under `device` in JSON. - A real `dev` invocation retains only parsed platform, requested port, device, build backend and app target in `.expo/agent-cli-last-dev.json`. Dry runs do not write it. This record is a recovery hint, not process-liveness evidence. - Status chooses the explicit internal platform override, then the saved request, then a sole bound platform, then the project default. A different explicit platform discards the other saved choices. No new public platform flags are added to `status`. - Recovery preserves an explicit local status port, the current own-server port, or the saved requested port. A port observed serving another project is omitted. Remote URL ports are not treated as local Metro requests. - Recovery uses #151's quoted retry builder and restores the dev plan steps when replacing a verification action. `rule` and `target` retain their documented plan meaning. - Rebinding guidance preserves the connected-app, web, EAS-session and unsupported-device-host exceptions. A worktree without a binding but with its own running server is told to stop that server before restarting to bind a device. ## Stack Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack. Based on #151. Review `src/dev/lastRequest.ts`, `src/status/statusAsync.ts`, `src/deviceBinding/summary.ts`, and detached dev's device reporting. ## Validation - Android startup without a lock, sole Android binding, stopped-request recovery, explicit overrides, device-name quoting, corrupt/unwritable context, dry-run behavior, foreign ports and restored plan steps have regression coverage. - A built-CLI Android start → stop → status test verifies retained platform, port and target choices. - At the integrated #152 tip: 5,138 unit tests across 241 files and 136 built-CLI dev-detach/status/device-lifecycle/stop tests passed. Build, typecheck, lint (existing warnings only), and formatting passed. The selected cold simulator creation/install/release smoke test passed in 127.55 seconds. The full smoke suite was not completed; partial bounded runs are not a full-suite pass. These fix regressions use subprocess/device stubs; native acceptance was not repeated for this source-fix pass. The saved request does not establish that a native build is running or fix separate startup-ownership issues. Device origin describes how it became bound, not what the current invocation did. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Why
A person wants the agent on their own simulator or USB phone. Without this PR there is no way to name one. Two worktrees must not share it: if worktree A holds
dogfood-explicit,dev --device dogfood-explicitin worktree B must refuse. No release or reap may shut it down.What changes
dev --device <name|udid|serial>binds that device asexplicit. Release and reap never shut it down, delete it or kill it.adb devices -l, not the emulator package.dev --plan --deviceprints aDevice:line (bind, take over, delete, or refuse), and JSONexplicitDevice. A refusing plan offers the refusal's How and exits 0.devcommand keeps its flags and adds--detach --wait-ready.Review guide
src/deviceBinding/explicit.ts. Look closely atreadExplicitChoiceandbindExplicitSection(transfer and delete under the lock).index.ts,preview.ts,emulator.ts,src/plan/rerun.ts,resolveOptions.ts(reads__EXPO_AGENT_CLI_CALLER_ARGVonce).explicit-test.ts,resolveOptions-test.ts,devAsync-test.ts.Stack
Review bottom-up in stack #150: #134 → #137 → #146 → #147 → #148 → #149 → #151 → #152. EAS session binding (#139) is separate from this stack.
Stacked on PR D (
vonovak/lifecycle-4-reap).Validation
device-lifecycle-testgains the plan block; the log records e2e green, without counts.--devicerace had one winner. Session 7 checked the How lines.AGENT_CLI_LIVE_UDIDnot run.Notes
explicitDevicegives the device facts as a sentence, not fields (M3, open).PORT_TAKEN_AFTER_RETRYusesrerunDevCommand.🤖 Generated with Claude Code