#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Orchestrate Monitor CLI (WU-1241)
 *
 * Monitors delegated agent progress and delegation health.
 * Wires CLI to delegation-monitor APIs in @lumenflow/core.
 * WU-1604: Registry entries can come from explicit delegation intent.
 *
 * Features:
 * - Stuck detection: identifies pending delegations older than threshold
 * - Zombie lock detection: identifies abandoned WU locks (dead PIDs)
 * - Recovery actions: signal agent, restart delegation, escalate to human
 * - Status reporting: active delegations, stuck delegations, zombie locks, suggestions
 *
 * Usage:
 *   pnpm orchestrate:monitor                    # Show delegation status
 *   pnpm orchestrate:monitor --threshold 15    # Custom threshold (15 min)
 *   pnpm orchestrate:monitor --recover          # Run recovery actions
 *   pnpm orchestrate:monitor --recover --dry-run # Show what would be done
 *   pnpm orchestrate:monitor --since 30m        # Show signals since time
 */
import { Command } from 'commander';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { EXIT_CODES, LUMENFLOW_PATHS, DelegationRegistryStore, analyzeDelegations, detectStuckDelegations, checkZombieLocks, generateSuggestions, formatMonitorOutput, formatRecoveryResults, runRecovery, processDelegationFailureSignals, formatSignalHandlerOutput, DEFAULT_THRESHOLD_MINUTES, calculateBackoff, } from '@lumenflow/core';
import { ORCHESTRATION_ARTIFACT_DIR, ORCHESTRATION_STATUS_VALUES, } from '@lumenflow/control-plane-sdk';
import { DEFAULT_STALL_THRESHOLD_MS, detectContaminatedCodePaths, getMainDirtyPaths, isWorktreeStalled, } from './orchestrate-integrity.js';
import { MONITOR_QUALITY_SIGNAL_TYPE, readMonitorQualitySignalsConfig, runQualityDetectionForActiveWorktrees, } from './orchestrate-monitor-quality.js';
import { getConfig } from '@lumenflow/core/config';
import { DelegationStatus } from '@lumenflow/core/delegation-registry-schema';
import { defaultWorktreeFrom } from '@lumenflow/core/wu-paths';
import { loadInitiativeWUs } from '@lumenflow/initiatives';
import { loadSignals } from '@lumenflow/memory/signal';
import { loadMemory } from '@lumenflow/memory/store';
import { ProcessExitError, createError, ErrorCodes } from '@lumenflow/core/error-handler';
import chalk from 'chalk';
import ms, {} from 'ms';
import { runCLI } from './cli-entry-point.js';
// ============================================================================
// WU-1242: Watch Mode Constants
// ============================================================================
/**
 * Default watch interval (5 minutes in milliseconds)
 */
export const DEFAULT_WATCH_INTERVAL_MS = 5 * 60 * 1000;
/**
 * Minimum watch interval (1 minute in milliseconds)
 */
export const MIN_WATCH_INTERVAL_MS = 60 * 1000;
/**
 * Maximum backoff interval (1 hour in milliseconds)
 */
export const MAX_BACKOFF_MS = 60 * 60 * 1000;
const LOG_PREFIX = '[orchestrate:monitor]';
const ATTEMPT_PREFIX = 'attempt-';
const LAUNCH_FILENAME = 'launch.json';
const STATUS_FILENAME = 'status.json';
const TRIGGER_SUBAGENT_STOP = 'subagent-stop';
const FALLBACK_SIGNAL_COMPATIBILITY_MODE = 'skip-legacy';
const SIGNAL_BLOCKER_PATTERN = /\b(gate|blocked|blocker|fail|failed|error|timeout|crash)\b/i;
/**
 * Alias of the SDK {@link ORCHESTRATION_STATUS_VALUES} so existing callsites keep
 * using `EXECUTION_STATES.FOO` while the CLI surfaces the full ADR-010 state
 * vocabulary (planned / eligible / handoff_emitted / claimed / active /
 * worker_returned / ready_for_finish / awaiting_merge / blocked / contaminated /
 * stalled / needs_relaunch / done).
 */
