class LittleGhost::Skills::Catalog
A Catalog lets an agent discover focused instructions without putting every skill in its prompt. The model sees short descriptions first and can load a skill’s full instructions when the task calls for them.
catalog = LittleGhost::Skills::Catalog.new(paths: ["app/skills"]) catalog.names # => ["refund_policy", "search_orders"] catalog.discovery_prompt.include?("refund_policy") # => true catalog.tool # a LittleGhost::Tool that loads full instructions on demand
Each immediate child directory may contain one SKILL.md with YAML front matter. Symbolic-link escapes, unsafe names, oversized files, and invalid YAML are rejected or skipped before instructions reach a model. Optional resource listings are limited by count and depth.
Choosing skill sources
Skill files become model instructions, so keep configured roots under application control and non-user-writable. The allowed-tools field tells the model what a skill expects; the Agent’s Tool list and each Tool’s application checks still decide what can run. For a workspace:// resource root, the Catalog verifies the named read-only grant and rejects direct writable aliases it can identify. LittleGhost cannot identify every alias created by an outer container or mount namespace, so the application must not expose the same files through another writable bind mount.
Public Class Methods
# File lib/little_ghost/skills/catalog.rb, line 51 def initialize( paths:, max_skills: DEFAULT_MAX_SKILLS, max_file_bytes: DEFAULT_MAX_FILE_BYTES, max_resource_files: DEFAULT_MAX_RESOURCE_FILES, only: nil, resource_root: nil, workspace: nil, sandbox: nil ) @paths = PathSet.new(paths) @max_skills = positive_integer(max_skills, :max_skills) @max_file_bytes = positive_integer(max_file_bytes, :max_file_bytes) @max_resource_files = positive_integer(max_resource_files, :max_resource_files) @only = Array(only).map(&:to_s).freeze if only @resource_root = ResourceRoot.normalize(resource_root) validate_workspace_resource_root!(workspace, sandbox) @skills = load_skills validate_workspace_resource_aliases!(sandbox) end
Loads valid skills immediately using the supplied safety limits. resource_root may be an absolute process-visible path. A workspace://name reference also requires workspace and sandbox; it must resolve to every configured skill root through a read-only file-tool grant.
Public Instance Methods
Source
# File lib/little_ghost/skills/catalog.rb, line 88 def discovery_prompt return "" if @skills.empty? lines = ["<available_skills>"] @skills.each_value do |skill| lines.concat([ "<skill>", "<name>#{ERB::Util.html_escape(skill.name)}</name>", "<description>#{ERB::Util.html_escape(skill.description)}</description>", "<location>#{ERB::Util.html_escape(skill.path)}</location>", "</skill>" ]) end lines << "</available_skills>" lines.join("\n") end
Produces the escaped, metadata-only prompt used for discovery.
Source
# File lib/little_ghost/skills/catalog.rb, line 73 def each(&block) @skills.each_value(&block) end
Yields each Skill in lookup order.
Source
# File lib/little_ghost/skills/catalog.rb, line 78 def fetch(name) @skills.fetch(name.to_s) { raise ConfigurationError, "Unknown skill: #{name}" } end
Finds the named Skill or raises ConfigurationError.
Source
# File lib/little_ghost/skills/catalog.rb, line 131 def format(skill) parts = [skill.instructions] metadata = [] metadata << "Allowed tools: #{skill.allowed_tools.join(", ")}" unless skill.allowed_tools.empty? metadata << "Compatibility: #{skill.compatibility}" if skill.compatibility metadata << "Location: #{skill.path}" parts << "\n---\n#{metadata.join("\n")}" unless metadata.empty? resources = skill_resources(skill) unless resources.empty? parts << "\nAvailable resources:\n#{resources.map { |path| " #{path}" }.join("\n")}" end parts.join("\n") end
Formats one Skill, including allowed tools, compatibility, and bounded resource paths.
Source
# File lib/little_ghost/skills/catalog.rb, line 83 def names @skills.keys.freeze end
Lists immutable skill names in lookup order.
Source
# File lib/little_ghost/skills/catalog.rb, line 106 def tool catalog = self Tool.define( name: "skills", description: <<~DESCRIPTION.strip, Activate a skill to load its full instructions. Use this tool to load the complete instructions for a skill listed in the available_skills section of your system prompt. DESCRIPTION input_schema: { type: "object", properties: {skill_name: {type: "string", description: "Name of the skill to activate."}}, required: ["skill_name"], additionalProperties: false } ) do |input| catalog.format(catalog.fetch(input.fetch("skill_name"))) rescue ConfigurationError => error raise ToolError, error.message end end
Exposes full instructions on demand through a skills Tool.