refactor(platform-apple): derive the runner host port instead of mirroring host-kit - #2689
Merged
Merged
Conversation
…oring host-kit `runner/host.ts` was a hand-maintained mirror of host-kit: ~100 lines of copied type declarations, a 50-member interface re-declaring every signature, and 50 delegators, so every host-kit change was re-typed here by hand. The port now derives from the modules it fronts. `AppleRunnerHost` is a set of `Pick`s over `import type * as` namespaces -- type-only declarations R77 and the eager-closure gate see nothing of -- so a host-kit signature change reaches the runner with no edit here, and the `Pick` list is the single place that says what the runner may reach. Delegators keep their names through a `delegate(name)` helper typed by derivation, so the 28 importers are unchanged. `core/runner-host.ts` keeps naming each import rather than spreading the modules whole: spreading drops its list but puts every export of all sixteen modules on the host object (41 members to 128, +1.8 kB shipped including a vitest-only memo reset) and lets a name shared by two of them silently rebind a capability. The binding holds names only; it re-types nothing. The runner modules take the types they use straight from the owning package with `import type`. `DefinedEnvMap` was never a host capability and moves to its only caller. Host-kit gains three type exports the port needs by name -- `ExecStreamOptions`, `DiagnosticEventInput`, and `DeadlineClock`, the read side of `Deadline` that a test host substitutes a clock for -- and no behavior change. The runner-facing slice of physical-device control moves to `core/physical-device-routing.ts`, below both sides, so the port does not reach the control module, whose launch and terminate members are typed against the runner's own command executor (R9/R10 type cycle). `host.ts`: 403 -> 187 LOC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPceQTJdXsEJaZsBM8ycek
Size Report
Startup median (7 runs, lower is better):
|
Member
Author
|
Reviewed at 172f58e. No blocking findings: the port is now derived from the modules it fronts, the mirror is gone, and the change removes about 175 net production lines. The whole-module spread binding from the issue was measured and rejected in the PR body (41 to 128 members, +1.8 kB, and silent rebinding when two modules export the same name), and that reason holds. Not blocking: |
|
thymikee
added a commit
that referenced
this pull request
Sep 19, 2026
The #2689 host port derives its types from host-kit namespaces and no longer mirrors them, so the adoption test imports DiagnosticEventInput from @agent-device/host-kit/diagnostics like every other runner test.
thymikee
added a commit
that referenced
this pull request
Sep 19, 2026
…ure assertion body #2689 derived the runner host port from the modules it fronts, so `ExecResult` is read from `@agent-device/host-kit/command` now rather than restated in `runner/host.ts`. The per-fixture assertion body had grown past the complexity the Fallow audit allows a changed file: the envelope checks move into `assertFailureEnvelope` and the tool-output check into `assertToolOutputReachable`, so each recorded shape is still asserted through the same path and the test body reads as one call.
thymikee
added a commit
that referenced
this pull request
Sep 19, 2026
… host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
The #2689 host port derives its types from host-kit namespaces and no longer mirrors them, so the adoption test imports DiagnosticEventInput from @agent-device/host-kit/diagnostics like every other runner test.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
…ure assertion body #2689 derived the runner host port from the modules it fronts, so `ExecResult` is read from `@agent-device/host-kit/command` now rather than restated in `runner/host.ts`. The per-fixture assertion body had grown past the complexity the Fallow audit allows a changed file: the envelope checks move into `assertFailureEnvelope` and the tool-output check into `assertToolOutputReachable`, so each recorded shape is still asserted through the same path and the test body reads as one call.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
… host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
…ure assertion body #2689 derived the runner host port from the modules it fronts, so `ExecResult` is read from `@agent-device/host-kit/command` now rather than restated in `runner/host.ts`. The per-fixture assertion body had grown past the complexity the Fallow audit allows a changed file: the envelope checks move into `assertFailureEnvelope` and the tool-output check into `assertToolOutputReachable`, so each recorded shape is still asserted through the same path and the test body reads as one call.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
The #2689 host port derives its types from host-kit namespaces and no longer mirrors them, so the adoption test imports DiagnosticEventInput from @agent-device/host-kit/diagnostics like every other runner test.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
… host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
…ure assertion body #2689 derived the runner host port from the modules it fronts, so `ExecResult` is read from `@agent-device/host-kit/command` now rather than restated in `runner/host.ts`. The per-fixture assertion body had grown past the complexity the Fallow audit allows a changed file: the envelope checks move into `assertFailureEnvelope` and the tool-output check into `assertToolOutputReachable`, so each recorded shape is still asserted through the same path and the test body reads as one call.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
The #2689 host port derives its types from host-kit namespaces and no longer mirrors them, so the adoption test imports DiagnosticEventInput from @agent-device/host-kit/diagnostics like every other runner test.
thymikee
added a commit
that referenced
this pull request
Sep 20, 2026
… host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers.
thymikee
added a commit
that referenced
this pull request
Sep 21, 2026
… host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers.
thymikee
added a commit
that referenced
this pull request
Sep 21, 2026
The #2689 host port derives its types from host-kit namespaces and no longer mirrors them, so the adoption test imports DiagnosticEventInput from @agent-device/host-kit/diagnostics like every other runner test.
thymikee
added a commit
that referenced
this pull request
Sep 21, 2026
* refactor(ios-runner): give the host DevToolsSecurity probe its own module runner-session.ts is 1,181 lines, so the next behavior owed a split first. The host developer-tools security probe is a self-contained preflight: it reads a macOS setting for this machine, not the iPhone's Developer Mode toggle, and today nothing in its name or its home says which machine it speaks for. Pure move. The startup step key is renamed to say what it actually verifies, and the existing coverage in runner-session.test.ts carries unchanged because the probe consumes the same runner host port. * feat(ios-runner): key runner startup failures on typed reasons A `build-for-testing` failure was the one Apple failure shape with no typed reason: `runner-artifact.ts` threw COMMAND_FAILED with a hint picked by substring-matching the lowercased message plus a JSON dump of the details, and nothing downstream could switch on which signing problem it was. `AGENTS.md` keys behavior on typed reasons and details, never error text, so the build path was the exception to the rule. `classifyRunnerStartupFailure` is now the one classifier reachable from that catch, and it reads `RUNNER_ERROR_RULES` rather than adding a second table: a row either carries recovery verdicts, a `buildFailure` reason and hint, or both. The reason and the hint beside it therefore cannot disagree, and an unproven cause is never claimed -- `build_failed_unclassified` keeps the cache-recovery hint it already gave. The host `DevToolsSecurity` refusal, which had a hint and no reason at all, now publishes its own reason keyed on the typed status it read, and that reason says host so it can never be read as the device's Developer Mode toggle. `resolveRunnerBuildFailureHint` is gone, not kept as a pass-through. Reasons are recorded as fixtures carrying the tool output, the command, the Xcode, and how the line reached the file; each one is driven through the real build catch and asserted on the normalized envelope, because every case is COMMAND_FAILED and the reason is the assertion. * refactor(ios-runner): reach the host dev-tools probe only when a device needs it A static edge from `runner-session.ts` to the new module grew three Apple facade closures by one module each (app-lifecycle, doctor, runner-operations), which the eager-import-closure ratchet refuses: the runner subtree is eagerly evaluated to answer a simulator request, and this preflight only ever runs for a physical iOS device. Function-scoped import keeps the preflight where it belongs without paying for it on the paths that can never use it. * fix(ios-runner): classify a build failure only from what xcodebuild published The startup rules read the whole `details` bag as JSON, and `execFailureDetails` puts `cmd`/`args` in that bag. A caller who pinned a profile therefore handed the profile rule its trigger: an unrelated Swift compile error classified as `signing_provisioning_profile_missing` and lost the cache-recovery hint. The same bag holds the `reason` and `hint` this classifier publishes, so a re-wrapped failure would match its own verdict. `runnerToolText` now builds the one haystack a startup rule may read — our message plus the tool's stdout and stderr — and the argv case has a fixture that proves a pinned profile is not evidence (#2680). Reading only `details` also dropped the message on the floor, so a failure the exec layer raised as a plain `Error` (which the catch wraps with `String(err)`) became unclassified while the same sentence in `stdout` classified. The message is in the haystack now, and a `message-only` fixture keeps both carriers at parity. `signing_style_conflict` is removed rather than kept as a guess: the hint named a `CODE_SIGN_STYLE` env lever that does not exist and claimed a cause nothing captured. The conflicting-settings line keeps a row so the profile row below cannot answer it with missing-profile advice, but it publishes `build_failed_unclassified` until a capture shows which setting disagrees and which lever clears it. Versioned help and the installation docs no longer list the withdrawn reason, and `.device-evidence/CHECKLIST.md` gained the capture that would let a follow-up claim it. Provenance got honest vocabulary. `OBSERVED_ON` stamped invented sentences as observed on Xcode 26.2, and one invocation was recorded as the producer of seven configurations that cannot coexist; nothing here was captured, so entries now say `shipped-sniff-trigger` (the matched substrings shipped before #2680, sentence reconstructed) or `invented-shape`, carry `xcodeVersion: 'unobserved'`, and omit `command` until a run records one. * fix(apple): take the exec result type from host-kit and thin the fixture assertion body #2689 derived the runner host port from the modules it fronts, so `ExecResult` is read from `@agent-device/host-kit/command` now rather than restated in `runner/host.ts`. The per-fixture assertion body had grown past the complexity the Fallow audit allows a changed file: the envelope checks move into `assertFailureEnvelope` and the tool-output check into `assertToolOutputReachable`, so each recorded shape is still asserted through the same path and the test body reads as one call. * fix(apple): make a named provisioning profile earn its reason One bare `provisioning profile` substring was the shipped sniffer's trigger, and it is a phrase a failing build prints while talking about something else: the codesign invocation, a settings dump, a note about the profile it used. Each row now requires the profile plus the complaint Xcode attaches to it — its `IDEProvisioningErrorDomain` diagnostic, "doesn't include", "has expired" — and a mention that says nothing keeps the cache-recovery advice it already had. * fix(apple): require a profile and its complaint in one line A whole-log AND proves two phrases exist, not that one qualifies the other: a note about the profile the build used, three lines above an unrelated expired-certificate warning, published `signing_provisioning_profile_missing` for a profile that was fine. The profile rows now read one line at a time and the expiry row asks for Xcode's own `has expired` phrase. The rules table grows a `toolTextLineIncludesAll` axis beside `toolTextIncludesAll`, and the bundle-identifier row that also paired two phrases moves onto it so no startup row is left reading a whole log; its cross-line negative rides along. `signing_no_profiles_for_bundle_id` shares the `PROFILE_UNUSABLE` advice instead of spelling it out a third time. `.device-evidence/CHECKLIST.md` belongs to the merged #2682 lane, so this stack's capture sheet lives beside it as `CHECKLIST-runner-failures.md`. * test(ios-runner): record the captured profile row and the host gating the rest need Co-Authored-By: Apex <noreply@callstack.com> --------- Co-authored-by: Apex <noreply@callstack.com>
thymikee
added a commit
that referenced
this pull request
Sep 21, 2026
… host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers.
thymikee
added a commit
that referenced
this pull request
Sep 21, 2026
…evice (#2690) * feat(apple): read what an iPhone reports about its own developer tooling `xcrun devicectl device info details` has always carried two separate answers about a device — `deviceProperties.developerModeStatus` for the Settings toggle and `ddiServicesAvailable` for the developer disk image — and #2682 read the payload for the tunnel only, so both states stayed invisible to the runner. `core/ios-device-readiness.ts` now reads them apart and publishes them through the physical-device control facet, which is the seam the runner already reaches devices through. The parser copies the tool's values without interpreting them; a state it does not recognise stays `unknown` rather than becoming either permission or accusation. `resolveCoreDeviceTunnelIp` now shares that one reader instead of re-running the command. The two reasons are declared as a subset of the startup vocabulary, apart from the rule-driven members, because no amount of tool text establishes them. The captured payload is committed with its device identifiers, hostnames and capture path masked. * fix(apple): answer a developer disk image complaint with device support One `devicectl` hint covered both "developer mode is disabled" and "developer disk image", and it named Developer Mode. That pairing runs one way only: a phone with the toggle off cannot mount the image either. The reverse is common — a device whose toggle is on, waiting for Xcode to finish installing device support — and people were sent to a Settings pane that was already correct while the actual wait went unmentioned. The two complaints are answered apart now. When the output names both, the toggle still wins, since it is the one that explains the other. #2683 adds the device's own reading of both states, which is what lets the tool-output path stop guessing which one it is looking at. * fix(ios-runner): charge runner log evidence to the command that wrote it One `runner.log` serves every command sent to a device and is never truncated between them, and a failing response was classified from the last 64 KB of it. A crash line an earlier command wrote therefore explained whatever failed next: two commands after an app died in AXRuntime, an unrelated timeout was reported as `target_app_axruntime_coretext_crash`, sending the reader to hunt a crash that had already been dealt with. A command now marks where the log had reached before anything is sent, including before its own readiness preflight, and only bytes past that mark are read. A log that has been rewritten behind the mark is shorter than it and is not read at all — that is not evidence about this command. * feat(ios-runner): ask the device before building the runner A phone that refuses to host development tooling fails opaquely today: the build cannot install, or the runner never reaches its port, and the caller reads a build log for a phone problem. The device already states the reason, so startup now asks it — after the host's own `DevToolsSecurity` probe and for physical iPhones only — and publishes `device_developer_mode_disabled` or `device_developer_disk_image_unavailable` with the hint beside the rule that names it. The toggle is weighed first because a phone with it off cannot serve its developer disk image either; the reverse never holds, and an image that is down on a phone with the toggle on is never restated as a toggle problem. A device that could not answer is left alone: `available: false` carries no verdict, and inventing one from a missing read turns an unplugged cable into a claim about someone's Settings. The probe is loaded at its step rather than imported at the top, for the same reason as the host probe above it: the runner subtree sits in the eager closure of seven Apple facades, and a check only a device ever needs has no business being evaluated to answer a simulator request. * refactor(apple): keep the device report on the device side of the runner port Two gates decided this shape, and both are worth recording because neither is visible from the code: - `scripts/layering` R9/R10: a type-only import of `runner/runner-contract.ts` from `core/physical-device-control.ts` closed a 13-file type-level cycle across the runner subtree, because pulling that module pulls the whole runner contract graph. The fact types now live with the module that reads the payload, and `runner/host.ts` mirrors the shape structurally — which is what that file prescribes for a host port anyway, with the composition-root assignment as the conformance check. - `eager-closure-budgets`: a separate `core/ios-device-readiness.ts` added one module to the eager closure of six Apple facades, because the physical-device control facet reaches it statically. The reader now sits beside the payload parse it consumes, where it adds nothing to any closure. Behaviour is unchanged: same two states, same unreadable shape, same tests carried over. * chore(gates): let a captured tool payload inside a package own the unit lane A recorded tool response checked in under a package (`…/src/**/__tests__/fixtures/*.json`, or a `fixtures/` dir beside the module that parses it) has no derivable owner: nothing builds a `.json` and no `.ts` sibling names it, so editing one failed the affected gate open to the full check set. That punished adding evidence — the capture in #2683 tripped it — while the pre-existing `snapshot-source/fixtures/wire-vocabulary.json` has been sitting in the same gap. The rule names the one suite that reads such a file. Root-level and `test/` fixtures still fail open as before, which is right: nothing outside a package says who consumes them. Editing the selector model fails the gate open by design (`selector-owning`), so this change cannot narrow its own selection; the full set runs for it. * fix(ios-runner): charge device and log evidence to the attempt that produced it Review round on the readiness work (#2683): - Drain the runner's queued log appends before drawing a log boundary, so bytes an earlier command has not finished writing cannot be read as this command's. The boundary is now one object (`RunnerLogAttempt`) with no log-path-only shape, and every call site reads through it. - Treat an uncorroborated `ddiServicesAvailable: false` as unreadable rather than as a developer-disk-image obstacle: the services only answer while the tunnel is up and the phone is booted. A disabled toggle keeps its answer regardless, since it already explains the image. - Publish each remedy once: the device report carries the wording `core/devicectl.ts` owns, and the preflight reads it instead of wordings its own. - Bound the readiness probe by the startup budget and its signal, and abort after the read if the caller gave up. - Probe the device before the host's developer-tools setting, so a device and a Mac that are both wrong publish the reason the caller can act on. - Mask the identity fields in the committed device payload and give the mask a test. * fix(apple): keep the device payload parser and its exports inside their modules The Fallow audit that CI runs on changed files was red on this branch: - Collapse the two payload shapes CoreDevice has shipped into one pair of sections before any field is read, so the parser stops spelling the nested fallback out per field and drops below the complexity threshold. Direct values still win. - Report a reported field through one helper that omits it when absent, keeping "the device said nothing" distinct from "the device said no". - Stop exporting `readIosDeviceDetails` and `nameIosDeviceReadinessObstacle`: each has one caller, in its own module, and a caller that needs a verdict reads `readIosDeviceReadiness` or the preflight that publishes the reason. - Move the build-fixture envelope assertions out of the test body into named helpers so the recorded shapes are asserted through the same path. * fix(apple): read the device report and exec types through the derived host port #2689 rewrote the runner host port to derive its types from host-kit and the owning core modules instead of mirroring them, which left this branch naming types that no longer exist there: - Derive `IosDeviceRunnerReadiness` from the control port's own return type rather than restating the device's states and remedies beside them. - Take `IosPhysicalDeviceRunnerControl` from `core/physical-device-routing.ts` and `ExecResult` from `@agent-device/host-kit/command`, which is where the port now points callers. * fix(ios-runner): carry the device's answer onto the failure it explains Developer Mode off was the only device state no later step could change, and it is the only one that keeps refusing a run before the build. The developer disk image is mounted on demand by CoreDevice during build and launch since iOS 17, so a phone that has just been rebooted reports it down while the very next build clears it: refusing there turned a self-clearing state into a failed run. The preflight now hands the states back, the build carries them, and an unavailable image is published onto a build that named no cause of its own — beside xcodebuild's own sentence, never in place of it. A log flush gained the deadline and abort it was missing, and a refused append is recorded instead of dropped, because an offset measured over bytes that never landed is not a boundary. * fix(ios-runner): let the device speak only where nothing matched Keying the enrichment on `build_failed_unclassified` read the absence of a row as a positive claim: a row that deliberately names no cause publishes the same reason, so a just-rebooted phone's disk-image state would have been published over a conflicting-settings failure. The classifier now reports whether a row matched, the catch that classifies carries that answer on `details.startupRuleMatched`, and the enrichment reads it. The enrichment also moves out of the build catch into the session's startup catch, which already holds the device's states and sees every step from the artifact to a runner that answers. A warm derived cache that fails at install and an external xctestrun that never launches now carry `details.developerDiskImage` too, which the build-catch-only version could not reach. * fix(ios-runner): let a host deadline speak for nothing A build the host killed at its own `buildTimeoutMs` arrives with no rule matched, and on a phone that happens to report its developer image down the enrichment answered with the image: a build that never finished cannot have been refused for want of device support, and the caller was sent to mount an image while their real problem was a 15-minute budget (#2690 review). The device now speaks only where nothing matched AND the host's deadline did not end the command. The build catch threads that fact the way it already threads `startupRuleMatched` — its wrapper buries the exec error too deep to inspect — and a failure that never passed through a catch is read with host-kit's `isCommandTimeoutError`, so the install and launch steps are covered by the exec's own `timeoutMs`. Two things the same catch was doing to the error it rebuilt: the cause is now passed through, so the reader of a daemon log still reaches the command that died, and the plumbing keys — whether a row matched, whether the deadline hit — are internal and stripped with the hoisted diagnostic meta, because a caller already holds the verdict they produced rather than the machinery that reached it. * fix(ios-runner): let an early exit carry the disk-image state it read Co-Authored-By: Apex <noreply@callstack.com> * fix(ios-runner): carry the disk-image state on the alive-child connect timeout too * docs(evidence): record the locked-phone rerun at eaf411e and why it publishes no disk-image state * test(ios-runner): move the parseRunnerResponse tests beside the session module they cover --------- Co-authored-by: Apex <noreply@callstack.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.
Summary
runner/host.tswas a hand-maintained mirror of host-kit. It is now derived from the modules it fronts, keeping the R77 seam and the eager-closure property. Closes #2658. 34 files touched.import type(R77 exempts type-only imports).DefinedEnvMapwas never a host capability — it moves to its only caller.AppleRunnerHostisPicks overimport type * asnamespaces of host-kit, kernel, provision-kit and the Apple-owned core modules. No signature is written here.delegate(name); the 28 importers are unchanged. TheDeadlineobject shim stays.core/runner-host.tskeeps naming each import rather than spreading the modules whole — see Rejected below. It holds names only; it re-types nothing.Host-kit gains three type exports the port needs by name and no behavior change:
ExecStreamOptions,DiagnosticEventInput, andDeadlineClock— the read side ofDeadline, which the class nowimplementsand whichRetryOptions.deadlineaccepts, so a test host can keep substituting its own clock now that the port no longer declares a structural copy.The runner-facing slice of physical-device control moves to
core/physical-device-routing.ts, below both sides. Without it the port reachesphysical-device-control.ts, whose launch/terminate members are typed against the runner's own command executor — a 19-file type cycle that fails R9/R10.Replay. #2599 (
ExecBackgroundOptions = Omit<ExecOptions, 'timeoutMs'>) becomes a zero-line change here. #2598 and #2595 each drop from 11 and 3 lines inhost.tsto 2: their type and signature changes are free, and what remains is the new capability itself — onePickname plus onedelegateline, as the issue predicts.host.ts: 403 → 187 LOC (≈137 code, ≈50 doc). The issue's ≈120 estimate did not count the preserved R77 header.Known consequence. Deriving from
typeof HostCommanddrops the oldExecOptionsnarrowing, socwd,binaryStdoutandstdinare now reachable through the port. That narrowing existed to keep the mirror small ("narrowed to what the runner uses"), and restoring it means re-declaring the exec signatures by hand — exactly what this PR deletes. The runner modules now importExecOptionsfrom host-kit directly anyway, so the port could not enforce it either.Rejected. The issue's binding spreads the modules whole. Measured, that puts every export of all sixteen on the host object (41 members → 128), ships +1.8 kB including the vitest-only
resetAllProcessMemosForTests, and makes a name shared by any two of them silently rebind a capability — a tripwire across four packages, catchable only at runtime. The issue's caveat licenses the named binding; tsc stays the sole enforcer of completeness.Validation
Tested at
5b29c32(pre-push amend of the pushed head; tree identical).Green:
pnpm typecheck,pnpm check:layering(R77 and R13 untouched; R9 back to the merge-base's 6-file max),pnpm check:production-exports,pnpm check:fallow(no issues in 35 changed files),pnpm lint,pnpm format:check,pnpm build,pnpm check:package,pnpm check:bundle-owner-files,pnpm check:gate-manifest,pnpm depgraph:test, theapple-runnerproject (49 files / 480 tests), and the eager-closure budget suite (666 tests) — the seven Apple façade closures do not grow.pnpm check:affected --runfails only its vitest lane, on three tests that fail identically on a cleanorigin/maincheckout in this container (root user, so theirchmod-based unwritable-path setup is a no-op):app-log-session-resource.test.ts > unwritable tombstone plus rejecting cleanup blocks same-process replacement,capture-kit durable-capture/adoption.test.ts > a failed terminal transition…,capture-kit durable-capture-resource.test.ts > an unconfirmed failed-adoption cleanup…. Everything else inpnpm test:unitpasses (10470 tests). CI on Linux runners is the authority here.No device-facing behavior changes: the port's runtime shape is the same object bound to the same functions.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PPceQTJdXsEJaZsBM8ycek
Generated by Claude Code