// deterministic plugin-skill resolution.
//
// `findSkillOwners` walks `<platformRoot>/plugins/*/skills/<skillName>/SKILL.md`
// once and returns every plugin that owns that skill. The `skill-find` MCP
// tool wraps this directly; `plugin-read`'s "not found" branch uses
// `computePluginReadHint` to surface the owning plugin when the agent
// guessed the wrong `pluginName` for a known skill.
//
// Pure filesystem read — no network, no subprocess. The walk is two-level
// (plugin dir → skill dir), never recursive, so deeper skill subtrees and
// sibling references/ directories are ignored.
import { existsSync, readdirSync, statSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
const SKILL_FILE_PATH = /^skills\/([a-z0-9][a-z0-9-]*)\//;
// the symmetric misuse: skill slug buried inside `pluginName`
// (e.g. `admin/skills/plainly`, `contacts/skills/plainly`) rather than the
// file argument. Match anywhere inside the string, bounded by start or slash
// on the left and slash or end on the right, so both `<plugin>/skills/<slug>`
// and bare `skills/<slug>` forms surface the slug.
const SKILL_IN_PLUGIN_NAME = /(?:^|\/)skills\/([a-z0-9][a-z0-9-]*)(?:\/|$)/;
// Task 516 — content-producer-owned authoring/render/publish skills that the
// admin seat must NOT load or read inline. The admin's job is to dispatch
// content-producer (via the Agent tool, subagent_type content-producer) for
// these deliverables, not author them from its own seat. Membership is each
// skill's own SKILL.md "Invoked by" declaration:
//   - professional-document — content-producer authoring (IDENTITY delegates
//     document authoring to content-producer).
//   - a4-print-documents     — content-producer's PDF render-constraints skill.
//   - publish-site           — SKILL.md: "Invoked from specialists:content-producer".
// Deliberately EXCLUDED because the admin invokes them directly:
//   - unzip-attachment — SKILL.md: "Invoked by the admin agent directly.
//     Admin owns all unzipping" (admin extracts, then routes to a specialist).
//   - deck-pages       — admin-owned, a content-producer refusal target.
export const CONTENT_PRODUCER_AUTHORING_SKILLS = new Set([
    "professional-document",
    "a4-print-documents",
    "publish-site",
]);
// The verbatim refusal text the skill-load / plugin-read handlers relay when
// the admin seat reaches for a content-producer-owned authoring skill. It
// names the route so the agent's next action is the dispatch, not a dead end.
export const AUTHORING_DISPATCH_HINT = "This deliverable is owned by content-producer. Dispatch it with the Agent tool and subagent_type content-producer.";
// The admin seat is the operator's own `claude rc` session: role is `admin`
// and no specialist is stamped. rc-daemon stamps MAXY_SESSION_ROLE=admin +
// MAXY_SPECIALIST='' for it; pty-spawner stamps MAXY_SPECIALIST=<name> on
// every dispatched specialist subagent (content-producer included), and
// role=public on public spawns. So `role === 'admin' && no specialist`
// uniquely identifies the admin seat. Any other / unidentified env fails
// open (returns false) — the gate must never brick a legitimate load.
export function isAdminSeat(env) {
    return env.MAXY_SESSION_ROLE === "admin" && !(env.MAXY_SPECIALIST && env.MAXY_SPECIALIST.length > 0);
}
// Extract a skill slug from a plugin-read call's arguments. The slug may sit
// in `file` (e.g. `skills/professional-document/SKILL.md` or a reference
// under that dir) or be buried in `pluginName` (the same wrong-guess shape
// computePluginReadHint handles). Returns undefined for PLUGIN.md and plain
// reference reads that are not under a `skills/<slug>/` path.
export function skillSlugFromReadArgs(pluginName, file) {
    return file.match(SKILL_IN_PLUGIN_NAME)?.[1] ?? pluginName.match(SKILL_IN_PLUGIN_NAME)?.[1];
}
// True when the admin seat is reaching for a content-producer-owned authoring
// skill — the signal both the skill-load and plugin-read handlers gate on.
export function adminAuthoringSkillBlock(slug, env) {
    if (!slug)
        return false;
    return isAdminSeat(env) && CONTENT_PRODUCER_AUTHORING_SKILLS.has(slug);
}
export function findSkillOwners(platformRoot, skillName) {
    const owners = [];
    const pluginsDir = resolve(platformRoot, "plugins");
    if (!existsSync(pluginsDir)) {
        return { status: "not-found", candidates: [] };
    }
    let entries;
    try {
        entries = readdirSync(pluginsDir);
    }
    catch {
        return { status: "not-found", candidates: [] };
    }
    for (const pluginName of entries) {
        const skillFile = resolve(pluginsDir, pluginName, "skills", skillName, "SKILL.md");
        if (!existsSync(skillFile))
            continue;
        try {
            if (!statSync(skillFile).isFile())
                continue;
        }
        catch {
            continue;
        }
        owners.push({ pluginName, file: `skills/${skillName}/SKILL.md` });
    }
    if (owners.length === 0)
        return { status: "not-found", candidates: [] };
    if (owners.length === 1)
        return { status: "unique", candidates: [owners[0]] };
    return { status: "ambiguous", candidates: owners };
}
// Returns the owning plugin and the canonical `skills/<slug>/SKILL.md`
// path when exactly one sibling plugin holds the skill the agent was
// trying to reach; null otherwise. Used by `plugin-read` to append
// `Did you mean pluginName="<owner>", file="skills/<slug>/SKILL.md"?`
// on wrong-guess errors.
//
// Two shapes of misuse are covered:
//   1. skill slug appears in `file`     — e.g. file="skills/plainly/SKILL.md"
//   2. skill slug appears in `pluginName` — e.g. pluginName="admin/skills/plainly"
//
// The file argument wins when both shapes match — it carries the more
// specific intent (the agent named a file directly).
export function computePluginReadHint(platformRoot, pluginName, file) {
    const skillName = file.match(SKILL_FILE_PATH)?.[1] ?? pluginName.match(SKILL_IN_PLUGIN_NAME)?.[1];
    if (!skillName)
        return null;
    const result = findSkillOwners(platformRoot, skillName);
    if (result.status !== "unique")
        return null;
    const owner = result.candidates[0];
    const correctedFile = `skills/${skillName}/SKILL.md`;
    if (owner.pluginName === pluginName && correctedFile === file)
        return null;
    return { pluginName: owner.pluginName, file: correctedFile };
}
// parse the `requiredInputs:` list from a SKILL.md frontmatter.
//
// The doctrine in `platform/templates/agents/admin/IDENTITY.md` "Skill-input
// resolution" routes a missing skill input through `AskUserQuestion` instead
// of memory/conversation/graph/task search. To make that routing actionable,
// the skill-load tool surfaces the declared `requiredInputs` as a notice
// block prepended to the SKILL body, and `parseClaudeStream` keys its
// `[skill-input-resolution]`/`[skill-input-missing]` emissions off the same
// declaration.
//
// Shape: each list entry is a slot. Within a slot, `|`-separated tokens
// declare alternative fields that satisfy the slot. Example for the
// `property-brochure` skill:
//
//   requiredInputs:
//     - propertyAssetsDir | listingURL
//
// Returns `[["propertyAssetsDir", "listingURL"]]`. Undefined when the
// frontmatter is missing or the key is absent. The parser is intentionally
// narrow: it scans only the leading `---\n...\n---\n` block, recognises the
// `requiredInputs:` key at column 0, and reads dash-prefixed list items at
// any indentation until the next column-0 key or the frontmatter close.
export function parseRequiredInputs(skillBody) {
    if (!skillBody.startsWith("---\n"))
        return undefined;
    const fmEnd = skillBody.indexOf("\n---\n", 4);
    if (fmEnd < 0)
        return undefined;
    const frontmatter = skillBody.slice(4, fmEnd);
    const lines = frontmatter.split("\n");
    let inBlock = false;
    const slots = [];
    for (const line of lines) {
        if (/^[A-Za-z][\w-]*:/.test(line)) {
            inBlock = line.startsWith("requiredInputs:");
            continue;
        }
        if (!inBlock)
            continue;
        const itemMatch = line.match(/^\s+-\s+(.+?)\s*$/);
        if (!itemMatch)
            continue;
        const slot = itemMatch[1].split("|").map(s => s.trim()).filter(s => s.length > 0);
        if (slot.length > 0)
            slots.push(slot);
    }
    return slots.length > 0 ? slots : undefined;
}
export async function loadSkill(platformRoot, skillName) {
    const result = findSkillOwners(platformRoot, skillName);
    if (result.status === "ambiguous") {
        return { status: "ambiguous", candidates: result.candidates };
    }
    if (result.status === "not-found") {
        return { status: "not-found" };
    }
    const [{ pluginName, file }] = result.candidates;
    const absolutePath = resolve(platformRoot, "plugins", pluginName, file);
    const body = await readFile(absolutePath, "utf-8");
    return { status: "unique", pluginName, file, absolutePath, body };
}
//# sourceMappingURL=skill-resolution.js.map