"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VSAFEngine = void 0;
exports.startMCPServer = startMCPServer;
const fs_1 = require("fs");
const path_1 = require("path");
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const core_1 = require("@vsaf/core");
const MAX_EXECUTORS = 10;
const IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
class VSAFEngine {
    executors = new Map();
    workspaces = new Map();
    runMeta = new Map();
    runStores = new Map();
    failedRuns = new Set();
    runLocks = new Map();
    shuttingDown = false;
    constructor() {
        const onShutdown = () => { this.shutdown(); };
        process.on('SIGINT', onShutdown);
        process.on('SIGTERM', onShutdown);
    }
    async start(params) {
        // Evict stale executors before starting a new run
        this.evict();
        const config = (0, core_1.loadConfig)(params.projectPath);
        const workflowPath = (0, path_1.join)(params.projectPath, '.vsaf', 'workflows', `${params.workflow}.yaml`);
        if (!(0, fs_1.existsSync)(workflowPath)) {
            throw new Error(`Workflow not found: "${params.workflow}" (expected at ${workflowPath})`);
        }
        const yamlContent = (0, fs_1.readFileSync)(workflowPath, 'utf-8');
        const graph = (0, core_1.parseWorkflow)(yamlContent);
        const storeDir = (0, path_1.join)(params.projectPath, '.vsaf', 'storage');
        // Initialize RunLock for this storage directory
        if (!this.runLocks.has(storeDir)) {
            this.runLocks.set(storeDir, new core_1.RunLock(storeDir));
        }
        // Prune orphan workspaces before creating new ones
        const wsManager = new core_1.WorkspaceManager({ exclude: config.isolation.exclude });
        try {
            await wsManager.pruneOrphans(params.projectPath, storeDir);
        }
        catch { /* pruneOrphans is best-effort */ }
        const ws = await wsManager.create(params.projectPath, params.workflow);
        const runStore = new core_1.RunStore(storeDir);
        runStore.sweepMarked();
        const run = runStore.create(params.workflow, params.args, ws.sourcePath, ws.artifactsPath, params.projectPath, yamlContent, ws.branchName);
        const callbacks = {
            onNodeStart: (nodeId) => {
                runStore.updateNode(run.runId, nodeId, { status: 'running', startedAt: new Date().toISOString() });
                runStore.save(run.runId);
            },
            onNodeComplete: (nodeId, result) => {
                runStore.updateNode(run.runId, nodeId, result);
                runStore.save(run.runId);
            },
            onNodeSkip: (nodeId) => {
                runStore.updateNode(run.runId, nodeId, { status: 'skipped' });
                runStore.save(run.runId);
            },
            onNodeFail: (nodeId, error, retryCount) => {
                runStore.updateNode(run.runId, nodeId, { status: 'failed', error }, { retryCount });
                runStore.save(run.runId);
            },
            onNodeReset: (nodeId) => {
                runStore.resetNode(run.runId, nodeId);
            },
        };
        const executor = new core_1.DAGExecutor(ws.sourcePath, params.projectPath, graph, callbacks);
        this.executors.set(run.runId, {
            executor,
            lastAccess: Date.now(),
            status: 'active',
        });
        this.workspaces.set(run.runId, { manager: wsManager, wsId: ws.id });
        this.runMeta.set(run.runId, { workflow: params.workflow, projectPath: params.projectPath, storeDir });
        this.runStores.set(run.runId, runStore);
        let step;
        try {
            step = await this.lockedStep(run.runId, { args: params.args, artifactsDir: ws.artifactsPath });
        }
        catch (err) {
            await this.cleanupRun(run.runId);
            throw err;
        }
        if (step.type === 'done') {
            await this.finishRun(run.runId);
        }
        else if (step.type === 'failed') {
            runStore.fail(run.runId);
            try {
                runStore.appendHistory(run.runId);
            }
            catch { /* best-effort */ }
            await this.cleanupRun(run.runId);
        }
        return { runId: run.runId, step };
    }
    async next(params) {
        const entry = this.executors.get(params.runId);
        if (!entry)
            throw new Error(`Run not found: ${params.runId}`);
        entry.lastAccess = Date.now();
        const runStore = this.runStores.get(params.runId);
        let step;
        try {
            step = await this.lockedStep(params.runId, {
                completedResults: params.results.map(r => {
                    let structured = r.structuredOutput;
                    if (!structured) {
                        try {
                            const parsed = JSON.parse(r.output);
                            if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
                                structured = parsed;
                            }
                        }
                        catch { /* leave undefined */ }
                    }
                    return { nodeId: r.nodeId, output: r.output, structuredOutput: structured };
                }),
            });
        }
        catch (err) {
            await this.cleanupRun(params.runId);
            throw err;
        }
        if (step.type === 'done') {
            await this.finishRun(params.runId);
        }
        else if (step.type === 'failed') {
            runStore.fail(params.runId);
            try {
                runStore.appendHistory(params.runId);
            }
            catch { /* best-effort */ }
            await this.cleanupRun(params.runId);
        }
        return { step };
    }
    status(params) {
        const meta = this.runMeta.get(params.runId);
        if (!meta)
            throw new Error(`Run not found: ${params.runId}`);
        const entry = this.executors.get(params.runId);
        let status;
        if (entry) {
            status = entry.status === 'done' ? 'completed' : 'running';
        }
        else if (this.failedRuns.has(params.runId)) {
            status = 'failed';
        }
        else {
            status = 'completed';
        }
        let nodes = entry?.executor.getNodeSummaries();
        // Fallback to RunStore when executor is evicted
        if (!nodes) {
            const runStore = this.runStores.get(params.runId) ?? new core_1.RunStore(meta.storeDir);
            const run = runStore.load(params.runId);
            if (run) {
                nodes = run.nodes;
                if (run.status === 'failed')
                    status = 'failed';
                else if (run.status === 'completed')
                    status = 'completed';
            }
        }
        return {
            runId: params.runId,
            workflow: meta.workflow,
            status,
            nodes,
        };
    }
    async resume(params) {
        const existingEntry = this.executors.get(params.runId);
        if (existingEntry?.status === 'active') {
            throw new Error(`Run is already active: ${params.runId}`);
        }
        const storeDir = (0, path_1.join)(params.projectPath, '.vsaf', 'storage');
        const runDir = (0, path_1.join)(storeDir, params.runId);
        if (!(0, fs_1.existsSync)(runDir)) {
            throw new Error(`Run directory not found: ${params.runId}`);
        }
        const state = (0, core_1.recoverState)(runDir);
        if (!state) {
            throw new Error(`Cannot recover state for run: ${params.runId}`);
        }
        // Reject resuming terminal runs
        if (state.runStatus === 'completed' || state.runStatus === 'failed') {
            throw new Error(`Cannot resume ${state.runStatus} run: ${params.runId}`);
        }
        // Verify workspace directory still exists
        if (!(0, fs_1.existsSync)(state.workspacePath)) {
            throw new Error(`Workspace directory missing for run ${params.runId}: ${state.workspacePath}`);
        }
        // Parse workflow from recovered YAML
        if (!state.workflowYaml) {
            throw new Error(`No workflow YAML in recovered state for run: ${params.runId}`);
        }
        const graph = (0, core_1.parseWorkflow)(state.workflowYaml);
        // Add unstarted nodes to pending so they are not lost after crash recovery
        for (const nodeId of graph.nodes.keys()) {
            if (!state.completed.has(nodeId) && !state.failed.has(nodeId) && !state.skipped.has(nodeId)) {
                state.pending.add(nodeId);
            }
        }
        // Initialize RunLock
        if (!this.runLocks.has(storeDir)) {
            this.runLocks.set(storeDir, new core_1.RunLock(storeDir));
        }
        const runStore = new core_1.RunStore(storeDir);
        // Load the run into the store's in-memory cache
        runStore.load(params.runId);
        const callbacks = {
            onNodeStart: (nodeId) => {
                runStore.updateNode(params.runId, nodeId, { status: 'running', startedAt: new Date().toISOString() });
                runStore.save(params.runId);
            },
            onNodeComplete: (nodeId, result) => {
                runStore.updateNode(params.runId, nodeId, result);
                runStore.save(params.runId);
            },
            onNodeSkip: (nodeId) => {
                runStore.updateNode(params.runId, nodeId, { status: 'skipped' });
                runStore.save(params.runId);
            },
            onNodeFail: (nodeId, error, retryCount) => {
                runStore.updateNode(params.runId, nodeId, { status: 'failed', error }, { retryCount });
                runStore.save(params.runId);
            },
            onNodeReset: (nodeId) => {
                runStore.resetNode(params.runId, nodeId);
            },
        };
        const executor = core_1.DAGExecutor.fromRecoveredState(state.workspacePath, state.projectPath, graph, callbacks, {
            pending: state.pending,
            completed: state.completed,
            failed: state.failed,
            skipped: state.skipped,
            nodeResults: state.nodeResults,
            retryCounts: state.retryCounts,
            args: state.args,
            artifactsPath: state.artifactsPath,
        });
        this.executors.set(params.runId, {
            executor,
            lastAccess: Date.now(),
            status: 'active',
        });
        this.runMeta.set(params.runId, { workflow: state.workflow, projectPath: state.projectPath, storeDir });
        this.runStores.set(params.runId, runStore);
        // Re-register workspace so finishRun can apply changes and destroy it
        const config = (0, core_1.loadConfig)(params.projectPath);
        const wsManager = new core_1.WorkspaceManager({ exclude: config.isolation.exclude });
        const wsType = (0, fs_1.existsSync)((0, path_1.join)(state.workspacePath, '.git'))
            ? 'worktree'
            : 'ephemeral';
        wsManager.adopt(params.runId, state.workspacePath, state.artifactsPath, state.projectPath, wsType, state.branchName);
        this.workspaces.set(params.runId, { manager: wsManager, wsId: params.runId });
        // Step the resumed executor to get next pending work
        let step;
        try {
            step = await this.lockedStep(params.runId, {});
        }
        catch (err) {
            await this.cleanupRun(params.runId);
            throw err;
        }
        if (step.type === 'done') {
            await this.finishRun(params.runId);
        }
        else if (step.type === 'failed') {
            runStore.fail(params.runId);
            try {
                runStore.appendHistory(params.runId);
            }
            catch { /* best-effort */ }
            await this.cleanupRun(params.runId);
        }
        return { runId: params.runId, step };
    }
    listRuns(projectPath) {
        const storeDir = (0, path_1.join)(projectPath, '.vsaf', 'storage');
        if (!(0, fs_1.existsSync)(storeDir))
            return [];
        const runStore = new core_1.RunStore(storeDir);
        return runStore.listResumable();
    }
    listHistory(projectPath, limit = 50) {
        const storeDir = (0, path_1.join)(projectPath, '.vsaf', 'storage');
        const runStore = new core_1.RunStore(storeDir);
        return runStore.listHistory(limit);
    }
    async lockedStep(runId, input) {
        const entry = this.executors.get(runId);
        if (!entry)
            throw new Error(`Run not found: ${runId}`);
        const meta = this.runMeta.get(runId);
        const lock = meta ? this.runLocks.get(meta.storeDir) : undefined;
        if (lock) {
            const acquired = lock.acquire(runId);
            if (!acquired) {
                throw new Error(`Could not acquire lock for run: ${runId}`);
            }
        }
        try {
            return await entry.executor.step(input);
        }
        finally {
            if (lock) {
                lock.release(runId);
            }
        }
    }
    evict() {
        const now = Date.now();
        // First pass: remove done executors past TTL
        for (const [runId, entry] of this.executors) {
            if (entry.status === 'done' && (now - entry.lastAccess) > IDLE_TTL_MS) {
                this.executors.delete(runId);
                this.workspaces.delete(runId);
                this.runStores.delete(runId);
                // Note: keep runMeta to support status() fallback; note: runMeta is small (3 strings per run)
                this.failedRuns.delete(runId);
            }
        }
        // Second pass: if still over max, remove oldest done entries (never evict active)
        if (this.executors.size > MAX_EXECUTORS) {
            const sorted = [...this.executors.entries()]
                .filter(([, e]) => e.status === 'done')
                .sort((a, b) => a[1].lastAccess - b[1].lastAccess);
            while (this.executors.size > MAX_EXECUTORS && sorted.length > 0) {
                const [runId] = sorted.shift();
                this.executors.delete(runId);
                this.workspaces.delete(runId);
                this.runStores.delete(runId);
                // Note: keep runMeta to support status() fallback; note: runMeta is small (3 strings per run)
                this.failedRuns.delete(runId);
            }
        }
    }
    shutdown() {
        if (this.shuttingDown)
            return;
        this.shuttingDown = true;
        // Release all locks
        for (const [runId, meta] of this.runMeta) {
            const lock = this.runLocks.get(meta.storeDir);
            if (lock) {
                try {
                    lock.release(runId);
                }
                catch { /* best-effort */ }
            }
        }
        // Write final snapshots for all active runs
        for (const [runId, entry] of this.executors) {
            if (entry.status === 'active') {
                const runStore = this.runStores.get(runId);
                if (runStore) {
                    try {
                        runStore.snapshotSync(runId);
                    }
                    catch { /* best-effort */ }
                }
            }
        }
    }
    async cleanupRun(runId) {
        this.failedRuns.add(runId);
        const wsInfo = this.workspaces.get(runId);
        if (wsInfo) {
            try {
                await wsInfo.manager.destroy(wsInfo.wsId);
            }
            catch { /* workspace cleanup is best-effort */ }
        }
        this.executors.delete(runId);
        this.workspaces.delete(runId);
        this.runStores.delete(runId);
    }
    async finishRun(runId) {
        const runStore = this.runStores.get(runId);
        if (runStore)
            runStore.complete(runId);
        const wsInfo = this.workspaces.get(runId);
        if (wsInfo) {
            try {
                await wsInfo.manager.applyChanges(wsInfo.wsId);
            }
            catch { /* apply is best-effort */ }
            try {
                await wsInfo.manager.destroy(wsInfo.wsId);
            }
            catch { /* destroy is best-effort */ }
        }
        // Persist run summary to history before deleting run data
        if (runStore) {
            try {
                runStore.appendHistory(runId);
            }
            catch { /* history is best-effort */ }
        }
        // Auto-delete completed run storage (two-phase: marker + rm)
        if (runStore) {
            try {
                runStore.deleteRun(runId);
            }
            catch { /* cleanup is best-effort */ }
        }
        // Mark as done rather than deleting, so eviction can clean up later
        const entry = this.executors.get(runId);
        if (entry) {
            entry.status = 'done';
            entry.lastAccess = Date.now();
        }
        this.workspaces.delete(runId);
        this.runStores.delete(runId);
        // Keep runMeta — status() needs it for in-memory lookup after disk is deleted
    }
}
exports.VSAFEngine = VSAFEngine;
async function startMCPServer() {
    const engine = new VSAFEngine();
    const server = new index_js_1.Server({ name: 'vsaf', version: '0.2.0' }, { capabilities: { tools: {} } });
    server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
        tools: [
            {
                name: 'vsaf_start',
                description: 'Start a VSAF DAG workflow. Returns first step (bash results + AI nodes to execute).',
                inputSchema: {
                    type: 'object',
                    properties: {
                        workflow: { type: 'string', description: 'Workflow name (e.g. sdlc/master-sdlc)' },
                        args: { type: 'string', description: 'Arguments passed to workflow' },
                        projectPath: { type: 'string', description: 'Project root path' },
                    },
                    required: ['workflow', 'args', 'projectPath'],
                },
            },
            {
                name: 'vsaf_next',
                description: 'Report AI node results and advance the workflow to the next step.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        runId: { type: 'string', description: 'Run ID from vsaf_start' },
                        results: {
                            type: 'array',
                            items: {
                                type: 'object',
                                properties: {
                                    nodeId: { type: 'string' },
                                    output: { type: 'string' },
                                    structuredOutput: { type: 'object', description: 'Structured output (auto-parsed from JSON if not provided)' },
                                },
                                required: ['nodeId', 'output'],
                            },
                            description: 'Completed AI node results',
                        },
                    },
                    required: ['runId', 'results'],
                },
            },
            {
                name: 'vsaf_status',
                description: 'Get current run status and progress.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        runId: { type: 'string', description: 'Run ID' },
                    },
                    required: ['runId'],
                },
            },
            {
                name: 'vsaf_resume',
                description: 'Resume a crashed or interrupted workflow run from its WAL.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        runId: { type: 'string', description: 'Run ID to resume' },
                        projectPath: { type: 'string', description: 'Project root path' },
                    },
                    required: ['runId', 'projectPath'],
                },
            },
            {
                name: 'vsaf_list_runs',
                description: 'List resumable (non-completed) workflow runs.',
                inputSchema: {
                    type: 'object',
                    properties: {
                        projectPath: { type: 'string', description: 'Project root path' },
                    },
                    required: ['projectPath'],
                },
            },
            {
                name: 'vsaf_list_history',
                description: 'List completed/failed workflow run history (persisted after run deletion).',
                inputSchema: {
                    type: 'object',
                    properties: {
                        projectPath: { type: 'string', description: 'Project root path' },
                        limit: { type: 'number', description: 'Max entries to return (default 50)' },
                    },
                    required: ['projectPath'],
                },
            },
        ],
    }));
    server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
        const args = request.params.arguments;
        try {
            switch (request.params.name) {
                case 'vsaf_start': {
                    const result = await engine.start(args);
                    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
                }
                case 'vsaf_next': {
                    const result = await engine.next(args);
                    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
                }
                case 'vsaf_status': {
                    const result = engine.status(args);
                    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
                }
                case 'vsaf_resume': {
                    const result = await engine.resume(args);
                    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
                }
                case 'vsaf_list_runs': {
                    const result = engine.listRuns(args.projectPath);
                    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
                }
                case 'vsaf_list_history': {
                    const projectPath = args.projectPath;
                    const limit = args.limit;
                    const result = engine.listHistory(projectPath, limit);
                    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
                }
                default:
                    throw new Error(`Unknown tool: ${request.params.name}`);
            }
        }
        catch (error) {
            return {
                content: [{ type: 'text', text: `Error: ${error.message}` }],
                isError: true,
            };
        }
    });
    const transport = new stdio_js_1.StdioServerTransport();
    await server.connect(transport);
}
//# sourceMappingURL=server.js.map