UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

2,402 lines • 67.1 kB
/**
 * TeammateBridge - Core bridge to Claude Code's TeammateTool
 *
 * Provides unified API for multi-agent orchestration using
 * native TeammateTool capabilities (Claude Code >= 2.1.19).
 *
 * Features:
 * - Team management (spawn, discover, join/leave)
 * - Mailbox messaging (write, broadcast)
 * - Plan approval workflow
 * - Delegation system
 * - Remote sync to Claude.ai
 * - Session memory persistence
 * - Teleport/session resume
 *
 * @module @claude-flow/teammate-plugin/bridge
 * @version 1.0.0-alpha.1
 */

import { EventEmitter } from 'events';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

import type {
  TeamConfig,
  TeammateSpawnConfig,
  TeamState,
  TeammateInfo,
  MailboxMessage,
  TeamPlan,
  PlanStep,
  TeamContext,
  DelegationRecord,
  RemoteSession,
  SyncResult,
  TeammateMemory,
  TeleportTarget,
  TeleportResult,
  AgentInput,
  ExitPlanModeInput,
  VersionInfo,
  PluginConfig,
  TeammateBridgeEvents,
  JoinRequest,
  MessageType,
  PlanStatus,
  BackendStatus,
  TmuxBackendConfig,
} from './types.js';

// BMSSP-powered optimizer imports
import { TopologyOptimizer, createTopologyOptimizer } from './topology-optimizer.js';
import type { PathResult, TopologyStats, OptimizationResult } from './topology-optimizer.js';
import { SemanticRouter, createSemanticRouter } from './semantic-router.js';
import type { TaskProfile, RoutingDecision, MatchResult } from './semantic-router.js';

import {
  MINIMUM_CLAUDE_CODE_VERSION,
  DEFAULT_PLUGIN_CONFIG,
  TeammateErrorCode,
  RATE_LIMIT_DEFAULTS,
  RETRY_DEFAULTS,
  HEALTH_CHECK_DEFAULTS,
} from './types.js';

import type {
  RateLimitConfig,
  MetricSnapshot,
  TeammateHealthCheck,
  TeamHealthReport,
  CircuitBreakerState,
} from './types.js';

import {
  DEFAULT_RATE_LIMIT_CONFIG,
  DEFAULT_HEALTH_CHECK_CONFIG,
  DEFAULT_RETRY_CONFIG,
  DEFAULT_CIRCUIT_BREAKER_CONFIG,
  SECURITY_LIMITS,
} from './types.js';

// Import utility classes from extracted modules
import { RateLimiter } from './utils/rate-limiter.js';
import { MetricsCollector } from './utils/metrics-collector.js';
import { HealthChecker } from './utils/health-checker.js';
import { CircuitBreaker } from './utils/circuit-breaker.js';

// ============================================================================
// TeammateError Class
// ============================================================================

export class TeammateError extends Error {
  constructor(
    message: string,
    public readonly code: TeammateErrorCode,
    public readonly teamName?: string,
    public readonly teammateId?: string,
    public readonly cause?: Error
  ) {
    super(message);
    this.name = 'TeammateError';
  }
}

// ============================================================================
// Security Constants (from centralized SECURITY_LIMITS)
// ============================================================================

const MAX_NAME_LENGTH = SECURITY_LIMITS.MAX_NAME_LENGTH;
const MAX_PAYLOAD_SIZE = SECURITY_LIMITS.MAX_PAYLOAD_SIZE;

/** Allowed characters in team/teammate names (alphanumeric, dash, underscore) */
const SAFE_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;

/** Reserved names that cannot be used for teams */
const RESERVED_NAMES = new Set(['..', '.', 'config', 'state', 'mailbox', 'memory', 'remote']);

// ============================================================================
// Utility Functions
// ============================================================================

function generateId(prefix: string): string {
  const timestamp = Date.now().toString(36);
  const random = Math.random().toString(36).slice(2, 10);
  return `${prefix}-${timestamp}-${random}`;
}

function compareVersions(a: string, b: string): number {
  const partsA = a.split('.').map(Number);
  const partsB = b.split('.').map(Number);

  for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
    const numA = partsA[i] || 0;
    const numB = partsB[i] || 0;
    if (numA > numB) return 1;
    if (numA < numB) return -1;
  }
  return 0;
}

function ensureDirectory(dirPath: string): void {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 }); // Secure permissions
  }
}

// ============================================================================
// Security Functions
// ============================================================================

/**
 * Validate and sanitize team/teammate names to prevent path traversal
 */
function validateName(name: string, type: 'team' | 'teammate'): string {
  if (!name || typeof name !== 'string') {
    throw new TeammateError(
      `Invalid ${type} name: must be a non-empty string`,
      TeammateErrorCode.PERMISSION_DENIED
    );
  }

  const trimmed = name.trim();

  if (trimmed.length === 0) {
    throw new TeammateError(
      `Invalid ${type} name: cannot be empty`,
      TeammateErrorCode.PERMISSION_DENIED
    );
  }

  if (trimmed.length > MAX_NAME_LENGTH) {
    throw new TeammateError(
      `Invalid ${type} name: exceeds maximum length of ${MAX_NAME_LENGTH}`,
      TeammateErrorCode.PERMISSION_DENIED
    );
  }

  if (!SAFE_NAME_PATTERN.test(trimmed)) {
    throw new TeammateError(
      `Invalid ${type} name: must contain only alphanumeric characters, dashes, and underscores`,
      TeammateErrorCode.PERMISSION_DENIED
    );
  }

  if (RESERVED_NAMES.has(trimmed.toLowerCase())) {
    throw new TeammateError(
      `Invalid ${type} name: '${trimmed}' is reserved`,
      TeammateErrorCode.PERMISSION_DENIED
    );
  }

  return trimmed;
}

/**
 * Validate that a path is within the allowed base directory
 */
function validatePath(targetPath: string, basePath: string): string {
  const resolvedTarget = path.resolve(targetPath);
  const resolvedBase = path.resolve(basePath);

  if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
    throw new TeammateError(
      'Path traversal attempt detected',
      TeammateErrorCode.PERMISSION_DENIED
    );
  }

  return resolvedTarget;
}

/**
 * Safely parse JSON with size limits
 */
function safeJSONParse<T>(content: string, maxSize: number = MAX_PAYLOAD_SIZE): T {
  if (content.length > maxSize) {
    throw new TeammateError(
      `JSON content exceeds maximum size of ${maxSize} bytes`,
      TeammateErrorCode.MAILBOX_FULL
    );
  }

  try {
    return JSON.parse(content) as T;
  } catch (error) {
    throw new TeammateError(
      'Invalid JSON content',
      TeammateErrorCode.PERMISSION_DENIED
    );
  }
}

/**
 * Sanitize environment variable value
 */
function sanitizeEnvValue(value: string): string {
  // Remove any null bytes or control characters except newlines
  return value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
}

// ============================================================================
// TeammateBridge Class
// ============================================================================

export class TeammateBridge extends EventEmitter {
  private claudeCodeVersion: string | null = null;
  private teammateToolAvailable: boolean = false;
  private initialized: boolean = false;

  private activeTeams: Map<string, TeamState> = new Map();
  private mailboxPollers: Map<string, NodeJS.Timeout> = new Map();
  private memoryPersistTimers: Map<string, NodeJS.Timeout> = new Map();
  private teammateMemories: Map<string, TeammateMemory> = new Map();

  // Performance: Debounced write queues
  private pendingWrites: Map<string, { data: unknown; timer: NodeJS.Timeout }> = new Map();
  private readonly writeDebounceMs = 500;

  // Performance: Cache for frequently accessed data
  private teamConfigCache: Map<string, { config: TeamConfig; timestamp: number }> = new Map();
  private readonly cacheMaxAgeMs = 30000; // 30 seconds

  // Rate limiting, metrics, health, circuit breaker
  private rateLimiter: RateLimiter;
  private metrics: MetricsCollector;
  private healthChecker: HealthChecker;
  private circuitBreaker: CircuitBreaker;

