Three layers test a skill: smoke, eval and A/B, each answering one question.
The skill (producer) declares its own smoke and eval; the runners (consumers) read the declarations and name no skill. Chain position: Skill map.
| Aspect | Smoke | Eval | A/B |
|---|---|---|---|
| Question | Runs end to end under its own constraints? | How good is one configuration's output on a real task? | What did one change do? |
| Declared in | smoke block in <leaf>.cue | eval/definition.json in the skill dir | variant descriptor at invocation |
| Entry point | make skill-smoke SKILL=<leaf> TOKEN=<label> | /skillroutine-eval <skill> token: <label> | /skillroutine-ab <skill> variant: <descriptor> |
| Result | verdict and coverage in the artifacts cache | evals/<skill>/<date>-<hash6>/ | evals/<skill>/ab-<date>-<hash6>/ |
/<skill> --help prints under Tests the smoke command with its required token label and whether an eval is declared. It reads the deployed help-test.md, rendered only with the session-helpers component (the skillhelp hook's).
Smoke: make skill-smoke SKILL=<leaf> TOKEN=<label>
runs/<label>/result.json is every layer's run record; A/B selects records by its fields.
| Field | Holds |
|---|---|
| session, model, cost, end state, check exit | Every run. |
| layer | smoke or eval |
| skill, skillVersion | The skill and the deployed version the run loaded. |
| mode | nightly or interactive |
| smoke | Smoke runs: verdict, cause, coverage. |
| scores | Eval runs with a scorer. |
The session's heartbeat rows sit beside it in runtime.json.
The workflow layer has no behaviour test: make workflow-gen-check (an audit unit) proves block freshness and the shape rules of every committed stage file. Only smoke and eval runs exercise a script's logic.
Checks the skill runs end to end under its own constraints, on a dummy input, in minutes. Never a quality judgement.
The smoke block in <leaf>.cue: a skip reason or a run. Every skill declares it; make skills-check fails without. The runner loads the model and resolves the defaults in memory; no file is rendered.
| Field | Meaning | Default |
|---|---|---|
| skip | Why the skill has no smoke run. | |
| invoke | Text after /<leaf>. {token} becomes the run's TOKEN label, so no label is checked in. | |
| mode | nightly or interactive (Modes). | nightly |
| fixture | tree and patch under the skill's smoke/ dir. | |
| expect | Anchors, or a check command. | |
| env | Repo-scoped values the run sets, by env key: every required repo-scoped variable of the skill, model-checked. | |
| limits | Budget and timeout. | 3 USD, 15 minutes |
| model | Run model. | sonnet |
| effort | Run effort. | low |
smoke/ is never deployed.
The consumer, skilleval, reads only the declaration and names no skill. It runs on the eval's machinery: provision, headless arm, heartbeat capture, cleanup.
| Command | Runs |
|---|---|
| make skill-smoke SKILL=<leaf> TOKEN=<label> | skilleval smoke |
| make skill-smoke-verdict SKILL=<leaf> SESSION=<id> | skilleval smoke-verdict |
A smoke runs only the deployed skill, so the runner first checks it came from the checkout's source. Deploy writes source <sha256> into ~/.claude/skills/<leaf>/.smine-deployed: the hash of every skill-dir file but eval/ and smoke/, projection blocks stripped. The runner hashes the checkout's skill dir alike (skills.IsDeployedCurrent); a different or missing stamp refuses with exit 3.
Change, deploy, smoke. The check hashes sources only, reading no install or repo values, so a smoke runs from any checkout.
A nightly smoke never sees the machine's repo-scoped values. Before the session starts, the runner:
SKILL_* variable but the arm's SKILL_CONTEXT_INDEX;env values as SKILL_<SKILL>_<KEY>;SMINE_SKILLENV_OFF: the skillenv hook exports nothing, neither the install record's defaults nor a registry row.| Value | In the smoke |
|---|---|
| An optional variable the block leaves out | Unset; its items drop as anywhere. |
| Install-scoped values | The deployed skill's, filled at deploy. |
| An interactive smoke | Runs in the person's own session, reading its values. |
context/context.json | Regenerated in each arm worktree at provisioning, as in session worktrees. |
| Mode | Runs |
|---|---|
| nightly (default) | One headless claude -p "/<leaf> <invoke>" in a throwaway worktree at the checkout's HEAD: declared model and effort, limits.usd budget, limits.minutes timeout, billed to the stored token. Fixture patch as the overlay, fixture tree copied onto the root, never over an existing file. Cleanup removes the arm worktree and every isolation worktree of a workflow the session launched (.claude/worktrees/<run id>-<n>), with branches. |
| interactive | The runner prints the prompt; a person runs it in a session; smoke-verdict judges that session from heartbeat's rows. |
A nightly session never touches the live proposal and backlog stores; the plans archive is fenced (no read, no write, whatever grant the skill holds). The store root is store/ in the throwaway worktree (SMINE_STORE_ROOT), as make rules ARGS="store-root" and store-check answer there.
Mechanical; a failure names its first cause.
| Verdict | When | Passes when |
|---|---|---|
| Terminal | expect.check, or no expectation | Wall time under limits.minutes, cost under limits.usd; a session exists, exit status 0, terminal reason completed, result success, no error flag; the check, run in the worktree after the session, exits 0. |
| Heartbeat | expect.anchors | Terminal rules; the session's run row of the skill exists, not killed, no failed check, every expected anchor; coverage minimums. A missing row's cause tells heartbeat not answering from no enforced run of the skill opening. |
| Interactive | smoke-verdict | No stream, so no limits or session rules: the run row exists and has ended, then the heartbeat row rules and coverage minimums. |
Heartbeat smokes only; zero for a terminal-verdict smoke. Two ratios, each over its total in the deployed enforce.json: declared anchors hit, declared checks passed at least once. expect.minAnchors (hits over all declared anchors) and expect.minChecks are minimum ratios; unset means 0.
Each run writes one record per skill and deployed version to ~/.cache/claude-routine/artifacts/smoke/<leaf>/<version>/, never under ~/.claude/smine, never committed; the previous same-version record stays beside it as <version>.<n>. A run executes in smoke/.staging/ and moves into place after cleanup.
| File | Holds |
|---|---|
| run.json | The manifest, with the token label. |
| runs/A/result.json | The run record. |
| runs/A/runtime.json | Heartbeat's rows. |
| stream, trace, diff | Nightly runs only; an interactive verdict writes just the three files above. |
| smoke.coverage field | Counts |
|---|---|
| anchorsDeclared | Anchors in the deployed enforce.json. |
| anchorsExpected | expect.anchors ids the run was judged against. |
| anchorsExpectedHit, anchorsHit | Expected anchors hit; declared anchors hit. |
| checksDeclared, checksPassed | Declared checks; checks passed at least once. |
The printed and badged summary; the second form is for a record without an expectation:
anchors <expected hit>/<expected> (<declared>), checks <passed>/<declared> anchors <hit> (<declared>), …
The config server shows the newest record on the skill card and skill head, dim and marked stale when its version is not the skill's. The Tests tab shows it as a card: expected anchors hit or missed, checks, cost, session, record dir, run line.
| Path | Holds |
|---|---|
| smoke/tree/ | Files the runner copies onto the worktree root. |
| smoke/*.patch | One patch of new-file changes, applied uncommitted. Go code goes only here: a .go file under smoke/tree/ would join the module's build. It never edits a tracked file, so it applies at any head. |
| smoke/tree/store/ | Seeded store of a skill working on the stores (proposals/<kind>.json, backlog/backlog.json); without it the store root is empty. |
smoke <leaf> <version>: pass smoke <leaf> <version>: fail — <cause> coverage: anchors <expected hit>/<expected> (<declared>), checks <passed>/<declared>
The coverage line follows for a heartbeat smoke. A skip prints its reason.
| Exit | Means |
|---|---|
| 0 | Pass, skip, or interactive instructions. |
| 1 | Fail or harness error. |
| 2 | Usage. |
| 3 | The deployed copy differs. |
Measures how good one skill configuration's output is on a real task, with time, tokens and cost.
eval/definition.json in the skill dir, schema skills/skillroutine/skillroutine-eval/reference/definition.schema.json (4.0, gate ACDSL-JSON-005).
| Part | Declares |
|---|---|
| Invocation | Free text, the skill's args as data, the args a run may set:. |
| Environment | Registry repo, range or checkout commit, overlay. |
| Limits | Budgets and prerequisites. |
| Configuration | model (default opus), effort (default high), replicas (default 1). |
| Check and reference | Optional check argv and reference commit. |
| Scoring | The scoring contract, below. |
/skillroutine-eval <skill> [replicas: <n>] token: <label> [golden: produce|test] [set: <name>=<value>]
go run ./cmd/skilleval init composes the prompt and freezes the run from the definition: prerequisites, the golden kind's file, token label, runs × armUsd against the window.
Provisions, launches the replicas, spawns the adherence judge (plus golden judges on a test run).
Cleans up, folds, reports. The skill interprets nothing in the definition.
Exactly one scoring contract per skill.
| Contract | Keys | Result |
|---|---|---|
| Golden | findings, goldens, scoring | The golden path, unchanged: candidates, golden judge, review score and minutes. |
| Scorer | scorer.command | An argv run in each run's worktree after the check; stdout is one JSON object {"metrics": {"<name>": <number>}}, names [a-z][a-z0-9_]*. Values land in result.json scores and in eval.json as score_<name>. A failing scorer leaves scores absent, its cause in runs/<label>/scorer.log. |
Every run also gets generic measures: mechanical verdict (smoke_pass), check_pass, wall and active time, output tokens, cost, permission denials, heartbeat check failures, adherence-judge step misses. Wall is the arm stream's own span summed per process, excluding a resumed run's session-limit pause; active time is peek's main-loop figure. The eval's notes give the definition's open args at the run prompt's values.
Record dir evals/<skill>/<date>-<hash6>[-<n>]/: the hash is the checkout commit, -<n> a same-day repeat.
| Written | Files |
|---|---|
| Always | run.json and its run.json.sha256 digest, SKILL.md (the deployed skill the runs loaded), overlay.patch, provision.json, eval.json (2.12, validated), report.md, interpretation.md (bullets only, at most 8 of 160 runes, gated) |
| Golden test run | golden.json, scoring.json |
| Produce run | seed.json |
| With a reference commit | reference.patch |
Per run, runs/<label>/ | result.json (the run record: layer: eval, skill, skillVersion, mode: nightly, scores), stream.jsonl, trace.md, diff.patch, check.log, scorer.log, runtime.json, adherence.json, artifacts/ (what the skill left in its artifact dir) |
| Per run, golden path | candidates.json, judge.json |
Cleanup salvages worktrees into the arm's <worktree>-out/ beside the worktree, never into the record, and removes it once every worktree is gone. The prebuild log lives there too.
The config server renders the records at /scripts/skills/{origin}/{name}/tests (?run=<dir> features a dir, ?focus=<run id> one run), top to bottom:
| # | Block | Shows |
|---|---|---|
| 1 | Smoke card | The newest smoke record's version (stale when not the skill's), verdict and cause, a hit-or-missed chip per expected anchor, checks, cost, session, record dir, and make skill-smoke SKILL=<leaf> TOKEN=<label> with the run's token label. |
| 2 | Config line | The eval run's configuration. |
| 3 | Hero | Review score, lost minutes and the minutes bar: the headline run's escaped, false-positive and found minutes from golden.perRun[].severities, oracle and null as ticks. |
| 4 | Breakdown table | Per severity: price, the golden's entries, the run's found, missed and false-positive counts and minutes, oracle, lost; the score spelled out from the totals. |
| 5 | Interpretation, Result, heatmap | Interpretation bullets, Result table, entry heatmap by topic. |
| 6 | Artifacts table | What each file is. |
| 7 | Golden cards | One per definition golden; production contributions are keyed by their dir, leave the history, get their own selector group. |
| 8 | History, definition | Run history, then the test definition. |
token: <label> names an OAuth token stored from the config server in ~/.config/claude-routine/tokens/<label>. Only the label travels; the arm process exports the value as CLAUDE_CODE_OAUTH_TOKEN. Init refuses an unknown or missing label, listing the stored ones. Reports name token <label>.
What one change did: the difference between two record sets of a lower layer.
/skillroutine-ab <skill> [layer: eval|smoke] variant: <descriptor> [replicas: <n>] token: <label>
Names exactly one factor against the definition's baseline. Two changes, none or an unknown factor is refused (ab-init exit 72).
| Descriptor | Layer | How it runs |
|---|---|---|
| model:<name>, effort:<level> | eval | Side by side: both sides in one run dir, equal replicas, shuffled blind labels. |
| no-<mcp server> | eval | Side by side. Void unless the baseline shows the server connected. |
| overlay:<repo-relative patch> | eval | Side by side; the variant applies the definition's overlay plus the patch. |
| version:<a>..<b> | eval, smoke | Runs nothing. Heartbeat and the gate read the one deployed copy, so versions compare sequentially: run at <a>, deploy, run at <b>, then A/B. The smoke layer's only descriptor, as its cache keeps one record per deployed version (exit 73 otherwise). |
Side by side, the blind compare judge ranks the outputs against the definition's reference commit when one exists.
| Comparison | Records |
|---|---|
| Side by side | The A/B dir's own runs, by configuration key. |
| Version, eval layer | Every evals/<skill>/<date>-<hash>/ run with layer: eval, the skill, and skillVersion <a> or <b>, measured by that dir's eval.json. Unfolded dirs are noted and skipped. |
| Version, smoke layer | ~/.cache/claude-routine/artifacts/smoke/<skill>/<version>/runs/A/result.json, measured by its verdict, cost, tokens, denials, wall time and coverage ratios. |
Record dir evals/<skill>/ab-<date>-<hash6>/.
| File | Holds |
|---|---|
| ab.json | Descriptor, layer, both record sets with measures, and per-measure differences: means, standard deviations, n, Δ mean, Welch's t, df, significance at alpha 0.05. |
| report.md, interpretation.md | The report and its interpretation. |
| run.json, eval.json, compare.json, stats.json, runs/ | Added by side-by-side runs. |
Statistics come from go run ./cmd/evalstats -ab <ab.json>; fewer than two records per side gives df 0 and no significance.
An IDE comparison (no-goland) needs a definition whose invocation opts into the server: the descriptor varies the runner, never the invocation.