// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * WU Spawn Strategy Resolver
 *
 * Extracted from wu-spawn.ts (WU-1652).
 * Contains client/strategy resolution, lane occupation checks,
 * and CLI argument parsing for wu:brief/wu:delegate commands.
 *
 * @module wu-spawn-strategy-resolver
 */
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/arg-parser';
import { WU_PATHS } from '@lumenflow/core/wu-paths';
import { parseYAML } from '@lumenflow/core/wu-yaml';
import { die, getErrorMessage } from '@lumenflow/core/error-handler';
import { WU_STATUS, PATTERNS, FILE_SYSTEM, EMOJI, BRANCHES } from '@lumenflow/core/wu-constants';
import { createGitForPath } from '@lumenflow/core/git-adapter';
// WU-1603: Check lane lock status before spawning
// WU-1325: Import lock policy getter for lane availability check
import { checkLaneLock } from '@lumenflow/core/lane-lock';
import { getLockPolicyForLane, getWipLimitForLane } from '@lumenflow/core/lane-checker';
import { validateSpawnArgs, recordSpawnToRegistry, formatSpawnRecordedMessage, } from '@lumenflow/core/wu-spawn-helpers';
import { generateDelegationId, } from '@lumenflow/core/delegation-registry-schema';
import { WUStateStore, WU_BRIEF_EVIDENCE_NOTE_PREFIX } from '@lumenflow/core/wu-state-store';
import { SpawnStrategyFactory } from '@lumenflow/core/spawn-strategy';
import { getConfig } from '@lumenflow/core/config';
import { resolveClientConfig } from '@lumenflow/core/wu-spawn-skills';
import { validateSpawnDependencies, formatDependencyError, } from '@lumenflow/core/dependency-validator';
import { checkMemoryLayerInitialized, getMemoryContextMaxSize, generateMemoryContextSection, } from '@lumenflow/core/wu-spawn-context';
import { generateTaskInvocation, generateCodexPrompt } from './wu-spawn-prompt-builders.js';
import { resolveStateDir } from './state-path-resolvers.js';
// WU-2144: Import worktree detection to gate evidence recording
import { isInWorktree as isInWorktreeDefault } from '@lumenflow/core/core/worktree-guard';
// WU-2141: Import sizing check for advisory/strict enforcement
import { checkBriefSizing } from './wu-brief-sizing.js';
import crypto from 'node:crypto';
// Re-export SpawnStrategyFactory for consumers
export { SpawnStrategyFactory };
const BRIEF_LOG_PREFIX = '[wu:brief]';
const DELEGATE_LOG_PREFIX = '[wu:delegate]';
const BRIEF_EVIDENCE_PROGRESS = 'wu:brief executed';
const BRIEF_EVIDENCE_MODE_FULL = 'full';
const BRIEF_EVIDENCE_MODE_CLAIM_AUTO = 'claim-auto';
function buildBriefAttestation(output, clientName, generatedAt = new Date().toISOString()) {
    return {
        algorithm: 'sha256',
        promptHash: crypto.createHash('sha256').update(output, 'utf8').digest('hex'),
        promptLength: output.length,
        generatedAt,
        clientName,
    };
}
// ─── Lane Occupation ───
/**
 * WU-1603: Check if a lane is currently occupied by another WU
 * WU-1325: Now considers lock_policy - lanes with policy=none are never occupied
 *
 * @param {string} lane - Lane name (e.g., "Operations: Tooling")
 * @returns {import('@lumenflow/core/lane-lock').LockMetadata|null} Lock metadata if occupied, null otherwise
 */
