/**
 * Agent Orchestrator - the core agent loop
 *
 * Capabilities beyond Claude Code:
 *   - Smart Context: auto-load related VL files via dependency graph
 *   - Blueprint Context: inject PRD/ServiceMap/UIMap architectural info
 *   - Plan Mode: explore → design → approve → implement workflow
 *   - Session Persistence: resume conversations across sessions
 *   - Warm-up Caching: pre-cache system prompt for fast first response
 *   - SSE Streaming: real-time token streaming for web UI
 *   - Parallel sub-agents with isolated context
 */
import { createProvider } from './llm-provider.js';
import path from 'path';
import fs from 'fs';

/**
 * Retry with exponential backoff for transient API errors
 * Handles: rate limits (429), overloaded (529), server errors (5xx), network errors
 */
async function withRetry(fn, { maxRetries = 3, onRetry } = {}) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isRetryable =
        err.status === 429 ||
        err.status === 529 ||
        err.status >= 500 ||
        err.error?.type === 'overloaded_error' ||
        err.code === 'ECONNRESET' ||
        err.code === 'ETIMEDOUT' ||
        err.code === 'ECONNREFUSED';

      if (!isRetryable || attempt === maxRetries) throw err;

      const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
      onRetry?.(attempt + 1, delay, err);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

export class AgentOrchestrator {
  constructor({ config, toolRegistry, contextManager, promptAssembler, cli, projectContext }) {
    this.config = config;
    this.toolRegistry = toolRegistry;
    this.contextManager = contextManager;
    this.promptAssembler = promptAssembler;
    this.cli = cli;
    this.projectContext = projectContext;
    // LLM provider: CLI (Team OAuth, zero cost) or API (per-token)
    try {
      this.client = createProvider(config);
    } catch {
      this.client = null;
    }
    this.todos = [];
    this.turnsSinceTodo = 0;
    this.isFirstMessage = true;
    this.subAgentIdCounter = 0;
    this._activeStream = null;  // Current Anthropic MessageStream (for abort)
    this._signal = null;        // AbortSignal from current request
    this._askUserLock = Promise.resolve();  // Serialize parallel AskUserQuestion calls
    this._backgroundAgents = new Map();     // Background agent results

    // Extended thinking (auto-enable for Opus models)
    this.thinkingEnabled = config.thinkingEnabled ?? config.model?.includes('opus');
    this.thinkingBudget = config.thinkingBudget || 10000;

    // Optional modules wired in by index.js
    this.smartContext = null;        // SmartContextLoader
    this.generationPipeline = null;  // GenerationPipeline
    this.blueprintContext = null;    // BlueprintContext
    this.sessionManager = null;      // SessionManager
    this.symbolIndex = null;         // VLSymbolIndex
    this.hooks = null;               // HooksManager

    // Skill system
    this.skills = new Map();
    this._registerBuiltinSkills();

    // Plan mode state
    this.planMode = false;
    this.planContent = null;

    // Last significant user input (for context recovery on short follow-ups)
    this._lastSignificantInput = null;

    // Permission system: destructive tool names that require confirmation in web mode
    this.destructiveTools = new Set(['WriteFile', 'EditFile', 'Bash', 'VLParse']);
    this.permissionMode = config.permissionMode || 'auto'; // 'auto' | 'confirm' | 'deny'
    this._pendingPermission = null;

    // Warm-up cache
    this._warmupDone = false;

    // Debounced file tree rescan (prevents flooding during rapid generation)
    this._rescanTimer = null;
  }

  /**
   * Schedule a debounced rescan + broadcast. Multiple calls within 1200ms
   * collapse into a single scan, preventing UI flooding during generation.
   */
  /** Check if there are unresolved compile errors from last compilation */
  _hasCompileErrors() {
    try {
      const compilePath = path.join(this.config.workDir, '.vl-code', 'last-compile.json');
      if (!fs.existsSync(compilePath)) return false;
      const data = JSON.parse(fs.readFileSync(compilePath, 'utf-8'));
      if (!data.errList?.length) return false;
      // Only consider recent errors (within 30 minutes)
      const age = Date.now() - new Date(data.timestamp).getTime();
      if (age >= 30 * 60 * 1000) return false;
      // Feed compile errors into smart context for relevance boosting
      if (this.smartContext) this.smartContext.updateCompileErrors(data.errList);
      return true;
    } catch { return false; }
  }

  /** Read the last compile report if it is recent enough. */
  _readLastCompileReport(maxAgeMs = 30 * 60 * 1000) {
    try {
      const compilePath = path.join(this.config.workDir, '.vl-code', 'last-compile.json');
      if (!fs.existsSync(compilePath)) return null;
      const data = JSON.parse(fs.readFileSync(compilePath, 'utf-8'));
      if (!data?.timestamp) return data;
      const age = Date.now() - new Date(data.timestamp).getTime();
      if (Number.isFinite(age) && age > maxAgeMs) return null;
      return data;
    } catch {
      return null;
    }
  }

  _formatCompileErrors(errList, limit = 10) {
    return (errList || []).slice(0, limit).map(e => {
      const file = e?.file || e?.filePath || e?.path || 'unknown';
      const line = e?.line || e?.lineNumber || '?';
      const message = e?.message || e?.msg || JSON.stringify(e);
      return `- ${file}:${line} — ${message}`;
    }).join('\n');
  }

  /**
   * Fix orphaned tool_use/tool_result blocks in both directions:
   * 1. tool_use with no following tool_result → inject stub tool_result
   * 2. tool_result whose tool_use_id doesn't exist in the immediately preceding
   *    assistant message → remove the orphaned tool_result block
   * Prevents 400 errors: "tool_use ids found without tool_result" and vice versa.
   */
  _fixOrphanedToolUse() {
    const msgs = this.contextManager.messages;
    if (msgs.length < 1) return;

    // Pass 1: remove tool_result blocks that reference non-existent tool_use IDs.
    // API requires each tool_result's tool_use_id to exist in the nearest preceding
    // assistant message (not just msgs[i-1], since consecutive user messages can exist
    // before getMessages() merges them).
    for (let i = 1; i < msgs.length; i++) {
      const msg = msgs[i];
      if (msg.role !== 'user' || !Array.isArray(msg.content)) continue;
      if (!msg.content.some(b => b.type === 'tool_result')) continue;

      // Find nearest preceding assistant message
      let assistantMsg = null;
      for (let j = i - 1; j >= 0; j--) {
        if (msgs[j].role === 'assistant') { assistantMsg = msgs[j]; break; }
      }
      if (!assistantMsg || !Array.isArray(assistantMsg.content)) continue;

      // Collect all tool_use IDs from that assistant message
      const prevToolUseIds = new Set(
        assistantMsg.content.filter(b => b.type === 'tool_use' && b.id).map(b => b.id)
      );

      // Remove tool_result blocks that reference IDs not in the assistant message
      msg.content = msg.content.filter(b => {
        if (b.type !== 'tool_result') return true;
        return prevToolUseIds.has(b.tool_use_id);
      });

      // If user message is now empty (only had orphaned tool_results), remove it
      if (msg.content.length === 0) {
        msgs.splice(i, 1);
        i--;
      }
    }

    // Pass 2: inject stub tool_results for any unpaired tool_use blocks
    const pendingToolUseIds = new Map(); // id → assistant message index
    for (let i = 0; i < msgs.length; i++) {
      const msg = msgs[i];
      if (!Array.isArray(msg.content)) continue;
      for (const block of msg.content) {
        if (block.type === 'tool_use' && block.id) {
          pendingToolUseIds.set(block.id, i);
        }
        if (block.type === 'tool_result' && block.tool_use_id) {
          pendingToolUseIds.delete(block.tool_use_id);
        }
      }
    }

    if (pendingToolUseIds.size > 0) {
      const orphanedIds = [...pendingToolUseIds.keys()];
      const results = orphanedIds.map(id => ({
        type: 'tool_result', tool_use_id: id, content: 'Aborted by user',
      }));
      msgs.push({ role: 'user', content: results });
    }
  }

  _scheduleRescan() {
    if (this._rescanTimer) clearTimeout(this._rescanTimer);
    this._rescanTimer = setTimeout(async () => {
      this._rescanTimer = null;
      try {
        await this.projectContext.scan();  // Properly awaits even if another scan is running
        this._broadcast?.({ type: 'project_reloaded' });
      } catch {}
    }, 1200);
  }

  /**
   * Meta Guard: after an AI tool writes a VL file, extract its metadata and compare
   * against the in-memory ProjectMeta. Broadcasts meta_drift if drift is detected.
   * Non-blocking — runs fire-and-forget from executeToolCallsStreaming.
   */
  async _checkMetaDrift(filePath) {
    try {
      if (!this.projectContext?.projectMeta) return;

      // Dynamically import to avoid circular dep at module load time
      const { extractSingleFile, mergeIntoMeta, validateMeta } = await import('../vl/metadata-extractor.js');
      if (!extractSingleFile) return;

      const fullPath = filePath.startsWith(this.config.workDir)
        ? filePath
        : path.join(this.config.workDir, filePath);

      const content = fs.readFileSync ? fs.readFileSync(fullPath, 'utf-8') : null;
      if (!content) return;

      const relativePath = fullPath.startsWith(this.config.workDir)
        ? fullPath.substring(this.config.workDir.length + 1) : fullPath;

      const extracted = extractSingleFile(relativePath, content, this.config.workDir);
      if (!extracted) return;

      // Clone meta before merge to detect actual changes
      const before = JSON.stringify(this.projectContext.projectMeta);
      mergeIntoMeta(this.projectContext.projectMeta, extracted);
      const after = JSON.stringify(this.projectContext.projectMeta);

      if (before !== after) {
        const validation = validateMeta ? validateMeta(this.projectContext.projectMeta) : { valid: true, issues: [] };
        this._broadcast?.({
          type: 'meta_drift',
          file: relativePath,
          valid: validation.valid,
          issues: (validation.issues || []).slice(0, 5),
          message: `ProjectMeta updated from ${relativePath}`,
        });
      }
    } catch { /* non-critical */ }
  }

