---
name: verify-accessibility
description: Systematically verify and fix accessibility issues in iOS, tvOS, visionOS and watchOS applications using Reveal Insights.
license: Proprietary. LICENSE.md has complete terms.
compatibility: Requires Xcode, axe, and Reveal.app with MCP enabled.
allowed-tools: Bash(xcodebuild:*) Bash(xcrun:*) Bash(lldb:*) Bash(defaults:*) Bash(axe:*) Read mcp__reveal__*
metadata:
  author: Reveal Team <support@revealapp.com>
  version: "0.4"
---

# Reveal Accessibility Verification Skill

Systematic accessibility testing and remediation using Reveal's MCP tools. Reveal performs the audit; your job is to get the app connected to Reveal, run the audit, then map issues to code and implement fixes.

## Critical Rules

1. **Do NOT explore the codebase until you have audit results.** The audit output tells you exactly where to look. Reading source files before snapshotting wastes time.
2. **Complete each phase fully before starting the next.** Do not jump between phases.
3. **Stop and report to the user after 3 failed attempts** at any step. Describe what you tried and what happened. Do not loop.
4. **Verify each step's output before proceeding.** If a command produces empty or unexpected output, that is a failure — handle it, don't ignore it.

## Prerequisites

- Reveal.app running with MCP enabled
- `axe` CLI on `$PATH` for Simulator UI navigation
- Xcode project in the working directory

## Before Starting

Ask the user:
1. Which screen(s) to test (e.g. "the Settings screen", "the map view")
2. Navigation steps to reach that screen, if not the initial screen (e.g. "tap Settings → tap Profile")

You can refine navigation later using `axe describe-ui`. Do not start reading source code yet.

---

## Phase 1: Build, Launch, and Connect to Reveal

**Goal:** A freshly-built app is running in the Simulator and visible in Reveal. Every audit must test current code, so always build and relaunch — never audit a stale app.

### Step 1a: Verify Reveal MCP is reachable

Call `list_apps`.

- **Returns successfully (empty array is fine)** → Reveal MCP is working. Continue to Step 1b.
- **Call fails or errors** → Check whether the MCP service is enabled:
  ```bash
  defaults read com.ittybittyapps.Reveal2 MCPServiceEnabled 2>/dev/null
  ```
  - **Returns `0` or key not found** → The MCP service is disabled. Enable it:
    ```bash
    defaults write com.ittybittyapps.Reveal2 MCPServiceEnabled -bool YES
    ```
    Wait 3 seconds and call `list_apps` again. If it still fails, tell the user: "Reveal's MCP service has been enabled, but still isn't responding. Please make sure Reveal.app is running and try again." **Stop here.**
  - **Returns `1`** → The MCP service is already enabled. The problem is elsewhere. Tell the user: "Reveal's MCP service is enabled but isn't responding. Please make sure Reveal.app is running and try again." **Stop here.**

### Step 1b: Discover the project

```bash
# List available schemes
xcodebuild -list

# Identify available simulators
xcrun simctl list devices available | grep -E "(iPhone|iPad|Apple TV|Apple Watch|Apple Vision)" | head -10
```

Ask the user to confirm the scheme and simulator if there are multiple options. Determine the bundle identifier from the project (check Info.plist or build settings).

### Step 1c: Build

`xcodebuild` output is very verbose. Run the build in a subagent to avoid flooding your context window. The subagent should run the build and return only the result (success/failure) and any error or warning messages:

```bash
xcodebuild -scheme <Scheme> -destination 'platform=iOS Simulator,name=<Device>' build
```

If the build fails, report the errors to the user. **Stop here** — do not attempt to fix build errors unless the user asks.

### Step 1d: Boot the simulator (if needed)

```bash
# Check if a simulator is already booted
xcrun simctl list devices booted
```

If no simulator, or the wrong simulator is booted:
```bash
xcrun simctl boot '<Device Name>'
open -a Simulator
```

### Step 1e: Launch the app

Terminate any stale instance and launch fresh:

```bash
xcrun simctl terminate booted <bundle.id> 2>/dev/null; true
xcrun simctl launch booted <bundle.id>
```

If launch fails:
- Check that the app is installed: `xcrun simctl listapps booted 2>&1 | grep <bundle.id>`
- If not installed, install from the build output: `xcrun simctl install booted <path-to-.app>`
- Retry the launch once. If it fails again, report the error to the user. **Stop here.**

### Step 1f: Check if the app is visible in Reveal

Call `list_apps`.

- **Target app appears** → The app already has RevealServer integrated. Go to Phase 2.
- **Target app missing** → RevealServer needs to be injected. Go to Step 1g.

### Step 1g: Inject RevealServer via LLDB

The app is not visible to Reveal, so RevealServer must be dynamically injected. The app must be fully running (its run loop must be active) before injection can work.

First, get the app's PID. It was printed by the launch command in step 1e (the number after the bundle identifier). Alternatively:

