Runs Metrics Compare — Standardized Multi-Run Metrics
What this skill solves
Users often need to place multiple runs of the same benchmark side by side to inspect improvement
or regression: success rate, average score, Odysseys per-rubric results, duration, turns, errors,
and helper usage. These values are distributed across runs/<id>/tasks.jsonl and pi session JSONL,
and several recurring pitfalls can distort the comparison. This skill packages the calculations
and safeguards into a parameterized script that produces the full comparison in one command.
Division of responsibility with pi-session-analyzer: that skill handles forensics (why an
individual task was slow or failed, execution paths, error clustering, and the largest outliers
between two runs); this skill handles statistics (standard metric tables for N runs). Use
this skill for the global view first, then use pi-session-analyzer for specific tasks.
One command
PY=.venv/bin/python
SKILL=.claude/skills/runs-metrics-compare
$PY $SKILL/scripts/runs_compare.py RUN_ID_A RUN_ID_B [RUN_ID_C ...] \
[--labels "a,b,c"] [--exclude rwb-foo-01 ...] [--only aggregate,helpers]
- Use the harness's own
.venv (with the loguru/ego_bench dependencies). Do not use another
project's venv. With the wrong interpreter, the session_parser import fails silently, causing
the entire "Turns / Tools / Cost" section and the tool_failures row to disappear without
terminating the script.
- Run from the harness root (the parent of
runs/), or use --runs-dir /abs/path/to/runs.
- Run order equals column order. Sort runs chronologically from oldest to newest so the trend is
immediately visible.
- Use
--labels for short column names (otherwise the script derives YYYYMMDD timestamps).
- Use
--only to emit selected sections (section names: align aggregate odysseys rubrics duration turns errors pertask helpers crosscheck).
Output sections: Run Metadata & Alignment → Aggregate → Odysseys → Per-Rubric → Duration →
Turns / Tools / Cost → Errors → Per-Task Matrix → Helper Distribution & DOM Share → Cross-Check.
Required conventions (hardened after observed pitfalls)
Data source = recalculate from JSONL; never read the HTML report.
Read verdict/score/num_rubrics[_passed]/category/started_at/ended_at/error/judge_error from
tasks.jsonl; read turns/tool/cost/tool_failures from session JSONL through the project's
ego_bench.session_parser. Use summary.json/run_metadata.json only for metadata
(model/judge_mode/skill_hash/ego_sha). Their aggregates cover all 50 tasks, including excluded
ones, so do not use them directly. The final script section automatically cross-checks the
full JSONL recalculation against summary.json; only an all-✓ result confirms that the pipeline
is aligned.
Align the shared base-task intersection across runs before aggregating.
Task sets may differ across runs (for example, a task may be renamed or change difficulty).
By default, the script takes the intersection of base_task_id across all runs and prints each
excluded task with its reason. Renamed tasks with different IDs are excluded automatically—for
example, rwb-linkedin-apply-01 (old) vs rwb-openai-careers-apply-01 (new) are both removed
during intersection alignment, with no manual --exclude needed.
Manually --exclude tasks whose content changed under the same ID.
If a task ID stays the same but its difficulty or rubric changes, the intersection retains it
even though it is not comparable. Remove it with --exclude <base_id>. Two automatic warnings
help detect this case:
⚠️ rubric dimensions differ across runs → num_rubrics changed, so Odysseys is not
comparable.
⚠️ category labels differ across runs → do not aggregate by category; inspect only the
per-task matrix. Category is an unstable label, and task difficulty bands may be relabeled
during dataset iteration.
judge_error / null verdict often cluster in excluded tasks.
Rejudge looks up rubrics by task_id in the current dataset, so removed or renamed old tasks
produce "no rubrics" errors. These errors usually disappear after alignment exclusions; do not
count them among retained-task errors.
Confirm matching judge_mode before reporting metrics (the metadata section lists it).
Agent vs simple mode and different judge models can shift pass rate. Compare only runs with the
same judge configuration. See [[odysseys-judge-variance-4axes]].
limits.mode and force-rerun determine whether two runs are the same experiment. The
metadata section lists both:
limits.mode: time (wall-clock limit) and steps (step limit) are different constraints;
do not compare them as equivalent.
- Force-rerun task count: reruns overwrite a task's result in place,
attempt_count remains 1,
and tasks.jsonl retains no trace. The discarded result appears only in
run_metadata.reruns. When the count is greater than zero, the script warns. First establish
why the rerun occurred (infrastructure failure ⇒ usually comparable; dissatisfaction with the
result ⇒ inflated success rate and not comparable). In a recent set of measured skillcmp
runs, rerun counts ranged from 0 to 29 and concentrated on the hardest task.
Use the per-rubric table to locate the exact criterion that regressed. A total success-rate
change such as 90%→60% is not actionable. The rubrics section (reading
metadata.rubric_scores from judges/<task_id>_judge.json) can show that expedia R2 fell from
3/5 to 1/5 and careers-apply R2 from 5/5 to 2/5 while all other rubrics remained perfect. This
artifact exists across run generations (verified back to 2026-06-26); legacy runs without
judges/ are skipped explicitly.
Special conventions for helper distribution and DOM share
To answer whether the agent relies mainly on DOM or visual/JS operations, scan helper calls in
every ego-browser command body and assign them to five interaction buckets: DOM perception
(snapshot), DOM reads (innerText/count/inputValue…), DOM actions (click/fill/press…),
visual (screenshot/mouse), and JS evaluation (evaluate/cdp), plus navigation plumbing.
Exclude navigation from the denominator of "DOM share."
Three correctness requirements are critical; missing any of them can reverse the conclusion:
- Recognize both call forms. During 2026-07-17…21, ego-lite fully namespaced the API
(
click(...) → page.locator(...).click(), useOrCreateTaskSpace →
taskSpaces.useOrCreate). The old (?<![.\w])NAME\( scan was completely blind to the new
form: it counted only 1 helper across 686 command bodies, producing "DOM 100% / visual 0% / JS
0%." The current scanner is receiver-aware and merges legacy bare names into the same canonical
set through LEGACY_ALIAS, allowing both generations of runs in one comparison table
(verified: old and new scripts produce byte-for-byte identical output on legacy runs).
- Track locator variables. Calls such as
rows.nth(1).click() after
const rows = page.locator('tr') account for 59% of element actions in new runs. Missing them
cuts the DOM-action count by more than half.
- Exclude agent-defined inline functions. Functions such as
setVal/parseNum/walk/findControl defined by the model in a heredoc are not built-ins and must
not count. Exclude them through both receiver awareness (el.click() inside page.evaluate has
a DOM-node receiver, not a locator) and the BUCKET allowlist.
Inspect two self-checks when reading the report:
- "Unclassified facade calls": parsed methods absent from
BUCKET. A nonempty result means
ego-lite added a capability and the taxonomy needs updating (the first deployment immediately
detected locator.setChecked / locator.elementCenter).
- Warning when more than half of command bodies contain no parsed built-in: the taxonomy is
behind ego-lite. Check §3.1 of ego-lite
skills/ego-browser/SKILL.md before reporting metrics;
do not trust that run's shares.
See references/helper_taxonomy.md for mappings and bucket definitions (§1.7 covers the
namespacing break). When ego-lite changes the API again, update both that file and
LEGACY_ALIAS/BUCKET in the script.
Mandatory reporting rules (shared with pi-session-analyzer)
- Answer the user's question in a three-sentence summary before showing tables.
- Distinguish statistical conclusions (full sample) from individual cases (
n=1, clues only).
- Separate task difficulty (multiple runs rise or fall together) from agent/skill implementation
(one-sided movement).
- Decompose headline gains or regressions by task (which tasks flipped or failed); do not report
only total success rate.
- Verify any risk based on theoretical possibility with evidence (artifacts, logs, or code paths)
before raising it. Label anything unverified explicitly.
Typical workflow
- Confirm the run IDs to compare (
ls runs/) and sort them chronologically.
- Run all script sections. Read Alignment and Cross-Check to confirm the conventions are correct:
exclusions match expectations, all cross-checks are ✓, judge_mode matches, and there are no
rubric/category mismatch warnings—or those warnings have been handled with
--exclude.
- Interpret each section and use the per-task matrix to locate flipped or regressed tasks.
- To investigate why a task failed, switch to
pi-session-analyzer (workflows A/E).
Maintenance
- Keep helper rename mappings synchronized between
references/helper_taxonomy.md and
LEGACY_ALIAS/BUCKET in the script.
- For a new metric field, first determine whether it belongs to
tasks.jsonl,
run_metadata.json, or judges/<task_id>_judge.json, then add it to the corresponding section.
- The script depends on the project's
ego_bench.session_parser (--repo-root defaults to this
harness root).
- After changing any counting convention, compare old and new output on a legacy run
(
git show HEAD:<script> > /tmp/old.py, then run both with the same arguments). Legacy-run
numbers must remain byte-for-byte identical; any change breaks forward compatibility.
1---2name: runs-metrics-compare3description: Compare standardized metrics across multiple ego-benchmark-harness runs by recalculating them from tasks.jsonl and session JSONL, never from HTML reports. Covers overall aggregates (success rate/average score), Odysseys per-rubric pass rates, duration distributions (avg/p50/p90/max), turns/tools/cost, error and exception counts, per-task success matrices, helper distributions, and the share of DOM operations. Use whenever users ask to compare data or metrics across runs, calculate aggregate/Odysseys/duration/turn/error/helper metrics, inspect differences among runs, or compare multiple rwb/real-world-bench runs. Use pi-session-analyzer instead for deep forensic analysis of why an individual task was slow or failed.4---56# Runs Metrics Compare — Standardized Multi-Run Metrics78## What this skill solves910Users often need to place multiple runs of the same benchmark side by side to inspect improvement11or regression: success rate, average score, Odysseys per-rubric results, duration, turns, errors,12and helper usage. These values are distributed across `runs/<id>/tasks.jsonl` and pi session JSONL,13and several recurring pitfalls can distort the comparison. This skill packages the calculations14and safeguards into a parameterized script that produces the full comparison in one command.1516> Division of responsibility with `pi-session-analyzer`: that skill handles **forensics** (why an17> individual task was slow or failed, execution paths, error clustering, and the largest outliers18> between two runs); this skill handles **statistics** (standard metric tables for N runs). Use19> this skill for the global view first, then use pi-session-analyzer for specific tasks.2021## One command2223```bash24PY=.venv/bin/python25SKILL=.claude/skills/runs-metrics-compare26$PY $SKILL/scripts/runs_compare.py RUN_ID_A RUN_ID_B [RUN_ID_C ...] \27 [--labels "a,b,c"] [--exclude rwb-foo-01 ...] [--only aggregate,helpers]28```2930- **Use the harness's own `.venv` (with the `loguru`/`ego_bench` dependencies)**. Do not use another31 project's venv. With the wrong interpreter, the `session_parser` import fails silently, causing32 the entire "Turns / Tools / Cost" section and the tool_failures row to disappear without33 terminating the script.34- Run from the harness root (the parent of `runs/`), or use `--runs-dir /abs/path/to/runs`.35- Run order equals column order. Sort runs chronologically from oldest to newest so the trend is36 immediately visible.37- Use `--labels` for short column names (otherwise the script derives `YYYYMMDD` timestamps).38- Use `--only` to emit selected sections (section names: `align aggregate odysseys rubrics duration turns errors pertask39 helpers crosscheck`).4041Output sections: Run Metadata & Alignment → Aggregate → Odysseys → Per-Rubric → Duration →42Turns / Tools / Cost → Errors → Per-Task Matrix → Helper Distribution & DOM Share → Cross-Check.4344## Required conventions (hardened after observed pitfalls)45461. **Data source = recalculate from JSONL; never read the HTML report.**47 Read `verdict/score/num_rubrics[_passed]/category/started_at/ended_at/error/judge_error` from48 `tasks.jsonl`; read turns/tool/cost/tool_failures from session JSONL through the project's49 `ego_bench.session_parser`. Use `summary.json`/`run_metadata.json` only for metadata50 (model/judge_mode/skill_hash/ego_sha). Their aggregates cover all 50 tasks, including excluded51 ones, so do not use them directly. The final script section automatically cross-checks the52 full JSONL recalculation against `summary.json`; only an all-✓ result confirms that the pipeline53 is aligned.54552. **Align the shared base-task intersection across runs before aggregating.**56 Task sets may differ across runs (for example, a task may be renamed or change difficulty).57 By default, the script takes the intersection of `base_task_id` across all runs and prints each58 excluded task with its reason. Renamed tasks with different IDs are excluded automatically—for59 example, `rwb-linkedin-apply-01` (old) vs `rwb-openai-careers-apply-01` (new) are both removed60 during intersection alignment, with no manual `--exclude` needed.61623. **Manually `--exclude` tasks whose content changed under the same ID.**63 If a task ID stays the same but its difficulty or rubric changes, the intersection retains it64 even though it is not comparable. Remove it with `--exclude <base_id>`. Two automatic warnings65 help detect this case:66 - `⚠️ rubric dimensions differ across runs` → `num_rubrics` changed, so Odysseys is not67 comparable.68 - `⚠️ category labels differ across runs` → do not aggregate by category; inspect only the69 per-task matrix. Category is an unstable label, and task difficulty bands may be relabeled70 during dataset iteration.71724. **judge_error / null verdict often cluster in excluded tasks.**73 Rejudge looks up rubrics by `task_id` in the current dataset, so removed or renamed old tasks74 produce "no rubrics" errors. These errors usually disappear after alignment exclusions; do not75 count them among retained-task errors.76775. **Confirm matching judge_mode before reporting metrics** (the metadata section lists it).78 Agent vs simple mode and different judge models can shift pass rate. Compare only runs with the79 same judge configuration. See [[odysseys-judge-variance-4axes]].80816. **`limits.mode` and `force-rerun` determine whether two runs are the same experiment.** The82 metadata section lists both:83 - `limits.mode`: `time` (wall-clock limit) and `steps` (step limit) are different constraints;84 do not compare them as equivalent.85 - Force-rerun task count: reruns overwrite a task's result in place, `attempt_count` remains 1,86 and `tasks.jsonl` retains no trace. The discarded result appears only in87 `run_metadata.reruns`. When the count is greater than zero, the script warns. First establish88 why the rerun occurred (infrastructure failure ⇒ usually comparable; dissatisfaction with the89 result ⇒ inflated success rate and not comparable). In a recent set of measured skillcmp90 runs, rerun counts ranged from 0 to 29 and concentrated on the hardest task.91927. **Use the per-rubric table to locate the exact criterion that regressed.** A total success-rate93 change such as 90%→60% is not actionable. The `rubrics` section (reading94 `metadata.rubric_scores` from `judges/<task_id>_judge.json`) can show that expedia R2 fell from95 3/5 to 1/5 and careers-apply R2 from 5/5 to 2/5 while all other rubrics remained perfect. This96 artifact exists across run generations (verified back to 2026-06-26); legacy runs without97 `judges/` are skipped explicitly.9899## Special conventions for helper distribution and DOM share100101To answer whether the agent relies mainly on DOM or visual/JS operations, scan helper calls in102every `ego-browser` command body and assign them to five interaction buckets: **DOM perception**103(snapshot), **DOM reads** (innerText/count/inputValue…), **DOM actions** (click/fill/press…),104**visual** (screenshot/mouse), and **JS evaluation** (evaluate/cdp), plus navigation plumbing.105Exclude navigation from the denominator of "DOM share."106107Three correctness requirements are critical; missing any of them can reverse the conclusion:108109- **Recognize both call forms.** During 2026-07-17…21, ego-lite fully namespaced the API110 (`click(...)` → `page.locator(...).click()`, `useOrCreateTaskSpace` →111 `taskSpaces.useOrCreate`). The old `(?<![.\w])NAME\(` scan was completely blind to the new112 form: it counted only 1 helper across 686 command bodies, producing "DOM 100% / visual 0% / JS113 0%." The current scanner is receiver-aware and merges legacy bare names into the same canonical114 set through `LEGACY_ALIAS`, allowing both generations of runs in one comparison table115 (verified: old and new scripts produce byte-for-byte identical output on legacy runs).116- **Track locator variables.** Calls such as `rows.nth(1).click()` after117 `const rows = page.locator('tr')` account for 59% of element actions in new runs. Missing them118 cuts the DOM-action count by more than half.119- **Exclude agent-defined inline functions.** Functions such as120 `setVal/parseNum/walk/findControl` defined by the model in a heredoc are not built-ins and must121 not count. Exclude them through both receiver awareness (`el.click()` inside `page.evaluate` has122 a DOM-node receiver, not a locator) and the `BUCKET` allowlist.123124Inspect two self-checks when reading the report:125126- **"Unclassified facade calls"**: parsed methods absent from `BUCKET`. A nonempty result means127 ego-lite added a capability and the taxonomy needs updating (the first deployment immediately128 detected `locator.setChecked` / `locator.elementCenter`).129- **Warning when more than half of command bodies contain no parsed built-in**: the taxonomy is130 behind ego-lite. Check §3.1 of ego-lite `skills/ego-browser/SKILL.md` before reporting metrics;131 do not trust that run's shares.132133See `references/helper_taxonomy.md` for mappings and bucket definitions (§1.7 covers the134namespacing break). When ego-lite changes the API again, update both that file and135`LEGACY_ALIAS`/`BUCKET` in the script.136137## Mandatory reporting rules (shared with pi-session-analyzer)1381391. Answer the user's question in a three-sentence summary before showing tables.1402. Distinguish statistical conclusions (full sample) from individual cases (`n=1`, clues only).1413. Separate task difficulty (multiple runs rise or fall together) from agent/skill implementation142 (one-sided movement).1434. Decompose headline gains or regressions by task (which tasks flipped or failed); do not report144 only total success rate.1455. Verify any risk based on theoretical possibility with evidence (artifacts, logs, or code paths)146 before raising it. Label anything unverified explicitly.147148## Typical workflow1491501. Confirm the run IDs to compare (`ls runs/`) and sort them chronologically.1512. Run all script sections. Read Alignment and Cross-Check to confirm the conventions are correct:152 exclusions match expectations, all cross-checks are ✓, judge_mode matches, and there are no153 rubric/category mismatch warnings—or those warnings have been handled with `--exclude`.1543. Interpret each section and use the per-task matrix to locate flipped or regressed tasks.1554. To investigate why a task failed, switch to `pi-session-analyzer` (workflows A/E).156157## Maintenance158159- Keep helper rename mappings synchronized between `references/helper_taxonomy.md` and160 `LEGACY_ALIAS`/`BUCKET` in the script.161- For a new metric field, first determine whether it belongs to `tasks.jsonl`,162 `run_metadata.json`, or `judges/<task_id>_judge.json`, then add it to the corresponding section.163- The script depends on the project's `ego_bench.session_parser` (`--repo-root` defaults to this164 harness root).165- After changing any counting convention, compare old and new output on a legacy run166 (`git show HEAD:<script> > /tmp/old.py`, then run both with the same arguments). Legacy-run167 numbers must remain byte-for-byte identical; any change breaks forward compatibility.