  /**
   * Warm-up: pre-cache system prompt and tool schemas
   * Sends a minimal API call to populate prompt cache for fast first response
   */
  async warmUp() {
    if (this._warmupDone) return;
    try {
      await this.client.messages.create({
        model: this.config.model,
        max_tokens: 1,
        system: this.promptAssembler.buildSystemPrompt(),
        tools: this.toolRegistry.getToolSchemas(),
        messages: [{ role: 'user', content: 'ping' }],
      });
      this._warmupDone = true;
    } catch {
      // Warm-up failure is non-critical
    }
  }

  async processUserMessage(userInput) {
    if (!this.client) throw new Error('Claude CLI not available. Ensure Claude Code is installed and you are logged in with Team subscription.');

    // Plan mode: collect plan without executing tools
    if (this.planMode) {
      return this.handlePlanMode(userInput);
    }

    // Build user message content blocks with system-reminders
    const contentBlocks = [];

    // Inject initial reminders on first message
    if (this.isFirstMessage) {
      for (const reminder of this.promptAssembler.buildInitialReminders()) {
        contentBlocks.push({ type: 'text', text: reminder });
      }
      // Inject session recovery context
      if (this.sessionManager) {
        const recovery = this.sessionManager.getRecoveryContext();
        if (recovery) {
          contentBlocks.push({ type: 'text', text: recovery });
          this.cli.print(chalk_dim('  ♻ Restored previous session context'));
        }
      }
      this.isFirstMessage = false;
    }

    // User's actual message
    contentBlocks.push({ type: 'text', text: userInput });

    // Context recovery: if this is a short follow-up and there's a prior task, remind the AI
    if (userInput.trim().length < 30 && this._lastSignificantInput) {
      contentBlocks.push({
        type: 'text',
        text: `<system-reminder>\nContext: the user's last significant request was: "${this._lastSignificantInput}"\nThis short message is likely a follow-up to that task.\n</system-reminder>`,
      });
    }
    if (userInput.trim().length > 20) {
      this._lastSignificantInput = userInput.trim().slice(0, 300);
    }

    // Smart Context: auto-load relevant files based on user message
    if (this.smartContext) {
      const relevant = this.smartContext.discoverRelevantFiles(userInput);
      if (relevant.length > 0) {
        const contextBlock = await this.smartContext.loadContext(relevant);
        if (contextBlock) {
          contentBlocks.push({ type: 'text', text: contextBlock });
          this.cli.print(chalk_dim(`  📎 Auto-loaded ${relevant.length} related files`));
        }
      }
    }

    // Blueprint Context: inject relevant blueprint entries
    if (this.blueprintContext?.hasBlueprints()) {
      // Extract file references from message to find relevant blueprints
      const fileRefs = this.extractFileReferences(userInput);
      for (const ref of fileRefs) {
        const bpContext = this.blueprintContext.getContextForFile(ref);
        if (bpContext) {
          contentBlocks.push({ type: 'text', text: bpContext });
          this.cli.print(chalk_dim(`  📐 Injected blueprint context for ${ref}`));
        }
      }
    }

    // Todo status reminder
    if (this.todos.length > 0) {
      const todoText = this.todos.map(t => {
        const icon = t.status === 'completed' ? '[✓]' : t.status === 'in_progress' ? '[→]' : '[ ]';
        return `${icon} ${t.content}`;
      }).join('\n');
      contentBlocks.push({
        type: 'text',
        text: `<system-reminder>\nCurrent todo list:\n${todoText}\n</system-reminder>`,
      });
    }

    this.contextManager.addUserMessage(contentBlocks);

    // Enter agent loop
    await this.agentLoop();
  }

  /**
   * Process user message with SSE streaming callbacks (for web UI)
   * @param {string} userInput
   * @param {Object} callbacks - { onText, onToolCall, onToolResult, onTodo, onDone, onError }
   */
  /**
   * Process user message with SSE streaming callbacks (for web UI)
   * @param {string} userInput
   * @param {Object} callbacks
   * @param {Object} options - { images: [{data, mediaType}], mentions: [string], selection: string }
   */
  async processUserMessageStreaming(userInput, callbacks = {}, options = {}) {
    if (!this.client) {
      callbacks.onError?.('Claude CLI not available. Ensure Claude Code is installed and you are logged in.');
      return;
    }
    const cb = callbacks;
    this._signal = options.signal || null;

    // Plan mode: handle approve/cancel or continue exploring
    if (this.planMode) {
      return this.handlePlanModeStreaming(userInput, cb, options);
    }

    // Auto-detect slash commands and route to executeSkill
    if (!options._fromSkill && typeof userInput === 'string') {
      const trimmed = userInput.trim();
      if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {
        const parts = trimmed.substring(1).split(/\s+/);
        const skillName = parts[0];
        if (this.skills.has(skillName)) {
          const args = parts.slice(1).join(' ') || undefined;
          return this.executeSkill(skillName, args, cb);
        }
      }
    }

    try {
      const contentBlocks = [];

      if (this.isFirstMessage) {
        for (const reminder of this.promptAssembler.buildInitialReminders()) {
          contentBlocks.push({ type: 'text', text: reminder });
        }
        this.isFirstMessage = false;
      }

      contentBlocks.push({ type: 'text', text: userInput });

      // Context recovery: if this is a short follow-up and there's a prior task, remind the AI
      if (userInput.trim().length < 30 && this._lastSignificantInput) {
        contentBlocks.push({
          type: 'text',
          text: `<system-reminder>\nContext: the user's last significant request was: "${this._lastSignificantInput}"\nThis short message is likely a follow-up to that task.\n</system-reminder>`,
        });
      }
      // Track the last significant input (> 20 chars, not just a command word)
      if (userInput.trim().length > 20) {
        this._lastSignificantInput = userInput.trim().slice(0, 300);
      }

      // Image attachments (vision support)
      if (options.images?.length) {
        for (const img of options.images) {
          contentBlocks.push({
            type: 'image',
            source: { type: 'base64', media_type: img.mediaType, data: img.data },
          });
        }
      }

      // @-mention: explicitly load mentioned files into context
      if (options.mentions?.length) {
        for (const mention of options.mentions) {
          const file = this.projectContext.getAllFiles().find(
            f => f.name === mention || f.path.endsWith(mention)
          );
          if (file) {
            try {
              const { result } = await this.toolRegistry.execute('ReadFile', { file_path: file.path });
              contentBlocks.push({
                type: 'text',
                text: `<file-context path="${file.path}">\n${result}\n</file-context>`,
              });
            } catch {}
          }
        }
      }

      // IDE selection context
      if (options.selection) {
        contentBlocks.push({
          type: 'text',
          text: `<ide-selection>\n${options.selection}\n</ide-selection>`,
        });
      }

      // Smart Context: auto-load relevant files based on user message
      if (this.smartContext) {
        const relevant = this.smartContext.discoverRelevantFiles(userInput);
        if (relevant.length > 0) {
          const contextBlock = await this.smartContext.loadContext(relevant, userInput.length);
          if (contextBlock) {
            contentBlocks.push({ type: 'text', text: contextBlock });
          }
        }
      }

      // Workflow intent detection: inject directive for generation or debug flows
      const intent = this.detectWorkflowIntent(userInput);
      if (intent) {
        let directive;
        if (intent.type === 'generation') {
          directive = `MANDATORY: Call VLGenerate(action:"generate", userRequest:"${userInput.replace(/"/g, '\\"').slice(0, 500)}") as your FIRST tool call. If no workspace is open, infer a short PascalCase English projectName and pass it explicitly. Do NOT use SubAgent, WriteFile, or any other tool first. After starting generation, poll VLGenerate(action:"status") until active=false. Do NOT use Bash sleep loops when the status tool can tell you the workflow state. Only after generation is inactive should you compile with VLCompile, then run AutoTestPipeline generate + run if compilation succeeds.`;
        } else if (intent.type === 'adjustment') {
          directive = `MANDATORY: Call VLAdjust(action:"adjust", changeRequest:"${userInput.replace(/"/g, '\\"').slice(0, 500)}") as your FIRST tool call. Do NOT try to edit files manually — the VLAdjust tool runs the appropriate adjustment workflow (add-page, add-service, theme-customize, add-feature, modify-data, batch, or general incremental update). After starting the workflow, poll VLAdjust(action:"status") until active=false. Do NOT use Bash sleep loops when the status tool can tell you the workflow state. Only after the workflow is inactive should you compile with VLCompile and run AutoTestPipeline if compilation succeeds.`;
        } else {
          directive = intent.context;
        }
        contentBlocks.push({
          type: 'text',
          text: `<system-reminder>\nWorkflow intent: ${intent.type}\n${directive}\n</system-reminder>`,
        });
      }

      // Todo reminder
      if (this.todos.length > 0) {
        const todoText = this.todos.map(t => {
          const icon = t.status === 'completed' ? '[✓]' : t.status === 'in_progress' ? '[→]' : '[ ]';
          return `${icon} ${t.content}`;
        }).join('\n');
        contentBlocks.push({
          type: 'text',
          text: `<system-reminder>\nCurrent todo list:\n${todoText}\n</system-reminder>`,
        });
        cb.onTodo?.(this.todos);
      }

      this.contextManager.addUserMessage(contentBlocks);

      // Enter streaming agent loop
      await this.agentLoopStreaming(cb);

      // Fix orphaned tool_use blocks after abort: ensure every tool_use has a matching tool_result
      this._fixOrphanedToolUse();

      // Final rescan after all tools complete (cancel any pending debounce)
      if (this._rescanTimer) clearTimeout(this._rescanTimer);
      this._rescanTimer = null;
      try {
        await this.projectContext.scan();
        this._broadcast?.({ type: 'project_reloaded' });
        // Trigger auto-metadata extraction after generation/modification
        this._broadcast?.({ type: 'auto_extract_meta' });
      } catch {}

      cb.onDone?.();
    } catch (err) {
      if (!this._signal?.aborted) cb.onError?.(err.message);
    } finally {
      this._signal = null;
      this._activeStream = null;
    }
  }

