Purpose
packages/platform-apple/src/runner/host.ts is the Apple runner host port: R77 keeps runner/** from value-importing @agent-device/host-kit/* so the seven Apple façade eager closures stay fixed. The port is right. Its shape is not: it is a hand-maintained mirror of host-kit, so every host-kit change is re-typed by hand.
Measured over the last 300 commits (main 91652a8fc5): 403 LOC, 63 exports, 28 importers inside the package, 11 touches. Nine of the eleven were not moves; six of those were a host-kit signature change or one new capability, each costing 3 to 16 lines here plus the binding in core/runner-host.ts (#2598, #2599, #2595, #2470, #2423, #2160).
Four hand-kept layers, and each host-kit change touches two or three of them:
- ~100 lines of copied type declarations (
ExecResult, RetryPolicy, ProcessLockOwner, DiagnosticEventInput, …). Seven of the names do not exist in host-kit under that name (ExecStreamOptions, RetryPolicy, RetryOptions, RetryAttemptContext, RetryTelemetryEvent, DiagnosticEventInput, DefinedEnvMap) and are re-typed from scratch.
- A 50-member
AppleRunnerHost interface that re-declares every function signature.
- Fifty one-line delegators (
export const runCmdSync: AppleRunnerHost['runCmdSync'] = (…) => requireHost().runCmdSync(…)).
core/runner-host.ts, which lists every name a third time.
Required behavior
Derive the port from the modules it fronts; keep the seam and the rule.
- Types. Delete the copied type block. The 15 runner files that import those types take them from the owning package with
import type (R77 exempts type-only imports; typecheck erases them). Export the seven missing names from the host-kit subpath that owns the function (command, retry, diagnostics) — a host-kit fix, not a platform-apple one.
- Interface. Replace the re-declared signatures with a name list over the real modules:
import type * as HostCommand from '@agent-device/host-kit/command';
import type * as HostProcess from '@agent-device/host-kit/process';
// one per subpath; ../core/tool-provider.ts, simctl.ts, physical-device-control.ts,
// plist-xml.ts, runner-owner-state.ts for the Apple-owned members
export type AppleRunnerHost =
Pick<typeof HostCommand, 'runCmdStreaming' | 'runCmdSync' | 'runCmdBackground' | 'requireExecSuccess' | 'isCommandTimeoutError' | 'shellQuote'>
& Pick<typeof HostProcess, 'isProcessAlive' | 'isProcessGroupAlive' | 'readProcessStartTime' | 'readProcessCommand' | 'signalPidsBestEffort' | 'signalProcessGroupBestEffort' | 'classifyOwnerLiveness'>
& /* … */
& { deadlineFromTimeoutMs: typeof HostRetry.Deadline.fromTimeoutMs };
import type * as is a type-only declaration: R77 and the eager-closure gate see nothing. A signature change in host-kit reaches the runner with zero edits here.
- Delegators. Keep the named exports so the 28 importers do not change, typed by derivation:
function delegate<K extends keyof AppleRunnerHost>(name: K): AppleRunnerHost[K] {
return ((...args: unknown[]) =>
(requireHost()[name] as (...a: unknown[]) => unknown)(...args)) as AppleRunnerHost[K];
}
export const runCmdSync = delegate('runCmdSync');
Keep the Deadline object shim as is (seven files call Deadline.fromTimeoutMs).
- Binding.
core/runner-host.ts spreads the real modules instead of listing names ({ ...HostCommand, ...HostProcess, …, deadlineFromTimeoutMs: Deadline.fromTimeoutMs, ...appleCore }). The Pick list becomes the single place that says what the runner may reach. test-host.ts is a Proxy and needs no change.
After this, adding a capability is one name in the Pick and one delegate line.
Completion conditions
Caveat
Spreading whole modules into the binding puts every export of those modules on the host object at runtime; the Pick type hides them. If check:production-exports or fallow objects, list the names explicitly in the binding instead. That keeps two lists, still with no hand-written signatures.
Out of scope
- Retiring the port or R77. The eager-closure property still binds the runner subtree.
- Changing any host-kit behavior. Only type exports are added there.
Purpose
packages/platform-apple/src/runner/host.tsis the Apple runner host port: R77 keepsrunner/**from value-importing@agent-device/host-kit/*so the seven Apple façade eager closures stay fixed. The port is right. Its shape is not: it is a hand-maintained mirror of host-kit, so every host-kit change is re-typed by hand.Measured over the last 300 commits (main
91652a8fc5): 403 LOC, 63 exports, 28 importers inside the package, 11 touches. Nine of the eleven were not moves; six of those were a host-kit signature change or one new capability, each costing 3 to 16 lines here plus the binding incore/runner-host.ts(#2598, #2599, #2595, #2470, #2423, #2160).Four hand-kept layers, and each host-kit change touches two or three of them:
ExecResult,RetryPolicy,ProcessLockOwner,DiagnosticEventInput, …). Seven of the names do not exist in host-kit under that name (ExecStreamOptions,RetryPolicy,RetryOptions,RetryAttemptContext,RetryTelemetryEvent,DiagnosticEventInput,DefinedEnvMap) and are re-typed from scratch.AppleRunnerHostinterface that re-declares every function signature.export const runCmdSync: AppleRunnerHost['runCmdSync'] = (…) => requireHost().runCmdSync(…)).core/runner-host.ts, which lists every name a third time.Required behavior
Derive the port from the modules it fronts; keep the seam and the rule.
import type(R77 exempts type-only imports; typecheck erases them). Export the seven missing names from the host-kit subpath that owns the function (command,retry,diagnostics) — a host-kit fix, not a platform-apple one.import type * asis a type-only declaration: R77 and the eager-closure gate see nothing. A signature change in host-kit reaches the runner with zero edits here.Deadlineobject shim as is (seven files callDeadline.fromTimeoutMs).core/runner-host.tsspreads the real modules instead of listing names ({ ...HostCommand, ...HostProcess, …, deadlineFromTimeoutMs: Deadline.fromTimeoutMs, ...appleCore }). ThePicklist becomes the single place that says what the runner may reach.test-host.tsis a Proxy and needs no change.After this, adding a capability is one name in the
Pickand onedelegateline.Completion conditions
host.ts.pnpm check:layeringgreen with R77 and R13 untouched; eager-closure budgets for the seven Apple façade entries unchanged (no new value import entersrunner/**).typecheck,check:production-exports, and theapple-runnervitest project green.host.ts≈ 120 LOC (from 403). Report the number in the PR.Caveat
Spreading whole modules into the binding puts every export of those modules on the host object at runtime; the
Picktype hides them. Ifcheck:production-exportsor fallow objects, list the names explicitly in the binding instead. That keeps two lists, still with no hand-written signatures.Out of scope