brunnr eval

Run the evaluation harness for a skill. Sends test fixtures to a Claude model and checks the output against structured assertions.


Usage

brunnr eval <skill> [--model MODEL] [-o PATH] [--dry-run]

Prerequisites

The eval command requires the anthropic Python package:

pip install brunnr[eval]

You also need the ANTHROPIC_API_KEY environment variable set:

export ANTHROPIC_API_KEY=sk-ant-...

Arguments

Argument Description
skill Skill slug to evaluate (e.g., ax-rubric). Must exist in skills/<slug>/SKILL.md.

Flags

Flag Short Description
--model Claude model to use. Default: claude-sonnet-4-20250514
--output -o Path to write JSON results file
--dry-run Validate fixtures without making API calls

How it works

1. Load the skill prompt

brunnr reads the SKILL.md from skills/<slug>/SKILL.md and uses it as the system prompt.

2. Load test fixtures

brunnr reads tests/<slug>/test-spec.json which defines test cases:

{
  "cases": [
    {
      "id": "case-1",
      "description": "Score a bad description",
      "fixture": "fixtures/bad-description.json",
      "assertions": [
        {"type": "score_range", "expected": {"min": 1, "max": 2}},
        {"type": "has_table", "expected": true},
        {"type": "has_rewrite", "expected": true}
      ]
    }
  ]
}

Each case references an optional fixture file with input data. The fixture's input or description field becomes the user message sent to the model.

3. Call Claude

For each test case, brunnr sends the skill prompt as the system message and the fixture input as the user message. The model generates a response.

4. Run assertions

brunnr checks the model's output against each assertion in the test case.


Assertion types

Type Expected What it checks
score_range {"min": N, "max": N} Extracts Score: X/5 from output, checks X is in range
has_table true or false Checks for a markdown table (\|...\|...\|...\|) in output
has_rewrite true or false Checks for the word "rewrite" or "rewritten" in output
contains "string" Case-insensitive substring check
format_valid (none) Checks for markdown headers, a score line, and a criteria table
criteria_pass ["criterion1", ...] Extracts criteria table rows, checks listed criteria have "pass"
criteria_fail ["criterion1", ...] Extracts criteria table rows, checks listed criteria have "fail"

Examples

Dry run (validate without API calls)

brunnr eval ax-rubric --dry-run
=== brunnr eval: ax-rubric ===
  2 test cases, model: claude-sonnet-4-20250514

  [case-1] Score a bad description
    DRY RUN - skipped

  [case-2] Score a good description
    DRY RUN - skipped

=== 0/0 passed (2 skipped) ===

Full evaluation

brunnr eval ax-rubric
=== brunnr eval: ax-rubric ===
  2 test cases, model: claude-sonnet-4-20250514

  [case-1] Score a bad description
    PASS: [score_range] Score 1 in [1, 2]
    PASS: [has_table] Table found
    PASS: [has_rewrite] Rewrite found

  [case-2] Score a good description
    PASS: [score_range] Score 5 in [4, 5]

=== 2/2 passed ===

Save results to file

brunnr eval ax-rubric -o results/ax-rubric.json

The output JSON includes the model's response (truncated to 2000 chars), assertion results, and timing:

{
  "skill": "ax-rubric",
  "model": "claude-sonnet-4-20250514",
  "summary": {"pass": 2, "fail": 0, "skipped": 0},
  "cases": [
    {
      "case_id": "case-1",
      "output": "### knowledge_base_query\n\n**Score: 1/5**\n...",
      "assertions": [
        {"assertion": {"type": "score_range", "expected": {"min": 1, "max": 2}}, "passed": true, "detail": "Score 1 in [1, 2]"}
      ],
      "duration_s": 3.42,
      "all_passed": true
    }
  ]
}

Use a different model

brunnr eval ax-rubric --model claude-opus-4-20250514

Writing test specs

See Writing Skills for details on how to create test specifications and fixtures for your skills.


Exit codes

Code Meaning
0 All test cases passed
1 At least one test case failed, or skill/fixtures not found