* 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>
* 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>
* feat: expose web network dump through agent-browser
* fix: reduce web network mapper complexity
* fix: gate web network headers by include mode
* test: assert compact web network summary
* refactor: simplify web network dump mapping
* refactor: trim web network coverage
* feat(recording): make iOS export quality configurable
Wire the existing recording-export-quality enum through the record command
down to the Swift export preset. Adds a `--export-quality <medium|high>`
option for iOS recordings that controls the AVAssetExportSession preset used
when a recording is re-encoded.
`medium` stays the default and selects AVAssetExportPresetMediumQuality, which
preserves the fast simulator-friendly export. `high` opts into
AVAssetExportPresetHighestQuality for evidence-grade output. This is separate
from the existing integer `--quality <5-10>` capture flag that scales render
resolution.
Closes#568
* fix(recording): apply export quality to touch-overlay export path
The --export-quality flag was only wired into the resize export path. The
touch-overlay re-encode (finalizeRecordingOverlay -> overlayRecordingTouches ->
recording-overlay.swift) ignored it and always picked AVAssetExportPresetMediumQuality,
so record stop with --export-quality high had no effect when the stop path
re-encodes only to burn in touch overlays.
Thread the recording's exportQuality through finalizeRecordingOverlay and
overlayRecordingTouches, pass it as --export-quality to recording-overlay.swift,
and resolve the preset there via the same exportPresetName() helper used by
recording-resize.swift. Medium stays the default when the arg is absent, so
behavior is unchanged for callers that do not set it.
* feat: align recording quality and size flags
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
* chore(daemon): takeover notice, dev state-dir pruning, session state-dir command surface
Implements the three follow-ups from #737:
1. Print a one-line stderr notice when the client replaces a running
daemon, stating identity and reason (version mismatch, code-signature
mismatch, or unreachable). Best effort; never fails the command.
2. Add 'pnpm clean:daemon --prune-dev' to remove worktree-scoped state
dirs under ~/.agent-device/dev/ that no live daemon owns (same
pid/start-time liveness check as server-lifecycle) and that have been
idle for 14+ days. Scoped dirs only; one line printed per removal.
3. Fold 'session state-dir' into the regular command surface: the
session contract resolves it locally via the new
client.sessions.stateDir(), the cli.ts pre-dispatch special case is
removed, and the MCP session tool now exposes the state-dir action.
Closes#737https://claude.ai/code/session_013WBrUjQ4WRxRkfVruALKX3
* docs: surface clean:daemon --prune-dev in AGENTS.md
Local agents discover daemon state-dir hygiene through AGENTS.md, not
the website docs, so document the prune flag next to the existing
worktree-scoped state-dir guidance.
https://claude.ai/code/session_013WBrUjQ4WRxRkfVruALKX3
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: document daemon trust model and auth hook threat model
Add the daemon trust model (loopback bind, per-boot token, version+code-
signature reuse, artifact safeguards) and AGENT_DEVICE_HTTP_AUTH_HOOK
guidance to the Security & Trust page.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* docs: make daemon token location state-dir aware
Aligns with the worktree-scoped daemon state directories introduced in #719.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* docs: scope the token requirement to authenticated endpoints
GET /health is intentionally unauthenticated (loopback-only liveness),
so the blanket 'every request' claim was inaccurate.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ios): unify multi-touch gestures on two-finger synthesis + hinted unsupported errors
Make RunnerSynthesizedGesture the single iOS multi-touch engine and drop the older
incompatible models:
- rotateGesture now drives the two-finger XCTest synthesis path (dx=dy=0, scale=1,
degrees), mirroring the pinch migration in #634. The native XCUIElement.rotate(withVelocity:)
injected a single synthetic rotation that React Native's rotation recognizer did not read
reliably; synthesis fixes it. velocity is ignored on iOS (kept in the wire contract for
compatibility; rotation direction comes from the sign of degrees).
- pinch is now synthesis on iOS and a clear UNSUPPORTED_OPERATION on tvOS/macOS. The macOS
coordinate double-tap+drag heuristic (performCoordinatePinch) is removed: synthesis is
iOS-only, so macOS multi-touch is reported honestly as unsupported rather than approximated.
- RunnerInteractionOutcome.unsupported now carries an actionable hint, mapped to ErrorPayload.hint
(#639). Every unsupported gesture/tvOS path returns a concise message plus a next-step hint
(existing messages kept verbatim).
Net: on iOS, pinch/rotate/transform all flow through one synthesis primitive. swipe/scroll/pan/
fling remain single-finger drags (correct, unchanged).
Coverage: examples/test-app/replays/gesture-lab.ad exercises pinch + rotate against the gesture
lab and asserts "pinch changed yes" / "rotate changed yes".
* fix(ios): fail-fast macOS pinch + align capability/docs with synthesis-only
Follow-through for removing the macOS coordinate pinch path (the runner now returns
UNSUPPORTED_OPERATION for macOS pinch): reject it at admission instead of round-tripping.
- capabilities.ts: pinch now matches rotate-gesture/transform-gesture (Android + iOS
simulator only); macOS dropped. Removes the now-unused isMacOsOrMobileAppleSimulator helper.
- capabilities.test.ts: pinch expected unsupported on macOS and tvOS.
- website/docs/docs/commands.md: pinch listed for Android + iOS simulators only (removed from
the macOS app-session list); documents that iOS rotate ignores the optional velocity arg
(synthesis uses a fixed duration; direction comes from the sign of degrees).
Addresses PR #645 review HIGH #2 and MEDIUM #3.
* fix(ios): surface a hinted unsupported error for synthesis gestures at admission
Removing macOS pinch from the capability matrix makes macOS pinch (and the already-excluded
rotate-gesture/transform-gesture on macOS/tvOS/physical iOS) fail fast in ensureGenericCommandReady
before reaching the runner. That left the runner's macOS-specific hint unreachable on the daemon
path, so callers only saw the generic "<cmd> is not supported on this device".
Add an optional unsupportedHint to the capability matrix and surface it at admission, so the
synthesis-only gestures fail fast (no runner round-trip) AND return an actionable hint pointing to
where they work (Android + iOS simulator). Applied to pinch / rotate-gesture / transform-gesture.
Addresses PR #645 review (P2: route macOS pinch to the hinted failure).