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")
- AGENT_NEUTRAL_PATHS =
Agent-neutral skill dirs (the emerging ‘npx skills` / Gemini CLI / goose convention): discovered in ADDITION to the configured rubino paths, scanned before them so a rubino-path skill of the same name wins (lowest precedence). No behavior change when the dirs are absent.
[".agents/skills", "~/.agents/skills"].freeze
- 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.
42 43 44 45 46 47 48 49 |
# File 'lib/rubino/skills/registry.rb', line 42 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.
62 63 64 65 66 |
# File 'lib/rubino/skills/registry.rb', line 62 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.
56 57 58 |
# File 'lib/rubino/skills/registry.rb', line 56 def self.trusted(**) new(include_project_local: project_local_trusted?, **) end |
Instance Method Details
#all ⇒ Object
Returns all discovered skills (discovers on first call)
93 94 95 96 |
# File 'lib/rubino/skills/registry.rb', line 93 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).
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/rubino/skills/registry.rb', line 72 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).
129 130 131 |
# File 'lib/rubino/skills/registry.rb', line 129 def enabled all.select { |skill| enabled?(skill.name) } end |
#enabled?(name) ⇒ Boolean
Whether a skill is enabled (default-enabled when no state row exists).
134 135 136 |
# File 'lib/rubino/skills/registry.rb', line 134 def enabled?(name) state_repository.enabled?(name) end |
#find(name) ⇒ Object
Finds a skill by name
99 100 101 102 |
# File 'lib/rubino/skills/registry.rb', line 99 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.
114 115 116 117 118 119 |
# File 'lib/rubino/skills/registry.rb', line 114 def load_skill(name) skill = find(name) return nil unless skill skill.content end |
#names ⇒ Object
Returns skill names
122 123 124 |
# File 'lib/rubino/skills/registry.rb', line 122 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).
107 108 109 |
# File 'lib/rubino/skills/registry.rb', line 107 def summaries enabled.map(&:summary) end |