export const EXECUTION_STATES = ORCHESTRATION_STATUS_VALUES;
function normalizeWuStatus(value) {
    return typeof value === 'string' ? value.trim().toLowerCase() : '';
}
function parseAttemptNumber(name) {
    if (!name.startsWith(ATTEMPT_PREFIX)) {
        return null;
    }
    const parsed = Number.parseInt(name.slice(ATTEMPT_PREFIX.length), 10);
    return Number.isNaN(parsed) ? null : parsed;
}
function readJsonFileIfExists(filePath) {
    if (!existsSync(filePath)) {
        return null;
    }
    try {
        return JSON.parse(readFileSync(filePath, 'utf8'));
    }
    catch {
        return null;
    }
}
function getLatestAttemptInfo(baseDir, initiativeId) {
    const rootDir = join(baseDir, ORCHESTRATION_ARTIFACT_DIR, initiativeId);
    if (!existsSync(rootDir)) {
        return null;
    }
    let latestAttempt = null;
    for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
        if (!entry.isDirectory()) {
            continue;
        }
        const parsed = parseAttemptNumber(entry.name);
        if (parsed === null) {
            continue;
        }
        latestAttempt = latestAttempt === null ? parsed : Math.max(latestAttempt, parsed);
    }
    if (latestAttempt === null) {
        return null;
    }
    return {
        attempt: latestAttempt,
        attemptDir: join(rootDir, `${ATTEMPT_PREFIX}${latestAttempt}`),
    };
}
function getSignalMessage(signal) {
    return signal.message.trim();
}
function isBlockerSignal(signal) {
    const signalText = `${signal.type ?? ''} ${signal.message}`.trim();
    return SIGNAL_BLOCKER_PATTERN.test(signalText);
}
function getLatestCheckpoint(nodes) {
    return ([...nodes]
        .filter((node) => node.type === 'checkpoint')
        .sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime())[0] ?? null);
}
function getLatestSignal(signals) {
    return ([...signals].sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime())[0] ?? null);
}
function getWorktreeRelativePath(wu) {
    if (typeof wu.doc.worktree_path === 'string' && wu.doc.worktree_path.trim().length > 0) {
        return wu.doc.worktree_path.trim();
    }
    if (typeof wu.doc.lane !== 'string' || wu.doc.lane.trim().length === 0) {
        return null;
    }
    return defaultWorktreeFrom({
        lane: wu.doc.lane,
        id: wu.id,
    });
}
function deriveWuExecutionState(input) {
    const { yamlStatus, stampPresent, worktreePresent, launchReceipt, delegation, blockerSignals } = input;
    const latestCheckpoint = input.latestCheckpoint;
    const checkpointTrigger = typeof latestCheckpoint?.metadata?.trigger === 'string'
        ? latestCheckpoint.metadata.trigger
        : '';
    const latestBlocker = blockerSignals[0];
    if (stampPresent || yamlStatus === 'done') {
        return {
            state: EXECUTION_STATES.DONE,
            reason: stampPresent ? 'done stamp present' : 'WU YAML status is done',
        };
    }
    if (yamlStatus === 'blocked') {
        return {
            state: EXECUTION_STATES.BLOCKED,
            reason: 'WU YAML status is blocked',
        };
    }
    if (latestBlocker) {
        return {
            state: EXECUTION_STATES.BLOCKED,
            reason: `blocker signal: ${getSignalMessage(latestBlocker)}`,
        };
    }
    if (checkpointTrigger === TRIGGER_SUBAGENT_STOP ||
        delegation?.status === DelegationStatus.COMPLETED) {
        return {
            state: EXECUTION_STATES.WORKER_RETURNED,
            reason: checkpointTrigger === TRIGGER_SUBAGENT_STOP
                ? 'sub-agent stop checkpoint recorded without completion'
                : 'delegation registry marked completed without a completion stamp',
        };
    }
    if (worktreePresent || yamlStatus === 'in_progress' || Boolean(delegation?.pickedUpAt)) {
        return {
            state: EXECUTION_STATES.ACTIVE,
            reason: worktreePresent
                ? 'worktree present'
                : delegation?.pickedUpAt
                    ? 'delegation pickup recorded'
                    : 'WU YAML status is in_progress',
        };
    }
    if (launchReceipt || delegation) {
        return {
            state: EXECUTION_STATES.HANDOFF_EMITTED,
            reason: launchReceipt
                ? `handoff emitted in attempt ${launchReceipt.launch_attempt ?? 'unknown'}`
                : 'delegation record exists without pickup evidence',
        };
    }
    return {
        state: EXECUTION_STATES.ELIGIBLE,
        reason: 'no launch or execution evidence',
    };
}
function buildProgress(wus) {
    const progress = {
        total: wus.length,
        done: 0,
        active: 0,
        pending: 0,
        blocked: 0,
        percentage: 0,
        handoffEmitted: 0,
        workerReturned: 0,
    };
    for (const wu of wus) {
        switch (wu.state) {
            case EXECUTION_STATES.DONE:
                progress.done += 1;
                break;
            case EXECUTION_STATES.BLOCKED:
            case EXECUTION_STATES.CONTAMINATED:
            case EXECUTION_STATES.STALLED:
            case EXECUTION_STATES.NEEDS_RELAUNCH:
                progress.blocked += 1;
                break;
            case EXECUTION_STATES.HANDOFF_EMITTED:
                progress.pending += 1;
                progress.handoffEmitted += 1;
                break;
            case EXECUTION_STATES.WORKER_RETURNED:
                progress.active += 1;
                progress.workerReturned += 1;
                break;
            case EXECUTION_STATES.CLAIMED:
            case EXECUTION_STATES.ACTIVE:
            case EXECUTION_STATES.READY_FOR_FINISH:
            case EXECUTION_STATES.AWAITING_MERGE:
                progress.active += 1;
                break;
            default:
                progress.pending += 1;
                break;
        }
    }
    progress.percentage =
        progress.total === 0 ? 0 : Math.round((progress.done / progress.total) * 100);
    return progress;
}
export function formatExecutionStateLabel(state) {
    return state;
}
function formatExecutionStateToken(state) {
    switch (state) {
        case EXECUTION_STATES.DONE:
            return chalk.green('✓ done');
        case EXECUTION_STATES.BLOCKED:
            return chalk.red('⛔ blocked');
        case EXECUTION_STATES.CONTAMINATED:
            return chalk.red('⚠ contaminated');
        case EXECUTION_STATES.STALLED:
            return chalk.red('⏳ stalled');
        case EXECUTION_STATES.NEEDS_RELAUNCH:
            return chalk.red('↻ needs_relaunch');
        case EXECUTION_STATES.READY_FOR_FINISH:
            return chalk.yellow('✅ ready_for_finish');
        case EXECUTION_STATES.AWAITING_MERGE:
            return chalk.yellow('⇪ awaiting_merge');
        case EXECUTION_STATES.WORKER_RETURNED:
            return chalk.yellow('↩ worker_returned');
        case EXECUTION_STATES.CLAIMED:
            return chalk.yellow('✋ claimed');
        case EXECUTION_STATES.ACTIVE:
            return chalk.yellow('⟳ active');
        case EXECUTION_STATES.HANDOFF_EMITTED:
            return chalk.cyan('↗ handoff_emitted');
        case EXECUTION_STATES.PLANNED:
            return chalk.gray('◌ planned');
        default:
            return chalk.gray('○ eligible');
    }
}
function filterSignalsByWu(signals, wuId) {
    return signals.filter((signal) => signal.wu_id === wuId);
}
function mapDelegationsByTarget(delegations) {
    const mapped = new Map();
    for (const delegation of delegations) {
        mapped.set(delegation.targetWuId, delegation);
    }
    return mapped;
}
async function loadDelegationMap(baseDir) {
    const store = new DelegationRegistryStore(join(baseDir, LUMENFLOW_PATHS.STATE_DIR));
    try {
        await store.load();
    }
    catch {
        return new Map();
    }
    return mapDelegationsByTarget(store.getAllDelegations());
}
async function loadIndexedMemory(baseDir) {
    try {
        return await loadMemory(join(baseDir, LUMENFLOW_PATHS.MEMORY_DIR));
    }
    catch {
        return null;
    }
}
async function loadAllSignals(baseDir) {
    try {
        return await loadSignals(baseDir, {
            compatibilityMode: FALLBACK_SIGNAL_COMPATIBILITY_MODE,
        });
    }
    catch {
        return [];
    }
}
function getStampedWuIds(baseDir) {
    const stamped = new Set();
    const stampDir = join(baseDir, LUMENFLOW_PATHS.STAMPS_DIR);
    if (!existsSync(stampDir)) {
        return stamped;
    }
    for (const file of readdirSync(stampDir)) {
        if (!file.endsWith('.done')) {
            continue;
        }
        stamped.add(file.slice(0, -'.done'.length));
    }
    return stamped;
}
export async function loadInitiativeExecutionSummary(baseDir, initiativeId, wus, options = {}) {
    const latestAttemptInfo = getLatestAttemptInfo(baseDir, initiativeId);
    const launchData = latestAttemptInfo
        ? readJsonFileIfExists(join(latestAttemptInfo.attemptDir, LAUNCH_FILENAME))
        : null;
    const statusData = latestAttemptInfo
        ? readJsonFileIfExists(join(latestAttemptInfo.attemptDir, STATUS_FILENAME))
        : null;
    const launchReceipts = launchData?.handoffs ?? [];
    const statusEntries = statusData?.wus ?? [];
    const launchReceiptByWu = new Map(launchReceipts.map((receipt) => [receipt.wu_id, receipt]));
    const memory = await loadIndexedMemory(baseDir);
    const signals = await loadAllSignals(baseDir);
    const delegationMap = await loadDelegationMap(baseDir);
    const stampedWuIds = getStampedWuIds(baseDir);
    const dirtyMainPaths = options.dirtyMainPaths ?? getMainDirtyPaths(baseDir);
    const stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
    const now = options.now ?? new Date();
    const liveWus = wus.map((wu) => {
        const launchReceipt = launchReceiptByWu.get(wu.id);
        const delegation = delegationMap.get(wu.id) ?? null;
        const wuNodes = memory?.byWu.get(wu.id) ?? [];
        const checkpoints = wuNodes.filter((node) => node.type === 'checkpoint');
        const latestCheckpoint = getLatestCheckpoint(checkpoints);
        const wuSignals = filterSignalsByWu(signals, wu.id);
        const blockerSignals = wuSignals.filter(isBlockerSignal);
        const latestSignal = getLatestSignal(wuSignals);
        const stampPresent = stampedWuIds.has(wu.id);
        const worktreeRelativePath = getWorktreeRelativePath(wu);
        const worktreePresent = worktreeRelativePath !== null && existsSync(join(baseDir, worktreeRelativePath));
        const yamlStatus = normalizeWuStatus(wu.doc.status);
        const derived = deriveWuExecutionState({
            yamlStatus,
            stampPresent,
            worktreePresent,
            launchReceipt,
            delegation,
            blockerSignals,
            latestCheckpoint,
        });
        const contaminatedPaths = detectContaminatedCodePaths(dirtyMainPaths, wu.doc.code_paths);
        const stalled = isWorktreeStalled({
            worktreePresent,
            latestCheckpointAt: latestCheckpoint?.created_at,
            latestSignalAt: latestSignal?.created_at,
            claimedAt: wu.doc.claimed_at,
        }, stallThresholdMs, now);
        return {
            wuId: wu.id,
            title: wu.doc.title ?? wu.id,
            lane: wu.doc.lane ?? 'unknown',
            state: derived.state,
            reason: derived.reason,
            yamlStatus,
            stampPresent,
            worktreePresent,
            logicalWave: launchReceipt?.logical_wave,
            launchAttempt: launchReceipt?.launch_attempt ?? launchData?.launch_attempt ?? undefined,
            launchReceipt,
            delegation,
            checkpointCount: checkpoints.length,
            latestCheckpointAt: latestCheckpoint?.created_at,
            latestCheckpointTrigger: typeof latestCheckpoint?.metadata?.trigger === 'string'
                ? latestCheckpoint.metadata.trigger
                : undefined,
            signalCount: wuSignals.length,
            latestSignalMessage: latestSignal ? getSignalMessage(latestSignal) : undefined,
            blockerSignals,
            contaminatedPaths: contaminatedPaths.length > 0 ? contaminatedPaths : undefined,
            stalled: stalled ? true : undefined,
            claimedAt: typeof wu.doc.claimed_at === 'string' ? wu.doc.claimed_at : undefined,
        };
    });
    return {
        initiativeId,
        launchAttempt: launchData?.launch_attempt ?? latestAttemptInfo?.attempt ?? null,
        logicalWaves: launchData?.logical_waves ?? statusData?.logical_waves ?? [],
        launchReceipts,
        statusEntries,
        wus: liveWus,
        progress: buildProgress(liveWus),
    };
}
/**
 * Project the SDK orchestration state onto the narrower WU YAML `status` vocabulary.
 * Integrity and recovery states (contaminated / stalled / needs_relaunch) collapse
 * to `blocked` so the YAML surface still signals "do not advance" instead of silently
 * showing `in_progress` as the legacy mapper did.
 */
