Why
Two device-readiness facts are guessed at from other people's output, and one failure classifier reads a file it cannot scope.
Developer Mode. verifyDeveloperModeForIosRunner (packages/platform-apple/src/runner/runner-session.ts:724-736) shells out to host DevToolsSecurity -status and throws COMMAND_FAILED with no reason. That is the macOS developer-tools security setting — not the device's Settings → Privacy & Security → Developer Mode toggle. What we do have is prose: resolveIosDevicectlHint (core/devicectl.ts:299-318, tested in core/__tests__/devicectl.test.ts:73-93) maps developer disk image / developer mode is disabled to the Settings hint, but only on devicectl paths, with no reason code, and it treats a disk-image failure as a symptom of Developer Mode being off (:308-312). Before splitting those into two reasons, prove they are distinguishable at all.
Evidence that can be stale. enrichRunnerFailureFromLog reads a 64 KB tail of the append-only session runner.log (runner/runner-failure-diagnostics.ts:5,46-53; append at runner-io.ts:28) while classifying a runtime failure (runner/runner-session.ts:996). A crash written by an earlier command can therefore be attributed to a later, unrelated failure — which is exactly the shape an external consumer built its own defense against, and its defense is only as good as our log format, which we do not version.
Task
- Read the device, not the tool output.
resolveTunnel already runs devicectl device info details --json-output and parses it (core/physical-device-coredevice.ts:160-195, parseIosDeviceDetailsPayload :205+), but reads only tunnel fields. First step is a captured payload: confirm whether it carries a developer-mode status and a DDI availability field. If it does not, stop and record that here — do not invent a probe or keep inferring from another tool's stderr.
- Publish the fact once, keyed on a typed reason, shared by the runner startup path and the devicectl hint path instead of each keeping its own copy.
runner-session.ts is 1,181 lines, so the probe does not land there (AGENTS.md: split before adding behavior).
- Bind log evidence to the attempt. Capture the log offset before the command runs and classify only bytes written since. A reason derived from
runner.log must be unable to cite a previous command's crash; the existing AXRuntime-crash and target-crash classifications inherit this.
- Where a fact cannot be established, say so through the existing unavailability shape (
{ available: false, reason, hint }, e.g. runtime.ts:103-107) rather than falling back to inference.
Acceptance criteria
Non-goals
Mounting or unmounting a developer disk image, pairing, Device Manager orchestration, and any recovery that acts on the device. Classification only. The build/signing reasons are the sibling issue.
Related: ADR 0010, ADR 0005, #2662. Blocked by: #2680, which owns the reason vocabulary this issue adds two members to.
Why
Two device-readiness facts are guessed at from other people's output, and one failure classifier reads a file it cannot scope.
Developer Mode.
verifyDeveloperModeForIosRunner(packages/platform-apple/src/runner/runner-session.ts:724-736) shells out to hostDevToolsSecurity -statusand throwsCOMMAND_FAILEDwith no reason. That is the macOS developer-tools security setting — not the device's Settings → Privacy & Security → Developer Mode toggle. What we do have is prose:resolveIosDevicectlHint(core/devicectl.ts:299-318, tested incore/__tests__/devicectl.test.ts:73-93) mapsdeveloper disk image/developer mode is disabledto the Settings hint, but only on devicectl paths, with no reason code, and it treats a disk-image failure as a symptom of Developer Mode being off (:308-312). Before splitting those into two reasons, prove they are distinguishable at all.Evidence that can be stale.
enrichRunnerFailureFromLogreads a 64 KB tail of the append-only sessionrunner.log(runner/runner-failure-diagnostics.ts:5,46-53; append atrunner-io.ts:28) while classifying a runtime failure (runner/runner-session.ts:996). A crash written by an earlier command can therefore be attributed to a later, unrelated failure — which is exactly the shape an external consumer built its own defense against, and its defense is only as good as our log format, which we do not version.Task
resolveTunnelalready runsdevicectl device info details --json-outputand parses it (core/physical-device-coredevice.ts:160-195,parseIosDeviceDetailsPayload:205+), but reads only tunnel fields. First step is a captured payload: confirm whether it carries a developer-mode status and a DDI availability field. If it does not, stop and record that here — do not invent a probe or keep inferring from another tool's stderr.runner-session.tsis 1,181 lines, so the probe does not land there (AGENTS.md: split before adding behavior).runner.logmust be unable to cite a previous command's crash; the existing AXRuntime-crash and target-crash classifications inherit this.{ available: false, reason, hint }, e.g.runtime.ts:103-107) rather than falling back to inference.Acceptance criteria
devicectl device info details; the issue or the code records the answer, and the two reasons exist only if they are distinguishable.verifyDeveloperModeForIosRunner's host check gets its own distinct reason and is not silently relabeled as a device-side check. It has no reason today, so this depends on the sibling issue creating the reason vocabulary.runner.logto tell them apart.pnpm check:affected --rungreen.Non-goals
Mounting or unmounting a developer disk image, pairing, Device Manager orchestration, and any recovery that acts on the device. Classification only. The build/signing reasons are the sibling issue.
Related: ADR 0010, ADR 0005, #2662. Blocked by: #2680, which owns the reason vocabulary this issue adds two members to.