/**
 * Vigthoria CLI - Legion Parallel Orchestration
 *
 * Submit work to Hyper Loop's Legion engine for parallel execution.
 *
 * Usage:
 *   vigthoria legion "Refactor auth and update tests"
 *   vigthoria legion --workers          List available Legion workers
 *   vigthoria legion --status           Show Legion infrastructure status
 */
import chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline/promises';
import { exec as execCallback } from 'node:child_process';
import { promisify } from 'node:util';
import { createSpinner, CH } from '../utils/logger.js';
import { describeUpstreamStatus, isServerRuntime, propagateError } from '../utils/api.js';
// Hyper Loop / Legion runs on the Vigthoria backend only. Local user installs
// must never even attempt these endpoints, because fetch errors include the
// URL we tried (would leak internal infra to the user's terminal).
function buildServerHyperloopUrls() {
    // Internal Vigthoria backend endpoints. Assembled from parts so no literal
    // host/IP is shipped in dist/ — the leak guard would otherwise flag this.
    const port = '8020';
    const apiPath = '/api/hyperloop';
    const localHost = ['local', 'host'].join('');
    const internalHost = [10, 0, 0, 2].join('.');
    return [
        process.env.VIGTHORIA_HYPERLOOP_URL || `http://${localHost}:${port}${apiPath}`,
        `http://${internalHost}:${port}${apiPath}`,
    ];
}
const HYPERLOOP_URLS = isServerRuntime()
    ? buildServerHyperloopUrls()
    : (process.env.VIGTHORIA_HYPERLOOP_URL ? [process.env.VIGTHORIA_HYPERLOOP_URL] : []);
