Jido.AI.Skill.Discovery (Jido AI v2.3.0)

Copy Markdown View Source

Discovers skills from project-level and user-level skill directories.

Implements the agentskills.io discovery spec:

  • Project-level: .agents/skills/ directory
  • User-level: ~/.agents/skills/ directory

Precedence Rules

Project-level skills override user-level skills when both have the same name. This follows deterministic precedence: project > user

Metadata Tracking

Each discovered skill includes:

  • name - Skill identifier
  • description - Brief description from SKILL.md frontmatter
  • skill_md_path - Absolute path to the SKILL.md file
  • root_dir - Skill root directory (parent of SKILL.md location)
  • scope - :project or :user indicating source
  • source_metadata - Additional discovery metadata

Usage

# Discover all skills
{:ok, skills} = Jido.AI.Skill.Discovery.discover()

# Discover from specific paths only
{:ok, skills} = Jido.AI.Skill.Discovery.discover_from([".agents/skills/"])

# Get a single skill by name
{:ok, spec} = Jido.AI.Skill.Discovery.find("code-review")

Summary

Functions

Discovers skills from both project and user directories.

Returns bounded, symlink-safe SKILL.md paths without parsing them.

Discovers skills from a list of specific paths.

Discovers skills from the project-level .agents/skills/ directory.

Discovers skills from the user-level ~/.agents/skills/ directory.

Finds a specific skill by name across all discovery sources.

Converts discovery metadata into a full Spec by loading the SKILL.md.

Types

discovery_metadata()

@type discovery_metadata() :: %{
  name: String.t(),
  description: String.t() | nil,
  skill_md_path: String.t(),
  root_dir: String.t(),
  scope: scope(),
  source_metadata: map()
}

scope()

@type scope() :: :project | :user | :custom

Functions

discover(opts \\ [])

@spec discover(keyword()) :: {:ok, [discovery_metadata()]} | {:error, term()}

Discovers skills from both project and user directories.

Returns skills with project-level taking precedence over user-level when names collide.

Returns

  • {:ok, [discovery_metadata]} - List of discovered skill metadata
  • {:error, reason} - Discovery failed

Examples

{:ok, skills} = Jido.AI.Skill.Discovery.discover()
# skills will have project-level skills overriding user-level

discover_files(paths, opts \\ [])

@spec discover_files(
  [String.t()],
  keyword()
) :: {:ok, [String.t()]} | {:error, term()}

Returns bounded, symlink-safe SKILL.md paths without parsing them.

This is the shared scanner used by discovery and registry loading. It accepts the same bounds, exclusions, and trust options as discover_from/2.

discover_from(paths, opts \\ [])

@spec discover_from(
  [String.t()],
  keyword()
) :: {:ok, [discovery_metadata()]} | {:error, term()}

Discovers skills from a list of specific paths.

Useful for scanning custom directories or testing.

Options

  • :scope - Assign scope metadata (:project or :user), defaults to :custom
  • :max_depth - Maximum directory depth to scan (default: 6)
  • :max_directories - Maximum directories visited across all paths (default: 2000)
  • :exclude_directories - Directory basenames to skip (default: .git and node_modules)
  • :trust - true, false, or a one-argument function that approves each root path

Examples

{:ok, skills} = Jido.AI.Skill.Discovery.discover_from(["priv/skills/"])
{:ok, skills} = Jido.AI.Skill.Discovery.discover_from(["priv/skills/"], scope: :project)

discover_from_project(base_path \\ ".agents/skills", opts \\ [])

@spec discover_from_project(
  String.t(),
  keyword()
) :: {:ok, [discovery_metadata()]} | {:error, term()}

Discovers skills from the project-level .agents/skills/ directory.

Returns

  • {:ok, [discovery_metadata]} - List of project-level skills
  • {:ok, []} - Directory doesn't exist or is empty
  • {:error, reason} - Discovery failed

discover_from_user(base_path \\ default_user_path(), opts \\ [])

@spec discover_from_user(
  String.t(),
  keyword()
) :: {:ok, [discovery_metadata()]} | {:error, term()}

Discovers skills from the user-level ~/.agents/skills/ directory.

Returns

  • {:ok, [discovery_metadata]} - List of user-level skills
  • {:ok, []} - Directory doesn't exist or is empty
  • {:error, reason} - Discovery failed

find(name, paths \\ nil, opts \\ [])

@spec find(String.t(), [String.t()] | nil, keyword()) ::
  {:ok, discovery_metadata()} | {:error, term()}

Finds a specific skill by name across all discovery sources.

Returns the first matching skill with project-level taking precedence.

When paths is given, searches those directories instead of the default project/user discovery sources — useful for scoped lookups and testing.

Examples

{:ok, metadata} = Jido.AI.Skill.Discovery.find("code-review")
{:error, :not_found} = Jido.AI.Skill.Discovery.find("unknown-skill")
{:ok, metadata} = Jido.AI.Skill.Discovery.find("local-skill", ["priv/skills/"])

to_spec(metadata, opts \\ [])

@spec to_spec(
  discovery_metadata() | map(),
  keyword()
) :: {:ok, Jido.AI.Skill.Spec.t()} | {:error, term()}

Converts discovery metadata into a full Spec by loading the SKILL.md.

Examples

{:ok, metadata} = Jido.AI.Skill.Discovery.find("code-review")
{:ok, spec} = Jido.AI.Skill.Discovery.to_spec(metadata)