Class: Rubino::Skills::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/skills/registry.rb

Overview

Discovers and manages skills from configured paths. Skills are loaded lazily - metadata is parsed upfront but full content is only loaded when the skill is invoked.

Constant Summary collapse

FLAT_GLOB =

Flat-file skills: <dir>/<name>.md (legacy, kept for back-compat).

"*.md"
DIR_GLOB =

Directory skills: <dir>/<name>/SKILL.md (Claude skill layout).

File.join("*", "SKILL.md")
BUILTIN_SKILLS_DIR =

Skills shipped *inside the gem* (skills/<name>/SKILL.md at the gem root, packaged via the gemspec’s git-ls-files list). These are ALWAYS discovered — they don’t depend on the user’s skills.paths config (which ‘setup` freezes into config.yml) and they survive the folder-trust filter because this is an absolute path under the installed gem, owned by the user, not anything a visited repo can influence. This is how built-in skills (e.g. ruby-expert) reach every install with no copy step and update automatically on gem upgrade.

File.expand_path("../../../skills", __dir__)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config: nil, state_repository: nil, include_project_local: true, include_builtin: nil) ⇒ Registry

include_project_local controls whether the cwd ‘.rubino/skills` catalogue is discovered. Folder-trust passes false for an UNtrusted primary root so a hostile repo’s skill descriptions can’t be auto- injected into the system prompt before the user vouches for the folder (the home ‘~/.rubino/skills` catalogue is always loaded — it’s the user’s own, not attacker-controllable by cd-ing into a repo). include_builtin controls whether the gem-bundled BUILTIN_SKILLS_DIR is scanned. Always on in production (built-ins ship with every install). When left nil it falls back to the ‘skills.include_builtin` config key (default true), so a caller that only has the config — like the prompt assembler, which builds its own Registry — can still opt out; tests that assert an exact catalogue pass false to isolate from the shipped skills.



36
37
38
39
40
41
42
43
# File 'lib/rubino/skills/registry.rb', line 36

def initialize(config: nil, state_repository: nil, include_project_local: true, include_builtin: nil)
  @config = config || Rubino.configuration
  @state_repository = state_repository
  @include_project_local = include_project_local
  @include_builtin = include_builtin.nil? ? (@config.dig("skills", "include_builtin") != false) : include_builtin
  @skills = {}
  @discovered = false
end

Class Method Details

.project_local_trusted?Boolean

Mirrors Context::PromptAssembler#project_local_trusted?: trust-gate the cwd, but never let the check itself break discovery on a real error.

Returns:

  • (Boolean)


56
57
58
59
60
# File 'lib/rubino/skills/registry.rb', line 56

def self.project_local_trusted?
  Rubino::Trust.trusted?(Rubino::Workspace.primary_root)
rescue StandardError
  true
end

.trustedObject

A registry aligned with the prompt assembler’s folder-trust gate (#63): in an untrusted cwd the project-local catalogue is excluded, so the /skills picker and activation surface only skills the assembler will actually pin into the system prompt — never a chip claiming an active skill whose SKILL.md is withheld.



50
51
52
# File 'lib/rubino/skills/registry.rb', line 50

def self.trusted(**)
  new(include_project_local: project_local_trusted?, **)
end

Instance Method Details

#allObject

Returns all discovered skills (discovers on first call)



87
88
89
90
# File 'lib/rubino/skills/registry.rb', line 87

def all
  discover! unless @discovered
  @skills.values
end

#discover!Object

Discovers all available skills from configured paths. Both the flat layout (<name>.md) and the directory layout (<name>/SKILL.md) are supported. When a name collides, the directory skill wins (it is the richer unit: it can carry bundled references/scripts/assets).



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/rubino/skills/registry.rb', line 66

def discover!
  previously_discovered = @discovered
  known_before = @skills.keys
  @skills.clear
  skill_paths.each do |dir|
    expanded = resolve_path(dir)
    next unless File.directory?(expanded)

    add_skills(Dir.glob(File.join(expanded, FLAT_GLOB)))
    add_skills(Dir.glob(File.join(expanded, DIR_GLOB)))
  end
  @discovered = true
  # Skill CREATION has no in-process tool — the agent writes files — so the
  # cleanest available signal is a RE-scan surfacing a skill we hadn't seen
  # before. Only count on a re-discover (not the first scan, which is just
  # initial enumeration) so existing skills aren't booked as "created".
  count_created!(known_before) if previously_discovered
  @skills
end

#enabledObject

Skills not toggled off in the StateRepository (default-enabled when no row exists). Single source of truth for the enabled-filter shared by the system-prompt index (via #summaries) and the ‘skill` tool (via this).



123
124
125
# File 'lib/rubino/skills/registry.rb', line 123

def enabled
  all.select { |skill| enabled?(skill.name) }
end

#enabled?(name) ⇒ Boolean

Whether a skill is enabled (default-enabled when no state row exists).

Returns:

  • (Boolean)


128
129
130
# File 'lib/rubino/skills/registry.rb', line 128

def enabled?(name)
  state_repository.enabled?(name)
end

#find(name) ⇒ Object

Finds a skill by name



93
94
95
96
# File 'lib/rubino/skills/registry.rb', line 93

def find(name)
  discover! unless @discovered
  @skills[name.to_s]
end

#load_skill(name) ⇒ Object

Loads and returns the full content of a skill by name. Returns nil when the skill is unknown; the disabled case is surfaced by #enabled? so the caller (SkillTool) can give a distinct “disabled” message.



108
109
110
111
112
113
# File 'lib/rubino/skills/registry.rb', line 108

def load_skill(name)
  skill = find(name)
  return nil unless skill

  skill.content
end

#namesObject

Returns skill names



116
117
118
# File 'lib/rubino/skills/registry.rb', line 116

def names
  all.map(&:name)
end

#summariesObject

Returns skill summaries for prompt inclusion (names + descriptions only). Disabled skills (per StateRepository) are excluded so a skill toggled off never appears in the system-prompt index (Skills::PromptIndex).



101
102
103
# File 'lib/rubino/skills/registry.rb', line 101

def summaries
  enabled.map(&:summary)
end