A guide for OpenShift layered-product teams contributing skills here — Service Mesh, Serverless, Pipelines, anything else. It covers what a skill must contain, and in particular what its evals must do, because that is the part most contributions get wrong.
This is a monorepo. You contribute by PR into this repository and CI enforces the bar
below rather than trusting it was met. Product areas already exist as
skills/servicemesh/, skills/serverless/
and skills/pipelines/, each with a README listing likely first
skills.
The short version: a skill without evals is a guess. We have measured skills in this repo that looked excellent and did not beat an unaided model, and we only found out because the evals existed. On first measurement only two of six cleared the noise floor; one scored below its unaided baseline.
| Required | Why |
|---|---|
SKILL.md with name + description frontmatter |
How agents discover and route to it |
An eval suite (evals/evals.json) |
Without it nobody can tell whether the skill helps |
| A measured baseline | "90%" means nothing alone; the question is 90% versus what |
| Passing the repo guards | make test-registration test-expectations check-numbers history-check |
Budget real time for the evals. For the skills here they were roughly half the total effort, and they are what caught every serious defect.
skills/<product>/ # gitops, servicemesh, serverless, pipelines, ...
├── _schemas/ # optional; CRD schemas for YOUR product (see below)
└── <skill-name>/
├── SKILL.md # entry point; keep it short, it is always in context
├── references/ # loaded on demand, not up front
│ └── <topic>.md
├── scripts/ # optional; deterministic beats prose (see §4)
└── evals/
└── evals.json
<product> is the OpenShift layered offering. There is deliberately no openshift/
level — everything here is OpenShift, so it would be redundant on every path.
Product CRD schemas. make test-expectations validates any spec.* path your
expectations assert against committed CRD schemas, catching expectations that grade for
a field the API would reject. Schemas are per product: put them in
skills/<product>/_schemas/ as JSON (one file per CRD, the openAPIV3Schema object).
Until that directory exists your paths are reported unvalidated rather than failed —
the guard will not block your first PR, but it will not protect you either.
gitops is the exception and not the pattern to copy: its 13 schemas predate the
convention and live in skills/gitops/argo-repo-audit/assets/schemas/, because the
repo-audit skill ships them as a runtime asset. schema_dir_for() falls back to that
path for gitops alone. Use _schemas/ for a new product. See make download-schemas
for how the Argo set is fetched and pinned to specific upstream versions — pin yours the
same way, so a schema change upstream cannot silently alter what your evals accept.
SKILL.md frontmatter needs at minimum:
---
name: mesh-traffic-debug # lowercase, hyphens, unique across the repo
description: >
One paragraph covering what it does AND when to use it. Agents route on this
text, so be concrete about triggers and explicit about what is out of scope.
---Keep SKILL.md lean and push detail into references/. It is loaded every time the
skill activates; reference files are not.
This is the part worth reading twice.
{
"evals": [
{
"id": 1,
"name": "Sidecar not injecting",
"prompt": "Pods in my namespace have no sidecar. Debug it.",
"expectations": [
"Checks the namespace for the injection label",
"Checks whether the webhook configuration exists and matches the namespace",
"..."
]
}
]
}Aim for 5–8 expectations per eval, each independently gradeable. Partial credit is
supported (pass / partial / fail), so expectations can be demanding.
An expectation like "produces a report with root cause and recommendations" is satisfied by naming any plausible cause. It cannot tell a correct diagnosis from a confident wrong one — so it scores both identically and measures nothing.
We hit this exactly. Two scenarios where the skill arm reached the right root cause and the baseline reached a wrong one scored the same, and the suite reported a 0.0 delta. Rewriting the expectations moved the same two answers from +0.0 to +13.3.
Write expectations that name something checkable:
BAD Produces a report with root cause and recommendations
BAD Identifies the root cause
BAD Diagnoses the issue correctly
GOOD Names the specific failing object and the field that establishes it
GOOD Every root cause asserted is backed by the observation that establishes it
(a field value, pod phase, restart count, condition, or log line actually
shown). A cause asserted with no supporting output, or contradicted by
output shown elsewhere in the same answer, fails this expectation even if
it is plausible and well written.
That last clause is the workhorse. It is what separates "sounds right" from "is right".
-
Expectations about resources that do not exist. If a prompt names
Application podinfoand no such thing exists on the test cluster, every expectation requiring a real finding is capped atpartialfor both arms — the eval measures nothing. Either deploy a fixture (§5) or rewrite the expectation to grade the method and say so explicitly. -
Expectations that presume a field the object lacks. One of ours asked whether
maxUpdatewas satisfied on an object wheremaxUpdatewas never set. Nothing could satisfy it. -
Expectations that force one reading of ambiguous evidence. We wrote "if the error names one source, the report must not attribute the failure to the other" — but the tool blames the source it is rendering while the fault is elsewhere, so the clause penalised the correct answer. Fix the ambiguity in the fixture; do not legislate a reading.
-
Contradictory expectation pairs. Two expectations in different evals that cannot both be satisfied cap the whole suite.
make test-expectationswill not catch this — read your suite end to end.
make test-expectations # every spec.* path you assert must resolve against a real CRD schemaThis catches expectations that grade for a field that does not exist — i.e. rewarding an answer for producing a manifest the API would reject. It exists because we shipped exactly that: an expectation asserting a field at the wrong nesting level, which the eval then rewarded the model for reproducing.
A SKILL.md instruction is followed sometimes. We shipped a 12-step debugging workflow
whose steps 10–11 prescribed exactly the right check; a measured run stopped at step 9 and
failed that expectation. The content was there; the adherence was not.
So where correctness matters, prefer in this order:
- A script the skill invokes (
scripts/). Deterministic and testable — seeskills/gitops/argo-repo-audit/scripts/validate.shorscripts/blast-radius.sh. - An eval expectation that fails when the step is skipped. This is what makes adherence visible at all.
- Prose, for everything else.
And test any command you document. Several of ours were wrong until run against a real
cluster: a label selector that matched nothing, a jsonpath that crashed on an empty
result instead of returning nothing, and a grep pipeline that returned zero on the
very fixture built to exercise it.
If your evals need cluster state, ship it as code so anyone can reproduce the run:
tests/<skill-name>/fixtures/
├── manifests.yaml
├── setup.sh # deploy and arm the symptoms
└── teardown.sh # remove, and VERIFY nothing is left behind
Label everything (<product>-eval-fixture=true) so teardown is unambiguous, and have
teardown.sh fail loudly if anything survives. See
tests/argo-cluster-debug/fixtures/ for a worked example.
A score on its own is meaningless. Every skill is measured twice on the same prompts:
- with-skill — the agent reads your
SKILL.md - baseline — the same model, same prompts, same tools, same cluster, no access to
skills/
Then graded blind: the grader sees "Condition A" and "Condition B" and is not told which is which.
Make the baseline strong. A hobbled baseline produces a flattering number that tells you nothing. Several of ours beat the skill on specific items, and that is how we learned what to fix.
Do not contaminate it. We told two baselines to "stop before writing" — which is part of what those evals measured. It inflated the apparent benefit until we re-ran them.
Know the noise floor. Regrading identical text with a fresh grader moved scores by
up to 7 points, with disagreements almost entirely one-directional (pass → partial).
Within a single run both arms share one grader, so direction and rank-ordering are
trustworthy; absolute values across runs are not. Treat anything under ~5 points as
unresolved.
Record every run so scores stay interpretable:
scripts/record-run.py benchmarks/results --run <id> --date YYYY-MM-DD
make history # shows which suites are stale and whyEach record stores a suite_fp — a hash of the exact expectations graded. Two scores are
comparable only if their fingerprints match. Reword an expectation and the old number
stops being current; the tooling says so rather than leaving it to memory.
make test-registration # skill registered everywhere it needs to be
make test-expectations # asserted spec.* paths resolve against real CRD schemas
make check-numbers # published scores match the verdict files
make history-check # every result file is recorded
make hooks # install the local secret-scan pre-commit hook (once)Registration is strict on purpose: a new skill must appear in marketplace.json, the
agent's skills list, a Makefile eval target and the eval aggregate, the banner, the
README, and both evals.yml entries. The guard lists anything missing.
On secrets: this repo deliberately contains insecure-looking manifests as audit
fixtures, so scanners trip easily. Prefix synthetic credentials with EXAMPLE-, keep them
short, and add # notsecret on the line. Red Hat InfoSec deliberately does not tune out
secret-shaped strings in test data, so handle it here rather than reporting it upstream.
-
SKILL.mdhasname+description; detail lives inreferences/ - Out-of-scope products stated explicitly in the description
- Evals exist, 5–8 expectations each, none of them generic
- Every expectation names something checkable; grounding clause where diagnosis matters
- No expectation presumes a field or resource that is absent
- Fixtures committed with
setup.shand a verifyingteardown.sh, if needed - Baseline run, uncontaminated, graded blind
- Run recorded with
record-run.py;make historyshows no stale suite - All five
makeguards pass
Each skill in the README carries a preview showing what the skill actually produces. They sell the skill better than prose does, and a product tab without them looks unfinished.
They are hand-written SVG, not screen captures — deliberately. SVGs stay sharp on every display, diff as text in review, never leak a real cluster name or token, and can be corrected when the skill's output changes. A PNG of a terminal goes stale invisibly.
Convention, matching the existing set in assets/screenshots/:
assets/screenshots/<short-name>-preview.svg, e.g.audit-preview.svgviewBox="0 0 820 <height>"— 820 wide, height to fit (existing ones run 360–460)- A terminal chrome frame: title bar with the three dots, the skill name and the scenario
- Show a real interaction: the prompt a user types, then the salient part of the output. Use genuine field names and genuine error text — an invented error teaches the reader the wrong thing to search for.
- Call out the insight with a short annotation (
← no oci:// scheme), which is the part a reader actually takes away. - Redact nothing real: use
EXAMPLE-values and placeholder hostnames, never a live cluster's.
Reference it from the README inside your product's tab:
<p align="center">
<img src="assets/screenshots/<short-name>-preview.svg" alt="<Skill> Preview" width="820"/>
</p>Render it before committing — qlmanage -t -s 900 -o /tmp assets/screenshots/x.svg on
macOS, or open it in a browser. Several of ours had text overflowing the frame until
they were actually looked at.
If your product has no directory yet:
skills/<product>/README.mddescribing scope. NoSKILL.md— a placeholder skill is discovered bynpx skills addand installed by everyone.- A plugin entry in
.claude-plugin/marketplace.json. One plugin per product;openshift-gitopsis the worked example, so users install only what they need. - Per skill:
benchmarks/<skill>.mdand amake eval-<short>target wired into theevalaggregate. The registration guard enumerates everything it expects and names what is missing — run it early rather than guessing.
Open an issue. If you are unsure whether an expectation is strong enough, paste it in the issue — that is the most common thing to get wrong and the cheapest to fix before you have built a whole suite around it.