Skip to content

refactor(platform-apple): derive the runner host port instead of mirroring host-kit - #2689

Merged
thymikee merged 1 commit into
mainfrom
claude/adversarial-review-2658-l5l30j
Sep 19, 2026
Merged

thymikee merged 1 commit into
mainfrom
claude/adversarial-review-2658-l5l30j

Conversation

@thymikee

Copy link
Copy Markdown
Member

Summary

runner/host.ts was 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.

  • Types. The ~100-line copied block is gone. The 23 runner files that used those types take them from the owning package with import type (R77 exempts type-only imports). DefinedEnvMap was never a host capability — it moves to its only caller.
  • Interface. AppleRunnerHost is Picks over import type * as namespaces of host-kit, kernel, provision-kit and the Apple-owned core modules. No signature is written here.
  • Delegators. The 41 named exports stay, typed by derivation through delegate(name); the 28 importers are unchanged. The Deadline object shim stays.
  • Binding. core/runner-host.ts keeps 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, and DeadlineClock — the read side of Deadline, which the class now implements and which RetryOptions.deadline accepts, 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 reaches physical-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 in host.ts to 2: their type and signature changes are free, and what remains is the new capability itself — one Pick name plus one delegate line, 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 HostCommand drops the old ExecOptions narrowing, so cwd, binaryStdout and stdin are 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 import ExecOptions from 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, the apple-runner project (49 files / 480 tests), and the eager-closure budget suite (666 tests) — the seven Apple façade closures do not grow.

pnpm check:affected --run fails only its vitest lane, on three tests that fail identically on a clean origin/main checkout in this container (root user, so their chmod-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 in pnpm test:unit passes (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

…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
@github-actions

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.64 MB 4.61 MB -21.9 kB
Package (unpacked) 4.64 MB 4.61 MB -21.9 kB
Package (download) 1.38 MB 1.37 MB -8.8 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.4 ms 27.1 ms -0.2 ms
CLI --help 77.9 ms 79.3 ms +1.4 ms

@thymikee

Copy link
Copy Markdown
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: delegate() casts through unknown[], so forwarded arity is checked only at call sites, which tsc already covers. CI is green.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 19, 2026
@thymikee
thymikee merged commit 8d71196 into main Sep 19, 2026
18 checks passed
@thymikee
thymikee deleted the claude/adversarial-review-2658-l5l30j branch September 19, 2026 18:40
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-19 18:40 UTC

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(platform-apple): derive the runner host port instead of mirroring host-kit

2 participants