  // Message TTL cleanup
  private messageTTLTimer: NodeJS.Timeout | null = null;
  private readonly messageTTLCheckIntervalMs = 60000; // 1 minute

  // BMSSP-powered optimizers
  private topologyOptimizers: Map<string, TopologyOptimizer> = new Map();
  private semanticRouter: SemanticRouter | null = null;
  private optimizersEnabled: boolean = false;

  private readonly config: PluginConfig;
  private readonly teamsDir: string;

  constructor(config: Partial<PluginConfig> = {}) {
    super();
    this.config = { ...DEFAULT_PLUGIN_CONFIG, ...config };
    this.teamsDir = path.join(os.homedir(), '.claude', 'teams');

    // Initialize rate limiter
    this.rateLimiter = new RateLimiter(DEFAULT_RATE_LIMIT_CONFIG);

    // Initialize metrics collector
    this.metrics = new MetricsCollector();

    // Initialize health checker with status change callback
    this.healthChecker = new HealthChecker(
      DEFAULT_HEALTH_CHECK_CONFIG,
      (check) => this.onHealthStatusChange(check)
    );

    // Initialize circuit breaker
    this.circuitBreaker = new CircuitBreaker(DEFAULT_CIRCUIT_BREAKER_CONFIG);
  }

  /**
   * Handle health status changes
   */
  private onHealthStatusChange(check: TeammateHealthCheck): void {
    this.emit('health:changed', check);

    if (check.status === 'unhealthy') {
      this.metrics.increment('healthChecksFailed');
      this.emit('health:unhealthy', check);
    } else if (check.status === 'healthy') {
      this.metrics.increment('healthChecksPassed');
    }
  }

  // ==========================================================================
  // Initialization
  // ==========================================================================

  /**
   * Initialize the bridge
   * Detects Claude Code version and TeammateTool availability
   */
  async initialize(): Promise<VersionInfo> {
    if (this.initialized) {
      return this.getVersionInfo();
    }

    // Detect Claude Code version
    try {
      const output = execSync('claude --version 2>/dev/null', {
        encoding: 'utf-8',
        timeout: 5000,
      }).trim();

      const match = output.match(/(\d+\.\d+\.\d+)/);
      this.claudeCodeVersion = match?.[1] ?? null;

      // TeammateTool requires >= 2.1.19
      if (this.claudeCodeVersion) {
        this.teammateToolAvailable =
          compareVersions(this.claudeCodeVersion, MINIMUM_CLAUDE_CODE_VERSION) >= 0;
      }
    } catch {
      this.claudeCodeVersion = null;
      this.teammateToolAvailable = false;
    }

    this.initialized = true;

    const versionInfo = this.getVersionInfo();

    this.emit('initialized', {
      claudeCodeVersion: this.claudeCodeVersion,
      teammateToolAvailable: this.teammateToolAvailable,
    });

    if (!this.teammateToolAvailable) {
      console.warn(
        `[TeammateBridge] TeammateTool not available. ` +
        `Requires Claude Code >= ${MINIMUM_CLAUDE_CODE_VERSION}, ` +
        `found: ${this.claudeCodeVersion ?? 'not installed'}`
      );
    }

    return versionInfo;
  }

  /**
   * Get version information
   */
  getVersionInfo(): VersionInfo {
    const missingFeatures: string[] = [];

    if (!this.teammateToolAvailable) {
      missingFeatures.push('TeammateTool', 'team_name', 'launchSwarm');
    }

    return {
      claudeCode: this.claudeCodeVersion,
      plugin: '1.0.0-alpha.1',
      compatible: this.teammateToolAvailable,
      missingFeatures,
    };
  }

  /**
   * Check if TeammateTool is available
   */
  isAvailable(): boolean {
    return this.teammateToolAvailable;
  }

  // ==========================================================================
  // Metrics & Observability
  // ==========================================================================

  /**
   * Get current metrics snapshot
   */
  getMetrics(): MetricSnapshot {
    return this.metrics.getSnapshot();
  }

  /**
   * Get latency percentile for an operation
   */
  getLatencyPercentile(
    operation: 'spawnLatency' | 'messageLatency' | 'planApprovalLatency',
    percentile: number
  ): number {
    return this.metrics.getPercentile(operation, percentile);
  }

  /**
   * Reset metrics
   */
  resetMetrics(): void {
    this.metrics.reset();
  }

  // ==========================================================================
  // Health Checks
  // ==========================================================================

  /**
   * Get health check for a specific teammate
   */
  getTeammateHealth(teammateId: string): TeammateHealthCheck | undefined {
    return this.healthChecker.getCheck(teammateId);
  }

  /**
   * Get health report for a team
   */
  getTeamHealth(teamName: string): TeamHealthReport {
    return this.healthChecker.getTeamReport(teamName);
  }

  // ==========================================================================
  // Rate Limiting
  // ==========================================================================

  /**
   * Get remaining rate limit quota for an operation
   */
  getRateLimitRemaining(operation: keyof RateLimitConfig): number {
    return this.rateLimiter.getRemaining(operation);
  }

  /**
   * Check if an operation is rate limited
   */
  isRateLimited(operation: keyof RateLimitConfig): boolean {
    const state = this.rateLimiter.getState(operation);
    return state?.blocked ?? false;
  }

  /**
   * Reset rate limits
   */
  resetRateLimits(operation?: string): void {
    this.rateLimiter.reset(operation);
  }

  // ==========================================================================
  // Circuit Breaker
  // ==========================================================================

  /**
   * Get circuit breaker state
   */
  getCircuitBreakerState(): CircuitBreakerState {
    return this.circuitBreaker.getState();
  }

  /**
   * Reset circuit breaker
   */
  resetCircuitBreaker(): void {
    this.circuitBreaker.reset();
  }

  /**
   * Get Claude Code version
   */
  getClaudeCodeVersion(): string | null {
    return this.claudeCodeVersion;
  }

  // ==========================================================================
  // BMSSP Optimization (10-15x faster routing)
  // ==========================================================================

  /**
   * Enable BMSSP-powered optimizers
   * @returns true if WASM acceleration available, false if using fallback
   */
  async enableOptimizers(): Promise<boolean> {
    if (this.optimizersEnabled) {
      return !this.semanticRouter?.['useFallback'];
    }

    try {
      // Initialize semantic router (neural routing)
      this.semanticRouter = await createSemanticRouter();
      this.optimizersEnabled = true;

      // Initialize topology optimizers for existing teams
      for (const [teamName, team] of this.activeTeams) {
        await this.initializeTeamOptimizer(teamName, team);
      }

      this.emit('optimizers:enabled', {
        wasmAvailable: !this.semanticRouter['useFallback'],
      });

      return !this.semanticRouter['useFallback'];
    } catch (error) {
      console.warn('[TeammateBridge] Failed to enable optimizers:', error);
      return false;
    }
  }

  /**
   * Check if optimizers are enabled
   */
  areOptimizersEnabled(): boolean {
    return this.optimizersEnabled;
  }

  /**
   * Find optimal message routing path using BMSSP
   */
  async findOptimalPath(
    teamName: string,
    fromId: string,
    toId: string
  ): Promise<PathResult | null> {
    if (!this.optimizersEnabled) {
      return null;
    }

    const optimizer = this.topologyOptimizers.get(teamName);
    if (!optimizer) {
      return null;
    }

    return optimizer.findShortestPath(fromId, toId);
  }

  /**
   * Get topology statistics for a team
   */
  getTopologyStats(teamName: string): TopologyStats | null {
    const optimizer = this.topologyOptimizers.get(teamName);
    return optimizer?.getStats() ?? null;
  }

  /**
   * Get optimization suggestions for team topology
   */
  getTopologyOptimizations(teamName: string): OptimizationResult | null {
    const optimizer = this.topologyOptimizers.get(teamName);
    return optimizer?.suggestOptimizations() ?? null;
  }

