/**
 * Workflow management library.
 *
 * Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
 * They optionally contain subagents/, skills/, and plugins/ subdirectories that
 * are composed at runtime by `agents run <workflow>`.
 */
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as yaml from 'yaml';
import { capableAgents, supports } from './capabilities.js';
import { getProjectAgentsDir, getSystemWorkflowsDir, getUserWorkflowsDir, getTrashWorkflowsDir, getEnabledExtraRepos, getPluginsDir, getSystemPluginsDir, getProjectPluginsDir, } from './state.js';
import { listInstalledVersions, getVersionHomePath } from './installations/versions.js';
import { dirsContentMatch, normalizeResourceContent } from './resource-content-diff.js';
/**
 * Hard upper bound on items a single `for_each` expands, absent an explicit
 * `max_items`. A guard against a runaway producer spawning unbounded teammates
 * (acceptance criterion in issue #343). Anthropic's Dynamic Workflows cap at
 * 1000; we default lower and let authors raise it deliberately.
 */
export const DEFAULT_FOR_EACH_CAP = 256;
/** Parse WORKFLOW.md frontmatter from a workflow directory. Returns null if invalid. */
export function parseWorkflowFrontmatter(workflowDir) {
    const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
    if (!fs.existsSync(workflowMdPath))
        return null;
    try {
        const content = fs.readFileSync(workflowMdPath, 'utf-8');
        const lines = content.split('\n');
        if (lines[0] !== '---')
            return null;
        const endIndex = lines.slice(1).findIndex(l => l === '---');
        if (endIndex < 0)
            return null;
        const frontmatter = lines.slice(1, endIndex + 1).join('\n');
        const parsed = yaml.parse(frontmatter);
        if (!parsed || typeof parsed !== 'object')
            return null;
        // Capability-scoping fields are wired into the run (see src/commands/exec.ts);
        // coerce to string arrays defensively so a malformed `tools: foo` (scalar) or
        // `tools: [Read, 3]` (mixed) never reaches buildExecCommand as a bad shape.
        const asStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string') ? v : undefined;
        return {
            name: parsed.name || '',
            description: parsed.description || '',
            model: parsed.model,
            tools: asStringArray(parsed.tools),
            skills: asStringArray(parsed.skills),
            mcpServers: asStringArray(parsed.mcpServers),
            allowedAgents: asStringArray(parsed.allowedAgents),
            secrets: asStringArray(parsed.secrets),
            loop: parseLoopBlock(parsed.loop),
            forEach: parseForEachBlock(parsed.for_each),
        };
    }
    catch {
        return null;
    }
}
function readWorkflowBody(workflowDir) {
    const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
    if (!fs.existsSync(workflowMdPath))
        return '';
    const content = fs.readFileSync(workflowMdPath, 'utf-8');
    const lines = content.split('\n');
    if (lines[0] !== '---')
        return content.trim();
    const endIndex = lines.slice(1).findIndex(l => l === '---');
    if (endIndex < 0)
        return content.trim();
    return lines.slice(endIndex + 2).join('\n').trim();
}
/**
 * Defensively coerce a frontmatter `loop:` value into a LoopConfigRaw.
 *
 * Mirrors the asStringArray discipline above: a malformed field is dropped to
 * undefined rather than passed through, so the loop driver never sees a bad
 * shape. Returns undefined when `loop:` is absent or not an object, or when no
 * recognized field survives coercion (an all-garbage block is treated as
 * "no loop", not "empty loop").
 *
 * Field rules:
 *   - until:          only the literal `signal` is accepted; anything else dropped.
 *   - max_iterations: a finite positive integer; non-numbers/<=0 dropped.
 *   - budget:         a finite positive number (tokens); non-numbers/<=0 dropped.
 *   - interval:       a string (e.g. "0", "30m"); non-strings dropped.
 */
export function parseLoopBlock(v) {
    if (!v || typeof v !== 'object' || Array.isArray(v))
        return undefined;
    const raw = v;
    const out = {};
    if (raw.until === 'signal')
        out.until = 'signal';
    if (typeof raw.max_iterations === 'number'
        && Number.isFinite(raw.max_iterations)
        && Number.isInteger(raw.max_iterations)
        && raw.max_iterations > 0) {
        out.max_iterations = raw.max_iterations;
    }
    if (typeof raw.budget === 'number' && Number.isFinite(raw.budget) && raw.budget > 0) {
        out.budget = raw.budget;
    }
    if (typeof raw.interval === 'string')
        out.interval = raw.interval;
    return Object.keys(out).length > 0 ? out : undefined;
}
/** A finite positive integer, or undefined. Shared guard for count-like fields. */
function asPosInt(v) {
    return typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v) && v > 0
        ? v
        : undefined;
}
/** A non-empty trimmed string, or undefined. */
function asNonEmptyString(v) {
    return typeof v === 'string' && v.trim().length > 0 ? v : undefined;
}
/**
 * Defensively coerce a frontmatter `verify:` sub-block into a ForEachVerifySpec.
 *
 * Requires an `agent`; drops the whole block otherwise (a verify panel with no
 * skeptic is meaningless). `votes` defaults to 1 (a single confirmation) and
 * `keep_if` to `majority`; both are validated against their allowed shapes.
 */
export function parseVerifyBlock(v) {
    if (!v || typeof v !== 'object' || Array.isArray(v))
        return undefined;
    const raw = v;
    const agent = asNonEmptyString(raw.agent);
    if (!agent)
        return undefined;
    const keepIf = raw.keep_if;
    const out = {
        agent,
        votes: asPosInt(raw.votes) ?? 1,
        keep_if: keepIf === 'all' || keepIf === 'any' || keepIf === 'majority'
            ? keepIf
            : 'majority',
    };
    const prompt = asNonEmptyString(raw.prompt);
    if (prompt)
        out.prompt = prompt;
    return out;
}
/**
 * Defensively coerce a frontmatter `for_each:` block into a ForEachSpec (issue
 * #343). Mirrors `parseLoopBlock`'s discipline: a block missing the two
 * load-bearing fields (`agent` + `prompt`) drops to undefined rather than
 * passing a half-formed spec to the expander. Optional numeric/verify fields
 * are individually validated and dropped when malformed.
 */
