ruby-skill-bench 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60f6da81bf101f4bb1e5a7b86112aa96fa430ca9a1db7f1331a47e51b194ea73
4
- data.tar.gz: 7f50c1e8a1891a93f0028f98c6a660e1d35ecfaf2104390d94a895ff983d9c93
3
+ metadata.gz: 78725cfc6d6bcc768b30cdd3da521d4895b7f24d1bd388e1056818a45ae71714
4
+ data.tar.gz: f589debec2945344a06c59443dab4b0a64ee969467cb814d86674e2b706acdf9
5
5
  SHA512:
6
- metadata.gz: a828560ad084e9bd1d3336c9002c8b17d47a12955bed6d53e3dd174ef0e971dab24699ab5da40d3445ece5d06afa29e9354e9d033e5d8a11b5c749872487c1cd
7
- data.tar.gz: b81d9dc73aa78be923e8fdecbf789aa5078e1f68c40d9330a0e11ca9632579dc8635e647a3f08730e1a478f3ea16a0e5379fae1b008b461f1f48d6d4714a9e3e
6
+ metadata.gz: 6981311a0bd893fb0f0a443cec3934623ae5fab6c1088ea8b720ce55ef0b901b6851bfc06aed29b355dd187cfc0864c24e59bc2959b24e2bd3479bdeb920db03
7
+ data.tar.gz: ed4e639a719d6ab023e53f43484d48e591d1289559d8b178705905e480dbc682fe109915a461d8b547a733507f132b113790db6b7c325df5df7c27719c649b52
data/README.md CHANGED
@@ -21,7 +21,7 @@ This repo is one of 6 in a composable AI skill ecosystem:
21
21
  | [`agent-mcp-runtime`](https://github.com/igmarin/agent-mcp-runtime) | Rust CLI runtime (pack resolution, MCP) |
22
22
  | [**`ruby-skill-bench`**](https://github.com/igmarin/ruby-skill-bench) | Benchmark/eval engine |
23
23
 
24
- See the [Ecosystem Overview](https://github.com/igmarin/agent-mcp-runtime/blob/main/docs/ecosystem.md) for the full architecture.
24
+ See the [Ecosystem Overview](https://github.com/igmarin/agent-mcp-runtime/blob/main/docs/ecosystem.md) for the full architecture. What comes next in this repo: [ROADMAP.md](ROADMAP.md).
25
25
 
26
26
  ---
27
27
 
@@ -31,7 +31,7 @@ See the [Ecosystem Overview](https://github.com/igmarin/agent-mcp-runtime/blob/m
31
31
  - **Isolated Git Sandboxes**: Every run operates in a temporary repo. Clean diffs, zero side-effects, 100% reproducibility.
32
32
  - **Blind Judging with Dimensions**: LLM judge scores baseline and context independently across 5 canonical dimensions (Correctness, Skill Adherence, Code Quality, Test Coverage, Documentation). Eval authors configure weights and thresholds via `criteria.json`.
33
33
  - **Sophisticated ReAct Loop**: Employs a robust `Thought → Tool → Observation` loop to handle complex, multi-step engineering tasks.
34
- - **Multi-Provider Ecosystem**: Native support for **OpenAI**, **Anthropic**, **Google Gemini**, **Azure OpenAI**, **Ollama**, **Groq**, **DeepSeek**, **Mistral**, and **OpenCode**.
34
+ - **Multi-Provider Ecosystem**: Native support for **OpenAI**, **Anthropic**, **Google Gemini**, **Azure OpenAI**, **Ollama**, **Groq**, **DeepSeek**, **Mistral**, **OpenCode**, **OpenRouter**, **xAI**, and **Amazon Bedrock**.
35
35
  - **Standardized Intelligence**: Consistent reporting format regardless of the underlying LLM provider.
36
36
 
37
37
  ---
@@ -61,18 +61,28 @@ CLI / API → RunnerService → Sandbox + ReAct Agent → LLM Client Layer → P
61
61
  | **OpenAI** | `SKILL_BENCH_OPENAI_API_KEY` | `:openai` |
62
62
  | **Anthropic** | `SKILL_BENCH_ANTHROPIC_API_KEY` | `:anthropic` |
63
63
  | **Gemini** | `SKILL_BENCH_GEMINI_API_KEY` | `:gemini` |
64
- | **Azure** | `SKILL_BENCH_AZURE_API_KEY` | `:azure` |
64
+ | **Azure** | `SKILL_BENCH_AZURE_OPENAI_API_KEY` | `:azure` |
65
65
  | **Ollama** | — | `:ollama` |
66
66
  | **Groq** | `SKILL_BENCH_GROQ_API_KEY` | `:groq` |
67
67
  | **DeepSeek** | `SKILL_BENCH_DEEPSEEK_API_KEY` | `:deepseek` |
68
- | **Mistral** | `SKILL_BENCH_MISTRAL_API_KEY` | `:mistral` |
68
+ | **Mistral** | set `api_key` in `skill-bench.json` (no env mapping) | `:mistral` |
69
69
  | **OpenCode** | `SKILL_BENCH_OPENCODE_API_KEY`, `SKILL_BENCH_OPENCODE_BASE_URL` | `:opencode` |
70
+ | **OpenRouter** | `SKILL_BENCH_OPENROUTER_API_KEY` | `:openrouter` |
71
+ | **xAI** | `SKILL_BENCH_XAI_API_KEY` or `XAI_API_KEY` | `:xai` |
72
+ | **Bedrock** | `SKILL_BENCH_BEDROCK_API_KEY` or `AWS_BEARER_TOKEN_BEDROCK` | `:bedrock` |
73
+ | **Mock** | — (offline, `skill-bench init --mock`) | `:mock` |
70
74
 
71
75
  > **Note:** Environment variables are loaded automatically. You can also configure provider settings in `skill-bench.json` (created by `skill-bench init`).
72
76
  >
73
77
  > **OpenCode requires a custom `base_url`:** OpenCode does not host a public LLM API. You must provide your own OpenAI-compatible endpoint (e.g. a LiteLLM proxy, self-hosted vLLM, or company gateway) via the `base_url` config key. Without it, the provider will fail with "Base URL not set for Opencode".
74
78
  >
75
- > **Mistral** uses Mistral's OpenAI-compatible chat completions API (default model `mistral-large-latest`). Set `SKILL_BENCH_MISTRAL_API_KEY` and scaffold it with `skill-bench init --mistral`.
79
+ > **Mistral** uses Mistral's OpenAI-compatible chat completions API (default model `mistral-large-latest`). Scaffold with `skill-bench init --mistral` and set `config.api_key` in `skill-bench.json`. There is no `SKILL_BENCH_MISTRAL_API_KEY` env mapping.
80
+ >
81
+ > **OpenRouter** uses OpenRouter's OpenAI-compatible API (default model `anthropic/claude-3.5-sonnet`). Set `SKILL_BENCH_OPENROUTER_API_KEY` and scaffold with `skill-bench init --openrouter`.
82
+ >
83
+ > **xAI** uses xAI's OpenAI-compatible chat completions API (default model `grok-4`). Set `SKILL_BENCH_XAI_API_KEY` or `XAI_API_KEY` and scaffold with `skill-bench init --xai`.
84
+ >
85
+ > **Amazon Bedrock** uses the Runtime OpenAI-compatible Chat Completions path (`https://bedrock-runtime.<region>.amazonaws.com/openai/v1/chat/completions`) with a Bedrock API key. Default region `us-east-1` (`SKILL_BENCH_BEDROCK_REGION` or `AWS_REGION`), default model `amazon.nova-lite-v1:0`. Scaffold with `skill-bench init --bedrock`. IAM SigV4 signing is not in this client.
76
86
 
77
87
  ### Command Allowlist
78
88
 
@@ -144,7 +154,7 @@ skill-bench init --openai
144
154
  }
145
155
  ```
146
156
 
147
- **Available providers:** `--openai`, `--anthropic`, `--gemini`, `--ollama`, `--azure`, `--groq`, `--deepseek`, `--mistral`, `--opencode`
157
+ **Available providers:** `--openai`, `--anthropic`, `--gemini`, `--ollama`, `--azure`, `--groq`, `--deepseek`, `--mistral`, `--opencode`, `--openrouter`, `--xai`, `--bedrock`, plus `--mock` for an offline config with no API key.
148
158
 
149
159
  **Zero-config offline path:** `skill-bench init --mock` scaffolds a minimal offline config that needs no API key and no network — `{"provider":"mock","max_execution_time":30}`. Use it to try the full flow (and run the bundled examples) before wiring up a real provider.
150
160
 
@@ -908,7 +918,7 @@ Your eval result depends on **both** conditions. Here is every scenario:
908
918
  - **Traceability**: Every thought and tool call is logged with full backtrace for post-mortem analysis.
909
919
  - **Robust Error Recovery**: Handles provider outages and rate limits gracefully with standardized error logging.
910
920
  - **XML-Safe Output**: JUnit XML output is properly escaped to prevent injection attacks.
911
- - **Test Coverage**: 700+ tests covering core engine, CLI commands, and all provider clients. Run `bundle exec rake test` to see the current count.
921
+ - **Test Coverage**: 910 tests covering core engine, CLI commands, and all provider clients. Run `bundle exec rake test` to see the current count.
912
922
 
913
923
  ## Testing
914
924
 
data/docs/architecture.md CHANGED
@@ -1,209 +1,170 @@
1
- # SkillBench Architecture
2
-
3
- Ruby Skill Bench provides a reproducible and isolated environment for testing AI agents. It consists of several decoupled components that orchestrate the evaluation flow.
4
-
5
- ## High-Level Flow
6
-
7
- 1. **`RunnerService`**: The entry point. Resolves eval, skill, and provider, then runs baseline and context agents.
8
- 2. **`Sandbox`**: Creates a temporary directory, copies task files, and initializes a Git repository for clean, reproducible runs.
9
- 3. **`ContextHydrator`**: Loads skill documentation (.md, .rb, .json, .yml, .yaml, .txt up to 50KB each) and wraps it in XML for the agent's system prompt.
10
- 4. **`ReactAgent`**: Autonomous agent following a **Thought → Tool → Observation** loop.
11
- 5. **`EvaluationRunner`**: Orchestrates blind judging — builds `JudgePrompt` for baseline and context outputs, calls `Judge` twice, then computes deltas via `DeltaReport`.
12
- 6. **`DeltaReport`**: Computes per-dimension deltas and determines verdict based on `pass_threshold` and `minimum_delta`.
13
- 7. **`Client`**: Provider-agnostic abstraction for LLM backends.
14
-
15
- ## Key Components
16
-
17
- ### `SkillBench::Services::RunnerService`
18
-
19
- - Resolves eval, skill, and provider configuration.
20
- - Runs baseline agent (no skill context) and context agent (with skill context).
21
- - Delegates judging and delta computation to `EvaluationRunner`.
22
- - Falls back to mock provider when config is unavailable.
23
-
24
- ### `SkillBench::EvaluationRunner`
25
-
26
- - Builds `JudgePrompt` for baseline and context outputs.
27
- - Calls `Judge` twice (blind scoring).
28
- - Uses `DeltaReport` to compute per-dimension deltas and final verdict.
29
-
30
- ### `SkillBench::DeltaReport`
31
-
32
- - Computes baseline vs context deltas per dimension.
33
- - Verdict requires: `context_total >= pass_threshold` AND `total_delta >= minimum_delta`.
34
-
35
- ### `SkillBench::CLI` Commands
36
-
37
- - `InitCommand` — Creates `skill-bench.json` configuration
38
- - `RunCommand` — Executes evaluations
39
- - `SkillCommand` — Scaffolds new skills with templates
40
- - `EvalCommand` — Creates evaluation scenarios
41
-
42
- ### `SkillBench::Services::TemplateRegistry`
1
+ # SkillBench architecture
2
+
3
+ ## Executive summary
4
+
5
+ Ruby Skill Bench is a Ruby gem that runs the same coding task twice: once without skill context (baseline) and once with it (context). An LLM judge scores each run independently. `DeltaReport` then decides pass or fail from the context total and the delta.
6
+
7
+ Source of truth for a run is the eval directory (`task.md` + `criteria.json`), the skill files loaded into context, and `skill-bench.json` (plus env overrides). Runtime artifacts are a temp git sandbox and `.skill-bench-trends.json`.
8
+
9
+ The rule a contributor must not break: host command execution is fail-closed. `run_command` runs inside the Docker `evaluator-sandbox` image when a daemon and image are available. Otherwise it refuses unless `allow_host_execution` is explicitly true.
10
+
11
+ ### System architecture
12
+
13
+ ```text
14
+ User / CI
15
+ |
16
+ v
17
+ bin/skill-bench -> SkillBench::CLI
18
+ |
19
+ +-- init / validate / skill / eval / compare
20
+ +-- run -> Commands::Run -> Services::RunnerService
21
+ |
22
+ +-- EvalResolver, SkillResolverService, ProviderResolver
23
+ +-- ContextLoaderService (ContextHydrator)
24
+ +-- Parallel: baseline agent | context agent
25
+ | Sandbox (tmp git, optional Docker)
26
+ | ReactAgent + Tools + Client
27
+ +-- Evaluation::Runner (blind Judge x2, concurrent)
28
+ +-- DeltaReport
29
+ +-- TrendRecorderService
30
+ +-- CostCalculator
31
+ v
32
+ OutputFormatter (human / json / junit / html)
33
+ ```
43
34
 
44
- - Provides pre-built templates for generating eval scaffolding
45
- - Supports three template types: `task_md`, `criteria_json`, `skill_md`
46
- - Offers 10 Rails pattern categories: `crud`, `api`, `background_job`, `controller`, `model`, `migration`, `concern`, `policy`, `form_object`, `view_component`
47
- - Enables variable interpolation using `{{variable_name}}` syntax
48
- - Used for programmatic eval creation and tool building
35
+ Outside the gem: LLM HTTP APIs (OpenAI-compatible and native), optional Docker daemon, optional ecosystem `registry.json` for `--pack`.
49
36
 
50
- ### `SkillBench::Sandbox`
37
+ ### Dependency hierarchy
51
38
 
52
- - Uses `Dir.mktmpdir` for isolation.
53
- - Captures state changes using `git diff`.
54
- - Validates sandbox path to prevent directory traversal.
55
- - Cleans up automatically after execution.
39
+ ```text
40
+ CLI -> Commands -> Services (RunnerService, BatchRunnerService, ComparisonRunner)
41
+ |
42
+ +--> Agent / Tools
43
+ +--> Execution (Sandbox, ContextHydrator)
44
+ +--> Evaluation::Runner -> Judge -> Clients
45
+ +--> Config
46
+ +--> Models (Eval, Skill, Criteria, Provider)
47
+ ```
56
48
 
57
- ### `SkillBench::ReactAgent`
49
+ Dependencies point inward: CLI does not call providers; agents do not load config files; clients do not know about evals. Adding a provider means a `Clients::Providers::*` class plus a `ProviderSchemas` entry. Adding a command means a `Cli::*Command` plus a `Commands::*` object. Do not reverse that.
58
50
 
59
- - Implements a stateful loop.
60
- - Supports tool usage (e.g., `read_file`, `write_file`, `run_shell_command`).
61
- - Manages conversation history.
51
+ ---
62
52
 
63
- ### `SkillBench::Clients::BaseClient`
53
+ ## Eval run lifecycle
64
54
 
65
- - Implements the **Template Method** pattern.
66
- - Handles Faraday connection setup and timeouts.
67
- - Centralizes error logging and response normalization.
68
- - Delegates to `ResponseParser`, `ResponseErrorHandler`, and `RequestBuilder`
55
+ Exact order for `skill-bench run <eval> --skill <name>`:
69
56
 
70
- ### `SkillBench::OutputFormatter`
57
+ 1. `SkillBench::CLI` shifts the subcommand and calls `Cli::RunCommand`.
58
+ 2. `RunCommand` parses flags (`--skill`, `--pack`, `--format`, `--all`, `--evals-dir`, `--summary`, `--cache`). `--cache` sets `SKILL_BENCH_CACHE=1`.
59
+ 3. `Commands::Run` calls `Services::RunnerService.call`.
60
+ 4. `EvalResolver` loads `task.md` and `criteria.json`. Path with `/` is used as-is; otherwise `evals/` is prepended.
61
+ 5. `SkillResolverService` resolves skills (local path, `skill_sources`, or `--pack` via `Registry::PackResolver`).
62
+ 6. `ProviderResolver` builds a `Models::Provider` from `Config` (defaults, then `~/.skill-bench.json`, then `./skill-bench.json`, then `ENV`).
63
+ 7. `ContextLoaderService` / `Execution::ContextHydrator` reads skill files. Allowed extensions: `.md`, `.rb`, `.json`, `.yml`, `.yaml`, `.txt`. Per-file cap 50_000 bytes. Total cap 1_000_000 bytes. Symlinks are skipped. Empty context is an error.
64
+ 8. `RunnerService` runs baseline and context agents concurrently (`Parallel.map`, two threads). Each agent runs inside `Execution::Sandbox.run`: copy sources into `Dir.mktmpdir`, hardened `git init`, start Docker if available, yield, stop container, delete the tempdir.
65
+ 9. `Evaluation::Runner` judges baseline and context concurrently. Both judges get `skill_context: nil` (skill text is only for the executing agent). The judge never sees both outputs in one call.
66
+ 10. `DeltaReport` computes per-dimension deltas. Verdict is `context_total >= pass_threshold AND total_delta >= minimum_delta`.
67
+ 11. `TrendRecorderService` appends `.skill-bench-trends.json` and keeps `.skill-bench-trends.json.bak`.
68
+ 12. `CostCalculator` estimates USD from aggregated token usage.
69
+ 13. `Cli::ResultPrinter` / `SkillBench::OutputFormatter` prints human, json, junit, or html and returns exit 0 or 1.
71
70
 
72
- - Formats results as human-readable text, JSON, or JUnit XML
73
- - Human format displays a dimension table with baseline, context, and delta columns
74
- - Escapes XML output to prevent injection
75
- - Provides exit codes for CI/CD integration
71
+ Batch mode (`--all` / `--evals-dir`) uses `BatchRunnerService` instead of a single `RunnerService` call. `compare` runs `RunnerService` twice via `ComparisonRunner`.
76
72
 
77
- ### `SkillBench::ErrorLogger`
73
+ Failure behavior:
78
74
 
79
- - Shared error logging module for all service objects
80
- - Logs error message and full backtrace
81
- - Uses `Rails.logger` when available, falls back to `warn`
75
+ - Missing provider config returns a config error envelope; it does not raise through the CLI.
76
+ - Agent `:error` status short-circuits judging.
77
+ - Judge parse failure returns `{ success: false, response: { error: { message: } } }`.
78
+ - `run_command` with no container and `allow_host_execution` false returns `HOST_EXECUTION_REFUSED` and does not exec.
82
79
 
83
- ## Data Flow: What Passes Between Components
80
+ ---
84
81
 
85
- Understanding what data moves between components helps debug issues and write better evals.
82
+ ## Components
86
83
 
87
- ### Flow 1: RunnerService → EvaluationRunner
84
+ ### `SkillBench::CLI`
88
85
 
89
- ```ruby
90
- # RunnerService builds this and passes it to EvaluationRunner.call
91
- evaluation = {
92
- task: "Create a UserRegistrationService...", # from task.md
93
- criteria: <Criteria object>, # from criteria.json
94
- skill_context: "<agent_context>...SKILL.md...</agent_context>", # from ContextHydrator
95
- baseline_output: '{"result":"...","status":":success"}', # from baseline agent run
96
- context_output: '{"result":"...","status":":success"}' # from context agent run
97
- }
98
- ```
86
+ Owns ARGV dispatch and exit codes. Does not own eval scoring. Subcommands: `init`, `run`, `compare`, `skill`, `eval`, `validate`/`doctor`, `help`.
99
87
 
100
- ### Flow 2: EvaluationRunner → Judge (two calls)
101
-
102
- ```ruby
103
- # First call — baseline (no skill context)
104
- JudgePrompt.call(
105
- task: task,
106
- criteria: criteria,
107
- skill_context: "", # empty string for baseline
108
- agent_output: baseline_output
109
- )
110
-
111
- # Second call — context (with skill context)
112
- JudgePrompt.call(
113
- task: task,
114
- criteria: criteria,
115
- skill_context: skill_context, # XML-wrapped SKILL.md
116
- agent_output: context_output
117
- )
118
- ```
88
+ ### `Services::RunnerService`
119
89
 
120
- ### Flow 3: Judge JudgeResponse
90
+ Owns one eval's baseline+context orchestration, token aggregation, and the envelope passed to the printer. Does not own HTTP or git. Depends on resolvers, sandbox/agent spawners, `Evaluation::Runner`, `TrendRecorderService`, `CostCalculator`.
121
91
 
122
- The judge returns a JSON string like:
92
+ ### `Execution::Sandbox`
123
93
 
124
- ```json
125
- {
126
- "dimensions": {
127
- "correctness": { "score": 28, "max_score": 30, "reasoning": "All requirements met." },
128
- "skill_adherence": { "score": 22, "max_score": 25, "reasoning": "Used .call pattern correctly." }
129
- },
130
- "overall_reasoning": "Solid implementation."
131
- }
132
- ```
94
+ Owns the temp directory, hardened git (`core.hooksPath=/dev/null`, no source `.git` copy), and container lifecycle. Image ref is `evaluator-sandbox:<VERSION>`. Docker run flags: `--network none`, non-root `--user uid:gid`, `--cap-drop ALL` (then CHOWN and DAC_OVERRIDE), `--security-opt no-new-privileges`, volume mount of the sandbox. Does not own command allowlisting (`Tools::RunCommand` does).
133
95
 
134
- `JudgeResponse` parses this, validates that scores are numeric and within bounds, and returns a structured object.
96
+ ### `Execution::ContextHydrator`
135
97
 
136
- ### Flow 4: DeltaReport Output
98
+ Owns packing skill files into XML for the agent system prompt. Does not own skill path resolution.
137
99
 
138
- ```ruby
139
- # DeltaReport receives two JudgeResponse objects
140
- baseline = {
141
- 'correctness' => { score: 12, max_score: 30 },
142
- 'skill_adherence' => { score: 5, max_score: 25 }
143
- }
100
+ ### `Agent::ReactAgent`
144
101
 
145
- context = {
146
- 'correctness' => { score: 28, max_score: 30 },
147
- 'skill_adherence' => { score: 22, max_score: 25 }
148
- }
102
+ Owns the Thought → Tool → Observation loop, capped at `max_iterations` (default 25). Tools: `read_file`, `write_file`, `run_command`. Does not own judging.
149
103
 
150
- # Produces:
151
- deltas = {
152
- 'correctness' => 16, # 28 - 12
153
- 'skill_adherence' => 17 # 22 - 5
154
- }
104
+ ### `Evaluation::Runner`
155
105
 
156
- baseline_total = 17 # 12 + 5
157
- context_total = 50 # 28 + 22
158
- verdict = context_total >= pass_threshold && (context_total - baseline_total) >= minimum_delta
159
- ```
106
+ Owns blind judging and the call to `DeltaReport`. Two judge HTTP calls, concurrent, order preserved. Does not own agent execution.
160
107
 
161
- ## Directory Structure
162
-
163
- The evaluator relies on a strict directory convention:
164
-
165
- ```bash
166
- project-root/
167
- ├── skill-bench.json # Provider configuration
168
- ├── skills/
169
- │ └── my-service/
170
- │ └── SKILL.md # Skill instructions
171
- ├── evals/
172
- │ └── my-first-eval/
173
- │ ├── task.md # Agent prompt
174
- │ └── criteria.json # Scoring rules
175
- └── .skill-bench-trends.json # Benchmark history (auto-generated)
176
- ```
108
+ ### `DeltaReport`
177
109
 
178
- A `.skill-bench-trends.json.bak` file is created automatically as a backup of the trend file.
110
+ Owns per-dimension arithmetic and the boolean verdict. Does not call the network.
179
111
 
180
- ### Skill Discovery
112
+ ### `Clients::BaseClient` and `ProviderRegistry`
181
113
 
182
- Skills are discovered recursively. These are all valid:
114
+ Own HTTP to LLM providers (Faraday, retries, response normalization). Registered providers: openai, anthropic, gemini, ollama, azure, groq, deepseek, mistral, opencode, openrouter, xai, bedrock, plus mock and null. Do not own eval files.
183
115
 
184
- ```bash
185
- skills/my-service/SKILL.md
186
- skills/api/rest-collection/SKILL.md
187
- skills/workflows/tdd-loop/SKILL.md
188
- ```
116
+ ### `Config`
189
117
 
190
- The `SkillResolver` walks `skills/` recursively and matches by directory name.
118
+ Owns the hierarchy Defaults → home JSON → local JSON → ENV. `EnvOverrides` maps `SKILL_BENCH_*` keys. Mistral has no env mapping; its key is `config.api_key` in JSON. Azure env key is `SKILL_BENCH_AZURE_OPENAI_API_KEY`.
191
119
 
192
- ### Eval Discovery
120
+ ### `SkillBench::OutputFormatter`
193
121
 
194
- Evals are resolved in this order:
122
+ Owns user-facing result text. Delegates to `JsonFormatter`, `JUnitFormatter`, `HtmlFormatter`, `DeltaTableFormatter`, `IterationFormatter`, `FeedbackGenerator`. Distinct from `Services::OutputFormatter`, which stringifies agent output for the judge.
195
123
 
196
- 1. If the path contains `/`, use it as-is (e.g., `evals/my-eval`)
197
- 2. Otherwise, prepend `evals/` (e.g., `my-eval` → `evals/my-eval`)
124
+ ### `ErrorLogger`
198
125
 
199
- The eval directory must contain at minimum:
126
+ Logs message plus first five backtrace lines. Uses `Rails.logger` when `Rails` is defined; otherwise `warn`.
200
127
 
201
- - `task.md` — the agent prompt
202
- - `criteria.json` — the scoring rules (optional; defaults to empty criteria if missing)
128
+ ---
203
129
 
204
130
  ## Command isolation
205
131
 
206
- Agent tool commands prefer a Docker container (`evaluator-sandbox`) when available.
207
- See [docker.md](docker.md) for the image contract and `rake docker:build`.
208
- Without Docker, `run_command` refuses host execution unless `allow_host_execution` is true.
209
-
132
+ See [docker.md](docker.md) for the image contract and `rake docker:build`. Live Docker tests are opt-in via `SKILL_BENCH_DOCKER_TESTS=1`.
133
+
134
+ ---
135
+
136
+ ## Source map
137
+
138
+ | Concept | Authoritative file |
139
+ | --- | --- |
140
+ | CLI dispatch | `lib/skill_bench/cli.rb` |
141
+ | Single eval | `lib/skill_bench/services/runner_service.rb` |
142
+ | Blind judge | `lib/skill_bench/evaluation/runner.rb` |
143
+ | Verdict | `lib/skill_bench/delta_report.rb` |
144
+ | Sandbox + Docker | `lib/skill_bench/execution/sandbox.rb` |
145
+ | Host exec gate | `lib/skill_bench/tools/run_command.rb` |
146
+ | Context caps | `lib/skill_bench/constants.rb` (`ContextHydration`) |
147
+ | Config hierarchy | `lib/skill_bench/config.rb` |
148
+ | Provider env keys | `lib/skill_bench/config/env_overrides.rb` |
149
+ | Provider list | `lib/skill_bench/clients/provider_schemas.rb` |
150
+ | Image contract | `docs/docker.md` |
151
+
152
+ Deprecated entry points still in the tree, not on the live path: `SkillBench::Runner`, `EvaluateCommand`, `Task::Evaluator`.
153
+
154
+ ---
155
+
156
+ ## Verification
157
+
158
+ | Claim | Evidence |
159
+ | --- | --- |
160
+ | CLI subcommands | `SkillBench::CLI#call` case in `lib/skill_bench/cli.rb` |
161
+ | Concurrent agents | `RunnerService#run_agents_concurrently` |
162
+ | Concurrent judges | `Evaluation::Runner#run_judges_concurrently` |
163
+ | Verdict formula | `DeltaReport#determine_verdict` |
164
+ | Fail-closed host exec | `Tools::RunCommand` + `Sandbox` comments and `allow_host_execution` default |
165
+ | Docker flags | `Sandbox#start_container` |
166
+ | Context size caps | `Constants::ContextHydration` + `ContextHydrator#collect_context_files` |
167
+ | `--cache` | `Cli::RunCommand` sets `ENV['SKILL_BENCH_CACHE']`; `HelpPrinter` documents it |
168
+ | OpenRouter env | `EnvOverrides::ENV_TO_PROVIDER_SETTINGS` |
169
+
170
+ Evidence gap: whether every provider in `ProviderSchemas` is registered at boot is covered by client tests under `test/evaluator/clients/`, not by a single registry integration test named in this document.
@@ -0,0 +1,9 @@
1
+ # Comparing skill revisions
2
+
3
+ Use the same task, fixture commit, acceptance tests, criteria file, model, host configuration, and repetition count for no skills, current skills, and revised skills. Keep a held-out task set. Run the current and revised conditions against the same no-skills baseline protocol; archive the actual outputs and provenance alongside each report.
4
+
5
+ `Evaluation::Runner` gives both independent judges the same task and criteria. Skill instructions are supplied only to the executing agent. Its `skill_context` argument remains accepted for compatibility but is deliberately omitted from judging. The active evaluator scores one output per independent request, so there is no paired presentation order. A future paired judge must shuffle anonymous outputs and map scores back outside its prompt.
6
+
7
+ Write criteria as observable task requirements before generating outputs. The legacy `skill_adherence` dimension name remains supported; its description must state an independently assessable requirement rather than ask the judge to infer whether a skill was used. Keep treatment names and instructions out of criteria and agent summaries sent to the judge.
8
+
9
+ Executable acceptance tests take precedence over prose scores. Preserve test failures and unavailable checks as evidence; a prose score cannot turn either into a passing execution result. Report repetitions, fixture and skill commits, host/model versions, actual usage, and incomplete runs. A small or inconclusive sample establishes no statistical improvement. Paid comparisons require a manual invocation and an explicit usage cap; the unit suite makes no provider calls.
@@ -42,7 +42,7 @@ skill-bench init --openai
42
42
 
43
43
  This creates `skill-bench.json` with the OpenAI provider config. Use `--force` to overwrite.
44
44
 
45
- **Available providers:** `--openai`, `--anthropic`, `--gemini`, `--ollama`, `--azure`, `--groq`, `--deepseek`, `--opencode`
45
+ **Available providers:** `--openai`, `--anthropic`, `--gemini`, `--ollama`, `--azure`, `--groq`, `--deepseek`, `--mistral`, `--opencode`, `--openrouter`, `--xai`, `--bedrock`, plus `--mock` for an offline config with no API key.
46
46
 
47
47
  > **What is `skill-bench.json`?** This is your config file. It stores your API key, chosen LLM model, timeout, and allowed shell commands. Think of it as `.env` but structured as JSON. You edit it; SkillBench reads it.
48
48
 
@@ -8,7 +8,7 @@ module SkillBench
8
8
  #
9
9
  # @return [Integer] Exit code (always 0)
10
10
  def self.call
11
- providers = SkillBench::Clients::ProviderSchemas.names.map { |name| "--#{name}" }.join(', ')
11
+ providers = (SkillBench::Clients::ProviderSchemas.names.map { |name| "--#{name}" } + ['--mock']).join(', ')
12
12
 
13
13
  puts <<~USAGE
14
14
  Usage: skill-bench <subcommand> [options]
@@ -28,6 +28,7 @@ module SkillBench
28
28
  --all Run every eval under evals/ (batch mode)
29
29
  --evals-dir DIR Run every eval under DIR (batch mode)
30
30
  --summary Emit a JSON summary gate for a batch run (batch mode)
31
+ --cache Enable content-addressed response caching
31
32
 
32
33
  compare <skill-name> --variant-a SPEC --variant-b SPEC --eval PATH
33
34
  Compare the same skill across two pack variants
@@ -20,4 +20,6 @@ require_relative 'providers/groq'
20
20
  require_relative 'providers/deepseek'
21
21
  require_relative 'providers/mistral'
22
22
  require_relative 'providers/openrouter'
23
+ require_relative 'providers/xai'
24
+ require_relative 'providers/bedrock'
23
25
  require_relative 'providers/mock'
@@ -51,6 +51,15 @@ module SkillBench
51
51
  openrouter: {
52
52
  api_key: nil,
53
53
  model: 'anthropic/claude-3.5-sonnet'
54
+ }.freeze,
55
+ xai: {
56
+ api_key: nil,
57
+ model: 'grok-4'
58
+ }.freeze,
59
+ bedrock: {
60
+ api_key: nil,
61
+ model: 'amazon.nova-lite-v1:0',
62
+ location: 'us-east-1'
54
63
  }.freeze
55
64
  }.freeze
56
65
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_client'
4
+ require_relative '../provider_registry'
5
+
6
+ module SkillBench
7
+ module Clients
8
+ module Providers
9
+ # Amazon Bedrock LLM client.
10
+ # Uses Bedrock Runtime's OpenAI-compatible Chat Completions path with a
11
+ # Bedrock API key (bearer). IAM SigV4 signing is a follow-up.
12
+ class Bedrock < BaseClient
13
+ SkillBench::Clients::ProviderRegistry.register(:bedrock, self)
14
+
15
+ DEFAULT_REGION = 'us-east-1'
16
+
17
+ # Returns the provider identifier.
18
+ #
19
+ # @return [Symbol]
20
+ def provider_name
21
+ :bedrock
22
+ end
23
+
24
+ protected
25
+
26
+ # Returns the Bedrock Runtime host for the configured region.
27
+ #
28
+ # @return [String]
29
+ def base_url
30
+ @base_url_config || "https://bedrock-runtime.#{region}.amazonaws.com"
31
+ end
32
+
33
+ # Returns the OpenAI-compatible chat completions path.
34
+ #
35
+ # @return [String]
36
+ def request_path
37
+ @request_path_config || '/openai/v1/chat/completions'
38
+ end
39
+
40
+ private
41
+
42
+ def region
43
+ loc = @location.to_s.strip
44
+ loc.empty? ? DEFAULT_REGION : loc
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_client'
4
+ require_relative '../provider_registry'
5
+
6
+ module SkillBench
7
+ module Clients
8
+ module Providers
9
+ # xAI (Grok) LLM client.
10
+ # Uses xAI's OpenAI-compatible chat completions API with bearer-token auth.
11
+ class Xai < BaseClient
12
+ SkillBench::Clients::ProviderRegistry.register(:xai, self)
13
+
14
+ # Returns the provider identifier.
15
+ #
16
+ # @return [Symbol]
17
+ def provider_name
18
+ :xai
19
+ end
20
+
21
+ protected
22
+
23
+ # Returns the base URL for the xAI API.
24
+ #
25
+ # The version segment lives in {#request_path} so Faraday does not drop
26
+ # it (an absolute request path replaces any path component of the
27
+ # connection base URL).
28
+ #
29
+ # @return [String]
30
+ def base_url
31
+ @base_url_config || 'https://api.x.ai'
32
+ end
33
+
34
+ # Returns the request path for chat completions.
35
+ #
36
+ # @return [String]
37
+ def request_path
38
+ @request_path_config || '/v1/chat/completions'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -36,7 +36,9 @@ module SkillBench
36
36
  groq: { api_key: nil, model: 'llama-3.3-70b-versatile' },
37
37
  deepseek: { api_key: nil, model: 'deepseek-chat' },
38
38
  opencode: { api_key: nil, model: 'opencode-model', base_url: nil },
39
- openrouter: { api_key: nil, model: 'anthropic/claude-3.5-sonnet' }
39
+ openrouter: { api_key: nil, model: 'anthropic/claude-3.5-sonnet' },
40
+ xai: { api_key: nil, model: 'grok-4' },
41
+ bedrock: { api_key: nil, model: 'amazon.nova-lite-v1:0', location: 'us-east-1' }
40
42
  }
41
43
  }
42
44
  end
@@ -49,7 +49,16 @@ module SkillBench
49
49
  'SKILL_BENCH_OPENCODE_MODEL' => %i[opencode model],
50
50
  'OPENCODE_MODEL' => %i[opencode model],
51
51
  'SKILL_BENCH_OPENROUTER_API_KEY' => %i[openrouter api_key],
52
- 'SKILL_BENCH_OPENROUTER_MODEL' => %i[openrouter model]
52
+ 'SKILL_BENCH_OPENROUTER_MODEL' => %i[openrouter model],
53
+ 'SKILL_BENCH_XAI_API_KEY' => %i[xai api_key],
54
+ 'XAI_API_KEY' => %i[xai api_key],
55
+ 'SKILL_BENCH_XAI_MODEL' => %i[xai model],
56
+ 'XAI_MODEL' => %i[xai model],
57
+ 'SKILL_BENCH_BEDROCK_API_KEY' => %i[bedrock api_key],
58
+ 'AWS_BEARER_TOKEN_BEDROCK' => %i[bedrock api_key],
59
+ 'SKILL_BENCH_BEDROCK_MODEL' => %i[bedrock model],
60
+ 'SKILL_BENCH_BEDROCK_REGION' => %i[bedrock location],
61
+ 'AWS_REGION' => %i[bedrock location]
53
62
  }.freeze
