#
tokens: 46276/50000 9/393 files (page 10/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 10 of 12. Use http://codebase.md/cameroncooke/xcodebuildmcp?page={x} to view the full context.

# Directory Structure

```
├── .axe-version
├── .claude
│   └── agents
│       └── xcodebuild-mcp-qa-tester.md
├── .cursor
│   ├── BUGBOT.md
│   └── environment.json
├── .cursorrules
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   └── workflows
│       ├── ci.yml
│       ├── README.md
│       ├── release.yml
│       ├── sentry.yml
│       └── stale.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .vscode
│   ├── extensions.json
│   ├── launch.json
│   ├── mcp.json
│   ├── settings.json
│   └── tasks.json
├── AGENTS.md
├── banner.png
├── build-plugins
│   ├── plugin-discovery.js
│   ├── plugin-discovery.ts
│   └── tsconfig.json
├── CHANGELOG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── docs
│   ├── CONFIGURATION.md
│   ├── DAP_BACKEND_IMPLEMENTATION_PLAN.md
│   ├── DEBUGGING_ARCHITECTURE.md
│   ├── DEMOS.md
│   ├── dev
│   │   ├── ARCHITECTURE.md
│   │   ├── CODE_QUALITY.md
│   │   ├── CONTRIBUTING.md
│   │   ├── ESLINT_TYPE_SAFETY.md
│   │   ├── MANUAL_TESTING.md
│   │   ├── NODEJS_2025.md
│   │   ├── PLUGIN_DEVELOPMENT.md
│   │   ├── README.md
│   │   ├── RELEASE_PROCESS.md
│   │   ├── RELOADEROO_FOR_XCODEBUILDMCP.md
│   │   ├── RELOADEROO_XCODEBUILDMCP_PRIMER.md
│   │   ├── RELOADEROO.md
│   │   ├── session_management_plan.md
│   │   ├── session-aware-migration-todo.md
│   │   ├── SMITHERY.md
│   │   ├── TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md
│   │   ├── TESTING.md
│   │   └── ZOD_MIGRATION_GUIDE.md
│   ├── DEVICE_CODE_SIGNING.md
│   ├── GETTING_STARTED.md
│   ├── investigations
│   │   ├── issue-154-screenshot-downscaling.md
│   │   ├── issue-163.md
│   │   ├── issue-debugger-attach-stopped.md
│   │   └── issue-describe-ui-empty-after-debugger-resume.md
│   ├── OVERVIEW.md
│   ├── PRIVACY.md
│   ├── README.md
│   ├── SESSION_DEFAULTS.md
│   ├── TOOLS.md
│   └── TROUBLESHOOTING.md
├── eslint.config.js
├── example_projects
│   ├── .vscode
│   │   └── launch.json
│   ├── iOS
│   │   ├── .cursor
│   │   │   └── rules
│   │   │       └── errors.mdc
│   │   ├── .vscode
│   │   │   └── settings.json
│   │   ├── Makefile
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   ├── MCPTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── MCPTest.xcscheme
│   │   └── MCPTestUITests
│   │       └── MCPTestUITests.swift
│   ├── iOS_Calculator
│   │   ├── .gitignore
│   │   ├── CalculatorApp
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── CalculatorApp.swift
│   │   │   └── CalculatorApp.xctestplan
│   │   ├── CalculatorApp.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── CalculatorApp.xcscheme
│   │   ├── CalculatorApp.xcworkspace
│   │   │   └── contents.xcworkspacedata
│   │   ├── CalculatorAppPackage
│   │   │   ├── .gitignore
│   │   │   ├── Package.swift
│   │   │   ├── Sources
│   │   │   │   └── CalculatorAppFeature
│   │   │   │       ├── BackgroundEffect.swift
│   │   │   │       ├── CalculatorButton.swift
│   │   │   │       ├── CalculatorDisplay.swift
│   │   │   │       ├── CalculatorInputHandler.swift
│   │   │   │       ├── CalculatorService.swift
│   │   │   │       └── ContentView.swift
│   │   │   └── Tests
│   │   │       └── CalculatorAppFeatureTests
│   │   │           └── CalculatorServiceTests.swift
│   │   ├── CalculatorAppTests
│   │   │   └── CalculatorAppTests.swift
│   │   └── Config
│   │       ├── Debug.xcconfig
│   │       ├── Release.xcconfig
│   │       ├── Shared.xcconfig
│   │       └── Tests.xcconfig
│   ├── macOS
│   │   ├── MCPTest
│   │   │   ├── Assets.xcassets
│   │   │   │   ├── AccentColor.colorset
│   │   │   │   │   └── Contents.json
│   │   │   │   ├── AppIcon.appiconset
│   │   │   │   │   └── Contents.json
│   │   │   │   └── Contents.json
│   │   │   ├── ContentView.swift
│   │   │   ├── MCPTest.entitlements
│   │   │   ├── MCPTestApp.swift
│   │   │   └── Preview Content
│   │   │       └── Preview Assets.xcassets
│   │   │           └── Contents.json
│   │   ├── MCPTest.xcodeproj
│   │   │   ├── project.pbxproj
│   │   │   └── xcshareddata
│   │   │       └── xcschemes
│   │   │           └── MCPTest.xcscheme
│   │   └── MCPTestTests
│   │       └── MCPTestTests.swift
│   └── spm
│       ├── .gitignore
│       ├── Package.resolved
│       ├── Package.swift
│       ├── Sources
│       │   ├── long-server
│       │   │   └── main.swift
│       │   ├── quick-task
│       │   │   └── main.swift
│       │   ├── spm
│       │   │   └── main.swift
│       │   └── TestLib
│       │       └── TaskManager.swift
│       └── Tests
│           └── TestLibTests
│               └── SimpleTests.swift
├── LICENSE
├── mcp-install-dark.png
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── analysis
│   │   └── tools-analysis.ts
│   ├── bundle-axe.sh
│   ├── check-code-patterns.js
│   ├── generate-loaders.ts
│   ├── generate-version.ts
│   ├── release.sh
│   ├── tools-cli.ts
│   ├── update-tools-docs.ts
│   └── verify-smithery-bundle.sh
├── server.json
├── smithery.config.js
├── smithery.yaml
├── src
│   ├── core
│   │   ├── __tests__
│   │   │   └── resources.test.ts
│   │   ├── generated-plugins.ts
│   │   ├── generated-resources.ts
│   │   ├── plugin-registry.ts
│   │   ├── plugin-types.ts
│   │   └── resources.ts
│   ├── doctor-cli.ts
│   ├── index.ts
│   ├── mcp
│   │   ├── resources
│   │   │   ├── __tests__
│   │   │   │   ├── devices.test.ts
│   │   │   │   ├── doctor.test.ts
│   │   │   │   ├── session-status.test.ts
│   │   │   │   └── simulators.test.ts
│   │   │   ├── devices.ts
│   │   │   ├── doctor.ts
│   │   │   ├── session-status.ts
│   │   │   └── simulators.ts
│   │   └── tools
│   │       ├── debugging
│   │       │   ├── debug_attach_sim.ts
│   │       │   ├── debug_breakpoint_add.ts
│   │       │   ├── debug_breakpoint_remove.ts
│   │       │   ├── debug_continue.ts
│   │       │   ├── debug_detach.ts
│   │       │   ├── debug_lldb_command.ts
│   │       │   ├── debug_stack.ts
│   │       │   ├── debug_variables.ts
│   │       │   └── index.ts
│   │       ├── device
│   │       │   ├── __tests__
│   │       │   │   ├── build_device.test.ts
│   │       │   │   ├── get_device_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_device.test.ts
│   │       │   │   ├── launch_app_device.test.ts
│   │       │   │   ├── list_devices.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_app_device.test.ts
│   │       │   │   └── test_device.test.ts
│   │       │   ├── build_device.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_device_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_device.ts
│   │       │   ├── launch_app_device.ts
│   │       │   ├── list_devices.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── stop_app_device.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── test_device.ts
│   │       ├── doctor
│   │       │   ├── __tests__
│   │       │   │   ├── doctor.test.ts
│   │       │   │   └── index.test.ts
│   │       │   ├── doctor.ts
│   │       │   ├── index.ts
│   │       │   └── lib
│   │       │       └── doctor.deps.ts
│   │       ├── logging
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── start_device_log_cap.test.ts
│   │       │   │   ├── start_sim_log_cap.test.ts
│   │       │   │   ├── stop_device_log_cap.test.ts
│   │       │   │   └── stop_sim_log_cap.test.ts
│   │       │   ├── index.ts
│   │       │   ├── start_device_log_cap.ts
│   │       │   ├── start_sim_log_cap.ts
│   │       │   ├── stop_device_log_cap.ts
│   │       │   └── stop_sim_log_cap.ts
│   │       ├── macos
│   │       │   ├── __tests__
│   │       │   │   ├── build_macos.test.ts
│   │       │   │   ├── build_run_macos.test.ts
│   │       │   │   ├── get_mac_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── launch_mac_app.test.ts
│   │       │   │   ├── re-exports.test.ts
│   │       │   │   ├── stop_mac_app.test.ts
│   │       │   │   └── test_macos.test.ts
│   │       │   ├── build_macos.ts
│   │       │   ├── build_run_macos.ts
│   │       │   ├── clean.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_mac_app_path.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── launch_mac_app.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_mac_app.ts
│   │       │   └── test_macos.ts
│   │       ├── project-discovery
│   │       │   ├── __tests__
│   │       │   │   ├── discover_projs.test.ts
│   │       │   │   ├── get_app_bundle_id.test.ts
│   │       │   │   ├── get_mac_bundle_id.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── list_schemes.test.ts
│   │       │   │   └── show_build_settings.test.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_mac_bundle_id.ts
│   │       │   ├── index.ts
│   │       │   ├── list_schemes.ts
│   │       │   └── show_build_settings.ts
│   │       ├── project-scaffolding
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── scaffold_ios_project.test.ts
│   │       │   │   └── scaffold_macos_project.test.ts
│   │       │   ├── index.ts
│   │       │   ├── scaffold_ios_project.ts
│   │       │   └── scaffold_macos_project.ts
│   │       ├── session-management
│   │       │   ├── __tests__
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── session_clear_defaults.test.ts
│   │       │   │   ├── session_set_defaults.test.ts
│   │       │   │   └── session_show_defaults.test.ts
│   │       │   ├── index.ts
│   │       │   ├── session_clear_defaults.ts
│   │       │   ├── session_set_defaults.ts
│   │       │   └── session_show_defaults.ts
│   │       ├── simulator
│   │       │   ├── __tests__
│   │       │   │   ├── boot_sim.test.ts
│   │       │   │   ├── build_run_sim.test.ts
│   │       │   │   ├── build_sim.test.ts
│   │       │   │   ├── get_sim_app_path.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── install_app_sim.test.ts
│   │       │   │   ├── launch_app_logs_sim.test.ts
│   │       │   │   ├── launch_app_sim.test.ts
│   │       │   │   ├── list_sims.test.ts
│   │       │   │   ├── open_sim.test.ts
│   │       │   │   ├── record_sim_video.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── stop_app_sim.test.ts
│   │       │   │   └── test_sim.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── build_run_sim.ts
│   │       │   ├── build_sim.ts
│   │       │   ├── clean.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── discover_projs.ts
│   │       │   ├── get_app_bundle_id.ts
│   │       │   ├── get_sim_app_path.ts
│   │       │   ├── index.ts
│   │       │   ├── install_app_sim.ts
│   │       │   ├── launch_app_logs_sim.ts
│   │       │   ├── launch_app_sim.ts
│   │       │   ├── list_schemes.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── record_sim_video.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── show_build_settings.ts
│   │       │   ├── stop_app_sim.ts
│   │       │   └── test_sim.ts
│   │       ├── simulator-management
│   │       │   ├── __tests__
│   │       │   │   ├── erase_sims.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── reset_sim_location.test.ts
│   │       │   │   ├── set_sim_appearance.test.ts
│   │       │   │   ├── set_sim_location.test.ts
│   │       │   │   └── sim_statusbar.test.ts
│   │       │   ├── boot_sim.ts
│   │       │   ├── erase_sims.ts
│   │       │   ├── index.ts
│   │       │   ├── list_sims.ts
│   │       │   ├── open_sim.ts
│   │       │   ├── reset_sim_location.ts
│   │       │   ├── set_sim_appearance.ts
│   │       │   ├── set_sim_location.ts
│   │       │   └── sim_statusbar.ts
│   │       ├── swift-package
│   │       │   ├── __tests__
│   │       │   │   ├── active-processes.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── swift_package_build.test.ts
│   │       │   │   ├── swift_package_clean.test.ts
│   │       │   │   ├── swift_package_list.test.ts
│   │       │   │   ├── swift_package_run.test.ts
│   │       │   │   ├── swift_package_stop.test.ts
│   │       │   │   └── swift_package_test.test.ts
│   │       │   ├── active-processes.ts
│   │       │   ├── index.ts
│   │       │   ├── swift_package_build.ts
│   │       │   ├── swift_package_clean.ts
│   │       │   ├── swift_package_list.ts
│   │       │   ├── swift_package_run.ts
│   │       │   ├── swift_package_stop.ts
│   │       │   └── swift_package_test.ts
│   │       ├── ui-testing
│   │       │   ├── __tests__
│   │       │   │   ├── button.test.ts
│   │       │   │   ├── describe_ui.test.ts
│   │       │   │   ├── gesture.test.ts
│   │       │   │   ├── index.test.ts
│   │       │   │   ├── key_press.test.ts
│   │       │   │   ├── key_sequence.test.ts
│   │       │   │   ├── long_press.test.ts
│   │       │   │   ├── screenshot.test.ts
│   │       │   │   ├── swipe.test.ts
│   │       │   │   ├── tap.test.ts
│   │       │   │   ├── touch.test.ts
│   │       │   │   └── type_text.test.ts
│   │       │   ├── button.ts
│   │       │   ├── describe_ui.ts
│   │       │   ├── gesture.ts
│   │       │   ├── index.ts
│   │       │   ├── key_press.ts
│   │       │   ├── key_sequence.ts
│   │       │   ├── long_press.ts
│   │       │   ├── screenshot.ts
│   │       │   ├── swipe.ts
│   │       │   ├── tap.ts
│   │       │   ├── touch.ts
│   │       │   └── type_text.ts
│   │       └── utilities
│   │           ├── __tests__
│   │           │   ├── clean.test.ts
│   │           │   └── index.test.ts
│   │           ├── clean.ts
│   │           └── index.ts
│   ├── server
│   │   ├── bootstrap.ts
│   │   └── server.ts
│   ├── smithery.ts
│   ├── test-utils
│   │   └── mock-executors.ts
│   ├── types
│   │   └── common.ts
│   ├── utils
│   │   ├── __tests__
│   │   │   ├── build-utils-suppress-warnings.test.ts
│   │   │   ├── build-utils.test.ts
│   │   │   ├── debugger-simctl.test.ts
│   │   │   ├── environment.test.ts
│   │   │   ├── session-aware-tool-factory.test.ts
│   │   │   ├── session-store.test.ts
│   │   │   ├── simulator-utils.test.ts
│   │   │   ├── test-runner-env-integration.test.ts
│   │   │   ├── typed-tool-factory.test.ts
│   │   │   └── workflow-selection.test.ts
│   │   ├── axe
│   │   │   └── index.ts
│   │   ├── axe-helpers.ts
│   │   ├── build
│   │   │   └── index.ts
│   │   ├── build-utils.ts
│   │   ├── capabilities.ts
│   │   ├── command.ts
│   │   ├── CommandExecutor.ts
│   │   ├── debugger
│   │   │   ├── __tests__
│   │   │   │   └── debugger-manager-dap.test.ts
│   │   │   ├── backends
│   │   │   │   ├── __tests__
│   │   │   │   │   └── dap-backend.test.ts
│   │   │   │   ├── dap-backend.ts
│   │   │   │   ├── DebuggerBackend.ts
│   │   │   │   └── lldb-cli-backend.ts
│   │   │   ├── dap
│   │   │   │   ├── __tests__
│   │   │   │   │   └── transport-framing.test.ts
│   │   │   │   ├── adapter-discovery.ts
│   │   │   │   ├── transport.ts
│   │   │   │   └── types.ts
│   │   │   ├── debugger-manager.ts
│   │   │   ├── index.ts
│   │   │   ├── simctl.ts
│   │   │   ├── tool-context.ts
│   │   │   ├── types.ts
│   │   │   └── ui-automation-guard.ts
│   │   ├── environment.ts
│   │   ├── errors.ts
│   │   ├── execution
│   │   │   ├── index.ts
│   │   │   └── interactive-process.ts
│   │   ├── FileSystemExecutor.ts
│   │   ├── log_capture.ts
│   │   ├── log-capture
│   │   │   ├── device-log-sessions.ts
│   │   │   └── index.ts
│   │   ├── logger.ts
│   │   ├── logging
│   │   │   └── index.ts
│   │   ├── plugin-registry
│   │   │   └── index.ts
│   │   ├── responses
│   │   │   └── index.ts
│   │   ├── runtime-registry.ts
│   │   ├── schema-helpers.ts
│   │   ├── sentry.ts
│   │   ├── session-status.ts
│   │   ├── session-store.ts
│   │   ├── simulator-utils.ts
│   │   ├── template
│   │   │   └── index.ts
│   │   ├── template-manager.ts
│   │   ├── test
│   │   │   └── index.ts
│   │   ├── test-common.ts
│   │   ├── tool-registry.ts
│   │   ├── typed-tool-factory.ts
│   │   ├── validation
│   │   │   └── index.ts
│   │   ├── validation.ts
│   │   ├── version
│   │   │   └── index.ts
│   │   ├── video_capture.ts
│   │   ├── video-capture
│   │   │   └── index.ts
│   │   ├── workflow-selection.ts
│   │   ├── xcode.ts
│   │   ├── xcodemake
│   │   │   └── index.ts
│   │   └── xcodemake.ts
│   └── version.ts
├── tsconfig.json
├── tsconfig.test.json
├── tsconfig.tests.json
├── tsup.config.ts
├── vitest.config.ts
└── XcodeBuildMCP.code-workspace
```

# Files

--------------------------------------------------------------------------------
/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for key_sequence plugin
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
  createMockExecutor,
  createNoopExecutor,
  mockProcess,
} from '../../../../test-utils/mock-executors.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import keySequencePlugin, { key_sequenceLogic } from '../key_sequence.ts';