export function parseForEachBlock(v) {
    if (!v || typeof v !== 'object' || Array.isArray(v))
        return undefined;
    const raw = v;
    const agent = asNonEmptyString(raw.agent);
    const prompt = asNonEmptyString(raw.prompt);
    if (!agent || !prompt)
        return undefined;
    const out = { agent, prompt };
    const produce = asNonEmptyString(raw.produce);
    if (produce)
        out.produce = produce;
    // `for_each: ${step}` references a prior step's list. Accept either the
    // snake-case `for_each` key or an explicit `items_ref`.
    const itemsRef = asNonEmptyString(raw.for_each) ?? asNonEmptyString(raw.items_ref);
    if (itemsRef)
        out.itemsRef = itemsRef;
    const name = asNonEmptyString(raw.name);
    if (name)
        out.name = name;
    const concurrency = asPosInt(raw.concurrency);
    if (concurrency)
        out.concurrency = concurrency;
    const maxItems = asPosInt(raw.max_items);
    if (maxItems)
        out.max_items = maxItems;
    const verify = parseVerifyBlock(raw.verify);
    if (verify)
        out.verify = verify;
    return out;
}
/**
 * Substitute `{{item}}` / `{{index}}` (and 1-based `{{n}}`) in a prompt
 * template. Unknown `{{...}}` tokens are left intact so a template can carry
 * placeholders the caller resolves elsewhere.
 */
export function renderForEachTemplate(template, item, index) {
    return template
        .replace(/\{\{\s*item\s*\}\}/g, item)
        .replace(/\{\{\s*index\s*\}\}/g, String(index))
        .replace(/\{\{\s*n\s*\}\}/g, String(index + 1));
}
/**
 * Expand a `for_each` spec against a producer's output into concrete teammate
 * descriptors (issue #343) — the heart of the declarative fan-out.
 *
 * Pure and deterministic: no I/O, no spawning. `src/lib/teams/forEach.ts`
 * feeds the result to `AgentManager.spawn`, staging each descriptor into the
 * supervisor's existing mid-flight-add path — so this reuses the dynamic-DAG
 * substrate rather than introducing a new engine.
 *
 * For N produced items (capped at `spec.max_items` / `DEFAULT_FOR_EACH_CAP`):
 *   - one `stage` teammate per item, depending on `producerName` if given;
 *   - when `verify` is set, `votes` `verify` teammates per item, each
 *     depending on that item's stage teammate.
 *
 * Names are unique (`<base>-<n>` and `<base>-<n>-verify-<v>`) so `--after`
 * linkage and the teams cycle check carry over unchanged.
 */
export function expandForEach(spec, items, opts = {}) {
    const cap = spec.max_items ?? DEFAULT_FOR_EACH_CAP;
    const producedCount = items.length;
    const used = items.slice(0, cap);
    const base = spec.name ?? 'item';
    const teammates = [];
    used.forEach((item, itemIndex) => {
        const stageName = `${base}-${itemIndex + 1}`;
        teammates.push({
            role: 'stage',
            name: stageName,
            agentType: spec.agent,
            prompt: renderForEachTemplate(spec.prompt, item, itemIndex),
            after: opts.producerName ? [opts.producerName] : [],
            item,
            itemIndex,
        });
        if (spec.verify) {
            const verifyPrompt = spec.verify.prompt ?? spec.prompt;
            for (let vote = 1; vote <= spec.verify.votes; vote++) {
                teammates.push({
                    role: 'verify',
                    name: `${stageName}-verify-${vote}`,
                    agentType: spec.verify.agent,
                    prompt: renderForEachTemplate(verifyPrompt, item, itemIndex),
                    after: [stageName],
                    item,
                    itemIndex,
                    vote,
                    votes: spec.verify.votes,
                    keep_if: spec.verify.keep_if,
                });
            }
        }
    });
    return {
        teammates,
        producedCount,
        usedCount: used.length,
        truncated: producedCount - used.length,
        cap,
    };
}
/**
 * Decide which subagent .md stems a workflow may use, given the discovered
 * subagent files and the parsed `allowedAgents` frontmatter. This is the
 * fail-closed security boundary for issue #324:
 *
 *   - `allowedAgents === undefined` (field absent)  -> NO restriction; allow all.
 *   - `allowedAgents === []`        (present, empty) -> allow ZERO; copy none.
 *   - `allowedAgents = [a, b]`                       -> allow only those stems.
 *
 * An explicit empty array must NEVER widen to "allow all" — that would copy
 * every subagent definition into the run, granting MORE access than declared.
 *
 * `available` are the .md filenames found in subagents/ (e.g. `security.md`).
 * Returns the stems to copy and any allowedAgents entries with no matching file.
 */
export function resolveAllowedSubagents(available, allowedAgents) {
    const stems = available.filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, ''));
    if (allowedAgents === undefined) {
        return { allowedStems: stems, missing: [] };
    }
    const allow = new Set(allowedAgents);
    const present = new Set(stems);
    return {
        allowedStems: stems.filter(s => allow.has(s)),
        missing: allowedAgents.filter(a => !present.has(a)),
    };
}
/** The Claude tool an orchestrator uses to dispatch subagents. Must stay in a
 *  workflow's `--tools` allowlist whenever the workflow ships dispatchable
 *  subagents, or the orchestrator has no way to reach them. */
export const SUBAGENT_DISPATCH_TOOL = 'Task';
/**
 * Keep the subagent-dispatch tool (`Task`) in a `tools:`-restricted workflow's
 * allowlist when the workflow actually ships subagents to dispatch.
 *
 * A WORKFLOW.md `tools:` list becomes Claude's `--tools` set, which *restricts*
 * the available built-ins. An orchestrator whose `subagents/` files were just
 * copied into the shared agents dir but whose `tools:` omits `Task` cannot
 * reach any of them — it silently no-ops (observed: the run emits only "I'll
 * wait for the completion notification" and exits). Omitting `Task` while
 * shipping a `subagents/` dir is always an authoring miss, so we re-add it at
 * the source rather than relying on every workflow author to remember.
 *
 * Returns `tools` unchanged when the workflow has no subagents or already lists
 * `Task`; otherwise appends `Task`.
 */
