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>/
↓
Verifier (optional, --verify) check the skill's code against the
gem's actual source; correct mismatches in place
↓
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,
and after --verify: a "verification" block with
source provenance + structured changes)
Cache::ROOT is set once at load time from GEMSKILL_DIR (default: ~/.gem/skills).
read_metadata / write_skill / merge_metadata let the verifier rewrite a
cached skill and annotate its metadata without clobbering the original
generated_at, model, or sources.
Verifier¶
lib/gem/skill/verifier.rb
Optional second pass, enabled by --verify. Generation synthesizes prose
sources (README, changelog, examples) which are frequently stale or wrong about
exact signatures. The verifier re-checks the generated skill against the gem's
actual source code — the only source of truth — and corrects mismatched
method signatures, default argument values, visibility, return values, and
behavioral claims.
Verifier.new(gem_name, version, model:).verify(skill_content)
# => Result(content:, changes:, changed:, verifiable:, source:, model:)
Ground truth comes from Fetcher#source_code (the gem's lib/**/*.rb), and
Fetcher#source_manifest records which files were examined. Whether the skill
actually changed is decided by a deterministic diff of the content before and
after — not by trusting the model's self-report — so the exit code is reliable.
If no installed source is available, verifiable is false and the skill is left
untouched.
Each correction in changes is a structured, issue-ready Hash
(category, symbol, skill_section, source_location, was, now,
detail, source_evidence) — detailed enough to file a documentation bug
against the gem. The Runner writes these, plus source provenance, into the
verification block of metadata.json.
Linker¶
lib/gem/skill/linker.rb
Creates and manages directory symlinks in the project's skill directory
(default .claude/skills/, Claude Code's convention):
The directory is Linker.project_dir, read from GEMSKILL_PROJECT_DIR each call
(default .claude/skills). Codex users set it to .agents or .codex so
bundle skill links into a Codex root instead. Symlinks point to the version
directory, not directly to SKILL.md; the assistant 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).
The cache itself is assistant-neutral. SKILL.md is a shared format; other
assistants read it from their own roots. Note that linking only makes a skill
available — some assistants (e.g. Codex) require it to be in the available-skills
list or referenced explicitly before it's active.
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:, verify:)
# => Runner::Result(error:, verify_fixed:, change_count:)
Captures errors into the result rather than raising, so the caller (the
concurrent fiber) can record them without killing other in-flight fibers. When
verify: is set, the result's verify_fixed lets the CLI aggregate across all
gems and exit 2 (EXIT_VERIFY_FIXED) if any skill was corrected.
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.