describe('Key Sequence Plugin', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(keySequencePlugin.name).toBe('key_sequence');
    });

    it('should have correct description', () => {
      expect(keySequencePlugin.description).toBe(
        'Press key sequence using HID keycodes on iOS simulator with configurable delay',
      );
    });

    it('should have handler function', () => {
      expect(typeof keySequencePlugin.handler).toBe('function');
    });

    it('should expose public schema without simulatorId field', () => {
      const schema = z.object(keySequencePlugin.schema);

      expect(schema.safeParse({ keyCodes: [40, 42, 44] }).success).toBe(true);
      expect(schema.safeParse({ keyCodes: [40], delay: 0.1 }).success).toBe(true);
      expect(schema.safeParse({ keyCodes: [] }).success).toBe(false);
      expect(schema.safeParse({ keyCodes: [-1] }).success).toBe(false);
      expect(schema.safeParse({ keyCodes: [256] }).success).toBe(false);
      expect(schema.safeParse({ keyCodes: [40], delay: -0.1 }).success).toBe(false);

      const withSimId = schema.safeParse({
        simulatorId: '12345678-1234-4234-8234-123456789012',
        keyCodes: [40],
      });
      expect(withSimId.success).toBe(true);
      expect('simulatorId' in (withSimId.data as any)).toBe(false);

      expect(schema.safeParse({}).success).toBe(false);
    });
  });

  describe('Handler Requirements', () => {
    it('should require simulatorId session default when not provided', async () => {
      const result = await keySequencePlugin.handler({ keyCodes: [40] });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Missing required session defaults');
      expect(message).toContain('simulatorId is required');
      expect(message).toContain('session-set-defaults');
    });

    it('should surface validation errors once simulator defaults exist', async () => {
      sessionStore.setDefaults({ simulatorId: '12345678-1234-4234-8234-123456789012' });

      const result = await keySequencePlugin.handler({ keyCodes: [] });

      expect(result.isError).toBe(true);
      const message = result.content[0].text;
      expect(message).toContain('Parameter validation failed');
      expect(message).toContain('keyCodes: At least one key code required');
    });
  });

  describe('Command Generation', () => {
    it('should generate correct axe command for basic key sequence', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'key sequence completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40, 42, 44],
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'key-sequence',
        '--keycodes',
        '40,42,44',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for key sequence with delay', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'key sequence completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [58, 59, 60],
          delay: 0.5,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'key-sequence',
        '--keycodes',
        '58,59,60',
        '--delay',
        '0.5',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command for single key in sequence', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'key sequence completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [255],
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/usr/local/bin/axe',
        'key-sequence',
        '--keycodes',
        '255',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });

    it('should generate correct axe command with bundled axe path', async () => {
      let capturedCommand: string[] = [];
      const trackingExecutor = async (command: string[]) => {
        capturedCommand = command;
        return {
          success: true,
          output: 'key sequence completed',
          error: undefined,
          process: mockProcess,
        };
      };

      const mockAxeHelpers = {
        getAxePath: () => '/path/to/bundled/axe',
        getBundledAxeEnvironment: () => ({ AXE_PATH: '/some/path' }),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [0, 1, 2, 3, 4],
          delay: 1.0,
        },
        trackingExecutor,
        mockAxeHelpers,
      );

      expect(capturedCommand).toEqual([
        '/path/to/bundled/axe',
        'key-sequence',
        '--keycodes',
        '0,1,2,3,4',
        '--delay',
        '1',
        '--udid',
        '12345678-1234-4234-8234-123456789012',
      ]);
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    it('should surface session default requirement when simulatorId is missing', async () => {
      const result = await keySequencePlugin.handler({ keyCodes: [40] });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('simulatorId is required');
    });

    it('should return success for valid key sequence execution', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Key sequence executed',
        error: undefined,
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40, 42, 44],
          delay: 0.1,
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          { type: 'text' as const, text: 'Key sequence [40,42,44] executed successfully.' },
        ],
        isError: false,
      });
    });

    it('should return success for key sequence without delay', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'Key sequence executed',
        error: undefined,
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40],
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [{ type: 'text' as const, text: 'Key sequence [40] executed successfully.' }],
        isError: false,
      });
    });

    it('should handle DependencyError when axe binary not found', async () => {
      const mockAxeHelpers = {
        getAxePath: () => null,
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40],
        },
        createNoopExecutor(),
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
          },
        ],
        isError: true,
      });
    });

    it('should handle AxeError from command execution', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Simulator not found',
      });

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40],
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: "Error: Failed to execute key sequence: axe command 'key-sequence' failed.\nDetails: Simulator not found",
          },
        ],
        isError: true,
      });
    });

    it('should handle SystemError from command execution', async () => {
      const mockExecutor = () => {
        throw new Error('ENOENT: no such file or directory');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40],
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result.content[0].text).toMatch(
        /^Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory/,
      );
      expect(result.isError).toBe(true);
    });

    it('should handle unexpected Error objects', async () => {
      const mockExecutor = () => {
        throw new Error('Unexpected error');
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40],
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result.content[0].text).toMatch(
        /^Error: System error executing axe: Failed to execute axe command: Unexpected error/,
      );
      expect(result.isError).toBe(true);
    });

    it('should handle unexpected string errors', async () => {
      const mockExecutor = () => {
        throw 'String error';
      };

      const mockAxeHelpers = {
        getAxePath: () => '/usr/local/bin/axe',
        getBundledAxeEnvironment: () => ({}),
        createAxeNotAvailableResponse: () => ({
          content: [
            {
              type: 'text' as const,
              text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nIf you installed via Smithery, ensure bundled artifacts are included or PATH is configured.',
            },
          ],
          isError: true,
        }),
      };

      const result = await key_sequenceLogic(
        {
          simulatorId: '12345678-1234-4234-8234-123456789012',
          keyCodes: [40],
        },
        mockExecutor,
        mockAxeHelpers,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text' as const,
            text: 'Error: System error executing axe: Failed to execute axe command: String error',
          },
        ],
        isError: true,
      });
    });
  });
});

```

--------------------------------------------------------------------------------
/src/core/generated-plugins.ts:
--------------------------------------------------------------------------------

```typescript
// AUTO-GENERATED - DO NOT EDIT
// This file is generated by the plugin discovery esbuild plugin

// Generated based on filesystem scan
export const WORKFLOW_LOADERS = {
  debugging: async () => {
    const { workflow } = await import('../mcp/tools/debugging/index.ts');
    const tool_0 = await import('../mcp/tools/debugging/debug_attach_sim.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/debugging/debug_breakpoint_add.ts').then(
      (m) => m.default,
    );
    const tool_2 = await import('../mcp/tools/debugging/debug_breakpoint_remove.ts').then(
      (m) => m.default,
    );
    const tool_3 = await import('../mcp/tools/debugging/debug_continue.ts').then((m) => m.default);
    const tool_4 = await import('../mcp/tools/debugging/debug_detach.ts').then((m) => m.default);
    const tool_5 = await import('../mcp/tools/debugging/debug_lldb_command.ts').then(
      (m) => m.default,
    );
    const tool_6 = await import('../mcp/tools/debugging/debug_stack.ts').then((m) => m.default);
    const tool_7 = await import('../mcp/tools/debugging/debug_variables.ts').then((m) => m.default);

    return {
      workflow,
      debug_attach_sim: tool_0,
      debug_breakpoint_add: tool_1,
      debug_breakpoint_remove: tool_2,
      debug_continue: tool_3,
      debug_detach: tool_4,
      debug_lldb_command: tool_5,
      debug_stack: tool_6,
      debug_variables: tool_7,
    };
  },
  device: async () => {
    const { workflow } = await import('../mcp/tools/device/index.ts');
    const tool_0 = await import('../mcp/tools/device/build_device.ts').then((m) => m.default);
    const tool_1 = await import('../mcp/tools/device/clean.ts').then((m) => m.default);
    const tool_2 = await import('../mcp/tools/device/discover_projs.ts').then((m) => m.default);
    const tool_3 = await import('../mcp/tools/device/get_app_bundle_id.ts').then((m) => m.default);
    const tool_4 = await import('../mcp/tools/device/get_device_app_path.ts').then(
      (m) => m.default,
    );
    const tool_5 = await import('../mcp/tools/device/install_app_device.ts').then((m) => m.default);
    const tool_6 = await import('../mcp/tools/device/launch_app_device.ts').then((m) => m.default);
    const tool_7 = await import('../mcp/tools/device/list_devices.ts').then((m) => m.default);
    const tool_8 = await import('../mcp/tools/device/list_schemes.ts').then((m) => m.default);
    const tool_9 = await import('../mcp/tools/device/show_build_settings.ts').then(
      (m) => m.default,
    );
    const tool_10 = await import('../mcp/tools/device/start_device_log_cap.ts').then(
      (m) => m.default,
    );
    const tool_11 = await import('../mcp/tools/device/stop_app_device.ts').then((m) => m.default);
    const tool_12 = await import('../mcp/tools/device/stop_device_log_cap.ts').then(
      (m) => m.default,
    );
    const tool_13 = await import('../mcp/tools/device/test_device.ts').then((m) => m.default);

    return {
      workflow,
      build_device: tool_0,
      clean: tool_1,
      discover_projs: tool_2,
      get_app_bundle_id: tool_3,
      get_device_app_path: tool_4,
      install_app_device: tool_5,
      launch_app_device: tool_6,
      list_devices: tool_7,
      list_schemes: tool_8,
      show_build_settings: tool_9,
      start_device_log_cap: tool_10,
      stop_app_device: tool_11,
      stop_device_log_cap: tool_12,
      test_device: tool_13,
    };
  },
  doctor: async () => {
    const { workflow } = await import('../mcp/tools/doctor/index.ts');
    const tool_0 = await import('../mcp/tools/doctor/doctor.ts').then((m) => m.default);

    return {
      workflow,
      doctor: tool_0,
    };
  },
  logging: async () => {
    const { workflow } = await import('../mcp/tools/logging/index.ts');
    const tool_0 = await import('../mcp/tools/logging/start_device_log_cap.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/logging/start_sim_log_cap.ts').then((m) => m.default);
    const tool_2 = await import('../mcp/tools/logging/stop_device_log_cap.ts').then(
      (m) => m.default,
    );
    const tool_3 = await import('../mcp/tools/logging/stop_sim_log_cap.ts').then((m) => m.default);

    return {
      workflow,
      start_device_log_cap: tool_0,
      start_sim_log_cap: tool_1,
      stop_device_log_cap: tool_2,
      stop_sim_log_cap: tool_3,
    };
  },
  macos: async () => {
    const { workflow } = await import('../mcp/tools/macos/index.ts');
    const tool_0 = await import('../mcp/tools/macos/build_macos.ts').then((m) => m.default);
    const tool_1 = await import('../mcp/tools/macos/build_run_macos.ts').then((m) => m.default);
    const tool_2 = await import('../mcp/tools/macos/clean.ts').then((m) => m.default);
    const tool_3 = await import('../mcp/tools/macos/discover_projs.ts').then((m) => m.default);
    const tool_4 = await import('../mcp/tools/macos/get_mac_app_path.ts').then((m) => m.default);
    const tool_5 = await import('../mcp/tools/macos/get_mac_bundle_id.ts').then((m) => m.default);
    const tool_6 = await import('../mcp/tools/macos/launch_mac_app.ts').then((m) => m.default);
    const tool_7 = await import('../mcp/tools/macos/list_schemes.ts').then((m) => m.default);
    const tool_8 = await import('../mcp/tools/macos/show_build_settings.ts').then((m) => m.default);
    const tool_9 = await import('../mcp/tools/macos/stop_mac_app.ts').then((m) => m.default);
    const tool_10 = await import('../mcp/tools/macos/test_macos.ts').then((m) => m.default);

    return {
      workflow,
      build_macos: tool_0,
      build_run_macos: tool_1,
      clean: tool_2,
      discover_projs: tool_3,
      get_mac_app_path: tool_4,
      get_mac_bundle_id: tool_5,
      launch_mac_app: tool_6,
      list_schemes: tool_7,
      show_build_settings: tool_8,
      stop_mac_app: tool_9,
      test_macos: tool_10,
    };
  },
  'project-discovery': async () => {
    const { workflow } = await import('../mcp/tools/project-discovery/index.ts');
    const tool_0 = await import('../mcp/tools/project-discovery/discover_projs.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/project-discovery/get_app_bundle_id.ts').then(
      (m) => m.default,
    );
    const tool_2 = await import('../mcp/tools/project-discovery/get_mac_bundle_id.ts').then(
      (m) => m.default,
    );
    const tool_3 = await import('../mcp/tools/project-discovery/list_schemes.ts').then(
      (m) => m.default,
    );
    const tool_4 = await import('../mcp/tools/project-discovery/show_build_settings.ts').then(
      (m) => m.default,
    );

    return {
      workflow,
      discover_projs: tool_0,
      get_app_bundle_id: tool_1,
      get_mac_bundle_id: tool_2,
      list_schemes: tool_3,
      show_build_settings: tool_4,
    };
  },
  'project-scaffolding': async () => {
    const { workflow } = await import('../mcp/tools/project-scaffolding/index.ts');
    const tool_0 = await import('../mcp/tools/project-scaffolding/scaffold_ios_project.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/project-scaffolding/scaffold_macos_project.ts').then(
      (m) => m.default,
    );

    return {
      workflow,
      scaffold_ios_project: tool_0,
      scaffold_macos_project: tool_1,
    };
  },
  'session-management': async () => {
    const { workflow } = await import('../mcp/tools/session-management/index.ts');
    const tool_0 = await import('../mcp/tools/session-management/session_clear_defaults.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/session-management/session_set_defaults.ts').then(
      (m) => m.default,
    );
    const tool_2 = await import('../mcp/tools/session-management/session_show_defaults.ts').then(
      (m) => m.default,
    );

    return {
      workflow,
      session_clear_defaults: tool_0,
      session_set_defaults: tool_1,
      session_show_defaults: tool_2,
    };
  },
  simulator: async () => {
    const { workflow } = await import('../mcp/tools/simulator/index.ts');
    const tool_0 = await import('../mcp/tools/simulator/boot_sim.ts').then((m) => m.default);
    const tool_1 = await import('../mcp/tools/simulator/build_run_sim.ts').then((m) => m.default);
    const tool_2 = await import('../mcp/tools/simulator/build_sim.ts').then((m) => m.default);
    const tool_3 = await import('../mcp/tools/simulator/clean.ts').then((m) => m.default);
    const tool_4 = await import('../mcp/tools/simulator/describe_ui.ts').then((m) => m.default);
    const tool_5 = await import('../mcp/tools/simulator/discover_projs.ts').then((m) => m.default);
    const tool_6 = await import('../mcp/tools/simulator/get_app_bundle_id.ts').then(
      (m) => m.default,
    );
    const tool_7 = await import('../mcp/tools/simulator/get_sim_app_path.ts').then(
      (m) => m.default,
    );
    const tool_8 = await import('../mcp/tools/simulator/install_app_sim.ts').then((m) => m.default);
    const tool_9 = await import('../mcp/tools/simulator/launch_app_logs_sim.ts').then(
      (m) => m.default,
    );
    const tool_10 = await import('../mcp/tools/simulator/launch_app_sim.ts').then((m) => m.default);
    const tool_11 = await import('../mcp/tools/simulator/list_schemes.ts').then((m) => m.default);
    const tool_12 = await import('../mcp/tools/simulator/list_sims.ts').then((m) => m.default);
    const tool_13 = await import('../mcp/tools/simulator/open_sim.ts').then((m) => m.default);
    const tool_14 = await import('../mcp/tools/simulator/record_sim_video.ts').then(
      (m) => m.default,
    );
    const tool_15 = await import('../mcp/tools/simulator/screenshot.ts').then((m) => m.default);
    const tool_16 = await import('../mcp/tools/simulator/show_build_settings.ts').then(
      (m) => m.default,
    );
    const tool_17 = await import('../mcp/tools/simulator/stop_app_sim.ts').then((m) => m.default);
    const tool_18 = await import('../mcp/tools/simulator/test_sim.ts').then((m) => m.default);

    return {
      workflow,
      boot_sim: tool_0,
      build_run_sim: tool_1,
      build_sim: tool_2,
      clean: tool_3,
      describe_ui: tool_4,
      discover_projs: tool_5,
      get_app_bundle_id: tool_6,
      get_sim_app_path: tool_7,
      install_app_sim: tool_8,
      launch_app_logs_sim: tool_9,
      launch_app_sim: tool_10,
      list_schemes: tool_11,
      list_sims: tool_12,
      open_sim: tool_13,
      record_sim_video: tool_14,
      screenshot: tool_15,
      show_build_settings: tool_16,
      stop_app_sim: tool_17,
      test_sim: tool_18,
    };
  },
  'simulator-management': async () => {
    const { workflow } = await import('../mcp/tools/simulator-management/index.ts');
    const tool_0 = await import('../mcp/tools/simulator-management/boot_sim.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/simulator-management/erase_sims.ts').then(
      (m) => m.default,
    );
    const tool_2 = await import('../mcp/tools/simulator-management/list_sims.ts').then(
      (m) => m.default,
    );
    const tool_3 = await import('../mcp/tools/simulator-management/open_sim.ts').then(
      (m) => m.default,
    );
    const tool_4 = await import('../mcp/tools/simulator-management/reset_sim_location.ts').then(
      (m) => m.default,
    );
    const tool_5 = await import('../mcp/tools/simulator-management/set_sim_appearance.ts').then(
      (m) => m.default,
    );
    const tool_6 = await import('../mcp/tools/simulator-management/set_sim_location.ts').then(
      (m) => m.default,
    );
    const tool_7 = await import('../mcp/tools/simulator-management/sim_statusbar.ts').then(
      (m) => m.default,
    );

    return {
      workflow,
      boot_sim: tool_0,
      erase_sims: tool_1,
      list_sims: tool_2,
      open_sim: tool_3,
      reset_sim_location: tool_4,
      set_sim_appearance: tool_5,
      set_sim_location: tool_6,
      sim_statusbar: tool_7,
    };
  },
  'swift-package': async () => {
    const { workflow } = await import('../mcp/tools/swift-package/index.ts');
    const tool_0 = await import('../mcp/tools/swift-package/swift_package_build.ts').then(
      (m) => m.default,
    );
    const tool_1 = await import('../mcp/tools/swift-package/swift_package_clean.ts').then(
      (m) => m.default,
    );
    const tool_2 = await import('../mcp/tools/swift-package/swift_package_list.ts').then(
      (m) => m.default,
    );
    const tool_3 = await import('../mcp/tools/swift-package/swift_package_run.ts').then(
      (m) => m.default,
    );
    const tool_4 = await import('../mcp/tools/swift-package/swift_package_stop.ts').then(
      (m) => m.default,
    );
    const tool_5 = await import('../mcp/tools/swift-package/swift_package_test.ts').then(
      (m) => m.default,
    );

    return {
      workflow,
      swift_package_build: tool_0,
      swift_package_clean: tool_1,
      swift_package_list: tool_2,
      swift_package_run: tool_3,
      swift_package_stop: tool_4,
      swift_package_test: tool_5,
    };
  },
  'ui-testing': async () => {
    const { workflow } = await import('../mcp/tools/ui-testing/index.ts');
    const tool_0 = await import('../mcp/tools/ui-testing/button.ts').then((m) => m.default);
    const tool_1 = await import('../mcp/tools/ui-testing/describe_ui.ts').then((m) => m.default);
    const tool_2 = await import('../mcp/tools/ui-testing/gesture.ts').then((m) => m.default);
    const tool_3 = await import('../mcp/tools/ui-testing/key_press.ts').then((m) => m.default);
    const tool_4 = await import('../mcp/tools/ui-testing/key_sequence.ts').then((m) => m.default);
    const tool_5 = await import('../mcp/tools/ui-testing/long_press.ts').then((m) => m.default);
    const tool_6 = await import('../mcp/tools/ui-testing/screenshot.ts').then((m) => m.default);
    const tool_7 = await import('../mcp/tools/ui-testing/swipe.ts').then((m) => m.default);
    const tool_8 = await import('../mcp/tools/ui-testing/tap.ts').then((m) => m.default);
    const tool_9 = await import('../mcp/tools/ui-testing/touch.ts').then((m) => m.default);
    const tool_10 = await import('../mcp/tools/ui-testing/type_text.ts').then((m) => m.default);

    return {
      workflow,
      button: tool_0,
      describe_ui: tool_1,
      gesture: tool_2,
      key_press: tool_3,
      key_sequence: tool_4,
      long_press: tool_5,
      screenshot: tool_6,
      swipe: tool_7,
      tap: tool_8,
      touch: tool_9,
      type_text: tool_10,
    };
  },
  utilities: async () => {
    const { workflow } = await import('../mcp/tools/utilities/index.ts');
    const tool_0 = await import('../mcp/tools/utilities/clean.ts').then((m) => m.default);

    return {
      workflow,
      clean: tool_0,
    };
  },
};

