# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]


## [0.7.2] - 2026-06-12

**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.

### Release workflow

- **Tag pushes validate without publishing** — the `Publish` GitHub Actions workflow now runs the release gate (`npm ci`, lint, CI tests, build, production dependency audit, and dist verification) on `v*` tags but no longer runs `npm whoami` or `npm 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.

### Runtime changes

- No runtime code changes beyond the 0.7.1 stability, WebKit resilience, proxy recovery, long-session hygiene, tool-tier, and AX diagnostic fixes.

### Validation

- Local validation for the 0.7.1 release line passed before this patch: `npm test -- --runInBand` (216 suites / 2951 tests), `npm run build`, and `npm run audit:prod`.
- The 0.7.2 tag validation workflow is expected to complete without invoking npm publish.

## [0.7.1] - 2026-06-12

**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.

### MCP tool catalog and startup weight

- **Default Tier 1 tool surface (#849)** — `MCPServer` now starts at Tier 1 by default instead of Tier 2, shrinking the default `tools/list` response from the broad advanced/native/Flutter catalog to the compact core automation surface. Users who need the previous broad surface can set `OPENSAFARI_TOOL_TIER=2` or start with `--all-tools`.
- **Safe tier fallback (#849)** — tools missing an explicit `TOOL_TIERS` entry 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.ts` enforces explicit tier assignment for every registered tool.
- **Documentation alignment (#849)** — README tier tables and programmatic `createServer()` examples now document the Tier 1 default and `allTools` opt-in path.

### WebKit and proxy resilience

- **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 `lastUrl` by default, avoiding surprise page-state loss after a transient WebKit interruption. `renavigateOnReconnect` remains 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_proxy` restarts (#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.

### MCP process reliability

- **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. `uncaughtException` remains 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.

### Long-session observability hygiene

- **Bounded HAR capture (#853)** — `HarCollector` now caps stored entries (`maxEntries`, default 2000), reports dropped requests, and skips `Network.getResponseBody` when encoded size already exceeds `maxBodySize`. 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 -> get` retrieval 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.

### AX diagnostics

- **Opt-in walker topology on AX failures (#843)** — setting `OPENSAFARI_AX_DEBUG_ON_FAILURE=1` re-runs failed `dump`/`query` AX reads once with `--debug` and attaches parsed `walker_*` topology to `AccessibilityBridgeError`. The success path is unchanged, and failed debug recapture never masks the original error.

### Validation

- 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 -- --runInBand` passed (216 suites / 2951 tests), and `npm run build` completed successfully.

## [0.7.0] - 2026-06-04

**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.

### Added

- **Stateful mobile semantic QA runtime foundations (#822, #823, #824, #827, #828, #830)** — introduces durable QA state contracts and the runtime that drives them:
  - `AppSessionState` and `ScreenStateSnapshot` contracts (`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 across `app_goto_screen`, `app_pop_until`, `app_dismiss_overlay`, `app_tap_element`, `app_type_element`, and `app_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_connect` build-mode & capability disclosure (#831, #836)** — `flutter_connect` now reports the app's build mode (debug / profile / release) and which VM-service capabilities are available, and performs a probe-backed `flutter_evaluate` so 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_semantics` gains an automation-readiness audit that flags low-quality / ambiguous semantics selectors before they cause flaky automation.

### Changed

- **Searched-tree diagnostics on not-found errors (#834, #837, #840)** — when `app_tap_element` (and related element-targeting tools) cannot find a target, and when `app_wait_for` times 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 heavier `debug_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_element` now 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 `recoverable` flag and `suggestion` instead of the generic `APP_STATE_UNKNOWN` fallback. "No device specified and no active device" (28 sites across `src/tools/**` and the canonical `resolveDeviceId` in `src/native/accessibility.ts`) now throws `StructuredErrorException.fromCode(ErrorCode.DEVICE_NOT_BOOTED, …)`, and "Not connected to Flutter VM Service. Run flutter_connect first." (12 sites) now throws `StructuredErrorException.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 `Flutter` job now gates the Tier-0 thesis (#820)** — the Flutter VM-Service live suite was entirely `continue-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()` selects `FlutterVMInputBackend`, and `swipe` dispatches gesture-arena events) run as a **blocking** step, while only the `tap`/`typeText` assertions — 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 in `docs/headless-architecture.md`.

### Fixed
- **Publish workflow honest-green (#817, #821)** — the `Publish` workflow has reported `failure` on every release since v0.6.0 even though each version reached npm, because `npm publish --provenance` returns a misleading `E404 … not in this registry` when the version already exists or the `NPM_TOKEN` lacks publish rights. `publish.yml` now (a) skips publishing with a success status when the exact `name@version` is already on the registry (idempotent re-runs / re-pushed tags), (b) runs an `npm whoami` auth preflight that fails with an actionable message instead of the masked E404, and (c) fans a failure out through the existing `_sentinel-notify` reusable workflow so a broken release pipeline cannot sit red unnoticed. NOTE: a genuinely-new version still requires a valid publish-scoped `NPM_TOKEN`.

### Docs

- **Flutter native debugging setup in getting-started (#832, #835)** — `docs/getting-started.md` now covers how to bring up native Flutter debugging end-to-end, and `docs/flutter-vm-attach.md` documents deterministic fast VM attach for debug/profile QA sessions.
- **Mobile semantic QA guide (#828)** — `docs/mobile-semantic-qa.md` describes 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.md` to prevent the stranded-PR / stacked-branch topology that stalled the 0.6.3 cut.

### Tests / CI

- Full suite green: **211 suites / 2917 tests** pass locally in ~28 seconds on the merged release branch; lint clean (0 errors).
- The Headless Smoke `Flutter` job 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.

## [0.6.3] - 2026-05-29

**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.

### Fixed

- **Headless Smoke Tests / `Simulator.app is NOT brought to the foreground by simhid taps`** — the test wrapper now allows the `dist/sim-hid-bridge` invocation up to 60 seconds. The previous 10 second budget consistently truncated the wrapper mid-`probeContext` (which itself spawns `dist/ax-bridge` with a 15 second timeout after a 1.2 second settle), surfacing as `Command failed: ... tap 200 500` with no useful stderr. The new budget covers the wrapper's own 15 second `execNative` + 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 UTC `Headless Smoke Tests` run on `main` without weakening any product assertion.
- **Headless Smoke Tests / `Flutter — headless VM-Service input` job** — the scheduled run on `main` was being cancelled at its 35-minute timeout every day because the `flutter-vm-input.live.test.ts` suite (a) failed its `tap`/`typeText` assertions, which read app state back through the native `ax-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 sibling `webview-smoke` job with `continue-on-error: true` for the ax-bridge limitation, runs jest with `--forceExit`, and the suite tears down its VM Service client in `afterAll`. This unblocks the daily 06:00 UTC `Headless Smoke Tests` run without weakening the headless Tier-0 routing / swipe-dispatch assertions, which still execute.

### Added

- **`debug_bundle_collect` MCP 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 in `docs/debug-bundle.md`.
- **Automatic `debug_bundle_collect` attachment on recoverable failures (#798 PR2)** — high-level semantic tools (starting with `app_pop_until`) now accept an optional `collectDebugBundleOnFailure` parameter. When set, recoverable failures automatically attach a compact bundle reference to the structured error response so MCP clients no longer have to discover and call `debug_bundle_collect` manually after every failed semantic action.

### Changed

- **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 with `respondWithStructuredError(code, message, extra?)` rather than ad-hoc `Error: ...` text or one-off `{ error }` payloads. MCP clients can read `error`, `message`, `recoverable`, and `suggestion` consistently 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-hoc `Error: ...` envelopes in this surface.
- **`app_pop_until` accepts `collectDebugBundleOnFailure`** as 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.

### Tests / CI

- 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 (`tryCreateSimulatorKitHIDBackend` resolves to simhid, Simulator.app stays in background, no AppleScript fallback loaded) is unchanged — only the wrapper-invocation budget moved.

### Notes

- 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 `develop` by a stacked-PR topology and merged into `develop` as the single integration PR #814.

## [0.6.2] - 2026-05-27

**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.

### Fixed

- **Main CI / SimulatorKit HID Sentinel macos-15 timeout** — fixes the failing main check where `tests/ci/sim-hid-sentinel.test.ts` timed 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 native `dist/sim-hid-bridge-native` binary over Swift interpreter cold compilation, and keeps hard failures tied to real private-API break evidence (`exit 78` plus structured `SIMULATORKIT_MISSING`, `CORESIMULATOR_MISSING`, `HID_CLIENT_FAILED`, or `HID_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 inspect `error`, `message`, `recoverable`, and `suggestion` consistently.
- **Canonical structured-error fields are authoritative** — `respondWithStructuredError` / `StructuredErrorException.toMcpResponse()` no longer allow tool-specific `extra` diagnostics to override the canonical `error`, `message`, `recoverable`, or `suggestion` fields. This prevents accidental reintroduction of ad-hoc error shapes while still preserving extra context fields.
- **Overlay dismissal postcondition proof** — `app_dismiss_overlay` can now verify a caller-supplied `waitForGone` AX postcondition before returning success. Failed verification reports structured `OVERLAY_DISMISS_FAILED` with `mode`, `deviceId`, `waitForGone`, and verification details instead of implying success from gesture dispatch alone.
- **`app_pop_until` route verification false negatives** — route postconditions now use the same `_ModalScopeStatus` element-tree evidence strategy as `flutter_get_route` instead of `ModalRoute.of(rootElement)`, avoiding false negatives when the Flutter root element is not itself inside the current route subtree.
- **Native `app_pop_until` route-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`, or `role`) 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.

### Added

- **Expanded structured error catalog (#797 PR1)** — adds stable `ErrorCode` entries 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`), and `app_pop_until` outcomes (`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 from `src/errors` for subsequent tool migrations.
- **`app_pop_until` postcondition contract and attempt history (#801 PR1)** — responses now include `strategy`, bounded `attempts[]`, and `postcondition` evidence while preserving the pre-existing `ok`, `status`, `popped`, and `target` fields. Optional postconditions support AX queries (`identifier`, `label`, `text`, `role`) and Flutter route names (`route`) with configurable `timeoutMs` / `intervalMs`.
- **`app_pop_until` native fallback ladder (#801 PR2)** — when Flutter VM service is unavailable, `app_pop_until` can 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_until` vs. `app_tap_element`, route vs. AX postconditions, and native fallback constraints.

### Changed

- **`app_pop_until` keeps Flutter VM as the preferred strategy** when connected and not explicitly bypassed with `forceFallback`; native fallback is only used for non-VM/release/native contexts or explicit fallback testing.
- **Native fallback success is postcondition-driven** for `until: "first"` and `until: "route"`; dispatch success alone is insufficient. For `until: "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.

### Tests / CI

- Added focused unit coverage for structured-error catalog expansion, canonical envelope clobber prevention, MCP server structured catch-all behavior, overlay postcondition verification, `app_pop_until` postcondition 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`, and `npm run build` pass on the release branch.

### Detailed diff since 0.6.1

- `tests/ci/sim-hid-sentinel.test.ts`: raises the slow bridge timeout from 45s to 90s and searches `dist/sim-hid-bridge-native` before Swift/interpreter fallbacks.
- `src/mcp-server.ts`, `tests/unit/mcp-server.test.ts`: preserves `StructuredErrorException` metadata 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`: adds `waitForGone` verification 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.

### Migration notes

- No tool names are removed and existing `app_pop_until` VM use remains compatible. Consumers may observe additional additive fields (`strategy`, `attempts`, `postcondition`) and should ignore unknown fields if not needed.
- Native `app_pop_until` for `until: "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`, and `suggestion` are reserved and remain catalog-controlled.

## [0.6.1] - 2026-05-17

**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_element` pasteboard backend now works on `AXSecureTextField` (password) elements (#760, #761)**. The readback contract introduced for #639 PR C compared `endsWith`/`includes` against 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 as `PASTE_NOT_APPLIED`. The same OS-mask divergence on the simhid path was escalating into `TEXT_INPUT_DROPPED` / `TEXT_INPUT_LAYOUT_MISMATCH` with `isError: 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 documented `verify: false` parameter symmetrically with the simhid path (it was previously a silent no-op for pasteboard).

**Headline change (rolled forward from 0.6.0)**:

- **`ax-bridge` recursive 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 typed `DEVICE_CONTENT_ROOT_EMPTY` error with exit code 1 instead of silently falling back to the bare `AXWindow`. 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 both `src/native/ax-bridge.swift` and the TypeScript reference scorer `src/native/ax-bridge-content-root.ts`; the two implementations are kept in lock-step by 6 fixture unit tests in `tests/unit/ax-bridge-content-root.test.ts`.

### Fixed

- **`app_type_element` AXSecureTextField paste verification (#760, #761)** — see headline above. `assertPasteApplied` accepts an optional `{ role, traits }` descriptor and returns silently when the descriptor signals a secure text field; `typeViaPasteboard` forwards the inspected node's role/traits into the assert and adds `secureField?: true` to `PasteboardTypeResult`. The tool layer echoes `secureField: true` in the success response so callers can distinguish "no readback because secure field" from "no readback because verify opted out". `verifyTypedText` on 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 in `tests/unit/pasteboard-input.test.ts`, 2 new cases in `tests/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.ts` previously claimed that a swipe failure under `OPENSAFARI_ENABLE_POINTERSERVICE=1` would "surface via the tier chain when we bubble back up". That is not the runtime behaviour: `getInputBackend` caches `PointerServiceInputBackend` as the selected backend, so `swipe()` hard-errors with `HeadlessInputUnavailableError` on 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_boot` now 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, guard `appendChar` before keyboard dispatch.
- **`fix(simulator)`: trailing-bracket strip + bundleId regex + simctl rotate route (#708 series)**. `launchctl` label normalization now strips all trailing bracket groups; `bundleId` regex tightened and not-installed detection centralized; `simctl rotate` routed through `deps.simctl.exec`; shutdown stays best-effort after nuclear erase.
- **`fix(webkit)`**: direct host.emit in EventBridge transport forwarding; `clearCookies` effective on `document.cookie` fallback; throwing protocol-event handler routed through `transport:error`; circular dep resolved, unimplemented `timeoutMs` removed, viewport query unified; RFC 6265 domain matching for `getCookies` filter; `enabledDomainsPerTarget` cleanup on RPC failure.

### Added

- **`TRANSITIONAL_STATE_TIMEOUT` classification + `--max-settle-retries` flag for `dist/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 returns `FOREGROUND_CONTEXT_UNAVAILABLE` while `<b>` is in `runningApps`, the wrapper performs one bounded re-probe (another `settleMs` window) and promotes to `TRANSITIONAL_STATE_TIMEOUT` if the tree is still empty. Capped by `--max-settle-retries <0|1|2|3>` (default `1`); set `0` to restore the pre-issue single-probe behaviour byte-for-byte. The surface classifier in `src/tools/raw-mobile-context.ts` stays 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 in `tests/unit/coord-regression.test.ts` (#722).
- **`feat(ax-bridge)`: `--debug` flag 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-matcher` gains an extension seam for app-specific labels (#639 follow-up).

### Refactored — major decomposition batch

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). `WebKitClient` is 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 `simctl` JSON 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 enforce `no-explicit-any` via lint override (#710 b).
- **Protocol typing (#710 a/b)**: typed DTOs + fixture builders for the WebKit RDP boundary (a); typed RDP guards with `console.type` fallback restored.

### Performance

- **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 `audit` and `serve` flows.
- **WebKit fast paths (#702 a/b, #725)**: new `evaluateValue` helper, 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 bootstatus` probes 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.

### Security

- **HTTP MCP transport hardening (#714)** plus follow-ups: tighter `/mcp` auth + 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_geolocation` gated 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.ts` bounds 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.

### CI / DX

- Lint enforces `no-explicit-any` on 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.

### Migration notes

- **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_element` response shape gains an optional `secureField: true` field** on the pasteboard backend when the focused element is an `AXSecureTextField`. Existing callers that ignore unknown response fields are unaffected.
- **The `verify: false` parameter on `app_type_element` is now honoured on both backends** (`auto`/`simhid` and `pasteboard`). 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.

## [0.6.0] - 2026-04-20

**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`.

### Fixed — `ax-bridge` recursive scored content-root search (#40, follow-up to #4)

- **`src/native/ax-bridge.swift`**: `findDeviceContentInWindow` replaced with `findDeviceContentRecursively`. Scoring is deterministic and integer-based so fixtures can be asserted exactly:
  - `AXGroup`/`AXScrollArea` with `iOSContentGroup` trait → +10
  - frame fits expected device-content rect (±15pt per edge) → +8
  - each app-semantics descendant (`AXTextField`, `AXStaticText`, `AXButton` with 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>"`). `AXMenuBar` and `AXWindow` are 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 bare `AXWindow`. The wrapper at `cli/ax-bridge.ts` forwards error JSON untouched.
- **Reproduction closed** (Xcode 26.4 / iOS 26.4): `node dist/ax-bridge query --device <udid> --role AXTextField` on a booted simulator with no foreground app now fails fast with `DEVICE_CONTENT_ROOT_EMPTY` (exit 1) instead of returning `{"total":0,"matches":[]}` with exit code 0.

### Added — TS reference scorer and 6-fixture unit suite

- **`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) empty `iOSContentGroup` between chrome children → `DEVICE_CONTENT_ROOT_EMPTY`, (b) populated Flutter tree with `iOSContentGroup` + ≥ 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 with `AXTable` at top level (no `iOSContentGroup` trait).

### Unreleased items carried forward into 0.6.0

- **`dist/sim-hid-bridge` wrapper CLI documented** in [`docs/headless-architecture.md`](docs/headless-architecture.md#raw-simhid-cli-reference), including `--settle-ms`, response-shape table, and classification table. Adds a cross-reference from [`docs/api-reference.md`](docs/api-reference.md) so MCP consumers can jump to the raw-CLI contract when scripting without the MCP server. Closes #45.
- **`app_tap_element` coordinate fallback now preserves the verified-interaction contract.** When `ax-press` cannot 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 carries `verified: false` / `effect: "verification_unavailable"` when proof is unavailable, or a typed `TAP_NO_EFFECT` error when the post-tap AX tree stays unchanged. The stricter contract matches `app_tap` and closes the false-positive-success gap for bundle-scoped native taps.
- **Raw mobile-bridge context diagnostics and expect-bundle guards.** `dist/ax-bridge` now exposes `context --device <udid> [--expect-bundle <bundle>] [--require-match true]`, returning machine-readable foreground classifications and expected-bundle matches for downstream QA. `dist/sim-hid-bridge` now ships as a wrapper around the native bridge and enriches `tap` / `swipe` JSON with post-input `classification`, `verified`, `frontmost`, and `expectedBundleMatched`, plus a matching `context` command. Raw HID commands can now fail fast with `--require-match true` instead of looking like a clean success after the simulator drifts to SpringBoard or chrome.

## [0.5.0] - 2026-04-17

**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_POINTERSERVICE` backend has a stability track.
- **ko-KR `app_alert_handle` live 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** (`--debug` build, split device profile path) (#596).
- **WebView fixture AX identifiers + bundle-ID alignment** (#593).
- **`app_webview_connect` documented** in `docs/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).

### Breaking — `simctl storekit` tool surface removed (#588, #623)

- **Removed tools**: `app_storekit_configure`, `app_storekit_test_session`, `app_storekit_receipt`. Related `src/tools/app-storekit-*.ts` and `src/native/simctl-storekit.ts` deleted; `tests/unit/app-storekit.test.ts` removed; prior `docs/storekit-automation.md` and api-reference entries dropped.
- **Why**: the `simctl storekit` subcommands 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 using `app_launch` + `app_deeplink` + `app_tap_element` + `app_alert_handle` against the localized StoreKit sheet. The recipe is pinned to `opensafari-mcp@0.4.9` so 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.md` for 0.5.0+.

### Added — WebView-in-Flutter HTTPS bundle_match live harness (#592)

- **New live integration suite `tests/integration/webview-flutter-https-bundleid.live.test.ts`** that boots a Flutter app embedding a real WebView, loads an HTTPS origin, and asserts `app_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 — the `flutter_inappwebview`-style bridge surfaces WebKit's page at a different debuggee index and the matching logic has to survive that.
- **`app_webview_connect` documented** in [`docs/api-reference.md`](docs/api-reference.md#app_webview_connect) (#618).
- **README Headless Capabilities row for WebView-in-Native** now reads *partial* with a pointer to the exact constraints ([`README.md`](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).

### Added — PointerService live coverage + sentinel (#590 Phase 1)

- **Live suite `tests/integration/pointer-service.live.test.ts`** covers `app_tap`/`app_swipe` through the experimental PointerService backend (enabled via `OPENSAFARI_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.ts` now probes PointerService symbols (`SimPointerClient_create`, `SimPointerClient_postEvent`) alongside the existing SimulatorKit probes. `.github/workflows/sim-hid-sentinel.yml` runs it daily. When a macOS / Xcode update drops one of the PointerService symbols, we find out in ≤24h.

### Added — Alert handle live suites + sentinel probe (#589)

- **2-button alert live suite (#622)**: `tests/integration/issue-589-alert-handle-2button.live.test.ts` drives a ko-KR `UIAlertController` with two localized buttons and exercises `app_alert_handle` via `action: "accept"`, `action: "dismiss"`, and `buttonLabels: ["구입", "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.ts` covers 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.ts` gains 125 new lines of ko-KR label-match assertions that guard against locale-loader regressions in Xcode updates.

### Added — Flutter + IAP (ko-KR) CI recipe (#597)

- **New end-to-end recipe at [`docs/recipes/flutter-iap-ko-kr.md`](docs/recipes/flutter-iap-ko-kr.md)** covering 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.md`](docs/ci-recipes.md#specialized-recipes)** under a new "Specialized Recipes" section. Pinned to `opensafari-mcp@0.4.9` so 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 (see `docs/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` (with `buttonLabels: ["구입", "Buy"]` from #589), `app_storekit_test_session`, and `app_storekit_receipt` (from #588).
- **ko-KR gotchas documented.** Set `AppleLocale=ko_KR` before 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 `--profile` Flutter builds to keep the VM Service online, and poll for the sandbox receipt with a short backoff since iOS occasionally flushes it lazily.

### Added — AX-based StoreKit QA pattern (#626, #588)

- **Re-added `docs/storekit-automation.md`** with a minimal AX-only QA pattern: drive the localized StoreKit sheet via `app_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.md` cross-reference** points at the new pattern.

### Docs — Flutter simulator QA recipe fix (#596)

- **[`docs/ci-recipes.md`](docs/ci-recipes.md)** and **[`docs/flutter-inspector.md`](docs/flutter-inspector.md)** now instruct `flutter build ios --debug` (not `--release`) for simulator-hosted QA runs — the release AOT path pushes the Dart VM into the `vm-service-unavailable` state 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 `--profile` flag.
- **Minor `src/tools/flutter-vm-input-backend.ts` comment update** aligns the inline remediation text with the corrected recipe paths.

### Changed — Fork-friendly sentinel alert destination (#599)

- **Private API Sentinel alerts are now parameterized.** `.github/workflows/private-api-sentinel.yml` no longer hard-codes the `#opensafari-sentinels` Slack channel. Forks and downstream orgs configure `secrets.SENTINEL_WEBHOOK_URL` (Slack / Discord / Mattermost Incoming Webhook) plus optional `vars.SENTINEL_CHANNEL` to redirect alerts without patching the workflow.
- **Reusable notify workflow.** `.github/workflows/_sentinel-notify.yml` switches payload shape on webhook host — `hooks.slack.com` receives 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_URL` is 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. See `docs/ci-integration.md` → "Private API Sentinel alerting" for setup and the fork checklist.

### Changed — iOS 26 investigation synthesis finalized (#591, #557)

- **`docs/simhid-ios26-investigation.md` is 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.md` Headless 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.md` cross-reference updated** — drops the "in review" framing and points at the new stability-commitments anchor.

### Docs — Private-API deployment-scope guidance (#601, #610)

- **`docs/private-apis.md` gains 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's `idb` takes.
- **License-interaction note** clarifies that the MIT grant on OpenSafari's source does not sublicense Apple's `SimulatorKit` / `CoreSimulator` frameworks — consumers remain bound by the Xcode / macOS license agreements for the loaded frameworks themselves.
- **One-time private-API warning updated.** `SimulatorKitHIDInputBackend` now prints `Where can I use this? macOS host / CI only — never bundle inside an iOS .ipa …` alongside the existing `docs/private-apis.md` pointer, with a `(see "Deployment scope")` anchor hint. Content asserted by `tests/unit/sim-hid-input-backend.test.ts` (3 new assertions tagged `Issue #601`).
- **No behavior change** — informational only. No runtime code paths altered.

### Changed — `_meta._telemetry` is on by default (#595)

- **Input tool responses now carry `_meta._telemetry` without 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 flip `OPENSAFARI_INPUT_TELEMETRY_META=1` after 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` (or `false`) 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.md` for a paste-able CI recipe.

### Added — Memory SLO infrastructure (#554)

- **Process-wide memory instrumentation.** `timedInput` now records `{rss_mb, heap_used_mb}` on every input-backend call. The telemetry rollup exposes per-backend `p50_rss_mb` / `p95_rss_mb` / `max_rss_mb` percentiles alongside the existing latency percentiles. Memory fields in tool responses are opt-in via `OPENSAFARI_TELEMETRY_INCLUDE_MEMORY=1`.
- **Per-cache memory budget documentation.** `docs/memory-budget.md` catalogues every module-level cache and singleton in `src/`, 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_MB` env var — when RSS crosses the cap, the telemetry sink emits a structured warning and `diagnose` reports `memory_status: "warn"`.
- **Enhanced `diagnose` memory block.** Now includes `rss_growth_mb_per_hour`, `soft_cap_mb`, and `notes` array for cache-budget violations.
- **60-minute soak test.** `tests/soak/long-session.soak.test.ts` round-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 via `v8.writeHeapSnapshot()` (no `--expose-gc` flag required) and, on any SLO miss, the test emits the snapshot paths plus the top-20 class growers for triage. Gated by `OPENSAFARI_RUN_SOAK=1`.
- **Nightly CI workflow.** `.github/workflows/memory-soak.yml` runs the soak test daily at 03:00 UTC. Seven consecutive failures auto-open a `memory-regression` issue.
- **Developer script.** `scripts/memory-inspect.ts` — one-shot 10-minute mixed-call session that prints a per-backend RSS/heap table for local triage.

### Upgrade Notes

- **If you import any `app_storekit_*` tool**: you will get a "tool not found" error on 0.5.0. Either pin to `opensafari-mcp@0.4.9` (supported via `docs/recipes/flutter-iap-ko-kr.md`), or migrate to the AX-based pattern in `docs/storekit-automation.md` (`app_tree` → `app_tap_element` → `app_alert_handle`).
- **If you parse MCP input-tool responses**: expect `_meta._telemetry` on every response by default now. Set `OPENSAFARI_INPUT_TELEMETRY_META=0` to restore 0.4.9 behavior.
- **If you operate a fork** with Private API Sentinel alerting enabled: set `secrets.SENTINEL_WEBHOOK_URL` and (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.

### Release metadata

- **Tag:** `v0.5.0`
- **Branch:** `main` (from `develop` — 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.

## [0.4.9] - 2026-04-15

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.

### Added — Tier 1.5 AX press, headless native tap on Xcode 26+ (#552)

- **`AccessibilityPressInputBackend` — Tier 1.5 headless element-targeted tap/focus.** `app_tap_element` and `app_type_element` now invoke `AXUIElementPerformAction(element, kAXPressAction)` through the existing `ax-bridge` Swift 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 and `Simulator.app` does 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_ACTIONABLE` and `PRESS_FAILED` are in-band with exit 0 for transparent fallback; bridge-level errors exit non-zero.
  - `app_tap_element` tries Tier 1.5 when `duration === 0` and the element has a path. Response carries `backend: 'ax-press'`, `_meta.headless: true`.
  - `app_type_element` uses AX press for tap-to-focus; typing flows through the selected input backend.
  - New `OPENSAFARI_DISABLE_AX_PRESS=1` env var to disable the Tier 1.5 path.
- **`InputBackendKind` gains the `'ax-press'` variant** for telemetry and `_meta.backendKind` consistency.

### Added — Flutter release / no-DDS routing polish (#553)

- **`FlutterVMClient.probeEvaluateCompile()`** — cheap evaluate probe (< 500 ms p95) that gates Tier-0 on compile capability, not just VM reachability. Release-mode Flutter apps and `simctl launch` without `flutter run` now fall through to lower tiers instead of surfacing a raw `code 113` error.
- **`FlutterVMInputBackendError.code`** is a structured union (`VM_NO_EVALUATE | DART_ERROR | UNKNOWN`) with actionable remediation messages.
- **WebSocket leak fix** — orphaned `FlutterVMClient` connections on negative probe results are now closed via `removeFlutterVMClient()` to prevent file descriptor leaks on release-mode apps.

### Added — iOS 26 investigation tooling (#491)

- **`sim-hid-bridge tap-digitizer` subcommand** (#491, #556): IOHIDEvent digitizer probe that synthesises `kIOHIDEventTypeDigitizer` and wraps with `IndigoHIDMessageForPointerEventFromHIDEventRef`. 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 from `docs/private-apis.md`.

### Changed

- **Headless SSOT reflects Tier 1.5.** `docs/headless-architecture.md` adds a Tier 1.5 routing-table row, backend details for `AccessibilityPressInputBackend`, updated "Practical impact on Xcode 26+" blockquote, scenario-matrix rows splitting native Xcode 26+ into element-targeted (headless) and coordinate-only (opt-in), and `OPENSAFARI_DISABLE_AX_PRESS` in the environment-variables table.

## [0.4.8] - 2026-04-15

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.

### Fixed — Xcode 26 native tap regression mitigation (Epic #491)

- **Disable SimHID tap/swipe routing on Xcode 26+** (#491, #537, #62034af3): `getInputBackend()` now skips the `SimulatorKitHIDInputBackend` for `tap`/`swipe` operations when the simulator's parent Xcode is 26.0 or newer. Apple's iOS 26.x Simulator runtime drops `IndigoHIDMessageForMouseNSEvent` handling in CoreSimulator's `SimDevice`, so `IOHIDEvent` injection 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 2 `simctl io input` (absent on Xcode 26), then Tier 3 AppleScript/CGEvent (focus-stealing, but functional). `HeadlessInputUnavailableError.reason` gains the new `'simhid-gated'` variant (#547) so callers can distinguish a cache hit that was intentionally skipped from a true unavailability.
- **`sim-hid-bridge` reports screen size in points, not pixels** (#491, f4368f4c): The SimulatorKit HID probe previously reported physical pixel bounds from `CoreSimulator`'s display service. All callers were dividing by `scale` again, 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.ts` no longer hard-codes the English string `"General"` when walking Settings. It reads `com.apple.Preferences` localization at runtime and falls back to the button's accessibility identifier, so the suite passes on non-English simulators and in locale-randomized CI.

### Added — iOS 26 investigation tooling (#491)

- **`sim-hid-bridge diag` subcommand** (#491, #551, a83fe57b): New read-only diagnostic command that reports SimulatorKit availability, device boot state, display bounds, scale, screen-size-in-points, and the resolved `hidProbeFn` symbol 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 via `diagnose` MCP tool.
- **`sim-hid-bridge tap-ps` subcommand** (#491, #555, 128e96fb): Experimental alternative tap implementation that drives Apple's Pointer Service (`CoreSimulator.framework`'s `SimPointerClient`) instead of `IOHIDEvent`. 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.ts` boots two simulators in parallel (one Flutter fixture, one native fixture), drives `app_tap`/`app_swipe`/`app_key_input` through each, and asserts `_meta.backendKind`, `_meta.headless`, and tap-landed verification via post-tap screenshot diff. Gated behind `OPENSAFARI_LIVE_SIMHID=1` so the default `npm test` stays 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 when `process.env.CI !== 'true'`.
- **Dual-boot routing tests** (#491, #549): `tests/unit/dual-boot-routing.test.ts` covers 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.ts` was 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.

### Added — Private API safety + sentinel CI (Epic #493)

- **Private API sentinel workflow** (#493, #541, #542, 70afa2b1): New `.github/workflows/private-api-sentinel.yml` runs daily at 07:00 UTC with six independent probes: `SimulatorKit.framework` reachable, `CoreSimulator.framework` reachable, `dlopen(SimulatorKit)` + `dlsym(SimulatorKitHIDInputBackend)`, `sim-hid-bridge diag` device-not-booted exit, `sim-hid-bridge diag` against a booted simulator, and `tap-ps` PointerService symbol probe. Alerts to the `#opensafari-sentinels` Slack 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.ts` is a TypeScript-level mirror of the workflow probes — six probes that run under `npm run test:sentinel` so local repros can validate the same gates the workflow enforces. Excluded from the default jest run (`tests/ci/` + `tests/sentinel/` both removed from default `testPathIgnorePatterns`-driven test globs).
- **One-time private API warning** (#493, #527, 898f799a): The first time per-process that `SimulatorKitHIDInputBackend` or `AccessibilityBridge` actually dispatches, `sim-hid-input-backend.ts` and `accessibility-bridge.ts` emit a single `console.error` informational line pointing at `docs/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 reference `docs/private-apis.md` in their `remediation` field.
- **idb vs OpenSafari call-pattern comparison** (#493, #529, fa20d179): `docs/private-apis.md` gains a side-by-side comparison of which private frameworks each project loads, the tap dispatch call pattern (idb: `SimDeviceIOClient` via idb_direct + XPC; OpenSafari: `dlopen(SimulatorKit)` + SimulatorKitHIDInputBackend), and a license-independence note (idb is MIT, OpenSafari's `sim-hid-bridge.swift` was written from Apple's public headers without reading idb source).
- **CI hardening: daily sentinel + one-time private-API notice** (#493, #532, af22ca7e, 68747103): `--passWithNoTests` added 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.

### Added — Structured telemetry (Epic #502)

- **`timedInput` wrapper + JSON telemetry sink for native input backends** (#502, ae378394): `src/metrics/input-telemetry.ts` wraps every `InputBackend.{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 to `OPENSAFARI_TELEMETRY_PATH` when set (default: unset → in-memory only) and is wired into `SimulatorKitHIDInputBackend`, `SimctlIOInputBackend`, `AppleScriptInputBackend`, and `NativeInputBackend`.
- **FlutterVMInputBackend telemetry** (#502, db534caa): `FlutterVMInputBackend` now emits the same `timedInput` envelope 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 as `outcome: 'error'` with the `error_reason` set to the structured `FlutterVMError.code`.
- **Opt-in `_telemetry` metadata 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}` when `OPENSAFARI_TELEMETRY_META=1` is 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.ts` reads the JSON telemetry sink, groups by `(backend × op)`, and emits p50/p95/p99 rollups plus per-bucket error-rate. Exported as `rollupLatency()` for in-process use and exposed via the `diagnose` MCP tool's new `latency_rollup` block when `OPENSAFARI_TELEMETRY_PATH` is readable.

### Added — CI headless smoke jobs (Epic #501)

- **Daily Safari headless smoke** (#501, #524, 38f4af87): New `.github/workflows/headless-smoke.yml` runs daily against a booted simulator + ios-webkit-debug-proxy + a `qa_*` audit suite. Asserts zero `AppleScriptInputBackend` calls 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.yml` that boots a simulator, installs the Flutter QA fixture, runs `app_tap`/`app_tap_element`/`app_type_element` through the FlutterVM Tier-0 backend, and asserts `_meta.headless === true` on 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.backendKind` for every response.
- **`node -e` return statement fix** (#501, #545, 259db32f): UDID picker shell snippets used `return` at the top level of a `node -e` expression, which is a syntax error under Node 20+. Replaced with `process.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.

### Changed

- **Documentation: headless architecture Tier 0/1 activation** (#492, #525): `docs/headless-architecture.md` no 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 full `sim-hid-bridge` exit-code contract, and the `OPENSAFARI_HEADLESS_ONLY` environment variable. `FlutterVMInputBackend` is reclassified from "planned" to "Production (Tier 0)" and `HeadlessInputUnavailableError.reason` documents 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.headless` envelope, 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.md` reflects that `getInputBackend()` skips Tier 1 for tap/swipe on Xcode 26+ pending the Apple `IndigoHIDMessageForMouseNSEvent` regression 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 returning `SimulatorKitHIDInputBackend`, 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.md` upgraded from "PoC" (v0.4.5 language) to "Production (Tier 1, partial on Xcode 26+)" to reflect the v0.4.6 shipping contract.

### Test Coverage

- **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), updated `sim-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 lint` shows 0 errors, 455 pre-existing warnings (no new regressions).

### Upgrade Notes

- **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=1` is set on Xcode 26+, native-app tap/swipe now throws `HeadlessInputUnavailableError` with `reason: '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, and `OPENSAFARI_TELEMETRY_META=1` to have `_telemetry` appear in MCP response envelopes.
- **Sentinel workflow requires a `SLACK_WEBHOOK_URL` secret** for alerting. Without it the workflow still runs but alert notifications are skipped.

### Release metadata

- npm: [`opensafari-mcp@0.4.8`](https://www.npmjs.com/package/opensafari-mcp/v/0.4.8)
- git tag: `v0.4.8`
- compare: [`v0.4.7…v0.4.8`](https://github.com/shaun0927/opensafari/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)

## [0.4.7] - 2026-04-15

### Added — FlutterVMInputBackend (Tier 0) ships in production (Epic #484, Issue #481)

- **`FlutterVMInputBackend` — Tier-0 headless Flutter input** (#481, #486): The Dart VM Service-based input backend now ships as the highest-priority routing tier in `getInputBackend()`. 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 Service `evaluate` — completely bypassing OS-level input.
  - `tap(x, y, duration?)` — synthetic `PointerDataPacket` with down/up phases via `PlatformDispatcher.onPointerDataPacket`
  - `swipe(x1, y1 → x2, y2, duration?)` — interpolated `PointerChange.move` events
  - `typeText(text)` — `TextInput.updateEditingState` platform message via primary focus
  - `keypress(hidUsage)` / `sendKey(name)` — `HardwareKeyboard` events with HID → `LogicalKeyboardKey` mapping
  - No CGEvent synthesis, no mouse cursor movement, no Simulator.app focus stealing
  - No `OPENSAFARI_ALLOW_FOCUS_INPUT` opt-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 Service `evaluate` RPC 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`)
- **DDS requirement documentation** (#481, #515): Documented in `docs/headless-architecture.md` and integration test fixtures that Flutter apps must be launched via `flutter run` (which starts Dart Development Service / DDS and the frontend compiler). Apps launched via `xcrun simctl launch` expose the VM Service socket but lack the compilation service, so `evaluate` calls fail. Integration test fixtures (`tests/integration/flutter-vm-input.live.test.ts`) updated to require a `flutter run`-launched fixture app.

### Test Coverage

- 1510 tests across 103 suites (up from 1488/102 in v0.4.6)
- New: extended `flutter-vm-input-backend.test.ts` coverage 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

### Notes

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.

## [0.4.6] - 2026-04-15

### Added — Headless automation hardening (Epic #484)

- **`SimulatorKitHIDInputBackend` — full HID injection** (#489, #490): The PoC `sim-hid-bridge.swift` now performs real `IOHIDEvent` injection via `SimulatorKit.framework` private API. Activated as **Tier 1** in `getInputBackend()` — native iOS app taps, swipes, and key presses are now headless on Xcode 26+ where `simctl io input` was 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.
- **`diagnose` MCP tool** (#498): New read-only diagnostic tool that reports backend availability, proxy status, environment variables, and a structured `headless_verdict` JSON. Registered at Tier 1 (always visible). Answers "is this setup truly headless?" in one call.
- **`_meta.backendKind` in 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 === true` to verify no focus-stealing backend was used.
- **`OPENSAFARI_HEADLESS_ONLY=1` environment variable** (#499): CI safety net that blocks the AppleScript/CGEvent fallback regardless of `OPENSAFARI_ALLOW_FOCUS_INPUT`. When set, any attempt to fall through to the focus-stealing backend throws `HeadlessInputUnavailableError` with `reason: 'headless-only'` and tailored remediation. Overrides `ALLOW_FOCUS_INPUT` with 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 of `getInputBackend()`, scenario matrix (Safari/Flutter/Native/WebView), environment variable reference, `HeadlessInputUnavailableError` handling 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, and `OPENSAFARI_HEADLESS_ONLY=1` configuration.
- **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.

### Fixed

- **Proxy socket finder race condition** (#494): `device_boot` consistently failed to auto-start the WebInspectorProxy because `findSocketPath(targetUdid)` was called exactly once with no retry — but `webinspectord_sim` needs several seconds after `simctl boot` to create its Unix socket. Added `waitForSocketPath()` 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_query` ax-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 from `sim-hid-input-backend.ts` — compiled binary (parent + same dir), Swift source (parent + same dir), plus guarded dev-only fallback via `OPENSAFARI_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.

### Test Coverage

- 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)

## [0.4.5] - 2026-04-15

### Added — Headless input backend infrastructure (Epic #484)

- **`SimulatorKitHIDInputBackend` (PoC)** (#483, #487): New input backend class that bridges to Apple's private `SimulatorKit.framework` via 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 99 `NOT_IMPLEMENTED`): the Swift binary validates args, dlopens both `SimulatorKit` and `CoreSimulator` frameworks 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 structured `InputBackendError` objects. `tryCreateSimulatorKitHIDBackend()` factory probes for the compiled binary or `dist/`-copied source with graceful null return when absent. Source-tree fallback gated behind `OPENSAFARI_ALLOW_SWIFT_INTERPRETER=1` to prevent path traversal when the package is consumed as a dependency.
  - New `docs/private-apis.md` documents: 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.

### Changed

- **`FlutterVMInputBackend` (Tier-0)** (#481, #486): New input backend that dispatches `PointerDataPacket`s, `TextInput.updateEditingState` platform messages, and `HardwareKeyboard` events directly into the Dart isolate via VM Service — no CGEvent, no Simulator.app foregrounding, no `OPENSAFARI_ALLOW_FOCUS_INPUT` opt-in required. Tier-0 routing added to `getInputBackend()`: 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.dart` for pointer dispatch, `editable_text.dart` for text input, `hardware_keyboard.dart` for key events) to ensure all required symbols are in lexical scope.

### Fixed

- **AppleScript input backend: every Tier-3 tap misses by 28pt on Xcode 26** (#482, #485): `AppleScriptInputBackend` hardcoded `TITLE_BAR_HEIGHT = 28` and added it to every iOS→macOS coordinate translation. On Xcode 26 / iOS 26.4 / iPhone 16 the `AccessibilityBridge` already 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 of `UI element 1 of window 1` (the iOS device screen content area within the macOS window). Per-device cache with explicit `refresh: true` invalidation. Fallback to raw window position with one-time `console.error` per device when the AX query fails.
- **`flutter_widget_at_point` returns wrapper type in `widget_type`** (#436, #479): The tool previously surfaced the raw inspector `_ElementDiagnosticableTreeNode` wrapper in the `widget_type` field because `summariseNode` reads the inspector's own `type` key first. The public payload now prefers `widgetRuntimeType` (e.g. `"ElevatedButton"`) and falls back to `description` before the wrapper `type`, so callers see the Flutter widget name that the checklist promises.
- **`flutter_widget_at_point` `ancestor_chain` always empty against real apps** (#436, #480): `flattenParentChain` only recognised the synthetic `{chain: [...]}` and `{result: {chain: [...]}}` shapes used by the unit tests. The live Flutter 3.11+ response from `ext.flutter.inspector.getParentChain` is `{type: "_extensionType", result: [{node, children}, ...]}` — the `result` key IS the chain array. That third shape is now detected via `Array.isArray(raw.result)`, so the tool returns the real ancestor path instead of an empty list.

## [0.4.4] - 2026-04-15

### Added — Flutter widget-at-point mapping and release-constraint branching

- **`flutter_widget_at_point`** (#436, #471): New MCP tool that maps a physical-pixel coordinate — matching the frame produced by `app_screenshot_native` — to the topmost Flutter widget at that point. The tool reads `devicePixelRatio` live from `FlutterView.platformDispatcher`, converts the physical (x, y) to logical pixels, drives a Dart-side hit-test via `renderView.hitTest(HitTestResult(), position: Offset(…))`, walks `HitTestResult.path` for the topmost `RenderObject` with a `DebugCreator`, selects the owning Element via `WidgetInspectorService.instance.setSelection`, then reads back the selected widget through `getSelectedSummaryWidget`.
  - Returns `{widget_type, description, creation_location, widget_id, ancestor_chain}`. The `ancestor_chain` is pulled from `ext.flutter.inspector.getParentChain` and filtered to user-defined widgets (anything under `package:flutter/`, `package:flutter_localizations/`, or an absolute `flutter/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-effort `ancestor_chain` — a failure in `getParentChain` still yields the topmost widget with `ancestor_chain: []` plus a stderr audit entry.
  - The Dart hit-test expression references `DebugCreator`, `WidgetInspectorService`, `RenderView`, and `HitTestResult` — symbols that live in `package:flutter/src/widgets/widget_inspector.dart` and are NOT re-exported through `flutter/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 via `getIsolate → libraries` and passing its `id` as `targetId`. Throws a dedicated `FlutterVMError('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 raise `FlutterVMError('INVALID_OBJECT_GROUP')`.
- **`FlutterVMClient.selectWidgetAtPoint` / `getParentChain`**: New public VM-client helpers wrapping the hit-test evaluate expression and `ext.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): `FlutterVMClient` now captures the Dart VM `version` string at `flutter_connect` time, parses it via the exported `parseDartVersion` helper, and branches `getRootWidgetSummaryTree` calls by Flutter major.
  - Flutter 3.x sessions try `ext.flutter.inspector.getRootWidgetSummaryTreeWithPreviews` first and fall back to `getRootWidgetSummaryTree` on VM Service error -32000 (seen on early 3.x releases that have the extension stub but no implementation).
  - Flutter 2.x sessions skip the `WithPreviews` variant 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_connect` responses now include `dartVersion` (structured `{raw, major, minor, patch, channel}`) and `flutterMajor` so downstream tools can gate behaviour per major. New accessors: `FlutterVMClient.getDartVersion()` / `getFlutterMajor()`.
- **`parseDartVersion` helper** (#472): Pure, exported helper that extracts `{major, minor, patch, channel?, raw}` from a Dart VM version string. Null-safe and whitespace-tolerant.

### Added — Live verification harness for #422 / #423

- **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 an `ElevatedButton` so `app_query` can be driven by both label and identifier.
  - A `TextField` wrapped in `Semantics(identifier: 'email-field')` so `app_query({identifier: 'email-field'})` and `app_type_element` can be verified against a live editable region.
  - A live `Counter: $n` text that increments on each button tap so downstream suites can assert state changes.
  - `build.sh` helper that runs `flutter pub get`, `flutter build ios --simulator --debug`, and an optional `xcrun simctl install` so reviewers can bring the fixture up with a single command.
  - Bundle id `com.opensafari.fixtures.flutterQaApp` — avoids collision with Flutter's `com.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.ts` that drives a booted simulator against a running Flutter app and proves `app_query` + `app_tap_element` resolve and interact with Flutter Semantics nodes (label, identifier, index, and ambiguous-match cases). Gated behind `jest.config.js` `testPathIgnorePatterns` so the default `npm test` stays headless.
- **Native (non-Flutter) integration suite** (#423, #474): `tests/integration/issue-423-native.live.test.ts` walks `com.apple.Preferences` (Settings → General → About) to prove the shared `AccessibilityBridge` path 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.ts` measures `app_query` / `app_tap_element` round-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.ts` builds the QA fixture above, installs it on a booted simulator, and verifies:
  1. `app_tree` populates even when `useVMServiceFallback: false` — the simctl-path activation must succeed standalone, proving parity with a real Flutter release build (where the Dart VM Service is stripped).
  2. `app_query({identifier: 'login-btn'})` and `app_query({identifier: 'email-field'})` return the expected `Semantics(identifier:)` nodes with correct role (`AXButton` / `AXGenericElement`), visibility, and enabled flags.
  - Honours `FLUTTER_BIN` env override with a `flutter`-on-PATH fallback so the suite runs uniformly on Apple Silicon brew, Intel brew, asdf, and nix installs.
  - `console.error` SKIP log suppressed when `process.env.CI` is set so the always-skipped-in-CI suite stops spamming shared CI output.

### Changed

- `FlutterVMClient.getDartVersion()` now returns the structured `DartVersion` type (exported from `src/flutter/flutter-types.ts`) — additive to the prior `null | undefined` shape for callers that were only checking truthiness. `getFlutterMajor()` normalises all absent-version states to `null` for consistent consumer code.

### Security

- **Dart injection hardening for `flutter_widget_at_point`** (#471): `objectGroup` is 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.

### Fixed

- Removed stale `flutter create` boilerplate `widget_test.dart` from the QA fixture (#476 review P1). The stub asserted `find.byIcon(Icons.add)` and a counter starting at `"0"`, but the fixture's `main.dart` was rewritten to render Semantics + TextField + `Counter: $n`; `flutter test` inside the fixture would have failed immediately, undermining the "fixture is stable" contract.
- Dropped the Apple-Silicon-only hardcoded `/opt/homebrew/bin/flutter` probe from the fixture integration test (#477/#478 review P1) in favour of a `FLUTTER_BIN` env override with a `flutter`-on-PATH fallback. The prior probe always threw on Intel Macs, nix, and asdf before the fallback ran.
- Tightened the `selectWidgetAtPoint` hit-result parsing: dropped the tautological `kind === 'Bool' && valueAsString === 'true'` disjunct that was subsumed by the primary `valueAsString === 'true'` check.

### Tests

- 1395+ tests / 97+ suites pass on `npm test` (the default headless run). New gated integration suites under `tests/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.ts` cover: 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, `objectGroup` sanitization, missing `widget_inspector` library handling, and best-effort `getParentChain` failure.
- 11 new unit tests in `tests/unit/flutter-version-branching.test.ts` cover: 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, and `getDartVersion` / `getFlutterMajor` pre- and post-connect state.

### Release metadata

- npm: [`opensafari-mcp@0.4.4`](https://www.npmjs.com/package/opensafari-mcp/v/0.4.4)
- git tag: `v0.4.4`
- compare: [`v0.4.3…v0.4.4`](https://github.com/shaun0927/opensafari/compare/v0.4.3...v0.4.4)
- merged PRs: #471, #472, #473, #474, #475, #476, #478

## [0.4.0] - 2026-04-14

### Added — Flutter Advanced Debugging & Profiling

- **`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 when `flutter_connect` fails to distinguish between a release build (VM Service disabled by design) and a configuration issue. Returns a `capabilities` map plus a `fallback_tools` list for release builds.
- **`flutter_toggle_debug_paint`** (#437): New MCP tool that flips Flutter's debug paint overlays (`size`, `baseline`, `repaint_rainbow`) and `time_dilation`, plus an `all_off` reset mode. Backed by `ext.flutter.debugPaint` / `ext.flutter.debugPaintBaselinesEnabled` / `ext.flutter.repaintRainbow` / `ext.flutter.timeDilation`. Useful for diagnosing overflow, padding, and repaint issues via `app_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 optional `prefix` filter. Groups results by namespace for easy LLM consumption.
- **`flutter_call_service_extension`** (#441): Generic invoker for any service extension. Auto-injects `isolateId`, enforces an `ext.` prefix, validates `args` is 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 Service `evaluate` / `evaluateInFrame` RPCs. Default scope is the main isolate's root library; `scope="frame"` with `frame_index` targets a paused stack frame (future-compatible with breakpoint support in #435). Results are normalised into a compact shape — primitives return `valueAsString`, composites expose 1-depth `fields`. 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 via `ext.flutter.inspector.getRootWidgetSummaryTreeWithPreviews`. Each node includes `type`, `description`, and `creationLocation` (file:line:column) so callers can jump straight to the source.
- **`flutter_inspect_selection`** (#436): Returns the currently selected widget via `ext.flutter.inspector.getSelectedSummaryWidget`, with an optional `show` flag that toggles the in-app inspector overlay (`ext.flutter.inspector.show`) to arm coordinate-based selection. Empty selection returns `status: "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 via `getCpuSamples` for a configurable window (max 120s) and returns a top-N list of `{function, self_us, total_us, samples}`. Pure `aggregateCpuSamples` helper exported for testing.
- **`flutter_timeline_capture`** (#439): Enables VM timeline streams (default `["Dart", "GC", "Embedder"]`), waits a window, fetches `getVMTimeline`, and writes Chrome Trace Event JSON loadable in `chrome://tracing` or Perfetto.
- **`flutter_track_rebuilds`** (#438): Drives the Flutter dirty-widget rebuild tracker. `start` / `report` / `stop` actions, optional `duration_ms` auto-stop, capped at 10,000 events per tracker.
- **`flutter_allocation_profile`** (#440): Per-class allocation profile via `getAllocationProfile`. Supports `gc_before` and `diff_against_previous` for the standard leak-hunt pattern (baseline → action → diff).
- **`flutter_heap_snapshot`** (#440): Full Dart heap snapshot via `requestHeapSnapshot`, written as binary importable by Flutter DevTools' Memory tab. Configurable `timeout_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? })` — wraps `addBreakpointWithScriptUri`
  - `flutter_remove_breakpoint({ breakpoint_id })` — wraps `removeBreakpoint`
  - `flutter_resume({ mode: "continue" | "step_into" | "step_over" | "step_out" })` — wraps `resume` with the matching `step` token
  - `flutter_get_stack({ limit? })` — wraps `getStack` with a compact per-frame summary (`function`, `location: {script_uri, line}`, `vars`)
  - `flutter_wait_for_pause({ timeout_ms?, poll_interval_ms? })` — polls for pause state, mirroring `app_wait_for`; returns `{timeout: true}` on timeout
- Per-device `BreakpointManager` lazily subscribes to the `Debug` stream and tracks pause state + active breakpoints. Pure helpers `resumeModeToStep`, `summariseFrame`, `_resetBreakpointManagers` exported for testability.

### Fixed

- **Breakpoint manager** (#435): Cleans listeners on disconnect and detects VM reconnect to avoid stale state.
- **Memory profiler** (#440): LRU cap on `previousSnapshots` prevents unbounded memory growth; `forgetAllocationHistory` exposed.
- **Track rebuilds** (#438): Rolls back listener registration if `track_rebuilds start` fails mid-setup.
- **Track rebuilds event name** (#438): Fixed filter to match `Flutter.RebuiltWidgets` (past tense, the actual event name Flutter emits from `widget_inspector.dart:2538`) instead of `Flutter.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_depth` and adds cycle guard to `summariseNode`.
- **Evaluate** (#434): Security docstring, audit log, Null handling, whitespace guard.
- **Service extensions** (#441): Caller cannot silently retarget `isolateId`.
- **Debug paint** (#437): `all_off` tolerates 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.

## [0.3.1] - 2026-04-13

### Security / Behavior change

- **Default-deny AppleScript/CGEvent input backend** (#405): The focus-stealing `AppleScriptInputBackend` is no longer instantiated automatically on Xcode 26+. When no headless input method is available, `getInputBackend()` throws `HeadlessInputUnavailableError` with actionable remediation guidance instead of silently moving the physical mouse cursor and activating `Simulator.app`.
  - To re-enable the legacy fallback, set `OPENSAFARI_ALLOW_FOCUS_INPUT=1` in 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 `backend` field (`simctl` / `webkit` / `applescript`) for audit/observability.

### Added

- **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.
- **`HeadlessInputUnavailableError`** class with structured fields (`deviceId`, `reason`, `remediation[]`) exported from the public barrel for typed error handling by MCP clients.

## [0.3.0] - 2026-04-13

### Added

- **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.

## [0.2.1] - 2026-04-05

### Added

- **NativeInputBackend abstraction** (`native-input-backend.ts`): Input backend layer with automatic Xcode version detection. Provides `InputBackend` interface (tap, swipe, typeText, keypress, sendKey) with two implementations:
  - `SimctlInputBackend`: Uses `xcrun simctl io input` commands (Xcode 15–16)
  - `AppleScriptInputBackend`: Uses `osascript` + Swift CGEvent for input (Xcode 26+)
- **Auto-detection**: Probes `simctl io input` on 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.

### Fixed

- **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 where `simctl io input` subcommand 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.

### Changed

- All native interaction tools now use `getInputBackend()` instead of direct `SimctlExecutor.exec()` calls. This is a transparent change — tools behave identically on Xcode versions that support `simctl io input`.

## [0.2.0] - 2026-04-05

### Added

- **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.md` with examples for native screenshots, log export, structured assertions, hybrid context switching, and artifact upload workflows.

### Scope & Limitations

- Native assertion `element_exists` / `element_visible` require Xcode 14+ with `simctl io enumerate` support.
- Hybrid context switching relies on ios-webkit-debug-proxy target discovery; apps must have Web Inspector enabled for WebView targets to appear.
- `app_screenshot_native` captures the full simulator display, not individual app windows.
- Screen recording (`app_record_video`) is available but marked as experimental.

## [0.1.5] - 2026-03-31

### Added

- **iPhone SE device presets**: Added `iphone-se-1`, `iphone-se-2`, and `iphone-se-3` presets 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.md` with detailed instructions for running `qa_full_audit` in GitHub Actions, GitLab CI, and Jenkins pipelines, including artifact collection and threshold gating.
- **API reference documentation**: Expanded `docs/api-reference.md` with `qa_full_audit` format specification.
- **E2E validation fixtures**: Added `buggy-page.html`, `clean-page.html`, and `validation-report.json` test 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.

### Fixed

- **WebKit error capture protocol**: Replaced Chrome-specific `Runtime.exceptionThrown` with WebKit-native `Console.messageAdded` for JavaScript error capture, fixing `onError` handler 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 `CrossViewportCapture` that could cause duplicate captures at boundary widths.
- **Lint and import cleanup**: Fixed unused `AnnotationResult` import in `audit.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 `NaN` guard for environment variable parsing to prevent CI test breakage.

### Changed

- **`assert_all_devices` tool simplified**: Removed the `includeScreenshot` parameter and per-device screenshot embedding from `assert_all_devices` results, reducing response payload size and eliminating the unused `screenshot` destructuring that caused the CI lint failure.
- **Cross-viewport compare refactored**: Moved HTML generation from inline string construction in `cross-viewport-compare.ts` to a dedicated `generateComparisonHtml()` function in `report-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.

## [0.1.2] - 2026-03-28

### Added
- 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