function projectOrchestrationStateToYamlStatus(state) {
    switch (state) {
        case ORCHESTRATION_STATUS_VALUES.DONE:
            return 'done';
        case ORCHESTRATION_STATUS_VALUES.BLOCKED:
        case ORCHESTRATION_STATUS_VALUES.CONTAMINATED:
        case ORCHESTRATION_STATUS_VALUES.STALLED:
        case ORCHESTRATION_STATUS_VALUES.NEEDS_RELAUNCH:
            return 'blocked';
        case ORCHESTRATION_STATUS_VALUES.PLANNED:
        case ORCHESTRATION_STATUS_VALUES.ELIGIBLE:
        case ORCHESTRATION_STATUS_VALUES.HANDOFF_EMITTED:
            return 'ready';
        case ORCHESTRATION_STATUS_VALUES.CLAIMED:
        case ORCHESTRATION_STATUS_VALUES.ACTIVE:
        case ORCHESTRATION_STATUS_VALUES.WORKER_RETURNED:
        case ORCHESTRATION_STATUS_VALUES.READY_FOR_FINISH:
        case ORCHESTRATION_STATUS_VALUES.AWAITING_MERGE:
            return 'in_progress';
        default:
            return 'in_progress';
    }
}
export function applyExecutionSummaryToWus(wus, summary) {
    const stateByWu = new Map(summary.wus.map((wu) => [wu.wuId, wu.state]));
    return wus.map((wu) => {
        const state = stateByWu.get(wu.id);
        if (!state) {
            return wu;
        }
        return {
            ...wu,
            doc: {
                ...wu.doc,
                status: projectOrchestrationStateToYamlStatus(state),
            },
        };
    });
}
function shouldIncludeExecutionSummary(summary) {
    if (summary.launchReceipts.length > 0) {
        return true;
    }
    return summary.wus.some((wu) => wu.state !== EXECUTION_STATES.ELIGIBLE);
}
export async function loadAllInitiativeExecutionSummaries(baseDir, wuId) {
    const orchestrationRoot = join(baseDir, ORCHESTRATION_ARTIFACT_DIR);
    if (!existsSync(orchestrationRoot)) {
        return [];
    }
    const summaries = [];
    for (const entry of readdirSync(orchestrationRoot, { withFileTypes: true })) {
        if (!entry.isDirectory() || !entry.name.startsWith('INIT-')) {
            continue;
        }
        try {
            const { wus } = loadInitiativeWUs(entry.name);
            const summary = await loadInitiativeExecutionSummary(baseDir, entry.name, wus);
            const filteredSummary = wuId && wuId.trim().length > 0
                ? {
                    ...summary,
                    wus: summary.wus.filter((wu) => wu.wuId === wuId),
                }
                : summary;
            if (filteredSummary.wus.length === 0 || !shouldIncludeExecutionSummary(filteredSummary)) {
                continue;
            }
            const filteredLaunchReceipts = wuId && wuId.trim().length > 0
                ? summary.launchReceipts.filter((receipt) => receipt.wu_id === wuId)
                : summary.launchReceipts;
            const normalizedSummary = {
                ...filteredSummary,
                launchReceipts: filteredLaunchReceipts,
                progress: buildProgress(filteredSummary.wus),
            };
            summaries.push(normalizedSummary);
        }
        catch {
            continue;
        }
    }
    return summaries.sort((left, right) => left.initiativeId.localeCompare(right.initiativeId));
}
export function formatExecutionEvidenceSection(executionEvidence) {
    if (executionEvidence.length === 0) {
        return '';
    }
    const lines = [];
    lines.push(chalk.bold('Execution Evidence:'));
    for (const summary of executionEvidence) {
        const launchLabel = summary.launchAttempt === null ? 'unknown' : summary.launchAttempt.toString();
        const logicalWaveLabel = summary.logicalWaves.length === 0 ? 'n/a' : summary.logicalWaves.join(', ');
        lines.push(`  ${summary.initiativeId}: attempt=${launchLabel}, logical_waves=${logicalWaveLabel}, done=${summary.progress.done}, active=${summary.progress.active}, pending=${summary.progress.pending}, blocked=${summary.progress.blocked}`);
        for (const wu of summary.wus) {
            const details = [];
            if (typeof wu.logicalWave === 'number') {
                details.push(`logical wave ${wu.logicalWave}`);
            }
            details.push(wu.reason);
            if (wu.latestCheckpointTrigger) {
                details.push(`checkpoint=${wu.latestCheckpointTrigger}`);
            }
            if (wu.latestSignalMessage && wu.state === EXECUTION_STATES.BLOCKED) {
                details.push(wu.latestSignalMessage);
            }
            lines.push(`    - ${wu.wuId}: ${formatExecutionStateToken(wu.state)} (${details.join('; ')})`);
        }
    }
    return lines.join('\n');
}
/**
 * Runs the delegation monitor.
 *
 * @param options - Monitor options
 * @returns MonitorResult with analysis data
 */