export type WorkflowName = keyof typeof WORKFLOW_LOADERS;

// Optional: Export workflow metadata for quick access
export const WORKFLOW_METADATA = {
  debugging: {
    name: 'Simulator Debugging',
    description:
      'Interactive iOS Simulator debugging tools: attach LLDB, manage breakpoints, inspect stack/variables, and run LLDB commands.',
  },
  device: {
    name: 'iOS Device Development',
    description:
      'Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Build, test, deploy, and debug apps on real hardware.',
  },
  doctor: {
    name: 'System Doctor',
    description:
      'Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability.',
  },
  logging: {
    name: 'Log Capture & Management',
    description:
      'Log capture and management tools for iOS simulators and physical devices. Start, stop, and analyze application and system logs during development and testing.',
  },
  macos: {
    name: 'macOS Development',
    description:
      'Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications.',
  },
  'project-discovery': {
    name: 'Project Discovery',
    description:
      'Discover and examine Xcode projects, workspaces, and Swift packages. Analyze project structure, schemes, build settings, and bundle information.',
  },
  'project-scaffolding': {
    name: 'Project Scaffolding',
    description:
      'Tools for creating new iOS and macOS projects from templates. Bootstrap new applications with best practices, standard configurations, and modern project structures.',
  },
  'session-management': {
    name: 'session-management',
    description:
      'Manage session defaults for projectPath/workspacePath, scheme, configuration, simulatorName/simulatorId, deviceId, useLatestOS and arch. These defaults are required by many tools and must be set before attempting to call tools that would depend on these values.',
  },
  simulator: {
    name: 'iOS Simulator Development',
    description:
      'Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators.',
  },
  'simulator-management': {
    name: 'Simulator Management',
    description:
      'Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance.',
  },
  'swift-package': {
    name: 'Swift Package Manager',
    description:
      'Swift Package Manager operations for building, testing, running, and managing Swift packages and dependencies. Complete SPM workflow support.',
  },
  'ui-testing': {
    name: 'UI Testing & Automation',
    description:
      'UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows.',
  },
  utilities: {
    name: 'Project Utilities',
    description:
      'Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files.',
  },
};

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for build_run_sim plugin (unified)
 * Following CLAUDE.md testing standards with dependency injection and literal validation
 */

import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
  createMockExecutor,
  createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
import { sessionStore } from '../../../../utils/session-store.ts';
import buildRunSim, { build_run_simLogic } from '../build_run_sim.ts';

