skill-scoreboard 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 (68) hide show
  1. skill_scoreboard-0.1.0/.github/workflows/ci.yml +34 -0
  2. skill_scoreboard-0.1.0/.github/workflows/publish.yml +102 -0
  3. skill_scoreboard-0.1.0/.gitignore +13 -0
  4. skill_scoreboard-0.1.0/LICENSE +21 -0
  5. skill_scoreboard-0.1.0/PKG-INFO +361 -0
  6. skill_scoreboard-0.1.0/README.md +334 -0
  7. skill_scoreboard-0.1.0/examples/probes.yaml +35 -0
  8. skill_scoreboard-0.1.0/examples/skills/fastapi-schema/SKILL.md +50 -0
  9. skill_scoreboard-0.1.0/examples/skills/helpful-helper/SKILL.md +11 -0
  10. skill_scoreboard-0.1.0/examples/skills/py-validation/SKILL.md +31 -0
  11. skill_scoreboard-0.1.0/examples/tasks/pydev-fastapi-pydantic-01.yaml +41 -0
  12. skill_scoreboard-0.1.0/examples/tasks/pydev-hello-01.yaml +23 -0
  13. skill_scoreboard-0.1.0/pyproject.toml +79 -0
  14. skill_scoreboard-0.1.0/src/skill_scoreboard/__init__.py +3 -0
  15. skill_scoreboard-0.1.0/src/skill_scoreboard/ab.py +93 -0
  16. skill_scoreboard-0.1.0/src/skill_scoreboard/cli.py +477 -0
  17. skill_scoreboard-0.1.0/src/skill_scoreboard/config.py +55 -0
  18. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/__init__.py +31 -0
  19. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/ast_assertions.py +142 -0
  20. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/base.py +48 -0
  21. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/llm_judge.py +80 -0
  22. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/pydev_static.py +98 -0
  23. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/pydev_tests.py +62 -0
  24. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/skill_activation.py +20 -0
  25. skill_scoreboard-0.1.0/src/skill_scoreboard/evaluators/trajectory_efficiency.py +54 -0
  26. skill_scoreboard-0.1.0/src/skill_scoreboard/llm.py +110 -0
  27. skill_scoreboard-0.1.0/src/skill_scoreboard/metrics/__init__.py +1 -0
  28. skill_scoreboard-0.1.0/src/skill_scoreboard/metrics/loops.py +153 -0
  29. skill_scoreboard-0.1.0/src/skill_scoreboard/metrics/stats.py +29 -0
  30. skill_scoreboard-0.1.0/src/skill_scoreboard/metrics/tokens.py +59 -0
  31. skill_scoreboard-0.1.0/src/skill_scoreboard/models/__init__.py +91 -0
  32. skill_scoreboard-0.1.0/src/skill_scoreboard/models/portfolio.py +143 -0
  33. skill_scoreboard-0.1.0/src/skill_scoreboard/models/results.py +98 -0
  34. skill_scoreboard-0.1.0/src/skill_scoreboard/models/task.py +187 -0
  35. skill_scoreboard-0.1.0/src/skill_scoreboard/models/trajectory.py +105 -0
  36. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/__init__.py +31 -0
  37. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/critic.py +76 -0
  38. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/lint.py +138 -0
  39. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/mine.py +84 -0
  40. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/scorecard.py +264 -0
  41. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/skills.py +80 -0
  42. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/tfidf.py +114 -0
  43. skill_scoreboard-0.1.0/src/skill_scoreboard/portfolio/triggers.py +251 -0
  44. skill_scoreboard-0.1.0/src/skill_scoreboard/report.py +177 -0
  45. skill_scoreboard-0.1.0/src/skill_scoreboard/runner/__init__.py +8 -0
  46. skill_scoreboard-0.1.0/src/skill_scoreboard/runner/base.py +28 -0
  47. skill_scoreboard-0.1.0/src/skill_scoreboard/runner/claude_cli.py +117 -0
  48. skill_scoreboard-0.1.0/src/skill_scoreboard/runner/ndjson.py +210 -0
  49. skill_scoreboard-0.1.0/src/skill_scoreboard/runner/replay.py +50 -0
  50. skill_scoreboard-0.1.0/src/skill_scoreboard/runner/workspace.py +132 -0
  51. skill_scoreboard-0.1.0/src/skill_scoreboard/runs.py +69 -0
  52. skill_scoreboard-0.1.0/src/skill_scoreboard/trial.py +112 -0
  53. skill_scoreboard-0.1.0/tests/conftest.py +123 -0
  54. skill_scoreboard-0.1.0/tests/fixtures/make_fixtures.py +331 -0
  55. skill_scoreboard-0.1.0/tests/fixtures/ndjson/error_max_turns.ndjson +5 -0
  56. skill_scoreboard-0.1.0/tests/fixtures/ndjson/loopy_no_skill.ndjson +15 -0
  57. skill_scoreboard-0.1.0/tests/fixtures/ndjson/success_with_skill.ndjson +9 -0
  58. skill_scoreboard-0.1.0/tests/fixtures/sessions/mined_session.jsonl +5 -0
  59. skill_scoreboard-0.1.0/tests/test_ab.py +162 -0
  60. skill_scoreboard-0.1.0/tests/test_cli.py +253 -0
  61. skill_scoreboard-0.1.0/tests/test_evaluators.py +208 -0
  62. skill_scoreboard-0.1.0/tests/test_live.py +70 -0
  63. skill_scoreboard-0.1.0/tests/test_metrics.py +141 -0
  64. skill_scoreboard-0.1.0/tests/test_ndjson.py +83 -0
  65. skill_scoreboard-0.1.0/tests/test_portfolio.py +350 -0
  66. skill_scoreboard-0.1.0/tests/test_runners.py +107 -0
  67. skill_scoreboard-0.1.0/tests/test_task_schema.py +127 -0
  68. skill_scoreboard-0.1.0/uv.lock +794 -0
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ check:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v5
16
+
17
+ # A uv-managed venv, not --system: the runner's system interpreter is
18
+ # externally managed (PEP 668) and refuses the install outright.
19
+ - name: Install project
20
+ run: |
21
+ uv python install 3.11
22
+ uv venv --python 3.11
23
+ uv pip install -e ".[dev]"
24
+
25
+ - name: Lint
26
+ run: uv run ruff check src tests
27
+
28
+ - name: Type check
29
+ run: uv run mypy src
30
+
31
+ # Fully offline: replay fixtures stand in for the CLI, and live-marked
32
+ # tests are deselected by default. No API key or network required.
33
+ - name: Test
34
+ run: uv run pytest -q
@@ -0,0 +1,102 @@
1
+ name: Publish
2
+
3
+ # Publishing is a one-way door: a version number can never be reused on PyPI,
4
+ # even after deletion. So every artifact is linted, type-checked, tested, and
5
+ # metadata-validated before upload, and a tag whose version disagrees with
6
+ # pyproject.toml fails the build instead of shipping the wrong number.
7
+ #
8
+ # Auth is PyPI Trusted Publishing (OIDC) — no API token is stored in this
9
+ # repository. Configure the publisher once per index at
10
+ # https://pypi.org/manage/account/publishing/ with:
11
+ # owner/repository: 700799/skill-scoreboard
12
+ # workflow name: publish.yml
13
+ # environment: pypi (and testpypi on test.pypi.org)
14
+
15
+ on:
16
+ push:
17
+ tags: ["v*"]
18
+ workflow_dispatch:
19
+ inputs:
20
+ target:
21
+ description: "Index to publish to"
22
+ type: choice
23
+ options: [testpypi, pypi]
24
+ default: testpypi
25
+
26
+ jobs:
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+
32
+ - name: Install uv
33
+ uses: astral-sh/setup-uv@v5
34
+
35
+ - name: Install project
36
+ run: |
37
+ uv python install 3.11
38
+ uv venv --python 3.11
39
+ uv pip install -e ".[dev]"
40
+
41
+ - name: Verify tag matches project version
42
+ if: startsWith(github.ref, 'refs/tags/v')
43
+ run: |
44
+ project=$(uv run python -c \
45
+ "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
46
+ tag="${GITHUB_REF_NAME#v}"
47
+ echo "pyproject: $project / tag: $tag"
48
+ if [ "$project" != "$tag" ]; then
49
+ echo "::error::tag $GITHUB_REF_NAME does not match pyproject version $project"
50
+ exit 1
51
+ fi
52
+
53
+ - name: Lint
54
+ run: uv run ruff check src tests
55
+
56
+ - name: Type check
57
+ run: uv run mypy src
58
+
59
+ - name: Test
60
+ run: uv run pytest -q
61
+
62
+ - name: Build
63
+ run: uv build
64
+
65
+ - name: Validate metadata
66
+ run: uvx twine check dist/*
67
+
68
+ - uses: actions/upload-artifact@v4
69
+ with:
70
+ name: dist
71
+ path: dist/
72
+ if-no-files-found: error
73
+
74
+ publish-testpypi:
75
+ needs: build
76
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
77
+ runs-on: ubuntu-latest
78
+ environment: testpypi
79
+ permissions:
80
+ id-token: write
81
+ steps:
82
+ - uses: actions/download-artifact@v4
83
+ with:
84
+ name: dist
85
+ path: dist
86
+ - uses: pypa/gh-action-pypi-publish@release/v1
87
+ with:
88
+ repository-url: https://test.pypi.org/legacy/
89
+
90
+ publish-pypi:
91
+ needs: build
92
+ if: github.event_name == 'push' || inputs.target == 'pypi'
93
+ runs-on: ubuntu-latest
94
+ environment: pypi
95
+ permissions:
96
+ id-token: write
97
+ steps:
98
+ - uses: actions/download-artifact@v4
99
+ with:
100
+ name: dist
101
+ path: dist
102
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+
12
+ # Framework outputs
13
+ runs/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 skill-scoreboard contributors
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,361 @@
1
+ Metadata-Version: 2.5
2
+ Name: skill-scoreboard
3
+ Version: 0.1.0
4
+ Summary: Benchmark Claude Code skills against a no-skill baseline, detect token waste and skill bypass, and rank a whole skill library KEEP/FIX/MERGE/RETIRE.
5
+ Project-URL: Repository, https://github.com/700799/skill-scoreboard
6
+ Author: skill-scoreboard contributors
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agent,benchmark,claude,claude-code,evaluation,llm,skills
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: anthropic>=0.40
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: pyyaml>=6.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: mypy>=1.10; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.5; extra == 'dev'
25
+ Requires-Dist: types-pyyaml; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # skill-scoreboard
29
+
30
+ **Measure whether your Claude Code skills actually earn their keep — then rank your whole library.**
31
+
32
+ A skill is just instructions in a Markdown file. That makes it easy to write and
33
+ impossible to evaluate by reading: a capable model often completes the task with
34
+ or without it. So "the tests passed" proves nothing about the skill, and a library
35
+ grows until nobody can say which entries help, which overlap, and which are quietly
36
+ costing tokens on every activation.
37
+
38
+ `skill-scoreboard` answers that with measurement instead of opinion. Every task runs
39
+ twice — **control** with skills disabled, **treatment** with the skill injected — and
40
+ the difference is the skill's value:
41
+
42
+ | metric | control | treatment | delta |
43
+ |---|---|---|---|
44
+ | pass rate | 0% | 100% | +100pp |
45
+ | mean total tokens | 65,400 | 33,575 | -48.7% |
46
+ | duplicate tool calls | 3.0 | 0.0 | -3.0 |
47
+ | file re-reads | 2.0 | 0.0 | -2.0 |
48
+ | skill activation | 0% | 100% | +100pp |
49
+
50
+ Point it at a whole directory of skills and it returns a ranked scoreboard with a
51
+ decision and a stated reason for each one:
52
+
53
+ | rank | skill | score | confidence | recommendation | reasons |
54
+ |---|---|---|---|---|---|
55
+ | 1 | `fastapi-schema` | 1.00 | 55% | **KEEP** | clears every tier that has run |
56
+ | 2 | `py-validation` | 1.00 | 15% | **MERGE → fastapi-schema** | overlaps fastapi-schema (similarity 0.55); fastapi-schema scores higher |
57
+ | 3 | `helpful-helper` | 0.43 | 15% | **FIX** | blocking lint findings: dead-reference; composite 0.43 is mid-range |
58
+
59
+ Both tables above are real output from the shipped examples, not mockups.
60
+
61
+ ### What makes it more than a test runner
62
+
63
+ - **It catches the bypass.** A skill whose tasks pass while its *activation rate*
64
+ stays low isn't working — the model is solving the problem from prior knowledge.
65
+ That pattern gets flagged `FIX` instead of counted as success.
66
+ - **It measures waste, not just correctness.** Duplicate tool calls, re-reading
67
+ large files without editing them, and runaway per-turn context growth are
68
+ detected from the agent's trajectory.
69
+ - **It checks adherence structurally.** Python AST assertions verify the skill's
70
+ prescribed constructs are present and its forbidden ones absent — so "used
71
+ Pydantic v2 `@field_validator`, never the v1 `@validator`" is a test, not a hope.
72
+ - **It never fakes confidence.** Every score carries the share of evidence behind
73
+ it. A skill with only a free static lint cannot outrank one measured end to end,
74
+ and is never retired on thin evidence.
75
+ - **It runs offline.** Recorded sessions replay deterministically, so the whole
76
+ suite — including the real `ruff`, `mypy`, and `pytest` evaluators — runs in CI
77
+ with no API key.
78
+
79
+ ### Two ways to use it
80
+
81
+ | | Command | Cost |
82
+ |---|---|---|
83
+ | **One skill, deeply** | `skill-scoreboard ab task.yaml` | Real agent runs |
84
+ | **A whole library** | `skill-scoreboard audit ./skills` → `rank` | Free static tier first, paid tiers only where they earn it |
85
+
86
+ ---
87
+
88
+ ## Install
89
+
90
+ ```bash
91
+ pip install skill-scoreboard # installs the `skill-scoreboard` command
92
+ ```
93
+
94
+ From a checkout, for development:
95
+
96
+ ```bash
97
+ uv venv && uv pip install -e ".[dev]"
98
+ ```
99
+
100
+ Requires Python 3.11+. Live runs need the [Claude Code CLI](https://code.claude.com/docs)
101
+ on `PATH`; the LLM judge and critic need API credentials. **Everything else runs
102
+ offline** — pass `--no-llm` and `--runner replay` to work with no network at all.
103
+
104
+ ## Quickstart
105
+
106
+ ```bash
107
+ skill-scoreboard validate examples/tasks/*.yaml examples/probes.yaml # schema-check
108
+ skill-scoreboard --no-llm audit examples/skills --json runs/audit.json # Tier 0: lint + overlap
109
+ skill-scoreboard mine ~/.claude/projects --skills-dir examples/skills --json runs/mine.json
110
+ skill-scoreboard --no-llm ab examples/tasks/pydev-fastapi-pydantic-01.yaml \
111
+ --runner replay --trials 2 \
112
+ --control-fixture tests/fixtures/ndjson/loopy_no_skill.ndjson \
113
+ --treatment-fixture tests/fixtures/ndjson/success_with_skill.ndjson
114
+ skill-scoreboard rank examples/skills --audit runs/audit.json --runs runs \
115
+ --mine runs/mine.json --md runs/leaderboard.md
116
+ ```
117
+
118
+ Drop `--runner replay` and its fixtures to run against the real CLI.
119
+
120
+ ---
121
+
122
+ ## The four measurements
123
+
124
+ ### 1. Task success & code quality
125
+
126
+ `pydev_static` runs **ruff** and **mypy** over the final workspace; `pydev_tests`
127
+ runs **pytest**; `ast_assertions` parses the resulting code with Python's own
128
+ **AST** module. Missing tools degrade to a recorded failure rather than a crash.
129
+
130
+ ### 2. Token waste & loop detection
131
+
132
+ Three anti-patterns, computed from the trajectory:
133
+
134
+ | Detector | What counts | Why it is not naive |
135
+ |---|---|---|
136
+ | Duplicate tool calls | Same tool + byte-identical input | Canonical JSON hashing; exempt list for legitimately repeated tools (`TodoWrite`) |
137
+ | Large-file re-reads | Re-reading a >100-line file | An `Edit`/`Write` to that file **resets** the window — re-reading your own change is legitimate |
138
+ | Context growth | Tokens added per turn (default limit 15k) | Exact when the stream carries per-message usage; falls back to a chars/4 estimate and **flags that it did** |
139
+
140
+ ### 3. Skill adherence vs. generic bypass
141
+
142
+ The framework's sharpest signal. Three independent checks:
143
+
144
+ 1. **`skill_activation`** — did the `Skill` tool actually fire for the target skill?
145
+ 2. **`ast_assertions`** — are the skill's prescribed constructs present and its
146
+ forbidden ones absent (e.g. Pydantic v2 `@field_validator` required,
147
+ v1 `@validator` banned)?
148
+ 3. **`llm_judge`** — a rubric graded by Claude for what static analysis can't see.
149
+
150
+ A skill whose tasks pass while activation stays low is flagged **FIX**: the model
151
+ is solving it from prior knowledge and the skill is along for the ride.
152
+
153
+ ### 4. A/B baseline differential
154
+
155
+ Every task runs in two arms:
156
+
157
+ | Arm | How | Meaning |
158
+ |---|---|---|
159
+ | **Control** | `claude -p --disable-slash-commands` | What the model does unaided |
160
+ | **Treatment** | Skill injected at `.claude/skills/<name>/SKILL.md` | What the skill adds |
161
+
162
+ Reports give `treatment − control` deltas for pass rate, tokens, cost, duration,
163
+ and each waste metric. Negative token/cost deltas mean the skill *saves*.
164
+
165
+ > The control arm uses `--disable-slash-commands`, **not** `--bare`. `--bare`
166
+ > only skips hooks, LSP, and plugin credentials — its help notes that "Skills
167
+ > still resolve via `/skill-name`", so a `--bare` baseline would silently load
168
+ > the very skill under test and understate its measured value.
169
+
170
+ ---
171
+
172
+ ## Ranking a whole library
173
+
174
+ Cost scales with how deeply you want to probe, so a large library can be triaged
175
+ before spending real money:
176
+
177
+ | Tier | Command | Cost | Produces |
178
+ |---|---|---|---|
179
+ | **0 · Static** | `skill-scoreboard audit <dir>` | free | Lint findings + TF-IDF overlap clusters |
180
+ | **1 · Triggers** | `skill-scoreboard audit <dir> --probes probes.yaml` | cheap | Precision/recall/F1 + cross-activation confusion matrix |
181
+ | **2 · A/B** | `skill-scoreboard ab <task.yaml>` | real runs | Measured value lift |
182
+ | **Critic** | `skill-scoreboard audit <dir> --critic` | one call/skill | Actionable rewrite suggestions |
183
+ | **Mining** | `skill-scoreboard mine <transcripts>` | free | Real-world activation frequency and cost |
184
+
185
+ **Tier 0** catches the problems you can find for free: descriptions too vague to
186
+ route on, skills whose own body costs thousands of tokens on every activation,
187
+ dead file references, and near-duplicate skills that should be merged.
188
+
189
+ **Tier 1** asks whether a skill fires when it should — and stays quiet when it
190
+ shouldn't. Each skill's positive probes double as negatives for every *other*
191
+ skill, which fills a confusion matrix and exposes skills that keep firing on
192
+ each other's work (a merge signal independent of text similarity).
193
+
194
+ ### Scoring
195
+
196
+ Composite score is a weighted mean over **whichever tiers have run**, with
197
+ weights renormalized across what's available — so a Tier 0-only library still
198
+ ranks, and the score sharpens as evidence accumulates.
199
+
200
+ ```
201
+ lint 0.15 · trigger 0.25 · ab 0.40 · critic 0.20 (override with --weights)
202
+
203
+ ab = 0.50 × pass-rate lift + 0.25 × token savings + 0.25 × adherence
204
+ ```
205
+
206
+ Recommendations, in precedence order:
207
+
208
+ - **RETIRE** — measured with no pass-rate lift, no token savings, and weak
209
+ triggering; or a poor composite backed by real evidence; or never activated
210
+ in mined sessions while scoring badly.
211
+ - **MERGE** — overlaps a higher-scoring sibling (TF-IDF cluster or
212
+ cross-activation). `merge_with` names the survivor.
213
+ - **FIX** — blocking lint errors, trigger F1 below 0.70, a mid-range composite,
214
+ or the bypass signature above.
215
+ - **KEEP** — clears every tier that has run.
216
+
217
+ **Confidence and ranking.** Renormalizing over available tiers alone would let a
218
+ skill with nothing but clean lint tie — or beat — one measured end to end and
219
+ found good. So each entry also carries a **confidence** (the share of scoring
220
+ weight actually backed by evidence), and *ranking* uses the composite shrunk
221
+ toward a neutral 0.5 in proportion to what's missing:
222
+
223
+ ```
224
+ ranked = composite × confidence + 0.5 × (1 − confidence)
225
+ ```
226
+
227
+ An unmeasured skill therefore cannot outrank a measured one, and the survivor of
228
+ a merge cluster is the best-*evidenced* member rather than the luckiest. The raw
229
+ composite is still what threshold decisions read, so the two roles stay separate.
230
+
231
+ **Guardrail:** a skill is never retired on static lint alone. With Tier 0 only,
232
+ the worst outcome is FIX, reason `insufficient_evidence`, and the leaderboard
233
+ prints `—` for every tier that didn't run. Thin evidence never impersonates a
234
+ measurement.
235
+
236
+ ---
237
+
238
+ ## Task schema
239
+
240
+ ```yaml
241
+ id: pydev-fastapi-pydantic-01
242
+ description: "Verify the fastapi-schema skill refactors raw dicts into Pydantic v2 models."
243
+ target_skill: "../skills/fastapi-schema/SKILL.md"
244
+
245
+ workspace:
246
+ initial_files:
247
+ app.py: |
248
+ from fastapi import FastAPI
249
+ app = FastAPI()
250
+
251
+ prompt: "Refactor app.py to return a Pydantic model."
252
+
253
+ run_limits:
254
+ max_turns: 6 # -> claude --max-turns
255
+ max_budget_usd: 0.15 # -> claude --max-budget-usd
256
+ # timeout_seconds, model, allowed_tools, disallowed_tools also supported
257
+
258
+ evaluations:
259
+ - type: skill_activation
260
+ expected: "fastapi-schema"
261
+ - type: pydev_static
262
+ linters: [ruff, mypy]
263
+ strict: true
264
+ - type: pydev_tests
265
+ - type: ast_assertions
266
+ assertions:
267
+ - {file: app.py, kind: decorator_used, value: field_validator}
268
+ - {file: app.py, kind: forbidden_decorator, value: validator}
269
+ - type: trajectory_efficiency
270
+ max_duplicate_tool_calls: 1
271
+ max_file_re_reads: 2
272
+ - type: llm_judge
273
+ rubric: "Did the code use Pydantic V2 `@field_validator` syntax?"
274
+ ```
275
+
276
+ The schema is strict: unknown fields and unknown evaluation types fail at load
277
+ time, not mid-run. `ast_assertions` kinds: `decorator_used`, `call_used`,
278
+ `import_used`, `class_inherits`, `function_defined`, and the `forbidden_*`
279
+ variants.
280
+
281
+ `probes.yaml` (for Tier 1) is documented in `examples/probes.yaml`.
282
+
283
+ ---
284
+
285
+ ## Architecture
286
+
287
+ Every measurement reads one normalized `Trajectory`, and three sources produce it:
288
+
289
+ ```
290
+ claude -p --output-format stream-json ─┐
291
+ recorded NDJSON fixture ───────────────┼──> runner/ndjson.py ──> Trajectory ──> metrics
292
+ real session transcript (skill-scoreboard mine) ────┘ └──> evaluators
293
+ ```
294
+
295
+ Because replay is a first-class source, the entire pipeline — including the
296
+ ruff/mypy/pytest evaluators — is exercised in CI with no API key and no network.
297
+ `ReplayRunner` re-applies the `Write`/`Edit` calls recorded in a fixture, so an
298
+ offline run rebuilds the workspace the agent actually produced instead of
299
+ grading the starting files.
300
+
301
+ Cost comes from the CLI's reported `total_cost_usd` when present; otherwise it
302
+ is estimated from a pricing table and **marked as estimated** in every report.
303
+
304
+ ## Development
305
+
306
+ ```bash
307
+ uv run ruff check src tests
308
+ uv run mypy src
309
+ uv run pytest # offline; live-marked tests are deselected by default
310
+ uv run pytest -m live # needs an authenticated claude CLI, spends real tokens
311
+ python tests/fixtures/make_fixtures.py # regenerate NDJSON fixtures
312
+ ```
313
+
314
+ ## Relationship to `claude plugin eval`
315
+
316
+ Claude Code ships an early-access, org-gated `claude plugin eval` that also runs
317
+ control/treatment arms with regex, tool-use, and LLM graders. This project is
318
+ standalone and complementary: it adds trajectory-level waste detection, AST
319
+ adherence checks, and portfolio-scale ranking, and it runs anywhere the CLI
320
+ runs. It targets the `2.1.x` stream-json shape (live-verified against CLI
321
+ 2.1.246 and 2.1.296) and parses defensively,
322
+ ignoring unknown event types and fields so format drift degrades rather than breaks.
323
+
324
+ ## Releasing
325
+
326
+ Publishing runs on [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
327
+ via `.github/workflows/publish.yml` — no API token is stored in this repository.
328
+
329
+ One-time setup, per index, at https://pypi.org/manage/account/publishing/
330
+ (and the same on test.pypi.org):
331
+
332
+ | Field | Value |
333
+ |---|---|
334
+ | PyPI project name | `skill-scoreboard` |
335
+ | Owner / repository | `700799` / `skill-scoreboard` |
336
+ | Workflow name | `publish.yml` |
337
+ | Environment | `pypi` (`testpypi` on TestPyPI) |
338
+
339
+ Then:
340
+
341
+ ```bash
342
+ # Dry run to TestPyPI first — Actions tab -> Publish -> Run workflow -> testpypi
343
+ # Real release:
344
+ git tag v0.1.0 && git push origin v0.1.0
345
+ ```
346
+
347
+ The workflow lints, type-checks, tests, builds, and runs `twine check` before
348
+ uploading, and **fails the build if the tag version disagrees with
349
+ `pyproject.toml`**. That guard matters: a version number can never be reused on
350
+ PyPI, even after deletion, so a wrong tag would otherwise burn it permanently.
351
+
352
+ To publish by hand with an API token instead (username is literally `__token__`):
353
+
354
+ ```bash
355
+ uv build && uvx twine check dist/*
356
+ uvx twine upload dist/*
357
+ ```
358
+
359
+ ## License
360
+
361
+ MIT