export async function runMonitor(options = {}) {
    const { baseDir = process.cwd(), thresholdMinutes = DEFAULT_THRESHOLD_MINUTES, recover = false, dryRun = false, wuId, } = options;
    // Load delegation registry
    // WU-1278: Use full LUMENFLOW_PATHS.STATE_DIR without stripping .lumenflow/ prefix
    const stateDir = join(baseDir, LUMENFLOW_PATHS.STATE_DIR);
    const store = new DelegationRegistryStore(stateDir);
    let delegations = [];
    try {
        await store.load();
        delegations = store.getAllDelegations();
    }
    catch {
        // Registry doesn't exist or is invalid - continue with empty delegations
    }
    // Run core analysis
    const analysis = analyzeDelegations(delegations);
    const stuckDelegations = detectStuckDelegations(delegations, thresholdMinutes);
    const zombieLocks = await checkZombieLocks({ baseDir });
    const suggestions = generateSuggestions(stuckDelegations, zombieLocks);
    const result = {
        analysis,
        stuckDelegations,
        zombieLocks,
        suggestions,
        dryRun,
    };
    const executionEvidence = await loadAllInitiativeExecutionSummaries(baseDir, wuId);
    if (executionEvidence.length > 0) {
        result.executionEvidence = executionEvidence;
    }
    // WU-2667: Quality signal — active worktrees with prod commits but zero tests.
    // Non-blocking: appended to MonitorResult so formatOutput can warn, and
    // persisted via mem:signal so orchestrators poll it through mem:inbox.
    const qualitySignals = await collectQualitySignals(baseDir, executionEvidence);
    if (qualitySignals.length > 0) {
        result.qualitySignals = qualitySignals;
    }
    // Run recovery if requested
    if (recover) {
        const recoveryResults = await runRecovery(stuckDelegations, { baseDir, dryRun });
        result.recoveryResults = recoveryResults;
    }
    return result;
}
/**
 * WU-2667: Derive the list of active worktrees from execution evidence, then
 * call the quality detector. Returns an empty list when the policy is
 * disabled or no active worktrees exist.
 */