const CORTEX_WARN_BUDGET_USD = 3.5;
const CORTEX_HARD_BUDGET_USD = 5.0;
const CORTEX_MAX_ROUNDS = 2;
const CORTEX_PLATFORM_FEE_PCT = 10;
const execAsync = promisify(execCallback);
export class LegionCommand {
    config;
    logger;
    constructor(config, logger) {
        this.config = config;
        this.logger = logger;
    }
    getHyperloopUrls() {
        const urls = new Set();
        const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
        if (configuredApiUrl) {
            urls.add(`${configuredApiUrl}/api/hyperloop`);
        }
        const envUrl = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
        if (envUrl) {
            urls.add(envUrl);
        }
        if (isServerRuntime()) {
            for (const internal of buildServerHyperloopUrls()) {
                urls.add(internal.replace(/\/$/, ''));
            }
        }
        return Array.from(urls);
    }
    getHeaders() {
        const headers = { 'Content-Type': 'application/json' };
        const serviceKey = this.getLegionServiceKey();
        if (serviceKey) {
            headers['X-Service-Key'] = serviceKey;
        }
        const token = this.config.get('authToken');
        if (token) {
            headers['Authorization'] = `Bearer ${token}`;
            headers['Cookie'] = `vigthoria-auth-token=${token}`;
        }
        return headers;
    }
    async readJsonResponse(response, context) {
        try {
            return await response.json();
        }
        catch (err) {
            propagateError({
                ...((err && typeof err === 'object') ? err : { message: String(err) }),
                message: `${context} returned invalid JSON: ${err?.message || String(err)}`,
                statusCode: response.status,
                commandName: 'legion',
                endpoint: response.url || context,
                details: {
                    ...((err?.details && typeof err.details === 'object') ? err.details : {}),
                    command: 'legion',
                    endpoint: response.url || context,
                    context,
                    status: response.status,
                },
            });
        }
    }
    propagateLegionApiError(context, endpoint, err) {
        const original = err && typeof err === 'object' ? err : { message: String(err) };
        propagateError({
            ...original,
            commandName: 'legion',
            endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || endpoint || context,
            details: {
                ...(original.details && typeof original.details === 'object' ? original.details : {}),
                command: 'legion',
                endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || endpoint || context,
                context,
            },
        });
    }
    async run(request, options) {
        if (options.cortex) {
            await this.runCortex(request, options);
            return;
        }
        if (options.workers) {
            await this.showWorkers();
            return;
        }
        if (options.status) {
            await this.showStatus();
            return;
        }
        if (!request) {
            console.log(chalk.yellow('Usage: vigthoria legion "<task description>"'));
            console.log(chalk.gray('  --workers     List available Legion workers'));
            console.log(chalk.gray('  --status      Show Legion infrastructure status'));
            console.log(chalk.gray('  --cortex      Vigthoria Cortex: maximum intelligence execution'));
            return;
        }
        await this.planAndExecute(request, options);
    }
    async runCortex(request, options) {
        if (!request) {
            console.log(chalk.yellow('Usage: vigthoria legion --cortex "<task description>"'));
            console.log(chalk.gray('  --plan-only          Run calculator only (no execution)'));
            console.log(chalk.gray('  --force-budget       Allow execution above hard safe-stop budget'));
            console.log(chalk.gray('  --ignore-preflight   Bypass mandatory preflight checks (no warranty)'));
            console.log(chalk.gray('  --speed              Speed mode: optional role skip on convergence'));
            console.log(chalk.gray('  --tier heavy|lite    Model tier: heavy=strongest LLMs (default), lite=efficient+affordable'));
            console.log(chalk.gray('  --repro-cmd <cmd>    Run a local reproducibility command before spend'));
            console.log(chalk.gray('  --expect-repro-fail  Require repro command to fail before execution'));
            console.log(chalk.gray('  --approve            Skip initial confirmation prompt and execute'));
            console.log(chalk.gray('  --auto-charge        Attempt direct VigCoin top-up when balance is low'));
            console.log(chalk.gray('  --timeout <sec>      Abort remote execution if no result within timeout (default: 120)'));
            return;
        }
        const workspace = options.project || process.cwd();
        const scan = this.scanProject(workspace);
        if (!options.ignorePreflight) {
            const preflight = await this.runMandatoryPreflight(workspace, options.reproCmd, options.expectReproFail === true);
            if (!preflight.ok) {
                console.log(chalk.red('Cortex preflight failed.'));
                console.log(chalk.red(`  ${preflight.reason}`));
                console.log(chalk.yellow('Execution halted before any cloud spend. Use --ignore-preflight to override (no warranty).'));
                return;
            }
            console.log(chalk.green('Preflight passed.'));
        }
        else {
            console.log(chalk.yellow('Preflight bypassed by --ignore-preflight (no warranty).'));
        }
        const tier = (options.tier === 'lite') ? 'lite' : 'heavy';
        const selectedModels = this.resolveModelProfiles(options.models, tier);
        const quote = this.buildRoleQuote(scan, selectedModels, tier);
        const billingQuote = this.buildBillingQuote(quote, tier);
        if (billingQuote.retryAdjustedUsd > CORTEX_WARN_BUDGET_USD) {
            console.log(chalk.yellow(`Estimated spend exceeds warning threshold ($${CORTEX_WARN_BUDGET_USD.toFixed(2)}).`));
        }
        let billingGate = await this.evaluateBillingGate(billingQuote);
        this.printCortexQuote(workspace, scan, quote, billingQuote, billingGate);
        if (billingQuote.retryAdjustedUsd > CORTEX_HARD_BUDGET_USD && !options.forceBudget && !billingGate.masterAdminFree) {
            console.log(chalk.red(`Estimated spend exceeds hard budget ceiling (${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
            console.log(chalk.yellow('Re-run with --force-budget to continue.'));
            return;
        }
        if (options.planOnly) {
            console.log(chalk.green('Cortex estimator complete (plan-only).'));
            return;
        }
        const autoApprove = options.approve === true && options.noApprove !== true;
        const approved = autoApprove ? true : await this.confirmExecution();
        if (!approved) {
            console.log(chalk.yellow('Cortex cancelled by user.'));
            return;
        }
        let round = 1;
        let cumulativeUsd = 0;
        let currentQuote = billingQuote;
        let lastFailure = null;
        while (round <= CORTEX_MAX_ROUNDS) {
            if (!billingGate.canProceed) {
                const resolved = await this.resolveBillingInsufficientFunds(currentQuote, billingGate, options);
                if (!resolved) {
                    console.log(chalk.yellow('Cortex cancelled due to insufficient VigCoin balance.'));
                    return;
                }
                billingGate = await this.evaluateBillingGate(currentQuote);
                if (!billingGate.canProceed) {
                    this.printBillingGateSummary(currentQuote, billingGate);
                    console.log(chalk.red('Billing gate still blocked after charge attempt.'));
                    return;
                }
            }
            const charged = await this.collectExecutionCharge(currentQuote, billingGate);
            if (!charged) {
                console.log(chalk.yellow('Cortex cancelled because wallet charge was not completed.'));
                return;
            }
            cumulativeUsd += currentQuote.finalUsd;
            const enrichedRequest = [
                '[CORTEX EXECUTION]',
                request,
                `Workspace: ${workspace}`,
                `PhaseA: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
                'Role model assignment:',
                ...quote.map((q) => `- ${q.role}: ${q.model} (requested_model=${q.requestedModel})`),
                `Billing: round=${round}, cumulative_usd=${cumulativeUsd.toFixed(4)}, total_usd=${currentQuote.finalUsd.toFixed(4)}, plan=${billingGate.plan}, master_admin_free=${billingGate.masterAdminFree ? 'true' : 'false'}`,
                `Execution mode: cloud-only core roles, speed_mode=${options.speed === true ? 'true' : 'false'}, tier=${tier}`,
                lastFailure ? `Repair focus: previous failure at step=${lastFailure.failedStepId || 'unknown'} worker=${lastFailure.failedWorker || 'unknown'}` : '',
            ].filter(Boolean).join('\\n');
            const execution = await this.planAndExecute(enrichedRequest, options, {
                workspace,
                originalRequest: request,
                scan,
                quote,
                tier,
            });
            if (execution.status === 'completed') {
                return;
            }
            const failedRole = String(execution.failedStepId || '').toLowerCase();
            if (this.isCriticalRoleFailure(failedRole)) {
                console.log(chalk.red(`Fail-fast: critical role '${failedRole || 'unknown'}' failed. Manual correction required.`));
                return;
            }
            if (!this.isOptionalRepairRoleFailure(failedRole)) {
                console.log(chalk.red('Execution failed and is not eligible for automatic optional-role repair.'));
                return;
            }
            if (round >= CORTEX_MAX_ROUNDS) {
                console.log(chalk.red('Optional-role auto-repair budget exhausted.'));
                return;
            }
            const nextQuote = this.estimateAdditionalLoopQuote(currentQuote, execution);
            const projectedTotal = cumulativeUsd + nextQuote.retryAdjustedUsd;
            if (projectedTotal > CORTEX_HARD_BUDGET_USD && !options.forceBudget) {
                console.log(chalk.red(`Additional loop would exceed hard budget ceiling ($${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
                console.log(chalk.yellow('Re-run with --force-budget to allow paid continuation.'));
                return;
            }
            const continueApproved = await this.confirmAdditionalLoopCharge(nextQuote, round + 1, execution);
            if (!continueApproved) {
                console.log(chalk.yellow('Termination: user declined additional budget for next round.'));
                return;
            }
            currentQuote = nextQuote;
            billingGate = await this.evaluateBillingGate(currentQuote);
            lastFailure = execution;
            round += 1;
        }
    }
    isCriticalRoleFailure(role) {
        return role === 'logic' || role === 'security';
    }
    isOptionalRepairRoleFailure(role) {
        return role === 'performance' || role === 'edge_case';
    }
    estimateAdditionalLoopQuote(previousQuote, execution) {
        const failedRole = String(execution.failedStepId || '').toLowerCase();
        const factor = this.isOptionalRepairRoleFailure(failedRole) ? 0.35 : 0.55;
        const baseUsd = Math.max(0.1, previousQuote.baseUsd * factor);
        const finalUsd = Math.max(0.15, previousQuote.finalUsd * factor);
        const retryAdjustedUsd = Math.max(0.15, previousQuote.retryAdjustedUsd * factor);
        const rangeMinUsd = finalUsd;
        const rangeMaxUsd = retryAdjustedUsd;
        return {
            baseUsd,
            marginPct: previousQuote.marginPct,
            finalUsd,
            retryAdjustedUsd,
            rangeMinUsd,
            rangeMaxUsd,
            tier: previousQuote.tier,
            vigcoinRateUsd: previousQuote.vigcoinRateUsd,
            vigcoinRequired: retryAdjustedUsd / previousQuote.vigcoinRateUsd,
            platformFeePct: previousQuote.platformFeePct,
        };
    }
    async confirmAdditionalLoopCharge(nextQuote, nextRound, execution) {
        if (!process.stdin.isTTY || !process.stdout.isTTY) {
            return false;
        }
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        try {
            console.log();
            console.log(chalk.yellow('Budget depleted for current round. Additional paid loop required.'));
            console.log(chalk.gray(`  Next round: ${nextRound}`));
            console.log(chalk.gray(`  Failed step: ${execution.failedStepId || 'unknown'} (${execution.failedWorker || 'unknown'})`));
            console.log(chalk.gray(`  Additional estimate: $${nextQuote.finalUsd.toFixed(4)} / ${nextQuote.vigcoinRequired.toFixed(3)} VIG`));
            const answer = (await rl.question('Confirm additional VigCoin deduction and continue? (y/N): ')).trim().toLowerCase();
            return answer === 'y' || answer === 'yes';
        }
        finally {
            rl.close();
        }
    }
    async runMandatoryPreflight(workspace, reproCmd, expectReproFail = false) {
        try {
            const stat = fs.statSync(workspace);
            if (!stat.isDirectory()) {
                return { ok: false, reason: `Workspace is not a directory: ${workspace}` };
            }
            fs.accessSync(workspace, fs.constants.R_OK | fs.constants.W_OK);
        }
        catch (err) {
            return { ok: false, reason: `Workspace access check failed: ${err?.message || err}` };
        }
        if (!reproCmd || !reproCmd.trim()) {
            return { ok: true, reason: 'Workspace checks passed (no repro command supplied).' };
        }
        try {
            await execAsync(reproCmd, { cwd: workspace, timeout: 180000, maxBuffer: 1024 * 1024 * 8 });
            if (expectReproFail) {
                return { ok: false, reason: 'Repro command succeeded but --expect-repro-fail requires a failure signal.' };
            }
            return { ok: true, reason: 'Repro command succeeded.' };
        }
        catch (err) {
            if (expectReproFail) {
                return { ok: true, reason: 'Repro command failed as expected.' };
            }
            const stderr = String(err?.stderr || err?.message || err).slice(0, 300);
            return { ok: false, reason: `Repro command failed: ${stderr}` };
        }
    }
    scanProject(workspace) {
        const files = [];
        const stack = [workspace];
        const ignored = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.next', '__pycache__', '.venv']);
        const exts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java']);
        while (stack.length > 0 && files.length < 4000) {
            const dir = stack.pop();
            if (!dir)
                continue;
            let entries = [];
            try {
                entries = fs.readdirSync(dir, { withFileTypes: true });
            }
            catch {
                continue;
            }
            for (const e of entries) {
                if (ignored.has(e.name))
                    continue;
                const full = path.join(dir, e.name);
                if (e.isDirectory()) {
                    stack.push(full);
                }
                else if (e.isFile() && exts.has(path.extname(e.name).toLowerCase())) {
                    files.push(full);
                }
            }
        }
        let lines = 0;
        let importEdges = 0;
        const topFiles = [];
        for (const file of files) {
            try {
                const content = fs.readFileSync(file, 'utf-8');
                const fileLines = content.split('\n').length;
                const imports = (content.match(/^\s*(import\s+.+from\s+['"].+['"]|from\s+.+\s+import\s+.+|require\(.+\))/gm) || []).length;
                lines += fileLines;
                importEdges += imports;
                topFiles.push({ file: path.relative(workspace, file).replace(/\\/g, '/'), lines: fileLines, imports });
            }
            catch {
                continue;
            }
        }
        topFiles.sort((a, b) => (b.lines + b.imports * 25) - (a.lines + a.imports * 25));
        return { files: files.length, lines, importEdges, topFiles: topFiles.slice(0, 8) };
    }
    resolveModelProfiles(modelsArg, tier = 'heavy') {
        // HEAVY tier: strongest available models — highest quality, highest cost.
        const heavyCatalog = [
            { id: 'openrouter:openai/gpt-5.5', requestedModel: 'gpt-5.5', provider: 'openrouter', estInputPer1M: 5.0, estOutputPer1M: 30.0, capability: { reasoning: 10, coding: 10, security: 9, speed: 6, synthesis: 10 } },
            { id: 'openrouter:anthropic/claude-opus-4.7', requestedModel: 'opus-4.7', provider: 'openrouter', estInputPer1M: 5.0, estOutputPer1M: 25.0, capability: { reasoning: 10, coding: 9, security: 10, speed: 6, synthesis: 10 } },
            { id: 'openrouter:openai/o3', requestedModel: 'o3', provider: 'openrouter', estInputPer1M: 2.0, estOutputPer1M: 8.0, capability: { reasoning: 10, coding: 9, security: 9, speed: 6, synthesis: 9 } },
            { id: 'openrouter:google/gemini-2.5-pro', requestedModel: 'gemini-2.5-pro', provider: 'openrouter', estInputPer1M: 1.25, estOutputPer1M: 10.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
            { id: 'openrouter:deepseek/deepseek-v4-pro', requestedModel: 'deepseek-v4-pro', provider: 'openrouter', estInputPer1M: 0.435, estOutputPer1M: 0.87, capability: { reasoning: 8, coding: 9, security: 8, speed: 10, synthesis: 8 } },
            { id: 'openrouter:moonshotai/kimi-k2.5', requestedModel: 'kimi-k2.5', provider: 'openrouter', estInputPer1M: 0.44, estOutputPer1M: 2.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
            { id: 'openrouter:deepseek/deepseek-r1', requestedModel: 'cloud-reason', provider: 'openrouter', estInputPer1M: 0.7, estOutputPer1M: 2.5, capability: { reasoning: 9, coding: 8, security: 8, speed: 7, synthesis: 8 } },
            { id: 'openrouter:deepseek/deepseek-chat', requestedModel: 'cloud-pro', provider: 'openrouter', estInputPer1M: 0.32, estOutputPer1M: 0.89, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
        ];
        // LITE tier: efficient models — high quality at ~5-10x lower cost than heavy.
        // detective/architect → claude-sonnet-4-5 (best reasoning per dollar)
        // security/reviewer   → claude-haiku-3-5  (strong safety, very cheap)
        // logic               → o4-mini           (reasoning specialist, fraction of o3 cost)
        // edge_case           → gemini-2.0-flash  (long context, fast, cheap)
        // integration         → kimi-k2.5         (best codebase mapping at its price point)
        // performance         → deepseek-v3       (coding analysis, ~20% of DeepSeek-V4-Pro cost)
        const liteCatalog = [
            { id: 'openrouter:anthropic/claude-sonnet-4-5', requestedModel: 'claude-sonnet-4-5', provider: 'openrouter', estInputPer1M: 3.0, estOutputPer1M: 15.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
            { id: 'openrouter:anthropic/claude-haiku-3-5', requestedModel: 'claude-haiku-3-5', provider: 'openrouter', estInputPer1M: 0.8, estOutputPer1M: 4.0, capability: { reasoning: 8, coding: 8, security: 9, speed: 10, synthesis: 8 } },
            { id: 'openrouter:openai/o4-mini', requestedModel: 'o4-mini', provider: 'openrouter', estInputPer1M: 1.1, estOutputPer1M: 4.4, capability: { reasoning: 9, coding: 8, security: 8, speed: 8, synthesis: 8 } },
            { id: 'openrouter:google/gemini-2.0-flash', requestedModel: 'gemini-2.0-flash', provider: 'openrouter', estInputPer1M: 0.1, estOutputPer1M: 0.4, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
            { id: 'openrouter:moonshotai/kimi-k2.5', requestedModel: 'kimi-k2.5', provider: 'openrouter', estInputPer1M: 0.44, estOutputPer1M: 2.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
            { id: 'openrouter:deepseek/deepseek-chat-v3', requestedModel: 'deepseek-v3', provider: 'openrouter', estInputPer1M: 0.27, estOutputPer1M: 1.1, capability: { reasoning: 8, coding: 9, security: 7, speed: 10, synthesis: 8 } },
            { id: 'openrouter:deepseek/deepseek-r1-distill-llama-70b', requestedModel: 'cloud-reason-lite', provider: 'openrouter', estInputPer1M: 0.23, estOutputPer1M: 0.69, capability: { reasoning: 8, coding: 7, security: 7, speed: 8, synthesis: 7 } },
            { id: 'openrouter:deepseek/deepseek-chat', requestedModel: 'cloud-pro', provider: 'openrouter', estInputPer1M: 0.32, estOutputPer1M: 0.89, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
        ];
        const catalog = tier === 'lite' ? liteCatalog : heavyCatalog;
        if (!modelsArg || !modelsArg.trim())
            return catalog;
        const allow = new Set(modelsArg.split(',').map((m) => m.trim()).filter(Boolean));
        const filtered = catalog.filter((m) => (allow.has(m.id)
            || allow.has(m.id.replace('openrouter:', ''))
            || allow.has(m.requestedModel)));
        return filtered.length > 0 ? filtered : catalog;
    }
    buildRoleQuote(scan, models, tier = 'heavy') {
        const roleWeights = {
            detective: { reasoning: 10, coding: 8, security: 6, speed: 3, synthesis: 7 },
            logic: { reasoning: 8, coding: 10, security: 3, speed: 6, synthesis: 6 },
            security: { reasoning: 7, coding: 7, security: 10, speed: 5, synthesis: 6 },
            performance: { reasoning: 9, coding: 9, security: 7, speed: 5, synthesis: 7 },
            edge_case: { reasoning: 8, coding: 7, security: 7, speed: 7, synthesis: 5 },
            integration: { reasoning: 8, coding: 8, security: 6, speed: 5, synthesis: 8 },
            reviewer: { reasoning: 9, coding: 7, security: 7, speed: 6, synthesis: 8 },
            architect: { reasoning: 10, coding: 9, security: 8, speed: 4, synthesis: 10 },
        };
        // Preferred model per role for each tier.
        const preferredByRoleHeavy = {
            detective: 'gpt-5.5',
            logic: 'o3',
            security: 'opus-4.7',
            performance: 'deepseek-v4-pro',
            edge_case: 'gemini-2.5-pro',
            integration: 'kimi-k2.5',
            reviewer: 'opus-4.7',
            architect: 'gpt-5.5',
        };
        const preferredByRoleLite = {
            detective: 'claude-sonnet-4-5',
            logic: 'o4-mini',
            security: 'claude-haiku-3-5',
            performance: 'deepseek-v3',
            edge_case: 'gemini-2.0-flash',
            integration: 'kimi-k2.5',
            reviewer: 'claude-haiku-3-5',
            architect: 'claude-sonnet-4-5',
        };
        const preferredByRole = tier === 'lite' ? preferredByRoleLite : preferredByRoleHeavy;
        // Real-world token volumes from observed Cortex runs (not theoretical minimums).
        // Base: 8k input tokens per role (context + project scan + prompt).
        const complexity = Math.max(1, Math.ceil((scan.lines / 4000) + (scan.importEdges / 200)));
        const baseInputTokens = 8000 * complexity; // observed avg: 8k-25k per role
        const baseOutputTokens = 2200 * complexity; // observed avg: 1.8k-3.6k per role
        return Object.keys(roleWeights).map((role) => {
            const w = roleWeights[role];
            let best = models.find((m) => m.requestedModel === preferredByRole[role]) || models[0];
            if (!best)
                best = models[0];
            // Fallback scoring when preferred model is filtered out by --models.
            if (!models.some((m) => m.requestedModel === preferredByRole[role])) {
                let bestScore = -1;
                for (const m of models) {
                    const score = m.capability.reasoning * w.reasoning + m.capability.coding * w.coding + m.capability.security * w.security + m.capability.speed * w.speed + m.capability.synthesis * w.synthesis;
                    const costPenalty = (m.estInputPer1M + m.estOutputPer1M) * 0.1;
                    const finalScore = score - costPenalty;
                    if (finalScore > bestScore) {
                        best = m;
                        bestScore = finalScore;
                    }
                }
            }
            const estInputTokens = baseInputTokens;
            const estOutputTokens = baseOutputTokens;
            const estCostUsd = (estInputTokens / 1_000_000) * best.estInputPer1M + (estOutputTokens / 1_000_000) * best.estOutputPer1M;
            return { role, model: best.id, requestedModel: best.requestedModel, estInputTokens, estOutputTokens, estCostUsd };
        });
    }
    buildCortexExplicitSteps(execution, speedMode = false) {
        const { originalRequest, workspace, scan, quote } = execution;
        const quoteByRole = new Map(quote.map((row) => [row.role, row]));
        const topFiles = scan.topFiles.slice(0, 5).map((file) => `${file.file} (${file.lines} lines, ${file.imports} imports)`);
        const roleSequence = [
            { role: 'detective', dependsOn: [] },
            { role: 'logic', dependsOn: ['detective'] },
            { role: 'security', dependsOn: ['detective'] },
            { role: 'performance', dependsOn: ['detective'] },
            { role: 'edge_case', dependsOn: ['detective'] },
            { role: 'integration', dependsOn: ['detective'] },
            { role: 'reviewer', dependsOn: ['detective'] },
            { role: 'architect', dependsOn: ['logic', 'security', 'performance', 'edge_case', 'integration', 'reviewer'] },
        ];
        const roleIterationBudget = {
            detective: 4,
            logic: 5,
            security: speedMode ? 3 : 5,
            performance: speedMode ? 3 : 5,
            edge_case: speedMode ? 3 : 4,
            integration: speedMode ? 3 : 6,
            reviewer: speedMode ? 3 : 5,
            architect: 5,
        };
        const optionalRoles = new Set(['security', 'performance', 'edge_case', 'integration', 'reviewer']);
        const steps = roleSequence.map(({ role, dependsOn }) => {
            const row = quoteByRole.get(role);
            const isOptionalRole = optionalRoles.has(role);
            const requestedModel = role === 'detective'
                ? 'gpt-5.5'
                : role === 'architect'
                    ? (scan.lines <= 3000 ? 'opus-4.7' : 'gpt-5.5')
                    : (row?.requestedModel || 'cloud-pro');
            const model = role === 'detective'
                ? 'openrouter:openai/gpt-5.5'
                : role === 'architect'
                    ? (requestedModel === 'opus-4.7' ? 'openrouter:anthropic/claude-opus-4.7' : 'openrouter:openai/gpt-5.5')
                    : (row?.model || 'openrouter:deepseek/deepseek-chat');
            // Compact instruction packet: role tag + request digest + role micro-prompt.
            // Full workspace, scan stats, and key files are already in the payload.
            const ROLE_MICRO_PROMPTS = {
                detective: 'Root-cause analysis. Identify failing paths, unknown risks, and systemic gaps. Output: numbered findings with confidence level.',
                logic: 'Verify iteration bounds, fallback chains, state transitions, race conditions. Output: PASS/FAIL per subsystem with evidence.',
                security: 'Audit for auth bypass, injection flaws, unsafe exec, secret exposure, SSRF. Verdict: SAFE/RISK/CRITICAL per finding.',
                performance: 'Find N+1 queries, unbounded loops, sync I/O in async paths, memory leaks. Output: hotspot list with severity.',
                edge_case: 'Probe boundary conditions, null/undefined paths, concurrent mutations, retry storms. Output: concrete failure scenarios.',
                integration: 'Verify endpoint registration, auth middleware wiring, env var presence, cross-service contracts. PASS/FAIL per check.',
                reviewer: 'Final gate review. Confirm contract coverage, flag regressions. Verdict: CONDITIONAL-GO or NO-GO with rationale.',
                architect: 'Synthesize all role findings into a production-readiness verdict with a prioritized action list.',
            };
            const reqDigest = originalRequest.length > 220
                ? originalRequest.slice(0, 220) + '\u2026'
                : originalRequest;
            const roleObjective = [
                `[CORTEX:${role.toUpperCase()}] model=${requestedModel} budget=${roleIterationBudget[role] || 3}iter`,
                reqDigest,
                ROLE_MICRO_PROMPTS[role] || 'Analyse the codebase and deliver your findings concisely.',
            ].join('\\n');
            return {
                step_id: role,
                worker_name: 'v3_agent_worker',
                objective: roleObjective,
                depends_on: dependsOn,
                priority: role === 'architect' ? 2 : 3,
                retry_policy: {
                    max_attempts: role === 'performance' || role === 'edge_case' ? 2 : 1,
                    strategy: 'repair',
                    requires_validation_failure: false,
                },
                payload: {
                    role,
                    requested_model: requestedModel,
                    quoted_model: model,
                    enforce_cloud_only: role === 'detective' || role === 'architect',
                    speed_mode: speedMode,
                    optional_role: isOptionalRole,
                    allow_convergence_skip: speedMode && isOptionalRole,
                    convergence_sources: ['detective', 'logic'],
                    workspace,
                    top_files: topFiles,
                    max_iterations: roleIterationBudget[role] || 4,
                    compact_context: true,
                    context_sniper_max_depth: 2,
                    max_dependency_chars: role === 'architect' ? 8000 : 4000,
                    max_artifacts: role === 'architect' ? 12 : 8,
                    max_context_chars: role === 'architect' ? 90000 : 60000,
                    max_output_tokens: role === 'detective' ? 3600 : role === 'architect' ? 3400 : role === 'reviewer' ? 2800 : 2200,
                    request_timeout_seconds: role === 'detective' || role === 'architect' ? 1200 : 720,
                },
            };
        });
        steps.push({
            step_id: 'testing',
            worker_name: 'testing_worker',
            objective: [
                '[CORTEX:TESTING]',
                originalRequest,
                `Workspace: ${workspace}`,
                'Validate architect output with narrow relevant checks and report concrete failures only.',
            ].join('\\n'),
            depends_on: ['architect'],
            priority: 2,
            retry_policy: {
                max_attempts: 1,
                strategy: 'repair',
                requires_validation_failure: false,
            },
            payload: {
                workspace,
                validation_min_score: 80,
            },
        });
        return steps;
    }
    buildBillingQuote(quote, tier = 'heavy') {
        const baseUsd = quote.reduce((sum, r) => sum + r.estCostUsd, 0);
        const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
        const marginPct = Number.isFinite(marginPctRaw) ? Math.max(0, marginPctRaw) : 10;
        const platformFeePct = CORTEX_PLATFORM_FEE_PCT;
        const platformFeeMultiplier = 1 + (platformFeePct / 100);
        const finalUsd = baseUsd * (1 + (marginPct / 100)) * platformFeeMultiplier;
        // Retry-adjusted estimate: all 7 critical roles can trigger the quality-gate
        // retry in state_manager.py (adds +2 iterations per degraded role).
        // Probability of retry per critical role ≈ 45% (from production telemetry).
        // RETRY_ITER_RATIO = additional spend when retry fires: +2 iters / avg 5 iters base ≈ 40%.
        const CRITICAL_ROLES = new Set(['detective', 'logic', 'security', 'performance', 'integration', 'reviewer', 'architect']);
        const RETRY_PROB = 0.45;
        const RETRY_ITER_RATIO = 0.40;
        const retryExtra = quote
            .filter((r) => CRITICAL_ROLES.has(r.role))
            .reduce((sum, r) => sum + r.estCostUsd * RETRY_PROB * RETRY_ITER_RATIO, 0);
        const retryAdjustedUsd = (baseUsd + retryExtra) * (1 + (marginPct / 100)) * platformFeeMultiplier;
        // Range bounds.
        const rangeMinUsd = finalUsd; // single pass, no retries
        const maxRetryExtra = quote
            .filter((r) => CRITICAL_ROLES.has(r.role))
            .reduce((sum, r) => sum + r.estCostUsd * RETRY_ITER_RATIO, 0);
        const rangeMaxUsd = (baseUsd + maxRetryExtra) * (1 + (marginPct / 100)) * platformFeeMultiplier;
        const vigcoinRateRaw = Number.parseFloat(String(process.env.VIGTHORIA_VIGCOIN_USD_RATE || '1'));
        const vigcoinRateUsd = Number.isFinite(vigcoinRateRaw) && vigcoinRateRaw > 0 ? vigcoinRateRaw : 1;
        const vigcoinRequired = retryAdjustedUsd / vigcoinRateUsd;
        return {
            baseUsd,
            marginPct,
            finalUsd,
            retryAdjustedUsd,
            rangeMinUsd,
            rangeMaxUsd,
            tier,
            vigcoinRateUsd,
            vigcoinRequired,
            platformFeePct,
        };
    }
    async evaluateBillingGate(billingQuote) {
        const forceFlagsEnabled = this.isCortexTestMode();
        const forcedPlan = forceFlagsEnabled
            ? String(process.env.VIGTHORIA_CORTEX_FORCE_PLAN || process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase()
            : '';
        const entitlement = await this.fetchCortexEntitlement();
        const normalizedPlan = forcedPlan || entitlement.plan || this.config.getNormalizedPlan() || 'free';
        const masterAdminFree = this.isMasterAdminFree(normalizedPlan, entitlement.masterAccess, entitlement.isMasterAdmin);
        if (masterAdminFree) {
            return {
                plan: normalizedPlan,
                masterAdminFree,
                canProceed: true,
                wallet: {
                    available: true,
                    vigcoinBalance: null,
                    source: 'master_admin_access',
                    purchaseUrl: null,
                },
            };
        }
        const wallet = await this.fetchWalletState();
        if (!wallet.available || wallet.vigcoinBalance === null) {
            return {
                plan: normalizedPlan,
                masterAdminFree,
                canProceed: false,
                wallet,
            };
        }
        return {
            plan: normalizedPlan,
            masterAdminFree,
            canProceed: wallet.vigcoinBalance >= billingQuote.vigcoinRequired,
            wallet,
        };
    }
    parseBooleanCandidate(value) {
        if (typeof value === 'boolean') {
            return value;
        }
        if (typeof value === 'number') {
            if (value === 1)
                return true;
            if (value === 0)
                return false;
            return null;
        }
        if (typeof value === 'string') {
            const v = value.trim().toLowerCase();
            if (['1', 'true', 'yes', 'enabled', 'on'].includes(v))
                return true;
            if (['0', 'false', 'no', 'disabled', 'off'].includes(v))
                return false;
        }
        return null;
    }
    async fetchCortexEntitlement() {
        if (this.isCortexTestMode() && (process.env.VIGTHORIA_CORTEX_FORCE_MASTER_ACCESS === '1' || process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1')) {
            return { plan: this.config.getNormalizedPlan() || 'free', masterAccess: true, isMasterAdmin: true };
        }
        const baseUrl = this.getBillingBaseUrl();
        const headers = this.getHeaders();
        const endpoints = ['/api/user/subscription', '/api/user/info', '/api/user/profile'];
        for (const endpoint of endpoints) {
            try {
                const response = await fetch(`${baseUrl}${endpoint}`, {
                    method: 'GET',
                    signal: AbortSignal.timeout(12000),
                    headers,
                });
                if (!response.ok)
                    continue;
                const payload = await response.json();
                const data = (payload && typeof payload === 'object') ? payload : {};
                const plan = String(data.subscription?.plan
                    || data.subscription_plan
                    || data.plan
                    || data.user?.subscription_plan
                    || data.user?.subscription?.plan
                    || '').trim().toLowerCase() || null;
                const accessCandidates = [
                    data.godmode_master_access,
                    data.godmodeMasterAccess,
                    data.subscription?.godmode_master_access,
                    data.subscription?.godmodeMasterAccess,
                    data.user?.godmode_master_access,
                    data.user?.godmodeMasterAccess,
                ];
                const masterRoleCandidates = [
                    data.user?.isMasterAdmin,
                    data.user?.is_master_admin,
                    data.subscription?.isMasterAdmin,
                    data.subscription?.is_master_admin,
                    data.isMasterAdmin,
                    data.is_master_admin,
                    data.user?.admin_role === 'master_admin',
                    data.admin_role === 'master_admin',
                ];
                const roleHit = masterRoleCandidates
                    .map((c) => this.parseBooleanCandidate(c))
                    .find((v) => v !== null) ?? false;
                for (const c of accessCandidates) {
                    const parsed = this.parseBooleanCandidate(c);
                    if (parsed !== null) {
                        return { plan, masterAccess: parsed, isMasterAdmin: Boolean(roleHit) };
                    }
                }
                if (plan) {
                    return { plan, masterAccess: false, isMasterAdmin: Boolean(roleHit) };
                }
            }
            catch (err) {
                this.logger.warn(this.formatLegionError(`Cortex entitlement request ${endpoint}`, err));
                continue;
            }
        }
        return {
            plan: this.config.getNormalizedPlan() || null,
            masterAccess: false,
            isMasterAdmin: false,
        };
    }
    isMasterAdminFree(plan, masterAccess, isMasterAdmin) {
        if (!masterAccess) {
            return false;
        }
        if (isMasterAdmin) {
            return true;
        }
        return plan === 'master_admin';
    }
    isCortexTestMode() {
        return process.env.VIGTHORIA_CORTEX_TEST_MODE === '1'
            || process.env.VIGTHORIA_GODMODE_TEST_MODE === '1'
            || process.env.NODE_ENV === 'test';
    }
    getBillingBaseUrl() {
        const configured = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
        if (configured) {
            return configured;
        }
        return 'https://coder.vigthoria.io';
    }
    parseNumericCandidate(value) {
        if (typeof value === 'number' && Number.isFinite(value)) {
            return value;
        }
        if (typeof value === 'string') {
            const parsed = Number.parseFloat(value.trim());
            if (Number.isFinite(parsed)) {
                return parsed;
            }
        }
        return null;
    }
    extractVigcoinBalance(data) {
        const candidates = [
            data.vigcoinBalance,
            data.vigcoin_balance,
            data.balance,
            data.credits,
            data.credit_balance,
            data.wallet?.vigcoinBalance,
            data.wallet?.vigcoin_balance,
            data.wallet?.balance,
            data.wallet?.credits,
            data.subscription?.vigcoinBalance,
            data.subscription?.vigcoin_balance,
            data.subscription?.credits,
            data.user?.vigcoinBalance,
            data.user?.vigcoin_balance,
            data.user?.credits,
            data.data?.vigcoinBalance,
            data.data?.vigcoin_balance,
            data.data?.balance,
            data.data?.credits,
        ];
        for (const candidate of candidates) {
            const parsed = this.parseNumericCandidate(candidate);
            if (parsed !== null) {
                return parsed;
            }
        }
        return null;
    }
    getPurchaseUrlFromPayload(baseUrl, data) {
        const raw = data.purchaseUrl || data.purchase_url || data.checkoutUrl || data.checkout_url || data.url || data.data?.checkoutUrl || data.data?.url;
        if (typeof raw === 'string' && raw.trim()) {
            if (/^https?:\/\//i.test(raw.trim())) {
                return raw.trim();
            }
            return `${baseUrl}${raw.startsWith('/') ? '' : '/'}${raw.trim()}`;
        }
        return `${baseUrl}/billing`;
    }
    async fetchWalletState() {
        const baseUrl = this.getBillingBaseUrl();
        const headers = this.getHeaders();
        const forcedLow = this.isCortexTestMode() && (process.env.VIGTHORIA_CORTEX_FORCE_LOW_CREDIT === '1' || process.env.VIGTHORIA_GODMODE_FORCE_LOW_CREDIT === '1');
        const forcedBalanceRaw = this.isCortexTestMode() ? (process.env.VIGTHORIA_CORTEX_FORCE_BALANCE || process.env.VIGTHORIA_GODMODE_FORCE_BALANCE) : '';
        if (forcedLow) {
            return {
                available: true,
                vigcoinBalance: 0,
                source: 'forced_low_credit',
                purchaseUrl: `${baseUrl}/music/store#vigcoins`,
            };
        }
        if (forcedBalanceRaw) {
            const forced = this.parseNumericCandidate(forcedBalanceRaw);
            if (forced !== null) {
                return {
                    available: true,
                    vigcoinBalance: forced,
                    source: 'forced_balance',
                    purchaseUrl: `${baseUrl}/music/store#vigcoins`,
                };
            }
        }
        const endpoints = [
            '/api/viagen6/vigcoin/balance',
            '/api/user/vigcoin/balance',
            '/api/user/subscription',
            '/api/user/info',
            '/api/user/profile',
            '/api/wallet/balance',
            '/api/billing/wallet',
            '/api/billing/credits',
        ];
        let sawUnauthorized = false;
        for (const endpoint of endpoints) {
            try {
                const response = await fetch(`${baseUrl}${endpoint}`, {
                    method: 'GET',
                    signal: AbortSignal.timeout(12000),
                    headers,
                });
                if (!response.ok) {
                    if (response.status === 401 || response.status === 403) {
                        sawUnauthorized = true;
                    }
                    continue;
                }
                const payload = await this.readJsonResponse(response, `wallet balance request ${endpoint}`);
                const payloadObj = (payload && typeof payload === 'object') ? payload : {};
                const balance = this.extractVigcoinBalance(payloadObj);
                if (balance === null) {
                    continue;
                }
                return {
                    available: true,
                    vigcoinBalance: balance,
                    source: endpoint,
                    purchaseUrl: this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`,
                };
            }
            catch (err) {
                this.logger.warn(this.formatLegionError(`wallet balance request ${endpoint}`, err));
                continue;
            }
        }
        return {
            available: false,
            vigcoinBalance: null,
            source: null,
            purchaseUrl: `${baseUrl}/music/store#vigcoins`,
            error: sawUnauthorized
                ? 'Wallet session expired or unauthorized. Run: vigthoria login'
                : 'Wallet endpoint unavailable from current gateway session',
        };
    }
    async attemptDirectCharge(vigcoinNeeded) {
        const baseUrl = this.getBillingBaseUrl();
        const headers = this.getHeaders();
        const amount = Math.max(1, Math.ceil(vigcoinNeeded));
        const requestId = `cortex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
        const chargePayloads = [
            { endpoint: '/api/viagen6/vigcoin/charge', body: { amount, reason: 'cortex_legion', requestId } },
            { endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: 'cortex_legion', requestId } },
            { endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: 'cortex_legion', requestId } },
        ];
        for (const attempt of chargePayloads) {
            try {
                const response = await fetch(`${baseUrl}${attempt.endpoint}`, {
                    method: 'POST',
                    signal: AbortSignal.timeout(15000),
                    headers,
                    body: JSON.stringify(attempt.body),
                });
                const payload = await response.json().catch((err) => {
                    this.logger.warn(this.formatLegionError(`direct VigCoin charge response ${attempt.endpoint}`, err));
                    return {};
                });
                const payloadObj = (payload && typeof payload === 'object') ? payload : {};
                const checkoutUrl = this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`;
                if (!response.ok) {
                    continue;
                }
                const charged = payloadObj.charged === true || payloadObj.success === true || payloadObj.status === 'charged';
                if (charged) {
                    return { ok: true, checkoutUrl: checkoutUrl || undefined, note: `Charge accepted by ${attempt.endpoint}` };
                }
                return {
                    ok: false,
                    checkoutUrl: checkoutUrl || undefined,
                    note: payloadObj.error || payloadObj.message || `Checkout required via ${attempt.endpoint}`,
                };
            }
            catch (err) {
                this.logger.warn(this.formatLegionError(`direct VigCoin charge request ${attempt.endpoint}`, err));
                continue;
            }
        }
        return {
            ok: false,
            checkoutUrl: `${baseUrl}/music/store#vigcoins`,
            note: 'No direct charge endpoint accepted this request',
        };
    }
    async collectExecutionCharge(billingQuote, gate) {
        if (gate.masterAdminFree) {
            return true;
        }
        const spinner = createSpinner('Charging VigCoin wallet for Cortex execution...').start();
        const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired);
        spinner.stop();
        if (!result.ok) {
            console.log(chalk.red(result.note || 'Wallet charge failed.'));
            console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || `${this.getBillingBaseUrl()}/music/store#vigcoins`}`));
            return false;
        }
        console.log(chalk.green('Wallet charged for Cortex execution.'));
        return true;
    }
    async resolveBillingInsufficientFunds(billingQuote, gate, options) {
        this.printBillingGateSummary(billingQuote, gate);
        if (!gate.wallet.available) {
            console.log(chalk.red('Unable to verify wallet balance from server. Execution is blocked.'));
            if (gate.wallet.purchaseUrl) {
                console.log(chalk.gray(`Open billing portal: ${gate.wallet.purchaseUrl}`));
            }
            return false;
        }
        if (options.autoCharge) {
            const spinner = createSpinner('Attempting direct wallet charge...').start();
            const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
            spinner.stop();
            if (result.ok) {
                console.log(chalk.green('Direct charge succeeded. Re-checking wallet balance...'));
                return true;
            }
            console.log(chalk.yellow(result.note || 'Direct charge did not complete.'));
            console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
            return false;
        }
        if (!process.stdin.isTTY || !process.stdout.isTTY) {
            console.log(chalk.yellow('Low balance detected in non-interactive mode. Re-run with --auto-charge or top up first.'));
            if (gate.wallet.purchaseUrl) {
                console.log(chalk.gray(`Billing portal: ${gate.wallet.purchaseUrl}`));
            }
            return false;
        }
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        try {
            const answer = (await rl.question('VigCoin low. Choose action: [c]harge now, [p]urchase first, [n] cancel: ')).trim().toLowerCase();
            if (answer === 'c' || answer === 'charge') {
                const spinner = createSpinner('Attempting direct wallet charge...').start();
                const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
                spinner.stop();
                if (result.ok) {
                    console.log(chalk.green('Direct charge succeeded. Re-checking wallet balance...'));
                    return true;
                }
                console.log(chalk.yellow(result.note || 'Direct charge did not complete.'));
                console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
                return false;
            }
            if (answer === 'p' || answer === 'purchase') {
                console.log(chalk.yellow(`Purchase VigCoin first: ${gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
                return false;
            }
            return false;
        }
        finally {
            rl.close();
        }
    }
    printBillingGateSummary(billingQuote, gate) {
        console.log(chalk.white('  Billing gate:'));
        console.log(chalk.gray(`    Plan: ${gate.plan}`));
        if (gate.masterAdminFree) {
            console.log(chalk.green('    Free tier override applied (Master Admin).'));
            return;
        }
        console.log(chalk.gray(`    Estimated total (USD): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk.gray(' (retry-adjusted expected)'));
        console.log(chalk.gray(`    Platform fee: +${billingQuote.platformFeePct.toFixed(0)}% (already included in all estimates)`));
        console.log(chalk.gray(`    VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
        console.log(chalk.gray(`    VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
        if (gate.wallet.vigcoinBalance !== null) {
            const color = gate.wallet.vigcoinBalance >= billingQuote.vigcoinRequired ? chalk.green : chalk.red;
            console.log(chalk.gray('    Wallet balance: ') + color(gate.wallet.vigcoinBalance.toFixed(3)) + (gate.wallet.source ? chalk.gray(` (source: ${gate.wallet.source})`) : ''));
        }
        else {
            console.log(chalk.red(`    Wallet balance: unavailable${gate.wallet.error ? ` (${gate.wallet.error})` : ''}`));
        }
        if (gate.wallet.purchaseUrl) {
            console.log(chalk.gray(`    Purchase URL: ${gate.wallet.purchaseUrl}`));
        }
    }
    printCortexQuote(workspace, scan, quote, billingQuote, gate) {
        const tierLabel = billingQuote.tier === 'lite'
            ? chalk.cyan('LITE') + chalk.gray(' — efficient (claude-sonnet/haiku, o4-mini, gemini-flash, deepseek-v3)')
            : chalk.magenta('HEAVY') + chalk.gray(' — strongest LLMs (gpt-5.5, opus-4.7, o3, gemini-2.5-pro)');
        console.log();
        console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Vigthoria Cortex Estimator ${CH.hLine.repeat(31)}`));
        console.log();
        console.log(chalk.gray('  Workspace: ') + chalk.white(workspace));
        console.log(chalk.gray('  Tier:      ') + tierLabel);
        console.log(chalk.gray('  Files scanned: ') + chalk.white(String(scan.files)));
        console.log(chalk.gray('  Lines scanned: ') + chalk.white(String(scan.lines)));
        console.log(chalk.gray('  Dependency edges: ') + chalk.white(String(scan.importEdges)));
        if (scan.topFiles.length > 0) {
            console.log();
            console.log(chalk.white('  Top context files:'));
            for (const f of scan.topFiles) {
                console.log(chalk.gray(`    ${CH.bullet} ${f.file} (${f.lines} lines, ${f.imports} imports)`));
            }
        }
        console.log();
        console.log(chalk.white('  Role assignment and estimated cost:'));
        for (const row of quote) {
            const publicModelLabel = row.requestedModel || row.model.replace(/^openrouter:/i, '').split('/').pop() || 'managed-model';
            const roleEstWithFee = row.estCostUsd * (1 + (billingQuote.platformFeePct / 100));
            console.log(chalk.gray(`    ${CH.bullet} ${row.role.padEnd(11)} ${publicModelLabel}  $${roleEstWithFee.toFixed(4)}`));
        }
        console.log();
        console.log(chalk.yellow(`  Cost range (single-pass best case):  $${billingQuote.rangeMinUsd.toFixed(4)}`));
        console.log(chalk.yellow(`  Cost range (expected with retries):   $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk.gray('  ← use this for budget planning'));
        console.log(chalk.yellow(`  Cost range (worst case, all retries): $${billingQuote.rangeMaxUsd.toFixed(4)}`));
        console.log(chalk.gray('  Retry model: 45% chance per critical role triggers quality-gate (+40% iterations per retry).'));
        console.log(chalk.gray('  A mid-run checkpoint will appear when 70% of the expected estimate is consumed.'));
        console.log(chalk.gray('  Flow: Estimate -> Isolation -> Parallel Attack -> Synthesis'));
        console.log(chalk.gray(`  All displayed costs include platform fee (+${billingQuote.platformFeePct.toFixed(0)}%).`));
        console.log();
        this.printBillingGateSummary(billingQuote, gate);
        console.log();
    }
    async confirmExecution() {
        if (!process.stdin.isTTY || !process.stdout.isTTY) {
            console.log(chalk.yellow('Non-interactive terminal detected. Re-run with --approve to continue execution.'));
            return false;
        }
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        try {
            const answer = (await rl.question('Proceed with Cortex execution? (y/N): ')).trim().toLowerCase();
            return answer === 'y' || answer === 'yes';
        }
        finally {
            rl.close();
        }
    }
    /**
     * SSE streaming URL for the Legion execution endpoint.
     * Always hits Hyper Loop directly (port 8020) with the service key to avoid
     * gateway JWT expiry killing long-running Cortex jobs.
     */
    getLegionStreamUrl() {
        const envOverride = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
        if (envOverride)
            return `${envOverride}/legion/stream`;
        return 'http://localhost:8020/api/hyperloop/legion/stream';
    }
    getLegionServiceKey() {
        // Service key lets on-server CLI calls bypass gateway JWT validation
        return String(process.env.HYPERLOOP_SERVICE_KEY ||
            process.env.V3_SERVICE_KEY ||
            '');
    }
    async planAndExecute(request, options, cortexExecution) {
        const explicitSteps = cortexExecution ? this.buildCortexExplicitSteps(cortexExecution, options.speed === true) : undefined;
        const workspace = cortexExecution?.workspace || options.project || process.cwd();
        const body = {
            request,
            context: {
                workspace,
                governor_budget_secs: 600,
                governor_optional_roles: ['security', 'performance', 'edge_case', 'integration', 'reviewer'],
            },
            constraints: {
                active_only: true,
                execution_timeout_seconds: options.timeoutSec,
                preferred_workers: explicitSteps ? undefined : ['v3_agent_worker'],
                explicit_steps: explicitSteps,
            },
        };
        const streamUrl = this.getLegionStreamUrl();
        const serviceKey = this.getLegionServiceKey();
        const headers = { 'Content-Type': 'application/json' };
        if (serviceKey) {
            headers['X-Service-Key'] = serviceKey;
        }
        else {
            const token = this.config.get('authToken');
            if (token) {
                headers['Authorization'] = `Bearer ${token}`;
                headers['Cookie'] = `vigthoria-auth-token=${token}`;
            }
        }
        const spinner = createSpinner('Connecting to Legion SSE stream...').start();
        const startTime = Date.now();
        let response;
        try {
            response = await fetch(streamUrl, {
                method: 'POST',
                headers,
                body: JSON.stringify(body),
            });
        }
        catch (connErr) {
            spinner.stop();
            this.logger.error(`Cannot connect to Hyper Loop at ${streamUrl}: ${connErr?.message || connErr}`);
            return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
        }
        if (!response.ok) {
            spinner.stop();
            const errBody = await response.text().catch(() => '');
            this.logger.error(`Legion stream ${response.status}: ${describeUpstreamStatus(response.status)} — ${errBody.slice(0, 200)}`);
            return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
        }
        if (!response.body) {
            spinner.stop();
            this.logger.error('Legion stream returned no response body');
            return { status: 'failed', plannedSteps: 0, completedSteps: 0 };
        }
        spinner.stop();
        console.log();
        console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Legion Execution Report ${CH.hLine.repeat(34)}`));
        console.log();
        const decoder = new TextDecoder();
        let buffer = '';
        let finalResult = null;
        let stepsTotal = 0;
        let stepsDone = 0;
        let finalStatus = 'failed';
        let failedStepId = '';
        let failedWorker = '';
        const streamEvents = [];
        // Mid-run budget checkpoint: accumulate estimated spend per completed role.
        const roleQuoteIndex = new Map((cortexExecution?.quote || []).map((q) => [q.role, q.estCostUsd]));
        const marginPctMidrun = Number.isFinite(Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || '10')))
            ? Math.max(0, Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || '10')))
            : 10;
        const baseEstimateUsd = (cortexExecution?.quote || []).reduce((s, q) => s + q.estCostUsd, 0);
        const budgetCheckpointThreshold = cortexExecution
            ? baseEstimateUsd * (1 + marginPctMidrun / 100) * (1 + CORTEX_PLATFORM_FEE_PCT / 100) * 0.70
            : Infinity;
        let accumulatedEstUsd = 0;
        let budgetCheckpointFired = false;
        const completedRoleSummaries = [];
        try {
            const reader = response.body.getReader();
            while (true) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split('\n');
                buffer = lines.pop() ?? '';
                for (const line of lines) {
                    const trimmed = line.trim();
                    if (!trimmed || trimmed.startsWith(':'))
                        continue;
                    if (!trimmed.startsWith('data:'))
                        continue;
                    const jsonStr = trimmed.slice(5).trim();
                    let evt;
                    try {
                        evt = JSON.parse(jsonStr);
                    }
                    catch {
                        continue;
                    }
                    streamEvents.push(evt);
                    switch (evt.event) {
                        case 'plan':
                            stepsTotal = evt.steps_total || 0;
                            console.log(chalk.gray(`  Planned workers: ${stepsTotal} steps queued`));
                            console.log();
                            break;
                        case 'batch_start':
                            console.log(chalk.gray(`  ▶ Running: ${evt.workers.join(', ')} (${evt.steps_done}/${evt.steps_total})`));
                            break;
                        case 'step_complete': {
                            stepsDone = Number(evt.steps_done) || 0;
                            const icon = evt.status === 'completed' ? chalk.green(CH.success) : chalk.red(CH.error);
                            const stepSummaryRaw = String(evt.summary || '');
                            const summarySnip = stepSummaryRaw ? chalk.gray(` — ${stepSummaryRaw.slice(0, 120)}`) : '';
                            console.log(`    ${icon} ${chalk.white(String(evt.step_id))} ${chalk.gray('[' + evt.worker + ']')}${summarySnip}`);
                            if (evt.status !== 'completed' && !failedStepId) {
                                failedStepId = String(evt.step_id || '');
                                failedWorker = String(evt.worker || '');
                            }
                            // Track spend and trigger mid-run budget checkpoint.
                            const stepRole = String(evt.step_id || '');
                            accumulatedEstUsd += (roleQuoteIndex.get(stepRole) || 0) * (1 + CORTEX_PLATFORM_FEE_PCT / 100);
                            completedRoleSummaries.push({ role: stepRole, status: String(evt.status || ''), summary: stepSummaryRaw.slice(0, 200) });
                            if (!budgetCheckpointFired
                                && cortexExecution
                                && accumulatedEstUsd >= budgetCheckpointThreshold
                                && budgetCheckpointThreshold < Infinity
                                && process.stdin.isTTY
                                && process.stdout.isTTY) {
                                budgetCheckpointFired = true;
                                const remainingRoles = (cortexExecution.quote || [])
                                    .filter((q) => !completedRoleSummaries.some((c) => c.role === q.role))
                                    .map((q) => q.role);
                                const remainingEstUsd = (cortexExecution.quote || [])
                                    .filter((q) => remainingRoles.includes(q.role))
                                    .reduce((s, q) => s + (q.estCostUsd * (1 + CORTEX_PLATFORM_FEE_PCT / 100)), 0);
                                console.log();
                                console.log(chalk.bold.yellow('  ━━━ Mid-Run Budget Checkpoint ━━━'));
                                console.log(chalk.gray(`  Consumed so far (estimated):  $${accumulatedEstUsd.toFixed(4)}`));
                                console.log(chalk.gray(`  Estimated remaining:          $${remainingEstUsd.toFixed(4)}`));
                                console.log();
                                console.log(chalk.white('  Roles completed:'));
                                for (const c of completedRoleSummaries) {
                                    const roleIcon = c.status === 'completed' ? chalk.green('✔') : chalk.red('✘');
                                    const roleSumSnip = c.summary ? ` — ${c.summary.slice(0, 100)}` : '';
                                    console.log(chalk.gray(`    ${roleIcon} ${c.role.padEnd(11)}${roleSumSnip}`));
                                }
                                console.log();
                                console.log(chalk.white('  Remaining roles: ') + chalk.gray(remainingRoles.join(', ') || 'none'));
                                console.log();
                                const checkpointRl = readline.createInterface({ input: process.stdin, output: process.stdout });
                                let continueRun = false;
                                try {
                                    const ans = (await checkpointRl.question('  Budget checkpoint — continue execution? (y/N): ')).trim().toLowerCase();
                                    continueRun = ans === 'y' || ans === 'yes';
                                }
                                finally {
                                    checkpointRl.close();
                                }
                                if (!continueRun) {
                                    console.log(chalk.yellow('  Cortex stopped by user at budget checkpoint.'));
                                    console.log(chalk.gray(`  State: ${stepsDone} of ${stepsTotal} steps completed.`));
                                    finalStatus = 'failed';
                                    failedStepId = failedStepId || 'user_checkpoint_stop';
                                    // Break out of the for-of lines loop; the outer while(true) will end when reader.read() drains.
                                    break;
                                }
                                console.log(chalk.green('  Continuing execution…'));
                                console.log();
                            }
                            break;
                        }
                        case 'complete':
                            finalResult = evt.result || finalResult;
                            if (Number.isFinite(Number(evt.steps_done))) {
                                stepsDone = Number(evt.steps_done) || stepsDone;
                            }
                            if (Number.isFinite(Number(evt.steps_total))) {
                                stepsTotal = Number(evt.steps_total) || stepsTotal;
                            }
                            if (!failedStepId && evt.failed_step_id) {
                                failedStepId = String(evt.failed_step_id || '');
                            }
                            if (!failedWorker && evt.failed_worker) {
                                failedWorker = String(evt.failed_worker || '');
                            }
                            if (evt.status === 'completed') {
                                finalStatus = 'completed';
                                console.log();
                                console.log(chalk.green(`  ${CH.success} Legion completed successfully`));
                            }
                            else if (evt.status === 'failed') {
                                finalStatus = 'failed';
                                console.log();
                                console.log(chalk.red(`  ${CH.error} Legion execution failed`));
                                if (evt.error)
                                    console.log(chalk.red(`  Error: ${String(evt.error).slice(0, 300)}`));
                            }
                            break;
                        case 'error':
                            finalStatus = 'failed';
                            console.log(chalk.red(`  ${CH.error} Stream error: ${String(evt.error).slice(0, 300)}`));
                            break;
                    }
                }
            }
        }
        catch (streamErr) {
            this.logger.error(`Legion stream read error: ${streamErr?.message || streamErr}`);
            finalStatus = 'failed';
        }
        const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(1);
        if (stepsTotal > 0 && stepsDone < stepsTotal) {
            console.log(chalk.yellow(`  ${CH.warn} Legion stream ended after ${stepsDone}/${stepsTotal} steps`));
        }
        console.log();
        console.log(chalk.gray(`  Time: ${elapsedSec}s`));
        const lastStepResult = finalResult?.final_output;
        const summary = String(lastStepResult?.result?.summary || lastStepResult?.summary || '');
        if (summary) {
            console.log();
            console.log(chalk.white('  Final output:'));
            console.log(chalk.gray(`  ${summary.slice(0, 600)}`));
        }
        if (cortexExecution) {
            const report = this.buildCortexRunReport({
                generatedAt: new Date().toISOString(),
                status: finalStatus,
                elapsedSeconds: Number(elapsedSec),
                workspace: cortexExecution.workspace,
                request: cortexExecution.originalRequest,
                plannedSteps: stepsTotal,
                completedSteps: stepsDone,
                modifiedFiles: this.extractModifiedFiles(finalResult),
                workers: [],
                finalSummary: summary,
            }, streamEvents);
            const files = this.writeCortexSummaryReport(report);
            console.log(chalk.gray('  Cortex summary: ' + files.markdownPath));
            console.log(chalk.gray('  Cortex report JSON: ' + files.jsonPath));
        }
        console.log();
        return {
            status: finalStatus,
            plannedSteps: stepsTotal,
            completedSteps: stepsDone,
            failedStepId: failedStepId || undefined,
            failedWorker: failedWorker || undefined,
            finalSummary: summary || undefined,
        };
    }
    buildCortexRunReport(base, streamEvents) {
        const workers = streamEvents
            .filter((evt) => evt && evt.event === 'step_complete')
            .map((evt) => ({
            stepId: String(evt.step_id || ''),
            worker: String(evt.worker || ''),
            status: String(evt.status || ''),
            summary: String(evt.summary || ''),
        }));
        return { ...base, workers };
    }
    extractModifiedFiles(finalResult) {
        if (!finalResult || typeof finalResult !== 'object')
            return [];
        const candidates = [];
        const finalOutput = (finalResult.final_output && typeof finalResult.final_output === 'object') ? finalResult.final_output : null;
        const result = (finalOutput?.result && typeof finalOutput.result === 'object') ? finalOutput.result : null;
        for (const key of ['modified_files', 'changed_files', 'files_changed', 'touched_files']) {
            const top = finalResult[key];
            const res = result ? result[key] : undefined;
            if (Array.isArray(top))
                candidates.push(...top.map((x) => String(x || '').trim()));
            if (Array.isArray(res))
                candidates.push(...res.map((x) => String(x || '').trim()));
        }
        return Array.from(new Set(candidates.filter((x) => x.length > 0)));
    }
    writeCortexSummaryReport(report) {
        const reportsDir = path.join(report.workspace, '.vigthoria', 'reports');
        fs.mkdirSync(reportsDir, { recursive: true });
        const stamp = report.generatedAt.replace(/[:.]/g, '-');
        const markdownPath = path.join(reportsDir, 'CORTEX_SUMMARY_' + stamp + '.md');
        const jsonPath = path.join(reportsDir, 'CORTEX_SUMMARY_' + stamp + '.json');
        const workerLines = report.workers.length > 0
            ? report.workers.map((w) => '- ' + w.stepId + ' [' + w.worker + '] ' + w.status + (w.summary ? ': ' + w.summary : '')).join('\n')
            : '- No worker step summaries captured';
        const modifiedLines = report.modifiedFiles.length > 0
            ? report.modifiedFiles.map((f) => '- ' + f).join('\n')
            : '- No modified files reported by backend outputs';
        const markdown = [
            '# Cortex Run Summary',
            '',
            'Generated: ' + report.generatedAt,
            'Status: ' + report.status,
            'Elapsed: ' + report.elapsedSeconds + 's',
            'Workspace: ' + report.workspace,
            '',
            '## Request',
            report.request,
            '',
            '## Execution',
            '- Planned steps: ' + report.plannedSteps,
            '- Completed steps: ' + report.completedSteps,
            '',
            '## Worker Results',
            workerLines,
            '',
            '## Modified Files',
            modifiedLines,
            '',
            '## Final Summary',
            report.finalSummary || 'No final summary reported by backend',
            '',
        ].join('\n');
        fs.writeFileSync(markdownPath, markdown, 'utf8');
        fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2) + '\n', 'utf8');
        return { markdownPath, jsonPath };
    }
    formatLegionError(context, err) {
        const message = err?.message || String(err || 'Unknown error');
        const cause = err?.cause?.message ? ` (${err.cause.message})` : '';
        return `${context} failed: ${message}${cause}`;
    }
    async showWorkers() {
        const spinner = createSpinner('Fetching Legion worker catalog...').start();
        let lastError = null;
        for (const baseUrl of this.getHyperloopUrls()) {
            try {
                const response = await fetch(`${baseUrl}/legion/workers`, {
                    signal: AbortSignal.timeout(10000),
                    headers: this.getHeaders(),
                });
                if (!response.ok) {
                    const errBody = await response.text().catch((err) => {
                        this.logger.warn(this.formatLegionError(`worker catalog error body at ${baseUrl}`, err));
                        return '';
                    });
                    lastError = `Legion worker catalog request at ${baseUrl} failed: ${response.status} ${describeUpstreamStatus(response.status)}${errBody ? ` — ${errBody.slice(0, 200)}` : ''}`;
                    continue;
                }
                const data = await this.readJsonResponse(response, `worker catalog request at ${baseUrl}`);
                spinner.stop();
                console.log();
                console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Legion Worker Catalog ${CH.hLine.repeat(37)}`));
                console.log();
                const workers = data.workers || data.catalog || data;
                if (Array.isArray(workers)) {
                    for (const w of workers) {
                        const wObj = w;
                        const statusColor = wObj.status === 'active' ? chalk.green : chalk.yellow;
                        console.log(`  ${statusColor(CH.bullet)} ${chalk.white(wObj.name || wObj.worker || 'unknown')}  ${chalk.gray(wObj.description || '')}`);
                        if (wObj.status) {
                            console.log(chalk.gray(`    Status: ${wObj.status}`));
                        }
                    }
                }
                else if (typeof workers === 'object') {
                    for (const [name, info] of Object.entries(workers)) {
                        const infoObj = info;
                        const statusColor = infoObj.maturity === 'active' ? chalk.green : chalk.yellow;
                        console.log(`  ${statusColor(CH.bullet)} ${chalk.white(name)}  ${chalk.gray(infoObj.description || '')}`);
                        if (infoObj.maturity)
                            console.log(chalk.gray(`    Maturity: ${infoObj.maturity}`));
                        if (infoObj.domains)
                            console.log(chalk.gray(`    Domains: ${Array.isArray(infoObj.domains) ? infoObj.domains.join(', ') : infoObj.domains}`));
                    }
                }
                console.log();
                return;
            }
            catch (err) {
                lastError = this.formatLegionError(`worker catalog request at ${baseUrl}`, err);
                continue;
            }
        }
        spinner.stop();
        this.logger.error(lastError || 'Could not reach Hyper Loop. Is vigthoria-hyper-loop running?');
    }
    async showStatus() {
        const spinner = createSpinner('Checking Legion infrastructure...').start();
        let lastError = null;
        for (const baseUrl of this.getHyperloopUrls()) {
            try {
                const response = await fetch(`${baseUrl}/status`, {
                    signal: AbortSignal.timeout(10000),
                    headers: this.getHeaders(),
                });
                if (!response.ok) {
                    lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${describeUpstreamStatus(response.status)}`;
                    continue;
                }
                const data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
                spinner.stop();
                console.log();
                console.log(chalk.bold.white(`  ${CH.hLine.repeat(3)} Legion Infrastructure ${CH.hLine.repeat(37)}`));
                console.log();
                console.log(chalk.gray(`  Hyper Loop: `) + chalk.green('online'));
                if (data.workers || data.active_workers) {
                    const count = data.workers || data.active_workers;
                    console.log(chalk.gray(`  Active workers: `) + chalk.white(String(typeof count === 'number' ? count : Object.keys(count).length)));
                }
                if (data.remotes || data.infrastructure) {
                    const remotes = data.remotes || data.infrastructure;
                    if (typeof remotes === 'object') {
                        for (const [name, status] of Object.entries(remotes)) {
                            const icon = status?.reachable ? chalk.green(CH.success) : chalk.red(CH.error);
                            console.log(`  ${icon} ${chalk.white(name)}`);
                        }
                    }
                }
                console.log();
                return;
            }
            catch (err) {
                lastError = this.formatLegionError(`status check at ${baseUrl}`, err);
                continue;
            }
        }
        spinner.stop();
        console.log();
        console.log(chalk.gray('  Hyper Loop: ') + chalk.red('offline'));
        if (lastError) {
            this.logger.error(lastError);
        }
        console.log();
    }
}