describe('build_run_sim tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(buildRunSim.name).toBe('build_run_sim');
    });

    it('should have correct description', () => {
      expect(buildRunSim.description).toBe('Builds and runs an app on an iOS simulator.');
    });

    it('should have handler function', () => {
      expect(typeof buildRunSim.handler).toBe('function');
    });

    it('should expose only non-session fields in public schema', () => {
      const schema = z.object(buildRunSim.schema);

      expect(schema.safeParse({}).success).toBe(true);

      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived',
          extraArgs: ['--verbose'],
          preferXcodebuild: false,
        }).success,
      ).toBe(true);

      expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false);
      expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);

      const schemaKeys = Object.keys(buildRunSim.schema).sort();
      expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort());
      expect(schemaKeys).not.toContain('scheme');
      expect(schemaKeys).not.toContain('simulatorName');
      expect(schemaKeys).not.toContain('projectPath');
    });
  });

  describe('Handler Behavior (Complete Literal Returns)', () => {
    // Note: Parameter validation is now handled by createTypedTool wrapper with Zod schema
    // The logic function receives validated parameters, so these tests focus on business logic

    it('should handle simulator not found', async () => {
      let callCount = 0;
      const mockExecutor: CommandExecutor = async (command) => {
        callCount++;
        if (callCount === 1) {
          // First call: build succeeds
          return createMockCommandResponse({
            success: true,
            output: 'BUILD SUCCEEDED',
          });
        } else if (callCount === 2) {
          // Second call: showBuildSettings fails to get app path
          return createMockCommandResponse({
            success: false,
            error: 'Could not get build settings',
          });
        }
        return createMockCommandResponse({
          success: false,
          error: 'Unexpected call',
        });
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: 'Build succeeded, but failed to get app path: Could not get build settings',
          },
        ],
        isError: true,
      });
    });

    it('should handle build failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed with error',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
    });

    it('should handle successful build and run', async () => {
      // Create a mock executor that simulates full successful flow
      let callCount = 0;
      const mockExecutor: CommandExecutor = async (command) => {
        callCount++;

        if (command.includes('xcodebuild') && command.includes('build')) {
          // First call: build succeeds
          return createMockCommandResponse({
            success: true,
            output: 'BUILD SUCCEEDED',
          });
        } else if (command.includes('xcodebuild') && command.includes('-showBuildSettings')) {
          // Second call: build settings to get app path
          return createMockCommandResponse({
            success: true,
            output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app\n',
          });
        } else if (command.includes('simctl') && command.includes('list')) {
          // Find simulator calls
          return createMockCommandResponse({
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 16.0': [
                  {
                    udid: 'test-uuid-123',
                    name: 'iPhone 16',
                    state: 'Booted',
                    isAvailable: true,
                  },
                ],
              },
            }),
          });
        } else if (
          command.includes('plutil') ||
          command.includes('PlistBuddy') ||
          command.includes('defaults')
        ) {
          // Bundle ID extraction
          return createMockCommandResponse({
            success: true,
            output: 'com.example.MyApp',
          });
        } else {
          // All other commands (boot, open, install, launch) succeed
          return createMockCommandResponse({
            success: true,
            output: 'Success',
          });
        }
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBe(false);
    });

    it('should handle exception with Error object', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Command failed',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
    });

    it('should handle exception with string error', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'String error',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toBeDefined();
      expect(Array.isArray(result.content)).toBe(true);
    });
  });

  describe('Command Generation', () => {
    it('should generate correct simctl list command with minimal parameters', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate the initial build command
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command after finding simulator', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      let callCount = 0;
      // Create tracking executor that succeeds on first call (list) and fails on second
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        callCount++;

        if (callCount === 1) {
          // First call: simulator list succeeds
          return createMockCommandResponse({
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 16.0': [
                  {
                    udid: 'test-uuid-123',
                    name: 'iPhone 16',
                    state: 'Booted',
                  },
                ],
              },
            }),
            error: undefined,
          });
        } else {
          // Second call: build command fails to stop execution
          return createMockCommandResponse({
            success: false,
            output: '',
            error: 'Test error to stop execution',
          });
        }
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate build command and then build settings command
      expect(callHistory).toHaveLength(2);

      // First call: build command
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');

      // Second call: build settings command to get app path
      expect(callHistory[1].command).toEqual([
        'xcodebuild',
        '-showBuildSettings',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
      ]);
      expect(callHistory[1].logPrefix).toBe('Get App Path');
    });

    it('should generate correct build settings command after successful build', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      let callCount = 0;
      // Create tracking executor that succeeds on first two calls and fails on third
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        callCount++;

        if (callCount === 1) {
          // First call: simulator list succeeds
          return createMockCommandResponse({
            success: true,
            output: JSON.stringify({
              devices: {
                'iOS 16.0': [
                  {
                    udid: 'test-uuid-123',
                    name: 'iPhone 16',
                    state: 'Booted',
                  },
                ],
              },
            }),
            error: undefined,
          });
        } else if (callCount === 2) {
          // Second call: build command succeeds
          return createMockCommandResponse({
            success: true,
            output: 'BUILD SUCCEEDED',
            error: undefined,
          });
        } else {
          // Third call: build settings command fails to stop execution
          return createMockCommandResponse({
            success: false,
            output: '',
            error: 'Test error to stop execution',
          });
        }
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          configuration: 'Release',
          useLatestOS: false,
        },
        trackingExecutor,
      );

      // Should generate build command and build settings command
      expect(callHistory).toHaveLength(2);

      // First call: build command
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');

      // Second call: build settings command
      expect(callHistory[1].command).toEqual([
        'xcodebuild',
        '-showBuildSettings',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16',
      ]);
      expect(callHistory[1].logPrefix).toBe('Get App Path');
    });

    it('should handle paths with spaces in command generation', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_run_simLogic(
        {
          workspacePath: '/Users/dev/My Project/MyProject.xcworkspace',
          scheme: 'My Scheme',
          simulatorName: 'iPhone 16 Pro',
        },
        trackingExecutor,
      );

      // Should generate build command first
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/Users/dev/My Project/MyProject.xcworkspace',
        '-scheme',
        'My Scheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });
  });

  describe('XOR Validation', () => {
    it('should error when neither projectPath nor workspacePath provided', async () => {
      const result = await buildRunSim.handler({
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('Provide a project or workspace');
    });

    it('should error when both projectPath and workspacePath provided', async () => {
      const result = await buildRunSim.handler({
        projectPath: '/path/project.xcodeproj',
        workspacePath: '/path/workspace.xcworkspace',
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain('workspacePath');
    });

    it('should succeed with only projectPath', async () => {
      // This test fails early due to build failure, which is expected behavior
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed',
      });

      const result = await build_run_simLogic(
        {
          projectPath: '/path/project.xcodeproj',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );
      // The test succeeds if the logic function accepts the parameters and attempts to build
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Build failed');
    });

    it('should succeed with only workspacePath', async () => {
      // This test fails early due to build failure, which is expected behavior
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'Build failed',
      });

      const result = await build_run_simLogic(
        {
          workspacePath: '/path/workspace.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );
      // The test succeeds if the logic function accepts the parameters and attempts to build
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Build failed');
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/build_run_sim.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Simulator Build & Run Plugin: Build Run Simulator (Unified)
 *
 * Builds and runs an app from a project or workspace on a specific simulator by UUID or name.
 * Accepts mutually exclusive `projectPath` or `workspacePath`.
 * Accepts mutually exclusive `simulatorId` or `simulatorName`.
 */

import * as z from 'zod';
import { ToolResponse, SharedBuildParams, XcodePlatform } from '../../../types/common.ts';
import { log } from '../../../utils/logging/index.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import { createTextResponse } from '../../../utils/responses/index.ts';
import { executeXcodeBuildCommand } from '../../../utils/build/index.ts';
import type { CommandExecutor } from '../../../utils/execution/index.ts';
import { determineSimulatorUuid } from '../../../utils/simulator-utils.ts';
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts';

// Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName
const baseOptions = {
  scheme: z.string().describe('The scheme to use (Required)'),
  simulatorId: z
    .string()
    .optional()
    .describe(
      'UUID of the simulator (from list_sims). Provide EITHER this OR simulatorName, not both',
    ),
  simulatorName: z
    .string()
    .optional()
    .describe(
      "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both",
    ),
  configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
  derivedDataPath: z
    .string()
    .optional()
    .describe('Path where build products and other derived data will go'),
  extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'),
  useLatestOS: z
    .boolean()
    .optional()
    .describe('Whether to use the latest OS version for the named simulator'),
  preferXcodebuild: z
    .boolean()
    .optional()
    .describe(
      'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.',
    ),
};

const baseSchemaObject = z.object({
  projectPath: z
    .string()
    .optional()
    .describe('Path to .xcodeproj file. Provide EITHER this OR workspacePath, not both'),
  workspacePath: z
    .string()
    .optional()
    .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'),
  ...baseOptions,
});

const buildRunSimulatorSchema = z.preprocess(
  nullifyEmptyStrings,
  baseSchemaObject
    .refine((val) => val.projectPath !== undefined || val.workspacePath !== undefined, {
      message: 'Either projectPath or workspacePath is required.',
    })
    .refine((val) => !(val.projectPath !== undefined && val.workspacePath !== undefined), {
      message: 'projectPath and workspacePath are mutually exclusive. Provide only one.',
    })
    .refine((val) => val.simulatorId !== undefined || val.simulatorName !== undefined, {
      message: 'Either simulatorId or simulatorName is required.',
    })
    .refine((val) => !(val.simulatorId !== undefined && val.simulatorName !== undefined), {
      message: 'simulatorId and simulatorName are mutually exclusive. Provide only one.',
    }),
);

export type BuildRunSimulatorParams = z.infer<typeof buildRunSimulatorSchema>;

// Internal logic for building Simulator apps.
async function _handleSimulatorBuildLogic(
  params: BuildRunSimulatorParams,
  executor: CommandExecutor,
  executeXcodeBuildCommandFn: typeof executeXcodeBuildCommand = executeXcodeBuildCommand,
): Promise<ToolResponse> {
  const projectType = params.projectPath ? 'project' : 'workspace';
  const filePath = params.projectPath ?? params.workspacePath;

  // Log warning if useLatestOS is provided with simulatorId
  if (params.simulatorId && params.useLatestOS !== undefined) {
    log(
      'warning',
      `useLatestOS parameter is ignored when using simulatorId (UUID implies exact device/OS)`,
    );
  }

  log(
    'info',
    `Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`,
  );

  // Create SharedBuildParams object with required configuration property
  const sharedBuildParams: SharedBuildParams = {
    workspacePath: params.workspacePath,
    projectPath: params.projectPath,
    scheme: params.scheme,
    configuration: params.configuration ?? 'Debug',
    derivedDataPath: params.derivedDataPath,
    extraArgs: params.extraArgs,
  };

  return executeXcodeBuildCommandFn(
    sharedBuildParams,
    {
      platform: XcodePlatform.iOSSimulator,
      simulatorId: params.simulatorId,
      simulatorName: params.simulatorName,
      useLatestOS: params.simulatorId ? false : params.useLatestOS,
      logPrefix: 'iOS Simulator Build',
    },
    params.preferXcodebuild as boolean,
    'build',
    executor,
  );
}

// Exported business logic function for building and running iOS Simulator apps.
export async function build_run_simLogic(
  params: BuildRunSimulatorParams,
  executor: CommandExecutor,
  executeXcodeBuildCommandFn: typeof executeXcodeBuildCommand = executeXcodeBuildCommand,
): Promise<ToolResponse> {
  const projectType = params.projectPath ? 'project' : 'workspace';
  const filePath = params.projectPath ?? params.workspacePath;

  log(
    'info',
    `Starting iOS Simulator build and run for scheme ${params.scheme} from ${projectType}: ${filePath}`,
  );

  try {
    // --- Build Step ---
    const buildResult = await _handleSimulatorBuildLogic(
      params,
      executor,
      executeXcodeBuildCommandFn,
    );

    if (buildResult.isError) {
      return buildResult; // Return the build error
    }

    // --- Get App Path Step ---
    // Create the command array for xcodebuild with -showBuildSettings option
    const command = ['xcodebuild', '-showBuildSettings'];

    // Add the workspace or project
    if (params.workspacePath) {
      command.push('-workspace', params.workspacePath);
    } else if (params.projectPath) {
      command.push('-project', params.projectPath);
    }

    // Add the scheme and configuration
    command.push('-scheme', params.scheme);
    command.push('-configuration', params.configuration ?? 'Debug');

    // Handle destination for simulator
    let destinationString: string;
    if (params.simulatorId) {
      destinationString = `platform=iOS Simulator,id=${params.simulatorId}`;
    } else if (params.simulatorName) {
      destinationString = `platform=iOS Simulator,name=${params.simulatorName}${(params.useLatestOS ?? true) ? ',OS=latest' : ''}`;
    } else {
      // This shouldn't happen due to validation, but handle it
      destinationString = 'platform=iOS Simulator';
    }
    command.push('-destination', destinationString);

    // Add derived data path if provided
    if (params.derivedDataPath) {
      command.push('-derivedDataPath', params.derivedDataPath);
    }

    // Add extra args if provided
    if (params.extraArgs && params.extraArgs.length > 0) {
      command.push(...params.extraArgs);
    }

    // Execute the command directly
    const result = await executor(command, 'Get App Path', true, undefined);

    // If there was an error with the command execution, return it
    if (!result.success) {
      return createTextResponse(
        `Build succeeded, but failed to get app path: ${result.error ?? 'Unknown error'}`,
        true,
      );
    }

    // Parse the output to extract the app path
    const buildSettingsOutput = result.output;

    // Try both approaches to get app path - first the project approach (CODESIGNING_FOLDER_PATH)
    let appBundlePath: string | null = null;

    // Project approach: Extract CODESIGNING_FOLDER_PATH from build settings to get app path
    const appPathMatch = buildSettingsOutput.match(/CODESIGNING_FOLDER_PATH = (.+\.app)/);
    if (appPathMatch?.[1]) {
      appBundlePath = appPathMatch[1].trim();
    } else {
      // Workspace approach: Extract BUILT_PRODUCTS_DIR and FULL_PRODUCT_NAME
      const builtProductsDirMatch = buildSettingsOutput.match(
        /^\s*BUILT_PRODUCTS_DIR\s*=\s*(.+)$/m,
      );
      const fullProductNameMatch = buildSettingsOutput.match(/^\s*FULL_PRODUCT_NAME\s*=\s*(.+)$/m);

      if (builtProductsDirMatch && fullProductNameMatch) {
        const builtProductsDir = builtProductsDirMatch[1].trim();
        const fullProductName = fullProductNameMatch[1].trim();
        appBundlePath = `${builtProductsDir}/${fullProductName}`;
      }
    }

    if (!appBundlePath) {
      return createTextResponse(
        `Build succeeded, but could not find app path in build settings.`,
        true,
      );
    }

    log('info', `App bundle path for run: ${appBundlePath}`);

    // --- Find/Boot Simulator Step ---
    // Use our helper to determine the simulator UUID
    const uuidResult = await determineSimulatorUuid(
      { simulatorId: params.simulatorId, simulatorName: params.simulatorName },
      executor,
    );

    if (uuidResult.error) {
      return createTextResponse(`Build succeeded, but ${uuidResult.error.content[0].text}`, true);
    }

    if (uuidResult.warning) {
      log('warning', uuidResult.warning);
    }

    const simulatorId = uuidResult.uuid;

    if (!simulatorId) {
      return createTextResponse(
        'Build succeeded, but no simulator specified and failed to find a suitable one.',
        true,
      );
    }

    // Check simulator state and boot if needed
    try {
      log('info', `Checking simulator state for UUID: ${simulatorId}`);
      const simulatorListResult = await executor(
        ['xcrun', 'simctl', 'list', 'devices', 'available', '--json'],
        'List Simulators',
      );
      if (!simulatorListResult.success) {
        throw new Error(simulatorListResult.error ?? 'Failed to list simulators');
      }

      const simulatorsData = JSON.parse(simulatorListResult.output) as {
        devices: Record<string, unknown[]>;
      };
      let targetSimulator: { udid: string; name: string; state: string } | null = null;

      // Find the target simulator
      for (const runtime in simulatorsData.devices) {
        const devices = simulatorsData.devices[runtime];
        if (Array.isArray(devices)) {
          for (const device of devices) {
            if (
              typeof device === 'object' &&
              device !== null &&
              'udid' in device &&
              'name' in device &&
              'state' in device &&
              typeof device.udid === 'string' &&
              typeof device.name === 'string' &&
              typeof device.state === 'string' &&
              device.udid === simulatorId
            ) {
              targetSimulator = {
                udid: device.udid,
                name: device.name,
                state: device.state,
              };
              break;
            }
          }
          if (targetSimulator) break;
        }
      }

      if (!targetSimulator) {
        return createTextResponse(
          `Build succeeded, but could not find simulator with UUID: ${simulatorId}`,
          true,
        );
      }

      // Boot if needed
      if (targetSimulator.state !== 'Booted') {
        log('info', `Booting simulator ${targetSimulator.name}...`);
        const bootResult = await executor(
          ['xcrun', 'simctl', 'boot', simulatorId],
          'Boot Simulator',
        );
        if (!bootResult.success) {
          throw new Error(bootResult.error ?? 'Failed to boot simulator');
        }
      } else {
        log('info', `Simulator ${simulatorId} is already booted`);
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error checking/booting simulator: ${errorMessage}`);
      return createTextResponse(
        `Build succeeded, but error checking/booting simulator: ${errorMessage}`,
        true,
      );
    }

    // --- Open Simulator UI Step ---
    try {
      log('info', 'Opening Simulator app');
      const openResult = await executor(['open', '-a', 'Simulator'], 'Open Simulator App');
      if (!openResult.success) {
        throw new Error(openResult.error ?? 'Failed to open Simulator app');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('warning', `Warning: Could not open Simulator app: ${errorMessage}`);
      // Don't fail the whole operation for this
    }

    // --- Install App Step ---
    try {
      log('info', `Installing app at path: ${appBundlePath} to simulator: ${simulatorId}`);
      const installResult = await executor(
        ['xcrun', 'simctl', 'install', simulatorId, appBundlePath],
        'Install App',
      );
      if (!installResult.success) {
        throw new Error(installResult.error ?? 'Failed to install app');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error installing app: ${errorMessage}`);
      return createTextResponse(
        `Build succeeded, but error installing app on simulator: ${errorMessage}`,
        true,
      );
    }

    // --- Get Bundle ID Step ---
    let bundleId;
    try {
      log('info', `Extracting bundle ID from app: ${appBundlePath}`);

      // Try multiple methods to get bundle ID - first PlistBuddy, then plutil, then defaults
      let bundleIdResult = null;

      // Method 1: PlistBuddy (most reliable)
      try {
        bundleIdResult = await executor(
          [
            '/usr/libexec/PlistBuddy',
            '-c',
            'Print :CFBundleIdentifier',
            `${appBundlePath}/Info.plist`,
          ],
          'Get Bundle ID with PlistBuddy',
        );
        if (bundleIdResult.success) {
          bundleId = bundleIdResult.output.trim();
        }
      } catch {
        // Continue to next method
      }

      // Method 2: plutil (workspace approach)
      if (!bundleId) {
        try {
          bundleIdResult = await executor(
            ['plutil', '-extract', 'CFBundleIdentifier', 'raw', `${appBundlePath}/Info.plist`],
            'Get Bundle ID with plutil',
          );
          if (bundleIdResult?.success) {
            bundleId = bundleIdResult.output?.trim();
          }
        } catch {
          // Continue to next method
        }
      }

      // Method 3: defaults (fallback)
      if (!bundleId) {
        try {
          bundleIdResult = await executor(
            ['defaults', 'read', `${appBundlePath}/Info`, 'CFBundleIdentifier'],
            'Get Bundle ID with defaults',
          );
          if (bundleIdResult?.success) {
            bundleId = bundleIdResult.output?.trim();
          }
        } catch {
          // All methods failed
        }
      }

      if (!bundleId) {
        throw new Error('Could not extract bundle ID from Info.plist using any method');
      }

      log('info', `Bundle ID for run: ${bundleId}`);
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error getting bundle ID: ${errorMessage}`);
      return createTextResponse(
        `Build and install succeeded, but error getting bundle ID: ${errorMessage}`,
        true,
      );
    }

    // --- Launch App Step ---
    try {
      log('info', `Launching app with bundle ID: ${bundleId} on simulator: ${simulatorId}`);
      const launchResult = await executor(
        ['xcrun', 'simctl', 'launch', simulatorId, bundleId],
        'Launch App',
      );
      if (!launchResult.success) {
        throw new Error(launchResult.error ?? 'Failed to launch app');
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      log('error', `Error launching app: ${errorMessage}`);
      return createTextResponse(
        `Build and install succeeded, but error launching app on simulator: ${errorMessage}`,
        true,
      );
    }

    // --- Success ---
    log('info', '✅ iOS simulator build & run succeeded.');

    const target = params.simulatorId
      ? `simulator UUID '${params.simulatorId}'`
      : `simulator name '${params.simulatorName}'`;
    const sourceType = params.projectPath ? 'project' : 'workspace';
    const sourcePath = params.projectPath ?? params.workspacePath;

    return {
      content: [
        {
          type: 'text',
          text: `✅ iOS simulator build and run succeeded for scheme ${params.scheme} from ${sourceType} ${sourcePath} targeting ${target}.
          
The app (${bundleId}) is now running in the iOS Simulator. 
If you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open.

Next Steps:
- Option 1: Capture structured logs only (app continues running):
  start_simulator_log_capture({ simulatorId: '${simulatorId}', bundleId: '${bundleId}' })
- Option 2: Capture both console and structured logs (app will restart):
  start_simulator_log_capture({ simulatorId: '${simulatorId}', bundleId: '${bundleId}', captureConsole: true })
- Option 3: Launch app with logs in one step (for a fresh start):
  launch_app_with_logs_in_simulator({ simulatorId: '${simulatorId}', bundleId: '${bundleId}' })

When done with any option, use: stop_sim_log_cap({ logSessionId: 'SESSION_ID' })`,
        },
      ],
      isError: false,
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    log('error', `Error in iOS Simulator build and run: ${errorMessage}`);
    return createTextResponse(`Error in iOS Simulator build and run: ${errorMessage}`, true);
  }
}

const publicSchemaObject = baseSchemaObject.omit({
  projectPath: true,
  workspacePath: true,
  scheme: true,
  configuration: true,
  simulatorId: true,
  simulatorName: true,
  useLatestOS: true,
} as const);

export default {
  name: 'build_run_sim',
  description: 'Builds and runs an app on an iOS simulator.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: baseSchemaObject,
  }),
  annotations: {
    title: 'Build Run Simulator',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<BuildRunSimulatorParams>({
    internalSchema: buildRunSimulatorSchema as unknown as z.ZodType<
      BuildRunSimulatorParams,
      unknown
    >,
    logicFunction: build_run_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
    exclusivePairs: [
      ['projectPath', 'workspacePath'],
      ['simulatorId', 'simulatorName'],
    ],
  }),
};

```

--------------------------------------------------------------------------------
/src/utils/debugger/backends/dap-backend.ts:
--------------------------------------------------------------------------------

```typescript
import type { DebuggerBackend } from './DebuggerBackend.ts';
import type { BreakpointInfo, BreakpointSpec, DebugExecutionState } from '../types.ts';
import type { CommandExecutor, InteractiveSpawner } from '../../execution/index.ts';
import { getDefaultCommandExecutor, getDefaultInteractiveSpawner } from '../../execution/index.ts';
import { log } from '../../logging/index.ts';
import type {
  DapEvent,
  EvaluateResponseBody,
  ScopesResponseBody,
  SetBreakpointsResponseBody,
  StackTraceResponseBody,
  StoppedEventBody,
  ThreadsResponseBody,
  VariablesResponseBody,
} from '../dap/types.ts';
import { DapTransport } from '../dap/transport.ts';
import { resolveLldbDapCommand } from '../dap/adapter-discovery.ts';

const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
const LOG_PREFIX = '[DAP Backend]';

type FileLineBreakpointRecord = { line: number; condition?: string; id?: number };
type FunctionBreakpointRecord = { name: string; condition?: string; id?: number };

type BreakpointRecord = {
  spec: BreakpointSpec;
  condition?: string;
};

class DapBackend implements DebuggerBackend {
  readonly kind = 'dap' as const;

  private readonly executor: CommandExecutor;
  private readonly spawner: InteractiveSpawner;
  private readonly requestTimeoutMs: number;
  private readonly logEvents: boolean;

  private transport: DapTransport | null = null;
  private unsubscribeEvents: (() => void) | null = null;
  private attached = false;
  private disposed = false;
  private queue: Promise<unknown> = Promise.resolve();

  private lastStoppedThreadId: number | null = null;
  private executionState: DebugExecutionState = { status: 'unknown' };
  private breakpointsById = new Map<number, BreakpointRecord>();
  private fileLineBreakpointsByFile = new Map<string, FileLineBreakpointRecord[]>();
  private functionBreakpoints: FunctionBreakpointRecord[] = [];
  private nextSyntheticId = -1;

  constructor(opts: {
    executor: CommandExecutor;
    spawner: InteractiveSpawner;
    requestTimeoutMs: number;
    logEvents: boolean;
  }) {
    this.executor = opts.executor;
    this.spawner = opts.spawner;
    this.requestTimeoutMs = opts.requestTimeoutMs;
    this.logEvents = opts.logEvents;
  }

  async attach(opts: { pid: number; simulatorId: string; waitFor?: boolean }): Promise<void> {
    void opts.simulatorId;
    return this.enqueue(async () => {
      if (this.disposed) {
        throw new Error('DAP backend disposed');
      }
      if (this.attached) {
        throw new Error('DAP backend already attached');
      }

      const adapterCommand = await resolveLldbDapCommand({ executor: this.executor });
      const transport = new DapTransport({
        spawner: this.spawner,
        adapterCommand,
        logPrefix: LOG_PREFIX,
      });
      this.transport = transport;
      this.unsubscribeEvents = transport.onEvent((event) => this.handleEvent(event));

      try {
        const init = await this.request<
          {
            clientID: string;
            clientName: string;
            adapterID: string;
            linesStartAt1: boolean;
            columnsStartAt1: boolean;
            pathFormat: string;
            supportsVariableType: boolean;
            supportsVariablePaging: boolean;
          },
          { supportsConfigurationDoneRequest?: boolean }
        >('initialize', {
          clientID: 'xcodebuildmcp',
          clientName: 'XcodeBuildMCP',
          adapterID: 'lldb-dap',
          linesStartAt1: true,
          columnsStartAt1: true,
          pathFormat: 'path',
          supportsVariableType: true,
          supportsVariablePaging: false,
        });

        await this.request('attach', {
          pid: opts.pid,
          waitFor: opts.waitFor ?? false,
        });

        if (init.supportsConfigurationDoneRequest !== false) {
          await this.request('configurationDone', {});
        }

        this.attached = true;
        log('info', `${LOG_PREFIX} attached to pid ${opts.pid}`);
      } catch (error) {
        this.cleanupTransport();
        throw error;
      }
    });
  }

  async detach(): Promise<void> {
    return this.enqueue(async () => {
      if (!this.transport) return;
      try {
        await this.request('disconnect', { terminateDebuggee: false });
      } finally {
        this.cleanupTransport();
      }
    });
  }

  async runCommand(command: string, opts?: { timeoutMs?: number }): Promise<string> {
    this.ensureAttached();

    try {
      const body = await this.request<
        { expression: string; context: string },
        EvaluateResponseBody
      >('evaluate', { expression: command, context: 'repl' }, opts);
      return formatEvaluateResult(body);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      if (/evaluate|repl|not supported/i.test(message)) {
        throw new Error(
          'DAP backend does not support LLDB command evaluation. Set XCODEBUILDMCP_DEBUGGER_BACKEND=lldb-cli to use the CLI backend.',
        );
      }
      throw error;
    }
  }

  async resume(opts?: { threadId?: number }): Promise<void> {
    return this.enqueue(async () => {
      this.ensureAttached();

      let threadId = opts?.threadId;
      if (!threadId) {
        const thread = await this.resolveThread();
        threadId = thread.id;
      }

      await this.request('continue', { threadId });
      this.executionState = { status: 'running' };
      this.lastStoppedThreadId = null;
    });
  }

  async addBreakpoint(
    spec: BreakpointSpec,
    opts?: { condition?: string },
  ): Promise<BreakpointInfo> {
    return this.enqueue(async () => {
      this.ensureAttached();

      if (spec.kind === 'file-line') {
        const current = this.fileLineBreakpointsByFile.get(spec.file) ?? [];
        const nextBreakpoints = [...current, { line: spec.line, condition: opts?.condition }];
        const updated = await this.setFileBreakpoints(spec.file, nextBreakpoints);
        const added = updated[nextBreakpoints.length - 1];
        if (!added?.id) {
          throw new Error('DAP breakpoint id missing for file breakpoint.');
        }
        return {
          id: added.id,
          spec,
          rawOutput: `Set breakpoint ${added.id} at ${spec.file}:${spec.line}`,
        };
      }

      const nextBreakpoints = [
        ...this.functionBreakpoints,
        { name: spec.name, condition: opts?.condition },
      ];
      const updated = await this.setFunctionBreakpoints(nextBreakpoints);
      const added = updated[nextBreakpoints.length - 1];
      if (!added?.id) {
        throw new Error('DAP breakpoint id missing for function breakpoint.');
      }
      return {
        id: added.id,
        spec,
        rawOutput: `Set breakpoint ${added.id} on ${spec.name}`,
      };
    });
  }

  async removeBreakpoint(id: number): Promise<string> {
    return this.enqueue(async () => {
      this.ensureAttached();

      const record = this.breakpointsById.get(id);
      if (!record) {
        throw new Error(`Breakpoint not found: ${id}`);
      }

      if (record.spec.kind === 'file-line') {
        const current = this.fileLineBreakpointsByFile.get(record.spec.file) ?? [];
        const nextBreakpoints = current.filter((breakpoint) => breakpoint.id !== id);
        await this.setFileBreakpoints(record.spec.file, nextBreakpoints);
      } else {
        const nextBreakpoints = this.functionBreakpoints.filter(
          (breakpoint) => breakpoint.id !== id,
        );
        await this.setFunctionBreakpoints(nextBreakpoints);
      }

      return `Removed breakpoint ${id}.`;
    });
  }

  async getStack(opts?: { threadIndex?: number; maxFrames?: number }): Promise<string> {
    this.ensureAttached();

    try {
      const thread = await this.resolveThread(opts?.threadIndex);
      const stack = await this.request<
        { threadId: number; startFrame?: number; levels?: number },
        StackTraceResponseBody
      >('stackTrace', {
        threadId: thread.id,
        startFrame: 0,
        levels: opts?.maxFrames,
      });

      if (!stack.stackFrames.length) {
        return `Thread ${thread.id}: no stack frames.`;
      }

      const threadLabel = thread.name
        ? `Thread ${thread.id} (${thread.name})`
        : `Thread ${thread.id}`;
      const formatted = stack.stackFrames.map((frame, index) => {
        const location = frame.source?.path ?? frame.source?.name ?? 'unknown';
        const line = frame.line ?? 0;
        return `frame #${index}: ${frame.name} at ${location}:${line}`;
      });

      return [threadLabel, ...formatted].join('\n');
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      if (/running|not stopped|no thread|no frames/i.test(message)) {
        throw new Error('Process is running; pause or hit a breakpoint to fetch stack.');
      }
      throw error;
    }
  }

  async getVariables(opts?: { frameIndex?: number }): Promise<string> {
    this.ensureAttached();

    try {
      const thread = await this.resolveThread();
      const frameIndex = opts?.frameIndex ?? 0;
      const stack = await this.request<
        { threadId: number; startFrame?: number; levels?: number },
        StackTraceResponseBody
      >('stackTrace', {
        threadId: thread.id,
        startFrame: 0,
        levels: frameIndex + 1,
      });

      if (stack.stackFrames.length <= frameIndex) {
        throw new Error(`Frame index ${frameIndex} is out of range.`);
      }

      const frame = stack.stackFrames[frameIndex];
      const scopes = await this.request<{ frameId: number }, ScopesResponseBody>('scopes', {
        frameId: frame.id,
      });

      if (!scopes.scopes.length) {
        return 'No scopes available.';
      }

      const sections: string[] = [];
      for (const scope of scopes.scopes) {
        if (!scope.variablesReference) {
          sections.push(`${scope.name}:\n  (no variables)`);
          continue;
        }

        const vars = await this.request<{ variablesReference: number }, VariablesResponseBody>(
          'variables',
          {
            variablesReference: scope.variablesReference,
          },
        );

        if (!vars.variables.length) {
          sections.push(`${scope.name}:\n  (no variables)`);
          continue;
        }

        const lines = vars.variables.map((variable) => `  ${formatVariable(variable)}`);
        sections.push(`${scope.name}:\n${lines.join('\n')}`);
      }

      return sections.join('\n\n');
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      if (/running|not stopped|no thread/i.test(message)) {
        throw new Error('Process is running; pause or hit a breakpoint to fetch variables.');
      }
      throw error;
    }
  }

  async getExecutionState(opts?: { timeoutMs?: number }): Promise<DebugExecutionState> {
    return this.enqueue(async () => {
      this.ensureAttached();

      if (this.executionState.status !== 'unknown') {
        return this.executionState;
      }

      try {
        const body = await this.request<undefined, ThreadsResponseBody>('threads', undefined, opts);
        const threads = body.threads ?? [];
        if (!threads.length) {
          return { status: 'unknown' };
        }

        const threadId = threads[0].id;
        try {
          await this.request<
            { threadId: number; startFrame?: number; levels?: number },
            StackTraceResponseBody
          >(
            'stackTrace',
            { threadId, startFrame: 0, levels: 1 },
            { timeoutMs: opts?.timeoutMs ?? this.requestTimeoutMs },
          );
          const state: DebugExecutionState = { status: 'stopped', threadId };
          this.executionState = state;
          return state;
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          if (/running|not stopped/i.test(message)) {
            const state: DebugExecutionState = { status: 'running', description: message };
            this.executionState = state;
            return state;
          }
          return { status: 'unknown', description: message };
        }
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (/running|not stopped/i.test(message)) {
          return { status: 'running', description: message };
        }
        return { status: 'unknown', description: message };
      }
    });
  }

  async dispose(): Promise<void> {
    if (this.disposed) return;
    this.disposed = true;
    try {
      this.cleanupTransport();
    } catch (error) {
      log('debug', `${LOG_PREFIX} dispose failed: ${String(error)}`);
    }
  }

  private ensureAttached(): void {
    if (!this.transport || !this.attached) {
      throw new Error('No active DAP session. Attach first.');
    }
  }

  private async request<A, B>(
    command: string,
    args?: A,
    opts?: { timeoutMs?: number },
  ): Promise<B> {
    const transport = this.transport;
    if (!transport) {
      throw new Error('DAP transport not initialized.');
    }

    return transport.sendRequest<A, B>(command, args, {
      timeoutMs: opts?.timeoutMs ?? this.requestTimeoutMs,
    });
  }

  private async resolveThread(threadIndex?: number): Promise<{ id: number; name?: string }> {
    const body = await this.request<undefined, ThreadsResponseBody>('threads');
    const threads = body.threads ?? [];
    if (!threads.length) {
      throw new Error('No threads available.');
    }

    if (typeof threadIndex === 'number') {
      if (threadIndex < 0 || threadIndex >= threads.length) {
        throw new Error(`Thread index ${threadIndex} is out of range.`);
      }
      return threads[threadIndex];
    }

    if (this.lastStoppedThreadId) {
      const stopped = threads.find((thread) => thread.id === this.lastStoppedThreadId);
      if (stopped) {
        return stopped;
      }
    }

    return threads[0];
  }

  private handleEvent(event: DapEvent): void {
    if (this.logEvents) {
      log('debug', `${LOG_PREFIX} event: ${JSON.stringify(event)}`);
    }

    if (event.event === 'stopped') {
      const body = event.body as StoppedEventBody | undefined;
      this.executionState = {
        status: 'stopped',
        reason: body?.reason,
        description: body?.description,
        threadId: body?.threadId,
      };
      if (body?.threadId) {
        this.lastStoppedThreadId = body.threadId;
      }
      return;
    }

    if (event.event === 'continued') {
      this.executionState = { status: 'running' };
      this.lastStoppedThreadId = null;
      return;
    }

    if (event.event === 'exited' || event.event === 'terminated') {
      this.executionState = { status: 'terminated' };
      this.lastStoppedThreadId = null;
    }
  }

  private cleanupTransport(): void {
    this.attached = false;
    this.lastStoppedThreadId = null;
    this.executionState = { status: 'unknown' };
    this.unsubscribeEvents?.();
    this.unsubscribeEvents = null;

    if (this.transport) {
      this.transport.dispose();
      this.transport = null;
    }
  }

  private async setFileBreakpoints(
    file: string,
    breakpoints: FileLineBreakpointRecord[],
  ): Promise<FileLineBreakpointRecord[]> {
    const response = await this.request<
      { source: { path: string }; breakpoints: Array<{ line: number; condition?: string }> },
      SetBreakpointsResponseBody
    >('setBreakpoints', {
      source: { path: file },
      breakpoints: breakpoints.map((bp) => ({ line: bp.line, condition: bp.condition })),
    });

    const updated = breakpoints.map((bp, index) => ({
      ...bp,
      id: resolveBreakpointId(response.breakpoints?.[index]?.id, () => this.nextSyntheticId--),
    }));

    this.replaceFileBreakpoints(file, updated);
    return updated;
  }

  private replaceFileBreakpoints(file: string, breakpoints: FileLineBreakpointRecord[]): void {
    const existing = this.fileLineBreakpointsByFile.get(file) ?? [];
    for (const breakpoint of existing) {
      if (breakpoint.id != null) {
        this.breakpointsById.delete(breakpoint.id);
      }
    }

    this.fileLineBreakpointsByFile.set(file, breakpoints);
    for (const breakpoint of breakpoints) {
      if (breakpoint.id != null) {
        this.breakpointsById.set(breakpoint.id, {
          spec: { kind: 'file-line', file, line: breakpoint.line },
          condition: breakpoint.condition,
        });
      }
    }
  }

  private async setFunctionBreakpoints(
    breakpoints: FunctionBreakpointRecord[],
  ): Promise<FunctionBreakpointRecord[]> {
    const response = await this.request<
      { breakpoints: Array<{ name: string; condition?: string }> },
      SetBreakpointsResponseBody
    >('setFunctionBreakpoints', {
      breakpoints: breakpoints.map((bp) => ({ name: bp.name, condition: bp.condition })),
    });

    const updated = breakpoints.map((bp, index) => ({
      ...bp,
      id: resolveBreakpointId(response.breakpoints?.[index]?.id, () => this.nextSyntheticId--),
    }));

    this.replaceFunctionBreakpoints(updated);
    return updated;
  }

  private replaceFunctionBreakpoints(breakpoints: FunctionBreakpointRecord[]): void {
    for (const breakpoint of this.functionBreakpoints) {
      if (breakpoint.id != null) {
        this.breakpointsById.delete(breakpoint.id);
      }
    }

    this.functionBreakpoints = breakpoints;
    for (const breakpoint of breakpoints) {
      if (breakpoint.id != null) {
        this.breakpointsById.set(breakpoint.id, {
          spec: { kind: 'function', name: breakpoint.name },
          condition: breakpoint.condition,
        });
      }
    }
  }

  private enqueue<T>(work: () => Promise<T>): Promise<T> {
    const next = this.queue.then(work, work) as Promise<T>;
    this.queue = next.then(
      () => undefined,
      () => undefined,
    );
    return next;
  }
}

function resolveBreakpointId(id: number | undefined, fallback: () => number): number {
  if (typeof id === 'number' && Number.isFinite(id)) {
    return id;
  }
  return fallback();
}

function formatEvaluateResult(body: EvaluateResponseBody): string {
  const parts = [body.output, body.result].filter((value) => value && value.trim().length > 0);
  return parts.join('\n');
}

function formatVariable(variable: { name: string; value: string; type?: string }): string {
  const typeSuffix = variable.type ? ` (${variable.type})` : '';
  return `${variable.name}${typeSuffix} = ${variable.value}`;
}

function parseRequestTimeoutMs(): number {
  const raw = process.env.XCODEBUILDMCP_DAP_REQUEST_TIMEOUT_MS;
  if (!raw) return DEFAULT_REQUEST_TIMEOUT_MS;
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return DEFAULT_REQUEST_TIMEOUT_MS;
  }
  return parsed;
}

function parseLogEvents(): boolean {
  return process.env.XCODEBUILDMCP_DAP_LOG_EVENTS === 'true';
}

export async function createDapBackend(opts?: {
  executor?: CommandExecutor;
  spawner?: InteractiveSpawner;
  requestTimeoutMs?: number;
}): Promise<DebuggerBackend> {
  const executor = opts?.executor ?? getDefaultCommandExecutor();
  const spawner = opts?.spawner ?? getDefaultInteractiveSpawner();
  const requestTimeoutMs = opts?.requestTimeoutMs ?? parseRequestTimeoutMs();
  const backend = new DapBackend({
    executor,
    spawner,
    requestTimeoutMs,
    logEvents: parseLogEvents(),
  });
  return backend;
}

```

--------------------------------------------------------------------------------
/docs/dev/session_management_plan.md:
--------------------------------------------------------------------------------

```markdown
# Stateful Session Defaults for MCP Tools — Design, Middleware, and Plan

Below is a concise architecture and implementation plan to introduce a session-aware defaults layer that removes repeated tool parameters from public schemas, while keeping all tool logic and tests unchanged.

## Architecture Overview

- **Core idea**: keep logic functions and tests untouched; move argument consolidation into a session-aware interop layer and expose minimal public schemas.
- **Data flow**:
  - Client calls a tool with zero or few args → session middleware merges session defaults → validates with the internal schema → calls the existing logic function.
- **Components**:
  - `SessionStore` (singleton, in-memory): set/get/clear/show defaults.
  - Session-aware tool factory: merges defaults, performs preflight requirement checks (allOf/oneOf), then validates with the tool's internal zod schema.
  - Public vs internal schema: plugins register a minimal "public" input schema; handlers validate with the unchanged "internal" schema.

## Core Types

```typescript
// src/utils/session-store.ts
export type SessionDefaults = {
  projectPath?: string;
  workspacePath?: string;
  scheme?: string;
  configuration?: string;
  simulatorName?: string;
  simulatorId?: string;
  deviceId?: string;
  useLatestOS?: boolean;
  arch?: 'arm64' | 'x86_64';
};
```

## Session Store (singleton)

```typescript
// src/utils/session-store.ts
import { log } from './logger.ts';

class SessionStore {
  private defaults: SessionDefaults = {};

  setDefaults(partial: Partial<SessionDefaults>): void {
    this.defaults = { ...this.defaults, ...partial };
    log('info', '[Session] Defaults set', { keys: Object.keys(partial) });
  }

  clear(keys?: (keyof SessionDefaults)[]): void {
    if (!keys || keys.length === 0) {
      this.defaults = {};
      log('info', '[Session] All defaults cleared');
      return;
    }
    for (const k of keys) delete this.defaults[k];
    log('info', '[Session] Defaults cleared', { keys });
  }

  get<K extends keyof SessionDefaults>(key: K): SessionDefaults[K] {
    return this.defaults[key];
  }

  getAll(): SessionDefaults {
    return { ...this.defaults };
  }
}

export const sessionStore = new SessionStore();
```

## Session-Aware Tool Factory

```typescript
// src/utils/typed-tool-factory.ts (add new helper, keep createTypedTool as-is)
import { z } from 'zod';
import { sessionStore, type SessionDefaults } from './session-store.ts';
import type { CommandExecutor } from './execution/index.ts';
import { createErrorResponse } from './responses/index.ts';
import type { ToolResponse } from '../types/common.ts';

export type SessionRequirement =
  | { allOf: (keyof SessionDefaults)[]; message?: string }
  | { oneOf: (keyof SessionDefaults)[]; message?: string };

function missingFromArgsAndSession(
  keys: (keyof SessionDefaults)[],
  args: Record<string, unknown>,
): string[] {
  return keys.filter((k) => args[k] == null && sessionStore.get(k) == null);
}

export function createSessionAwareTool<TParams>(opts: {
  internalSchema: z.ZodType<TParams>;
  logicFunction: (params: TParams, executor: CommandExecutor) => Promise<ToolResponse>;
  getExecutor: () => CommandExecutor;
  requirements?: SessionRequirement[]; // preflight, friendlier than raw zod errors
}) {
  const { internalSchema, logicFunction, getExecutor, requirements = [] } = opts;

  return async (rawArgs: Record<string, unknown>): Promise<ToolResponse> => {
    try {
      // Merge: explicit args take precedence over session defaults
      const merged: Record<string, unknown> = { ...sessionStore.getAll(), ...rawArgs };

      // Preflight requirement checks (clear message how to fix)
      for (const req of requirements) {
        if ('allOf' in req) {
          const missing = missingFromArgsAndSession(req.allOf, rawArgs);
          if (missing.length > 0) {
            return createErrorResponse(
              'Missing required session defaults',
              `${req.message ?? `Required: ${req.allOf.join(', ')}`}\n` +
                `Set with: session-set-defaults { ${missing.map((k) => `"${k}": "..."`).join(', ')} }`,
            );
          }
        } else if ('oneOf' in req) {
          const missing = missingFromArgsAndSession(req.oneOf, rawArgs);
          // oneOf satisfied if at least one is present in merged
          const satisfied = req.oneOf.some((k) => merged[k] != null);
          if (!satisfied) {
            return createErrorResponse(
              'Missing required session defaults',
              `${req.message ?? `Provide one of: ${req.oneOf.join(', ')}`}\n` +
                `Set with: session-set-defaults { "${req.oneOf[0]}": "..." }`,
            );
          }
        }
      }

      // Validate against unchanged internal schema (logic/api untouched)
      const validated = internalSchema.parse(merged);
      return await logicFunction(validated, getExecutor());
    } catch (error) {
      if (error instanceof z.ZodError) {
        const msgs = error.errors.map((e) => `${e.path.join('.') || 'root'}: ${e.message}`);
        return createErrorResponse(
          'Parameter validation failed',
          `Invalid parameters:\n${msgs.join('\n')}\n` +
            `Tip: set session defaults via session-set-defaults`,
        );
      }
      throw error;
    }
  };
}
```

## Plugin Migration Pattern (Example: build_sim)

Public schema hides session fields; handler uses session-aware factory with internal schema and requirements; logic function unchanged.

```typescript
// src/mcp/tools/simulator/build_sim.ts (key parts only)
import { z } from 'zod';
import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';

// Existing internal schema (unchanged)…
const baseOptions = { /* as-is (scheme, simulatorId, simulatorName, configuration, …) */ };
const baseSchemaObject = z.object({
  projectPath: z.string().optional(),
  workspacePath: z.string().optional(),
  ...baseOptions,
});
const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject);
const buildSimulatorSchema = baseSchema
  .refine(/* as-is: projectPath XOR workspacePath */)
  .refine(/* as-is: simulatorId XOR simulatorName */);

export type BuildSimulatorParams = z.infer<typeof buildSimulatorSchema>;

// Public schema = internal minus session-managed fields
const sessionManaged = [
  'projectPath',
  'workspacePath',
  'scheme',
  'configuration',
  'simulatorId',
  'simulatorName',
  'useLatestOS',
] as const;

const publicSchemaObject = baseSchemaObject.omit(
  Object.fromEntries(sessionManaged.map((k) => [k, true])) as Record<string, true>,
);

export default {
  name: 'build_sim',
  description: 'Builds an app for an iOS simulator.',
  schema: publicSchemaObject.shape, // what the MCP client sees
  handler: createSessionAwareTool<BuildSimulatorParams>({
    internalSchema: buildSimulatorSchema,
    logicFunction: build_simLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [
      { allOf: ['scheme'], message: 'scheme is required' },
      { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' },
      { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' },
    ],
  }),
};
```

This same pattern applies to `build_run_sim`, `test_sim`, device/macos tools, etc. Public schemas become minimal, while internal schemas and logic remain unchanged.

## New Tool Group: session-management

### session_set_defaults.ts

```typescript
// src/mcp/tools/session-management/session_set_defaults.ts
import { z } from 'zod';
import { sessionStore, type SessionDefaults } from '../../../utils/session-store.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';

const schemaObj = z.object({
  projectPath: z.string().optional(),
  workspacePath: z.string().optional(),
  scheme: z.string().optional(),
  configuration: z.string().optional(),
  simulatorName: z.string().optional(),
  simulatorId: z.string().optional(),
  deviceId: z.string().optional(),
  useLatestOS: z.boolean().optional(),
  arch: z.enum(['arm64', 'x86_64']).optional(),
});
type Params = z.infer<typeof schemaObj>;

async function logic(params: Params): Promise<import('../../../types/common.ts').ToolResponse> {
  sessionStore.setDefaults(params as Partial<SessionDefaults>);
  const current = sessionStore.getAll();
  return { content: [{ type: 'text', text: `Defaults updated:\n${JSON.stringify(current, null, 2)}` }] };
}

export default {
  name: 'session-set-defaults',
  description: 'Set session defaults used by other tools.',
  schema: schemaObj.shape,
  handler: createTypedTool(schemaObj, logic, getDefaultCommandExecutor),
};
```

### session_clear_defaults.ts

```typescript
// src/mcp/tools/session-management/session_clear_defaults.ts
import { z } from 'zod';
import { sessionStore } from '../../../utils/session-store.ts';
import { createTypedTool } from '../../../utils/typed-tool-factory.ts';
import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts';

const keys = [
  'projectPath','workspacePath','scheme','configuration',
  'simulatorName','simulatorId','deviceId','useLatestOS','arch',
] as const;
const schemaObj = z.object({
  keys: z.array(z.enum(keys)).optional(),
  all: z.boolean().optional(),
});

async function logic(params: z.infer<typeof schemaObj>) {
  if (params.all || !params.keys) sessionStore.clear();
  else sessionStore.clear(params.keys);
  return { content: [{ type: 'text', text: 'Session defaults cleared' }] };
}

export default {
  name: 'session-clear-defaults',
  description: 'Clear selected or all session defaults.',
  schema: schemaObj.shape,
  handler: createTypedTool(schemaObj, logic, getDefaultCommandExecutor),
};
```

### session_show_defaults.ts

```typescript
// src/mcp/tools/session-management/session_show_defaults.ts
import { sessionStore } from '../../../utils/session-store.ts';

export default {
  name: 'session-show-defaults',
  description: 'Show current session defaults.',
  schema: {}, // no args
  handler: async () => {
    const current = sessionStore.getAll();
    return { content: [{ type: 'text', text: JSON.stringify(current, null, 2) }] };
  },
};
```

## Step-by-Step Implementation Plan (Incremental, buildable at each step)

1. **Add SessionStore** ✅ **DONE**
   - New file: `src/utils/session-store.ts`.
   - No existing code changes; run: `npm run build`, `lint`, `test`.
   - Commit checkpoint (after review): see Commit & Review Protocol below.

2. **Add session-management tools** ✅ **DONE**
   - New folder: `src/mcp/tools/session-management` with the three tools above.
   - Register via existing plugin discovery (same pattern as others).
   - Build and test.
   - Commit checkpoint (after review).

3. **Add session-aware tool factory** ✅ **DONE**
   - Add `createSessionAwareTool` to `src/utils/typed-tool-factory.ts` (keep `createTypedTool` intact).
   - Unit tests for requirement preflight and merge precedence.
   - Commit checkpoint (after review).

4. **Migrate 2-3 representative tools**
   - Example: `simulator/build_sim`, `macos/build_macos`, `device/build_device`.
   - Create `publicSchemaObject` (omit session fields), switch handler to `createSessionAwareTool` with requirements.
   - Keep internal schema and logic unchanged. Build and test.
   - Commit checkpoint (after review).

5. **Migrate remaining tools in small batches**
   - Apply the same pattern across simulator/device/macos/test utilities.
   - After each batch: `npm run typecheck`, `lint`, `test`.
   - Commit checkpoint (after review).

6. **Final polish**
   - Add tests for session tools and session-aware preflight error messages.
   - Ensure public schemas no longer expose session parameters globally.
   - Commit checkpoint (after review).

## Standard Testing & DI Checklist (Mandatory)

- Handlers must use dependency injection; tests must never call real executors.
- For validation-only tests, calling the handler is acceptable because Zod validation occurs before executor acquisition.
- For logic tests that would otherwise trigger `getDefaultCommandExecutor`, export the logic function and test it directly (no executor needed if logic doesn’t use one):

```ts
// Example: src/mcp/tools/session-management/session_clear_defaults.ts
export async function sessionClearDefaultsLogic(params: Params): Promise<ToolResponse> { /* ... */ }
export default {
  name: 'session-clear-defaults',
  handler: createTypedTool(schemaObj, sessionClearDefaultsLogic, getDefaultCommandExecutor),
};

// Test: import logic and call directly to avoid real executor
import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts';
```

- Add tests for the new group and tools:
  - Group metadata test: `src/mcp/tools/session-management/__tests__/index.test.ts`
  - Tool tests: `session_set_defaults.test.ts`, `session_clear_defaults.test.ts`, `session_show_defaults.test.ts`
  - Utils tests: `src/utils/__tests__/session-store.test.ts`
  - Factory tests: `src/utils/__tests__/session-aware-tool-factory.test.ts` covering:
    - Preflight requirements (allOf/oneOf)
    - Merge precedence (explicit args override session defaults)
    - Zod error reporting with helpful tips

- Always run locally before requesting review:
  - `npm run typecheck`
  - `npm run lint`
  - `npm run format:check`
  - `npm run build`
  - `npm run test`
  - Perform a quick manual CLI check (mcpli or reloaderoo) per the Manual Testing section

### Minimal Changes Policy for Tests (Enforced)

- Only make material, essential edits to tests required by the code change (e.g., new preflight error messages or added/removed fields).
- Do not change sample input values or defaults in tests (e.g., flipping a boolean like `preferXcodebuild`) unless strictly necessary to validate behavior.
- Preserve the original intent and coverage of logic-function tests; keep handler vs logic boundaries intact.
- When session-awareness is added, prefer setting/clearing session defaults around tests rather than altering existing assertions or sample inputs.

### Tool Description Policy (Enforced)

- Keep tool descriptions concise (maximum one short sentence).
- Do not mention session defaults, setup steps, examples, or parameter relationships in descriptions.
- Use clear, imperative phrasing (e.g., "Builds an app for an iOS simulator.").
- Apply consistently across all migrated tools; update any tests that assert `description` to match the concise string only.

## Commit & Review Protocol (Enforced)

At the end of each numbered step above:

1. Ensure all checks pass: `typecheck`, `lint`, `format:check`, `build`, `test`; then perform a quick manual CLI test (mcpli or reloaderoo) per the Manual Testing section.
   - Verify tool descriptions comply with the Tool Description Policy (concise, no session-defaults mention).
2. Stage only the files for that step.
3. Prepare a concise commit message focused on the “why”.
4. Request manual review and approval before committing. Do not push.

Example messages per step:

- Step 1 (SessionStore)
  - `chore(utils): add in-memory SessionStore for session defaults`
  - Body: “Introduces singleton SessionStore with set/get/clear/show for session defaults; no behavior changes.”

- Step 2 (session-management tools)
  - `feat(session-management): add set/clear/show session defaults tools and workflow metadata`
  - Body: “Adds tools to manage session defaults and exposes workflow metadata; minimal schemas via typed factory.”

- Step 3 (middleware)
  - `feat(utils): add createSessionAwareTool with preflight requirements and args>session merge`
  - Body: “Session-aware interop layer performing requirements checks and Zod validation against internal schema.”

- Step 6 (tests/final polish)
  - `test(session-management): add tool, store, and middleware tests; export logic for DI`
  - Body: “Covers group metadata, tools, SessionStore, and factory (requirements/merge/errors). No production behavior changes.”

Approval flow:
- After preparing messages and confirming checks, request maintainer approval.
- On approval: commit locally (no push).
- On rejection: revise and re-run checks.

Note on commit hooks and selective commits:
- The pre-commit hook runs format/lint/build and can auto-add or modify files, causing additional files to be included in the commit. If you must commit a minimal subset, skip hooks with: `git commit --no-verify` (use sparingly and run `npm run typecheck && npm run lint && npm run test` manually first).

## Safety, Buildability, Testability

- Logic functions and their types remain unchanged; existing unit tests that import logic directly continue to pass.
- Public schemas shrink; MCP clients see smaller input schemas without session fields.
- Handlers validate with internal schemas after session-defaults merge, preserving runtime guarantees.
- Preflight requirement checks return clear guidance, e.g., "Provide one of: projectPath or workspacePath" + "Set with: session-set-defaults { "projectPath": "..." }".

## Developer Usage

- **Set defaults once**:
  - `session-set-defaults { "workspacePath": "...", "scheme": "App", "simulatorName": "iPhone 16" }`
- **Run tools without args**:
  - `build_sim {}`
- **Inspect/reset**:
  - `session-show-defaults {}`
  - `session-clear-defaults { "all": true }`

## Manual Testing with mcpli (CLI)

The following commands exercise the session workflow end‑to‑end using the built server.

1) Build the server (required after code changes):

```bash
npm run build
```

2) Discover a scheme (optional helper):

```bash
mcpli --raw list-schemes --projectPath "/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj" -- node build/index.js
```

3) Set the session defaults (project/workspace, scheme, and simulator):

```bash
mcpli --raw session-set-defaults \
  --projectPath "/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj" \
  --scheme MCPTest \
  --simulatorName "iPhone 16" \
  -- node build/index.js
```

4) Verify defaults are stored:

```bash
mcpli --raw session-show-defaults -- node build/index.js
```

5) Run a session‑aware tool with zero or minimal args (defaults are merged automatically):

```bash
# Optionally provide a scratch derived data path and a short timeout
mcpli --tool-timeout=60 --raw build-sim --derivedDataPath "/tmp/XBMCP_DD" -- node build/index.js
```

Troubleshooting:

- If you see validation errors like “Missing required session defaults …”, (re)run step 3 with the missing keys.
- If you see connect ECONNREFUSED or the daemon appears flaky:
  - Check logs: `mcpli daemon log --since=10m -- node build/index.js`
  - Restart daemon: `mcpli daemon restart -- node build/index.js`
  - Clean daemon state: `mcpli daemon clean -- node build/index.js` then `mcpli daemon start -- node build/index.js`
  - After code changes, always: `npm run build` then `mcpli daemon restart -- node build/index.js`

Notes:

- Public schemas for session‑aware tools intentionally omit session fields (e.g., `scheme`, `projectPath`, `simulatorName`). Provide them once via `session-set-defaults` and then call the tool with zero/minimal flags.
- Use `--tool-timeout=<seconds>` to cap long‑running builds during manual testing.
- mcpli CLI normalizes tool names: tools exported with underscores (e.g., `build_sim`) can be invoked with hyphens (e.g., `build-sim`). Copy/paste samples using hyphens are valid because mcpli converts underscores to dashes.

