"use strict";
/**
 * WorkflowEngine — execute, checkpoint, resume, and abort workflow runs.
 *
 * Story 4.2 — OpenLife v1.2 Royal Stack.
 *
 * The engine is the maestro. It iterates the workflow `sequence`,
 * dispatches each executable step through an injected
 * `WorkflowStepExecutor` (default = Gatekeeper-backed), records
 * progress to `.openlife/workflows/<runId>.json` via AtomicWriter,
 * appends a step-level event log to `.openlife/workflows/events/
 * <runId>.jsonl`, and saves a MissionCheckpoint after each successful
 * step.
 *
 * Crash safety: every state transition is durable. If the daemon dies
 * mid-step, calling `resume(runId)` reads the last checkpoint and
 * continues from the next pending step (idempotent steps may re-execute
 * once; non-idempotent are skipped to avoid double-side-effects).
 *
 * Elicit support: a step with `elicit: true` causes the engine to set
 * run status to `awaiting_input` and return control. The caller must
 * later invoke `provideInput(runId, stepId, payload)` to resume.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowEngine = exports.SimulatedStepExecutor = void 0;
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const ToolsetGuard_1 = require("../toolset/ToolsetGuard");
const WorkflowSchema_1 = require("./WorkflowSchema");
const AtomicWriter_1 = require("../util/AtomicWriter");
const MissionState_1 = require("../MissionState");
// ─────────────────────────────────────────────────────────────
// Default executor — simulated mode. Reports each step as completed
// without invoking real LLMs. Story 4.2 ships this as the v1.2
// minimum; deeper Gatekeeper integration is wired in Epic 5 once the
// AIOBuilder CLI commands materialize.
// ─────────────────────────────────────────────────────────────
class SimulatedStepExecutor {
    // eslint-disable-next-line @typescript-eslint/require-await
    async execute(inv) {
        if (inv.step.elicit) {
            return { status: 'awaiting_input' };
        }
        const artifacts = [];
        if (Array.isArray(inv.step.creates))
            artifacts.push(...inv.step.creates);
        if (Array.isArray(inv.step.updates))
            artifacts.push(...inv.step.updates);
        return {
            status: 'completed',
            artifacts,
            result: JSON.stringify({
                simulated: true,
                stepId: inv.step.id,
                agent: inv.step.agent,
                squad: inv.step.squad,
                action: inv.step.action,
            }),
        };
    }
}
exports.SimulatedStepExecutor = SimulatedStepExecutor;
class WorkflowEngine {
    stateDir;
    executor;
    checkpointStore;
    constructor(opts = {}) {
        this.stateDir = opts.stateDir
            || process.env.OPENLIFE_STATE_DIR
            || path.join(process.cwd(), '.openlife');
        this.executor = opts.executor || WorkflowEngine.defaultExecutor();
        this.checkpointStore = opts.checkpointStore || new MissionState_1.MissionCheckpointStore();
    }
    /** Pick the default step executor. Simulated unless OPENLIFE_REAL_WORKFLOW=1,
     *  in which case steps run through real catalog/method agents (Phase 5).
     *  Heavy deps are lazy-required only when the real executor is requested, so
     *  deterministic commands and dry-runs stay fast and free. */
    static defaultExecutor() {
        if (process.env.OPENLIFE_REAL_WORKFLOW !== '1') {
            return new SimulatedStepExecutor();
        }
        try {
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            const { CatalogStepExecutor } = require('./CatalogStepExecutor');
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            const { CatalogAgentExecutor } = require('../CatalogAgentExecutor');
            // eslint-disable-next-line @typescript-eslint/no-var-requires
            const { Brain } = require('../Brain');
            let memory;
            try {
                // eslint-disable-next-line @typescript-eslint/no-var-requires
                const { OmniMemory } = require('../../memory/OmniMemory');
                memory = new OmniMemory();
            }
            catch {
                memory = undefined;
            }
            return new CatalogStepExecutor(new CatalogAgentExecutor(new Brain(), memory));
        }
        catch {
            // If real execution can't be wired, fail safe to simulation.
            return new SimulatedStepExecutor();
        }
    }
    /**
     * Execute a workflow from scratch. Returns the final WorkflowRunState
     * (the caller is responsible for inspecting `status` — terminal states
     * are `completed`, `failed`, `aborted`, or `awaiting_input`).
     */
    async run(workflow, context = {}, opts = {}) {
        (0, ToolsetGuard_1.assertToolsetAllowed)('workflows', 'WorkflowEngine.run');
        // v1.3 — service mode requires explicit opt-in (Story 2.3).
        // If the workflow declares mode: 'service', the caller MUST pass
        // { allowServiceMode: true }. Silent task → service promotion refused.
        const declaredMode = workflow.mode || 'task';
        if (declaredMode === 'service' && !opts.allowServiceMode) {
            const err = new Error(`workflow '${workflow.id}' declares mode='service' but caller did not pass allowServiceMode=true; service mode is explicit-only`);
            err.code = 'service_mode_requires_explicit_opt_in';
            throw err;
        }
        const runId = this.newRunId(workflow.id);
        const state = {
            runId,
            workflowId: workflow.id,
            status: 'pending',
            startedAt: new Date().toISOString(),
            completedSteps: [],
            steps: {},
            artifacts: [],
            context: { ...context, __mode: declaredMode },
        };
        this.persistState(state);
        this.emitEvent({
            ts: new Date().toISOString(),
            runId,
            type: 'run.started',
            detail: { workflowId: workflow.id, mode: declaredMode },
        });
        state.status = 'running';
        this.persistState(state);
        return await this.executeFrom(workflow, state, 0);
    }
    /**
     * Read the persisted state for a run without modifying it.
     */
    status(runId) {
        const filePath = this.stateFilePath(runId);
        if (!fs.existsSync(filePath))
            return null;
        try {
            return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
        }
        catch {
            return null;
        }
    }
    /**
     * Resume a previously-paused or crashed run. Requires the original
     * Workflow (the engine does not persist workflow definitions — only
     * runs of them, identified by `workflowId` in state).
     */
    async resume(workflow, runId) {
        const state = this.status(runId);
        if (!state) {
            throw new Error(`workflow run not found: ${runId}`);
        }
        if (state.status === 'completed' || state.status === 'failed' || state.status === 'aborted') {
            return state; // terminal — no-op
        }
        if (state.workflowId !== workflow.id) {
            throw new Error(`workflowId mismatch: state has '${state.workflowId}', supplied '${workflow.id}'`);
        }
        this.emitEvent({ ts: new Date().toISOString(), runId, type: 'run.resumed', detail: { fromStatus: state.status } });
        // Find the index to resume from: first non-completed executable step
        const resumeIdx = this.findResumeIndex(workflow.sequence, state);
        state.status = 'running';
        this.persistState(state);
        return await this.executeFrom(workflow, state, resumeIdx);
    }
    /**
     * Mark a run as aborted. Idempotent.
     */
    abort(runId, reason = 'manual') {
        const state = this.status(runId);
        if (!state)
            return null;
        if (state.status === 'completed' || state.status === 'failed' || state.status === 'aborted') {
            return state;
        }
        state.status = 'aborted';
        state.failureReason = reason;
        state.completedAt = new Date().toISOString();
        this.persistState(state);
        this.emitEvent({ ts: new Date().toISOString(), runId, type: 'run.aborted', detail: { reason } });
        this.checkpointStore.clear(runId);
        return state;
    }
    /**
     * Provide user input for a step that paused with elicit. Re-runs that
     * step with the input merged into context.
     */
    async provideInput(workflow, runId, stepId, payload) {
        const state = this.status(runId);
        if (!state)
            throw new Error(`workflow run not found: ${runId}`);
        if (state.status !== 'awaiting_input' || state.currentStep !== stepId) {
            throw new Error(`run ${runId} is not awaiting input on step ${stepId} (status=${state.status}, currentStep=${state.currentStep ?? 'none'})`);
        }
        state.context = { ...state.context, [`elicit:${stepId}`]: payload };
        state.status = 'running';
        this.persistState(state);
        // Mark this step's elicit gate as satisfied so the executor can proceed.
        // The simplest approach: re-run from this step's index, but clear its
        // elicit flag in-memory for this run only.
        const idx = workflow.sequence.findIndex((e) => (0, WorkflowSchema_1.isExecutableStep)(e) && e.id === stepId);
        if (idx < 0)
            throw new Error(`step ${stepId} not in workflow ${workflow.id}`);
        // Clone the workflow with elicit=false on this specific step so the
        // resumed run advances past it.
        const patched = {
            ...workflow,
            sequence: workflow.sequence.map((e) => {
                if ((0, WorkflowSchema_1.isExecutableStep)(e) && e.id === stepId)
                    return { ...e, elicit: false };
                return e;
            }),
        };
        return await this.executeFrom(patched, state, idx);
    }
    // ─────────────────────────────────────────────────────────
    // Internals
    // ─────────────────────────────────────────────────────────
    async executeFrom(workflow, state, startIdx) {
        for (let i = startIdx; i < workflow.sequence.length; i++) {
            const entry = workflow.sequence[i];
            if (!(0, WorkflowSchema_1.isExecutableStep)(entry)) {
                // Phase marker — narrative only, no execution
                continue;
            }
            const step = entry;
            // Skip already-completed steps (resume path)
            if (state.completedSteps.includes(step.id)) {
                continue;
            }
            // Optional step + condition not met → skip cleanly
            if (step.optional && !this.conditionMet(step, state)) {
                state.steps[step.id] = {
                    stepId: step.id,
                    status: 'skipped',
                    completedAt: new Date().toISOString(),
                };
                state.completedSteps.push(step.id);
                this.persistState(state);
                this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'step.skipped', stepId: step.id, detail: { reason: 'optional_condition_not_met' } });
                continue;
            }
            // Begin step
            const stepRec = {
                stepId: step.id,
                status: 'running',
                startedAt: new Date().toISOString(),
            };
            state.steps[step.id] = stepRec;
            state.currentStep = step.id;
            this.persistState(state);
            this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'step.started', stepId: step.id });
            let result;
            try {
                result = await this.executor.execute({ runId: state.runId, step, context: state.context || {} });
            }
            catch (err) {
                stepRec.status = 'failed';
                stepRec.error = err instanceof Error ? err.message : String(err);
                stepRec.completedAt = new Date().toISOString();
                state.status = 'failed';
                state.failureReason = `step ${step.id} threw: ${stepRec.error}`;
                state.completedAt = new Date().toISOString();
                this.persistState(state);
                this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'step.failed', stepId: step.id, detail: { error: stepRec.error } });
                this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'run.failed', detail: { failedStep: step.id } });
                return state;
            }
            if (result.status === 'awaiting_input') {
                state.status = 'awaiting_input';
                stepRec.status = 'running'; // stays running until input arrives
                this.persistState(state);
                this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'run.awaiting_input', stepId: step.id });
                return state;
            }
            if (result.status === 'failed') {
                stepRec.status = 'failed';
                stepRec.error = result.error || 'unknown failure';
                stepRec.completedAt = new Date().toISOString();
                state.status = 'failed';
                state.failureReason = `step ${step.id} failed: ${stepRec.error}`;
                state.completedAt = new Date().toISOString();
                this.persistState(state);
                this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'step.failed', stepId: step.id, detail: { error: stepRec.error } });
                this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'run.failed', detail: { failedStep: step.id } });
                return state;
            }
            // completed or skipped — both advance the run
            stepRec.status = result.status;
            stepRec.completedAt = new Date().toISOString();
            stepRec.artifacts = result.artifacts;
            stepRec.result = result.result;
            state.completedSteps.push(step.id);
            if (result.artifacts)
                state.artifacts.push(...result.artifacts);
            state.currentStep = undefined;
            state.lastCheckpointAt = new Date().toISOString();
            this.persistState(state);
            this.checkpointStore.saveCheckpoint(state.runId, step.id, {
                workflowId: workflow.id,
                completedSteps: state.completedSteps,
                artifacts: state.artifacts,
            });
            this.emitEvent({
                ts: new Date().toISOString(),
                runId: state.runId,
                type: result.status === 'completed' ? 'step.completed' : 'step.skipped',
                stepId: step.id,
                detail: { artifacts: result.artifacts || [] },
            });
        }
        // All steps processed → mark run completed
        state.status = 'completed';
        state.completedAt = new Date().toISOString();
        this.persistState(state);
        this.emitEvent({ ts: new Date().toISOString(), runId: state.runId, type: 'run.completed', detail: { totalSteps: state.completedSteps.length } });
        this.checkpointStore.clear(state.runId);
        return state;
    }
    conditionMet(step, state) {
        if (!step.condition)
            return true;
        // v1.4 (Story 10.3) — full expression engine: AND / OR / NOT / == / !=
        // plus parentheses. Backward-compat: a bare identifier `foo` still
        // evaluates to `ctx.foo === true`.
        const { evaluateCondition } = require('./ConditionParser');
        const ctx = (state.context || {});
        return evaluateCondition(step.condition, ctx);
    }
    findResumeIndex(sequence, state) {
        for (let i = 0; i < sequence.length; i++) {
            const e = sequence[i];
            if (!(0, WorkflowSchema_1.isExecutableStep)(e))
                continue;
            if (!state.completedSteps.includes(e.id))
                return i;
        }
        return sequence.length;
    }
    persistState(state) {
        (0, AtomicWriter_1.writeJsonAtomic)(this.stateFilePath(state.runId), state);
    }
    emitEvent(event) {
        (0, AtomicWriter_1.appendJsonlAtomic)(this.eventLogPath(event.runId), event);
    }
    stateFilePath(runId) {
        return path.join(this.stateDir, 'workflows', `${runId}.json`);
    }
    eventLogPath(runId) {
        return path.join(this.stateDir, 'workflows', 'events', `${runId}.jsonl`);
    }
    newRunId(workflowId) {
        const ts = Date.now().toString(36);
        const rnd = crypto.randomBytes(4).toString('hex');
        return `${workflowId}-${ts}-${rnd}`;
    }
    /** Exposed for tests so they can assert resolved storage location. */
    _stateDir() {
        return this.stateDir;
    }
}
exports.WorkflowEngine = WorkflowEngine;
