How It Works¶
gem-skill is built as a pipeline of independent modules. Each has a single responsibility and can be used or tested in isolation.
Pipeline overview¶
Gemfile.lock / gem name
↓
Lockfile parse direct deps + gemspec deps
↓
Fetcher collect documentation from multiple sources
↓
Generator call LLM, produce SKILL.md content
↓
Cache write to ~/.gem/skills/<gem>/<version>/
↓
Linker symlink .claude/skills/<gem> → cache dir
Runner.install_skill is the glue that drives steps 2–5 for a single gem.
The CLI commands (gem skill, bundle skill) fan it out concurrently across
multiple gems using async fibers.
Modules¶
Lockfile¶
lib/gem/skill/lockfile.rb
Parses Gemfile.lock to produce a { gem_name => version } hash of the gems
to process. Reads from two sections:
DEPENDENCIES— direct deps listed inGemfilePATH→specs:— runtime deps from anygemspecreferenced bygemspecinGemfile
Versions are resolved from the GEM → specs: section, which contains the full
lockfile-resolved version for every gem.
Fetcher¶
lib/gem/skill/fetcher.rb
Collects documentation from up to three sources, tried in priority order:
- Local gem install — reads
READMEandCHANGELOGfrom the gem's install directory viaGem::Specification - RubyGems API — fetches summary, runtime dependencies, source URI
- GitHub raw README — fetched when the gem is not installed locally; tries
mainthenmasterbranches, and four common README filename variants
Content is truncated at 60,000 characters per source to avoid blowing the LLM context window.
Generator¶
lib/gem/skill/generator.rb
Calls the LLM via ruby_llm. Constructs a detailed prompt instructing the
model to produce a structured SKILL.md covering:
- Overview, Installation, Core API, Common Patterns, Gotchas, Configuration, Testing
Supports both streaming (live output) and non-streaming modes. Strips any markdown code fence wrapper the model adds despite being told not to.
The model is configurable via GEMSKILL_MODEL or --model.
Cache¶
lib/gem/skill/cache.rb
Manages the global skill cache. Structure:
~/.gem/skills/ (GEMSKILL_DIR)
└── <gem_name>/
└── <version>/
├── SKILL.md
└── metadata.json (gem, version, model, generated_at, sources)
Cache::ROOT is set once at load time from GEMSKILL_DIR (default: ~/.gem/skills).
Linker¶
lib/gem/skill/linker.rb
Creates and manages directory symlinks in .claude/skills/ inside a project:
Symlinks point to the version directory, not directly to SKILL.md.
Claude Code discovers SKILL.md by reading inside the linked directory.
Linker.prune_dead_links removes any symlink whose target no longer exists
in the cache (e.g. after gem skill purge).
Runner¶
lib/gem/skill/runner.rb
Shared core used by both CLI commands. Drives one gem through the cache-check → generate → link sequence:
Runner.install_skill(gem_name, version, spinner, force:, model:)
# Returns nil on success, error message string on failure
Returns the error message rather than raising, so the caller (the concurrent fiber) can record it without killing other in-flight fibers.
Concurrency¶
Both CLI commands use the async gem with Async::Barrier:
Async do
barrier = Async::Barrier.new
gems.each do |gem_name, version|
barrier.async { Runner.install_skill(...) }
end
barrier.wait
ensure
barrier.stop
end
Each gem gets its own fiber. Fibers yield to the event loop during network I/O (HTTP fetches, LLM API calls), so all gems make progress concurrently on a single thread. This is more memory-efficient than one thread per gem.
Plugin architecture¶
gem-skill registers itself in two ways:
RubyGems plugin (lib/rubygems_plugin.rb)¶
Auto-loaded by RubyGems on every gem command via the rubygems_plugin
naming convention. Prepends Gem::Skill::InstallSkillOption onto
Gem::Commands::InstallCommand to add the --with-skill flag.
After all gem installs complete, Gem.post_install collects gem names/versions
into a pending list, and at_exit fires generate_pending_skills to process
them concurrently.
Bundler plugin (plugins.rb)¶
Registered via bundle plugin install gem-skill (or gem skill setup).
Bundler's plugin API loads plugins.rb and discovers the
Gem::Skill::BundlerPlugin class, which routes bundle skill SUBCOMMAND to
Gem::Skill::BundlerCommand.