## Next Steps

Would you like me to proceed with Phase 1–3 implementation (store + session tools + middleware), then migrate a first tool (build_sim) and run the test suite?

```

--------------------------------------------------------------------------------
/src/mcp/tools/simulator/__tests__/build_sim.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import * as z from 'zod';
import {
  createMockExecutor,
  createMockCommandResponse,
} from '../../../../test-utils/mock-executors.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
import { sessionStore } from '../../../../utils/session-store.ts';

// Import the plugin and logic function
import buildSim, { build_simLogic } from '../build_sim.ts';

describe('build_sim tool', () => {
  beforeEach(() => {
    sessionStore.clear();
  });

  describe('Export Field Validation (Literal)', () => {
    it('should have correct name', () => {
      expect(buildSim.name).toBe('build_sim');
    });

    it('should have correct description', () => {
      expect(buildSim.description).toBe('Builds an app for an iOS simulator.');
    });

    it('should have handler function', () => {
      expect(typeof buildSim.handler).toBe('function');
    });

    it('should have correct public schema (only non-session fields)', () => {
      const schema = z.object(buildSim.schema);

      // Public schema should allow empty input
      expect(schema.safeParse({}).success).toBe(true);

      // Valid public inputs
      expect(
        schema.safeParse({
          derivedDataPath: '/path/to/derived',
          extraArgs: ['--verbose'],
          preferXcodebuild: false,
        }).success,
      ).toBe(true);

      // Invalid types on public inputs
      expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false);
      expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false);
      expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false);
    });
  });

  describe('Parameter Validation', () => {
    it('should handle missing both projectPath and workspacePath', async () => {
      const result = await buildSim.handler({
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('Provide a project or workspace');
    });

    it('should handle both projectPath and workspacePath provided', async () => {
      const result = await buildSim.handler({
        projectPath: '/path/to/project.xcodeproj',
        workspacePath: '/path/to/workspace',
        scheme: 'MyScheme',
        simulatorName: 'iPhone 16',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('projectPath');
      expect(result.content[0].text).toContain('workspacePath');
    });

    it('should handle empty workspacePath parameter', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      // Empty string passes validation but may cause build issues
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle missing scheme parameter', async () => {
      const result = await buildSim.handler({
        workspacePath: '/path/to/workspace',
        simulatorName: 'iPhone 16',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('scheme is required');
    });

    it('should handle empty scheme parameter', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: '',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      // Empty string passes validation but may cause build issues
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme .',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle missing both simulatorId and simulatorName', async () => {
      const result = await buildSim.handler({
        workspacePath: '/path/to/workspace',
        scheme: 'MyScheme',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Missing required session defaults');
      expect(result.content[0].text).toContain('Provide simulatorId or simulatorName');
    });

    it('should handle both simulatorId and simulatorName provided', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'Build succeeded' });

      // Should fail with XOR validation
      const result = await buildSim.handler({
        workspacePath: '/path/to/workspace',
        scheme: 'MyScheme',
        simulatorId: 'ABC-123',
        simulatorName: 'iPhone 16',
      });

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toContain('Parameter validation failed');
      expect(result.content[0].text).toContain('Mutually exclusive parameters provided');
      expect(result.content[0].text).toContain('simulatorId');
      expect(result.content[0].text).toContain('simulatorName');
    });

    it('should handle empty simulatorName parameter', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'For iOS Simulator platform, either simulatorId or simulatorName must be provided',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: '',
        },
        mockExecutor,
      );

      // Empty simulatorName passes validation but causes early failure in destination construction
      expect(result.isError).toBe(true);
      expect(result.content[0].text).toBe(
        'For iOS Simulator platform, either simulatorId or simulatorName must be provided',
      );
    });
  });

  describe('Command Generation', () => {
    it('should generate correct build command with minimal parameters (workspace)', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate one build command
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command with minimal parameters (project)', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_simLogic(
        {
          projectPath: '/path/to/MyProject.xcodeproj',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        trackingExecutor,
      );

      // Should generate one build command
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-project',
        '/path/to/MyProject.xcodeproj',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command with all optional parameters', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          configuration: 'Release',
          derivedDataPath: '/custom/derived/path',
          extraArgs: ['--verbose'],
          useLatestOS: false,
        },
        trackingExecutor,
      );

      // Should generate one build command with all parameters
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Release',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16',
        '-derivedDataPath',
        '/custom/derived/path',
        '--verbose',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should handle paths with spaces in command generation', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_simLogic(
        {
          workspacePath: '/Users/dev/My Project/MyProject.xcworkspace',
          scheme: 'My Scheme',
          simulatorName: 'iPhone 16 Pro',
        },
        trackingExecutor,
      );

      // Should generate one build command with paths containing spaces
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/Users/dev/My Project/MyProject.xcworkspace',
        '-scheme',
        'My Scheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });

    it('should generate correct build command with useLatestOS set to true', async () => {
      const callHistory: Array<{
        command: string[];
        logPrefix?: string;
        useShell?: boolean;
        opts?: { env?: Record<string, string>; cwd?: string };
      }> = [];

      // Create tracking executor
      const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => {
        callHistory.push({ command, logPrefix, useShell, opts });
        return createMockCommandResponse({
          success: false,
          output: '',
          error: 'Test error to stop execution early',
        });
      };

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/MyProject.xcworkspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          useLatestOS: true,
        },
        trackingExecutor,
      );

      // Should generate one build command with OS=latest
      expect(callHistory).toHaveLength(1);
      expect(callHistory[0].command).toEqual([
        'xcodebuild',
        '-workspace',
        '/path/to/MyProject.xcworkspace',
        '-scheme',
        'MyScheme',
        '-configuration',
        'Debug',
        '-skipMacroValidation',
        '-destination',
        'platform=iOS Simulator,name=iPhone 16,OS=latest',
        'build',
      ]);
      expect(callHistory[0].logPrefix).toBe('iOS Simulator Build');
    });
  });

  describe('Response Processing', () => {
    it('should handle successful build', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle successful build with all optional parameters', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          configuration: 'Release',
          derivedDataPath: '/path/to/derived',
          extraArgs: ['--verbose'],
          useLatestOS: false,
          preferXcodebuild: true,
        },
        mockExecutor,
      );

      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });

    it('should handle build failure', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: '',
        error: 'Build failed: Compilation error',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: '❌ [stderr] Build failed: Compilation error',
          },
          {
            type: 'text',
            text: '❌ iOS Simulator Build build failed for scheme MyScheme.',
          },
        ],
        isError: true,
      });
    });

    it('should handle build warnings', async () => {
      const mockExecutor = createMockExecutor({
        success: true,
        output: 'warning: deprecated method used\nBUILD SUCCEEDED',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.content).toEqual(
        expect.arrayContaining([
          {
            type: 'text',
            text: expect.stringContaining('⚠️'),
          },
          {
            type: 'text',
            text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
          },
          {
            type: 'text',
            text: expect.stringContaining('Next Steps:'),
          },
        ]),
      );
    });

    it('should handle command executor errors', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        error: 'spawn xcodebuild ENOENT',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content[0].text).toBe('❌ [stderr] spawn xcodebuild ENOENT');
    });

    it('should handle mixed warning and error output', async () => {
      const mockExecutor = createMockExecutor({
        success: false,
        output: 'warning: deprecated method\nerror: undefined symbol',
        error: 'Build failed',
      });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      expect(result.isError).toBe(true);
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '⚠️ Warning: warning: deprecated method',
        },
        {
          type: 'text',
          text: '❌ Error: error: undefined symbol',
        },
        {
          type: 'text',
          text: '❌ [stderr] Build failed',
        },
        {
          type: 'text',
          text: '❌ iOS Simulator Build build failed for scheme MyScheme.',
        },
      ]);
    });

    it('should use default configuration when not provided', async () => {
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
          // configuration intentionally omitted - should default to Debug
        },
        mockExecutor,
      );

      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });
  });

  describe('Error Handling', () => {
    it('should handle catch block exceptions', async () => {
      // Create a mock that throws an error when called
      const mockExecutor = createMockExecutor({ success: true, output: 'BUILD SUCCEEDED' });

      // Mock the handler to throw an error by passing invalid parameters to internal functions
      const result = await build_simLogic(
        {
          workspacePath: '/path/to/workspace',
          scheme: 'MyScheme',
          simulatorName: 'iPhone 16',
        },
        mockExecutor,
      );

      // Should handle the build successfully
      expect(result.content).toEqual([
        {
          type: 'text',
          text: '✅ iOS Simulator Build build succeeded for scheme MyScheme.',
        },
        {
          type: 'text',
          text: expect.stringContaining('Next Steps:'),
        },
      ]);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/mcp/tools/logging/start_device_log_cap.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Logging Plugin: Start Device Log Capture
 *
 * Starts capturing logs from a specified Apple device by launching the app with console output.
 */

import * as path from 'path';
import type { ChildProcess } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
import * as z from 'zod';
import { log } from '../../../utils/logging/index.ts';
import type { CommandExecutor, FileSystemExecutor } from '../../../utils/execution/index.ts';
import {
  getDefaultCommandExecutor,
  getDefaultFileSystemExecutor,
} from '../../../utils/execution/index.ts';
import { ToolResponse } from '../../../types/common.ts';
import {
  createSessionAwareTool,
  getSessionAwareToolSchemaShape,
} from '../../../utils/typed-tool-factory.ts';
import {
  activeDeviceLogSessions,
  type DeviceLogSession,
} from '../../../utils/log-capture/device-log-sessions.ts';
import type { WriteStream } from 'fs';

/**
 * Log file retention policy for device logs:
 * - Old log files (older than LOG_RETENTION_DAYS) are automatically deleted from the temp directory
 * - Cleanup runs on every new log capture start
 */
const LOG_RETENTION_DAYS = 3;
const DEVICE_LOG_FILE_PREFIX = 'xcodemcp_device_log_';

// Note: Device and simulator logging use different approaches due to platform constraints:
// - Simulators use 'xcrun simctl' with console-pty and OSLog stream capabilities
// - Devices use 'xcrun devicectl' with console output only (no OSLog streaming)
// The different command structures and output formats make sharing infrastructure complex.
// However, both follow similar patterns for session management and log retention.
const EARLY_FAILURE_WINDOW_MS = 5000;
const INITIAL_OUTPUT_LIMIT = 8_192;
const DEFAULT_JSON_RESULT_WAIT_MS = 8000;

const FAILURE_PATTERNS = [
  /The application failed to launch/i,
  /Provide a valid bundle identifier/i,
  /The requested application .* is not installed/i,
  /NSOSStatusErrorDomain/i,
  /NSLocalizedFailureReason/i,
  /ERROR:/i,
];

type JsonOutcome = {
  errorMessage?: string;
  pid?: number;
};

type DevicectlLaunchJson = {
  result?: {
    process?: {
      processIdentifier?: unknown;
    };
  };
  error?: {
    code?: unknown;
    domain?: unknown;
    localizedDescription?: unknown;
    userInfo?: Record<string, unknown> | undefined;
  };
};

function getJsonResultWaitMs(): number {
  const raw = process.env.XBMCP_LAUNCH_JSON_WAIT_MS;
  if (raw === undefined) {
    return DEFAULT_JSON_RESULT_WAIT_MS;
  }

  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    return DEFAULT_JSON_RESULT_WAIT_MS;
  }

  return parsed;
}

function safeParseJson(text: string): DevicectlLaunchJson | null {
  try {
    const parsed = JSON.parse(text) as unknown;
    if (!parsed || typeof parsed !== 'object') {
      return null;
    }
    return parsed as DevicectlLaunchJson;
  } catch {
    return null;
  }
}

function extractJsonOutcome(json: DevicectlLaunchJson | null): JsonOutcome | null {
  if (!json) {
    return null;
  }

  const resultProcess = json.result?.process;
  const pidValue = resultProcess?.processIdentifier;
  if (typeof pidValue === 'number' && Number.isFinite(pidValue)) {
    return { pid: pidValue };
  }

  const error = json.error;
  if (!error) {
    return null;
  }

  const parts: string[] = [];

  if (typeof error.localizedDescription === 'string' && error.localizedDescription.length > 0) {
    parts.push(error.localizedDescription);
  }

  const userInfo = error.userInfo ?? {};
  const recovery = userInfo?.NSLocalizedRecoverySuggestion;
  const failureReason = userInfo?.NSLocalizedFailureReason;
  const bundleIdentifier = userInfo?.BundleIdentifier;

  if (typeof failureReason === 'string' && failureReason.length > 0) {
    parts.push(failureReason);
  }

  if (typeof recovery === 'string' && recovery.length > 0) {
    parts.push(recovery);
  }

  if (typeof bundleIdentifier === 'string' && bundleIdentifier.length > 0) {
    parts.push(`BundleIdentifier = ${bundleIdentifier}`);
  }

  const domain = error.domain;
  const code = error.code;
  const domainPart = typeof domain === 'string' && domain.length > 0 ? domain : undefined;
  const codePart = typeof code === 'number' && Number.isFinite(code) ? code : undefined;

  if (domainPart || codePart !== undefined) {
    parts.push(`(${domainPart ?? 'UnknownDomain'} code ${codePart ?? 'unknown'})`);
  }

  if (parts.length === 0) {
    return { errorMessage: 'Launch failed' };
  }

  return { errorMessage: parts.join('\n') };
}

async function removeFileIfExists(
  targetPath: string,
  fileExecutor: FileSystemExecutor,
): Promise<void> {
  try {
    if (fileExecutor.existsSync(targetPath)) {
      await fileExecutor.rm(targetPath, { force: true });
    }
  } catch {
    // Best-effort cleanup only
  }
}

async function pollJsonOutcome(
  jsonPath: string,
  fileExecutor: FileSystemExecutor,
  timeoutMs: number,
): Promise<JsonOutcome | null> {
  const start = Date.now();

  const readOnce = async (): Promise<JsonOutcome | null> => {
    try {
      const exists = fileExecutor.existsSync(jsonPath);

      if (!exists) {
        return null;
      }

      const content = await fileExecutor.readFile(jsonPath, 'utf8');

      const outcome = extractJsonOutcome(safeParseJson(content));
      if (outcome) {
        await removeFileIfExists(jsonPath, fileExecutor);
        return outcome;
      }
    } catch {
      // File may still be written; try again later
    }

    return null;
  };

  const immediate = await readOnce();
  if (immediate) {
    return immediate;
  }

  if (timeoutMs <= 0) {
    return null;
  }

  let delay = Math.min(100, Math.max(10, Math.floor(timeoutMs / 4) || 10));

  while (Date.now() - start < timeoutMs) {
    await new Promise((resolve) => setTimeout(resolve, delay));
    const result = await readOnce();
    if (result) {
      return result;
    }
    delay = Math.min(400, delay + 50);
  }

  return null;
}

type WriteStreamWithClosed = WriteStream & { closed?: boolean };

/**
 * Start a log capture session for an iOS device by launching the app with console output.
 * Uses the devicectl command to launch the app and capture console logs.
 * Returns { sessionId, error? }
 */
export async function startDeviceLogCapture(
  params: {
    deviceUuid: string;
    bundleId: string;
  },
  executor: CommandExecutor = getDefaultCommandExecutor(),
  fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor(),
): Promise<{ sessionId: string; error?: string }> {
  // Clean up old logs before starting a new session
  await cleanOldDeviceLogs(fileSystemExecutor);

  const { deviceUuid, bundleId } = params;
  const logSessionId = uuidv4();
  const logFileName = `${DEVICE_LOG_FILE_PREFIX}${logSessionId}.log`;
  const tempDir = fileSystemExecutor.tmpdir();
  const logFilePath = path.join(tempDir, logFileName);
  const launchJsonPath = path.join(tempDir, `devicectl-launch-${logSessionId}.json`);

  let logStream: WriteStream | undefined;

  try {
    await fileSystemExecutor.mkdir(tempDir, { recursive: true });
    await fileSystemExecutor.writeFile(logFilePath, '');

    logStream = fileSystemExecutor.createWriteStream(logFilePath, { flags: 'a' });

    logStream.write(
      `\n--- Device log capture for bundle ID: ${bundleId} on device: ${deviceUuid} ---\n`,
    );

    // Use executor with dependency injection instead of spawn directly
    const result = await executor(
      [
        'xcrun',
        'devicectl',
        'device',
        'process',
        'launch',
        '--console',
        '--terminate-existing',
        '--device',
        deviceUuid,
        '--json-output',
        launchJsonPath,
        bundleId,
      ],
      'Device Log Capture',
      true,
      undefined,
      true,
    );

    if (!result.success) {
      log(
        'error',
        `Device log capture process reported failure: ${result.error ?? 'unknown error'}`,
      );
      if (logStream && !logStream.destroyed) {
        logStream.write(
          `\n--- Device log capture failed to start ---\n${result.error ?? 'Unknown error'}\n`,
        );
        logStream.end();
      }
      return {
        sessionId: '',
        error: result.error ?? 'Failed to start device log capture',
      };
    }

    const childProcess = result.process;
    if (!childProcess) {
      throw new Error('Device log capture process handle was not returned');
    }

    const session: DeviceLogSession = {
      process: childProcess,
      logFilePath,
      deviceUuid,
      bundleId,
      logStream,
      hasEnded: false,
    };

    let bufferedOutput = '';
    const appendBufferedOutput = (text: string): void => {
      bufferedOutput += text;
      if (bufferedOutput.length > INITIAL_OUTPUT_LIMIT) {
        bufferedOutput = bufferedOutput.slice(bufferedOutput.length - INITIAL_OUTPUT_LIMIT);
      }
    };

    let triggerImmediateFailure: ((message: string) => void) | undefined;

    const handleOutput = (chunk: unknown): void => {
      if (!logStream || logStream.destroyed) return;
      const text =
        typeof chunk === 'string'
          ? chunk
          : chunk instanceof Buffer
            ? chunk.toString('utf8')
            : String(chunk ?? '');
      if (text.length > 0) {
        appendBufferedOutput(text);
        const extracted = extractFailureMessage(bufferedOutput);
        if (extracted) {
          triggerImmediateFailure?.(extracted);
        }
        logStream.write(text);
      }
    };

    childProcess.stdout?.setEncoding?.('utf8');
    childProcess.stdout?.on?.('data', handleOutput);
    childProcess.stderr?.setEncoding?.('utf8');
    childProcess.stderr?.on?.('data', handleOutput);

    const cleanupStreams = (): void => {
      childProcess.stdout?.off?.('data', handleOutput);
      childProcess.stderr?.off?.('data', handleOutput);
    };

    const earlyFailure = await detectEarlyLaunchFailure(
      childProcess,
      EARLY_FAILURE_WINDOW_MS,
      () => bufferedOutput,
      (handler) => {
        triggerImmediateFailure = handler;
      },
    );

    if (earlyFailure) {
      cleanupStreams();
      session.hasEnded = true;

      const failureMessage =
        earlyFailure.errorMessage && earlyFailure.errorMessage.length > 0
          ? earlyFailure.errorMessage
          : `Device log capture process exited immediately (exit code: ${
              earlyFailure.exitCode ?? 'unknown'
            })`;

      log('error', `Device log capture failed to start: ${failureMessage}`);
      if (logStream && !logStream.destroyed) {
        try {
          logStream.write(`\n--- Device log capture failed to start ---\n${failureMessage}\n`);
        } catch {
          // best-effort logging
        }
        logStream.end();
      }

      await removeFileIfExists(launchJsonPath, fileSystemExecutor);

      childProcess.kill?.('SIGTERM');
      return { sessionId: '', error: failureMessage };
    }

    const jsonOutcome = await pollJsonOutcome(
      launchJsonPath,
      fileSystemExecutor,
      getJsonResultWaitMs(),
    );

    if (jsonOutcome?.errorMessage) {
      cleanupStreams();
      session.hasEnded = true;

      const failureMessage = jsonOutcome.errorMessage;

      log('error', `Device log capture failed to start (JSON): ${failureMessage}`);

      if (logStream && !logStream.destroyed) {
        try {
          logStream.write(`\n--- Device log capture failed to start ---\n${failureMessage}\n`);
        } catch {
          // ignore secondary logging failures
        }
        logStream.end();
      }

      childProcess.kill?.('SIGTERM');
      return { sessionId: '', error: failureMessage };
    }

    if (jsonOutcome?.pid && logStream && !logStream.destroyed) {
      try {
        logStream.write(`Process ID: ${jsonOutcome.pid}\n`);
      } catch {
        // best-effort logging only
      }
    }

    childProcess.once?.('error', (err) => {
      log(
        'error',
        `Device log capture process error (session ${logSessionId}): ${
          err instanceof Error ? err.message : String(err)
        }`,
      );
    });

    childProcess.once?.('close', (code) => {
      cleanupStreams();
      session.hasEnded = true;
      if (logStream && !logStream.destroyed && !(logStream as WriteStreamWithClosed).closed) {
        logStream.write(`\n--- Device log capture ended (exit code: ${code ?? 'unknown'}) ---\n`);
        logStream.end();
      }
      void removeFileIfExists(launchJsonPath, fileSystemExecutor);
    });

    // For testing purposes, we'll simulate process management
    // In actual usage, the process would be managed by the executor result
    activeDeviceLogSessions.set(logSessionId, session);

    log('info', `Device log capture started with session ID: ${logSessionId}`);
    return { sessionId: logSessionId };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    log('error', `Failed to start device log capture: ${message}`);
    if (logStream && !logStream.destroyed && !(logStream as WriteStreamWithClosed).closed) {
      try {
        logStream.write(`\n--- Device log capture failed: ${message} ---\n`);
      } catch {
        // ignore secondary stream write failures
      }
      logStream.end();
    }
    await removeFileIfExists(launchJsonPath, fileSystemExecutor);
    return { sessionId: '', error: message };
  }
}

type EarlyFailureResult = {
  exitCode: number | null;
  errorMessage?: string;
};

function detectEarlyLaunchFailure(
  process: ChildProcess,
  timeoutMs: number,
  getBufferedOutput?: () => string,
  registerImmediateFailure?: (handler: (message: string) => void) => void,
): Promise<EarlyFailureResult | null> {
  if (process.exitCode != null) {
    if (process.exitCode === 0) {
      const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
      return Promise.resolve(
        failureFromOutput ? { exitCode: process.exitCode, errorMessage: failureFromOutput } : null,
      );
    }
    const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
    return Promise.resolve({ exitCode: process.exitCode, errorMessage: failureFromOutput });
  }

  return new Promise<EarlyFailureResult | null>((resolve) => {
    let settled = false;

    const finalize = (result: EarlyFailureResult | null): void => {
      if (settled) return;
      settled = true;
      process.removeListener('close', onClose);
      process.removeListener('error', onError);
      clearTimeout(timer);
      resolve(result);
    };

    registerImmediateFailure?.((message) => {
      finalize({ exitCode: process.exitCode ?? null, errorMessage: message });
    });

    const onClose = (code: number | null): void => {
      const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
      if (code === 0 && failureFromOutput) {
        finalize({ exitCode: code ?? null, errorMessage: failureFromOutput });
        return;
      }
      if (code === 0) {
        finalize(null);
      } else {
        finalize({ exitCode: code ?? null, errorMessage: failureFromOutput });
      }
    };

    const onError = (error: Error): void => {
      finalize({ exitCode: null, errorMessage: error.message });
    };

    const timer = setTimeout(() => {
      const failureFromOutput = extractFailureMessage(getBufferedOutput?.());
      if (failureFromOutput) {
        process.kill?.('SIGTERM');
        finalize({ exitCode: process.exitCode ?? null, errorMessage: failureFromOutput });
        return;
      }
      finalize(null);
    }, timeoutMs);

    process.once('close', onClose);
    process.once('error', onError);
  });
}

function extractFailureMessage(output?: string): string | undefined {
  if (!output) {
    return undefined;
  }
  const normalized = output.replace(/\r/g, '');
  const lines = normalized
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);

  const shouldInclude = (line?: string): boolean => {
    if (!line) return false;
    return (
      line.startsWith('NS') ||
      line.startsWith('BundleIdentifier') ||
      line.startsWith('Provide ') ||
      line.startsWith('The application') ||
      line.startsWith('ERROR:')
    );
  };

  for (const pattern of FAILURE_PATTERNS) {
    const matchIndex = lines.findIndex((line) => pattern.test(line));
    if (matchIndex === -1) {
      continue;
    }

    const snippet: string[] = [lines[matchIndex]];
    const nextLine = lines[matchIndex + 1];
    const thirdLine = lines[matchIndex + 2];
    if (shouldInclude(nextLine)) snippet.push(nextLine);
    if (shouldInclude(thirdLine)) snippet.push(thirdLine);
    const message = snippet.join('\n').trim();
    if (message.length > 0) {
      return message;
    }
    return lines[matchIndex];
  }

  return undefined;
}

/**
 * Deletes device log files older than LOG_RETENTION_DAYS from the temp directory.
 * Runs quietly; errors are logged but do not throw.
 */
// Device logs follow the same retention policy as simulator logs but use a different prefix
// to avoid conflicts. Both clean up logs older than LOG_RETENTION_DAYS automatically.
async function cleanOldDeviceLogs(fileSystemExecutor: FileSystemExecutor): Promise<void> {
  const tempDir = fileSystemExecutor.tmpdir();
  let files: unknown[];
  try {
    files = await fileSystemExecutor.readdir(tempDir);
  } catch (err) {
    log(
      'warn',
      `Could not read temp dir for device log cleanup: ${err instanceof Error ? err.message : String(err)}`,
    );
    return;
  }
  const now = Date.now();
  const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
  const fileNames = files.filter((file): file is string => typeof file === 'string');

  await Promise.all(
    fileNames
      .filter((f) => f.startsWith(DEVICE_LOG_FILE_PREFIX) && f.endsWith('.log'))
      .map(async (f) => {
        const filePath = path.join(tempDir, f);
        try {
          const stat = await fileSystemExecutor.stat(filePath);
          if (now - stat.mtimeMs > retentionMs) {
            await fileSystemExecutor.rm(filePath, { force: true });
            log('info', `Deleted old device log file: ${filePath}`);
          }
        } catch (err) {
          log(
            'warn',
            `Error during device log cleanup for ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
          );
        }
      }),
  );
}