54
63
 
55
64
  # Returns provider overrides from the given environment.
@@ -31,6 +31,7 @@ module SkillBench
31
31
  def initialize(task:, criteria:, skill_context:, baseline_output:, context_output:, judge_params: {})
32
32
  @task = task
33
33
  @criteria = criteria
34
+ # Callers still pass skill_context; it is not forwarded to judges.
34
35
  @skill_context = skill_context
35
36
  @baseline_output = baseline_output
36
37
  @context_output = context_output
@@ -66,17 +67,17 @@ module SkillBench
66
67
  # @return [Array(Hash, Hash)] Baseline and context judge results, in order.
67
68
  def run_judges_concurrently
68
69
  runs = [
69
- -> { judge_run(baseline_output, nil) },
70
- -> { judge_run(context_output, skill_context) }
70
+ -> { judge_run(baseline_output) },
71
+ -> { judge_run(context_output) }
71
72
  ]
72
73
  Parallel.map(runs, in_threads: runs.size, &:call)
73
74
  end
74
75
 
75
- def judge_run(output, context)
76
+ def judge_run(output)
76
77
  prompt_result = Judge::Prompt.call(
77
78
  task: task,
78
79
  criteria: criteria,
79
- skill_context: context,
80
+ skill_context: nil,
80
81
  agent_output: output
81
82
  )
