#!/usr/bin/env node
/**
 * Feature Agent Worker
 *
 * Background worker entry point that runs as a child process via fork().
 * Initializes DI, creates the feature agent graph, and executes it.
 * Sends periodic heartbeats so the CLI can detect stuck/crashed workers.
 *
 * CLI Args: --feature-id <id> --run-id <id> --repo <path> --spec-dir <path>
 */
import 'reflect-metadata';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { unlinkSync } from 'node:fs';
import { Command } from '@langchain/langgraph';
import { initializeContainer, container } from '../../../di/container.js';
import { createFeatureAgentGraph } from './feature-agent-graph.js';
import { createFastFeatureAgentGraph } from './fast-feature-agent-graph.js';
import { createCheckpointer } from '../common/checkpointer.js';
import { AgentRunStatus, SdlcLifecycle } from '../../../../domain/generated/output.js';
import { initializeSettings } from '../../../services/settings.service.js';
import { InitializeSettingsUseCase } from '../../../../application/use-cases/settings/initialize-settings.use-case.js';
import { setHeartbeatContext } from './heartbeat.js';
import { setPhaseTimingContext, recordLifecycleEvent } from './phase-timing-context.js';
import { setLifecycleContext } from './lifecycle-context.js';
import { setSdlcBoardContext } from './sdlc-board-context.js';
import { setLogPrefix, getLogPrefix } from './log-context.js';
import { FeatureAgentLifecyclePublisher } from './feature-agent-lifecycle-publisher.js';
import { FeatureAgentGateQuestionPublisher } from './feature-agent-gate-question-publisher.js';
import { FeatureAgentSupervisorGateEvaluator } from './feature-agent-supervisor-gate-evaluator.js';
import { UpdateFeatureLifecycleUseCase } from '../../../../application/use-cases/features/update/update-feature-lifecycle.use-case.js';
import { CleanupFeatureWorktreeUseCase } from '../../../../application/use-cases/features/cleanup-feature-worktree.use-case.js';
/**
 * Parse CLI arguments into a WorkerArgs object.
 * Throws if any required argument is missing.
 */
export function parseWorkerArgs(args) {
    const getArg = (name) => {
        const index = args.indexOf(`--${name}`);
        if (index === -1 || index + 1 >= args.length) {
            throw new Error(`Missing required argument: --${name}`);
        }
        return args[index + 1];
    };
    const worktreeIdx = args.indexOf('--worktree-path');
    const worktreePath = worktreeIdx !== -1 && worktreeIdx + 1 < args.length ? args[worktreeIdx + 1] : undefined;
    const approvalIdx = args.indexOf('--approval-gates');
    let approvalGates;
    if (approvalIdx !== -1 && approvalIdx + 1 < args.length) {
        try {
            approvalGates = JSON.parse(args[approvalIdx + 1]);
        }
        catch {
            // Fall back to no gates if JSON parse fails
            approvalGates = undefined;
        }
    }
    const resume = args.includes('--resume');
    const resumeFromInterrupt = args.includes('--resume-from-interrupt');
    const push = args.includes('--push');
    const openPr = args.includes('--open-pr');
    const forkAndPr = args.includes('--fork-and-pr');
    const commitSpecs = !args.includes('--no-commit-specs');
    const ciWatchEnabled = !args.includes('--no-ci-watch');
    const enableEvidence = args.includes('--enable-evidence');
    const commitEvidence = args.includes('--commit-evidence');
    const fast = args.includes('--fast');
    const threadIdx = args.indexOf('--thread-id');
    const threadId = threadIdx !== -1 && threadIdx + 1 < args.length ? args[threadIdx + 1] : undefined;
    const agentTypeIdx = args.indexOf('--agent-type');
    const agentType = agentTypeIdx !== -1 && agentTypeIdx + 1 < args.length
        ? args[agentTypeIdx + 1]
        : undefined;
    const resumePayloadIdx = args.indexOf('--resume-payload');
    const resumePayload = resumePayloadIdx !== -1 && resumePayloadIdx + 1 < args.length
        ? args[resumePayloadIdx + 1]
        : undefined;
    const modelIdx = args.indexOf('--model');
    const model = modelIdx !== -1 && modelIdx + 1 < args.length ? args[modelIdx + 1] : undefined;
    const resumeReasonIdx = args.indexOf('--resume-reason');
    const resumeReason = resumeReasonIdx !== -1 && resumeReasonIdx + 1 < args.length
        ? args[resumeReasonIdx + 1]
        : undefined;
    return {
        featureId: getArg('feature-id'),
        runId: getArg('run-id'),
        repo: getArg('repo'),
        specDir: getArg('spec-dir'),
        worktreePath,
        approvalGates,
        resume,
        threadId,
        resumeFromInterrupt,
        push,
        openPr,
        forkAndPr,
        commitSpecs,
        ciWatchEnabled,
        enableEvidence,
        commitEvidence,
        resumePayload,
        agentType,
        fast,
        model,
        resumeReason,
    };
}
/** Simple worker logger — writes to stdout which is redirected to log file by the parent. */
function log(message) {
    const ts = new Date().toISOString();
    process.stdout.write(`[${ts}] ${getLogPrefix()}[WORKER] ${message}\n`);
}
/** Heartbeat interval (30 seconds) */
const HEARTBEAT_INTERVAL_MS = 30_000;
/**
 * Start periodic heartbeat that updates last_heartbeat in the DB.
 * Returns a cleanup function to stop the interval.
 */
