/**
 * Skill Learning Gate -- processes pending built-in skill updates in a focused
 * agent pass, isolated from normal message-handling.
 *
 * When a skill has been locally modified the auto-updater cannot safely
 * overwrite it. Instead, it writes all new skill files to a snapshot
 * directory (`pending-skill-updates/{name}/`). On the next wake cycle,
 * if any such snapshots exist, the agent enters Learning Mode:
 *
 *  1. The main loop sets `agentState.learning = true` and calls this gate.
 *     No proactive owner notification is sent on gate entry; if the user
 *     sends a message during the learning session, the InterruptHandler
 *     (seeing `agentState.learning`) acknowledges via the fast responder
 *     and requeues the message instead of injecting it.
 *  2. Run a single focused Claude session that reviews one pending update,
 *     merges it into the installed skill, and deletes the snapshot when done.
 *  3. Return so the main loop can resume normal message handling.
 *     If more skills are pending, they will be processed in subsequent cycles
 *     (one skill per wake cycle, to keep the context focused).
 */
import { getPendingSkillUpdates, readPendingSkillUpdate, readPendingSourceHash, clearPendingSkillUpdate, writeLocalMeta, } from "../skills/install.js";
import { getSkillsDir, getConfigDir } from "../config/index.js";
import { logger } from "../logger.js";
import path from "path";
import fs from "node:fs";
/** Recursively list all files under a directory, returning relative paths. */
function listFilesRecursive(dir, prefix) {
    const results = [];
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
        if (entry.isDirectory()) {
            results.push(...listFilesRecursive(path.join(dir, entry.name), rel));
        }
        else {
            results.push(rel);
        }
    }
    return results;
}
/**
 * Maximum number of consecutive wake cycles the learning gate will attempt
 * to learn a single skill before giving up and clearing it.
 */
const MAX_LEARNING_ATTEMPTS = 3;
/** Per-skill attempt counter: skillName -> attempt count. */
const learningAttempts = new Map();
/**
 * Check whether Learning Mode should run. Returns true if there are pending
 * skill updates with remaining attempts, false otherwise.
 *
 * Skills that have exceeded the attempt limit are individually cleared so
 * other pending skills can still be learned. Cleared snapshots will be
 * re-created on the next restart by `installBuiltinSkills()`.
 */
export function hasPendingSkillLearning() {
    const pending = getPendingSkillUpdates();
    if (pending.length === 0) {
        learningAttempts.clear();
        return false;
    }
    // Clear any skills that have exceeded the attempt limit
    for (const name of pending) {
        const attempts = learningAttempts.get(name) ?? 0;
        if (attempts >= MAX_LEARNING_ATTEMPTS) {
            logger.warn(`Skill Learning Mode: giving up on "${name}" after ${MAX_LEARNING_ATTEMPTS} attempts. ` +
                `It will be re-queued on the next restart.`);
            clearPendingSkillUpdate(name);
            learningAttempts.delete(name);
        }
    }
    // Re-check after clearing exhausted skills
    return getPendingSkillUpdates().length > 0;
}
/**
 * Build the Learning Mode content blocks for a single skill.
 * These replace the normal wake context for the learning cycle.
 */
