feat(evals): Tier 0 static lint + Tier 1 activation evals for all 18 skills - #89
Conversation
Repo-wide skill lint under evals/lint (pytest, seconds, no LLM) — the
first tier of the skill eval loop (generalizes sl-toolkit's
test_skill_consistency.py as designed):
- frontmatter parses as YAML; skill name is kebab-case, <=64 chars, and
equals its directory; description present and <=1024 chars
- command and agent frontmatter has a description (the dispatcher
surface)
- marketplace.json <-> plugin.json consistency: sources exist, names
match, versions match, every plugin on disk is published
- no dangling skill-internal references (references/, template/,
assets/, examples/ mentions must exist); relative markdown links
resolve; skill scripts are executable and self-locate via BASH_SOURCE
(per CLAUDE.md), with a justified exemption list
Real drift the lint caught, fixed here:
- keboola-git SKILL.md frontmatter was invalid YAML ('Triggers:' colon
inside a plain scalar) -> block scalar
- generate-vcr-tests command frontmatter was invalid YAML (unquoted
argument-hint with two flow sequences) -> quoted
- duckdb-transformation and keboola-config skill names didn't match
their directories ('DuckDB Transformation', 'Keboola Configuration')
- all 11 keboola-cli agents declared whenToUse: instead of description:,
which Claude Code does not read — the agents were invisible to the
dispatcher
- semantic-layer-usage pointed at references/ files that live in the
sibling dataapp-development skill -> qualified paths
Patch-bumped the four affected plugins + marketplace per CLAUDE.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tier 1 of the skill eval loop: measure the routing precision/recall of every skill's description field — the invocation surface a model reads when deciding which skill to load. - evals/activation/run_activation.py: shows the classifier (Claude Haiku, temperature 0) the FULL marketplace skill list plus one labeled utterance and asks which skills it would invoke; grading is deterministic (skill in answer iff should_trigger). Per-skill precision/recall, misroute listing, summary.json, --ci thresholds (>=85% overall, >=60% per skill), --skill filter, --dry-run. - Activation cases for all 18 skills (194 labeled utterances) at plugins/<plugin>/evals/<skill>/trigger-evals.json — generalizing the existing component-developer/get-started precedent. Negatives are hard: drawn from sibling skills' territory, not unrelated topics. - Offline case validation (no API key): every skill must have a case set; sets must parse, belong to a real skill, and have >=6 cases with both labels. - .github/workflows/skill-evals.yml: Tier 0 lint + case validation on every PR; Tier 1 activation gated on the ANTHROPIC_API_KEY secret (skips gracefully on forks). Baseline live run: 95.9% overall accuracy across 194 cases, recall 1.00 on all 18 skills; the 8 misroutes are co-activation false positives on deliberately borderline negatives — the description-tuning backlog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Tier 1 is now live in CI. The
Tier 1 ran all 194 labeled utterances across the 18 skills through the Haiku router in CI: Two things worth noting:
The reviewer note in the PR description about the missing secret is now resolved. 🤖 Generated with Claude Code |
Mined first user messages from real Kai conversations (OTEL trace analysis tables in the Keboola AI project, fct_otel_conversations) and turned the recurring intent patterns into activation cases. Every utterance is an anonymized paraphrase — client names, project IDs, URLs and client-specific table names replaced with generic equivalents; nothing is copied verbatim. What real traffic contributed: - job-error debugging is the dominant real intent (the 'Resolve this job error' deep-link) — new debug-component positives for output- mapping mismatches, OAuth grant failures, python tracebacks, plus the SQL-transformation-error hard negative (the #1 real-world boundary: transformation errors are not Python-component debugging) - transformation/config editing, flow scheduling, conditional flows -> keboola-config; security-finding fixes, broken-flow audits, SQL review -> keboola-cli (with the transformation-review overlap between the two captured as deliberate boundary cases) - data-app creation/preview/perf asks -> dataapp-development; a real 'use the keboola managed repo, I have no github account' -> keboola-git, with 'set up GitHub integration for our flows' as its hard negative - deprecated-component config migration as a hard negative for BOTH migrate-to-uv and develop-component/get-started (config migration is not code migration) - ~a quarter of real traffic is Czech/Slovak — added cs/sk utterances across skills to test routing across languages Two gold corrections surfaced by the live run: converting an existing script / building a brand-new custom component is initial scaffolding (get-started) per develop-component's own boundary, so those are negatives for develop-component. Live run: 239 cases, 94.1% overall accuracy, VERDICT: PASS. The 14 misroutes are the sharpened description-tuning backlog — notably the keboola-cli vs keboola-config transformation-review overlap and 'conditional flow' attracting build-component-ui. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Case set expanded with real-world utterances (88f99cb): mined first user messages from actual Kai conversations (the OTEL trace analysis tables — What real traffic contributed that the synthetic set missed:
The live run also produced two gold corrections (the router was right, my labels were wrong): converting an existing script or building a brand-new custom component is initial scaffolding → get-started, per develop-component's own boundary rule. Result: 239 cases, 94.1% overall accuracy, 🤖 Generated with Claude Code |
vojtabiberle
left a comment
There was a problem hiding this comment.
Four review notes from a local multi-dimensional review (3 minors + 1 nit). None block; see the APPROVE review for the overall verdict.
| f"overall accuracy {summary['overall_accuracy']:.1%} < {args.min_accuracy:.1%}" | ||
| ) | ||
| for s in summary["per_skill"]: | ||
| if s["accuracy"] < args.min_skill_accuracy: |
There was a problem hiding this comment.
[minor] Per-skill CI gate on accuracy alone can pass a zero-recall skill
The --ci per-skill gate only checks accuracy < min_skill_accuracy (default 0.6). But recall is the metric Tier 1 exists to protect — "does the skill activate on the utterances it promises to catch". With the balance floor being MIN_POSITIVE=2 / MIN_NEGATIVE=2 over MIN_CASES=6, a set of 2 positives + 4 negatives that triggers on none of its positives (recall 0.0) still scores 4/6 = 66.7% > 60% and passes. A description that has gone completely dead would sail through the per-skill floor.
summarize() already computes precision/recall, so this is a few lines: add a per-skill recall floor (e.g. --min-skill-recall), or gate on min(precision, recall).
| args = parser.parse_args() | ||
|
|
||
| skills = discover_skills() | ||
| by_dir = {s.dir_name: s for s in skills} |
There was a problem hiding this comment.
[minor] Skill lookup keyed by bare dir-name — cross-plugin collision would mis-map
by_dir here (and SKILLS_BY_DIR in test_activation_cases.py) is keyed on dir_name only. If two plugins ever ship a skill directory with the same name (e.g. both a review/), the dict silently keeps the last one, and a case set for the other plugin's same-named skill either fails the plugin assertion or gets attributed to the wrong plugin in the summary. No collision exists today, so this is latent — but the design assumes a global dir-name uniqueness that nothing enforces.
Skill.qualified (f"{plugin}:{dir_name}") already exists as the natural composite key — suggest keying both maps on (plugin, dir_name) and looking up case sets by the same.
|
|
||
| def parse_answer(text: str) -> list[str]: | ||
| """Extract the JSON array of skill names from the classifier's reply.""" | ||
| match = re.search(r"\[.*?\]", text, re.DOTALL) |
There was a problem hiding this comment.
[minor] parse_answer takes the first bracket pair — stray brackets grade as a silent false-negative
The lazy \[.*?\] matches the first [...] in the reply. If the classifier ever emits prose containing brackets before the JSON (e.g. Based on [the description] I'd pick ["x"]), the regex captures [the description], json.loads fails, and parse_answer returns [] — scored as "invoked nothing", i.e. a false negative in grading rather than a visible error. Risk is low at temperature 0 with the strict "Reply with ONLY a JSON array" prompt, but it fails silently.
Suggest preferring the last bracket match (or stripping a ```json fence), and on JSONDecodeError recording the raw reply in the result row so a parse failure is distinguishable from a genuine empty answer.
| working-directory: evals | ||
| run: uv run --group dev pytest -q | ||
|
|
||
| tier1-activation: |
There was a problem hiding this comment.
[nit] tier1-activation doesn't depend on tier0-lint — a red lint still burns a paid API run
The two jobs run in parallel, so if Tier 0 (the cheap, deterministic gate) fails, Tier 1 still fires ~194 Haiku calls on an already-failing PR. Adding needs: tier0-lint to this job means the paid tier only runs once the free tier is green.
vojtabiberle
left a comment
There was a problem hiding this comment.
RECOMMEND APPROVE WITH NITS — no blockers, no majors.
Adds a self-contained evals/ harness (pytest + uv): Tier 0 static lint over every SKILL.md / command / agent / manifest, and Tier 1 an LLM-classifier activation eval measuring each skill description:'s routing precision/recall against 194 labeled utterances. Ships the repo's first CI workflow and fixes real pre-existing drift (invalid YAML frontmatter, name↔dir mismatches, 11 agents using an unread whenToUse: field). Well-scoped, self-contained, tests green (323 offline + 95.9% live baseline).
Left 3 minors + 1 nit as inline comments — none block. The one worth prioritizing is the per-skill CI gate keying on accuracy alone, which can pass a zero-recall skill; that undercuts the harness's stated purpose but is a small fix (recall is already computed). The fork-secret handling in the workflow is correct (pull_request, not pull_request_target).
Security reviewed inline; connection/platform-impact dimension is N/A (different repo, no cross-service contract touched).
…er hardening, CI job ordering Review fixes (vojtabiberle): - --ci gains a per-skill recall floor (--min-skill-recall, default 0.6). Recall is the metric Tier 1 exists to protect; a dead description could previously pass the accuracy floor on its negatives alone (2 pos + 4 neg with zero recall = 66.7% accuracy > 60%). - Skill lookups are keyed (plugin, dir_name) instead of bare dir_name — in both the runner and the offline case validation — so a future cross-plugin skill-directory name collision can't silently mis-attribute case sets. Skill.qualified was already the natural key. - parse_answer prefers a ```json fence, then tries bracket candidates LAST-first (prose like 'Based on [the description] ... ["x"]' used to grade as a silent false negative), and an unparseable reply now returns the raw text, recorded as parse_error on the result row and flagged [UNPARSEABLE REPLY] in the misroute list. Offline unit tests added (test_parse_answer.py). - tier1-activation now has needs: tier0-lint — the paid classifier run only fires once the free deterministic tier is green. Live run after changes: 239 cases, 94.1%, VERDICT: PASS (all skills above both the accuracy and the new recall floor). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @vojtabiberle — all four addressed in 3d79875, CI green on the new run (330 offline tests + live Tier 1 at 94.1% PASS):
🤖 Generated with Claude Code |
Resolve conflicts from the PR #89 skill-evals work landing on main while this branch consolidated the 6 plugins into a single `keboola` plugin. Conflict resolutions: - .claude-plugin/marketplace.json: kept the consolidated single-plugin manifest (v2.0.0). Main's patch version bumps applied to the 6 now-deleted plugins and no longer have a target. - plugins/{component-developer,dataapp-developer,keboola-cli,keboola-git}/ .claude-plugin/plugin.json: kept the deletions; main's changes were version-only bumps to plugins removed by the consolidation. Ported main's essential drift fixes into the consolidated plugin (these are required by main's new Tier 0 lint and all resolve correctly under plugins/keboola/): - SKILL name fixes: duckdb-transformation, keboola-config - agent frontmatter whenToUse -> description (11 agents) - semantic-layer-usage cross-skill reference path (still a sibling skill) - generate-vcr-tests argument-hint YAML quoting, keboola-git description Adopted main's documented eval layout for the single plugin: relocated all 18 trigger-evals.json to plugins/keboola/evals/<skill>/ (including moving get-started out of its skill dir), matching the harness glob plugins/*/evals/*/. Updated the harness's install.sh self-locate exemption from the old component-developer plugin name to keboola. Tier 0 lint: 311 passed. Activation discovery finds all 18 skills. claude plugin validate: passed.
Why
Skills have had no quality gate: descriptions drift, references go dead, and nobody measures whether a skill actually activates on the utterances it promises to catch. This is the ai-kit half of the four-tier skill eval loop designed with keboola/KaiBench (
docs/skill-evals.md, landing in KaiBench#40): the fast tiers (0–1) live here and run on every PR; the heavy behavior/live tiers run from KaiBench.Tier 0 — static lint (
evals/lint/, pytest, seconds, no LLM)Generalizes sl-toolkit's
test_skill_consistency.pyrepo-wide:nameis kebab-case and equals its directory;descriptionpresent and ≤1024 charsdescription(the dispatcher surface)marketplace.json↔plugin.json: sources exist, names match, versions match, every plugin on disk is publishedreferences/,template/,assets/,examples/); relative markdown links resolve; skill scripts executable + BASH_SOURCE self-location per CLAUDE.md (with a justified exemption list)Real drift the lint caught on first run, fixed in this PR:
keboola-gitSKILL.md frontmatter was invalid YAML (bareTriggers:colon inside a plain scalar)generate-vcr-testscommand frontmatter was invalid YAML (unquotedargument-hintwith two flow sequences)duckdb-transformation/keboola-configskill names didn't match their directories (DuckDB Transformation,Keboola Configuration)whenToUse:instead ofdescription:— a field Claude Code doesn't read, making the agents invisible to the dispatcher (component-developer's agents, which demonstrably load, usedescription:)semantic-layer-usagepointed atreferences/files that live in the siblingdataapp-developmentskillAffected plugins patch-bumped per CLAUDE.md (component-developer 3.3.1, keboola-cli 1.1.1, keboola-git 1.0.1, dataapp-developer 1.5.1; marketplace 1.12.1).
Tier 1 — activation evals (
evals/activation/)Measures the routing precision/recall of every skill's
description:field. The classifier (Claude Haiku, temperature 0) is shown the full marketplace skill list — the surface the real harness routes on — plus one labeled utterance, and asked which skills it would invoke; grading is deterministic (skill in answeriffshould_trigger).plugins/<plugin>/evals/<skill>/trigger-evals.json, generalizing the existing component-developer/get-started precedent. Negatives are hard: drawn from sibling skills' territory (debug vs develop vs get-started vs test…), not unrelated topics.--cigates at ≥85% overall / ≥60% per-skill accuracy;summary.jsonrecords per-skill precision/recall and every misroute with the router's actual pick.Baseline live run: 95.9% overall accuracy, recall 1.00 on all 18 skills. The 8 misroutes are co-activation false positives on deliberately borderline negatives — that list is the description-tuning backlog this loop exists to drive.
CI
.github/workflows/skill-evals.yml— this repo's first workflow. Tier 0 + offline case validation on every PR; Tier 1 runs when theANTHROPIC_API_KEYsecret is available (skips gracefully on forks) and uploadssummary.jsonas an artifact.Verification
cd evals && uv run --group dev pytest -q→ 323 passed (lint + case validation)🤖 Generated with Claude Code