import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ExecutionLogger } from "../logging/logger.js";
import { safeSliceHead } from "../utils/safe-slice.js";
import { hasStorageIntent } from "./storage-policy.js";
export const SKILL_CONTENT_LIMITS = {
    fileMaxChars: 128 * 1024,
    readMaxChars: 32 * 1024,
    autoLoadMaxSkills: 4,
    autoLoadPerSkillMaxChars: 12 * 1024,
    autoLoadTotalMaxChars: 24 * 1024,
    listMaxSkills: 200,
    listMaxChars: 16 * 1024,
};
const BUILTIN_SKILLS_DIR = fileURLToPath(new URL("../../builtin-skills/", import.meta.url));
const X_TASK_RE = /(?:https?:\/\/)?(?:www\.)?(?:x|twitter)\.com\b|\btwitter\b|\btweets?\b|\bretweets?\b|\bquote\s*tweet\b|tweetTextarea_|tweetButton|SearchBox_Search_Input|cellInnerDiv|推特|转推|quote\s*转发|X\.com|X上|X搜索|X账号|X主页|X\s+Web3/i;
const SOURCEPOOL_TASK_RE = /(?:https?:\/\/)?pool\.btcdev\.me\b|\bsourcepool\b|\bsource\s*pool\b|\bmaterial(?:s)?\s+library\b|素材库|素材池|素材检索|素材搜索/i;
const BUILTIN_SKILLS = [
    {
        name: "x",
        description: "X.com/Twitter browser automation operating notes.",
        filePath: path.join(BUILTIN_SKILLS_DIR, "x.md"),
        aliases: ["twitter", "x-com", "x_com"],
        matches: (text) => X_TASK_RE.test(text),
    },
    {
        name: "sourcepool",
        description: "Read materials (news articles, videos, social posts) from the sourcepool aggregated content library.",
        filePath: path.join(BUILTIN_SKILLS_DIR, "sourcepool.md"),
        aliases: ["source-pool", "source_pool", "materials", "material-library"],
        matches: (text) => SOURCEPOOL_TASK_RE.test(text),
    },
];
function safeSkillName(raw) {
    return raw.replace(/\.md$/, "").replace(/[^a-zA-Z0-9_-]/g, "_");
}
function isExplicitBuiltinRef(raw) {
    return raw.trim().toLowerCase().startsWith("builtin:");
}
function stripBuiltinPrefix(raw) {
    return raw.trim().replace(/^builtin:/i, "");
}
function truncateWithNote(content, maxChars, note, force = false) {
    if (!force && content.length <= maxChars) {
        return { content, truncated: false };
    }
    const suffix = `\n\n...[truncated: ${note}; original ${content.length} chars]`;
    const headBudget = Math.max(0, maxChars - suffix.length);
    return {
        content: `${safeSliceHead(content, headBudget)}${safeSliceHead(suffix, maxChars)}`,
        truncated: true,
    };
}
function parseFrontmatter(content) {
    const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
    if (!fmMatch)
        return {};
    const fields = {};
    for (const line of fmMatch[1].split("\n")) {
        const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
        if (match)
            fields[match[1]] = match[2].trim();
    }
    return fields;
}
function upsertFrontmatter(content, fields) {
    const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
    if (!fmMatch) {
        const fm = Object.entries(fields)
            .map(([k, v]) => `${k}: ${v}`)
            .join("\n");
        return `---\n${fm}\n---\n\n${content}`;
    }
    const existing = fmMatch[1];
    const additions = Object.entries(fields)
        .filter(([k]) => !new RegExp(`^${k}:`, "m").test(existing))
        .map(([k, v]) => `${k}: ${v}`)
        .join("\n");
    if (!additions)
        return content;
    return content.replace(/^---\n([\s\S]*?)\n---/, `---\n${existing}\n${additions}\n---`);
}
/**
 * Validate that `steps` is a non-empty array of {action: string, params: object, ...}.
 * Returns null on success, or a human-readable error string.
 *
 * Used at every skill load / save / install entry point to keep malformed
 * steps from reaching ScriptRunner (where `resolveVariables` on a missing
 * params object throws before step-level fallback can recover) AND to
 * disambiguate "this JSON array is actually steps" from "this JSON array
 * is just an example payload in an instruction skill" in extractStepsJson.
 */
