Skip to content

Release v1.2.0: close all milestone issues (security, perf, features, docs)#81

Merged
igmarin merged 18 commits into
mainfrom
release/1.2.0
Jun 30, 2026
Merged

Release v1.2.0: close all milestone issues (security, perf, features, docs)#81
igmarin merged 18 commits into
mainfrom
release/1.2.0

Conversation

@igmarin

@igmarin igmarin commented Jun 30, 2026

Copy link
Copy Markdown
Owner

v1.2.0 — quality program release

Closes the v1.2.0 milestone: every open issue in the issue tab and the ruby-skill-bench project board, each delivered with YARD docs and Minitest coverage.

Note: this branch is based on feat/token-cost-accounting-40, so it includes the token & cost accounting work from #77 (#40) plus its cost-calculator fix. Merging this supersedes PR #77 — that PR can be closed.

Features

Security

Performance

Fixed

Docs / community

Verification

  • rake test → 865 runs, 0 failures, 0 errors, 4 skips
  • rubocop, reek, rake yard:coverage, bundle-audit, rake package:build package:verify — all green

Summary by CodeRabbit

  • New Features

    • Added a validate/doctor pre-flight check command.
    • Added HTML report output, batch JUnit/JSON summary output, and token/cost reporting.
    • Added optional response caching, offline mock setup, and support for a new provider.
  • Bug Fixes

    • Improved handling of mock configuration and usage tracking across runs.
  • Documentation

    • Updated setup, output format, security, CI, and architecture docs.
    • Added GitHub issue, pull request, and community guidelines templates.

igmarin added 13 commits June 30, 2026 14:07
PRICES lacked the claude-sonnet-4 / claude-opus-4 families that the
project actually passes in (config/defaults.rb -> claude-sonnet-4-20250514,
clients/provider_schemas.rb -> claude-opus-4-7), so price_for fell through
to nil and cost was reported unknown for the default Anthropic model.

