Commit Graph

930 Commits

Author SHA1 Message Date
Michał Pierzchała ccb2c85081 fix: centralize known cli command detection 2026-07-03 11:16:05 +02:00
Michał Pierzchała 0453826b2e fix: report unknown command before flag-support validation (#1038)
* fix: report unknown command before flag-support validation

When an unknown command is provided with flags, the parser was
validating flag support before checking if the command exists,
resulting in misleading "Flags are not supported for command X"
errors instead of "Unknown command: X".

This fix reorders the validation to check for unknown commands
first, reporting more helpful error messages. Additionally, when
the unknown command is 'tap', the error now includes a hint
suggesting 'press or click' as alternatives.

Closes #1036

* refactor: drop as-any casts and note the cli.ts sync requirement

Review follow-ups: widen the command lists to readonly string[] instead
of narrowing the value, and document that isCommandKnown must stay in
sync with the cli.ts dispatch fall-through.
2026-07-03 10:15:13 +02:00
Michał Pierzchała a79dad69f4 feat(interaction): surface targetHittable/hint for non-hittable press/fill targets (#1040)
When a resolved press/fill/click selector or ref target lands on a node
that reports hittable: false (after any hittable-ancestor promotion), the
success response now carries targetHittable: false and a hint suggesting
the interaction may have had no visible effect. Resolution semantics are
unchanged: no failing, no filtering, no strict-unique matching — iOS AX
hittable flags are unreliable on deep RN trees, and react-navigation's
maestro suite depends on today's auto-disambiguation behavior (#1037).

Also documents the selector disambiguation policy (deepest node, then
smallest area, exact ties fail) in `help workflow`, including that
replay/replay-heal apply the same policy for touch/fill/get-text.
2026-07-03 10:14:45 +02:00
Michał Pierzchała 9b19aeee7d fix: hint @ref when a bare snapshot ref is passed as interaction target (#1039)
When a user forgets the @ prefix on a snapshot ref (e.g., 'click e3' instead
of 'click @e3'), the positional parser would attempt to parse it as
coordinates, resulting in a confusing error message "Expected x to be a
finite number."

Add a check in readInteractionTargetFromPositionals and readFillTargetFromPositionals
to detect bare snapshot refs matching /^e\d+$/ and throw INVALID_ARGS with
a helpful hint message suggesting the correct @-prefixed form.

Closes #1035
2026-07-03 10:01:21 +02:00
Michał Pierzchała bff28009e2 0.18.3 v0.18.3 2026-07-02 21:03:48 +02:00
Michał Pierzchała 1d916b1d76 fix: capture ios device logs with devicectl console (#1026)
* fix: report unsupported ios device log streaming

* docs: clarify test app device verification

* docs: clarify alternate metro port flow

* refactor: split app log diagnostics helpers

* refactor: tighten app log diagnostics shape

* fix: capture ios device logs with devicectl console

* fix: tighten ios device log capture state

* refactor: clarify ios log capture support flow

* refactor: consolidate ios log diagnostics flow
2026-07-02 21:03:35 +02:00
Michał Pierzchała a4b35c4226 feat: list daemon artifacts through proxy sessions (#1027) 2026-07-02 20:27:51 +02:00
Szymon Dziedzic 08594fced9 feat: expose daemon artifact inventory (#1020)
* feat: expose daemon artifact inventory

* refactor: isolate daemon artifact HTTP routes

* test: reduce proxy artifact test complexity

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-07-02 20:14:16 +02:00
Michał Pierzchała c4e1fc887a feat: warm the iOS runner build cache from doctor and boot (#1033)
* feat: warm the iOS runner build cache from doctor and boot

Cold-cold startup (first run ever on a machine) pays a ~10s xcodebuild
build-without-testing before the first open can even start the runner.
The background cache prewarm existed but only fired on simulator cold
boot during open — too late for the fresh-machine flow.

- doctor now resolves an iOS simulator from the session or device
  inventory and, when the runner artifact is missing, kicks the
  best-effort background build and reports it as an ios-runner-cache
  check ('build started in the background' / 'artifact cached'). The
  artifact builds against a generic simulator destination, so any
  simulator record works and the result is shared across runtimes.
- boot fires the same warmup for already-booted iOS simulators (cold
  boots already warmed via the onIosSimulatorColdBootStart hook).
- help workflow, help debugging, doctor --help, and the agent-device
  skill now say to run doctor first on fresh machines, so agents
  (including small models) discover the warmup instead of paying the
  build inside their first open. Doctor's help no longer claims to be
  strictly read-only.

Verified live: wiped artifact cache -> doctor returns immediately with
the warming check while the build runs detached -> artifact present
~15s later -> second doctor reports cached; first open then skips the
build entirely (~10s off cold-cold).

* feat: show doctor runner warmup progress and guard remote/non-mac hosts

The warmup now emits request-progress lines ('Checking iOS runner build
cache (device)...' / 'Warming ... in the background') so doctor
progressively shows what it is doing, and it is guarded to environments
where a local xcodebuild makes sense: non-macOS hosts and provider-backed
devices skip it entirely (--remote already returned before device
checks). Tests cover both guards with a mocked platform.

* fix: validate the runner cache probe like the ensure path

Review finding (P2): hasCachedAppleRunnerArtifact treated any .xctestrun
under the expected derived dir as cached, while the open path rejects
partial/stale caches via cache-metadata and product-path validation - so
doctor could report 'artifact cached', skip the warmup, and leave the
first open paying the build anyway.

The probe now runs the same evaluateExistingXctestrun no-build evaluation
the ensure path uses (extracted into a shared device-scoped helper) and
reports cached only on reuse_ready. Regression tests cover the empty and
stray-xctestrun-only derived dirs; live-verified that a cache with
deleted metadata now triggers the background warmup instead of reporting
cached, and that a genuinely valid cache still probes true.
2026-07-02 20:09:33 +02:00
Michał Pierzchała 091179e91d 0.18.2 v0.18.2 2026-07-02 17:45:48 +02:00
devin-ai-integration[bot] d18066ff2b fix(cloud-webdriver): stop artifacts command from allocating a new provider session (#1030)
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 17:44:21 +02:00
devin-ai-integration[bot] a3c492be10 fix(cloud-webdriver): extend remote lease TTL for cloud provider connections (#1029)
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 17:11:26 +02:00
devin-ai-integration[bot] 7777fa549e fix(aws-device-farm): send requested platform in WebDriver capabilities (#1028)
The AWS Device Farm runtime is registered with a hardcoded platform of 'android', so baseSessionForLease builds the createSession capabilities with platformName 'Android' before prepareSession resolves the real platform. On an iOS device Device Farm rejects this with "reserved ... invalid values: ['platformName']". Rebuild webdriverCapabilities in the AWS prepareSession using the resolved platform and device name (mirroring the BrowserStack provider).

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-02 16:33:50 +02:00
Michał Pierzchała 6dc0aa550e perf: collapse iOS simulator relaunch into one simctl launch call (#1024)
* perf: collapse iOS simulator relaunch into one simctl launch call

open --relaunch on a simulator dispatched close (simctl terminate,
~0.7s), settled 300ms, then dispatched open (simctl launch, ~0.8s).
simctl launch --terminate-running-process does the whole relaunch in one
call (~0.94s), so the fast path skips the close dispatch and the
post-close settle: steady relaunch ~2.9s -> ~2.0s.

The terminateRunningApp option threads from the open context through the
interactor to buildIosSimulatorLaunchArgs; only the simulator bundle-id
launch path consumes it. Runtime hints keep working - they are
user-defaults writes read at the next launch, which the collapsed call
still performs. --clear-app-state keeps the close-first ordering so it
never mutates a running app's container; real devices and Android keep
the two-step relaunch.

* fix: keep close-first ordering for URL relaunches on simulators

Review finding: open <app> <url> --relaunch reached the collapsed fast
path, but the URL dispatch branch never consumes terminateRunningApp -
a deep-link open never launches the app, so there is nothing to attach
the terminate to. The session layer skipped the close while the platform
layer did not terminate either, silently losing relaunch semantics.

The collapse now applies only to the single app-launch form
(openPositionals.length === 1); URL relaunches keep the explicit
close-then-open sequence, with a regression test.
2026-07-02 15:53:33 +02:00
Michał Pierzchała b67053e975 fix: bound retained iOS runner lifetime with an idle stop (#1025)
* fix: bound retained iOS runner lifetime with an idle stop

A runner retained after session close (#1021) holds the device's runner
lease indefinitely, which blocks every other daemon on the machine from
using the device - observed in the wild when a leftover verification
daemon squatted a simulator through its retained runner.

Session close now schedules an idle stop when it retains the runner:
if nothing touches the runner within 5 minutes (any ensureRunnerSession
call cancels the timer), it is stopped and the lease released.
AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS overrides the window; 0 disables
idle stops entirely (the previous retain-until-daemon-exit behavior).
The timer is unref'd and cleared on explicit stop and shutdown detach.

Runners warmed by explicit prepare ios-runner are not timed - prepare
expresses intent to keep a warm runner, and its ensureRunnerSession call
cancels any pending stop.

* test: reset idle-stop scheduler mock between session tests

Review finding on #1025: every other mock in the file resets in
beforeEach; without it the retention assertion could pass on call
history leaked from an earlier close test.
2026-07-02 15:29:45 +02:00
Michał Pierzchała 3e8401a88e fix: surface open timing in debug diagnostics (#1023) 2026-07-02 14:12:45 +02:00
Michał Pierzchała 505e2af12f feat: add opt-in exec command diagnostics (#1019)
* feat: add opt-in exec command diagnostics

* fix: flush exec trace diagnostics on success

* fix: trace background exec spawns

* test: lower exec trace test complexity
2026-07-02 14:05:47 +02:00
Michał Pierzchała fcb7e32f1c fix: clarify clean-xcuitest output (#1018)
* fix: clarify clean-xcuitest output

* fix: simplify clean-xcuitest formatter

* fix: clarify clean-xcuitest failure output
2026-07-02 13:53:42 +02:00
Michał Pierzchała 9e14f0e8eb fix: retain hot iOS simulator runner on safe close (#1021) 2026-07-02 13:51:11 +02:00
Michał Pierzchała ce39f106ee perf: overlap iOS runner prewarm, seed booted-memo, hand runners off across daemon restarts (#1011)
* perf: start iOS simulator runner prewarm before close/open dispatch

Since simulator close/open ride simctl and never touch the runner, the
xcodebuild ramp can overlap the app (re)launch instead of following it.
Real devices keep the post-open prewarm because relaunchCloseApp tears
their runner down first; the Maestro prewarm-before-open path is
unchanged.

Daemon-cold plain open: first snapshot 4.6-5.1s -> ~2.5s (the ramp gets a
~2s head start). Daemon-cold open --relaunch: ~8.9s -> ~8.2s mean; most
of the head start is eaten by ramp slowdown under concurrent simctl work,
matching earlier contention observations.

* perf: seed the simulator booted-memo from the device inventory parse

A simctl listing that reports a simulator Booted is the same observation
ensureBootedSimulator would make, so resolving a device now seeds the
memo and the ensureDeviceReady boot check that follows in the same
request skips its own ~0.7s listing. Daemon-cold plain open: ~2.6s ->
~2.0s.

* perf: hand iOS simulator runners off across daemon restarts

Graceful daemon shutdown detaches healthy simulator runner sessions
instead of killing them: the lease token is rewritten to a detached form
(so the daemon's own teardown paths no longer classify it as owned) and
the xcodebuild/runner pair keeps running. The next daemon start adopts a
stale lease when the runner process is alive, the artifact fingerprint
matches the current toolchain, and an uptime probe answers - otherwise
the existing cleanup-and-restart hygiene applies unchanged. Crash-killed
daemons leave the same stale lease, so crash recovery and deliberate
handoff share one path.

The adopted session wraps the orphaned xcodebuild in a pid-backed
surrogate child; every downstream consumer (liveness, kill-tree,
early-exit detection, disposal wait) operates on pid/exitCode, which a
low-frequency exit poll maintains.

Bounds and escape hatches: at most one detached runner per device, the
runner's XCTWaiter self-expires after 24h, clean:daemon still kills by
lease runnerPid, and AGENT_DEVICE_IOS_RUNNER_DETACH=0 disables both
detach and adoption.

Daemon-cold open --relaunch with a handed-off runner: ~8s -> ~3.2s
(startup.durationMs ~780), for both SIGTERM and SIGKILL'd predecessors.

* fix: keep scoped simulator-set runners out of the shutdown handoff

Review finding on #1011: detach released the XCTestDevices device-set
redirect while the runner kept running, and adoption rebuilt the session
without it - a custom-set runner could outlive the symlink/lock that its
xcodebuild depends on, breaking scoped simulator-set isolation.

The redirect's lifetime is bound to the owning session by design, so
scoped-set runners simply do not participate in the handoff: shutdown
detach skips any session holding a redirect (it goes through the normal
dispose-and-restore path), and adoption refuses devices that resolve to a
custom simulator set. Default-set sessions - the only ones that never
hold a redirect - keep the fast handoff.

* fix: gate relaunch runner teardown on Apple platforms explicitly

'device.platform !== \'android\'' predates the platform set growing to
include linux and web - for those it would take the runner-session lock
and walk the lease cleanup path on relaunch for a device that can never
have an XCUITest runner. isApplePlatform() states the actual intent:
Apple non-simulator targets tear down, simulators stay hot, everything
else never touches the runner.

* refactor: quality pass over the iOS startup perf stack

Applied from a 4-angle review (reuse / simplification / efficiency /
altitude) of the #1011 diff:

- RunnerSession.child is now a RunnerProcessHandle (pid/exitCode) instead
  of ChildProcess: every consumer only reads pid/exitCode and kills by
  pid, the adoption surrogate no longer needs double casts, and the
  compiler enforces what a comment used to promise. Real children satisfy
  the handle structurally.
- session-open prewarm scheduling collapsed into schedulePrewarm/
  awaitPrewarm closures; the fallback guard is a dedicated local instead
  of reading the OpenTiming telemetry object (kept separate from the
  promise itself, which is legitimately undefined when prewarm is
  unavailable).
- adoption: expected derived path computed once and threaded through
  (resolveExpectedRunnerCacheMetadata does a recursive source-stat walk
  per call, and it runs under the lease lock); guard chain flattened into
  skip(reason) early returns; probe timeout 2s -> 500ms (localhost
  refusal is instant, the timeout only bounds the wedged-runner case);
  dead path.dirname fallback removed.
- shared normalizeRunnerStartupTimeoutMs and buildRunnerSessionId in
  runner-session-types; deleted the verbatim copies.
- detach kill switch parses through utils/source-value parseBooleanLiteral
  (now exported) instead of a bespoke dialect that treated 'no' as
  enabled.
- dropped buildDetachedRunnerLease's untriggerable idempotence guard,
  direct Map iteration in the detach loop, and a freshness warning on
  markSimulatorBooted for future callers.
2026-07-02 12:38:41 +02:00
Michał Pierzchała 657442260e fix: reduce iOS runner keepalive log noise (#1017) 2026-07-02 12:29:13 +02:00
Michał Pierzchała 996d93e979 perf: cut iOS open --relaunch from 11s to 2.85s (#1010)
* perf: keep iOS simulator runner hot across open --relaunch

open --relaunch tore down the XCUITest runner session before closing the
app, then paid a full xcodebuild test-without-building restart (~6s) after
reopening it. Simulator close/open go through simctl and never touch the
runner, so a healthy runner survives the relaunch; stale runners are
recovered by the readiness preflight + invalidate/restart path that landed
after #705 reverted #700's version of this.

Steady-state open --relaunch on iPhone 17 Pro sim: 11.0s -> 4.9s
(startup.durationMs 5929 -> 1574). Real devices keep the conservative
teardown.

* perf: drop redundant simctl inventory listings from the iOS open path

A single open --relaunch spawned `simctl list devices -j` (~0.7s each)
three times: session-device re-resolve, closeIosApp's booted check, and
launchIosSimulatorApp's booted check.

- ensureBootedSimulator now keeps a 5s recently-observed-Booted memo
  (mirroring DEVICE_READY_CACHE_TTL_MS at the daemon layer) so repeated
  boot checks inside one request cost nothing; shutdownSimulator
  invalidates it, boot transitions seed it.
- refreshSessionDeviceIfNeeded skips the re-resolve while the device's
  XCUITest runner session is alive - a live runner attached to the UDID
  already proves the simulator exists and is booted.

With the runner kept hot across relaunch, steady-state open --relaunch on
iPhone 17 Pro sim drops 4.9s -> 2.85s (startup.durationMs ~1574 -> ~760).
Combined with the previous commit: 11.0s -> 2.85s.

* test: expect single simctl listing in tvOS provider flow

The recently-observed-Booted memo removes the repeat state listings the
launch and terminate boot checks used to make; the transcript now records
one listing per flow.
2026-07-02 11:33:43 +02:00
Michał Pierzchała ed1f30c34f feat: surface appleOs discriminant on public device output (#1009)
* feat: surface appleOs discriminant on public device output

Additive, non-breaking: the public device/result shapes now carry the
stored Apple-OS discriminant (iPhone/iPad/tvOS/visionOS/macOS) alongside
the existing leaf platform (ios/macos). Consumers can distinguish Apple
OSes on the wire instead of only the collapsed ios/macos leaves.

- session-inventory devices/session_list stop stripping appleOs and emit
  it only for Apple devices (non-Apple omit it); platform stays the leaf.
- boot/shutdown success results (contracts/device.ts) gain optional appleOs.
- AgentDeviceDevice/AgentDeviceSessionDevice + client normalizers carry it.
- appleOs values never equal the internal 'apple' token, so the
  apple-platform-output-guard is unaffected.

Tests: devices projection per Apple fixture (ios/ipados/tvos/visionos/
macos) + non-Apple omission; client normalizer coverage.

* fix: gate public appleOs surfacing to Apple platforms

appleOs is Apple-only, but the daemon projection (devices, session_list, boot,
shutdown) and the client normalizers preserved it whenever the field was present —
so a malformed/legacy non-Apple record carrying a valid Apple OS value would leak
it on the public wire. Gate every emission site on isApplePlatform(platform), not
just field presence.

Regression tests: a non-Apple device carrying a stray appleOs='macos' drops it in
both the daemon devices projection and normalizeDevice.
2026-07-02 11:10:05 +02:00
Michał Pierzchała 127e2be4a6 docs: require provider-integration + coverage verification for platform/response changes (#1008)
Process guardrail (addresses the recurring miss behind the Platform-collapse leaks):
- Testing Matrix: platform/device-response changes must also run test:integration:provider
  and test:coverage — check:unit alone misses the provider-integration project (which runs the
  apple-platform-output leak guard).
- PR Readiness: don't call a PR CI-green off a local --project unit run; the Integration Tests
  and Coverage jobs run provider-integration, so verify those on the actual PR head.
2026-07-02 10:45:49 +02:00
Michał Pierzchała 7488346513 feat: route recording + provider platform gates through PlatformPlugin facets (#1007)
* feat: route recording + provider platform gates through PlatformPlugin facets

Phase 3 step b.3 (issue #974): land the two deferred daemon-owned
PlatformPlugin facets as DATA-ONLY seams, mirroring appLog/perf.

recording facet: PlatformPlugin.recording.resolveBackendTag(device)
returns a neutral RecordingBackendTag string ('web'|'android'|'macos'|
'ios-device'|'ios-simulator'|'unsupported', daemon-owned, type-only in the
plugin). resolveRecordingBackendForDevice now maps the tag back to the
daemon-owned backend instance via RECORDING_BACKENDS_BY_TAG, with a
'?? unsupported' fallthrough for the factless linux family. Backend
instances + RecordingBackend type stay in the daemon.

providers facet: PlatformPlugin.providers.platformGatedResolvers declares,
per family, which platform-gated request provider resolvers apply (the data
that replaces each descriptor's hand 'device.platform === …' gate). The
daemon still owns resolver invocation, wrapper composition, and request-scope
concurrency isolation; only the gate moved to data. Ungated resolvers
(appLog/recording providers) stay ungated in the daemon.

Both facet types are type-only in the plugin (R3-clean, like LogBackend), so
no platforms/->daemon value edge and no CLI cold-start regression. Pinned by
two table-equivalence parity tests (independent verbatim oracle, full
platform x kind x target matrix, facet-presence + fallthrough + end-to-end
routing).

* docs: mark b.3 recording/providers facets as landed in CONTEXT.md

This PR routes the recording + providers PlatformPlugin facets, so the durable
architecture context can no longer list them as deferred/on-the-daemon-branch.
Update the Deferred section: all four b.3 facets now route through the plugin;
only the perf sampling body (buildPerfResponseData) still branches in the daemon.
2026-07-02 09:42:50 +02:00
Michał Pierzchała b01fc190f5 fix: pin agent-cdp 1.6.1 (#1006) 2026-07-02 07:41:46 +02:00
Michał Pierzchała b6128c0088 docs: retire plans/perfect-shape.md — roadmap complete (#1003)
* docs: retire plans/perfect-shape.md — roadmap complete

The perfect-shape roadmap (two-registry thesis: CommandDescriptor +
PlatformPlugin, typed-result spine, folder DAG + layering lint, agent-cost,
and the Apple apple+appleOs platform model with a non-breaking leaf wire) is
substantively complete and merged. Per its own §5 retirement note, the durable
decisions now live in ADR-0008 (command descriptor) and ADR-0009 (Apple/AppleOS),
and current-state terms in CONTEXT.md; this removes the last plan file.

- Delete plans/perfect-shape.md (plans/ is now empty and gone).
- CONTEXT.md: add "Architecture (perfect-shape refactor, completed 2026-07)"
  end-state summary plus a "Deferred / next-minor" note (Phase 2c client-types
  narrowing, b.3 recording/providers facets, strict DAG back-edge inversion,
  legacy alias drops) so nothing is lost.
- Repoint every remaining perfect-shape.md/§ reference (ADRs 0003/0008/0009,
  ci.yml, scripts/layering/check.ts, and the platform-plugin/apple comments)
  to ADR-0008/0009 or CONTEXT.md. No dangling references remain.

Docs/comment-only; tsc, oxlint, oxfmt, and the layering DAG check all pass.

* docs: repoint dangling perfect-shape section refs before retiring the roadmap

Removing plans/perfect-shape.md left three comments citing bare section numbers
with no surviving target. The rationales are already inlined, so drop the numbers
(and point the do-not-flatten note at the durable ADR):
- src/platforms/apple/plugin.ts: `(§7)` -> "do-not-flatten; see docs/adr/0009".
- src/core/interactors/register-builtins.ts: "the §5.1 ... sketch" -> "an ... sketch".
- scripts/layering/check.ts: drop `(§5.5 ...)`, keep the inline "re-export barrels only".
2026-07-02 07:19:37 +02:00
Michał Pierzchała 36917157ab test: surface-wide guard that no command response emits internal apple platform (#1005)
Adds a provider-integration guard (apple-platform-output-guard.test.ts) that
stands up a fake-provider daemon for BOTH a macOS Apple session and an
iOS-simulator Apple session, drives EVERY public command off PUBLIC_COMMANDS,
and deep-scans each serialized response for the internal 'apple' platform token
(any string VALUE or object KEY that exactly equals 'apple').

The guard is catalog-driven: a partition test fails if a new public command is
neither in DRIVEN_COMMANDS nor SKIPPED_COMMANDS, so a new command can't silently
escape the check. All 50 public commands are driven; the skip-set is empty.

Caught + fixed a leak PR #1004 misses: doctor's data.platform (session-doctor.ts)
echoed the raw internal device.platform ('apple') when doctor ran against a bound
session with no --platform flag. Now projected via publicPlatformString(device).

doctor's byPlatform.apple KEY leak stays tracked to PR #1004 via a narrow,
documented allowlist entry (not duplicated here).
2026-07-02 07:18:56 +02:00
Michał Pierzchała fa76a91eca fix: project doctor output platform to the public leaf after the collapse (#1004)
The Platform collapse projected most `doctor` output but missed the device
inventory breakdown: `deviceInventoryEvidence` keyed `byPlatform` by the raw
internal `device.platform`, so an Apple session emitted `byPlatform: { apple: … }`
in the doctor response — a machine-facing `apple` leak (approach b keeps output
leaf `ios`/`macos`). Key it by `publicPlatformString(device)` instead (now
`Map<PublicPlatform, …>`, so an `apple` key is compile-impossible), which also
splits Apple devices into ios vs macos in the breakdown.

Also project two doctor command SUGGESTION strings (`apps --platform …`,
`close --platform …`) to the leaf — `--platform apple` was valid but broader than
the session's actual device.

The doctor descriptor routing (command-descriptor `daemon` block) and its lack of
a platform-specific `supports` gate are already correct; toolchain checks already
accept `apple`. Verified: tsc, oxlint, oxfmt, layering, fallow, provider-integration
82/82; full unit green except the known android fillAndroid contention flake
(passes 92/92 in isolation).
2026-07-01 20:47:15 +02:00
Michał Pierzchała cd1551bf42 refactor: collapse public Platform ios/macos into apple (#979) (#1002)
* refactor: collapse public Platform ios/macos into apple (#979)

Phase 3 d.3: collapse the internal `Platform` union from `ios`/`macos` to a
single `apple` platform, with `appleOs` as the sole OS discriminant. Approach
(b) NON-BREAKING: the daemon still ACCEPTS the legacy `ios`/`macos` selectors on
every read path and still EMITS the leaf `ios`/`macos` strings on every output,
so machine consumers see no change.

Kernel (src/kernel/device.ts):
- PLATFORMS = ['apple','android','linux','web']; add PUBLIC_PLATFORMS (leaf) and
  PublicPlatform; PLATFORM_SELECTORS keeps legacy `ios`/`macos` as input aliases.
- New predicates: isMacOs (appleOs- or legacy-leaf-based), isIosFamily (the
  post-collapse equivalent of `platform === 'ios'`), publicPlatformString (output
  projection), deviceFieldsFromPublicPlatform (inverse), isPublicPlatform.
- isMobilePlatform and matchesPlatformSelector are now device-aware (appleOs).

Discovery now stamps `platform: 'apple'` (+ appleOs); ~125 internal
`device.platform === 'ios'|'macos'` branch sites migrated to the predicates,
behavior-preserving. Apple plugin owns `['apple']`; platformDescriptors collapse
to one `apple` row.

Output projection (approach b) emits the leaf via publicPlatformString at:
devices / session_list (session-inventory), boot / shutdown / appstate /
prepare-ios-runner (session-state, session), the selector/backend platform
(selector-runtime/screenshot-runtime/snapshot-runtime/interaction-runtime),
proxy device key, request-lock backfill, runtime-set binding, click-button
validation, and both `.ad` context-line writers.

Contracts/client keep leaf types (PublicPlatform); read paths (parsePlatform,
REPLAY_METADATA_PLATFORMS, matchesPlatformSelector) accept `apple` + legacy
leaves. Adds a parity test gate (platform-collapse-parity.test.ts).

Refs #979 (part of #972).

* fix: project platform to the public leaf at open/perf response sites (#979)

The Platform collapse left two daemon response builders emitting the raw
internal `device.platform` ('apple'), which the client normalizer rejects
(isPublicPlatform excludes 'apple') — dropping the resolved device from the
response:
- session-open-surface.ts: `open` result `platform`/device projection.
- session-perf.ts: the perf/frames/memory base response builders.
Both now go through `publicPlatformString(device)`, so output stays the leaf
`ios`/`macos` per approach (b). (The android/non-apple perf branches were
already leaf-safe.)

Also update macos-desktop provider test: the lifecycle mock observes the
INTERNAL DeviceInfo, which is now `platform:'apple'` (+ appleOs:'macos'), so the
recorded tag is `prepare:apple:desktop`.

Fixes the provider-integration assertions that blocked both the Integration
Tests and Coverage CI jobs (both run the provider-integration project).
Verified: provider-integration 82/82, coverage passes, tsc/oxlint/oxfmt/layering/
fallow green.

* fix: project platform to the public leaf at nested output sites (#979)

The Platform collapse (approach b) projects device.platform through
publicPlatformString at emit sites so machine consumers keep seeing the
leaf ios/macos and never the internal `apple`. Several nested output
fields were missed. Project them and narrow their emitted types to
PublicPlatform:

- Apple perf memory snapshot support (buildAppleMemorySnapshotSupport) —
  response.support.platform / artifact.support.platform, plus the
  sibling sampleAppleFramePerf error data.
- Apple xctrace perf capture/result platform surfaced in the perf
  cpu-profile started/stopped response data.
- snapshotDiagnostics.stats.platform (recordSnapshotTiming) surfaced in
  snapshot/test response data and the slow-snapshot warning string.
- doctor target-app evidence.platform + human summary, and doctor
  target-app-device evidence.booted[].platform.
- provider/cloud UNSUPPORTED_OPERATION error.data.platform for cloud
  Apple devices (reachable via deviceFieldsFromPublicPlatform).

Internal 'apple' emissions (selector-matching input, diagnostic
emitDiagnostic telemetry, session appLog state, replay .ad flags) are
left as-is. Adds focused tests pinning Apple perf memory support to the
leaf and a guard asserting no emitted platform field equals 'apple'.
2026-07-01 19:29:46 +02:00
Michał Pierzchała d1b56c0897 0.18.1 v0.18.1 2026-07-01 16:26:05 +02:00
Michał Pierzchała 4cd40aa621 feat: polish replay test progress reporter (#998)
* feat: polish replay test progress reporter

* test: stabilize replay reporter cursor test in CI

* refactor: dedupe replay reporter live progress checks

* fix: make Expo build cache path configurable
2026-07-01 16:21:56 +02:00
Michał Pierzchała d82675ca80 refactor: relocate Apple plugin, interactor, interactions under platforms/apple (#975) (#1001)
Phase 3 d.1. Makes getInteractor's core -> platforms routing the final shape by
moving the Apple-specific plugin pieces under src/platforms/apple/ while keeping
the generic registry where non-interactor core code can still import it.

Moved to src/platforms/apple/:
- plugin.ts       - the applePlugin instance (APPLE_SUPPORTS_BY_DEFAULT closures,
                    appLog/perf facets, createInteractor/discoverDevices), extracted
                    from the former core/platform-plugin/register-builtins.ts
- interactor.ts   - was core/interactors/apple.ts (createAppleInteractor)
- interactions.ts - was platforms/ios/interactions.ts (the Apple interaction
                    dispatcher: iOS synthesized gesture / tvOS remote-press /
                    macOS desktop-scroll); platforms/ios/ is now removed
- __tests__/watchos-sentinel.test.ts - co-located with its subject

Stayed in core/ (layering):
- The generic registry + PlatformPlugin type stay in core/platform-plugin/plugin.ts.
  core/capabilities.ts (non-interactor core) imports getPlugin/tryGetPlugin, and R3
  forbids core outside core/interactors/ from statically importing platforms/, so the
  registry cannot move.
- register-builtins.ts moved to core/interactors/register-builtins.ts (still core/):
  the android/linux/web wiring plus the registry-population entry point. As an
  interactor-seam module it is the one place R3 permits a static value import of
  platforms/, so it pulls in applePlugin and keeps the exhaustiveness assertion.
- apple-os-capabilities.ts stays in core/ (smallest move; the moved Apple closures
  reach it via a legal platforms -> core import).

Layering guard passes (R1/R2/R3, 678 files). The applePlugin only reaches leaf code
via lazy dynamic import(), so the new static platforms/apple/plugin.ts import at the
seam does not regress CLI cold-start.
2026-07-01 16:21:24 +02:00
Michał Pierzchała f0b926c21f fix: trim root type exports (#999) 2026-07-01 14:47:03 +02:00
Michał Pierzchała 73c4439574 refactor: read Apple capability closures from a per-AppleOS table (#978) (#997)
Introduce a per-`AppleOS` capability data table
(`src/core/platform-plugin/apple-os-capabilities.ts`) — the capability-axis
sibling of `RUNNER_PLATFORM_PROFILES` and the Swift `#if os()` guards — and have
the Apple capability closures read `device.appleOs` through it, collapsing the
scattered `target !== 'tv'` / `platform !== 'macos'` / `isTvOsDevice` predicates
into one lookup (`resolveDeviceAppleOs` + `appleOsCapabilities`).

Discipline (perfect-shape §7 / ADR-0009 step d.5): relocate the OS-axis
predicates into data, never change behavior. Only the AppleOS-shaped facts moved
to the table; the DEVICE-shaped nuance (simulator vs physical device — e.g.
two-finger synthesis is iOS-simulator-only, and the physical-iOS hint) stays in
the reading closure, and non-Apple branches keep their verbatim verdicts
(`appleOsCapabilities` returns `undefined` off the Apple family).

Parity gate: a new table-equivalence test
(`apple-os-capability-table-parity.test.ts`) pins the table-driven closures
byte-for-byte against an INDEPENDENT verbatim copy of the original predicates
across the full {command × sample-device} matrix, and the existing
capability-plugin-routing-parity test is extended with appleOs-bearing
iPadOS/visionOS fixtures so the stored-`appleOs` read path is covered
(iOS/iPadOS/tvOS/macOS/visionOS).

Deferred (behavior-change risk / out of scope): the `isTvOsDevice` interaction
leaves (dispatch-interactions, interactors/apple, platforms/ios) are irreducible
per-device gesture/focus synthesis, not capability admission; per-gesture table
granularity (pinch/rotate/transform) is left as one `multiTouchSynthesis` field
since all three are uniform today.
2026-07-01 14:25:07 +02:00
Michał Pierzchała a3e967526a refactor: rename ios-runner -> apple-runner (#981) (#996)
Finish the cosmetic ios-runner -> apple-runner rename now that the
top-level XCTest runner is the OS-agnostic Apple engine
(iOS/iPadOS/tvOS/macOS/visionOS from one Xcode project).

Cosmetic only, no behavior change:
- git mv ios-runner/ -> apple-runner/ (AgentDeviceRunner, README, RUNNER_PROTOCOL)
- Update repo project-path consumers: build-xcuitest-apple.sh, package.json
  files globs, .fallowrc.json, write-xcuitest-cache-metadata.mjs,
  runner-xctestrun.ts fingerprint/project paths, recording overlay + test,
  daemon-client-timeout kill pattern, setup-apple-replay hashFiles glob,
  ci.yml swift-compat scan, AGENTS.md.
- Rename runtime home cache/derived/lease dir default
  ~/.agent-device/ios-runner -> ~/.agent-device/apple-runner (build script,
  package/clean scripts, runner-xctestrun RUNNER_DERIVED_ROOT, runner-lease,
  runner-contract hint, cli-help/commands.md docs) and the tests asserting it.
- Rename OS-agnostic runner symbols: runIosRunnerCommand ->
  runAppleRunnerCommand, prewarmIosRunnerCache -> prewarmAppleRunnerCache,
  createIosRunnerCachePrewarmOnColdBoot / createIosRunnerCacheColdBootPrewarmForOpen
  -> createAppleRunner* (+ call sites, type aliases, test mocks).

Intentionally left as ios-runner (out of scope / would change behavior):
- prepare ios-runner CLI subcommand (user-facing command name)
- AGENT_DEVICE_IOS_RUNNER_* env var names and .tmp/ios-runner-derived CI values
- ios-runner-prebuilt cache-key-prefix, ci.yml job id, workflow/ADR filenames
- agent-device-ios-runner-<version> release artifact basenames

Part of #972 (Phase 3 - Apple PlatformPlugin).
2026-07-01 14:24:48 +02:00
Michał Pierzchała d21b8ce32e feat: add doctor command (#883)
* feat: add doctor command

* fix: reduce doctor command complexity

* fix: classify doctor integration flags

* fix: simplify doctor setup

* refactor: split doctor checks

* fix: simplify doctor check set

* fix: include stopped android avds in devices

* fix: report doctor device inventory

* refactor: reuse device inventory selectors

* fix: summarize doctor inventory by platform

* fix: show metro cwd in doctor

* refactor: simplify metro doctor lookup

* fix: update doctor imports after apple consolidation

* feat: make doctor Metro probe controllable and surface hidden toolchain failures

Two gaps found while verifying the doctor command on a real environment:

- Metro host/port were uncontrollable from the CLI: --metro-host/--metro-port
  were rejected by allowedFlags, and readDoctorOptions only read them from
  req.runtime (populated by remote/connection profiles, never a plain CLI
  flag). The Metro check's own hint told users to 'pass the correct
  --metro-host/--metro-port', which did not exist. Declare the flags and read
  them from req.flags (runtime kept as fallback) so the probe can target any
  endpoint, e.g. from outside an RN/Expo project directory.

- A broken per-platform toolchain was silently hidden: readDoctorDeviceInventory
  dropped inventory failures whenever any other platform returned devices, so a
  broken Xcode or Android SDK still reported a green 'pass'. Keep the failures
  and surface each as a warn (device-<platform>) when other platforms have
  devices; scoped --platform runs stay quiet.

* fix: align doctor CI expectations

* feat: extend doctor preflight checks

* fix: keep doctor checks within ci gates

* fix: simplify doctor metro surface

* refactor: trim doctor bundle impact

* fix: restore useful doctor diagnostics

* refactor: reuse doctor output helpers

* refactor: share device inventory grouping

* refactor: keep doctor focused on preflight checks

* refactor: simplify doctor toolchain probes

* fix: keep scoped simulator hint generic

* fix: clarify doctor Xcode selection context

* fix: recognize provider scope in remote doctor

* fix: address doctor review gaps

* fix: keep doctor metro checks inferred
2026-07-01 14:24:32 +02:00
Michał Pierzchała 6e8e45fdaf refactor(daemon): route perf support gate through PlatformPlugin perf facet (#995)
Add the `perf` facet to `PlatformPlugin`, typed platform-neutral as
`{ supportsMetrics(device: DeviceInfo): boolean }` (never the iOS provider
seam). Populate it by wrapping the Apple + Android arms of the existing
`supportsPlatformPerfMetrics` predicate verbatim (both return `true`), leave
linux/web factless, and route `supportsPlatformPerfMetrics` in
`daemon/handlers/session-perf.ts` through `tryGetPlugin(...).perf?.supportsMetrics`
with a `?? false` fallthrough that preserves the former hand disjunction.

Pinned by a table-equivalence parity test
(`daemon/__tests__/perf-plugin-routing-parity.test.ts`) that mirrors the merged
appLog gate: an independent verbatim copy of the former predicate is the BEFORE
oracle across the exhaustive platform x kind x target device matrix, facet
presence/fallthrough are asserted, and `buildPerfResponseData` is exercised with
no-app sessions to prove the daemon actually routes through the facet.

Only the support gate is routed; the perf sampling body (`buildPerfResponseData`)
and the Android-only native-collector gate stay on their daemon branch until each
clears the same gate. Follows the merged appLog facet template (type-only
core->daemon edge, lazy `registerBuiltinPlatformPlugins()`).

Refs #974
2026-07-01 13:37:32 +02:00
Michał Pierzchała 56b41a53ab feat: add cross-platform audio probe (#880)
* feat: add web audio probe

* fix: stabilize web audio probe

* test: cover audio probe review gaps

* fix: address audio probe review feedback

* feat: support macOS audio probe

* docs: document audio probe help

* feat: support simulator audio probe

* test: account for host audio platform support

* refactor: deepen audio probe lifecycle

* perf: trim audio probe package size

* refactor: address audio probe review comments

* refactor: remove audio probe leftovers

* fix: encode audio probe eval options as data

* fix: document audio probe eval sanitization

* fix: sanitize audio probe eval options

* fix: use codeql-recognized eval option sanitizer

* fix: allowlist audio probe eval options

* fix: avoid json-stringified audio eval action

* refactor: trim audio probe input surface

* fix: align audio probe with apple helper paths

* test: update audio capability parity oracle

* refactor: isolate host audio probe backend

* fixup! refactor: isolate host audio probe backend

* fixup! refactor: isolate host audio probe backend

* fixup! test: update audio capability parity oracle
2026-07-01 13:27:45 +02:00
Michał Pierzchała a707348b9b feat: add hosted WebDriver provider support (#948)
* feat: add cloud webdriver artifacts

* fix: clean up local session after provider release failure

* fix: tag cloud webdriver provider requests

* feat: connect hosted webdriver providers

* docs: document hosted provider credentials

* refactor: tighten cloud webdriver provider internals

* refactor: consolidate cloud webdriver provider definitions

* refactor: collapse hosted webdriver runtime wrapper

* refactor: reduce cloud webdriver smell surface

* docs: clarify hosted provider interfaces

* fix: avoid regex slash trimming in webdriver urls

* docs: rename hosted providers to device clouds

* fix: align provider profile imports with remote modules

* fix: skip local android recovery for provider devices

* test: classify cloud provider integration flags

* fix: close active cloud connection session

* test: cover provider disconnect cli flow

* fix: make cloud webdriver sessions launchable

* fix: align cloud webdriver input gestures

* fix: avoid keyboard input during cloud scroll

* fix: constrain cloud webdriver scroll gestures

* refactor: isolate cloud webdriver scroll frame

* refactor: deduplicate cloud webdriver helpers

* refactor: tighten cloud webdriver action types

* fix: harden cloud webdriver release

* fix: polish provider disconnect diagnostics

* refactor: group connection profile helpers

* fix: repair rebased internal paths

* fix: satisfy cloud webdriver CI guards
2026-07-01 13:01:47 +02:00
Michał Pierzchała 3454ff1aa5 refactor: relocate Apple supports()/unsupportedHint() closures onto the plugin (#973) (#993)
Phase 3 step b.2. Move the per-command supports() / unsupportedHint() device
closures VERBATIM off the command-descriptor facet onto the owning
PlatformPlugin's capability.supportsByDefault / unsupportedHintByDefault
(perfect-shape §7 / ADR-0009: relocate, never flatten). Bodies are byte-for-byte
identical; only their ownership moves to the Apple plugin, the family that owns
every discriminating device (macOS-coordinate-pinch, tvOS-no-touch, physical-iOS,
two-finger-synthesis).

The relocation is faithful because every closure is a no-op (returns true /
undefined) on non-Apple devices, so consulting it only for the Apple family
leaves admission unchanged across the full device matrix. isCommandSupportedOnDevice
and unsupportedHintForDevice now read the closure off getPlugin(device.platform);
the command facet carries platform/kind buckets only, and supports/unsupportedHint
are removed from the CommandCapability type.

Parity gate (byte-for-byte, before deleting the hand sites): independent VERBATIM
oracle in capability-plugin-routing-parity.test.ts pins (a) production admission +
hint output unchanged across the {platform x command x kind x target} matrix, and
(b) the relocated Apple-plugin closures are behaviorally identical to the originals
across the device-fixtures sample matrix, with a guard that no non-Apple family
grew a gate.
2026-07-01 12:38:26 +02:00
Michał Pierzchała c0b9c54c44 refactor(apple): promote tvOS to an explicit Apple-OS leaf (#976) (#992)
Name the tvOS Apple-OS leaf instead of branching on a `target === 'tv'`
string smeared across the Apple interaction paths (Phase 3 d.2, part of #972).

- Add `isTvOsDevice(device)` to kernel/device.ts — the sink, so core, the
  command-descriptor registry, and the platform code can all gate on one
  explicit, Apple-only predicate (the layering DAG forbids core importing
  platforms). Android TV shares `target: 'tv'` but is a distinct leaf, so the
  `platform === 'ios'` gate is load-bearing.
- Extract the XCUIRemote focus-navigation command builder into a real
  `src/platforms/apple/os/tvos/` leaf (mirroring os/macos/), delivering the
  dedicated tvOS leaf ADR-0009 deferred.
- Route the unambiguous tvOS gates (interactor back/home/scroll, iOS-touch
  synthesis, registry capability predicates) through `isTvOsDevice`.

Do-not-flatten preserved: the tvOS focus-only interaction contract stays
per-OS. back/home/scroll drive XCUIRemote focus; coordinate tap goes to the
runner un-synthesized (rejected off the focused element); pinch/rotate/
transform stay UNSUPPORTED. This is a rename/extraction — behavior is
byte-identical for tvOS devices.

Intentionally left (noted inline): devices.ts `resolveAppleOs` (the canonical
target→appleOs classification source), and the dispatch pinch/rotate/transform
gates — those `target === 'tv'` checks also reject Android TV, so narrowing
them to the Apple-only leaf would change Android-TV behavior.

Tests: `isTvOsDevice` Apple-only gate (excludes Android TV); tvOS back/home
focus navigation via remotePress menu/home + iOS keeps in-app back; tvOS
rotate/transform reject UNSUPPORTED at dispatch (alongside existing pinch).
2026-07-01 12:24:18 +02:00
Michał Pierzchała 643ed2cbd3 feat: route daemon app-log backend through PlatformPlugin appLog facet (#974) (#991)
Phase 3 b.3: add the first daemon-owned facet to `PlatformPlugin`, typed
against a PLATFORM-NEUTRAL wrapper and pinned by a table-equivalence parity
test before the daemon lookup routes through it.

- `PlatformPlugin.appLog.resolveBackend(device): LogBackend` — a neutral
  string-union tag (never the iOS-simulator provider seam). Optional facet,
  present only on families with an app-log backend (Apple + Android).
- Populate by wrapping the existing `resolveLogBackend` branch verbatim on the
  Apple/Android plugins; linux/web omit the facet and the daemon lookup
  preserves the historical `'android'` fallthrough.
- Route `src/daemon/app-log.ts`'s `resolveLogBackend` through
  `tryGetPlugin(...).appLog?.resolveBackend(...)`; populate the registry at
  module load (idempotent, lazy — mirrors `core/capabilities.ts`).
- Pin with `applog-plugin-routing-parity.test.ts`: the routed function is
  byte-identical to an independent verbatim copy of the former hand branch
  across the device fixtures + exhaustive platform x kind x target matrix.

Deferred (not populated / not parity-added to the contract): `providers`
(load-bearing scope-orchestration seam), `recording` (needs the
de-iOS-naming start/stop-context redesign), and `perf` (heavy internal
response-builder). Each stays the daemon branch's source of truth.
2026-07-01 12:23:40 +02:00
Michał Pierzchała f627203cca feat: gate watchOS as an explicit unsupported sentinel (closes #977) (#990)
XCUITest cannot drive watchOS UI (no XCUIApplication), so a watchOS device has no
runner backend. Today `appleOs: 'watchos'` silently falls through to the iOS
runner profile (resolveRunnerPlatformNameForAppleOs). Reject it explicitly at the
admission seam — createAppleInteractor — with a clear UNSUPPORTED_PLATFORM error,
before any runner work.

Discovery never produces watchOS today (resolveAppleOs only yields
ios/ipados/tvos/visionos), so this changes no real-device behavior; it makes the
"declared but unsupported" watchOS case honest instead of a silent wrong fallback.
Per-AppleOS capability-table integration is deferred to d.5 (#978).

Adds a focused unit test asserting the watchOS rejection and that a non-watchOS
appleOs does not trigger the sentinel.
2026-07-01 12:23:16 +02:00
Michał Pierzchała 1d1cf0eb02 test: relocate the 2 deferred Apple runner/recording tests (closes #980) (#988)
Completes the #983 relocation. The 2 files deferred there compute runtime fs
paths (fileURLToPath/__dirname), so they needed path-depth fixes on top of import
re-relativization:

- runner-client.test.ts -> src/platforms/apple/core/__tests__/ (joins the rest of
  the Apple engine tests); repoRoot recomputed one level deeper.
- recording-scripts.test.ts -> src/recording/__tests__/ (beside the overlay.ts it
  tests); ios-runner RecordingScripts + test/integration/support paths recomputed
  one level shallower.

src/platforms/ios/__tests__/ is now empty (all Apple-engine tests live beside
their source under apple/core and recording). Pure test relocation — tsc, oxlint,
oxfmt, layering guard, fallow green; full unit suite passes (2883).
2026-07-01 11:35:27 +02:00
Michał Pierzchała 2e0879a260 fix: exempt sdk/ public barrels from R3 platforms-seam (unblock main) (#987)
#984 added the R3 platforms-seam layering rule and #986 moved the public SDK
entry barrels into src/sdk/; the two merged mutually inconsistent, so the
Layering Guard is failing on main. sdk/ are public re-export barrels that
legitimately expose platform symbols and are off the CLI cold path (not imported
by bin.ts), so they are a correct R3 exemption alongside core/interactors and the
daemon server — not a cold-start regression.
2026-07-01 10:21:03 +02:00
Michał Pierzchała 87c43562d3 refactor: consolidate public SDK entry barrels into src/sdk/ — Phase 5 (#986)
Move the package's public entry points into a dedicated src/sdk/ folder so the
public surface lives in one clearly-owned place, per plans/perfect-shape.md §5.5
("sdk/ = re-export barrels only") and the §3 target DAG. Follows the #951/#960
pattern (behaviorless path codemod; rslib entry KEYS unchanged so dist output
paths — and therefore package.json `exports` — stay byte-identical).

Barrel structure (all 11 public subpaths now resolve through src/sdk/):
- 9 already-thin barrels moved verbatim (git rename, internal import paths
  re-depthed by one ../): index, artifacts, metro, batch, remote-config,
  install-source, android-adb, contracts, selectors.
- io and finders keep their implementation at src root (real logic + internal
  importers); src/sdk/io.ts and src/sdk/finders.ts re-export them (export *).

package.json exports/main/types: UNCHANGED. Each rslib entry key is preserved
(index, io, ...), so dist output stays dist/src/<name>.{js,d.ts} and the
published subpaths + files do not move. No exports rewrite was needed.

Repoints:
- rslib.config.ts: 11 entry sources -> src/sdk/*.ts (keys unchanged).
- 7 public *-test imports -> src/sdk/*.
- 3 internal src importers of the selectors barrel (find.ts, selector-read.ts,
  resolution.ts) split to import impl directly (utils/selectors-parse.ts +
  daemon/selectors.ts) — removes internal dependence on the public barrel.
- .fallowrc.json entrypoints + vitest coverage exclude (src/sdk/** glob).

Published-surface verification (npm pack --dry-run before/after):
- exports map identical; 146 packed files identical EXCEPT one internal
  code-split chunk renumbered (dist/src/9836.js -> 893.js, byte-identical
  content).
- All 11 public .d.ts byte-identical; 10/11 public .js byte-identical;
  selectors.js differs only by that internal chunk reference. Same symbols.

Gates: tsc 0 · rslib build 0 · oxfmt/oxlint clean · fallow audit (31 files) clean
· vitest unit 2883 pass.
2026-07-01 10:03:57 +02:00
Michał Pierzchała e229957602 refactor: split daemon server runtime into daemon/server/ — Phase 5 (#985)
Phase-5 §5.5 folder move (server side; the daemon/client/ split shipped in
#962). Extracts the process-bootstrap / server-runtime cluster into
src/daemon/server/ as a pure, behaviorless path codemod — no logic changes.

Moved (server bootstrap/runtime — the layer that spins up the daemon and
owns the platform graph; each imported only by the bootstrap layer + each
other):
  src/daemon-runtime.ts          -> src/daemon/server/daemon-runtime.ts
  src/daemon/http-server.ts      -> src/daemon/server/http-server.ts
  src/daemon/transport.ts        -> src/daemon/server/transport.ts
  src/daemon/server-lifecycle.ts -> src/daemon/server/server-lifecycle.ts
  src/daemon/server-shutdown.ts  -> src/daemon/server/server-shutdown.ts

Left in src/daemon/ root (request core / shared wire helpers, out of scope):
  request-router.ts, handlers/, session-store.ts, lease-registry.ts, context.ts
  (the daemon's request layer) and http-contract.ts / http-health.ts /
  http-errors.ts / config.ts (HTTP wire contract + daemon config shared across
  client, remote, and cli — not server-only).

Left: src/daemon.ts (the thin process entry) stays at src/ with the other
package entrypoints; it is coupled to its physical path by four non-import
string references (rslib entry, config dev-mode sentinel, process-identity
detection regex, daemon-client launch srcPath), so moving it is beyond a pure
import codemod.

Rewrote every from/import/import()/type-only specifier per importer
(resolve-based path.relative recompute) across src and test, and renamed the
fallow health-baseline key for http-server.ts. daemon-runtime's static
platforms/ import is now inside the daemon-server seam the layering lint
(#984 R3) allows.

Verification: tsc --noEmit 0; layering check (branch script) unchanged (3
pre-existing R3 violations, 0 new); oxfmt clean; oxlint --deny-warnings 0;
fallow audit --base origin/main clean (14 files); rslib build 0
(internal/daemon entry still emits); vitest 17 passed (daemon-entrypoint,
http-server-rpc-validation, server-shutdown + 3 provider-integration).
2026-07-01 10:02:59 +02:00
Michał Pierzchała 3d70943550 feat: enforce import-direction DAG (Phase-5 layering lint) (#984)
Generalize the inline CI "Layering Guard" grep into a structured
import-direction lint (scripts/layering/check.ts) over the resolved
import graph, per plans/perfect-shape.md §5.5.

The full target DAG (kernel ◄ platforms ◄ core ◄ commands ◄ {cli,
client, daemon/server}; client ◄ daemon/client) is only partly realized
— the client/remote/metro extraction, the daemon/server split, and the
utils dissolution are still pending Phase-5 moves, so the tree still
holds legitimate back-edges (platforms→core, commands→cli, utils→*).
Enforcing the whole DAG today would need a mass import rewrite that
Phase 5 defers. The lint therefore enforces the three invariants the
completed moves (kernel/, daemon/client/) already guarantee and that are
green today:

  R1 kernel-sink      — nothing under src/kernel/ imports another zone,
                        except the one type-only kernel→contracts re-export.
  R2 commands-floor   — nothing below the command surface (kernel,
                        platforms, core, daemon) imports src/commands/.
                        Generalizes the former guard (daemon + platforms).
  R3 platforms-seam   — platforms/ is statically imported only at the
                        core interactor seam (src/core/interactors/) and by
                        the daemon server; elsewhere use a dynamic import()
                        or a type-only import, preserving CLI cold-start.

Dynamic import('../platforms/*') and `import type` stay allowed.

Fixes the three pre-existing R3 violations by converting static
platforms value imports to dynamic imports (all in already-async call
sites, behavior-preserving and cold-start-improving):
  - src/client/client.ts        debug.symbols → lazy symbolicateCrashArtifact
  - src/cli/commands/web.ts      setup/doctor → lazy agent-browser-tool
  - src/core/dispatch-interactions.ts  runner-sequence → lazy (matches the
                                       file's own dynamic-import pattern)

Wire the check into the Layering Guard CI job and add a check:layering
package.json script (also folded into check:tooling). scripts/layering/**
is excluded from fallow (untested CI script, like scripts/perf/**).
2026-07-01 10:02:45 +02:00
Michał Pierzchała 6cd0ec3d20 test: relocate Apple engine unit tests from platforms/ios to platforms/apple/core (#980) (#983)
After #968 moved the OS-agnostic Apple engine to src/platforms/apple/core/, its
tests still lived in src/platforms/ios/__tests__/. Move the 19 that test Apple
engine code into src/platforms/apple/core/__tests__/ so tests sit beside their
source, re-relativizing every import / dynamic import / vi.mock / vi.importActual
specifier to the new depth.

Deferred (still in src/platforms/ios/__tests__/): recording-scripts.test.ts and
runner-client.test.ts — both compute runtime fs paths (__dirname / fileURLToPath)
to ios-runner artifacts, so they need path-string fixes, not just specifier
re-relativization. Tracked under #980.

Pure test relocation — no source or behavior change. tsc + oxlint + oxfmt green;
the moved suites pass (384 tests across the apple + deferred dirs).
2026-07-01 09:16:25 +02:00