```bash
PID=$(xcrun simctl spawn booted launchctl list | grep <bundle.id> | awk '{print $1}')
```

If `$PID` is empty, the app may not be running. Wait 3 seconds and retry. If still empty after 3 attempts, report to the user. **Stop here.**

Now attach LLDB and inject RevealServer. This command:
1. Attaches to the running app
2. Allows re-importing the Reveal LLDB commands (in case they're already loaded via the user's `~/.lldbinit`)
3. Imports the Reveal LLDB commands
4. Runs `reveal load --autostart` which loads the RevealServer framework and starts it
5. Detaches, leaving the app running with Reveal support active

```bash
lldb -b \
  -o "process attach --pid $PID" \
  -o "settings set interpreter.require-overwrite false" \
  -o "command script import '$HOME/Library/Application Support/Reveal/RevealLLDBCommands/reveallldb'" \
  -o "reveal load --autostart" \
  -o "process detach" \
  -o "quit"
```

Use single quotes for the path — it contains spaces. Do not skip or simplify this command.

**If LLDB reports "process is not yet ready to execute Reveal Server commands":** the app's run loop hasn't started yet. Wait 5 seconds and retry the entire LLDB sequence. Retry up to 3 times.

If LLDB reports other errors (e.g. "unable to attach"), re-check the PID and retry once. If it still fails, report the exact error to the user. **Stop here.**

### Step 1h: Verify injection succeeded

Wait 3 seconds after LLDB exits, then call `list_apps`.

- **Target app appears** → Go to Phase 2.
- **Target app missing** → Retry injection (step 1g) once. If still missing after retry, tell the user: "RevealServer was injected but the app isn't appearing in Reveal. The app may need to be restarted." **Stop here.**

---

## Phase 2: Snapshot and Audit

**Goal:** Obtain structured accessibility audit results for the target screen.

### Step 2a: Navigate to the target screen

Get the booted simulator's UDID:

```bash
UDID=$(xcrun simctl list devices booted -j | python3 -c "import sys,json; devs=[d for r in json.loads(sys.stdin.read())['devices'].values() for d in r if d['state']=='Booted']; print(devs[0]['udid'] if devs else '')")
```

If `$UDID` is empty, no simulator is booted — go back to the boot step in Phase 1.

Inspect the current screen:

```bash
axe describe-ui --udid $UDID
```

If the user provided navigation steps, execute them one at a time using `axe tap`. After each tap, run `axe describe-ui` to confirm the navigation succeeded before proceeding to the next tap:

```bash
# Navigate by accessibility identifier (preferred) or label
axe tap --udid $UDID --id "settings-button"
axe describe-ui --udid $UDID

axe tap --udid $UDID --label "Profile"
axe describe-ui --udid $UDID
```

Other `axe` commands available: `axe type --udid $UDID "text"`, `axe swipe --udid $UDID --direction up`.

**Record the navigation steps you used** — you will repeat them after each rebuild.

Once you are on the correct screen, confirm with the user: "I can see [description of screen]. Ready to snapshot and run the accessibility audit?"

### Step 2b: Snapshot

```
snapshot(bundleIdentifier: "<bundle.id>")
```

This returns a `snapshotPath` to a `.reveal` file. If there are multiple instances of the app (shown in `list_apps`), pass `id` to disambiguate.

If the snapshot fails, the app may have been backgrounded or crashed. Check with `axe describe-ui` and re-navigate if needed.

### Step 2c: Analyze

```
insights(path: "<snapshotPath>")
```

Returns structured JSON with failing rules grouped by rule set. Each result includes:
- `ruleID`, `ruleName`, `state`, `ruleDescription`
- `element`: `className`, `accessibilityIdentifier`, `accessibilityLabel`, `traits`, `owningController`, `contextHints.searchableText`

For more detail on any rule:
```
describe_rule(ruleID: "<ruleID>")
```

Present a summary of findings to the user, grouped by severity/rule. Ask how they'd like to proceed before moving to Phase 3.

---

## Phase 3: Fix Issues

**Goal:** Map audit findings to source code, implement fixes, and verify.

**Now** you may explore the codebase. Use element info from the audit to locate relevant code:

1. Search for `accessibilityIdentifier` values in source
2. Search for `accessibilityLabel` text
3. Use `owningController.className` to find the containing view controller or view
4. Use `contextHints.searchableText` for additional anchors

### Implement fixes

Work through issues one at a time or in batches of related issues. Check in with the user before moving between rule categories:
- "Would you like to fix the next issue, or test a different screen?"
- "Should I continue fixing similar issues in this rule set?"

### Verify fixes

After implementing fixes, repeat Phase 1 from step 1c onward (build, launch, inject RevealServer if needed, verify in Reveal), then repeat Phase 2 (navigate, snapshot, analyze).

Compare the new results against the previous audit to confirm issues are resolved.