export function ensureSubagentDispatchTool(tools, hasSubagents) {
    if (!hasSubagents || tools.includes(SUBAGENT_DISPATCH_TOOL))
        return tools;
    return [...tools, SUBAGENT_DISPATCH_TOOL];
}
/**
 * Prune stale workflow-managed subagent files from the shared per-agent agents
 * dir before a scoped run writes the permitted set (issue #401, follow-up to
 * #324). A prior *unrestricted* run of a workflow copies every subagent
 * definition into the shared `~/.claude/agents/` dir; a later run that declares
 * `allowedAgents:` copies only the permitted ones but never removes the
 * leftovers — so an unlisted subagent stays on disk and remains dispatchable,
 * silently defeating the fail-closed scope.
 *
 * Fail-closed fix (mirrors how `cleanupWorkflowMcpConfig` only tears down what
 * the workflow itself created): remove any file that (a) belongs to THIS
 * workflow's subagents/ — matched by filename, i.e. the workflow-managed
 * universe — and (b) is NOT in the permitted set. A user's own hand-placed
 * subagent shares no name with a workflow subagent file, so it is never
 * touched. Permitted files are left in place; the caller (re)copies them.
 *
 * `workflowSubagentFiles` are the .md filenames in the workflow's subagents/
 * dir (e.g. `security.md`); `allowedStems` are the permitted stems from
 * `resolveAllowedSubagents`. Returns the filenames actually removed.
 */
export function pruneStaleWorkflowSubagents(sharedAgentsDir, workflowSubagentFiles, allowedStems) {
    if (!fs.existsSync(sharedAgentsDir))
        return [];
    const allow = new Set(allowedStems);
    const pruned = [];
    for (const file of workflowSubagentFiles) {
        if (!file.endsWith('.md'))
            continue;
        const stem = file.replace(/\.md$/, '');
        if (allow.has(stem))
            continue; // permitted → the copy step will (re)write it
        const target = path.join(sharedAgentsDir, file);
        if (fs.existsSync(target)) {
            fs.rmSync(target, { force: true });
            pruned.push(file);
        }
    }
    return pruned;
}
/** Count subagent .md files in a workflow's subagents/ directory. */
export function countWorkflowSubagents(workflowDir) {
    const subagentsDir = path.join(workflowDir, 'subagents');
    if (!fs.existsSync(subagentsDir))
        return 0;
    try {
        return fs.readdirSync(subagentsDir).filter(f => f.endsWith('.md')).length;
    }
    catch {
        return 0;
    }
}
function getWorkflowBody(workflowDir) {
    const workflowMdPath = path.join(workflowDir, 'WORKFLOW.md');
    if (!fs.existsSync(workflowMdPath))
        return '';
    const content = fs.readFileSync(workflowMdPath, 'utf-8');
    const lines = content.split('\n');
    if (lines[0] === '---') {
        const endIndex = lines.slice(1).findIndex(l => l === '---');
        if (endIndex >= 0)
            return lines.slice(endIndex + 2).join('\n').trim();
    }
    return content.trim();
}
function indentD2BlockString(content) {
    return content
        .split('\n')
        .map(line => `  ${line}`)
        .join('\n');
}
function containsFlowDiagram(content) {
    return /```(?:mermaid|d2)\b/i.test(content);
}
const KIMI_WORKFLOW_MARKER = 'agents_workflow';
const OPENCLAW_WORKFLOW_MARKER_ENV = 'AGENTS_CLI_WORKFLOW';
/** Convert a canonical agents-cli workflow bundle into a Kimi flow skill. */
export function transformWorkflowForKimi(workflowPath, name) {
    const fm = parseWorkflowFrontmatter(workflowPath);
    if (!fm)
        throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
    const body = getWorkflowBody(workflowPath);
    const frontmatter = yaml.stringify({
        name,
        description: fm.description,
        type: 'flow',
        [KIMI_WORKFLOW_MARKER]: name,
    }).trim();
    if (containsFlowDiagram(body)) {
        return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
    }
    const instructions = (body || fm.description).trim();
    return `---\n${frontmatter}\n---\n\n\`\`\`d2\nBEGIN -> step -> END\nstep: |md\n${indentD2BlockString(instructions)}\n|\n\`\`\`\n`;
}
/**
 * Convert a canonical agents-cli workflow bundle into an Antigravity workflow
 * markdown file. Antigravity discovers workflows as flat `<name>.md` files under
 * `~/.gemini/config/global_workflows/` (scanned by `agy` at startup) and exposes
 * each as a `/<name>` slash command. Frontmatter carries the required `description`
 * plus the shared `agents_workflow` ownership marker so agents-cli never clobbers a
 * user-authored workflow of the same name.
 */
export function transformWorkflowForAntigravity(workflowPath, name) {
    const fm = parseWorkflowFrontmatter(workflowPath);
    if (!fm)
        throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
    const body = getWorkflowBody(workflowPath) || fm.description;
    const frontmatter = yaml.stringify({
        description: fm.description,
        name: fm.name || name,
        [KIMI_WORKFLOW_MARKER]: name,
    }).trim();
    return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
}
/** Convert a canonical agents-cli workflow bundle into an OpenClaw Lobster file. */
export function transformWorkflowForOpenClaw(workflowPath, name) {
    const fm = parseWorkflowFrontmatter(workflowPath);
    if (!fm)
        throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
    const body = getWorkflowBody(workflowPath) || fm.description || name;
    return yaml.stringify({
        name: fm.name || name,
        args: {
            agent: {
                default: 'main',
            },
            prompt: {
                default: '',
            },
        },
        env: {
            [OPENCLAW_WORKFLOW_MARKER_ENV]: name,
            AGENTS_WORKFLOW_DESCRIPTION: fm.description || name,
            AGENTS_WORKFLOW_BODY: body.trim(),
        },
        steps: [
            {
                id: 'run_openclaw',
                command: 'openclaw agent --agent "$LOBSTER_ARG_AGENT" --message "$(printf \'%s\\n\\n%s\\n\' "$AGENTS_WORKFLOW_BODY" "$LOBSTER_ARG_PROMPT")"',
            },
        ],
    });
}
/** Marker comment prefix written into agents-cli-managed Grok `.rhai` files. */
export const GROK_WORKFLOW_MARKER = 'agents_workflow';
/** Escape a string for embedding inside a Rhai double-quoted literal. */
function escapeRhaiString(s) {
    return s
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/\r\n/g, '\\n')
        .replace(/\n/g, '\\n')
        .replace(/\r/g, '\\n');
}
/**
 * Convert a canonical agents-cli workflow bundle into a Grok native Rhai
 * workflow script. Grok discovers saved workflows as
 * `~/.grok/workflows/<name>.rhai` (and project `.grok/workflows/`) and exposes
 * each as a `/<name>` slash command (enabled by default since v0.2.111).
 *
 * The projection is a single-agent orchestrator that feeds the WORKFLOW.md
 * body as the agent prompt plus the caller's `args.prompt` (or string args).
 * Multi-phase fan-out is left to hand-authored Rhai — agents-cli's job is to
 * land the orchestrator instructions in the native path.
 */