82
83
  return prompt_result unless prompt_result[:success]
@@ -127,9 +127,10 @@ module SkillBench
127
127
  #
128
128
  # @raise [RuntimeError] when any git command fails.
129
129
  def setup_git
130
+ # Subcommands only — git_command prepends the binary + GIT_HARDENING flags.
130
131
  subcommands = [
131
132
  ['init', '--quiet'],
132
- ['config', 'user.email', 'evaluator@tessl.io'],
133
+ ['config', 'user.email', 'evaluator@example.com'],
133
134
  ['config', 'user.name', 'Evaluator Sandbox'],
134
135
  ['add', '.'],
135
136
  ['commit', '--quiet', '-m', 'Initial commit']
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error_logger'
4
+
5
+ module SkillBench
6
+ module Judge
7
+ # Sample statistics over repeated judge totals.
8
+ class Variance
9
+ # @param totals [Array<Numeric>] Judge totals from repeated runs of the same eval.
10
+ # @return [Hash] `{ success: true, response: { n:, mean:, stddev:, spread: } }`
11
+ def self.call(totals:)
12
+ new(totals).call
13
+ end
14
+
15
+ # @param totals [Array<Numeric>]
16
+ def initialize(totals)
17
+ @totals = totals
18
+ end
19
+
20
+ # @return [Hash]
21
+ def call
22
+ nums = Array(@totals).map { |total| Float(total) }
23
+ return empty_result(count: nums.size) if nums.size < 2
24
+
25
+ mean = nums.sum / nums.size
26
+ sample_variance = nums.sum { |value| (value - mean)**2 } / (nums.size - 1)
27
+
28
+ {
29
+ success: true,
30
+ response: {
31
+ n: nums.size,
32
+ mean: mean,
33
+ stddev: Math.sqrt(sample_variance),
34
+ spread: nums.max - nums.min
35
+ }
36
+ }
37
+ rescue StandardError => e
38
+ SkillBench::ErrorLogger.log_error(e, 'Judge::Variance Error')
39
+ { success: false, response: { error: { message: e.message } } }
40
+ end
41
+
42
+ private
43
+
44
+ def empty_result(count:)
45
+ { success: true, response: { n: count, mean: nil, stddev: nil, spread: nil } }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -65,7 +65,23 @@ module SkillBench
65
65
  context_icon = trend_icon(trend[:context_trend])
66
66
  baseline_delta = trend[:baseline_delta]
67
67
  context_delta = trend[:context_delta]
68
- " TREND: baseline #{baseline_icon} (#{delta_str(baseline_delta)}), context #{context_icon} (#{delta_str(context_delta)})"
68
+ lines = [
69
+ " TREND: baseline #{baseline_icon} (#{delta_str(baseline_delta)}), context #{context_icon} (#{delta_str(context_delta)})"
70
+ ]
71
+ lines << format_judge_variance(trend[:judge_variance])
72
+ lines.compact.join("\n")
73
+ end
74
+
75
+ private_class_method def self.format_judge_variance(stats)
76
+ return nil unless stats.is_a?(Hash) && stats[:n].to_i >= 2
77
+
78
+ Kernel.format(
79
+ ' JUDGE n=%<n>d mean=%<mean>.1f σ=%<stddev>.1f spread=%<spread>.1f',
80
+ n: stats[:n],
81
+ mean: stats[:mean],
82
+ stddev: stats[:stddev],
83
+ spread: stats[:spread]
84
+ )
69
85
  end
70
86
  end
71
87
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../judge/variance'
4
+
3
5
  module SkillBench
4
6
  class TrendTracker
5
7
  # Calculates performance trends between evaluation results
@@ -25,7 +27,8 @@ module SkillBench
25
27
  context_trend: trend_direction(current_context, previous_context),
26
28
  baseline_delta: current_baseline - previous_baseline,
27
29
  context_delta: current_context - previous_context,
28
- previous_run: previous[:timestamp]
30
+ previous_run: previous[:timestamp],
31
+ judge_variance: judge_variance(matching, current_context)
29
32
  }