  /**
   * Find best teammate for a task using semantic routing
   */
  async findBestTeammateForTask(
    teamName: string,
    task: {
      id: string;
      description: string;
      requiredSkills: string[];
      priority?: 'urgent' | 'high' | 'normal' | 'low';
    }
  ): Promise<RoutingDecision | null> {
    if (!this.optimizersEnabled || !this.semanticRouter) {
      return null;
    }

    const team = this.activeTeams.get(teamName);
    if (!team) {
      return null;
    }

    // Build profiles from team if not already done
    await this.semanticRouter.buildFromTeam(team);

    const taskProfile: TaskProfile = {
      id: task.id,
      description: task.description,
      requiredSkills: task.requiredSkills,
      priority: task.priority ?? 'normal',
    };

    return this.semanticRouter.findBestMatch(taskProfile);
  }

  /**
   * Batch route multiple tasks to teammates
   */
  async batchRouteTasksToTeammates(
    teamName: string,
    tasks: Array<{
      id: string;
      description: string;
      requiredSkills: string[];
      priority?: 'urgent' | 'high' | 'normal' | 'low';
    }>
  ): Promise<Map<string, RoutingDecision>> {
    if (!this.optimizersEnabled || !this.semanticRouter) {
      return new Map();
    }

    const team = this.activeTeams.get(teamName);
    if (!team) {
      return new Map();
    }

    // Build profiles from team
    await this.semanticRouter.buildFromTeam(team);

    const taskProfiles: TaskProfile[] = tasks.map(t => ({
      id: t.id,
      description: t.description,
      requiredSkills: t.requiredSkills,
      priority: t.priority ?? 'normal',
    }));

    return this.semanticRouter.batchMatch(taskProfiles);
  }

  /**
   * Update teammate performance for learning
   */
  updateTeammatePerformance(
    teammateId: string,
    taskSuccess: boolean,
    latencyMs: number
  ): void {
    if (this.semanticRouter) {
      this.semanticRouter.updatePerformance(teammateId, taskSuccess, latencyMs);
    }
  }

  /**
   * Get semantic distance between two teammates
   */
  getTeammateSemanticDistance(id1: string, id2: string): number {
    if (!this.semanticRouter) {
      return Infinity;
    }
    return this.semanticRouter.getSemanticDistance(id1, id2);
  }

  /**
   * Initialize optimizer for a team
   */
  private async initializeTeamOptimizer(teamName: string, team: TeamState): Promise<void> {
    if (!this.optimizersEnabled) return;

    // Create topology optimizer
    const optimizer = await createTopologyOptimizer(team.topology);
    await optimizer.buildFromTeam(team);
    this.topologyOptimizers.set(teamName, optimizer);

    // Register teammates with semantic router
    if (this.semanticRouter) {
      for (const teammate of team.teammates) {
        this.semanticRouter.registerTeammate(teammate);
      }
    }
  }

  // ==========================================================================
  // Team Management
  // ==========================================================================

  /**
   * Spawn a new team
   */
  async spawnTeam(config: Partial<TeamConfig> & { name: string }): Promise<TeamState> {
    this.ensureAvailable();

    // Security: Validate team name
    const validatedName = validateName(config.name, 'team');

    const fullConfig: TeamConfig = {
      topology: 'hierarchical',
      maxTeammates: 8,
      spawnBackend: 'auto',
      planModeRequired: false,
      autoApproveJoin: true,
      messageRetention: 3600000,
      delegationEnabled: true,
      remoteSync: this.config.remoteSync,
      ...config,
      name: validatedName, // Use validated name
    };

    const teamState: TeamState = {
      name: fullConfig.name,
      createdAt: new Date(),
      teammates: [],
      pendingJoinRequests: [],
      activePlans: [],
      messageCount: 0,
      topology: fullConfig.topology,
      context: {
        teamName: fullConfig.name,
        sharedVariables: {},
        inheritedPermissions: [],
        workingDirectory: process.cwd(),
        gitBranch: this.getCurrentGitBranch(),
        gitRepo: this.getCurrentGitRepo(),
        environmentVariables: {},
      },
      delegations: [],
    };

    // Set environment for team context (sanitized)
    process.env.CLAUDE_CODE_TEAM_NAME = sanitizeEnvValue(fullConfig.name);

    if (fullConfig.planModeRequired) {
      process.env.CLAUDE_CODE_PLAN_MODE_REQUIRED = 'true';
    }

    // Create team directory
    const teamDir = path.join(this.teamsDir, fullConfig.name);
    ensureDirectory(teamDir);
    ensureDirectory(path.join(teamDir, 'mailbox'));
    ensureDirectory(path.join(teamDir, 'memory'));

    // Save team config with secure permissions
    fs.writeFileSync(
      path.join(teamDir, 'config.json'),
      JSON.stringify(fullConfig, null, 2),
      { encoding: 'utf-8', mode: 0o600 }
    );

    this.activeTeams.set(fullConfig.name, teamState);

    // Start mailbox polling
    this.startMailboxPoller(fullConfig.name);

    // Start memory persistence if enabled
    if (this.config.memory.autoPersist) {
      this.startMemoryPersistence(fullConfig.name);
    }

    // Initialize optimizer if enabled
    if (this.optimizersEnabled) {
      await this.initializeTeamOptimizer(fullConfig.name, teamState);
    }

    this.emit('team:spawned', { team: fullConfig.name, config: fullConfig });
    this.metrics.increment('teamsCreated');
    this.metrics.increment('activeTeams');

    return teamState;
  }

  /**
   * Discover existing teams
   */
  async discoverTeams(): Promise<string[]> {
    this.ensureAvailable();

    try {
      if (!fs.existsSync(this.teamsDir)) {
        return [];
      }

      const entries = fs.readdirSync(this.teamsDir, { withFileTypes: true });
      return entries
        .filter(d => d.isDirectory())
        .filter(d => fs.existsSync(path.join(this.teamsDir, d.name, 'config.json')))
        .map(d => d.name);
    } catch {
      return [];
    }
  }

  /**
   * Load an existing team
   */
  async loadTeam(teamName: string): Promise<TeamState> {
    this.ensureAvailable();

    // Security: Validate team name to prevent path traversal
    const validatedName = validateName(teamName, 'team');

    const teamDir = path.join(this.teamsDir, validatedName);
    // Security: Ensure path is within teams directory
    validatePath(teamDir, this.teamsDir);

    const configPath = path.join(teamDir, 'config.json');

    if (!fs.existsSync(configPath)) {
      throw new TeammateError(
        `Team not found: ${validatedName}`,
        TeammateErrorCode.TEAM_NOT_FOUND,
        validatedName
      );
    }

    // Security: Use safe JSON parsing
    const configContent = fs.readFileSync(configPath, 'utf-8');
    const config = safeJSONParse<TeamConfig>(configContent);

    // Load state file if exists
    const statePath = path.join(teamDir, 'state.json');
    let teamState: TeamState;

    if (fs.existsSync(statePath)) {
      // Security: Use safe JSON parsing
      const stateContent = fs.readFileSync(statePath, 'utf-8');
      const savedState = safeJSONParse<any>(stateContent);
      teamState = {
        ...savedState,
        createdAt: new Date(savedState.createdAt),
        teammates: savedState.teammates.map((t: any) => ({
          ...t,
          spawnedAt: new Date(t.spawnedAt),
          lastHeartbeat: t.lastHeartbeat ? new Date(t.lastHeartbeat) : undefined,
        })),
      };
    } else {
      teamState = {
        name: teamName,
        createdAt: new Date(),
        teammates: [],
        pendingJoinRequests: [],
        activePlans: [],
        messageCount: 0,
        topology: config.topology,
        context: {
          teamName,
          sharedVariables: {},
          inheritedPermissions: [],
          workingDirectory: process.cwd(),
          environmentVariables: {},
        },
        delegations: [],
      };
    }

    this.activeTeams.set(teamName, teamState);

    // Start mailbox polling
    this.startMailboxPoller(teamName);

    return teamState;
  }