export function validateSkillSteps(steps) {
    if (!Array.isArray(steps) || steps.length === 0) {
        return "'steps' must be a non-empty array.";
    }
    for (let i = 0; i < steps.length; i++) {
        const s = steps[i];
        if (!s || typeof s !== "object" || Array.isArray(s)) {
            return `step[${i}] must be an object.`;
        }
        const action = s.action;
        if (typeof action !== "string" || !action.trim()) {
            return `step[${i}].action must be a non-empty string.`;
        }
        const params = s.params;
        if (!params || typeof params !== "object" || Array.isArray(params)) {
            return `step[${i}].params must be an object.`;
        }
    }
    return null;
}
export function extractStepsJson(content) {
    const explicit = content.match(/## Steps\s*\n+```json\s*\n([\s\S]*?)\n```/i);
    if (explicit)
        return explicit[1];
    // Fallback for skills authored without an explicit `## Steps` header:
    // accept the first JSON array block that ALSO shape-checks as steps.
    // Without the shape check, any instruction skill embedding a JSON array
    // example (API responses, sample payloads, plain lists) gets misdetected
    // and fails installation with "Invalid skill steps: ..." instead of
    // landing in the instruction-skill path.
    const blocks = [...content.matchAll(/```json\s*\n([\s\S]*?)\n```/g)];
    for (const block of blocks) {
        try {
            const parsed = JSON.parse(block[1]);
            if (Array.isArray(parsed) && validateSkillSteps(parsed) === null) {
                return block[1];
            }
        }
        catch {
            // Ignore non-JSON or invalid JSON examples in instruction skills.
        }
    }
    return null;
}
export class SkillGenerator {
    skillsDir;
    llmClient;
    constructor(workspace, llmClient) {
        this.skillsDir = path.join(workspace, "skills");
        this.llmClient = llmClient;
        fs.mkdirSync(this.skillsDir, { recursive: true });
    }
    resolveBuiltinSkill(skillRef, opts = {}) {
        const explicit = isExplicitBuiltinRef(skillRef);
        if (!explicit && opts.allowUnqualified === false)
            return null;
        const safeName = safeSkillName(explicit ? stripBuiltinPrefix(skillRef) : skillRef);
        return (BUILTIN_SKILLS.find((s) => s.name === safeName || s.aliases.includes(safeName)) ?? null);
    }
    isBuiltinSkill(skillRef) {
        const builtin = this.resolveBuiltinSkill(skillRef, {
            allowUnqualified: this.resolveSkillPath(skillRef) === null,
        });
        return builtin !== null;
    }
    getAutoLoadedInstructionSkills(text) {
        let remainingTotal = SKILL_CONTENT_LIMITS.autoLoadTotalMaxChars;
        return BUILTIN_SKILLS.filter((skill) => skill.matches(text))
            .filter((skill) => fs.existsSync(skill.filePath))
            .slice(0, SKILL_CONTENT_LIMITS.autoLoadMaxSkills)
            .map((skill) => {
            const loaded = this.readSkillFileContent(skill.filePath, {
                maxChars: SKILL_CONTENT_LIMITS.autoLoadPerSkillMaxChars,
                truncate: true,
            });
            return loaded
                ? { skill, rawContent: loaded.content, wasTruncated: loaded.truncated }
                : null;
        })
            .filter((item) => item !== null)
            .map(({ skill, rawContent, wasTruncated }) => {
            const perSkillMax = Math.min(SKILL_CONTENT_LIMITS.autoLoadPerSkillMaxChars, Math.max(0, remainingTotal));
            const capped = truncateWithNote(rawContent, perSkillMax, `auto-loaded built-in skill '${skill.name}' exceeded prompt cap`);
            remainingTotal = Math.max(0, remainingTotal - capped.content.length);
            return {
                name: skill.name,
                description: skill.description,
                content: capped.content,
                builtin: true,
                readonly: true,
                truncated: wasTruncated || capped.truncated,
                ...(wasTruncated || capped.truncated
                    ? { originalChars: rawContent.length }
                    : {}),
            };
        });
    }
    renderLoadedInstructionSkillSection(skills) {
        if (skills.length === 0)
            return "";
        let remainingTotal = SKILL_CONTENT_LIMITS.autoLoadTotalMaxChars;
        return skills
            .slice(0, SKILL_CONTENT_LIMITS.autoLoadMaxSkills)
            .map((skill) => {
            const perSkillMax = Math.min(SKILL_CONTENT_LIMITS.autoLoadPerSkillMaxChars, Math.max(0, remainingTotal));
            const capped = truncateWithNote(skill.content, perSkillMax, `loaded instruction skill '${skill.name}' exceeded prompt cap`);
            remainingTotal = Math.max(0, remainingTotal - capped.content.length);
            const flags = skill.builtin ? " (built-in, read-only)" : "";
            return (`## Loaded Instruction Skill: ${skill.name}${flags}\n` +
                `${capped.content.trim()}\n`);
        })
            .join("\n");
    }
    renderAutoLoadedInstructionSkillSection(text) {
        return this.renderLoadedInstructionSkillSection(this.getAutoLoadedInstructionSkills(text));
    }
    /** Generate a skill from an execution log file */
    async generate(logFilePath) {
        const entries = ExecutionLogger.readLog(logFilePath);
        if (entries.length === 0) {
            return "Error: log file is empty.";
        }
        // Filter to actionable steps (exclude snapshots used for reading)
        const actionEntries = entries.filter((e) => e.success && e.action !== "snapshot" && e.action !== "screenshot");
        const logSummary = entries
            .map((e) => `Step ${e.step}: ${e.action} ${JSON.stringify(Object.fromEntries(Object.entries(e).filter(([k]) => !["step", "action", "timestamp", "success"].includes(k))))} (${e.success ? "ok" : "FAILED"})`)
            .join("\n");
        // Ask LLM to generate a structured skill
        const response = await this.llmClient.chat([
            {
                role: "user",
                content: `Based on this browser automation execution log, generate a reusable skill definition.

The skill should capture the essential steps that can be replayed later.

Selector picking:
- Element references like e1, e2 are EPHEMERAL — they will not exist on replay.
- Prefer stable CSS selectors in the "ref" field. The execution log will mark "selectorUsed" / "stableSelector" when one is known — copy that selector into the step.
- Order of preference: data-testid > unique #id > [name=] > [aria-label=] > [placeholder=] > role+text combinations.
- Examples: "[data-testid='submit']", "#search", "input[name='q']".

Verification (CRITICAL for submit/search/upload steps):
- For any action that is supposed to change page state (click on a Post/Send/Submit/Search button, type with submit:true, file upload), include an "expect" object in the params. Replay should fail loudly when the resulting state isn't visible.
- Examples:
  - Submit: { "action": "act", "params": { "kind": "click", "ref": "[data-testid='submit']", "expect": { "selectorGone": "[data-testid='submit']", "timeoutMs": 10000 } } }
  - Search: { "action": "act", "params": { "kind": "type", "ref": "input[name='q']", "text": "{{query}}", "submit": true, "expect": { "urlContains": "q=", "selectorVisible": "[data-testid='result']" } } }

For data-extraction steps prefer the high-level helpers over evaluate:
extractVisibleText / queryByText / findClickableNearText / extractLinks.

Execution log:
${logSummary}

Please respond with a JSON object in this exact format:
{
  "name": "skill-name",
  "description": "What this skill does",
  "steps": [
    { "action": "navigate", "params": { "url": "..." }, "description": "..." },
    { "action": "act", "params": { "kind": "click", "ref": "button[data-testid='submit']", "expect": { "urlContains": "..." } }, "description": "..." },
    { "action": "act", "params": { "kind": "type", "ref": "input.search-box", "text": "{{query}}" }, "description": "..." }
  ],
  "notes": ["any important notes about this skill"]
}

Use {{variable_name}} for parts that should be parameterized (e.g., search terms).
Only include the JSON, no other text.`,
            },
        ], undefined, "You are an expert at analyzing browser automation logs and creating reusable scripts. Output only valid JSON.");
        try {
            // Extract JSON from response
            const jsonMatch = response.content.match(/\{[\s\S]*\}/);
            if (!jsonMatch) {
                return "Error: LLM did not return valid JSON.";
            }
            const skill = JSON.parse(jsonMatch[0]);
            return this.saveSkill(skill);
        }
        catch (err) {
            return `Error parsing skill: ${err instanceof Error ? err.message : String(err)}`;
        }
    }
    /** Save a skill to a markdown file with metadata */
    saveSkill(skill, meta) {
        if (isExplicitBuiltinRef(skill.name)) {
            return `Error: built-in skill '${stripBuiltinPrefix(skill.name)}' is read-only and cannot be overwritten.`;
        }
        const safeName = safeSkillName(skill.name);
        const filePath = path.join(this.skillsDir, `${safeName}.md`);
        const stepsJson = JSON.stringify(skill.steps, null, 2);
        const notes = skill.notes?.map((n) => `- ${n}`).join("\n") || "";
        const usageCount = meta?.usageCount ?? 0;
        const successRate = meta?.successRate ?? 100;
        const lastImproved = meta?.lastImproved ?? "";
        const content = `---
name: ${skill.name}
description: ${skill.description}
created: ${new Date().toISOString()}
usageCount: ${usageCount}
successRate: ${successRate}
lastImproved: ${lastImproved}
---

## Steps

\`\`\`json
${stepsJson}
\`\`\`

## Notes
${notes}
`;
        if (content.length > SKILL_CONTENT_LIMITS.fileMaxChars) {
            return `Error: skill '${safeName}' is too large (${content.length} chars; max ${SKILL_CONTENT_LIMITS.fileMaxChars}).`;
        }
        fs.writeFileSync(filePath, content, "utf-8");
        return `Skill saved: ${filePath}`;
    }
    /** Save a markdown instruction skill exactly as authored, adding dipclaw metadata if missing. */
    saveInstructionSkill(name, content, meta) {
        if (isExplicitBuiltinRef(name)) {
            return `Error: built-in skill '${stripBuiltinPrefix(name)}' is read-only and cannot be overwritten.`;
        }
        const safeName = safeSkillName(name);
        if (content.length > SKILL_CONTENT_LIMITS.fileMaxChars) {
            return `Error: instruction skill '${safeName}' is too large (${content.length} chars; max ${SKILL_CONTENT_LIMITS.fileMaxChars}).`;
        }
        const filePath = path.join(this.skillsDir, `${safeName}.md`);
        const fm = parseFrontmatter(content);
        const enriched = upsertFrontmatter(content, {
            name: fm.name || safeName,
            description: fm.description || "(instruction skill)",
            type: "instruction",
            created: new Date().toISOString(),
            usageCount: String(meta?.usageCount ?? 0),
            successRate: String(meta?.successRate ?? 100),
            lastImproved: meta?.lastImproved ?? "",
        });
        if (enriched.length > SKILL_CONTENT_LIMITS.fileMaxChars) {
            return `Error: instruction skill '${safeName}' is too large (${enriched.length} chars; max ${SKILL_CONTENT_LIMITS.fileMaxChars}).`;
        }
        fs.writeFileSync(filePath, enriched, "utf-8");
        return `Instruction skill saved: ${filePath}`;
    }
    /**
     * Resolve a skill reference to an absolute file path, or null if not found.
     * Strips a trailing ".md" before sanitizing so callers can pass either
     * "foo" or "foo.md". Falls back to the raw ref iff it's a safe single-
     * segment basename — no path traversal.
     */
    resolveSkillPath(skillRef) {
        if (isExplicitBuiltinRef(skillRef))
            return null;
        const safeName = safeSkillName(skillRef);
        const primary = path.join(this.skillsDir, `${safeName}.md`);
        if (fs.existsSync(primary))
            return primary;
        if (/^[A-Za-z0-9_.-]+\.md$/.test(skillRef)) {
            const fallback = path.join(this.skillsDir, skillRef);
            if (fs.existsSync(fallback))
                return fallback;
        }
        return null;
    }
    /** True if a user-installed workspace skill exists for this reference/name. */
    workspaceSkillExists(skillRef) {
        return this.resolveSkillPath(skillRef) !== null;
    }
    resolveSkillFile(skillRef) {
        const explicitBuiltin = this.resolveBuiltinSkill(skillRef, {
            allowUnqualified: false,
        });
        if (explicitBuiltin) {
            return { filePath: explicitBuiltin.filePath, builtin: explicitBuiltin };
        }
        const workspacePath = this.resolveSkillPath(skillRef);
        if (workspacePath)
            return { filePath: workspacePath, builtin: null };
        const builtin = this.resolveBuiltinSkill(skillRef);
        if (builtin)
            return { filePath: builtin.filePath, builtin };
        return null;
    }
    readSkillFileContent(filePath, opts = {}) {
        if (!fs.existsSync(filePath))
            return null;
        const maxChars = opts.maxChars ?? SKILL_CONTENT_LIMITS.fileMaxChars;
        const stat = fs.statSync(filePath);
        const maxPrefixBytes = Math.max(1, maxChars * 4);
        if (stat.size > maxPrefixBytes) {
            if (!opts.truncate)
                return null;
            const fd = fs.openSync(filePath, "r");
            try {
                const buf = Buffer.allocUnsafe(maxPrefixBytes);
                const bytesRead = fs.readSync(fd, buf, 0, maxPrefixBytes, 0);
                const prefix = buf.subarray(0, bytesRead).toString("utf-8");
                return truncateWithNote(prefix, maxChars, `skill file exceeded ${maxChars} char read cap; original ${stat.size} bytes`, true);
            }
            finally {
                fs.closeSync(fd);
            }
        }
        const raw = fs.readFileSync(filePath, "utf-8");
        if (raw.length <= maxChars)
            return { content: raw, truncated: false };
        if (opts.truncate) {
            return truncateWithNote(raw, maxChars, `skill file exceeded ${maxChars} char read cap`);
        }
        return null;
    }
    /** True if a skill file already exists for this reference/name. */
    skillExists(skillRef) {
        return this.resolveSkillFile(skillRef) !== null;
    }
    /** Load a skill from a markdown file */
    loadSkill(skillRef) {
        const target = this.resolveSkillFile(skillRef);
        if (!target)
            return null;
        const loaded = this.readSkillFileContent(target.filePath);
        if (!loaded)
            return null;
        const content = loaded.content;
        // Parse frontmatter
        const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
        const name = fmMatch?.[1].match(/name:\s*(.+)/)?.[1] || skillRef;
        const description = fmMatch?.[1].match(/description:\s*(.+)/)?.[1] || "";
        // Parse steps JSON
        const stepsJson = extractStepsJson(content);
        if (!stepsJson)
            return null;
        try {
            const steps = JSON.parse(stepsJson);
            // Parse notes
            const notesSection = content.split("## Notes")[1];
            const notes = notesSection
                ? notesSection
                    .trim()
                    .split("\n")
                    .filter((l) => l.startsWith("- "))
                    .map((l) => l.slice(2))
                : [];
            return { name, description, steps, notes };
        }
        catch {
            return null;
        }
    }
    /** Read capped markdown content of a skill file, or null if missing. */
    readSkillFile(skillRef) {
        const target = this.resolveSkillFile(skillRef);
        if (!target)
            return null;
        return this.readSkillFileContent(target.filePath, {
            maxChars: SKILL_CONTENT_LIMITS.readMaxChars,
            truncate: true,
        })?.content ?? null;
    }
    /** Return lightweight metadata for both executable scripts and instruction skills. */
    getSkillInfo(skillRef) {
        const target = this.resolveSkillFile(skillRef);
        if (!target)
            return null;
        const loaded = this.readSkillFileContent(target.filePath);
        if (!loaded) {
            return {
                name: path.basename(target.filePath, ".md"),
                description: "(skill file exceeds load limit)",
                kind: "invalid",
                ...(target.builtin ? { builtin: true, readonly: true } : {}),
            };
        }
        const content = loaded.content;
        const fallbackName = path.basename(target.filePath, ".md");
        const fm = parseFrontmatter(content);
        const name = fm.name || fallbackName;
        const description = fm.description || "";
        const stepsJson = extractStepsJson(content);
        if (stepsJson) {
            try {
                const steps = JSON.parse(stepsJson);
                if (Array.isArray(steps)) {
                    return {
                        name,
                        description,
                        kind: "script",
                        stepsCount: steps.length,
                        ...(target.builtin ? { builtin: true, readonly: true } : {}),
                    };
                }
                return {
                    name,
                    description,
                    kind: "invalid",
                    ...(target.builtin ? { builtin: true, readonly: true } : {}),
                };
            }
            catch {
                return {
                    name,
                    description,
                    kind: "invalid",
                    ...(target.builtin ? { builtin: true, readonly: true } : {}),
                };
            }
        }
        if (Object.keys(fm).length > 0) {
            return {
                name,
                description,
                kind: "instruction",
                ...(target.builtin ? { builtin: true, readonly: true } : {}),
            };
        }
        return {
            name,
            description,
            kind: "invalid",
            ...(target.builtin ? { builtin: true, readonly: true } : {}),
        };
    }
    /** Delete a skill file. Returns true if removed, false if not found. */
    deleteSkill(skillRef) {
        const filePath = this.resolveSkillPath(skillRef);
        if (!filePath)
            return false;
        fs.unlinkSync(filePath);
        return true;
    }
    /** List all available skills */
    listSkills() {
        const workspaceFiles = fs
            .readdirSync(this.skillsDir)
            .filter((f) => f.endsWith(".md"));
        const files = [
            ...BUILTIN_SKILLS.map((s) => `builtin:${s.name}`),
            ...workspaceFiles,
        ];
        if (files.length === 0)
            return "(no skills available)";
        const lines = files
            .slice(0, SKILL_CONTENT_LIMITS.listMaxSkills)
            .map((f) => {
            const skill = this.getSkillInfo(f);
            if (!skill || skill.kind === "invalid")
                return `- ${f} (invalid)`;
            const meta = this.loadSkillMeta(f);
            const stats = meta.usageCount > 0
                ? ` [used ${meta.usageCount}x, ${meta.successRate}% ok]`
                : "";
            const kind = skill.kind === "script"
                ? `${skill.stepsCount ?? 0} steps`
                : "instruction";
            const flags = skill.builtin ? " [built-in, read-only]" : "";
            const displayName = skill.builtin ? `builtin:${skill.name}` : skill.name;
            return `- ${displayName}: ${skill.description} (${kind})${flags}${stats}`;
        })
            .join("\n");
        const capped = truncateWithNote(lines, SKILL_CONTENT_LIMITS.listMaxChars, "skill list exceeded display cap").content;
        if (files.length > SKILL_CONTENT_LIMITS.listMaxSkills) {
            return `${capped}\n... (${files.length - SKILL_CONTENT_LIMITS.listMaxSkills} more skills omitted)`;
        }
        return capped;
    }
    /** Load metadata from a skill file */
    loadSkillMeta(skillRef) {
        const defaults = { usageCount: 0, successRate: 100, lastImproved: "" };
        const target = this.resolveSkillFile(skillRef);
        if (!target)
            return defaults;
        const loaded = this.readSkillFileContent(target.filePath);
        if (!loaded)
            return defaults;
        const content = loaded.content;
        const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
        if (!fmMatch)
            return defaults;
        const fm = fmMatch[1];
        return {
            usageCount: Number(fm.match(/usageCount:\s*(\d+)/)?.[1] ?? 0),
            successRate: Number(fm.match(/successRate:\s*([\d.]+)/)?.[1] ?? 100),
            lastImproved: fm.match(/lastImproved:\s*(.+)/)?.[1]?.trim() ?? "",
        };
    }
    /** Update usage stats for a skill after execution */
    updateSkillMeta(skillRef, success) {
        const target = this.resolveSkillFile(skillRef);
        if (!target || target.builtin)
            return;
        const skill = this.loadSkill(skillRef);
        if (!skill)
            return;
        const meta = this.loadSkillMeta(skillRef);
        meta.usageCount++;
        const total = meta.usageCount;
        // Rolling success rate
        meta.successRate = Math.round(((meta.successRate * (total - 1)) / 100 + (success ? 1 : 0)) / total * 100);
        this.saveSkill(skill, meta);
    }
    /**
     * Determine if an execution log is worth auto-generating a skill from.
     * Criteria: enough meaningful browser actions, all successful, not trivially short.
     */
    shouldAutoGenerate(logFilePath) {
        const entries = ExecutionLogger.readLog(logFilePath);
        if (entries.length === 0)
            return false;
        // Refuse to auto-generate skills for tasks that depend on structured
        // storage. The current script schema is linear — no branching, no
        // page-state variable extraction — which means a generated skill can
        // write store entries after success but cannot reliably run the required
        // read/compare step before acting on a candidate. Until the schema gains
        // guards + variables, keep these tasks LLM-driven.
        const taskStart = entries.find((e) => e.action === "task_start");
        const prompt = typeof taskStart?.prompt === "string" ? taskStart.prompt : "";
        const storagePolicies = Array.isArray(taskStart?.storagePolicies)
            ? taskStart.storagePolicies
            : [];
        if (storagePolicies.length > 0 || SkillGenerator.isStorageSensitivePrompt(prompt)) {
            console.log(`  Skipping skill auto-gen: task depends on structured storage. Linear scripts cannot safely enforce storage policies yet.`);
            return false;
        }
        const actionEntries = entries.filter((e) => e.action !== "snapshot" && e.action !== "screenshot" && e.action !== "script_start" && e.action !== "script_end");
        // Need at least 3 meaningful actions
        if (actionEntries.length < 3)
            return false;
        // Must have a navigate action (needs a real workflow, not just clicks)
        const hasNavigate = actionEntries.some((e) => e.action === "navigate");
        if (!hasNavigate)
            return false;
        // Failure rate must be under 30%
        const failures = actionEntries.filter((e) => !e.success);
        if (failures.length / actionEntries.length > 0.3)
            return false;
        return true;
    }
    /**
     * Heuristic: does the task prompt request structured cross-run state?
     * If so, a linear script can't enforce that correctly (see comment in
     * shouldAutoGenerate). Match is intentionally broad — false positives
     * (keep something LLM-driven that could have been a script) are cheap;
     * false negatives can produce visible repeated or stale work.
     *
     * Exported as a static helper so tests / future call sites can reuse it
     * without instantiating SkillGenerator.
     */
    static isStorageSensitivePrompt(prompt) {
        return hasStorageIntent(prompt);
    }
    /**
     * Improve a skill based on its execution report.
     * When steps used LLM fallback, ask the LLM to revise those steps.
     */
    async improveSkill(skill, report) {
        // Only improve if there were fallbacks but the skill overall completed
        if (report.fallbackCount === 0 || !report.completed)
            return null;
        const fallbackSteps = report.steps
            .filter((s) => s.usedFallback)
            .map((s) => ({
            index: s.index,
            originalAction: skill.steps[s.index]?.action,
            originalParams: skill.steps[s.index]?.params,
            originalDescription: skill.steps[s.index]?.description,
            fallbackAction: s.fallbackAction,
            error: s.error,
        }));
        const response = await this.llmClient.chat([
            {
                role: "user",
                content: `A browser automation skill "${skill.name}" was executed but some steps needed LLM fallback because the original steps failed. Please revise the skill to fix these problematic steps.

Current skill steps:
${JSON.stringify(skill.steps, null, 2)}

Steps that needed fallback:
${JSON.stringify(fallbackSteps, null, 2)}

Return the COMPLETE revised steps array as JSON. Keep steps that worked unchanged. Fix the steps that failed based on what the fallback actually did. Use {{variable_name}} for parameterized parts.

Return ONLY the JSON array of steps, no other text.`,
            },
        ], undefined, "You are an expert at improving browser automation scripts. Output only valid JSON.");
        try {
            const jsonMatch = response.content.match(/\[[\s\S]*\]/);
            if (!jsonMatch)
                return null;
            const improvedSteps = JSON.parse(jsonMatch[0]);
            // Update the skill
            const improvedSkill = {
                ...skill,
                steps: improvedSteps,
                notes: [
                    ...(skill.notes || []),
                    `Auto-improved on ${new Date().toISOString().slice(0, 10)}: fixed ${fallbackSteps.length} step(s)`,
                ],
            };
            const meta = this.loadSkillMeta(skill.name);
            meta.lastImproved = new Date().toISOString();
            const result = this.saveSkill(improvedSkill, meta);
            console.log(`🧠 Skill "${skill.name}" auto-improved: ${fallbackSteps.length} step(s) revised`);
            return result;
        }
        catch (err) {
            console.error("Failed to improve skill:", err);
            return null;
        }
    }
}
//# sourceMappingURL=skill-generator.js.map