async function collectQualitySignals(baseDir, executionEvidence) {
    const policy = readMonitorQualitySignalsConfig(getConfig());
    if (!policy.enabled) {
        return [];
    }
    const activeWorktrees = collectActiveWorktreesForQualityCheck(executionEvidence);
    if (activeWorktrees.length === 0) {
        return [];
    }
    return runQualityDetectionForActiveWorktrees({
        baseDir,
        now: new Date(),
        policy,
        activeWorktrees,
    });
}
function collectActiveWorktreesForQualityCheck(executionEvidence) {
    const seen = new Set();
    const collected = [];
    for (const summary of executionEvidence) {
        for (const wu of summary.wus) {
            if (!wu.worktreePresent || wu.state === EXECUTION_STATES.DONE) {
                continue;
            }
            if (seen.has(wu.wuId)) {
                continue;
            }
            seen.add(wu.wuId);
            const worktreePath = deriveWorktreePathForQualityCheck(wu);
            if (!worktreePath) {
                continue;
            }
            collected.push({
                wuId: wu.wuId,
                lane: wu.lane,
                worktreePath,
                claimedAt: wu.claimedAt ?? wu.delegation?.pickedUpAt ?? wu.latestCheckpointAt,
            });
        }
    }
    return collected;
}
function deriveWorktreePathForQualityCheck(wu) {
    if (typeof wu.lane !== 'string' || wu.lane.trim().length === 0) {
        return null;
    }
    return defaultWorktreeFrom({ lane: wu.lane, id: wu.wuId });
}
/**
 * Formats monitor result for display.
 *
 * @param result - Monitor result to format
 * @returns Formatted output string
 */
