Evaluate a Plugin: Fixed Suite, PAT Export, Deterministic Metrics
How to measure a plugin's skills run over run: send a fixed prompt suite through a real client, export every conversation with a personal access token, compute deterministic metrics, and compare versions over time in the console and in git.
On this page
TL;DR: An eval sends the same prompts to every skill of a plugin through a real, governed client, then exports each resulting conversation with a personal access token (PAT) and computes metrics from the export. Every metric is a count, a sum or a fixed rule over exported files. No model scores anything, so the same export always gives the same numbers. Change the skills, ship them, run the same suite again, and compare. The console's Versions page shows the same change by marketplace hash.
Why this shape
- Fixed inputs. The prompts live in
scripts/eval/prompts/<plugin>.yaml, and their hash is stamped on every run. Runs with different prompt hashes are flagged as not comparable, so a change in the numbers is a change in the skills. - A real client. The suite runs in Claude Code through the Dynapps bridge, so every turn goes through the gateway, governance, cost accounting and the audit record exactly as a user's would. The eval measures what users get.
- Deterministic scoring. Tokens, cost, turns, tool calls and errors come from the gateway's own record of each conversation. Error classes are fixed patterns over tool results. Quality review is done separately by a person or by the engineer changing the skills, and it is never mixed into the metrics.
- Two histories. Each run's metrics are written locally under
evals/<plugin>/<label>/(the directory is gitignored: runs read real users' data). Each shipped change also produces a new marketplace version in the console, which is the shared history.
The loop
edit skills → ship (new image → marketplace hash moves → bridge syncs)
→ scripts/eval/eval.sh run <label>
→ scripts/eval/eval.sh export <label>
→ scripts/eval/eval.sh metrics <label> --against <previous label>
→ read the deltas; open Versions → Compare in the console
The first run on a plugin is its baseline. Every later run is compared with the baseline and with the run before it.
Run
scripts/eval/eval.sh run r2-2026-09-25 dynapps-salesforce
This runs every skill in the suite four at a time. Each run is a headless Claude Code session on the bridge host, with the model set in the suite file. The prompt is the skill's slash command, the fixed prompt, and the suite's constant suffix (scope to the test records, never write, never ask). The run manifest records:
| Field | Meaning |
|---|---|
plugin_version |
The version in the plugin config at run time. |
prompts_hash |
The hash of the prompt file. Runs are compared only when this matches. |
skills |
A content hash per skill directory, so a skill that did not change can be told apart from one that did. |
runs.<skill>.session_id |
The client session ID. It is the key the export uses. |
runs.<skill>.attempts |
2 when the first attempt started before the connector tools were ready. That is a harness retry, not a skill failure. |
Before the marketplace is deployed
To measure a change the same day, before it ships, install this tree's skills straight into the eval host's plugin cache:
python3 scripts/eval/patch-bridge.py dynapps-salesforce # back up, then install
python3 scripts/eval/patch-bridge.py dynapps-salesforce --restore # put the served version back
The run manifest records the patch under bridge_patch. The gateway still credits these conversations to the marketplace version it serves, so the console's Versions page shows them under the old hash until the deploy. The deterministic metrics are unaffected. The next bridge sync replaces a patch, so check that .eval-patch is still in the cache before a run.
Export with a personal access token
A PAT (sp-live-…) is accepted on GET requests to /admin/export/… only. It resolves to its owner and sees exactly what that person's console session would. Any other method or path returns 401. Issue one under Account → Devices → Personal access tokens, or use the bridge's own token. eval.sh export does the following:
| Request | Returns |
|---|---|
GET /admin/export/transcripts?session_id=<client session id> |
JSON Lines, one ConversationBundle per conversation in that session (schema version 1). |
GET /admin/export/transcripts/{context_id}?format=json|markdown |
One conversation. |
GET /admin/export/transcripts?source=sessions&from=…&to=… |
Every conversation in a window, capped at 500. |
GET /admin/export/analysis-skills?from=…&to=…&format=json |
One row per skill: invocations, people, requests, tokens, cost, tool failures, p95. |
GET /admin/export/analysis-versions?marketplace=<id>&from=…&to=…&format=json |
One row per marketplace version: served window, invocations, requests, cost, p50/p95. |
GET /admin/export/analysis-marketplaces?from=…&to=…&format=json |
One row per marketplace. |
Every dataset also takes format=csv|json|jsonl|markdown, and /preview returns the row count without the body.
curl -H "Authorization: Bearer $PAT" \
"https://<gateway>/admin/export/transcripts?session_id=<id>" -o run.jsonl
A bundle carries facts (the conversation's record: tokens, cost, turns, latency, tool calls, governance decisions), requests (per turn), tool_calls and tool_ledger, skills (each invoked skill with its plugin_id, marketplace_id and marketplace_hash), decisions, messages and transcript. The export also saves the client's local transcript of the same session, because only it holds full tool results, which are needed to classify errors.
The metrics
Every per-run figure is one row of the analysis-plugin-eval dataset: one row per conversation that invoked one of the marketplace's skills, scored by the fixed rules in plugin_eval.sql. The console's Versions → Evaluation tab aggregates exactly these rows, and eval.sh metrics reads the same rows, so the page, the file and a run's report are one set of numbers. If the gateway doesn't serve the dataset yet, the harness applies a line-for-line copy of the same rules to the exported conversation. When both exist it compares them and fails the run on any disagreement.
curl -H "Authorization: Bearer $PAT" \
"https://<gateway>/admin/export/analysis-plugin-eval?marketplace=<id>&days=30&format=json"
Tool calls are read from every turn of the conversation. Each turn repeats the thread before it, so a call is counted once. A call's result is the message after it.
| Column | Rule |
|---|---|
context_id, client_session_id |
The conversation, and the client session that ran it (the harness keys on this). |
skill, plugin_id, marketplace_hash |
The skill invoked and the marketplace version that served it. |
turns, requests, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens |
The conversation's record. |
cost_usd |
Turn spend, excluding side calls (titles, summaries). |
duration_seconds, p50_ms, p95_ms |
Wall time, and median and p95 request latency. |
mcp_calls, connector_calls, builtin_calls |
MCP tool calls; calls to the plugin's own MCP servers; built-in tool calls. |
schema_errors |
Results with INVALID_FIELD, No such column, No such relation, INVALID_TYPE, is not supported, MALFORMED_QUERY or ValidationError. |
access_errors |
INSUFFICIENT_ACCESS, Permission '…' denied or PERMISSION_DENIED. |
upstream_errors |
A 5xx status_code or UNKNOWN_EXCEPTION from the connector's backend. |
bad_arguments |
The tool rejected the call's shape: Invalid JSON payload, Invalid value at '…', or a name that does not match its pattern. |
timeouts |
timed out. |
failed_calls |
Calls whose result is in any class above. |
repeated_calls |
Calls re-issued with identical input: retry loops. |
widgets |
display_widget calls that rendered. |
writes |
Calls to a create, update, delete, upsert, send or insert tool, or with a POST, PATCH, PUT or DELETE method. |
answer_chars |
Length of the final answer. |
placeholder_mentions |
MOCK, placeholder or sample data in the final answer: output built on invented data. |
tools_unavailable |
The final answer says a tool or connector was unavailable: the run never reached its connector. |
completed |
A widget rendered, or the final answer is at least 200 characters. |
success |
Completed, at least one call to the plugin's connector, no schema errors, no writes, and tools were available. |
The harness adds guards of its own on top of the console's figures, reported separately and never shown in the console. For example, unscoped_queries counts record queries without the suite's test-data filter. The suite's success_rate also requires those guards to pass, while console_success_rate is exactly the console's figure.
analysis-plugin-eval-tools returns one row per version, skill and tool: calls, conversations, failed calls by class and repeated calls, by the same rules. It names the tool that is failing, not just the skill.
Levels
| Level | Where |
|---|---|
| Marketplace version | The Evaluation tab's version table; marketplace_hash in both exports. |
| Plugin | The tab's plugin table; plugin_id. |
| Skill | The tab's skill table; skill. |
| Tool | The tab's tool table; analysis-plugin-eval-tools. |
| Conversation | Each row of analysis-plugin-eval; each skill links to its latest run. |
| Day | The tab's day table for the selected version; first_invoked_at. |
Attention list
The top of the tab lists what needs a look, by fixed thresholds:
| Flag | Rule |
|---|---|
| Wrote to a connector | Any write call. |
| Ran without its tools | Any run with tools_unavailable. |
| Succeeds less often | The skill's success rate is below its rate under the baseline version. |
| Costs more per run | The skill's average cost is 25% or more above the baseline. |
| Repeats calls | A run re-issued the same call 3 or more times. |
| Tool fails | A tool with 3 or more calls fails 20% or more of them. |
Every item links to the latest conversation that shows it.
Read the change in the console
-
Analysis → Versions → the plugin's marketplace → Evaluation. Pick a version, and optionally a version to compare with (by default, the previous version that has conversations). The headline shows success, completion, cost, tokens, turns, connector calls and failed calls, each with its change. Below it:
- a table of every version with conversations, oldest first, each compared with the one before it;
- each skill under the selected version, compared with the same skill under the baseline, linked to its latest conversation.
Export on this tab downloads
analysis-plugin-evalfor the same window. -
History and Compare. Usage, spend and latency per version, and what changed between two versions skill by skill.
-
Analysis → Conversations. Open any run by its
context_idfrommetrics.csvto read the transcript and tool ledger.
A run made on skills patched into the client before a deploy is served by the old marketplace version, so the console lists it under that version until the deploy. The harness's report still shows it as its own run.
Repeat
After each change, run the same suite with a new label, export it, and compute the metrics against the baseline and against the previous run. Runs stay on the machine that made them: evals/ is gitignored, because transcripts and per-case results describe real users' work. The console's Evaluation tab is the shared record.