Class: Rubino::Skills::Registry
- Inherits:
-
Object
- Object
- Rubino::Skills::Registry
- 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.("../../../skills", __dir__)
Class Method Summary collapse
-
.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.
-
.trusted ⇒ Object
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.
Instance Method Summary collapse
-
#all ⇒ Object
Returns all discovered skills (discovers on first call).
-
#discover! ⇒ Object
Discovers all available skills from configured paths.
-
#enabled ⇒ Object
Skills not toggled off in the StateRepository (default-enabled when no row exists).
-
#enabled?(name) ⇒ Boolean
Whether a skill is enabled (default-enabled when no state row exists).
-
#find(name) ⇒ Object
Finds a skill by name.
-
#initialize(config: nil, state_repository: nil, include_project_local: true, include_builtin: nil) ⇒ Registry
constructor
include_project_localcontrols whether the cwd ‘.rubino/skills` catalogue is discovered. -
#load_skill(name) ⇒ Object
Loads and returns the full content of a skill by name.
-
#names ⇒ Object
Returns skill names.
-
#summaries ⇒ Object
Returns skill summaries for prompt inclusion (names + descriptions only).
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.
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 |
.trusted ⇒ Object
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
#all ⇒ Object
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| = resolve_path(dir) next unless File.directory?() add_skills(Dir.glob(File.join(, FLAT_GLOB))) add_skills(Dir.glob(File.join(, 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 |
#enabled ⇒ Object
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).
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 |
#names ⇒ Object
Returns skill names
116 117 118 |
# File 'lib/rubino/skills/registry.rb', line 116 def names all.map(&:name) end |
#summaries ⇒ Object
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 |