AGENTS.md · git:20261018.5fbbfce · 2026-10-18 · sha256 66fc787a5a14771b

AGENTS.md git:20261018.5fbbfceA

Immutable. This exact content never changes and is served at /api/v1/blob/66fc787a5a14771b.

# AGENTS.md - Developer Guide

Canonical guidance for AI coding agents (Codex, Claude Code, and others) and developers working
with this repository. `CLAUDE.md` only imports this file - edit here, not there.

## Project Overview

iOS Simulator Skill is a production-ready Agent Skill providing 30 scripts for iOS app building, testing, and automation. It wraps Apple's `xcrun simctl` and Facebook's `idb` tools with semantic interfaces designed for AI agents and developers.

**Key Statistics:**
- 30 production scripts
- 7 script categories (Build, Device State, Navigation, Testing, Permissions, Simulator Discovery, Lifecycle)
- 7 shared utility modules (~2,400 lines)
- 100% token-optimized default output
- Unit tests under `tests/` (run `pytest tests/`)

## Principles

Every script is a tool an agent calls in a loop, so these rules beat convenience. New code and
reviews are checked against them.

1. **Low noise, high signal.** An agent-facing message is one line that says what happened and,
   on failure, what to do next. No usage dumps, tracebacks or prose. Default output is 3-5 lines;
   `--verbose` and `--json` give more. Cap lists (`IOS_SIM_MAX_ELEMENTS`) and end them with
   "... and N more".
2. **Never claim an effect you didn't get.** Say how an action was done ("(accessibility press)")
   and warn when its effect can't be confirmed, e.g. a coordinate tap on a toggle. A wrong "Tapped"
   costs an agent more than an honest failure.
3. **Act only on an unambiguous target.** idb markers are substring, first-match: only use one when
   nothing else on screen contains it, otherwise fall back to the safer path.
4. **Verify against a live simulator before relying on idb behaviour.** Upstream publishes no
   changelog and the docs drift from reality. Ship only what you saw working, and record what you
   dropped and why.
5. **Gate features by trying, not by version.** Run the command and fall back (or hint at an
   upgrade) on failure. Don't add `--help` probes to hot paths: each costs ~0.35s.
6. **simctl first.** It's Apple-maintained, with no companion to break. Use idb only for what simctl
   can't do (UI interaction, notification list/clear, photos clear).
7. **Don't change the device silently.** Device-wide side effects (e.g. axbridge's automation mode)
   sit behind an explicit flag like `--deep`, and tests restore whatever they change.
8. **Semantic over pixels.** Find elements by meaning via the accessibility tree; coordinates are
   the last resort.
9. **One helper per repeated agent-facing message.** Shared wording lives in `common/` (e.g.
   `with_idb_upgrade_hint`) once it appears a third time (AHA), so messages can't drift apart.
10. **Destructive operations need `--yes`.** Anything that deletes data the user can't easily
    recreate (erasing or deleting a simulator, uninstalling an app, clearing the photo library)
    refuses without `--yes` in one line that says what would be lost, so the agent asks the user
    first. Never prompt interactively: an agent has no one to answer.

## Project Structure