  /** Agent loop with streaming LLM responses + SSE callbacks */
  async agentLoopStreaming(cb) {
    const MAX_TURNS = 30;
    let turns = 0;

    while (turns < MAX_TURNS) {
      if (this._signal?.aborted) { cb.onError?.('Aborted by user'); break; }
      turns++;

      if (this.contextManager.needsCompression()) {
        await this.contextManager.compress(this.client);
      }

      // Sanitize: fix any orphaned tool_use/tool_result pairs before sending
      this._fixOrphanedToolUse();

      // Streaming LLM call
      const { content, stopReason } = await this.callLLMStreaming(cb);

      let hasToolUse = false;
      const toolUseBlocks = [];
      const textBlocks = [];

      for (const block of content) {
        if (block.type === 'text') textBlocks.push(block.text);
        if (block.type === 'tool_use') { hasToolUse = true; toolUseBlocks.push(block); }
      }

      // Store assistant message
      this.contextManager.addAssistantMessage(content);

      // If no tool use, the agent is done
      if (!hasToolUse) break;

      // Execute tool calls with SSE notification
      await this.executeToolCallsStreaming(toolUseBlocks, cb);
      this.turnsSinceTodo++;

      // If model said end_turn with tools, execute them but stop after
      if (stopReason === 'end_turn') break;
    }
  }

  /** Streaming LLM call – sends text/thinking tokens to callback as they arrive */
  async callLLMStreaming(cb) {
    const systemPrompt = this.promptAssembler.buildSystemPrompt();
    const tools = this.toolRegistry.getToolSchemas();
    const messages = this.contextManager.getMessages();

    const params = {
      model: this.config.model,
      max_tokens: this.contextManager.maxOutputTokens,
      system: systemPrompt,
      tools,
      messages,
    };

    // Extended thinking for capable models
    if (this.thinkingEnabled) {
      params.thinking = { type: 'enabled', budget_tokens: this.thinkingBudget };
    }

    const stream = await withRetry(
      () => this.client.messages.stream(params),
      {
        onRetry: (attempt, delay, err) => {
          cb.onRetry?.(attempt, delay, err);
        },
      }
    );

    // Store stream for external abort
    this._activeStream = stream;
    if (this._signal) {
      if (this._signal.aborted) {
        stream.abort();
        this._activeStream = null;
        return { content: [], stopReason: 'aborted' };
      }
      this._signal.addEventListener('abort', () => { try { stream.abort(); } catch {} }, { once: true });
    }

    let currentText = '';
    let currentThinking = '';
    let currentToolUse = null;

    try {
      for await (const event of stream) {
        if (event.type === 'content_block_start') {
          if (event.content_block.type === 'text') {
            currentText = '';
          } else if (event.content_block.type === 'thinking') {
            currentThinking = '';
            cb.onThinking?.('start');
          } else if (event.content_block.type === 'tool_use') {
            currentToolUse = { type: 'tool_use', id: event.content_block.id, name: event.content_block.name, input: '' };
          }
        } else if (event.type === 'content_block_delta') {
          if (event.delta.type === 'text_delta') {
            currentText += event.delta.text;
            cb.onText?.(event.delta.text);
          } else if (event.delta.type === 'thinking_delta') {
            currentThinking += event.delta.thinking;
            cb.onThinking?.('delta', event.delta.thinking);
          } else if (event.delta.type === 'input_json_delta') {
            if (currentToolUse) currentToolUse.input += event.delta.partial_json;
          }
        } else if (event.type === 'content_block_stop') {
          if (currentThinking) {
            cb.onThinking?.('end');
            currentThinking = '';
          }
          if (currentText) {
            currentText = '';
          }
          if (currentToolUse) {
            try { currentToolUse.input = JSON.parse(currentToolUse.input); } catch { currentToolUse.input = {}; }
            cb.onToolCall?.(currentToolUse.name, currentToolUse.input);
            currentToolUse = null;
          }
        }
      }
    } catch (e) {
      // If aborted, return gracefully instead of throwing
      if (this._signal?.aborted) {
        this._activeStream = null;
        return { content: [], stopReason: 'aborted' };
      }
      throw e;
    }

    this._activeStream = null;

    // Use finalMessage for accurate content blocks (includes thinking signatures)
    const finalMessage = await stream.finalMessage();

    // Update context with real token usage from API
    if (finalMessage.usage) {
      this.contextManager.updateFromUsage(finalMessage.usage);
    }

    return { content: finalMessage.content, stopReason: finalMessage.stop_reason };
  }

  /** Execute tool calls with streaming callbacks + permission checks */
  async executeToolCallsStreaming(toolUseBlocks, cb) {
    const executions = toolUseBlocks.map(async (block) => {
      const { id, name, input } = block;
      if (this._signal?.aborted) return { id, name, result: 'Aborted by user' };

      cb.onToolCall?.(name, summarizeInput(name, input));

      // Permission check for destructive operations
      if (this.permissionMode === 'confirm' && this.destructiveTools.has(name)) {
        cb.onPermissionRequest?.(name, summarizeInput(name, input));
        // In confirm mode, notify but still execute (user can toggle to 'deny')
      }

      if (name === 'TodoWrite') {
        const now = Date.now();
        const prevMap = new Map(this.todos.map(t => [t.content, t]));
        this.todos = (input.todos || []).map(t => {
          const prev = prevMap.get(t.content);
          const todo = { ...t };
          // Track timestamps for timing display
          if (t.status === 'in_progress' && (!prev || prev.status !== 'in_progress')) {
            todo.startedAt = now;
          } else if (prev?.startedAt) {
            todo.startedAt = prev.startedAt;
          }
          if (t.status === 'completed' && (!prev || prev.status !== 'completed')) {
            todo.completedAt = now;
            if (prev?.startedAt) todo.startedAt = prev.startedAt;
          } else if (prev?.completedAt) {
            todo.completedAt = prev.completedAt;
          }
          return todo;
        });
        this.turnsSinceTodo = 0;
        cb.onTodo?.(this.todos);
        return { id, result: 'Todos updated successfully.' };
      }

      if (name === 'SubAgent') {
        const result = await this.executeSubAgent(input);
        cb.onToolResult?.(name, result);
        return { id, result };
      }

      // AskUserQuestion: send to UI and wait for response
      // Serialize via lock: if Claude calls AskUserQuestion multiple times in the same
      // response batch (parallel), queue them so each is shown and answered in order.
      // Without this, parallel calls overwrite _pendingQuestion → orphaned Promises → freeze.
      if (name === 'AskUserQuestion') {
        if (!this._askUserLock) this._askUserLock = Promise.resolve();
        const answer = await (this._askUserLock = this._askUserLock.then(
          () => new Promise((resolve) => {
            this._pendingQuestion = { resolve, question: input };
            cb.onAskUser?.(input);
          })
        ));
        this._pendingQuestion = null;
        return { id, result: `User answered: ${answer}` };
      }

      // ToolSearch: discover/activate deferred tools
      if (name === 'ToolSearch') {
        const { result, error } = await this.toolRegistry.execute(name, input);
        const output = error ? `Error: ${error}` : (typeof result === 'string' ? result : JSON.stringify(result));
        cb.onToolResult?.(name, output.substring(0, 300));
        return { id, result: output };
      }

      // Memory tool: persist/retrieve cross-session memory
      if (name === 'Memory') {
        const { result, error } = await this.toolRegistry.execute(name, input);
        const output = error ? `Error: ${error}` : (typeof result === 'string' ? result : JSON.stringify(result));
        cb.onToolResult?.(name, output.substring(0, 300));
        return { id, result: output };
      }

      // Hooks: run pre-tool hooks
      if (this.hooks) {
        const hookResult = await this.hooks.runPre(name, input);
        if (hookResult?.blocked) {
          return { id, result: `Blocked by hook: ${hookResult.reason}` };
        }
        // Semantic event hooks
        if (name === 'VLValidate') await this.hooks.runEvent('pre-validate', { file_path: input.file_path });
        if (name === 'EditFile' || name === 'VLEditSection') await this.hooks.runEvent('pre-edit', { file_path: input.file_path });
      }

      // Re-check abort before executing (signal may have fired while queued)
      if (this._signal?.aborted) {
        cb.onToolResult?.(name, 'Aborted by user');
        return { id, name, result: 'Aborted by user' };
      }

      const { result, error } = await this.toolRegistry.execute(name, input);

      // Extract diff/undo metadata from EditFile before stringifying
      let output;
      let diffData = null;
      if (error) {
        output = `Error: ${error}`;
      } else if (result && typeof result === 'object' && result.diff !== undefined) {
        // EditFile returns {result, diff, undoId, lineNum, replacementCount}
        output = result.result;
        diffData = { diff: result.diff, undoId: result.undoId, filePath: input.file_path, lineNum: result.lineNum };
      } else {
        output = typeof result === 'string' ? result : JSON.stringify(result);
      }
      cb.onToolResult?.(name, output.substring(0, 300), diffData);

      // Hooks: run post-tool hooks
      if (this.hooks) {
        await this.hooks.runPost(name, input, output);
        if (name === 'VLValidate') await this.hooks.runEvent('post-validate', { file_path: input.file_path, result: output.substring(0, 200) });
        if (name === 'EditFile' || name === 'VLEditSection') await this.hooks.runEvent('post-edit', { file_path: input.file_path });
        if (name === 'WriteFile' && input.file_path?.match(/\.(vx|sc|cp|vs|vdb)$/)) await this.hooks.runEvent('post-generate', { file_path: input.file_path });
      }

      // File-mutating tools: debounced rescan + record edit for smart context recency
      const fileMutators = ['WriteFile', 'EditFile', 'VLEditSection', 'WorkflowRun', 'VLParse', 'Bash'];
      if (fileMutators.includes(name)) {
        this._scheduleRescan();
        if (this.smartContext && input.file_path) {
          this.smartContext.recordEdit(input.file_path);
        }

        // Meta Guard: detect ProjectMeta drift after AI writes to VL files
        const vlFileMatch = input.file_path?.match(/\.(vx|sc|cp|vs|vdb)$/);
        if (vlFileMatch && (name === 'WriteFile' || name === 'EditFile' || name === 'VLEditSection')) {
          this._checkMetaDrift(input.file_path).catch(() => {});
        }
      }

      return { id, name, result: output };
    });

    const results = await Promise.all(executions);

    for (const { id, name: toolName, result } of results) {
      try {
        let content = result;
        const reminder = this.promptAssembler.buildToolReminder(toolName, {
          turnsSinceTodo: this.turnsSinceTodo,
          hasPendingTasks: this.todos.some(t => t.status !== 'completed'),
          turnCount: this.contextManager.turnCount,
          isVLProject: this.projectContext.isVLProject(),
          hasBlueprints: this.blueprintContext?.hasBlueprints(),
          hasCompileErrors: this._hasCompileErrors(),
        });
        if (reminder) content += `\n${reminder}`;
        this.contextManager.addToolResult(id, content);
      } catch (e) {
        // Fallback: add minimal tool result to maintain tool_use/tool_result pairing
        try { this.contextManager.addToolResult(id, `Error processing result: ${e.message}`); } catch {}
      }
    }
  }

  async agentLoop() {
    const MAX_TURNS = 30;
    let turns = 0;

    while (turns < MAX_TURNS) {
      turns++;

      // Check if compression needed
      if (this.contextManager.needsCompression()) {
        this.cli.print(chalk_dim('  ⚡ Compressing conversation...'));
        await this.contextManager.compress(this.client);
      }

      // Call LLM
      const response = await this.callLLM();

      // Process response content blocks
      let hasToolUse = false;
      const toolUseBlocks = [];
      const textBlocks = [];

      for (const block of response.content) {
        if (block.type === 'text') {
          textBlocks.push(block.text);
        } else if (block.type === 'tool_use') {
          hasToolUse = true;
          toolUseBlocks.push(block);
        } else if (block.type === 'thinking') {
          // Extended thinking — show brief indicator in CLI
          const preview = block.thinking?.substring(0, 80) || '';
          this.cli.print(chalk_dim(`  💭 Thinking... ${preview}${block.thinking?.length > 80 ? '...' : ''}`));
        }
      }

      // Print text output
      if (textBlocks.length > 0) {
        this.cli.printAssistant(textBlocks.join('\n'));
      }

      // Store assistant message in context
      this.contextManager.addAssistantMessage(response.content);

      // If no tool use, we're done with this turn
      if (!hasToolUse || response.stop_reason === 'end_turn') {
        if (!hasToolUse) break;
      }

      // Execute tool calls (parallel for independent calls)
      await this.executeToolCalls(toolUseBlocks);

      this.turnsSinceTodo++;
    }
  }

  async callLLM() {
    const systemPrompt = this.promptAssembler.buildSystemPrompt();
    const tools = this.toolRegistry.getToolSchemas();
    const messages = this.contextManager.getMessages();

    const params = {
      model: this.config.model,
      max_tokens: this.contextManager.maxOutputTokens,
      system: systemPrompt,
      tools,
      messages,
    };

    // Extended thinking for capable models
    if (this.thinkingEnabled) {
      params.thinking = { type: 'enabled', budget_tokens: this.thinkingBudget };
    }

    try {
      const response = await withRetry(
        () => this.client.messages.create(params),
        {
          onRetry: (attempt, delay, err) => {
            this.cli?.print(chalk_dim(`  ⚡ API retry ${attempt}/3 (${err.status || err.code}) in ${(delay / 1000).toFixed(1)}s`));
          },
        }
      );

      // Update context with real token usage from API
      if (response.usage) {
        this.contextManager.updateFromUsage(response.usage);
      }

      return response;
    } catch (err) {
      if (err.message?.includes('output token')) {
        throw new Error(
          `Response exceeded ${this.contextManager.maxOutputTokens} token limit. ` +
          `Set VL_CODE_MAX_OUTPUT_TOKENS to increase.`
        );
      }
      throw err;
    }
  }

  async executeToolCalls(toolUseBlocks) {
    // Execute all tool calls in parallel (Claude Code style)
    const executions = toolUseBlocks.map(async (block) => {
      const { id, name, input } = block;

      this.cli.printToolCall(name, summarizeInput(name, input));

      // Special handling for TodoWrite
      if (name === 'TodoWrite') {
        this.todos = input.todos || [];
        this.turnsSinceTodo = 0;
        this.cli.printTodo(this.todos);
        return { id, result: 'Todos updated successfully.' };
      }

      // Special handling for SubAgent (Task equivalent)
      if (name === 'SubAgent') {
        const result = await this.executeSubAgent(input);
        return { id, result };
      }

      // Execute regular tool
      const { result, error } = await this.toolRegistry.execute(name, input);

      const output = error ? `Error: ${error}` : (typeof result === 'string' ? result : JSON.stringify(result));
      this.cli.printToolResult(name, output.substring(0, 120));

      return { id, name, result: output };
    });

    const results = await Promise.all(executions);

    // Add all tool results to context
    for (const { id, name: toolName, result } of results) {
      let content = result;

      // Inject system-reminders into tool results (conditional)
      const reminder = this.promptAssembler.buildToolReminder(toolName, {
        turnsSinceTodo: this.turnsSinceTodo,
        hasPendingTasks: this.todos.some(t => t.status !== 'completed'),
        hasCompileErrors: this._hasCompileErrors(),
      });
      if (reminder) {
        content += `\n${reminder}`;
      }

      this.contextManager.addToolResult(id, content);
    }
  }

  /**
   * Execute a sub-agent with isolated context
   * Supports: explore, plan, generate, general (full tool access)
   * Supports: background execution, model override, resume
   */
  async executeSubAgent(input) {
    const {
      prompt,
      agentType = 'explore',
      maxTurns = 10,
      runInBackground = false,
      resume,
      model,
    } = input;

    // Resume: continue a previous agent from its saved message history
    if (resume) {
      this._backgroundAgents = this._backgroundAgents || new Map();
      const prev = this._backgroundAgents.get(resume);
      if (!prev) return `Agent ${resume} not found. Cannot resume.`;
      if (prev.status === 'running') return `Agent ${resume} is still running.`;
      if (!prev.messages) return `Agent ${resume} has no saved context to resume from.`;

      // Append new user message to saved context and re-run
      const agentId = `agent_${++this.subAgentIdCounter}`;
      prev.messages.push({ role: 'user', content: prompt });
      const resumeResult = await this._runSubAgentWithMessages(
        agentId, prev.messages, prev.agentType || agentType, maxTurns, model || prev.model
      );
      return resumeResult;
    }

    const agentId = `agent_${++this.subAgentIdCounter}`;

    // Background execution: run in a separate async context
    if (runInBackground) {
      this._backgroundAgents = this._backgroundAgents || new Map();
      const agentPromise = this._runSubAgent(agentId, prompt, agentType, maxTurns, model);
      this._backgroundAgents.set(agentId, {
        promise: agentPromise, status: 'running', result: null,
        agentType, model, messages: null, startedAt: new Date().toISOString(),
      });
      agentPromise.then(result => {
        const entry = this._backgroundAgents.get(agentId);
        if (entry) { entry.status = 'done'; entry.result = result; }
      }).catch(err => {
        const entry = this._backgroundAgents.get(agentId);
        if (entry) { entry.status = 'error'; entry.result = err.message; }
      });
      return `Background agent ${agentId} started (${agentType}). Use /agents or getBackgroundAgentResult("${agentId}") to check.`;
    }

    return this._runSubAgent(agentId, prompt, agentType, maxTurns, model);
  }

