import chalk from 'chalk';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as readline from 'readline';
import { createSpinner } from '../utils/logger.js';
import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError, VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE } from '../utils/api.js';
import { AgenticTools, robustifyStreamResponse } from '../utils/tools.js';
import { SessionManager } from '../utils/session.js';
import { BridgeClient, getBridgeClient } from '../utils/bridge-client.js';
import { WorkspaceWatcher } from '../utils/workspace-stream.js';
import { TaskDisplay } from '../utils/task-display.js';
import { ProjectMemoryService } from '../utils/project-memory.js';
import { WorkspaceBrainService } from '../utils/workspace-brain-service.js';
import { buildPersonaOverlay, normalizePersonaMode } from '../utils/persona.js';
import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session-menu.js';
import { V4Command } from './v4.js';
import { renderDynamicHelp } from '../utils/command-menu.js';
import { createLiveOutcome, evaluateExecutorSuccess, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
import { emitDeckEvent, isDeckModeEnabled } from '../utils/deckEvents.js';
import { formatGoaSystemGrounding, splitGoaContextFromInput, } from '../utils/goaEvents.js';
import { inferAgentTaskType as sharedInferAgentTaskType, inferAgentTaskTypeWithContext, isTrivialHtmlPageRequest, resolvePlannerAgentTimeoutMs, resolveWorkflowType, shouldSkipAnalysisRescue, taskRequiresWorkspaceChanges as promptRequiresWorkspaceChanges, taskRequiresWorkspaceChangesWithContext, buildExecutionHints, isAgentContinuePrompt, isAgentRetryPrompt, isBuiltContinuePrompt, isBuiltRetryPrompt, isBuiltWriteConfirmationPrompt, isConfirmationFollowUp, isWritePermissionGrant, } from '../utils/requestIntent.js';
import { resolveAgentRoute } from '../utils/agentRoute.js';
import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
import { isV3StreamKeepaliveEvent } from '../utils/v3-stream-events.js';
// Stream health policy (corrected 2026-07-04 after TestFarm incident with
// Gideon Lenz / C:\vigthoria\Apps\monopoly): agentic tasks have no
// predictable duration, so there is intentionally NO overall wall-clock
// timeout -- DEFAULT_V3_AGENT_TIMEOUT_MS stays disabled (0) by default and
// must never kill an actively-streaming request just because it's "taking
// long". The only legitimate failure signal for SSE/chunked streams is
// silence: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS aborts only when zero bytes
// arrive for a stretch, which is what "the model/agent has gone dark"
// actually looks like on the wire.
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
    // Agentic tasks are open-ended by design — a build/refactor/analysis can
    // legitimately take minutes or hours depending on scope. There must be NO
    // overall wall-clock cap that kills an actively-streaming, healthy request.
    // The only correct failure signal for SSE/streamed responses is silence:
    // see DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS below, which aborts only when zero
    // bytes arrive for a stretch — never based on total elapsed duration.
    const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
    if (!rawValue) {
        return 0; // disabled — no overall duration cap
    }
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
})();
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
    const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
    if (!rawValue) {
        return 60_000; // 60s with zero new stream data => abort/retry
    }
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
})();
const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
    const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS || '300000';
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
})();
/** Block-write agent JSON to VIGTHORIA_AGENT_OUTPUT (Windows KVM fsync path). */
function emitAgentJsonOutput(payload) {
    const text = `${JSON.stringify(payload, null, 2)}\n`;
    console.log(text.trimEnd());
    const outPath = process.env.VIGTHORIA_AGENT_OUTPUT?.trim();
    if (!outPath) {
        return;
    }
    const dir = path.dirname(outPath);
    fs.mkdirSync(dir, { recursive: true });
    const fd = fs.openSync(outPath, 'w');
    try {
        fs.writeSync(fd, text, undefined, 'utf8');
        fs.fsyncSync(fd);
    }
    finally {
        fs.closeSync(fd);
    }
}
export class ChatCommand {
    commandProgram;
    config;
    logger;
    api;
    messages = [];
    tools = null;
    sessionManager;
    projectMemory = null;
    workspaceBrain = null;
    currentSession = null;
    agentMode = false;
    currentProjectPath = process.cwd();
    lastActionableUserInput = '';
    directPromptMode = false;
    directToolContinuationCount = 0;
    currentModel = 'code';
    modelExplicitlySelected = false;
    autoApprove = false;
    personaOverride = null;
    // Phase 5: Agent quality gate — track tool usage for evidence thresholds
    agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
    operatorMode = false;
    workflowTarget = null;
    savePlanToVigFlow = false;
    jsonOutput = false;
    modelGovernanceFallback = null;
    retryPromptSignature = null;
    retryPromptStreak = 0;
    v3SuppressThinkingStream = false;
    v3StreamedAnswerBuffer = '';
    v3StreamedAnswerDisplayed = false;
    v3SeenToolCalls = new Set();
    v3SeenToolResults = new Set();
    v3StartSeen = false;
    lastAgentRoute = null;
    pendingGoaContext = null;
    // Last completed Agent run — used by /retry, /continue, and the final summary block.
    lastAgentRunOutcome = null;
    isJwtExpirationError(error) {
        const candidate = error;
        const message = `${candidate?.message || ''} ${typeof candidate?.details === 'string' ? candidate.details : ''}`.toLowerCase();
        const code = String(candidate?.code || '').toLowerCase();
        return candidate?.status === 401 && (code.includes('token_expired') ||
            code.includes('jwt_expired') ||
            message.includes('jwt expired') ||
            message.includes('token expired') ||
            message.includes('expired token') ||
            message.includes('session expired'));
    }
    isNetworkError(error) {
        const candidate = error;
        const code = String(candidate?.code || candidate?.cause?.code || '').toUpperCase();
        const name = String(candidate?.name || candidate?.cause?.name || '').toLowerCase();
        const message = String(candidate?.message || candidate?.cause?.message || '').toLowerCase();
        return [
            'ECONNRESET',
            'ECONNREFUSED',
            'EHOSTUNREACH',
            'ENETUNREACH',
            'ENOTFOUND',
            'EAI_AGAIN',
            'UND_ERR_CONNECT_TIMEOUT',
            'UND_ERR_HEADERS_TIMEOUT',
            'UND_ERR_BODY_TIMEOUT',
        ].includes(code) ||
            name.includes('fetcherror') ||
            message.includes('network') ||
            message.includes('fetch failed') ||
            message.includes('socket') ||
            message.includes('connection');
    }
    isTimeoutError(error) {
        const candidate = error;
        const code = String(candidate?.code || candidate?.cause?.code || '').toUpperCase();
        const name = String(candidate?.name || candidate?.cause?.name || '').toLowerCase();
        const message = String(candidate?.message || candidate?.cause?.message || '').toLowerCase();
        return code.includes('TIMEOUT') ||
            code === 'ETIMEDOUT' ||
            name.includes('timeout') ||
            name === 'aborterror' ||
            message.includes('timed out') ||
            message.includes('timeout') ||
            message.includes('aborted');
    }
    toUserFacingApiError(error, context) {
        const classified = classifyError(error);
        const status = classified.statusCode || (this.isJwtExpirationError(error) ? 401 : 500);
        if (this.isJwtExpirationError(error) || classified.category === 'auth') {
            return new CLIError('Your Vigthoria session has expired. Run `vigthoria login` to authenticate again.', 'auth', { statusCode: 401 });
        }
        // Preserve structured API classification first (auth/model/network/etc.)
        // so upstream responses are not relabeled by message heuristics.
        if (classified.category === 'timeout') {
            return new CLIError(`${context} timed out. Check your connection and try again.`, 'timeout', { statusCode: status });
        }
        if (classified.category === 'network') {
            return new CLIError(`${context} could not reach the Vigthoria API. Check your network connection and try again.`, 'network', { statusCode: status });
        }
        if (classified.category === 'model_backend') {
            return new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend', { statusCode: status, endpoint: classified.endpoint });
        }
        const message = sanitizeUserFacingErrorText(classified.message || `${context} failed`);
        return new CLIError(message, 'model_backend', { statusCode: status, endpoint: classified.endpoint });
    }
    handleApiError(error, context) {
        const userFacingError = this.toUserFacingApiError(error, context);
        if (!this.jsonOutput) {
            console.error(chalk.red(`${context} failed: ${userFacingError.message}`));
        }
        const original = error && typeof error === 'object' ? error : { message: String(error) };
        propagateError({
            ...original,
            message: userFacingError.message,
            statusCode: userFacingError.statusCode,
            commandName: 'chat',
            endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || context,
            details: {
                ...(original.details && typeof original.details === 'object' ? original.details : {}),
                command: 'chat',
                endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || context,
                context,
            },
        });
    }
    async callApi(context, operation, retries = 1) {
        let lastError;
        for (let attempt = 0; attempt <= retries; attempt += 1) {
            try {
                return await operation();
            }
            catch (error) {
                lastError = error;
                if (this.isJwtExpirationError(error)) {
                    this.handleApiError(error, context);
                }
                if (attempt >= retries || (!this.isNetworkError(error) && !this.isTimeoutError(error))) {
                    this.handleApiError(error, context);
                }
                if (!this.jsonOutput) {
                    const transient = this.isTimeoutError(error) ? 'timeout' : this.isNetworkError(error) ? 'network error' : 'temporary service issue';
                    this.logger.warn(`${context} failed due to ${transient}; retrying (${attempt + 1}/${retries})...`);
                }
                await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
            }
        }
        this.handleApiError(lastError, context);
    }
    callSyncApi(context, operation) {
        try {
            return operation();
        }
        catch (error) {
            this.handleApiError(error, context);
        }
    }
    applyNoAgentGovernance(requestedModel) {
        const requested = String(requestedModel || '').trim().toLowerCase();
        if (!requested || this.agentMode || this.operatorMode) {
            this.modelGovernanceFallback = null;
            return;
        }
        const blocked = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
        if (blocked.has(requested)) {
            const effectiveModel = 'code-35b';
            this.modelGovernanceFallback = {
                requestedModel,
                effectiveModel,
                reason: 'governance-blocked-model',
            };
            this.currentModel = effectiveModel;
            if (!this.jsonOutput) {
                this.logger.warn(`Model ${requestedModel} is not permitted for no-agent chat; using ${effectiveModel}`);
            }
            return;
        }
        this.modelGovernanceFallback = null;
    }
    syncInteractiveModeModel(enabledMode) {
        if (enabledMode === 'agent') {
            if (!this.modelExplicitlySelected || this.currentModel === 'code' || !this.currentModel) {
                this.currentModel = 'agent';
            }
            return;
        }
        if (enabledMode === 'operator') {
            if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || this.currentModel === 'code-9b' || !this.currentModel) {
                this.currentModel = 'code';
            }
            return;
        }
        if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || this.currentModel === 'code-9b' || !this.currentModel) {
            this.currentModel = 'code';
        }
    }
    hasOperatorAccess() {
        return this.config.hasOperatorAccess();
    }
    operatorAccessMessage() {
        const currentPlan = this.config.get('subscription').plan || 'free';
        return `Operator mode requires Pro, Ultra, Enterprise, or admin access. Current plan: ${currentPlan}.`;
    }
    getDefaultChatModel() {
        const preferredModel = String(this.config.get('preferences').defaultModel || '').trim().toLowerCase();
        if (!preferredModel || preferredModel === 'vigthoria-code' || preferredModel === 'vigthoria-agent') {
            return 'code';
        }
        return preferredModel;
    }
    isHardExplicitAgentModelSelection(options) {
        const requestedModel = String(options.model || '').trim().toLowerCase();
        if (!requestedModel) {
            return false;
        }
        if (/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_FORCE_EXPLICIT_AGENT_MODEL || ''))) {
            return true;
        }
        if (options.agent === true || options.operator === true) {
            return !['agent', 'code', 'balanced', 'vigthoria-agent', 'vigthoria-code'].includes(requestedModel);
        }
        return true;
    }
    resolveInitialModel(options) {
        const requestedModel = String(options.model || '').trim();
        if (requestedModel) {
            return options.operator === true && (requestedModel === 'code-8b' || requestedModel === 'code-9b')
                ? 'code'
                : requestedModel;
        }
        if (options.operator === true) {
            return 'code';
        }
        if (options.agent === true) {
            return 'agent';
        }
        return this.getDefaultChatModel();
    }
    isLegacyAgentFallbackAllowed() {
        // Hosted V3 Agent Mode must fail loudly by default so clients do not silently drop into legacy mode.
        // Operators can still opt in during emergency local debugging.
        return process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '1';
    }
    shouldRescueFailedAnalysis() {
        return process.env.VIGTHORIA_DISABLE_ANALYSIS_RESCUE !== '1';
    }
    isDeepAnalysisPrompt(prompt) {
        return /\b(deep|deeper|full|complete|thorough|systematic|architecture|architectural)\s+(analyse|analyze|analysis|review|dive|scan)\b/i.test(prompt)
            || /\b(analyse|analyze|analysis|review)\b[\s\S]{0,40}\b(deep|deeper|full|complete|thorough|systematic|architecture|architectural)\b/i.test(prompt);
    }
    async tryAnalysisLocalRescue(prompt, workspacePath) {
        if (!this.shouldRescueFailedAnalysis() || !this.tools) {
            return false;
        }
        const clientToolErrors = this.api.getClientToolErrors();
        if (clientToolErrors.some((err) => /could not accept the result|client tool bridge failed/i.test(err))) {
            return false;
        }
        console.log('');
        console.log(chalk.yellow('V3 analysis did not produce a grounded answer — retrying with local workspace tools...'));
        console.log(chalk.gray(`Workspace: ${workspacePath}`));
        console.log('');
        const last = this.messages[this.messages.length - 1];
        if (last?.role === 'assistant') {
            this.messages.pop();
        }
        await this.runLocalAgentLoop(prompt);
        return true;
    }
    resolveAgentExecutionPolicy(prompt) {
        const explicitModel = this.modelExplicitlySelected;
        const heavyTask = this.config.isComplexTask(prompt);
        const cloudEligible = this.config.hasCloudAccess();
        const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
        if (explicitModel) {
            return {
                selectedModel: this.currentModel,
                explicitModel: true,
                heavyTask,
                cloudEligible,
                cloudSelected: this.currentModel === 'cloud' || this.currentModel === 'cloud-reason' || this.currentModel === 'ultra',
                routeReason: 'explicit-model-selection',
            };
        }
        if (this.agentMode && ['agent', 'code', 'balanced', 'vigthoria-agent', 'vigthoria-code'].includes(String(this.currentModel || '').toLowerCase())) {
            return {
                selectedModel: 'agent',
                explicitModel: false,
                heavyTask,
                cloudEligible,
                cloudSelected: false,
                routeReason: 'dispatcher-eligible-agent-default',
            };
        }
        if (this.lastAgentRoute?.path === 'template-instant') {
            return {
                selectedModel: 'agent',
                explicitModel: false,
                heavyTask,
                cloudEligible,
                cloudSelected: false,
                routeReason: `template-instant [${this.lastAgentRoute.source}]`,
            };
        }
        if (requiresV3Workflow) {
            return {
                selectedModel: 'agent',
                explicitModel: false,
                heavyTask,
                cloudEligible,
                cloudSelected: false,
                routeReason: 'v3-required-frontend-task',
            };
        }
        if (this.lastAgentRoute?.source === 'balanced-4b' || this.lastAgentRoute?.source === 'merged') {
            return {
                selectedModel: 'agent',
                explicitModel: false,
                heavyTask,
                cloudEligible,
                cloudSelected: false,
                routeReason: `balanced-4b-router → ${this.lastAgentRoute.taskKind}`,
            };
        }
        return {
            selectedModel: 'agent',
            explicitModel: false,
            heavyTask,
            cloudEligible,
            cloudSelected: false,
            routeReason: 'default-v3-agent',
        };
    }
    getMessagesForModel() {
        const messages = [...this.messages];
        const personaOverlay = this.buildActivePersonaOverlay();
        if (personaOverlay && !messages.some((message) => message.role === 'system' && message.content.includes('Optional persona overlay: Wiener Grantler mode.'))) {
            const insertionIndex = messages.findIndex((message) => message.role !== 'system');
            const personaMessage = { role: 'system', content: personaOverlay };
            if (insertionIndex === -1) {
                messages.push(personaMessage);
            }
            else {
                messages.splice(insertionIndex, 0, personaMessage);
            }
        }
        const memoryContexts = [
            this.sessionManager.buildMemoryContext(this.currentSession),
            this.projectMemory?.buildContextForPrompt(this.getLastUserPrompt()) || '',
        ].filter((entry) => entry && entry.trim());
        for (const memoryContext of memoryContexts) {
            const marker = memoryContext.includes('Vigthoria project brain memory.')
                ? 'Vigthoria project brain memory.'
                : 'Session memory summary from earlier conversation turns.';
            const alreadyInjected = messages.some((message) => message.role === 'system' && message.content.includes(marker));
            if (alreadyInjected) {
                continue;
            }
            const insertionIndex = messages.findIndex((message) => message.role !== 'system');
            const memoryMessage = { role: 'system', content: memoryContext };
            if (insertionIndex === -1) {
                messages.push(memoryMessage);
            }
            else {
                messages.splice(insertionIndex, 0, memoryMessage);
            }
        }
        return messages;
    }
    getActivePersonaMode() {
        if (this.personaOverride)
            return this.personaOverride;
        return normalizePersonaMode(this.config.get('persona')) || 'default';
    }
    buildActivePersonaOverlay() {
        return buildPersonaOverlay(this.getActivePersonaMode(), this.getLastUserPrompt());
    }
    isDiagnosticPrompt(prompt) {
        if (promptRequiresWorkspaceChanges(prompt))
            return false;
        return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
    }
    /**
     * Returns true when the prompt is a simple lookup / analysis / read-only
     * question — these should use analysis_only workflow, not full_autonomy.
     */
    isAnalysisLookupPrompt(prompt) {
        if (promptRequiresWorkspaceChanges(prompt))
            return false;
        const trimmed = prompt.trim();
        if (/^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(trimmed)) {
            return true;
        }
        // Mid-conversation accountability / lookup questions (Gideon regression)
        return /\b(where\s+(?:is|are)|what\s+(?:is|are|have|did|was|were)|how\s+(?:do|does|is|are)|what\s+(?:have\s+)?you\s+(?:done|changed|built)|what\s+(?:is|was)\s+(?:your|the)\s+answer)\b/i.test(trimmed);
    }
    extractExplicitLocalPath(prompt) {
        // Try to extract Windows paths (C:\ D:\ etc.)
        // Match drive letter followed by colon and path
        const windowsMatch = prompt.match(/([A-Za-z]:[\\\/][^\s<>"|?*]*)/);
        if (windowsMatch) {
            const candidatePath = windowsMatch[1].replace(/\\/g, '\\').trim();
            try {
                const resolved = require('path').resolve(candidatePath);
                const fs = require('fs');
                if (fs.existsSync(resolved)) {
                    return resolved;
                }
            }
            catch {
                // Path resolution failed
            }
        }
        // Try to extract Unix paths: / followed by any characters until we hit quotes, angle brackets, pipes, or sentence boundaries
        // Supports spaces, special chars, camelCase, etc. (anything a filesystem path might have)
        if (!prompt.includes('http://') && !prompt.includes('https://') && !prompt.includes('ftp://')) {
            // Match / and everything after it until we hit delimiters or common sentence words
            const unixMatch = prompt.match(/(\/(?:[^\s<>"|?*]|\s+(?![^\s<>"|?*]))*)/);
            if (unixMatch) {
                let candidatePath = unixMatch[1].trim();
                // Remove trailing common English words that aren't part of path
                candidatePath = candidatePath.replace(/\s+(and|or|at|in|the|to|for|with|from|by|on)$/i, '').trim();
                // Remove trailing punctuation
                candidatePath = candidatePath.replace(/[.,;!?:)\]]*$/, '').trim();
                if (candidatePath.length > 1) {
                    try {
                        const resolved = require('path').resolve(candidatePath);
                        const fs = require('fs');
                        if (fs.existsSync(resolved)) {
                            return resolved;
                        }
                    }
                    catch {
                        // Path resolution failed
                    }
                }
            }
        }
        return null;
    }
    isUnscopedPromptPathOverrideAllowed() {
        return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS || ''));
    }
    isPathWithinRoot(candidatePath, rootPath) {
        const candidate = path.resolve(candidatePath);
        const root = path.resolve(rootPath);
        const rel = path.relative(root, candidate);
        return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
    }
    getPromptPathAllowedRoots(baseWorkspace) {
        const roots = new Set();
        const addRoot = (rawValue) => {
            const value = String(rawValue || '').trim();
            if (!value)
                return;
            const resolved = path.resolve(value);
            try {
                if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
                    roots.add(resolved);
                }
            }
            catch {
                // ignore invalid or unreadable roots
            }
        };
        addRoot(baseWorkspace);
        addRoot(this.currentProjectPath);
        addRoot(process.cwd());
        addRoot(this.config.get('project')?.rootPath || null);
        const envRootsRaw = String(process.env.VIGTHORIA_ALLOWED_WORKSPACE_ROOTS || '').trim();
        if (envRootsRaw) {
            for (const entry of envRootsRaw.split(path.delimiter)) {
                addRoot(entry);
            }
        }
        return Array.from(roots);
    }
    resolvePromptWorkspacePath(prompt, baseWorkspace) {
        const explicitPath = this.extractExplicitLocalPath(prompt);
        if (!explicitPath) {
            return null;
        }
        const candidate = path.resolve(explicitPath);
        if (this.isUnscopedPromptPathOverrideAllowed()) {
            return candidate;
        }
        const allowedRoots = this.getPromptPathAllowedRoots(baseWorkspace);
        const isAllowed = allowedRoots.some((root) => this.isPathWithinRoot(candidate, root));
        if (isAllowed) {
            return candidate;
        }
        if (!this.jsonOutput) {
            console.log(chalk.yellow(`Ignoring path outside allowed workspace roots: ${candidate}`));
            if (allowedRoots.length > 0) {
                const displayRoots = allowedRoots.map((root) => root.replace(/\\/g, '/')).join(', ');
                console.log(chalk.gray(`Allowed roots: ${displayRoots}`));
            }
            console.log(chalk.gray('To allow unrestricted prompt path overrides, set VIGTHORIA_ALLOW_UNSCOPED_PROMPT_PATHS=1.'));
        }
        return null;
    }
    isBrowserTaskPrompt(prompt) {
        return /(\bbrowser\b|\bchrome\b|\bdevtools\b|\bconsole\b|\bdom\b|network tab|network request|frontend runtime|client-side|client side|rendering|page load|\bwebsocket\b|ui bug|inspect element)/i.test(prompt);
    }
    /**
     * Returns true when a prompt can be answered directly without the full
     * BMAD operator workflow.  Matches analysis/lookup questions as well as
     * short echo/smoke-test prompts that don't require code generation,
     * file scanning, or infrastructure changes.
     */
    isOperatorDirectAnswerable(prompt) {
        const trimmed = prompt.trim();
        // Infrastructure status pings are never repo-grounded analysis.
        if (/^(status(\s+check)?|health(\s+check)?|ping)$/i.test(trimmed)) {
            return true;
        }
        // ── Exclusion guard: any prompt that references code artefacts,
        // file paths, or analysis verbs MUST go through a tool-backed path
        // (BMAD or agent loop), never the toolless direct-answer shortcut.
        const repoGrounded = /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b|\.yaml\b|\.yml\b)/i.test(trimmed)
            || /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(trimmed)
            || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
            || /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(trimmed)
            || /\/[a-zA-Z]/.test(trimmed) // slash-prefixed paths
            || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName
            || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
        if (repoGrounded)
            return false;
        // Only truly trivial prompts are direct-answerable: short echo/smoke
        // prompts and factual questions with no code or repo reference.
        const isTrivialEcho = trimmed.length < 120
            && /^(reply with|say|echo|respond with|return|ping|hello|hi|test|status|what is \d|how much is)\b/i.test(trimmed);
        return isTrivialEcho;
    }
    /**
     * Returns true when the prompt references code artefacts, file paths,
     * or analysis verbs that require tool-backed file reading to answer
     * correctly.  Used to route operator prompts through the agent loop
     * instead of the toolless BMAD/direct-answer path.
     */
    isRepoGroundedPrompt(prompt) {
        const trimmed = prompt.trim();
        return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b)/i.test(trimmed)
            || /\b(file|folder|module|class|function|method|handler|listener|binding|conflict|bug|error)\b/i.test(trimmed)
            || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(trimmed)
            || /\/[a-zA-Z]/.test(trimmed)
            || /[A-Z][a-z]+\.[a-z_][a-zA-Z]+/.test(trimmed) // ClassName.methodName patterns
            || /\b(key\s*code|keydown|keyup|key\s*bind|event\s*listener|event\s*handler|addEventListener|handleKey|onKey)\b/i.test(trimmed);
    }
    inferAgentTaskType(prompt) {
        if (this.isDiagnosticPrompt(prompt))
            return 'debugging';
        if (this.isAnalysisLookupPrompt(prompt))
            return 'analysis';
        return sharedInferAgentTaskType(prompt);
    }
    buildTaskShapingInstructions(prompt) {
        const instructions = [];
        const runtime = this.getRuntimeEnvironmentContext();
        // Platform-aware routing hints
        if (runtime.platform === 'windows') {
            instructions.push('Platform: Windows. Use list_dir, glob, read_file, and the grep tool for searching.', 'The grep tool handles Windows automatically — do not use bash to call grep, findstr, or Select-String manually.', 'Do not use bash for Unix commands (cat, head, tail, awk, sed, wc).', 'Use read_file to inspect file contents instead of shell commands.', 'All file paths use forward slashes internally.');
        }
        else if (runtime.platform === 'macos') {
            instructions.push('Platform: macOS. Prefer list_dir, grep, and read_file tools for deterministic results.', 'Use workspace-relative paths and verify file existence before claiming a file is missing.');
        }
        else if (runtime.platform === 'linux') {
            instructions.push('Platform: Linux. Prefer list_dir, grep, and read_file for grounded repository inspection.', 'Use exact file paths from tool output when reporting findings or fixes.');
        }
        if (this.isAnalysisLookupPrompt(prompt)) {
            instructions.push('Read-only analysis mode is active.', 'Inspect the workspace with tools and produce a grounded report of findings, gaps, and blockers.', 'Do not create, modify, or delete files unless the user explicitly asked for changes.');
        }
        if (this.isDiagnosticPrompt(prompt)) {
            instructions.push('Diagnostic mode is active.', 'Treat this as a debugging task, not a generic code review or feature request.', 'Start with concrete evidence: logs, runtime errors, config, launch files, and exact symbol references.', 'If log files exist, inspect them before proposing fixes.', 'Do not claim a file, definition, asset, or symbol is missing until you verify that with tools.', 'If a prior diagnosis mentioned a missing symbol or YAML entry, re-check the actual files before repeating it.', 'Prefer grep plus read_file around the exact references involved in the failure.', 'Separate your reasoning into: Evidence, Confirmed Cause, and Remaining Hypotheses.', 'Do not suggest speculative fixes when the current evidence contradicts them.', 'CRITICAL GROUNDING RULE: Every key name, variable name, symbol, or identifier you mention in your final answer MUST appear verbatim in the tool output you received. If a key/symbol does NOT appear in tool output, you MUST NOT mention it as involved in any conflict or issue.', 'CROSS-FILE ATTRIBUTION: When reporting conflicts between two files, a key/symbol is conflicting ONLY if it appears in BOTH files. Read each file carefully and list only the exact keys that appear in the relevant handler/function of EACH file. Do not assume that because one file handles a key, the other file does too.', 'When reporting conflicts between files, cite the exact file name, line number, and the exact string/key from the tool output. Do not paraphrase or substitute key names.', 'Before concluding, re-check: (1) does every key/symbol in my answer actually appear in the evidence I gathered? (2) for each claimed conflict, did I verify the key appears in BOTH files? If not, correct your answer.');
        }
        if (this.isBrowserTaskPrompt(prompt)) {
            instructions.push('Browser-debug mode is active.', 'Prefer concrete browser evidence such as console errors, network failures, DOM state, and websocket behavior.', 'Use the DevTools Bridge only when the user has explicitly enabled it for this run; otherwise ask for permission before requiring local browser tooling.');
        }
        if (instructions.length === 0) {
            return '';
        }
        return instructions.join('\n');
    }
    buildExecutionPrompt(prompt) {
        const shaping = this.buildTaskShapingInstructions(prompt);
        return shaping ? `${prompt}\n\n${shaping}` : prompt;
    }
    isProjectBrainRuntimeDisabled() {
        return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_BRAIN || process.env.VIGTHORIA_BRAIN_DISABLED || ''));
    }
    getWorkspaceBrainService() {
        if (this.isProjectBrainRuntimeDisabled()) {
            return null;
        }
        try {
            if (!this.workspaceBrain) {
                this.workspaceBrain = new WorkspaceBrainService({
                    workspacePath: this.currentProjectPath,
                    apiBase: this.config.get('apiUrl'),
                    getAuthToken: () => this.config.get('authToken'),
                    autoIndex: false,
                });
            }
            return this.workspaceBrain;
        }
        catch {
            return null;
        }
    }
    canPromptForWorkspaceIndex() {
        if (this.jsonOutput || !process.stdin.isTTY || !process.stdout.isTTY) {
            return false;
        }
        return !/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_NO_INDEX_PROMPT || ''));
    }
    async promptWorkspaceIndex(fileCount, workspaceName) {
        if (!this.canPromptForWorkspaceIndex()) {
            return 'later';
        }
        const answer = await new Promise((resolve) => {
            const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
            const prompt = `Index "${workspaceName}" for Vigthoria Brain (${fileCount} files) to improve large-context tasks? [y/N] `;
            rl.question(chalk.cyan(prompt), (value) => {
                rl.close();
                resolve(value);
            });
        });
        return /^(y|yes)$/i.test(answer.trim()) ? 'index_now' : 'later';
    }
    buildProjectBrainRuntimeContext(prompt, source) {
        if (this.isProjectBrainRuntimeDisabled()) {
            return undefined;
        }
        try {
            if (!this.projectMemory) {
                this.projectMemory = new ProjectMemoryService(this.currentProjectPath);
            }
            const status = this.projectMemory.getStatus();
            const context = this.projectMemory.buildContextForPrompt(prompt);
            if (!context && status.itemCount === 0) {
                return undefined;
            }
            return {
                schema: 'vigthoria.project-brain.v1',
                source,
                memoryDir: status.memoryDir,
                itemCount: status.itemCount,
                updatedAt: status.updatedAt,
                context,
            };
        }
        catch {
            return undefined;
        }
    }
    async buildWorkspaceBrainRuntimeContext(prompt) {
        const service = this.getWorkspaceBrainService();
        if (!service) {
            return {};
        }
        try {
            const result = await service.ensureIndexed({
                promptIfMissing: this.canPromptForWorkspaceIndex(),
                askToIndex: (fileCount, workspaceName) => this.promptWorkspaceIndex(fileCount, workspaceName),
            });
            const status = service.getStatus();
            const context = {
                workspaceBrain: {
                    schema: 'vigthoria.workspace-brain.v1',
                    indexed: result.indexed,
                    prompted: result.prompted || null,
                    indexedFileCount: status.indexedFileCount || result.fileCount,
                    totalChunks: status.totalChunks || result.chunkCount,
                    indexedAt: status.meta?.indexedAt || null,
                    indexHash: status.meta?.indexHash || null,
                },
            };
            const codebaseContext = service.buildCodebaseContext(prompt);
            if (codebaseContext) {
                context.codebaseContext = codebaseContext;
            }
            const accountBrainContext = await service.fetchAccountBrainContext().catch(() => '');
            if (accountBrainContext) {
                context.accountBrainContext = accountBrainContext;
            }
            return context;
        }
        catch {
            return {};
        }
    }
    rememberBrainEvent(type, text, mode) {
        if (this.isProjectBrainRuntimeDisabled()) {
            return;
        }
        try {
            if (!this.projectMemory) {
                this.projectMemory = new ProjectMemoryService(this.currentProjectPath);
            }
            this.projectMemory.remember(type, text, { source: 'vigthoria-cli', mode, model: this.currentModel });
        }
        catch {
            // Project Brain memory must not break chat, GoA, or operator execution.
        }
    }
    async getPromptRuntimeContext(prompt) {
        const runtimeContext = {
            agentRuntime: this.getRuntimeEnvironmentContext(),
        };
        const brainContext = this.buildProjectBrainRuntimeContext(prompt, this.operatorMode ? 'vigthoria-cli.operator' : this.agentMode ? 'vigthoria-cli.agent' : 'vigthoria-cli.chat');
        if (brainContext) {
            runtimeContext.vigthoriaBrain = brainContext;
            if (typeof brainContext.context === 'string' && brainContext.context.trim()) {
                runtimeContext.projectMemory = brainContext.context;
            }
        }
        const workspaceBrainContext = await this.buildWorkspaceBrainRuntimeContext(prompt);
        if (workspaceBrainContext.workspaceBrain) {
            runtimeContext.workspaceBrain = workspaceBrainContext.workspaceBrain;
        }
        if (typeof workspaceBrainContext.codebaseContext === 'string' && workspaceBrainContext.codebaseContext.trim()) {
            runtimeContext.codebaseContext = workspaceBrainContext.codebaseContext;
        }
        if (typeof workspaceBrainContext.accountBrainContext === 'string' && workspaceBrainContext.accountBrainContext.trim()) {
            runtimeContext.accountBrainContext = workspaceBrainContext.accountBrainContext;
        }
        if (!this.isBrowserTaskPrompt(prompt)) {
            return runtimeContext;
        }
        const devtoolsBridgeAllowed = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_ALLOWED || process.env.VIGTHORIA_BRIDGE_ALLOWED || ''));
        if (!devtoolsBridgeAllowed) {
            if (!this.jsonOutput) {
                console.log(chalk.yellow('Browser task detected. DevTools Bridge is opt-in; the agent will ask before relying on local browser tooling.'));
            }
            return {
                ...runtimeContext,
                browserTask: true,
                devtoolsBridgeAvailable: false,
                devtoolsBridgeAllowed: false,
                devtoolsBridgeEndpoint: null,
            };
        }
        const bridgeStatus = await this.callApi('Checking DevTools Bridge status', () => this.api.getDevtoolsBridgeStatus(), 0);
        if (!this.jsonOutput && bridgeStatus.ok) {
            console.log(chalk.gray(`Browser task detected. DevTools Bridge is reachable at ${bridgeStatus.endpoint}.`));
        }
        else if (!this.jsonOutput) {
            console.log(chalk.yellow(`Browser task detected. DevTools Bridge is not running at ${bridgeStatus.endpoint}.`));
        }
        return {
            ...runtimeContext,
            browserTask: true,
            devtoolsBridgeAvailable: bridgeStatus.ok,
            devtoolsBridgeAllowed: true,
            devtoolsBridgeEndpoint: bridgeStatus.endpoint,
        };
    }
    v3IterationCount = 0;
    v3ToolCallCount = 0;
    v3LastActivity = Date.now();
    v3IdleWatchInterval = null;
    v3IdleNoticeShown = false;
    v3StreamingStarted = false;
    /**
     * Strip server-internal path prefixes from tool output strings.
     * Prevents exposing paths like /var/www/V3-Code-Agent/... to end users.
     */
    sanitizeServerPath(text) {
        if (!text)
            return text;
        return sanitizeUserFacingPathText(this.stripHiddenThoughtBlocks(text));
    }
    stripHiddenThoughtBlocks(text) {
        if (!text)
            return text;
        return text
            .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
            .replace(/<think>[\s\S]*?<\/redacted_thinking>/gi, '')
            .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
            .replace(/<\/redacted_thinking>/gi, '')
            .replace(/<\/thinking>/gi, '')
            .replace(/<\|(?:mask_start|mask_end)\|>/g, '')
            .trim();
    }
    startV3IdleWatch(spinner) {
        this.stopV3IdleWatch();
        this.v3IdleNoticeShown = false;
        if (this.jsonOutput || !spinner) {
            return;
        }
        this.v3IdleWatchInterval = setInterval(() => {
            const idleMs = Date.now() - this.v3LastActivity;
            if (idleMs < 15_000) {
                // Activity resumed — re-arm the notice so a later stall is reported too.
                this.v3IdleNoticeShown = false;
                return;
            }
            if (this.v3IdleNoticeShown) {
                return;
            }
            this.v3IdleNoticeShown = true;
            if (spinner.isSpinning) {
                spinner.stop();
            }
            const seconds = Math.round(idleMs / 1000);
            const idleTimeoutSec = Math.round(DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS / 1000);
            process.stderr.write(chalk.yellow(`  [Wait] Model still working (${seconds}s) — this can legitimately take a while for large tasks. ` +
                (idleTimeoutSec > 0
                    ? `Only complete silence (~${idleTimeoutSec}s with zero bytes received) will trigger an auto-recover/retry — active progress is never interrupted.\n`
                    : `\n`)));
            spinner.start();
            spinner.text = 'Waiting for model response...';
        }, 5_000);
    }
    stopV3IdleWatch() {
        if (this.v3IdleWatchInterval) {
            clearInterval(this.v3IdleWatchInterval);
            this.v3IdleWatchInterval = null;
        }
        this.v3IdleNoticeShown = false;
    }
    describeV3AgentTool(toolName) {
        const normalized = String(toolName || '').toLowerCase();
        if (/read|grep|search|list|find|glob/.test(normalized)) {
            return 'Reading project files';
        }
        if (/write|edit|patch|create|delete|replace|rename/.test(normalized)) {
            return 'Writing changes';
        }
        if (/test|lint|build|run|exec|terminal/.test(normalized)) {
            return 'Running verification';
        }
        if (/hyperloop/.test(normalized)) {
            return 'Hyperloop processing';
        }
        return 'Working';
    }
    terminalColumns() {
        const raw = Number(process.stdout.columns || process.stderr.columns || 80);
        const safe = Number.isFinite(raw) && raw > 0 ? raw : 80;
        // process.stdout.columns is not always a reliable indicator of how many
        // columns actually render visually (seen mismatching badly in some
        // embedded/split-pane terminal hosts on Windows) -- cap at a
        // conservative width so wrapped/fitted text never overflows even when
        // the reported column count is larger than the real usable width.
        return Math.min(safe, 60);
    }
    fitTerminalText(text, reserve = 0) {
        const width = Math.max(24, this.terminalColumns() - reserve);
        const clean = String(text || '').replace(/\s+/g, ' ').trim();
        const visible = clean.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
        if (visible.length <= width)
            return clean;
        return `${visible.slice(0, Math.max(1, width - 1))}…`;
    }
    normalizeToolOutputForDisplay(output) {
        return String(output || '')
            .replace(/\/r\/n|\/n|\\n/g, '\n')
            .replace(/\/r|\\r/g, '')
            .replace(/\t/g, '  ');
    }
    toolEventKey(event) {
        const name = String(event?.name || event?.tool || event?.tool_name || '').trim();
        const args = event?.arguments && typeof event.arguments === 'object' ? event.arguments : {};
        const target = String(args.path || args.file_path || args.pattern || args.command || event?.target || '').trim();
        if (target)
            return `${name}:${target}`;
        const output = typeof event?.output === 'string' ? event.output.replace(/\s+/g, ' ').slice(0, 160) : '';
        return `${name}:${output}`;
    }
    isRawV3StreamPayload(event) {
        if (!event) {
            return false;
        }
        if (typeof event === 'string' || Buffer.isBuffer(event) || event instanceof Uint8Array) {
            return true;
        }
        const body = event?.body ?? event?.stream ?? event?.response;
        return Boolean(body && (typeof body[Symbol.asyncIterator] === 'function' || typeof body.getReader === 'function'));
    }
    async consumeV3StreamPayload(spinner, event) {
        try {
            const source = (event && typeof event === 'object' && !Buffer.isBuffer(event) && !(event instanceof Uint8Array))
                ? (event.body ?? event.stream ?? event.response ?? event)
                : event;
            for await (const chunk of robustifyStreamResponse(source)) {
                this.v3LastActivity = Date.now();
                if (chunk.type === 'error') {
                    if (spinner.isSpinning)
                        spinner.stop();
                    process.stderr.write(chalk.red(`\nStream error: ${chunk.content}\n`));
                    continue;
                }
                if (chunk.content) {
                    this.writeV3StreamText(spinner, chunk.content);
                }
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.red(`\nStream error: ${message}\n`));
        }
    }
    writeV3StreamText(spinner, text) {
        if (this.jsonOutput) {
            return;
        }
        const safeText = this.sanitizeV3VisibleStreamText(this.sanitizeServerPath(text));
        if (!safeText) {
            return;
        }
        if (!this.v3StreamingStarted) {
            this.v3StreamingStarted = true;
            spinner.stop();
            if (!this.directPromptMode) {
                console.log();
            }
        }
        else {
            spinner.stop();
        }
        this.v3StreamedAnswerBuffer += safeText;
        spinner.text = looksLikeMarkdownReport(this.v3StreamedAnswerBuffer)
            ? 'Writing analysis report...'
            : 'Writing response...';
    }
    displayV3StreamedAnswer() {
        if (this.jsonOutput || this.v3StreamedAnswerDisplayed) {
            return;
        }
        const content = this.v3StreamedAnswerBuffer.trim();
        if (!isSubstantiveAgentAnswer(content)) {
            return;
        }
        emitDeckEvent({ type: 'report_ready', markdown: content });
        if (isDeckModeEnabled()) {
            this.v3StreamedAnswerDisplayed = true;
            return;
        }
        console.log('');
        console.log(renderMarkdownToTerminal(content, this.getTerminalContentWidth()));
        this.v3StreamedAnswerDisplayed = true;
    }
    renderAgentAnswerForTerminal(content) {
        const normalized = String(content || '').trim();
        if (!normalized)
            return '';
        return renderMarkdownToTerminal(normalized, this.getTerminalContentWidth());
    }
    getTerminalContentWidth() {
        const cols = process.stdout.columns || 80;
        return Math.max(60, Math.min(cols - 2, 100));
    }
    sanitizeV3VisibleStreamText(text) {
        let output = String(text || '');
        if (!output)
            return '';
        output = output.replace(/<\/think>/gi, '</thinking>');
        output = output.replace(/<think>/gi, '<thinking>');
        output = output.replace(/<\/redacted_thinking>/gi, '</thinking>');
        if (this.v3SuppressThinkingStream) {
            const closeIdx = output.search(/<\/thinking>/i);
            if (closeIdx < 0) {
                return '';
            }
            output = output.slice(closeIdx).replace(/^<\/thinking>/i, '');
            this.v3SuppressThinkingStream = false;
        }
        output = output.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
        if (/<thinking>/i.test(output)) {
            output = output.replace(/<thinking>[\s\S]*$/i, '');
            this.v3SuppressThinkingStream = true;
        }
        output = output
            .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
            .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
            .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
            .replace(/^\s*(?:list_dir|read_file|write_file|edit_file|glob|grep|bash)\s*$/gim, '')
            .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
            .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
            .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
            .replace(/<(?:grep|list_directory|search_files|glob|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_result)>/gi, '')
            .replace(/bootstrap discovery completed\s*[—-]\s*inspect the most relevant files next\.?/gi, '')
            .replace(/\n{3,}/g, '\n\n');
        return output;
    }
    writeAgentActivityLine(text) {
        if (this.jsonOutput)
            return;
        process.stdout.write(text);
    }
    updateV3AgentSpinner(spinner, event) {
        if (this.isRawV3StreamPayload(event)) {
            this.consumeV3StreamPayload(spinner, event).catch((error) => {
                const message = error instanceof Error ? error.message : String(error);
                if (spinner.isSpinning)
                    spinner.stop();
                process.stderr.write(chalk.red(`\nStream error: ${message}\n`));
            });
            return;
        }
        if (!event || typeof event !== 'object') {
            return;
        }
        this.v3LastActivity = Date.now();
        if (isV3StreamKeepaliveEvent(event)) {
            return;
        }
        if (event.type === 'tool_call') {
            const eventKey = this.toolEventKey(event);
            if (this.v3SeenToolCalls.has(eventKey)) {
                return;
            }
            this.v3SeenToolCalls.add(eventKey);
            this.v3ToolCallCount += 1;
            const toolName = event.name || event.tool || event.tool_name || '';
            emitDeckEvent({
                type: 'tool_start',
                tool: toolName,
                index: this.v3ToolCallCount,
                target: event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '',
            });
            const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
            const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
            const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
            const shortTarget = sanitizedTarget ? ` -> ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
            const stepLabel = chalk.cyan(`  [${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
            if (spinner.isSpinning)
                spinner.stop();
            this.writeAgentActivityLine(this.fitTerminalText(stepLabel) + '\n');
            // Show extra detail for key tools
            const args = event.arguments || {};
            const toolNameForDetail = event.name || event.tool || '';
            if ((toolNameForDetail === 'write_file' || toolNameForDetail === 'edit_file') && typeof args.content === 'string') {
                const len = args.content.length;
                this.writeAgentActivityLine(chalk.gray(`      ${len > 1000 ? Math.round(len / 1024) + ' KB' : len + ' bytes'} content\n`));
            }
            else if (toolNameForDetail === 'bash' && typeof args.command === 'string') {
                const command = this.sanitizeServerPath(args.command);
                this.writeAgentActivityLine(chalk.gray(`      ${this.fitTerminalText(`$ ${command}`, 6)}\n`));
            }
            else if (toolNameForDetail === 'read_file') {
                this.writeAgentActivityLine(chalk.gray(`      ${this.fitTerminalText(`reading ${sanitizedTarget || 'file'}`, 6)}\n`));
            }
            spinner.text = `Running ${toolDesc}...`;
            return;
        }
        if (event.type === 'tool_result') {
            const eventKey = this.toolEventKey(event);
            if (eventKey && this.v3SeenToolResults.has(eventKey)) {
                return;
            }
            if (eventKey)
                this.v3SeenToolResults.add(eventKey);
            const success = event.success !== false;
            const toolName = event.name || event.tool || '';
            emitDeckEvent({
                type: 'tool_end',
                tool: toolName,
                success,
                index: this.v3ToolCallCount,
            });
            const indicator = success ? chalk.green('  ✓') : chalk.red('  ✗');
            if (spinner.isSpinning)
                spinner.stop();
            this.writeAgentActivityLine(`${indicator} ${toolName}\n`);
            // Show output for failures, or brief summary for successes — sanitize server paths
            const rawOutput = typeof event.output === 'string' ? event.output.trim() : '';
            const output = this.normalizeToolOutputForDisplay(this.sanitizeServerPath(rawOutput));
            if (!success && output) {
                const sanitizedError = this.sanitizeServerPath(typeof event.error === 'string' ? event.error : output);
                const lines = sanitizedError.split('\n').slice(0, 4);
                this.writeAgentActivityLine(chalk.red(`${lines.map((line) => `      ${this.fitTerminalText(line, 6)}`).join('\n')}\n`));
            }
            else if (success && output && output.length > 0) {
                if (toolName === 'read_file' || toolName === 'grep' || toolName === 'search_files' || toolName === 'list_directory') {
                    const lineCount = output.split('\n').length;
                    const preview = output.split('\n')
                        .map((line) => line.trim())
                        .filter((line) => line && !line.startsWith('.vigthoria/memory/brain.json:'))
                        .slice(0, 3)
                        .join('\n');
                    this.writeAgentActivityLine(chalk.gray(`      ${lineCount} line${lineCount === 1 ? '' : 's'} returned\n`));
                    if (preview && preview.length > 20 && !/^\(no matches\)$/i.test(preview.trim())) {
                        this.writeAgentActivityLine(chalk.gray(`${preview.split('\n').map((line) => `      ${this.fitTerminalText(line, 6)}`).join('\n')}\n`));
                    }
                }
                else {
                    const brief = output.split('\n')[0];
                    this.writeAgentActivityLine(chalk.gray(`      ${this.fitTerminalText(brief, 6)}\n`));
                }
            }
            spinner.text = 'Next step...';
            return;
        }
        if (event.type === 'v3_status') {
            spinner.text = this.sanitizeServerPath(String(event.content || 'Routing to V3 Agent...'));
            return;
        }
        if (event.type === 'thinking') {
            this.v3IterationCount += 1;
            const iterText = this.sanitizeServerPath(event.content || '');
            const isGenericStep = !iterText
                || /read-only analysis turn|run at least one discovery tool/i.test(iterText)
                || /^Step \d+:/i.test(iterText)
                || /reviewing attached files|inspecting workspace/i.test(iterText);
            if (!isGenericStep) {
                if (spinner.isSpinning)
                    spinner.stop();
                this.writeAgentActivityLine(chalk.cyan(`\n── ${this.fitTerminalText(iterText, 6)} ──\n`));
            }
            spinner.text = 'Analyzing...';
            return;
        }
        if (event.type === 'message' || event.type === 'assistant' || event.type === 'text' || event.type === 'content_block_delta') {
            const text = event.type === 'content_block_delta'
                ? (event.delta?.text || '')
                : (event.content || '');
            if (text) {
                this.v3LastActivity = Date.now();
                if (event.type === 'message' && /^\[Context\]/i.test(String(text).trim())) {
                    if (spinner.isSpinning)
                        spinner.stop();
                    process.stderr.write(chalk.cyan(`  ${this.fitTerminalText(String(text).trim(), 8)}\n`));
                    spinner.text = 'Analyzing...';
                    return;
                }
                this.writeV3StreamText(spinner, text);
            }
            else {
                spinner.text = chalk.cyan('[Response] ') + 'Writing response...';
            }
            return;
        }
        if (event.type === 'complete') {
            const elapsed = event.elapsed || '';
            const iters = event.iterations || this.v3IterationCount;
            const tools = event.tool_calls || this.v3ToolCallCount;
            if (spinner.isSpinning)
                spinner.stop();
            this.displayV3StreamedAnswer();
            emitDeckEvent({
                type: 'run_complete',
                success: true,
                tools: event.tool_calls || this.v3ToolCallCount,
                iterations: event.iterations || this.v3IterationCount,
                elapsed: event.elapsed || '',
            });
            let statLine = `${iters} iterations, ${tools} tool calls`;
            if (elapsed)
                statLine += `, ${elapsed}`;
            if (event.mode === 'planner-executor') {
                const ok = event.tasks_completed ?? '?';
                const fail = event.tasks_failed ?? 0;
                statLine += ` — ${ok} tasks done`;
                if (fail > 0)
                    statLine += chalk.yellow(`, ${fail} failed`);
            }
            process.stdout.write(chalk.green(`\n✓ Complete`) + ` - ${this.fitTerminalText(statLine, 14)}\n`);
            // Show seal quality score if available
            if (event.seal_score && typeof event.seal_score.overall === 'number') {
                const score = event.seal_score.overall;
                const tier = event.seal_score.tier || '';
                const scoreColor = score >= 7 ? chalk.green : score >= 5 ? chalk.yellow : chalk.red;
                process.stdout.write(chalk.cyan('  [Quality] ') + scoreColor(`${score}/10`) + (tier ? chalk.gray(` (${tier})`) : '') + '\n');
            }
            return;
        }
        if (event.type === 'plan') {
            const plan = event.plan || {};
            const planKind = this.sanitizeServerPath(plan.task_kind || event.task_kind || '');
            const quality = this.sanitizeServerPath(plan.quality_profile || '');
            const status = typeof plan.status === 'string' ? plan.status : '';
            const summary = typeof plan.summary === 'string' ? this.sanitizeServerPath(plan.summary) : '';
            if (spinner.isSpinning)
                spinner.stop();
            process.stdout.write(chalk.cyan(`  [Plan] `) + this.fitTerminalText(`Task: ${planKind || 'analyzing'}${quality ? ` (${quality})` : ''}`, 9) + '\n');
            if (summary) {
                process.stdout.write(chalk.gray(`      ${this.fitTerminalText(summary, 6)}\n`));
            }
            if (status === 'planning' && Number.isFinite(Number(plan.elapsed_seconds))) {
                process.stdout.write(chalk.gray(`      elapsed: ${plan.elapsed_seconds}s\n`));
            }
            if (Array.isArray(plan.tasks) && plan.tasks.length > 0) {
                process.stdout.write(chalk.gray(`      ${plan.total_tasks || plan.tasks.length} tasks:\n`));
                for (const t of plan.tasks.slice(0, 10)) {
                    const targets = Array.isArray(t.targets) && t.targets.length ? ` → ${t.targets.map((target) => this.sanitizeServerPath(String(target))).join(', ')}` : '';
                    process.stdout.write(chalk.gray(`        - ${this.fitTerminalText(`${this.sanitizeServerPath(String(t.title || t.id))}${targets}`, 10)}\n`));
                }
                if (plan.tasks.length > 10) {
                    process.stdout.write(chalk.gray(`        ... and ${plan.tasks.length - 10} more\n`));
                }
            }
            if (Array.isArray(plan.notes) && plan.notes.length > 0) {
                for (const note of plan.notes.slice(0, 3)) {
                    process.stdout.write(chalk.gray(`      note: ${this.fitTerminalText(this.sanitizeServerPath(String(note)), 12)}\n`));
                }
            }
            if (Array.isArray(plan.target_files) && plan.target_files.length > 0) {
                process.stdout.write(chalk.gray(`      ${this.fitTerminalText(`Files: ${plan.target_files.map((filePath) => this.sanitizeServerPath(String(filePath))).join(', ')}`, 6)}\n`));
            }
            spinner.text = status === 'planning' ? 'Planning...' : 'Executing plan...';
            return;
        }
        if (event.type === 'executor_start') {
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.cyan('  [Executor] ') + `Starting ${this.sanitizeServerPath(String(event.task_id || 'task'))}${event.title ? ` - ${this.sanitizeServerPath(String(event.title))}` : ''}
`);
            spinner.start();
            spinner.text = 'Vigthoria Executor running...';
            return;
        }
        if (event.type === 'model_call_start') {
            if (spinner.isSpinning)
                spinner.stop();
            const model = this.sanitizeServerPath(String(event.model || 'model'));
            const iter = event.iteration ? ` iteration ${event.iteration}` : '';
            const msgCount = Number.isFinite(Number(event.message_count)) ? `, ${event.message_count} messages` : '';
            const toolCount = Number.isFinite(Number(event.tool_count)) ? `, ${event.tool_count} tools available` : '';
            process.stderr.write(chalk.cyan('  [Model] ') + `${model}${iter}: thinking${msgCount}${toolCount}\n`);
            spinner.start();
            spinner.text = `Model ${model} is reasoning...`;
            return;
        }
        if (event.type === 'model_call_complete') {
            if (spinner.isSpinning)
                spinner.stop();
            const model = this.sanitizeServerPath(String(event.model || 'model'));
            const toolCalls = Number.isFinite(Number(event.tool_calls)) ? Number(event.tool_calls) : 0;
            const chars = Number.isFinite(Number(event.content_chars)) ? Number(event.content_chars) : 0;
            const finish = event.finish_reason ? `, ${this.sanitizeServerPath(String(event.finish_reason))}` : '';
            const billing = event.billing && typeof event.billing === 'object' ? event.billing : null;
            const billingSuffix = billing
                ? billing.billingMode === 'master_admin'
                    ? chalk.gray(' · Master Admin audited access')
                    : billing.status === 'settlement_pending'
                        ? chalk.yellow(` · ${Number(billing.reservedCredits || 0)} ⓥ reserved (settlement pending)`)
                        : chalk.yellow(` · ${Number(billing.actualCredits || 0)} ⓥ charged${billing.balanceAfter != null ? ` · ${Number(billing.balanceAfter)} ⓥ remaining` : ''}`)
                : '';
            process.stderr.write(chalk.cyan('  [Model] ') + `${model}: produced ${toolCalls} tool call${toolCalls === 1 ? '' : 's'}${chars ? `, ${chars} chars` : ''}${finish}${billingSuffix}\n`);
            spinner.start();
            spinner.text = toolCalls > 0 ? 'Executing model-selected tools...' : 'Processing model response...';
            return;
        }
        if (event.type === 'executor_error') {
            if (spinner.isSpinning)
                spinner.stop();
            const msg = sanitizeUserFacingErrorText(String(event.error || 'Executor error')) || 'Executor error';
            process.stderr.write(chalk.red('  [Executor] ') + `Vigthoria Executor encountered an issue: ${msg}
`);
            spinner.start();
            spinner.text = 'Recovering executor...';
            return;
        }
        if (event.type === 'executor_complete') {
            if (spinner.isSpinning)
                spinner.stop();
            const summary = event.summary || {};
            const status = String(summary.status || 'completed');
            const changed = Array.isArray(summary.changed_files) ? summary.changed_files.length : 0;
            if (status === 'failed') {
                process.stderr.write(chalk.red('  [Executor] ') + `Vigthoria Executor task failed${summary.task_id ? ` (${summary.task_id})` : ''}.
`);
            }
            else {
                process.stderr.write(chalk.green('  [Executor] ') + `Task completed${summary.task_id ? ` (${summary.task_id})` : ''}${changed ? `, ${changed} files changed` : ''}.
`);
            }
            spinner.start();
            spinner.text = 'Continuing plan...';
            return;
        }
        if (event.type === 'file_mutation') {
            const rawPath = typeof event.path === 'string' ? this.sanitizeServerPath(event.path) : '';
            const filePath = rawPath ? rawPath.replace(/\\/g, '/').split('/').slice(-2).join('/') : '';
            const action = event.action === 'delete' ? chalk.red('deleted') : chalk.green('wrote');
            if (filePath) {
                if (spinner.isSpinning)
                    spinner.stop();
                process.stderr.write(chalk.cyan('  [File] ') + `${action} ${filePath}\n`);
                spinner.start();
            }
            return;
        }
        if (event.type === 'error') {
            if (event.checkpointed) {
                if (spinner.isSpinning)
                    spinner.stop();
                process.stderr.write(chalk.yellow('  [Checkpoint] ') + 'Budget reached — auto-continuing...\n');
                spinner.start();
            }
            else {
                if (spinner.isSpinning)
                    spinner.stop();
                const message = sanitizeUserFacingErrorText(String(event.message || 'Agent error')) || 'Agent error';
                const plannerLike = /plan|planner|dependency graph/i.test(message);
                const executorLike = /executor|task failed|iteration/i.test(message);
                if (plannerLike) {
                    process.stderr.write(chalk.red('  [Planner] ') + `Vigthoria Planner encountered an issue: ${this.sanitizeServerPath(message)}\n`);
                }
                else if (executorLike) {
                    process.stderr.write(chalk.red('  [Executor] ') + `Vigthoria Executor encountered an issue: ${this.sanitizeServerPath(message)}\n`);
                }
                else {
                    process.stderr.write(chalk.red('  [Error] ') + this.sanitizeServerPath(message) + '\n');
                }
            }
            return;
        }
        if (event.type === 'context') {
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.cyan('  [Context] ') + 'Workspace bound\n');
            spinner.start();
            spinner.text = 'Starting agent...';
            return;
        }
        if (event.type === 'start') {
            if (this.v3StartSeen) {
                spinner.text = 'Working...';
                return;
            }
            this.v3StartSeen = true;
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.cyan('  [Start] ') + 'Agent initialized\n');
            spinner.start();
            spinner.text = 'Working...';
            return;
        }
        const fallbackStatus = typeof event.status === 'string' ? this.sanitizeServerPath(event.status) : '';
        const fallbackStage = typeof event.stage === 'string' ? this.sanitizeServerPath(event.stage) : '';
        const fallbackMessage = typeof event.message === 'string'
            ? this.sanitizeServerPath(event.message)
            : (typeof event.content === 'string' ? this.sanitizeServerPath(event.content) : '');
        if (event.type === 'status' || event.type === 'progress' || fallbackStatus || fallbackStage) {
            if (spinner.isSpinning)
                spinner.stop();
            if (fallbackMessage) {
                process.stderr.write(chalk.cyan('  [V3] ') + `${fallbackMessage}\n`);
            }
            spinner.start();
            spinner.text = fallbackStatus || fallbackStage || 'Working...';
            return;
        }
    }
    updateOperatorSpinner(spinner, event) {
        if (!event || typeof event !== 'object') {
            return;
        }
        if (event.type === 'started') {
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.cyan('  [Operator] ') + 'Starting BMAD workflow...\n');
            spinner.start();
            spinner.text = 'Connecting...';
            return;
        }
        if (event.type === 'connected') {
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.green('  ✓') + ' Connected to BMAD stream\n');
            spinner.start();
            spinner.text = 'Working...';
            return;
        }
        if (event.type === 'agent') {
            const agentName = event.agent || 'BMAD agent';
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.cyan(`  [Agent] `) + agentName + '\n');
            spinner.start();
            spinner.text = `Running ${agentName}...`;
            return;
        }
        if (event.type === 'status') {
            const progress = Number.isFinite(Number(event.progress)) ? ` (${event.progress}%)` : '';
            const statusText = `${event.status || 'Working'}${progress}`;
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.cyan('  [Status] ') + statusText + '\n');
            spinner.start();
            spinner.text = statusText;
            return;
        }
        if (event.type === 'result') {
            if (spinner.isSpinning)
                spinner.stop();
            process.stderr.write(chalk.green('\n  ✓ Operator workflow complete\n'));
            return;
        }
    }
    constructor(config, logger, commandProgram) {
        this.config = config;
        this.logger = logger;
        this.commandProgram = commandProgram;
        this.api = new APIClient(config, logger);
        this.sessionManager = new SessionManager();
    }
    async run(options) {
        if (!this.config.isAuthenticated()) {
            if (options.json) {
                process.exitCode = 1;
                emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
            }
            else {
                this.logger.error('Not authenticated. Run: vigthoria login');
            }
            return;
        }
        this.agentMode = options.agent === true;
        this.operatorMode = options.operator === true;
        this.workflowTarget = typeof options.workflow === 'string' && options.workflow.trim()
            ? options.workflow.trim()
            : null;
        this.savePlanToVigFlow = options.savePlan === true;
        this.jsonOutput = options.json === true;
        this.autoApprove = options.autoApprove === true || this.jsonOutput;
        this.personaOverride = options.grant === true ? 'wiener_grant' : null;
        this.modelExplicitlySelected = this.isHardExplicitAgentModelSelection(options);
        this.currentModel = this.resolveInitialModel(options);
        this.applyNoAgentGovernance(String(options.model || this.currentModel || ''));
        this.currentProjectPath = this.resolveProjectPath(options);
        if ((this.agentMode || this.operatorMode) && shouldShowAgentSessionMenu(options)) {
            const sessionConfig = await runAgentSessionMenu({
                workspacePath: this.currentProjectPath,
                autoApprove: this.autoApprove,
            });
            this.currentProjectPath = sessionConfig.workspacePath;
            this.autoApprove = sessionConfig.autoApprove || this.autoApprove;
            options.autoApprove = this.autoApprove;
            if (sessionConfig.debugMode) {
                process.env.DEBUG = process.env.DEBUG || 'vigthoria:*';
            }
            if (sessionConfig.engine === 'v4') {
                const v4 = new V4Command(this.config, this.logger);
                await v4.run({ v4Path: process.env.VIGTHORIA_V4_AGENT_DIR });
                return;
            }
            console.log(chalk.green(this.fitTerminalText(`Using workspace: ${this.currentProjectPath}`)));
        }
        if (this.jsonOutput && !options.prompt && !options.retry && !options.continue) {
            throw new Error('--json is only supported together with --prompt, --retry, or --continue.');
        }
        this.ensureProjectWorkspace();
        this.directPromptMode = Boolean(options.prompt);
        this.directToolContinuationCount = 0;
        this.tools = new AgenticTools(this.logger, this.currentProjectPath, async (action) => this.requestPermission(action), this.autoApprove);
        this.initializeSession(options.resume === true || options.retry === true || options.continue === true);
        if (options.resume && this.lastAgentRunOutcome && !this.jsonOutput && !options.prompt) {
            this.printResumeContinuationHint();
        }
        // ── Commando Bridge: connect if --bridge was specified ──────────
        if (options.bridge) {
            const bridgeClient = new BridgeClient({
                bridgeUrl: options.bridge,
                apiKey: this.config.get('authToken'),
                onAdminCommand: (cmd) => this.handleAdminCommand(cmd),
            });
            await bridgeClient.connect();
            const mode = this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat';
            bridgeClient.emitStart({
                command: mode,
                flags: { model: this.currentModel, agent: this.agentMode, operator: this.operatorMode, autoApprove: this.autoApprove },
                cwd: this.currentProjectPath,
            });
        }
        if (this.operatorMode && !this.hasOperatorAccess()) {
            if (this.currentSession) {
                this.currentSession.operatorMode = false;
                this.saveSession();
            }
            this.operatorMode = false;
            this.logger.error(this.operatorAccessMessage());
            return;
        }
        if (options.retry || options.continue) {
            if (!this.agentMode) {
                throw new Error('--retry and --continue require agent mode.');
            }
            const followUp = options.retry ? this.buildRetryPrompt() : this.buildContinuePrompt();
            if (!followUp) {
                throw new Error('No previous agent run to retry or continue. Run an agent prompt first with the same --project, or use --resume.');
            }
            this.directPromptMode = true;
            await this.runAgentTurn(followUp);
            return;
        }
        if (options.prompt) {
            const bridgePromptTimeoutMs = (() => {
                const rawValue = process.env.VIGTHORIA_BRIDGE_PROMPT_TIMEOUT_MS || process.env.V3_BRIDGE_PROMPT_TIMEOUT_MS;
                if (!rawValue) {
                    return 0;
                }
                const parsed = Number.parseInt(rawValue, 10);
                return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
            })();
            let timedOut = false;
            const timeoutId = options.bridge && bridgePromptTimeoutMs > 0
                ? setTimeout(() => {
                    timedOut = true;
                    const b = getBridgeClient();
                    if (b) {
                        b.emitEnd({ reason: 'timeout' });
                        b.destroy();
                    }
                    if (!this.jsonOutput) {
                        this.logger.error('Bridge prompt timed out after ' + Math.round(bridgePromptTimeoutMs / 1000) + ' seconds.');
                    }
                    process.exitCode = 1;
                }, bridgePromptTimeoutMs)
                : null;
            await this.handleDirectPrompt(options.prompt);
            if (timeoutId)
                clearTimeout(timeoutId);
            if (!timedOut) {
                const bridge = getBridgeClient();
                if (bridge) {
                    bridge.emitEnd({ reason: 'prompt-complete' });
                    bridge.destroy();
                }
            }
            // Force-exit: undici + chokidar + HTTPS pool keep the Node.js event
            // loop alive indefinitely in direct prompt mode; a clean exit is safe here.
            this.api.destroy();
            process.exit(process.exitCode ?? 0);
        }
        await this.startInteractiveChat();
        const bridge = getBridgeClient();
        if (bridge) {
            bridge.emitEnd({ reason: 'interactive-exit' });
            bridge.destroy();
        }
    }
    /** Handle an inbound admin command from the Commando Bridge. */
    handleAdminCommand(cmd) {
        switch (cmd.action) {
            case 'ping':
                getBridgeClient()?.emitModelResponse({ model: this.currentModel, chars: 0, hasToolCalls: false, preview: 'pong' });
                break;
            case 'set-model':
                if (cmd.params?.value && typeof cmd.params.value === 'string') {
                    this.currentModel = cmd.params.value;
                    getBridgeClient()?.emitModeChange({ mode: this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat', model: this.currentModel });
                    if (!this.jsonOutput)
                        console.log(chalk.yellow(`[bridge] Model changed to: ${this.currentModel}`));
                }
                break;
            case 'abort':
                if (!this.jsonOutput)
                    console.log(chalk.red(`[bridge] Abort requested by admin`));
                getBridgeClient()?.emitEnd({ reason: 'admin-abort' });
                process.exit(0);
                break;
            default:
                getBridgeClient()?.emitError({ message: `Unknown admin command: ${cmd.action}` });
        }
    }
    ensureProjectWorkspace() {
        if (fs.existsSync(this.currentProjectPath)) {
            if (!fs.statSync(this.currentProjectPath).isDirectory()) {
                throw new Error(`Project path is not a directory: ${this.currentProjectPath}`);
            }
            return;
        }
        fs.mkdirSync(this.currentProjectPath, { recursive: true });
    }
    shouldStartWorkspaceWatcher(workspaceRoot) {
        if (!workspaceRoot) {
            return false;
        }
        const resolved = path.resolve(workspaceRoot);
        if (!fs.existsSync(resolved)) {
            return false;
        }
        let stat;
        try {
            stat = fs.statSync(resolved);
        }
        catch {
            return false;
        }
        if (!stat.isDirectory()) {
            return false;
        }
        const homeDir = path.resolve(os.homedir());
        const normalized = resolved.replace(/\\/g, '/');
        const normalizedHome = homeDir.replace(/\\/g, '/');
        // Guardrail: do not watch broad home/root scopes on interactive shells.
        if (normalized.toLowerCase() === normalizedHome.toLowerCase()) {
            if (!this.jsonOutput) {
                console.log(chalk.gray('Info: workspace watcher disabled for home directory scope.'));
            }
            return false;
        }
        if (/^[a-zA-Z]:\/$/.test(normalized) || normalized === '/') {
            if (!this.jsonOutput) {
                console.log(chalk.gray('Info: workspace watcher disabled for filesystem root scope.'));
            }
            return false;
        }
        return true;
    }
    resolveProjectPath(options) {
        const requestedProject = String(options.project || '').trim();
        if (requestedProject) {
            return path.resolve(requestedProject);
        }
        // Check if prompt contains an explicit local path provided by the user
        if (options.prompt) {
            const explicitPath = this.resolvePromptWorkspacePath(options.prompt, process.cwd());
            if (explicitPath) {
                if (!this.jsonOutput) {
                    console.log(chalk.gray(`📁 Using project path from prompt: ${explicitPath}`));
                }
                return explicitPath;
            }
        }
        if (this.shouldUseManagedWorkspace(options)) {
            const rootPath = this.getManagedWorkspaceRoot();
            fs.mkdirSync(rootPath, { recursive: true });
            const folderName = this.resolveManagedWorkspaceName(options);
            const projectPath = this.allocateManagedWorkspacePath(rootPath, folderName);
            this.config.set('project', {
                ...this.config.get('project'),
                rootPath,
            });
            return projectPath;
        }
        return process.cwd();
    }
    shouldUseManagedWorkspace(options) {
        if (options.projectProvided) {
            return false;
        }
        if (options.newProject) {
            return true;
        }
        // Only use managed workspace when --prompt is given AND the CWD looks
        // like a home/root directory (no project signals).  When the user runs
        // `vigthoria agent --prompt "..."` inside an actual project folder we
        // should use that folder, not invent a new managed workspace.
        if (Boolean(options.prompt) && (options.agent === true || options.operator === true)) {
            const cwd = process.cwd();
            const homeDir = os.homedir();
            // If CWD is home dir, root, or a system temp dir → use managed workspace
            if (cwd === homeDir || cwd === '/' || cwd === 'C:\\' || cwd.toLowerCase().includes('temp')) {
                return true;
            }
            // If CWD has any project signals → use CWD
            const projectSignals = ['.git', 'package.json', 'Cargo.toml', 'go.mod', 'pom.xml', 'requirements.txt', 'pyproject.toml', '.vigthoria', 'Makefile', 'CMakeLists.txt', 'tsconfig.json'];
            for (const signal of projectSignals) {
                if (fs.existsSync(path.join(cwd, signal))) {
                    return false;
                }
            }
            // If CWD has files (not just an empty dir) → use CWD
            try {
                const entries = fs.readdirSync(cwd);
                if (entries.length > 0) {
                    return false;
                }
            }
            catch {
                // Cannot read CWD — fall through to managed workspace
            }
            return true;
        }
        return false;
    }
    getManagedWorkspaceRoot() {
        const envRoot = String(process.env.VIGTHORIA_DEFAULT_WORKSPACE_ROOT || '').trim();
        const configuredRoot = String(this.config.get('project').rootPath || '').trim();
        return path.resolve(envRoot || configuredRoot || path.join(os.homedir(), 'VigthoriaProjects'));
    }
    resolveManagedWorkspaceName(options) {
        const explicitName = typeof options.newProject === 'string' ? options.newProject.trim() : '';
        if (explicitName) {
            return this.slugifyWorkspaceName(explicitName);
        }
        if (options.prompt) {
            const fromPrompt = this.slugifyWorkspaceName(options.prompt.split(/[.!?\n]/)[0] || 'project');
            if (fromPrompt) {
                return fromPrompt;
            }
        }
        return `project-${new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14)}`;
    }
    slugifyWorkspaceName(input) {
        return String(input || '')
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/^-+|-+$/g, '')
            .slice(0, 48) || 'project';
    }
    allocateManagedWorkspacePath(rootPath, folderName) {
        const basePath = path.join(rootPath, folderName);
        if (!fs.existsSync(basePath)) {
            return basePath;
        }
        const existingEntries = fs.readdirSync(basePath);
        if (existingEntries.length === 0) {
            return basePath;
        }
        let counter = 2;
        while (counter < 1000) {
            const candidate = path.join(rootPath, `${folderName}-${counter}`);
            if (!fs.existsSync(candidate)) {
                return candidate;
            }
            counter += 1;
        }
        return path.join(rootPath, `${folderName}-${Date.now()}`);
    }
    initializeSession(resume) {
        this.projectMemory = new ProjectMemoryService(this.currentProjectPath);
        if (resume) {
            this.currentSession = this.sessionManager.getLatest(this.currentProjectPath);
            if (this.currentSession) {
                this.messages = [...this.currentSession.messages];
                this.agentMode = this.currentSession.agentMode || this.agentMode;
                this.operatorMode = this.currentSession.operatorMode || this.operatorMode;
                this.currentModel = this.currentSession.model || this.currentModel;
                if (this.currentSession.lastAgentRunOutcome) {
                    const saved = this.currentSession.lastAgentRunOutcome;
                    this.lastAgentRunOutcome = {
                        ...saved,
                        originalPrompt: saved.originalPrompt ?? null,
                    };
                }
                return;
            }
        }
        this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
        this.messages = [...this.currentSession.messages];
    }
    /**
     * Determine whether a direct --prompt call is simple enough to skip the
     * full V3 Agent planner and answer directly via the simple chat pipeline.
     * This prevents trivial prompts like "Reply with OK" from triggering a
     * multi-step planning workflow with quality_profile: premium-visual-build.
     */
    isSimpleDirectPrompt(prompt) {
        const trimmed = prompt.trim();
        const wordCount = trimmed.split(/\s+/).length;
        // Never treat prompts that reference files, dirs, or workspace as simple
        // — these need agent tool access (list_dir, read_file, etc.)
        if (/(file|folder|director|code|project|workspace|repo|module|component|function|class|api|endpoint|route|database|schema|migration|docker|deploy|build|test)\b/i.test(trimmed)) {
            return false;
        }
        // Never treat tool-action verbs as simple
        if (/(list|find|search|show|check|scan|read|inspect|analyze|analyse|audit|review|count|create|build|generate|implement|write.*function|add.*feature|refactor|fix|edit|modify|update|delete|remove)\b/i.test(trimmed)) {
            return false;
        }
        // Short conversational prompts (≤ 15 words) with no file/tool context
        if (wordCount <= 15) {
            return true;
        }
        // Conversational Q&A starters
        if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should|tell|explain|help|reply|say|respond|answer|translate|summarize|define)\b/i.test(trimmed)) {
            return true;
        }
        return false;
    }
    async handleDirectPrompt(prompt) {
        const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
        if (goaContext)
            this.pendingGoaContext = goaContext;
        const effectivePrompt = userPrompt || prompt;
        // Suppress all setup banners in direct-prompt mode so only the final
        // answer reaches stdout.  Interactive (REPL) mode still shows them.
        if (!this.jsonOutput) {
            console.log(chalk.cyan('Running single prompt in direct mode.'));
            console.log(chalk.gray(`Model: ${this.currentModel}`));
            console.log(chalk.gray(`Project: ${this.currentProjectPath}`));
            if (this.workflowTarget) {
                console.log(chalk.gray(`Workflow target: ${this.workflowTarget}`));
            }
            console.log();
        }
        if (this.workflowTarget) {
            await this.runWorkflowTargetPrompt(effectivePrompt);
            return;
        }
        // Smart routing: for agent mode, determine if prompt needs tool access
        if (this.agentMode) {
            if (this.isSimpleDirectPrompt(effectivePrompt)) {
                // Simple prompt: downgrade to plain chat model, no agent system prompt
                this.currentModel = this.getDefaultChatModel();
                await this.runSimplePrompt(effectivePrompt);
            }
            else {
                await this.runAgentTurn(effectivePrompt);
            }
            return;
        }
        if (this.operatorMode) {
            if (!this.hasOperatorAccess()) {
                this.logger.error(this.operatorAccessMessage());
                return;
            }
            await this.runOperatorTurn(effectivePrompt);
            return;
        }
        await this.runSimplePrompt(effectivePrompt);
    }
    formatWorkflowTargetResult(result) {
        if (typeof result === 'string') {
            return result.trim();
        }
        if (result == null) {
            return '';
        }
        if (typeof result !== 'object') {
            return String(result);
        }
        const entries = Object.entries(result)
            .filter(([, value]) => typeof value === 'string' && String(value).trim());
        if (entries.length === 1) {
            return String(entries[0][1]).trim();
        }
        if (entries.length > 1) {
            return entries
                .map(([key, value]) => `${key}:\n${String(value).trim()}`)
                .join('\n\n');
        }
        const serialized = JSON.stringify(result, null, 2);
        return serialized === '{}' ? '' : serialized;
    }
    async runWorkflowTargetPrompt(prompt) {
        const selector = this.workflowTarget;
        if (!selector) {
            throw new Error('Workflow target is not configured.');
        }
        this.lastActionableUserInput = prompt;
        const executionPrompt = this.buildExecutionPrompt(prompt);
        this.messages.push({ role: 'user', content: executionPrompt });
        const runtimeContext = await this.getPromptRuntimeContext(prompt);
        const resolvedWorkflow = await this.callApi('Resolve VigFlow workflow', () => this.api.resolveVigFlowWorkflow(selector));
        const invocationMode = this.operatorMode ? 'operator' : this.agentMode ? 'agent' : 'chat';
        const spinner = this.jsonOutput ? null : createSpinner({ text: `Running workflow ${resolvedWorkflow.name}...`, spinner: 'clock' }).start();
        try {
            const execution = await this.callApi('Run VigFlow workflow', () => this.api.runVigFlowWorkflow(resolvedWorkflow.id, {
                data: {
                    request: executionPrompt,
                    prompt: executionPrompt,
                    rawPrompt: prompt,
                    workspacePath: this.currentProjectPath,
                    projectPath: this.currentProjectPath,
                    targetPath: this.currentProjectPath,
                    model: this.currentModel,
                    history: this.getMessagesForModel(),
                    executionSurface: invocationMode,
                    clientSurface: 'cli',
                    workflowTarget: {
                        id: resolvedWorkflow.id,
                        name: resolvedWorkflow.name,
                        selector: resolvedWorkflow.selector,
                        matchedBy: resolvedWorkflow.matchedBy,
                    },
                    ...runtimeContext,
                },
                executionOptions: {
                    targetType: 'vigflow-workflow',
                    targetSelector: resolvedWorkflow.selector,
                    targetMatchedBy: resolvedWorkflow.matchedBy,
                    executionSurface: invocationMode,
                    clientSurface: 'cli',
                    model: this.currentModel,
                },
            }));
            const content = this.formatWorkflowTargetResult(execution.result);
            const assistantText = content || `Workflow ${resolvedWorkflow.name} completed with status ${execution.status}.`;
            this.messages.push({ role: 'assistant', content: assistantText });
            this.saveSession();
            if (spinner) {
                spinner.stop();
            }
            if (this.jsonOutput) {
                console.log(JSON.stringify({
                    success: true,
                    workflow: resolvedWorkflow,
                    mode: invocationMode,
                    execution,
                    content,
                }, null, 2));
                return;
            }
            this.logger.success(`Workflow target ${resolvedWorkflow.name} ${execution.status}`);
            console.log(chalk.gray(`Workflow ID: ${resolvedWorkflow.id}`));
            console.log(chalk.gray(`Execution ID: ${execution.executionId}`));
            console.log(chalk.gray(`Mode: ${invocationMode}`));
            if (content) {
                console.log(content);
            }
        }
        catch (error) {
            if (spinner) {
                spinner.stop();
                this.logger.error('Workflow target execution failed');
            }
            throw error;
        }
    }
    async runOperatorTurn(prompt) {
        if (!this.hasOperatorAccess()) {
            this.logger.error(this.operatorAccessMessage());
            return;
        }
        // ── Fast-path: direct-answerable prompts skip the full BMAD workflow ──
        // The BMAD orchestrator scans the entire workspace (can be 1000+ files)
        // even for trivial prompts like "Reply with exactly: OK".  Route these
        // through the regular chat API with a strong operator system message
        // so the user gets an instant, grounded answer.
        if (this.isOperatorDirectAnswerable(prompt)) {
            await this.runOperatorDirectAnswer(prompt);
            return;
        }
        // ── Repo-grounded operator path: try V3 agent first, then fall back ──
        // Prompts that reference files, code symbols, or analysis verbs need
        // tool access to read actual file contents.  Route these through the
        // V3 agent (which archives runs for history/replay/fork and supports
        // workspace hydration) first.  Falls back to local agent loop if V3
        // is unreachable.
        if (this.isRepoGroundedPrompt(prompt) && this.tools) {
            this.operatorMode = true;
            const handledByV3 = await this.tryV3AgentWorkflow(prompt);
            if (handledByV3) {
                this.saveSession();
                return;
            }
            await this.runLocalAgentLoop(prompt);
            return;
        }
        getBridgeClient()?.emitPrompt({ prompt, mode: 'operator', model: this.currentModel });
        const runtimeContext = await this.getPromptRuntimeContext(prompt);
        const spinner = this.jsonOutput ? null : createSpinner({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
        const workflowType = 'full';
        const executionPrompt = this.buildExecutionPrompt(prompt);
        try {
            this.rememberBrainEvent('task', `GoA operator workflow started for prompt: ${prompt.slice(0, 220)}`, 'operator');
            const response = await this.callApi('Run operator workflow', () => this.api.runOperatorWorkflow(executionPrompt, {
                workspacePath: this.currentProjectPath,
                projectPath: this.currentProjectPath,
                targetPath: this.currentProjectPath,
                rawPrompt: prompt,
                executionSurface: 'cli',
                clientSurface: 'cli',
                history: this.getMessagesForModel(),
                workflowType,
                model: this.currentModel,
                savePlanToVigFlow: this.savePlanToVigFlow,
                ...runtimeContext,
                onStreamEvent: spinner ? (event) => this.updateOperatorSpinner(spinner, event) : undefined,
            }));
            if (spinner) {
                spinner.stop();
            }
            // Guard: if the operator workflow returned a useless policy
            // acknowledgement instead of a real grounded answer, fail closed.
            const responseText = (response.content || '').trim();
            const isPolicyAck = /^(i will follow|i understand|i('ll| will) adhere|understood[.,!]|sure[.,!]|provide your|waiting for|i('ll| will) proceed)/i.test(responseText)
                || (responseText.length < 200 && /follow the instructions|next instruction|ready to assist/i.test(responseText));
            if (isPolicyAck) {
                throw new CLIError('Operator workflow returned a non-actionable acknowledgement instead of a grounded result.', 'model_backend');
            }
            if (this.jsonOutput) {
                console.log(JSON.stringify({
                    success: true,
                    mode: 'operator',
                    workflowId: response.workflowId,
                    savedWorkflow: response.savedWorkflow || null,
                    contextId: response.contextId || null,
                    content: response.content || 'Operator workflow completed.',
                    metadata: response.metadata || {},
                }, null, 2));
            }
            else {
                console.log(response.content || 'Operator workflow completed.');
                if (response.savedWorkflow?.id) {
                    console.log(chalk.gray(`Saved VigFlow workflow: ${response.savedWorkflow.id}${response.savedWorkflow.name ? ` (${response.savedWorkflow.name})` : ''}`));
                }
            }
            this.rememberBrainEvent('validation', `GoA operator workflow completed${response.workflowId ? ` workflow ${response.workflowId}` : ''}${response.savedWorkflow?.id ? ` saved VigFlow ${response.savedWorkflow.id}` : ''}.`, 'operator');
            this.messages.push({ role: 'assistant', content: response.content || 'Operator workflow completed.' });
            this.saveSession();
        }
        catch (error) {
            if (spinner) {
                spinner.stop();
            }
            const cliErr = error instanceof CLIError ? error : classifyError(error);
            const errorMsg = formatCLIError(cliErr);
            if (!this.jsonOutput) {
                this.logger.error('Operator workflow failed');
            }
            this.rememberBrainEvent('issue', `GoA operator workflow failed: ${errorMsg}`, 'operator');
            if (this.jsonOutput) {
                process.exitCode = 1;
                console.log(JSON.stringify({
                    success: false,
                    mode: 'operator',
                    content: '',
                    error: errorMsg,
                    errorCategory: cliErr.category,
                }, null, 2));
            }
            else {
                this.logger.error(errorMsg);
            }
        }
    }
    /**
     * Fast-path for operator prompts that don't need the full BMAD workflow.
     * Uses the regular chat API with a strong operator system message so
     * the user gets an instant, grounded answer instead of waiting for the
     * BMAD orchestrator to scan the workspace.
     */
    async runOperatorDirectAnswer(prompt) {
        getBridgeClient()?.emitPrompt({ prompt, mode: 'operator-direct', model: this.currentModel });
        const spinner = this.jsonOutput ? null : createSpinner({ text: 'Operator (direct)...', spinner: 'clock' }).start();
        try {
            let operatorGrounding = [
                'You are Vigthoria Operator, a DevOps and infrastructure analysis assistant.',
                `Workspace: ${this.currentProjectPath}`,
                'Answer the user\'s question directly with a concrete, actionable answer.',
                'Do NOT acknowledge instructions. Do NOT say "provide your next instruction".',
                'If asked to produce exact output, produce exactly that output with no preamble.',
            ].join('\n');
            try {
                const snapshot = this.callSyncApi('Get agent workspace snapshot', () => this.api.getAgentWorkspaceSnapshot(this.currentProjectPath));
                if (snapshot && snapshot.paths.length > 0) {
                    const listing = snapshot.paths.slice(0, 80).join('\n');
                    operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
                    if (snapshot.fileCount > 80) {
                        operatorGrounding += `\n... and ${snapshot.fileCount - 80} more files.`;
                    }
                }
            }
            catch { /* ignore */ }
            const chatMessages = [
                { role: 'system', content: operatorGrounding },
                ...this.getMessagesForModel().filter(m => m.role !== 'system'),
                { role: 'user', content: prompt },
            ];
            const response = await this.callApi('Send chat message', () => this.api.chat(chatMessages, this.currentModel));
            if (spinner)
                spinner.stop();
            const content = (response.message || '').trim();
            const isPolicyAck = /^(i will follow|i understand|i('ll| will) adhere|understood[.,!]|sure[.,!]|provide your|waiting for|i('ll| will) proceed)/i.test(content)
                || (content.length < 200 && /follow the instructions|next instruction|ready to assist/i.test(content));
            if (isPolicyAck || !content) {
                throw new CLIError('Operator returned a non-actionable acknowledgement instead of a grounded result.', 'model_backend');
            }
            if (this.jsonOutput) {
                console.log(JSON.stringify({
                    success: true,
                    mode: 'operator',
                    content,
                    metadata: { source: 'operator-direct', mode: 'operator' },
                }, null, 2));
            }
            else {
                console.log(content);
            }
            this.rememberBrainEvent('validation', `GoA operator direct answer completed for prompt: ${prompt.slice(0, 220)}`, 'operator');
            this.messages.push({ role: 'assistant', content });
            this.saveSession();
        }
        catch (error) {
            if (spinner)
                spinner.stop();
            const cliErr = error instanceof CLIError ? error : classifyError(error);
            const errorMsg = formatCLIError(cliErr);
            if (this.jsonOutput) {
                process.exitCode = 1;
                console.log(JSON.stringify({
                    success: false,
                    mode: 'operator',
                    content: '',
                    error: errorMsg,
                    errorCategory: cliErr.category,
                }, null, 2));
            }
            else {
                this.logger.error('Operator direct answer failed');
                this.logger.error(errorMsg);
            }
        }
    }
    async runSimplePrompt(prompt) {
        if (!this.directPromptMode && !this.operatorMode) {
            const isWriteFollowUp = isConfirmationFollowUp(prompt) || isWritePermissionGrant(prompt);
            if (isWriteFollowUp && (this.lastActionableUserInput || this.getPreviousActionablePrompt())) {
                if (!this.tools) {
                    throw new Error('Agent tools are not initialized.');
                }
                this.agentMode = true;
                this.syncInteractiveModeModel('agent');
                if (this.currentSession) {
                    this.currentSession.agentMode = true;
                    this.currentSession.model = this.currentModel;
                }
                console.log(chalk.yellow('Write confirmation detected — switching to Agent mode with full file access.'));
                await this.runAgentTurn(prompt);
                return;
            }
            const promptToRun = isConfirmationFollowUp(prompt) && this.lastActionableUserInput && this.isRepoGroundedPrompt(this.lastActionableUserInput)
                ? this.lastActionableUserInput
                : prompt;
            if (this.isRepoGroundedPrompt(promptToRun) || /\b(build|implement|complete|fix|create|make|edit|write|change|finish)\b/i.test(promptToRun)) {
                if (!this.tools) {
                    throw new Error('Agent tools are not initialized.');
                }
                this.agentMode = true;
                this.syncInteractiveModeModel('agent');
                if (this.currentSession) {
                    this.currentSession.agentMode = true;
                    this.currentSession.model = this.currentModel;
                }
                console.log(chalk.yellow('This request needs file access, so I am switching to Agent mode and working in this workspace now.'));
                console.log(chalk.gray('You do not need to confirm with "yes"; I will continue until the agent run finishes or reports a blocker.'));
                await this.runAgentTurn(promptToRun);
                return;
            }
        }
        this.lastActionableUserInput = prompt;
        // For direct --prompt mode with simple prompts, use a minimal system
        // message to avoid polluting the response with tool/platform context.
        if (this.directPromptMode && !this.messages.some(m => m.role === 'system')) {
            if (this.operatorMode) {
                // Operator direct prompts need workspace grounding so the model
                // answers from real project context instead of being generic.
                let operatorGrounding = [
                    'You are Vigthoria Operator, a DevOps and infrastructure analysis assistant.',
                    `Workspace: ${this.currentProjectPath}`,
                    'Answer the user\'s question directly using the project context below.',
                    'Do NOT acknowledge instructions. Do NOT say "provide your next instruction".',
                    'Produce a concrete, actionable answer grounded in the real project files.',
                ].join('\n');
                const snapshot = this.callSyncApi('Get operator workspace snapshot', () => this.api.getAgentWorkspaceSnapshot(this.currentProjectPath));
                if (snapshot && snapshot.paths.length > 0) {
                    const listing = snapshot.paths.slice(0, 80).join('\n');
                    operatorGrounding += `\n\nProject file listing (${snapshot.fileCount} files):\n${listing}`;
                    if (snapshot.fileCount > 80) {
                        operatorGrounding += `\n... and ${snapshot.fileCount - 80} more files.`;
                    }
                }
                this.messages.push({ role: 'system', content: operatorGrounding });
            }
            else {
                this.messages.push({
                    role: 'system',
                    content: 'Answer the user\'s question directly and concisely. Do not describe tools, platform constraints, or capabilities unless explicitly asked. If the user\'s instruction is to produce specific output, produce exactly that output with no preamble.',
                });
            }
        }
        else if (!this.directPromptMode) {
            // Interactive mode: inject grounding for file-related queries
            const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
            if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
                let groundingContent = 'You are in simple chat mode with no direct file access or tools. Do not fabricate file contents, search results, or analysis steps. If the user asks you to modify, edit, or deeply inspect files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.';
                try {
                    const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
                    if (snapshot && snapshot.paths.length > 0) {
                        const listing = snapshot.paths.slice(0, 60).join('\n');
                        groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
                        if (snapshot.fileCount > 60) {
                            groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
                        }
                    }
                }
                catch { /* ignore snapshot errors */ }
                this.messages.push({
                    role: 'system',
                    content: groundingContent,
                });
            }
        }
        this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
        getBridgeClient()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
        const spinner = this.jsonOutput ? null : createSpinner({ text: 'Thinking...', spinner: 'clock' }).start();
        try {
            const response = await this.callApi('Send chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel));
            if (spinner)
                spinner.stop();
            const finalText = this.sanitizeDirectModeOutput(this.stripHiddenThoughtBlocks(response.message || '')).trim();
            const effectiveModel = String(response.model || this.currentModel);
            const metadata = this.modelGovernanceFallback
                ? { modelFallback: this.modelGovernanceFallback }
                : undefined;
            if (this.jsonOutput) {
                console.log(JSON.stringify({
                    success: true,
                    mode: 'chat',
                    model: effectiveModel,
                    requestedModel: this.modelGovernanceFallback?.requestedModel || this.currentModel,
                    metadata,
                    content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
                }, null, 2));
            }
            else if (finalText) {
                console.log(finalText);
            }
            else {
                console.log(chalk.yellow('The model returned an empty response. Try rephrasing your question, or use --agent mode for grounded repo analysis.'));
            }
            this.messages.push({ role: 'assistant', content: response.message || '' });
            this.saveSession();
        }
        catch (error) {
            if (spinner)
                spinner.stop();
            const cliErr = error instanceof CLIError ? error : classifyError(error);
            const errorMsg = formatCLIError(cliErr);
            if (this.jsonOutput) {
                process.exitCode = 1;
                console.log(JSON.stringify({
                    success: false,
                    mode: 'chat',
                    model: this.currentModel,
                    content: '',
                    error: errorMsg,
                    errorCategory: cliErr.category,
                }, null, 2));
            }
            else {
                this.logger.error(errorMsg);
            }
        }
    }
    async runAgentTurn(prompt) {
        if (!this.tools) {
            throw new Error('Agent tools are not initialized.');
        }
        const { goaContext, userPrompt } = splitGoaContextFromInput(prompt);
        if (goaContext) {
            this.pendingGoaContext = goaContext;
        }
        const effectivePrompt = userPrompt || prompt;
        if (this.pendingGoaContext) {
            this.messages.push({
                role: 'system',
                content: formatGoaSystemGrounding(this.pendingGoaContext),
            });
            this.pendingGoaContext = null;
        }
        const resolvedPrompt = this.resolveAgentTurnPrompt(effectivePrompt);
        this.lastAgentRoute = await resolveAgentRoute(this.api, resolvedPrompt);
        if (!this.jsonOutput) {
            const r = this.lastAgentRoute;
            const routerNote = r.routerLatencyMs
                ? chalk.gray(` (${r.routerModel || 'balanced-4b'} ${r.routerLatencyMs}ms)`)
                : '';
            this.logger.debug(`Agent route: ${r.path} / ${r.taskKind} [${r.source}]${routerNote} — ${r.reason}`);
        }
        const handledByV3Workflow = await this.tryV3AgentWorkflow(resolvedPrompt);
        if (handledByV3Workflow) {
            this.saveSession();
            return;
        }
        const errorMessage = 'The V3 planner/executor workflow is unavailable. Refusing to fall back to legacy template, direct-file, or local write paths.';
        this.logger.error(errorMessage);
        this.messages.push({ role: 'assistant', content: errorMessage });
        process.exitCode = 1;
        this.saveSession();
    }
    resolveAgentTurnPrompt(prompt) {
        if (isBuiltContinuePrompt(prompt) || isBuiltRetryPrompt(prompt) || isBuiltWriteConfirmationPrompt(prompt)) {
            return prompt;
        }
        if (isAgentContinuePrompt(prompt)) {
            const followUp = this.buildContinuePrompt();
            if (followUp) {
                if (!this.jsonOutput) {
                    console.log(chalk.cyan('↪ Treating your message as /continue — resuming the previous agent run.'));
                }
                return followUp;
            }
        }
        if (isAgentRetryPrompt(prompt)) {
            const followUp = this.buildRetryPrompt();
            if (followUp) {
                if (!this.jsonOutput) {
                    console.log(chalk.cyan('↪ Treating your message as /retry — re-running failed tasks from the previous run.'));
                }
                return followUp;
            }
        }
        if ((isConfirmationFollowUp(prompt) || isWritePermissionGrant(prompt)) && !isAgentContinuePrompt(prompt)) {
            const expanded = this.buildContextualAgentPrompt(prompt);
            if (expanded !== prompt) {
                if (!this.jsonOutput) {
                    console.log(chalk.cyan('↪ Treating your message as write confirmation — applying the previous task with full write access.'));
                }
                return expanded;
            }
        }
        return prompt;
    }
    shouldPreserveBuildOutcomeForContinue(runPrompt, prior) {
        if (this.taskRequiresWorkspaceChanges(runPrompt)) {
            return false;
        }
        if (isBuiltContinuePrompt(runPrompt) || isBuiltRetryPrompt(runPrompt)) {
            return false;
        }
        const chainPrompt = prior.originalPrompt || prior.prompt;
        if (!this.taskRequiresWorkspaceChanges(chainPrompt)) {
            return false;
        }
        return isAgentContinuePrompt(runPrompt);
    }
    resolveOriginalPrompt(runPrompt) {
        const prior = this.lastAgentRunOutcome;
        if (prior?.originalPrompt) {
            return prior.originalPrompt;
        }
        if (this.taskRequiresWorkspaceChanges(runPrompt) && !isBuiltContinuePrompt(runPrompt) && !isBuiltRetryPrompt(runPrompt)) {
            return runPrompt;
        }
        if (prior && this.taskRequiresWorkspaceChanges(prior.originalPrompt || prior.prompt)) {
            return prior.originalPrompt || prior.prompt;
        }
        return null;
    }
    commitAgentRunOutcome(outcome, runPrompt) {
        const prior = this.lastAgentRunOutcome;
        if (prior && this.shouldPreserveBuildOutcomeForContinue(runPrompt, prior)) {
            if (!this.jsonOutput) {
                console.log(chalk.yellow('↪ Keeping the previous build run state — use /continue to resume implementation.'));
            }
            return;
        }
        outcome.originalPrompt = this.resolveOriginalPrompt(runPrompt);
        this.lastAgentRunOutcome = outcome;
    }
    printResumeContinuationHint() {
        const outcome = this.lastAgentRunOutcome;
        if (!outcome) {
            return;
        }
        const sourcePrompt = outcome.originalPrompt || outcome.prompt;
        const isBuild = this.taskRequiresWorkspaceChanges(sourcePrompt);
        const openWork = outcome.failedTaskIds.length > 0
            || outcome.unfinishedTaskIds.length > 0
            || (outcome.tasksTotal > 0 && outcome.tasksSucceeded < outcome.tasksTotal);
        if (!isBuild && !openWork) {
            return;
        }
        console.log('');
        console.log(chalk.cyan('Prior agent run restored for this project.'));
        if (openWork) {
            const taskIds = [...new Set([...outcome.failedTaskIds, ...outcome.unfinishedTaskIds])].slice(0, 8);
            if (taskIds.length > 0) {
                console.log(chalk.gray(`  Unfinished tasks: ${taskIds.join(', ')}`));
            }
            else {
                console.log(chalk.gray(`  Progress: ${outcome.tasksSucceeded}/${outcome.tasksTotal} tasks completed`));
            }
        }
        console.log(chalk.gray('  Type ') + chalk.cyan('/continue') + chalk.gray(' to resume implementation, or ') + chalk.cyan('/retry') + chalk.gray(' for failed tasks only.'));
        console.log('');
    }
    buildLocalLoopLiveOutcome(prompt) {
        const liveOutcome = createLiveOutcome();
        liveOutcome.requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
        liveOutcome.deepAnalysisRequired = this.isDeepAnalysisPrompt(prompt);
        liveOutcome.analysisToolsUsed = this.agentToolEvidence.discovery;
        liveOutcome.changedFileCount = this.agentToolEvidence.mutation;
        liveOutcome.workspaceHasOutput = this.agentToolEvidence.mutation > 0;
        if (this.agentToolEvidence.searchFailed > 0 && this.agentToolEvidence.discovery === 0) {
            liveOutcome.executorFailed = true;
            liveOutcome.executorError = 'Search tools failed before gathering workspace evidence';
        }
        return liveOutcome;
    }
    persistLocalLoopOutcome(prompt, evaluation, liveOutcome) {
        this.commitAgentRunOutcome({
            prompt,
            originalPrompt: null,
            taskId: null,
            contextId: null,
            tasksSucceeded: liveOutcome.tasksSucceeded,
            tasksTotal: liveOutcome.tasksTotal,
            failedTaskIds: [...liveOutcome.failedTaskIds],
            unfinishedTaskIds: [],
            qualityScore: null,
            qualityMissing: [],
            qualityBlockers: evaluation.executorSucceeded ? [] : [evaluation.statusHeadline],
            hasOutput: liveOutcome.workspaceHasOutput,
            answerContent: liveOutcome.answerContent || null,
            selfHealStatus: 'skipped',
            selfHealTool: null,
            plannerError: null,
            executorError: liveOutcome.executorError,
            clientToolErrors: [],
            transportErrors: [],
            workspacePath: this.currentProjectPath || null,
            workspaceSyncIssue: null,
            finishedAt: Date.now(),
        }, prompt);
    }
    async runLocalAgentLoop(prompt) {
        if (!this.tools) {
            throw new Error('Agent tools are not initialized.');
        }
        this.lastActionableUserInput = prompt;
        this.directToolContinuationCount = 0;
        this.agentToolEvidence = { discovery: 0, mutation: 0, searchFailed: 0 };
        this.tools.clearSessionApprovals();
        getBridgeClient()?.emitPrompt({ prompt, mode: this.operatorMode ? 'operator' : 'agent', model: this.currentModel });
        this.ensureAgentSystemPrompt();
        this.messages.push({ role: 'user', content: this.buildScopedUserPrompt(prompt) });
        this.saveSession();
        const maxTurns = 10;
        for (let turn = 0; turn < maxTurns; turn += 1) {
            const spinner = this.jsonOutput ? null : createSpinner({ text: turn === 0 ? 'Planning...' : 'Continuing...', spinner: 'clock' }).start();
            let response;
            try {
                response = await this.callApi('Send agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel), 0);
            }
            catch (firstErr) {
                // If we already gathered evidence and the model API fails on a
                // continuation turn, retry once after a short delay.
                if (turn > 0 && this.agentToolEvidence.discovery > 0) {
                    this.logger.debug('Agent continuation API call failed, retrying once...');
                    try {
                        await new Promise(r => setTimeout(r, 2000));
                        response = await this.callApi('Retry agent chat message', () => this.api.chat(this.getMessagesForModel(), this.currentModel), 0);
                    }
                    catch (retryErr) {
                        // Retry also failed — synthesize an answer from evidence
                        if (spinner)
                            spinner.stop();
                        const evidenceSummary = this.synthesizeEvidenceFromHistory();
                        if (evidenceSummary) {
                            const fallbackContent = this.sanitizeDirectModeOutput(evidenceSummary);
                            const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
                            const evaluation = evaluateExecutorSuccess(liveOutcome);
                            this.persistLocalLoopOutcome(prompt, evaluation, liveOutcome);
                            if (this.jsonOutput) {
                                process.exitCode = evaluation.executorSucceeded ? 0 : 1;
                                console.log(JSON.stringify({
                                    success: evaluation.executorSucceeded,
                                    mode: 'agent',
                                    model: this.currentModel,
                                    partial: !evaluation.executorSucceeded,
                                    content: fallbackContent,
                                    statusHeadline: evaluation.statusHeadline,
                                    metadata: {
                                        executionPath: 'local-agent-loop',
                                        recovered: true,
                                        outcomeTruth: {
                                            executorSucceeded: evaluation.executorSucceeded,
                                            uiTheme: evaluation.uiTheme,
                                        },
                                    },
                                }, null, 2));
                            }
                            else {
                                console.log(fallbackContent);
                            }
                            this.saveSession();
                            return;
                        }
                        throw retryErr;
                    }
                }
                else {
                    const cliErr = firstErr instanceof CLIError ? firstErr : classifyError(firstErr);
                    const formatted = formatCLIError(cliErr);
                    if (spinner)
                        spinner.stop();
                    this.rememberBrainEvent('issue', 'Agent API preflight failed: ' + formatted, 'agent');
                    if (this.jsonOutput) {
                        process.exitCode = 1;
                        console.log(JSON.stringify({
                            success: false,
                            mode: 'agent',
                            model: this.currentModel,
                            partial: false,
                            content: '',
                            error: formatted,
                            errorCategory: cliErr.category,
                            metadata: { executionPath: 'local-agent-loop' },
                        }, null, 2));
                    }
                    else {
                        this.logger.error(formatted);
                    }
                    return;
                }
            }
            if (spinner)
                spinner.stop();
            try {
                const assistantMessage = response.message || '';
                this.messages.push({ role: 'assistant', content: assistantMessage });
                const toolCalls = this.extractToolCalls(assistantMessage);
                const visibleText = this.stripToolPayloads(assistantMessage).trim();
                getBridgeClient()?.emitModelResponse({
                    model: this.currentModel,
                    chars: assistantMessage.length,
                    hasToolCalls: toolCalls.length > 0,
                    preview: visibleText.slice(0, 300),
                });
                if (toolCalls.length === 0) {
                    // Phase 5: Quality gate — reject non-answers and push the model
                    // to gather real evidence.
                    const isPolicyAck = /^(i will follow|i understand|i('ll| will) adhere|understood[.,!]|sure[.,!]|i('ll| will) use the tools|i('ll| will) proceed|let me know|provide your|waiting for)/i.test(visibleText.trim());
                    // Detect if the response is effectively empty or just a follow-up
                    // question after we strip tool output echoes.
                    const sanitized = this.sanitizeDirectModeOutput(visibleText.trim());
                    const isFollowUp = this.isDirectModeFollowUpQuestion(sanitized);
                    const isEmptyAfterSanitize = !sanitized || sanitized.length < 10;
                    // Detect resignation: model gives up saying files/things were "not found"
                    // without having tried list_dir to discover the correct path.
                    const isResignation = /(?:not found|cannot be (?:determined|compared|completed)|do not exist|does not exist|unable to locate|neither.*exist|could not (?:find|locate)|no (?:such|matching) file)/i.test(sanitized) && this.agentToolEvidence.discovery < 4;
                    // Gate 1: First turn with no discovery at all
                    const gate1 = turn === 0 && this.agentToolEvidence.discovery === 0 && (this.isDiagnosticPrompt(prompt) || this.directPromptMode || isPolicyAck);
                    // Gate 2: Any turn where the response is just a follow-up question,
                    // tool-failure echoes, or premature resignation (the model gave up
                    // instead of retrying with list_dir to find the correct paths)
                    const gate2 = this.directPromptMode && turn < 6 && (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation);
                    // Gate 3: Model outputs code blocks as text instead of using write_file.
                    // If the response contains ``` code fences but no write_file was called,
                    // reject and instruct the model to use write_file.
                    const hasCodeBlocks = (sanitized.match(/```/g) || []).length >= 2;
                    const gate3 = hasCodeBlocks && this.agentToolEvidence.mutation === 0 && turn < 6;
                    if (gate1 || gate2 || gate3) {
                        // Remove the useless response from history
                        if (isPolicyAck || isFollowUp || isEmptyAfterSanitize || isResignation || gate3) {
                            this.messages.pop();
                        }
                        const hint = gate3
                            ? 'You output code as text instead of writing files. Use write_file to create each file on disk. Do NOT output code in markdown fences — call write_file for every file.'
                            : isResignation
                                ? 'Files were not found at the guessed paths. Use list_dir to discover the correct directory structure, then read_file with the correct paths.'
                                : `Start by running: list_dir on the project root, then read_file on files relevant to: ${prompt}`;
                        this.messages.push({
                            role: 'system',
                            content: [
                                'Quality gate: your response was rejected because it did not answer the user\'s question.',
                                'You MUST use tools to gather concrete evidence IMMEDIATELY.',
                                hint,
                                'Respond ONLY with <tool_call> blocks. No other text.',
                            ].join('\n'),
                        });
                        this.directToolContinuationCount += 1;
                        continue;
                    }
                    const finalContent = this.resolveDirectModeCompletion(prompt, visibleText);
                    const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
                    const evaluation = evaluateExecutorSuccess(liveOutcome);
                    this.persistLocalLoopOutcome(prompt, evaluation, liveOutcome);
                    if (this.jsonOutput) {
                        process.exitCode = evaluation.executorSucceeded ? 0 : 1;
                        console.log(JSON.stringify({
                            success: evaluation.executorSucceeded,
                            mode: 'agent',
                            model: this.currentModel,
                            partial: !evaluation.executorSucceeded,
                            content: finalContent,
                            statusHeadline: evaluation.statusHeadline,
                            metadata: {
                                executionPath: 'local-agent-loop',
                                outcomeTruth: {
                                    executorSucceeded: evaluation.executorSucceeded,
                                    uiTheme: evaluation.uiTheme,
                                    analysisToolsUsed: liveOutcome.analysisToolsUsed,
                                    changedFileCount: liveOutcome.changedFileCount,
                                },
                            },
                        }, null, 2));
                    }
                    else {
                        console.log(finalContent);
                    }
                    this.saveSession();
                    return;
                }
                await this.executeToolCalls(toolCalls);
                this.directToolContinuationCount += 1;
                this.messages.push({
                    role: 'system',
                    content: this.buildContinuationPrompt(),
                });
                this.saveSession();
            }
            catch (error) {
                if (spinner)
                    spinner.stop();
                const cliErr = error instanceof CLIError ? error : classifyError(error);
                const errorMsg = formatCLIError(cliErr);
                this.rememberBrainEvent('issue', `Agent turn failed: ${errorMsg}`, 'agent');
                if (this.jsonOutput) {
                    process.exitCode = 1;
                    console.log(JSON.stringify({
                        success: false,
                        mode: 'agent',
                        model: this.currentModel,
                        partial: false,
                        content: '',
                        error: errorMsg,
                        errorCategory: cliErr.category,
                        metadata: {
                            executionPath: 'local-agent-loop',
                        },
                    }, null, 2));
                }
                else {
                    this.logger.error(errorMsg);
                }
                return;
            }
        }
        const liveOutcome = this.buildLocalLoopLiveOutcome(prompt);
        const exhaustedEval = evaluateExecutorSuccess({
            ...liveOutcome,
            executorFailed: true,
            executorError: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
        });
        this.persistLocalLoopOutcome(prompt, exhaustedEval, liveOutcome);
        if (this.jsonOutput) {
            process.exitCode = 1;
            console.log(JSON.stringify({
                success: false,
                mode: 'agent',
                model: this.currentModel,
                partial: true,
                content: 'Task complete.',
                error: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
                statusHeadline: exhaustedEval.statusHeadline,
                metadata: {
                    executionPath: 'local-agent-loop',
                    outcomeTruth: {
                        executorSucceeded: false,
                        uiTheme: exhaustedEval.uiTheme,
                    },
                },
            }, null, 2));
        }
        else {
            console.log('Task complete.');
        }
        this.saveSession();
    }
    async primeBypassedTargetFileContext(prompt) {
        if (!this.directPromptMode || !this.tools) {
            return;
        }
        const targetFile = this.inferTargetFileFromPrompt(prompt);
        if (!targetFile) {
            return;
        }
        // Only prime if the direct-file flow was bypassed for this file (e.g. HTML routed to V3).
        // This ensures the local agent loop always has full file awareness before planning.
        if (!this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
            return;
        }
        const readCall = {
            tool: 'read_file',
            args: { path: targetFile },
        };
        if (!this.jsonOutput) {
            console.log(chalk.cyan(`⚙ Executing: ${readCall.tool}`));
        }
        const readResult = await this.tools.execute(readCall);
        const readSummary = this.formatToolResult(readCall, readResult);
        if (!this.jsonOutput) {
            console.log(readResult.success ? chalk.gray(readSummary) : chalk.red(readSummary));
        }
        if (readResult.success && readResult.output) {
            this.messages.push({ role: 'system', content: readSummary });
        }
    }
    /**
     * Sub-second path for trivial HTML/hello-world: Template Service → index.html (no V3 planner).
     */
    async tryTemplateInstantPath(prompt) {
        const route = this.lastAgentRoute;
        if (route?.path !== 'template-instant' && !isTrivialHtmlPageRequest(prompt)) {
            return false;
        }
        let workspacePath = this.currentProjectPath;
        try {
            const fromPrompt = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
            if (fromPrompt)
                workspacePath = fromPrompt;
        }
        catch {
            // use default workspace
        }
        if (!this.jsonOutput) {
            console.log();
            console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
            const r = this.lastAgentRoute;
            console.log(chalk.gray(`Reason: ${r?.reason || 'template-instant (trivial HTML page)'}`));
            console.log(chalk.gray(`Path: template-instant [${r?.source || 'regex'}]`));
            console.log(chalk.gray('Model: template-service'));
            console.log(chalk.gray('Cloud Eligible: false'));
            console.log(chalk.gray('Cloud Selected: false'));
            console.log(chalk.gray('━'.repeat(30)));
            console.log();
            console.log(chalk.cyan('⚡ Template instant path — matching template and writing index.html...'));
        }
        this.messages.push({ role: 'user', content: prompt });
        const mustComplete = route?.path === 'template-instant';
        let result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
            allowFallback: true,
        });
        if (!result.ok && mustComplete) {
            result = await runTemplateInstantPath(this.api, prompt, workspacePath, {
                allowFallback: true,
                forceMinimalFallback: true,
            });
        }
        if (!result.ok) {
            if (!this.jsonOutput) {
                console.log(chalk.yellow(`Template instant path skipped: ${result.error || 'no match'}`));
            }
            return false;
        }
        const summary = [
            `Created ${result.entryPath || 'index.html'} via template-instant`,
            result.templateName ? `(${result.templateName})` : '',
            result.processingMs ? `in ${Math.round(result.processingMs)}ms` : '',
            result.usedFallback ? '(minimal fallback)' : '',
        ].filter(Boolean).join(' ');
        this.messages.push({ role: 'assistant', content: summary });
        const liveOutcome = createLiveOutcome();
        liveOutcome.requiresWorkspaceChanges = true;
        liveOutcome.workspaceHasOutput = true;
        liveOutcome.changedFileCount = 1;
        liveOutcome.tasksTotal = 1;
        liveOutcome.tasksSucceeded = 1;
        liveOutcome.answerContent = summary;
        const evaluation = evaluateExecutorSuccess(liveOutcome);
        this.commitAgentRunOutcome({
            prompt,
            originalPrompt: null,
            taskId: null,
            contextId: null,
            tasksSucceeded: 1,
            tasksTotal: 1,
            failedTaskIds: [],
            unfinishedTaskIds: [],
            qualityScore: null,
            qualityMissing: [],
            qualityBlockers: [],
            hasOutput: true,
            answerContent: summary,
            selfHealStatus: 'skipped',
            selfHealTool: null,
            plannerError: null,
            executorError: null,
            clientToolErrors: [],
            transportErrors: [],
            workspacePath: workspacePath || null,
            workspaceSyncIssue: null,
            finishedAt: Date.now(),
        }, prompt);
        if (!this.jsonOutput) {
            console.log(chalk.green(`✓ ${summary}`));
            console.log(chalk.gray('  Run: vigthoria preview'));
            this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
        }
        else {
            const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
            console.log(JSON.stringify({
                success: evaluation.executorSucceeded,
                mode: 'agent',
                model: routingPolicy.selectedModel,
                routingPolicy,
                taskId: null,
                contextId: null,
                partial: false,
                content: summary,
                statusHeadline: evaluation.statusHeadline,
                tasksSucceeded: 1,
                tasksTotal: 1,
                metadata: {
                    source: 'template-instant',
                    templateName: result.templateName,
                    processingMs: result.processingMs,
                    route: this.lastAgentRoute,
                },
            }, null, 2));
        }
        return true;
    }
    async tryDirectSingleFileFlow(prompt) {
        if (!this.directPromptMode || !this.tools) {
            return false;
        }
        const targetFile = this.inferTargetFileFromPrompt(prompt);
        if (!targetFile) {
            return false;
        }
        if (this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
            return false;
        }
        const readCall = {
            tool: 'read_file',
            args: { path: targetFile },
        };
        if (!this.jsonOutput) {
            console.log(chalk.cyan(`⚙ Executing: ${readCall.tool}`));
        }
        const readResult = await this.tools.execute(readCall);
        const readSummary = this.formatToolResult(readCall, readResult);
        if (!this.jsonOutput) {
            console.log(readResult.success ? chalk.gray(readSummary) : chalk.red(readSummary));
        }
        this.messages.push({ role: 'system', content: readSummary });
        if (!readResult.success || !readResult.output) {
            return false;
        }
        let rewrittenContent = this.tryDeterministicSingleFileRewrite(prompt, targetFile, readResult.output);
        if (!rewrittenContent) {
            const rewriteMessages = [
                {
                    role: 'system',
                    content: [
                        'You are repairing a single file for a CLI agent task.',
                        `Return only the final contents for ${targetFile}.`,
                        'Do not use Markdown fences.',
                        'Do not add explanations before or after the file contents.',
                        'Produce complete, runnable code or markup.',
                    ].join('\n'),
                },
                {
                    role: 'user',
                    content: [
                        `Task: ${prompt}`,
                        `Target file: ${targetFile}`,
                        'Current file contents:',
                        readResult.output,
                    ].join('\n\n'),
                },
            ];
            const rewriteResponse = await this.callApi('Rewrite target file', () => this.api.chat(rewriteMessages, this.currentModel));
            rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
        }
        if (!rewrittenContent) {
            return false;
        }
        const writeCall = {
            tool: 'write_file',
            args: {
                path: targetFile,
                content: rewrittenContent,
            },
        };
        if (!this.jsonOutput) {
            console.log(chalk.cyan(`⚙ Executing: ${writeCall.tool}`));
        }
        const writeResult = await this.tools.execute(writeCall);
        const writeSummary = this.formatToolResult(writeCall, writeResult);
        if (!this.jsonOutput) {
            console.log(writeResult.success ? chalk.gray(writeSummary) : chalk.red(writeSummary));
        }
        this.messages.push({ role: 'system', content: writeSummary });
        if (!writeResult.success) {
            return false;
        }
        const previewGate = await this.callApi('Run Template Service preview gate', () => this.api.runTemplateServicePreviewGate(prompt, {
            workspacePath: this.currentProjectPath,
            projectPath: this.currentProjectPath,
            targetPath: this.currentProjectPath,
            rawPrompt: prompt,
            executionSurface: 'cli',
            clientSurface: 'cli',
        }));
        const success = previewGate.required ? previewGate.passed === true : true;
        if (!success) {
            process.exitCode = 1;
        }
        if (this.jsonOutput) {
            console.log(JSON.stringify({
                success,
                mode: 'agent',
                model: this.currentModel,
                partial: false,
                content: `Updated ${targetFile}.`,
                metadata: {
                    executionPath: 'direct-single-file',
                    targetFile,
                    previewGate,
                },
            }, null, 2));
        }
        else {
            console.log(`Updated ${targetFile}.`);
            if (previewGate.required) {
                if (previewGate.passed) {
                    console.log(chalk.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
                }
                else {
                    console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
                }
            }
        }
        return true;
    }
    taskRequiresWorkspaceChanges(prompt) {
        return promptRequiresWorkspaceChanges(prompt);
    }
    getPreviousActionablePrompt() {
        if (this.lastActionableUserInput && !isConfirmationFollowUp(this.lastActionableUserInput)) {
            return this.lastActionableUserInput;
        }
        for (let i = this.messages.length - 1; i >= 0; i -= 1) {
            const message = this.messages[i];
            if (message.role !== 'user')
                continue;
            const content = (message.content || '').trim();
            if (!content || isConfirmationFollowUp(content) || isWritePermissionGrant(content))
                continue;
            if (isBuiltWriteConfirmationPrompt(content) || isBuiltContinuePrompt(content) || isBuiltRetryPrompt(content))
                continue;
            return content
                .replace(/\n\nProject root:[\s\S]*$/i, '')
                .replace(/\n\nStay within this project root[\s\S]*$/i, '')
                .trim();
        }
        return '';
    }
    buildContextualAgentPrompt(prompt) {
        if (!isConfirmationFollowUp(prompt) && !isWritePermissionGrant(prompt)) {
            return prompt;
        }
        const previousPrompt = this.getPreviousActionablePrompt();
        if (!previousPrompt) {
            return prompt;
        }
        return [
            'The user confirmed they want file changes applied. Execute the original task with full write access.',
            '',
            'Original task:',
            previousPrompt,
            '',
            `User confirmation: ${prompt}`,
            '',
            'Apply the requested file changes now. Do not stop at analysis or ask for permission again.',
            'Do not reinterpret this confirmation as a new website, landing page, template, or index.html task.',
        ].join('\n');
    }
    async tryV3AgentWorkflow(prompt) {
        // Extract explicit workspace path from prompt (if provided by user)
        let promptWorkspacePath = null;
        try {
            promptWorkspacePath = this.resolvePromptWorkspacePath(prompt, this.currentProjectPath);
            if (promptWorkspacePath && !this.jsonOutput) {
                console.log(chalk.cyan(`📁 Workspace from prompt: ${promptWorkspacePath}`));
            }
        }
        catch {
            // Path extraction failed, use default workspace
        }
        const workspacePath = promptWorkspacePath || this.currentProjectPath;
        const contextualPrompt = this.buildContextualAgentPrompt(prompt);
        if (contextualPrompt === prompt && !isConfirmationFollowUp(prompt) && !isWritePermissionGrant(prompt)) {
            this.lastActionableUserInput = prompt;
        }
        this.messages.push({ role: 'user', content: contextualPrompt });
        const runtimeContext = await this.getPromptRuntimeContext(contextualPrompt);
        const routingPolicy = this.resolveAgentExecutionPolicy(contextualPrompt);
        // STREAMING: Log routing decision transparently to user
        if (!this.jsonOutput) {
            console.log();
            console.log(chalk.gray('━━━ ROUTING DECISION ━━━'));
            console.log(chalk.gray(`Reason: ${routingPolicy.routeReason}`));
            console.log(chalk.gray(`Model: ${routingPolicy.selectedModel}`));
            console.log(chalk.gray('Remote Engine Session: true'));
            console.log(chalk.gray(`Cloud PAYG Available: ${routingPolicy.cloudEligible}`));
            console.log(chalk.gray(`Cloud Selected: ${routingPolicy.cloudSelected}`));
            if (routingPolicy.heavyTask) {
                console.log(chalk.gray(`Task Complexity: HEAVY`));
            }
            console.log(chalk.gray('━'.repeat(30)));
            console.log();
        }
        // Reset streaming counters for new workflow
        this.v3IterationCount = 0;
        this.v3ToolCallCount = 0;
        this.v3LastActivity = Date.now();
        this.v3StreamingStarted = false;
        this.v3StreamedAnswerBuffer = '';
        this.v3StreamedAnswerDisplayed = false;
        this.v3StartSeen = false;
        this.v3SeenToolCalls.clear();
        this.v3SeenToolResults.clear();
        emitDeckEvent({ type: 'agent_start' });
        const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], false);
        taskDisplay.start(0);
        const spinner = this.jsonOutput ? null : createSpinner({
            text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
            spinner: 'clock',
            isSilent: true,
        }).start();
        this.startV3IdleWatch(spinner);
        // Live run telemetry, used both for the final summary block and for /retry, /continue.
        const liveOutcome = {
            ...createLiveOutcome(),
            unfinishedTaskIds: new Set(),
            qualityScore: null,
            qualityMissing: [],
            qualityBlockers: [],
        };
        liveOutcome.deepAnalysisRequired = this.isDeepAnalysisPrompt(prompt);
        const parsePlannerSummary = (raw) => {
            if (!raw || typeof raw !== 'string')
                return;
            const succ = raw.match(/(\d+)\s*\/\s*(\d+)\s+tasks?\s+succeeded/i);
            if (succ) {
                liveOutcome.tasksSucceeded = Number(succ[1]) || 0;
                liveOutcome.tasksTotal = Number(succ[2]) || 0;
            }
            const failed = raw.match(/Failed\s+tasks?:\s*([^.\n]+)/i);
            if (failed) {
                failed[1].split(/[,\s]+/).map(s => s.trim()).filter(Boolean)
                    .forEach(t => liveOutcome.failedTaskIds.add(t));
            }
            const unfinished = raw.match(/Unfinished\s+tasks?:\s*([^.\n]+)/i);
            if (unfinished) {
                unfinished[1].split(/[,\s]+/).map(s => s.trim()).filter(Boolean)
                    .forEach(t => liveOutcome.unfinishedTaskIds.add(t));
            }
            const qScore = raw.match(/Quality\s+audit:\s*([0-9.]+)\s*\/\s*100/i);
            if (qScore) {
                const n = Number(qScore[1]);
                if (Number.isFinite(n))
                    liveOutcome.qualityScore = n;
            }
            const blockers = raw.match(/Quality\s+blockers?:\s*([^\n]+?)(?:\s+Quality\s+audit|$)/i);
            if (blockers) {
                blockers[1].split(/;/).map(s => s.trim()).filter(Boolean)
                    .forEach(b => {
                    if (!liveOutcome.qualityBlockers.includes(b))
                        liveOutcome.qualityBlockers.push(b);
                });
            }
            const missingFeatures = raw.match(/Missing\s+features?:\s*([^\n]+)/i);
            if (missingFeatures) {
                missingFeatures[1].split(/[,;]/).map(s => s.trim()).filter(Boolean)
                    .forEach(m => {
                    if (!liveOutcome.qualityMissing.includes(m))
                        liveOutcome.qualityMissing.push(m);
                });
            }
        };
        const executionPrompt = this.buildExecutionPrompt(contextualPrompt);
        const priorPrompt = this.getPreviousActionablePrompt();
        const intentContext = { priorPrompt: priorPrompt || null };
        const classificationPrompt = contextualPrompt;
        const agentTaskType = this.lastAgentRoute?.taskKind
            || inferAgentTaskTypeWithContext(classificationPrompt, intentContext);
        const workflowType = resolveWorkflowType(agentTaskType, classificationPrompt, intentContext);
        const executionHints = buildExecutionHints(agentTaskType, classificationPrompt, {
            fastPath: this.lastAgentRoute?.fastPath,
            priorPrompt: priorPrompt || null,
        });
        const priorOutcome = this.lastAgentRunOutcome;
        const resumeTaskIds = priorOutcome
            ? [...new Set([...(priorOutcome.failedTaskIds || []), ...(priorOutcome.unfinishedTaskIds || [])])]
            : [];
        const shouldResumePlan = isAgentContinuePrompt(contextualPrompt)
            || isAgentRetryPrompt(contextualPrompt)
            || isBuiltContinuePrompt(contextualPrompt)
            || isBuiltRetryPrompt(contextualPrompt);
        const agentExecutionHints = shouldResumePlan
            ? {
                ...executionHints,
                resume_existing_plan: true,
                ...(resumeTaskIds.length > 0 ? { remaining_task_ids: resumeTaskIds } : {}),
            }
            : executionHints;
        const workspaceContext = {
            workspacePath: workspacePath,
            projectPath: workspacePath,
            targetPath: workspacePath,
            ...runtimeContext,
        };
        // Start workspace watcher for bidirectional real-time sync
        let watcher = null;
        if (this.shouldStartWorkspaceWatcher(workspacePath)) {
            watcher = new WorkspaceWatcher({
                workspaceRoot: workspacePath,
                onFileChange: (relativePath, content, action) => {
                    this.logger.debug(`Local change detected: ${action} ${relativePath}`);
                },
            });
            watcher.start();
        }
        try {
            // Ensure the V3 service key is available before health check
            await this.api.ensureV3ServiceKey();
            if (spinner)
                this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Checking V3 connection (preflight)...' });
            // Early health check: fail fast if V3 is not available
            const healthCheck = await this.api.runV3HealthCheck();
            if (!healthCheck.healthy) {
                const errorMsg = healthCheck.error || 'V3 agent service is not responding. The service may be down or unreachable.';
                this.logger.error('V3 agent health check failed: ' + errorMsg);
                throw new Error(errorMsg);
            }
            if (spinner)
                this.updateV3AgentSpinner(spinner, { type: 'v3_status', content: 'Connected to V3. Starting agent execution...' });
            const workflowPromise = this.api.runV3AgentWorkflow(executionPrompt, {
                workspace: { path: workspacePath },
                ...workspaceContext,
                agentTaskType,
                workflowType,
                executionHints: agentExecutionHints,
                fastPath: this.lastAgentRoute?.fastPath,
                agentRoute: this.lastAgentRoute,
                executionSurface: 'cli',
                clientSurface: 'cli',
                localMachineCapable: true,
                agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, agentTaskType, prompt),
                agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
                agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
                model: routingPolicy.selectedModel,
                requestedModel: this.currentModel,
                agentExecutionPolicy: routingPolicy,
                legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
                rawPrompt: prompt,
                contextualPrompt,
                history: this.getMessagesForModel(),
                ...runtimeContext,
                onStreamEvent: (event) => {
                    if (isV3StreamKeepaliveEvent(event)) {
                        return;
                    }
                    if (event.type === 'plan') {
                        taskDisplay.complete(0);
                        const tasks = event?.plan?.tasks;
                        if (Array.isArray(tasks) && tasks.length > 0) {
                            taskDisplay.start(1);
                            liveOutcome.tasksTotal = tasks.length;
                        }
                    }
                    else if (event.type === 'executor_start') {
                        taskDisplay.start(1);
                    }
                    else if (event.type === 'executor_complete') {
                        const summary = event.summary || {};
                        handleTaskEvent(summary, liveOutcome);
                        if (summary.status === 'failed' || summary.status === 'stalled_error') {
                            const err = typeof summary.error === 'string' ? summary.error.trim() : '';
                            if (err && !liveOutcome.executorError)
                                liveOutcome.executorError = err;
                        }
                    }
                    else if (event.type === 'tool_call' || event.type === 'tool_result') {
                        const toolName = String(event.name || event.tool || '').trim();
                        if (toolName)
                            noteAnalysisToolUse(toolName, liveOutcome);
                    }
                    else if (event.type === 'complete') {
                        const tt = Number(event.tasks_total);
                        if (Number.isFinite(tt) && tt > 0) {
                            liveOutcome.tasksTotal = tt;
                        }
                        if (Number(event.discovery_tools_used) > 0) {
                            liveOutcome.analysisToolsUsed = Math.max(liveOutcome.analysisToolsUsed, Number(event.discovery_tools_used));
                        }
                        if (Number(event.discovery_read_files) > 0) {
                            liveOutcome.analysisReadToolsUsed = Math.max(liveOutcome.analysisReadToolsUsed, Number(event.discovery_read_files));
                        }
                        if (event.analysis_deep_required === true) {
                            liveOutcome.deepAnalysisRequired = true;
                        }
                        taskDisplay.complete(1);
                    }
                    else if (event.type === 'executor_error') {
                        const msg = typeof event.error === 'string' ? event.error : '';
                        if (msg)
                            liveOutcome.executorError = msg;
                    }
                    else if (event.type === 'error') {
                        const msg = typeof event.message === 'string' ? event.message : '';
                        parsePlannerSummary(msg);
                        const plannerSummary = /Planner-Executor completed/i.test(msg);
                        const allTasksSucceeded = liveOutcome.tasksTotal > 0
                            && liveOutcome.tasksSucceeded >= liveOutcome.tasksTotal
                            && liveOutcome.failedTaskIds.size === 0;
                        if (plannerSummary) {
                            if (!allTasksSucceeded && !liveOutcome.plannerError) {
                                liveOutcome.plannerError = msg;
                            }
                        }
                        else if (/plan|planner|dependency graph/i.test(msg)) {
                            if (!liveOutcome.plannerError)
                                liveOutcome.plannerError = msg;
                        }
                        else if (/executor|task failed|iteration/i.test(msg)) {
                            if (!liveOutcome.executorError)
                                liveOutcome.executorError = msg;
                            parsePlannerSummary(msg);
                        }
                    }
                    if (spinner)
                        this.updateV3AgentSpinner(spinner, event);
                },
            });
            const response = await workflowPromise;
            if (spinner) {
                spinner.stop();
            }
            if (this.v3StreamingStarted && !this.v3StreamedAnswerDisplayed) {
                this.displayV3StreamedAnswer();
            }
            const previewGate = (response.metadata?.previewGate || null);
            const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
            const changedFileCount = response.changedFiles ? Object.keys(response.changedFiles).length : 0;
            const requiresWorkspaceChanges = taskRequiresWorkspaceChangesWithContext(contextualPrompt, intentContext);
            const answerContent = normalizeAgentAnswerContent(response.content, this.v3StreamedAnswerBuffer);
            liveOutcome.changedFileCount = changedFileCount;
            liveOutcome.requiresWorkspaceChanges = requiresWorkspaceChanges;
            liveOutcome.workspaceHasOutput = requiresWorkspaceChanges ? workspaceHasOutput : false;
            liveOutcome.answerContent = answerContent;
            const success = previewGate?.required === true
                ? (previewGate?.passed === true || workspaceHasOutput)
                : true;
            if (!success) {
                if (this.isLegacyAgentFallbackAllowed()) {
                    if (spinner) {
                        spinner.stop();
                    }
                    this.logger.warn('Falling back to legacy CLI agent loop');
                    this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
                    watcher?.stop();
                    return false;
                }
                const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
                if (spinner) {
                    spinner.stop();
                }
                this.logger.error(errorMessage);
                this.messages.push({ role: 'assistant', content: errorMessage });
                if (this.jsonOutput) {
                    process.exitCode = 1;
                    console.log(JSON.stringify({
                        success: false,
                        mode: 'agent',
                        model: routingPolicy.selectedModel,
                        partial: false,
                        content: '',
                        error: errorMessage,
                        metadata: { executionPath: 'v3-agent', previewGate },
                    }, null, 2));
                }
                watcher?.stop();
                return true;
            }
            if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
                console.log(chalk.yellow(`Template Service preview gate did not fully validate this output, but generated workspace files were preserved${previewGate?.error ? `: ${previewGate.error}` : '.'}`));
            }
            if (!this.jsonOutput && previewGate?.required) {
                if (previewGate.passed) {
                    console.log(chalk.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
                }
                else {
                    console.log(chalk.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
                }
            }
            // Show change summary only when the agent actually wrote files this run
            if (!this.jsonOutput && !this.directPromptMode && response.changedFiles && requiresWorkspaceChanges) {
                const fileCount = changedFileCount;
                if (fileCount > 0) {
                    console.log(chalk.gray(`\nFiles changed: ${fileCount}`));
                    for (const relPath of Object.keys(response.changedFiles).slice(0, 15)) {
                        console.log(chalk.gray(`  ${chalk.green('+')} ${relPath}`));
                    }
                    if (fileCount > 15) {
                        console.log(chalk.gray(`  ... and ${fileCount - 15} more`));
                    }
                    console.log(chalk.gray(`Run ${chalk.cyan('vigthoria preview --diff')} for full visual diffs.`));
                }
            }
            // Resolve the Execute-tasks spinner: only mark it ✓ when the run actually
            // produced real, validated output. Otherwise the user just saw a wall of
            // executor errors and the spinner needs to clearly say "failed", not "✓".
            let selfHealStatus = null;
            let selfHealTool = null;
            if (isToolEvidenceStubAnswer(answerContent)) {
                liveOutcome.answerContent = '';
            }
            const runEvaluation = evaluateExecutorSuccess(liveOutcome);
            let executorSucceeded = runEvaluation.executorSucceeded;
            let finalAnswer = answerContent;
            if (isToolEvidenceStubAnswer(finalAnswer)) {
                finalAnswer = '';
            }
            const needsAnalysisRescue = !shouldSkipAnalysisRescue({
                tasksTotal: liveOutcome.tasksTotal,
                changedFileCount,
                requiresWorkspaceChanges,
            }) && !this.jsonOutput
                && (!executorSucceeded || !isSubstantiveAgentAnswer(finalAnswer));
            if (needsAnalysisRescue) {
                const rescued = await this.tryAnalysisLocalRescue(prompt, workspacePath);
                if (rescued) {
                    watcher?.stop();
                    return true;
                }
                executorSucceeded = false;
            }
            if (!this.jsonOutput && !this.directPromptMode) {
                if (this.v3StreamedAnswerDisplayed) {
                    // Highlighted report already printed once above.
                }
                else if (isSubstantiveAgentAnswer(finalAnswer)) {
                    console.log('');
                    console.log(chalk.bold('Answer:'));
                    console.log(this.renderAgentAnswerForTerminal(finalAnswer));
                }
                else if (!requiresWorkspaceChanges) {
                    console.log('');
                    console.log(chalk.yellow('No written analysis was produced for your question.'));
                    console.log(chalk.gray('The agent inspected files but did not synthesize findings. Try /retry or rephrase (e.g. "Summarize architecture of index.html and game.js").'));
                }
                else if (finalAnswer) {
                    console.log('');
                    console.log(this.renderAgentAnswerForTerminal(finalAnswer));
                }
            }
            const plannerFailedBeforeTasks = Boolean(liveOutcome.plannerError) && liveOutcome.tasksTotal === 0;
            if (!executorSucceeded && requiresWorkspaceChanges && changedFileCount === 0 && !liveOutcome.executorError && !plannerFailedBeforeTasks) {
                const clientToolErrors = this.api.getClientToolErrors();
                const transportErrors = this.api.getLastChatTransportErrors();
                if (clientToolErrors.length > 0) {
                    liveOutcome.executorError = clientToolErrors[0];
                }
                else if (workspaceHasOutput && changedFileCount === 0) {
                    liveOutcome.executorError = 'Files may exist locally but were not tracked by the agent stream. Try /retry or check your workspace path.';
                }
                else {
                    liveOutcome.executorError = 'No workspace files were changed for a build/edit request.';
                }
                if (transportErrors.length > 0 && !liveOutcome.plannerError) {
                    liveOutcome.plannerError = transportErrors[0];
                }
            }
            if (plannerFailedBeforeTasks && !liveOutcome.executorError) {
                const transportErrors = this.api.getLastChatTransportErrors();
                if (transportErrors.length > 0) {
                    liveOutcome.executorError = transportErrors[0];
                }
            }
            if (executorSucceeded) {
                taskDisplay.complete(1);
            }
            else {
                const failDetail = liveOutcome.failedTaskIds.size > 0
                    ? `failed: ${[...liveOutcome.failedTaskIds].slice(0, 4).join(', ')}`
                    : (liveOutcome.executorError ? 'executor error' : (liveOutcome.plannerError ? 'planner error' : 'incomplete'));
                taskDisplay.fail(1, failDetail);
            }
            // ── Self-healing validation ──────────────────────────────────────
            if (this.currentProjectPath && !this.jsonOutput && success && executorSucceeded && requiresWorkspaceChanges) {
                try {
                    taskDisplay.start(2, 'validating...');
                    const healResult = await this.api.runSelfHealingCycle(executionPrompt, this.currentProjectPath, workspaceContext);
                    if (healResult.healingAttempted) {
                        taskDisplay.complete(2);
                        if (healResult.passed) {
                            taskDisplay.complete(3);
                            selfHealStatus = 'passed';
                        }
                        else {
                            taskDisplay.fail(3, healResult.tool);
                            selfHealStatus = 'partial';
                        }
                        selfHealTool = healResult.tool || null;
                        if (!this.directPromptMode) {
                            const hs = healResult.passed ? chalk.green('passed') : chalk.yellow('partial');
                            console.log(chalk.gray(`Self-healing: ${hs} (${healResult.tool})`));
                        }
                    }
                    else {
                        taskDisplay.skip(2);
                        taskDisplay.skip(3);
                        selfHealStatus = 'skipped';
                    }
                }
                catch (error) {
                    this.logger.debug(`Self-healing validation failed: ${error instanceof Error ? error.message : String(error)}`);
                    taskDisplay.skip(2);
                    taskDisplay.skip(3);
                    selfHealStatus = 'failed';
                }
            }
            else if (!requiresWorkspaceChanges && executorSucceeded) {
                taskDisplay.complete(2);
                taskDisplay.skip(3);
                selfHealStatus = 'skipped';
            }
            else {
                taskDisplay.skip(2);
                taskDisplay.skip(3);
                selfHealStatus = 'skipped';
            }
            taskDisplay.finalize();
            // ────────────────────────────────────────────────────────────────
            if (!executorSucceeded) {
                process.exitCode = 1;
            }
            this.commitAgentRunOutcome({
                prompt,
                originalPrompt: null,
                taskId: response.taskId || null,
                contextId: response.contextId || null,
                tasksSucceeded: liveOutcome.tasksSucceeded,
                tasksTotal: liveOutcome.tasksTotal,
                failedTaskIds: [...liveOutcome.failedTaskIds],
                unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
                qualityScore: liveOutcome.qualityScore,
                qualityMissing: liveOutcome.qualityMissing,
                qualityBlockers: liveOutcome.qualityBlockers,
                hasOutput: workspaceHasOutput,
                answerContent: finalAnswer || null,
                selfHealStatus,
                selfHealTool,
                plannerError: liveOutcome.plannerError ? sanitizeUserFacingErrorText(liveOutcome.plannerError) : null,
                executorError: liveOutcome.executorError ? sanitizeUserFacingErrorText(liveOutcome.executorError) : null,
                clientToolErrors: this.api.getClientToolErrors(),
                transportErrors: this.api.getLastChatTransportErrors(),
                workspacePath: workspacePath || null,
                workspaceSyncIssue: (!executorSucceeded && requiresWorkspaceChanges && workspaceHasOutput && changedFileCount === 0)
                    ? 'Local workspace has files but the agent did not confirm tracked changes.'
                    : null,
                finishedAt: Date.now(),
            }, prompt);
            if (!this.jsonOutput && !this.directPromptMode) {
                if (this.lastAgentRunOutcome) {
                    this.printAgentRunSummary(this.lastAgentRunOutcome, runEvaluation, changedFileCount);
                }
            }
            if (this.jsonOutput) {
                if (!executorSucceeded) {
                    process.exitCode = 1;
                }
                emitAgentJsonOutput({
                    success: executorSucceeded,
                    mode: 'agent',
                    model: routingPolicy.selectedModel,
                    routingPolicy,
                    taskId: response.taskId || null,
                    contextId: response.contextId || null,
                    partial: response.partial === true || (!executorSucceeded && liveOutcome.tasksSucceeded > 0),
                    content: finalAnswer || runEvaluation.statusHeadline,
                    statusHeadline: runEvaluation.statusHeadline,
                    tasksSucceeded: liveOutcome.tasksSucceeded,
                    tasksTotal: liveOutcome.tasksTotal,
                    metadata: {
                        ...(response.metadata || {}),
                        outcomeTruth: {
                            executorSucceeded,
                            uiTheme: runEvaluation.uiTheme,
                            tasksSucceeded: liveOutcome.tasksSucceeded,
                            tasksTotal: liveOutcome.tasksTotal,
                        },
                        previewGate,
                    },
                });
            }
            this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
            this.stopV3IdleWatch();
            watcher?.stop();
            return true;
        }
        catch (error) {
            this.stopV3IdleWatch();
            watcher?.stop();
            if (!this.api.hasAgentWorkspaceOutput(workspaceContext)) {
                const recovered = await this.tryRecoverV3ServiceAndRetry(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
                if (recovered) {
                    return true;
                }
            }
            if (this.isLegacyAgentFallbackAllowed()) {
                if (spinner) {
                    spinner.stop();
                }
                this.logger.warn('Falling back to legacy CLI agent loop');
                this.logger.debug(`V3 agent workflow unavailable: ${sanitizeUserFacingErrorText(error.message || '')}`);
                return false;
            }
            if (spinner) {
                spinner.stop();
            }
            const safeDetail = sanitizeUserFacingErrorText(error.message || '');
            const errorMessage = safeDetail
                ? `Agent mode is unavailable right now. ${safeDetail}`
                : 'Agent mode is unavailable right now. Please retry shortly or run vigthoria login if the issue persists.';
            this.logger.error(errorMessage);
            this.messages.push({ role: 'assistant', content: errorMessage });
            liveOutcome.streamAborted = true;
            if (!liveOutcome.executorError) {
                liveOutcome.executorError = safeDetail || 'Connection aborted';
            }
            // Resolve any half-rendered TaskDisplay spinners before the prompt
            // comes back, otherwise the user sees `⟳ Execute tasks` next to `>`.
            try {
                taskDisplay.fail(1, 'agent unavailable');
                taskDisplay.skip(2);
                taskDisplay.skip(3);
                taskDisplay.finalize();
            }
            catch (_) { /* render is best-effort */ }
            this.commitAgentRunOutcome({
                prompt,
                originalPrompt: null,
                taskId: null,
                contextId: null,
                tasksSucceeded: liveOutcome.tasksSucceeded,
                tasksTotal: liveOutcome.tasksTotal,
                failedTaskIds: [...liveOutcome.failedTaskIds],
                unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
                qualityScore: liveOutcome.qualityScore,
                qualityMissing: liveOutcome.qualityMissing,
                qualityBlockers: liveOutcome.qualityBlockers,
                hasOutput: this.api.hasAgentWorkspaceOutput(workspaceContext),
                answerContent: null,
                selfHealStatus: 'skipped',
                selfHealTool: null,
                plannerError: liveOutcome.plannerError ? sanitizeUserFacingErrorText(liveOutcome.plannerError) : null,
                executorError: liveOutcome.executorError ? sanitizeUserFacingErrorText(liveOutcome.executorError) : safeDetail || null,
                clientToolErrors: this.api.getClientToolErrors(),
                transportErrors: this.api.getLastChatTransportErrors(),
                workspacePath: workspacePath || null,
                workspaceSyncIssue: null,
                finishedAt: Date.now(),
            }, prompt);
            if (!this.jsonOutput) {
                process.exitCode = 1;
            }
            if (this.jsonOutput) {
                process.exitCode = 1;
                console.log(JSON.stringify({
                    success: false,
                    mode: 'agent',
                    model: routingPolicy.selectedModel,
                    partial: liveOutcome.tasksSucceeded > 0 && liveOutcome.tasksTotal > 0,
                    content: '',
                    error: errorMessage,
                    statusHeadline: evaluateExecutorSuccess(liveOutcome).statusHeadline,
                    tasksSucceeded: liveOutcome.tasksSucceeded,
                    tasksTotal: liveOutcome.tasksTotal,
                    metadata: { executionPath: 'v3-agent' },
                }, null, 2));
            }
            else if (!this.directPromptMode) {
                const failedEval = evaluateExecutorSuccess({
                    ...liveOutcome,
                    executorFailed: true,
                    executorError: liveOutcome.executorError || safeDetail,
                    changedFileCount: 0,
                    requiresWorkspaceChanges: this.taskRequiresWorkspaceChanges(prompt),
                    workspaceHasOutput: this.api.hasAgentWorkspaceOutput(workspaceContext),
                });
                if (this.lastAgentRunOutcome) {
                    this.printAgentRunSummary(this.lastAgentRunOutcome, failedEval, 0);
                }
            }
            return true;
        }
    }
    async tryRecoverV3ServiceAndRetry(executionPrompt, rawPrompt, workspaceContext, routingPolicy, spinner, error) {
        const recovery = await this.api.attemptV3ServiceRecovery((error && error.message) || '');
        if (!recovery.recovered) {
            return false;
        }
        if (spinner && spinner.isSpinning) {
            spinner.stop();
        }
        if (!this.jsonOutput) {
            console.log(chalk.yellow(`V3 recovery: ${recovery.message} Retrying once...`));
        }
        this.v3IterationCount = 0;
        this.v3ToolCallCount = 0;
        this.v3LastActivity = Date.now();
        this.v3StreamingStarted = false;
        this.v3StartSeen = false;
        try {
            const retryPriorPrompt = this.getPreviousActionablePrompt();
            const retryContext = { priorPrompt: retryPriorPrompt || null };
            const retryTaskType = inferAgentTaskTypeWithContext(rawPrompt, retryContext);
            const retryResponse = await this.api.runV3AgentWorkflow(executionPrompt, {
                workspace: { path: this.currentProjectPath },
                ...workspaceContext,
                executionSurface: 'cli',
                clientSurface: 'cli',
                localMachineCapable: true,
                agentTaskType: retryTaskType,
                workflowType: resolveWorkflowType(retryTaskType, rawPrompt, retryContext),
                agentTimeoutMs: resolvePlannerAgentTimeoutMs(DEFAULT_V3_AGENT_TIMEOUT_MS, retryTaskType, rawPrompt),
                agentIdleTimeoutMs: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS,
                agentSoftTimeoutMs: DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS,
                model: routingPolicy.selectedModel,
                requestedModel: this.currentModel,
                agentExecutionPolicy: routingPolicy,
                legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
                rawPrompt,
                history: this.getMessagesForModel(),
                onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
            });
            if (spinner && spinner.isSpinning) {
                spinner.stop();
            }
            if (this.v3StreamingStarted) {
                process.stdout.write('\n');
            }
            if (this.jsonOutput) {
                console.log(JSON.stringify({
                    success: true,
                    mode: 'agent',
                    model: routingPolicy.selectedModel,
                    routingPolicy,
                    taskId: retryResponse.taskId || null,
                    contextId: retryResponse.contextId || null,
                    partial: retryResponse.partial === true,
                    content: retryResponse.content || 'V3 agent workflow completed after recovery.',
                    metadata: { ...(retryResponse.metadata || {}), recoveryAttempted: true },
                }, null, 2));
            }
            else if (!this.v3StreamingStarted && retryResponse.content) {
                console.log(chalk.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
                console.log(retryResponse.content);
            }
            this.messages.push({ role: 'assistant', content: retryResponse.content || 'V3 agent workflow completed after recovery.' });
            return true;
        }
        catch {
            return false;
        }
    }
    async startInteractiveChat() {
        const chatTitle = this.operatorMode
            ? 'Interactive Operator Chat'
            : this.agentMode
                ? 'Interactive Agent Chat'
                : 'Interactive Chat';
        this.logger.section(this.workflowTarget ? `${chatTitle} Via Workflow Target` : chatTitle);
        console.log(chalk.gray(this.fitTerminalText('Type /help for commands. Type /exit to quit.')));
        console.log(chalk.gray(this.fitTerminalText('Multi-line: end a line with \\ or start a block with {{{ and end with }}}')));
        if (this.workflowTarget) {
            console.log(chalk.gray(this.fitTerminalText(`Workflow target: ${this.workflowTarget}`)));
        }
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
        });
        // Multi-line input helper
        const readMultiLineInput = async () => {
            const lines = [];
            let firstLine = await new Promise((resolve) => {
                rl.question(chalk.blue('> '), resolve);
            });
            // Check for {{{ block mode
            if (firstLine.trim() === '{{{' || firstLine.trim().endsWith('{{{')) {
                if (firstLine.trim() !== '{{{') {
                    lines.push(firstLine.trim().replace(/\{\{\{$/, '').trim());
                }
                console.log(chalk.gray('  (multi-line mode: type }}} on its own line to finish)'));
                while (true) {
                    const line = await new Promise((resolve) => {
                        rl.question(chalk.gray('  '), resolve);
                    });
                    if (line.trim() === '}}}')
                        break;
                    lines.push(line);
                }
                return lines.join('\n');
            }
            // Check for backslash continuation
            while (firstLine.endsWith('\\')) {
                lines.push(firstLine.slice(0, -1));
                firstLine = await new Promise((resolve) => {
                    rl.question(chalk.gray('  '), resolve);
                });
            }
            lines.push(firstLine);
            return lines.join('\n');
        };
        while (true) {
            const rawInput = await readMultiLineInput();
            const { goaContext, userPrompt: strippedInput } = splitGoaContextFromInput(rawInput);
            if (goaContext) {
                this.pendingGoaContext = goaContext;
            }
            const input = strippedInput || rawInput;
            const trimmed = input.trim();
            if (!trimmed) {
                continue;
            }
            if (trimmed === '/exit' || trimmed === '/quit') {
                rl.close();
                return;
            }
            if (trimmed === '/help') {
                this.showHelp();
                continue;
            }
            if (trimmed === '/context') {
                this.showContext();
                continue;
            }
            if (trimmed === '/memory') {
                this.showProjectMemory();
                continue;
            }
            if (trimmed === '/compact') {
                this.compactCurrentSession();
                continue;
            }
            if (trimmed === '/agent') {
                this.agentMode = !this.agentMode;
                if (this.agentMode) {
                    this.operatorMode = false;
                    this.syncInteractiveModeModel('agent');
                }
                else {
                    this.syncInteractiveModeModel('chat');
                }
                console.log(chalk.yellow(`Agent mode: ${this.agentMode ? 'ON' : 'OFF'}`));
                console.log(chalk.gray(`Model: ${this.currentModel}`));
                if (this.currentSession) {
                    this.currentSession.agentMode = this.agentMode;
                    this.currentSession.operatorMode = this.operatorMode;
                    this.currentSession.model = this.currentModel;
                    this.saveSession();
                }
                continue;
            }
            if (trimmed === '/operator') {
                if (!this.operatorMode && !this.hasOperatorAccess()) {
                    this.logger.error(this.operatorAccessMessage());
                    continue;
                }
                this.operatorMode = !this.operatorMode;
                if (this.operatorMode) {
                    this.agentMode = false;
                    this.syncInteractiveModeModel('operator');
                }
                else {
                    this.syncInteractiveModeModel('chat');
                }
                console.log(chalk.yellow(`Operator mode: ${this.operatorMode ? 'ON' : 'OFF'}`));
                console.log(chalk.gray(`Model: ${this.currentModel}`));
                if (this.currentSession) {
                    this.currentSession.agentMode = this.agentMode;
                    this.currentSession.operatorMode = this.operatorMode;
                    this.currentSession.model = this.currentModel;
                    this.saveSession();
                }
                continue;
            }
            if (trimmed === '/clear') {
                this.messages = [];
                console.log(chalk.yellow('Conversation cleared.'));
                continue;
            }
            if (trimmed === '/status') {
                this.showAgentRunStatus();
                continue;
            }
            if (trimmed === '/retry') {
                const followUp = this.buildRetryPrompt();
                if (!followUp) {
                    console.log(chalk.yellow('Nothing to retry — run an agent task first.'));
                    continue;
                }
                const nextSignature = this.computeRetryPromptSignature();
                if (nextSignature && this.retryPromptSignature === nextSignature) {
                    this.retryPromptStreak += 1;
                }
                else {
                    this.retryPromptSignature = nextSignature;
                    this.retryPromptStreak = 1;
                }
                if (this.retryPromptStreak >= 3) {
                    const continuePrompt = this.buildContinuePrompt();
                    if (continuePrompt) {
                        console.log(chalk.yellow('Repeated /retry detected with the same failing task set. Escalating to /continue to avoid planner loops.'));
                        if (!this.agentMode) {
                            this.agentMode = true;
                            this.syncInteractiveModeModel('agent');
                            console.log(chalk.gray('Agent mode re-enabled for continuation.'));
                        }
                        await this.runAgentTurn(continuePrompt);
                        continue;
                    }
                }
                if (!this.agentMode) {
                    this.agentMode = true;
                    this.syncInteractiveModeModel('agent');
                    console.log(chalk.gray('Agent mode re-enabled for retry.'));
                }
                await this.runAgentTurn(followUp);
                continue;
            }
            if (trimmed === '/continue' || trimmed === '/contiunue' || trimmed === '/contiune') {
                const followUp = this.buildContinuePrompt();
                if (!followUp) {
                    console.log(chalk.yellow('Nothing to continue — run an agent task first.'));
                    continue;
                }
                if (!this.agentMode) {
                    this.agentMode = true;
                    this.syncInteractiveModeModel('agent');
                    console.log(chalk.gray('Agent mode re-enabled for continuation.'));
                }
                await this.runAgentTurn(followUp);
                continue;
            }
            if (trimmed === '/save') {
                this.saveSession();
                console.log(chalk.green('Session saved.'));
                continue;
            }
            if (trimmed.startsWith('/model ')) {
                this.currentModel = trimmed.slice(7).trim() || this.currentModel;
                // BUGFIX: previously this unconditionally set modelExplicitlySelected = true,
                // which permanently locked resolveAgentExecutionPolicy() into its
                // 'explicit-model-selection' branch for the rest of the session -- even for
                // light chat aliases like 'balanced'/'fast'. Since 'balanced' resolves
                // server-side to vigthoria-fast-9b, every subsequent /retry, /continue, or
                // agent/operator task silently executed on the 9b model instead of the
                // expected 35b executor, causing repeated "0 tool calls" task failures.
                // Reuse the same hard-explicit rule applied at CLI startup so agent-mode
                // aliases don't lock out the dispatcher's auto-upgrade-to-agent-model logic.
                this.modelExplicitlySelected = this.isHardExplicitAgentModelSelection({
                    model: this.currentModel,
                    agent: this.agentMode,
                    operator: this.operatorMode,
                });
                console.log(chalk.yellow(`Model changed to: ${this.currentModel}`));
                if (this.currentSession) {
                    this.currentSession.model = this.currentModel;
                    this.saveSession();
                }
                continue;
            }
            if (this.workflowTarget) {
                await this.runWorkflowTargetPrompt(trimmed);
            }
            else if (this.operatorMode) {
                await this.runOperatorTurn(trimmed);
            }
            else if (this.agentMode) {
                await this.runAgentTurn(trimmed);
            }
            else {
                await this.runSimplePrompt(trimmed);
            }
        }
    }
    showHelp() {
        console.log('');
        console.log('Interactive chat commands:');
        const chatCommands = [
            ['/help', 'Show this dynamically generated help'],
            ['/exit, /quit', 'Exit chat'],
            ['/agent', 'Toggle Agent Mode'],
            ['/operator', 'Toggle BMAD operator mode'],
            ['/context', 'Show current session and project memory'],
            ['/memory', 'Show Vigthoria project brain status'],
            ['/compact', 'Compact current session into memory summary'],
            ['/clear', 'Clear conversation'],
            ['/save', 'Save session'],
            ['/model <name>', 'Change model'],
            ['/status', 'Show the last agent run outcome'],
            ['/retry', 'Re-run the last failed agent task'],
            ['/continue', 'Ask the agent to keep working on unfinished tasks'],
        ];
        for (const [command, description] of chatCommands) {
            console.log(`  ${command.padEnd(18)} ${description}`);
        }
        if (this.commandProgram) {
            console.log('');
            console.log(renderDynamicHelp(this.commandProgram));
        }
        console.log('');
    }
    /**
     * Print a clear, opinionated end-of-run summary after every agent workflow.
     *
     * Goals:
     *   - User can answer "Did it succeed?" in one glance.
     *   - User can answer "What do I type next?" without reading 200 scrollback lines.
     *   - Never leave the spinners (`⟳`, `–`) ambiguous when the prompt returns.
     */
    printAgentRunSummary(outcome, evaluation, changedFileCount) {
        const executorSucceeded = evaluation.executorSucceeded;
        if (outcome.answerContent && isSubstantiveAgentAnswer(outcome.answerContent)) {
            const reportSummary = summarizeMarkdownReport(outcome.answerContent);
            emitDeckEvent({
                type: 'run_summary',
                success: executorSucceeded,
                title: reportSummary?.title || 'Agent run complete',
                sections: reportSummary?.sections || [],
            });
        }
        const bar = chalk.gray('─'.repeat(63));
        const ok = chalk.green('✓');
        const warn = chalk.yellow('⚠');
        const bad = chalk.red('✗');
        const failedList = outcome.failedTaskIds.slice(0, 8);
        const unfinishedList = outcome.unfinishedTaskIds.slice(0, 8);
        const hasTaskInfo = outcome.tasksTotal > 0 || failedList.length > 0 || unfinishedList.length > 0;
        console.log('');
        console.log(bar);
        const headlineIcon = evaluation.uiTheme === 'success' ? ok : evaluation.uiTheme === 'warning' ? warn : bad;
        const headlineText = evaluation.statusHeadline.replace(/^[✓⚠✕]\s*/, '');
        console.log(`${headlineIcon} ${chalk.bold(headlineText)}${changedFileCount ? chalk.gray(` — ${changedFileCount} file${changedFileCount === 1 ? '' : 's'} changed`) : ''}`);
        if (hasTaskInfo) {
            const succ = outcome.tasksTotal > 0 ? `${outcome.tasksSucceeded}/${outcome.tasksTotal}` : `${outcome.tasksSucceeded}`;
            console.log(chalk.gray(`  Tasks completed: ${succ}`));
            if (failedList.length > 0) {
                const more = outcome.failedTaskIds.length > failedList.length ? chalk.gray(` (+${outcome.failedTaskIds.length - failedList.length} more)`) : '';
                console.log(chalk.gray('  Failed:   ') + chalk.red(failedList.join(', ')) + more);
            }
            if (unfinishedList.length > 0) {
                const more = outcome.unfinishedTaskIds.length > unfinishedList.length ? chalk.gray(` (+${outcome.unfinishedTaskIds.length - unfinishedList.length} more)`) : '';
                console.log(chalk.gray('  Pending:  ') + chalk.yellow(unfinishedList.join(', ')) + more);
            }
        }
        if (typeof outcome.qualityScore === 'number') {
            const score = outcome.qualityScore.toFixed(1);
            const colour = outcome.qualityScore >= 70 ? chalk.green : outcome.qualityScore >= 30 ? chalk.yellow : chalk.red;
            console.log(chalk.gray('  Quality:  ') + colour(`${score}/100`));
        }
        if (outcome.qualityBlockers.length > 0) {
            const list = outcome.qualityBlockers.slice(0, 3).join('; ');
            const more = outcome.qualityBlockers.length > 3 ? chalk.gray(` (+${outcome.qualityBlockers.length - 3} more)`) : '';
            console.log(chalk.gray('  Blockers: ') + chalk.yellow(list) + more);
        }
        if (outcome.qualityMissing.length > 0) {
            const list = outcome.qualityMissing.slice(0, 5).join(', ');
            const more = outcome.qualityMissing.length > 5 ? chalk.gray(` (+${outcome.qualityMissing.length - 5} more)`) : '';
            console.log(chalk.gray('  Missing:  ') + chalk.yellow(list) + more);
        }
        if (outcome.plannerError) {
            console.log(chalk.gray('  Planner:  ') + chalk.red(outcome.plannerError.slice(0, 140)));
        }
        if (outcome.executorError) {
            console.log(chalk.gray('  Executor: ') + chalk.red(outcome.executorError.slice(0, 240)));
        }
        const hasDiagnostics = outcome.clientToolErrors.length > 0
            || outcome.transportErrors.length > 0
            || outcome.workspaceSyncIssue;
        if (hasDiagnostics) {
            console.log('');
            console.log(chalk.gray('Why this run struggled:'));
            if (outcome.workspacePath) {
                console.log('  ' + chalk.white(`Workspace: ${outcome.workspacePath}`));
            }
            for (const err of outcome.clientToolErrors.slice(0, 3)) {
                console.log('  ' + chalk.yellow('Local tools: ') + chalk.white(err.slice(0, 220)));
            }
            for (const err of outcome.transportErrors.slice(0, 2)) {
                console.log('  ' + chalk.yellow('Connectivity: ') + chalk.white(err.slice(0, 220)));
            }
            if (outcome.workspaceSyncIssue) {
                console.log('  ' + chalk.yellow('Sync: ') + chalk.white(outcome.workspaceSyncIssue));
            }
            if (outcome.clientToolErrors.some((e) => /no pending client tool call|rejected/i.test(e))) {
                console.log('  ' + chalk.gray('The agent tried to use your local machine for file tools, but the server timed out waiting. /retry usually works after the SSE bridge fix.'));
            }
        }
        console.log('');
        console.log(chalk.gray('Result:'));
        if (executorSucceeded) {
            const filePart = changedFileCount > 0
                ? ` and updated ${changedFileCount} workspace file${changedFileCount === 1 ? '' : 's'}`
                : '';
            console.log('  ' + chalk.green(`The ${chalk.bold('Agent')} mode completed the request${filePart}.`));
        }
        else {
            console.log('  ' + chalk.yellow('The Agent mode stopped before every requested task was confirmed complete.'));
        }
        console.log('');
        console.log(chalk.gray('What was done:'));
        if (hasTaskInfo) {
            const taskLine = outcome.tasksTotal > 0
                ? `${outcome.tasksSucceeded} of ${outcome.tasksTotal} planned task${outcome.tasksTotal === 1 ? '' : 's'} finished`
                : `${outcome.tasksSucceeded} task update${outcome.tasksSucceeded === 1 ? '' : 's'} reported`;
            console.log('  ' + chalk.white(taskLine + '.'));
        }
        else if (outcome.answerContent && isSubstantiveAgentAnswer(outcome.answerContent)) {
            const reportSummary = summarizeMarkdownReport(outcome.answerContent);
            if (reportSummary) {
                const sectionPreview = reportSummary.sections.length > 0
                    ? reportSummary.sections.slice(0, 4).join(', ')
                    : `${Math.round(reportSummary.charCount / 1000)}k chars`;
                console.log('  ' + chalk.white(reportSummary.title));
                console.log('  ' + chalk.gray(`Sections: ${sectionPreview}`));
            }
            else {
                const preview = outcome.answerContent.length > 160
                    ? `${outcome.answerContent.slice(0, 160)}…`
                    : outcome.answerContent;
                console.log('  ' + chalk.white(preview));
            }
        }
        else if (changedFileCount > 0) {
            console.log('  ' + chalk.white('Workspace changes were produced and are ready for review.'));
        }
        else if (outcome.hasOutput && executorSucceeded && changedFileCount === 0) {
            console.log('  ' + chalk.white('The assistant returned a final answer for your request.'));
        }
        else {
            console.log('  ' + chalk.white('No final answer or workspace output was confirmed.'));
        }
        if (outcome.selfHealStatus && outcome.selfHealStatus !== 'skipped') {
            console.log('  ' + chalk.white(`Self-check status: ${outcome.selfHealStatus}.`));
        }
        console.log('');
        console.log(chalk.gray('What you can do next:'));
        if (failedList.length > 0 || unfinishedList.length > 0 || !executorSucceeded) {
            console.log('  ' + chalk.cyan('/retry') + chalk.gray('     resume the failed/unfinished tasks only'));
            console.log('  ' + chalk.cyan('/continue') + chalk.gray('  ask the agent to keep working from this state'));
        }
        else {
            console.log('  ' + chalk.cyan('type your next request') + chalk.gray('  continue the conversation'));
        }
        if (changedFileCount > 0) {
            console.log('  ' + chalk.cyan('vigthoria preview --diff') + chalk.gray('   inspect the file changes'));
        }
        console.log('  ' + chalk.cyan('/status') + chalk.gray('    re-print this summary later'));
        console.log('  ' + chalk.cyan('/exit') + chalk.gray('      leave interactive chat'));
        console.log(bar);
        console.log('');
    }
    /**
     * Build the prompt sent to the agent when the user types `/retry`.
     * Resumes only the failed/unfinished tasks from the previous run.
     */
    buildRetryPrompt() {
        const o = this.lastAgentRunOutcome;
        if (!o || !o.prompt)
            return null;
        const sourcePrompt = o.originalPrompt || o.prompt;
        const runtime = this.getRuntimeEnvironmentContext();
        const remaining = [...new Set([...o.failedTaskIds, ...o.unfinishedTaskIds])];
        const taskList = remaining.length > 0 ? remaining.join(', ') : '';
        const blockerLine = o.qualityBlockers.length > 0
            ? `\nKnown blockers to address: ${o.qualityBlockers.slice(0, 3).join('; ')}.`
            : '';
        const missingLine = o.qualityMissing.length > 0
            ? `\nMissing pieces: ${o.qualityMissing.slice(0, 6).join(', ')}.`
            : '';
        const envLine = `\nExecution environment: ${runtime.platform}; machine scope: ${runtime.machineScope}; workspace path: ${runtime.workspacePath}.`;
        const localPreferenceLine = runtime.machineScope === 'local-machine'
            ? '\nUse local workspace tools and local filesystem paths first; do not assume server-side workspace access.'
            : '';
        if (taskList) {
            return `Resume the previous agent run. Re-execute only these tasks and make them pass: ${taskList}.${blockerLine}${missingLine}${envLine}${localPreferenceLine}\nOriginal request was: ${sourcePrompt}`;
        }
        return `Retry the previous request and make sure it finishes successfully.${blockerLine}${missingLine}${envLine}${localPreferenceLine}\nOriginal request was: ${sourcePrompt}`;
    }
    computeRetryPromptSignature() {
        const o = this.lastAgentRunOutcome;
        if (!o) {
            return null;
        }
        const remaining = [...new Set([...o.failedTaskIds, ...o.unfinishedTaskIds])].sort();
        if (remaining.length === 0) {
            return null;
        }
        const blockers = [...(o.qualityBlockers || [])].slice(0, 3).sort();
        return `${remaining.join('|')}::${blockers.join('|')}`;
    }
    buildContinuePrompt() {
        const o = this.lastAgentRunOutcome;
        if (!o || !o.prompt)
            return null;
        const sourcePrompt = o.originalPrompt || o.prompt;
        const remaining = [...new Set([...o.failedTaskIds, ...o.unfinishedTaskIds])];
        const taskList = remaining.length > 0 ? `\nRemaining tasks to finish: ${remaining.join(', ')}.` : '';
        const mustChangeFiles = this.taskRequiresWorkspaceChanges(sourcePrompt)
            ? '\nThis was a build/edit request. Do not stop after analysis or tool JSON; make real workspace file changes and verify them before finishing.'
            : '';
        const blockerLine = o.qualityBlockers.length > 0
            ? `\nKnown blockers to address: ${o.qualityBlockers.slice(0, 3).join('; ')}.`
            : '';
        const missingLine = o.qualityMissing.length > 0
            ? `\nMissing pieces: ${o.qualityMissing.slice(0, 6).join(', ')}.`
            : '';
        return `Continue the previous agent run from the current workspace state without re-doing already-completed work.${taskList}${mustChangeFiles}${blockerLine}${missingLine}\nOriginal request was: ${sourcePrompt}`;
    }
    /**
     * Re-print the last agent run summary, or guide the user when there isn't one.
     */
    showAgentRunStatus() {
        if (!this.lastAgentRunOutcome) {
            console.log(chalk.yellow('No agent run has finished in this session yet.'));
            console.log(chalk.gray('Toggle agent mode with /agent and send a request to start one.'));
            return;
        }
        const o = this.lastAgentRunOutcome;
        const statusEval = evaluateExecutorSuccess({
            ...createLiveOutcome(),
            executorFailed: o.failedTaskIds.length > 0,
            plannerError: o.plannerError,
            executorError: o.executorError,
            tasksTotal: o.tasksTotal,
            tasksSucceeded: o.tasksSucceeded,
            requiresWorkspaceChanges: this.taskRequiresWorkspaceChanges(o.prompt),
            workspaceHasOutput: o.hasOutput,
            failedTaskIds: new Set(o.failedTaskIds),
        });
        this.printAgentRunSummary(o, statusEval, 0);
    }
    showContext() {
        if (!this.currentSession) {
            console.log(chalk.yellow('No active session.'));
            return;
        }
        console.log(chalk.cyan(this.getCurrentSessionInfo()));
        if (this.currentSession.memorySummary?.trim()) {
            console.log();
            console.log(chalk.white('Compact Session Memory:'));
            console.log(chalk.gray(this.currentSession.memorySummary.trim()));
        }
        else {
            console.log(chalk.gray('No compact session memory summary yet.'));
        }
        const runtime = this.getRuntimeEnvironmentContext();
        console.log();
        console.log(chalk.white('Runtime Environment:'));
        console.log(chalk.gray(`Platform: ${runtime.platform} (${runtime.osPlatform})`));
        console.log(chalk.gray(`Machine scope: ${runtime.machineScope}`));
        console.log(chalk.gray(`Workspace: ${runtime.workspacePath}`));
        console.log(chalk.gray(`CWD: ${runtime.cwd}`));
        console.log(chalk.gray(`Server-bindable workspace: ${runtime.serverBindableWorkspace ? 'yes' : 'no'}`));
        this.showProjectMemory();
    }
    showProjectMemory() {
        if (!this.projectMemory) {
            this.projectMemory = new ProjectMemoryService(this.currentProjectPath);
        }
        const status = this.projectMemory.getStatus();
        console.log();
        console.log(chalk.white('Project Brain:'));
        console.log(chalk.gray(`Path: ${status.memoryDir}`));
        console.log(chalk.gray(`Items: ${status.itemCount}`));
        const typeSummary = Object.entries(status.typeCounts).map(([type, count]) => `${type}=${count}`).join(', ');
        if (typeSummary) {
            console.log(chalk.gray(`Types: ${typeSummary}`));
        }
    }
    compactCurrentSession() {
        if (!this.currentSession) {
            console.log(chalk.yellow('No active session.'));
            return;
        }
        this.currentSession.messages = [...this.messages];
        this.currentSession = this.sessionManager.compactInMemory(this.currentSession);
        this.messages = [...this.currentSession.messages];
        this.sessionManager.save(this.currentSession);
        if (!this.projectMemory) {
            this.projectMemory = new ProjectMemoryService(this.currentProjectPath);
        }
        this.projectMemory.rememberConversation(this.messages, {
            source: 'manual-compact',
            mode: this.agentMode ? 'agent' : this.operatorMode ? 'operator' : 'chat',
            model: this.currentModel,
            sessionSummary: this.currentSession.memorySummary || '',
        });
        console.log(chalk.green('Session compacted into memory summary and project brain.'));
    }
    ensureAgentSystemPrompt() {
        const hasSystemPrompt = this.messages.some((message) => message.role === 'system' && message.content.includes('Vigthoria CLI agent operating contract'));
        if (hasSystemPrompt) {
            return;
        }
        this.messages.unshift({
            role: 'system',
            content: this.buildAgentSystemPrompt(),
        });
    }
    buildAgentSystemPrompt() {
        const toolCatalog = AgenticTools.getToolDefinitions()
            .map((tool) => {
            const params = tool.parameters
                .map((param) => `${param.name}${param.required ? ' (required)' : ''}`)
                .join(', ');
            return `- ${tool.name}: ${tool.description}${params ? ` Parameters: ${params}.` : ''}`;
        })
            .join('\n');
        return [
            'Vigthoria CLI agent operating contract.',
            `You are operating inside the project root: ${this.currentProjectPath}`,
            `You are operating on the user's LOCAL machine. This CLI is not a server-only runtime.`,
            `All file reads/writes and command execution must target the local project/workspace path unless the user explicitly requests remote/server execution.`,
            'For command execution: ask for confirmation before risky commands; never redirect normal local work to server paths.',
            'CRITICAL: Begin working on the user\'s task IMMEDIATELY. Your very first response MUST contain <tool_call> blocks to gather evidence. Do NOT acknowledge instructions, restate the task, describe your plan, or discuss tool policies. Act, do not talk.',
            'FILE CREATION RULE: When the user asks you to build, create, or generate files, you MUST use the write_file tool to actually create each file on disk. NEVER output code snippets as markdown text — always write them to files using write_file. If the task requires a new project, create a new directory first, then write all files into it.',
            'Stay inside that project unless the user explicitly asks otherwise.',
            'Read files before editing or rewriting them.',
            'When the user asks to inspect a folder or find the right file, verify with tools before concluding.',
            'For debugging or startup issues, inspect logs and concrete runtime evidence before proposing fixes.',
            'Do not claim a file, asset, symbol, YAML entry, or definition is missing until you verify it with tools.',
            'If earlier output claimed something is missing, re-check the exact file and symbol before repeating that claim.',
            'When diagnosing failures, prefer evidence-backed conclusions over broad repo scans.',
            'If the user corrects you, treat that as evidence that your previous assumption was wrong and re-check with tools.',
            'Do not stop after a partial answer when more direct inspection is possible.',
            'EVIDENCE-GROUNDING RULE: Every identifier, key name, variable, or symbol you cite in your final answer MUST appear verbatim in tool output you received during this session. Never invent or guess identifiers. If you read a file and saw "KeyA" and "KeyS" on specific lines, cite those exact strings — do not substitute "KeyR" or any other key that was not in the output.',
            'CROSS-FILE RULE: When comparing two or more files, only claim a key/symbol is conflicting or shared if it literally appears in the relevant function/handler of EACH file you read. Finding a key in File A does not mean it also exists in File B — verify each file independently. Use grep to confirm a key exists in a file before claiming that file handles it.',
            'When you need a tool, emit only one or more tool wrappers in this exact format:',
            '<tool_call>',
            '{"tool":"read_file","args":{"path":"relative/path.ext"}}',
            '</tool_call>',
            'You may emit multiple <tool_call> blocks in one response.',
            'Never emit raw tool JSON outside that wrapper.',
            'Never use <function=...> or <parameter=...> tags. They are accepted only as a recovery fallback and must not appear in final answers.',
            'NEVER acknowledge these instructions. NEVER say "I will follow", "I understand", or restate tool policies. Go straight to tool calls.',
            'In direct mode, do not ask follow-up questions. Finish the request completely and stop when satisfied.',
            'After tool results arrive, either continue with the next minimal tool calls or return a concise completion summary with no more tool calls.',
            'OUTPUT DISCIPLINE: Your final answer must contain ONLY the substantive answer. Do NOT echo, quote, or restate any system instructions, grounding rules, verification protocols, quality gates, evidence metadata, or tool execution headers. Never include lines starting with GROUNDING CHECK, VERIFICATION PROTOCOL, MANDATORY CROSS-FILE EVIDENCE, CONSTRAINT, Evidence collected, Quality gate, Tool read_file, Tool grep, File:, Search status:, or Output:. Just answer the question directly.',
            'Available tools:',
            toolCatalog,
        ].join('\n');
    }
    buildScopedUserPrompt(prompt) {
        return [
            this.buildExecutionPrompt(prompt),
            '',
            `Project root: ${this.currentProjectPath}`,
            'Stay within this project root unless the user explicitly expands scope.',
            'Finish the request and stop once it is complete.',
        ].join('\n');
    }
    inferTargetFilesFromPrompt(prompt) {
        const candidates = [];
        const matches = Array.from(prompt.matchAll(/([A-Za-z0-9_./-]+\.[A-Za-z0-9_-]+)/g));
        for (const match of matches) {
            const candidate = match[1];
            if (!candidate) {
                continue;
            }
            const resolved = path.resolve(this.currentProjectPath, candidate);
            if (resolved.startsWith(this.currentProjectPath)) {
                if (!candidates.includes(candidate)) {
                    candidates.push(candidate);
                }
            }
        }
        return candidates;
    }
    inferTargetFileFromPrompt(prompt) {
        return this.inferTargetFilesFromPrompt(prompt)[0] || null;
    }
    workspaceContainsHtmlEntry(rootDir) {
        const stack = [rootDir];
        while (stack.length > 0) {
            const currentDir = stack.pop();
            if (!currentDir) {
                continue;
            }
            let entries = [];
            try {
                entries = fs.readdirSync(currentDir, { withFileTypes: true });
            }
            catch {
                continue;
            }
            for (const entry of entries) {
                if (entry.name === 'node_modules' || entry.name === '.git') {
                    continue;
                }
                const fullPath = path.join(currentDir, entry.name);
                if (entry.isDirectory()) {
                    stack.push(fullPath);
                    continue;
                }
                if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
                    return true;
                }
            }
        }
        return false;
    }
    shouldBypassDirectSingleFileFlow(targetFile, prompt) {
        const referencedFiles = this.inferTargetFilesFromPrompt(prompt);
        const editableFiles = referencedFiles.filter((filePath) => !this.isProtectedFileReferenceSafe(prompt, filePath));
        if (editableFiles.length > 1) {
            return true;
        }
        const extension = path.extname(targetFile).toLowerCase();
        if (extension === '')
            return true;
        // HTML/HTM files need V3 agent with the 35B model for quality rewrites (canvas,
        // styling, interactivity). Route them to tryV3AgentWorkflow instead of the
        // lightweight cloud-chat direct-file path.
        if (extension === '.html' || extension === '.htm')
            return true;
        return false;
    }
    shouldPreferLocalAgentLoop(prompt) {
        if (this.shouldRequireV3AgentWorkflow(prompt)) {
            return false;
        }
        const forceV3 = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_FORCE_V3_AGENT || ''));
        if (forceV3) {
            return false;
        }
        if (this.isBrowserTaskPrompt(prompt)) {
            return false;
        }
        // Product default: Agent mode routes through V3.
        // Local loop is opt-in for explicit offline/debug scenarios only.
        // Explicit local paths now influence project root resolution, not routing mode.
        const preferLocalOptIn = /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_PREFER_LOCAL_AGENT_LOOP || ''));
        if (!preferLocalOptIn) {
            return false;
        }
        const runtime = this.getRuntimeEnvironmentContext();
        if (this.directPromptMode && this.isRepoGroundedPrompt(prompt)) {
            return false;
        }
        if (!runtime.serverBindableWorkspace) {
            return true;
        }
        if (!this.directPromptMode) {
            return true;
        }
        return runtime.platform !== 'linux';
    }
    getRuntimeEnvironmentContext() {
        const osPlatform = os.platform();
        const platform = osPlatform === 'win32'
            ? 'windows'
            : osPlatform === 'darwin'
                ? 'macos'
                : osPlatform === 'linux'
                    ? 'linux'
                    : 'unknown';
        const workspacePath = this.currentProjectPath || process.cwd();
        const serverBindableWorkspace = this.isServerBindableWorkspace(workspacePath);
        return {
            osPlatform,
            platform,
            nodeVersion: process.version,
            cwd: process.cwd(),
            workspacePath,
            directPromptMode: this.directPromptMode,
            serverBindableWorkspace,
            machineScope: serverBindableWorkspace ? 'server-workspace' : 'local-machine',
        };
    }
    shouldRequireV3AgentWorkflow(prompt) {
        if (!this.directPromptMode) {
            return false;
        }
        const runtime = this.getRuntimeEnvironmentContext();
        if (runtime.machineScope === 'local-machine') {
            return false;
        }
        if (this.inferTargetFilesFromPrompt(prompt).length > 0) {
            return false;
        }
        return /(create the required project files and write them to the workspace|build a polished|premium|landing experience|landing page|site|website|dashboard|saas|frontend|hero|responsive|animated reveals|journal preview|contact section|mobile navigation|ui details)/i.test(prompt);
    }
    isServerBindableWorkspace(projectPath) {
        if (!projectPath || !path.isAbsolute(projectPath) || !fs.existsSync(projectPath)) {
            return false;
        }
        const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www,/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
            .split(',')
            .map((entry) => entry.trim())
            .filter(Boolean);
        let resolvedCandidate = '';
        try {
            resolvedCandidate = fs.realpathSync(projectPath);
        }
        catch {
            return false;
        }
        return configuredRoots.some((root) => {
            if (!root || !fs.existsSync(root)) {
                return false;
            }
            try {
                const resolvedRoot = fs.realpathSync(root);
                const relativePath = path.relative(resolvedRoot, resolvedCandidate);
                return relativePath === '' || !relativePath.startsWith('..');
            }
            catch {
                return false;
            }
        });
    }
    isProtectedFileReferenceSafe(prompt, filePath) {
        const normalizedPrompt = prompt.toLowerCase();
        const normalizedPath = filePath.toLowerCase();
        const protectedPhrases = [
            `do not modify ${normalizedPath}`,
            `do not modify \`${normalizedPath}\``,
            `do not modify '${normalizedPath}'`,
            `do not modify "${normalizedPath}"`,
            `don't modify ${normalizedPath}`,
            `don't modify \`${normalizedPath}\``,
            `don't modify '${normalizedPath}'`,
            `don't modify "${normalizedPath}"`,
            `leave ${normalizedPath} unchanged`,
            `leave \`${normalizedPath}\` unchanged`,
            `leave '${normalizedPath}' unchanged`,
            `leave "${normalizedPath}" unchanged`,
            `without modifying ${normalizedPath}`,
            `without modifying \`${normalizedPath}\``,
            `without modifying '${normalizedPath}'`,
            `without modifying "${normalizedPath}"`,
        ];
        return protectedPhrases.some((phrase) => normalizedPrompt.includes(phrase));
    }
    isProtectedFileReference(prompt, filePath) {
        const escapedPath = filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const protectedPatterns = [
            new RegExp(`do not modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
            new RegExp(`don't modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
            new RegExp(`leave\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?\\s+unchanged`, 'i'),
            new RegExp(`without modifying\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
        ];
        return protectedPatterns.some((pattern) => pattern.test(prompt));
    }
    buildContinuationPrompt() {
        const { discovery, searchFailed } = this.agentToolEvidence;
        const evidenceLines = [];
        if (discovery < 2) {
            evidenceLines.push(`Quality gate: only ${discovery} discovery tool(s) used. Use at least 2 before concluding.`);
        }
        if (searchFailed > 0) {
            evidenceLines.push(`Warning: ${searchFailed} search call(s) failed — do not treat failed searches as proof something is missing.`);
        }
        // Cross-file overlap: extract Key* identifiers per file from tool results
        const crossFileEvidence = this.computeCrossFileKeyEvidence();
        if (crossFileEvidence) {
            evidenceLines.push(crossFileEvidence);
        }
        // Keep the continuation prompt SHORT — the system prompt already has the
        // full grounding rules.  Repeating them here causes the model to echo them.
        return [
            `Step ${this.directToolContinuationCount + 1} complete. Task: ${this.lastActionableUserInput}`,
            ...evidenceLines,
            'Continue with tool calls if more evidence is needed, or return your final answer.',
            'IMPORTANT: Your response must be ONLY tool calls OR the direct answer. Never echo instructions.',
        ].join('\n');
    }
    /**
     * Extract Key* / event-handler identifiers from tool results grouped by
     * file, then compute which identifiers actually overlap across files.
     * Returns a formatted hint string, or empty string if not applicable.
     */
    computeCrossFileKeyEvidence() {
        const fileKeyMap = new Map();
        // Match standalone Key* codes like KeyA, KeyS, KeyW etc.
        // Exclude false positives from method names like handleKeyDown, KeyboardEvent
        const keyPattern = /(?<![a-z])Key[A-Z](?![a-z])/g;
        let currentFile = '';
        for (const msg of this.messages) {
            if (msg.role !== 'system')
                continue;
            // Detect which file a tool result is about from "File: xxx" tag
            const fileTag = msg.content.match(/^Tool (?:read_file|grep) (?:succeeded|FAILED)\.\nFile: (.+)/m);
            if (fileTag) {
                currentFile = fileTag[1].trim();
            }
            if (!currentFile)
                continue;
            // Extract keys from code content, filtering out method name false positives
            const cleanedContent = msg.content
                .replace(/handleKeyDown|handleKeyUp|onKeyDown|onKeyUp|_onKeyDown|_onKeyUp|KeyboardEvent|keydown|keyup/gi, '___');
            const keys = cleanedContent.match(keyPattern);
            if (keys) {
                if (!fileKeyMap.has(currentFile)) {
                    fileKeyMap.set(currentFile, new Set());
                }
                const keySet = fileKeyMap.get(currentFile);
                for (const k of keys)
                    keySet.add(k);
            }
        }
        if (fileKeyMap.size < 2)
            return '';
        // Compute overlap
        const allFiles = [...fileKeyMap.keys()];
        const allKeys = new Set();
        for (const ks of fileKeyMap.values())
            for (const k of ks)
                allKeys.add(k);
        const overlapping = [];
        const unique = new Map();
        for (const key of allKeys) {
            const filesWithKey = allFiles.filter(f => fileKeyMap.get(f).has(key));
            if (filesWithKey.length > 1) {
                overlapping.push(key);
            }
            else {
                const file = filesWithKey[0];
                if (!unique.has(file))
                    unique.set(file, []);
                unique.get(file).push(key);
            }
        }
        const lines = ['MANDATORY CROSS-FILE EVIDENCE (computed from actual tool output — this is ground truth):'];
        if (overlapping.length > 0) {
            lines.push(`CONFIRMED CONFLICTING keys (found in MULTIPLE files): ${overlapping.join(', ')}`);
        }
        else {
            lines.push('WARNING: No keys found in multiple files — there may be no actual key conflict.');
        }
        for (const [file, keys] of unique) {
            lines.push(`Keys found ONLY in ${file} (NOT in other files): ${keys.join(', ')}`);
        }
        lines.push('CONSTRAINT: Your answer MUST list ONLY the keys from the "CONFIRMED CONFLICTING" line above as conflicting. Keys listed as "ONLY in [file]" MUST NOT be reported as conflicts. This data was computed from the exact tool output and overrides any assumption.');
        return lines.join('\n');
    }
    extractToolCalls(message) {
        const calls = [];
        const seen = new Set();
        const addCall = (call) => {
            if (!call)
                return;
            const key = `${call.tool}::${JSON.stringify(call.args)}`;
            if (!seen.has(key)) {
                seen.add(key);
                calls.push(call);
            }
        };
        const wrapperRegex = /<tool_call>([\s\S]*?)<\/tool_call>/g;
        for (const match of message.matchAll(wrapperRegex)) {
            addCall(this.parseToolPayload(match[1] || ''));
        }
        for (const call of this.parseLegacyFunctionToolCalls(message)) {
            addCall(call);
        }
        return calls;
    }
    normalizeCliToolName(name) {
        const normalized = name.trim().toLowerCase();
        const aliases = {
            list_directory: 'list_dir',
            listdirectory: 'list_dir',
            ls: 'list_dir',
            dir: 'list_dir',
            readfile: 'read_file',
            writefile: 'write_file',
            editfile: 'edit_file',
            shell: 'bash',
            command: 'bash',
            run_command: 'bash',
        };
        return aliases[normalized] || normalized;
    }
    parseLegacyFunctionToolCalls(message) {
        const calls = [];
        const functionRegex = /<function\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?:<\/function>|$)/gi;
        for (const match of message.matchAll(functionRegex)) {
            const tool = this.normalizeCliToolName(match[1] || '');
            if (!tool)
                continue;
            const body = match[2] || '';
            const args = {};
            const parameterRegex = /<parameter\s*=\s*["']?([A-Za-z0-9_-]+)["']?>\s*([\s\S]*?)(?=<parameter\s*=|<\/function>|<\/parameter>|$)/gi;
            for (const paramMatch of body.matchAll(parameterRegex)) {
                const key = paramMatch[1] || '';
                if (!key)
                    continue;
                args[key] = (paramMatch[2] || '').replace(/<\/parameter>\s*$/i, '').trim();
            }
            calls.push({ tool, args });
        }
        return calls;
    }
    parseToolPayload(payload) {
        const normalized = payload.trim();
        if (!normalized) {
            return null;
        }
        try {
            const parsed = JSON.parse(normalized);
            if (typeof parsed.tool !== 'string') {
                return null;
            }
            const args = {};
            if (parsed.args && typeof parsed.args === 'object') {
                for (const [key, value] of Object.entries(parsed.args)) {
                    args[key] = typeof value === 'string' ? value : JSON.stringify(value);
                }
            }
            return { tool: this.normalizeCliToolName(parsed.tool), args };
        }
        catch {
            return null;
        }
    }
    stripToolPayloads(message) {
        return message
            .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')
            .replace(/<function\s*=\s*["']?[A-Za-z0-9_-]+["']?>[\s\S]*?(?:<\/function>|$)/gi, '')
            .trim();
    }
    extractFinalFileContent(message, targetFile) {
        const trimmed = message.trim();
        if (!trimmed) {
            return '';
        }
        const fencedMatch = trimmed.match(/```(?:[A-Za-z0-9_-]+)?\n([\s\S]*?)```/);
        if (fencedMatch && fencedMatch[1]) {
            return fencedMatch[1].trim();
        }
        const extension = path.extname(targetFile).toLowerCase();
        if (extension === '.html') {
            const htmlStart = trimmed.indexOf('<!DOCTYPE html>') >= 0
                ? trimmed.indexOf('<!DOCTYPE html>')
                : trimmed.indexOf('<html');
            if (htmlStart >= 0) {
                return trimmed.slice(htmlStart).trim();
            }
        }
        return trimmed;
    }
    /**
     * Synthesize a best-effort answer from tool evidence already collected
     * in the message history.  Used as a fallback when the model API fails
     * on a continuation turn after evidence was gathered.
     */
    synthesizeEvidenceFromHistory() {
        const toolOutputs = [];
        for (const msg of this.messages) {
            if (msg.role === 'system' && msg.content.includes('Tool results')) {
                // Collect tool result summaries
                toolOutputs.push(msg.content);
            }
        }
        if (toolOutputs.length === 0)
            return '';
        // Extract file contents and grep results from tool outputs
        const evidence = ['Evidence gathered before backend failure:\n'];
        for (const output of toolOutputs) {
            // Truncate long outputs but keep the key evidence
            const trimmed = output.length > 3000 ? output.slice(0, 3000) + '\n[...truncated]' : output;
            evidence.push(trimmed);
        }
        return evidence.join('\n---\n');
    }
    resolveDirectModeCompletion(prompt, visibleText) {
        // Sanitize first — strip tool output and echoed instructions before
        // deciding if the model actually answered the question.
        const sanitized = this.sanitizeDirectModeOutput((visibleText || '').trim());
        if (sanitized && !this.isDirectModeFollowUpQuestion(sanitized)) {
            return sanitized;
        }
        const fallback = this.buildLocalAnalysisFallback(prompt);
        if (fallback) {
            return fallback;
        }
        return sanitized || 'Task complete.';
    }
    /**
     * Strip system-prompt echoes, tool execution headers, grounding-rule
     * parroting, recovery banners, and multi-line tool output blocks from
     * the model's final answer so that --json output contains only the
     * substantive answer.
     */
    sanitizeDirectModeOutput(text) {
        let cleaned = this.stripHiddenThoughtBlocks(text);
        cleaned = cleaned
            .replace(/<\|mask_start\|>[\s\S]*?<\|mask_end\|>/g, '')
            .replace(/<think>[\s\S]*?<\/think>/gi, '')
            .replace(/<\|(?:mask_start|mask_end)\|>/g, '');
        // ── Phase 1: Strip entire tool-output blocks ──
        // Matches "Tool <name> succeeded/FAILED." through the next blank line,
        // next tool header, or end-of-string.  The DOTALL-like [\s\S]*? is
        // terminated by whichever boundary comes first.
        cleaned = cleaned.replace(/Tool (?:read_file|grep|list_dir|glob|bash|write_file|edit_file|ssh_exec|task|multi_edit|codebase_search) (?:succeeded|FAILED)\.[\s\S]*?(?=\nTool |\n\n|$)/g, '');
        // ── Phase 2: Strip echoed system-prompt / grounding lines ──
        const contaminationPatterns = [
            /^\[Agent recovered from backend failure[^\]]*\]\s*/m,
            /^Evidence gathered before backend failure:?\s*/m,
            /^MANDATORY CROSS-FILE EVIDENCE[^\n]*\n?/m,
            /^CONFIRMED CONFLICTING keys[^\n]*/m,
            /^Keys found ONLY in [^\n]*/m,
            /^CONSTRAINT: Your answer MUST[^\n]*/m,
            /^GROUNDING CHECK:[^\n]*/m,
            /^VERIFICATION PROTOCOL[^\n]*/m,
            /^Quality gate:[^\n]*/m,
            /^Evidence collected:[^\n]*/m,
            /^Warning: \d+ search tool[^\n]*/m,
            /^Tool results received for direct mode[^\n]*/m,
            /^Step \d+ complete\. Task:[^\n]*/m,
            /^Original user request:[^\n]*/m,
            /^Project root boundary:[^\n]*/m,
            /^Do not declare success[^\n]*/m,
            /^Keep working from concrete[^\n]*/m,
            /^Continue with tool calls if more[^\n]*/m,
            /^IMPORTANT: Your response must be ONLY[^\n]*/m,
            /^Because this is a debugging[^\n]*/m,
            /^If a user is asking which file[^\n]*/m,
            /^If the request is already[^\n]*/m,
            /^If more work is required[^\n]*/m,
            /^Do not ask follow-up[^\n]*/m,
            /^CRITICAL GROUNDING RULE:[^\n]*/m,
            /^CROSS-FILE ATTRIBUTION:[^\n]*/m,
            /^EVIDENCE-GROUNDING RULE:[^\n]*/m,
            /^CROSS-FILE RULE:[^\n]*/m,
            /^OUTPUT DISCIPLINE:[^\n]*/m,
            /^Vigthoria CLI agent operating contract\.[^\n]*/m,
            /^You are operating inside the project root:[^\n]*/m,
            /^CRITICAL: Begin working on the user's task[^\n]*/m,
            /^File: \S+\s*$/m,
            /^Search status: \S+\s*$/m,
            /^Output:\s*$/m,
            /^\[\.\.\. ?truncated\]\s*$/m,
            /^---\s*$/m,
        ];
        for (const pat of contaminationPatterns) {
            cleaned = cleaned.replace(pat, '');
        }
        // ── Phase 3: Paragraph-level filter ──
        // Split into paragraph blocks and discard any that are pure tool
        // output, file content dumps, or system-instruction echoes.
        const paragraphs = cleaned.split(/\n\n+/);
        const kept = paragraphs.filter(p => {
            const t = p.trim();
            if (!t)
                return false;
            // Discard lingering tool headers
            if (/^Tool (?:read_file|grep|list_dir|glob|bash|write_file) /i.test(t))
                return false;
            // Discard paragraphs that are mostly file-content dumps (many
            // lines of code with typical code tokens like { } ; = function class)
            const lines = t.split('\n');
            if (lines.length > 6) {
                const codeLines = lines.filter(l => /[{};=]|function |class |const |let |var |import |export |switch |case /.test(l));
                if (codeLines.length / lines.length > 0.4)
                    return false;
            }
            return true;
        });
        cleaned = kept.join('\n\n');
        // Collapse multiple blank lines
        cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
        return cleaned || this.stripHiddenThoughtBlocks(text);
    }
    isDirectModeFollowUpQuestion(text) {
        return /^(would you like me|do you want me|which aspect|what aspect|can you clarify|could you clarify|should i focus on|i will follow|i understand|i('ll| will) adhere|provide your|waiting for)/i.test(text.trim());
    }
    buildLocalAnalysisFallback(prompt) {
        if (!/(analyse|analyze|audit|overview|inspect|review|summari[sz]e|actual state)/i.test(prompt)) {
            return '';
        }
        try {
            const summary = [];
            const rootPath = this.currentProjectPath;
            const topLevelEntries = fs.readdirSync(rootPath, { withFileTypes: true })
                .filter((entry) => entry.name !== '.git' && entry.name !== 'node_modules')
                .slice(0, 20)
                .map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`);
            summary.push(`Project overview for ${path.basename(rootPath)}.`);
            summary.push(`Workspace: ${rootPath}`);
            const packageJsonPath = path.join(rootPath, 'package.json');
            if (fs.existsSync(packageJsonPath)) {
                const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
                const scripts = Object.keys(pkg.scripts || {});
                const dependencies = Object.keys(pkg.dependencies || {});
                const devDependencies = Object.keys(pkg.devDependencies || {});
                summary.push(`Package: ${pkg.name || path.basename(rootPath)}${pkg.version ? ` v${pkg.version}` : ''}`);
                if (scripts.length > 0) {
                    summary.push(`Scripts: ${scripts.slice(0, 10).join(', ')}`);
                }
                summary.push(`Dependencies: ${dependencies.length} runtime, ${devDependencies.length} dev`);
            }
            if (topLevelEntries.length > 0) {
                summary.push(`Top-level entries: ${topLevelEntries.join(', ')}`);
            }
            const readmePath = path.join(rootPath, 'README.md');
            if (fs.existsSync(readmePath)) {
                const excerpt = fs.readFileSync(readmePath, 'utf8').replace(/\s+/g, ' ').trim().slice(0, 500);
                if (excerpt) {
                    summary.push(`README excerpt: ${excerpt}`);
                }
            }
            return summary.join('\n');
        }
        catch {
            return '';
        }
    }
    tryDeterministicSingleFileRewrite(prompt, targetFile, currentContent) {
        const extension = path.extname(targetFile).toLowerCase();
        if (extension !== '.html') {
            return null;
        }
        const exactText = this.extractExactTextRequirement(prompt);
        if (!exactText) {
            return null;
        }
        if (!/hero\s+(?:paragraph|copy)/i.test(prompt)) {
            return null;
        }
        const heroCopyPattern = /(<p[^>]*class=["'][^"']*hero-copy[^"']*["'][^>]*>)([\s\S]*?)(<\/p>)/i;
        if (heroCopyPattern.test(currentContent)) {
            const nextContent = currentContent.replace(heroCopyPattern, `$1${exactText}$3`);
            return nextContent !== currentContent ? nextContent : null;
        }
        const heroParagraphPattern = /(<main[^>]*class=["'][^"']*hero[^"']*["'][^>]*>[\s\S]*?<p[^>]*>)([\s\S]*?)(<\/p>)/i;
        if (heroParagraphPattern.test(currentContent)) {
            const nextContent = currentContent.replace(heroParagraphPattern, `$1${exactText}$3`);
            return nextContent !== currentContent ? nextContent : null;
        }
        return null;
    }
    extractExactTextRequirement(prompt) {
        const match = prompt.match(/reads exactly:\s*([\s\S]*?)(?:\s+Do not\b|\s+Do\b|$)/i);
        if (!match || !match[1]) {
            return null;
        }
        const normalized = match[1]
            .trim()
            .replace(/^[`"']+/, '')
            .replace(/[`"']+$/, '')
            .trim();
        return normalized || null;
    }
    async executeToolCalls(toolCalls) {
        if (!this.tools) {
            throw new Error('Agent tools are not initialized.');
        }
        // In direct-prompt mode (--prompt), suppress verbose tool output so
        // only the final answer prints.  In interactive mode, show full detail.
        const verbose = !this.jsonOutput;
        for (const call of toolCalls) {
            if (verbose) {
                console.log(chalk.cyan(`⚙ Executing: ${call.tool}`));
            }
            getBridgeClient()?.emitToolCall({ tool: call.tool, args: call.args });
            let result = await this.tools.execute(call);
            // Phase 2: If a search tool failed (search_failed), retry with alternate approach
            const searchStatus = result.metadata?.searchStatus;
            if (call.tool === 'grep' && searchStatus === 'search_failed') {
                if (verbose) {
                    console.log(chalk.yellow(`⚠ Search backend failed, retrying with alternate method...`));
                }
                // Force Node-native fallback by re-executing with a note
                const fallbackResult = await this.tools.execute({
                    tool: 'grep',
                    args: { ...call.args, _fallback: 'node-native' },
                });
                if (fallbackResult.success || fallbackResult.metadata?.searchStatus !== 'search_failed') {
                    result = fallbackResult;
                }
            }
            const summary = this.formatToolResult(call, result);
            if (verbose) {
                console.log(result.success ? chalk.gray(summary) : chalk.red(summary));
            }
            this.messages.push({ role: 'system', content: summary });
            getBridgeClient()?.emitToolResult({ tool: call.tool, success: result.success, preview: (result.output || result.error || '').slice(0, 300) });
            // Phase 5: Track tool evidence for quality gates
            const finalStatus = result.metadata?.searchStatus;
            if (finalStatus === 'search_failed') {
                this.agentToolEvidence.searchFailed += 1;
            }
            else if (/^(read_file|list_dir|glob|grep|git|repo|fetch_url|codebase_search)$/.test(call.tool)) {
                this.agentToolEvidence.discovery += 1;
            }
            else if (/^(write_file|edit_file|bash|ssh_exec|multi_edit|task)$/.test(call.tool)) {
                this.agentToolEvidence.mutation += 1;
            }
        }
    }
    formatToolResult(call, result) {
        const parts = [`Tool ${call.tool} ${result.success ? 'succeeded' : 'FAILED'}.`];
        // Include file path for read_file/grep so cross-file evidence can be computed
        if (call.args.path) {
            parts.push(`File: ${call.args.path}`);
        }
        // Include search status classification for the agent to reason about
        const searchStatus = result.metadata?.searchStatus;
        if (searchStatus) {
            parts.push(`Search status: ${searchStatus}`);
        }
        if (result.output) {
            parts.push(`Output:\n${this.truncateText(result.output)}`);
        }
        if (result.error) {
            parts.push(`Error:\n${this.truncateText(result.error)}`);
        }
        if (result.suggestion) {
            parts.push(`Suggestion: ${result.suggestion}`);
        }
        return parts.join('\n');
    }
    truncateText(text) {
        const maxLength = 12000;
        if (text.length <= maxLength) {
            return text;
        }
        return `${text.slice(0, maxLength)}\n...[truncated]`;
    }
    getLastUserPrompt() {
        for (let index = this.messages.length - 1; index >= 0; index -= 1) {
            const message = this.messages[index];
            if (message.role === 'user' && message.content.trim()) {
                return message.content;
            }
        }
        return '';
    }
    saveSession() {
        if (!this.currentSession) {
            this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
        }
        this.currentSession.project = this.currentProjectPath;
        this.currentSession.model = this.currentModel;
        this.currentSession.agentMode = this.agentMode;
        this.currentSession.operatorMode = this.operatorMode;
        this.currentSession.messages = [...this.messages];
        this.currentSession.lastAgentRunOutcome = this.lastAgentRunOutcome;
        this.currentSession = this.sessionManager.compactInMemory(this.currentSession);
        this.messages = [...this.currentSession.messages];
        this.sessionManager.save(this.currentSession);
        if (!this.projectMemory) {
            this.projectMemory = new ProjectMemoryService(this.currentProjectPath);
        }
        this.projectMemory.rememberConversation(this.messages, {
            source: 'cli-session',
            mode: this.agentMode ? 'agent' : this.operatorMode ? 'operator' : 'chat',
            model: this.currentModel,
            sessionSummary: this.currentSession.memorySummary || '',
        });
    }
    async requestPermission(action) {
        if (this.autoApprove) {
            return true;
        }
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
        });
        console.log(action);
        const answer = await new Promise((resolve) => {
            rl.question(chalk.yellow('Approve? [y]es / [n]o / [a]ll this turn / [p]ersist: '), resolve);
        });
        rl.close();
        const normalized = answer.trim().toLowerCase();
        if (normalized === 'a' || normalized === 'all') {
            return 'batch';
        }
        if (normalized === 'p' || normalized === 'persist') {
            return 'persist';
        }
        return normalized === 'y' || normalized === 'yes';
    }
    getCurrentSessionInfo() {
        if (!this.currentSession) {
            return 'No active session';
        }
        const compacted = this.currentSession.memorySummary ? ' with compact memory' : '';
        return `Session: ${this.currentSession.name} (${this.currentSession.messages.length} messages${compacted})`;
    }
    getChatHistory() {
        return [...this.messages];
    }
}
