Commit Graph

994 Commits

Author SHA1 Message Date
Michał Pierzchała 0be806cc50 refactor: seed src/kernel with snapshot.ts type module — Phase 5 slice 2 (#932)
Establishes the kernel/ layer from the perfect-shape DAG (plans/perfect-shape.md
§5.5) by moving the foundational snapshot type module (Rect, SnapshotNode,
SnapshotQualityVerdict, centerOfRect — re-exported by contracts.ts and imported
by ~130 files) from src/utils/snapshot.ts to src/kernel/snapshot.ts.

Pure path codemod, no behavior change. snapshot.ts is a leaf (zero imports, after
the preceding cycle-break PR), so kernel/ takes no upward dependency — and every
one of its ~130 importers (utils, core, daemon, platforms, commands, tests)
becomes a clean downward import toward kernel. This also unblocks the future
snapshot/ AX-domain extraction: those domain files now import DOWN into kernel
rather than sideways within utils.

Imports rewritten by a resolve-based codemod (compares each specifier's resolved
path to the old module, so the other snapshot.ts files under commands/platforms/
daemon are untouched). 138 sites across 128 src files + 2 integration worlds.

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, full vitest suite
(308 files / 2863 tests), fallow audit clean (133 files), rslib build, Layering
Guard empty; kernel/snapshot.ts confirmed import-free.
2026-06-29 17:30:46 +02:00
Michał Pierzchała 0942ff072b refactor: break snapshot.ts <-> snapshot-quality.ts type cycle — Phase 5 prep (#931)
snapshot.ts (the foundational snapshot type module; Rect/SnapshotNode, 96
importers, re-exported by contracts.ts) imported SnapshotQualityVerdict from
snapshot-quality.ts, which in turn imports SnapshotNode from snapshot.ts — a
type-level cycle that blocks moving snapshot.ts down into a kernel layer.

Move the SnapshotQualityVerdict type definition into snapshot.ts (it is
referenced by a SnapshotNode field) and re-export it from snapshot-quality.ts so
every existing `from './snapshot-quality.ts'` import is unaffected. The
validation logic (readSnapshotQualityVerdict + const Sets) stays put.

Result: snapshot.ts has zero imports (a clean leaf), unblocking the kernel-seed
move and the eventual snapshot/ domain extraction. No behavior change.

Verified: tsc, oxfmt + oxlint --deny-warnings, fallow audit clean, 369 tests
across utils pass.
2026-06-29 17:12:03 +02:00
Michał Pierzchała ed701f3d1f feat: support resumable proxy uploads (#919)
* feat: support resumable proxy uploads

* refactor: reduce upload proxy fallow findings

* refactor: localize proxy upload routing

* test: simplify resumable proxy upload coverage

* refactor: deepen upload route handling

* refactor: tighten upload http types

* test: avoid dynamic proxy route dispatch

* feat: show remote upload progress

* fix: show install phase after upload

* fix: isolate resumable upload attempts

* refactor: split upload client internals

* test: make install notice test independent of CI env
2026-06-29 16:21:00 +02:00
Michał Pierzchała 4b7ee24637 refactor: extract screenshot-diff domain out of utils — Phase 5 slice 1 (#928)
First Phase 5 (layering) move: a pure path codemod, no behavior change. Relocates
the cohesive screenshot-diff domain (9 source files + 3 tests) from src/utils/
into a dedicated top-level src/screenshot-diff/ folder, per the target folder DAG
in plans/perfect-shape.md §5.5.

Boundary respects the real dependency graph:
- screenshot-diff-pixels.ts STAYS in utils — it is the pixel-diff primitive the
  shared png-worker depends on, not domain code. No moved file imports it.
- the moved files keep importing shared utils (png, exec, screenshot-geometry,
  snapshot, errors) via ../utils/.
- consumers updated: commands/capture/runtime/diff-screenshot.ts and
  utils/output.ts (the diff result-type formatter).

fallow-baselines/health.json keys for the two flagged files are renamed to the
new paths to preserve the baseline (no new findings).

Verified: tsc --noEmit, oxfmt + oxlint --deny-warnings, fallow audit (clean, 16
files), rslib build, and 431 tests across screenshot-diff/utils/capture all pass;
Layering Guard empty. git tracks all 12 as renames.

Note: utils/output.ts now imports the screenshot-diff result types cross-folder
(it formats diff output); when the import-direction lint lands it should move up
out of utils. Tracked as a Phase 5 follow-up.
2026-06-29 16:10:46 +02:00
Michał Pierzchała 54178319d8 perf: reduce iOS simulator screenshot overhead (#927) 2026-06-29 15:53:19 +02:00
Michał Pierzchała 9dc07cc56e perf: reuse Apple runner cache across version bumps (#900)
* perf: reuse Apple runner cache across version bumps

* perf: remove unused Apple runner symbols

* perf: keep Swift runner unit tests out of runtime builds

* perf: skip Apple runner asset catalog in runtime builds

* perf: use concrete simulator for xcuitest script builds

* perf: show cold Apple runner startup progress

* perf: prewarm Apple runner cache during simulator boot

* refactor: dedupe Apple runner option plumbing
2026-06-29 15:43:15 +02:00
Michał Pierzchała c702f0978f feat: add opt-in agent-cost nodeCount signal — Phase 4 slice 3 (#926)
Extends the opt-in cost block (slice 1 wallClockMs, slice 2/#925
runnerRoundTrips) with a command-agnostic nodeCount: the size of the UI/
accessibility node tree a command returns. In production only the snapshot
node-tree commands put a `nodes` array on response.data, so the router reads
it generically (Array.isArray(response.data.nodes)) and omits nodeCount
everywhere else. It lets an agent size a snapshot before re-fetching at a
different depth/scope, without parsing the full tree.

Purely additive and gated on meta.includeCost + response.ok, exactly like the
existing cost fields: with the flag off the serialized response is byte-
identical to today (Maestro .ad recompare safe). nodeCount is a pure read of
the existing nodes array — the parity test proves deleting the cost block
leaves a payload deep-equal to the flag-off response. Additive / semver-minor.
2026-06-29 15:36:05 +02:00
Michał Pierzchała 70844df4f2 feat: expose opt-in agent-cost over MCP via includeCost — Phase 4 slice 2 (#924)
Slice 1 added the opt-in cost.wallClockMs field on the daemon response; it
already rides on response.data → MCP structuredContent. This slice adds the
MCP-boundary opt-in so agents can request it: an includeCost tool argument
(mirroring the existing stateDir / mcpOutputFormat MCP config fields) that maps
to the client `cost` config (→ meta.includeCost on the daemon).

- readClientConfig sets client.cost = true only when includeCost === true, and
  rejects non-boolean values at the boundary.
- includeCost is stripped from the command input so it never leaks as a flag.
- withMcpConfigSchema advertises includeCost: boolean in each tool inputSchema.

Default-off is unchanged: with includeCost absent or false, the client config
and the MCP structuredContent are byte-identical to today. Additive /
semver-minor. Per-command MCP outputSchema and richer signals stay deferred.
2026-06-29 15:35:51 +02:00
Michał Pierzchała 3ce768160b feat: opt-in agent-cost runnerRoundTrips — Phase 4 slice 2 (#925) 2026-06-29 14:41:41 +02:00
Michał Pierzchała 59e1f4bd4a perf: speed up iOS simulator screenshots (#918)
* perf: skip boot probe for iOS simulator screenshots

* fix: recover skipped simulator screenshot boot checks

* refactor: simplify ios screenshot options
2026-06-29 14:35:21 +02:00
Michał Pierzchała 4eccfdde5c feat: infer install target from artifact (#923)
* feat: infer install target from artifact

* fix: allocate proxy lease for install
2026-06-29 14:33:40 +02:00
Michał Pierzchała 62f923dec4 feat: opt-in agent-cost wallClockMs behind --cost — Phase 4 slice 1 (#922)
* feat: opt-in agent-cost wallClockMs behind --cost

Add per-command wall-clock latency as a purely additive, opt-in response
field (cost.wallClockMs) gated behind a new global --cost flag.

The flag plumbs end to end mirroring --debug: cli-flags definition +
GLOBAL_FLAG_KEYS, AgentDeviceClientConfig/overrides, buildClientConfig,
buildMeta (meta.includeCost), the DaemonRequestMeta contract, and the
boundary parse in daemonCommandRequestSchema so it survives the HTTP edge.

The graft lives in request-router handleRequest (the seam that owns the
outer wall-clock incl. lock + execute + finalize). It mirrors the
conditional registerDownloadableArtifacts spread: when --cost is off OR the
response is an error, the response is returned untouched. Only on an opted-in
successful response is cost appended, so the default serialized DaemonResponse
is byte-identical to today (Maestro .ad recompare safe). Proven by the parity
test (flag-off identity, flag-on additive-only, error path, boundary survival).

Additive / semver-minor. MCP exposure and richer signals (roundTrips,
nodeCount) are deferred to follow-up slices.

* test: classify --cost flag as outside provider-backed integration

The integration progress guard (test:integration:progress:check) treats every
public CLI flag as either device-observable (requiring provider-backed coverage)
or intentionally excluded. --cost is a diagnostics/output flag (a purely additive
response field, not device-observable), so it joins json/help/version/verbose in
the 'config, output, diagnostics, and transport' exclusion bucket.
2026-06-29 13:50:12 +02:00
Michał Pierzchała 43399780a7 refactor: consolidate apple/mobile platform family-groupings via shared predicates — Phase 3 (#921)
Replace scattered inline `platform === 'ios' || === 'macos'` and `=== 'ios' || === 'android'` family-grouping branches with the shared `isApplePlatform`/`isMobilePlatform` leaf predicates. Strictly behaviorless: each site yields an identical boolean for every possible platform input. Adds `isMobilePlatform` to utils/device.ts (leaf predicate, no registry/core import). Part of the Phase 3 PlatformPlugin/ADR-0009 effort to reduce open-coded platform branches; defers the ios+macos→apple Platform collapse and load-bearing table relocations per ADR-0009.
2026-06-29 13:14:50 +02:00
Michał Pierzchała ef282d2b1a docs: add cloud agents video (#915) 2026-06-28 17:44:30 +02:00
Michał Pierzchała a2c47ad050 feat: typed command results, batch 1 (Phase 2 typed-results) (#913)
* feat: typed command results, batch 1 (boot, shutdown, viewport)

Wire the dormant CommandResultMap spine into the public client return types
for three commands whose daemon handlers return clean, closed result objects:

- boot      -> BootCommandResult     (src/daemon/handlers/session-state.ts)
- shutdown  -> ShutdownCommandResult (src/daemon/handlers/session-state.ts)
- viewport  -> ViewportCommandResult (src/core/dispatch.ts handleViewportCommand)

Each result type mirrors the handler's literal return exactly; the public
client methods narrow from the generic Record (CommandRequestResult) to these
precise shapes. No field removed; shapes that the runtime leaves open stay open.

* fix: export typed command results
2026-06-28 14:46:35 +02:00
Michał Pierzchała c49e4fc4a4 feat: PlatformDescriptor registry deriving the capability bucket — Phase 3 step 1 (#914)
* feat: derive capability bucket from a PlatformDescriptor registry

Introduce src/core/platform-descriptor/ (mirroring src/core/command-descriptor/):
a 5-row PlatformDescriptor registry carrying each leaf platform's capability
bucket and isApple flag, pure derive folds, and a byte-for-byte parity test.

Rewrite selectCapabilityForPlatform to fold the registry via the derive fn and
delete the hand switch. Behaviorless and parity-proven; layering-safe (all in
core, no utils->core inversion). Defers the ios/macos->apple collapse and the
interactor/discovery/runner-profile tables to later per ADR-0009.

* refactor: drop unused definePlatformDescriptor helper

The identity helper had no consumers (the registry uses `as const satisfies`),
so Fallow flagged it as a newly-added unused export. Remove it.
2026-06-28 14:10:42 +02:00
Michał Pierzchała 9422881cb4 feat: derive the batch command union from the as-const registry (simplify #909) — Phase 1 step 7 (#912)
The command-descriptor registry is now `as const` (#910), so each entry keeps
its literal `name` and literal `batchable`. Derive StructuredBatchCommandName
from it via `Extract<…, { batchable: true }>['name']` instead of the 43-member
hand-authored union from #909, and delete the now-tautological exhaustive Record
membership assertion in parity.test.ts (type and value now derive from the same
`batchable: true` entries).

Strictly behaviorless: the derived union is the identical 43-member set
(confirmed bidirectionally assignable to the old hand union, member count
unchanged), the runtime allowlist value is unchanged, and the public
BatchCommandName re-export is structurally identical (consumer switches on
specific batch command names still typecheck).
2026-06-27 21:23:51 +02:00
Michał Pierzchała c4d6c8a9ae feat: additive CommandResult type spine — Phase 1 step 6 (#911)
* feat: registry-driven exhaustive command dispatch — Phase 1 step 5

Part A (enabler): make commandDescriptors `as const` so each entry keeps its
literal `name`, and export `Command` — the literal command-name union.

Part B: replace the dispatchKnownCommand switch with a
`Record<DispatchCommand, DispatchHandler>` lookup table. The Record type forces
every dispatch command to have a handler (a missing entry is now a COMPILE
error, replacing the runtime `default: throw` as the coverage net); an
`Object.hasOwn` guard preserves the identical INVALID_ARGS error for unknown
commands. DispatchCommand is hand-authored to match the switch cases verbatim
(it is not the registry `generic` route, and `swipe-preset`/`read` aren't
registry names). Strictly behaviorless: same routing, same handler args, same
results.

* feat: additive CommandResult type spine — Phase 1 step 6
2026-06-27 21:02:17 +02:00
Michał Pierzchała 6a65e612e1 feat: registry-driven exhaustive command dispatch — Phase 1 step 5 (#910)
Part A (enabler): make commandDescriptors `as const` so each entry keeps its
literal `name`, and export `Command` — the literal command-name union.

Part B: replace the dispatchKnownCommand switch with a
`Record<DispatchCommand, DispatchHandler>` lookup table. The Record type forces
every dispatch command to have a handler (a missing entry is now a COMPILE
error, replacing the runtime `default: throw` as the coverage net); an
`Object.hasOwn` guard preserves the identical INVALID_ARGS error for unknown
commands. DispatchCommand is hand-authored to match the switch cases verbatim
(it is not the registry `generic` route, and `swipe-preset`/`read` aren't
registry names). Strictly behaviorless: same routing, same handler args, same
results.
2026-06-27 20:59:36 +02:00
Michał Pierzchała 607883d66c feat: build batch allowlist from descriptors, delete the hand list — Phase 1 step 4 (#909) 2026-06-27 20:38:58 +02:00
Michał Pierzchała 96bc7b190c feat: build capability matrix from descriptors, delete the hand table — Phase 1 step 3 (#908)
Build BASE_COMMAND_CAPABILITY_MATRIX from deriveCapabilityMatrix(commandDescriptors)
and delete the hand-authored literal (ADR-0008, Phase 1 step 3). Behaviorless: #906
proved the derived matrix is byte-equal to the literal, including the
supports/unsupportedHint closures across the sample-device matrix.

Delete the now-dead local predicate helpers and capability bundles that only the
literal referenced (isNotMacOs, isMacOsOrAppleSimulator, isIosMobileSimulator,
synthesisGestureUnsupportedHint, ALL_DEVICE_COMMAND_CAPABILITY, APP_* bundles,
LINUX_DEVICE, LINUX_NONE); their copies now live in registry.ts. WEB_* command
sets and addWebCommandCapabilities are kept.

The registry/derive/types modules only type-import CommandCapability from
capabilities.ts, so the new value-level dependency forms no runtime cycle (tsc
clean + runtime import() of capabilities.ts loads with no TDZ/circular-init error).

Rewrite the capability parity tests (now tautologies) into an admission invariant:
every matrix entry is selectable (platform bucket or supports predicate) and the
public-command coverage floor is unchanged. Daemon and batch-name parity tests kept.
2026-06-27 20:17:30 +02:00
Michał Pierzchała 47abc8c416 feat: build daemon command registry from descriptors, delete the hand table — Phase 1 step 2 (#907) 2026-06-27 19:46:54 +02:00
Michał Pierzchała 4648051b09 feat: command-descriptor registry (additive, parity-tested) — Phase 1 step 1 (#906)
Introduce a single dormant CommandDescriptor registry as the strangler-fig
spine for ADR-0008. Per command it carries the daemon route + request-policy
traits (incl. closure traits), an optional capability entry, and
batchable/mcpExposed flags — copied verbatim from today's hand tables.

Pure derive folds reconstruct DAEMON_COMMAND_DESCRIPTORS, the capability
matrix, and the structured-batch names; parity tests prove each derivation
byte-equal to the live hand table. Nothing reads the registry yet.

Purely additive: no consumers changed, nothing deleted, no import inversion.
The only edits outside the new directory expose three existing tables
(DAEMON_COMMAND_DESCRIPTORS, DaemonCommandDescriptor, BASE_COMMAND_CAPABILITY_MATRIX)
so the parity tests can read them.
2026-06-27 18:02:43 +02:00
Michał Pierzchała 29e19b8e3f docs: add ADR 0008 (command descriptor) + ADR 0009 (Apple consolidation) (#905)
Locks the two axis decisions and starts retiring plans/ into ADRs. ADR 0008
(Proposed) records the command-descriptor registry composing domain-owned facets
and deriving the ~10 tables, bound by ADR 0003's four invariants; ADR 0009
(Accepted, groundwork shipped in #896) records the AppleOS leaf axis under one
'apple' Platform. perfect-shape.md links both and marks Phase 0 + Tier-A dedup as
merged.
2026-06-27 17:09:30 +02:00
Michał Pierzchała 91d1b4c11f refactor: dedup daemon handler capability/session/recordAction boilerplate (#904)
Collapse three repeated daemon-handler patterns into shared helpers in
src/daemon/handlers/response.ts and handler-utils.ts:

- requireCommandSupported(command, device, { message?, hint? }) replaces ~21
  verbatim isCommandSupportedOnDevice UNSUPPORTED_OPERATION guards. The richest
  variant (generic dispatch) folds the unsupportedHintForDevice hint behind the
  `hint` option; custom-message sites pass `message`.
- noActiveSessionError() / NO_ACTIVE_SESSION_MESSAGE replace the duplicated
  'No active session. Run open first.' SESSION_NOT_FOUND literal across handlers
  (and the inline router/AppError variants).
- recordSessionAction gains optional positionals/flags overrides so ~15 inline
  sessionStore.recordAction({ command, positionals, flags, result }) literals
  (find, record-trace, record-trace-recording, session, session-close,
  session-perf-xctrace, install-source) route through the one wrapper.

behaviorless: every site keeps its exact error code/message (and hint where
present) and recorded action shape; helpers reproduce each variant.
2026-06-27 15:29:32 +02:00
Michał Pierzchała d291e4c599 refactor: extract shared Android instrumentation-helper (#903)
Both the snapshot helper and the multi-touch helper drive 'am instrument -w'
and parse the same INSTRUMENTATION_STATUS/INSTRUMENTATION_RESULT key/value
records, and both validate a bundled JSON manifest with the same integer and
literal field rules.

Extract the genuinely-identical parts into a shared
src/platforms/android/instrumentation-helper.ts:
- parseInstrumentationRecords (status + result record parsing) and the
  shared key/value reader
- readInstrumentationResultNumber / readInstrumentationResultBoolean
- readAndroidHelperManifestInteger / readAndroidHelperManifestLiteral
  (label-parameterized so each helper's exact error message is preserved)

multitouch-helper now consumes the shared record parser (was its own
~25-line copy) and the shared numeric reader; snapshot-helper-capture
re-exports the shared metadata readers under their existing public names so
snapshot-helper-session keeps importing them unchanged.

Behaviorless: the divergent, behavior-bearing parts are intentionally left
in each consumer (install caches Set vs Map + install-policy/forget/retry,
installed-versionCode regex + timeout, sha256File stream-vs-read,
readString trim semantics and the sha256 validator that depends on it,
artifact resolution + per-helper manifest schemas).
2026-06-27 14:57:18 +02:00
Michał Pierzchała fbec5cf621 refactor: collapse duplicated batch-step validation to one schema (#902) 2026-06-27 14:56:35 +02:00
Michał Pierzchała 9b79e21091 refactor: remove shallow re-export modules and a dead branch (#901) 2026-06-27 14:56:17 +02:00
Michał Pierzchała fc81c9edc5 refactor: derive replay metadata platforms from the canonical selectors (#899) 2026-06-27 14:02:00 +02:00
Michał Pierzchała cd00ff8699 refactor: validate JSON-RPC at the MCP/HTTP boundary (#897)
* refactor: validate JSON-RPC at the MCP/HTTP boundary

Wire the previously dead jsonRpcRequestSchema into the MCP stdio decode path and
add a sibling commandRpcParamsSchema for the daemon HTTP command params, replacing
unchecked casts of attacker-controllable wire input with real boundary parsing.

- mcp/server.ts: each inbound payload (and each batch element) is parsed via
  jsonRpcRequestSchema instead of being force-cast to JsonRpcMessage. Valid
  requests (with id), notifications (no id), and batches are accepted and routed
  exactly as before; only genuinely malformed input (non-object, or wrong-typed
  jsonrpc/method/id) is rejected with the standard -32600 Invalid Request error.
  JSON.parse failures still surface as -32700, and malformed input never crashes.
- contracts.ts: add commandRpcParamsSchema next to jsonRpcRequestSchema validating
  the command RPC params; optionalStringArray helper added.
- daemon/http-server.ts: replace the 'params as unknown as Partial<DaemonRequest>'
  double-cast with commandRpcParamsSchema.parse(params).

* fix(daemon): reject malformed command params as 400/-32602

methodToDaemonRequest validated command params via commandRpcParamsSchema.parse, which throws a plain Error on malformed input. That mapped to UNKNOWN -> HTTP 500 / -32000 and leaked the internal '$.x' schema path on the wire. Reporting malformed client input as a server error is the wrong signal.

Wrap the parse so failures throw AppError('INVALID_ARGS', 'Invalid params: ...') with the schema path sigil stripped, and map normalized INVALID_ARGS to JSON-RPC -32602 in the boundary catch so malformed command params now surface as 400/-32602, matching the explicit sibling checks.
2026-06-27 14:01:03 +02:00
Michał Pierzchała 22a9b2fee2 feat: add AppleOS discriminant to the device model (additive) (#896)
* feat: add AppleOS discriminant to the device model (additive)

Add an explicit, stored AppleOS ('ios' | 'ipados' | 'tvos' | 'watchos' |
'visionos' | 'macos') and an optional appleOs field on DeviceInfo so Apple
operating systems are first-class instead of inferred late from DeviceTarget.

- device.ts: add AppleOS type, optional DeviceInfo.appleOs, and make
  resolveApplePlatformName prefer device.appleOs while falling back to the
  existing target-based inference for legacy records. iOS and iPadOS both map
  to the single iOS runner profile; tvOS -> tvOS; macOS -> macOS.
- Populate appleOs at discovery only (no widened filters): iPhone/iPod -> ios,
  iPad -> ipados, tvOS -> tvos, host Mac -> macos.
- resolveRunnerPlatformName threads device.appleOs through.

Non-breaking groundwork for the platforms/apple consolidation: records without
appleOs resolve byte-identically, and no runner SDK/destination selection
changes (iPad still resolves to the iOS runner profile).

* fix: keep appleOs internal and derive ipados from simctl device type
2026-06-27 14:00:32 +02:00
Michał Pierzchała d16f01cb5b refactor: derive platform allow-lists from the canonical device tuples (#895)
The set of platforms was hand-restated in three places that must agree:
the canonical tuples in src/utils/device.ts, the --platform flag's
enumValues in cli-flags.ts, and the leaf-platform validation in
client-normalizers.ts. The two non-canonical copies could drift.

- Export PLATFORMS from device.ts and add an isPlatform() leaf-platform
  type guard derived from it (excludes the `apple` selector).
- Derive the --platform enumValues and usageLabel from PLATFORM_SELECTORS.
- Derive normalizeOpenDevice's leaf-platform check from isPlatform();
  per-platform udid/serial identifier shaping is unchanged.

Behaviorless: the derived sets equal the previous hardcoded sets.
2026-06-27 13:48:55 +02:00
Michał Pierzchała 5a67623585 perf: reduce Apple runner build overhead (#898) 2026-06-27 13:48:38 +02:00
Michał Pierzchała 93d5275e69 refactor: type-safe recording backends + exhaustive capability gating (#894)
* docs: add perfect-shape architecture roadmap

Captures the target architecture (two-registry thesis: CommandDescriptor +
PlatformPlugin over a clean folder DAG with a typed-result spine) and a sequenced,
strangler-fig migration path, grounded in a survey of the current codebase.

This PR implements the first two behaviorless Phase-0 items from that roadmap; the
larger registry work is deliberately deferred to later, independently shippable PRs.

* refactor: parametrize RecordingBackend by recording tag

RecordingBackend is now generic over the recording's platform tag, so each
backend's stop() receives an already-narrowed recording. This deletes all five
'recording as Extract<ActiveRecording, { platform: ... }>' casts — the textbook
discriminated-union-narrowing-by-cast anti-pattern — and makes a backend/tag
mismatch unrepresentable.

start() stays wide (DaemonResponse | ActiveRecording) because a device platform
does not map 1:1 to a recording tag (an iOS device resolves to either the 'ios' or
'ios-device-runner' recording). Device resolution returns a stop-less view
(RecordingStartBackend); stop is dispatched per active recording via the new
exhaustive stopActiveRecording(), replacing resolveRecordingBackendForRecording().

Behaviorless: pure type-level change, no runtime behavior change.

* refactor: make capability platform selection exhaustive

isCommandSupportedOnDevice resolved the per-platform capability bucket with an
if/else ladder whose final branch funneled every unmatched platform into
capability.web. That silently absorbs a future Platform with no compile error.

Replace it with selectCapabilityForPlatform(), an exhaustive switch over the
Platform union with a 'never' guard, so adding a new platform is a compile error
here instead of a silent web mis-gate. Identical behavior for all five current
platforms (ios/macos -> apple, android, linux, web).

* docs(adr): amend ADR 0003 for the single-declaration/derivation model

Ratifies the PR review caveat into the ADR itself: the daemon command registry
boundary is about ownership + the predicate interface, not the physical file a trait
is typed in. A derived/projected daemon registry is permitted only if it preserves
four invariants (daemon-owned declaration, unchanged predicate interface, no leakage
into public projections, one declaration per concern enforced by types). The original
decision stands; collapsing daemon policy into a public command registry remains
forbidden.

* docs: refine command axis to facet composition (ADR 0003-aligned)

- §2/§5.2: CommandDescriptor composes domain-owned facets (surface@commands,
  capability@core, daemon@src/daemon) and projects them — compose-with, not
  collapse-into. Adds the four ADR-0003 invariants.
- §6: mark the two shipped Phase-0 items (generic RecordingBackend<P>, exhaustive
  capability selection); link the Apple plan from Phase 3.
- §5.1: Apple as the first PlatformPlugin instance, owning an AppleOS leaf axis.
- §8: before/after diagrams for the command axis + the two-axis summary.

* docs: add apple-platform-consolidation plan (AppleOS leaf axis)

One 'apple' Platform with an AppleOS discriminant (ios/ipados/tvos/watchos/
visionos/macos) rather than six Platform literals (which would collide with the
cross-platform 'target' axis). Captures the 4-investigator survey: ~85% of
platforms/ios is already the OS-agnostic Apple engine; the XCTest runner already
builds ios|macos|tvos; macOS is included as a distinct AppKit leaf (already
entangled). visionOS is scoped net-new work; watchOS is an unsupported sentinel
(XCUITest can't drive it). Before/after diagrams, per-OS readiness, sequencing.
2026-06-27 12:33:48 +02:00
Michał Pierzchała 78ca8fb9be 0.18.0 v0.18.0 2026-06-26 19:53:24 +02:00
Michał Pierzchała a822325375 feat: add integrated device leasing (#890)
* feat: add integrated device leasing

* fix: keep metro bearer token out of generated proxy profile

The proxy connect profile is written to disk as a non-secret remote config,
but it unconditionally copied `metroBearerToken` into that file, leaking the
secret at rest. Mirror the cloud path, which keeps `daemonAuthToken` in-memory
only: the token still flows through this connect via the returned flags, and
later commands re-supply it via AGENT_DEVICE_METRO_BEARER_TOKEN. Extend the
non-secret-profile test to assert the bearer token is absent from disk.

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

* fix: always release device lease on session close

releaseSessionLease + sessionStore.delete ran only on the happy path, after
several awaits (app-log/perf/snapshot teardown, platform close dispatch,
runner stop) that can throw. A failed close therefore stranded the device
lease until the inactivity expiry. Wrap teardown in try/finally so ownership
is always freed; the original error still propagates after finally.

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

* fix: reconcile integrated device leasing

* docs: simplify remote lease guidance

* refactor: satisfy leasing fallow checks

* fix: harden integrated device leasing

* refactor: deepen device lease lifecycle

* refactor: centralize lease scope projection

* fix: harden proxy lease e2e flow

* fix: address lease review feedback

* refactor: tighten lease release cleanup

* fix: simplify proxy startup output

* fix: harden cloud lease identity

* fix: color proxy startup output

* fix: simplify proxy tunnel placeholder

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 19:37:08 +02:00
Michał Pierzchała 8f92572ae4 feat: expose web screen recording (#891) 2026-06-26 19:36:50 +02:00
Michał Pierzchała a9069692de refactor: model recording backends (#893)
* refactor: model recording backends

* refactor: simplify recording backend seam
2026-06-26 18:30:35 +02:00
Michał Pierzchała 2d6fc98222 refactor: tighten daemon output helpers (#892)
* refactor: tighten daemon screenshot typing

* refactor: share screenshot result typing

* refactor: inline record shape checks

* refactor: inline record readers

* refactor: type batch and network output

* refactor: type batch run response

* refactor: narrow batch and observability output types

* refactor: validate typed batch runtime hints

* refactor: restore record boundary guard

* refactor: split daemon output parsers

* refactor: narrow screenshot overlay parser

* refactor: type screenshot overlay data

* refactor: type shutdown result payload
2026-06-26 16:32:52 +02:00
Michał Pierzchała 8c2b1adeb1 feat: support agent-cdp remote bridge sessions (#878) 2026-06-26 12:51:21 +02:00
Michał Pierzchała c16a8d5efb docs: clarify agent-device help entrypoint (#884)
* docs: clarify agent-device help entrypoint

* docs: trim duplicated help guidance
2026-06-26 12:11:29 +02:00
Michał Pierzchała 053bbace30 0.17.10 v0.17.10 2026-06-25 21:48:19 +02:00
Michał Pierzchała be1e1c9931 feat: add cdp command agent-cdp passthrough (#873)
* feat: add agent-cdp passthrough

* docs: narrow agent-cdp memory guidance

* chore: pin agent-cdp 1.6.0

* test: cover agent-cdp guidance

* fix: preserve agent-cdp passthrough flags

* fix: expose CDP wrapper as cdp

* docs: move CDP workflow to debugging guide

* docs: mention cdp in command reference
2026-06-25 21:44:47 +02:00
Michał Pierzchała 8c49d54ff1 fix: clarify proxy runner ownership (#882)
* fix: clarify proxy runner ownership

* fix: tighten proxy runner review follow-ups
2026-06-25 21:44:31 +02:00
Michał Pierzchała 98c0b1d3bf test: migrate test app to expo dev client (#881)
* test: migrate test app to expo dev client

* docs: align test app device targeting

* docs: clarify dev client setup tradeoffs

* docs: remove stale sdk reference
2026-06-25 21:43:14 +02:00
Michał Pierzchała 24cb2b622c fix: refine Apple provider pressure reporting (#877) 2026-06-25 17:34:50 +02:00
Michał Pierzchała 56d8e3902a test: migrate Android recording sizing coverage (#876) 2026-06-25 17:33:51 +02:00
Michał Pierzchała c142514ca6 docs: clarify agent-device QA mental model (#875) 2026-06-25 17:19:22 +02:00
Michał Pierzchała 19417f0a01 fix: avoid copying pnpm node_modules into worktrees (#872) 2026-06-25 12:12:29 +02:00
Michał Pierzchała fa1b0b7c8a docs: configure agent skill conventions (#871) 2026-06-25 11:02:37 +02:00