function startHeartbeat(runId, runRepository) {
    const interval = setInterval(async () => {
        try {
            const now = new Date();
            await runRepository.updateStatus(runId, AgentRunStatus.running, {
                lastHeartbeat: now,
                updatedAt: now,
            });
        }
        catch {
            // Heartbeat failure is non-fatal — just log it
            log('Heartbeat update failed (non-fatal)');
        }
    }, HEARTBEAT_INTERVAL_MS);
    return () => clearInterval(interval);
}
/**
 * Run the feature agent worker with the given arguments.
 * Initializes DI, creates the graph, and executes it.
 */
export async function runWorker(args) {
    // Set log prefix early so all subsequent log lines carry agent/model info
    const agentLabel = args.agentType ?? 'default';
    setLogPrefix(agentLabel, args.model);
    // Log the full reconstructed spawn command for debugging
    const cmdParts = [
        'feature-agent-worker',
        '--feature-id',
        args.featureId,
        '--run-id',
        args.runId,
        '--repo',
        args.repo,
        '--spec-dir',
        args.specDir,
        ...(args.worktreePath ? ['--worktree-path', args.worktreePath] : []),
        ...(args.approvalGates ? ['--approval-gates', JSON.stringify(args.approvalGates)] : []),
        ...(args.resume ? ['--resume'] : []),
        ...(args.threadId ? ['--thread-id', args.threadId] : []),
        ...(args.resumeFromInterrupt ? ['--resume-from-interrupt'] : []),
        ...(args.push ? ['--push'] : []),
        ...(args.openPr ? ['--open-pr'] : []),
        ...(args.forkAndPr ? ['--fork-and-pr'] : []),
        ...(args.commitSpecs === false ? ['--no-commit-specs'] : []),
        ...(args.resumePayload ? ['--resume-payload', args.resumePayload] : []),
        ...(args.agentType ? ['--agent-type', args.agentType] : []),
        ...(args.fast ? ['--fast'] : []),
        ...(args.model ? ['--model', args.model] : []),
    ];
    log(`Starting worker — full command:`);
    log(`  ${cmdParts.join(' ')}`);
    log('Initializing container...');
    await initializeContainer();
    // Initialize settings in the worker process
    log('Loading settings...');
    const initSettingsUseCase = container.resolve(InitializeSettingsUseCase);
    const settings = await initSettingsUseCase.execute();
    initializeSettings(settings);
    const runRepository = container.resolve('IAgentRunRepository');
    const executorProvider = container.resolve('IAgentExecutorProvider');
    // Create executor — use pinned agentType when resuming, otherwise fall back to settings
    let executor;
    if (args.agentType) {
        log(`Creating executor from pinned agent type: ${args.agentType}`);
        const factory = container.resolve('IAgentExecutorFactory');
        executor = factory.createExecutor(args.agentType, settings.agent);
    }
    else {
        log('Creating executor from configured agent settings...');
        executor = await executorProvider.getExecutor();
    }
    // Resolve merge node dependencies
    const gitPrService = container.resolve('IGitPrService');
    const featureRepository = container.resolve('IFeatureRepository');
    const cleanupFeatureWorktreeUseCase = container.resolve(CleanupFeatureWorktreeUseCase);
    const graphDeps = {
        executor,
        mergeNodeDeps: {
            getDiffSummary: (cwd, baseBranch) => gitPrService.getPrDiffSummary(cwd, baseBranch),
            hasRemote: (cwd) => gitPrService.hasRemote(cwd),
            getDefaultBranch: (cwd) => gitPrService.getDefaultBranch(cwd),
            verifyMerge: (cwd, featureBranch, baseBranch, premergeBaseSha) => gitPrService.verifyMerge(cwd, featureBranch, baseBranch, premergeBaseSha),
            revParse: (cwd, ref) => gitPrService.revParse(cwd, ref),
            localMergeSquash: (cwd, featureBranch, baseBranch, commitMessage, hasRemote) => gitPrService.localMergeSquash(cwd, featureBranch, baseBranch, commitMessage, hasRemote),
            featureRepository,
            gitPrService,
            gitForkService: container.resolve('IGitForkService'),
            cleanupFeatureWorktreeUseCase,
        },
    };
    // Use threadId for checkpoint path so resume runs share the same checkpoint DB.
    // Falls back to runId for backwards compatibility with existing runs.
    const checkpointId = args.threadId ?? args.runId;
    const checkpointPath = join(homedir(), '.shep', 'checkpoints', `${checkpointId}.db`);
    log(`Creating checkpointer at ${checkpointPath} (thread: ${checkpointId})`);
    const checkpointer = createCheckpointer(checkpointPath);
    // Both graph factories return compiled graphs with identical FeatureAgentAnnotation
    // state shape and invoke() interface. Cast through unknown because the compiled
    // graphs have different node name types but share the same runtime contract.
    const graph = args.fast
        ? createFastFeatureAgentGraph(graphDeps, checkpointer)
        : createFeatureAgentGraph(graphDeps, checkpointer);
    // Mark the run as running with our PID
    const now = new Date();
    log(`Updating status to running (PID ${process.pid})...`);
    await runRepository.updateStatus(args.runId, AgentRunStatus.running, {
        pid: process.pid,
        startedAt: now,
        lastHeartbeat: now,
        updatedAt: now,
    });
    // Start heartbeat so the CLI can detect if we're still alive
    const stopHeartbeat = startHeartbeat(args.runId, runRepository);
    // Set heartbeat context so node-helpers can update current node
    setHeartbeatContext(args.runId, runRepository);
    // Set phase timing context so executeNode() records per-phase durations
    const timingRepository = container.resolve('IPhaseTimingRepository');
    setPhaseTimingContext(args.runId, timingRepository);
    // Capture repos for SIGTERM handler
    runRepoForSignal = runRepository;
    timingRepoForSignal = timingRepository;
    // Set lifecycle context so nodes update the feature lifecycle as they execute.
    // Route through UpdateFeatureLifecycleUseCase so CheckAndUnblockFeaturesUseCase
    // fires on every transition, automatically unblocking eligible blocked children.
    const updateLifecycleUseCase = container.resolve(UpdateFeatureLifecycleUseCase);
    setLifecycleContext(args.featureId, updateLifecycleUseCase);
    // Set SDLC board context so the implement node can write task/sub-task progress
    // to the board in real time. Best-effort: tracker errors are swallowed in the context.
    const sdlcBoardTracker = container.resolve('ISdlcBoardTracker');
    setSdlcBoardContext(args.featureId, sdlcBoardTracker);
    // Record lifecycle event
    await recordLifecycleEvent(args.resume ? 'run:resumed' : 'run:started');
    // Resolve the lifecycle publisher once. It is itself feature-flag-gated
    // through SendAgentMessageUseCase, so when `featureFlags.collaboration`
    // is off the calls below are no-ops and no rows are written.
    const lifecyclePublisher = container.resolve(FeatureAgentLifecyclePublisher);
    // Gate-question publisher: emits a parallel AgentQuestion of
    // kind=blocking on every waiting_approval transition so the unified
    // inbox covers background-mode gates (spec 093, task 20). Same flag
    // gating — no-op when collaboration is off.
    const gateQuestionPublisher = container.resolve(FeatureAgentGateQuestionPublisher);
    // Supervisor gate evaluator: on every waiting_approval transition
    // ask the configured supervisor (if any) to evaluate the gate
    // (spec 093, task 29). In autonomous mode the supervisor may
    // resolve the gate directly; in advisory / co-sign modes the
    // decision is recorded for the user but the gate stays open.
    const supervisorGateEvaluator = container.resolve(FeatureAgentSupervisorGateEvaluator);
    const lifecycleScope = {
        runId: args.runId,
        featureId: args.featureId,
        repositoryPath: args.repo,
    };
    await lifecyclePublisher.publishStarted(lifecycleScope);
    try {
        const graphConfig = { configurable: { thread_id: checkpointId } };
        let result;
        if (args.resume && args.resumeFromInterrupt) {
            // Resume from an interrupt (human-in-the-loop approval)
            let resumeValue = { approved: true }; // default backward-compatible
            if (args.resumePayload) {
                try {
                    resumeValue = JSON.parse(args.resumePayload);
                    log(`Resuming with typed payload: ${args.resumePayload.slice(0, 200)}`);
                }
                catch (e) {
                    log(`Warning: Failed to parse --resume-payload, using default approval: ${e}`);
                }
            }
            // Derive state updates from the resume payload so executeNode() can
            // read _approvalAction from state instead of from interrupt() return
            // value (fixes the dual-interrupt stale replay bug).
            const stateUpdate = {};
            if (typeof resumeValue === 'object' &&
                resumeValue !== null &&
                'rejected' in resumeValue &&
                resumeValue.rejected === true) {
                stateUpdate._approvalAction = 'rejected';
                stateUpdate._rejectionFeedback = resumeValue.feedback ?? null;
            }
            else {
                stateUpdate._approvalAction = 'approved';
                stateUpdate._rejectionFeedback = null;
            }
            log('Resuming graph from interrupt checkpoint...');
            result = await graph.invoke(new Command({ resume: resumeValue, update: stateUpdate }), graphConfig);
        }
        else if (args.resume) {
            // Resume from error — delete the stale checkpoint and re-invoke from
            // scratch. Each producer node checks its completedPhases entry and skips
            // if already done, so completed phases replay instantly. The phase that
            // failed has its completedPhases entry cleared by routeValidation (or was
            // never marked complete if the executor threw), so it re-executes fully.
            //
            // Why delete the checkpoint instead of resuming from it:
            // When a validate/repair loop exhausts retries and throws, the checkpoint
            // captures the validation node with maxed-out retries. Resuming from that
            // checkpoint would re-evaluate the same conditional edge and throw again
            // immediately — the user's retry would be stuck in an infinite loop.
            log('Deleting stale checkpoint for fresh resume...');
            try {
                unlinkSync(checkpointPath);
                log('Checkpoint deleted successfully');
            }
            catch {
                log('No checkpoint to delete (first run or already cleaned)');
            }
            // Re-create checkpointer after deleting the old DB
            const freshCheckpointer = createCheckpointer(checkpointPath);
            const freshGraph = args.fast
                ? createFastFeatureAgentGraph(graphDeps, freshCheckpointer)
                : createFeatureAgentGraph(graphDeps, freshCheckpointer);
            log('Resuming graph with fresh checkpoint...');
            result = await freshGraph.invoke({
                featureId: args.featureId,
                repositoryPath: args.repo,
                worktreePath: args.worktreePath ?? args.repo,
                specDir: args.specDir,
                error: undefined, // Clear previous error state
                ...(args.approvalGates ? { approvalGates: args.approvalGates } : {}),
                ...(args.model ? { model: args.model } : {}),
                ...(args.resumeReason ? { resumeReason: args.resumeReason } : {}),
                push: args.push ?? false,
                openPr: args.openPr ?? false,
                forkAndPr: args.forkAndPr ?? false,
                commitSpecs: args.commitSpecs ?? true,
                ciWatchEnabled: args.ciWatchEnabled ?? true,
                enableEvidence: args.enableEvidence ?? false,
                commitEvidence: args.commitEvidence ?? false,
            }, graphConfig);
        }
        else {
            log('Starting graph invocation...');
            await lifecyclePublisher.publishPhaseChanged({
                ...lifecycleScope,
                phase: 'graph-start',
            });
            result = await graph.invoke({
                featureId: args.featureId,
                repositoryPath: args.repo,
                worktreePath: args.worktreePath ?? args.repo,
                specDir: args.specDir,
                ...(args.approvalGates ? { approvalGates: args.approvalGates } : {}),
                ...(args.model ? { model: args.model } : {}),
                push: args.push ?? false,
                openPr: args.openPr ?? false,
                forkAndPr: args.forkAndPr ?? false,
                commitSpecs: args.commitSpecs ?? true,
                ciWatchEnabled: args.ciWatchEnabled ?? true,
                enableEvidence: args.enableEvidence ?? false,
                commitEvidence: args.commitEvidence ?? false,
            }, graphConfig);
        }
        log(`Graph invocation completed. Error: ${result.error ?? 'none'}`);
        stopHeartbeat();
        // Check if graph was interrupted (human-in-the-loop approval needed)
        const interruptPayload = result.__interrupt__;
        if (interruptPayload && interruptPayload.length > 0) {
            const now = new Date();
            // Extract the node name from the interrupt payload so the rejection
            // use case can tag feedback with the correct phase (e.g. "merge").
            const interruptValue = interruptPayload[0]?.value;
            const interruptNode = typeof interruptValue?.node === 'string' ? interruptValue.node : undefined;
            await runRepository.updateStatus(args.runId, AgentRunStatus.waitingApproval, {
                updatedAt: now,
                ...(interruptNode ? { result: `node:${interruptNode}` } : {}),
            });
            await lifecyclePublisher.publishBlocked({
                ...lifecycleScope,
                reason: interruptNode ? `waiting_approval:${interruptNode}` : 'waiting_approval',
            });
            await gateQuestionPublisher.publishWaitingApproval({
                ...lifecycleScope,
                interruptNode,
            });
            // Consult the supervisor (no-op when none configured / flag off).
            // In autonomous mode the supervisor may close the gate directly;
            // in advisory / co-sign modes the decision is recorded but the
            // gate stays in waiting_approval for the user.
            const supervisorOutcome = await supervisorGateEvaluator.evaluateForGate({
                ...lifecycleScope,
                interruptNode,
            });
            if (supervisorOutcome.autoResolved) {
                log(`Supervisor auto-resolved gate (verdict=${supervisorOutcome.verdict}, autonomy=${supervisorOutcome.effectiveAutonomy})`);
            }
            else if (supervisorOutcome.evaluated) {
                log(`Supervisor advised on gate (verdict=${supervisorOutcome.verdict}, autonomy=${supervisorOutcome.effectiveAutonomy}); user action still required`);
            }
            log('Run paused — waiting for human approval');
            return;
        }
        // Check if the graph itself reported an error in state
        if (result.error) {
            const failedAt = new Date();
            await runRepository.updateStatus(args.runId, AgentRunStatus.failed, {
                error: result.error,
                completedAt: failedAt,
                updatedAt: failedAt,
            });
            await recordLifecycleEvent('run:failed');
            log(`Run marked as failed: ${result.error}`);
            return;
        }
        const completedAt = new Date();
        await runRepository.updateStatus(args.runId, AgentRunStatus.completed, {
            result: result.messages?.join('\n') ?? '',
            completedAt,
            updatedAt: completedAt,
        });
        await recordLifecycleEvent('run:completed');
        await lifecyclePublisher.publishCompleted(lifecycleScope);
        log('Run marked as completed');
    }
    catch (error) {
        stopHeartbeat();
        const failedAt = new Date();
        const message = error instanceof Error ? error.message : String(error);
        log(`Graph invocation error: ${message}`);
        await runRepository.updateStatus(args.runId, AgentRunStatus.failed, {
            error: message,
            completedAt: failedAt,
            updatedAt: failedAt,
        });
        // Reset the feature lifecycle to Started so it doesn't appear stuck
        // in a running phase (e.g., Requirements, Implementation) when the agent has failed.
        try {
            const feature = await featureRepository.findById(args.featureId);
            if (feature && feature.lifecycle !== SdlcLifecycle.Maintain) {
                await featureRepository.update({
                    ...feature,
                    lifecycle: SdlcLifecycle.Started,
                    updatedAt: failedAt,
                });
                log('Feature lifecycle reset to Started');
            }
        }
        catch (resetErr) {
            log(`Failed to reset feature lifecycle: ${resetErr instanceof Error ? resetErr.message : String(resetErr)}`);
        }
        await recordLifecycleEvent('run:failed');
        log('Run marked as failed');
    }
}
// Catch unhandled errors globally so they always appear in the log file
process.on('uncaughtException', (err) => {
    log(`UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}`);
    process.exit(1);
});
process.on('unhandledRejection', (reason) => {
    const msg = reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason);
    log(`UNHANDLED REJECTION: ${msg}`);
    process.exit(1);
});
// Handle IPC disconnect (parent exited) gracefully — this is expected for detached workers
process.on('disconnect', () => {
    log('Parent disconnected (IPC channel closed) — continuing as detached worker');
});
// Handle SIGTERM for graceful shutdown
let runIdForSignal;
let runRepoForSignal;
let timingRepoForSignal;
process.on('SIGTERM', async () => {
    log('Received SIGTERM, shutting down...');
    if (runIdForSignal && runRepoForSignal) {
        const now = new Date();
        await runRepoForSignal.updateStatus(runIdForSignal, AgentRunStatus.interrupted, {
            error: 'Process received SIGTERM',
            completedAt: now,
            updatedAt: now,
        });
        await recordLifecycleEvent('run:stopped', runIdForSignal, timingRepoForSignal);
    }
    process.exit(0);
});
// Main execution when run as a script
// When using fork(), the forked process runs the entire module, but we want to detect if we're the entrypoint
const isMainModule = typeof require !== 'undefined'
    ? require.main === module
    : process.argv[1]?.includes('feature-agent-worker');
if (isMainModule) {
    log('Worker process starting...');
    try {
        const args = parseWorkerArgs(process.argv.slice(2));
        runIdForSignal = args.runId;
        runWorker(args)
            .then(() => {
            runRepoForSignal = container.resolve('IAgentRunRepository');
            log('Worker completed successfully, exiting.');
            process.exit(0);
        })
            .catch((err) => {
            const msg = err instanceof Error ? err.message : String(err);
            log(`Worker fatal error: ${msg}`);
            process.exit(1);
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        log(`Worker setup error: ${msg}`);
        process.exit(1);
    }
}