Add both families, preserving longest-prefix matching so dated variants
still resolve. Covered by a new test asserting the real default strings.
Tree diagram referenced the dead .skill-bench-history.json; the engine
writes .skill-bench-trends.json. Adds a note that .skill-bench-trends.json.bak
is the auto-backup. Closes #39.
Adds Contributor Covenant 2.1, bug-report/feature-request issue templates
mirroring CONTRIBUTING.md, an issue-template config, and a PR template.
Closes #38.
…seam (#21)

Documents in the README that allowlisted wrapper binaries (rake/rspec/make/
find/git) equal arbitrary host execution, tied to the fail-closed host model.
Adds an optional, default-off command_argument_constraints hook so high-risk
allowed commands can be constrained; behavior is unchanged when unconfigured.
Closes #21.
…run (#54, #31)

Config#mock? distinguishes an explicit {"provider":"mock"} config from a
failed/empty load, so ProviderResolver no longer prints a misleading
'Config load failed' warning on a clean offline run; genuine failures still
warn. Config.loaded memoizes the parse per absolute path, invalidated by
mtime, so skill-bench.json is parsed at most once per run while a rewritten
file is still re-read. Closes #54. Closes #31.
skill-bench init --mock scaffolds a valid offline {"provider":"mock",
"max_execution_time":30} config. Adds Providers::Mistral, an OpenAI-compatible
subclass (api.mistral.ai), registered like deepseek/groq, with a :mistral
schema. Bedrock (SigV4) noted as a follow-up. Closes #53. Closes #47.
skill-bench validate (alias doctor) runs CriteriaValidator over criteria.json,
hand-rolled schema-validates skill-bench.json (provider/max_execution_time/config),
and reports missing provider API keys via BaseClient#missing_config_keys — all
without running an eval or hitting the network. Closes #45.
Adds Services::ResponseCache (SHA-256 over provider/model/system_prompt/messages/
tools/temperature) consulted by Client.call when caching is enabled via --cache or
SKILL_BENCH_CACHE. Default off = unchanged behavior. Identical LLM calls — notably
compare's twice-run skill-less baseline — hit the cache instead of the network.
Null/mock clients are excluded. Closes #42.
Services::HtmlFormatter renders the delta report table, baseline/context
iteration timelines, and the tokens/cost header as a self-contained HTML
document with all dynamic text CGI-escaped. Wired :html into
OutputFormatter.format and the --format help (human, json, junit, html).
Closes #44.
JUnitFormatter.format_batch emits one <testcase> per eval for batch runs;
SummaryFormatter renders an aggregate JSON gate (passed/failed/total, summed
tokens/cost, worst-delta eval); run --all threads --format junit and --summary
through BatchResultPrinter while preserving the batch exit code. Adds a top-level
composite action.yml so downstream repos can use igmarin/ruby-skill-bench@v1.
Closes #46.
PackageVerifier required lib/skill_bench/source_path_resolver.rb, but the file
lives at lib/skill_bench/execution/source_path_resolver.rb, so rake package:verify
always failed. Points the entry at the real path.
Expands the [1.2.0] CHANGELOG with the full release set (validate/doctor,
html format, token/cost, caching, summary/JUnit batch + action.yml, Mistral,
init --mock, security note, config-parse perf, mock-warning fix, docs) and
documents the new commands, flags, the Mistral provider, token/cost output,
and the GitHub Action in the README.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@igmarin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 90ee566e-3c6e-4c5c-9847-51a83a30c2a8

📥 Commits

Reviewing files that changed from the base of the PR and between 097f7b9 and ca0f1e3.

📒 Files selected for processing (18)
  • .github/PULL_REQUEST_TEMPLATE.md
  • .reek.yml
  • .rubocop.yml
  • action.yml
  • examples/README.md
  • lib/skill_bench.rb
  • lib/skill_bench/cli/batch_result_printer.rb
  • lib/skill_bench/cli/init_command.rb
  • lib/skill_bench/cli/run_command.rb
  • lib/skill_bench/cli/validate_command.rb
  • lib/skill_bench/client.rb
  • lib/skill_bench/clients/all.rb
  • lib/skill_bench/config/json_loader.rb
  • lib/skill_bench/services/json_formatter.rb
  • lib/skill_bench/services/response_cache.rb
  • lib/skill_bench/tools/run_command.rb
  • test/evaluator/tools/run_command_test.rb
  • test/services/json_formatter_test.rb
📝 Walkthrough

Walkthrough

This PR adds nine features to skill-bench: a validate/doctor pre-flight CLI command, an HTML report formatter, opt-in content-addressed response caching, batch CI summary/JUnit aggregation with a composite GitHub Action, a Mistral provider, init --mock offline scaffolding, per-command argument constraints, token/cost accounting, and memoized config loading. Community health files and docs were also added.

Changes

Core Feature Implementation

Layer / File(s) Summary
Validate/doctor command
lib/skill_bench/cli/validate_command.rb, lib/skill_bench/cli.rb, lib/skill_bench/cli/help_printer.rb, README.md, test/cli/validate_command_test.rb
New ValidateCommand checks criteria JSON, config shape, and provider key presence, wired into CLI dispatch and help text, documented in README.
Config memoization & mock-aware resolver
lib/skill_bench/models/config.rb, lib/skill_bench/services/provider_resolver.rb, test/agent_eval/models/config_test.rb, test/agent_eval/services/provider_resolver_test.rb
Config.loaded memoizes parsed config by mtime; Config#mock? added; ProviderResolver uses memoized loader and silences the warning for explicit mock config.
init --mock
lib/skill_bench/cli/init_command.rb, lib/skill_bench/commands/init.rb, test/agent_eval/commands/init_test.rb, test/cli/init_command_test.rb, examples/README.md, README.md
--mock flag generates a minimal mock config without API key/config block.
Mistral provider
lib/skill_bench/clients/providers/mistral.rb, lib/skill_bench/clients/all.rb, lib/skill_bench/clients/provider_schemas.rb, test/evaluator/clients/providers/mistral_test.rb, README.md
New OpenAI-compatible Mistral client registered and tested.
Token usage accounting
lib/skill_bench/agent/react_agent/step.rb, lib/skill_bench/agent/react_agent/loop_runner.rb, lib/skill_bench/services/agent_spawner_service.rb, lib/skill_bench/services/cost_calculator.rb, lib/skill_bench/services/runner_service.rb, test/...
Usage flows from Step through LoopRunner, AgentSpawnerService into RunnerService, which computes cost via new CostCalculator.
Output formats (human/JSON/JUnit/HTML)
lib/skill_bench/output_formatter.rb, lib/skill_bench/services/json_formatter.rb, lib/skill_bench/services/junit_formatter.rb, lib/skill_bench/services/html_formatter.rb, lib/skill_bench/cli/result_printer.rb, test/...
Human/JSON output gain tokens/cost fields; JUnit formatter refactored for batch support; new HtmlFormatter renders delta/legacy reports.
Batch summary & printer routing
lib/skill_bench/services/summary_formatter.rb, lib/skill_bench/cli/batch_result_printer.rb, lib/skill_bench/cli/run_command.rb, lib/skill_bench.rb, test/...
SummaryFormatter produces JSON gate output; BatchResultPrinter/RunCommand route --summary/--format options.
Composite GitHub Action
action.yml, README.md
New action.yml wraps skill-bench run --all for downstream CI.
Response caching
lib/skill_bench/services/response_cache.rb, lib/skill_bench/client.rb, lib/skill_bench/services/compare_option_parser.rb, test/client_caching_test.rb, test/services/response_cache_test.rb, README.md
ResponseCache provides content-addressed in-memory caching, gated by SKILL_BENCH_CACHE/--cache, wired into Client.call.
Argument constraints
lib/skill_bench/config/*, lib/skill_bench/tools/run_command.rb, test/evaluator/tools/run_command_test.rb, README.md
command_argument_constraints config plumbed through store/defaults/applier/facade; enforced in RunCommand before execution.
Package verifier path fix
lib/skill_bench/package_verifier.rb
REQUIRED_FILES updated for relocated source_path_resolver.rb.

Community Health Files and Docs

Layer / File(s) Summary
Community templates and Code of Conduct
.github/ISSUE_TEMPLATE/*, .github/PULL_REQUEST_TEMPLATE.md, CODE_OF_CONDUCT.md
Adds bug report/feature request templates, issue config, PR template, and Contributor Covenant.
Changelog and architecture docs
CHANGELOG.md, docs/architecture.md, .reek.yml
Expands 1.2.0 changelog sections; documents .skill-bench-trends.json.bak; updates Reek detector exclusions.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ValidateCommand
  participant CriteriaValidator
  participant ConfigLoader
  participant ProviderRegistry
  CLI->>ValidateCommand: call(argv)
  ValidateCommand->>CriteriaValidator: validate criteria.json
  ValidateCommand->>ConfigLoader: load skill-bench.json
  ValidateCommand->>ProviderRegistry: resolve provider client
  ProviderRegistry-->>ValidateCommand: missing_config_keys
  ValidateCommand-->>CLI: PASS/FAIL report + exit code
Loading
sequenceDiagram
  participant Client
  participant ResponseCache
  participant ProviderClient
  Client->>Client: cache_eligible?(client_class)
  alt eligible and cache hit
    Client->>ResponseCache: fetch(key)
    ResponseCache-->>Client: cached response
  else not eligible or miss
    Client->>ProviderClient: call(system_prompt, messages, ...)
    ProviderClient-->>Client: response
    Client->>ResponseCache: store(key, response)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • igmarin/ruby-skill-bench#13: Both PRs modify loop_runner.rb/step.rb, touching the same iteration finalization and step-outcome fields.
  • igmarin/ruby-skill-bench#15: Both PRs modify RunnerService.call/success response shape, layering tokens/cost onto the same code.
  • igmarin/ruby-skill-bench#9: Both PRs modify Client.call's provider-resolution path, one adding caching, the other a provider override.

Poem

A rabbit hops through skill-bench's den,
Counting tokens, caching again,
Mistral joins the provider crew,
HTML reports in tidy hue,
🥕 Validate first, then run with glee —
A doctor's check for CI! 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes a package-verifier path fix that is not covered by any linked issue. Move the package-verifier change into a separate PR or add a linked issue if it is intended scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 51.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the release-focused changes in the PR.
Linked Issues check ✅ Passed The PR addresses the linked issues for validate, HTML output, caching, CI aggregation, Mistral, init --mock, docs, config memoization, and warning suppression.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

# Conflicts:
#	test/evaluator/tools/run_command_test.rb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/PULL_REQUEST_TEMPLATE.md (1)

17-26: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the yard:coverage command syntax.

Line 23 lists bundle exec yard:coverage, but the standard invocation is bundle exec rake yard:coverage (or bundle exec rake yard:coverage depending on the Rake task name). Without rake, most shells will attempt to run a binary named yard:coverage, which does not exist.

- [ ] `bundle exec yard:coverage` passes
+ [ ] `bundle exec rake yard:coverage` passes
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/PULL_REQUEST_TEMPLATE.md around lines 17 - 26, The checklist entry
for the YARD coverage command uses the wrong invocation syntax, so update the
pull request template’s checklist item to use the Rake task form instead of a
bare `yard:coverage` command. Make the change in the template text where the
existing `bundle exec yard:coverage` appears, keeping it consistent with the
other `bundle exec rake ...` entries and preserving the `yard:coverage` task
name.
lib/skill_bench/config/applier.rb (1)

40-47: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Expose command_argument_constraints in JsonLoader skill-bench.json drops this key today, so the new constraint never reaches Applier for JSON-based configs. Add it to the slice(...) whitelist in lib/skill_bench/config/json_loader.rb.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/config/applier.rb` around lines 40 - 47,
`command_argument_constraints` is being applied in
`Applier#apply_scalar_values`, but JSON configs never pass it through because
`JsonLoader` filters it out. Update `JsonLoader`’s `slice(...)` whitelist to
include `command_argument_constraints` so the value from `skill-bench.json`
reaches `Applier` and gets assigned to the store.
🧹 Nitpick comments (9)
lib/skill_bench/cli/init_command.rb (1)

63-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

--mock missing from "provider required" hint.

error_missing_provider only enumerates ProviderSchemas.names, so users who omit a provider are never told --mock is a valid offline option.

💡 Proposed fix
       def error_missing_provider
-        providers = SkillBench::Clients::ProviderSchemas.names.map { |provider_name| "--#{provider_name}" }.join(', ')
+        providers = (SkillBench::Clients::ProviderSchemas.names.map { |provider_name| "--#{provider_name}" } + ['--mock']).join(', ')
         warn "Error: provider is required. Use one of: #{providers}"
         1
       end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/init_command.rb` around lines 63 - 67, The
provider-required hint in error_missing_provider omits the offline --mock option
because it only uses SkillBench::Clients::ProviderSchemas.names; update the
provider list construction in this method to include --mock alongside the
schema-based providers so the warning mentions every valid init option.
examples/README.md (1)

7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confusing wording: "scaffold ... by hand".

The tip says running skill-bench init --mock scaffolds the config "by hand as a fallback", which is contradictory — the command does it automatically; writing the JSON yourself would be the manual fallback.

✏️ Proposed fix
-> Tip: run `skill-bench init --mock` to scaffold that offline mock config
-> (`{"provider":"mock","max_execution_time":30}`) by hand as a fallback.
+> Tip: run `skill-bench init --mock` to scaffold that offline mock config
+> (`{"provider":"mock","max_execution_time":30}`), or write it by hand as a fallback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/README.md` around lines 7 - 9, The README tip in the mock config
section uses contradictory wording by saying the CLI command “scaffold[s] ... by
hand”; update the sentence in the documentation to clearly separate the
automatic path from the manual fallback. Keep the reference to the `skill-bench
init --mock` command, and rephrase the fallback so it explicitly refers to
writing the JSON config manually instead of implying the command does it “by
hand.”
test/cli/validate_command_test.rb (1)

26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reaching into Models::Config internal ivar from a different layer's test.

Models::Config.instance_variable_set(:@loaded, nil) directly pokes the memoization internals being introduced in the sibling "Config memoization" layer. instance_variable_set silently no-ops if the ivar name changes (e.g., renamed during refactor), causing memoized config to leak across test runs without any failure signal — a brittle, hard-to-debug coupling.

Suggested fix: expose a public reset hook
-        Models::Config.instance_variable_set(:`@loaded`, nil)
+        Models::Config.reset_loaded_cache
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/validate_command_test.rb` at line 26, The test is tightly coupled to
Models::Config’s internal memoization state by setting `@loaded` directly, which
is brittle and can break silently if the implementation changes. Add a public
reset hook on Models::Config to clear the cached config state, then update
validate_command_test to call that helper instead of instance_variable_set so
the test uses the supported API.
lib/skill_bench/cli/validate_command.rb (1)

162-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared env-override logic

provider_client_options/env_setting duplicate the same SKILL_BENCH_<PROVIDER>_<SETTING><PROVIDER>_<SETTING> precedence already used by Models::Provider. Extracting one helper would keep validation and provider resolution in sync if that naming or order changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/validate_command.rb` around lines 162 - 188, The env
override resolution in missing_provider_keys/provider_client_options/env_setting
duplicates the same SKILL_BENCH_<PROVIDER>_<SETTING> then <PROVIDER>_<SETTING>
precedence already used by Models::Provider. Refactor ValidateCommand to reuse a
shared helper or the existing Models::Provider logic for building
provider_client_options so validation stays in sync if the naming or precedence
changes, and keep env_setting only as a thin delegate or remove it if no longer
needed.
test/agent_eval/services/provider_resolver_test.rb (1)

97-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer Minitest's built-in capture_io over a custom stderr-capturing helper.

Minitest::Assertions#capture_io already redirects and restores both $stdout/$stderr around a block and returns the captured strings, making this private helper redundant.

♻️ Proposed refactor
-      private
-
-      def capture_stderr
-        original = $stderr
-        $stderr = StringIO.new
-        yield
-        $stderr.string
-      ensure
-        $stderr = original
-      end
-
       def write_mock_config

And update call sites to use capture_io { ... }.last (stderr is the second element of the returned array) in place of capture_stderr { ... }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/agent_eval/services/provider_resolver_test.rb` around lines 97 - 107,
Replace the custom `capture_stderr` helper in `provider_resolver_test.rb` with
Minitest’s built-in `capture_io`, since it already handles redirecting and
restoring both streams; update every `capture_stderr { ... }` call site to use
`capture_io { ... }.last` so the tests still read captured stderr while removing
the redundant private method.
lib/skill_bench/models/config.rb (1)

27-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reflection-based cache reset is fragile; expose a public API instead.

The docstring instructs resetting by setting @loaded to nil directly, and both test/agent_eval/models/config_test.rb and test/agent_eval/services/provider_resolver_test.rb do this via instance_variable_set at 5 call sites. If @loaded is ever renamed, instance_variable_set silently no-ops (no error), and tests would start leaking stale cache across runs without any signal. A small public method removes the duplication and the encapsulation break.

♻️ Proposed addition
+      # Clears the memoized config cache. Intended for test isolation.
+      #
+      # `@return` [void]
+      def self.reset_cache!
+        `@loaded` = {}
+      end
+
       # Returns the configuration for a path, memoizing the parse per run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/models/config.rb` around lines 27 - 50, The cache reset for
SkillBench::Models::Config is being done via direct ivar mutation and
reflection, which is brittle and duplicated across tests. Add a small public
reset API on SkillBench::Models::Config to clear the `@loaded` cache, update the
loaded memoization docs to reference that API instead of setting the ivar
directly, and replace all instance_variable_set calls in config and
provider_resolver tests with the new method.
lib/skill_bench/services/runner_service.rb (1)

177-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared usage-accumulation helpers to avoid triple duplication.

empty_usage/add_usage/token_count here are identical to the same-named methods in lib/skill_bench/agent/react_agent/loop_runner.rb (lines 75-100), and token_count's symbol/string-key fallback logic is reimplemented again in lib/skill_bench/services/cost_calculator.rb (lines 82-88). Consider extracting a shared module (e.g. SkillBench::Support::TokenUsage) with empty, add, and count so a future fix to usage-key handling doesn't need to be applied in three places.

♻️ Sketch of a shared helper module
module SkillBench
  module Support
    module TokenUsage
      def self.empty
        { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
      end

      def self.add(total, usage)
        usage ||= {}
        {
          prompt_tokens: total[:prompt_tokens] + count(usage, :prompt_tokens),
          completion_tokens: total[:completion_tokens] + count(usage, :completion_tokens),
          total_tokens: total[:total_tokens] + count(usage, :total_tokens)
        }
      end

      def self.count(usage, key)
        (usage[key] || usage[key.to_s] || 0).to_i
      end
    end
  end
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/services/runner_service.rb` around lines 177 - 221, The
token-usage helper logic is duplicated across RunnerService, LoopRunner, and
CostCalculator, so consolidate it into a shared helper/module (for example,
SkillBench::Support::TokenUsage) with the equivalent of empty_usage, add_usage,
and token_count. Update RunnerService#aggregate_usage and the other call sites
to use the shared methods, preserving the symbol/string-key fallback behavior in
token_count so future fixes only need to happen in one place.
lib/skill_bench/output_formatter.rb (1)

141-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate token/cost rendering logic between formatters.

build_usage_line formats Tokens: N | Est. Cost: $X.XXXX here, and per graph evidence Services::HtmlFormatter independently builds the same "Tokens"/"Est. Cost" string with its own Kernel.format('$%.4f', cost) call (html_formatter.rb:114). Consider extracting a shared helper (e.g., a small Services::UsageFormatter or module method) that both the human and HTML formatters call, so cost-formatting/precision stays consistent if it ever changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/output_formatter.rb` around lines 141 - 171, The token/cost
summary formatting is duplicated between OutputFormatter’s build_usage_line and
Services::HtmlFormatter, which risks inconsistent formatting if either changes.
Extract the shared “Tokens / Est. Cost” rendering into a common helper (for
example, a Services::UsageFormatter method) and have both build_usage_line and
the HTML formatter call it so the cost precision and label stay aligned in one
place.
action.yml (1)

57-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider pinning the gem version for reproducible CI gates.

gem install ruby-skill-bench always installs latest; an unpinned dependency could break consumer pipelines unexpectedly on a future release. Consider adding an optional version input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 57 - 59, The ruby-skill-bench install step in the
workflow is unpinned, so CI may change behavior when new gem releases are
published. Update the workflow around the install step to support a versioned
install, ideally by adding an optional version input and using it in the install
command so consumers can pin ruby-skill-bench to a specific release. Keep the
change localized to the action definition where the gem is installed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@action.yml`:
- Around line 61-73: Replace the direct GitHub expression interpolation in the
Run skill-bench step with shell variables passed via env:, because the current
use of inputs.* inside the bash script can be script-injected before bash
parsing. Update the run script in action.yml to read those values from
environment variables instead of expanding them inline, and keep the
argument-building logic in the skill-bench step so it uses the safe variables
when constructing run_args and invoking skill-bench.

In `@lib/skill_bench/cli/run_command.rb`:
- Around line 87-91: The run command is passing an unsupported `:summary` option
into the single-eval path, causing `Commands::Run.run` to reject the keyword.
Update `RunCommand#run_single` (and any option handoff it uses) to strip or
ignore `options[:summary]` before calling `Commands::Run.run`, keeping `summary`
only for the batch-run flow where it is actually handled.
- Line 78: BatchResultPrinter.call is still falling through to
OutputFormatter.format_batch for non-junit batch runs, so --all with html/json
does not use the expected structured output. Update
BatchResultPrinter.batch_output to explicitly handle :html and :json (using the
same batch-specific path as :junit, or equivalent), or otherwise restrict the
supported batch formats advertised by RunCommand so they only include formats
actually implemented.

In `@lib/skill_bench/cli/validate_command.rb`:
- Around line 140-169: Avoid instantiating the provider client in
missing_provider_keys, since Clients::BaseClient#initialize can raise on
unrelated config like base_url/endpoint and bypass the intended FAIL result.
Update check_provider_key/missing_provider_keys to inspect required credential
keys from provider metadata or a non-instantiating helper instead of
Clients::ProviderRegistry.for(...).new(...) and send(:missing_config_keys), so
the CLI stays decoupled from provider-private internals.

In `@lib/skill_bench/client.rb`:
- Around line 38-46: `Client.call` is building a cache key in
`Services::ResponseCache.key` without all request-affecting provider config, so
different provider endpoints can collide. Update the cache key generation in
`Client.call` to also include the relevant `ProviderConfig` fields that change
the actual request, such as `base_url`, `request_path`, `endpoint`, `location`,
`project_id`, and `api_version`, alongside the existing `resolved`, `model`,
`system_prompt`, `messages`, `tools`, and `temperature` values.

In `@lib/skill_bench/services/json_formatter.rb`:
- Around line 27-35: The with_usage_fields method in JsonFormatter still allows
an explicit nil tokens value to overwrite the computed fallback, so fix the
merge behavior there by preventing nil :tokens from replacing the derived usage
data or by merging in the opposite order. Update the logic around
result[:tokens], result.dig(:response, :tokens), and EMPTY_USAGE so the output
always preserves a non-nil tokens value, and add a test covering the tokens: nil
case.

In `@lib/skill_bench/services/response_cache.rb`:
- Around line 68-89: ResponseCache#fetch is not thread-safe, so concurrent
Parallel.map callers can race between key? and []= and trigger duplicate
requests. Update the shared cache access in fetch (and any related store
helpers) to serialize the read/write around the per-process store, using a mutex
or equivalent synchronization on the store in ResponseCache so only one thread
computes and stores a missing value per key.

In `@lib/skill_bench/tools/run_command.rb`:
- Around line 96-104: `arguments_permitted?` is failing open because it looks up
`SkillBench::Config.command_argument_constraints` using a String `base_cmd`
while the config may contain symbol keys. Update
`RunCommand.arguments_permitted?` to normalize the lookup key (or normalize keys
in `SkillBench::Config.command_argument_constraints`), so
`constraints[base_cmd]` finds symbol-keyed entries consistently and forbidden
arguments are actually checked. Keep the fix centered on `arguments_permitted?`
and the config accessor used by it.

In `@test/evaluator/tools/run_command_test.rb`:
- Around line 103-134: The new constraint tests only cover the facade path and
miss the JSON-loading/key-type issues; update the tests around RunCommand.call
and SkillBench::Config so they load command_argument_constraints from a real
skill-bench.json via Config::JsonLoader after Config.reset/load_from_file
instead of assigning the hash directly. Add a regression case that uses the
loaded config to verify the constraint is enforced, so the behavior exercised
matches the bug locations in Config::Applier and RunCommand.

---

Outside diff comments:
In @.github/PULL_REQUEST_TEMPLATE.md:
- Around line 17-26: The checklist entry for the YARD coverage command uses the
wrong invocation syntax, so update the pull request template’s checklist item to
use the Rake task form instead of a bare `yard:coverage` command. Make the
change in the template text where the existing `bundle exec yard:coverage`
appears, keeping it consistent with the other `bundle exec rake ...` entries and
preserving the `yard:coverage` task name.

In `@lib/skill_bench/config/applier.rb`:
- Around line 40-47: `command_argument_constraints` is being applied in
`Applier#apply_scalar_values`, but JSON configs never pass it through because
`JsonLoader` filters it out. Update `JsonLoader`’s `slice(...)` whitelist to
include `command_argument_constraints` so the value from `skill-bench.json`
reaches `Applier` and gets assigned to the store.

---

Nitpick comments:
In `@action.yml`:
- Around line 57-59: The ruby-skill-bench install step in the workflow is
unpinned, so CI may change behavior when new gem releases are published. Update
the workflow around the install step to support a versioned install, ideally by
adding an optional version input and using it in the install command so
consumers can pin ruby-skill-bench to a specific release. Keep the change
localized to the action definition where the gem is installed.

In `@examples/README.md`:
- Around line 7-9: The README tip in the mock config section uses contradictory
wording by saying the CLI command “scaffold[s] ... by hand”; update the sentence
in the documentation to clearly separate the automatic path from the manual
fallback. Keep the reference to the `skill-bench init --mock` command, and
rephrase the fallback so it explicitly refers to writing the JSON config
manually instead of implying the command does it “by hand.”

In `@lib/skill_bench/cli/init_command.rb`:
- Around line 63-67: The provider-required hint in error_missing_provider omits
the offline --mock option because it only uses
SkillBench::Clients::ProviderSchemas.names; update the provider list
construction in this method to include --mock alongside the schema-based
providers so the warning mentions every valid init option.

In `@lib/skill_bench/cli/validate_command.rb`:
- Around line 162-188: The env override resolution in
missing_provider_keys/provider_client_options/env_setting duplicates the same
SKILL_BENCH_<PROVIDER>_<SETTING> then <PROVIDER>_<SETTING> precedence already
used by Models::Provider. Refactor ValidateCommand to reuse a shared helper or
the existing Models::Provider logic for building provider_client_options so
validation stays in sync if the naming or precedence changes, and keep
env_setting only as a thin delegate or remove it if no longer needed.

In `@lib/skill_bench/models/config.rb`:
- Around line 27-50: The cache reset for SkillBench::Models::Config is being
done via direct ivar mutation and reflection, which is brittle and duplicated
across tests. Add a small public reset API on SkillBench::Models::Config to
clear the `@loaded` cache, update the loaded memoization docs to reference that
API instead of setting the ivar directly, and replace all instance_variable_set
calls in config and provider_resolver tests with the new method.

In `@lib/skill_bench/output_formatter.rb`:
- Around line 141-171: The token/cost summary formatting is duplicated between
OutputFormatter’s build_usage_line and Services::HtmlFormatter, which risks
inconsistent formatting if either changes. Extract the shared “Tokens / Est.
Cost” rendering into a common helper (for example, a Services::UsageFormatter
method) and have both build_usage_line and the HTML formatter call it so the
cost precision and label stay aligned in one place.

In `@lib/skill_bench/services/runner_service.rb`:
- Around line 177-221: The token-usage helper logic is duplicated across
RunnerService, LoopRunner, and CostCalculator, so consolidate it into a shared
helper/module (for example, SkillBench::Support::TokenUsage) with the equivalent
of empty_usage, add_usage, and token_count. Update RunnerService#aggregate_usage
and the other call sites to use the shared methods, preserving the
symbol/string-key fallback behavior in token_count so future fixes only need to
happen in one place.

In `@test/agent_eval/services/provider_resolver_test.rb`:
- Around line 97-107: Replace the custom `capture_stderr` helper in
`provider_resolver_test.rb` with Minitest’s built-in `capture_io`, since it
already handles redirecting and restoring both streams; update every
`capture_stderr { ... }` call site to use `capture_io { ... }.last` so the tests
still read captured stderr while removing the redundant private method.

In `@test/cli/validate_command_test.rb`:
- Line 26: The test is tightly coupled to Models::Config’s internal memoization
state by setting `@loaded` directly, which is brittle and can break silently if
the implementation changes. Add a public reset hook on Models::Config to clear
the cached config state, then update validate_command_test to call that helper
instead of instance_variable_set so the test uses the supported API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 1f1025e8-27c3-4a38-be27-729fd16c0471

📥 Commits

Reviewing files that changed from the base of the PR and between f388709 and 097f7b9.

📒 Files selected for processing (66)
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/ISSUE_TEMPLATE/config.yml
  • .github/ISSUE_TEMPLATE/feature_request.md
  • .github/PULL_REQUEST_TEMPLATE.md
  • .reek.yml
  • CHANGELOG.md
  • CODE_OF_CONDUCT.md
  • README.md
  • action.yml
  • docs/architecture.md
  • examples/README.md
  • lib/skill_bench.rb
  • lib/skill_bench/agent/react_agent/loop_runner.rb
  • lib/skill_bench/agent/react_agent/step.rb
  • lib/skill_bench/cli.rb
  • lib/skill_bench/cli/batch_result_printer.rb
  • lib/skill_bench/cli/help_printer.rb
  • lib/skill_bench/cli/init_command.rb
  • lib/skill_bench/cli/result_printer.rb
  • lib/skill_bench/cli/run_command.rb
  • lib/skill_bench/cli/validate_command.rb
  • lib/skill_bench/client.rb
  • lib/skill_bench/clients/all.rb
  • lib/skill_bench/clients/provider_schemas.rb
  • lib/skill_bench/clients/providers/mistral.rb
  • lib/skill_bench/commands/init.rb
  • lib/skill_bench/config.rb
  • lib/skill_bench/config/applier.rb
  • lib/skill_bench/config/defaults.rb
  • lib/skill_bench/config/facade_writers.rb
  • lib/skill_bench/config/store.rb
  • lib/skill_bench/models/config.rb
  • lib/skill_bench/output_formatter.rb
  • lib/skill_bench/package_verifier.rb
  • lib/skill_bench/services/agent_spawner_service.rb
  • lib/skill_bench/services/compare_option_parser.rb
  • lib/skill_bench/services/cost_calculator.rb
  • lib/skill_bench/services/html_formatter.rb
  • lib/skill_bench/services/json_formatter.rb
  • lib/skill_bench/services/junit_formatter.rb
  • lib/skill_bench/services/provider_resolver.rb
  • lib/skill_bench/services/response_cache.rb
  • lib/skill_bench/services/runner_service.rb
  • lib/skill_bench/services/summary_formatter.rb
  • lib/skill_bench/tools/run_command.rb
  • test/agent_eval/commands/init_test.rb
  • test/agent_eval/models/config_test.rb
  • test/agent_eval/output_formatter_test.rb
  • test/agent_eval/services/agent_spawner_service_test.rb
  • test/agent_eval/services/provider_resolver_test.rb
  • test/agent_eval/services/runner_service_test.rb
  • test/cli/batch_result_printer_test.rb
  • test/cli/init_command_test.rb
  • test/cli/run_command_test.rb
  • test/cli/validate_command_test.rb
  • test/client_caching_test.rb
  • test/evaluator/clients/providers/mistral_test.rb
  • test/evaluator/react_agent/loop_runner_test.rb
  • test/evaluator/react_agent/step_test.rb
  • test/evaluator/tools/run_command_test.rb
  • test/services/cost_calculator_test.rb
  • test/services/html_formatter_test.rb
  • test/services/json_formatter_test.rb
  • test/services/junit_formatter_test.rb
  • test/services/response_cache_test.rb
  • test/services/summary_formatter_test.rb

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/PULL_REQUEST_TEMPLATE.md (1)

17-26: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the yard:coverage command syntax.

Line 23 lists bundle exec yard:coverage, but the standard invocation is bundle exec rake yard:coverage (or bundle exec rake yard:coverage depending on the Rake task name). Without rake, most shells will attempt to run a binary named yard:coverage, which does not exist.

- [ ] `bundle exec yard:coverage` passes
+ [ ] `bundle exec rake yard:coverage` passes
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/PULL_REQUEST_TEMPLATE.md around lines 17 - 26, The checklist entry
for the YARD coverage command uses the wrong invocation syntax, so update the
pull request template’s checklist item to use the Rake task form instead of a
bare `yard:coverage` command. Make the change in the template text where the
existing `bundle exec yard:coverage` appears, keeping it consistent with the
other `bundle exec rake ...` entries and preserving the `yard:coverage` task
name.
lib/skill_bench/config/applier.rb (1)

40-47: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Expose command_argument_constraints in JsonLoader skill-bench.json drops this key today, so the new constraint never reaches Applier for JSON-based configs. Add it to the slice(...) whitelist in lib/skill_bench/config/json_loader.rb.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/config/applier.rb` around lines 40 - 47,
`command_argument_constraints` is being applied in
`Applier#apply_scalar_values`, but JSON configs never pass it through because
`JsonLoader` filters it out. Update `JsonLoader`’s `slice(...)` whitelist to
include `command_argument_constraints` so the value from `skill-bench.json`
reaches `Applier` and gets assigned to the store.
🧹 Nitpick comments (9)
lib/skill_bench/cli/init_command.rb (1)

63-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

--mock missing from "provider required" hint.

error_missing_provider only enumerates ProviderSchemas.names, so users who omit a provider are never told --mock is a valid offline option.

💡 Proposed fix
       def error_missing_provider
-        providers = SkillBench::Clients::ProviderSchemas.names.map { |provider_name| "--#{provider_name}" }.join(', ')
+        providers = (SkillBench::Clients::ProviderSchemas.names.map { |provider_name| "--#{provider_name}" } + ['--mock']).join(', ')
         warn "Error: provider is required. Use one of: #{providers}"
         1
       end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/init_command.rb` around lines 63 - 67, The
provider-required hint in error_missing_provider omits the offline --mock option
because it only uses SkillBench::Clients::ProviderSchemas.names; update the
provider list construction in this method to include --mock alongside the
schema-based providers so the warning mentions every valid init option.
examples/README.md (1)

7-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confusing wording: "scaffold ... by hand".

The tip says running skill-bench init --mock scaffolds the config "by hand as a fallback", which is contradictory — the command does it automatically; writing the JSON yourself would be the manual fallback.

✏️ Proposed fix
-> Tip: run `skill-bench init --mock` to scaffold that offline mock config
-> (`{"provider":"mock","max_execution_time":30}`) by hand as a fallback.
+> Tip: run `skill-bench init --mock` to scaffold that offline mock config
+> (`{"provider":"mock","max_execution_time":30}`), or write it by hand as a fallback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/README.md` around lines 7 - 9, The README tip in the mock config
section uses contradictory wording by saying the CLI command “scaffold[s] ... by
hand”; update the sentence in the documentation to clearly separate the
automatic path from the manual fallback. Keep the reference to the `skill-bench
init --mock` command, and rephrase the fallback so it explicitly refers to
writing the JSON config manually instead of implying the command does it “by
hand.”
test/cli/validate_command_test.rb (1)

26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reaching into Models::Config internal ivar from a different layer's test.

Models::Config.instance_variable_set(:@loaded, nil) directly pokes the memoization internals being introduced in the sibling "Config memoization" layer. instance_variable_set silently no-ops if the ivar name changes (e.g., renamed during refactor), causing memoized config to leak across test runs without any failure signal — a brittle, hard-to-debug coupling.

Suggested fix: expose a public reset hook
-        Models::Config.instance_variable_set(:`@loaded`, nil)
+        Models::Config.reset_loaded_cache
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/validate_command_test.rb` at line 26, The test is tightly coupled to
Models::Config’s internal memoization state by setting `@loaded` directly, which
is brittle and can break silently if the implementation changes. Add a public
reset hook on Models::Config to clear the cached config state, then update
validate_command_test to call that helper instead of instance_variable_set so
the test uses the supported API.
lib/skill_bench/cli/validate_command.rb (1)

162-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared env-override logic

provider_client_options/env_setting duplicate the same SKILL_BENCH_<PROVIDER>_<SETTING><PROVIDER>_<SETTING> precedence already used by Models::Provider. Extracting one helper would keep validation and provider resolution in sync if that naming or order changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/validate_command.rb` around lines 162 - 188, The env
override resolution in missing_provider_keys/provider_client_options/env_setting
duplicates the same SKILL_BENCH_<PROVIDER>_<SETTING> then <PROVIDER>_<SETTING>
precedence already used by Models::Provider. Refactor ValidateCommand to reuse a
shared helper or the existing Models::Provider logic for building
provider_client_options so validation stays in sync if the naming or precedence
changes, and keep env_setting only as a thin delegate or remove it if no longer
needed.
test/agent_eval/services/provider_resolver_test.rb (1)

97-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer Minitest's built-in capture_io over a custom stderr-capturing helper.

Minitest::Assertions#capture_io already redirects and restores both $stdout/$stderr around a block and returns the captured strings, making this private helper redundant.

♻️ Proposed refactor
-      private
-
-      def capture_stderr
-        original = $stderr
-        $stderr = StringIO.new
-        yield
-        $stderr.string
-      ensure
-        $stderr = original
-      end
-
       def write_mock_config

And update call sites to use capture_io { ... }.last (stderr is the second element of the returned array) in place of capture_stderr { ... }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/agent_eval/services/provider_resolver_test.rb` around lines 97 - 107,
Replace the custom `capture_stderr` helper in `provider_resolver_test.rb` with
Minitest’s built-in `capture_io`, since it already handles redirecting and
restoring both streams; update every `capture_stderr { ... }` call site to use
`capture_io { ... }.last` so the tests still read captured stderr while removing
the redundant private method.
lib/skill_bench/models/config.rb (1)

27-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reflection-based cache reset is fragile; expose a public API instead.

The docstring instructs resetting by setting @loaded to nil directly, and both test/agent_eval/models/config_test.rb and test/agent_eval/services/provider_resolver_test.rb do this via instance_variable_set at 5 call sites. If @loaded is ever renamed, instance_variable_set silently no-ops (no error), and tests would start leaking stale cache across runs without any signal. A small public method removes the duplication and the encapsulation break.

♻️ Proposed addition
+      # Clears the memoized config cache. Intended for test isolation.
+      #
+      # `@return` [void]
+      def self.reset_cache!
+        `@loaded` = {}
+      end
+
       # Returns the configuration for a path, memoizing the parse per run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/models/config.rb` around lines 27 - 50, The cache reset for
SkillBench::Models::Config is being done via direct ivar mutation and
reflection, which is brittle and duplicated across tests. Add a small public
reset API on SkillBench::Models::Config to clear the `@loaded` cache, update the
loaded memoization docs to reference that API instead of setting the ivar
directly, and replace all instance_variable_set calls in config and
provider_resolver tests with the new method.
lib/skill_bench/services/runner_service.rb (1)

177-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared usage-accumulation helpers to avoid triple duplication.

empty_usage/add_usage/token_count here are identical to the same-named methods in lib/skill_bench/agent/react_agent/loop_runner.rb (lines 75-100), and token_count's symbol/string-key fallback logic is reimplemented again in lib/skill_bench/services/cost_calculator.rb (lines 82-88). Consider extracting a shared module (e.g. SkillBench::Support::TokenUsage) with empty, add, and count so a future fix to usage-key handling doesn't need to be applied in three places.

♻️ Sketch of a shared helper module
module SkillBench
  module Support
    module TokenUsage
      def self.empty
        { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
      end

      def self.add(total, usage)
        usage ||= {}
        {
          prompt_tokens: total[:prompt_tokens] + count(usage, :prompt_tokens),
          completion_tokens: total[:completion_tokens] + count(usage, :completion_tokens),
          total_tokens: total[:total_tokens] + count(usage, :total_tokens)
        }
      end

      def self.count(usage, key)
        (usage[key] || usage[key.to_s] || 0).to_i
      end
    end
  end
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/services/runner_service.rb` around lines 177 - 221, The
token-usage helper logic is duplicated across RunnerService, LoopRunner, and
CostCalculator, so consolidate it into a shared helper/module (for example,
SkillBench::Support::TokenUsage) with the equivalent of empty_usage, add_usage,
and token_count. Update RunnerService#aggregate_usage and the other call sites
to use the shared methods, preserving the symbol/string-key fallback behavior in
token_count so future fixes only need to happen in one place.
lib/skill_bench/output_formatter.rb (1)

141-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate token/cost rendering logic between formatters.

build_usage_line formats Tokens: N | Est. Cost: $X.XXXX here, and per graph evidence Services::HtmlFormatter independently builds the same "Tokens"/"Est. Cost" string with its own Kernel.format('$%.4f', cost) call (html_formatter.rb:114). Consider extracting a shared helper (e.g., a small Services::UsageFormatter or module method) that both the human and HTML formatters call, so cost-formatting/precision stays consistent if it ever changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/output_formatter.rb` around lines 141 - 171, The token/cost
summary formatting is duplicated between OutputFormatter’s build_usage_line and
Services::HtmlFormatter, which risks inconsistent formatting if either changes.
Extract the shared “Tokens / Est. Cost” rendering into a common helper (for
example, a Services::UsageFormatter method) and have both build_usage_line and
the HTML formatter call it so the cost precision and label stay aligned in one
place.
action.yml (1)

57-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider pinning the gem version for reproducible CI gates.

gem install ruby-skill-bench always installs latest; an unpinned dependency could break consumer pipelines unexpectedly on a future release. Consider adding an optional version input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 57 - 59, The ruby-skill-bench install step in the
workflow is unpinned, so CI may change behavior when new gem releases are
published. Update the workflow around the install step to support a versioned
install, ideally by adding an optional version input and using it in the install
command so consumers can pin ruby-skill-bench to a specific release. Keep the
change localized to the action definition where the gem is installed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@action.yml`:
- Around line 61-73: Replace the direct GitHub expression interpolation in the
Run skill-bench step with shell variables passed via env:, because the current
use of inputs.* inside the bash script can be script-injected before bash
parsing. Update the run script in action.yml to read those values from
environment variables instead of expanding them inline, and keep the
argument-building logic in the skill-bench step so it uses the safe variables
when constructing run_args and invoking skill-bench.

In `@lib/skill_bench/cli/run_command.rb`:
- Around line 87-91: The run command is passing an unsupported `:summary` option
into the single-eval path, causing `Commands::Run.run` to reject the keyword.
Update `RunCommand#run_single` (and any option handoff it uses) to strip or
ignore `options[:summary]` before calling `Commands::Run.run`, keeping `summary`
only for the batch-run flow where it is actually handled.
- Line 78: BatchResultPrinter.call is still falling through to
OutputFormatter.format_batch for non-junit batch runs, so --all with html/json
does not use the expected structured output. Update
BatchResultPrinter.batch_output to explicitly handle :html and :json (using the
same batch-specific path as :junit, or equivalent), or otherwise restrict the
supported batch formats advertised by RunCommand so they only include formats
actually implemented.

In `@lib/skill_bench/cli/validate_command.rb`:
- Around line 140-169: Avoid instantiating the provider client in
missing_provider_keys, since Clients::BaseClient#initialize can raise on
unrelated config like base_url/endpoint and bypass the intended FAIL result.
Update check_provider_key/missing_provider_keys to inspect required credential
keys from provider metadata or a non-instantiating helper instead of
Clients::ProviderRegistry.for(...).new(...) and send(:missing_config_keys), so
the CLI stays decoupled from provider-private internals.

In `@lib/skill_bench/client.rb`:
- Around line 38-46: `Client.call` is building a cache key in
`Services::ResponseCache.key` without all request-affecting provider config, so
different provider endpoints can collide. Update the cache key generation in
`Client.call` to also include the relevant `ProviderConfig` fields that change
the actual request, such as `base_url`, `request_path`, `endpoint`, `location`,
`project_id`, and `api_version`, alongside the existing `resolved`, `model`,
`system_prompt`, `messages`, `tools`, and `temperature` values.

In `@lib/skill_bench/services/json_formatter.rb`:
- Around line 27-35: The with_usage_fields method in JsonFormatter still allows
an explicit nil tokens value to overwrite the computed fallback, so fix the
merge behavior there by preventing nil :tokens from replacing the derived usage
data or by merging in the opposite order. Update the logic around
result[:tokens], result.dig(:response, :tokens), and EMPTY_USAGE so the output
always preserves a non-nil tokens value, and add a test covering the tokens: nil
case.

In `@lib/skill_bench/services/response_cache.rb`:
- Around line 68-89: ResponseCache#fetch is not thread-safe, so concurrent
Parallel.map callers can race between key? and []= and trigger duplicate
requests. Update the shared cache access in fetch (and any related store
helpers) to serialize the read/write around the per-process store, using a mutex
or equivalent synchronization on the store in ResponseCache so only one thread
computes and stores a missing value per key.

In `@lib/skill_bench/tools/run_command.rb`:
- Around line 96-104: `arguments_permitted?` is failing open because it looks up
`SkillBench::Config.command_argument_constraints` using a String `base_cmd`
while the config may contain symbol keys. Update
`RunCommand.arguments_permitted?` to normalize the lookup key (or normalize keys
in `SkillBench::Config.command_argument_constraints`), so
`constraints[base_cmd]` finds symbol-keyed entries consistently and forbidden
arguments are actually checked. Keep the fix centered on `arguments_permitted?`
and the config accessor used by it.

In `@test/evaluator/tools/run_command_test.rb`:
- Around line 103-134: The new constraint tests only cover the facade path and
miss the JSON-loading/key-type issues; update the tests around RunCommand.call
and SkillBench::Config so they load command_argument_constraints from a real
skill-bench.json via Config::JsonLoader after Config.reset/load_from_file
instead of assigning the hash directly. Add a regression case that uses the
loaded config to verify the constraint is enforced, so the behavior exercised
matches the bug locations in Config::Applier and RunCommand.

---

Outside diff comments:
In @.github/PULL_REQUEST_TEMPLATE.md:
- Around line 17-26: The checklist entry for the YARD coverage command uses the
wrong invocation syntax, so update the pull request template’s checklist item to
use the Rake task form instead of a bare `yard:coverage` command. Make the
change in the template text where the existing `bundle exec yard:coverage`
appears, keeping it consistent with the other `bundle exec rake ...` entries and
preserving the `yard:coverage` task name.

In `@lib/skill_bench/config/applier.rb`:
- Around line 40-47: `command_argument_constraints` is being applied in
`Applier#apply_scalar_values`, but JSON configs never pass it through because
`JsonLoader` filters it out. Update `JsonLoader`’s `slice(...)` whitelist to
include `command_argument_constraints` so the value from `skill-bench.json`
reaches `Applier` and gets assigned to the store.

---

Nitpick comments:
In `@action.yml`:
- Around line 57-59: The ruby-skill-bench install step in the workflow is
unpinned, so CI may change behavior when new gem releases are published. Update
the workflow around the install step to support a versioned install, ideally by
adding an optional version input and using it in the install command so
consumers can pin ruby-skill-bench to a specific release. Keep the change
localized to the action definition where the gem is installed.

In `@examples/README.md`:
- Around line 7-9: The README tip in the mock config section uses contradictory
wording by saying the CLI command “scaffold[s] ... by hand”; update the sentence
in the documentation to clearly separate the automatic path from the manual
fallback. Keep the reference to the `skill-bench init --mock` command, and
rephrase the fallback so it explicitly refers to writing the JSON config
manually instead of implying the command does it “by hand.”

In `@lib/skill_bench/cli/init_command.rb`:
- Around line 63-67: The provider-required hint in error_missing_provider omits
the offline --mock option because it only uses
SkillBench::Clients::ProviderSchemas.names; update the provider list
construction in this method to include --mock alongside the schema-based
providers so the warning mentions every valid init option.

In `@lib/skill_bench/cli/validate_command.rb`:
- Around line 162-188: The env override resolution in
missing_provider_keys/provider_client_options/env_setting duplicates the same
SKILL_BENCH_<PROVIDER>_<SETTING> then <PROVIDER>_<SETTING> precedence already
used by Models::Provider. Refactor ValidateCommand to reuse a shared helper or
the existing Models::Provider logic for building provider_client_options so
validation stays in sync if the naming or precedence changes, and keep
env_setting only as a thin delegate or remove it if no longer needed.

In `@lib/skill_bench/models/config.rb`:
- Around line 27-50: The cache reset for SkillBench::Models::Config is being
done via direct ivar mutation and reflection, which is brittle and duplicated
across tests. Add a small public reset API on SkillBench::Models::Config to
clear the `@loaded` cache, update the loaded memoization docs to reference that
API instead of setting the ivar directly, and replace all instance_variable_set
calls in config and provider_resolver tests with the new method.

In `@lib/skill_bench/output_formatter.rb`:
- Around line 141-171: The token/cost summary formatting is duplicated between
OutputFormatter’s build_usage_line and Services::HtmlFormatter, which risks
inconsistent formatting if either changes. Extract the shared “Tokens / Est.
Cost” rendering into a common helper (for example, a Services::UsageFormatter
method) and have both build_usage_line and the HTML formatter call it so the
cost precision and label stay aligned in one place.

In `@lib/skill_bench/services/runner_service.rb`:
- Around line 177-221: The token-usage helper logic is duplicated across
RunnerService, LoopRunner, and CostCalculator, so consolidate it into a shared
helper/module (for example, SkillBench::Support::TokenUsage) with the equivalent
of empty_usage, add_usage, and token_count. Update RunnerService#aggregate_usage
and the other call sites to use the shared methods, preserving the
symbol/string-key fallback behavior in token_count so future fixes only need to
happen in one place.

In `@test/agent_eval/services/provider_resolver_test.rb`:
- Around line 97-107: Replace the custom `capture_stderr` helper in
`provider_resolver_test.rb` with Minitest’s built-in `capture_io`, since it
already handles redirecting and restoring both streams; update every
`capture_stderr { ... }` call site to use `capture_io { ... }.last` so the tests
still read captured stderr while removing the redundant private method.

In `@test/cli/validate_command_test.rb`:
- Line 26: The test is tightly coupled to Models::Config’s internal memoization
state by setting `@loaded` directly, which is brittle and can break silently if
the implementation changes. Add a public reset hook on Models::Config to clear
the cached config state, then update validate_command_test to call that helper
instead of instance_variable_set so the test uses the supported API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 1f1025e8-27c3-4a38-be27-729fd16c0471

📥 Commits

Reviewing files that changed from the base of the PR and between f388709 and 097f7b9.

📒 Files selected for processing (66)
  • .github/ISSUE_TEMPLATE/bug_report.md
  • .github/ISSUE_TEMPLATE/config.yml
  • .github/ISSUE_TEMPLATE/feature_request.md
  • .github/PULL_REQUEST_TEMPLATE.md
  • .reek.yml
  • CHANGELOG.md
  • CODE_OF_CONDUCT.md
  • README.md
  • action.yml
  • docs/architecture.md
  • examples/README.md
  • lib/skill_bench.rb
  • lib/skill_bench/agent/react_agent/loop_runner.rb
  • lib/skill_bench/agent/react_agent/step.rb
  • lib/skill_bench/cli.rb
  • lib/skill_bench/cli/batch_result_printer.rb
  • lib/skill_bench/cli/help_printer.rb
  • lib/skill_bench/cli/init_command.rb
  • lib/skill_bench/cli/result_printer.rb
  • lib/skill_bench/cli/run_command.rb
  • lib/skill_bench/cli/validate_command.rb
  • lib/skill_bench/client.rb
  • lib/skill_bench/clients/all.rb
  • lib/skill_bench/clients/provider_schemas.rb
  • lib/skill_bench/clients/providers/mistral.rb
  • lib/skill_bench/commands/init.rb
  • lib/skill_bench/config.rb
  • lib/skill_bench/config/applier.rb
  • lib/skill_bench/config/defaults.rb
  • lib/skill_bench/config/facade_writers.rb
  • lib/skill_bench/config/store.rb
  • lib/skill_bench/models/config.rb
  • lib/skill_bench/output_formatter.rb
  • lib/skill_bench/package_verifier.rb
  • lib/skill_bench/services/agent_spawner_service.rb
  • lib/skill_bench/services/compare_option_parser.rb
  • lib/skill_bench/services/cost_calculator.rb
  • lib/skill_bench/services/html_formatter.rb
  • lib/skill_bench/services/json_formatter.rb
  • lib/skill_bench/services/junit_formatter.rb
  • lib/skill_bench/services/provider_resolver.rb
  • lib/skill_bench/services/response_cache.rb
  • lib/skill_bench/services/runner_service.rb
  • lib/skill_bench/services/summary_formatter.rb
  • lib/skill_bench/tools/run_command.rb
  • test/agent_eval/commands/init_test.rb
  • test/agent_eval/models/config_test.rb
  • test/agent_eval/output_formatter_test.rb
  • test/agent_eval/services/agent_spawner_service_test.rb
  • test/agent_eval/services/provider_resolver_test.rb
  • test/agent_eval/services/runner_service_test.rb
  • test/cli/batch_result_printer_test.rb
  • test/cli/init_command_test.rb
  • test/cli/run_command_test.rb
  • test/cli/validate_command_test.rb
  • test/client_caching_test.rb
  • test/evaluator/clients/providers/mistral_test.rb
  • test/evaluator/react_agent/loop_runner_test.rb
  • test/evaluator/react_agent/step_test.rb
  • test/evaluator/tools/run_command_test.rb
  • test/services/cost_calculator_test.rb
  • test/services/html_formatter_test.rb
  • test/services/json_formatter_test.rb
  • test/services/junit_formatter_test.rb
  • test/services/response_cache_test.rb
  • test/services/summary_formatter_test.rb
🛑 Comments failed to post (9)
action.yml (1)

61-73: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use env: instead of direct ${{ }} interpolation in the run: script.

Quoting ${{ inputs.* }} inside the script does not prevent injection — GitHub substitutes the expression into the script text before bash parses it, so a value like "; rm -rf / # can break out of the quotes and execute arbitrary commands on the runner. This is GitHub's documented "script injection" pattern and the recommended mitigation is to pass values through env: and reference them as shell variables.

🔒️ Proposed fix
     - name: Run skill-bench
       shell: bash
+      env:
+        SB_EVALS_DIR: ${{ inputs.evals-dir }}
+        SB_FORMAT: ${{ inputs.format }}
+        SB_SKILL: ${{ inputs.skill }}
+        SB_ARGS: ${{ inputs.args }}
       # A non-zero exit here fails the step — that is the CI gate.
       run: |
-        run_args=(run --all --evals-dir "${{ inputs.evals-dir }}" --format "${{ inputs.format }}")
-        if [ -n "${{ inputs.skill }}" ]; then
-          run_args+=(--skill "${{ inputs.skill }}")
+        run_args=(run --all --evals-dir "$SB_EVALS_DIR" --format "$SB_FORMAT")
+        if [ -n "$SB_SKILL" ]; then
+          run_args+=(--skill "$SB_SKILL")
         fi
-        if [ -n "${{ inputs.args }}" ]; then
+        if [ -n "$SB_ARGS" ]; then
           # Intentionally unquoted so multiple extra flags split into words.
-          run_args+=(${{ inputs.args }})
+          run_args+=($SB_ARGS)
         fi
         skill-bench "${run_args[@]}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    - name: Run skill-bench
      shell: bash
      env:
        SB_EVALS_DIR: ${{ inputs.evals-dir }}
        SB_FORMAT: ${{ inputs.format }}
        SB_SKILL: ${{ inputs.skill }}
        SB_ARGS: ${{ inputs.args }}
      # A non-zero exit here fails the step — that is the CI gate.
      run: |
        run_args=(run --all --evals-dir "$SB_EVALS_DIR" --format "$SB_FORMAT")
        if [ -n "$SB_SKILL" ]; then
          run_args+=(--skill "$SB_SKILL")
        fi
        if [ -n "$SB_ARGS" ]; then
          # Intentionally unquoted so multiple extra flags split into words.
          run_args+=($SB_ARGS)
        fi
        skill-bench "${run_args[@]}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@action.yml` around lines 61 - 73, Replace the direct GitHub expression
interpolation in the Run skill-bench step with shell variables passed via env:,
because the current use of inputs.* inside the bash script can be
script-injected before bash parsing. Update the run script in action.yml to read
those values from environment variables instead of expanding them inline, and
keep the argument-building logic in the skill-bench step so it uses the safe
variables when constructing run_args and invoking skill-bench.
lib/skill_bench/cli/run_command.rb (2)

78-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "def self.format_batch" lib/skill_bench/output_formatter.rb lib/skill_bench/services/html_formatter.rb

Repository: igmarin/ruby-skill-bench

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== batch_result_printer ==\n'
sed -n '1,220p' lib/skill_bench/services/batch_result_printer.rb

printf '\n== output_formatter ==\n'
sed -n '1,220p' lib/skill_bench/output_formatter.rb

printf '\n== html_formatter ==\n'
sed -n '1,240p' lib/skill_bench/services/html_formatter.rb

printf '\n== run command ==\n'
sed -n '1,220p' lib/skill_bench/cli/run_command.rb

printf '\n== commands run ==\n'
fd -a 'run.rb' lib/skill_bench && sed -n '1,220p' lib/skill_bench/commands/run.rb

printf '\n== compare option parser ==\n'
sed -n '1,220p' lib/skill_bench/services/compare_option_parser.rb

Repository: igmarin/ruby-skill-bench

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== lib/skill_bench tree ==\n'
find lib/skill_bench -maxdepth 4 -type f | sort

printf '\n== target identifiers ==\n'
rg -n "BatchResultPrinter|OutputFormatter|HtmlFormatter|CompareOptionParser|class RunCommand|module Commands|def self.format_batch|summary" lib/skill_bench

Repository: igmarin/ruby-skill-bench

Length of output: 17085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== cli/batch_result_printer.rb ==\n'
sed -n '1,220p' lib/skill_bench/cli/batch_result_printer.rb

printf '\n== output_formatter.rb ==\n'
sed -n '1,240p' lib/skill_bench/output_formatter.rb

printf '\n== services/html_formatter.rb ==\n'
sed -n '1,260p' lib/skill_bench/services/html_formatter.rb

printf '\n== commands/run.rb ==\n'
sed -n '1,220p' lib/skill_bench/commands/run.rb

Repository: igmarin/ruby-skill-bench

Length of output: 21040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' lib/skill_bench/cli/run_command.rb

Repository: igmarin/ruby-skill-bench

Length of output: 4161


Batch --all --format html/json still renders human output. BatchResultPrinter.batch_output only handles :junit; every other format falls through to OutputFormatter.format_batch, so the new batch help text advertises modes this path doesn’t implement. Add explicit :html/:json handling here or narrow the documented batch formats.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/run_command.rb` at line 78, BatchResultPrinter.call is
still falling through to OutputFormatter.format_batch for non-junit batch runs,
so --all with html/json does not use the expected structured output. Update
BatchResultPrinter.batch_output to explicitly handle :html and :json (using the
same batch-specific path as :junit, or equivalent), or otherwise restrict the
supported batch formats advertised by RunCommand so they only include formats
actually implemented.

87-91: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'def self.run($$$)' --lang ruby lib/skill_bench/commands/run.rb

Repository: igmarin/ruby-skill-bench

Length of output: 717


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## run_command.rb\n'
sed -n '1,180p' lib/skill_bench/cli/run_command.rb

printf '\n## run.rb\n'
sed -n '1,120p' lib/skill_bench/commands/run.rb

Repository: igmarin/ruby-skill-bench

Length of output: 5092


Strip :summary before delegating single-eval runs. run_single forwards options[:summary] to Commands::Run.run, but that method only accepts eval_name, skill_names, pack, and registry_manifest, so skill-bench run <eval> --summary ... raises ArgumentError: unknown keyword: :summary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/run_command.rb` around lines 87 - 91, The run command is
passing an unsupported `:summary` option into the single-eval path, causing
`Commands::Run.run` to reject the keyword. Update `RunCommand#run_single` (and
any option handoff it uses) to strip or ignore `options[:summary]` before
calling `Commands::Run.run`, keeping `summary` only for the batch-run flow where
it is actually handled.
lib/skill_bench/cli/validate_command.rb (1)

140-169: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect provider client constructors/missing_config_keys for eager validation or network calls
fd -t f . lib/skill_bench/clients/providers --exec cat -n {} \;
rg -n 'def initialize' lib/skill_bench/clients --type=ruby -A 10
rg -n 'missing_config_keys' lib/skill_bench --type=ruby -A 5

Repository: igmarin/ruby-skill-bench

Length of output: 36102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' lib/skill_bench/clients/base_client.rb | cat -n
printf '\n--- provider_config ---\n'
sed -n '1,220p' lib/skill_bench/clients/provider_config.rb | cat -n
printf '\n--- provider_registry ---\n'
sed -n '1,220p' lib/skill_bench/clients/provider_registry.rb | cat -n
printf '\n--- validate_command slice ---\n'
sed -n '120,190p' lib/skill_bench/cli/validate_command.rb | cat -n

Repository: igmarin/ruby-skill-bench

Length of output: 18416


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- validate_command top ---'
sed -n '1,120p' lib/skill_bench/cli/validate_command.rb | cat -n
printf '\n%s\n' '--- search for constructor overrides / network primitives in providers ---'
rg -n 'def initialize|Net::HTTP|Faraday|HTTP\.|fetch\(|open\(' lib/skill_bench/clients/providers lib/skill_bench/clients -g '*.rb'

Repository: igmarin/ruby-skill-bench

Length of output: 7928


Avoid constructing the provider just to inspect missing keys. BaseClient#initialize runs local config validation, so a bad base_url/endpoint can escape this path and fall through to the top-level Error: rescue instead of a FAIL row. send(:missing_config_keys) also couples the CLI to provider-private internals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/cli/validate_command.rb` around lines 140 - 169, Avoid
instantiating the provider client in missing_provider_keys, since
Clients::BaseClient#initialize can raise on unrelated config like
base_url/endpoint and bypass the intended FAIL result. Update
check_provider_key/missing_provider_keys to inspect required credential keys
from provider metadata or a non-instantiating helper instead of
Clients::ProviderRegistry.for(...).new(...) and send(:missing_config_keys), so
the CLI stays decoupled from provider-private internals.
lib/skill_bench/client.rb (1)

38-46: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
printf '\n== files ==\n'
git ls-files | rg '^(lib/skill_bench/client\.rb|lib/.*/response_cache\.rb|lib/skill_bench/.+|spec/.+client.+|spec/.+response_cache.+)$'

printf '\n== client outline ==\n'
ast-grep outline lib/skill_bench/client.rb --view expanded

printf '\n== response cache outline ==\n'
fd -i 'response_cache.rb' lib spec

Repository: igmarin/ruby-skill-bench

Length of output: 6587


🏁 Script executed:

set -e
sed -n '1,220p' lib/skill_bench/client.rb
printf '\n---\n'
fd -i 'response_cache.rb' lib spec -x sh -c 'echo "FILE: $1"; sed -n "1,240p" "$1"' sh {}

Repository: igmarin/ruby-skill-bench

Length of output: 7130


🏁 Script executed:

set -e
printf 'CLIENT\n'
sed -n '1,220p' lib/skill_bench/client.rb
printf '\nCACHE\n'
fd -i 'response_cache.rb' lib spec -x sh -c 'echo "--- $1 ---"; sed -n "1,260p" "$1"' sh {}
printf '\nREFERENCES\n'
rg -n "ResponseCache|SKILL_BENCH_CACHE|cache_key|cache\(" lib spec

Repository: igmarin/ruby-skill-bench

Length of output: 8018


🏁 Script executed:

rg -n "\b(max_tokens|top_p|stop|response_format|seed|temperature|tools|model)\b" lib/skill_bench/clients lib/skill_bench/client.rb lib/skill_bench/services

Repository: igmarin/ruby-skill-bench

Length of output: 9134


🏁 Script executed:

sed -n '1,220p' lib/skill_bench/clients/base_client.rb
printf '\n---\n'
sed -n '1,240p' lib/skill_bench/clients/request_builder.rb
printf '\n---\n'
sed -n '1,240p' lib/skill_bench/clients/provider_schemas.rb
printf '\n---\n'
sed -n '1,240p' lib/skill_bench/clients/providers/openai.rb

Repository: igmarin/ruby-skill-bench

Length of output: 12255


🏁 Script executed:

sed -n '1,220p' lib/skill_bench/clients/provider_config.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/anthropic.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/azure_openai.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/ollama.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/opencode.rb

Repository: igmarin/ruby-skill-bench

Length of output: 15656


🏁 Script executed:

rg -n "def request_body|def base_url|def request_path|`@api_version`|`@location`|`@project_id`|`@endpoint`|`@model`|`@tools`|max_tokens|top_p|stop|response_format|seed" lib/skill_bench/clients/providers

Repository: igmarin/ruby-skill-bench

Length of output: 3556


🏁 Script executed:

sed -n '1,240p' lib/skill_bench/clients/provider_config.rb
printf '\n---\n'
sed -n '1,240p' lib/skill_bench/clients/providers/anthropic.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/azure_openai.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/ollama.rb
printf '\n---\n'
sed -n '1,220p' lib/skill_bench/clients/providers/opencode.rb

Repository: igmarin/ruby-skill-bench

Length of output: 15656


Include request-affecting provider config in the cache key. Client.call caches only on provider, model, system_prompt, messages, tools, and temperature, but ProviderConfig also lets base_url, request_path, endpoint, location, project_id, and api_version vary the actual request. Different endpoints/configs can therefore reuse the same cached response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/client.rb` around lines 38 - 46, `Client.call` is building a
cache key in `Services::ResponseCache.key` without all request-affecting
provider config, so different provider endpoints can collide. Update the cache
key generation in `Client.call` to also include the relevant `ProviderConfig`
fields that change the actual request, such as `base_url`, `request_path`,
`endpoint`, `location`, `project_id`, and `api_version`, alongside the existing
`resolved`, `model`, `system_prompt`, `messages`, `tools`, and `temperature`
values.
lib/skill_bench/services/json_formatter.rb (1)

27-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'FILES:\n'
git ls-files 'lib/skill_bench/services/json_formatter.rb' 'spec' 'test' | sed -n '1,200p'

printf '\nOUTLINE json_formatter.rb:\n'
ast-grep outline lib/skill_bench/services/json_formatter.rb --view expanded || true

printf '\nRELEVANT TEST SEARCH:\n'
rg -n "with_usage_fields|tokens: nil|EMPTY_USAGE|pretty_generate|usage" lib spec test -S || true

printf '\nFILE CONTENT (json_formatter.rb):\n'
nl -ba lib/skill_bench/services/json_formatter.rb | sed -n '1,220p'

Repository: igmarin/ruby-skill-bench

Length of output: 27638


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'json_formatter.rb:\n'
sed -n '1,220p' lib/skill_bench/services/json_formatter.rb

printf '\njson_formatter tests:\n'
sed -n '1,240p' test/services/json_formatter_test.rb

printf '\ncall sites that build result hashes with tokens:\n'
rg -n "tokens:" lib test -S | sed -n '1,240p'

Repository: igmarin/ruby-skill-bench

Length of output: 9771


Merge order can still emit tokens: null in lib/skill_bench/services/json_formatter.rb:27-35. defaults.merge(result) lets an explicit tokens: nil override the computed zeroed fallback, so the JSON shape guarantee still breaks for that input. Flip the merge order or ignore nil :tokens, and add a test for tokens: nil.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/services/json_formatter.rb` around lines 27 - 35, The
with_usage_fields method in JsonFormatter still allows an explicit nil tokens
value to overwrite the computed fallback, so fix the merge behavior there by
preventing nil :tokens from replacing the derived usage data or by merging in
the opposite order. Update the logic around result[:tokens],
result.dig(:response, :tokens), and EMPTY_USAGE so the output always preserves a
non-nil tokens value, and add a test covering the tokens: nil case.
lib/skill_bench/services/response_cache.rb (1)

68-89: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for thread usage around batch/eval execution that could call Client.call concurrently
rg -nP --type=ruby '\bThread\.new\b|\bThread\b' lib/skill_bench

Repository: igmarin/ruby-skill-bench

Length of output: 280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant files first
printf '\n== response_cache outline ==\n'
ast-grep outline lib/skill_bench/services/response_cache.rb --view expanded || true

printf '\n== batch runner outline ==\n'
ast-grep outline lib/skill_bench/services/batch_runner_service.rb --view expanded || true

printf '\n== search for cache usage ==\n'
rg -n "ResponseCache|response_cache|fetch\(" lib/skill_bench

Repository: igmarin/ruby-skill-bench

Length of output: 4593


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the batch runner and response cache implementations in manageable slices.
for f in lib/skill_bench/services/response_cache.rb lib/skill_bench/services/batch_runner_service.rb; do
  echo "== $f =="
  wc -l "$f"
done

echo "== response_cache lines 1-220 =="
sed -n '1,220p' lib/skill_bench/services/response_cache.rb | cat -n

echo "== batch_runner_service lines 1-260 =="
sed -n '1,260p' lib/skill_bench/services/batch_runner_service.rb | cat -n

Repository: igmarin/ruby-skill-bench

Length of output: 10436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the command runner thread usage that was found, and whether it shares cache state.
sed -n '140,230p' lib/skill_bench/tools/run_command.rb | cat -n

echo "== any references to BatchRunnerService call sites =="
rg -n "BatchRunnerService\.new|BatchRunnerService\b" lib/skill_bench

Repository: igmarin/ruby-skill-bench

Length of output: 5246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for actual concurrency in the service layer and cache wiring.
printf '\n== thread / parallel primitives ==\n'
rg -n --type=ruby '\bThread\.new\b|\bParallel\b|\bConcurrent::|\bmutex\b|\bQueue\b|\bfork\b' lib/skill_bench

printf '\n== response cache call sites and ownership ==\n'
rg -n --type=ruby 'ResponseCache|response_cache' lib/skill_bench

printf '\n== batch runner / eval runner call graph hints ==\n'
rg -n --type=ruby 'BatchRunnerService|EvalRunner|RunnerService|run_batch|batch' lib/skill_bench/services lib/skill_bench

Repository: igmarin/ruby-skill-bench

Length of output: 10597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the response cache and the code paths that might share it.
echo "== response_cache.rb =="
wc -l lib/skill_bench/services/response_cache.rb
sed -n '1,220p' lib/skill_bench/services/response_cache.rb | cat -n

echo "== batch_runner_service.rb =="
wc -l lib/skill_bench/services/batch_runner_service.rb
sed -n '1,260p' lib/skill_bench/services/batch_runner_service.rb | cat -n

echo "== thread/parallel primitives in lib/skill_bench =="
rg -n --type=ruby '\bThread\.new\b|\bParallel\b|\bConcurrent::|\bMutex\b|\bQueue\b|\bfork\b' lib/skill_bench

echo "== response cache call sites =="
rg -n --type=ruby 'ResponseCache|response_cache|fetch\(' lib/skill_bench

echo "== batch/eval runner references =="
rg -n --type=ruby 'BatchRunnerService|EvalRunner|RunnerService|run_batch|batch' lib/skill_bench/services lib/skill_bench

Repository: igmarin/ruby-skill-bench

Length of output: 24542


Serialize the shared response cache
ResponseCache is process-wide, and batch/runner paths call Client from Parallel.map threads. The current key?/[]= sequence can race on the same digest, causing duplicate LLM requests and defeating dedup under concurrent runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/services/response_cache.rb` around lines 68 - 89,
ResponseCache#fetch is not thread-safe, so concurrent Parallel.map callers can
race between key? and []= and trigger duplicate requests. Update the shared
cache access in fetch (and any related store helpers) to serialize the
read/write around the per-process store, using a mutex or equivalent
synchronization on the store in ResponseCache so only one thread computes and
stores a missing value per key.
lib/skill_bench/tools/run_command.rb (1)

96-104: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

arguments_permitted? fails open on symbol-keyed constraints.

constraints[base_cmd] looks up with base_cmd (always a String, from command.shellsplit). But SkillBench::Config.command_argument_constraints can hold symbol keys — either set directly via config.command_argument_constraints = { echo: ['-n'] } in a Ruby setup block, or (once the JsonLoader gap noted in applier.rb is fixed) loaded from JSON via JSON.parse(..., symbolize_names: true). In either case constraints[base_cmd] returns nil, disallowed.nil? is true, and the method returns true — silently permitting the very arguments the constraint was meant to block. This is a fail-open bug in a security control, not just an unreachable feature.

🐛 Proposed fix
       def self.arguments_permitted?(base_cmd, argv)
         constraints = SkillBench::Config.command_argument_constraints
         return true if constraints.nil? || constraints.empty?

-        disallowed = constraints[base_cmd]
+        disallowed = constraints[base_cmd] || constraints[base_cmd.to_sym]
         return true if disallowed.nil? || disallowed.empty?

         argv.drop(1).none? { |arg| disallowed.any? { |bad| arg.include?(bad.to_s) } }
       end

Alternatively, normalize keys once at the Config.command_argument_constraints accessor (store.command_argument_constraints&.transform_keys(&:to_s) || {}) so every consumer gets a consistent key type.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

      def self.arguments_permitted?(base_cmd, argv)
        constraints = SkillBench::Config.command_argument_constraints
        return true if constraints.nil? || constraints.empty?

        disallowed = constraints[base_cmd] || constraints[base_cmd.to_sym]
        return true if disallowed.nil? || disallowed.empty?

        argv.drop(1).none? { |arg| disallowed.any? { |bad| arg.include?(bad.to_s) } }
      end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/skill_bench/tools/run_command.rb` around lines 96 - 104,
`arguments_permitted?` is failing open because it looks up
`SkillBench::Config.command_argument_constraints` using a String `base_cmd`
while the config may contain symbol keys. Update
`RunCommand.arguments_permitted?` to normalize the lookup key (or normalize keys
in `SkillBench::Config.command_argument_constraints`), so
`constraints[base_cmd]` finds symbol-keyed entries consistently and forbidden
arguments are actually checked. Keep the fix centered on `arguments_permitted?`
and the config accessor used by it.
test/evaluator/tools/run_command_test.rb (1)

103-134: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tests only cover the Ruby-facade path, masking the JSON-loading and key-type bugs.

Both new constraint tests set SkillBench::Config.command_argument_constraints = { 'echo' => ['-n'] } directly via the facade writer, with String keys matching base_cmd's type. This never exercises loading constraints from an actual skill-bench.json file through Config::JsonLoader, which is the scenario where the missing-slice-key and symbol/string key-mismatch bugs (flagged in lib/skill_bench/config/applier.rb and lib/skill_bench/tools/run_command.rb) actually manifest. Once those are fixed, add a regression test that writes a JSON config with command_argument_constraints and verifies RunCommand.call honors it after Config.reset/load_from_file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/evaluator/tools/run_command_test.rb` around lines 103 - 134, The new
constraint tests only cover the facade path and miss the JSON-loading/key-type
issues; update the tests around RunCommand.call and SkillBench::Config so they
load command_argument_constraints from a real skill-bench.json via
Config::JsonLoader after Config.reset/load_from_file instead of assigning the
hash directly. Add a regression case that uses the loaded config to verify the
constraint is enforced, so the behavior exercised matches the bug locations in
Config::Applier and RunCommand.

igmarin added 4 commits June 30, 2026 16:58
…mbol keys

JsonLoader's slice whitelist dropped command_argument_constraints, so the #21
arg-constraint feature was dead from skill-bench.json. Adds it to the whitelist,
and makes RunCommand.arguments_permitted? look up the command under both String
and Symbol keys (JSON loads symbol keys via symbolize_names). Adds a regression
test that loads the constraint from a real skill-bench.json.
…ock hint

run_single now strips batch-only keys (summary/all/evals_dir) before
Commands::Run.run, fixing a keyword error on 'run <eval> --summary'.
Batch --format json now emits the JSON summary instead of human text.
ValidateCommand rescues provider-client init errors into a FAIL result
(decoupled from base_url validation). init's missing-provider hint lists --mock.
Cache key now includes request-affecting provider config (base_url/request_path/
endpoint/location/project_id/api_version) so distinct endpoints don't collide.
ResponseCache serializes store access with a mutex (computing off-lock so
distinct keys stay concurrent). JsonFormatter no longer lets an explicit
tokens: nil overwrite the usage fallback. ParameterLists stops counting kwargs.
action.yml now passes inputs through env vars instead of interpolating GitHub
expressions into the bash script. PR template uses 'bundle exec rake yard:coverage';
examples README clarifies the init --mock vs by-hand fallback wording.
@igmarin
igmarin merged commit b835392 into main Jun 30, 2026
5 checks passed
@igmarin
igmarin deleted the release/1.2.0 branch June 30, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment