Skip to content

[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
vonovak/fix-runtime-skip-foreign-appsfrom
vonovak/fix-runtime-scope-bound-device
Closed

vonovak wants to merge 4 commits into
vonovak/fix-runtime-skip-foreign-appsfrom
vonovak/fix-runtime-scope-bound-device

Conversation

@vonovak

@vonovak vonovak commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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:reload then 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):

  1. dev --android --eas --detach --wait-ready on port 8081. Another booted iOS simulator runs the same app id and also connects to 8081.
  2. 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.
  3. Night 3, an iOS session on 8081 beside a local iPhone 17 Pro with old JavaScript: runtime:tree (no flag) printed testID=calm-summary "339 of 359 streets are calm". That screen is not in this worktree's code. The agent lost 6 minutes, then used raw agent-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

vonovak and others added 4 commits October 26, 2026 19:17
…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
vonovak force-pushed the vonovak/fix-runtime-skip-foreign-apps branch from 40adfd1 to b7424ef Compare October 26, 2026 18:24
@vonovak
vonovak force-pushed the vonovak/fix-runtime-scope-bound-device branch from 8f5d642 to 3ad9ec9 Compare October 26, 2026 18:24
@vonovak
vonovak removed this pull request from stack #210 October 27, 2026 17:52
@vonovak
vonovak force-pushed the vonovak/fix-runtime-skip-foreign-apps branch from b7424ef to a994a61 Compare 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. 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 vonovak closed this Oct 27, 2026
@vonovak
vonovak deleted the vonovak/fix-runtime-scope-bound-device branch 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 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant