Class: Pikuri::Skill::Catalog

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/skill/catalog.rb

Overview

Discovery + validation of skills — Markdown files with YAML frontmatter that the agent loads on demand via Tool.

A skill is a directory containing a SKILL.md file. The frontmatter declares the skill’s name and description; everything in the directory (helper scripts, reference notes, templates) is treated as sidecar content the LLM can pull in via the regular read tool after loading the skill. Pikuri implements the Agent Skills standard leniently — invalid frontmatter is warned about but the skill still loads, except a missing description which is fatal (no description means the LLM has no signal for when to invoke it).

Catalog vs. tool

Catalog owns the discovery layer: where skills live, how the frontmatter is parsed, what wins on a name collision. The Tool class is the loading layer: it receives a catalog at construction and looks names up against it when the LLM invokes skill. The two are paired by Extension — non-empty catalog ⇒Tool is appended to the agent’s tool list and the catalog’s prompt block is appended to the system prompt.

The seam

Catalog is an abstract base with two bundled implementations: Empty (the EMPTY singleton, used by pikuri-chat and as the default for any caller that doesn’t have a filesystem story) and Bundled (the on-disk scanner). The seam lets future hosts (e.g. an in-editor pikuri reading skills from a virtual filesystem, or a synthetic test catalog) swap in without touching Tool or Extension.

Direct Known Subclasses

Bundled, Empty

Defined Under Namespace

Classes: Bundled, Empty, Skill

Constant Summary collapse

MAX_NAME_LENGTH =

Frontmatter name cap per the Agent Skills standard.

64
MAX_DESCRIPTION_LENGTH =

Frontmatter description cap per the Agent Skills standard.

1024
SKILL_DIR_NAMES =

The three skill directories pikuri scans, in precedence order. .pikuri first (the user’s own pikuri-specific skills), .claude second (so existing Claude Code skills travel along for free), .agents last (the cross-harness convention PI also honors). Same names are used for both global roots (under ~/) and project roots (relative to CWD, walked up).

['.pikuri/skills', '.claude/skills', '.agents/skills'].freeze

Instance Method Summary collapse

Instance Method Details

#empty?Boolean

Returns true when #list is empty. Extension keys its auto-wiring (system-prompt augmentation + Tool registration) off this — empty catalog ⇒ no surface change.

Returns:

  • (Boolean)

    true when #list is empty. Extension keys its auto-wiring (system-prompt augmentation + Tool registration) off this — empty catalog ⇒ no surface change.



92
93
94
# File 'lib/pikuri/skill/catalog.rb', line 92

def empty?
  list.empty?
end

#format_for_promptString

System-prompt block advertising every loaded skill to the LLM. Follows the Agent Skills standard’s XML shape (PI uses the same one) so a skill folder authored against any compliant harness renders consistently here.

Returns “” for an empty catalog so callers can unconditionally concatenate without an if. The leading nn separates the block from whatever the caller’s static system prompt ends with.

Returns:

  • (String)


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/pikuri/skill/catalog.rb', line 106

def format_for_prompt
  return '' if empty?

  lines = [
    '',
    '',
    'The following skills provide specialized instructions for specific tasks.',
    "Use the `skill` tool with the skill's name to load its full instructions.",
    '',
    '<available_skills>'
  ]
  list.each do |skill|
    lines << '  <skill>'
    lines << "    <name>#{escape_xml(skill.name)}</name>"
    lines << "    <description>#{escape_xml(skill.description)}</description>"
    lines << "    <location>#{escape_xml(skill.location)}</location>"
    lines << '  </skill>'
  end
  lines << '</available_skills>'
  lines.join("\n")
end

#get(name) ⇒ Skill?

Look up a skill by name.

Parameters:

  • name (String)

Returns:

Raises:

  • (NotImplementedError)

    in the abstract base



85
86
87
# File 'lib/pikuri/skill/catalog.rb', line 85

def get(name)
  raise NotImplementedError, "#{self.class}#get must be implemented"
end

#listArray<Skill>

Skills available to the LLM, in stable insertion order (which equals the order they were discovered, which equals precedence).

Returns:

Raises:

  • (NotImplementedError)

    in the abstract base



76
77
78
# File 'lib/pikuri/skill/catalog.rb', line 76

def list
  raise NotImplementedError, "#{self.class}#list must be implemented"
end