export function formatOutput(result) {
    const lines = [];
    // Use core formatMonitorOutput for base formatting
    const baseOutput = formatMonitorOutput(result);
    lines.push(baseOutput);
    // Add recovery results if present
    if (result.recoveryResults && result.recoveryResults.length > 0) {
        lines.push('');
        lines.push(formatRecoveryResults(result.recoveryResults));
    }
    if (result.executionEvidence && result.executionEvidence.length > 0) {
        lines.push('');
        lines.push(formatExecutionEvidenceSection(result.executionEvidence));
    }
    if (result.qualitySignals && result.qualitySignals.length > 0) {
        lines.push('');
        lines.push(formatQualitySignalsSection(result.qualitySignals));
    }
    // Add dry-run notice if applicable
    if (result.dryRun) {
        lines.push('');
        lines.push(chalk.yellow('=== DRY RUN MODE ==='));
        lines.push(chalk.yellow('No actions were taken. Remove --dry-run to execute.'));
    }
    return lines.join('\n');
}
/**
 * WU-2667: Format the quality-signal section. Warning class — never fails
 * the monitor exit code. The canonical delivery channel is the mem:inbox
 * signal; this section exists so interactive monitor runs surface the same
 * information without requiring a second `pnpm mem:inbox` call.
 */
export function formatQualitySignalsSection(signals) {
    const lines = [];
    lines.push(chalk.bold(`Quality Signals (${MONITOR_QUALITY_SIGNAL_TYPE}):`));
    for (const emission of signals) {
        const locLine = `${emission.productionLoc} runtime LOC without tests`;
        lines.push(chalk.yellow(`  ⚠ ${emission.wuId} [${emission.lane}]: ${locLine}`));
        if (emission.firstProductionCommit) {
            lines.push(chalk.gray(`    first prod commit: ${emission.firstProductionCommit.hash.slice(0, 10)} ${emission.firstProductionCommit.subject}`));
        }
    }
    return lines.join('\n');
}
function parseTimeString(timeStr) {
    const msValue = ms(timeStr);
    if (typeof msValue === 'number') {
        return new Date(Date.now() - msValue);
    }
    const date = new Date(timeStr);
    if (isNaN(date.getTime())) {
        throw createError(ErrorCodes.INVALID_DURATION, `Invalid time format: ${timeStr}`);
    }
    return date;
}
function loadRecentSignals(since, baseDir = process.cwd()) {
    const signals = [];
    // WU-1278: Use full LUMENFLOW_PATHS.MEMORY_DIR without stripping .lumenflow/ prefix
    const memoryDir = join(baseDir, LUMENFLOW_PATHS.MEMORY_DIR);
    if (!existsSync(memoryDir)) {
        return signals;
    }
    const files = readdirSync(memoryDir).filter((f) => f.endsWith('.ndjson'));
    for (const file of files) {
        const filePath = join(memoryDir, file);
        const content = readFileSync(filePath, 'utf-8');
        const lines = content.trim().split('\n').filter(Boolean);
        for (const line of lines) {
            try {
                const signal = JSON.parse(line);
                const signalTime = new Date(signal.timestamp);
                if (signalTime >= since) {
                    signals.push(signal);
                }
            }
            catch {
                // Skip malformed lines
            }
        }
    }
    return signals.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
}
/**
 * Format signal type with appropriate color
 */
function formatSignalType(signalType) {
    if (signalType === 'complete') {
        return chalk.green(signalType);
    }
    if (signalType === 'error') {
        return chalk.red(signalType);
    }
    return chalk.yellow(signalType);
}
/**
 * Display signals in --signals-only mode
 */
async function displaySignals(opts) {
    const baseDir = process.cwd();
    const since = parseTimeString(opts.since);
    console.log(chalk.cyan(`${LOG_PREFIX} Loading signals since ${since.toISOString()}...`));
    const signals = loadRecentSignals(since, baseDir);
    if (signals.length === 0) {
        console.log(chalk.yellow(`${LOG_PREFIX} No signals found.`));
        console.log(chalk.gray('Agents may still be starting up, or memory layer not initialized.'));
        return;
    }
    const filtered = opts.wu ? signals.filter((s) => s.wuId === opts.wu) : signals;
    console.log(chalk.bold(`\nRecent Signals (${filtered.length}):\n`));
    for (const signal of filtered) {
        const time = new Date(signal.timestamp).toLocaleTimeString();
        const wu = signal.wuId ? chalk.cyan(signal.wuId) : chalk.gray('system');
        const type = formatSignalType(signal.type);
        console.log(`  ${chalk.gray(time)} [${wu}] ${type}: ${signal.message || ''}`);
    }
    console.log('');
    console.log(chalk.gray(`Use: pnpm mem:inbox --since ${opts.since} for more details`));
}
/**
 * Run main delegation monitoring mode
 */