30
33
  end
31
34
 
@@ -54,6 +57,12 @@ module SkillBench
54
57
 
55
58
  current > previous ? :improved : :regressed
56
59
  end
60
+
61
+ def judge_variance(matching, current_context)
62
+ totals = matching.filter_map { |entry| entry[:context_total] } + [current_context]
63
+ result = SkillBench::Judge::Variance.call(totals: totals)
64
+ result[:success] ? result[:response] : nil
65
+ end
57
66
  end
58
67
  end
59
68
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SkillBench
4
4
  # The current gem version.
5
- VERSION = '1.3.0'
5
+ VERSION = '1.4.0'
6
6
  end
data/lib/skill_bench.rb CHANGED
@@ -22,6 +22,7 @@ require_relative 'skill_bench/judge'
22
22
  require_relative 'skill_bench/judge/judge'
23
23
  require_relative 'skill_bench/judge/prompt'
24
24
  require_relative 'skill_bench/judge/response'
25
+ require_relative 'skill_bench/judge/variance'
25
26
 
26
27
  # Agent subsystem
27
28
  require_relative 'skill_bench/agent'
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-skill-bench
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Marin
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: cgi
@@ -43,14 +44,20 @@ dependencies:
43
44
  requirements:
44
45
  - - "~>"
45
46
  - !ruby/object:Gem::Version