  /**
   * Request to join an existing team
   */
  async requestJoin(teamName: string, agentInfo: Partial<TeammateInfo> & { id: string; name: string; role: string }): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    const request: JoinRequest = {
      agentId: agentInfo.id,
      agentName: agentInfo.name,
      requestedAt: new Date(),
      role: agentInfo.role,
    };

    team.pendingJoinRequests.push(request);

    this.emit('team:join_requested', {
      team: teamName,
      agent: {
        id: agentInfo.id,
        name: agentInfo.name,
        role: agentInfo.role,
        status: 'idle',
        spawnedAt: new Date(),
        messagesSent: 0,
        messagesReceived: 0,
      } as TeammateInfo,
    });

    // Auto-approve if configured
    const teamDir = path.join(this.teamsDir, teamName);
    const configPath = path.join(teamDir, 'config.json');
    if (fs.existsSync(configPath)) {
      const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as TeamConfig;
      if (config.autoApproveJoin) {
        await this.approveJoin(teamName, agentInfo.id);
      }
    }
  }

  /**
   * Approve a join request
   */
  async approveJoin(teamName: string, agentId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    const requestIndex = team.pendingJoinRequests.findIndex(r => r.agentId === agentId);

    if (requestIndex === -1) {
      throw new TeammateError(
        `No pending join request for agent: ${agentId}`,
        TeammateErrorCode.TEAMMATE_NOT_FOUND,
        teamName,
        agentId
      );
    }

    const request = team.pendingJoinRequests.splice(requestIndex, 1)[0];

    const teammate: TeammateInfo = {
      id: agentId,
      name: request.agentName,
      role: request.role,
      status: 'active',
      spawnedAt: new Date(),
      messagesSent: 0,
      messagesReceived: 0,
    };

    team.teammates.push(teammate);

    this.emit('team:join_approved', { team: teamName, agent: agentId });
  }

  /**
   * Reject a join request
   */
  async rejectJoin(teamName: string, agentId: string, reason?: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    team.pendingJoinRequests = team.pendingJoinRequests.filter(r => r.agentId !== agentId);

    this.emit('team:join_rejected', { team: teamName, agent: agentId, reason });
  }

  // ==========================================================================
  // Teammate Spawning
  // ==========================================================================

  /**
   * Spawn a teammate using native AgentInput
   */
  async spawnTeammate(config: TeammateSpawnConfig): Promise<TeammateInfo> {
    this.ensureAvailable();

    // Rate limiting check
    if (!this.rateLimiter.checkLimit('spawnPerMinute')) {
      this.metrics.increment('rateLimitBlocks');
      throw new TeammateError(
        'Spawn rate limit exceeded. Please wait before spawning more teammates.',
        TeammateErrorCode.PERMISSION_DENIED
      );
    }
    this.metrics.increment('rateLimitHits');

    const startTime = Date.now();

    // Security: Validate teammate name
    const validatedName = validateName(config.name, 'teammate');

    const teamName = config.teamName ?? process.env.CLAUDE_CODE_TEAM_NAME;

    if (teamName) {
      const team = this.activeTeams.get(teamName);
      if (team) {
        const teamDir = path.join(this.teamsDir, teamName);
        const configPath = path.join(teamDir, 'config.json');
        if (fs.existsSync(configPath)) {
          const teamConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as TeamConfig;
          if (team.teammates.length >= teamConfig.maxTeammates) {
            throw new TeammateError(
              `Team ${teamName} has reached maximum teammates (${teamConfig.maxTeammates})`,
              TeammateErrorCode.PERMISSION_DENIED,
              teamName
            );
          }
        }
      }
    }

    const teammateId = generateId('teammate');

    const teammateInfo: TeammateInfo = {
      id: teammateId,
      name: validatedName, // Use validated name
      role: config.role,
      status: 'active',
      spawnedAt: new Date(),
      messagesSent: 0,
      messagesReceived: 0,
      currentTask: config.prompt.slice(0, 100),
      delegatedPermissions: config.delegatedPermissions,
    };

    // Build AgentInput
    const agentInput = this.buildAgentInput({ ...config, teamName });

    // Add to team
    if (teamName) {
      let team = this.activeTeams.get(teamName);
      if (!team) {
        team = await this.loadTeam(teamName);
      }
      team.teammates.push(teammateInfo);

      // Create mailbox file
      const mailboxPath = this.getMailboxPath(teamName, teammateId);
      ensureDirectory(path.dirname(mailboxPath));
      if (!fs.existsSync(mailboxPath)) {
        fs.writeFileSync(mailboxPath, '[]', 'utf-8');
      }
    }

    // Record metrics
    const spawnLatency = Date.now() - startTime;
    this.metrics.recordLatency('spawnLatency', spawnLatency);
    this.metrics.increment('teammatesSpawned');
    this.metrics.increment('activeTeammates');

    // Start health checking for this teammate
    if (teamName) {
      this.healthChecker.startChecking(teammateId, teamName, async () => {
        // Health check: verify teammate exists in team state
        const team = this.activeTeams.get(teamName);
        const teammate = team?.teammates.find(t => t.id === teammateId);
        return teammate?.status === 'active';
      });
    }

    this.emit('teammate:spawned', { teammate: teammateInfo, agentInput });

    return teammateInfo;
  }

  /**
   * Build AgentInput for Claude Code Task tool
   */
  buildAgentInput(config: TeammateSpawnConfig): AgentInput {
    return {
      description: `${config.role}: ${config.name}`,
      prompt: config.prompt,
      subagent_type: config.role,
      model: config.model,
      name: config.name,
      team_name: config.teamName ?? process.env.CLAUDE_CODE_TEAM_NAME,
      allowed_tools: config.allowedTools,
      mode: config.mode,
      run_in_background: config.runInBackground ?? true,
      max_turns: config.maxTurns,
    };
  }

  // ==========================================================================
  // Messaging
  // ==========================================================================

  /**
   * Send message to a specific teammate
   */
  async sendMessage(
    teamName: string,
    fromId: string,
    toId: string,
    message: { type: MessageType; payload: unknown; priority?: MailboxMessage['priority'] }
  ): Promise<MailboxMessage> {
    this.ensureAvailable();

    // Rate limiting check
    if (!this.rateLimiter.checkLimit('messagesPerMinute')) {
      this.metrics.increment('rateLimitBlocks');
      throw new TeammateError(
        'Message rate limit exceeded',
        TeammateErrorCode.PERMISSION_DENIED,
        teamName
      );
    }

    const startTime = Date.now();
    const team = this.getTeamOrThrow(teamName);

    const fullMessage: MailboxMessage = {
      id: generateId('msg'),
      from: fromId,
      to: toId,
      timestamp: new Date(),
      acknowledged: false,
      ...message,
    };

    // Write to recipient's mailbox
    await this.writeToMailbox(teamName, toId, fullMessage);

    team.messageCount++;

    // Update sender stats
    const sender = team.teammates.find(t => t.id === fromId);
    if (sender) sender.messagesSent++;

    // Record metrics
    this.metrics.recordLatency('messageLatency', Date.now() - startTime);
    this.metrics.increment('messagesSent');

    this.emit('message:sent', { team: teamName, message: fullMessage });

    return fullMessage;
  }

  /**
   * Broadcast message to all teammates
   */
  async broadcast(
    teamName: string,
    fromId: string,
    message: { type: MessageType; payload: unknown; priority?: MailboxMessage['priority'] }
  ): Promise<MailboxMessage> {
    this.ensureAvailable();

    // Rate limiting check
    if (!this.rateLimiter.checkLimit('broadcastsPerMinute')) {
      this.metrics.increment('rateLimitBlocks');
      throw new TeammateError(
        'Broadcast rate limit exceeded',
        TeammateErrorCode.PERMISSION_DENIED,
        teamName
      );
    }

    const team = this.getTeamOrThrow(teamName);

    const fullMessage: MailboxMessage = {
      id: generateId('broadcast'),
      from: fromId,
      to: 'broadcast',
      timestamp: new Date(),
      acknowledged: false,
      ...message,
    };

    // Write to all teammate mailboxes
    for (const teammate of team.teammates) {
      if (teammate.id !== fromId) {
        await this.writeToMailbox(teamName, teammate.id, fullMessage);
        teammate.messagesReceived++;
      }
    }

    team.messageCount += team.teammates.length - 1;

    // Record metrics
    this.metrics.increment('broadcastsSent');

    this.emit('message:broadcast', { team: teamName, message: fullMessage });

    return fullMessage;
  }

  /**
   * Read messages from mailbox
   */
  async readMailbox(teamName: string, teammateId: string): Promise<MailboxMessage[]> {
    this.ensureAvailable();

    // Security: Validate names to prevent path traversal
    const validatedTeam = validateName(teamName, 'team');
    const validatedTeammate = validateName(teammateId, 'teammate');

    const mailboxPath = this.getMailboxPath(validatedTeam, validatedTeammate);

    try {
      if (!fs.existsSync(mailboxPath)) {
        return [];
      }

      const content = fs.readFileSync(mailboxPath, 'utf-8');
      // Security: Use safe JSON parsing
      const messages = safeJSONParse<MailboxMessage[]>(content);

      // Clear mailbox after reading
      fs.writeFileSync(mailboxPath, '[]', 'utf-8');

      // Parse dates
      return messages.map(m => ({
        ...m,
        timestamp: new Date(m.timestamp),
      }));
    } catch {
      return [];
    }
  }

  // ==========================================================================
  // Plan Approval System
  // ==========================================================================

  /**
   * Submit a plan for approval
   */
  async submitPlan(
    teamName: string,
    plan: Omit<TeamPlan, 'id' | 'approvals' | 'rejections' | 'status' | 'createdAt'>
  ): Promise<TeamPlan> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    const fullPlan: TeamPlan = {
      id: generateId('plan'),
      approvals: [],
      rejections: [],
      status: 'pending',
      createdAt: new Date(),
      ...plan,
    };

    team.activePlans.push(fullPlan);

    // Broadcast plan for approval
    await this.broadcast(teamName, plan.proposedBy, {
      type: 'plan',
      payload: fullPlan,
    });

    this.emit('plan:submitted', { team: teamName, plan: fullPlan });

    return fullPlan;
  }

  /**
   * Approve a plan
   */
  async approvePlan(teamName: string, planId: string, approverId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const plan = this.getPlanOrThrow(team, planId);

    if (plan.status !== 'pending') {
      throw new TeammateError(
        `Plan ${planId} is not pending (status: ${plan.status})`,
        TeammateErrorCode.PLAN_NOT_APPROVED,
        teamName
      );
    }

    if (!plan.approvals.includes(approverId)) {
      plan.approvals.push(approverId);
    }

    this.emit('plan:approval_added', { team: teamName, planId, approverId });

    // Check if we have enough approvals
    if (plan.approvals.length >= plan.requiredApprovals) {
      plan.status = 'approved';
      plan.approvedAt = new Date();
      this.emit('plan:approved', { team: teamName, plan });
    }
  }

  /**
   * Reject a plan
   */
  async rejectPlan(teamName: string, planId: string, rejecterId: string, reason?: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const plan = this.getPlanOrThrow(team, planId);

    plan.rejections.push(rejecterId);
    plan.status = 'rejected';

    this.emit('plan:rejected', { team: teamName, plan, rejecterId, reason });
  }

  /**
   * Pause plan execution
   */
  async pausePlan(teamName: string, planId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const plan = this.getPlanOrThrow(team, planId);

    if (plan.status !== 'executing') {
      throw new TeammateError(
        `Plan ${planId} is not executing`,
        TeammateErrorCode.PLAN_NOT_APPROVED,
        teamName
      );
    }

    plan.status = 'paused';
    plan.pausedAt = plan.currentStep;

    this.emit('plan:paused', { team: teamName, planId, atStep: plan.currentStep ?? 0 });
  }

  /**
   * Resume plan execution
   */
  async resumePlan(teamName: string, planId: string, fromStep?: number): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const plan = this.getPlanOrThrow(team, planId);

    if (plan.status !== 'paused') {
      throw new TeammateError(
        `Plan ${planId} is not paused`,
        TeammateErrorCode.PLAN_NOT_APPROVED,
        teamName
      );
    }

    plan.status = 'executing';
    plan.currentStep = fromStep ?? plan.pausedAt ?? 0;

    this.emit('plan:resumed', { team: teamName, planId, fromStep: plan.currentStep });
  }

  /**
   * Launch swarm to execute approved plan
   */
  async launchSwarm(teamName: string, planId: string, teammateCount?: number): Promise<ExitPlanModeInput> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const plan = this.getPlanOrThrow(team, planId);

    if (plan.status !== 'approved') {
      throw new TeammateError(
        `Plan ${planId} is not approved`,
        TeammateErrorCode.PLAN_NOT_APPROVED,
        teamName
      );
    }

    const exitPlanInput: ExitPlanModeInput = {
      launchSwarm: true,
      teammateCount: teammateCount ?? plan.steps.length,
      allowedPrompts: plan.steps.map(step => ({
        tool: 'Bash' as const,
        prompt: step.action,
      })),
    };

    plan.status = 'executing';
    plan.startedAt = new Date();
    plan.currentStep = 0;

    this.emit('swarm:launched', {
      team: teamName,
      plan,
      exitPlanInput,
      teammateCount: exitPlanInput.teammateCount!,
    });

    return exitPlanInput;
  }

  // ==========================================================================
  // Delegation System (NEW)
  // ==========================================================================

  /**
   * Delegate authority to a teammate
   */
  async delegateToTeammate(
    teamName: string,
    fromId: string,
    toId: string,
    permissions: string[]
  ): Promise<DelegationRecord> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    // Check if source has these permissions or delegation rights
    const fromTeammate = team.teammates.find(t => t.id === fromId);
    if (!fromTeammate) {
      throw new TeammateError(
        `Teammate ${fromId} not found`,
        TeammateErrorCode.TEAMMATE_NOT_FOUND,
        teamName,
        fromId
      );
    }

    // Check delegation depth
    const existingDelegationChain = this.getDelegationChain(team, fromId);
    if (existingDelegationChain.length >= this.config.delegation.maxDepth) {
      throw new TeammateError(
        `Delegation depth exceeded (max: ${this.config.delegation.maxDepth})`,
        TeammateErrorCode.DELEGATION_DEPTH_EXCEEDED,
        teamName
      );
    }

    const delegation: DelegationRecord = {
      id: generateId('delegation'),
      fromId,
      toId,
      permissions,
      grantedAt: new Date(),
      expiresAt: this.config.delegation.autoExpireMs
        ? new Date(Date.now() + this.config.delegation.autoExpireMs)
        : undefined,
      depth: existingDelegationChain.length + 1,
      active: true,
    };

    team.delegations.push(delegation);

    // Update target teammate
    const toTeammate = team.teammates.find(t => t.id === toId);
    if (toTeammate) {
      toTeammate.delegatedFrom = fromId;
      toTeammate.delegatedPermissions = [
        ...(toTeammate.delegatedPermissions ?? []),
        ...permissions,
      ];
    }

    // Notify via message
    await this.sendMessage(teamName, fromId, toId, {
      type: 'delegation',
      payload: { action: 'granted', permissions },
    });

    this.emit('delegate:granted', { team: teamName, from: fromId, to: toId, permissions });

    return delegation;
  }

  /**
   * Revoke delegation from a teammate
   */
  async revokeDelegation(teamName: string, fromId: string, toId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    const delegationIndex = team.delegations.findIndex(
      d => d.fromId === fromId && d.toId === toId && d.active
    );

    if (delegationIndex === -1) {
      throw new TeammateError(
        `No active delegation from ${fromId} to ${toId}`,
        TeammateErrorCode.DELEGATION_DENIED,
        teamName
      );
    }

    team.delegations[delegationIndex].active = false;

    // Update target teammate
    const toTeammate = team.teammates.find(t => t.id === toId);
    if (toTeammate) {
      const delegation = team.delegations[delegationIndex];
      toTeammate.delegatedPermissions = (toTeammate.delegatedPermissions ?? [])
        .filter(p => !delegation.permissions.includes(p));

      if (toTeammate.delegatedPermissions.length === 0) {
        toTeammate.delegatedFrom = undefined;
      }
    }

    // Notify via message
    await this.sendMessage(teamName, fromId, toId, {
      type: 'delegation',
      payload: { action: 'revoked' },
    });

    this.emit('delegate:revoked', { team: teamName, from: fromId, to: toId });
  }

  // ==========================================================================
  // Team Context (NEW)
  // ==========================================================================

  /**
   * Update team context
   */
  async updateTeamContext(
    teamName: string,
    updates: Partial<TeamContext>
  ): Promise<TeamContext> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    const updatedKeys: string[] = [];

    if (updates.sharedVariables) {
      team.context.sharedVariables = {
        ...team.context.sharedVariables,
        ...updates.sharedVariables,
      };
      updatedKeys.push(...Object.keys(updates.sharedVariables));
    }

    if (updates.inheritedPermissions) {
      team.context.inheritedPermissions = [
        ...new Set([...team.context.inheritedPermissions, ...updates.inheritedPermissions]),
      ];
      updatedKeys.push('inheritedPermissions');
    }

    if (updates.workingDirectory) {
      team.context.workingDirectory = updates.workingDirectory;
      updatedKeys.push('workingDirectory');
    }

    if (updates.gitBranch !== undefined) {
      team.context.gitBranch = updates.gitBranch;
      updatedKeys.push('gitBranch');
    }

    if (updates.environmentVariables) {
      team.context.environmentVariables = {
        ...team.context.environmentVariables,
        ...updates.environmentVariables,
      };
      updatedKeys.push(...Object.keys(updates.environmentVariables));
    }

    // Broadcast context update
    await this.broadcast(teamName, 'system', {
      type: 'context_update',
      payload: { updates, keys: updatedKeys },
    });

    this.emit('context:updated', { team: teamName, keys: updatedKeys });

    return team.context;
  }

  /**
   * Get team context
   */
  getTeamContext(teamName: string): TeamContext {
    const team = this.getTeamOrThrow(teamName);
    return { ...team.context };
  }

  // ==========================================================================
  // Permission Updates (NEW)
  // ==========================================================================

  /**
   * Update teammate permissions
   */
  async updateTeammatePermissions(
    teamName: string,
    teammateId: string,
    changes: { add?: string[]; remove?: string[] }
  ): Promise<string[]> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const teammate = team.teammates.find(t => t.id === teammateId);

    if (!teammate) {
      throw new TeammateError(
        `Teammate ${teammateId} not found`,
        TeammateErrorCode.TEAMMATE_NOT_FOUND,
        teamName,
        teammateId
      );
    }

    const currentPermissions = teammate.delegatedPermissions ?? [];
    let newPermissions = [...currentPermissions];

    const added: string[] = [];
    const removed: string[] = [];

    if (changes.add) {
      for (const perm of changes.add) {
        if (!newPermissions.includes(perm)) {
          newPermissions.push(perm);
          added.push(perm);
        }
      }
    }

    if (changes.remove) {
      newPermissions = newPermissions.filter(p => {
        if (changes.remove!.includes(p)) {
          removed.push(p);
          return false;
        }
        return true;
      });
    }

    teammate.delegatedPermissions = newPermissions;

    this.emit('permissions:updated', { team: teamName, teammateId, added, removed });

    return newPermissions;
  }

  // ==========================================================================
  // Remote Sync (NEW)
  // ==========================================================================

  /**
   * Push team to Claude.ai remote
   */
  async pushTeamToRemote(teamName: string): Promise<RemoteSession> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    // This would integrate with Claude Code's pushToRemote functionality
    // For now, we simulate the remote session creation
    const remoteSession: RemoteSession = {
      remoteSessionId: generateId('remote'),
      remoteSessionUrl: `https://claude.ai/project/${generateId('proj')}`,
      syncedAt: new Date(),
      status: 'connected',
    };

    team.remoteSessionId = remoteSession.remoteSessionId;
    team.remoteSessionUrl = remoteSession.remoteSessionUrl;

    // Save remote session info with secure permissions
    // Security: Validate team name
    const validatedName = validateName(teamName, 'team');
    const teamDir = path.join(this.teamsDir, validatedName);
    fs.writeFileSync(
      path.join(teamDir, 'remote.json'),
      JSON.stringify(remoteSession, null, 2),
      { encoding: 'utf-8', mode: 0o600 }
    );

    this.emit('remote:pushed', { team: teamName, remoteUrl: remoteSession.remoteSessionUrl });

    return remoteSession;
  }

  /**
   * Sync with remote
   */
  async syncWithRemote(teamName: string): Promise<SyncResult> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    if (!team.remoteSessionId) {
      throw new TeammateError(
        `Team ${teamName} is not connected to remote`,
        TeammateErrorCode.REMOTE_SYNC_FAILED,
        teamName
      );
    }

    // Simulate sync result
    const result: SyncResult = {
      success: true,
      changesPushed: 0,
      changesPulled: 0,
      conflicts: 0,
      remoteUrl: team.remoteSessionUrl,
    };

    this.emit('remote:synced', { team: teamName, result });

    return result;
  }

  // ==========================================================================
  // Session Memory (NEW)
  // ==========================================================================

  /**
   * Save teammate memory
   */
  async saveTeammateMemory(teamName: string, teammateId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const teammate = team.teammates.find(t => t.id === teammateId);

    if (!teammate) {
      throw new TeammateError(
        `Teammate ${teammateId} not found`,
        TeammateErrorCode.TEAMMATE_NOT_FOUND,
        teamName,
        teammateId
      );
    }

    const memoryKey = `${teamName}:${teammateId}`;
    let memory = this.teammateMemories.get(memoryKey);

    if (!memory) {
      memory = {
        sessionId: generateId('session'),
        teammateId,
        teamName,
        transcript: [],
        context: {},
        nestedMemories: [],
        createdAt: new Date(),
        updatedAt: new Date(),
        size: 0,
      };
    }

    // Read mailbox messages as transcript
    const mailboxPath = this.getMailboxPath(teamName, teammateId);
    if (fs.existsSync(mailboxPath)) {
      const messages = JSON.parse(fs.readFileSync(mailboxPath, 'utf-8')) as MailboxMessage[];
      memory.transcript.push(...messages);
    }

    memory.updatedAt = new Date();
    memory.size = JSON.stringify(memory).length;

    this.teammateMemories.set(memoryKey, memory);

    // Persist to disk with secure permissions
    // Security: Validate names for path safety
    const validatedTeam = validateName(teamName, 'team');
    const validatedTeammate = validateName(teammateId, 'teammate');
    const memoryDir = path.join(this.teamsDir, validatedTeam, 'memory');
    ensureDirectory(memoryDir);
    fs.writeFileSync(
      path.join(memoryDir, `${validatedTeammate}.json`),
      JSON.stringify(memory, null, 2),
      { encoding: 'utf-8', mode: 0o600 }
    );

    this.emit('memory:saved', { team: teamName, teammateId, size: memory.size });
  }

  /**
   * Load teammate memory
   */
  async loadTeammateMemory(teamName: string, teammateId: string): Promise<TeammateMemory | null> {
    this.ensureAvailable();

    const memoryPath = path.join(this.teamsDir, teamName, 'memory', `${teammateId}.json`);

    if (!fs.existsSync(memoryPath)) {
      return null;
    }

    try {
      const memory = JSON.parse(fs.readFileSync(memoryPath, 'utf-8')) as TeammateMemory;

      // Parse dates
      memory.createdAt = new Date(memory.createdAt);
      memory.updatedAt = new Date(memory.updatedAt);
      memory.transcript = memory.transcript.map(m => ({
        ...m,
        timestamp: new Date(m.timestamp),
      }));

      const memoryKey = `${teamName}:${teammateId}`;
      this.teammateMemories.set(memoryKey, memory);

      this.emit('memory:loaded', { team: teamName, teammateId });

      return memory;
    } catch {
      throw new TeammateError(
        `Failed to load memory for ${teammateId}`,
        TeammateErrorCode.MEMORY_LOAD_FAILED,
        teamName,
        teammateId
      );
    }
  }

  /**
   * Share transcript between teammates
   */
  async shareTranscript(
    teamName: string,
    fromId: string,
    toId: string,
    options?: { start?: number; end?: number }
  ): Promise<void> {
    this.ensureAvailable();

    const memory = await this.loadTeammateMemory(teamName, fromId);

    if (!memory) {
      throw new TeammateError(
        `No memory found for ${fromId}`,
        TeammateErrorCode.MEMORY_LOAD_FAILED,
        teamName,
        fromId
      );
    }

    let transcript = memory.transcript;

    if (options?.start !== undefined || options?.end !== undefined) {
      transcript = transcript.slice(options.start, options.end);
    }

    await this.sendMessage(teamName, fromId, toId, {
      type: 'result',
      payload: {
        transcriptShare: true,
        messages: transcript,
        fromTeammate: fromId,
      },
    });

    this.emit('transcript:shared', {
      team: teamName,
      from: fromId,
      to: toId,
      messageCount: transcript.length,
    });
  }

  // ==========================================================================
  // Teleport (NEW)
  // ==========================================================================

  /**
   * Check if team can teleport
   */
  async canTeleport(teamName: string, target: TeleportTarget): Promise<{ canTeleport: boolean; blockers: string[] }> {
    this.ensureAvailable();

    const blockers: string[] = [];

    // Check team exists
    if (!this.activeTeams.has(teamName)) {
      const teams = await this.discoverTeams();
      if (!teams.includes(teamName)) {
        blockers.push(`Team ${teamName} not found`);
      }
    }

    // Check git compatibility if gitAware
    if (this.config.teleport.gitAware && target.gitRepo) {
      const currentRepo = this.getCurrentGitRepo();
      if (currentRepo && currentRepo !== target.gitRepo) {
        blockers.push(`Git repository mismatch: current=${currentRepo}, target=${target.gitRepo}`);
      }
    }

    // Check working directory exists
    if (target.workingDirectory && !fs.existsSync(target.workingDirectory)) {
      blockers.push(`Working directory not found: ${target.workingDirectory}`);
    }

    return {
      canTeleport: blockers.length === 0,
      blockers,
    };
  }

  /**
   * Teleport team to new context
   */
  async teleportTeam(teamName: string, target: TeleportTarget): Promise<TeleportResult> {
    this.ensureAvailable();

    this.emit('teleport:started', { team: teamName, target });

    const { canTeleport, blockers } = await this.canTeleport(teamName, target);

    if (!canTeleport) {
      const error = `Cannot teleport: ${blockers.join(', ')}`;
      this.emit('teleport:failed', { team: teamName, error });
      return { success: false, blockers };
    }

    try {
      // Save current state
      await this.saveTeamState(teamName);

      // Update context
      const team = await this.loadTeam(teamName);

      if (target.workingDirectory) {
        team.context.workingDirectory = target.workingDirectory;
      }

      if (target.gitBranch) {
        team.context.gitBranch = target.gitBranch;
      }

      if (target.gitRepo) {
        team.context.gitRepo = target.gitRepo;
      }

      // Save updated state
      await this.saveTeamState(teamName);

      const result: TeleportResult = {
        success: true,
        teamState: team,
      };

      this.emit('teleport:completed', { team: teamName, result });

      return result;
    } catch (error) {
      const errorMsg = error instanceof Error ? error.message : 'Unknown error';
      this.emit('teleport:failed', { team: teamName, error: errorMsg });
      return { success: false, blockers: [errorMsg] };
    }
  }

  // ==========================================================================
  // Shutdown
  // ==========================================================================

  /**
   * Request teammate shutdown
   */
  async requestShutdown(teamName: string, teammateId: string, reason?: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const teammate = team.teammates.find(t => t.id === teammateId);

    if (teammate) {
      teammate.status = 'shutdown_pending';
    }

    this.emit('teammate:shutdown_requested', { team: teamName, teammateId, reason });
  }

  /**
   * Approve teammate shutdown
   */
  async approveShutdown(teamName: string, teammateId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);

    // Save memory before shutdown
    if (this.config.memory.autoPersist) {
      try {
        await this.saveTeammateMemory(teamName, teammateId);
      } catch {
        // Ignore memory save errors during shutdown
      }
    }

    team.teammates = team.teammates.filter(t => t.id !== teammateId);

    this.emit('teammate:shutdown_approved', { team: teamName, teammateId });
  }

  /**
   * Reject teammate shutdown
   */
  async rejectShutdown(teamName: string, teammateId: string): Promise<void> {
    this.ensureAvailable();

    const team = this.getTeamOrThrow(teamName);
    const teammate = team.teammates.find(t => t.id === teammateId);

    if (teammate) {
      teammate.status = 'active';
    }

    this.emit('teammate:shutdown_rejected', { team: teamName, teammateId });
  }

  /**
   * Cleanup team resources
   */
  async cleanup(teamName: string): Promise<void> {
    this.ensureAvailable();

    // Stop mailbox poller
    const poller = this.mailboxPollers.get(teamName);
    if (poller) {
      clearInterval(poller);
      this.mailboxPollers.delete(teamName);
    }

    // Stop memory persistence
    const memoryTimer = this.memoryPersistTimers.get(teamName);
    if (memoryTimer) {
      clearInterval(memoryTimer);
      this.memoryPersistTimers.delete(teamName);
    }

    // Performance: Flush any pending writes for this team
    this.flushPendingWrites();

    // Save final state
    try {
      await this.saveTeamState(teamName);
    } catch {
      // Ignore save errors during cleanup
    }

    // Performance: Invalidate cache
    this.invalidateConfigCache(teamName);

    // Dispose optimizer for this team
    const optimizer = this.topologyOptimizers.get(teamName);
    if (optimizer) {
      optimizer.dispose();
      this.topologyOptimizers.delete(teamName);
    }

    // Remove from active teams
    this.activeTeams.delete(teamName);

    // Clear environment
    if (process.env.CLAUDE_CODE_TEAM_NAME === teamName) {
      delete process.env.CLAUDE_CODE_TEAM_NAME;
    }

    this.emit('team:cleanup', { team: teamName });
  }

  // ==========================================================================
  // Utility Methods
  // ==========================================================================

  /**
   * Get team state
   */
  getTeamState(teamName: string): TeamState | undefined {
    return this.activeTeams.get(teamName);
  }

  /**
   * Get all active teams
   */
  getAllTeams(): Map<string, TeamState> {
    return new Map(this.activeTeams);
  }

  /**
   * Get backend status
   */
  async getBackendStatus(): Promise<BackendStatus[]> {
    const statuses: BackendStatus[] = [];

    // tmux backend
    try {
      execSync('which tmux', { encoding: 'utf-8' });
      statuses.push({
        backend: 'tmux',
        available: true,
        activeTeammates: 0,
        capacity: 100,
      });
    } catch {
      statuses.push({
        backend: 'tmux',
        available: false,
        activeTeammates: 0,
        capacity: 0,
      });
    }

    // in_process backend (always available)
    let inProcessCount = 0;
    for (const team of this.activeTeams.values()) {
      inProcessCount += team.teammates.length;
    }

    statuses.push({
      backend: 'in_process',
      available: true,
      activeTeammates: inProcessCount,
      capacity: 50,
    });

    return statuses;
  }

  // ==========================================================================
  // Private Methods
  // ==========================================================================

  private ensureAvailable(): void {
    if (!this.initialized) {
      throw new TeammateError(
        'TeammateBridge not initialized. Call initialize() first.',
        TeammateErrorCode.VERSION_INCOMPATIBLE
      );
    }

    if (!this.teammateToolAvailable && !this.config.fallbackToMCP) {
      throw new TeammateError(
        `TeammateTool not available. Requires Claude Code >= ${MINIMUM_CLAUDE_CODE_VERSION}, ` +
        `found: ${this.claudeCodeVersion ?? 'not installed'}`,
        TeammateErrorCode.VERSION_INCOMPATIBLE
      );
    }
  }

  private getTeamOrThrow(teamName: string): TeamState {
    const team = this.activeTeams.get(teamName);
    if (!team) {
      throw new TeammateError(
        `Team not found: ${teamName}`,
        TeammateErrorCode.TEAM_NOT_FOUND,
        teamName
      );
    }
    return team;
  }

  private getPlanOrThrow(team: TeamState, planId: string): TeamPlan {
    const plan = team.activePlans.find(p => p.id === planId);
    if (!plan) {
      throw new TeammateError(
        `Plan not found: ${planId}`,
        TeammateErrorCode.PLAN_NOT_FOUND,
        team.name
      );
    }
    return plan;
  }

  private getMailboxPath(teamName: string, teammateId: string): string {
    return path.join(this.teamsDir, teamName, 'mailbox', `${teammateId}.json`);
  }

  private async writeToMailbox(
    teamName: string,
    teammateId: string,
    message: MailboxMessage
  ): Promise<void> {
    // Security: Path validation already done by getMailboxPath via validateName
    const mailboxPath = this.getMailboxPath(teamName, teammateId);
    ensureDirectory(path.dirname(mailboxPath));

    let messages: MailboxMessage[] = [];

    if (fs.existsSync(mailboxPath)) {
      try {
        const content = fs.readFileSync(mailboxPath, 'utf-8');
        // Security: Use safe JSON parsing
        messages = safeJSONParse<MailboxMessage[]>(content);
      } catch {
        messages = [];
      }
    }

    // Check mailbox size limit
    if (messages.length >= this.config.mailbox.maxMessages) {
      // Remove oldest messages
      messages = messages.slice(-Math.floor(this.config.mailbox.maxMessages * 0.9));
    }

    messages.push(message);

    // Security: Write with restricted permissions (owner read/write only)
    fs.writeFileSync(mailboxPath, JSON.stringify(messages, null, 2), {
      encoding: 'utf-8',
      mode: 0o600,
    });
  }

  private startMailboxPoller(teamName: string): void {
    if (this.mailboxPollers.has(teamName)) {
      return;
    }

    const pollInterval = this.config.mailbox.pollingIntervalMs;

    const poller = setInterval(async () => {
      const team = this.activeTeams.get(teamName);
      if (!team) {
        clearInterval(poller);
        this.mailboxPollers.delete(teamName);
        return;
      }

      for (const teammate of team.teammates) {
        try {
          const mailboxPath = this.getMailboxPath(teamName, teammate.id);
          if (fs.existsSync(mailboxPath)) {
            const content = fs.readFileSync(mailboxPath, 'utf-8');
            const messages: MailboxMessage[] = JSON.parse(content);

            if (messages.length > 0) {
              this.emit('mailbox:messages', {
                team: teamName,
                teammateId: teammate.id,
                messages: messages.map(m => ({
                  ...m,
                  timestamp: new Date(m.timestamp),
                })),
              });
            }
          }
        } catch {
          // Ignore individual mailbox read errors
        }
      }
    }, pollInterval);

    this.mailboxPollers.set(teamName, poller);
  }

  private startMemoryPersistence(teamName: string): void {
    if (this.memoryPersistTimers.has(teamName)) {
      return;
    }

    const persistInterval = this.config.memory.persistIntervalMs;

    const timer = setInterval(async () => {
      const team = this.activeTeams.get(teamName);
      if (!team) {
        clearInterval(timer);
        this.memoryPersistTimers.delete(teamName);
        return;
      }

      for (const teammate of team.teammates) {
        try {
          await this.saveTeammateMemory(teamName, teammate.id);
        } catch {
          // Ignore individual memory save errors
        }
      }
    }, persistInterval);

    this.memoryPersistTimers.set(teamName, timer);
  }

  private async saveTeamState(teamName: string): Promise<void> {
    const team = this.activeTeams.get(teamName);
    if (!team) return;

    // Security: Validate team name
    const validatedName = validateName(teamName, 'team');
    const teamDir = path.join(this.teamsDir, validatedName);
    ensureDirectory(teamDir);

    // Security: Write with restricted permissions
    fs.writeFileSync(
      path.join(teamDir, 'state.json'),
      JSON.stringify(team, null, 2),
      { encoding: 'utf-8', mode: 0o600 }
    );
  }

  /**
   * Performance: Debounced file write to reduce I/O
   */
  private debouncedWrite(filePath: string, data: unknown): void {
    const existing = this.pendingWrites.get(filePath);
    if (existing) {
      clearTimeout(existing.timer);
    }

    const timer = setTimeout(() => {
      try {
        fs.writeFileSync(filePath, JSON.stringify(data, null, 2), {
          encoding: 'utf-8',
          mode: 0o600,
        });
        this.pendingWrites.delete(filePath);
      } catch (error) {
        // Log error but don't throw - debounced writes are best-effort
        this.emit('error', error);
      }
    }, this.writeDebounceMs);

    this.pendingWrites.set(filePath, { data, timer });
  }

  /**
   * Performance: Flush all pending writes immediately
   */
  private flushPendingWrites(): void {
    for (const [filePath, { data, timer }] of this.pendingWrites) {
      clearTimeout(timer);
      try {
        fs.writeFileSync(filePath, JSON.stringify(data, null, 2), {
          encoding: 'utf-8',
          mode: 0o600,
        });
      } catch {
        // Ignore errors during flush
      }
    }
    this.pendingWrites.clear();
  }

  /**
   * Performance: Get cached team config or load from disk
   */
  private getCachedTeamConfig(teamName: string): TeamConfig | null {
    const cached = this.teamConfigCache.get(teamName);
    const now = Date.now();

    if (cached && now - cached.timestamp < this.cacheMaxAgeMs) {
      return cached.config;
    }

    // Cache miss or expired - load from disk
    const validatedName = validateName(teamName, 'team');
    const configPath = path.join(this.teamsDir, validatedName, 'config.json');

    if (!fs.existsSync(configPath)) {
      return null;
    }

    try {
      const content = fs.readFileSync(configPath, 'utf-8');
      const config = safeJSONParse<TeamConfig>(content);
      this.teamConfigCache.set(teamName, { config, timestamp: now });
      return config;
    } catch {
      return null;
    }
  }

  /**
   * Performance: Invalidate team config cache
   */
  private invalidateConfigCache(teamName: string): void {
    this.teamConfigCache.delete(teamName);
  }

  private getDelegationChain(team: TeamState, teammateId: string): DelegationRecord[] {
    const chain: DelegationRecord[] = [];
    let currentId = teammateId;

    while (true) {
      const delegation = team.delegations.find(d => d.toId === currentId && d.active);
      if (!delegation) break;

      chain.push(delegation);
      currentId = delegation.fromId;

      // Prevent infinite loops
      if (chain.length > 10) break;
    }

    return chain;
  }

  private getCurrentGitBranch(): string | undefined {
    try {
      return execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
        encoding: 'utf-8',
      }).trim() || undefined;
    } catch {
      return undefined;
    }
  }

  private getCurrentGitRepo(): string | undefined {
    try {
      return execSync('git config --get remote.origin.url 2>/dev/null', {
        encoding: 'utf-8',
      }).trim() || undefined;
    } catch {
      return undefined;
    }
  }
}

// ============================================================================
// Factory Function
// ============================================================================

/**
 * Create and initialize a TeammateBridge instance
 */
export async function createTeammateBridge(
  config?: Partial<PluginConfig>
): Promise<TeammateBridge> {
  const bridge = new TeammateBridge(config);
  await bridge.initialize();
  return bridge;
}

export default TeammateBridge;