```
ios-simulator-skill/            # Repository root
├── .claude-plugin/marketplace.json  # Claude Code marketplace
├── .agents/plugins/marketplace.json # Codex marketplace
├── ios-simulator-skill/        # Distributable package
│   ├── .claude-plugin/
│   │   └── plugin.json        # Claude Code plugin manifest
│   ├── .codex-plugin/
│   │   └── plugin.json        # Codex plugin manifest (keep version in sync)
│   ├── assets/icon.svg        # Codex plugin icon
│   └── skills/
│       └── ios-simulator-skill/
│           ├── SKILL.md       # Entry point (table of contents)
│           ├── agents/openai.yaml  # Codex skill UI metadata
│           └── scripts/       # 30 production scripts
│               ├── build_and_test.py
│               ├── xcode/     # Xcode integration module
│               ├── log_monitor.py
│               ├── hang_watcher.py    # HangBuster session recorder + legacy stream
│               ├── screen_mapper.py
│               ├── navigator.py
│               ├── gesture.py
│               ├── keyboard.py
│               ├── app_launcher.py
│               ├── accessibility_audit.py
│               ├── visual_diff.py
│               ├── test_recorder.py
│               ├── app_state_capture.py
│               ├── clipboard.py
│               ├── status_bar.py
│               ├── push_notification.py
│               ├── privacy_manager.py
│               ├── simctl_boot.py
│               ├── simctl_shutdown.py
│               ├── simctl_create.py
│               ├── simctl_delete.py
│               ├── simctl_erase.py
│               ├── sim_health_check.sh
│               └── common/    # Shared utilities
│                   ├── hang_pipeline.py   # HangBuster filter pipeline (pure fns)
│                   └── hang_sessions.py   # HangBuster session storage
├── .github/workflows/
├── pyproject.toml
└── README.md
```

## Architecture Patterns

### Pattern 1: Class-Based Script Design

All scripts use class-based architecture for testability:

```python
class DeviceManager:
    def __init__(self, udid: str | None = None):
        self.udid = udid

    def execute(self, **kwargs) -> tuple[bool, str]:
        # Return (success, message) tuple
        pass

def main():
    parser = argparse.ArgumentParser()
    manager = DeviceManager(args.udid)
    success, message = manager.execute()
    print(message)
    sys.exit(0 if success else 1)
```

### Pattern 2: Auto-UDID Detection

All scripts support optional `--udid` with auto-detection:

```python
try:
    udid = resolve_device_identifier(args.udid)  # May be None
except RuntimeError as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)
```

### Pattern 3: Output Formatting

Consistent format across all scripts:

```
# Default (3-5 lines)
Device booted: iPhone 16 Pro (ABC123) [2.1s]

# --verbose (50+ lines)
Device booted successfully.
Device UDID: ABC123DEF456...
Boot time: 2.1 seconds

# --json (20-30 lines)
{"action": "boot", "udid": "ABC123", "success": true}
```

### Pattern 4: Batch Operations

Most scripts support batch operations:

```bash
python scripts/simctl_boot.py --all
python scripts/simctl_boot.py --type iPhone
```

## Script Categories

### Build & Development (2)
- **build_and_test.py**: Build with progressive disclosure
- **log_monitor.py**: Real-time log monitoring

### Device State (3)
- **appearance.py**: Dark mode, Dynamic Type, increase contrast, locale/region
- **location.py**: GPS coordinates, city presets, GPX route playback
- **media.py**: Photo library seeding (simctl addmedia) and clearing (idb photos clear, idb 1.7+)

### Navigation & Interaction (5)
- **screen_mapper.py**: Analyze screen
- **navigator.py**: Semantic element finding
- **gesture.py**: Touch simulation
- **keyboard.py**: Text input and keys
- **app_launcher.py**: App lifecycle

### Testing & Analysis (9)
- **accessibility_audit.py**: WCAG compliance
- **visual_diff.py**: Screenshot comparison
- **test_recorder.py**: Test documentation
- **app_state_capture.py**: Debugging snapshots
- **sim_health_check.sh**: Environment verification
- **model_inspector.py**: Core Data and SwiftData model inspection
- **container.py**: App sandbox files, UserDefaults, Core Data store paths
- **hang_watcher.py** (HangBuster): session-scoped hang recorder with progressive disclosure (`--start`/`--stop`/`--get-details`/`--diff`); raw NDJSON mode (`--raw-capture` + size cap + gzip) for `jq` exploration; bounded auto-restart on stream EOF (`IOS_SIM_HANG_MAX_RESTARTS`) so crashed workers are marked `crashed` not stale `running`; aggregate disk cap (`IOS_SIM_HANG_TOTAL_CAP_MB`) pruned on every `--start`; legacy `--watch`/`--since` paths preserved
- **localization_audit.py**: String catalog gaps, missing keys, placeholder mismatches

