Repository navigation
[agent-cli] A no-flag runtime read scopes to the device bound to this worktree (llp/0035 §Readers) - #205
Closed
vonovak wants to merge 4 commits into
Closed
[agent-cli] A no-flag runtime read scopes to the device bound to this worktree (llp/0035 §Readers)#205vonovak wants to merge 4 commits into
vonovak wants to merge 4 commits into
Conversation
This was referenced Oct 24, 2026
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-runtime-skip-foreign-apps
branch
from
October 26, 2026 17:44
08abee4 to
40adfd1
Compare
vonovak
force-pushed
the
vonovak/fix-runtime-scope-bound-device
branch
from
October 26, 2026 17:44
c774050 to
8f5d642
Compare
…cts and one run scope (refactor) Why: the next fixes add device kinds (an EAS session, a bound simulator) and placements (another worktree's app, a local simulator outside the scope). Today "which targets does this run read" is spread over several shapes: DeviceNameIndex mixes what the device tools report with how it is used, scopeTargets takes a bare platform, each caller derives the "no platform" case itself, and CdpClient builds its own index when a caller passes none. Each new kind would add a field to each shape. What moved: - New src/runtime/runScope.ts: DeviceFacts (booted simulators with name and udid, Android models; facts only), RunScope (platform by flag or null; device kind 'any'), Placement (a union with payloads), placeTargets, and the facts readers that replace buildDeviceNameIndexAsync / IfNeededAsync, keeping "spawn simctl and adb only when a target cannot be placed without them". - The booted-simulator parser in targetPlatform.ts is gone: facts read simctl through src/device/simulators.ts, which now has one parser for both the iOS-only list and the every-runtime list. - targetPlatform.ts keeps only platform inference (platformOfTarget, platformsOfTargets). DeviceNameIndex, EMPTY_DEVICE_INDEX, ScopedTargets and scopeTargets are deleted. - preflight, smoke (its cached facts), openRoute (both sites), waitReady, bundleCheck, runtime eval/errors, interact, the network collector and the runtime reload rung pass scope and facts. CdpClient takes both as required options; its fallback index build is deleted (every production caller already passed one). - Tests: one DeviceFacts builder (makeDeviceFacts) and one unscoped client fixture replace the inline index literals; the scopeTargets case moved into an it.each table for placeTargets; the every-runtime parser test moved next to the iOS parser test. - llp/0005: the device index is called the device facts where a rule names it. No behavior change: the package unit suite passes with only mechanical test changes (5680 tests on main, 5680 here: two parser tests merged into one, the scopeTargets test became a two-row placeTargets table); the runtime-preflight, runtime-reload and cloud-simulator e2e files pass before and after (84/84). A scope with no platform reads every target and spawns no device tool, as the null-platform branches did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one unclaimed target (llp/0005 §Android)
Night-1 dogfood, part of N2-06: runtime reads on an iOS EAS session found
no target. The session's app registers the project's own app id and the
cloud simulator's model name. No local tool reports that name, so no rule
placed the target, and an iOS-scoped read said there was no app on iOS.
navigate --eas opened the launcher URL as well as the route link, because
its attach wait counted no iOS app.
The change:
- RunScope.device gains { kind: 'eas-flag' }: a run with --eas and a
platform. The session's platform is scope.platform, not stored twice.
scopeOfFlags({ platform, eas }) replaces scopeOfPlatformFlag;
scopeOfDevice(device) gives the scope of a device navigate resolved
(a cloud device is the session).
- placeTargets applies the rule to the whole list: in an iOS eas-flag
scope, exactly one undetermined target is read. Two stay undetermined.
platformOfTarget stays pure platform inference. placeTarget is no
longer exported, because one target alone cannot apply the rule.
- The runtime preflight, runtime:reload --method runtime, and navigate
(target decision and both attach waits) pass the scope. smoke builds
its scope at call time, because a config's EAS device sets --eas after
the smoke deps are built.
- llp/0005 states the rule and names its tests.
Red before:
- runScope-test "placeTargets on an EAS Simulator session > reads the
one unclaimed target on an iOS session" and "... beside a booted local
simulator" (the cloud target stayed undetermined)
- smokeDeps-test "evaluate on an EAS Simulator iOS session > reads the
one unclaimed target when a config's EAS device sets --eas after the
deps are built" ("No app connected to the dev server could be shown to
be running on ios, and 1 could not be placed at all.")
- e2e cloud-simulator-test "counts the session's development build as
the iOS app, and opens only the route link" (two links opened,
expected ['demoapp://'])
Guard: runScope-test "placeTargets on an EAS Simulator session > reads
neither of two unclaimed targets", "... reads no unclaimed target on a
run without --eas", "... on an Android session, whose names have their
own rule"; the e2e files cloud-simulator-test and runtime-preflight-test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (llp/0005 §Target owner)
Night-1 dogfood, part of N2-06, and night-3 N3-01: runtime reads reached
another worktree's app. A simulator that another worktree bound, still
booted, loads its JavaScript from the same dev server port, so runtime:*
read that app as this worktree's app, and runtime:reload checked the
bundle for its platform. The old layer (L2) matched by device name and
read every binding file with no lease check, so a released binding of
another worktree made this worktree's own app "foreign" (review F1).
The change:
- deviceBinding/records.ts: liveIosBindings(projectRoot, now) returns the
live local-ios bindings of every worktree (isStale: expired or deleted
root), keyed by udid, with own: true for this worktree's.
- DeviceFacts gains iosBindings: LiveIosBinding[]. deviceFactsForScopeAsync
takes the projectRoot; it reads the bindings for a run in a project and
spawns simctl also when a target has the name of a simulator another
worktree binds. Runs with no root (dev:wait) and navigate and smoke read
none, as the comments and the LLP say.
- Ownership rule in placeTargets: a target is another worktree's only when
every booted simulator with its device name has a udid that only other
worktrees bind. Placement gains { group: 'other-worktree'; projectRoots };
PlacedTargets gains skipped; SkipPlacement and SkippedApp derive from
Placement. One sentence builder, describeSkippedApps, for stderr, the
refusal and the client error.
- preflight: one refusal builder emptyScopeError(devServerUrl, scope,
placed); a run with only other worktrees' apps refuses with
NO_APP_CONNECTED (exit 1) and names them. A run that skips an app says so
on stderr.
- cdpClient: NoTargetOnPlatformError becomes NoTargetInScopeError(scope,
placed), a message-only error, also thrown when only skipped apps remain.
- runtime:reload: skippedApps: SkippedApp[] in the report; its bundle check
and interact's read the scoped appTargets; the --method runtime rung uses
the preflight's scope and facts instead of reading facts again. Reload
behavior is otherwise unchanged.
- llp/0005 §Target owner: the rules with their tests, and the lease
trade-off (runtime reads do not renew a lease, so after 60 min of only
runtime:* reads another worktree reads that app again).
Red before:
- records-test "a live iOS binding" (4 rows): liveIosBindings is not a
function
- runScope-test "placeTargets and the worktree that bound the simulator"
(7 rows) and "keeps a skipped target out of the read set" (3 rows): the
clone target was read; no skipped group
- preflight-test "beside an app of another worktree > should read this
worktree's app, and name the skipped one on stderr": appTargets held both
- cdpClient-test "should refuse when only another worktree's app is
listed, and name that worktree": describeSkippedApps is not a function
- reloadAsync-test "does not check the bundle of another worktree's app,
and reports that app": asked ['ios', 'android'], expected ['android']
- reloadAsync-test and e2e runtime-reload-test "stable set of top-level
keys": no skippedApps key
- e2e runtime-preflight-test "a dev server whose only app runs on another
worktree's simulator" (5 commands): runtime:eval exited 0, the others
failed with their own *_FAILED codes instead of NO_APP_CONNECTED
Guard: preflight-test "should read the app on a simulator whose other
binding ran out, and renew no lease" (the F1 regression; passes on the
parent, which has no ownership rule), runScope-test "placeTargets on an
EAS Simulator session", preflight-test "scoped to a platform", the e2e
files runtime-preflight-test and runtime-reload-test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… worktree (llp/0035 §Readers)
Night-1 dogfood DF24, behind N2-06 and night-3 N3-01: a runtime read with
no platform flag read the wrong platform. With an iOS app and an Android
app on one dev server, runtime:reload checked the iOS bundle for a
worktree bound to an Android EAS session, and the reads took whichever app
the dev server listed first.
The change:
- RunScope.platform.by gains 'bound-device'. RunScope.device gains
{ kind: 'bound'; device: BoundDevice } (main's type). The platform is
derived from the device by scopeOfBoundDevice, not stored twice.
readsEasSession(scope) covers --eas and a bound session;
describeBoundDevice is the one formatter of the scope line.
- placeTargets: a bound local simulator reads only the targets that carry
its name; the others are skipped with the new placement
{ group: 'other-device' } (in skippedApps and the skip sentence). A
bound Android device places like a platform scope. A bound iOS EAS
session keeps the one-unclaimed-target rule.
- preflight: with no platform flag, a project root, and more than one app
the read may read, the scope is the bound device: main's
findBoundDeviceAsync(root, { extend: false }), else the one live cloud
binding (main's liveCloudBindings); with --eas, the cloud binding only.
A rung that refuses or fails gives no scope. One app is read as it is.
The read prints "Scoped to <platform>: <device>, the device bound to
this worktree." on stderr. The refusal under a bound scope says to pass
the other platform's flag, not to drop one. RuntimeConnection drops its
unread platform field; commands take the platform from scope.
- resolveBundleCheckPlatformsAsync takes the run scope: scope.platform
gives [value] with source scope.platform.by. BundlePlatformSource derives
from by. interact and reload pass their scope; dev:wait passes its flag
scope (a named web is checked as named). No caller ternaries. The
foreign filter in platformsOfTargets is already gone at R2.
- runtime:reload: the report gets scope; the bundle wait counts the
scope's platform. reloadAsync.ts stays at 1677 lines.
- smoke: its facts read the live iOS bindings (it has a projectRoot), so
evaluate and collectErrors skip another worktree's app. The facts cache
is kept per target list, because facts cached for an empty list placed
the clone's Expo Go as readable. smokeAsync.ts 1293 -> 1287 lines.
- dev:wait and navigate use their flags only, on purpose (comment + LLP).
- llp/0035 §Readers: one bullet for the runtime:* rule; llp/0005 §Target
owner (smoke reads the bindings) and §Android (the scope wins the bundle
platform).
Red before:
- preflight-test "with two apps and a device bound to this worktree"
rows "no flag reads the platform of the Android device it names",
"... of the EAS Simulator session it names", "no flag reads only the
app on the simulator it names" (scope.platform was null), and "should
name the other platform's flag when no app is on the bound device's
platform" (no refusal: both apps were read)
- runScope-test "placeTargets on the device bound to this worktree" (3
rows): scopeOfBoundDevice is not a function
- reloadAsync-test "checks the platform of the device bound to this
worktree, and reports the scope": asked ['ios', 'android'], expected
['android']
- reloadAsync-test and e2e runtime-reload-test "stable set of top-level
keys": no scope key
- smokeDeps-test "evaluate beside an app of another worktree": reason
"No target found." instead of the skip sentence
Guard: preflight-test row "--ios reads the platform the flag names"
(passes before and after), preflight-test "beside an app of another
worktree", runScope-test "placeTargets on an EAS Simulator session",
reloadAsync-test "asks the dev server for the platform the connected app
is on", the e2e files runtime-preflight-test and runtime-reload-test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vonovak
force-pushed
the
vonovak/fix-runtime-skip-foreign-apps
branch
from
October 26, 2026 18:24
40adfd1 to
b7424ef
Compare
vonovak
force-pushed
the
vonovak/fix-runtime-scope-bound-device
branch
from
October 26, 2026 18:24
8f5d642 to
3ad9ec9
Compare
vonovak
force-pushed
the
vonovak/fix-runtime-skip-foreign-apps
branch
from
October 27, 2026 17:53
b7424ef to
a994a61
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
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
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.
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.
Stack #210, PR 4 of 7. All together, this stack makes every runtime command act on the device bound to this worktree and on nothing else that is attached to the same dev server, and say which device it acted on. This PR makes a read with no platform flag use the bound device's platform.
Problem
A runtime read with no platform flag read the wrong platform. With an iOS app and an Android app on one dev server, the read took the app that the dev server listed first.
runtime:reloadthen checked the iOS bundle for a worktree bound to an Android EAS session.Seen in dogfooding (night 1 DF24; night 2 N2-06; night 3 N3-01):
dev --android --eas --detach --wait-readyon port 8081. Another booted iOS simulator runs the same app id and also connects to 8081.runtime:reload(no flag) → "Apps connected 2", "Bundle served after yes · 1 · iOS Bundled 45ms", "Bundle compiles · for ios · also checked android". The bound device is Android. The reload and the bundle check were about the iOS app.iPhone 17 Prowith old JavaScript:runtime:tree(no flag) printedtestID=calm-summary"339 of 359 streets are calm". That screen is not in this worktree's code. The agent lost 6 minutes, then used rawagent-device.Fix
With no flag and more than one app connected, the scope is the device bound to this worktree (local or EAS session). The read uses that device's platform, the bundle check asks for that platform, and stderr says "Scoped to : , the device bound to this worktree."
🤖 Generated with Claude Code