export function checkLaneOccupation(lane) {
    // WU-1325: Lanes with lock_policy=none never report as occupied
    const lockPolicy = getLockPolicyForLane(lane);
    if (lockPolicy === 'none') {
        return null;
    }
    const lockStatus = checkLaneLock(lane);
    if (lockStatus.locked && lockStatus.metadata) {
        return lockStatus.metadata;
    }
    return null;
}
export function generateLaneOccupationWarning(lockMetadata, targetWuId, options = {}) {
    const { isStale = false } = options;
    let warning = `⚠️  Lane "${lockMetadata.lane}" is occupied by ${lockMetadata.wuId}\n`;
    // WU-1346: Use injected values if provided, otherwise look up from config
    const lockPolicy = options.lockPolicy ?? getLockPolicyForLane(lockMetadata.lane);
    const wipLimit = options.wipLimit ?? getWipLimitForLane(lockMetadata.lane);
    warning += `   This violates WIP=${wipLimit} (lock_policy=${lockPolicy}).\n\n`;
    if (isStale) {
        warning += `   ⏰ This lock is STALE (>24 hours old) - the WU may be abandoned.\n`;
        warning += `   Consider using pnpm wu:block --id ${lockMetadata.wuId} if work is stalled.\n\n`;
    }
    warning += `   Options:\n`;
    warning += `   1. Wait for ${lockMetadata.wuId} to complete or block\n`;
    warning += `   2. Choose a different lane for ${targetWuId}\n`;
    warning += `   3. Block ${lockMetadata.wuId} if work is stalled: pnpm wu:block --id ${lockMetadata.wuId}`;
    return warning;
}
const BRIEF_PARSER_CONFIG = {
    name: 'wu-brief',
    description: 'Generate handoff prompt for sub-agent WU execution',
};
const DELEGATE_PARSER_CONFIG = {
    name: 'wu-delegate',
    description: 'Generate delegation prompt and record explicit lineage intent',
};
function parseAndValidateArgs(parserConfig = BRIEF_PARSER_CONFIG) {
    const args = createWUParser({
        name: parserConfig.name,
        description: parserConfig.description,
        options: [
            WU_OPTIONS.id,
            WU_OPTIONS.thinking,
            WU_OPTIONS.noThinking,
            WU_OPTIONS.budget,
            WU_OPTIONS.codex,
            WU_OPTIONS.parentWu, // WU-1945: Parent WU for spawn registry tracking
            WU_OPTIONS.client,
            WU_OPTIONS.vendor,
            WU_OPTIONS.noContext, // WU-1240: Skip memory context injection
            WU_OPTIONS.evidenceOnly, // WU-2222: Evidence recording without prompt generation
            WU_OPTIONS.strictSizing, // WU-2141: Strict sizing enforcement
        ],
        required: ['id'],
        allowPositionalId: true,
    });
    // Validate thinking mode options
    try {
        validateSpawnArgs(args);
    }
    catch (e) {
        die(e.message);
    }
    return args;
}
// ─── WU Document Loading ───
/**
 * Load and validate WU document from YAML file
 */
function loadWUDocument(id, wuPath) {
    // Check if WU file exists
    if (!existsSync(wuPath)) {
        die(`WU file not found: ${wuPath}\n\n` +
            `Cannot spawn a sub-agent for a WU that doesn't exist.\n\n` +
            `Options:\n` +
            `  1. Create the WU first: pnpm wu:create --id ${id} --lane <lane> --title "..."\n` +
            `  2. Check if the WU ID is correct`);
    }
    // Read WU file
    let text;
    try {
        text = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
    }
    catch (e) {
        die(`Failed to read WU file: ${wuPath}\n\n` +
            `Error: ${e.message}\n\n` +
            `Options:\n` +
            `  1. Check file permissions: ls -la ${wuPath}\n` +
            `  2. Ensure the file exists and is readable`);
    }
    // Parse YAML
    try {
        return parseYAML(text);
    }
    catch (e) {
        die(`Failed to parse WU YAML ${wuPath}\n\n` +
            `Error: ${e.message}\n\n` +
            `Options:\n` +
            `  1. Validate YAML syntax: pnpm wu:validate --id ${id}\n` +
            `  2. Fix YAML errors manually and retry`);
    }
}
// ─── Client Resolution ───
/**
 * Resolve the client name from args and config
 */