### Advanced Testing & Permissions (4)
- **clipboard.py**: Clipboard management
- **status_bar.py**: Status bar control
- **push_notification.py**: Push notifications
- **privacy_manager.py**: Permission management

### Simulator Discovery (2)
- **sim_list.py**: List simulators with progressive disclosure (96% token reduction)
- **simulator_selector.py**: Suggest best simulator from recent use, latest iOS, boot status

### Device Lifecycle Management (5)
- **simctl_boot.py**: Boot device
- **simctl_shutdown.py**: Shutdown device
- **simctl_create.py**: Create device
- **simctl_delete.py**: Delete device
- **simctl_erase.py**: Reset device

## Shared Utilities

### device_utils.py (~450 lines)
- `resolve_device_identifier()`: UDID/name/booted resolution
- `list_simulators()`: List with state filtering
- `_extract_device_type()`: Parse device type from name

### screenshot_utils.py (~346 lines)
- `capture_screenshot()`: File or inline mode
- `generate_screenshot_name()`: Semantic naming
- `resize_screenshot()`: Token optimization

### cache_utils.py (~258 lines)
- `ProgressiveCache`: Large output caching with TTL

### hang_pipeline.py (~630 lines)
Pure-function HangBuster filter pipeline (parse → normalise → threshold → bucket → cluster → aggregate → rank → format). Reusable but scoped — promote to a generic `log_filters.py` only when a second consumer appears (AHA).

- `parse_log_line()`, `normalise_message()`, `bucket_severity()`, `compute_fingerprint()`
- `cluster_events()`, `rank_clusters()`, aggregators (bursts/quiet/process)
- `format_l0/l1/l2()`, `format_cluster_detail()`, `format_diff()`
- `estimate_tokens()` (char/4 heuristic), `compress_to_budget()`
- `diff_sessions()` with `fingerprint_version` guard
- Dataclasses: `NormalisedEvent`, `Cluster`, `SessionSummary`, `Severity` (StrEnum)

### hang_sessions.py (~400 lines)
- `SessionStore`: filesystem-backed session repository at `~/.ios-simulator-skill/sessions/<id>/{meta.json, events.jsonl, summary.json, raw.ndjson.gz?, auto_samples.jsonl?}`
- Session ID format: `hang-YYYYMMDD-HHmmss-<4hex>` (random suffix avoids same-second collisions)
- Atomic meta writes (`.tmp` + `replace`); worker writes its own pid (no pidfile race)
- Status state machine: `pending → running → (stopped | crashed)`. `mark_crashed` records `stopped_at_ms` so `build_summary` reflects the capture window, not the time of inspection. `persist_worker_counters` preserves terminal status (cannot clobber CRASHED/STOPPED back to RUNNING).
- TTL prune via `IOS_SIM_HANG_SESSION_TTL_HOURS` on every `--start`
- Aggregate cap via `IOS_SIM_HANG_TOTAL_CAP_MB` (`prune_to_aggregate_cap`) — oldest-first eviction; runs alongside TTL prune on every `--start`
- `raw_path()` + `session_total_bytes()` accessors for raw-capture mode

## Quality Standards

1. Type hints (modern Python syntax)
2. Docstrings on all functions
3. Specific exception handling
4. --help support on all scripts
5. Black formatter compliance
6. Ruff linter (0 errors)
7. Never use shell=True

## Contributing

New scripts should:
- Use class-based design for > 50 lines of logic
- Support --udid and auto-detection
- Support --json output
- Provide --help documentation
- Follow Black and Ruff standards
- Update SKILL.md table of contents
- Work with real simulators before submission
- Pass a check against [Principles](#principles), especially 1-3 for anything an agent reads

## Release Process

1. Update version in SKILL.md frontmatter, pyproject.toml, and both plugin.json manifests (`.claude-plugin/`, `.codex-plugin/`)
2. Verify CI passes (Black, Ruff)
3. Create GitHub release with vX.X.X tag
4. Attach zipped ios-simulator-skill/ directory