async function runDelegationMonitoring(opts) {
    const baseDir = process.cwd();
    const thresholdMinutes = parseInt(opts.threshold, 10);
    if (isNaN(thresholdMinutes) || thresholdMinutes <= 0) {
        const message = `${LOG_PREFIX} Invalid threshold: ${opts.threshold}`;
        console.error(chalk.red(message));
        throw new ProcessExitError(message, EXIT_CODES.FAILURE);
    }
    console.log(chalk.cyan(`${LOG_PREFIX} Analyzing delegation health...`));
    console.log(chalk.gray(`  Threshold: ${thresholdMinutes} minutes`));
    console.log(chalk.gray(`  Recovery: ${opts.recover ? 'enabled' : 'disabled'}`));
    console.log(chalk.gray(`  Dry-run: ${opts.dryRun ? 'yes' : 'no'}`));
    console.log('');
    const result = await runMonitor({
        baseDir,
        thresholdMinutes,
        recover: opts.recover,
        dryRun: opts.dryRun,
        wuId: opts.wu,
    });
    console.log(formatOutput(result));
    if (opts.recover) {
        console.log('');
        console.log(chalk.cyan(`${LOG_PREFIX} Processing delegation failure signals...`));
        const signalResult = await processDelegationFailureSignals({ baseDir, dryRun: opts.dryRun });
        console.log(formatSignalHandlerOutput(signalResult));
    }
    if (result.stuckDelegations.length > 0 || result.zombieLocks.length > 0) {
        throw new ProcessExitError(`${LOG_PREFIX} Delegation monitor detected stuck delegations or zombie locks.`, EXIT_CODES.ERROR);
    }
}
/**
 * Parses CLI arguments into watch mode options.
 *
 * @param opts - Raw CLI options
 * @returns Parsed watch mode options
 */
export function parseWatchOptions(opts) {
    let intervalMs = DEFAULT_WATCH_INTERVAL_MS;
    if (opts.interval) {
        // Check if it's a plain number (no unit suffix) - treat as minutes
        if (/^\d+$/.test(opts.interval)) {
            const minutes = parseInt(opts.interval, 10);
            intervalMs = minutes * 60 * 1000;
        }
        else {
            // Parse with ms library (handles units like "5m", "1h", "30s")
            const parsed = ms(opts.interval);
            if (typeof parsed === 'number') {
                intervalMs = parsed;
            }
        }
    }
    // Enforce minimum interval
    if (intervalMs < MIN_WATCH_INTERVAL_MS) {
        intervalMs = MIN_WATCH_INTERVAL_MS;
    }
    return { intervalMs };
}
/**
 * Formats watch cycle output for display.
 *
 * @param result - Monitor result from the cycle
 * @param cycleNumber - The cycle number (1-indexed)
 * @param timestamp - Timestamp of the cycle
 * @returns Formatted output string
 */
export function formatWatchCycleOutput(result, cycleNumber, timestamp) {
    const lines = [];
    // Cycle header with timestamp
    const timeStr = timestamp.toISOString().replace('T', ' ').substring(0, 19);
    lines.push(chalk.cyan(`=== Patrol Cycle #${cycleNumber} [${timeStr}] ===`));
    lines.push('');
    // Quick summary
    const { analysis, stuckDelegations, zombieLocks } = result;
    const statusLine = [
        `Pending: ${analysis.pending}`,
        `Completed: ${analysis.completed}`,
        `Stuck: ${stuckDelegations.length}`,
        `Zombies: ${zombieLocks.length}`,
    ].join(' | ');
    if (stuckDelegations.length === 0 && zombieLocks.length === 0) {
        lines.push(chalk.green(`  ${statusLine}`));
        lines.push(chalk.green('  All delegations healthy.'));
    }
    else {
        lines.push(chalk.yellow(`  ${statusLine}`));
        // Show stuck delegations
        if (stuckDelegations.length > 0) {
            lines.push('');
            lines.push(chalk.yellow('  Stuck delegations:'));
            for (const info of stuckDelegations) {
                lines.push(chalk.yellow(`    - ${info.delegation.targetWuId} (${info.ageMinutes}min)`));
            }
        }
        // Show zombie locks
        if (zombieLocks.length > 0) {
            lines.push('');
            lines.push(chalk.yellow('  Zombie locks:'));
            for (const lock of zombieLocks) {
                lines.push(chalk.yellow(`    - ${lock.lane} (PID ${lock.pid})`));
            }
        }
    }
    lines.push('');
    return lines.join('\n');
}
/**
 * Creates a watch mode runner for continuous delegation monitoring.
 *
 * @param options - Configuration options
 * @returns WatchModeRunner instance
 */