export function transformWorkflowForGrok(workflowPath, name) {
    const fm = parseWorkflowFrontmatter(workflowPath);
    if (!fm)
        throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
    const body = getWorkflowBody(workflowPath);
    const description = (fm.description || name).trim();
    const instructions = (body || description).trim();
    return [
        `// ${GROK_WORKFLOW_MARKER}: ${name}`,
        `// Managed by agents-cli — re-sync from ~/.agents/workflows/${name}/; do not edit by hand.`,
        `let meta = #{`,
        `    name: "${escapeRhaiString(name)}",`,
        `    description: "${escapeRhaiString(description)}",`,
        `    phases: [ #{ title: "Run", detail: "orchestrator" } ],`,
        `};`,
        ``,
        `let user_prompt = if args == () { () } else if type_of(args) == "string" { args } else { args.prompt };`,
        `if user_prompt == () { pause("verification", "Pass a prompt as args.prompt or a string arg."); }`,
        ``,
        `phase("Run");`,
        `let instructions = "${escapeRhaiString(instructions)}";`,
        `let prompt = instructions + "\\n\\n---\\n\\nUser request:\\n" + user_prompt;`,
        `let r = agent(prompt, #{ label: "orchestrator", capability_mode: "all" });`,
        `if r != () && r.success { complete(r.output); }`,
        `complete(#{ summary: "workflow failed", error: if r == () { "no result" } else { "agent failed" } });`,
        ``,
    ].join('\n');
}
/** Read the agents_workflow marker from a Grok `.rhai` file, if present. */
export function grokWorkflowMarker(filePath) {
    try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const match = content.match(new RegExp(`^//\\s*${GROK_WORKFLOW_MARKER}:\\s*(\\S+)`, 'm'));
        return match?.[1] ?? null;
    }
    catch {
        return null;
    }
}
function expandWorkflowPath(ref) {
    if (ref === '~')
        return process.env.HOME ?? ref;
    if (ref.startsWith('~/')) {
        const home = process.env.HOME;
        return home ? path.join(home, ref.slice(2)) : ref;
    }
    return ref;
}
function isWorkflowDir(dir) {
    return fs.existsSync(path.join(dir, 'WORKFLOW.md'));
}
function resolveWorkflowPath(ref, cwd) {
    const expanded = expandWorkflowPath(ref);
    const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
    return isWorkflowDir(candidate) ? candidate : null;
}
/** Plugin-root marketplace dirs in project → user → system → extra order. */
function pluginMarketplaceDirs(cwd) {
    const pluginsDirs = [];
    const projectPlugins = getProjectPluginsDir(cwd);
    if (projectPlugins)
        pluginsDirs.push(projectPlugins);
    pluginsDirs.push(getPluginsDir(), getSystemPluginsDir());
    for (const extra of getEnabledExtraRepos()) {
        pluginsDirs.push(path.join(extra.dir, 'plugins'));
    }
    return pluginsDirs;
}
function isPluginDirectory(pluginRoot, entry) {
    if (entry.name.startsWith('.'))
        return false;
    let isDir = entry.isDirectory();
    if (!isDir && entry.isSymbolicLink()) {
        try {
            isDir = fs.statSync(pluginRoot).isDirectory();
        }
        catch {
            isDir = false;
        }
    }
    return isDir;
}
/**
 * Plugin `workflows/` directories in discovery order (project → user → system →
 * extra). Used by name resolution and listing so a plugin-packaged workflow is
 * runnable via `agents run <name>` without a separate install into
 * ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
 * plugins beat user/system plugins (same first-hit-wins as other layers).
 *
 * Pass `pluginName` to restrict to one plugin (for `name@plugin` resolution).
 */
export function listPluginWorkflowDirs(cwd = process.cwd(), pluginName) {
    const pluginRoots = [];
    for (const pluginsDir of pluginMarketplaceDirs(cwd)) {
        if (!fs.existsSync(pluginsDir))
            continue;
        let entries;
        try {
            entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            if (pluginName !== undefined && entry.name !== pluginName)
                continue;
            const pluginRoot = path.join(pluginsDir, entry.name);
            if (!isPluginDirectory(pluginRoot, entry))
                continue;
            const workflowsDir = path.join(pluginRoot, 'workflows');
            if (fs.existsSync(workflowsDir))
                pluginRoots.push(workflowsDir);
        }
    }
    return pluginRoots;
}
/**
 * True when `ref` is a single bare workflow / source identifier (no path
 * separators, no `..`, no `@`). Name lookup must not path-join multi-segment
 * or traversal refs into search roots. `name@source` is parsed separately.
 */
export function isBareWorkflowName(ref) {
    if (!ref || ref === '.' || ref === '..')
        return false;
    if (ref.includes('/') || ref.includes('\\'))
        return false;
    if (ref.includes('..'))
        return false;
    if (ref.includes('@'))
        return false;
    // Reject absolute paths (posix or Windows).
    if (path.isAbsolute(ref))
        return false;
    return path.basename(ref) === ref;
}
/**
 * Parse a workflow run target: optional `workflow:` type prefix and optional
 * `@source` pin. Returns null when the form is not a valid name lookup.
 */