// Define schema as ZodObject
const startDeviceLogCapSchema = z.object({
  deviceId: z.string().describe('UDID of the device (obtained from list_devices)'),
  bundleId: z.string().describe('Bundle identifier of the app to launch and capture logs for.'),
});

const publicSchemaObject = startDeviceLogCapSchema.omit({ deviceId: true } as const);

// Use z.infer for type safety
type StartDeviceLogCapParams = z.infer<typeof startDeviceLogCapSchema>;

/**
 * Core business logic for starting device log capture.
 */
export async function start_device_log_capLogic(
  params: StartDeviceLogCapParams,
  executor: CommandExecutor,
  fileSystemExecutor?: FileSystemExecutor,
): Promise<ToolResponse> {
  const { deviceId, bundleId } = params;

  const resolvedFileSystemExecutor = fileSystemExecutor ?? getDefaultFileSystemExecutor();

  const { sessionId, error } = await startDeviceLogCapture(
    {
      deviceUuid: deviceId,
      bundleId: bundleId,
    },
    executor,
    resolvedFileSystemExecutor,
  );

  if (error) {
    return {
      content: [
        {
          type: 'text',
          text: `Failed to start device log capture: ${error}`,
        },
      ],
      isError: true,
    };
  }

  return {
    content: [
      {
        type: 'text',
        text: `✅ Device log capture started successfully\n\nSession ID: ${sessionId}\n\nNote: The app has been launched on the device with console output capture enabled.\n\nNext Steps:\n1. Interact with your app on the device\n2. Use stop_device_log_cap({ logSessionId: '${sessionId}' }) to stop capture and retrieve logs`,
      },
    ],
  };
}

export default {
  name: 'start_device_log_cap',
  description: 'Starts log capture on a connected device.',
  schema: getSessionAwareToolSchemaShape({
    sessionAware: publicSchemaObject,
    legacy: startDeviceLogCapSchema,
  }),
  annotations: {
    title: 'Start Device Log Capture',
    destructiveHint: true,
  },
  handler: createSessionAwareTool<StartDeviceLogCapParams>({
    internalSchema: startDeviceLogCapSchema as unknown as z.ZodType<
      StartDeviceLogCapParams,
      unknown
    >,
    logicFunction: start_device_log_capLogic,
    getExecutor: getDefaultCommandExecutor,
    requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }],
  }),
};

```

--------------------------------------------------------------------------------
/docs/DAP_BACKEND_IMPLEMENTATION_PLAN.md:
--------------------------------------------------------------------------------

```markdown
## Goal & constraints (grounded in current code)

Implement a real **`lldb-dap` Debug Adapter Protocol backend** that plugs into the existing debugger architecture without changing MCP tool names/schemas. The DAP backend remains **opt-in only** via `XCODEBUILDMCP_DEBUGGER_BACKEND=dap` (current selection logic in `src/utils/debugger/debugger-manager.ts`).

Key integration points already in place:

- **Backend contract**: `src/utils/debugger/backends/DebuggerBackend.ts`
- **Backend selection & session lifecycle**: `src/utils/debugger/debugger-manager.ts`
- **MCP tool surface area**: `src/mcp/tools/debugging/*` (attach, breakpoints, stack, variables, command, detach)
- **Subprocess patterns**: `src/utils/execution/interactive-process.ts` (interactive, piped stdio, test-safe default spawner)
- **DI/test safety**: defaults throw under Vitest (`getDefaultCommandExecutor`, `getDefaultInteractiveSpawner`)
- **Docs baseline**: `docs/DAP_BACKEND_IMPLEMENTATION_PLAN.md`, `docs/DEBUGGING_ARCHITECTURE.md`

---

## Implementation status (current)

Implemented modules and behavior (as of this document):

- DAP protocol and transport: `src/utils/debugger/dap/types.ts`, `src/utils/debugger/dap/transport.ts`
- Adapter discovery: `src/utils/debugger/dap/adapter-discovery.ts`
- Backend implementation: `src/utils/debugger/backends/dap-backend.ts`
- Conditional breakpoints: backend-level support via `DebuggerBackend.addBreakpoint(..., { condition })`
- Tool updates: `src/mcp/tools/debugging/debug_breakpoint_add.ts` passes conditions to backend
- Health check: `doctor` now reports `lldb-dap` availability
- Tests: DAP transport framing, backend mapping, and debugger manager selection tests

### MCP tool → DAP request mapping (current)

| MCP tool | DebuggerManager call | DAP requests |
| --- | --- | --- |
| `debug_attach_sim` | `createSession` → `attach` | `initialize` → `attach` → `configurationDone` |
| `debug_lldb_command` | `runCommand` | `evaluate` (context: `repl`) |
| `debug_stack` | `getStack` | `threads` → `stackTrace` |
| `debug_variables` | `getVariables` | `threads` → `stackTrace` → `scopes` → `variables` |
| `debug_breakpoint_add` | `addBreakpoint` | `setBreakpoints` / `setFunctionBreakpoints` |
| `debug_breakpoint_remove` | `removeBreakpoint` | `setBreakpoints` / `setFunctionBreakpoints` |
| `debug_detach` | `detach` | `disconnect` |

### Breakpoint strategy (current)

- Breakpoints are stateful: DAP removal re-applies `setBreakpoints`/`setFunctionBreakpoints` with the remaining list.
- Conditions are passed as part of the breakpoint request in both backends:
  - DAP: `breakpoints[].condition` or `functionBreakpoints[].condition`
  - LLDB CLI: `breakpoint modify -c "<condition>" <id>`

