* perf(daemon): offload PNG decode/encode and screenshot pixel diff to a worker thread
PNG decode (inflateSync) and per-pixel screenshot diffing previously ran
synchronously on the daemon event loop, stalling all concurrent sessions
while multi-MB screenshots were processed.
- add src/utils/png-worker.ts worker_threads entry (rslib internal/png-worker)
handling one decode, encode, or diff-pixels job per message
- add src/utils/png-worker-client.ts async wrappers (decodePngAsync,
encodePngAsync, computeScreenshotDiffPixelsAsync) that lazily spawn the
worker, resolve it next to the current module in dev (.ts) and dist (.js)
like the companion tunnel entry, and fall back to the in-process
synchronous path when the worker is unavailable
- extract the unchanged pixel-compare loop into
src/utils/screenshot-diff-pixels.ts so both paths share identical logic
- route daemon call sites (screenshot-overlay annotate, compareScreenshots)
through the async wrappers; results stay byte-identical
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* refactor(daemon): apply review findings to PNG worker offload
- guard postMessage failures: a job-specific send error (e.g. DataCloneError)
now cleans up its pending entry and falls back to sync for that call only
- resolve the worker entry via a shared src/utils/internal-entry.ts helper
that returns null on any resolution failure (non-file import.meta.url)
- match repo precedent: spawn the worker with --experimental-strip-types
when the resolved entry is a .ts module
- report permanent worker degradation once (scoped diagnostic + process
warning with the failure reason) instead of silently going sync-only
- daemon lifecycle: pre-warm the worker at startup and terminate it during
shutdown with a 1s best-effort timeout (daemon-only)
- collapse the three async wrappers onto one kind-typed job runner with a
single unavailability channel (rejection, no null path) and drop the dead
mismatched-result guards
- derive the diff-pixels contract types from screenshot-diff-pixels.ts and
share toBuffer via the contract module
- serialize worker errors with normalizeError and reconstruct AppError
(code/message/details) client-side; the worker reuses decodePng so decode
failures are identical on both paths
- transfer result buffers back to the client when a view fully owns its
ArrayBuffer; clone pooled buffers to protect Node's shared buffer pool
- decode baseline/current screenshots concurrently in compareScreenshots
- move resizePngFileToMaxSize to src/utils/png-resize.ts and route its
decode/encode through the worker (daemon screenshot --max-size path)
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* fix(fallow): declare png worker entry and simplify resultTransferList
The Fallow audit flagged src/utils/png-worker.ts as unreachable from any
entry point and resultTransferList as over the complexity threshold.
- Declare src/utils/png-worker.ts in .fallowrc.json's entry list: it is a
worker_threads entry point loaded at runtime (mirrors src/daemon.ts /
src/companion-tunnel.ts and the internal/png-worker rslib entry).
- Flatten resultTransferList into filter/map over an extracted
ownsEntireArrayBuffer predicate, preserving the exact transfer rule
(byteOffset === 0, byteLength === owner.byteLength, real ArrayBuffer).
- Add direct unit coverage asserting fully-owned buffers are transferred
while pooled/offset views are not.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
---------
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(daemon): timing-safe token comparison and daemon.json permission hardening
Use crypto.timingSafeEqual (via SHA-256 digests, length-independent) for the
three daemon token checks, and chmod daemon.json to 0600 after writes since
writeFileSync only applies mode on creation.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* fix(deps): clear CVE-2026-9277 by overriding shell-quote to >=1.8.4 in test-app
Override added to examples/test-app/pnpm-workspace.yaml (package.json-level
overrides are silently ignored for this nested app, see the comment there).
Lockfile change is limited to shell-quote 1.8.3 -> 1.8.4; pnpm audit is clean.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(types): rename NetworkLogBackend -> LogBackend
The union is reused for both network-log and app-log backends, so the
network-specific name was misleading. Also renames the resolveNetworkLogBackend
helper to resolveLogBackend. No behavior change.
* refactor(types): add string-enum helpers (isStringMember/parseStringMember)
Small utils/string-enum.ts helpers for the now-common 'as const tuple' string
enums: a membership guard and a strict exact-match parser. Adopt at the strict
sites (parseSessionSurface, parseScrollDirection, parseSwipePreset, isPerfArea,
isPerfAction), replacing hand-rolled switch/includes boilerplate. Alias-accepting
parsers (device rotation) keep their custom logic. Error messages unchanged.
Chose focused helpers over a defineStringEnum factory: the modules are already
concise with tuple-as-source, and a factory would force export-name churn for no
gain.
* refactor(types): dedupe runner/interaction tail shapes
- RunnerXctestrunCacheKind / RunnerXctestrunArtifactState shared between
RunnerXctestrunArtifact and AppleRunnerPrepareResult.
- Annotate buildInteractionSurfaceSignature with its existing
InteractionSurfaceSignature alias.
- RunnerOpts / runnerOptionsFromContext use Pick<RunnerContext, ...>.
(Skipped daemon-invoke-fn: maestro already has its own MaestroRuntimeInvoke
alias, so forcing a shared DaemonInvoke creates naming inconsistency for ~2 lines.)
* refactor(types): dedupe remaining tail shapes
- ElementSelectorTarget (core/interactor-types.ts) shared by
DirectIosSelectorTarget and ElementSelectorTapOptions (= Omit<...,'raw'>).
- Collapse client-types ClientCommandBaseOptions into the identical exported
DeviceCommandBaseOptions; merge the two byte-identical snapshot-pick aliases.
- File-local SelectorRuntimeError and LineWriter for repeated inline shapes.
- Export mcp ToolResult and use it for the router's textToolResult return.
Pure refactor, type-only.
* address review: adopt helpers, fix alias direction, tidy tail dedupes
- Adopt parseStringMember/isStringMember at parseGestureDirection and the
swipe-pattern guard in dispatch-interactions.ts (same module the PR touched;
byte-identical error/behavior).
- Extract RunnerCallOptions = Pick<RunnerContext,...> in interactor-types.ts;
reuse in dispatch-interactions + ios/interactions (was written twice).
- Invert the ElementSelector alias direction: ElementSelectorTapOptions stays the
plain core type; DirectIosSelectorTarget = ElementSelectorTapOptions & { raw }
(keeps the daemon-only raw field in the daemon module).
- Drop redundant cleanStaleBundles in AppleRunnerLifecycleOptions (already in
AppleRunnerCommandOptions); drop the now-unnecessary cast in isStringMember.
- callTool returns Promise<ToolResult>; rename SelectorRuntimeError ->
DirectIosSelectorErrorResult (matches the DirectIosSelector* family).
- Dedupe the log-backend device mapping: export resolveLogBackend and reuse it
in resolveSessionLogBackendLabel.
Pure refactor; typecheck/lint green, affected tests pass.
* chore(fallow): fit config to repo profile so baselines stay near-empty
- Raise health thresholds in .fallowrc.json to the smallest values that
pass on a clean tree (maxCyclomatic 58, maxCognitive 77, maxCrap 591)
instead of grandfathering ~180 findings in fallow-baselines/health.json.
- Raise duplicates.minTokens to 66, the smallest value covering the four
tolerated clone groups (largest is 65 tokens).
- Regenerate baselines: health.json shrinks from ~18.6 KB of grandfathered
finding counts to refactoring-target metadata only; dead-code.json is
empty.
- Upgrade fallow 2.52.0 -> 2.91.0: 2.87.0 made ignorePatterns silence the
"examples/test-app is not declared as a workspace" warning, which 2.52.0
emitted regardless of config.
- Remove the unused ensureAdb export (and its now-unused imports) from
src/platforms/android/adb.ts; it is not re-exported by any public entry
and has no references anywhere in the repo.
- Document local (pnpm fallow) vs CI (fallow audit) usage in
CONTRIBUTING.md.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* chore(fallow): keep default thresholds, gate locally via diff-based audit
Revise the previous commit after review: pinning global thresholds at the
repo's historical maxima (cyclomatic 58, cognitive 77, CRAP 591, minTokens
66) weakened the gate for brand-new code and left zero headroom on the
worst existing functions. Restore the original design — fallow default
thresholds with legacy findings grandfathered per-file in
fallow-baselines/health.json — and fix the local-DX problem at the script
level instead:
- .fallowrc.json: drop the health/duplicates overrides so fallow defaults
(cyclomatic 20, cognitive 15, CRAP 30, minTokens 50) apply to new code.
- fallow-baselines/health.json: regenerate at default thresholds under
fallow 2.91 (201 grandfathered findings across 108 files); dead-code
baseline stays empty.
- package.json: `pnpm fallow` now runs `fallow audit --base origin/main`,
the same diff-based gate CI uses, so it passes on a clean tree. The old
full-tree summary moves to `pnpm fallow:all` (expected to report legacy
findings). `check:fallow` is unchanged (CI passes an explicit --base).
- CONTRIBUTING.md: correct the fallow docs accordingly.
Verified: clean tree passes; a new unused export fails the audit; a new
cyclomatic-25 function fails the audit; +1 branch growth in an already-
grandfathered function (classifyBootFailure) is absorbed by the baseline.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
---------
Co-authored-by: Claude <noreply@anthropic.com>
* test(maestro): cover runtime target resolution and point parsing behaviors
Pin previously untested behaviors of the Maestro compat target-resolution
layer: selector platform mapping, visible-text query extraction, fuzzy text
exact/partial ordering, index selection and out-of-range errors, childOf
failure messages, ancestor rect inheritance, zero-size rect visibility
errors, regex selector handling (including invalid regexes), malformed
selector INVALID_ARGS errors, visible match counts, and absolute/percentage
point coordinate parsing with malformed-input rejections.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
* test(maestro): tighten assertions per review
- Reduce the malformed-selector test to a single propagation case using a
tolerant /Unknown selector key/i regex; exact parser wording stays owned
by src/daemon/__tests__/selectors.test.ts.
- Make the blank fuzzy-query whitespace explicit via a hoisted
' '.repeat(3) query interpolated into the expected message.
- Add readMaestroSelectorPlatform boundary cases pinning the
case-sensitive === 'android' contract ('Android' and 'tvos' -> 'ios').
- Introduce a resolveIosNode helper to collapse the repeated
resolveMaestroNodeFromSnapshot(..., 'ios', IOS_TAB_FRAME) call sites in
the new tests.
- Add '50,75%' and '100.5,200' to the parseMaestroPoint rejection loop
(percent on one coordinate only; integer-only absolute regex).
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(types): documentary NormalizedRect/NormalizedPoint aliases
Add NormalizedRect/NormalizedPoint aliases of Rect/Point (utils/snapshot.ts)
to convey coordinate space at the two normalized-percentage sites that were
typed as absolute Rect: ScreenshotDiffRegion.normalizedRect and the OCR
block normalizedRect (+ the OCR center-distance path). Documentary only — the
aliases are structurally Rect/Point, so this is intent-conveying, not enforced.
Full nominal branding was scoped out as disproportionate: the normalized
surface is 2 fields with no current cross-space mixing, and rectCenter/
squaredDistance are intentionally space-agnostic.
* address review: relocate NormalizedRect/Point to screenshot-geometry + fix doc
- Move the documentary aliases out of the general snapshot.ts into
screenshot-geometry.ts (the screenshot-diff geometry module), where their
only consumers live (better collocation).
- Correct the doc: coords are normalized to the screenshot IMAGE's dimensions,
not 'viewport'.
Add a Lint & Format job (oxlint --deny-warnings + new format:check script) and
a warn-only guard that flags imports of src/commands/* from src/daemon and
src/platforms; the guard flips to a hard failure once shared contracts move
out of the commands layer.
https://claude.ai/code/session_01LXZXzxi55sZ11DSyqWyBA2
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(types): reuse canonical string-unions across the codebase
Replace inline duplicates of SessionSurface, ClickButton, DeviceRotation,
BackMode, ScrollDirection, Platform/PlatformSelector/DeviceTarget,
AlertAction, and LeaseBackend with imports of the existing canonical types.
Extract BackMode into a core/back-mode.ts leaf (matching click-button.ts /
session-surface.ts) to keep backend.ts dependency-clean. Add 'satisfies
readonly T[]' drift-guards to the runtime enum tuples consumed by enumField.
Pure refactor: single source of truth for these unions, no behavior change.
* refactor(types): consolidate gesture + daemon/network/metro mode unions
- Add SwipePattern to core/scroll-gesture.ts; reuse SwipePreset,
ScrollInputDirection, ScrollDirection at remaining inline sites.
- Move DaemonServerMode / DaemonTransportPreference / SessionIsolationMode /
NetworkIncludeMode to contracts.ts (the client<->daemon boundary); daemon/
config.ts and daemon/network-log.ts re-export them. Drop client-types.ts's
three private alias copies.
- Reuse MetroPrepareKind in metro.ts and remote-config-schema.ts.
- Add satisfies-guards to the runtime enum tuples.
Pure refactor, type-only changes.
* refactor(types): single-source remaining string-literal unions
- ElementSelectorKey (core/interactor-types.ts) for the 'id|label|text|value'
selector-key subset across core/daemon/ios/maestro.
- GESTURE_KINDS + GestureKind (command-catalog.ts), dropping the duplicate
GESTURE_KIND_VALUES.
- AndroidTextInputAction (android/adb-executor.ts); reuse NetworkLogBackend for
the app-log backend union.
- Reuse contracts.ts JsonRpcId / JsonRpcRequestEnvelope in mcp router + server.
- New commands/log-command-contract.ts (LOG_ACTION_VALUES + LogAction),
mirroring perf-command-contract.ts.
- Named Android snapshot helper metadata unions (transport/captureMode/install
reason) shared between helper + backend metadata.
Pure refactor, type-only changes; full unit suite green.
* refactor(types): consolidate geometry shapes
- Reuse canonical Rect (utils/snapshot.ts) for inline {x,y,width,height}
literals (parsing, output, screenshot-diff regions/ocr, atspi-bridge).
- GestureReferenceFrame (core/scroll-gesture.ts) as the single reference-frame
type; TouchReferenceFrame becomes an alias; replace inline
{referenceWidth,referenceHeight} across daemon + maestro + commands.
- New ImageDimensions (screenshot-geometry.ts) and MovementRange
(screenshot-diff-ocr.ts) for repeated {width,height} / {min,max} shapes.
- File-local AndroidRecordingSize in record-trace-android.ts.
Pure refactor, type-only changes. Point literals with normalized-vs-absolute
semantics deliberately left untouched.
* refactor(types): consolidate result/options/resolver shapes (+oxfmt)
- Generic PlatformProviderResolver<T> collapses 6 near-identical resolver
types in request-platform-providers.ts.
- Reuse canonical DaemonError (contracts.ts) / NormalizedError (utils/errors.ts)
for the inline error DTOs in daemon/types.ts and utils/output.ts.
- Single DaemonFailureResponse: FailedDaemonResponse becomes an alias and the
maestro-local redefinition is dropped.
- Extract one shared toBackendResult() into commands/runtime-types.ts, deleting
5 duplicate copies.
- Normalize whitespace with oxfmt across the touched files.
Full unit suite green.
* refactor(types): share TransformGestureParams + RepeatedInput bundles
- TransformGestureParams (core/scroll-gesture.ts) replaces 4 identical inline
transform-gesture param shapes (core interactor, dispatch, android multitouch,
client options).
- Reuse RepeatedInput (commands/command-input.ts) for the tap-modifier bundle in
PressCommandOptions and BackendTapOptions.
Pure refactor, type-only.
* refactor(types): extract BackendResultEnvelope mix-in
Replace the repeated inline { backendResult?: Record<string, unknown>;
message?: string } pair on ~17 single-object command result types with
'& BackendResultEnvelope' (commands/runtime-types.ts). Discriminated-union
variants and single-field result types are intentionally left inline.
Pure refactor, type-only changes; failing files re-verified green in isolation
(full-suite failures were flaky timeouts).
* refactor(types): dedupe runner/replay/selector/exec result shapes
- RunnerSessionOptions = AppleRunnerLifecycleOptions (field-identical).
- Reuse ReplayActionBlockInvoker for the maestro + daemon replay invoker types.
- SelectorSnapshotOptions aliases the canonical SelectorSnapshotInput; annotate
selectorSnapshotOptionsFromFlags.
- Reuse ExecResult for the { stdout; stderr; exitCode } subset in
record-trace-errors and app-log-process.
Pure refactor, type-only.
* refactor(types): make canonical tuples the single source for enum unions
Each string-enum module now exports an 'as const' literal tuple as the single
source and derives its union via (typeof TUPLE)[number]: SESSION_SURFACES,
CLICK_BUTTONS, DEVICE_ROTATIONS, BACK_MODES, SCROLL_DIRECTIONS/SWIPE_PRESETS/
SWIPE_PATTERNS, SCROLL_INPUT_DIRECTIONS, ALERT_ACTIONS, DEVICE_TARGETS. The
command metadata/input files import these tuples instead of redefining local
*_VALUES copies, which also retires 10 now-redundant 'satisfies' drift-guards.
Member order preserved everywhere (enumField error messages unchanged). Platform
tuples intentionally left as-is (command-input vs contracts use different orders).
Net -39 lines; tuple+type dual maintenance eliminated for these unions.
* feat: record replay test videos
* fix: align replay test video timing
* fix: leave replay test video tail visible
* refactor: clarify replay test video lifecycle
* refactor: pass replay video attempt context directly
* fix: warn on replay video finalization failure