Getting started¶
This page takes one skill from nothing to a CI gate. Every command is shown with what it prints. The first four steps cost nothing and need no API key.
1. Install¶
uv tool install "skill-lens[pydantic-ai]"
pip install "skill-lens[pydantic-ai]" works the same way. The pydantic-ai extra installs
the real-agent runner and judge; without it, skill-lens still runs the offline default
runner. The langchain extra installs the LangChain runner and judge the same way, and the
two can be installed together: "skill-lens[pydantic-ai,langchain]". From a checkout of this
repository, uv sync --extra pydantic-ai (or --extra langchain) installs the same thing,
and every command then runs as uv run skill-lens ....
An installed GitHub Copilot CLI or Claude Code is a third way to run a real agent, and it needs no extra and no API key — see Runners.
2. Scaffold a suite¶
A skill is a directory containing SKILL.md. Its eval cases live beside it. Say you have:
skills/
refund/
SKILL.md
skill-lens init ./skills/refund
Wrote skills/refund/evals/refund.eval.yaml
Fill in every TODO(skill-lens), then run: skill-lens list skills/refund
The file holds five cases — the common case, the policy edge with mock tools and a
trajectory check, both halves of a triggering pair, and a workspace case for a skill that
writes a file. Every value you must supply reads TODO(skill-lens). A case still holding
one refuses to run (exit 2, naming the field), so the scaffold can never pass by
checking nothing. Delete the cases that do not apply; keep the ones that do.
Pointed at a directory of skills, init scaffolds every skill that has no suite and skips
the rest — see CLI.
3. Fill it in¶
For a first run, two cases are enough. Replace the generated file with:
# skills/refund/evals/refund.eval.yaml
cases:
- name: refuses a refund outside the return window
task: I want a refund for order 1234
tags: [smoke]
tools:
- name: lookup_order
description: Look up an order by its id
parameters:
order_id: string
returns: '{"id": "1234", "status": "delivered", "days_since_delivery": 45}'
- name: issue_refund
description: Issue a refund for an order
parameters:
order_id: string
returns: '{"ok": true}'
trajectory:
called: [lookup_order] # it must look the order up
forbidden: [issue_refund] # and must not refund this one
assertions:
- kind: contains
value: "1234" # name the order you are talking about
- name: never leaks a stack trace
task: I want a refund for order 1234
assertions:
- kind: not_contains
value: Traceback
Mock tools execute nothing: calling one records the call and returns returns verbatim,
so the trajectory is genuinely the model's choice. Case, mock tool, trajectory, assertion —
Concepts defines every term this page uses without stopping to explain it.
The full field reference is Eval files; deciding which cases a skill
needs is Writing evals.
4. Validate for free¶
skill-lens list ./skills
refund 2 case(s) skills/refund
list discovers skills and validates every eval file without calling a runner — no key,
no spend. A malformed file, an unknown assertion kind or a leftover TODO(skill-lens)
stops here with exit 2.
5. Run offline and read a failure¶
skill-lens run ./skills
The default runner is fake: scripted, offline, free. It answers every task with
[fake] <skill> handled: <task> and never calls a tool, so it exercises the whole pipeline
and fails the first case — which is what we want to look at:
[FAIL] refund :: refuses a refund outside the return window (fake)
trajectory: lookup_order was never called
called:lookup_order: lookup_order was never called
output: [fake] refund handled: I want a refund for order 1234
[PASS] refund :: never leaks a stack trace (fake)
1 passed, 1 failed, 0 errored — pass rate 50%
Gate FAILED:
- pass rate 50% is below the required 100%
Read it top down. The contains('1234') assertion held — the fake echo names the order —
but the trajectory check did not: lookup_order was never called. Below the checks is
what the agent actually did: its output:, and a tool calls: list when there were any
(here there were none, which is exactly the problem). A real agent that answered the same
way would fail for the same reason, and you would see the words it chose. Output is cut at
500 characters; a cut is never silent, and --full-output prints all of it. Exit code 0
means the gate passed, 1 failed, 2 something in your own files, flags or environment is
wrong — that is the whole contract with your pipeline. See Gating, which lists
every cause of exit 2. When a run prints something this
walkthrough does not explain, Troubleshooting is keyed by the exact
message.
6. Run against a real agent¶
export OPENAI_API_KEY=...
skill-lens run ./skills --runner pydantic-ai --model openai:gpt-4o-mini
Before spending anything the CLI prints its ceiling:
Plan: up to 1 arm(s) x 1 repeat(s) x 1 runner(s) x 2 case(s) = 2 runs
Rerun one case by any distinctive part of its name:
skill-lens run ./skills --runner pydantic-ai --case "return window"
A --case that matches nothing fails the gate rather than reporting an empty success.
Runners, tools and budgets are covered in Runners.
7. Commit a configuration¶
Rather than repeat the flags, commit skill-lens.toml at your repository root:
default_runner = "pydantic-ai"
model = "openai:gpt-4o-mini"
judge = "pydantic-ai" # turns on the LLM judge for cases with a rubric
min_pass_rate = 1.0
concurrency = 4
Flags still win over the file. Every key, annotated, is in
examples/skill-lens.toml;
the reference is Configuration. Secrets never go in the file — API keys
come from the environment only.
8. Gate pull requests¶
- uses: EmadMokhtar/skill-evaluator@v0.25.0
with:
path: ./skills
runner: pydantic-ai
model: openai:gpt-4o-mini
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
The action installs skill-lens, runs it, publishes a JUnit report for the test pane and a
Markdown summary for the job summary or a pull-request comment, and exits with the gate's
code. Complete workflows are in CI integration.
Next: did the edit help?¶
Once the gate is green, the interesting question is whether an edit to SKILL.md made the
skill better. Add version: to the frontmatter (three-part, like 1.0.0 — see
why it must be text), bump it
with each meaningful edit, and run:
skill-lens run ./skills --runner pydantic-ai --baseline previous
Every case runs twice — the working copy and the previous version resolved from git — and
the report carries the delta. --min-delta turns that into a gate. This repository's own
examples/greeting is versioned for exactly this walkthrough:
Comparative evals.