export function parseWorkflowRef(ref) {
    let r = ref.trim();
    if (r.startsWith('workflow:'))
        r = r.slice('workflow:'.length);
    if (!r)
        return null;
    const at = r.lastIndexOf('@');
    if (at > 0) {
        const name = r.slice(0, at);
        const source = r.slice(at + 1);
        if (!isBareWorkflowName(name) || !isBareWorkflowName(source))
            return null;
        return { name, source };
    }
    if (!isBareWorkflowName(r))
        return null;
    return { name: r };
}
/**
 * Resolve an `agents run <workflow>` reference.
 *
 * Directories are accepted anywhere on disk when they contain WORKFLOW.md.
 * Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
 * Pin a source with `name@plugin` or `name@extra-alias` (optional `workflow:` prefix).
 */
export function resolveWorkflowRef(ref, cwd = process.cwd()) {
    const direct = resolveWorkflowPath(ref, cwd);
    if (direct)
        return direct;
    const parsed = parseWorkflowRef(ref);
    if (!parsed)
        return null;
    // Source-qualified: only that plugin or extra-repo workflows/ (no layered fallback).
    if (parsed.source) {
        for (const dir of listPluginWorkflowDirs(cwd, parsed.source)) {
            const workflowPath = path.join(dir, parsed.name);
            if (isWorkflowDir(workflowPath))
                return workflowPath;
        }
        for (const extra of getEnabledExtraRepos()) {
            if (extra.alias !== parsed.source)
                continue;
            const workflowPath = path.join(extra.dir, 'workflows', parsed.name);
            if (isWorkflowDir(workflowPath))
                return workflowPath;
        }
        return null;
    }
    const projectAgentsDir = getProjectAgentsDir(cwd);
    const searchDirs = [
        ...(projectAgentsDir ? [path.join(projectAgentsDir, 'workflows')] : []),
        getUserWorkflowsDir(),
        ...listPluginWorkflowDirs(cwd),
        ...getEnabledExtraRepos().map(r => path.join(r.dir, 'workflows')),
        getSystemWorkflowsDir(),
    ];
    for (const dir of searchDirs) {
        const workflowPath = path.join(dir, parsed.name);
        if (isWorkflowDir(workflowPath))
            return workflowPath;
    }
    return null;
}
/**
 * Discover all workflow directories (those containing WORKFLOW.md) in a local path.
 * Checks if the path itself is a workflow, then scans a top-level workflows/ subdirectory,
 * then falls back to scanning all immediate subdirectories.
 */
export function discoverWorkflowsFromRepo(repoPath) {
    const results = [];
    // The path itself may be a single workflow directory.
    if (fs.existsSync(path.join(repoPath, 'WORKFLOW.md'))) {
        const frontmatter = parseWorkflowFrontmatter(repoPath);
        if (frontmatter) {
            return [{
                    name: path.basename(repoPath),
                    path: repoPath,
                    frontmatter,
                    subagentCount: countWorkflowSubagents(repoPath),
                }];
        }
    }
    // Try a workflows/ subdirectory first, then fall back to scanning root subdirectories.
    const workflowsSubdir = path.join(repoPath, 'workflows');
    const scanDir = fs.existsSync(workflowsSubdir) ? workflowsSubdir : repoPath;
    let entries;
    try {
        entries = fs.readdirSync(scanDir, { withFileTypes: true });
    }
    catch {
        return results;
    }
    for (const entry of entries) {
        if (!entry.isDirectory() || entry.name.startsWith('.'))
            continue;
        const workflowPath = path.join(scanDir, entry.name);
        const frontmatter = parseWorkflowFrontmatter(workflowPath);
        if (frontmatter) {
            results.push({
                name: entry.name,
                path: workflowPath,
                frontmatter,
                subagentCount: countWorkflowSubagents(workflowPath),
            });
        }
    }
    return results;
}
/**
 * List all workflows in central storage + plugin packages.
 * Precedence: user > plugin > extra > system (first writer wins; project is
 * cwd-scoped and handled by resolveWorkflowRef / the resource handler).
 */
