skill-lens 0.2.0__tar.gz
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.
- skill_lens-0.2.0/.claude/skills/writing-skill-evals/SKILL.md +99 -0
- skill_lens-0.2.0/.claude/skills/writing-skill-evals/evals/writing-skill-evals.eval.yaml +93 -0
- skill_lens-0.2.0/.claude/skills/writing-skill-evals/references/auditing.md +38 -0
- skill_lens-0.2.0/.claude/skills/writing-skill-evals/references/case-design.md +51 -0
- skill_lens-0.2.0/.claude/skills/writing-skill-evals/references/eval-file-syntax.md +100 -0
- skill_lens-0.2.0/.github/copilot-instructions.md +60 -0
- skill_lens-0.2.0/.github/instructions/cli-gating.instructions.md +27 -0
- skill_lens-0.2.0/.github/instructions/docs.instructions.md +27 -0
- skill_lens-0.2.0/.github/instructions/evaluators.instructions.md +34 -0
- skill_lens-0.2.0/.github/instructions/models-loaders.instructions.md +25 -0
- skill_lens-0.2.0/.github/instructions/runners.instructions.md +46 -0
- skill_lens-0.2.0/.github/instructions/tests.instructions.md +23 -0
- skill_lens-0.2.0/.github/pull_request_template.md +14 -0
- skill_lens-0.2.0/.github/workflows/ci.yml +180 -0
- skill_lens-0.2.0/.github/workflows/docs.yml +55 -0
- skill_lens-0.2.0/.github/workflows/refresh-cassettes.yml +109 -0
- skill_lens-0.2.0/.github/workflows/release.yml +200 -0
- skill_lens-0.2.0/.gitignore +19 -0
- skill_lens-0.2.0/.pre-commit-config.yaml +8 -0
- skill_lens-0.2.0/.python-version +1 -0
- skill_lens-0.2.0/ARCHITECTURE.md +441 -0
- skill_lens-0.2.0/CHANGELOG.md +16 -0
- skill_lens-0.2.0/CLAUDE.md +280 -0
- skill_lens-0.2.0/LICENSE +21 -0
- skill_lens-0.2.0/PKG-INFO +317 -0
- skill_lens-0.2.0/README.md +269 -0
- skill_lens-0.2.0/action.yml +219 -0
- skill_lens-0.2.0/docs/architecture.md +1 -0
- skill_lens-0.2.0/docs/ci.md +145 -0
- skill_lens-0.2.0/docs/cli.md +109 -0
- skill_lens-0.2.0/docs/comparative-evals.md +267 -0
- skill_lens-0.2.0/docs/configuration.md +86 -0
- skill_lens-0.2.0/docs/contributing.md +93 -0
- skill_lens-0.2.0/docs/eval-files.md +161 -0
- skill_lens-0.2.0/docs/gating.md +126 -0
- skill_lens-0.2.0/docs/getting-started.md +81 -0
- skill_lens-0.2.0/docs/index.md +55 -0
- skill_lens-0.2.0/docs/releasing.md +173 -0
- skill_lens-0.2.0/docs/roadmap.md +55 -0
- skill_lens-0.2.0/docs/runners.md +109 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-07-30-skill-eval-m0-m1.md +2359 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-08-01-skill-eval-m2.md +2668 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-08-02-docs-infrastructure.md +1940 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-08-03-skill-eval-m3.md +2828 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-08-03-skill-eval-m4.md +3213 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-08-03-writing-skill-evals.md +1411 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-08-05-skill-eval-m5.md +2709 -0
- skill_lens-0.2.0/docs/superpowers/plans/2026-09-09-skill-eval-m5-part2.md +1086 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-07-30-skill-eval-design.md +203 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-08-01-skill-eval-m2-design.md +250 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-08-02-docs-infrastructure-design.md +210 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-08-03-skill-eval-m3-design.md +345 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-08-03-skill-eval-m4-design.md +402 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-08-03-writing-skill-evals-design.md +181 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-08-05-skill-eval-m5-design.md +479 -0
- skill_lens-0.2.0/docs/superpowers/specs/2026-09-09-skill-eval-m5-part2-design.md +293 -0
- skill_lens-0.2.0/docs/writing-evals.md +37 -0
- skill_lens-0.2.0/examples/ci/skill-lens-cli.yml +44 -0
- skill_lens-0.2.0/examples/ci/skill-lens.yml +98 -0
- skill_lens-0.2.0/examples/greeting/SKILL.md +7 -0
- skill_lens-0.2.0/examples/greeting/greeting.eval.yaml +23 -0
- skill_lens-0.2.0/examples/order-support/SKILL.md +15 -0
- skill_lens-0.2.0/examples/order-support/order-support.eval.yaml +97 -0
- skill_lens-0.2.0/mkdocs.yml +63 -0
- skill_lens-0.2.0/pyproject.toml +128 -0
- skill_lens-0.2.0/scripts/check_commits.py +96 -0
- skill_lens-0.2.0/scripts/check_docs_updated.py +81 -0
- skill_lens-0.2.0/scripts/legacy-commits.txt +6 -0
- skill_lens-0.2.0/src/skill_lens/__init__.py +10 -0
- skill_lens-0.2.0/src/skill_lens/cases/__init__.py +0 -0
- skill_lens-0.2.0/src/skill_lens/cases/loader.py +187 -0
- skill_lens-0.2.0/src/skill_lens/cli.py +323 -0
- skill_lens-0.2.0/src/skill_lens/comparison.py +242 -0
- skill_lens-0.2.0/src/skill_lens/config.py +112 -0
- skill_lens-0.2.0/src/skill_lens/evaluators/__init__.py +0 -0
- skill_lens-0.2.0/src/skill_lens/evaluators/assertion.py +79 -0
- skill_lens-0.2.0/src/skill_lens/evaluators/base.py +18 -0
- skill_lens-0.2.0/src/skill_lens/evaluators/budget.py +106 -0
- skill_lens-0.2.0/src/skill_lens/evaluators/judge.py +112 -0
- skill_lens-0.2.0/src/skill_lens/evaluators/trajectory.py +121 -0
- skill_lens-0.2.0/src/skill_lens/gating.py +110 -0
- skill_lens-0.2.0/src/skill_lens/judges/__init__.py +0 -0
- skill_lens-0.2.0/src/skill_lens/judges/base.py +26 -0
- skill_lens-0.2.0/src/skill_lens/judges/fake.py +38 -0
- skill_lens-0.2.0/src/skill_lens/judges/prompt.py +81 -0
- skill_lens-0.2.0/src/skill_lens/judges/pydantic_ai.py +95 -0
- skill_lens-0.2.0/src/skill_lens/models.py +385 -0
- skill_lens-0.2.0/src/skill_lens/orchestrator.py +377 -0
- skill_lens-0.2.0/src/skill_lens/reporters/__init__.py +0 -0
- skill_lens-0.2.0/src/skill_lens/reporters/console.py +196 -0
- skill_lens-0.2.0/src/skill_lens/reporters/json_reporter.py +67 -0
- skill_lens-0.2.0/src/skill_lens/reporters/junit.py +214 -0
- skill_lens-0.2.0/src/skill_lens/reporters/markdown.py +376 -0
- skill_lens-0.2.0/src/skill_lens/runners/__init__.py +0 -0
- skill_lens-0.2.0/src/skill_lens/runners/base.py +25 -0
- skill_lens-0.2.0/src/skill_lens/runners/fake.py +36 -0
- skill_lens-0.2.0/src/skill_lens/runners/preflight.py +38 -0
- skill_lens-0.2.0/src/skill_lens/runners/pricing.py +33 -0
- skill_lens-0.2.0/src/skill_lens/runners/pydantic_ai.py +225 -0
- skill_lens-0.2.0/src/skill_lens/runners/tools.py +116 -0
- skill_lens-0.2.0/src/skill_lens/scaffold.py +99 -0
- skill_lens-0.2.0/src/skill_lens/skills/__init__.py +0 -0
- skill_lens-0.2.0/src/skill_lens/skills/baseline.py +108 -0
- skill_lens-0.2.0/src/skill_lens/skills/loader.py +107 -0
- skill_lens-0.2.0/src/skill_lens/yaml_loading.py +49 -0
- skill_lens-0.2.0/tests/cassettes/test_cassettes/test_a_baseline_run_reaches_the_provider_without_the_skill_name.yaml +111 -0
- skill_lens-0.2.0/tests/cassettes/test_cassettes/test_a_real_agent_leaves_an_offered_skill_alone_on_an_unrelated_task.yaml +120 -0
- skill_lens-0.2.0/tests/cassettes/test_cassettes/test_a_real_agent_reaches_for_an_offered_skill.yaml +367 -0
- skill_lens-0.2.0/tests/cassettes/test_cassettes/test_a_real_judge_drives_the_evaluator_end_to_end.yaml +428 -0
- skill_lens-0.2.0/tests/cassettes/test_cassettes/test_a_real_judge_grades_a_rubric_with_evidence.yaml +184 -0
- skill_lens-0.2.0/tests/cassettes/test_cassettes/test_real_traffic_drives_the_whole_loop.yaml +258 -0
- skill_lens-0.2.0/tests/conftest.py +159 -0
- skill_lens-0.2.0/tests/fixtures/ci-smoke/SKILL.md +6 -0
- skill_lens-0.2.0/tests/fixtures/ci-smoke/evals/ci-smoke.eval.yaml +9 -0
- skill_lens-0.2.0/tests/fixtures/ci-smoke-fail/SKILL.md +6 -0
- skill_lens-0.2.0/tests/fixtures/ci-smoke-fail/evals/ci-smoke-fail.eval.yaml +10 -0
- skill_lens-0.2.0/tests/test_action.py +84 -0
- skill_lens-0.2.0/tests/test_arms.py +148 -0
- skill_lens-0.2.0/tests/test_assertion_evaluator.py +178 -0
- skill_lens-0.2.0/tests/test_baseline_resolution.py +159 -0
- skill_lens-0.2.0/tests/test_budget_evaluator.py +155 -0
- skill_lens-0.2.0/tests/test_case_loader.py +477 -0
- skill_lens-0.2.0/tests/test_cassette_workflow.py +211 -0
- skill_lens-0.2.0/tests/test_cassettes.py +223 -0
- skill_lens-0.2.0/tests/test_check_docs_updated.py +72 -0
- skill_lens-0.2.0/tests/test_cli.py +626 -0
- skill_lens-0.2.0/tests/test_cli_init.py +122 -0
- skill_lens-0.2.0/tests/test_comparison.py +206 -0
- skill_lens-0.2.0/tests/test_config.py +205 -0
- skill_lens-0.2.0/tests/test_conftest_replay.py +95 -0
- skill_lens-0.2.0/tests/test_docs.py +169 -0
- skill_lens-0.2.0/tests/test_examples.py +27 -0
- skill_lens-0.2.0/tests/test_fake_judge.py +47 -0
- skill_lens-0.2.0/tests/test_fake_runner.py +93 -0
- skill_lens-0.2.0/tests/test_framework_isolation.py +37 -0
- skill_lens-0.2.0/tests/test_gating.py +201 -0
- skill_lens-0.2.0/tests/test_integration_live.py +50 -0
- skill_lens-0.2.0/tests/test_judge_evaluator.py +138 -0
- skill_lens-0.2.0/tests/test_judge_prompt.py +145 -0
- skill_lens-0.2.0/tests/test_junit_reporter.py +246 -0
- skill_lens-0.2.0/tests/test_markdown_reporter.py +429 -0
- skill_lens-0.2.0/tests/test_models.py +312 -0
- skill_lens-0.2.0/tests/test_naming.py +68 -0
- skill_lens-0.2.0/tests/test_orchestrator.py +578 -0
- skill_lens-0.2.0/tests/test_preflight.py +45 -0
- skill_lens-0.2.0/tests/test_pricing.py +40 -0
- skill_lens-0.2.0/tests/test_pydantic_ai_judge.py +194 -0
- skill_lens-0.2.0/tests/test_pydantic_ai_runner.py +499 -0
- skill_lens-0.2.0/tests/test_release_config.py +182 -0
- skill_lens-0.2.0/tests/test_release_workflow.py +191 -0
- skill_lens-0.2.0/tests/test_reporters.py +470 -0
- skill_lens-0.2.0/tests/test_scaffold.py +93 -0
- skill_lens-0.2.0/tests/test_shipped_skill.py +119 -0
- skill_lens-0.2.0/tests/test_skill_loader.py +210 -0
- skill_lens-0.2.0/tests/test_tools.py +133 -0
- skill_lens-0.2.0/tests/test_trajectory_evaluator.py +198 -0
- skill_lens-0.2.0/tests/test_version.py +6 -0
- skill_lens-0.2.0/tests/test_yaml_loading.py +17 -0
- skill_lens-0.2.0/uv.lock +1606 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-skill-evals
|
|
3
|
+
description: Use when writing, running, or auditing skill-lens eval suites for an Agent Skill — deciding which cases a skill needs, choosing between assertions, judge rubrics and trajectory checks, and reading a failing case correctly
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing skill evals
|
|
7
|
+
|
|
8
|
+
An eval suite is worth exactly what its cases check. The two failure modes are a suite
|
|
9
|
+
that is green because it checks nothing, and a red case that gets the skill edited when
|
|
10
|
+
the eval was wrong. Everything here exists to prevent one of those.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. **Orient.** Read the target `SKILL.md`. Look for existing evals in `evals/` or
|
|
15
|
+
`*.eval.yaml` beside it, and run `skill-lens list <path>` to see what the tool already
|
|
16
|
+
discovers.
|
|
17
|
+
2. **Scaffold, or extend.** If step 1 found no suite, run `skill-lens init <skill-dir>` —
|
|
18
|
+
do not hand-roll the file structure, the generated file already carries the triggering
|
|
19
|
+
pair and the placeholders that stop an unfinished suite from running. If a suite
|
|
20
|
+
already exists, do not run `init`: it exits 2 rather than touch an existing file. Read
|
|
21
|
+
`references/auditing.md` and extend the suite you found instead. (`--force` overwrites
|
|
22
|
+
the file outright, which is not what you want when a suite is already there to build
|
|
23
|
+
on.)
|
|
24
|
+
3. **Mine the skill for claims.** Every "always", "never" and "must" in the instructions
|
|
25
|
+
is a candidate case. The frontmatter `description` is a claim too, and it is precisely
|
|
26
|
+
what the `mode: offered` cases test.
|
|
27
|
+
4. **Propose the case list and confirm it.** Show the user the cases you intend to write,
|
|
28
|
+
one line each, before writing any YAML. Ask only for what `SKILL.md` cannot tell you:
|
|
29
|
+
which tools exist and what they return, which policy edges are real, what a good answer
|
|
30
|
+
sounds like. Do not ask for what the file already says.
|
|
31
|
+
5. **Write the cases.** Replace every `TODO(skill-lens)`. Read
|
|
32
|
+
`references/eval-file-syntax.md` for the fields, and `references/case-design.md` for
|
|
33
|
+
patterns to draw from.
|
|
34
|
+
6. **Validate, then run.** `skill-lens list <path>` is the structural check — it parses
|
|
35
|
+
every case at zero cost and catches a malformed file before anything else runs; it is
|
|
36
|
+
what this repo's own CI self-check uses. `skill-lens run <path>` is a different thing:
|
|
37
|
+
it needs a configured runner, and under the defaults (`FakeRunner`, `FakeJudge`) a
|
|
38
|
+
`mode: offered` case and any case with a `judge:` block come back `errored`, not
|
|
39
|
+
`failed` — the fake runner cannot report a triggering decision and the fake judge does
|
|
40
|
+
not grade. Neither is a statement about the skill. Configure a real runner (and a judge,
|
|
41
|
+
if any case needs one) before reading those results as anything.
|
|
42
|
+
7. **Triage every red case** by the rule below before editing anything.
|
|
43
|
+
|
|
44
|
+
## Choosing the check
|
|
45
|
+
|
|
46
|
+
| Use | When |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `assertions` | The check is mechanical and stable: an id appears, a traceback does not. |
|
|
49
|
+
| `judge` | The claim is about quality, tone or reasoning. "Explains it plainly" is not a substring. |
|
|
50
|
+
| `trajectory` | The failure is invisible in the output — deciding without looking the order up, calling the tool that was forbidden. |
|
|
51
|
+
| `budget` | Guarding against a regression into a tool-call loop or a runaway answer. |
|
|
52
|
+
|
|
53
|
+
## Triage: the eval, the skill, or the harness?
|
|
54
|
+
|
|
55
|
+
A red case means one of three different things, and naming which comes before any edit.
|
|
56
|
+
Start with whether the case is `failed` or `errored` — that alone rules one branch out or
|
|
57
|
+
in before you look at anything else.
|
|
58
|
+
|
|
59
|
+
**The eval is wrong** when the case `failed` and the output was actually fine: a regex
|
|
60
|
+
tight enough to feel rigorous but that rejects phrasing a model may legitimately vary, an
|
|
61
|
+
assertion on wording the skill never promised, a budget below what the task honestly
|
|
62
|
+
costs. `examples/greeting/greeting.eval.yaml` in the skill-lens repo documents a real
|
|
63
|
+
instance — a single-sentence regex relaxed after real model output failed it for no good
|
|
64
|
+
reason.
|
|
65
|
+
|
|
66
|
+
**The skill is wrong** when the case `failed` and the output genuinely was not what the
|
|
67
|
+
skill claims: the instruction is missing, or is present but too weak to survive a
|
|
68
|
+
plausible prompt.
|
|
69
|
+
|
|
70
|
+
**The harness is wrong** when the case `errored`. An errored case never ran to a real
|
|
71
|
+
verdict — the runner or the judge broke, or was never configured for what the case asks
|
|
72
|
+
of it. A `mode: offered` case under the default fake runner, or a `judge:` case under the
|
|
73
|
+
default fake judge, errors this way by design: the fake runner cannot report a triggering
|
|
74
|
+
decision, and the fake judge does not grade. An errored case says nothing about the
|
|
75
|
+
skill — fix the configuration (a real runner, a real judge), and change neither the eval
|
|
76
|
+
nor the SKILL.md.
|
|
77
|
+
|
|
78
|
+
Fixing the eval is yours to do. **Changing the target `SKILL.md` is proposed and
|
|
79
|
+
confirmed, never silent** — the user is the author of their skill, and an eval that gets
|
|
80
|
+
its own subject rewritten to match it has stopped measuring anything.
|
|
81
|
+
|
|
82
|
+
## Rules that are not negotiable
|
|
83
|
+
|
|
84
|
+
- **Ship the negative control.** A suite of triggering positives scores a skill that fires
|
|
85
|
+
on everything at 100%. `mode: offered` cases come in pairs.
|
|
86
|
+
- **Never leave a case with no check.** At least one of `assertions`, `trajectory` or
|
|
87
|
+
`judge` — a case with none of the three passes without checking anything. `mode:
|
|
88
|
+
offered` cases legitimately carry `trajectory.skill_triggered` and no `assertions`;
|
|
89
|
+
that still counts as a check.
|
|
90
|
+
- **Every rubric entry must be independently checkable and evidenced.** skill-lens records
|
|
91
|
+
a check that passes without citing evidence as a failure, so a vague entry costs a case
|
|
92
|
+
rather than buying coverage.
|
|
93
|
+
- **Never leave a `TODO(skill-lens)` behind.** The run will refuse it, which is the point,
|
|
94
|
+
but a suite that cannot run is not a suite.
|
|
95
|
+
|
|
96
|
+
## Auditing an existing suite
|
|
97
|
+
|
|
98
|
+
Read `references/auditing.md` and work its checklist. Report findings; do not rewrite the
|
|
99
|
+
user's suite unasked.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Evals for the skill that writes evals. Four cases, three concerns:
|
|
2
|
+
# - triggering: does it fire on an eval-writing request, and stay quiet
|
|
3
|
+
# otherwise (the negative control this skill insists on for everyone else).
|
|
4
|
+
# - authoring: does it read the skill before writing the file, and is what it
|
|
5
|
+
# writes a suite rather than a shape -- judged, because "includes a negative
|
|
6
|
+
# control" is a property of the YAML, not a substring of the reply.
|
|
7
|
+
# - auditing: handed a deliberately weak suite, does it name what is missing.
|
|
8
|
+
cases:
|
|
9
|
+
- name: reaches for the skill on an eval-writing request
|
|
10
|
+
mode: offered
|
|
11
|
+
task: Write evals for my order-support skill in ./skills/order-support
|
|
12
|
+
tags: [triggering]
|
|
13
|
+
trajectory:
|
|
14
|
+
skill_triggered: true
|
|
15
|
+
|
|
16
|
+
- name: leaves unrelated work alone
|
|
17
|
+
mode: offered
|
|
18
|
+
task: Rename the `parse` function in utils.py to `parse_input`
|
|
19
|
+
tags: [triggering]
|
|
20
|
+
trajectory:
|
|
21
|
+
skill_triggered: false
|
|
22
|
+
|
|
23
|
+
- name: reads the skill before writing the suite
|
|
24
|
+
task: >-
|
|
25
|
+
Write an eval suite for the skill at ./skills/order-support. Read the
|
|
26
|
+
skill first, then write the file.
|
|
27
|
+
tags: [authoring, judged]
|
|
28
|
+
tools:
|
|
29
|
+
- name: read_file
|
|
30
|
+
description: Read a file from disk
|
|
31
|
+
parameters:
|
|
32
|
+
path: string
|
|
33
|
+
returns: |
|
|
34
|
+
---
|
|
35
|
+
name: order-support
|
|
36
|
+
description: Handle customer refund requests against the 30-day return policy
|
|
37
|
+
---
|
|
38
|
+
Always call lookup_order before saying anything about an order's state.
|
|
39
|
+
Never issue a refund for an order delivered more than 30 days ago.
|
|
40
|
+
- name: write_file
|
|
41
|
+
description: Write text to a file on disk
|
|
42
|
+
parameters:
|
|
43
|
+
path: string
|
|
44
|
+
content: string
|
|
45
|
+
returns: '{"ok": true}'
|
|
46
|
+
- name: run_command
|
|
47
|
+
description: Run a shell command and return its output
|
|
48
|
+
parameters:
|
|
49
|
+
command: string
|
|
50
|
+
returns: 'order-support 0 case(s) skills/order-support'
|
|
51
|
+
trajectory:
|
|
52
|
+
order: [read_file, write_file]
|
|
53
|
+
max_calls: 8
|
|
54
|
+
judge:
|
|
55
|
+
expected: >-
|
|
56
|
+
An eval suite covering both sides of the 30-day policy, with a
|
|
57
|
+
mode: offered pair including the negative control, and no placeholders
|
|
58
|
+
left in it.
|
|
59
|
+
rubric:
|
|
60
|
+
- The suite it wrote includes a case where a refund is refused
|
|
61
|
+
- The suite it wrote includes a case where a refund is allowed
|
|
62
|
+
# Quoted: a plain YAML scalar cannot contain ": ".
|
|
63
|
+
- "The suite it wrote includes a mode: offered case expecting skill_triggered true"
|
|
64
|
+
- "The suite it wrote includes a mode: offered case expecting skill_triggered false"
|
|
65
|
+
- No case in the suite it wrote still contains the text TODO
|
|
66
|
+
- Every non-triggering case it wrote has at least one assertion, judge or trajectory check
|
|
67
|
+
|
|
68
|
+
- name: names what a weak suite is missing
|
|
69
|
+
task: Review the eval file at ./skills/greeting/evals/greeting.eval.yaml and tell me what is wrong with it
|
|
70
|
+
tags: [auditing, judged]
|
|
71
|
+
tools:
|
|
72
|
+
- name: read_file
|
|
73
|
+
description: Read a file from disk
|
|
74
|
+
parameters:
|
|
75
|
+
path: string
|
|
76
|
+
returns: |
|
|
77
|
+
cases:
|
|
78
|
+
- name: greets someone
|
|
79
|
+
task: greet Ada
|
|
80
|
+
- name: fires on a greeting request
|
|
81
|
+
mode: offered
|
|
82
|
+
task: say hello to Ada
|
|
83
|
+
trajectory:
|
|
84
|
+
skill_triggered: true
|
|
85
|
+
trajectory:
|
|
86
|
+
called: [read_file]
|
|
87
|
+
judge:
|
|
88
|
+
expected: >-
|
|
89
|
+
A review naming the assertion-free first case and the missing negative
|
|
90
|
+
control for the triggering case.
|
|
91
|
+
rubric:
|
|
92
|
+
- The review says the first case checks nothing because it has no assertions
|
|
93
|
+
- The review says the triggering case has no negative control
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Auditing an existing suite
|
|
2
|
+
|
|
3
|
+
Work the checklist, then report findings with the file and case named. Do not rewrite the
|
|
4
|
+
user's suite unasked.
|
|
5
|
+
|
|
6
|
+
## Checklist
|
|
7
|
+
|
|
8
|
+
- [ ] **Vacuous cases.** Any `loaded` case with no `assertions`, no `trajectory`, no
|
|
9
|
+
`judge`? It passes without checking anything.
|
|
10
|
+
- [ ] **Missing negative control.** Any `mode: offered` positive with no negative
|
|
11
|
+
counterpart? The suite cannot distinguish a well-targeted skill from one that fires
|
|
12
|
+
on everything.
|
|
13
|
+
- [ ] **Claims with no case.** List the skill's "always/never/must" statements and find
|
|
14
|
+
the case for each. Name the ones with none.
|
|
15
|
+
- [ ] **One-sided edges.** A policy case that only proves the refusal, never the approval.
|
|
16
|
+
- [ ] **Over-tight assertions.** `equals` or a `regex` pinning phrasing the skill never
|
|
17
|
+
promised; an assertion that would fail on a legitimately different good answer.
|
|
18
|
+
- [ ] **Fixture assertions.** An assertion whose value comes from a mock tool's `returns`
|
|
19
|
+
rather than from the skill's behavior.
|
|
20
|
+
- [ ] **Unevidenceable rubric entries.** Anything you could not prove by quoting the
|
|
21
|
+
output ("is helpful", "is well structured"), or compound entries hiding which half
|
|
22
|
+
failed.
|
|
23
|
+
- [ ] **Budgets that never bind, or bind too tightly.** A ceiling far above any plausible
|
|
24
|
+
run checks nothing; one at the current spend turns every prompt change red.
|
|
25
|
+
- [ ] **A cost limit as the only budget check** on a model with no pricing entry — the
|
|
26
|
+
check is skipped, so the case fails for having verified nothing.
|
|
27
|
+
- [ ] **Leftover placeholders.** `TODO(skill-lens)` anywhere.
|
|
28
|
+
- [ ] **Tags.** Is there a `smoke` subset a fast CI job could run?
|
|
29
|
+
- [ ] **Cases that error under the configured runner or judge.** An `errored` case never
|
|
30
|
+
ran to a verdict; it says nothing about the skill. Check whether the harness is
|
|
31
|
+
simply unconfigured (default fake runner, default fake judge) or the case declares
|
|
32
|
+
something this runner cannot report (`mode: offered` under a runner with no
|
|
33
|
+
triggering support, for instance).
|
|
34
|
+
|
|
35
|
+
## Reporting
|
|
36
|
+
|
|
37
|
+
For each finding: the file and case, what is wrong, and the smallest change that fixes it.
|
|
38
|
+
Rank by what would let a broken skill through, not by what is easiest to fix.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Designing the cases
|
|
2
|
+
|
|
3
|
+
## Deriving cases from the skill
|
|
4
|
+
|
|
5
|
+
Read the instructions and list every claim. Claims look like: "always call X first",
|
|
6
|
+
"never do Y after 30 days", "answer in one short sentence", "ask before writing files".
|
|
7
|
+
Each claim gets at least one case; the interesting ones get two, one either side of the
|
|
8
|
+
line.
|
|
9
|
+
|
|
10
|
+
The frontmatter `description` is the claim that triggering tests: it is the only text an
|
|
11
|
+
agent sees when deciding whether to reach for the skill.
|
|
12
|
+
|
|
13
|
+
## A minimum suite
|
|
14
|
+
|
|
15
|
+
1. **The common case** — the prompt this skill exists for, with an assertion on something
|
|
16
|
+
every good answer contains.
|
|
17
|
+
2. **The edge** — the prompt that reaches the policy line, with `trajectory` if getting
|
|
18
|
+
there requires a tool call.
|
|
19
|
+
3. **The other side of the edge** — the near-identical prompt where the answer flips. A
|
|
20
|
+
skill that refuses everything passes case 2 alone.
|
|
21
|
+
4. **The triggering pair** — `mode: offered`, positive and negative.
|
|
22
|
+
|
|
23
|
+
## Patterns by skill archetype
|
|
24
|
+
|
|
25
|
+
| Archetype | What to check |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Policy skill (refunds, approvals) | `trajectory` proving it looked before deciding; `forbidden` on the destructive tool; both sides of the policy line |
|
|
28
|
+
| Tool-using skill | `order` for a required sequence; `max_calls` against loops; a case where the tool returns an error string |
|
|
29
|
+
| Formatting skill | `regex` — but only on structure the skill actually promised; a judge for "reads plainly" |
|
|
30
|
+
| Knowledge skill | `contains` on the fact; `not_contains` on the plausible wrong answer; a judge for reasoning |
|
|
31
|
+
|
|
32
|
+
## Writing rubric entries
|
|
33
|
+
|
|
34
|
+
Each entry is checked independently and must be evidenced by quoting the output. That
|
|
35
|
+
makes the test for a good entry mechanical: **could you point at the sentence that proves
|
|
36
|
+
it?**
|
|
37
|
+
|
|
38
|
+
- Good: "The reply names order 1234." "The reply states the return window has closed."
|
|
39
|
+
- Bad: "The reply is helpful." "The response is well structured." Nothing can be quoted
|
|
40
|
+
as proof, and an unsupported pass is an LLM judge's characteristic failure mode.
|
|
41
|
+
|
|
42
|
+
Split compound entries. "Names the order and explains the policy" hides which half failed.
|
|
43
|
+
|
|
44
|
+
## Assertions that age badly
|
|
45
|
+
|
|
46
|
+
- A regex pinning phrasing the skill never promised. Check the structure the skill
|
|
47
|
+
committed to, nothing more.
|
|
48
|
+
- `equals` on anything a model generates freely.
|
|
49
|
+
- Asserting on a number the mock tool returns — that tests the fixture, not the skill.
|
|
50
|
+
- A budget set at the current spend. Leave headroom, or every prompt improvement is a
|
|
51
|
+
red case.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Eval file syntax
|
|
2
|
+
|
|
3
|
+
A file has one top-level `cases:` list. Unknown keys inside a case or an assertion are
|
|
4
|
+
rejected — without that, a typo like `assertion:` would yield a case that passes
|
|
5
|
+
vacuously.
|
|
6
|
+
|
|
7
|
+
## Case fields
|
|
8
|
+
|
|
9
|
+
| Field | Required | Meaning |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `name` | yes | Case name, shown in reports |
|
|
12
|
+
| `task` | yes | The prompt handed to the runner |
|
|
13
|
+
| `assertions` | no | Output checks; a case with none passes vacuously |
|
|
14
|
+
| `tools` | no | Mock tools the agent may call |
|
|
15
|
+
| `trajectory` | no | Which tools must and must not have been called, and in what order |
|
|
16
|
+
| `budget` | no | Ceilings on tokens, cost, and latency |
|
|
17
|
+
| `judge` | no | A rubric for an LLM judge |
|
|
18
|
+
| `mode` | no | `loaded` (default) or `offered` |
|
|
19
|
+
| `tags` | no | Labels for `--tag` filtering |
|
|
20
|
+
|
|
21
|
+
## Assertion kinds
|
|
22
|
+
|
|
23
|
+
| `kind` | Passes when |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `contains` | `value` appears in the output |
|
|
26
|
+
| `not_contains` | `value` does not appear in the output |
|
|
27
|
+
| `regex` | `value` matches anywhere in the output (`re.search`) |
|
|
28
|
+
| `equals` | the stripped output equals `value` exactly |
|
|
29
|
+
|
|
30
|
+
Every assertion must hold for the case to pass. An unknown kind or a malformed regex
|
|
31
|
+
aborts the run as an authoring error rather than being reported as a skill failure.
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
Nothing executes. Calling a mock tool records the call and returns `returns` verbatim, so
|
|
36
|
+
the trajectory is genuinely the model's choice and a run has no side effects. Mock tools
|
|
37
|
+
accept any arguments — a hallucinated argument must not surface as an infra error.
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
tools:
|
|
41
|
+
- name: lookup_order # must be a valid identifier
|
|
42
|
+
description: Look up an order by its id
|
|
43
|
+
parameters:
|
|
44
|
+
order_id: string # string | integer | number | boolean
|
|
45
|
+
returns: '{"id": "1234", "status": "delivered"}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Trajectory
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
trajectory:
|
|
52
|
+
called: [lookup_order] # must have been called
|
|
53
|
+
forbidden: [issue_refund] # must not have been
|
|
54
|
+
order: [lookup_order, issue_refund] # relative order, not exhaustive
|
|
55
|
+
max_calls: 3
|
|
56
|
+
skill_triggered: true # mode: offered only
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every name in `called`, `forbidden` and `order` must be a tool the case itself declares.
|
|
60
|
+
|
|
61
|
+
## Budget
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
budget:
|
|
65
|
+
max_tokens: 2000
|
|
66
|
+
max_cost_usd: 0.01
|
|
67
|
+
max_latency_ms: 20000
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
An unpriced model makes a cost limit unverifiable, so that check is skipped rather than
|
|
71
|
+
counted as passed — a cost limit as the only budget check then fails the case, because
|
|
72
|
+
nothing was verified.
|
|
73
|
+
|
|
74
|
+
## Judge
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
judge:
|
|
78
|
+
expected: A short, plain-language refusal that names the order id.
|
|
79
|
+
rubric:
|
|
80
|
+
- The reply names order 1234
|
|
81
|
+
- The reply explains that the return window has closed
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
One verdict per rubric entry, each with its evidence; skill-lens derives pass and score
|
|
85
|
+
from those. A check that passes without evidence is recorded as a failure. An empty
|
|
86
|
+
rubric, or a blank entry, is an authoring error. Judging costs money and is opted into
|
|
87
|
+
with `judge = "pydantic-ai"` in `skill-lens.toml`; the default `judge = "fake"` reports a
|
|
88
|
+
judged case as **errored** rather than passing a rubric nobody checked.
|
|
89
|
+
|
|
90
|
+
## Triggering (`mode: offered`)
|
|
91
|
+
|
|
92
|
+
The skill is not force-loaded; it is registered as a tool named after the skill
|
|
93
|
+
(`order-support` becomes `order_support`) and described by its frontmatter description.
|
|
94
|
+
Calling it delivers the instructions. Check it with `skill_triggered`, not by naming the
|
|
95
|
+
tool in `called:`. Setting `skill_triggered` on a `loaded` case is an authoring error.
|
|
96
|
+
|
|
97
|
+
## Placeholders
|
|
98
|
+
|
|
99
|
+
`skill-lens init` writes `TODO(skill-lens)` into every field you must supply. A case still
|
|
100
|
+
containing one aborts the run with exit 2, naming the field.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copilot instructions for skill-lens
|
|
2
|
+
|
|
3
|
+
`skill-lens` is a standalone CLI and library that runs evaluations on Agent Skills
|
|
4
|
+
(`SKILL.md` files). Skills under test and their eval cases are **inputs** — nothing about a
|
|
5
|
+
skill under test is vendored here. The tool runs as a CI gate where the exit code is the
|
|
6
|
+
contract. Full design: [`ARCHITECTURE.md`](../ARCHITECTURE.md).
|
|
7
|
+
|
|
8
|
+
## The two seams
|
|
9
|
+
|
|
10
|
+
- `Runner.run(skill, case) -> RunResult` — where every agent framework plugs in.
|
|
11
|
+
- `Evaluator.evaluate(case, result) -> EvalScore` — where every scoring strategy plugs in.
|
|
12
|
+
|
|
13
|
+
`models.py` holds every Pydantic model; no other module defines a data shape.
|
|
14
|
+
|
|
15
|
+
## Invariants — flag any change that breaks one
|
|
16
|
+
|
|
17
|
+
These are decided behaviors with tests asserting them, not accidents.
|
|
18
|
+
|
|
19
|
+
1. **`errored` is not `failed`.** `failed` = the case ran and scored below the bar (an eval
|
|
20
|
+
signal). `errored` = the runner itself blew up (an infra signal). Runners must **never
|
|
21
|
+
raise** for provider failures — they set `RunResult.error`. Errored cases fail the gate.
|
|
22
|
+
2. **A run executing zero cases fails the gate.** "Nothing ran" is a broken run.
|
|
23
|
+
3. **Authoring errors abort the run; they never score as failures.** An unknown assertion
|
|
24
|
+
kind, a bad regex, or an unknown YAML key is a mistake in the user's files and says
|
|
25
|
+
nothing about the skill. They propagate out of the orchestrator and `cli.py` exits 2.
|
|
26
|
+
4. **Exit codes are the CI contract:** gate passed `0`, gate failed `1`, user or authoring
|
|
27
|
+
error `2`. A JSON-write failure escalates to 2 only when the gate already passed.
|
|
28
|
+
5. **`extra="forbid"`** on every user-authored model. Without it a typo like `assertion:`
|
|
29
|
+
yields a case that passes vacuously.
|
|
30
|
+
6. **All file IO pins `encoding="utf-8"`** and re-raises as a typed parse error naming the
|
|
31
|
+
file and field.
|
|
32
|
+
7. **YAML goes through `skill_lens.yaml_loading.safe_load`**, never `yaml.safe_load`.
|
|
33
|
+
8. **Secrets come from environment variables only** — never from `skill-lens.toml`.
|
|
34
|
+
9. **No agent-framework type appears outside `runners/pydantic_ai.py`.**
|
|
35
|
+
10. **Cost lookup degrades, never raises.** An unpriced model yields `cost_usd = 0.0` and a
|
|
36
|
+
`cost_note`. An unpriceable budget check is *skipped*, not passed.
|
|
37
|
+
11. **`skill_lens` (underscore) never appears in user-facing output.** The user-facing name
|
|
38
|
+
is `skill-lens` everywhere.
|
|
39
|
+
|
|
40
|
+
## Do not suggest
|
|
41
|
+
|
|
42
|
+
- Importing an agent framework anywhere outside `runners/pydantic_ai.py`.
|
|
43
|
+
- Replacing `yaml_loading.safe_load` with `yaml.safe_load`.
|
|
44
|
+
- Raising from a runner when a provider call fails.
|
|
45
|
+
- Removing `extra="forbid"` to make a model more permissive.
|
|
46
|
+
- Making a mock tool reject unexpected arguments — a hallucinated argument is an eval
|
|
47
|
+
signal, and raising would surface it as an infra error.
|
|
48
|
+
- Treating a check that could not be performed as passed.
|
|
49
|
+
|
|
50
|
+
## Conventions
|
|
51
|
+
|
|
52
|
+
- **Test-driven.** The failing test comes first. The default test tier must stay zero-cost,
|
|
53
|
+
offline and deterministic — `pytest` runs with `--block-network`.
|
|
54
|
+
- **Conventional Commits are enforced, not stylistic.** `cz bump` derives the version and
|
|
55
|
+
changelog from history. PRs are squash-merged, so **the PR title becomes the commit on
|
|
56
|
+
`main`** and must be conventional too. Flag a non-conforming PR title in review.
|
|
57
|
+
- **Documentation ships with the change.** A change to a flag, config key, `EvalCase` field
|
|
58
|
+
or assertion kind must update the matching page under `docs/`; a change to a protocol or
|
|
59
|
+
an invariant must update `ARCHITECTURE.md`.
|
|
60
|
+
- Line length 100, `ruff` for lint and format.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "src/skill_lens/cli.py,src/skill_lens/gating.py,src/skill_lens/orchestrator.py,src/skill_lens/reporters/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reviewing the CLI, gate, orchestrator and reporters
|
|
6
|
+
|
|
7
|
+
This is the contract surface. CI depends on the exit code; humans depend on the output.
|
|
8
|
+
|
|
9
|
+
- **Exit codes are the contract:** gate passed `0`, gate failed `1`, user or authoring error
|
|
10
|
+
`2`. A JSON-write failure escalates to 2 **only when the gate itself passed** — it must
|
|
11
|
+
never mask an already-failing gate. Flag any change that widens or reorders these.
|
|
12
|
+
- **A run executing zero cases fails the gate.** "Nothing ran" is a broken run, not a pass —
|
|
13
|
+
otherwise a mistyped path reports success forever. The reason must name the cause: no
|
|
14
|
+
skills found, all skills skipped for having no cases, or every case filtered out by
|
|
15
|
+
`--tag`.
|
|
16
|
+
- **Errored cases fail the gate by default**, so CI never goes green on a run that did not
|
|
17
|
+
happen.
|
|
18
|
+
- **Authoring errors abort the run.** `orchestrator.run_evals` deliberately lets them
|
|
19
|
+
propagate; `cli.py` catches them via `_AUTHORING_ERRORS` and prints the message without a
|
|
20
|
+
traceback. Flag any `try`/`except` in the orchestrator that would swallow one into a
|
|
21
|
+
failing score.
|
|
22
|
+
- **`skill_lens` (underscore) never appears in user-facing output.** The name is
|
|
23
|
+
`skill-lens`: command, config file, distribution, prose.
|
|
24
|
+
- A new runner must be registered in `cli._RUNNERS`, and if it spends money it needs the
|
|
25
|
+
preflight key check before construction so a missing key costs nothing and exits 2.
|
|
26
|
+
- Gate reasons are read by whoever is staring at a red pipeline. Each should say what failed
|
|
27
|
+
and against which threshold.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "docs/**,*.md,mkdocs.yml"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reviewing documentation
|
|
6
|
+
|
|
7
|
+
- **`README.md` is a landing page.** Reference prose lives in `docs/`. Flag any PR that
|
|
8
|
+
reintroduces command, config or eval-file reference material into the README — it will
|
|
9
|
+
drift.
|
|
10
|
+
- **`ARCHITECTURE.md` has exactly one copy**, at the repository root.
|
|
11
|
+
`docs/architecture.md` includes it with a `pymdownx.snippets` directive. Flag any
|
|
12
|
+
duplication of its content.
|
|
13
|
+
- **`ARCHITECTURE.md` must use absolute links only (full URLs), never relative links.**
|
|
14
|
+
`tests/test_docs.py` resolves its relative links from the repo root, but mkdocs resolves
|
|
15
|
+
them from `docs/` because the file is inlined into `docs/architecture.md` by a snippet —
|
|
16
|
+
a relative link can pass the test and still fail `mkdocs build --strict`.
|
|
17
|
+
- **A new page must be added to `nav:` in `mkdocs.yml`.** `tests/test_docs.py` fails on an
|
|
18
|
+
orphan page, and `mkdocs build --strict` fails on a nav entry with no file.
|
|
19
|
+
- **`docs/superpowers/` is a historical archive** of specs and plans. It is excluded from
|
|
20
|
+
the built site and does not count as documenting a change. Its contents were superseded by
|
|
21
|
+
what shipped — read `src/` as the source of truth.
|
|
22
|
+
- Documentation ships **with** the change: a new flag updates `docs/cli.md`, a new config
|
|
23
|
+
key updates `docs/configuration.md`, a new assertion kind updates `docs/eval-files.md`, a
|
|
24
|
+
new invariant updates `ARCHITECTURE.md`.
|
|
25
|
+
- Do not add `pymdownx.emoji` or mermaid `custom_fences` to `mkdocs.yml`: both need
|
|
26
|
+
`!!python/name:` YAML tags, which break the plain-YAML nav parsing in `tests/test_docs.py`.
|
|
27
|
+
- Relative links must resolve — there is a test for it.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "src/skill_lens/evaluators/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reviewing evaluator code
|
|
6
|
+
|
|
7
|
+
An evaluator turns a `RunResult` into a pass/fail verdict with a score and human-readable
|
|
8
|
+
detail. Its *score* is about the skill; a low score is never used to report a harness
|
|
9
|
+
problem, and a harness problem is never reported as a low score.
|
|
10
|
+
|
|
11
|
+
- **A check that could not be performed is not a pass.** `BudgetEvaluator` *skips* an
|
|
12
|
+
unpriceable cost limit rather than passing it, so a case whose only budget check is an
|
|
13
|
+
unpriced cost limit fails — nothing was verified. `JudgeEvaluator` follows the same rule:
|
|
14
|
+
an unjudged rubric, and a check that passes without citing evidence, are not passes. Flag
|
|
15
|
+
any "if we can't check it, assume it's fine" branch.
|
|
16
|
+
- **`errored` is for infra, and it must set `passed=False`.** An evaluator sets
|
|
17
|
+
`EvalScore.errored` when the harness broke — a judge endpoint returning 500, a verdict
|
|
18
|
+
that does not match its rubric, an offered case on a runner that cannot report triggering
|
|
19
|
+
— never when the skill scored badly. The orchestrator turns any errored score into an
|
|
20
|
+
errored case. A model validator makes `errored=True, passed=True` unconstructable.
|
|
21
|
+
- **No agent framework in this directory.** `JudgeEvaluator` reaches a model only through
|
|
22
|
+
the injected `Judge` protocol. Flag any direct client or framework import.
|
|
23
|
+
- **Eval-side spend goes on `EvalScore.cost_usd`, never on `RunResult`.** `budget:` measures
|
|
24
|
+
the skill's efficiency; what we spent grading it is the harness's cost and is reported
|
|
25
|
+
separately.
|
|
26
|
+
- **Authoring errors propagate; they never become a failing score.** An unknown assertion
|
|
27
|
+
kind, a malformed regex, or a tool name in a `trajectory` block that the case never
|
|
28
|
+
declared is a mistake in the user's files. Scoring it as a failure would be a lie about
|
|
29
|
+
the skill. These raise and `cli.py` exits 2.
|
|
30
|
+
- **New assertion kinds go in `_CHECKS`** in `assertion.py` and must be documented in
|
|
31
|
+
`docs/eval-files.md`. `tests/test_docs.py` fails until both happen.
|
|
32
|
+
- `detail` is read by a human staring at a red CI run. It should name what failed and with
|
|
33
|
+
what value, not just that something did.
|
|
34
|
+
- Every assertion in a case must hold for the case to pass. A case with no assertions passes.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "src/skill_lens/models.py,src/skill_lens/config.py,src/skill_lens/yaml_loading.py,src/skill_lens/cases/**,src/skill_lens/skills/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reviewing models and loaders
|
|
6
|
+
|
|
7
|
+
This is where user-authored files become typed objects. Every mistake here is silent.
|
|
8
|
+
|
|
9
|
+
- **`extra="forbid"` on every user-authored model** — `EvalCase`, `AssertionSpec`,
|
|
10
|
+
`ToolSpec`, `TrajectorySpec`, `BudgetSpec`, `Config`. Without it a typo like `assertion:`
|
|
11
|
+
yields a case that passes vacuously, the worst failure mode an eval tool has. Flag any
|
|
12
|
+
removal or any new user-authored model that omits it.
|
|
13
|
+
- **`models.py` holds every data shape.** Other modules import from it; they do not define
|
|
14
|
+
their own.
|
|
15
|
+
- **All file IO pins `encoding="utf-8"`** and re-raises `OSError`/`UnicodeDecodeError` as a
|
|
16
|
+
typed parse error (`SkillParseError`, `CaseParseError`, `ConfigError`) naming the file and
|
|
17
|
+
the field. A raw traceback reaching the user is a bug.
|
|
18
|
+
- **YAML goes through `yaml_loading.safe_load`.** PyYAML's `SafeLoader` is YAML 1.1 and
|
|
19
|
+
turns bare `yes`/`no`/`on`/`off` into booleans; an assertion `value: yes` is meant as the
|
|
20
|
+
string.
|
|
21
|
+
- **Secrets never come from `skill-lens.toml`.** A config file is committed; a key must not
|
|
22
|
+
be. Flag any new config field that would hold a credential.
|
|
23
|
+
- **Derived values are properties, not stored fields** — `RunResult.tokens`, `errored`, and
|
|
24
|
+
the `RunReport` aggregates. A stored copy can disagree with its source.
|
|
25
|
+
- Skills with no eval files are reported as **skipped**, visibly — never silently dropped.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "src/skill_lens/runners/**,src/skill_lens/judges/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reviewing runner and judge code
|
|
6
|
+
|
|
7
|
+
These two directories are the framework boundary. Everything the core sees is a plain
|
|
8
|
+
`RunResult` or `JudgeVerdict`. The two share a contract, so the rules below apply to both
|
|
9
|
+
unless one is named.
|
|
10
|
+
|
|
11
|
+
- **Never raise for a provider failure.** Timeouts, rate limits, auth errors and malformed
|
|
12
|
+
responses become `RunResult(error=...)` or `JudgeVerdict(error=...)`. Raising turns an
|
|
13
|
+
infra problem into an unhandled crash and loses the errored/failed distinction the gate
|
|
14
|
+
depends on.
|
|
15
|
+
- **The whole capture path belongs inside the guard.** Reading messages, summing usage,
|
|
16
|
+
pricing and constructing the result must all sit inside the `try`, not just the model
|
|
17
|
+
call — a serialisation failure on a real provider's message shape must report errored,
|
|
18
|
+
not crash the run. This was a real bug once.
|
|
19
|
+
- **Only `runners/pydantic_ai.py` and `judges/pydantic_ai.py` may import an agent
|
|
20
|
+
framework.** `runners/tools.py` builds framework-neutral `MockTool`s — a name, a JSON
|
|
21
|
+
schema and a callable — and the adapters wrap them.
|
|
22
|
+
`tests/test_framework_isolation.py` enforces the boundary across the whole package.
|
|
23
|
+
- **A judge reports per-check verdicts only** — never an overall verdict or a blended
|
|
24
|
+
score. skill-lens derives `passed` and `score` from the per-check results, because an
|
|
25
|
+
unsupported PASS hidden inside a single number is invisible. `JudgeOutput` is deliberately
|
|
26
|
+
narrower than `JudgeVerdict` so the model cannot assert its own cost or outcome.
|
|
27
|
+
- **`FakeJudge` unscripted returns an error, not a pass.** That is what makes
|
|
28
|
+
`judge = "fake"` safe as the built-in default: a rubric with no configured judge is
|
|
29
|
+
errored, never a quiet green.
|
|
30
|
+
- **`judges/prompt.py` is pure and deterministic.** Same input, byte-identical output, every
|
|
31
|
+
process — cassettes match on the request body, so any instability makes them unmatchable.
|
|
32
|
+
No `hash()`, no randomness, no time.
|
|
33
|
+
- **Mock tools accept any arguments.** A model hallucinating an argument is an eval signal
|
|
34
|
+
about the skill; rejecting it would surface as an infra error instead.
|
|
35
|
+
- **Nothing executes in a mock tool.** It records the call and returns its canned value, so
|
|
36
|
+
the trajectory is genuinely the model's choice and a run has no side effects.
|
|
37
|
+
- **Pricing never fails a run.** An unpriced model yields `cost_usd = 0.0` plus a
|
|
38
|
+
`cost_note`. Flag any code path where a pricing lookup can raise.
|
|
39
|
+
- **`RunResult.tokens` is derived** from the input/output split. Flag any attempt to set it.
|
|
40
|
+
- **`FakeRunner.run` returns `model_copy(deep=True)`** so a caller cannot corrupt scripted
|
|
41
|
+
state.
|
|
42
|
+
- **Cassettes are replay-only and secret-free.** Recording is a deliberate, key-bearing act.
|
|
43
|
+
A missing cassette skips; a mismatched request fails rather than reaching the network.
|
|
44
|
+
Flag any credential or account-identifying header that could reach a recorded file.
|
|
45
|
+
- A runner that spends money sets `needs_api_key = True` so the preflight check runs before
|
|
46
|
+
any request.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "tests/**"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reviewing tests
|
|
6
|
+
|
|
7
|
+
- **Test-driven:** the failing test comes first. A PR adding behavior with no test that
|
|
8
|
+
would have failed before it is incomplete.
|
|
9
|
+
- **The default tier is zero-cost, offline and deterministic.** `pytest` runs with
|
|
10
|
+
`--block-network` and deselects `integration`. Flag any default-tier test that could reach
|
|
11
|
+
the network, need a key, or depend on wall-clock time or ordering.
|
|
12
|
+
- **Marker tiers:** unmarked = offline pipeline (`FakeRunner`); `cassette` = replays
|
|
13
|
+
recorded provider traffic, free, selected by default; `integration` = real API spend,
|
|
14
|
+
opt-in only.
|
|
15
|
+
- **`conftest.py` chdirs every test into a fresh `tmp_path`** so config upward-discovery
|
|
16
|
+
cannot pick up an ambient `skill-lens.toml`. A test that reads repository files must
|
|
17
|
+
anchor on `Path(__file__).resolve().parents[1]`, never `Path.cwd()`. Flag any test relying
|
|
18
|
+
on the working directory.
|
|
19
|
+
- **Cassettes must be secret-free.** Both request and response headers are scrubbed, on two
|
|
20
|
+
different vcrpy hooks. Flag anything that could write a credential or an account
|
|
21
|
+
identifier to disk.
|
|
22
|
+
- A test asserting an error path should assert the message a user actually sees, not just
|
|
23
|
+
the exception type.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## What and why
|
|
2
|
+
|
|
3
|
+
<!-- What changes, and what problem it solves. The diff shows what; explain why. -->
|
|
4
|
+
|
|
5
|
+
## Checklist
|
|
6
|
+
|
|
7
|
+
- [ ] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) —
|
|
8
|
+
it becomes the commit on `main` when this is squash-merged.
|
|
9
|
+
- [ ] Documentation is updated in the same PR: `docs/` for user-facing behavior,
|
|
10
|
+
`ARCHITECTURE.md` for design or invariants, `mkdocs.yml` for a new page.
|
|
11
|
+
(If genuinely not needed, add the `no-docs-needed` label.)
|
|
12
|
+
- [ ] `uv run pytest` passes.
|
|
13
|
+
- [ ] `uv run ruff check .` and `uv run ruff format --check .` pass.
|
|
14
|
+
- [ ] `uv run mkdocs build --strict` passes if any documentation changed.
|