Skip to content

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 in Gemfile
  • PATHspecs: — runtime deps from any gemspec referenced by gemspec in Gemfile

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:

  1. Local gem install — reads README and CHANGELOG from the gem's install directory via Gem::Specification
  2. RubyGems API — fetches summary, runtime dependencies, source URI
  3. GitHub raw README — fetched when the gem is not installed locally; tries main then master branches, 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):

<project_dir>/<gem_name>  →  ~/.gem/skills/<gem_name>/<version>/

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.