Repository navigation
[agent-cli] A runtime read in a bound worktree reads only the device bound to it - #206
Merged
vonovak merged 15 commits intoOct 30, 2026
Conversation
This was referenced Oct 24, 2026
Merged
vonovak
marked this pull request as ready for review
October 24, 2026 03:41
vonovak
marked this pull request as draft
October 24, 2026 05:08
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 26, 2026 17:44
d17776a to
da5a399
Compare
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 26, 2026 18:24
da5a399 to
edb4c99
Compare
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 27, 2026 17:53
edb4c99 to
9aab98b
Compare
vonovak
added a commit
that referenced
this pull request
Oct 27, 2026
…bound to it, with or without a platform flag (llp/0035 §Readers) Fold #205 into #206. ScopeDevice is a device value; scope.bound records whether it came from this worktree's binding. The shared resolver owns binding selection, file expiry, conditional simulator confirmation and placement. Smoke reads local then cloud binding files until its memoized device resolution supplies a device. The four smoke file-binding cases prove expiry, shut-down fallback, exclusion of a second simulator, and a named development build that needs no device-tool read. Runtime preflight tables retain flag precedence, cloud fallback and refusal hints. The reload report exposes the scope and the bound-device bundle-platform source. Validation: 5755 unit tests, all type checks, lint, format; 35 preflight e2e tests. Code lines: smokeAsync 774; smoke/scope 61; reloadAsync 1027; resolver 104.
vonovak
changed the base branch from
vonovak/fix-runtime-scope-bound-device
to
vonovak/fix-runtime-skip-foreign-apps
October 27, 2026 17:53
vonovak
added a commit
that referenced
this pull request
Oct 27, 2026
…bound to it, with or without a platform flag (llp/0035 §Readers) Fold #205 into #206. SelectedDevice is a device value; targetSelection.bound records whether it came from this worktree's binding. The shared resolver owns binding selection, file expiry, conditional simulator confirmation and placement. Smoke reads local then cloud binding files until its memoized device resolution supplies a device. The four smoke file-binding cases prove expiry, shut-down fallback, exclusion of a second simulator, and a named development build that needs no device-tool read. Runtime preflight tables retain flag precedence, cloud fallback and refusal hints. The reload report exposes the scope and the bound-device bundle-platform source. Validation: 5755 unit tests, all type checks, lint, format; 35 preflight e2e tests.
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 27, 2026 21:29
9aab98b to
3f8611e
Compare
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 27, 2026 21:52
3f8611e to
3cc1848
Compare
vonovak
added a commit
that referenced
this pull request
Oct 27, 2026
…bound to it, with or without a platform flag (llp/0035 §Readers) Fold #205 into #206. SelectedDevice is a device value; targetSelection.bound records whether it came from this worktree's binding. The shared resolver owns binding selection, file expiry, conditional simulator confirmation and placement. Smoke reads local then cloud binding files until its memoized device resolution supplies a device. The four smoke file-binding cases prove expiry, shut-down fallback, exclusion of a second simulator, and a named development build that needs no device-tool read. Runtime preflight tables retain flag precedence, cloud fallback and refusal hints. The reload report exposes the scope and the bound-device bundle-platform source. Validation: 5755 unit tests, all type checks, lint, format; 35 preflight e2e tests.
vonovak
added a commit
that referenced
this pull request
Oct 27, 2026
…bound to it, with or without a platform flag (llp/0035 §Readers) Fold #205 into #206. SelectedDevice is a device value; targetSelection.bound records whether it came from this worktree's binding. The shared resolver owns binding selection, file expiry, conditional simulator confirmation and placement. Smoke reads local then cloud binding files until its memoized device resolution supplies a device. The four smoke file-binding cases prove expiry, shut-down fallback, exclusion of a second simulator, and a named development build that needs no device-tool read. Runtime preflight tables retain flag precedence, cloud fallback and refusal hints. The reload report exposes the scope and the bound-device bundle-platform source. Validation: 5755 unit tests, all type checks, lint, format; 35 preflight e2e tests.
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 27, 2026 22:01
3cc1848 to
6fa246c
Compare
This was referenced Oct 27, 2026
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 30, 2026 08:41
3204714 to
2480717
Compare
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 30, 2026 09:09
2480717 to
9124f48
Compare
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 30, 2026 09:21
9124f48 to
d3df277
Compare
…bound to it, with or without a platform flag (llp/0035 §Readers) SelectedDevice is the device a run reads. targetSelection.bound says that it came from this worktree's binding. The shared resolver picks the scope for runtime reads and smoke: this worktree's local binding file on the run's platform while its lease is live, else the one live cloud binding, else the flags. No inspection decides the scope. A shut-down bound simulator has no app, so a read refuses and names it, and smoke boots it. Only an own binding file that cannot be read refuses, with NO_BOUND_DEVICE. A local simulator scope reads only the app with the simulator's name. A local Android scope reads the app whose device name carries the bound serial's model. The target's model and adb's model:<name> are compared in one spelling, every character that is not a letter or digit as _, so a "Pixel 9" app is on model:Pixel_9. When another attached device has that model, the run asks adb reverse --list once on each of them, and reads only when the bound serial alone reverses this dev server's port. Otherwise it refuses and names the forward to remove. An EAS Simulator Android scope skips an app on an attached device of its model, as an iOS scope skips an app on a booted simulator. Only the tools of the scope's platform must answer: an Android session reads its app when simctl failed, and a host with no adb has no attached device. The inventory keeps attached Android devices once, with their serials. A read beside a shut-down bound simulator and a live session of this worktree reads nothing, and the refusal says that only a command that takes --eas reaches the session. The preflight prints the scope on stderr. The reload report names the scope and the bound-device source of its bundle platform. The per-device rows and their tests are in llp/0005 §Which app a run reads; the rung order, refusals and known limits are in llp/0035 §Readers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bound-device rows that the bound-selection e2e table already pins go. New e2e rows: two emulators of one model that neither reverses the port, a bound emulator that is not attached, a cloud Android session beside an arm64 emulator, smoke on a bound simulator whose app appears late, and the refusal text when a cloud scope excludes another worktree's local app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runtime:reload selects its bound cloud platform read the host's real adb, so an attached emulator of the cloud app's model claimed that app and failed the test. A stub adb with no device keeps it to the fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
llp/0005 and llp/0035 cite the bound-selection e2e tables, the smoke-binding cases and the late-target smoke case in place of the unit tests they replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…R before it reads a binding (llp/0035 §Readers) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reads the bound device's app (llp/0035 §Readers) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, with the device's id and name (llp/0035 §Readers) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns, as the preflight's does (llp/0035 §Readers) emptyScopeError takes the device inventory as a required argument, so no caller can drop the twin text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le EAS binding, and skips the session under AGENT_CLI_NO_DEVICE (llp/0035 §Readers) Live-session sentence moves into the Why. An unreadable cloud binding file refuses NO_BOUND_DEVICE. AGENT_CLI_NO_DEVICE=1 skips the cloud binding unless --eas. The twin How line follows the reverse list. E2E rows pin each case; the stub adb gains STUB_ADB_REVERSE_FAIL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… while adb is silent (llp/0005 §Which app a run reads) The scope-wide adb rule now holds only an Android scope; with no platform, a silent adb holds the Android-shaped targets alone. The preflight test mock keeps its booted-simulator list beside the adb get-state answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pp on that simulator A bound simulator scopes smoke to targets that carry its name, so a nameless target is another device's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an EAS session binding still scopes the run (llp/0035 §Readers) navigate resolves its device on its own path and keeps the session binding there, so skipping it in the runtime scope alone made --print-url and navigate disagree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no platform (llp/0005 §Which app a run reads) Classification holds only Android-shaped targets on a silent adb with no platform; the retry reads its own, wider predicate, scopeToolsSilent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e no answer, for placement and for the wait's retry The placement rule and the retry rule asked two predicates that differed by one platform check; both now ask silentTool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the refresh now takes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
force-pushed
the
vonovak/fix-cloud-scope-own-target
branch
from
October 30, 2026 09:36
d3df277 to
05ffdd2
Compare
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
…onnection (#202) **Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR gives every runtime command one decision about which apps a run reads, so each layer above changes one place. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 1/8). ## Problem On main, each runtime caller (reads, waits, the bundle check, smoke) builds its own scope from the flags, and the device facts from simctl and adb are mixed with decisions. smoke also keeps a failed device lookup for the rest of the run, so a later phase cannot recover. ## Fix `TargetSelection` is the run's scope, `DeviceInventory` holds the device tools' facts, and one resolver classifies the targets. `RuntimeConnection` carries the classified targets, and every reader, wait and deferred command reads `classifiedTargets.read`. An attachment wait keeps the facts for an unchanged list and reads them again, within its budget, for a changed list. smoke keeps a successful device choice until a boot or a session start replaces it, and does not keep a failed lookup. The rules are in llp/0005 §Which app a run reads. Red test: smoke targetSelection-test "asks the device tools once for a list that does not change" (4 simctl calls before the fix, 1 after). Also: "retries a failed device lookup, shares concurrent discovery, and retains the successful choice". Live check: run at the stack top only, not at this layer. The local iOS simulator runs (#207, #218) and the EAS session runs (#203, #200) go through this layer's scope, classifier and inventory refresh. None of them counted device-tool calls or forced a failed lookup, so this layer's claim rests on its red tests. Watch: an unscoped wait now asks the device tools once for each changed list that holds an unplaced target. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
…ps on local devices (#203) **Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR makes an EAS Simulator scope read the session's app and skip the apps that local devices run. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 2/8). ## Problem The app on an EAS Simulator iOS session registers under the cloud simulator's model name, for example `iPhone 17`. No local tool reports that name, so nothing placed the target (DF3, N2-06, N2-09). `runtime:tree --ios` refused with "No app connected", and smoke reported `app` unknown after 192 s. ## Fix On an EAS Simulator scope for iOS, or with no platform flag, a target that a local device tool names is skipped. The one target that nothing places is the session's app. While the scope's device tools give no answer, the targets stay undetermined. `--eas` alone is a cloud scope. An attachment wait reads the facts again for a changed list. It asks a tool that gave no answer again at most once a second, and its first read counts as the first ask. Red tests: runtime targetSelection-test "holds every unclaimed target while the local simulators are unknown"; smoke targetSelection-test "asks a failing simctl again at most once a second while the wait polls"; preflight-test "should refuse two apps nothing places, and say how to name the session's platform"; e2e cloud-simulator-test "reloads the session's app, not the app on a booted local simulator" and "reads neither unclaimed app with no platform". Live check: at the stack top, on an EAS iOS session (Expo Go, v2 tunnel) with a local simulator app on the same dev server: `runtime:reload --eas` skipped the local app as `other-device` and proved 2 of 2 reloads by `debugger-context-replaced`; `runtime:reload --ios` skipped the cloud app and proved 3 of 3 the same way. The `live-cloud` suite passed 17 of 17 at the stack top. Watch: a session whose model name equals a booted local simulator's name is skipped as a local app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
**Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR makes runtime reads and smoke skip an app on a simulator that another worktree bound. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 3/8). ## Problem A booted simulator that another worktree bound loads its JavaScript from `127.0.0.1:8081`. When this worktree's dev server takes port 8081, that app connects to it, and runtime reads read it as this worktree's app (DF22–DF24, root cause DF29). In worktree `mapfilter`, bound to an Android EAS session, `runtime:tree --json` returned `bundle.platform: "ios"`. It read worktree `legend`'s simulator, with old JavaScript. ## Fix A target is another worktree's app when every booted simulator with its device name has a udid that only other worktrees bind with a live lease. When the simulator list cannot be read, a target whose name only other worktrees' bindings hold is skipped. Runtime reads, the debugger client and smoke skip that app, with or without a platform flag, and name the device and the worktree on stderr. `runtime:reload` lists it in `skippedApps` and does not check its platform's bundle. A released or expired binding does not count. smoke names the skipped apps once per run, in its report and on stderr. A runtime read whose list empties after the preflight refuses like the preflight: `NO_APP_CONNECTED`, exit 22. An `--android` run does not ask `simctl` because another worktree holds an iOS lease. The no-app refusals live in `scopeRefusal.ts`, so the debugger client refuses the same way without importing the preflight. Red test: targetSelection-test "skips a target with a foreign binding's name when the simulator list could not be read". Also: e2e runtime-target-selection-test "runtime:eval skips a renamed foreign simulator and names its worktree". Live check: run at the stack top only, not at this layer. The local iOS simulator and EAS session runs (#207, #203) go through this layer's binding lookup, but no app from another worktree was attached, so this layer's claim rests on its red tests. Watch: at this layer, a broadcast reload still reaches the skipped app. #207 stops that. Not in this PR: one `EmptyScopeReason` model for the refusal texts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
…217) **Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR moves the reload broadcast into its own module, so the change in #207 to what a reload reaches reads as a behavior diff. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 5/8). ## Problem The command-socket rung (the dev server's reload broadcast and the peer-churn wait after it) is inside `src/runtime/reload/reloadAsync.ts`. That file also holds the reload policy, the evidence wait and the relaunch. #207 changes this rung. A move inside the same diff would hide that change. ## Fix A pure move, with no change in behavior. `reloadOverDevServerAsync`, the `CommandSocketChurn` type, `PEER_CHURN_TIMEOUT_MS`, `PEER_POLL_INTERVAL_MS` and the private `waitForPeerChurnAsync` move to the new `src/runtime/reload/rungOne.ts`. The two constants are exported because `reloadAsync.ts` still reads them. `reloadAsync.ts`, `smoke/smokeAsync.ts` and `reloadAsync-test.ts` import them from `rungOne.ts`. The `reloadAsync.ts` code-line cap goes down to 922. Red test: none, because the move changes no behavior. The unit suite passes with only import changes. Live check: run at the stack top only. This layer moves code and changes no behavior. At the top, rung 1 broadcast on a local iOS simulator and was proved by `message-socket-peers` on 3 of 3 reloads. Watch: `reloadAsync.ts` and `rungOne.ts` share the two churn constants until the next split. Not in this PR: the split of `reloadAsync.ts` into evidence, relaunch and report modules. #207 starts it with `evidence.ts`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
…ext (#218) **Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR gives #207 a proof, per debugger target, that a reload replaced that app's JavaScript context. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 6/8). ## Problem A `Page.reload` keeps the target's page id, so a fresh target cannot prove it. On main, nothing proves that one target's context was replaced. An acknowledged `Page.reload` does not show that the app reloaded. ## Fix `jsContextReplacedAsync` sets a global with a per-check name, takes the action, and reads the global on the same target id until it is gone. One debugger session owns the marker, the action, the reads and the cleanup. A context that cannot keep the marker is unmarkable, and the action is not taken. An action that the app answers with a CDP error is thrown. An action whose answer is lost may have run, so the marker is read anyway. Each phase has its own bound, and an absolute deadline bounds the whole proof. The open handshake counts against a request's budget, and a request that times out names the whole budget the caller gave. The phase table is in llp/0005 §What proves a reload. Red tests: contextReplaced-test "does not mistake another concurrent check for a replaced context"; "the proof of a Page.reload" rows "reads the marker when the socket drops before Page.reload answers" and "does not prove a reload whose answer was lost and whose context stayed"; cdpClient-test "should name the caller's budget when the socket opened late". Live check: at the stack top, on an SDK 58.0.21 development build on a local iOS 27.0 simulator, with a second app on another simulator so that the run skips one app: `runtime:reload --ios --json` reported `verifiedBy: debugger-context-replaced` (`outcome: context-replaced`) on 4 of 4 reloads, exit 0. On an EAS iOS session (Expo Go, v2 tunnel) with a local simulator app on the same dev server: `runtime:reload --eas` skipped the local app as `other-device` and proved 2 of 2 reloads by `debugger-context-replaced`; `runtime:reload --ios` skipped the cloud app and proved 3 of 3 the same way. On Android at the stack top, with Expo Go on a local API 29 emulator and on an EAS Android session on one dev server: `runtime:reload --android` skipped the cloud app and proved 3 of 3 by `debugger-context-replaced`; `runtime:reload --android --eas` skipped the local app and proved 3 of 3 the same way. Watch: on Android the proof was seen with Expo Go only, not yet with a development build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
…skips an app (#207) **Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR makes a reload reach only this worktree's apps when the run skips an app, and prove the reload with this worktree's apps only. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 7/8). ## Problem The dev server sends its reload broadcast to every app on its port. N2-17: a worktree bound to an iOS EAS session ran `runtime:reload`. It printed "Skipped 1 app on local simulators … iPhone 17 Pro", then "Reloaded yes · via the dev server's client command socket … 2 client(s)". The broadcast also reloaded the skipped app. ## Fix Rung 1 is decided on one target list read after the bundle gate. While the scope skips an app, rung 1 sends no broadcast. It sends CDP `Page.reload` to each own target and proves each with the context marker, under one shared deadline. When nothing is skipped, rung 1 stays the broadcast, and the attempt says when it also reached an app outside the scope. Peer churn beside an app outside the scope makes the attempt `ok: false, delivered: true`: the reload then needs a fresh own target or a bundle. Nothing of an app outside the scope proves the reload. The failure text says whether nothing was watched, the churn was discounted, or the watched observations did not happen. When no own app could keep the marker, it says no `Page.reload` was sent. It names `--eas` and the tunnel only on a cloud run. `Page.reload` has 8 s to prove itself, and the first proof stops the other checks (`stopped`). A `Page.reload` whose answer was lost is not delivered. When the read before rung 1 fails, the run keeps its first placement and sends nothing (mechanism `none`). After a relaunch that nothing can prove, local or cloud, the run exits 22 at once instead of waiting out `--timeout`. CI runs the reload cases that need a local simulator on the macOS job. Red tests: rungOne-test "a broadcast beside an app nothing places: no bundle proves it"; e2e runtime-target-selection-test "sends Page.reload to its own app only, and a page of the skipped app proves nothing ($pageReload)", "sends no Page.reload when its own app cannot keep the marker" and "runtime:reload does not take another platform's churn as its proof". Live check (before the 8 s window and the relaunch exit; not rerun since): at the stack top, on an SDK 58 development build on a local iOS simulator. With a second app on another simulator, the run skipped it as `other-device`, sent `Page.reload` to its own app only, and proved 4 of 4 reloads by `debugger-context-replaced`. With one app, it broadcast and proved 3 of 3 by `message-socket-peers`. On an EAS iOS session (Expo Go, v2 tunnel) with a local simulator app on the same dev server: `runtime:reload --eas` skipped the local app as `other-device` and proved 2 of 2 reloads by `debugger-context-replaced`; `runtime:reload --ios` skipped the cloud app and proved 3 of 3 the same way. Watch: when rung 1 broadcasts, an app that attaches after the last target list also gets the broadcast. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
added a commit
that referenced
this pull request
Oct 30, 2026
**Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.** This PR makes `navigate` decide "attached" from the device it opens. Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 8/8). ## Problem `navigate` counted the attached apps of every platform. N3-25 (also DF22): after `dev --android --eas`, with only a local `iPhone 17 Pro` app on port 8081, `navigate "/street/…"` with no flag counted the iOS app as attached. It skipped the launcher, opened only the route link, and exited 22 after 120 s. On Android, the stuck-app recovery could also stop an app that another device ran on the same dev server. ## Fix `navigate` validates the route, resolves the device, and then reads the attached apps of that device's scope to decide the app kind, the route URL and the launcher. One reader serves the launcher wait and the route wait. It reads the device facts again only for a changed list, within the budget of the wait that polls. So a cloud app that attaches after the accepted Expo Go dialog counts. When an Android twin of the bound device is attached, `navigate` reads `adb reverse` before the launcher decision and again after this run's own `adb reverse`. When nothing attached on Android, the recovery stops `--app-id`, else the app config's app, else Expo Go. smoke hands its device to the route open, so the open and the waits are about one device. `--print-url` reads the scope as `navigate` does, `adb reverse` included, once the dev server lists an app. The recovery stops Expo Go when this run opened it, and says the app was stopped only when `pidof` saw it running. The first commit is a pure move of route URL resolution to `src/navigate/routeResolution.ts`. The second commit holds the behavior. Red tests: navigate-target-selection-test "no flag, bound Android emulator, only an iOS app and another model's Android app attached"; navigateAsync-test "asks adb for the device list once while the attach wait polls an unchanged list"; e2e cloud-simulator-test "counts the app that attaches after the dialog is accepted"; navigate-target-selection-test adds three Android twin rows, one of them "no flag, bound Android emulator beside a twin, neither reverses the port before this run". Live check: the `live-cloud` iOS suite passed 17 of 17 at the stack top on an EAS session: `navigate / --eas` exit 0, attached after 3 ms. Plain main gives the same results. Watch: after a wait's budget ends, its later polls keep the last device facts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every runtime command acts on the device bound to this worktree, and nothing else attached to the same dev server, and says which device it acted on.
This PR makes the device bound to this worktree the scope of every runtime read and of smoke, with or without a platform flag.
Stack: #202 → #203 → #204 → #206 → #217 → #218 → #207 → #200 (this is 4/8).
Problem
N2-33: a worktree bound to an iOS EAS session ran
runtime:tree, which read a localiPhone 17 Proapp with the same bundle id and named the session as the device it read. N2-30: on the same kind of worktree,runtime:tree --iosrefused with "No app connected" while the session's app was connected.Fix
The scope is this worktree's local binding on the run's platform while its lease is live, else the one live cloud binding, else the flags. A shut-down bound simulator has no app, so a read refuses and names it, and smoke boots it. On Android, the run reads the app whose device name carries the bound serial's model, in the spelling
adb devices -luses (Pixel 9ismodel:Pixel_9). With two devices of that model, only the one that alone reverses the port is read. An EAS Simulator Android session skips an app on an attached emulator of its model. A wait asksadb reverseagain for a changed list, so a twin that attaches mid-run cannot authorize a read. One function,silentTool, names the device tool that gave no answer, for placement and for the wait's retry. A command with no dev server refusesNO_DEV_SERVERbefore it reads a binding; an unreadable EAS binding refusesNO_BOUND_DEVICE.AGENT_CLI_NO_DEVICE=1skips only this machine's bindings, so an EAS session still scopes the run.runtime:reload --jsonnames the scope underscope: platform, how it was chosen, whether it is bound, and the device's backend, id and name.Red tests: targetPlatform-test "matches %s to the attached Android device adb names model:Pixel_9"; e2e runtime-bound-selection-test "eval with android-cloud, flag none, reads androidSession"; targetSelection-test "classifyTargets on a bound phone whose model adb spells differently". Also: e2e "runtime:tree refuses with NO_APP_CONNECTED and does not name the session as read".
Live check (adb, on an API 29 emulator whose
Build.MODELisAndroid SDK built for arm64):adb devices -lprintedmodel:Android_SDK_built_for_arm64, and the match accepted the React Native nameAndroid SDK built for arm64 - 10 - API 29.adb reverse --listprintedhost-14 tcp:8197 tcp:8197; the parser readtcp:8197/tcp:8197. At the stack top,runtime:reload --androidread the bound emulator by that model, with an EAS Android app on the same dev server skipped, on 3 of 3 runs.Watch: no physical phone was checked.
Not in this PR: one device type for
SelectedDeviceandNavigateDevice; oneEmptyScopeReasonmodel for the refusal texts.🤖 Generated with Claude Code