export function createWatchModeRunner(options) {
    const { checkFn, intervalMs, onOutput = console.log } = options;
    let currentIntervalMs = intervalMs;
    let consecutiveFailures = 0;
    let running = false;
    let timer = null;
    let cycleCount = 0;
    function scheduleNext() {
        if (!running)
            return;
        timer = setTimeout(() => {
            // WU-1551: Add .catch() to prevent unhandled promise rejections
            void runCycle()
                .then(() => scheduleNext())
                .catch(() => {
                // Error already handled in runCycle's try/catch.
                // This .catch() is a safety net for unhandled rejections.
                scheduleNext();
            });
        }, currentIntervalMs);
    }
    async function runCycle() {
        if (!running)
            return;
        cycleCount++;
        const cycleNumber = cycleCount;
        try {
            const result = await checkFn();
            // Success - reset backoff
            consecutiveFailures = 0;
            currentIntervalMs = intervalMs;
            // Output cycle result
            const output = formatWatchCycleOutput(result, cycleNumber, new Date());
            onOutput(output);
        }
        catch (error) {
            // Failure - apply backoff
            consecutiveFailures++;
            currentIntervalMs = calculateBackoff(consecutiveFailures, intervalMs);
            if (currentIntervalMs > MAX_BACKOFF_MS) {
                currentIntervalMs = MAX_BACKOFF_MS;
            }
            const errorMsg = error instanceof Error ? error.message : String(error);
            onOutput(chalk.red(`=== Patrol Cycle #${cycleNumber} ERROR ===`));
            onOutput(chalk.red(`  ${errorMsg}`));
            onOutput(chalk.yellow(`  Next check in ${Math.round(currentIntervalMs / 1000)}s (backoff)`));
            onOutput('');
        }
    }
    return {
        start() {
            if (running)
                return;
            running = true;
            onOutput(chalk.cyan(`${LOG_PREFIX} Starting watch mode (interval: ${intervalMs / 1000}s)`));
            onOutput('');
            scheduleNext();
        },
        stop() {
            running = false;
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            onOutput(chalk.cyan(`${LOG_PREFIX} Stopping watch mode. Exiting gracefully.`));
        },
        get isRunning() {
            return running;
        },
        get currentIntervalMs() {
            return currentIntervalMs;
        },
        get consecutiveFailures() {
            return consecutiveFailures;
        },
    };
}
/**
 * Run watch mode (continuous patrol)
 */
async function runWatchMode(opts) {
    const baseDir = process.cwd();
    const thresholdMinutes = parseInt(opts.threshold, 10);
    if (isNaN(thresholdMinutes) || thresholdMinutes <= 0) {
        const message = `${LOG_PREFIX} Invalid threshold: ${opts.threshold}`;
        console.error(chalk.red(message));
        throw new ProcessExitError(message, EXIT_CODES.FAILURE);
    }
    const watchOptions = parseWatchOptions(opts);
    console.log(chalk.cyan(`${LOG_PREFIX} Starting continuous patrol mode...`));
    console.log(chalk.gray(`  Threshold: ${thresholdMinutes} minutes`));
    console.log(chalk.gray(`  Interval: ${watchOptions.intervalMs / 1000} seconds`));
    console.log(chalk.gray(`  Recovery: ${opts.recover ? 'enabled' : 'disabled'}`));
    console.log(chalk.gray(`  Press Ctrl+C to stop`));
    console.log('');
    const checkFn = async () => {
        return runMonitor({
            baseDir,
            thresholdMinutes,
            recover: opts.recover,
            dryRun: opts.dryRun,
        });
    };
    const runner = createWatchModeRunner({
        checkFn,
        intervalMs: watchOptions.intervalMs,
    });
    // Handle graceful shutdown
    const shutdown = () => {
        runner.stop();
        process.exitCode = EXIT_CODES.SUCCESS;
    };
    process.on('SIGINT', shutdown);
    process.on('SIGTERM', shutdown);
    runner.start();
    // Keep process alive
    await new Promise(() => {
        // Never resolves - waits for signal
    });
}
// CLI program
const program = new Command()
    .name('orchestrate:monitor')
    .description('Monitor delegated agent progress and delegation health (WU-1241, WU-1242)')
    .option('--threshold <minutes>', 'Stuck detection threshold in minutes (default: 30)', '30')
    .option('--recover', 'Run recovery actions for stuck delegations', false)
    .option('--dry-run', 'Show what would be done without taking action', false)
    .option('--since <time>', 'Show signals since (e.g., 30m, 1h)', '30m')
    .option('--wu <id>', 'Filter by WU ID')
    .option('--signals-only', 'Only show signals (skip delegation analysis)', false)
    .option('--watch', 'Continuous patrol mode (WU-1242)', false)
    .option('--interval <time>', 'Patrol interval for watch mode (e.g., 5m, 10m, 1h)', '5m')
    .action(async (opts) => {
    try {
        if (opts.signalsOnly) {
            await displaySignals(opts);
            return;
        }
        if (opts.watch) {
            await runWatchMode(opts);
            return;
        }
        await runDelegationMonitoring(opts);
    }
    catch (err) {
        if (err instanceof ProcessExitError) {
            throw err;
        }
        const message = err instanceof Error ? err.message : String(err);
        console.error(chalk.red(`${LOG_PREFIX} Error: ${message}`));
        throw new ProcessExitError(message, EXIT_CODES.ERROR);
    }
});
export async function main() {
    await program.parseAsync(process.argv);
}
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=orchestrate-monitor.js.map