  async _runSubAgent(agentId, prompt, agentType, maxTurns, modelOverride) {
    const subMessages = [{ role: 'user', content: prompt }];
    return this._runSubAgentWithMessages(agentId, subMessages, agentType, maxTurns, modelOverride);
  }

  async _runSubAgentWithMessages(agentId, subMessages, agentType, maxTurns, modelOverride) {
    this.cli?.print?.(`  ⊕ SubAgent(${agentType}) started: ${(subMessages[subMessages.length - 1]?.content || '').substring(0, 60)}...`);

    const subSystemPrompt = this.promptAssembler.buildSubAgentPrompt(agentType);
    const subTools = this.getSubAgentTools(agentType);
    const subModel = modelOverride || this.config.model;

    let turns = 0;
    while (turns < maxTurns) {
      turns++;

      try {
        const params = {
          model: subModel,
          max_tokens: agentType === 'general' ? 16000 : 8000,
          system: subSystemPrompt,
          tools: subTools,
          messages: subMessages,
        };

        const response = await withRetry(
          () => this.client.messages.create(params),
          { maxRetries: 2 }
        );

        let hasToolUse = false;
        let textOutput = '';

        for (const block of response.content) {
          if (block.type === 'text') textOutput += block.text;
          if (block.type === 'tool_use') hasToolUse = true;
        }

        subMessages.push({ role: 'assistant', content: response.content });

        if (!hasToolUse || response.stop_reason === 'end_turn') {
          this.cli?.print?.(`  ⊖ SubAgent(${agentType}) done (${turns} turns)`);
          // Save messages for potential resume
          this._backgroundAgents = this._backgroundAgents || new Map();
          const existing = this._backgroundAgents.get(agentId);
          if (existing) {
            existing.messages = subMessages;
          } else {
            this._backgroundAgents.set(agentId, {
              status: 'done', result: textOutput, messages: subMessages,
              agentType, model: subModel, startedAt: new Date().toISOString(),
            });
          }
          return textOutput || 'Sub-agent completed without text output.';
        }

        // Execute sub-agent tool calls in parallel
        // All agent types benefit from parallel execution:
        // - explore/plan: read-only tools are inherently safe to parallelize
        // - general/generate: independent tool calls run concurrently
        const toolBlocks = response.content.filter(b => b.type === 'tool_use');
        const results = await Promise.all(toolBlocks.map(async block => {
          const { result, error } = await this.toolRegistry.execute(block.name, block.input);
          return { id: block.id, output: error ? `Error: ${error}` : (typeof result === 'string' ? result : JSON.stringify(result)) };
        }));
        subMessages.push({
          role: 'user',
          content: results.map(r => ({ type: 'tool_result', tool_use_id: r.id, content: r.output })),
        });
      } catch (err) {
        return `Sub-agent error: ${err.message}`;
      }
    }
    return 'Sub-agent reached max turns.';
  }

  /** Resolve a pending AskUserQuestion (called from server when user answers) */
  resolveUserAnswer(answer) {
    if (this._pendingQuestion) {
      this._pendingQuestion.resolve(answer);
    }
  }

  /** Get background agent result */
  getBackgroundAgentResult(agentId) {
    const entry = this._backgroundAgents?.get(agentId);
    if (!entry) return { status: 'not_found' };
    return { status: entry.status, result: entry.result, agentType: entry.agentType, startedAt: entry.startedAt };
  }

  /** List all background agents with their status */
  listBackgroundAgents() {
    if (!this._backgroundAgents || this._backgroundAgents.size === 0) return [];
    return [...this._backgroundAgents.entries()].map(([id, entry]) => ({
      id,
      status: entry.status,
      agentType: entry.agentType || 'unknown',
      startedAt: entry.startedAt || null,
      hasResult: entry.result !== null,
      resumable: entry.status !== 'running' && !!entry.messages,
      resultPreview: entry.result ? String(entry.result).substring(0, 120) : null,
    }));
  }

