Commit Graph

919 Commits

Author SHA1 Message Date
Michał Pierzchała 23caaa0a2a fix: address landing PR feedback 2026-07-01 20:50:02 +02:00
Michał Pierzchała 97e242bcff feat: reconcile landing home preview 2026-07-01 20:50:02 +02:00
Michał Pierzchała 275bf86290 fix: avoid duplicate product card keys 2026-07-01 20:50:02 +02:00
Michał Pierzchała 43cd88e9ec feat: reconcile product pages with Figma 2026-07-01 20:50:02 +02:00
Michał Pierzchała 6dd28fda5e fix: use GitHub pixel icon 2026-07-01 20:50:02 +02:00
Michał Pierzchała edcf9016e3 fix: use Figma React Foundation logo 2026-07-01 20:50:02 +02:00
Michał Pierzchała 7fc0055480 fix: align Callstack card with Figma 2026-07-01 20:50:02 +02:00
Michał Pierzchała 4d114be194 perf: optimize landing Figma assets 2026-07-01 20:50:02 +02:00
Michał Pierzchała 8407879e84 fix: use sharp Figma client logos 2026-07-01 20:50:02 +02:00
Michał Pierzchała 084caccba5 fix: align QA use case row icons with Figma 2026-07-01 20:50:02 +02:00
Michał Pierzchała df06d9edc1 fix: align use case tabs and pills with design 2026-07-01 20:50:02 +02:00
Michał Pierzchała f127e78280 fix: replace toolkit lane images with higher resolution exports 2026-07-01 20:50:02 +02:00
Michał Pierzchała 99236ad881 fix: link demo cta to contact form 2026-07-01 20:50:02 +02:00
Michał Pierzchała 18e621fce6 fix: update home cta github label 2026-07-01 20:50:02 +02:00
Michał Pierzchała ceae1ceff6 fix: serve toolkit lane images at retina resolution 2026-07-01 20:50:02 +02:00
Michał Pierzchała 2db6cff7b9 feat: add live github star link 2026-07-01 20:50:02 +02:00
Michał Pierzchała 4ea2425f66 feat: add launch landing site 2026-07-01 20:50:01 +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
Michał Pierzchała db0e084c30 docs: retire plans/phase3-platform-plugin-progress.md; track remaining work in issues (#982)
The remaining Phase 3 Apple PlatformPlugin work (steps b + d) is now filed as
GitHub issues under umbrella #972, so the standalone progress plan is redundant
and a staleness hazard (it already drifted once re: cost.runnerRoundTrips).

- Remove plans/phase3-platform-plugin-progress.md.
- Repoint its references at the durable sources: perfect-shape.md (x3) and
  ADR-0009 now link the Phase 3 tracking issue #972; the plugin.ts step-b facet
  note points at ADR-0009 (+ issue #974). Design rationale stays in
  perfect-shape.md and ADR-0009; live status lives in the issues.
2026-07-01 09:16:01 +02:00
Michał Pierzchała 62b3eb5c4c docs: clarify request-count gate vs. restored cost.runnerRoundTrips in Phase 3 plan (#971)
The Step (c) request-count bullet claimed both the dev-only CI gate AND the
runtime `cost.runnerRoundTrips` surface were removed in #968. #970 restored the
public agent-cost field, so the bullet is stale/misleading for the next
Apple/agent-cost worker.

Split the bullet: the dev-only request-count CI gate (the #966 --debug ndjson
counter + smoke-ios assertion) stays removed (zero runner events on main runs);
the runtime `cost.runnerRoundTrips` agent-cost field (ResponseCost /
buildResponseCost over RUNNER_ROUND_TRIP_PHASES) is a separate pre-existing
surface, restored in #970, and remains part of the agent-cost contract.
2026-07-01 07:37:53 +02:00
Michał Pierzchała f9445d0041 fix: restore runnerRoundTrips agent-cost field dropped in #968 (#970)
PR #968 (apple-platform-consolidation) accidentally dropped the
runnerRoundTrips field from the agent-cost block during an over-broad
conflict resolution. This restores the shipped Phase-4 feature:

- src/kernel/contracts.ts: re-add ResponseCost.runnerRoundTrips: number
- src/utils/diagnostics.ts: restore the countDiagnosticEventsByPhase()
  accessor over the flush-surviving phaseCounts tally (the tally itself
  survived #968; only the accessor was removed)
- src/daemon/request-router.ts: repopulate runnerRoundTrips in the cost
  graft by counting the two real round-trip diagnostic phases
  (ios_runner_command_send + ios_runner_readiness_preflight). The
  RUNNER_ROUND_TRIP_PHASES constant is now defined locally (its former
  home, the dev-only runner-request-count.ts, was removed in #968 and is
  out of scope to restore)
- request-router-cost.test.ts: restore the round-trip counting test and
  the runnerRoundTrips:0 assertion

Byte-identical-default invariant preserved: with --cost OFF or on an
error response the serialized payload is unchanged.
2026-07-01 07:24:44 +02:00
Michał Pierzchała 3df1e145b4 docs: fix stale platforms/ios paths after the Apple consolidation (#968) (#969)
Post-#968 follow-up: the OS-agnostic Apple runner engine moved from
src/platforms/ios/ to src/platforms/apple/core/, but several docs/config still
pointed at the old locations, misleading agents that grep those paths.

- AGENTS.md: repoint the runner-seam map, the Apple-family sync rule, the
  record/trace seam, the search-roots hint, and the platform-backends list at
  src/platforms/apple/core/...; rename "iOS Runner Seams" -> "Apple Runner Seams".
- .fallowrc.json: drop the two stale ignoreExports entries for the removed
  src/platforms/ios/apps.ts and src/platforms/ios/index.ts (the live
  src/platforms/apple/core/apps.ts entry already covers those test-only exports).
- ios-runner/{README,RUNNER_PROTOCOL}.md: point the TypeScript-client links at
  src/platforms/apple/core/runner/runner-client.ts.

Docs/config only; no behavior change. fallow audit + build/lint stay green, and a
live iOS simulator replay suite (6/6) confirms the consolidated runner works.
2026-07-01 07:24:22 +02:00
Michał Pierzchała 26ac865c63 refactor: consolidate Apple platform internals (#968) 2026-06-30 21:30:46 +02:00
Michał Pierzchała f4882bc706 feat: support live replay test reporters (#959)
* feat: support live replay test reporters

* refactor: simplify replay progress readers

* fix: preserve verbose replay reporter progress

* feat: expose semantic replay reporter hooks

* refactor: trim replay reporter context

* refactor: trim reporter progress internals

* refactor: move replay test reporting under replay

* refactor: make live replay reporter hooks synchronous and simplify dispatch

Live reporter hooks (onSuiteStart/onTestStart/onTestStep/onTestResult)
were typed as `void | Promise<void>` but fired from the synchronous daemon
progress stream reader without being awaited, so a stateful async reporter
could receive onSuiteEnd before its live work settled. Type them as `void`
to make the contract honest; onSuiteEnd stays awaited for async flushing.

A returned promise from a misbehaving custom JS reporter is still caught so
it cannot crash the CLI with an unhandled rejection, but it is documented as
unsupported and not awaited.

Collapse the four near-identical per-event hook dispatch branches into a
single table-driven path, and document the synchronous-hook and
exit-code-escalation contracts. Add a regression test covering a throwing
live hook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XXHAYxWpvSzqc6CtneYL8J

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-30 21:10:22 +02:00
Michał Pierzchała 60d1bd1765 fix: respect prepare timeout for runner health checks (#967)
* fix: respect prepare timeout for runner health checks

* fix: share prepare ios-runner timeout budget
2026-06-30 18:27:54 +02:00
Michał Pierzchała edc8dd059b ci: automate iOS runner request-count gate for the Apple runner unwind (Phase 3 step c prep) (#966)
Replaces the manual "run with --debug, hand-count the runner phases" check with
an automated, committed assertion so the Phase 3 step (c) runner relocation (and
future runner refactors) can prove byte-identical runner request behavior.

- src/daemon/runner-request-count.ts: pure, unit-testable counter. Parses the
  daemon --debug diagnostics ndjson and counts the iOS-runner round-trip phases,
  plus baseline parse/compare logic. Owns RUNNER_ROUND_TRIP_PHASES as the single
  source of truth, now imported by request-router.ts (was a local const) so the
  in-process cost graft and the external counter never drift.
- src/daemon/__tests__/runner-request-count.test.ts: 13 unit tests over synthetic
  ndjson fixtures (tolerant parse, counting, baseline parse/compare). Run in the
  normal unit suite; no hardware.
- scripts/runner-request-count/: assertion harness (run.ts) + committed baseline
  (expected-counts.json). Drives the existing smoke-ios replay scenario with
  --debug in an isolated --state-dir, counts runner round-trips from daemon.log,
  and asserts against the baseline. --update regenerates the baseline. Infra
  hiccups are inconclusive (don't fail); only a real count drift fails.
- .github/workflows/ios.yml: new "Assert iOS runner request count" step in the
  smoke-ios job, reusing the booted simulator.
- package.json: `validate:runner-count` script. .fallowrc.json: harness entry.

The baseline ships unarmed (established=false); the harness records observed
counts (printed + uploaded as a test/artifacts artifact) without failing, so the
maintainer arms it once from a real CI run.
2026-06-30 17:26:01 +02:00
Michał Pierzchała 903a35624e fix: extend iOS physical install timeout (#964) 2026-06-30 17:05:16 +02:00
Michał Pierzchała f430888baa refactor: route capability bucket through PlatformPlugin + pin supports() closures (Phase 3 step b) (#965)
b.1: isCommandSupportedOnDevice now reads each platform's capability bucket
from getPlugin(device.platform).capability.bucket (the PlatformPlugin registry,
ADR-0009) instead of the platformDescriptors fold. capabilities.ts registers the
builtin plugins at module load (idempotent, lazy closures only) so the admission
path populates the registry without depending on core/interactors.ts load order.

b.2: the per-command supports()/unsupportedHint() closures stay VERBATIM on the
command-descriptor facet; they cannot move to the plugin's per-FAMILY
capability.supportsByDefault without flattening their per-command shape
(perfect-shape §7). A new table-equivalence parity test pins both the bucket-route
swap and the closures byte-for-byte across the full platform x command x
device-kind x target matrix.
2026-06-30 17:04:33 +02:00
Michał Pierzchała bcf910a2bb refactor: split daemon client driver into daemon/client/ — Phase 5 (#962)
Move the daemon CLIENT driver (the in-process side that sends requests to a
running daemon) out of the src/ root into src/daemon/client/, per
plans/perfect-shape.md §5.5 ('daemon/client/ <- daemon-client*.ts'; the
daemon- prefix co-located client driver + server bootstrap at src root).

Files moved (7): daemon-client{,-lifecycle,-metadata,-progress,-rpc,-timeout,
-transport}.

- git renames; 19 importers repointed via the resolve-based codemod
  (intra-set stays ./, kernel -> ../../, daemon/remote deps recomputed)
- Layering Guard verified: none import src/commands/* (safe under src/daemon/)
- not a public export; no rslib impact
- update fallow-baselines/health.json keys

Behaviorless path codemod; typecheck/lint/format/build/tests green.
2026-06-30 15:51:53 +02:00