---

## Architectural decisions to make (explicit)

### 1) Spawn model: one `lldb-dap` process per debug session
**Decision**: Each `DebuggerManager.createSession()` creates a new backend instance, which owns a single `lldb-dap` subprocess for the lifetime of that session.

- Aligns with current LLDB CLI backend (one long-lived interactive `lldb` per session).
- Keeps multi-session support (`DebuggerManager.sessions: Map`) straightforward.

### 2) Transport abstraction: DAP framing + request correlation in a dedicated module
**Decision**: Build a dedicated DAP transport that:
- implements `Content-Length` framing
- correlates requests/responses by `seq`
- emits DAP events

This keeps `DapBackend` focused on **mapping MCP tool operations → DAP requests**.

### 3) Breakpoint conditions support: move condition handling into the backend API
**Decision**: Extend internal debugger API to support conditional breakpoints *without relying on* “LLDB command follow-ups” (which are CLI-specific).

This avoids depending on DAP `evaluate` for breakpoint modification and keeps semantics consistent across backends.

---

## Implementation plan (by component / file)

### A) Add DAP protocol & transport layer

#### New files

##### 1) `src/utils/debugger/dap/types.ts`
Define minimal DAP types used by the backend (not a full spec).

Example types (illustrative, not exhaustive):

```ts
export type DapRequest<C = unknown> = {
  seq: number;
  type: 'request';
  command: string;
  arguments?: C;
};

export type DapResponse<B = unknown> = {
  seq: number;
  type: 'response';
  request_seq: number;
  success: boolean;
  command: string;
  message?: string;
  body?: B;
};

export type DapEvent<B = unknown> = {
  seq: number;
  type: 'event';
  event: string;
  body?: B;
};
```

Also define bodies used in mapping:
- `InitializeResponseBody` (capabilities)
- `ThreadsResponseBody`
- `StackTraceResponseBody`
- `ScopesResponseBody`
- `VariablesResponseBody`
- `SetBreakpointsResponseBody`
- `EvaluateResponseBody`
- event bodies: `StoppedEventBody`, `OutputEventBody`, `TerminatedEventBody`

**Side effects / impact**: none outside debugger subsystem; ensures type safety inside DAP modules.

---

##### 2) `src/utils/debugger/dap/transport.ts`
Implement DAP over stdio.

**Dependencies / imports**
- `node:events` (EventEmitter) or a small typed emitter pattern
- `src/utils/execution/index.ts` for `InteractiveSpawner` and `InteractiveProcess` types
- `src/utils/logging/index.ts` for `log`
- `src/utils/CommandExecutor.ts` type (for adapter discovery helper if kept here)

**Core responsibilities**
- Spawn adapter process (or accept an already spawned `InteractiveProcess`)
- Parse stdout stream into discrete DAP messages using `Content-Length` framing
- Maintain:
  - `nextSeq: number`
  - `pending: Map<number, { resolve, reject, timeout }>` keyed by request `seq`
- Expose:
  - `sendRequest(command, args, opts?) => Promise<body>`
  - event subscription: `onEvent(handler)` or `on('event', ...)`
  - lifecycle: `dispose()` (must not throw)

**Key function signatures**

```ts
export type DapTransportOptions = {
  spawner: InteractiveSpawner;
  adapterCommand: string[]; // e.g. ['xcrun', 'lldb-dap'] or [resolvedPath]
  env?: Record<string, string>;
  cwd?: string;
  logPrefix?: string;
};

export class DapTransport {
  constructor(opts: DapTransportOptions);

  sendRequest<A, B>(
    command: string,
    args?: A,
    opts?: { timeoutMs?: number },
  ): Promise<B>;

  onEvent(handler: (evt: DapEvent) => void): () => void;

  dispose(): void; // best-effort, never throw
}
```

**Framing logic**
- Maintain an internal `Buffer`/string accumulator for stdout.
- Repeatedly:
  - find `\r\n\r\n`
  - parse headers for `Content-Length`
  - wait until body bytes are available
  - `JSON.parse` body into `{ type: 'response' | 'event' | 'request' }`

**Process failure handling**
- On adapter `exit`/`error`, reject all pending requests with a clear error (and include exit detail).
- Log stderr output at `debug` level; do **not** feed stderr into framing.

**Concurrency**
- Transport supports multiple in-flight requests concurrently (DAP allows it).
- Backend may still serialize higher-level operations if stateful.

**Side effects**
- Add a long-lived child process per session.
- Requires careful memory management in the framing buffer (ensure you slice consumed bytes).

---

### B) Adapter discovery (`xcrun --find lldb-dap`)

#### New helper (recommended)
##### 3) `src/utils/debugger/dap/adapter-discovery.ts` (new)
**Purpose**: centralize resolution and produce actionable errors when DAP is explicitly selected but unavailable.

**Uses**
- `CommandExecutor` to run `xcrun --find lldb-dap`
- `log` for diagnostics
- throw a `DependencyError` (from `src/utils/errors.ts`) or plain `Error` with a consistent message

Example signature:

```ts
import type { CommandExecutor } from '../../execution/index.ts';

export async function resolveLldbDapCommand(opts: {
  executor: CommandExecutor;
}): Promise<string[]>;
// returns e.g. ['xcrun', 'lldb-dap'] OR [absolutePath]
```

**Design choice**
- Returning `['xcrun','lldb-dap']` is simplest (no dependency on parsing).
- Returning `[absolutePath]` provides a stronger “tool exists” guarantee.

**Impact**
- Enables a clean error message early in session creation.
- Keeps `DapBackend` simpler.

---

### C) Implement `DapBackend` (current)

#### Modify file: `src/utils/debugger/backends/dap-backend.ts`

**Implemented** as a real backend that:
- discovers adapter (`resolveLldbDapCommand`)
- creates `DapTransport`
- performs DAP handshake (`initialize`)
- attaches by PID (`attach`)
- maps backend interface methods to DAP requests

**Dependencies**
- `DapTransport`
- `resolveLldbDapCommand`
- `getDefaultCommandExecutor` and `getDefaultInteractiveSpawner` (production defaults)
- `log`
- existing backend interface/types

**Constructor / factory**
Update `createDapBackend()` to accept injectable deps, mirroring the CLI backend’s injection style.

```ts
export async function createDapBackend(opts?: {
  executor?: CommandExecutor;
  spawner?: InteractiveSpawner;
  requestTimeoutMs?: number;
}): Promise<DebuggerBackend>;
```

> This is critical for tests because defaults throw under Vitest.

**Session state to maintain inside `DapBackend`**
- `transport: DapTransport | null`
- `attached: boolean`
- `lastStoppedThreadId: number | null`
- `cachedThreads: { id: number; name?: string }[] | null` (optional)
- breakpoint registry:
  - `breakpointsById: Map<number, BreakpointSpec & { condition?: string }>`
  - for DAP “remove breakpoint”, you must re-issue `setBreakpoints`/`setFunctionBreakpoints` with the updated list, so also keep:
    - `fileLineBreakpointsByFile: Map<string, Array<{ line: number; condition?: string; id?: number }>>`
    - `functionBreakpoints: Array<{ name: string; condition?: string; id?: number }>`
- optional cached stack frames from the last `stackTrace` call (for variables lookup)

**Backend lifecycle mapping**
- `attach()`:
  1) spawn `lldb-dap`
  2) `initialize`
  3) `attach` with pid (+ waitFor mapping)
  4) `configurationDone` if required by lldb-dap behavior (plan for it even if no-op)
  5) mark attached

- `detach()`
  - send `disconnect` with `terminateDebuggee: false` (do not kill app)
  - dispose transport / kill process

- `dispose()`
  - best-effort cleanup; **must not throw** (important because `DebuggerManager.createSession` calls dispose to clean up on attach failure)

**Method mappings (MCP tools → DebuggerManager → DapBackend)**

1) `runCommand(command: string, opts?)`
- Map to DAP `evaluate` with `context: 'repl'`
- Return string output from `EvaluateResponse.body.result` and/or `body.output`
- If adapter doesn’t support command-style repl evaluation, return a clear error message suggesting `lldb-cli` backend.

2) `getStack(opts?: { threadIndex?: number; maxFrames?: number })`
- DAP sequence:
  - `threads`
  - select thread:
    - if a `stopped` event has a `threadId`, prefer that when `threadIndex` is undefined
    - else map `threadIndex` to array index (document this)
  - `stackTrace({ threadId, startFrame: 0, levels: maxFrames })`
- Format output as readable text (LLDB-like) to keep tool behavior familiar:
  - `frame #<i>: <name> at <path>:<line>`
- If stackTrace fails due to running state, return a helpful error:
  - “Process is running; pause or hit a breakpoint to fetch stack.”

3) `getVariables(opts?: { frameIndex?: number })`
- DAP sequence:
  - resolve thread as above
  - `stackTrace` to get frames
  - choose frame by `frameIndex` (default 0)
  - `scopes({ frameId })`
  - for each scope: `variables({ variablesReference })`
- Format output as text with sections per scope:
  - `Locals:\n  x = 1\n  y = ...`

4) `addBreakpoint(spec: BreakpointSpec, opts?: { condition?: string })`
- For `file-line`:
  - update `fileLineBreakpointsByFile[file]`
  - call `setBreakpoints({ source: { path: file }, breakpoints: [{ line, condition }] })`
  - parse returned `breakpoints[]` to find matching line and capture `id`
- For `function`:
  - update `functionBreakpoints`
  - call `setFunctionBreakpoints({ breakpoints: [{ name, condition }] })`
- Return `BreakpointInfo`:
  - `id` must be a number (from DAP breakpoint id; if missing, generate a synthetic id and store mapping, but prefer real id)
  - `rawOutput` can be a pretty JSON snippet or a short text summary

5) `removeBreakpoint(id: number)`
- Look up spec in `breakpointsById`
- Remove it from the corresponding registry
- Re-issue `setBreakpoints` or `setFunctionBreakpoints` with the remaining breakpoints
- Return text confirmation

**Important: DAP vs existing condition flow**
- Today `debug_breakpoint_add` sets condition by issuing an LLDB command after creation.
- With the above, condition becomes part of breakpoint creation and removal logic, backend-agnostic.

---

### D) Internal API adjustment for conditional breakpoints (recommended)

#### Modify: `src/utils/debugger/backends/DebuggerBackend.ts`
Update signature:

```ts
addBreakpoint(spec: BreakpointSpec, opts?: { condition?: string }): Promise<BreakpointInfo>;
```

#### Modify: `src/utils/debugger/debugger-manager.ts`
Update method:

```ts
async addBreakpoint(
  id: string | undefined,
  spec: BreakpointSpec,
  opts?: { condition?: string },
): Promise<BreakpointInfo>
```

Pass `opts` through to `backend.addBreakpoint`.

**Impact**
- Requires updating both backends + the tool call site.
- Improves cross-backend compatibility and avoids “DAP evaluate must support breakpoint modify”.

#### Modify: `src/utils/debugger/backends/lldb-cli-backend.ts`
Implement condition via LLDB command internally after breakpoint creation (current behavior, just moved):

- after parsing breakpoint id:
  - if `opts?.condition`, run `breakpoint modify -c "<escaped>" <id>`

This keeps condition support identical for LLDB CLI users.

---

### E) Update MCP tool logic to use new breakpoint API

#### Modify: `src/mcp/tools/debugging/debug_breakpoint_add.ts`
Change logic to pass `condition` into `ctx.debugger.addBreakpoint(...)` and remove the follow-up `breakpoint modify ...` command.

**Before**
- call `addBreakpoint()`
- if condition, call `runCommand("breakpoint modify ...")`

**After**
- call `addBreakpoint(sessionId, spec, { condition })`
- no extra `runCommand` required

**Impact / side effects**
- Output remains the same shape, but the “rawOutput” content for DAP may differ (acceptable).
- Improves backend portability.

---

### F) Backend selection & opt-in behavior (already mostly correct)

#### Modify (optional but recommended): `src/utils/debugger/debugger-manager.ts`
Keep selection rules but improve failure clarity:

- If backend kind is `dap`, and adapter discovery fails, throw an error like:
  - `DAP backend selected but lldb-dap not found. Ensure Xcode is installed and xcrun can locate lldb-dap, or set XCODEBUILDMCP_DEBUGGER_BACKEND=lldb-cli.`

Also ensure that dispose failures do not mask attach failures:
- in `createSession` catch, wrap `dispose()` in its own try/catch (even if backend should not throw).

---

### G) Diagnostics / “doctor” integration (validation surface)

#### Modify: `src/mcp/tools/doctor/doctor.ts` (not shown in provided contents)
Add a DAP capability line:
- `lldb-dap available: yes/no`
- if env selects dap, include a prominent warning/error section when missing

Implementation approach:
- reuse `CommandExecutor` and call `xcrun --find lldb-dap`
- do not fail doctor entirely if missing; just report

**Side effects**
- Improves discoverability and reduces “mystery failures” when users opt into dap.

---

## Concurrency & state management plan

### Transport-level
- Fully concurrent in-flight DAP requests supported via:
  - `seq` generation
  - `pending` map keyed by `seq`
- Each request can set its own timeout (`timeoutMs`).

### Backend-level
Use a serialized queue **only where state mutation occurs**, e.g.:
- updating breakpoint registries
- attach/detach transitions

Pattern (same as LLDB CLI backend):

```ts
private queue: Promise<unknown> = Promise.resolve();

private enqueue<T>(work: () => Promise<T>): Promise<T> { ... }
```

**Reasoning**
- Prevent races such as:
  - addBreakpoint + removeBreakpoint in parallel, reissuing `setBreakpoints` inconsistently.

---

## Error handling & logging strategy

### Error taxonomy (pragmatic, consistent with current tools)
- Backend throws `Error` with clear messages.
- MCP tools already catch and wrap errors via `createErrorResponse(...)`.

### Where to log
- `DapTransport`:
  - `log('debug', ...)` for raw events (optionally gated by env)
  - `log('error', ...)` on process exit while requests are pending
- `DapBackend`:
  - minimal `info` logs on attach/detach
  - `debug` logs for request mapping (command names, not full payloads unless opted in)

### New optional env flags (config plan)
Document these (no need to require them):
- `XCODEBUILDMCP_DAP_REQUEST_TIMEOUT_MS` (default to 30_000)
- `XCODEBUILDMCP_DAP_LOG_EVENTS=true` (default false)

---

## Tests (architecture-aware, DI-compliant)

Even though this is “testing”, it directly impacts design because default spawners/executors throw under Vitest.

### 1) Add a first-class mock interactive spawner utility
#### Modify: `src/test-utils/mock-executors.ts`
Add:

```ts
export function createMockInteractiveSpawner(script: {
  // map writes -> stdout/stderr emissions, or a programmable fake
}): InteractiveSpawner;
```

This avoids ad-hoc manual mocks and matches the project’s “approved mocks live in test-utils” philosophy.

### 2) DAP framing tests
New: `src/utils/debugger/dap/__tests__/transport-framing.test.ts`
- Feed partial header/body chunks into the transport parser using `PassThrough` streams behind a mock InteractiveProcess.
- Assert:
  - correct parsing across chunk boundaries
  - multiple messages in one chunk
  - invalid Content-Length handling

### 3) Backend mapping tests (no real lldb-dap)
New: `src/utils/debugger/backends/__tests__/dap-backend.test.ts`
- Use `createMockExecutor()` to fake adapter discovery.
- Use `createMockInteractiveSpawner()` to simulate an adapter that returns scripted DAP responses:
  - initialize → success
  - attach → success
  - threads/stackTrace/scopes/variables → stable fixtures
- Validate:
  - `getStack()` formatting
  - `getVariables()` formatting
  - breakpoint add/remove registry behavior
  - `dispose()` never throws

### 4) DebuggerManager selection test
New: `src/utils/debugger/__tests__/debugger-manager-dap.test.ts`
- Inject a custom `backendFactory` that returns a fake backend (or the scripted DAP backend) and verify:
  - env selection
  - attach failure triggers dispose
  - current session behavior unchanged

---

## Docs updates (grounded in existing docs)

### 1) Update `docs/DAP_BACKEND_IMPLEMENTATION_PLAN.md`
Replace/extend the existing outline with the following:
- finalized module list (`dap/types.ts`, `dap/transport.ts`, discovery helper)
- breakpoint strategy (stateful re-issue `setBreakpoints`)
- explicit mapping table per MCP tool

### 2) Update `docs/DEBUGGING_ARCHITECTURE.md`
Add a section “DAP Backend (lldb-dap)”:
- how it’s selected (opt-in)
- differences vs LLDB CLI (structured stack/variables, breakpoint reapplication)
- note about process state (stack/variables usually require stopped context)
- explain that conditional breakpoints are implemented backend-side

---

## Configuration & validation steps (manual / operational)

### Validation steps (local)
1. Ensure `lldb-dap` is discoverable:
   - `xcrun --find lldb-dap`
2. Run server with DAP enabled:
   - `XCODEBUILDMCP_DEBUGGER_BACKEND=dap node build/index.js`
3. Use existing MCP tool flow:
   - `debug_attach_sim` (attach by PID or bundleId)
   - `debug_breakpoint_add` (with condition)
   - trigger breakpoint (or pause via `debug_lldb_command` if implemented via evaluate)
   - `debug_stack`, `debug_variables`
   - `debug_detach`

### Expected behavioral constraints to document
- If the target is running and no stop context exists, DAP `stackTrace`/`variables` may fail; return guidance in tool output (“pause or set breakpoint”).

---

## Summary of files modified / added

### Add
- `src/utils/debugger/dap/types.ts`
- `src/utils/debugger/dap/transport.ts`
- `src/utils/debugger/dap/adapter-discovery.ts` (recommended)

### Modify
- `src/utils/debugger/backends/dap-backend.ts` (real implementation)
- `src/utils/debugger/backends/DebuggerBackend.ts` (add breakpoint condition option)
- `src/utils/debugger/backends/lldb-cli-backend.ts` (support condition via new opts)
- `src/utils/debugger/debugger-manager.ts` (pass-through opts; optional improved error handling)
- `src/mcp/tools/debugging/debug_breakpoint_add.ts` (use backend-level condition support)
- `src/mcp/tools/doctor/doctor.ts` (report `lldb-dap` availability)
- `docs/DAP_BACKEND_IMPLEMENTATION_PLAN.md`
- `docs/DEBUGGING_ARCHITECTURE.md`
- `src/test-utils/mock-executors.ts` (add mock interactive spawner)

---

## Critical “don’t miss” requirements
- `dispose()` in DAP backend and transport must be **best-effort and never throw** because `DebuggerManager.createSession()` will call dispose on attach failure.
- Avoid any use of default executors/spawners in tests; ensure `createDapBackend()` accepts injected `executor` + `spawner`.
- Breakpoint removal requires stateful re-application with `setBreakpoints` / `setFunctionBreakpoints`; plan for breakpoint registries from day one.

```
Page 10/12FirstPrevNextLast