/**
 * src/agent/skills.ts
 * نظام تحميل المهارات — Skill loader for tenant workspace SKILLS/ folder
 */
import { promises as fs } from 'fs';
import { dirname, join, resolve } from 'path';
import { fileURLToPath } from 'url';
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
import { getDataDir } from '../utils/data-dir.js';
// Universal skills shipped with the gateway (workspace-template/SKILLS/*.md).
// Loaded as baseline when a tenant has no per-tenant skills yet, so every new
// business gets greeting / complaint / escalate / follow-up / answer-inquiry /
// zatca-invoice from day zero. Per-tenant skills override or extend these.
const __dirname = dirname(fileURLToPath(import.meta.url));
const UNIVERSAL_SKILLS_DIR = resolve(__dirname, '../../workspace-template/SKILLS');
// User-installed skills live under <dataDir>/skills/. Populated by
// `muassis skill add <url>` or by the chat-side URL preprocessor.
// Loaded after universal so they can override universal skills by name.
function getUserSkillsDir() {
    return join(getDataDir(), 'skills');
}
const log = createChildLogger('agent:skills');
// ─── Skill File Parser ──────────────────────────────────────────────
const SECTION_PATTERNS = {
    name: /^#\s+(.+)$/m,
    whenToUse: /^##\s*(?:متى تستخدم|when[_ ]to[_ ]use|الاستخدام)\s*\n([\s\S]*?)(?=^##|\z)/mi,
    instructions: /^##\s*(?:التعليمات|instructions|خطوات)\s*\n([\s\S]*?)(?=^##|\z)/mi,
    examples: /^##\s*(?:أمثلة|examples|نماذج)\s*\n([\s\S]*?)(?=^##|\z)/mi,
    keywords: /^##\s*(?:كلمات مفتاحية|keywords|الكلمات)\s*\n([\s\S]*?)(?=^##|\z)/mi,
};
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
function parseFrontmatter(content) {
    const match = content.match(FRONTMATTER_RE);
    if (!match)
        return { meta: {}, body: content };
    const meta = {};
    for (const line of match[1].split('\n')) {
        // Skip comments and obvious garbage lines
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('#'))
            continue;
        const idx = line.indexOf(':');
        if (idx <= 0)
            continue;
        const key = line.slice(0, idx).trim().toLowerCase();
        const val = line.slice(idx + 1).trim();
        // (Round-7 fix) Only assign non-empty values. Empty `priority:` would
        // overwrite the default '5' fallback and parseInt('') → NaN, which
        // breaks skill-match sorting.
        if (!key || !val)
            continue;
        meta[key] = val;
    }
    return { meta, body: content.slice(match[0].length) };
}
export function parseSkillFile(content) {
    const { meta, body } = parseFrontmatter(content);
    const nameMatch = body.match(SECTION_PATTERNS.name);
    const name = meta['name'] || meta['الاسم'] || nameMatch?.[1]?.trim() || 'unnamed';
    const whenToUse = meta['when_to_use'] || meta['متى_تستخدم'] ||
        extractSection(body, SECTION_PATTERNS.whenToUse) || '';
    const instructions = extractSection(body, SECTION_PATTERNS.instructions) ||
        // Fallback: everything after the first heading
        body.replace(/^#\s+.+\n/, '').trim();
    const examplesRaw = extractSection(body, SECTION_PATTERNS.examples) || '';
    const examples = examplesRaw
        .split('\n')
        .map(l => l.replace(/^[-*]\s*/, '').trim())
        .filter(Boolean);
    const keywordsRaw = meta['keywords'] || meta['كلمات'] ||
        extractSection(body, SECTION_PATTERNS.keywords) || '';
    const keywords = keywordsRaw
        .split(/[,،\n]/)
        .map(k => k.replace(/^[-*]\s*/, '').trim())
        .filter(Boolean);
    // (Round-7 fix) Clamp priority to [1, 10] and fall back to 5 on NaN/garbage.
    // Without this, parseInt('abc') → NaN poisons sort comparisons (skills
    // ranked at random) and parseInt('99') → 99 starves all other skills.
    const rawPriority = parseInt(meta['priority'] || meta['أولوية'] || '5', 10);
    const priority = Number.isFinite(rawPriority) ? Math.max(1, Math.min(10, rawPriority)) : 5;
    return { name, whenToUse, instructions, examples, keywords, priority, raw: content };
}
function extractSection(body, pattern) {
    // Make the pattern work with end-of-string too
    const adjusted = new RegExp(pattern.source.replace('\\z', '$'), pattern.flags);
    const match = body.match(adjusted);
    return match?.[1]?.trim() || null;
}
// ─── Skill Loader ───────────────────────────────────────────────────
// Cache so we don't re-read the same .md files on every message.
// (Audit H9) Cache is invalidated when a skill file is added/edited via
// `muassis skill add`, `skill_install_from_url` tool, or via fs watcher.
// Otherwise, agent wouldn't see new disk skills until gateway restart.
let universalSkillsCache = null;
let userSkillsCache = null;
const kgMergeCache = new Map();
const KG_MERGE_TTL_MS = 60_000;
/** Force the next loadSkills() call to re-read disk. Call this whenever
 * a skill file is created/updated/deleted. */
export function invalidateSkillsCache(reason) {
    universalSkillsCache = null;
    userSkillsCache = null;
    kgMergeCache.clear();
    if (reason)
        log.debug({ reason }, 'skills cache invalidated');
}
async function loadUniversalSkillsFromDisk() {
    if (universalSkillsCache)
        return universalSkillsCache;
    try {
        const entries = await fs.readdir(UNIVERSAL_SKILLS_DIR, { withFileTypes: true });
        const skills = [];
        for (const entry of entries) {
            if (!entry.isFile() || !entry.name.endsWith('.md'))
                continue;
            try {
                const content = await fs.readFile(resolve(UNIVERSAL_SKILLS_DIR, entry.name), 'utf-8');
                skills.push(parseSkillFile(content));
            }
            catch (err) {
                log.warn({ file: entry.name, err: err instanceof Error ? err.message : String(err) }, 'Failed to parse universal skill');
            }
        }
        universalSkillsCache = skills;
        log.info({ count: skills.length, dir: UNIVERSAL_SKILLS_DIR }, 'Universal skills loaded from disk');
        return skills;
    }
    catch (err) {
        log.warn({ err: err instanceof Error ? err.message : String(err), dir: UNIVERSAL_SKILLS_DIR }, 'No universal skills directory');
        universalSkillsCache = [];
        return [];
    }
}
/**
 * Skills the user has installed on this device — markdown files dropped
 * into <dataDir>/skills/. Cleared on each call (no caching) so newly
 * pasted-from-chat skills show up the next time loadSkills() runs.
 */
async function loadUserSkillsFromDisk() {
    if (userSkillsCache)
        return userSkillsCache;
    const dir = getUserSkillsDir();
    let entries;
    try {
        entries = await fs.readdir(dir, { withFileTypes: true });
    }
    catch (err) {
        if (err.code === 'ENOENT') {
            userSkillsCache = [];
            return userSkillsCache;
        }
        log.warn({ err: err instanceof Error ? err.message : String(err), dir }, 'Failed to read user skills dir');
        return [];
    }
    const skills = [];
    for (const entry of entries) {
        if (!entry.isFile() || !entry.name.endsWith('.md'))
            continue;
        try {
            const content = await fs.readFile(resolve(dir, entry.name), 'utf-8');
            skills.push(parseSkillFile(content));
        }
        catch (err) {
            log.warn({ file: entry.name, err: err instanceof Error ? err.message : String(err) }, 'Failed to parse user skill');
        }
    }
    userSkillsCache = skills;
    if (skills.length > 0) {
        log.info({ count: skills.length, dir }, 'User skills loaded from disk');
    }
    return skills;
}
/**
 * Merge tenant-specific skills with universal baseline.
 * Tenant skills win on name conflict so a business can override `الترحيب`.
 */
function mergeSkills(universal, tenant) {
    const byName = new Map();
    for (const s of universal)
        byName.set(s.name, s);
    for (const s of tenant)
        byName.set(s.name, s); // tenant overrides
    return Array.from(byName.values());
}
export async function loadSkills(tenantId) {
    const [universal, userInstalled] = await Promise.all([
        loadUniversalSkillsFromDisk(),
        loadUserSkillsFromDisk(),
    ]);
    // User-installed skills override universal ones with the same name.
    const baseline = mergeSkills(universal, userInstalled);
    const tenantSkills = [];
    try {
        // Tenant-specific skills live in the tenant's Supabase Storage bucket.
        // In device mode the SQLite shim doesn't implement Storage — fall through
        // gracefully so the KG-merge stage still runs (KG-only skills via
        // `skill_create` need to surface even when there's no per-tenant disk
        // catalog).
        const bucket = `tenant-${tenantId}`;
        const storageList = supabase.storage?.from?.(bucket)?.list;
        if (typeof storageList !== 'function') {
            log.debug({ tenantId }, 'Supabase storage unavailable (device mode) — skipping per-tenant disk skills');
        }
        else {
            const { data: files, error } = await supabase.storage
                .from(bucket)
                .list('SKILLS', { limit: 100, sortBy: { column: 'name', order: 'asc' } });
            if (error) {
                log.warn({ tenantId, error: error.message }, 'Tenant storage list failed — continuing with baseline + KG');
            }
            else {
                const mdFiles = (files || []).filter((f) => f.name.endsWith('.md'));
                for (const file of mdFiles) {
                    try {
                        const { data, error: dlError } = await supabase.storage
                            .from(bucket)
                            .download(`SKILLS/${file.name}`);
                        if (dlError || !data) {
                            log.warn({ tenantId, file: file.name, error: dlError?.message }, 'Failed to download tenant skill');
                            continue;
                        }
                        const content = await data.text();
                        tenantSkills.push(parseSkillFile(content));
                    }
                    catch (err) {
                        log.warn({ tenantId, file: file.name, err: err instanceof Error ? err.message : String(err) }, 'Error parsing tenant skill file');
                    }
                }
            }
        }
    }
    catch (err) {
        log.warn({ tenantId, err: err instanceof Error ? err.message : String(err) }, 'Tenant skill disk load threw — continuing to KG merge');
    }
    const merged = mergeSkills(baseline, tenantSkills);
    // ─── KG layer: merge in evolved skills + lessons_learned ───
    // The kg_entities table is the EVOLVED layer. Disk = baseline template,
    // KG = lessons learned + skills created in conversation. We attach
    // lessons_learned to existing baseline skills, and import KG-only skills
    // (created by skill_create) as fresh entries.
    //
    // (v2.5.5) Cached for 60s per-tenant. Hot-path messages used to take
    // ~50-150ms here per request even when nothing in the KG changed.
    // `invalidateSkillsCache()` clears this when skill writers run.
    const cacheKey = tenantId;
    const cached = kgMergeCache.get(cacheKey);
    const now = Date.now();
    let evolved;
    if (cached && cached.expiresAt > now) {
        evolved = cached.value;
    }
    else {
        evolved = await mergeKgSkills(tenantId, merged).catch((err) => {
            log.warn({ tenantId, err: err instanceof Error ? err.message : String(err) }, 'KG skill merge failed — using disk skills only');
            return merged;
        });
        kgMergeCache.set(cacheKey, { value: evolved, expiresAt: now + KG_MERGE_TTL_MS });
    }
    log.info({ tenantId, universal: universal.length, userInstalled: userInstalled.length, tenant: tenantSkills.length, kgMerged: evolved.length - merged.length, total: evolved.length }, 'Skills loaded');
    return evolved;
}
/**
 * Merge baseline skills (disk) with their KG-stored evolution layer.
 *
 * Two cases per KG row:
 *   1. Skill name matches a baseline skill → append `lessons_learned` as a
 *      "## دروس مُتَعلَّمة سابقاً" section to the instructions. The brain sees
 *      these in the prompt next time it loads the skill.
 *   2. Skill name doesn't match (KG-only, created via `skill_create` tool) →
 *      build a synthetic Skill from `properties.{when_to_use, instructions,
 *      examples}` and add to the catalog.
 *
 * No mutation — returns a new array.
 */
async function mergeKgSkills(tenantId, baseline) {
    const { data: rows } = await supabase
        .from('kg_entities')
        .select('id, name, name_normalized, properties')
        .eq('tenant_id', tenantId)
        .eq('kind', 'skill')
        .limit(500);
    if (!rows || rows.length === 0)
        return baseline;
    const byNameNorm = new Map();
    for (const s of baseline)
        byNameNorm.set(s.name.toLowerCase(), s);
    const kgRows = rows;
    // (Audit M6) Detect duplicates: should be impossible given the unique
    // index `idx_kg_entities_uniq` but we've seen it happen via manual DB
    // edits or migration glitches. Log + dedupe by keeping the most-recent.
    const dupeCheck = new Map();
    for (const r of kgRows) {
        const key = r.name_normalized;
        const arr = dupeCheck.get(key) ?? [];
        arr.push(r);
        dupeCheck.set(key, arr);
    }
    for (const [key, group] of dupeCheck) {
        if (group.length > 1) {
            log.warn({ tenantId, name_normalized: key, count: group.length, ids: group.map((g) => g.id) }, '⚠️  duplicate KG skill rows detected — keeping last, ignoring rest');
        }
    }
    // Reduce kgRows to one-per-name_normalized (last wins)
    const dedupedKgRows = [];
    const seen = new Set();
    for (let i = kgRows.length - 1; i >= 0; i--) {
        const r = kgRows[i];
        if (seen.has(r.name_normalized))
            continue;
        seen.add(r.name_normalized);
        dedupedKgRows.unshift(r);
    }
    const result = [];
    const consumedNorms = new Set();
    for (const skill of baseline) {
        const norm = skill.name.toLowerCase();
        const kg = dedupedKgRows.find((r) => r.name_normalized === norm);
        if (!kg) {
            result.push(skill);
            continue;
        }
        consumedNorms.add(norm);
        const props = (kg.properties || {});
        const lessons = Array.isArray(props.lessons_learned) ? props.lessons_learned : [];
        // Filter out superseded lessons (H2) and split by kind (avoid vs repeat).
        const live = lessons.filter((l) => !l.superseded_at);
        const avoid = live.filter((l) => l.kind !== 'repeat');
        const repeat = live.filter((l) => l.kind === 'repeat');
        if (live.length === 0) {
            result.push(skill);
            continue;
        }
        const sections = [];
        if (avoid.length > 0) {
            const avoidBlock = avoid
                .map((l, i) => {
                const text = typeof l.text === 'string' ? l.text : '';
                const sev = typeof l.severity === 'string' ? l.severity : 'medium';
                return `${i + 1}. [${sev}] ${text}`;
            })
                .join('\n');
            sections.push(`\n\n## دروس مُتَعلَّمة (تجنّب — طبّقها لا تتجاهلها)\n${avoidBlock}`);
        }
        if (repeat.length > 0) {
            const repeatBlock = repeat
                .map((l, i) => {
                const text = typeof l.text === 'string' ? l.text : '';
                return `${i + 1}. ${text}`;
            })
                .join('\n');
            sections.push(`\n\n## ما اشتغل سابقاً (كرّرها — أنماط ناجحة)\n${repeatBlock}`);
        }
        const augmented = {
            ...skill,
            instructions: skill.instructions + sections.join(''),
            raw: skill.raw,
        };
        result.push(augmented);
    }
    // KG-only skills (created via skill_create tool, never on disk)
    for (const kg of dedupedKgRows) {
        if (consumedNorms.has(kg.name_normalized))
            continue;
        const props = (kg.properties || {});
        const whenToUse = typeof props.when_to_use === 'string' ? props.when_to_use : '';
        const instructions = typeof props.instructions === 'string' ? props.instructions : '';
        if (!instructions)
            continue;
        const examples = Array.isArray(props.examples) ? props.examples.filter((e) => typeof e === 'string') : [];
        const lessons = Array.isArray(props.lessons_learned) ? props.lessons_learned : [];
        const lessonsBlock = lessons.length > 0
            ? '\n\n## دروس مُتَعلَّمة سابقاً\n' +
                lessons.map((l, i) => `${i + 1}. ${typeof l.text === 'string' ? l.text : ''}`).join('\n')
            : '';
        result.push({
            name: kg.name,
            whenToUse,
            instructions: instructions + lessonsBlock,
            examples,
            keywords: [],
            priority: 5,
            raw: instructions,
        });
    }
    return result;
}
// ─── Skill Matcher ──────────────────────────────────────────────────
// Tokenize Arabic + English + mixed text for BM25-style scoring.
// Strips diacritics, normalizes alif variants, collapses runs, and splits
// on whitespace + punctuation so "عملاء,منتج" and "عملاء منتج" tokenize
// the same way. Length filter drops one-letter noise tokens.
const AR_DIACRITICS = /[ً-ٰٟ]/g;
function tokenize(text) {
    return text
        .toLowerCase()
        .replace(AR_DIACRITICS, '')
        .replace(/[إأآا]/g, 'ا')
        .replace(/ى/g, 'ي')
        .replace(/ة/g, 'ه')
        .split(/[\s,،.!?؟:;""''`()[\]{}\/\\\-—_+=*&^%$#@~|]+/u)
        .filter((t) => t.length >= 2);
}
/**
 * Score skills by token overlap of (name + whenToUse + keywords) against
 * (message + intent + recent context). Returns ALL skills above threshold,
 * ranked, capped at `limit`. Replaces the old single-best matcher — the
 * brain is smart enough to pick from a shortlist; forcing 1-of-220 was the
 * "skills feel fake" bug. Multi-skill mode lets the model see e.g.
 * `follow-up`, `qualify-lead`, AND `customer-segmentation` simultaneously.
 *
 * No keyword shortlist — every token contributes via inverse-rarity weighting
 * so distinctive words like "زاتكا" beat noise like "اريد".
 */
export function matchSkills(skills, intent, message, options = {}) {
    if (skills.length === 0)
        return [];
    const limit = options.limit ?? 5;
    const minScore = options.minScore ?? 8;
    const queryTokens = new Set([...tokenize(message), ...tokenize(intent)]);
    if (queryTokens.size === 0)
        return [];
    // Document-frequency: how many skills mention each token? Rare tokens
    // (low df) carry more weight when they hit.
    const df = new Map();
    const skillTokens = skills.map((s) => {
        const corpus = [s.name, s.whenToUse, ...s.keywords, ...s.examples].join(' ');
        return new Set(tokenize(corpus));
    });
    for (const tokens of skillTokens) {
        for (const t of tokens)
            df.set(t, (df.get(t) ?? 0) + 1);
    }
    const N = skills.length;
    const scored = [];
    for (let i = 0; i < skills.length; i++) {
        const skill = skills[i];
        const tokens = skillTokens[i];
        let score = 0;
        const hits = [];
        for (const q of queryTokens) {
            if (tokens.has(q)) {
                const docFreq = df.get(q) ?? 1;
                const idf = Math.log(1 + N / docFreq);
                score += idf * 10;
                hits.push(q);
            }
        }
        // Name match boost — a skill whose name appears in the query should win
        const nameTokens = new Set(tokenize(skill.name));
        let nameOverlap = 0;
        for (const nt of nameTokens)
            if (queryTokens.has(nt))
                nameOverlap++;
        if (nameOverlap > 0)
            score += nameOverlap * 8;
        // Priority modifier — small nudge so ties resolve toward higher-priority skills
        score += Math.max(0, 10 - skill.priority) * 0.5;
        if (score >= minScore) {
            scored.push({
                skill,
                score: Math.round(score * 10) / 10,
                reason: hits.length > 0 ? `tokens: ${hits.slice(0, 5).join(', ')}` : 'name overlap',
            });
        }
    }
    scored.sort((a, b) => b.score - a.score);
    return scored.slice(0, limit);
}
/** Back-compat shim for callers that want a single best match. */
export function matchSkill(skills, intent, message) {
    const matches = matchSkills(skills, intent, message, { limit: 1 });
    return matches[0] ?? null;
}
// ─── Format skill for injection into prompt ─────────────────────────
/**
 * Hybrid skill injection: full body for top-K immediately-relevant skills,
 * compact catalog for the rest. (Audit C1 fix.)
 *
 * Why both layers:
 * - Full bodies for top 3 means the brain actually has the instructions +
 *   lessons_learned text in front of it — not just a hint that they exist.
 * - Compact catalog for remaining 12 (out of top-15) gives the brain a
 *   menu of "you can also use these" without burning the token budget.
 *
 * Compact-only catalog (the old behavior) was the "skills feel fake" root
 * cause: the brain saw names + 140-char hints and never the actual
 * instructions or accumulated lessons. Now the top matches always get
 * the full body.
 *
 * Token budget guideline:
 * - 3 full skills × ~1500 chars = 4500 chars (~1500 tokens)
 * - 12 catalog lines × ~150 chars = 1800 chars (~600 tokens)
 * - Total: ~2100 tokens for skill context. Comfortable on 64K-context models.
 *
 * (H1 fix) Removed the disempowering footer ("skills are guides, tools drive
 * reality"). Replaced with an instruction to apply the relevant skill.
 */
/**
 * (v2.5.4) Skills that ALWAYS get full body when they match, regardless
 * of BM25 rank. These are quality-critical — partial application produces
 * the "AI design slop" pattern (homogenized purple gradients, Inter font,
 * single-section layouts) that the skill bodies exist to prevent.
 */
// (v3.0.2) Name strings must match EXACTLY what's in each skill file's
// frontmatter `name:` field. Two design skills had wrong strings here in
// v3.0.1, which silently dropped them from the force-full-body path —
// the agent was producing "AI design slop" because those skills got
// truncated to their compact catalog entry instead of full body.
const FORCE_FULL_BODY_SKILLS = new Set([
    'Impeccable Design (Bundled — pbakaus/impeccable)',
    'Impeccable UI Design',
    'Taste Skill (Bundled — leonxlnx/taste-skill)',
    'Huashu Design (Bundled — alchaincyf/huashu-design)',
    'UI/UX Pro Max (Bundled — nextlevelbuilder/ui-ux-pro-max-skill)',
    'فاتورة ZATCA',
    'ZATCA VAT Filing',
    'Pitch Deck — Investor Presentation',
    'Sales Proposal — Bilingual Saudi B2B',
    'Bilingual Contracts (Arabic + English)',
]);
export function formatSkillCatalog(matches, options = {}) {
    if (matches.length === 0)
        return '';
    const fullBodyK = Math.min(options.fullBodyTopK ?? 3, matches.length);
    const lines = [
        '## مهارات لتطبيقها الآن',
        'هذه المهارات منتقاة لطلب المالك. **طبّق الأعلى صلة منها** — ليست اقتراحات اختيارية.',
        '',
    ];
    // (v2.5.4) Force-promote critical skills into the full-body block even
    // when they didn't make top-K by BM25 score. Real-world test caught
    // "Impeccable Design" at rank 4 producing tutorial-quality HTML because
    // it only got the compact catalog entry.
    const forcedIndices = new Set();
    for (let i = fullBodyK; i < matches.length; i++) {
        if (FORCE_FULL_BODY_SKILLS.has(matches[i].skill.name))
            forcedIndices.add(i);
    }
    const fullBodyIndices = new Set();
    for (let i = 0; i < fullBodyK; i++)
        fullBodyIndices.add(i);
    for (const i of forcedIndices)
        fullBodyIndices.add(i);
    // Top-K with full body (instructions + lessons_learned + examples)
    let rank = 0;
    for (const i of [...fullBodyIndices].sort((a, b) => a - b)) {
        rank++;
        const m = matches[i];
        const skill = m.skill;
        const forced = forcedIndices.has(i) ? ' *(forced full-body — quality-critical)*' : '';
        lines.push(`### ${rank}. ${skill.name}${skill.whenToUse ? ` — ${skill.whenToUse}` : ''}${forced}`);
        lines.push('');
        // Body is already augmented with lessons_learned by mergeKgSkills
        const body = (skill.instructions || '').trim();
        if (body) {
            // Cap each body at 2.5K to bound token use; lessons_learned section
            // gets priority by being at the end (untruncated unless body is huge).
            lines.push(body.length > 2500 ? body.slice(0, 2500) + '\n…[skill body truncated; call lookup_skill if you need more]' : body);
        }
        if (skill.examples && skill.examples.length > 0) {
            lines.push('', '**أمثلة:**');
            for (const ex of skill.examples.slice(0, 3))
                lines.push(`- ${ex}`);
        }
        lines.push('');
    }
    // Remaining matches as compact menu (skip already-promoted ones)
    const remaining = matches.filter((_, i) => !fullBodyIndices.has(i));
    if (remaining.length > 0) {
        lines.push('---', '', '## مهارات أخرى متاحة (استدعِ `lookup_skill` لقراءة محتواها الكامل)');
        for (const m of remaining) {
            const when = (m.skill.whenToUse || '').replace(/\s+/g, ' ').slice(0, 140);
            lines.push(`- **${m.skill.name}**${when ? ` — ${when}` : ''}`);
        }
    }
    return lines.join('\n');
}
/**
 * Single-skill format (legacy, kept for back-compat). New code paths use
 * `formatSkillCatalog` to inject a shortlist instead. The forced "🧰 سأستخدم
 * مهارة X للرد" prefix that this function used to add is gone — it made the
 * agent sound like a chatbot narrating its own moves. Smart agents act
 * silently; skill names surface in the dashboard's tool feed instead.
 */
export function formatSkillPrompt(match) {
    const { skill } = match;
    return [
        `📋 مهارة مرشحة: ${skill.name}`,
        skill.whenToUse ? `_متى: ${skill.whenToUse}_` : '',
        '',
        skill.instructions,
        '',
        skill.examples.length > 0
            ? `أمثلة:\n${skill.examples.map(e => `- ${e}`).join('\n')}`
            : '',
    ].filter(Boolean).join('\n');
}
//# sourceMappingURL=skills.js.map