All notable changes to this project will be documented in this file.
OpenSafari 0.7.2 is a release-process patch for the 0.7.x stability line. It keeps the 0.7.1 runtime fixes intact while making the GitHub release validation workflow match the maintainer's manual npm publishing process.
- Tag pushes validate without publishing — the
PublishGitHub Actions workflow now runs the release gate (npm ci, lint, CI tests, build, production dependency audit, and dist verification) onv*tags but no longer runsnpm whoamiornpm publish. - Manual npm publish preserved — the workflow reports whether the package version is already present on npm and leaves publishing to the maintainer's local terminal after checks are green.
- Clearer failure notification — release validation failures now describe the failed validation gate instead of implying an automated npm publish failure.
- No runtime code changes beyond the 0.7.1 stability, WebKit resilience, proxy recovery, long-session hygiene, tool-tier, and AX diagnostic fixes.
- Local validation for the 0.7.1 release line passed before this patch:
npm test -- --runInBand(216 suites / 2951 tests),npm run build, andnpm run audit:prod. - The 0.7.2 tag validation workflow is expected to complete without invoking npm publish.
OpenSafari 0.7.1 is a stability and long-session performance release. It reduces the default MCP tool catalog to a compact Tier 1 surface, hardens WebKit and proxy recovery so transient simulator/WebKit failures no longer cascade into page reloads or permanent Safari-tool outages, keeps the MCP server alive after isolated unhandled promise rejections, and bounds observability buffers and watchdog timers for long-running sessions. It also adds opt-in AX walker topology diagnostics for recoverable accessibility-tree failures.
- Default Tier 1 tool surface (#849) —
MCPServernow starts at Tier 1 by default instead of Tier 2, shrinking the defaulttools/listresponse from the broad advanced/native/Flutter catalog to the compact core automation surface. Users who need the previous broad surface can setOPENSAFARI_TOOL_TIER=2or start with--all-tools. - Safe tier fallback (#849) — tools missing an explicit
TOOL_TIERSentry now fall back to Tier 3 rather than Tier 2, preventing newly-added tools from silently expanding the default schema payload.tests/unit/tool-tier-drift.test.tsenforces explicit tier assignment for every registered tool. - Documentation alignment (#849) — README tier tables and programmatic
createServer()examples now document the Tier 1 default andallToolsopt-in path.
- Heartbeat hysteresis (#851) — WebKit heartbeat probes now tolerate consecutive transient failures before reconnecting. A single busy page, navigation gap, or temporarily absent target no longer immediately tears down the WebSocket.
- No implicit reload on reconnect (#851) — reconnect no longer re-navigates to
lastUrlby default, avoiding surprise page-state loss after a transient WebKit interruption.renavigateOnReconnectremains available as an explicit opt-in. - Catch-safe disconnect handling (#851) — fire-and-forget disconnect paths now catch and log teardown failures, preventing reconnect bookkeeping from becoming stuck or leaking process-level rejections.
- Supervised
ios_webkit_debug_proxyrestarts (#852) — unexpected proxy exits now trigger bounded exponential-backoff restarts. Intentional stop and internal teardown paths suppress the supervisor, so manual shutdown remains deterministic while crash recovery becomes automatic.
- Unhandled rejection policy (#850) — unhandled promise rejections are logged without calling
process.exit(1), so one leaked async rejection no longer terminates the MCP transport and all managed simulators.uncaughtExceptionremains fatal. - Regression coverage (#850, #851) — unit coverage now verifies non-Error rejection logging, catch-safe WebKit teardown, heartbeat failure thresholds, failure-counter reset, and explicit reconnect re-navigation behavior.
- Bounded HAR capture (#853) —
HarCollectornow caps stored entries (maxEntries, default 2000), reports dropped requests, and skipsNetwork.getResponseBodywhen encoded size already exceedsmaxBodySize. This prevents long captures on chatty pages from growing without bound or fetching oversized bodies just to discard them. - Bounded session log collectors (#853) — console/error/network collectors now use a bounded LRU session map while preserving the
stop -> getretrieval workflow. - Timer liveness hygiene (#853) — crash watcher, simulator pool monitors, and simulator memory monitor intervals are now
unref()'d so watchdogs do not keep an otherwise-finished process alive.
- Opt-in walker topology on AX failures (#843) — setting
OPENSAFARI_AX_DEBUG_ON_FAILURE=1re-runs faileddump/queryAX reads once with--debugand attaches parsedwalker_*topology toAccessibilityBridgeError. The success path is unchanged, and failed debug recapture never masks the original error.
- GitHub PR checks passed for #843 and #849–#853 (
lint,test,dependency-audit,build; #851 also passed the headless Safari, Flutter, native HID, and WebView smoke suites). - Local integrated validation before merging to
main: all six PRs merged cleanly in a scratch branch,npm test -- --runInBandpassed (216 suites / 2951 tests), andnpm run buildcompleted successfully.
OpenSafari 0.7.0 is a stateful-QA and observability release. It lands the foundations for stateful mobile semantic QA (durable AppSessionState / ScreenStateSnapshot contracts, a verified semantic navigation controller, a shared settle/postcondition policy, and an upgraded scenario runner), and makes previously-silent automation failures actionable by attaching searched-tree diagnostics to not-found errors, surfacing semantics-activation and raw-coordinate fallbacks as warnings, and disclosing Flutter build mode and VM capabilities at connect time. All emitted diagnostics are redacted and length-bounded.
- Stateful mobile semantic QA runtime foundations (#822, #823, #824, #827, #828, #830) — introduces durable QA state contracts and the runtime that drives them:
AppSessionStateandScreenStateSnapshotcontracts (src/tools/app-state-snapshot.ts) define a portable, serializable description of where a session is and what is on screen, so QA flows can reason about state instead of replaying blind tap sequences.- A semantic navigation controller (
src/tools/semantic-navigation.ts) performs verified screen transitions — each navigation asserts its destination postcondition rather than relaunching the app on every step, eliminating relaunch loops. - A shared settle / postcondition policy (
src/tools/settle-policy.ts) standardizes how high-level mobile actions wait for the UI to quiesce and how they prove their effect, applied consistently acrossapp_goto_screen,app_pop_until,app_dismiss_overlay,app_tap_element,app_type_element, andapp_wait_for. - The scenario runner is upgraded for mobile semantic flows with resume-from-current-state support (
src/orchestration/scenario-runner.ts,src/tools/scenario-tools.ts), so a scenario can pick up from the live device state instead of restarting from scratch.
- Flutter
flutter_connectbuild-mode & capability disclosure (#831, #836) —flutter_connectnow reports the app's build mode (debug / profile / release) and which VM-service capabilities are available, and performs a probe-backedflutter_evaluateso callers know up front whether expression evaluation and inspection are usable on the attached VM rather than discovering it on first failure. - Flutter semantics selector quality audit (#825) —
qa_flutter_semanticsgains an automation-readiness audit that flags low-quality / ambiguous semantics selectors before they cause flaky automation.
- Searched-tree diagnostics on not-found errors (#834, #837, #840) — when
app_tap_element(and related element-targeting tools) cannot find a target, and whenapp_wait_fortimes out, the structured error now attaches a redacted, length-bounded summary of the accessibility tree that was actually searched, so the caller can see why the match failed instead of only that it did. Diagnostics are distinct from, and complementary to, the heavierdebug_bundle_collect. - Semantics-activation failures surfaced as warnings (#833, #838) — when the native accessibility semantics activation degrades silently, affected tools now emit an actionable warning across the surface instead of proceeding as if nothing happened.
- Raw-coordinate fallback warning (#839) —
app_tap_elementnow warns when it falls back to raw screen coordinates for an unknown preset, making a silent precision degradation visible to the operator. - Diagnostics are redacted and bounded (#841, #833) — all newly-emitted diagnostic strings are passed through redaction and the searched-tree dump is size-capped, so attaching diagnostics never leaks sensitive content or blows up the response payload.
- Catalog-coded the two most common recoverable failure conditions so MCP clients receive a specific error code plus
recoverableflag andsuggestioninstead of the genericAPP_STATE_UNKNOWNfallback. "No device specified and no active device" (28 sites acrosssrc/tools/**and the canonicalresolveDeviceIdinsrc/native/accessibility.ts) now throwsStructuredErrorException.fromCode(ErrorCode.DEVICE_NOT_BOOTED, …), and "Not connected to Flutter VM Service. Run flutter_connect first." (12 sites) now throwsStructuredErrorException.fromCode(ErrorCode.FLUTTER_VM_NOT_CONNECTED, …). Each site preserves its original message string. The error envelope was already structured via the MCP server catch-all; this upgrades specificity (enabling client-side auto-recovery) for the highest-frequency cases. - Headless Smoke
Flutterjob now gates the Tier-0 thesis (#820) — the Flutter VM-Service live suite was entirelycontinue-on-error(advisory) after the #816 hang fix, so even a regression in headless Tier-0 routing or gesture dispatch would not fail CI. The step is now split: the ax-independent assertions (getInputBackend()selectsFlutterVMInputBackend, andswipedispatches gesture-arena events) run as a blocking step, while only thetap/typeTextassertions — which verify their effect through the ax-bridge (needs Simulator.app + TCC Accessibility, unavailable on GitHub-hosted runners) — remain advisory. The load-bearing headless claim is verified on every run; the ax-dependent readback stays advisory until it is reworked off the ax-bridge. Documented indocs/headless-architecture.md.
- Publish workflow honest-green (#817, #821) — the
Publishworkflow has reportedfailureon every release since v0.6.0 even though each version reached npm, becausenpm publish --provenancereturns a misleadingE404 … not in this registrywhen the version already exists or theNPM_TOKENlacks publish rights.publish.ymlnow (a) skips publishing with a success status when the exactname@versionis already on the registry (idempotent re-runs / re-pushed tags), (b) runs annpm whoamiauth preflight that fails with an actionable message instead of the masked E404, and (c) fans a failure out through the existing_sentinel-notifyreusable workflow so a broken release pipeline cannot sit red unnoticed. NOTE: a genuinely-new version still requires a valid publish-scopedNPM_TOKEN.
- Flutter native debugging setup in getting-started (#832, #835) —
docs/getting-started.mdnow covers how to bring up native Flutter debugging end-to-end, anddocs/flutter-vm-attach.mddocuments deterministic fast VM attach for debug/profile QA sessions. - Mobile semantic QA guide (#828) —
docs/mobile-semantic-qa.mddescribes the new stateful QA contracts, the semantic navigation controller, and the resume-from-current-state scenario flow. - Single-integration branch policy (#819) — documented in
CLAUDE.mdto prevent the stranded-PR / stacked-branch topology that stalled the 0.6.3 cut.
- Full suite green: 211 suites / 2917 tests pass locally in ~28 seconds on the merged release branch; lint clean (0 errors).
- The Headless Smoke
Flutterjob now runs the ax-independent Tier-0 routing + swipe-dispatch assertions as a blocking gate (see Changed), so a headless regression fails CI on every run.
OpenSafari 0.6.3 is a portability and reliability patch release. It lands the structured-error rollout across every agent-facing MCP tool, introduces the debug_bundle_collect MCP tool with automatic attachment on recoverable failures, and unblocks the scheduled Headless Smoke Tests run on main that has been red since 2026-05-22 because the sim-hid-bridge smoke probe was timing out before the wrapper's own probe-context flow could complete on GitHub-hosted runners.
- Headless Smoke Tests /
Simulator.app is NOT brought to the foreground by simhid taps— the test wrapper now allows thedist/sim-hid-bridgeinvocation up to 60 seconds. The previous 10 second budget consistently truncated the wrapper mid-probeContext(which itself spawnsdist/ax-bridgewith a 15 second timeout after a 1.2 second settle), surfacing asCommand failed: ... tap 200 500with no useful stderr. The new budget covers the wrapper's own 15 secondexecNative+ 1.2 second settle + 15 second ax-bridge probe envelope while still failing fast on a genuinely hung bridge. This unblocks the daily 06:00 UTCHeadless Smoke Testsrun onmainwithout weakening any product assertion. - Headless Smoke Tests /
Flutter — headless VM-Service inputjob — the scheduled run onmainwas being cancelled at its 35-minute timeout every day because theflutter-vm-input.live.test.tssuite (a) failed itstap/typeTextassertions, which read app state back through the nativeax-bridge(it requires Simulator.app + TCC Accessibility, unavailable on GitHub-hosted runners), and (b) then left the live VM Service WebSocket + heartbeat open, so jest never exited ("Jest did not exit one second after the test run has completed") and the job burned its full budget. The step now mirrors the siblingwebview-smokejob withcontinue-on-error: truefor the ax-bridge limitation, runs jest with--forceExit, and the suite tears down its VM Service client inafterAll. This unblocks the daily 06:00 UTCHeadless Smoke Testsrun without weakening the headless Tier-0 routing / swipe-dispatch assertions, which still execute.
debug_bundle_collectMCP tool (#798 PR1) — single tool that gathers compact, local, redacted failure evidence for a device/session: device/session identity, backend health summary, screenshot path/metadata, AX summary, recent app and system logs when available, fresh crash report summaries, network/HAR/intercept status, Flutter route and widget state when a VM Service is connected, and explicit redaction notes. Collection is best-effort: individual evidence failures are surfaced inside the bundle rather than failing the whole call, unless the device or session cannot be resolved at all. Documented indocs/debug-bundle.md.- Automatic
debug_bundle_collectattachment on recoverable failures (#798 PR2) — high-level semantic tools (starting withapp_pop_until) now accept an optionalcollectDebugBundleOnFailureparameter. When set, recoverable failures automatically attach a compact bundle reference to the structured error response so MCP clients no longer have to discover and calldebug_bundle_collectmanually after every failed semantic action.
- Tier-0 tool error envelopes migrated to the structured catalog (#797 PR2) — every Tier-0 agent-facing tool handler (
app_tap,app_swipe,app_type_text,app_screenshot_native,app_launch,app_terminate,app_open_url,app_activate, the lifecycle helpers, and the simulator/device controls) now responds withrespondWithStructuredError(code, message, extra?)rather than ad-hocError: ...text or one-off{ error }payloads. MCP clients can readerror,message,recoverable, andsuggestionconsistently across this surface. - Tier-1 tool error envelopes migrated and guarded against regression (#797 PR3) — long-tail tools (
app_list_running,app_key_input, the alert/permission helpers, the QA session tools, the auth/biometric/OTP helpers, the Flutter inspection surface, and the network/HAR tools) now share the same structured envelope. A new lint guard (scripts/check-structured-errors.js/ Jest unit coverage) prevents tool handlers from re-introducing ad-hocError: ...envelopes in this surface. app_pop_untilacceptscollectDebugBundleOnFailureas the first integration of the auto-attach helper. Behavior is opt-in: when the parameter is omitted the response is byte-identical to 0.6.2.
- Added 4 new unit suites (
tier0-error-envelope.test.ts,debug-bundle-collect.test.ts,debug-bundle-attach.test.ts,redaction.test.ts) plus catalog-expansion coverage. Local: 202 suites / 2853 tests pass in ~26 seconds. - The Headless Smoke Tests
Native — SimulatorKit HID (Tier 1)job should turn green on the next scheduled run; behaviour of the asserted invariants (tryCreateSimulatorKitHIDBackendresolves to simhid, Simulator.app stays in background, no AppleScript fallback loaded) is unchanged — only the wrapper-invocation budget moved.
- Carries the full chain originally landed by PRs #805 (#797 PR1, already shipped in 0.6.2), #810 (#797 PR2), #811 (#797 PR3), #808 (#798 PR1), and #812 (#798 PR2), which were stranded outside
developby a stacked-PR topology and merged intodevelopas the single integration PR #814.
OpenSafari 0.6.2 is a reliability and semantic-navigation patch release. It carries the post-0.6.1 fixes from develop into the release line: the macOS 15 SimulatorKit HID sentinel no longer fails main because of a cold-start timeout, agent-facing MCP failures are consistently machine-readable, overlay dismissal now proves caller-requested postconditions, and app_pop_until becomes a portable semantic back-navigation primitive for Flutter VM, release-build, UIKit, and SwiftUI flows.
- Main CI / SimulatorKit HID Sentinel macos-15 timeout — fixes the failing main check where
tests/ci/sim-hid-sentinel.test.tstimed out after 45s on the first fake-UDID framework-load probe even though later probes succeeded. The sentinel now uses a 90s Jest/exec budget, prefers the nativedist/sim-hid-bridge-nativebinary over Swift interpreter cold compilation, and keeps hard failures tied to real private-API break evidence (exit 78plus structuredSIMULATORKIT_MISSING,CORESIMULATOR_MISSING,HID_CLIENT_FAILED, orHID_FUNCTIONS_MISSING). This preserves BC-break detection without blocking main on transient macos-15 startup latency. - Structured MCP server catch-all failures — thrown tool errors now route through the structured-error carrier instead of plain
Error: ...text when possible. Unknown thrown values fall back to a stable catalog-backed envelope so clients can inspecterror,message,recoverable, andsuggestionconsistently. - Canonical structured-error fields are authoritative —
respondWithStructuredError/StructuredErrorException.toMcpResponse()no longer allow tool-specificextradiagnostics to override the canonicalerror,message,recoverable, orsuggestionfields. This prevents accidental reintroduction of ad-hoc error shapes while still preserving extra context fields. - Overlay dismissal postcondition proof —
app_dismiss_overlaycan now verify a caller-suppliedwaitForGoneAX postcondition before returning success. Failed verification reports structuredOVERLAY_DISMISS_FAILEDwithmode,deviceId,waitForGone, and verification details instead of implying success from gesture dispatch alone. app_pop_untilroute verification false negatives — route postconditions now use the same_ModalScopeStatuselement-tree evidence strategy asflutter_get_routeinstead ofModalRoute.of(rootElement), avoiding false negatives when the Flutter root element is not itself inside the current route subtree.- Native
app_pop_untilroute-only verification guard — native fallback without a connected Flutter VM no longer accepts route-only postconditions, because route names cannot be verified in release/UIKit/SwiftUI contexts. Callers must provide AX evidence (identifier,label,text, orrole) or connect the Flutter VM. Mixed route+AX specs keep route verification when VM is connected and drop to AX-only verification when VM is unavailable.
- Expanded structured error catalog (#797 PR1) — adds stable
ErrorCodeentries for common agent-facing failures: input validation (INVALID_INPUT,MISSING_REQUIRED_PARAM,INVALID_URL), device/session/backend resolution (DEVICE_NOT_BOOTED,SESSION_NOT_FOUND,BACKEND_NOT_CONNECTED), Flutter VM failures (FLUTTER_VM_NOT_CONNECTED,FLUTTER_EVAL_FAILED), overlay/keyboard dismissal (OVERLAY_DISMISS_FAILED,KEYBOARD_DISMISS_FAILED), alert/permission helpers (ALERT_NO_EFFECT,PERMISSION_RESET_DENIED), andapp_pop_untiloutcomes (POP_UNTIL_EXHAUSTED,POP_UNTIL_NO_FALLBACK_AVAILABLE,MISSING_POSTCONDITION). respondWithStructuredError(code, message, extra?)helper — one-line MCP tool response helper for catalog-backed errors. It emits the shared JSON envelope and is exported fromsrc/errorsfor subsequent tool migrations.app_pop_untilpostcondition contract and attempt history (#801 PR1) — responses now includestrategy, boundedattempts[], andpostconditionevidence while preserving the pre-existingok,status,popped, andtargetfields. Optional postconditions support AX queries (identifier,label,text,role) and Flutter route names (route) with configurabletimeoutMs/intervalMs.app_pop_untilnative fallback ladder (#801 PR2) — when Flutter VM service is unavailable,app_pop_untilcan now dispatch semantic native back steps and verify the requested postcondition after each attempt. The ladder tries AX-identified back affordances first, then iOS interactive-pop edge swipe, then Escape for sheets/custom navigators. It records each attempt and stops as soon as verification succeeds.docs/app-pop-until.md— new agent-facing documentation covering inputs, response shape, error codes,app_pop_untilvs.app_tap_element, route vs. AX postconditions, and native fallback constraints.
app_pop_untilkeeps Flutter VM as the preferred strategy when connected and not explicitly bypassed withforceFallback; native fallback is only used for non-VM/release/native contexts or explicit fallback testing.- Native fallback success is postcondition-driven for
until: "first"anduntil: "route"; dispatch success alone is insufficient. Foruntil: "count", success can be based on the requested number of successful dispatches when no postcondition is supplied. - Error metadata now carries recovery guidance across newly cataloged failures so MCP clients can decide whether to retry, connect a backend, boot a simulator, adjust input, or ask the user.
- Added focused unit coverage for structured-error catalog expansion, canonical envelope clobber prevention, MCP server structured catch-all behavior, overlay postcondition verification,
app_pop_untilpostcondition parsing, Flutter route/AX verification, native fallback strategy selection, no-backend diagnostics, exhaustion behavior, count caps, and route-only native rejection. - Local verification for this release preparation: targeted Jest suites, full
npm test -- --runInBand, andnpm run buildpass on the release branch.
tests/ci/sim-hid-sentinel.test.ts: raises the slow bridge timeout from 45s to 90s and searchesdist/sim-hid-bridge-nativebefore Swift/interpreter fallbacks.src/mcp-server.ts,tests/unit/mcp-server.test.ts: preservesStructuredErrorExceptionmetadata through the MCP server error path and adds regression coverage.src/errors/codes.ts,src/errors/respond.ts,src/errors/index.ts,src/errors/structured-error.ts,tests/unit/error-catalog-expansion.test.ts: expands the catalog, exports the helper, and locks canonical error-envelope precedence.src/tools/app-dismiss-overlay.ts,tests/unit/app-dismiss-overlay.test.ts: addswaitForGoneverification and structured failure reporting for overlay dismissal.src/tools/app-pop-until.ts,tests/unit/app-pop-until-contract.test.ts,tests/unit/app-pop-until-native-fallback.test.ts,docs/app-pop-until.md: adds postcondition/attempt response shape, route/AX verification, native back fallback ladder, route-only native guard, docs, and unit coverage.
- No tool names are removed and existing
app_pop_untilVM use remains compatible. Consumers may observe additional additive fields (strategy,attempts,postcondition) and should ignore unknown fields if not needed. - Native
app_pop_untilforuntil: "first"/until: "route"requires a verifiable postcondition. In non-VM contexts, use AX signals rather than route-only assertions. - Clients that attach extra diagnostics to structured errors must use distinct field names; canonical
error,message,recoverable, andsuggestionare reserved and remain catalog-controlled.
OpenSafari 0.6.1 is a catch-up release that lands every change accumulated on develop since 0.5.0 onto main and npm. v0.6.0 was tagged on 2026-05-16 but never merged to main and never published to the npm registry (npm view opensafari-mcp versions confirms 0.5.0 was the latest published until this release). Rather than ship a separate 0.6.0 → 0.6.1 pair, 0.6.1 rolls the entire 0.6.0 body forward together with one targeted pasteboard fix uncovered after the 0.6.0 tag. This is a feature-and-fix release; there are no breaking changes since 0.5.0.
Headline fix (post-0.6.0):
app_type_elementpasteboard backend now works onAXSecureTextField(password) elements (#760, #761). The readback contract introduced for #639 PR C comparedendsWith/includesagainst the focused element's AX value, but iOS masks the value of any secure text field with bullet characters (••••…) regardless of plaintext content, so every successful password paste was rejected asPASTE_NOT_APPLIED. The same OS-mask divergence on the simhid path was escalating intoTEXT_INPUT_DROPPED/TEXT_INPUT_LAYOUT_MISMATCHwithisError: true. After this release the verifier detects secure fields by role (AXSecureTextField) or trait (AXSecureTextField,secure text field) and returns silently — the readback contract is inconclusive by design for this element class. The pasteboard backend also now honours the documentedverify: falseparameter symmetrically with the simhid path (it was previously a silent no-op for pasteboard).
Headline change (rolled forward from 0.6.0):
ax-bridgerecursive scored content-root search (#40, follow-up to #4). Replaces the single-pass immediate-child content-root heuristic with a recursive, deterministic, integer-scored search, closing the silent-empty-content bug that blocked the "Functional success" section of #4 on Xcode 26.4 / iOS 26.4. The raw bridge now refuses to return a chrome-only tree: when no subtree exposes any app-level accessibility semantics, it emits a typedDEVICE_CONTENT_ROOT_EMPTYerror with exit code 1 instead of silently falling back to the bareAXWindow. Full per-rubric scoring (iOSContentGroup +10 / fits expected rect +8 / app-semantics descendants +5 capped at +25 / toolbar or menu bar −10 / zero descendants −5) plus chrome denylist now live in bothsrc/native/ax-bridge.swiftand the TypeScript reference scorersrc/native/ax-bridge-content-root.ts; the two implementations are kept in lock-step by 6 fixture unit tests intests/unit/ax-bridge-content-root.test.ts.
app_type_elementAXSecureTextField paste verification (#760, #761) — see headline above.assertPasteAppliedaccepts an optional{ role, traits }descriptor and returns silently when the descriptor signals a secure text field;typeViaPasteboardforwards the inspected node's role/traits into the assert and addssecureField?: truetoPasteboardTypeResult. The tool layer echoessecureField: truein the success response so callers can distinguish "no readback because secure field" from "no readback because verify opted out".verifyTypedTexton the simhid path detects the same signal and returns{ verified: 'unknown', verify_method: 'ax-value-not-readable' }instead of escalating to a structured input-error code. Unit coverage: 10 new cases intests/unit/pasteboard-input.test.ts, 2 new cases intests/unit/app-type-element.test.ts(inside the Tier-3 readback describe), both passing alongside the existing 2632 unit tests.- PointerService Phase 1 swipe semantics clarified (#649).
src/tools/pointer-service-input-backend.tspreviously claimed that a swipe failure underOPENSAFARI_ENABLE_POINTERSERVICE=1would "surface via the tier chain when we bubble back up". That is not the runtime behaviour:getInputBackendcachesPointerServiceInputBackendas the selected backend, soswipe()hard-errors withHeadlessInputUnavailableErroron Xcode 26+ and does NOT re-enter the tier chain. The comment has been rewritten to match the shipped code and to direct callers to the two working escape hatches (leave the env flag unset, or use an element-targeted swipe). No runtime behaviour changed. - Boot / lifecycle / network reliability batch (#752, #753, #754, #755, #756, #757).
device_bootnow keeps boot diagnostics reliable when WebKit is late (#756, #757); zombie cleanup no longer spins indefinitely on stale locks (#754, #755); network interception is scoped to MCP sessions and preserves XHR restore semantics under session intercepts (#752, #753); the proxy lifecycle remains stable under parallel startup; Flutter VM resolution remains stable under parallel input. fix(dom-input): always dispatch input event (#726). Multiple iterations addressing review feedback — always fire key events with conditional value write, guardappendCharbefore keyboard dispatch.fix(simulator): trailing-bracket strip + bundleId regex + simctl rotate route (#708 series).launchctllabel normalization now strips all trailing bracket groups;bundleIdregex tightened and not-installed detection centralized;simctl rotaterouted throughdeps.simctl.exec; shutdown stays best-effort after nuclear erase.fix(webkit): direct host.emit in EventBridge transport forwarding;clearCookieseffective ondocument.cookiefallback; throwing protocol-event handler routed throughtransport:error; circular dep resolved, unimplementedtimeoutMsremoved, viewport query unified; RFC 6265 domain matching forgetCookiesfilter;enabledDomainsPerTargetcleanup on RPC failure.
TRANSITIONAL_STATE_TIMEOUTclassification +--max-settle-retriesflag fordist/sim-hid-bridge(#46). The wrapper now distinguishes "expected app is running but its UI is still loading" from "no AX data at all": when--expect-bundle <b>is supplied and the first settle window returnsFOREGROUND_CONTEXT_UNAVAILABLEwhile<b>is inrunningApps, the wrapper performs one bounded re-probe (anothersettleMswindow) and promotes toTRANSITIONAL_STATE_TIMEOUTif the tree is still empty. Capped by--max-settle-retries <0|1|2|3>(default1); set0to restore the pre-issue single-probe behaviour byte-for-byte. The surface classifier insrc/tools/raw-mobile-context.tsstays surface-scoped and never emits the new variant itself — promotion is a wrapper-layer concern.feat(tap): scale AX frame coords from macOS-pt to iOS-pt (#693 WU3, #720) plus dump-root size emission (#693 WU3-prep, #695) and structured ErrorJSON STDOUT from the ax-bridge wrapper (#693 WU1, #694). Closes the long-standing tap-coordinate offset on retina simulators by carrying the macOS-pt → iOS-pt scale factor through the wrapper and applying it at coordinate dispatch. Regression coverage intests/unit/coord-regression.test.ts(#722).feat(ax-bridge):--debugflag emits machine-readable stderr (#660) plus walker candidate diagnostics. Bare-flag coercion restricted to debug/verbose (#660). Gated ko-KR push-permission live suite added (#660, #692).localized-button-matchergains an extension seam for app-specific labels (#639 follow-up).
The 0.6.0 cycle landed three large internal decompositions to support the simulator-chrome and reliability work. All public surfaces (tool names, schemas, response shapes) are preserved.
- WebKit module split (#706 1/5–5/5): error classes (1/5), protocol transport (2/5), target session manager (3/5), browser command implementations (4/5), typed event adapters + finalized facade (5/5).
WebKitClientis now a thin facade over focused submodules; the public import surface is unchanged. Multiple post-merge fixes preserved behaviour contracts flagged in review. - Simulator module split (#708 1/4–4/4): errors + device catalog (1/4), lifecycle (2/4), app manager (3/4), UI controller + finalized facade (4/4). Hardens
simctlJSON parsing and the fuzzy device resolver. - Input layer split (#707 a/b): backends and resolver into focused modules (a); remaining backends consolidated into
src/input/(b). DOM-input script builders centralized for webkit + native input (#709). Migrated paths now enforceno-explicit-anyvia lint override (#710 b). - Protocol typing (#710 a/b): typed DTOs + fixture builders for the WebKit RDP boundary (a); typed RDP guards with
console.typefallback restored.
- CLI lazy-load (#700 a, #729; #700 b, #728): command implementations and MCP handler implementations are now lazy-loaded behind static schemas; cold-start measurably faster, especially in
auditandserveflows. - WebKit fast paths (#702 a/b, #725): new
evaluateValuehelper, screenshot fast path, batched navigation-state read, deduplicated domain enables. - Native-input batching (#705, #723): reduces process spawns via a batching capability on the simctl backend.
- Proxy readiness (#701, #727): split process readiness from target readiness so the proxy can serve target traffic the instant the target is reachable, without waiting for the proxy to settle on its own port.
- Simulator boot polling (#703, #724): bootstatus-aware polling with shared state cache; eliminates redundant
simctl bootstatusprobes when multiple tools observe boot in parallel. - Web Inspector socket discovery (#704, #719): cached with staged backoff; first-call cost paid once per simulator boot.
- HTTP MCP transport hardening (#714) plus follow-ups: tighter
/mcpauth + insecure-mode posture, hardened HTTP auth comparison, high-risk tool gating in HTTP mode (with blocked-tool hiding), expanded high-risk gate to JS+VM bypass surfaces,mock_geolocationgated as HTTP high-risk and non-finite numerics rejected. All HTTP-only — STDIO transport unaffected. - Auth profile persistence hardening (#716) with follow-up review feedback applied.
tests/unit/auth-manager-persistence.test.tsbounds the atomic temp filename to a fixed-length hash prefix. - Audit log retention hardening (#711, #717).
- CI: separate runtime and dev dependency audits (#712, #718) so dev-only vulnerabilities do not block runtime audit policy.
- Lint enforces
no-explicit-anyon migrated webkit-rdp paths (#710 b); migrated-path override ordered after the tests glob to avoid suppressing the rule in test code (#741 follow-up). - Audit policy for runtime vs dev dependencies separated.
- No tool-name or schema changes since 0.5.0. All
app_*,webkit_*, and bridge tools keep their parameter shapes and response envelopes. app_type_elementresponse shape gains an optionalsecureField: truefield on the pasteboard backend when the focused element is anAXSecureTextField. Existing callers that ignore unknown response fields are unaffected.- The
verify: falseparameter onapp_type_elementis now honoured on both backends (auto/simhidandpasteboard). Callers that were relying on the previous silent-no-op on pasteboard should review their flows — readback skip is now actually applied. - HTTP MCP transport tightens defaults; STDIO transport (the default) is unaffected. Review the security section above if you operate a forked HTTP deployment.
OpenSafari 0.6.0 is a simulator-chrome-regression release. It replaces the single-pass immediate-child content-root heuristic in ax-bridge with a recursive, deterministic, integer-scored search, closing the silent-empty-content bug that blocked the "Functional success" section of #4 on Xcode 26.4 / iOS 26.4. The raw bridge now refuses to return a chrome-only tree: when no subtree exposes any app-level accessibility semantics, it emits a typed DEVICE_CONTENT_ROOT_EMPTY error with exit code 1 instead of silently falling back to the bare AXWindow.
src/native/ax-bridge.swift:findDeviceContentInWindowreplaced withfindDeviceContentRecursively. Scoring is deterministic and integer-based so fixtures can be asserted exactly:AXGroup/AXScrollAreawithiOSContentGrouptrait → +10- frame fits expected device-content rect (±15pt per edge) → +8
- each app-semantics descendant (
AXTextField,AXStaticText,AXButtonwith non-chrome label,AXCell,AXImage,AXLink) → +5, capped at +25 AXToolbar/AXMenuBar→ −10- zero descendants → −5
- Chrome denylist rejects exact labels (Action, Home, Save Screen, Rotate, Volume Up/Down, Sleep/Wake, AXCloseButton, AXFullScreenButton, AXMinimizeButton) and the simulator window-title prefix (
"iPhone <model> – iOS <version>").AXMenuBarandAXWindoware rejected at depth > 0. - Typed error: when no candidate subtree contains any app-semantics role, the bridge returns
{"code":"DEVICE_CONTENT_ROOT_EMPTY"}with exit code 1 instead of falling back to the bareAXWindow. The wrapper atcli/ax-bridge.tsforwards error JSON untouched. - Reproduction closed (Xcode 26.4 / iOS 26.4):
node dist/ax-bridge query --device <udid> --role AXTextFieldon a booted simulator with no foreground app now fails fast withDEVICE_CONTENT_ROOT_EMPTY(exit 1) instead of returning{"total":0,"matches":[]}with exit code 0.
src/native/ax-bridge-content-root.ts: TypeScript port of the Swift rubric so the algorithm is unit-testable from Jest. The two implementations MUST stay in lock-step; any change to the rubric, chrome denylist, geometry formula, or fallback policy must land in both files together.tests/unit/ax-bridge-content-root.test.ts: 6 fixture trees covering (a) emptyiOSContentGroupbetween chrome children →DEVICE_CONTENT_ROOT_EMPTY, (b) populated Flutter tree withiOSContentGroup+ ≥ 3 app-semantics descendants, (c) SpringBoard-only, (d) nested content two levels below window, (e) two candidate groups where only one contains app-semantics (DOM-order-independent), (f) Settings-app shape withAXTableat top level (noiOSContentGrouptrait).
dist/sim-hid-bridgewrapper CLI documented indocs/headless-architecture.md, including--settle-ms, response-shape table, and classification table. Adds a cross-reference fromdocs/api-reference.mdso MCP consumers can jump to the raw-CLI contract when scripting without the MCP server. Closes #45.app_tap_elementcoordinate fallback now preserves the verified-interaction contract. Whenax-presscannot prove a post-action effect and the tool falls back to a coordinate backend, OpenSafari no longer returns a plain clean success by transport alone. The response now carriesverified: false/effect: "verification_unavailable"when proof is unavailable, or a typedTAP_NO_EFFECTerror when the post-tap AX tree stays unchanged. The stricter contract matchesapp_tapand closes the false-positive-success gap for bundle-scoped native taps.- Raw mobile-bridge context diagnostics and expect-bundle guards.
dist/ax-bridgenow exposescontext --device <udid> [--expect-bundle <bundle>] [--require-match true], returning machine-readable foreground classifications and expected-bundle matches for downstream QA.dist/sim-hid-bridgenow ships as a wrapper around the native bridge and enrichestap/swipeJSON with post-inputclassification,verified,frontmost, andexpectedBundleMatched, plus a matchingcontextcommand. Raw HID commands can now fail fast with--require-match trueinstead of looking like a clean success after the simulator drifts to SpringBoard or chrome.
OpenSafari 0.5.0 is a stability-commitments release. It closes out the Xcode 26 investigation epic with an authoritative stability table, makes cross-context automation first-class (WebView-in-Flutter and WebView-in-native live harnesses land as product-grade E2E tests), expands alert / PointerService / IAP coverage, and — because we would rather ship truthful docs than broken tools — reverts the simctl storekit bindings that could not be made to work against real Xcode. Input telemetry is now on by default for every MCP input tool. Private-API deployment scope, StoreKit posture, and fork-friendly sentinel routing are all documented so teams operating forks or inside regulated orgs get clean upgrade guidance from the release notes alone.
Breaking: app_storekit_configure, app_storekit_test_session, and app_storekit_receipt are removed (#588, #623). They depended on simctl storekit subcommands that do not exist / do not behave as documented against real Xcode. Teams that relied on these tools should either (a) pin to opensafari-mcp@0.4.9 and follow the new docs/recipes/flutter-iap-ko-kr.md recipe until an AX-based replacement ships, or (b) drive StoreKit via app_alert_handle + app_tap_element against the localized sheet directly (see docs/storekit-automation.md). No other public tool or env var has changed.
Default behavior change: _meta._telemetry is emitted on every MCP input tool response (#595). Opt out with OPENSAFARI_INPUT_TELEMETRY_META=0. Response envelopes grow ~100 bytes/call.
Headline additions (all headless, all live-tested where applicable):
- Production-grade WebView ↔ native and WebView-in-Flutter HTTPS bundle_match live suites (#592).
- PointerService live coverage + sentinel probe (#590 Phase 1) so the experimental
OPENSAFARI_ENABLE_POINTERSERVICEbackend has a stability track. - ko-KR
app_alert_handlelive suites — 2-button + semantic-key paths + sentinel label-match probe (#589). - Flutter + IAP ko-KR end-to-end recipe pinned to 0.4.9 (#597).
- Flutter simulator QA recipe corrections (
--debugbuild, split device profile path) (#596). - WebView fixture AX identifiers + bundle-ID alignment (#593).
app_webview_connectdocumented indocs/api-reference.md(#592).- Private-API deployment-scope guidance + license-interaction notes (#601, #610).
- Fork-friendly sentinel alerting via
SENTINEL_WEBHOOK_URL/SENTINEL_CHANNEL(#599). - iOS 26 investigation synthesis finalized with a stability-commitments table (#591, #557).
- Removed tools:
app_storekit_configure,app_storekit_test_session,app_storekit_receipt. Relatedsrc/tools/app-storekit-*.tsandsrc/native/simctl-storekit.tsdeleted;tests/unit/app-storekit.test.tsremoved; priordocs/storekit-automation.mdand api-reference entries dropped. - Why: the
simctl storekitsubcommands we relied on (config load, sandbox-transaction list/approve, sandbox receipt extraction) either do not exist in released Xcode or do not respond to flags in the way Apple's release notes imply. Rather than ship tools that silently no-op against a real simulator, 0.5.0 removes them until an AX-based replacement can be authored. - Forward path:
docs/recipes/flutter-iap-ko-kr.md(new in this release, #597) documents the full IAP flow usingapp_launch+app_deeplink+app_tap_element+app_alert_handleagainst the localized StoreKit sheet. The recipe is pinned toopensafari-mcp@0.4.9so teams who need the removed tools have a clean pin path. A new AX-based StoreKit QA pattern lives in the (re-added)docs/storekit-automation.mdfor 0.5.0+.
- New live integration suite
tests/integration/webview-flutter-https-bundleid.live.test.tsthat boots a Flutter app embedding a real WebView, loads an HTTPS origin, and assertsapp_webview_connect({ bundle_match })selects the right WebKit debuggee by bundle ID. Complements the native-context live test (webview-native-context.live.test.ts, also updated here) with a Flutter host, the harder case — theflutter_inappwebview-style bridge surfaces WebKit's page at a different debuggee index and the matching logic has to survive that. app_webview_connectdocumented indocs/api-reference.md(#618).- README Headless Capabilities row for WebView-in-Native now reads partial with a pointer to the exact constraints (
README.md, #631), instead of the previous unqualified ✅. - Fixture corrections (#593): webview_flutter_bridge fixture now ships AX identifiers on every actionable button (#633) and the fixture bundle ID lines up with the test expectation so there is a single source of truth (#625).
- Live suite
tests/integration/pointer-service.live.test.tscoversapp_tap/app_swipethrough the experimental PointerService backend (enabled viaOPENSAFARI_ENABLE_POINTERSERVICE=1) against a Flutter fixture with explicit AX-visible tap targets. Validates that the tap actually lands (post-tap screenshot diff) and not just that IOHIDEvent was ack'd — which is the exact trap Xcode 26's native routing fell into (#491). 405 new lines of assertions across 6 test cases. - CI sentinel probe (#621):
tests/ci/sim-hid-sentinel.test.tsnow probes PointerService symbols (SimPointerClient_create,SimPointerClient_postEvent) alongside the existing SimulatorKit probes..github/workflows/sim-hid-sentinel.ymlruns it daily. When a macOS / Xcode update drops one of the PointerService symbols, we find out in ≤24h.
- 2-button alert live suite (#622):
tests/integration/issue-589-alert-handle-2button.live.test.tsdrives a ko-KRUIAlertControllerwith two localized buttons and exercisesapp_alert_handleviaaction: "accept",action: "dismiss", andbuttonLabels: ["구입", "Buy"]— the label-match path that survives future iOS reorderings of alert sheets. 293 new lines. - Semantic-key alert live suite (#620):
tests/integration/issue-589-alert-handle-semantic.live.test.tscovers the ko-KR semantic-key path (e.g.,key: "allow"mapping to "허용") with 222 new lines of assertions. - Sentinel label-match probe (#619):
tests/sentinel/private-api-probe.test.tsgains 125 new lines of ko-KR label-match assertions that guard against locale-loader regressions in Xcode updates.
- New end-to-end recipe at
docs/recipes/flutter-iap-ko-kr.mdcovering the most common commercial Flutter scenario: boot the simulator in ko-KR, launch the fixture, deep-link into the purchase flow, drive StoreKit, accept the localized sheet, extract the sandbox receipt, and verify against a backend. - Linked from
docs/ci-recipes.mdunder a new "Specialized Recipes" section. Pinned toopensafari-mcp@0.4.9so consumers can copy the manifest verbatim and know exactly which APIs it relies on — the StoreKit-configure / receipt / test-session tools were reverted in #623 (unimplementable against Xcode simctl as of 0.5.0), so teams who still need in-simulator IAP coverage can pin to 0.4.9 while an AX-based replacement is authored (seedocs/storekit-automation.md). - Paste-ready GitHub Actions + generic shell variants. The shell variant is self-contained and runs from any macOS agent (Buildkite, GitLab self-hosted, a developer's laptop). Both use only tools shipped in
opensafari-mcp@0.4.9—app_launch,app_deeplink,app_storekit_configure,app_tap_element,app_alert_handle(withbuttonLabels: ["구입", "Buy"]from #589),app_storekit_test_session, andapp_storekit_receipt(from #588). - ko-KR gotchas documented. Set
AppleLocale=ko_KRbefore booting (not after — mounted SpringBoard strings do not re-render), match StoreKit sheets by localized label list rather than index, disable Ask to Buy explicitly, use--profileFlutter builds to keep the VM Service online, and poll for the sandbox receipt with a short backoff since iOS occasionally flushes it lazily.
- Re-added
docs/storekit-automation.mdwith a minimal AX-only QA pattern: drive the localized StoreKit sheet viaapp_tree→app_tap_element(by identifier) →app_alert_handle(by label). Replaces the reverted simctl-based tools (#623) with a pattern that works on Xcode 26+ without any private API. docs/api-reference.mdcross-reference points at the new pattern.
docs/ci-recipes.mdanddocs/flutter-inspector.mdnow instructflutter build ios --debug(not--release) for simulator-hosted QA runs — the release AOT path pushes the Dart VM into thevm-service-unavailablestate the FlutterVM Tier-0 backend cannot route against, and the previous recipe silently demoted QA sessions to Tier-1 without the operator noticing. Device profile recipe split into its own section with the correct--profileflag.- Minor
src/tools/flutter-vm-input-backend.tscomment update aligns the inline remediation text with the corrected recipe paths.
- Private API Sentinel alerts are now parameterized.
.github/workflows/private-api-sentinel.ymlno longer hard-codes the#opensafari-sentinelsSlack channel. Forks and downstream orgs configuresecrets.SENTINEL_WEBHOOK_URL(Slack / Discord / Mattermost Incoming Webhook) plus optionalvars.SENTINEL_CHANNELto redirect alerts without patching the workflow. - Reusable notify workflow.
.github/workflows/_sentinel-notify.ymlswitches payload shape on webhook host —hooks.slack.comreceives Slack's{channel, text}schema, everything else gets Discord's{content}schema (Mattermost and Rocket.Chat compatible). - Missing secret is a no-op. When
SENTINEL_WEBHOOK_URLis unset the notify job logs the reason and exits 0; the sentinel matrix status is unaffected and the GitHub tracking issue still gets opened, so a missing webhook never silently masks a regression. Seedocs/ci-integration.md→ "Private API Sentinel alerting" for setup and the fork checklist.
docs/simhid-ios26-investigation.mdis now the canonical decision-blocking artifact for Xcode 26 tap regression. Falsification table reformatted with per-row hypothesis / probe / outcome / evidence columns; remaining candidates ranked by effort (S / M / L) × expected yield with an explicit dependency graph.- Stability commitments section added. Authoritative table distinguishing stable surfaces (Safari, Flutter, element-targeted native), opt-in experimental (coordinate tap/swipe via
OPENSAFARI_ENABLE_POINTERSERVICE), opt-in last-resort (AppleScript/CGEvent), and evolving (WebView cross-context). Promotion criterion published for the PointerService backend (≥ 99% success over 2 weeks of nightly sentinel runs on Xcode 26.0 / 26.1 with zero AppleScript fallbacks). README.mdHeadless Capabilities matrix split the Xcode 26+ native row into element-targeted (✅ stable via AX press) vs coordinate (⚠ experimental opt-in, tracked in #590).docs/private-apis.mdcross-reference updated — drops the "in review" framing and points at the new stability-commitments anchor.
docs/private-apis.mdgains a "Deployment scope" section drawing the host-vs-device line explicitly: ✅ allowed on developer Macs / macOS CI runners / internal dev tooling; ❌ not allowed bundled inside an iOS.ipa(App Store / TestFlight / Ad Hoc / Enterprise). Rationale cites App Review Guideline 2.5.1 / 2.5.2,/Library/Developer/PrivateFrameworks/provenance, and the same host-side posture Facebook'sidbtakes.- License-interaction note clarifies that the MIT grant on OpenSafari's source does not sublicense Apple's
SimulatorKit/CoreSimulatorframeworks — consumers remain bound by the Xcode / macOS license agreements for the loaded frameworks themselves. - One-time private-API warning updated.
SimulatorKitHIDInputBackendnow printsWhere can I use this? macOS host / CI only — never bundle inside an iOS .ipa …alongside the existingdocs/private-apis.mdpointer, with a(see "Deployment scope")anchor hint. Content asserted bytests/unit/sim-hid-input-backend.test.ts(3 new assertions taggedIssue #601). - No behavior change — informational only. No runtime code paths altered.
- Input tool responses now carry
_meta._telemetrywithout opt-in. All 10 MCP input tools (app_tap,app_swipe,app_scroll_native,app_key_input,app_double_tap,app_type_text,app_tap_element,app_type_element,app_dismiss_keyboard,app_alert_handle) emit the compact per-call projection (operation,elapsed_ms,ok,error?) by default. CI and benchmarking harnesses no longer need to discover and flipOPENSAFARI_INPUT_TELEMETRY_META=1after hitting a mystery slowdown — observability is now Pareto-default. - Opt-out preserved for consumers who care about payload size: set
OPENSAFARI_INPUT_TELEMETRY_META=0(orfalse) to suppress_meta._telemetry. Any other value — including unset — enables it. - Response-size overhead is ~100 bytes per call (one telemetry record). See
docs/ci-recipes.mdfor a paste-able CI recipe.
- Process-wide memory instrumentation.
timedInputnow records{rss_mb, heap_used_mb}on every input-backend call. The telemetry rollup exposes per-backendp50_rss_mb/p95_rss_mb/max_rss_mbpercentiles alongside the existing latency percentiles. Memory fields in tool responses are opt-in viaOPENSAFARI_TELEMETRY_INCLUDE_MEMORY=1. - Per-cache memory budget documentation.
docs/memory-budget.mdcatalogues every module-level cache and singleton insrc/, with eviction policy, max-size target, and source-file link. A contract test (tests/unit/memory-budget.test.ts) keeps the doc in sync with code. - Memory soft-cap watchdog. Optional
OPENSAFARI_MEMORY_SOFT_CAP_MBenv var — when RSS crosses the cap, the telemetry sink emits a structured warning anddiagnosereportsmemory_status: "warn". - Enhanced
diagnosememory block. Now includesrss_growth_mb_per_hour,soft_cap_mb, andnotesarray for cache-budget violations. - 60-minute soak test.
tests/soak/long-session.soak.test.tsround-robins across all backend tiers, asserting RSS delta ≤ 100 MB, rolling growth rate ≤ 3 MB/min, and — new — that no retained-object class grows by more than 1000 instances between the 30-minute and 60-minute marks. Heap snapshots at 0 / 30 / 60 min are written viav8.writeHeapSnapshot()(no--expose-gcflag required) and, on any SLO miss, the test emits the snapshot paths plus the top-20 class growers for triage. Gated byOPENSAFARI_RUN_SOAK=1. - Nightly CI workflow.
.github/workflows/memory-soak.ymlruns the soak test daily at 03:00 UTC. Seven consecutive failures auto-open amemory-regressionissue. - Developer script.
scripts/memory-inspect.ts— one-shot 10-minute mixed-call session that prints a per-backend RSS/heap table for local triage.
- If you import any
app_storekit_*tool: you will get a "tool not found" error on 0.5.0. Either pin toopensafari-mcp@0.4.9(supported viadocs/recipes/flutter-iap-ko-kr.md), or migrate to the AX-based pattern indocs/storekit-automation.md(app_tree→app_tap_element→app_alert_handle). - If you parse MCP input-tool responses: expect
_meta._telemetryon every response by default now. SetOPENSAFARI_INPUT_TELEMETRY_META=0to restore 0.4.9 behavior. - If you operate a fork with Private API Sentinel alerting enabled: set
secrets.SENTINEL_WEBHOOK_URLand (optionally)vars.SENTINEL_CHANNEL. Missing secret is a no-op; the tracking issue still opens. - If you ship an iOS app that bundles any of our code: read
docs/private-apis.md→ "Deployment scope" first. OpenSafari is macOS-host / CI-only — never inside an.ipa. - If you rely on
OPENSAFARI_ENABLE_POINTERSERVICE: the backend is now covered by a live suite and a daily sentinel, but promotion to default requires 2 weeks at ≥ 99% success on Xcode 26.0 / 26.1 with zero AppleScript fallbacks. Still opt-in in 0.5.0.
- Tag:
v0.5.0 - Branch:
main(fromdevelop— merged 2026-04-17) - PRs merged into this release: #618, #619, #620, #621, #622, #623, #625, #626, #627, #628, #629, #630, #631, #632, #633 (15)
- Build/lint/test state on publish:
npm run build✅,npm run lint✅,npm test✅ (114 suites / 1686 tests green on develop@9db79f4c) - Required checks on
develop: build, lint, test — all pass. Live suites (Flutter,Native,WebView,Safari) are not required by branch protection but were green on each constituent PR.
This release closes the Xcode 26+ headless tap gap with a new Tier 1.5 AX press backend, hardens Flutter VM routing for release builds, adds process-wide memory tracking to diagnose, and ships the complete iOS 26 investigation synthesis. Together with the Tier-1 SimHID gating from v0.4.8, element-targeted native automation (app_tap_element, app_type_element) is now fully headless on Xcode 26+ — no Simulator.app focus required.
AccessibilityPressInputBackend— Tier 1.5 headless element-targeted tap/focus.app_tap_elementandapp_type_elementnow invokeAXUIElementPerformAction(element, kAXPressAction)through the existingax-bridgeSwift helper before falling through to the coordinate-based backend chain. The path does not synthesise OS-level input, so the physical mouse cursor never moves andSimulator.appdoes not need to be foregrounded — and critically it works on Xcode 26+ where Tier-1 SimHID tap/swipe is disabled (#537). This closes the bulk of the native-automation gap that epic #484 tracks.- New
ax-bridge press --path <index-path> --device <udid>sub-command with uniform JSON response ({ ok, code, path, actions, role, identifier, label, message, axErrorCode }).PRESS_NOT_ACTIONABLEandPRESS_FAILEDare in-band with exit 0 for transparent fallback; bridge-level errors exit non-zero. app_tap_elementtries Tier 1.5 whenduration === 0and the element has a path. Response carriesbackend: 'ax-press',_meta.headless: true.app_type_elementuses AX press for tap-to-focus; typing flows through the selected input backend.- New
OPENSAFARI_DISABLE_AX_PRESS=1env var to disable the Tier 1.5 path.
- New
InputBackendKindgains the'ax-press'variant for telemetry and_meta.backendKindconsistency.
FlutterVMClient.probeEvaluateCompile()— cheap evaluate probe (< 500 ms p95) that gates Tier-0 on compile capability, not just VM reachability. Release-mode Flutter apps andsimctl launchwithoutflutter runnow fall through to lower tiers instead of surfacing a rawcode 113error.FlutterVMInputBackendError.codeis a structured union (VM_NO_EVALUATE | DART_ERROR | UNKNOWN) with actionable remediation messages.- WebSocket leak fix — orphaned
FlutterVMClientconnections on negative probe results are now closed viaremoveFlutterVMClient()to prevent file descriptor leaks on release-mode apps.
sim-hid-bridge tap-digitizersubcommand (#491, #556): IOHIDEvent digitizer probe that synthesiseskIOHIDEventTypeDigitizerand wraps withIndigoHIDMessageForPointerEventFromHIDEventRef. Hypothesis falsified (wrapper returns nil for digitizer events) — shipped as negative-result evidence with reusable IOKit scaffolding for next investigation candidate.- iOS 26 tap regression synthesis document (#491, #557):
docs/simhid-ios26-investigation.md— comprehensive investigation synthesis covering symptom, reproduction, falsification log for 4 candidates, shipped tooling catalogue, remaining candidates ranked by feasibility, and actionable next-step checklist. Cross-referenced fromdocs/private-apis.md.
- Headless SSOT reflects Tier 1.5.
docs/headless-architecture.mdadds a Tier 1.5 routing-table row, backend details forAccessibilityPressInputBackend, updated "Practical impact on Xcode 26+" blockquote, scenario-matrix rows splitting native Xcode 26+ into element-targeted (headless) and coordinate-only (opt-in), andOPENSAFARI_DISABLE_AX_PRESSin the environment-variables table.
This release is a stabilization + observability cut focused on the Xcode 26 headless-input regression discovered after v0.4.6/v0.4.7 shipped. It disables the one SimulatorKitHID code path that silently misses target (native tap/swipe on Xcode 26+), hardens the remaining tiers with telemetry and daily CI probes, documents the investigation in full, and adds three new sim-hid-bridge subcommands for on-device diagnosis. No new MCP tools are added — the surface stays compatible with 0.4.7, but _meta now carries _telemetry latency/routing info for every input call.
- Disable SimHID tap/swipe routing on Xcode 26+ (#491, #537, #62034af3):
getInputBackend()now skips theSimulatorKitHIDInputBackendfortap/swipeoperations when the simulator's parent Xcode is 26.0 or newer. Apple's iOS 26.x Simulator runtime dropsIndigoHIDMessageForMouseNSEventhandling in CoreSimulator'sSimDevice, soIOHIDEventinjection succeeds at the HID layer but never reaches the app's hit-test (the tap is ack'd, the UI does not respond). Keys, buttons, and text input remain on Tier 1 — only pointer ops fall through. Falls through to Tier 2simctl io input(absent on Xcode 26), then Tier 3 AppleScript/CGEvent (focus-stealing, but functional).HeadlessInputUnavailableError.reasongains the new'simhid-gated'variant (#547) so callers can distinguish a cache hit that was intentionally skipped from a true unavailability. sim-hid-bridgereports screen size in points, not pixels (#491, f4368f4c): The SimulatorKit HID probe previously reported physical pixel bounds fromCoreSimulator's display service. All callers were dividing byscaleagain, double-scaling the result on any Retina simulator. The bridge now returns logical point dimensions directly and all routing-layer coordinate math was adjusted accordingly.- Native Settings.app test locale-aware (#423, #535):
tests/integration/issue-423-native.live.test.tsno longer hard-codes the English string"General"when walking Settings. It readscom.apple.Preferenceslocalization at runtime and falls back to the button's accessibility identifier, so the suite passes on non-English simulators and in locale-randomized CI.
sim-hid-bridge diagsubcommand (#491, #551, a83fe57b): New read-only diagnostic command that reports SimulatorKit availability, device boot state, display bounds, scale, screen-size-in-points, and the resolvedhidProbeFnsymbol in one JSON payload. Exits with the same classification as other subcommands (0 success, 64 BAD_ARGS, 69 DEVICE_NOT_BOOTED, 78 SIMULATORKIT_UNAVAILABLE). Used by the new daily sentinel probes and surfaced viadiagnoseMCP tool.sim-hid-bridge tap-pssubcommand (#491, #555, 128e96fb): Experimental alternative tap implementation that drives Apple's Pointer Service (CoreSimulator.framework'sSimPointerClient) instead ofIOHIDEvent. Ships as an investigation tool — not wired into the routing chain — so the #491 investigation can A/B compare IOHIDEvent vs PointerService against the same simulator. Paired with the tap-digitizer probe (#556, still in review) and the synthesis doc (#557, still in review) to form the public investigation artifact.- Live integration suite for SimulatorKitHID (#491, #536):
tests/integration/sim-hid-live-integration.test.tsboots two simulators in parallel (one Flutter fixture, one native fixture), drivesapp_tap/app_swipe/app_key_inputthrough each, and asserts_meta.backendKind,_meta.headless, and tap-landed verification via post-tap screenshot diff. Gated behindOPENSAFARI_LIVE_SIMHID=1so the defaultnpm teststays headless; used by the new daily sentinel workflow. - Screenshot-on-failure reporter (#491, #548): Live integration suites now auto-save a simulator screenshot to
artifacts/test-failures/on every failed assertion, so regressions in headless tap routing surface with the actual pixel state rather than just a stack trace. The reporter is a Jest custom reporter and is a no-op whenprocess.env.CI !== 'true'. - Dual-boot routing tests (#491, #549):
tests/unit/dual-boot-routing.test.tscovers the case where Flutter and native simulators are booted simultaneously under different UDIDs — validates that per-device backend caches (cachedSimHidBackend, FlutterVM negative cache) do not leak across device IDs. - Live integration alignment post-#537 (#491, #546): After #537 disabled SimHID tap/swipe on Xcode 26+,
tests/integration/sim-hid-live.test.tswas rewritten to assert the post-disablement routing contract (SimHID only for keys/buttons, AppleScript for tap/swipe) instead of the pre-#537 all-headless claim.
- Private API sentinel workflow (#493, #541, #542, 70afa2b1): New
.github/workflows/private-api-sentinel.ymlruns daily at 07:00 UTC with six independent probes:SimulatorKit.frameworkreachable,CoreSimulator.frameworkreachable,dlopen(SimulatorKit)+dlsym(SimulatorKitHIDInputBackend),sim-hid-bridge diagdevice-not-booted exit,sim-hid-bridge diagagainst a booted simulator, andtap-psPointerService symbol probe. Alerts to the#opensafari-sentinelsSlack channel with the probe name and the exact error on any red probe, so macOS / Xcode updates that break the private-API contract are caught in ≤24h instead of in a customer repro. - Sentinel probe tests (#493, #520):
tests/sentinel/private-api-probe.test.tsis a TypeScript-level mirror of the workflow probes — six probes that run undernpm run test:sentinelso local repros can validate the same gates the workflow enforces. Excluded from the default jest run (tests/ci/+tests/sentinel/both removed from defaulttestPathIgnorePatterns-driven test globs). - One-time private API warning (#493, #527, 898f799a): The first time per-process that
SimulatorKitHIDInputBackendorAccessibilityBridgeactually dispatches,sim-hid-input-backend.tsandaccessibility-bridge.tsemit a singleconsole.errorinformational line pointing atdocs/private-apis.md. Subsequent calls stay silent. The message includes the Apple framework path, the documented BC-break monitoring strategy (sentinel CI + fallback tiers), and the license note vs Facebook idb. Error responses also now referencedocs/private-apis.mdin theirremediationfield. - idb vs OpenSafari call-pattern comparison (#493, #529, fa20d179):
docs/private-apis.mdgains a side-by-side comparison of which private frameworks each project loads, the tap dispatch call pattern (idb:SimDeviceIOClientvia idb_direct + XPC; OpenSafari:dlopen(SimulatorKit)+ SimulatorKitHIDInputBackend), and a license-independence note (idb is MIT, OpenSafari'ssim-hid-bridge.swiftwas written from Apple's public headers without reading idb source). - CI hardening: daily sentinel + one-time private-API notice (#493, #532, af22ca7e, 68747103):
--passWithNoTestsadded to the sentinel workflow so empty probe suites don't fail the run while the workflow is being bootstrapped. Telemetry tests (tests/unit/mcp-telemetry-metadata.test.ts) adapted so they coexist with the one-time private-API warning log line.
timedInputwrapper + JSON telemetry sink for native input backends (#502, ae378394):src/metrics/input-telemetry.tswraps everyInputBackend.{tap,swipe,key,button,typeText}call with a monotonic-clock duration measurement and a structured JSON payload ({backend, op, device_id, duration_ms, headless, outcome, error_reason?}). The sink writes toOPENSAFARI_TELEMETRY_PATHwhen set (default: unset → in-memory only) and is wired intoSimulatorKitHIDInputBackend,SimctlIOInputBackend,AppleScriptInputBackend, andNativeInputBackend.- FlutterVMInputBackend telemetry (#502, db534caa):
FlutterVMInputBackendnow emits the sametimedInputenvelope so Tier-0 dispatches appear alongside Tier-1 in the aggregate view. Failure modes (VM Service disconnect, DDS probe fail, library-scope evaluate error) surface asoutcome: 'error'with theerror_reasonset to the structuredFlutterVMError.code. - Opt-in
_telemetrymetadata on MCP input tools (#502 Phase 2, #528, 55b2bb8c): All 10 input tool responses (app_tap,app_swipe,app_scroll_native,app_key_input,app_double_tap,app_type_text,app_tap_element,app_type_element,app_dismiss_keyboard,app_alert_handle) now include_meta._telemetry: {duration_ms, backend_kind, op, outcome}whenOPENSAFARI_TELEMETRY_META=1is set. Off by default so the MCP response envelope stays minimal; CI and benchmarking harnesses flip it on. - p50/p95/p99 latency rollup aggregator (#502, #544, 033f6db8):
src/metrics/latency-rollup.tsreads the JSON telemetry sink, groups by(backend × op), and emits p50/p95/p99 rollups plus per-bucket error-rate. Exported asrollupLatency()for in-process use and exposed via thediagnoseMCP tool's newlatency_rollupblock whenOPENSAFARI_TELEMETRY_PATHis readable.
- Daily Safari headless smoke (#501, #524, 38f4af87): New
.github/workflows/headless-smoke.ymlruns daily against a booted simulator + ios-webkit-debug-proxy + aqa_*audit suite. Asserts zeroAppleScriptInputBackendcalls in the telemetry log and uploads screenshots on failure. Serves as the headless contract for the Safari audit path. - Flutter headless smoke job (#501, #526, bd11d9c8): Second job in
headless-smoke.ymlthat boots a simulator, installs the Flutter QA fixture, runsapp_tap/app_tap_element/app_type_elementthrough the FlutterVM Tier-0 backend, and asserts_meta.headless === trueon every response. - Native SimulatorKitHID headless smoke (#501, #530, c617aa9f): Third job that targets a native UIKit app (
com.apple.Preferences) on Xcode 26+, validates the post-#537 routing (keys via Tier 1, tap via Tier 3 with AppleScript fallback), and asserts the sentinel path produces a_meta.backendKindfor every response. node -ereturn statement fix (#501, #545, 259db32f): UDID picker shell snippets usedreturnat the top level of anode -eexpression, which is a syntax error under Node 20+. Replaced withprocess.exit(code). The pre-fix jobs were silently selecting the first simulator in the list instead of the matching Xcode-26 runtime, which masked the #491 regression for a week.
- Documentation: headless architecture Tier 0/1 activation (#492, #525):
docs/headless-architecture.mdno longer describes SimulatorKitHID as a PoC — it now documents the 5-tier routing table (Flutter VM → SimulatorKit HID → simctl → WebKit → AppleScript), an updated Mermaid decision flowchart, the Xcode-26-vs-legacy scenario matrix, the fullsim-hid-bridgeexit-code contract, and theOPENSAFARI_HEADLESS_ONLYenvironment variable.FlutterVMInputBackendis reclassified from "planned" to "Production (Tier 0)" andHeadlessInputUnavailableError.reasondocuments the'headless-only'variant. - README tier table + comparison refresh (#492, #525): The Input Backend Selection table lists all five tiers (0–4) with the new Tier-0 Flutter and Tier-1 SimulatorKit HID rows, example tool responses include the
_meta.backendKind/_meta.headlessenvelope, and the Headless Capabilities section includes a dedicated comparison vs Appium, idb, and XCUITest. - Xcode 26+ SimHID tap/swipe caveat (#492, refs #491, #537, #550):
docs/headless-architecture.mdreflects thatgetInputBackend()skips Tier 1 for tap/swipe on Xcode 26+ pending the AppleIndigoHIDMessageForMouseNSEventregression fix. The Tier-1 status row is downgraded from "Production" to "Partial (tap/swipe disabled on Xcode 26+)", the Mermaid flowchart branches on op kind before returningSimulatorKitHIDInputBackend, and the scenario matrix rows for native iOS on Xcode 26+ are split into keys/buttons (headless) vs tap/swipe (blocked). README "Headless Capabilities" and "Headless input vs other iOS automation tools" tables carry the matching⚠️ caveat with a link to #491. - SimulatorKitHIDInputBackend marked as production Tier 1 (#492, #534): Status in
docs/headless-architecture.mdupgraded from "PoC" (v0.4.5 language) to "Production (Tier 1, partial on Xcode 26+)" to reflect the v0.4.6 shipping contract.
- 1558 tests across 106 suites (up from 1510/103 in v0.4.7).
- New test files:
mcp-telemetry-metadata.test.ts,input-telemetry.test.ts,latency-rollup.test.ts,sim-hid-live-integration.test.ts,dual-boot-routing.test.ts,screenshot-reporter.test.ts,private-api-probe.test.ts(sentinel), updatedsim-hid-sentinel.test.ts. - Extended:
native-input-backend.test.ts(simhid-gated reason),sim-hid-input-backend.test.ts(one-time warning + screen-size-in-points),accessibility-bridge.test.ts(one-time warning),diagnose.test.ts(latency rollup block),issue-423-native.live.test.ts(locale-aware). - All tests green on develop.
npm run lintshows 0 errors, 455 pre-existing warnings (no new regressions).
- No breaking API changes. MCP clients on 0.4.7 continue to work without modification.
- Xcode 26+ users regain the focus-stealing fallback for tap/swipe. If
OPENSAFARI_HEADLESS_ONLY=1is set on Xcode 26+, native-app tap/swipe now throwsHeadlessInputUnavailableErrorwithreason: 'headless-only'instead of silently missing target. CI jobs that rely on headless native tap should pin to Xcode 16.x or switch to the Flutter VM Tier 0 path where possible. - Telemetry is opt-in. Set
OPENSAFARI_TELEMETRY_PATH=<file>to enable the JSON sink, andOPENSAFARI_TELEMETRY_META=1to have_telemetryappear in MCP response envelopes. - Sentinel workflow requires a
SLACK_WEBHOOK_URLsecret for alerting. Without it the workflow still runs but alert notifications are skipped.
- npm:
opensafari-mcp@0.4.8 - git tag:
v0.4.8 - compare:
v0.4.7…v0.4.8 - merged PRs: #520, #524, #526, #527, #528, #529, #530, #532, #534, #535, #536, #537, #541, #542, #544, #545, #546, #547, #548, #549, #550, #551, #555 (plus fix commits f4368f4c, ae378394, db534caa, 70afa2b1)
FlutterVMInputBackend— Tier-0 headless Flutter input (#481, #486): The Dart VM Service-based input backend now ships as the highest-priority routing tier ingetInputBackend(). When the target device runs a Flutter app in debug or profile mode, pointer events, text input, and key presses are dispatched directly into the Dart isolate via VM Serviceevaluate— completely bypassing OS-level input.tap(x, y, duration?)— syntheticPointerDataPacketwith down/up phases viaPlatformDispatcher.onPointerDataPacketswipe(x1, y1 → x2, y2, duration?)— interpolatedPointerChange.moveeventstypeText(text)—TextInput.updateEditingStateplatform message via primary focuskeypress(hidUsage)/sendKey(name)—HardwareKeyboardevents with HID →LogicalKeyboardKeymapping- No CGEvent synthesis, no mouse cursor movement, no Simulator.app focus stealing
- No
OPENSAFARI_ALLOW_FOCUS_INPUTopt-in required — Flutter route is always headless - Per-device negative cache (30s TTL) prevents repeated discovery probes for non-Flutter devices
- 1.5s discovery timeout bounds VM Service probe so native iOS apps don't stall input tools
- Per-operation library scoping for Dart
evaluate(#481, #514): The Dart VM ServiceevaluateRPC compiles expressions in the scope of a specific library. Different operations now target the library that exposes their required symbols:- tap/swipe →
widgets/binding.dart(PlatformDispatcher,PointerDataPacket,PointerChange,PointerDeviceKind) - typeText →
widgets/editable_text.dart(FocusManager,EditableTextState,TextEditingValue,SelectionChangedCause) - keypress/sendKey →
services/hardware_keyboard.dart(HardwareKeyboard,KeyDownEvent,KeyUpEvent,LogicalKeyboardKey)
- tap/swipe →
- DDS requirement documentation (#481, #515): Documented in
docs/headless-architecture.mdand integration test fixtures that Flutter apps must be launched viaflutter run(which starts Dart Development Service / DDS and the frontend compiler). Apps launched viaxcrun simctl launchexpose the VM Service socket but lack the compilation service, soevaluatecalls fail. Integration test fixtures (tests/integration/flutter-vm-input.live.test.ts) updated to require aflutter run-launched fixture app.
- 1510 tests across 103 suites (up from 1488/102 in v0.4.6)
- New: extended
flutter-vm-input-backend.test.tscoverage for library-scoped evaluate, integration test for tap/swipe/typeText/keypress against a fixture Flutter app - All tests green on develop with FlutterVMInputBackend coexisting with SimulatorKitHID Tier 1 and the rest of the routing chain
This release graduates FlutterVMInputBackend from DRAFT (where it sat in v0.4.5) to production-ready Tier 0. Combined with the SimulatorKitHID Tier 1 work in v0.4.6, OpenSafari now has end-to-end headless input coverage for both Flutter apps and native iOS apps on Xcode 26+ where simctl io input was removed.
SimulatorKitHIDInputBackend— full HID injection (#489, #490): The PoCsim-hid-bridge.swiftnow performs realIOHIDEventinjection viaSimulatorKit.frameworkprivate API. Activated as Tier 1 ingetInputBackend()— native iOS app taps, swipes, and key presses are now headless on Xcode 26+ wheresimctl io inputwas removed.tap(x, y, duration?),swipe(x1,y1 → x2,y2, duration?),key(hidUsage),button(home|lock|sound-up|sound-down)all implemented.- Exit code classification: 0 (success), 64 (BAD_ARGS), 69 (DEVICE_NOT_BOOTED), 78 (SIMULATORKIT_UNAVAILABLE), 99 (NOT_IMPLEMENTED).
- CI sentinel tests (
tests/ci/sim-hid-sentinel.test.ts) probe SimulatorKit availability daily.
diagnoseMCP tool (#498): New read-only diagnostic tool that reports backend availability, proxy status, environment variables, and a structuredheadless_verdictJSON. Registered at Tier 1 (always visible). Answers "is this setup truly headless?" in one call._meta.backendKindin input tool responses (#504): All 10 input tools (app_tap,app_swipe,app_scroll_native,app_key_input,app_double_tap,app_type_text,app_tap_element,app_type_element,app_dismiss_keyboard,app_alert_handle) now include_meta: { backendKind, headless, deviceId }in their success responses. CI can assert_meta.headless === trueto verify no focus-stealing backend was used.OPENSAFARI_HEADLESS_ONLY=1environment variable (#499): CI safety net that blocks the AppleScript/CGEvent fallback regardless ofOPENSAFARI_ALLOW_FOCUS_INPUT. When set, any attempt to fall through to the focus-stealing backend throwsHeadlessInputUnavailableErrorwithreason: 'headless-only'and tailored remediation. OverridesALLOW_FOCUS_INPUTwith a warning log when both are set.docs/headless-architecture.md(#496): Comprehensive documentation of the multi-tier input backend routing system — Tier table, Mermaid flowchart ofgetInputBackend(), scenario matrix (Safari/Flutter/Native/WebView), environment variable reference,HeadlessInputUnavailableErrorhandling guide, private API policy cross-reference.docs/ci-recipes.md(#497): Copy-paste ready CI workflow recipes for GitHub Actions, Buildkite, and GitLab CI. Covers simulator boot-wait pattern, proxy verification, screenshot artifacts, JUnit reports, andOPENSAFARI_HEADLESS_ONLY=1configuration.- README "Headless mobile QA automation" tagline (#500): Project description updated with headless positioning. New Headless Capabilities comparison table (Safari ✅ / Flutter
⚠️ / Native⚠️ / WebView⚠️ ) with links to architecture docs.
- Proxy socket finder race condition (#494):
device_bootconsistently failed to auto-start the WebInspectorProxy becausefindSocketPath(targetUdid)was called exactly once with no retry — butwebinspectord_simneeds several seconds aftersimctl bootto create its Unix socket. AddedwaitForSocketPath()that polls every 500ms for up to 10 seconds. All WebKit-dependent tools (navigate,screenshot,app_tap, etc.) now work reliably after cold boot. app_tree/app_queryax-bridge not found (#495):AccessibilityBridge.resolveBridgePath()had dead code in candidate 1 and no dev-mode source tree fallback. Rewrote to match the robust 5-candidate pattern fromsim-hid-input-backend.ts— compiled binary (parent + same dir), Swift source (parent + same dir), plus guarded dev-only fallback viaOPENSAFARI_ALLOW_SWIFT_INTERPRETER=1. Error message now lists all searched paths.- DDS probe for Tier-0 backend (#519): FlutterVM Tier-0 backend selection now probes for Dart Development Service availability before activation, preventing connection failures on Flutter apps that don't expose DDS.
- 1488 tests across 102 suites (up from 1476/100 in v0.4.5)
- New test files:
accessibility-bridge.test.ts(6 tests),diagnose.test.ts(10 tests),sim-hid-sentinel.test.ts(5 tests) - Extended:
native-input-backend.test.ts(+7 HEADLESS_ONLY tests),socket-finder.test.ts(+4 polling tests),proxy-timing.test.ts(+2 tests),app-interaction-tools.test.ts/app-scroll-native.test.ts/app-dismiss-keyboard.test.ts/app-alert-handle.test.ts(_meta assertions)
SimulatorKitHIDInputBackend(PoC) (#483, #487): New input backend class that bridges to Apple's privateSimulatorKit.frameworkvia a Swift helper binary (src/native/sim-hid-bridge.swift). This is the foundation for headless native-iOS-app automation — HID event injection without moving the user's mouse cursor or stealing Simulator.app focus. Ships as a PoC stub (exit code 99NOT_IMPLEMENTED): the Swift binary validates args, dlopens bothSimulatorKitandCoreSimulatorframeworks to prove they are reachable, but defers actual HID injection to a follow-up PR. Routing is intentionally NOT activated — the backend class is exported for manual testing only;getInputBackend()continues to select existing tiers. Node wrapper (src/tools/sim-hid-input-backend.ts) maps Swift exit codes (64/69/78/99) to structuredInputBackendErrorobjects.tryCreateSimulatorKitHIDBackend()factory probes for the compiled binary ordist/-copied source with graceful null return when absent. Source-tree fallback gated behindOPENSAFARI_ALLOW_SWIFT_INTERPRETER=1to prevent path traversal when the package is consumed as a dependency.- New
docs/private-apis.mddocuments: which private frameworks are loaded, where they live, the dlopen rationale, BC-break monitoring strategy (sentinel CI + preserved fallback tiers), license note vs Facebook idb (MIT, independently written), and the maintenance contract. - 21 unit tests covering arg construction, exit-code classification, JSON parse failure, factory null path, and swift interpreter fallback.
- New
FlutterVMInputBackend(Tier-0) (#481, #486): New input backend that dispatchesPointerDataPackets,TextInput.updateEditingStateplatform messages, andHardwareKeyboardevents directly into the Dart isolate via VM Service — no CGEvent, no Simulator.app foregrounding, noOPENSAFARI_ALLOW_FOCUS_INPUTopt-in required. Tier-0 routing added togetInputBackend(): when a Flutter VM is discoverable, the new backend is selected before all existing tiers. Per-device negative cache (30s TTL) + 1.5s discovery timeout prevent native-app latency regression. Scoped evaluate calls target per-operation Flutter libraries (mouse_tracker.dartfor pointer dispatch,editable_text.dartfor text input,hardware_keyboard.dartfor key events) to ensure all required symbols are in lexical scope.
- AppleScript input backend: every Tier-3 tap misses by 28pt on Xcode 26 (#482, #485):
AppleScriptInputBackendhardcodedTITLE_BAR_HEIGHT = 28and added it to every iOS→macOS coordinate translation. On Xcode 26 / iOS 26.4 / iPhone 16 theAccessibilityBridgealready returns frames in macOS-window-relative coordinates, so the +28pt offset sent every CGEvent tap into the empty white space below the actual button (verified live: Login button center at screen y=566, backend computed y=591). Replaced with dynamic measurement via AppleScript that reads the position ofUI element 1 of window 1(the iOS device screen content area within the macOS window). Per-device cache with explicitrefresh: trueinvalidation. Fallback to raw window position with one-timeconsole.errorper device when the AX query fails. flutter_widget_at_pointreturns wrapper type inwidget_type(#436, #479): The tool previously surfaced the raw inspector_ElementDiagnosticableTreeNodewrapper in thewidget_typefield becausesummariseNodereads the inspector's owntypekey first. The public payload now preferswidgetRuntimeType(e.g."ElevatedButton") and falls back todescriptionbefore the wrappertype, so callers see the Flutter widget name that the checklist promises.flutter_widget_at_pointancestor_chainalways empty against real apps (#436, #480):flattenParentChainonly recognised the synthetic{chain: [...]}and{result: {chain: [...]}}shapes used by the unit tests. The live Flutter 3.11+ response fromext.flutter.inspector.getParentChainis{type: "_extensionType", result: [{node, children}, ...]}— theresultkey IS the chain array. That third shape is now detected viaArray.isArray(raw.result), so the tool returns the real ancestor path instead of an empty list.
flutter_widget_at_point(#436, #471): New MCP tool that maps a physical-pixel coordinate — matching the frame produced byapp_screenshot_native— to the topmost Flutter widget at that point. The tool readsdevicePixelRatiolive fromFlutterView.platformDispatcher, converts the physical (x, y) to logical pixels, drives a Dart-side hit-test viarenderView.hitTest(HitTestResult(), position: Offset(…)), walksHitTestResult.pathfor the topmostRenderObjectwith aDebugCreator, selects the owning Element viaWidgetInspectorService.instance.setSelection, then reads back the selected widget throughgetSelectedSummaryWidget.- Returns
{widget_type, description, creation_location, widget_id, ancestor_chain}. Theancestor_chainis pulled fromext.flutter.inspector.getParentChainand filtered to user-defined widgets (anything underpackage:flutter/,package:flutter_localizations/, or an absoluteflutter/packages/flutter/…SDK checkout is dropped) so LLM consumers see only the app's own widget hierarchy. - Out-of-bounds coordinates (x < 0, x ≥ width, y < 0, y ≥ height) short-circuit to
{widget_type: null, reason: "out-of-bounds"}without paying a VM Service round-trip. In-bounds misses return{widget_type: null, reason: "no-hit"}. Best-effortancestor_chain— a failure ingetParentChainstill yields the topmost widget withancestor_chain: []plus a stderr audit entry. - The Dart hit-test expression references
DebugCreator,WidgetInspectorService,RenderView, andHitTestResult— symbols that live inpackage:flutter/src/widgets/widget_inspector.dartand are NOT re-exported throughflutter/material.dart. To avoid "Undefined name" failures on user apps that only import material, the evaluate call is now scoped to the inspector library by resolving it viagetIsolate → librariesand passing itsidastargetId. Throws a dedicatedFlutterVMError('NO_INSPECTOR_LIB')if the library is not loaded in the isolate. objectGroup(the Flutter Inspector lifetime scope) is validated against/^[A-Za-z0-9_-]+$/before interpolation into the Dart source literal, blocking Dart injection via a quote-escaped payload. Invalid values raiseFlutterVMError('INVALID_OBJECT_GROUP').
- Returns
FlutterVMClient.selectWidgetAtPoint/getParentChain: New public VM-client helpers wrapping the hit-test evaluate expression andext.flutter.inspector.getParentChain. Exported so downstream tooling can reuse the coord→widget pipeline without re-implementing the hit-test Dart expression.- Flutter version branching for inspector service extensions (#436, #472):
FlutterVMClientnow captures the Dart VMversionstring atflutter_connecttime, parses it via the exportedparseDartVersionhelper, and branchesgetRootWidgetSummaryTreecalls by Flutter major.- Flutter 3.x sessions try
ext.flutter.inspector.getRootWidgetSummaryTreeWithPreviewsfirst and fall back togetRootWidgetSummaryTreeon VM Service error -32000 (seen on early 3.x releases that have the extension stub but no implementation). - Flutter 2.x sessions skip the
WithPreviewsvariant entirely — it does not exist on 2.x, and attempting the call was a guaranteed -32601 ("method not found") round-trip on every call. - Unknown / unparseable versions preserve the historical try/catch fallback so the client stays forwards-compatible with future Flutter majors.
flutter_connectresponses now includedartVersion(structured{raw, major, minor, patch, channel}) andflutterMajorso downstream tools can gate behaviour per major. New accessors:FlutterVMClient.getDartVersion()/getFlutterMajor().
- Flutter 3.x sessions try
parseDartVersionhelper (#472): Pure, exported helper that extracts{major, minor, patch, channel?, raw}from a Dart VM version string. Null-safe and whitespace-tolerant.
- Flutter QA fixture app (#422, #476): Added a dedicated Flutter fixture under
tests/fixtures/flutter-qa-app/. The fixture exercises the surfaces #422 depends on:Semantics(label: 'Login')+Semantics(identifier: 'login-btn')wrapping anElevatedButtonsoapp_querycan be driven by both label and identifier.- A
TextFieldwrapped inSemantics(identifier: 'email-field')soapp_query({identifier: 'email-field'})andapp_type_elementcan be verified against a live editable region. - A live
Counter: $ntext that increments on each button tap so downstream suites can assert state changes. build.shhelper that runsflutter pub get,flutter build ios --simulator --debug, and an optionalxcrun simctl installso reviewers can bring the fixture up with a single command.- Bundle id
com.opensafari.fixtures.flutterQaApp— avoids collision with Flutter'scom.example.*sample prefix on shared simulators.
- Live Flutter integration suite (#423, #473): New opt-in Jest suite at
tests/integration/issue-423-flutter.live.test.tsthat drives a booted simulator against a running Flutter app and provesapp_query+app_tap_elementresolve and interact with Flutter Semantics nodes (label, identifier, index, and ambiguous-match cases). Gated behindjest.config.jstestPathIgnorePatternsso the defaultnpm teststays headless. - Native (non-Flutter) integration suite (#423, #474):
tests/integration/issue-423-native.live.test.tswalkscom.apple.Preferences(Settings → General → About) to prove the sharedAccessibilityBridgepath works identically on UIKit apps — no Flutter-specific branching exists in the bridge, so Settings.app is sufficient to demonstrate parity. - Performance harness (#423, #475):
tests/integration/issue-423-perf.live.test.tsmeasuresapp_query/app_tap_elementround-trip time and asserts an RSS budget (≤ 50 MB over a 100-iteration loop under--expose-gc) so regressions surface as perf failures rather than silent slowdowns. - Fixture release-constraint integration test (#422, #478, replaces #477):
tests/integration/flutter-fixture-ax.test.tsbuilds the QA fixture above, installs it on a booted simulator, and verifies:app_treepopulates even whenuseVMServiceFallback: false— the simctl-path activation must succeed standalone, proving parity with a real Flutter release build (where the Dart VM Service is stripped).app_query({identifier: 'login-btn'})andapp_query({identifier: 'email-field'})return the expectedSemantics(identifier:)nodes with correct role (AXButton/AXGenericElement), visibility, and enabled flags.
- Honours
FLUTTER_BINenv override with aflutter-on-PATH fallback so the suite runs uniformly on Apple Silicon brew, Intel brew, asdf, and nix installs. console.errorSKIP log suppressed whenprocess.env.CIis set so the always-skipped-in-CI suite stops spamming shared CI output.
FlutterVMClient.getDartVersion()now returns the structuredDartVersiontype (exported fromsrc/flutter/flutter-types.ts) — additive to the priornull | undefinedshape for callers that were only checking truthiness.getFlutterMajor()normalises all absent-version states tonullfor consistent consumer code.
- Dart injection hardening for
flutter_widget_at_point(#471):objectGroupis now rejected before interpolation when it contains any character outside[A-Za-z0-9_-]. The prior implementation concatenated the raw caller-provided string into a Dart source literal via'${objectGroup}', which a malicious caller could have used to break out of the quoted context and execute arbitrary Dart on the target device. Local MCP is a trusted-caller context, but defence-in-depth is cheap.
- Removed stale
flutter createboilerplatewidget_test.dartfrom the QA fixture (#476 review P1). The stub assertedfind.byIcon(Icons.add)and a counter starting at"0", but the fixture'smain.dartwas rewritten to render Semantics + TextField +Counter: $n;flutter testinside the fixture would have failed immediately, undermining the "fixture is stable" contract. - Dropped the Apple-Silicon-only hardcoded
/opt/homebrew/bin/flutterprobe from the fixture integration test (#477/#478 review P1) in favour of aFLUTTER_BINenv override with aflutter-on-PATH fallback. The prior probe always threw on Intel Macs, nix, and asdf before the fallback ran. - Tightened the
selectWidgetAtPointhit-result parsing: dropped the tautologicalkind === 'Bool' && valueAsString === 'true'disjunct that was subsumed by the primaryvalueAsString === 'true'check.
- 1395+ tests / 97+ suites pass on
npm test(the default headless run). New gated integration suites undertests/integration/**are excluded from the default run and exercised manually by reviewers with a booted simulator. - 25 new unit tests in
tests/unit/flutter-widget-at-point.test.tscover: DPR=2/3 conversion, out-of-bounds short-circuit, no-hit path, successful hit with ancestor-chain filtering, not-connected errors, non-finite coordinate rejection,objectGroupsanitization, missingwidget_inspectorlibrary handling, and best-effortgetParentChainfailure. - 11 new unit tests in
tests/unit/flutter-version-branching.test.tscover: Dart version parsing (happy / whitespace / invalid / missing channel), 3.x happy path / 3.x WithPreviews fallback / 2.x direct call / unknown-version try/catch fallback, andgetDartVersion/getFlutterMajorpre- and post-connect state.
- npm:
opensafari-mcp@0.4.4 - git tag:
v0.4.4 - compare:
v0.4.3…v0.4.4 - merged PRs: #471, #472, #473, #474, #475, #476, #478
flutter_build_mode(#442): New MCP tool that detects the Flutter build mode (debug / profile / release) of the running app and reports which opensafari tools are usable in that mode. Use it whenflutter_connectfails to distinguish between a release build (VM Service disabled by design) and a configuration issue. Returns acapabilitiesmap plus afallback_toolslist for release builds.flutter_toggle_debug_paint(#437): New MCP tool that flips Flutter's debug paint overlays (size,baseline,repaint_rainbow) andtime_dilation, plus anall_offreset mode. Backed byext.flutter.debugPaint/ext.flutter.debugPaintBaselinesEnabled/ext.flutter.repaintRainbow/ext.flutter.timeDilation. Useful for diagnosing overflow, padding, and repaint issues viaapp_screenshot_native.flutter_list_service_extensions(#441): Enumerates every VM Service extension registered by the running Flutter app — including third-party ones (ext.riverpod.*,ext.isar.*, BLoC observers) — with an optionalprefixfilter. Groups results by namespace for easy LLM consumption.flutter_call_service_extension(#441): Generic invoker for any service extension. Auto-injectsisolateId, enforces anext.prefix, validatesargsis an object, audit-logs every call to stderr. Covers Riverpod / BLoC / Isar / etc. without shipping per-library wrappers (Option B from the issue).flutter_evaluate(#434): New MCP tool that evaluates arbitrary Dart expressions against a running Flutter app's main isolate via the VM Serviceevaluate/evaluateInFrameRPCs. Default scope is the main isolate's root library;scope="frame"withframe_indextargets a paused stack frame (future-compatible with breakpoint support in #435). Results are normalised into a compact shape — primitives returnvalueAsString, composites expose 1-depthfields. Debug/profile builds only.FlutterVMClient.evaluate/evaluateInFrame: New public VM-client helpers that auto-resolve the root library target and surface typed errors (NO_ISOLATE,NO_ROOT_LIB).flutter_root_widget(#436): Dumps the running Flutter app's widget summary tree viaext.flutter.inspector.getRootWidgetSummaryTreeWithPreviews. Each node includestype,description, andcreationLocation(file:line:column) so callers can jump straight to the source.flutter_inspect_selection(#436): Returns the currently selected widget viaext.flutter.inspector.getSelectedSummaryWidget, with an optionalshowflag that toggles the in-app inspector overlay (ext.flutter.inspector.show) to arm coordinate-based selection. Empty selection returnsstatus: "empty"with a usage hint.FlutterVMClient.getRootWidgetSummaryTree/getSelectedWidget/setInspectorShow: New VM-client helpers wrapping the Flutter Inspector service extensions.flutter_cpu_profile(#439): Samples the Dart VM CPU profiler viagetCpuSamplesfor a configurable window (max 120s) and returns a top-N list of{function, self_us, total_us, samples}. PureaggregateCpuSampleshelper exported for testing.flutter_timeline_capture(#439): Enables VM timeline streams (default["Dart", "GC", "Embedder"]), waits a window, fetchesgetVMTimeline, and writes Chrome Trace Event JSON loadable inchrome://tracingor Perfetto.flutter_track_rebuilds(#438): Drives the Flutter dirty-widget rebuild tracker.start/report/stopactions, optionalduration_msauto-stop, capped at 10,000 events per tracker.flutter_allocation_profile(#440): Per-class allocation profile viagetAllocationProfile. Supportsgc_beforeanddiff_against_previousfor the standard leak-hunt pattern (baseline → action → diff).flutter_heap_snapshot(#440): Full Dart heap snapshot viarequestHeapSnapshot, written as binary importable by Flutter DevTools' Memory tab. Configurabletimeout_ms(default 60s, max 10min).- Breakpoint / step debugging (#435): Five new MCP tools that drive the Dart VM Service debugger end-to-end.
flutter_set_breakpoint({ script_uri, line, column? })— wrapsaddBreakpointWithScriptUriflutter_remove_breakpoint({ breakpoint_id })— wrapsremoveBreakpointflutter_resume({ mode: "continue" | "step_into" | "step_over" | "step_out" })— wrapsresumewith the matchingsteptokenflutter_get_stack({ limit? })— wrapsgetStackwith a compact per-frame summary (function,location: {script_uri, line},vars)flutter_wait_for_pause({ timeout_ms?, poll_interval_ms? })— polls for pause state, mirroringapp_wait_for; returns{timeout: true}on timeout
- Per-device
BreakpointManagerlazily subscribes to theDebugstream and tracks pause state + active breakpoints. Pure helpersresumeModeToStep,summariseFrame,_resetBreakpointManagersexported for testability.
- Breakpoint manager (#435): Cleans listeners on disconnect and detects VM reconnect to avoid stale state.
- Memory profiler (#440): LRU cap on
previousSnapshotsprevents unbounded memory growth;forgetAllocationHistoryexposed. - Track rebuilds (#438): Rolls back listener registration if
track_rebuilds startfails mid-setup. - Track rebuilds event name (#438): Fixed filter to match
Flutter.RebuiltWidgets(past tense, the actual event name Flutter emits fromwidget_inspector.dart:2538) instead ofFlutter.RebuildWidgets. Without this fix, no rebuild events were captured in live apps. - CPU profiler (#439): Resets timeline flags on capture failure to avoid leaving streams enabled.
- Widget inspector (#436): Clamps
max_depthand adds cycle guard tosummariseNode. - Evaluate (#434): Security docstring, audit log, Null handling, whitespace guard.
- Service extensions (#441): Caller cannot silently retarget
isolateId. - Debug paint (#437):
all_offtolerates partial failure and caps dilation. - Build mode (#442): Reports
'unknown'when URL discovered without connect. - Lint (#452): Fixed 12 pre-existing lint errors blocking CI on feature branches.
- Default-deny AppleScript/CGEvent input backend (#405): The focus-stealing
AppleScriptInputBackendis no longer instantiated automatically on Xcode 26+. When no headless input method is available,getInputBackend()throwsHeadlessInputUnavailableErrorwith actionable remediation guidance instead of silently moving the physical mouse cursor and activatingSimulator.app.- To re-enable the legacy fallback, set
OPENSAFARI_ALLOW_FOCUS_INPUT=1in the environment. - All affected tools (
app_tap,app_swipe_native,app_scroll_native,app_double_tap,app_type_text,app_key_input) surface the error as a structured MCP tool error. - Tool results now include a
backendfield (simctl/webkit/applescript) for audit/observability.
- To re-enable the legacy fallback, set
- WebKit reconnect retry: When a WebKit client exists but reports disconnected,
getInputBackend()attempts a one-shot reconnect before falling through, reducing false positives from transient proxy/tab drops. HeadlessInputUnavailableErrorclass with structured fields (deviceId,reason,remediation[]) exported from the public barrel for typed error handling by MCP clients.
- Flutter app QA automation: 12 new MCP tools for automating and testing Flutter apps on iOS Simulator, including Dart VM Service bridge, widget tree inspection, hot reload, and network traffic capture.
- Semantic element targeting:
app_tap_element,app_wait_for,app_assert_element— interact with UI elements by label/identifier instead of fragile x,y coordinates. - Flutter QA detectors: Automated checks for tap target sizes (
qa_flutter_touch_targets), accessibility coverage (qa_flutter_semantics), and dark mode rendering (qa_flutter_dark_mode). - Flutter network monitoring: HTTP proxy-based traffic capture for any app including Flutter.
- NativeInputBackend abstraction (
native-input-backend.ts): Input backend layer with automatic Xcode version detection. ProvidesInputBackendinterface (tap, swipe, typeText, keypress, sendKey) with two implementations:SimctlInputBackend: Usesxcrun simctl io inputcommands (Xcode 15–16)AppleScriptInputBackend: Usesosascript+ Swift CGEvent for input (Xcode 26+)
- Auto-detection: Probes
simctl io inputon first use and automatically falls back to AppleScript/CGEvent when unavailable. Result is cached for process lifetime. - HID-to-AppleScript key mapping: Translates USB HID key codes to macOS virtual key codes for the AppleScript backend, supporting Return, Escape, Tab, Space, arrow keys, Backspace, and Home.
- Native app tool surface docs (
docs/native-app-tool-surface.md): Complete reference for all 31+ native app automation tools. - CI integration examples (
docs/ci-integration.md): Expanded with native screenshot, log export, assertion, and hybrid context switching workflow examples.
- Xcode 26 compatibility: All 8 native interaction tools (
app_tap,app_double_tap,app_type_text,app_swipe_native,app_key_input,app_scroll_native,app_alert_handle,app_dismiss_keyboard) now work on Xcode 26.4 wheresimctl io inputsubcommand was removed. - app_alert_handle simplified: Replaced dual simctl/AppleScript fallback code with unified
InputBackend.sendKey()delegation, reducing code by 60 lines. - app_dismiss_keyboard unified: Migrated from direct simctl calls to InputBackend, ensuring consistent behavior across Xcode versions.
- All native interaction tools now use
getInputBackend()instead of directSimctlExecutor.exec()calls. This is a transparent change — tools behave identically on Xcode versions that supportsimctl io input.
- Native app screenshot capture (
app_screenshot_native): Full simulator screen capture with deterministic status bar masking for diffable CI screenshots. Supports PNG/JPEG output with base64 encoding. - Device log export (
app_logs): Structured JSON log export from simulator using NSPredicate filtering. Supports filtering by bundle ID, log level (default/info/debug/error/fault), time range, and text search. - Native assertions (
app_assert): CI-friendly structured assertion tool with 5 assertion types (app_running,element_exists,element_visible,screen_contains_text,text_matches). Returns JSON results with pass/fail, duration, and timestamp for pipeline integration. - Hybrid context switching (
app_webview_connect,set_active_context): Discover WebView targets inside running native apps and switch automation context between Safari and embedded WebViews. Target classification distinguishes Safari pages from WebView content by URL scheme. - CI documentation for native tools: Expanded
docs/ci-integration.mdwith examples for native screenshots, log export, structured assertions, hybrid context switching, and artifact upload workflows.
- Native assertion
element_exists/element_visiblerequire Xcode 14+ withsimctl io enumeratesupport. - Hybrid context switching relies on ios-webkit-debug-proxy target discovery; apps must have Web Inspector enabled for WebView targets to appear.
app_screenshot_nativecaptures the full simulator display, not individual app windows.- Screen recording (
app_record_video) is available but marked as experimental.
- iPhone SE device presets: Added
iphone-se-1,iphone-se-2, andiphone-se-3presets covering all three iPhone SE generations (320x568, 375x667 @2x/3x) for small-screen QA testing. - HTML report screenshots: QA audit HTML reports now embed original and annotated page screenshots directly in the report via base64
<img>tags, providing visual context alongside detector results. - Device comparison HTML layout: Cross-viewport comparison tool now generates a structured HTML report with per-device cards showing device name, viewport dimensions, and screenshot — replacing the previous inline HTML string construction.
- Zombie cleanup scoping & configuration: Zombie device cleanup now only targets devices registered by OpenSafari processes (via a PID-based device registry), preventing accidental shutdown of unrelated simulators. New environment variables (
OPENSAFARI_ZOMBIE_CLEANUP_ENABLED,OPENSAFARI_ZOMBIE_CLEANUP_INTERVAL_MS,OPENSAFARI_ZOMBIE_CLEANUP_MAX_AGE_MS) allow fine-grained control over cleanup behavior. - CI/CD integration guide: New
docs/ci-integration.mdwith detailed instructions for runningqa_full_auditin GitHub Actions, GitLab CI, and Jenkins pipelines, including artifact collection and threshold gating. - API reference documentation: Expanded
docs/api-reference.mdwithqa_full_auditformat specification. - E2E validation fixtures: Added
buggy-page.html,clean-page.html, andvalidation-report.jsontest fixtures for QA detector end-to-end validation. - New test suites: Added comprehensive tests for zombie cleanup cross-session behavior (#263), proxy initialization timing (#264), socket finder verification (#265), HTML report generation (#211), and E2E gesture verification.
- WebKit error capture protocol: Replaced Chrome-specific
Runtime.exceptionThrownwith WebKit-nativeConsole.messageAddedfor JavaScript error capture, fixingonErrorhandler that was silently failing on real Safari (#200). - TOCTOU race in zombie cleanup: Eliminated time-of-check-to-time-of-use race condition by introducing a single-lock registry partition (
getOrphanedAndLiveDeviceIds) that atomically reads both orphaned and live device sets in one operation. - Accessibility detector regex: Reverted incorrect double-escaping in accessibility detector template literal regex and removed unused imports (#254).
- Cross-viewport breakpoint logic: Removed redundant breakpoint condition in
CrossViewportCapturethat could cause duplicate captures at boundary widths. - Lint and import cleanup: Fixed unused
AnnotationResultimport inaudit.ts, duplicate variable declaration in auth integration test, unused imports in E2E gesture test, and various lint errors in zombie cleanup tests. - Test infrastructure: Corrected test import paths and added
NaNguard for environment variable parsing to prevent CI test breakage.
assert_all_devicestool simplified: Removed theincludeScreenshotparameter and per-device screenshot embedding fromassert_all_devicesresults, reducing response payload size and eliminating the unusedscreenshotdestructuring that caused the CI lint failure.- Cross-viewport compare refactored: Moved HTML generation from inline string construction in
cross-viewport-compare.tsto a dedicatedgenerateComparisonHtml()function inreport-html.ts, improving maintainability and enabling reuse. - E2E gesture test relocated: Moved E2E gesture verification test from unit to integration directory to reflect its actual test scope.
- Initial release of OpenSafari MCP server
- 41+ MCP tools for iOS Safari automation
- SimulatorManager: boot, shutdown, screenshot, appearance, rotation
- WebKitClient: navigate, evaluate, screenshot via WebKit Remote Debugging Protocol
- 13 iOS QA detectors: auto-zoom, touch targets, safe area, keyboard overlap, etc.
- qa_full_audit with scoring and regression detection
- Multi-simulator parallel testing with batch operations
- Cross-viewport visual comparison with Claude Vision format
- Login persistence via cookie export/import
- CLI: serve, auth, doctor, devices commands
- Self-healing: crash recovery, resource monitoring, graceful shutdown