function resolveClientName(args, config, logPrefix = BRIEF_LOG_PREFIX) {
    let clientName = args.client;
    if (!clientName && args.vendor) {
        console.warn(`${logPrefix} ${EMOJI.WARNING} Warning: --vendor is deprecated. Use --client.`);
        clientName = args.vendor;
    }
    // Codex handling (deprecated legacy flag)
    if (args.codex && !clientName) {
        console.warn(`${logPrefix} ${EMOJI.WARNING} Warning: --codex is deprecated. Use --client codex-cli.`);
        clientName = 'codex-cli';
    }
    const resolved = clientName || config.agents.defaultClient || 'claude-code';
    if (!clientName && !config.agents.defaultClient) {
        console.warn(`${logPrefix} Warning: No --client specified. Falling back to 'claude-code'. ` +
            `Available clients: claude-code, codex-cli, cursor, gemini-cli, windsurf`);
    }
    return resolved;
}
/**
 * Check lane occupation and warn if occupied by a different WU
 */
async function checkAndWarnLaneOccupation(lane, id, logPrefix = BRIEF_LOG_PREFIX) {
    if (!lane)
        return;
    const existingLock = checkLaneOccupation(lane);
    if (existingLock && existingLock.wuId !== id) {
        // Lane is occupied by a different WU
        const { isLockStale } = await import('@lumenflow/core/lane-lock');
        const isStale = isLockStale(existingLock);
        const warning = generateLaneOccupationWarning(existingLock, id, { isStale });
        console.warn(`${logPrefix} ${EMOJI.WARNING}\n${warning}\n`);
    }
}
/**
 * Record auditable wu:brief execution evidence in wu-events.jsonl.
 *
 * WU-2132: Completion lifecycle now requires proof that wu:brief was run.
 * WU-2144: Evidence recording is gated on worktree context. When run from
 * main checkout (e.g. orchestrator generating briefs), no state is mutated.
 */
export async function recordWuBriefEvidence(options, dependencies = {}) {
    const { wuId, workspaceRoot, clientName, evidenceMode, promptHash, forceRecord, claimedMode, claimedBranch, worktreePath, } = options;
    const checkWorktree = dependencies.isInWorktree ?? isInWorktreeDefault;
    const isInWorktree = checkWorktree({ cwd: workspaceRoot });
    let shouldRecordEvidence = forceRecord === true || isInWorktree;
    // WU-2508: Track effective root for state dir — overridden when recording
    // cross-checkout evidence for worktree-mode WUs.
    let effectiveStateRoot = workspaceRoot;
    if (!shouldRecordEvidence) {
        const normalizedClaimedMode = typeof claimedMode === 'string' ? claimedMode.trim() : '';
        // WU-2508: When claimedMode is 'worktree' but agent is running from main checkout,
        // resolve the worktree's state dir and record evidence there.
        if (normalizedClaimedMode === 'worktree' && worktreePath) {
            const absoluteWorktreePath = path.resolve(workspaceRoot, worktreePath);
            if (existsSync(absoluteWorktreePath)) {
                shouldRecordEvidence = true;
                effectiveStateRoot = absoluteWorktreePath;
            }
        }
        const isBranchClaim = normalizedClaimedMode === 'branch-pr' || normalizedClaimedMode === 'branch-only';
        if (!shouldRecordEvidence && isBranchClaim) {
            const getCurrentBranch = dependencies.getCurrentBranch ??
                (async (cwd) => {
                    return createGitForPath(cwd).getCurrentBranch();
                });
            const activeBranch = await getCurrentBranch(workspaceRoot)
                .then((branch) => branch.trim())
                .catch(() => '');
            const normalizedClaimedBranch = typeof claimedBranch === 'string' ? claimedBranch.trim() : '';
            if (normalizedClaimedBranch.length > 0) {
                shouldRecordEvidence = activeBranch === normalizedClaimedBranch;
            }
            else {
                shouldRecordEvidence =
                    activeBranch.length > 0 &&
                        activeBranch !== BRANCHES.MAIN &&
                        activeBranch !== BRANCHES.MASTER;
            }
        }
    }
    // WU-2144 + WU-2222 + WU-2508:
    // - worktree mode: record from worktree context OR cross-checkout via worktreePath
    // - branch-pr/branch-only mode: record from claimed branch context
    if (!shouldRecordEvidence) {
        return;
    }
    const stateDir = resolveStateDir(effectiveStateRoot);
    const createStore = dependencies.createStore ?? ((dir) => new WUStateStore(dir));
    const store = createStore(stateDir);
    const note = `${WU_BRIEF_EVIDENCE_NOTE_PREFIX} generated via ${clientName}`;
    const resolvedEvidenceMode = evidenceMode ??
        (clientName === 'wu:claim:auto' ? BRIEF_EVIDENCE_MODE_CLAIM_AUTO : BRIEF_EVIDENCE_MODE_FULL);
    const nextStepsParts = [`client=${clientName}`, `mode=${resolvedEvidenceMode}`];
    if (promptHash) {
        nextStepsParts.push(`hash=${promptHash}`);
    }
    await store.checkpoint(wuId, note, {
        progress: BRIEF_EVIDENCE_PROGRESS,
        nextSteps: nextStepsParts.join(';'),
    });
}
/**
 * Emit prompt output and optionally persist parent/child lineage.
 *
 * WU-1604: Prompt generation (wu:brief) is side-effect free.
 * Only explicit delegation mode records lineage intent.
 */