46
- version: '2.20'
47
+ version: '2.21'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.21.2
47
51
  type: :runtime
48
52
  prerelease: false
49
53
  version_requirements: !ruby/object:Gem::Requirement
50
54
  requirements:
51
55
  - - "~>"
52
56
  - !ruby/object:Gem::Version
53
- version: '2.20'
57
+ version: '2.21'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.21.2
54
61
  - !ruby/object:Gem::Dependency
55
62
  name: parallel
56
63
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +87,7 @@ files:
80
87
  - README.md
81
88
  - bin/skill-bench
82
89
  - docs/architecture.md
90
+ - docs/blind-comparisons.md
83
91
  - docs/docker.md
84
92
  - docs/first-eval-guide.md
85
93
  - docs/testing-guide.md
@@ -113,6 +121,7 @@ files:
113
121
  - lib/skill_bench/clients/provider_schemas.rb
114
122
  - lib/skill_bench/clients/providers/anthropic.rb
115
123
  - lib/skill_bench/clients/providers/azure_openai.rb
124
+ - lib/skill_bench/clients/providers/bedrock.rb
116
125
  - lib/skill_bench/clients/providers/deepseek.rb
117
126
  - lib/skill_bench/clients/providers/gemini.rb
118
127
  - lib/skill_bench/clients/providers/groq.rb