function buildLearningContext(skillName) {
    const skillsDir = getSkillsDir();
    const pendingDir = path.join(getConfigDir(), "pending-skill-updates");
    const now = new Date().toLocaleString();
    const lines = [];
    lines.push(`[Wake Event] trigger=learning time=${now}`);
    lines.push("");
    lines.push("## Skill Learning Mode");
    lines.push(`You are in **Learning Mode**. The built-in skill **"${skillName}"** has an upstream ` +
        `update that could not be auto-applied because you have made local modifications to it. ` +
        `Your task is to merge the update into your installed skill, preserving your customisations ` +
        `while incorporating the new upstream content.`);
    lines.push("");
    lines.push(`Compare your installed version against the new upstream version, ` +
        `merge the changes intelligently (keep your customisations, add new upstream content), ` +
        `write the merged result back to your installed skill directory, and then delete the ` +
        `pending snapshot directory when done.`);
    lines.push("");
    lines.push(`Installed skills directory: \`${skillsDir}\``);
    lines.push(`Pending update snapshots directory: \`${pendingDir}\``);
    lines.push("");
    lines.push(`---`);
    lines.push(`### Skill: ${skillName}`);
    lines.push("");
    const installedDir = path.join(skillsDir, skillName);
    const snapshotFiles = readPendingSkillUpdate(skillName);
    // List installed files (recursively, skip dotfiles like .skill-local)
    if (fs.existsSync(installedDir)) {
        const installedFiles = listFilesRecursive(installedDir, "").filter((f) => !f.startsWith("."));
        lines.push(`**Installed files:** ${installedFiles.join(", ") || "(none)"}`);
        lines.push(`**Installed path:** \`${installedDir}\``);
    }
    else {
        lines.push(`**Installed path:** \`${installedDir}\` (not yet installed)`);
    }
    lines.push("");
    // Show new upstream files
    if (snapshotFiles && snapshotFiles.size > 0) {
        lines.push(`**New upstream files (${snapshotFiles.size}):** ${[...snapshotFiles.keys()].join(", ")}`);
        lines.push(`**Snapshot path:** \`${path.join(pendingDir, skillName)}\``);
    }
    else {
        lines.push(`**New upstream files:** (snapshot empty -- delete pending directory and skip)`);
    }
    lines.push("");
    lines.push("---");
    lines.push("");
    lines.push("After merging this skill, use the `finish` tool to complete Learning Mode. " +
        "Do NOT process other messages or tasks during this cycle -- focus only on the skill merge above.");
    return [{ type: "text", text: lines.join("\n") }];
}
/**
 * Run the Skill Learning Gate.
 *
 * Processes exactly **one** pending skill update per invocation. The main
 * loop calls this gate repeatedly (once per wake cycle) until all pending
 * updates are consumed. This one-at-a-time approach keeps the agent
 * context focused and avoids confusion from merging multiple skills
 * simultaneously.
 *
 * @param cycleRunner   The wake-cycle runner used to execute the learning session
 */
export async function runSkillLearningGate(cycleRunner) {
    // Discard any incomplete snapshots (missing .update-meta) before proceeding.
    // They will be re-created on the next restart by installBuiltinSkills().
    for (const name of getPendingSkillUpdates()) {
        if (!readPendingSourceHash(name)) {
            clearPendingSkillUpdate(name);
            logger.warn(`Discarded incomplete pending skill update for "${name}" (missing .update-meta)`);
        }
    }
    const pendingNames = getPendingSkillUpdates();
    if (pendingNames.length === 0)
        return;
    // Pick the first pending skill to learn this cycle
    const skillName = pendingNames[0];
    const attempts = (learningAttempts.get(skillName) ?? 0) + 1;
    learningAttempts.set(skillName, attempts);
    const remainingCount = pendingNames.length - 1;
    logger.system(`Skill Learning Mode: learning "${skillName}" (attempt ${attempts}/${MAX_LEARNING_ATTEMPTS})` +
        (remainingCount > 0 ? `, ${remainingCount} more pending` : ""));
    // Build focused learning context for this single skill
    const contentBlocks = buildLearningContext(skillName);
    // Capture the source hash before the session runs, because the agent
    // will delete the snapshot directory (including .update-meta) on success.
    const sourceHash = readPendingSourceHash(skillName);
    // Run the dedicated learning session (no messages, heartbeat-style trigger)
    try {
        await cycleRunner.run(contentBlocks, "heartbeat", [], "", 0);
        // Check if the agent successfully merged this skill (snapshot deleted)
        const stillPending = new Set(getPendingSkillUpdates());
        if (!stillPending.has(skillName)) {
            // Success -- update .skill-local so installBuiltinSkills() knows
            // this source version has been learned.
            if (sourceHash) {
                const skillTarget = path.join(getSkillsDir(), skillName);
                if (fs.existsSync(skillTarget)) {
                    writeLocalMeta(skillTarget, sourceHash);
                    logger.debug(`Updated local metadata for merged skill: ${skillName}`);
                }
            }
            learningAttempts.delete(skillName);
            logger.system(`Skill Learning Mode: successfully merged "${skillName}".`);
        }
        else {
            logger.warn(`Skill Learning Mode: agent did not complete merge for "${skillName}".`);
        }
    }
    catch (err) {
        logger.warn(`Skill learning session error for "${skillName}": ${err instanceof Error ? err.message : String(err)}`);
        // On error, don't clear the snapshot -- hasPendingSkillLearning() will
        // retry up to MAX_LEARNING_ATTEMPTS and then clear it.
    }
}
//# sourceMappingURL=skill-learning-gate.js.map