gem-skill 0.1.3 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/CLAUDE.md +63 -0
- data/README.md +68 -15
- data/docs/cache.md +82 -5
- data/docs/commands/bundle-skill.md +12 -3
- data/docs/commands/gem-skill.md +91 -6
- data/docs/configuration.md +61 -0
- data/docs/how-it-works.md +58 -9
- data/docs/index.md +15 -11
- data/docs/skill-files.md +73 -10
- data/lib/gem/skill/cache.rb +24 -0
- data/lib/gem/skill/cli/bundle_command.rb +57 -22
- data/lib/gem/skill/cli/gem_command.rb +171 -9
- data/lib/gem/skill/fetcher.rb +52 -0
- data/lib/gem/skill/frontmatter.rb +64 -0
- data/lib/gem/skill/generator.rb +46 -13
- data/lib/gem/skill/linker.rb +17 -3
- data/lib/gem/skill/runner.rb +75 -9
- data/lib/gem/skill/verifier.rb +139 -0
- data/lib/gem/skill/version.rb +1 -1
- data/lib/gem/skill.rb +36 -1
- data/ruby-gem-skills/SKILL.md +53 -0
- metadata +37 -5
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "ruby_llm/providers/lms"
|
|
5
|
+
require "ruby_llm/providers/apfel"
|
|
6
|
+
|
|
7
|
+
module Gem::Skill
|
|
8
|
+
# Second-pass quality gate for a generated SKILL.md.
|
|
9
|
+
#
|
|
10
|
+
# Generation synthesizes prose sources (README, changelog, examples) which are
|
|
11
|
+
# frequently wrong or stale about exact signatures. The verifier re-checks the
|
|
12
|
+
# generated skill against the gem's ACTUAL source code — the only source of
|
|
13
|
+
# truth — and corrects mismatched method signatures, default argument values,
|
|
14
|
+
# visibility, return values, and behavioral claims.
|
|
15
|
+
#
|
|
16
|
+
# Whether the skill actually changed is decided by a deterministic diff of the
|
|
17
|
+
# content before and after, not by trusting the model's self-report, so callers
|
|
18
|
+
# can rely on #changed? for an exit code and a "fixed" flag.
|
|
19
|
+
class Verifier
|
|
20
|
+
BEGIN_MARK = "===BEGIN SKILL==="
|
|
21
|
+
END_MARK = "===END SKILL==="
|
|
22
|
+
|
|
23
|
+
SYSTEM_INSTRUCTIONS = <<~SYSTEM
|
|
24
|
+
You verify a generated Claude Code SKILL.md for a Ruby gem against the gem's
|
|
25
|
+
ACTUAL SOURCE CODE. The source code is the only source of truth. READMEs,
|
|
26
|
+
changelogs, and docstrings are frequently stale or wrong; when the SKILL.md
|
|
27
|
+
disagrees with the source, the source always wins.
|
|
28
|
+
|
|
29
|
+
Check every concrete claim against the source: method signatures, default
|
|
30
|
+
argument values, keyword vs positional arguments, public/private/protected
|
|
31
|
+
visibility, return values, constant and class/module names, default option
|
|
32
|
+
values, and described runtime behavior (including what arguments a yielded
|
|
33
|
+
block actually receives). Correct anything the source contradicts.
|
|
34
|
+
|
|
35
|
+
Rules:
|
|
36
|
+
- Do NOT invent APIs, methods, or options that are absent from the source.
|
|
37
|
+
- Do NOT restructure, re-style, or "improve" content that is already correct.
|
|
38
|
+
Preserve correct text verbatim so the diff stays minimal.
|
|
39
|
+
- Only change what the source proves is wrong.
|
|
40
|
+
SYSTEM
|
|
41
|
+
|
|
42
|
+
PROMPT = <<~PROMPT
|
|
43
|
+
Verify the SKILL.md below for "%<gem_name>s" v%<version>s against the gem's
|
|
44
|
+
source code. Correct every claim the source contradicts.
|
|
45
|
+
|
|
46
|
+
Output ONLY the full corrected SKILL.md in raw Markdown (even if you change
|
|
47
|
+
nothing), wrapped exactly between these marker lines and with no other text:
|
|
48
|
+
%<begin_mark>s
|
|
49
|
+
<corrected SKILL.md here>
|
|
50
|
+
%<end_mark>s
|
|
51
|
+
|
|
52
|
+
============================================================
|
|
53
|
+
CURRENT SKILL.md
|
|
54
|
+
============================================================
|
|
55
|
+
|
|
56
|
+
%<skill>s
|
|
57
|
+
|
|
58
|
+
============================================================
|
|
59
|
+
GEM SOURCE CODE (ground truth)
|
|
60
|
+
============================================================
|
|
61
|
+
|
|
62
|
+
%<source>s
|
|
63
|
+
PROMPT
|
|
64
|
+
|
|
65
|
+
# content: the (possibly corrected) skill markdown
|
|
66
|
+
# changed: true iff content differs from the original (diff-based)
|
|
67
|
+
# verifiable: false when no source was available to check against
|
|
68
|
+
# model: the model used for verification
|
|
69
|
+
Result = Data.define(:content, :changed, :verifiable, :model) do
|
|
70
|
+
def changed? = changed
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
attr_reader :gem_name, :version, :model
|
|
74
|
+
|
|
75
|
+
def initialize(gem_name, version, model: Generator::DEFAULT_MODEL)
|
|
76
|
+
@gem_name = gem_name
|
|
77
|
+
@version = version
|
|
78
|
+
@model = model
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Verify skill_content against the gem source. Returns a Result.
|
|
82
|
+
def verify(skill_content)
|
|
83
|
+
fetcher = Fetcher.new(gem_name, version)
|
|
84
|
+
source = fetcher.source_code
|
|
85
|
+
if source.nil? || source.strip.empty?
|
|
86
|
+
return Result.new(content: skill_content, changed: false, verifiable: false, model: model)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raw = build_chat.ask(format_prompt(skill_content, source)).content.to_s
|
|
90
|
+
# Re-apply frontmatter to both sides so the diff compares like-for-like and
|
|
91
|
+
# the stored skill always keeps valid frontmatter, even if the model dropped it.
|
|
92
|
+
original = Frontmatter.build(gem_name, version, skill_content)
|
|
93
|
+
corrected = Frontmatter.build(gem_name, version, extract_skill(raw, skill_content))
|
|
94
|
+
changed = normalize(corrected) != normalize(original)
|
|
95
|
+
|
|
96
|
+
Result.new(content: (changed ? corrected : skill_content), changed: changed,
|
|
97
|
+
verifiable: true, model: model)
|
|
98
|
+
rescue RubyLLM::Error => e
|
|
99
|
+
raise Error, e.message
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def build_chat
|
|
105
|
+
model_id, provider = Gem::Skill.parse_model(model)
|
|
106
|
+
RubyLLM.chat(model: model_id, provider: provider).with_instructions(SYSTEM_INSTRUCTIONS)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def format_prompt(skill_content, source)
|
|
110
|
+
format(
|
|
111
|
+
PROMPT,
|
|
112
|
+
gem_name: gem_name,
|
|
113
|
+
version: version,
|
|
114
|
+
begin_mark: BEGIN_MARK,
|
|
115
|
+
end_mark: END_MARK,
|
|
116
|
+
skill: skill_content,
|
|
117
|
+
source: source
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Pull the corrected skill out from between the markers. If the model didn't
|
|
122
|
+
# honor the protocol, fall back to the original so we never corrupt the cache.
|
|
123
|
+
def extract_skill(raw, fallback)
|
|
124
|
+
start = raw.index(BEGIN_MARK)
|
|
125
|
+
return fallback unless start
|
|
126
|
+
|
|
127
|
+
body = raw[(start + BEGIN_MARK.length)..]
|
|
128
|
+
stop = body.index(END_MARK)
|
|
129
|
+
body = body[0...stop] if stop
|
|
130
|
+
|
|
131
|
+
body = body.to_s.strip
|
|
132
|
+
body.empty? ? fallback : body
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def normalize(text)
|
|
136
|
+
text.to_s.gsub(/[ \t]+$/, "").strip
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/gem/skill/version.rb
CHANGED
data/lib/gem/skill.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require_relative "skill/version"
|
|
4
4
|
require_relative "skill/cache"
|
|
5
5
|
require_relative "skill/fetcher"
|
|
6
|
+
require_relative "skill/frontmatter"
|
|
6
7
|
require_relative "skill/generator"
|
|
8
|
+
require_relative "skill/verifier"
|
|
7
9
|
require_relative "skill/linker"
|
|
8
10
|
require_relative "skill/lockfile"
|
|
9
11
|
require_relative "skill/runner"
|
|
@@ -11,6 +13,16 @@ require_relative "skill/runner"
|
|
|
11
13
|
module Gem::Skill
|
|
12
14
|
class Error < StandardError; end
|
|
13
15
|
|
|
16
|
+
# Exit status used when --verify found and corrected mistakes in a generated
|
|
17
|
+
# skill (grep-style: 0 = clean, 1 = error, 2 = verify applied fixes).
|
|
18
|
+
EXIT_VERIFY_FIXED = 2
|
|
19
|
+
|
|
20
|
+
# The bundled "router" skill that teaches assistants how to find cached gem
|
|
21
|
+
# skills in ~/.gem/skills. `gem skill setup` copies it into the assistants'
|
|
22
|
+
# default skill roots. Lives at the repo/gem root, beside README/CHANGELOG.
|
|
23
|
+
ROUTER_SKILL_NAME = "ruby-gem-skills"
|
|
24
|
+
ROUTER_SKILL_DIR = File.expand_path("../../#{ROUTER_SKILL_NAME}", __dir__)
|
|
25
|
+
|
|
14
26
|
ENV_KEY_MAP = {
|
|
15
27
|
anthropic_api_key: "ANTHROPIC_API_KEY",
|
|
16
28
|
openai_api_key: "OPENAI_API_KEY",
|
|
@@ -18,14 +30,37 @@ module Gem::Skill
|
|
|
18
30
|
mistral_api_key: "MISTRAL_API_KEY",
|
|
19
31
|
deepseek_api_key: "DEEPSEEK_API_KEY",
|
|
20
32
|
openrouter_api_key: "OPENROUTER_API_KEY",
|
|
21
|
-
xai_api_key: "XAI_API_KEY"
|
|
33
|
+
xai_api_key: "XAI_API_KEY",
|
|
34
|
+
lms_api_base: "LMS_API_BASE",
|
|
35
|
+
lms_api_key: "LMS_API_KEY",
|
|
36
|
+
apfel_api_base: "APFEL_API_BASE",
|
|
37
|
+
apfel_api_key: "APFEL_API_KEY"
|
|
22
38
|
}.freeze
|
|
23
39
|
|
|
40
|
+
# Split a "provider/model" string into [model_id, provider_symbol] when the
|
|
41
|
+
# prefix names a registered RubyLLM provider, e.g.
|
|
42
|
+
# "lms/qwen/qwen3.8-27b" -> ["qwen/qwen3.8-27b", :lms]
|
|
43
|
+
# Otherwise the string is a bare model id: ["gpt-5.5", nil]. A bare id is
|
|
44
|
+
# resolved by RubyLLM's own provider preference, so ids that contain a "/"
|
|
45
|
+
# but don't start with a provider slug (e.g. "qwen/...") pass through intact.
|
|
46
|
+
def self.parse_model(model_string)
|
|
47
|
+
prefix, rest = model_string.to_s.split("/", 2)
|
|
48
|
+
return [model_string, nil] unless rest && RubyLLM::Provider.providers.key?(prefix.to_sym)
|
|
49
|
+
|
|
50
|
+
[rest, prefix.to_sym]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# HTTP read timeout in seconds (GEMSKILL_REQUEST_TIMEOUT to override).
|
|
54
|
+
# RubyLLM's default of 300 is tuned for hosted APIs; a local model (lms/apfel)
|
|
55
|
+
# can legitimately take longer than that to finish one skill generation.
|
|
56
|
+
REQUEST_TIMEOUT = ENV.fetch("GEMSKILL_REQUEST_TIMEOUT", 900).to_i
|
|
57
|
+
|
|
24
58
|
# Configure RubyLLM from environment variables. Called automatically by the
|
|
25
59
|
# CLI commands so users don't need a separate initializer for standalone use.
|
|
26
60
|
# No-op if RubyLLM is already configured (e.g. in a Rails app).
|
|
27
61
|
def self.configure_llm!
|
|
28
62
|
RubyLLM.configure do |config|
|
|
63
|
+
config.request_timeout = REQUEST_TIMEOUT
|
|
29
64
|
ENV_KEY_MAP.each do |attr, env_var|
|
|
30
65
|
value = ENV[env_var]
|
|
31
66
|
config.public_send(:"#{attr}=", value) if value
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ruby-gem-skills
|
|
3
|
+
description: "Locate and load gem-skill's cached, version-specific SKILL.md for a Ruby gem. Use whenever working with, debugging, or writing Ruby code that uses a third-party gem (Bundler/Gemfile projects or installed gems) to get accurate, version-pinned API knowledge before relying on memory."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ruby-gem-skills
|
|
7
|
+
|
|
8
|
+
Detailed, version-specific knowledge for installed Ruby gems is generated by the
|
|
9
|
+
`gem-skill` tool and cached on this machine at:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
~/.gem/skills/<gem_name>/<version>/SKILL.md
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
(Use `$GEMSKILL_DIR` instead of `~/.gem/skills` if that variable is set.)
|
|
16
|
+
|
|
17
|
+
Most assistants do not scan that directory by default, so follow this procedure
|
|
18
|
+
to find and load a gem's skill before answering from memory.
|
|
19
|
+
|
|
20
|
+
## When to use
|
|
21
|
+
|
|
22
|
+
- Writing or debugging Ruby code that calls a third-party gem's API.
|
|
23
|
+
- Verifying exact method signatures, options, defaults, or behavior for a
|
|
24
|
+
specific gem version.
|
|
25
|
+
|
|
26
|
+
## How to load a gem's skill
|
|
27
|
+
|
|
28
|
+
1. Determine the gem version in use:
|
|
29
|
+
- In a project with a `Gemfile.lock`, read the locked version for the gem.
|
|
30
|
+
- Otherwise run `gem list <gem_name>` (or `bundle show <gem_name>`) to find
|
|
31
|
+
the installed version.
|
|
32
|
+
2. Read the cached skill for that exact version:
|
|
33
|
+
```
|
|
34
|
+
~/.gem/skills/<gem_name>/<version>/SKILL.md
|
|
35
|
+
```
|
|
36
|
+
3. If it exists, treat its contents as the authoritative, version-pinned
|
|
37
|
+
reference for that gem and prefer it over training memory. Skills are
|
|
38
|
+
version-specific — always match the version actually in use.
|
|
39
|
+
|
|
40
|
+
## If the skill is missing
|
|
41
|
+
|
|
42
|
+
Tell the user how to generate it:
|
|
43
|
+
|
|
44
|
+
- One gem: `gem skill install <gem_name>`
|
|
45
|
+
- Every gem in a project (from the project root): `bundle skill install`
|
|
46
|
+
- Verify an existing skill against the gem's real source: `gem skill verify <gem_name>`
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- A `metadata.json` sits beside each `SKILL.md` with provenance (model, sources,
|
|
51
|
+
and verification status). It is for tooling and humans, not required reading.
|
|
52
|
+
- A green checkmark next to a version in `gem skill list` means that skill was
|
|
53
|
+
verified against the gem's actual source code.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gem-skill
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -29,14 +29,42 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '2.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ruby_llm-providers-apfel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: ruby_llm-providers-lms
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
69
|
name: tty-spinner
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +93,7 @@ files:
|
|
|
65
93
|
- ".envrc"
|
|
66
94
|
- ".github/workflows/deploy-github-pages.yml"
|
|
67
95
|
- CHANGELOG.md
|
|
96
|
+
- CLAUDE.md
|
|
68
97
|
- LICENSE.txt
|
|
69
98
|
- README.md
|
|
70
99
|
- Rakefile
|
|
@@ -82,14 +111,17 @@ files:
|
|
|
82
111
|
- lib/gem/skill/cli/bundle_command.rb
|
|
83
112
|
- lib/gem/skill/cli/gem_command.rb
|
|
84
113
|
- lib/gem/skill/fetcher.rb
|
|
114
|
+
- lib/gem/skill/frontmatter.rb
|
|
85
115
|
- lib/gem/skill/generator.rb
|
|
86
116
|
- lib/gem/skill/linker.rb
|
|
87
117
|
- lib/gem/skill/lockfile.rb
|
|
88
118
|
- lib/gem/skill/runner.rb
|
|
119
|
+
- lib/gem/skill/verifier.rb
|
|
89
120
|
- lib/gem/skill/version.rb
|
|
90
121
|
- lib/rubygems_plugin.rb
|
|
91
122
|
- mkdocs.yml
|
|
92
123
|
- plugins.rb
|
|
124
|
+
- ruby-gem-skills/SKILL.md
|
|
93
125
|
- scripts/e2e_test
|
|
94
126
|
- sig/gem/skill.rbs
|
|
95
127
|
homepage: https://github.com/madbomber/gem-skill
|
|
@@ -117,14 +149,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
117
149
|
requirements:
|
|
118
150
|
- - ">="
|
|
119
151
|
- !ruby/object:Gem::Version
|
|
120
|
-
version: 3.
|
|
152
|
+
version: '3.4'
|
|
121
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
154
|
requirements:
|
|
123
155
|
- - ">="
|
|
124
156
|
- !ruby/object:Gem::Version
|
|
125
157
|
version: '0'
|
|
126
158
|
requirements: []
|
|
127
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.21
|
|
128
160
|
specification_version: 4
|
|
129
161
|
summary: Generate and manage Claude Code AI skills from Ruby gem documentation.
|
|
130
162
|
test_files: []
|