@@ -123,6 +132,7 @@ files:
123
132
  - lib/skill_bench/clients/providers/openai.rb
124
133
  - lib/skill_bench/clients/providers/opencode.rb
125
134
  - lib/skill_bench/clients/providers/openrouter.rb
135
+ - lib/skill_bench/clients/providers/xai.rb
126
136
  - lib/skill_bench/clients/request_builder.rb
127
137
  - lib/skill_bench/clients/response_builder.rb
128
138
  - lib/skill_bench/clients/response_error_handler.rb
@@ -165,6 +175,7 @@ files:
165
175
  - lib/skill_bench/judge/judge.rb
166
176
  - lib/skill_bench/judge/prompt.rb
167
177
  - lib/skill_bench/judge/response.rb
178
+ - lib/skill_bench/judge/variance.rb
168
179
  - lib/skill_bench/migration/provider_migrator.rb
169
180
  - lib/skill_bench/models/config.rb
170
181
  - lib/skill_bench/models/criteria_validator.rb
@@ -234,6 +245,7 @@ licenses:
234
245
  metadata:
235
246
  rubygems_mfa_required: 'true'
236
247
  source_code_uri: https://github.com/igmarin/ruby-skill-bench
248
+ post_install_message:
237
249
  rdoc_options: []
238
250
  require_paths:
239
251
  - lib
@@ -248,7 +260,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
260
  - !ruby/object:Gem::Version
249
261
  version: '0'
250
262
  requirements: []
251
- rubygems_version: 4.0.16
263
+ rubygems_version: 3.5.22
264
+ signing_key:
252
265
  specification_version: 4
253
266
  summary: The evaluation engine for AI Agent Skills benchmarking.
254
267
  test_files: []