export async function emitSpawnOutputWithRegistry(options, dependencies = {}) {
    const { id, output, isCodexClient, parentWu, lane, briefAttestation, recordDelegationIntent = false, logPrefix: prefix = BRIEF_LOG_PREFIX, } = options;
    const log = dependencies.log ?? console.log;
    const recordSpawn = dependencies.recordSpawn ?? recordSpawnToRegistry;
    const formatSpawnMessage = dependencies.formatSpawnMessage ?? formatSpawnRecordedMessage;
    const createDelegationStore = dependencies.createDelegationStore ?? ((stateDir) => new WUStateStore(stateDir));
    const cwd = dependencies.cwd ?? process.cwd();
    // WU-2425: Write full output to .logs/ to survive Bash tool truncation.
    // Truncation cuts from the end, so the file path on line 1 always survives.
    const filePrefix = prefix.includes('delegate') ? 'wu-delegate' : 'wu-brief';
    const logsDir = path.join(cwd, '.logs');
    const outputFilePath = path.join(logsDir, `${filePrefix}-${id}.md`);
    mkdirSync(logsDir, { recursive: true });
    writeFileSync(outputFilePath, output, 'utf8');
    const lineCount = output.split('\n').length;
    log(`${prefix} Full output saved to: .logs/${filePrefix}-${id}.md (${lineCount} lines)`);
    log(`${prefix} If output below appears truncated, read the file above.`);
    if (isCodexClient) {
        log(`${prefix} Generated Codex/GPT prompt for ${id}`);
        log(`${prefix} Copy the Markdown below:\n`);
        log(output.trimEnd());
    }
    else {
        log(`${prefix} Generated Task tool invocation for ${id}`);
        log(`${prefix} Copy the block below to spawn a sub-agent:\n`);
        log(output);
    }
    if (!recordDelegationIntent || !parentWu) {
        return;
    }
    // WU-2557: honor the DI-captured `cwd` (line 503) instead of redundantly
    // calling process.cwd() here. Bypassing the DI contract caused silent audit
    // log contamination: tests and callers that injected a sandbox cwd were
    // overridden, and the delegation event landed in whichever .lumenflow/state/
    // vitest resolved from process.cwd() — typically the main checkout. Four
    // fake WU-1604 → WU-1600 delegation events leaked into committed history
    // before the pattern was detected (dlg-188e, dlg-fa8b, dlg-fd42, dlg-5e44).
    // See .lumenflow/rules/known-fake-events.md for the full scar.
    const stateDir = resolveStateDir(cwd);
    const delegationId = generateDelegationId(parentWu, id);
    try {
        await createDelegationStore(stateDir).delegate(id, parentWu, delegationId);
    }
    catch (error) {
        die(`${prefix} Failed to record durable delegation intent for ${id}: ${getErrorMessage(error)}\n\n` +
            `The wu-events delegation marker must be written before registry lineage is recorded.\n\n` +
            `Fix options:\n` +
            `  1. Ensure ${stateDir} is writable\n` +
            `  2. Retry: pnpm wu:delegate --id ${id} --parent-wu ${parentWu} --client <client>`);
    }
    const registryResult = await recordSpawn({
        parentWuId: parentWu,
        targetWuId: id,
        lane: lane || 'Unknown',
        baseDir: stateDir,
        briefAttestation,
        delegationId,
    });
    const registryMessage = formatSpawnMessage(registryResult.spawnId, registryResult.error);
    log(`\n${registryMessage}`);
}
/**
 * Shared entry point for wu:brief and wu:delegate.
 */
export async function runBriefLogic(options = {}) {
    const { mode = 'brief', parserConfig = mode === 'delegate' ? DELEGATE_PARSER_CONFIG : BRIEF_PARSER_CONFIG, logPrefix = BRIEF_LOG_PREFIX, } = options;
    const args = parseAndValidateArgs(parserConfig);
    const explicitDelegation = mode === 'delegate';
    const effectiveLogPrefix = explicitDelegation ? DELEGATE_LOG_PREFIX : logPrefix;
    // WU-2202: Validate dependencies BEFORE UnsafeAny other operation
    // This prevents false lane occupancy reports when yaml package is missing
    const commandLabel = explicitDelegation ? 'wu:delegate' : 'wu:brief';
    const depResult = await validateSpawnDependencies();
    if (!depResult.valid) {
        die(formatDependencyError(commandLabel, depResult.missing));
    }
    if (explicitDelegation && !args.parentWu) {
        die('wu:delegate requires --parent-wu to record delegation lineage intent.\n\n' +
            'Example:\n' +
            '  pnpm wu:delegate --id WU-123 --parent-wu WU-100 --client <client>');
    }
    // WU-2359: --evidence-only removed, but keep early exit for delegate conflict
    if (explicitDelegation && args.evidenceOnly) {
        die('--evidence-only has been removed (WU-2359).\n\n' +
            'Use:\n' +
            '  - pnpm wu:delegate --id WU-123 --parent-wu WU-100 --client <client>  # delegation prompt + lineage\n' +
            '  - pnpm wu:brief --id WU-123 --client <client>                         # handoff prompt + evidence');
    }
    if (!explicitDelegation && args.parentWu) {
        console.warn(`${effectiveLogPrefix} ${EMOJI.WARNING} --parent-wu does not record lineage in generation-only mode.`);
        console.warn(`${effectiveLogPrefix} ${EMOJI.WARNING} Use wu:delegate for explicit, side-effectful delegation intent tracking.`);
        console.warn('');
    }
    const id = args.id.toUpperCase();
    if (!PATTERNS.WU_ID.test(id)) {
        die(`Invalid WU id '${args.id}'. Expected format WU-123`);
    }
    const wuPath = WU_PATHS.WU(id);
    const doc = loadWUDocument(id, wuPath);
    // Warn if WU is not in ready or in_progress status
    const validStatuses = [WU_STATUS.READY, WU_STATUS.IN_PROGRESS];
    const status = doc.status || 'unknown';
    if (!validStatuses.includes(status)) {
        console.warn(`${effectiveLogPrefix} ${EMOJI.WARNING} Warning: ${id} has status '${status}'.`);
        console.warn(`${effectiveLogPrefix} ${EMOJI.WARNING} Sub-agents typically work on ready or in_progress WUs.`);
        console.warn('');
    }
    // WU-1603: Check if lane is already occupied and warn
    await checkAndWarnLaneOccupation(doc.lane, id, effectiveLogPrefix);
    // WU-2141: Check sizing compliance (advisory or strict)
    const sizingEstimate = doc.sizing_estimate;
    const sizingResult = checkBriefSizing({
        wuId: id,
        logPrefix: effectiveLogPrefix,
        strictSizing: Boolean(args.strictSizing),
        sizingEstimate,
    });
    if (!sizingResult.pass) {
        const errorList = sizingResult.errors.map((e) => `  - ${e}`).join('\n');
        die(`${effectiveLogPrefix} Sizing check failed (--strict-sizing):\n\n${errorList}\n\n` +
            `Options:\n` +
            `  1. Add sizing_estimate metadata to WU YAML\n` +
            `  2. Add exception_type and exception_reason if thresholds are intentionally exceeded\n` +
            `  3. Remove --strict-sizing to use advisory mode`);
    }
    // Build thinking mode options for task invocation
    const thinkingOptions = {
        thinking: args.thinking,
        noThinking: args.noThinking,
        budget: args.budget,
    };
    // Client Resolution
    const config = getConfig();
    const clientName = resolveClientName(args, config, effectiveLogPrefix);
    const baseDir = process.cwd();
    const recordEvidenceOrFail = async (options) => {
        try {
            await recordWuBriefEvidence({
                wuId: id,
                workspaceRoot: baseDir,
                clientName,
                evidenceMode: options.evidenceMode,
                promptHash: options.promptHash,
                forceRecord: options.forceRecord,
                claimedMode: typeof doc.claimed_mode === 'string' ? doc.claimed_mode : undefined,
                claimedBranch: typeof doc.claimed_branch === 'string' ? doc.claimed_branch : undefined,
                worktreePath: typeof doc.worktree_path === 'string' ? doc.worktree_path : undefined,
            });
        }
        catch (error) {
            die(`${effectiveLogPrefix} Failed to record wu:brief evidence for ${id}: ${error.message}\n\n` +
                `Fix options:\n` +
                `  1. Ensure state directory is writable\n` +
                `  2. Retry: pnpm wu:brief --id ${id}`);
        }
    };
    // WU-2359: --evidence-only removed. wu:brief always gives full context.
    if (args.evidenceOnly) {
        die(`--evidence-only has been removed (WU-2359).\n\n` +
            `wu:brief now always outputs full WU context AND records evidence.\n` +
            `There is no longer a need for a silent evidence-only mode.\n\n` +
            `Use instead:\n` +
            `  pnpm wu:brief --id ${id} --client <client>`);
    }
    // WU-1240: Generate memory context if not skipped
    let memoryContextContent = '';
    const shouldIncludeMemoryContext = !args.noContext;
    if (shouldIncludeMemoryContext) {
        const isMemoryInitialized = await checkMemoryLayerInitialized(baseDir);
        if (isMemoryInitialized) {
            const maxSize = getMemoryContextMaxSize(config);
            memoryContextContent = await generateMemoryContextSection(baseDir, {
                wuId: id,
                lane: doc.lane,
                maxSize,
            });
            if (memoryContextContent) {
                console.log(`${effectiveLogPrefix} Memory context loaded (${memoryContextContent.length} bytes)`);
            }
        }
    }
    // Create strategy
    const strategy = SpawnStrategyFactory.create(clientName);
    const clientContext = { name: clientName, config: resolveClientConfig(config, clientName) };
    const isCodexClient = clientName === 'codex-cli' || args.codex;
    if (isCodexClient) {
        const prompt = generateCodexPrompt(doc, id, strategy, {
            ...thinkingOptions,
            client: clientContext,
            config,
        });
        const briefAttestation = explicitDelegation
            ? buildBriefAttestation(prompt, clientName)
            : undefined;
        await emitSpawnOutputWithRegistry({
            id,
            output: prompt,
            isCodexClient: true,
            parentWu: args.parentWu,
            lane: doc.lane,
            briefAttestation,
            recordDelegationIntent: explicitDelegation,
            logPrefix: effectiveLogPrefix,
        });
        await recordEvidenceOrFail({
            evidenceMode: 'prompt',
            promptHash: briefAttestation?.promptHash,
            forceRecord: explicitDelegation,
        });
        return;
    }
    // Generate and output the Task invocation
    const invocation = generateTaskInvocation(doc, id, strategy, {
        ...thinkingOptions,
        client: clientContext,
        config,
        // WU-1240: Include memory context in spawn prompt
        baseDir,
        includeMemoryContext: shouldIncludeMemoryContext && memoryContextContent.length > 0,
        memoryContextContent,
        noContext: args.noContext,
    });
    const briefAttestation = explicitDelegation
        ? buildBriefAttestation(invocation, clientName)
        : undefined;
    await emitSpawnOutputWithRegistry({
        id,
        output: invocation,
        isCodexClient: false,
        parentWu: args.parentWu,
        lane: doc.lane,
        briefAttestation,
        recordDelegationIntent: explicitDelegation,
        logPrefix: effectiveLogPrefix,
    });
    await recordEvidenceOrFail({
        evidenceMode: 'prompt',
        promptHash: briefAttestation?.promptHash,
        forceRecord: explicitDelegation,
    });
}
//# sourceMappingURL=wu-spawn-strategy-resolver.js.map