export function listInstalledWorkflows(cwd = process.cwd()) {
    const result = new Map();
    const extraRepos = getEnabledExtraRepos();
    const searchDirs = [
        getUserWorkflowsDir(),
        ...listPluginWorkflowDirs(cwd),
        ...extraRepos.map(r => path.join(r.dir, 'workflows')),
        getSystemWorkflowsDir(),
    ];
    for (const dir of searchDirs) {
        if (!fs.existsSync(dir))
            continue;
        let entries;
        try {
            entries = fs.readdirSync(dir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            if (!entry.isDirectory() || entry.name.startsWith('.'))
                continue;
            if (result.has(entry.name))
                continue; // Higher-priority layer already present
            const workflowPath = path.join(dir, entry.name);
            const frontmatter = parseWorkflowFrontmatter(workflowPath);
            if (!frontmatter)
                continue;
            result.set(entry.name, {
                name: entry.name,
                path: workflowPath,
                frontmatter,
                subagentCount: countWorkflowSubagents(workflowPath),
            });
        }
    }
    return result;
}
/** Copy a workflow directory into user central storage (~/.agents/workflows/<name>/). */
export function installWorkflowCentrally(sourcePath, name) {
    const targetPath = path.join(getUserWorkflowsDir(), name);
    try {
        fs.mkdirSync(getUserWorkflowsDir(), { recursive: true });
        if (fs.existsSync(targetPath)) {
            fs.rmSync(targetPath, { recursive: true, force: true });
        }
        fs.cpSync(sourcePath, targetPath, { recursive: true });
        return { success: true };
    }
    catch (err) {
        return { success: false, error: err.message };
    }
}
/** Move a workflow from user central storage to trash. */
export function removeWorkflow(name) {
    const sourcePath = path.join(getUserWorkflowsDir(), name);
    if (!fs.existsSync(sourcePath)) {
        return { success: false, error: `Workflow '${name}' not found in ~/.agents/workflows/` };
    }
    try {
        const trashDir = getTrashWorkflowsDir();
        fs.mkdirSync(trashDir, { recursive: true });
        fs.renameSync(sourcePath, path.join(trashDir, `${name}-${Date.now()}`));
        return { success: true };
    }
    catch (err) {
        return { success: false, error: err.message };
    }
}
/**
 * Antigravity user workflows are NOT version-isolated. `agy` scans a single,
 * shared, HOME-global directory at startup — `~/.gemini/config/global_workflows/`
 * — and that dir is a real directory in the user's home, never symlinked into a
 * per-version home (only `~/.gemini/antigravity-cli` is version-scoped). Writing
 * into a version home therefore lands somewhere agy never reads. So every
 * antigravity version resolves to the same real shared dir; `versionHome` is
 * intentionally ignored. (Verified via strace of `agy`: it opens
 * `$HOME/.gemini/config/global_workflows/<name>.md` and never the version home.)
 */
function antigravityWorkflowsDir() {
    return path.join(process.env.HOME ?? os.homedir(), '.gemini', 'config', 'global_workflows');
}
function workflowTargetRoot(agent, versionHome) {
    if (agent === 'kimi')
        return path.join(versionHome, '.kimi-code', 'skills');
    if (agent === 'antigravity')
        return antigravityWorkflowsDir();
    if (agent === 'openclaw')
        return path.join(versionHome, '.openclaw', 'workflows');
    if (agent === 'grok')
        return path.join(versionHome, '.grok', 'workflows');
    return path.join(versionHome, 'workflows');
}
/** List workflow names synced into a specific agent version home. */
export function listWorkflowsForAgent(agent, versionHome) {
    if (agent === 'kimi') {
        const skillsDir = workflowTargetRoot(agent, versionHome);
        if (!fs.existsSync(skillsDir))
            return [];
        return fs.readdirSync(skillsDir, { withFileTypes: true })
            .filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
            .filter(d => kimiWorkflowMarker(path.join(skillsDir, d.name, 'SKILL.md')) === d.name)
            .map(d => d.name);
    }
    if (agent === 'antigravity') {
        const dir = workflowTargetRoot(agent, versionHome);
        if (!fs.existsSync(dir))
            return [];
        try {
            return fs.readdirSync(dir, { withFileTypes: true })
                .filter(d => d.isFile() && d.name.endsWith('.md') && !d.name.startsWith('.'))
                .map(d => d.name.slice(0, -'.md'.length))
                .filter(base => antigravityWorkflowMarker(path.join(dir, `${base}.md`)) === base);
        }
        catch {
            return [];
        }
    }
    if (agent === 'goose') {
        const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
        if (!fs.existsSync(recipesDir))
            return [];
        try {
            return fs.readdirSync(recipesDir, { withFileTypes: true })
                .filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
                .map(d => d.name.slice(0, -'.yaml'.length));
        }
        catch {
            return [];
        }
    }
    if (agent === 'openclaw') {
        const dir = workflowTargetRoot(agent, versionHome);
        if (!fs.existsSync(dir))
            return [];
        try {
            return fs.readdirSync(dir, { withFileTypes: true })
                .filter(d => d.isFile() && d.name.endsWith('.lobster') && !d.name.startsWith('.'))
                .map(d => d.name.slice(0, -'.lobster'.length))
                .filter(base => openclawWorkflowMarker(path.join(dir, `${base}.lobster`)) === base);
        }
        catch {
            return [];
        }
    }
    if (agent === 'grok') {
        const dir = workflowTargetRoot(agent, versionHome);
        if (!fs.existsSync(dir))
            return [];
        try {
            return fs.readdirSync(dir, { withFileTypes: true })
                .filter(d => d.isFile() && d.name.endsWith('.rhai') && !d.name.startsWith('.'))
                .map(d => d.name.slice(0, -'.rhai'.length))
                .filter(base => grokWorkflowMarker(path.join(dir, `${base}.rhai`)) === base);
        }
        catch {
            return [];
        }
    }
    const workflowsDir = workflowTargetRoot(agent, versionHome);
    if (!fs.existsSync(workflowsDir))
        return [];
    try {
        return fs.readdirSync(workflowsDir, { withFileTypes: true })
            .filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
            .map(d => d.name);
    }
    catch {
        return [];
    }
}
function parseSubrecipeFrontmatter(filePath) {
    const content = fs.readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    if (lines[0] !== '---')
        return { body: content.trim() };
    const endIndex = lines.slice(1).findIndex(l => l === '---');
    if (endIndex < 0)
        return { body: content.trim() };
    const frontmatter = lines.slice(1, endIndex + 1).join('\n');
    let parsed = {};
    try {
        const value = yaml.parse(frontmatter);
        if (value && typeof value === 'object' && !Array.isArray(value)) {
            parsed = value;
        }
    }
    catch { /* ignore malformed subagent frontmatter */ }
    return {
        name: typeof parsed.name === 'string' ? parsed.name : undefined,
        description: typeof parsed.description === 'string' ? parsed.description : undefined,
        body: lines.slice(endIndex + 2).join('\n').trim(),
    };
}
function selectedWorkflowSubagents(workflowPath, allowedAgents) {
    const subagentsDir = path.join(workflowPath, 'subagents');
    if (!fs.existsSync(subagentsDir))
        return [];
    const allowed = allowedAgents ? new Set(allowedAgents) : null;
    return fs.readdirSync(subagentsDir, { withFileTypes: true })
        .filter(e => e.isFile() && e.name.endsWith('.md') && !e.name.startsWith('.'))
        .map(e => e.name.slice(0, -'.md'.length))
        .filter(name => !allowed || allowed.has(name))
        .sort();
}
function writeGooseSubrecipe(workflowPath, subrecipeName, destDir) {
    const sourcePath = path.join(workflowPath, 'subagents', `${subrecipeName}.md`);
    const parsed = parseSubrecipeFrontmatter(sourcePath);
    const body = parsed.body || parsed.description || subrecipeName;
    const recipe = {
        version: '1.0.0',
        title: parsed.name || subrecipeName,
        description: parsed.description || `Subrecipe for ${subrecipeName}`,
        instructions: body,
        prompt: body,
    };
    fs.mkdirSync(destDir, { recursive: true });
    fs.writeFileSync(path.join(destDir, `${subrecipeName}.yaml`), yaml.stringify(recipe), 'utf-8');
}
/**
 * Render the main Goose recipe YAML for a workflow — the exact bytes
 * `syncWorkflowToGooseRecipe` writes to `<name>.yaml`. Extracted so the sync
 * writer and the doctor content-drift check (`workflowContentMatches`) render
 * from ONE source and can never disagree. Returns null on invalid frontmatter.
 */
function renderGooseRecipeYaml(workflowPath, name) {
    const frontmatter = parseWorkflowFrontmatter(workflowPath);
    if (!frontmatter)
        return null;
    const body = readWorkflowBody(workflowPath) || frontmatter.description || name;
    const subagents = selectedWorkflowSubagents(workflowPath, frontmatter.allowedAgents);
    const recipe = {
        version: '1.0.0',
        title: frontmatter.name || name,
        description: frontmatter.description || name,
        instructions: body,
        prompt: body,
    };
    if (frontmatter.model) {
        recipe.settings = { goose_model: frontmatter.model };
    }
    if (subagents.length > 0) {
        recipe.sub_recipes = subagents.map(subagentName => ({
            name: subagentName,
            path: `./${name}.subrecipes/${subagentName}.yaml`,
            description: `Workflow subrecipe ${subagentName}`,
        }));
    }
    return yaml.stringify(recipe);
}
function syncWorkflowToGooseRecipe(workflowPath, name, versionHome) {
    const frontmatter = parseWorkflowFrontmatter(workflowPath);
    if (!frontmatter) {
        return { success: false, error: `Workflow '${name}' has invalid WORKFLOW.md frontmatter` };
    }
    const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
    const recipePath = path.join(recipesDir, `${name}.yaml`);
    const subrecipesDir = path.join(recipesDir, `${name}.subrecipes`);
    const subagents = selectedWorkflowSubagents(workflowPath, frontmatter.allowedAgents);
    const recipeYaml = renderGooseRecipeYaml(workflowPath, name);
    if (recipeYaml == null) {
        return { success: false, error: `Workflow '${name}' has invalid WORKFLOW.md frontmatter` };
    }
    try {
        fs.mkdirSync(recipesDir, { recursive: true });
        if (fs.existsSync(subrecipesDir)) {
            fs.rmSync(subrecipesDir, { recursive: true, force: true });
        }
        for (const subagentName of subagents) {
            writeGooseSubrecipe(workflowPath, subagentName, subrecipesDir);
        }
        fs.writeFileSync(recipePath, recipeYaml, 'utf-8');
        return { success: true };
    }
    catch (err) {
        return { success: false, error: err.message };
    }
}
/**
 * True when workflow `name` materialized in `agent`'s version home byte-matches
 * what the writer would produce NOW from `sourcePath` — the content-drift
 * predicate `agents doctor` uses. Layout-aware per the same per-harness map the
 * writer (`syncWorkflowToVersion`) and detector use: Claude/default copy the
 * whole WORKFLOW.md tree (recursive dir compare); Kimi/Antigravity/OpenClaw/Grok
 * render one transformed file; Goose renders a recipe YAML. Antigravity's target
 * is the shared HOME-global dir, not a per-version path. Returns false when the
 * home copy is absent (surfaced as `missing` at the name level, not here).
 */
export function workflowContentMatches(agent, versionHome, name, sourcePath) {
    const root = workflowTargetRoot(agent, versionHome);
    const renderedMatches = (homePath, expected) => {
        let actual;
        try {
            actual = fs.readFileSync(homePath, 'utf-8');
        }
        catch {
            return false;
        }
        return normalizeResourceContent(actual) === normalizeResourceContent(expected);
    };
    switch (agent) {
        case 'kimi':
            return renderedMatches(path.join(root, name, 'SKILL.md'), transformWorkflowForKimi(sourcePath, name));
        case 'antigravity':
            return renderedMatches(path.join(root, `${name}.md`), transformWorkflowForAntigravity(sourcePath, name));
        case 'openclaw':
            return renderedMatches(path.join(root, `${name}.lobster`), transformWorkflowForOpenClaw(sourcePath, name));
        case 'grok':
            return renderedMatches(path.join(root, `${name}.rhai`), transformWorkflowForGrok(sourcePath, name));
        case 'goose': {
            const expected = renderGooseRecipeYaml(sourcePath, name);
            if (expected == null)
                return false;
            return renderedMatches(path.join(versionHome, '.config', 'goose', 'recipes', `${name}.yaml`), expected);
        }
        default:
            return dirsContentMatch(sourcePath, path.join(root, name));
    }
}
/** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
    if (agent === 'goose') {
        return syncWorkflowToGooseRecipe(workflowPath, name, versionHome);
    }
    try {
        if (agent === 'kimi') {
            const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
            const targetFile = path.join(targetDir, 'SKILL.md');
            if (fs.existsSync(targetFile)) {
                const marker = kimiWorkflowMarker(targetFile);
                if (marker !== name) {
                    return { success: false, error: `Kimi skill '${name}' already exists and is not managed by agents-cli` };
                }
            }
            fs.mkdirSync(targetDir, { recursive: true });
            fs.writeFileSync(targetFile, transformWorkflowForKimi(workflowPath, name), 'utf-8');
            return { success: true };
        }
        if (agent === 'antigravity') {
            const targetDir = workflowTargetRoot(agent, versionHome);
            const targetFile = path.join(targetDir, `${name}.md`);
            if (fs.existsSync(targetFile)) {
                const marker = antigravityWorkflowMarker(targetFile);
                if (marker !== name) {
                    return { success: false, error: `Antigravity workflow '${name}' already exists and is not managed by agents-cli` };
                }
            }
            fs.mkdirSync(targetDir, { recursive: true });
            fs.writeFileSync(targetFile, transformWorkflowForAntigravity(workflowPath, name), 'utf-8');
            return { success: true };
        }
        if (agent === 'openclaw') {
            const targetDir = workflowTargetRoot(agent, versionHome);
            const targetFile = path.join(targetDir, `${name}.lobster`);
            if (fs.existsSync(targetFile)) {
                const marker = openclawWorkflowMarker(targetFile);
                if (marker !== name) {
                    return { success: false, error: `OpenClaw workflow '${name}' already exists and is not managed by agents-cli` };
                }
            }
            fs.mkdirSync(targetDir, { recursive: true });
            fs.writeFileSync(targetFile, transformWorkflowForOpenClaw(workflowPath, name), 'utf-8');
            return { success: true };
        }
        if (agent === 'grok') {
            const targetDir = workflowTargetRoot(agent, versionHome);
            const targetFile = path.join(targetDir, `${name}.rhai`);
            if (fs.existsSync(targetFile)) {
                const marker = grokWorkflowMarker(targetFile);
                if (marker !== name) {
                    return { success: false, error: `Grok workflow '${name}' already exists and is not managed by agents-cli` };
                }
            }
            fs.mkdirSync(targetDir, { recursive: true });
            fs.writeFileSync(targetFile, transformWorkflowForGrok(workflowPath, name), 'utf-8');
            return { success: true };
        }
        const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
        fs.mkdirSync(workflowTargetRoot(agent, versionHome), { recursive: true });
        if (fs.existsSync(targetDir)) {
            fs.rmSync(targetDir, { recursive: true, force: true });
        }
        fs.cpSync(workflowPath, targetDir, { recursive: true });
        return { success: true };
    }
    catch (err) {
        return { success: false, error: err.message };
    }
}
/** Remove a workflow from a specific agent version home. */
export function removeWorkflowFromVersion(agent, version, name) {
    const versionHome = getVersionHomePath(agent, version);
    if (agent === 'antigravity') {
        const targetFile = path.join(workflowTargetRoot(agent, versionHome), `${name}.md`);
        if (!fs.existsSync(targetFile)) {
            return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
        }
        if (antigravityWorkflowMarker(targetFile) !== name) {
            return { success: false, error: `Antigravity workflow '${name}' is not managed by agents-cli` };
        }
        try {
            fs.rmSync(targetFile, { force: true });
            return { success: true };
        }
        catch (err) {
            return { success: false, error: err.message };
        }
    }
    if (agent === 'goose') {
        const recipePath = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.yaml`);
        const subrecipesDir = path.join(versionHome, '.config', 'goose', 'recipes', `${name}.subrecipes`);
        if (!fs.existsSync(recipePath) && !fs.existsSync(subrecipesDir)) {
            return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
        }
        try {
            if (fs.existsSync(recipePath))
                fs.rmSync(recipePath, { force: true });
            if (fs.existsSync(subrecipesDir))
                fs.rmSync(subrecipesDir, { recursive: true, force: true });
            return { success: true };
        }
        catch (err) {
            return { success: false, error: err.message };
        }
    }
    if (agent === 'openclaw') {
        const targetFile = path.join(workflowTargetRoot(agent, versionHome), `${name}.lobster`);
        if (!fs.existsSync(targetFile)) {
            return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
        }
        if (openclawWorkflowMarker(targetFile) !== name) {
            return { success: false, error: `OpenClaw workflow '${name}' is not managed by agents-cli` };
        }
        try {
            fs.rmSync(targetFile, { force: true });
            return { success: true };
        }
        catch (err) {
            return { success: false, error: err.message };
        }
    }
    if (agent === 'grok') {
        const targetFile = path.join(workflowTargetRoot(agent, versionHome), `${name}.rhai`);
        if (!fs.existsSync(targetFile)) {
            return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
        }
        if (grokWorkflowMarker(targetFile) !== name) {
            return { success: false, error: `Grok workflow '${name}' is not managed by agents-cli` };
        }
        try {
            fs.rmSync(targetFile, { force: true });
            return { success: true };
        }
        catch (err) {
            return { success: false, error: err.message };
        }
    }
    const targetPath = path.join(workflowTargetRoot(agent, versionHome), name);
    if (!fs.existsSync(targetPath)) {
        return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
    }
    try {
        if (agent === 'kimi') {
            const targetFile = path.join(targetPath, 'SKILL.md');
            if (kimiWorkflowMarker(targetFile) !== name) {
                return { success: false, error: `Kimi skill '${name}' is not managed by agents-cli` };
            }
        }
        fs.rmSync(targetPath, { recursive: true, force: true });
        return { success: true };
    }
    catch (err) {
        return { success: false, error: err.message };
    }
}
function parseSkillFrontmatter(filePath) {
    const content = fs.readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    if (lines[0] !== '---')
        return null;
    const endIndex = lines.slice(1).findIndex(l => l === '---');
    if (endIndex < 0)
        return null;
    const frontmatter = lines.slice(1, endIndex + 1).join('\n');
    const parsed = yaml.parse(frontmatter);
    return parsed && typeof parsed === 'object' ? parsed : null;
}
function kimiWorkflowMarker(filePath) {
    try {
        const fm = parseSkillFrontmatter(filePath);
        return fm?.type === 'flow' && typeof fm.agents_workflow === 'string' ? fm.agents_workflow : null;
    }
    catch {
        return null;
    }
}
function antigravityWorkflowMarker(filePath) {
    try {
        const fm = parseSkillFrontmatter(filePath);
        return typeof fm?.agents_workflow === 'string' ? fm.agents_workflow : null;
    }
    catch {
        return null;
    }
}
function openclawWorkflowMarker(filePath) {
    try {
        const parsed = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
        if (!parsed || typeof parsed !== 'object' || !parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) {
            return null;
        }
        const marker = parsed.env[OPENCLAW_WORKFLOW_MARKER_ENV];
        return typeof marker === 'string' ? marker : null;
    }
    catch {
        return null;
    }
}
/** Iterate all installed (agent, version) pairs that support workflows. */
export function iterWorkflowsCapableVersions(filter) {
    const result = [];
    for (const agentId of capableAgents('workflows')) {
        if (filter?.agent && filter.agent !== agentId)
            continue;
        const versions = listInstalledVersions(agentId);
        for (const version of versions) {
            if (filter?.version && filter.version !== version)
                continue;
            // Honour version floors (e.g. grok workflows since 0.2.111).
            if (!supports(agentId, 'workflows', version).ok)
                continue;
            result.push({ agent: agentId, version });
        }
    }
    return result;
}
