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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gem::Skill
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.2"
5
5
  end
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.1.3
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: '1.0'
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: '1.0'
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.2.0
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.14
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: []