  getSubAgentTools(agentType) {
    const readOnlyTools = ['ReadFile', 'Grep', 'Glob', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLImpact'];
    const allToolSchemas = this.toolRegistry.getToolSchemas();

    if (agentType === 'explore' || agentType === 'plan') {
      return allToolSchemas.filter(t => readOnlyTools.includes(t.name));
    }
    // general-purpose and generate get all tools
    return allToolSchemas;
  }

  // ========== Plan Mode ==========

  /** Read-only tools allowed during plan mode exploration */
  static PLAN_MODE_TOOLS = new Set([
    'ReadFile', 'Grep', 'Glob', 'VLValidate', 'VLMetadata', 'VLSymbols',
    'VLImpact', 'VLSyntaxRef', 'TodoWrite', 'SubAgent', 'AskUserQuestion',
  ]);

  /** Enter plan mode: LLM explores and designs before implementing */
  enterPlanMode(cb) {
    this.planMode = true;
    this.planContent = null;
    if (cb) {
      cb.onPlanMode?.({ phase: 'enter' });
    } else {
      this.cli.print(chalk_dim('  📋 Entered plan mode. Describe what you want to build.'));
    }
  }

  /** Exit plan mode */
  exitPlanMode(cb) {
    this.planMode = false;
    const plan = this.planContent;
    this.planContent = null;
    if (cb) {
      cb.onPlanMode?.({ phase: 'exit' });
    }
    return plan;
  }

  /** Handle user input during plan mode (CLI) */
  async handlePlanMode(userInput) {
    if (userInput.trim() === '/approve') {
      const plan = this.exitPlanMode();
      this.cli.print(chalk_dim('  ✓ Plan approved. Implementing...'));
      await this.processUserMessage(`Implement the following plan:\n\n${plan}`);
      return;
    }
    if (userInput.trim() === '/cancel') {
      this.exitPlanMode();
      this.cli.print(chalk_dim('  ✗ Plan cancelled.'));
      return;
    }

    const contentBlocks = [
      { type: 'text', text: `<system-reminder>\nYou are in PLAN MODE. Do NOT make any changes to files.\nExplore the codebase, analyze the request, and produce a detailed implementation plan.\nThe user will type /approve to accept or /cancel to abort.\n</system-reminder>` },
      { type: 'text', text: userInput },
    ];

    this.contextManager.addUserMessage(contentBlocks);

    // Restrict tools during plan mode exploration
    const savedFilter = this.toolRegistry._skillFilter;
    this.toolRegistry._skillFilter = [...AgentOrchestrator.PLAN_MODE_TOOLS];
    try {
      await this.agentLoop();
    } finally {
      this.toolRegistry._skillFilter = savedFilter;
    }

    // Capture the last assistant response as the plan
    const messages = this.contextManager.getMessages();
    const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
    if (lastAssistant) {
      const text = Array.isArray(lastAssistant.content)
        ? lastAssistant.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
        : lastAssistant.content;
      this.planContent = text;
    }

    this.cli.print(chalk_dim('\n  Type /approve to implement this plan, or /cancel to abort.'));
  }

  /** Handle user input during plan mode (Web/Streaming) */
  async handlePlanModeStreaming(userInput, cb, options = {}) {
    this._signal = options.signal || null;

    try {
      // Approve: exit plan mode, implement the plan
      if (userInput.trim() === '/approve' || userInput.trim().toLowerCase() === 'approve') {
        const plan = this.exitPlanMode(cb);
        if (!plan) {
          cb.onText?.('No plan to approve. Plan mode exited.');
          cb.onDone?.();
          return;
        }
        // Feed the plan as implementation context (with full tool access)
        await this.processUserMessageStreaming(
          `Implement the following approved plan:\n\n${plan}`,
          cb, options
        );
        return;
      }

      // Cancel: exit plan mode
      if (userInput.trim() === '/cancel' || userInput.trim().toLowerCase() === 'cancel') {
        this.exitPlanMode(cb);
        cb.onText?.('Plan cancelled.');
        cb.onDone?.();
        return;
      }

      // Exploring: inject plan mode instruction, restrict tools
      const contentBlocks = [
        { type: 'text', text: `<system-reminder>\nYou are in PLAN MODE (read-only exploration). Do NOT make any changes to files.\nExplore the codebase, analyze the request, and produce a detailed implementation plan.\nOnly read-only tools are available. WriteFile, EditFile, and Bash are disabled.\nWhen your plan is ready, present it clearly. The user will click Approve or Cancel.\n</system-reminder>` },
        { type: 'text', text: userInput },
      ];

      // Smart Context in plan mode
      if (this.smartContext) {
        const relevant = this.smartContext.discoverRelevantFiles(userInput);
        if (relevant.length > 0) {
          const contextBlock = await this.smartContext.loadContext(relevant, userInput.length);
          if (contextBlock) contentBlocks.push({ type: 'text', text: contextBlock });
        }
      }

      // @mentions
      if (options.mentions?.length) {
        for (const mention of options.mentions) {
          const file = this.projectContext.getAllFiles().find(
            f => f.name === mention || f.path.endsWith(mention)
          );
          if (file) {
            try {
              const { result } = await this.toolRegistry.execute('ReadFile', { file_path: file.path });
              contentBlocks.push({ type: 'text', text: `<file-context path="${file.path}">\n${result}\n</file-context>` });
            } catch {}
          }
        }
      }

      this.contextManager.addUserMessage(contentBlocks);

      // Restrict to read-only tools during plan exploration
      const savedFilter = this.toolRegistry._skillFilter;
      this.toolRegistry._skillFilter = [...AgentOrchestrator.PLAN_MODE_TOOLS];
      try {
        await this.agentLoopStreaming(cb);
      } finally {
        this.toolRegistry._skillFilter = savedFilter;
      }

      // Capture plan from last assistant response
      const messages = this.contextManager.getMessages();
      const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
      if (lastAssistant) {
        const text = Array.isArray(lastAssistant.content)
          ? lastAssistant.content.filter(b => b.type === 'text').map(b => b.text).join('\n')
          : lastAssistant.content;
        this.planContent = text;
      }

      // Notify frontend that plan is ready for approval
      cb.onPlanMode?.({ phase: 'ready', plan: this.planContent });
      cb.onDone?.();
    } catch (err) {
      if (!this._signal?.aborted) cb.onError?.(err.message);
    } finally {
      this._signal = null;
      this._activeStream = null;
    }
  }

  // ========== Compile-Fix Loop ==========

  /**
   * Automated compile → analyze errors → fix → recompile loop.
   * Max 3 rounds. Sends progress via SSE callbacks.
   */
  async compileFixLoop(cb, maxRounds = 3) {
    for (let round = 1; round <= maxRounds; round++) {
      cb.onText?.(`\n**Compile-Fix Round ${round}/${maxRounds}**\n`);
      cb.onPlanMode?.({ phase: 'compile_fix', round, maxRounds });

      // 1. Compile
      const { result: compileResult, error: compileError } = await this.toolRegistry.execute('VLCompile', {});
      const compileOutput = compileError ? `Error: ${compileError}` : (typeof compileResult === 'string' ? compileResult : JSON.stringify(compileResult));
      cb.onToolResult?.('VLCompile', compileOutput.substring(0, 300));

      let parsedCompile = null;
      try { parsedCompile = typeof compileResult === 'string' ? JSON.parse(compileResult) : compileResult; } catch {}

      // 2. Check for errors from the persisted compile report first.
      const report = this._readLastCompileReport();
      const errList = Array.isArray(report?.errList) ? report.errList : [];
      const warningCount = Number(report?.warningCount || parsedCompile?.warningCount || 0);

      if (errList.length === 0) {
        if (parsedCompile?.success === false && (parsedCompile?.error || compileError)) {
          const reason = parsedCompile?.error || compileError || 'Unknown compile failure';
          cb.onText?.(`\nCompile step failed before producing actionable diagnostics: ${reason}\n`);
          return { success: false, rounds: round, error: reason };
        }
        const warningNote = warningCount > 0 ? ` (${warningCount} warning(s))` : '';
        cb.onText?.(`\nCompilation successful${warningNote}. No hard errors.\n`);
        return { success: true, rounds: round };
      }

      cb.onText?.(`\nFound ${errList.length} error(s). Analyzing and fixing...\n`);
      if (!this.client) {
        cb.onText?.('\nLLM provider unavailable. Compile errors were collected but cannot be auto-fixed.\n');
        return { success: false, rounds: round, error: 'LLM provider unavailable' };
      }

      // 3. Feed errors to LLM for analysis and fix
      const errorContext = this._formatCompileErrors(errList, 12);
      const fixPrompt = `The VL project has ${errList.length} real compile error(s) from the parsevl compiler. Fix them using the latest compile report in .vl-code/last-compile.json.

Errors:
${errorContext}

Rules:
1. Start from the compiler diagnostics above and prioritize those exact files.
2. Read the affected files before editing them.
3. Use VLSyntaxRef whenever the error suggests uncertain VL syntax.
4. Apply minimal edits with EditFile. Do not rewrite unrelated files.
5. Do not use VLAdjust for compiler-error repair.
6. After edits, stop so the next round can recompile and verify.`;

      // Use streaming agent loop for the fix
      const contentBlocks = [{ type: 'text', text: fixPrompt }];
      this.contextManager.addUserMessage(contentBlocks);

      const savedFilter = this.toolRegistry._skillFilter;
      this.toolRegistry._skillFilter = ['ReadFile', 'EditFile', 'VLSyntaxRef', 'VLValidate', 'Grep', 'Glob', 'TodoWrite'];
      try {
        await this.agentLoopStreaming(cb);
      } finally {
        this.toolRegistry._skillFilter = savedFilter;
      }
    }

    // If we get here, max rounds reached
    cb.onText?.(`\nReached max ${maxRounds} rounds. Some errors may remain.\n`);
    return { success: false, rounds: maxRounds };
  }

  // ========== Session Persistence ==========

  /** Save current session state (call periodically or on important events) */
  async saveSession() {
    if (!this.sessionManager) return;

    // Build a summary of current conversation
    const messages = this.contextManager.getMessages();
    const recentText = messages.slice(-6).map(m => {
      const text = typeof m.content === 'string' ? m.content :
        Array.isArray(m.content) ? m.content.map(b => b.text || b.content || '').join('\n') : '';
      return `[${m.role}]: ${text.substring(0, 300)}`;
    }).join('\n');

    await this.sessionManager.saveSummary(recentText);
    await this.sessionManager.saveTodos(this.todos);
  }

  // ========== Skill System ==========

  /** Register built-in skills (slash commands) */
  _registerBuiltinSkills() {
    this.skills.set('compile', {
      name: 'compile',
      description: 'Compile the VL project and get preview URLs',
      tools: ['VLCompile', 'ReadFile', 'EditFile', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Compile the VL project NOW by calling VLCompile. Do NOT analyze, validate, or read files first — just compile directly. After compilation, report the results (preview URLs, errors). If there are compile errors, read the problematic files and fix them, then recompile.`,
    });

    this.skills.set('generate', {
      name: 'generate',
      description: 'Generate a VL project from a description',
      tools: ['VLGenerate', 'ReadFile', 'WriteFile', 'Glob', 'VLValidate', 'VLMetadata', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Generate a VL project by calling VLGenerate with action "generate" and userRequest set to the user's requirement. Do this NOW as your first action — do NOT try to write files manually. If no workspace is open, infer a concise PascalCase English projectName and pass it explicitly.

User requirement: ${args || 'a simple app'}

After VLGenerate starts:
1. Poll VLGenerate with action "status" until active=false. Do not use Bash sleep loops when the status tool can tell you the workflow state.
2. Compile immediately with VLCompile.
3. If compilation succeeds, run AutoTestPipeline with action "generate" and then action "run".
4. If compilation fails, fix the compile errors before proceeding.
5. Report the final generation, compile, and test status.`,
    });

    this.skills.set('validate-all', {
      name: 'validate-all',
      description: 'Validate all VL files and fix issues',
      tools: ['ReadFile', 'Glob', 'VLValidate', 'VLAutoFix', 'VLMetadata', 'VLSyntaxRef', 'TodoWrite'],
      prompt: () => `Run VLValidate on all VL files in the project. Report any issues found. Then run VLAutoFix to automatically fix what can be fixed. Report the final status.`,
    });

    this.skills.set('blueprint', {
      name: 'blueprint',
      description: 'Analyze or regenerate blueprint files',
      tools: ['ReadFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'TodoWrite'],
      prompt: (args) => `Analyze the current VL project blueprint files (PRD.json, ServiceMap.json, UIMap.json). ${args || 'Check consistency across all three files, verify valueDomains match, and report any discrepancies. Suggest fixes for any issues found.'}`,
    });

    this.skills.set('refactor', {
      name: 'refactor',
      description: 'Refactor VL code with impact analysis',
      tools: ['ReadFile', 'EditFile', 'Glob', 'Grep', 'VLValidate', 'VLSymbols', 'VLImpact', 'VLEditSection', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Perform a refactoring analysis on the VL project. ${args || 'Identify code smells, unused symbols, inconsistent naming, and opportunities for improvement.'}. Use VLImpact to check what would break. Present a plan before making any changes.`,
    });

    this.skills.set('sync', {
      name: 'sync',
      description: 'Sync code with blueprints',
      tools: ['ReadFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLImpact', 'TodoWrite'],
      prompt: (args) => `Analyze the current VL code and blueprint files. Find any discrepancies where the code doesn't match the blueprints (missing services, missing components, wrong types). ${args || 'Generate a report of all inconsistencies and propose fixes.'}`,
    });

    this.skills.set('deploy', {
      name: 'deploy',
      description: 'Deploy project to VL platform',
      tools: ['ReadFile', 'Glob', 'VLParse', 'WorkspaceAPI', 'Bash', 'TodoWrite'],
      prompt: (args) => `Deploy the current VL project to the VL platform. Use VLParse to package and upload all VL files. ${args || 'Report the deployment status and preview URL.'}`,
    });

    this.skills.set('metadata', {
      name: 'metadata',
      description: 'Extract or update ProjectMeta.json (single source of truth)',
      tools: ['ReadFile', 'WriteFile', 'Glob', 'Grep', 'VLMetadata', 'VLSymbols', 'TodoWrite'],
      prompt: (args) => `Extract complete project metadata into .vl-code/ProjectMeta.json. ${args || 'Scan ALL VL files, extract all apps, pages, sections, components, services, virtual tables, and database tables. The ProjectMeta.json is the SINGLE SOURCE OF TRUTH — it must accurately reflect every symbol in the codebase. Report any discrepancies found.'}`,
    });

    this.skills.set('debug', {
      name: 'debug',
      description: 'Debug VL files — find and fix issues (with browser inspection)',
      tools: ['ReadFile', 'EditFile', 'Glob', 'Grep', 'VLValidate', 'VLAutoFix', 'VLSymbols', 'VLImpact', 'VLEditSection', 'VLSyntaxRef', 'BrowserInspect', 'TodoWrite'],
      prompt: (args) => `Debug the VL project. ${args || 'Diagnose compile errors, structural VL issues, and runtime/browser issues.'}

Workflow:
1. Compile first with VLCompile unless the user explicitly asks for runtime-only browser inspection.
2. If compilation reports hard errors, prioritize those compiler errors first. Read the affected files, verify syntax with VLSyntaxRef, fix with EditFile, then recompile.
3. Only after compilation succeeds should you use BrowserInspect to check runtime UI issues, screenshots, or console errors.
4. Report the root cause, the files changed, and whether compilation/runtime verification passed.`,
    });

    this.skills.set('review', {
      name: 'review',
      description: 'Code review — check quality and suggest improvements',
      tools: ['ReadFile', 'Glob', 'Grep', 'VLValidate', 'VLSymbols', 'VLImpact', 'TodoWrite'],
      prompt: (args) => `Review the VL project code. ${args || 'Check all VL files for: syntax compliance, naming conventions, unused symbols, missing cross-references, inconsistent types, and potential runtime issues. Provide a structured review with severity levels (critical/warning/info).'}`,
    });

    this.skills.set('plan', {
      name: 'plan',
      description: 'Plan changes before implementing — analyze impact and propose steps',
      tools: ['ReadFile', 'Glob', 'Grep', 'VLSymbols', 'VLImpact', 'VLMetadata', 'TodoWrite'],
      prompt: (args) => `Plan the following change: ${args || 'Analyze the project and suggest what needs to change.'}. Use VLImpact to check what files would be affected. Use VLSymbols to trace dependencies. Create a detailed step-by-step plan with impact analysis. Do NOT make any changes — only plan.`,
    });

    this.skills.set('add-page', {
      name: 'add-page',
      description: 'Add a new page/section to the project',
      tools: ['ReadFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Add a new page/section to the VL project: ${args || 'a new page'}. Steps: 1) Read ProjectMeta.json. 2) Add the new page entry. 3) Create the .sc section file. 4) Create any new .cp components needed. 5) Create any new .vs services needed. 6) Update the .vx app file to route to the new section. 7) Validate all new/modified files. 8) Update ProjectMeta.json.`,
    });

    this.skills.set('add-service', {
      name: 'add-service',
      description: 'Add a new backend service domain',
      tools: ['ReadFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Add a new service domain to the VL project: ${args || 'a new service'}. Steps: 1) Read ProjectMeta.json. 2) Create the .vs service domain file with proper SERVICE declarations. 3) Create .vdb database table if needed. 4) Update ProjectMeta.json. 5) Validate all new files.`,
    });

    this.skills.set('theme', {
      name: 'theme',
      description: 'Update or regenerate theme tokens',
      tools: ['ReadFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'TodoWrite'],
      prompt: (args) => `Work on the VL project theme. ${args || 'Read the current .vth theme file, analyze the design tokens, and suggest improvements for consistency and completeness. Check that all referenced tokens in .sc and .cp files are defined in the theme.'}`,
    });

    this.skills.set('test', {
      name: 'test',
      description: 'Test VL components in compiled preview using vlid:/data-vl-id targeting (VL 3.5)',
      tools: ['VLComponentTest', 'BrowserInspect', 'ReadFile', 'Glob', 'Grep', 'VLSymbols', 'VLMetadata', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Test VL components in the compiled preview app. ${args || 'Use VLComponentTest to: 1) Open the preview URL. 2) Run listIds to discover all VL component IDs (data-vl-id attributes — use vlid:xxx selectors). 3) Take a screenshot. 4) Click and interact with components using vlid:componentId selectors (e.g. vlid:loginBtn, vlid:submitButton). 5) Assert expected elements visible. Report test results with pass/fail status.'}`,
    });

    this.skills.set('compile-fix', {
      name: 'compile-fix',
      description: 'Compile, auto-detect errors, fix them, and recompile (up to 3 rounds)',
      tools: ['VLCompile', 'ReadFile', 'EditFile', 'VLSyntaxRef', 'VLValidate', 'Grep', 'Glob', 'TodoWrite'],
      // Special: handled via compileFixLoop instead of normal skill flow
      prompt: (args) => `__COMPILE_FIX_LOOP__`,
      isLoop: true,
    });

    this.skills.set('fix', {
      name: 'fix',
      description: 'Alias of /compile-fix: compile, repair hard errors, and verify by recompiling',
      tools: ['VLCompile', 'ReadFile', 'EditFile', 'VLSyntaxRef', 'VLValidate', 'Grep', 'Glob', 'TodoWrite'],
      prompt: () => `__COMPILE_FIX_LOOP__`,
      isLoop: true,
    });

    this.skills.set('design-flow', {
      name: 'design-flow',
      description: 'Design a workflow DAG for a task (visual, non-linear plan)',
      tools: ['ReadFile', 'Glob', 'Grep', 'VLSymbols', 'VLImpact', 'VLMetadata', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Design a VL Workflow DAG for this task: ${args || 'Analyze the project and create a workflow.'}

You MUST output a valid VL Workflow JSON (version "3.16") wrapped in a \`\`\`json code fence.

1. Analyze the user's request and current project state
2. Determine what steps are needed
3. Design a DAG with proper dependencies

Node types:
- LLM_NNN_Name: Call Claude with messages, output to $vars or /files
- Tool_NNN_Name: Call a local VLCode tool from the workflow registry
- Set_NNN_Name: Set variables
- Write_NNN_Name: Write files
- Branch_NNN_Name: Conditional branching (cases array: [[condition, targetId], ...])
- Loop_NNN_Name: Iterate over array (mode: "serial" or "parallel", children)
- Fork_NNN_Name: Run children in parallel
- Pause_NNN_Name: Wait for developer review (resumeResultTarget: "$varName")
- Stop_NNN_Name: Terminal node

Structure:
{
  "version": "3.16",
  "name": "descriptive_name",
  "registry": { "params": [], "vars": [], "docs": {} },
  "steps": [{ "id": "LLM_010_Name", "meta": { "title": "Human title" }, "in": {...}, "out": {...}, "next": "..." }]
}

Include Pause nodes where developer review would be valuable.
After the JSON, briefly explain the workflow structure.`,
    });

    this.skills.set('autotest', {
      name: 'autotest',
      description: 'Run full automated test pipeline: generate test cases, execute with Playwright, evaluate, auto-fix',
      tools: ['AutoTestPipeline', 'VLCompile', 'ReadFile', 'WriteFile', 'EditFile', 'Glob', 'VLMetadata', 'TodoWrite'],
      prompt: (args) => `Run the full automated test pipeline for this VL project.
${args ? 'User request: ' + args : ''}

Follow these steps using the AutoTestPipeline tool:

1. COMPILE: First compile the project using VLCompile to get preview URLs. Skip only if a valid preview URL already exists for the current workspace.
2. GENERATE: Call AutoTestPipeline with action "generate" to generate test cases from project metadata.
   Report the generated test cases (count, names, priorities).
3. RUN: Call AutoTestPipeline with action "run" to execute all test cases with Playwright.
   The browser will open visibly so the user can watch. Report pass/fail results.
4. If there are FAILURES:
   a. Call AutoTestPipeline with action "fix" to analyze failure causes.
   b. Review the fix suggestions. If there are test-level fixes, note them.
   c. If there are VL source fixes, call AutoTestPipeline with action "fixVL" to get specific code changes.
   d. Apply VL source fixes using EditFile, then recompile with VLCompile.
   e. Re-run failed tests: call AutoTestPipeline with action "run" and caseIds of failed tests.
   f. Repeat up to 3 times.
5. REPORT: Call AutoTestPipeline with action "report" for a final summary.

Use TodoWrite to track progress through each phase.`,
    });

    this.skills.set('test-fix', {
      name: 'test-fix',
      description: 'Fix failed automated test cases and VL source issues',
      tools: ['AutoTestPipeline', 'VLCompile', 'ReadFile', 'EditFile', 'WriteFile', 'Glob', 'Grep', 'VLAutoFix', 'VLSyntaxRef', 'VLSymbols', 'VLMetadata', 'TodoWrite'],
      prompt: (args) => `Fix test failures from the last AutoTest run.
${args ? 'Focus: ' + args : ''}

1. Call AutoTestPipeline with action "status" to see current test results.
2. If no results exist, read .vl-code/autotest-results.json for saved results.
3. Call AutoTestPipeline with action "fix" to analyze all failures.
4. For test-level issues (wrong selectors, timing): note them for the user.
5. For VL source issues: call AutoTestPipeline with action "fixVL", then apply fixes with EditFile.
6. Recompile with VLCompile after source modifications.
7. Re-run tests: call AutoTestPipeline with action "run" to verify fixes.
8. Report final status.`,
    });

    this.skills.set('add-feature', {
      name: 'add-feature',
      description: 'Add or modify a feature in an existing page (button, form, chart, dialog, etc.)',
      tools: ['VLAdjust', 'ReadFile', 'EditFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Add or modify a feature in an existing VL project page.

Feature request: ${args || 'Add a feature to an existing page'}

Call VLAdjust with action "adjust", workflow "add-feature", and changeRequest set to the feature description. The add-feature workflow will:
1. Plan which existing files need modification
2. Pause for developer review of the plan
3. Edit affected sections, services, and create new components if needed
4. Validate all changes

After the workflow completes:
1. Verify the changes by reading modified files.
2. Compile with VLCompile.
3. If compile succeeds, run AutoTestPipeline generate + run.`,
    });

    this.skills.set('modify-data', {
      name: 'modify-data',
      description: 'Add or modify database fields/tables with automatic cascade to services and sections',
      tools: ['VLAdjust', 'ReadFile', 'EditFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Modify the data model of an existing VL project.

Data change request: ${args || 'Add a field to the database'}

Call VLAdjust with action "adjust", workflow "modify-data", and changeRequest set to the data change description. The modify-data workflow will:
1. Analyze the data change and trace cascade impact (DB → Services → Sections)
2. Pause for developer review of the cascade plan
3. Apply database changes first
4. Cascade to affected services and sections in parallel
5. Update ProjectMeta

After the workflow:
1. Verify the cascade by reading affected files.
2. Compile with VLCompile.
3. If compile succeeds, run AutoTestPipeline generate + run.`,
    });

    this.skills.set('batch-adjust', {
      name: 'batch-adjust',
      description: 'Apply multiple small adjustments in one pass across different files',
      tools: ['VLAdjust', 'ReadFile', 'EditFile', 'WriteFile', 'Glob', 'Grep', 'VLValidate', 'VLMetadata', 'VLSymbols', 'VLSyntaxRef', 'TodoWrite'],
      prompt: (args) => `Apply multiple adjustments to an existing VL project in one pass.

Batch request: ${args || 'Multiple changes'}

Call VLAdjust with action "adjust", workflow "batch", and changeRequest set to the full description of all changes. The batch workflow will:
1. Decompose the request into atomic, independent changes
2. Pause for developer review of the change list
3. Execute each change serially
4. Validate all modified files
5. Update ProjectMeta

After the workflow:
1. Verify the results.
2. Compile with VLCompile.
3. If compile succeeds, run AutoTestPipeline generate + run.`,
    });
  }

  /**
   * Execute a skill by name.
   * If the skill defines a `tools` array, only those tools are available during execution.
   */
  async executeSkill(skillName, args, callbacks) {
    const skill = this.skills.get(skillName);
    if (!skill) {
      const available = [...this.skills.keys()].map(k => `/${k}`).join(', ');
      return `Unknown skill: /${skillName}. Available skills: ${available}`;
    }
    // Special loop-based skills (compile-fix)
    if (skill.isLoop) {
      if (callbacks) {
        const result = await this.compileFixLoop(callbacks);
        callbacks.onDone?.();
        return result;
      }
      return null;
    }

    const expandedPrompt = skill.prompt(args);

    // Apply tool restriction if skill defines allowed tools
    let savedFilter = null;
    if (skill.tools) {
      savedFilter = this.toolRegistry._skillFilter;
      this.toolRegistry._skillFilter = skill.tools;
    }

    try {
      if (callbacks) {
        await this.processUserMessageStreaming(expandedPrompt, callbacks, { _fromSkill: true });
      } else {
        await this.processUserMessage(expandedPrompt);
      }
    } finally {
      // Restore full tool access
      if (savedFilter !== null) {
        this.toolRegistry._skillFilter = savedFilter;
      } else if (skill.tools) {
        this.toolRegistry._skillFilter = null;
      }
    }
    return null;
  }

  /** Get all registered skills (for UI display) */
  getSkills() {
    return [...this.skills.entries()].map(([name, skill]) => ({
      name, description: skill.description, tools: skill.tools || null,
    }));
  }

  // ========== Agent Resume ==========

  /** Save agent state for resume (called when context overflows or user disconnects) */
  getResumeState() {
    return {
      todos: this.todos,
      turnsSinceTodo: this.turnsSinceTodo,
      planMode: this.planMode,
      planContent: this.planContent,
      messageCount: this.contextManager.messages.length,
      turnCount: this.contextManager.turnCount,
      timestamp: new Date().toISOString(),
    };
  }

  /** Restore agent state from a saved resume point */
  restoreFromResume(state) {
    if (!state) return;
    this.todos = state.todos || [];
    this.turnsSinceTodo = state.turnsSinceTodo || 0;
    this.planMode = state.planMode || false;
    this.planContent = state.planContent || null;
  }

  // ========== Helper Methods ==========

  /**
   * Detect workflow intent from user message
   * Returns { type, context } if intent found, null otherwise
   */
  detectWorkflowIntent(message) {
    const lower = message.toLowerCase();

    // Generation intent — English + Chinese keywords
    const genKeywordsEN = ['generate', 'create project', 'build app', 'build a', 'create a vl', 'generate vl', 'new project'];
    const genKeywordsCN = ['开发', '生成', '创建', '新建', '搭建', '做一个', '弄一个', '写一个', '帮我做', '设计一个'];
    const isGeneration = genKeywordsEN.some(k => lower.includes(k)) || genKeywordsCN.some(k => message.includes(k));

    if (isGeneration) {
      return {
        type: 'generation',
        context: `The user wants to generate a VL project. You MUST call the VLGenerate tool with action "generate" and pass the user's request as userRequest. Do NOT try to write files manually — the VLGenerate tool runs the full codegen workflow pipeline (Meta → DB → Theme → Services → Components → Sections → Apps) which produces much better results. Call VLGenerate NOW as your first action.`,
      };
    }

    const repairKeywordsEN = ['compile error', 'compilation error', 'build error', 'syntax error', 'fix compile', 'recompile'];
    const repairKeywordsCN = ['编译错误', '编译失败', '构建错误', '语法错误', '修复报错', '重新编译'];
    const isRepair = (repairKeywordsEN.some(k => lower.includes(k)) || repairKeywordsCN.some(k => message.includes(k))) && this.projectContext.isVLProject();

    if (isRepair) {
      return {
        type: 'repair',
        context: `The user wants to repair compile/build errors in an existing VL project. Do NOT call VLAdjust first. Compile with VLCompile, read .vl-code/last-compile.json for the hard errors, fix only the affected files, and recompile until the errors are gone.`,
      };
    }

    // Debug/modify intent (only when project has files)
    const debugKeywordsEN = ['modify', 'add feature', 'fix', 'change', 'update', 'adjust', 'debug', 'refactor', 'add a'];
    const debugKeywordsCN = ['修改', '添加', '增加', '调整', '修复', '改一下', '加一个', '优化'];
    const isDebug = (debugKeywordsEN.some(k => lower.includes(k)) || debugKeywordsCN.some(k => message.includes(k))) && this.projectContext.isVLProject();

    if (isDebug) {
      const hasMeta = fs.existsSync(path.join(this.config.workDir, '.vl-code', 'ProjectMeta.json'));
      return {
        type: 'adjustment',
        context: hasMeta
          ? `The user wants to modify an existing VL project. Use VLAdjust tool (action:"adjust") — it auto-selects the right workflow (add-page, add-service, theme-customize, add-feature, modify-data, batch, or general).`
          : `The user wants to modify an existing VL project but no ProjectMeta.json found. Consider extracting metadata first using VLMetadata tool, then use VLAdjust.`,
      };
    }

    return null;
  }

  /** Extract file references from user message */
  extractFileReferences(message) {
    const refs = [];
    // Match @FileName, FileName.ext, or "path/to/file"
    for (const file of this.projectContext.getAllFiles()) {
      if (message.includes(`@${file.name}`) ||
          message.includes(path.basename(file.path)) ||
          message.includes(file.name)) {
        refs.push(file.path);
      }
    }
    return [...new Set(refs)].slice(0, 5);
  }
}

function summarizeInput(name, input) {
  switch (name) {
    case 'ReadFile': return input.file_path;
    case 'EditFile': return `${input.file_path} (replace)`;
    case 'WriteFile': return input.file_path;
    case 'Bash': return input.command?.substring(0, 60);
    case 'Grep': return `"${input.pattern}" in ${input.path || '.'}`;
    case 'Glob': return input.pattern;
    case 'SubAgent': return `${input.agentType}: ${input.prompt?.substring(0, 40)}...`;
    default: return JSON.stringify(input).substring(0, 60);
  }
}

function chalk_dim(text) {
  // Avoid importing chalk in this module for simplicity
  return `\x1b[2m${text}\x1b[0m`;
}
