Four of six failures in our first trusted run were our own bugs, not the model's
The score said 85.4% of attempted tasks passed. Reading every failure against the agent's own transcript said something more useful: the instrument was wrong in three places, and no number of reruns would have found it.
The first run of this benchmark that we trusted scored 85.4% of attempted tasks. Then we read every failure against what the agent actually said and did on the phone. Of six failures, three were caused by our own checks, one was a task that could only be failed because of the owner's device, and one or two were the model's. The audit changed the interpretation of the run completely, and it took less time than the run did.
What the audit found, task by task
The method is the cheap part: for each failed task, put the agent's own transcript next to the device-state check that failed it, and ask which of the two is wrong. The agent's account is not evidence that it succeeded, because a model that believes it tapped Send will say it sent the message. It is excellent evidence about the harness, because a model that says "I created the reminder and the list went from 5 items to 6" and then fails a check is telling you the check is looking in the wrong place.
| task | what the check said | what the transcript showed | verdict | source |
|---|---|---|---|---|
contacts.search | no "Cancel" button found | the agent completed the search and listed the results | our bug: that UI has no Cancel | FINDINGS s.12 |
multi.settings_to_safari | task abandoned mid-way | the agent read the iOS version correctly, then stopped | our bug: shared mode paused it, believing the owner had picked the phone up | FINDINGS s.12 |
reminders.create | the reminder text was not on screen | the agent created it and verified the list count going 5 to 6 | our bug: the check refused to scroll | FINDINGS s.12 |
safari.newtab | no new tab | the agent diagnosed 502 open tabs and that iOS disables the control at roughly 500 | our unfair task, retired rather than fixed | FINDINGS s.12 |
safari.navigate.example | foreground is springboard | "Safari is open and showing example.com" | genuine catch, and the reason the checks exist | FINDINGS s.12 |
clock.timer.set | no timer set | 26 turns, 228 seconds, no result | genuine model failure: picker wheels are hard | FINDINGS s.12, s.15 |
system.notifications.open | no "Clear" button | Notification Centre was open and empty | our bug, found later: Clear exists only when notifications are waiting | FINDINGS s.17 |
The last row is the one that matters most, because it was found after the first three were fixed. It is the fourth check of the same shape: an assertion on a control that only exists in one of the device's two legitimate states. On an empty phone the task is unpassable no matter what the agent does, and nothing in the result file says so.
Five ways this rig produced a number that was not true
Four of them made the number look better. That direction is not a coincidence: a harness fault usually removes work from the model or credits work it did not finish, and only rarely invents extra difficulty. Each one is now a guard in code rather than a note in a document.
| fault | what would have been published | the guard now | where |
|---|---|---|---|
| Rate limit after 5 of 32 tasks, the other 27 recorded as model failures | 12.5% | UsageLimitReached, the task is marked skipped and the run aborts with an explanation | bench/runner.py |
| The phone locked mid-run, every task after it failed identically | a page of red that reads as a model that cannot use a phone | RigUnavailable, raised on a locked phone that does not come back | bench/runner.py |
| A verification step created a WebDriverAgent session, which sent the phone home before reading it | every task failing with "foreground is springboard" | the observation path uses only sessionless routes | FINDINGS s.6 |
clock.alarm.set_time hit its 240 second budget with zero completed turns and $0.00 spent, then passed both checks from what was left on screen | a pass | result.passed = passed and not timed_out, and checks match the wheel's own value rather than any digits on screen | bench/runner.py, FINDINGS s.21 |
| Retired tasks kept their last result in the file and kept being counted; a leaderboard over 74 tasks sat above a footer over 60 | two different rates on one page | scoring keeps only live tasks and only the latest attempt per task, with a publication mode that scores over published tasks | bench/runner.py score() |
The pattern behind all five is one distinction: the model tried and got it wrong and we could not ask the model are different facts. A benchmark that cannot tell them apart is not producing a noisy number, it is producing fiction, and the fiction is stable across reruns because the cause is in the harness.
This is not a local problem
The Agentic Benchmark Checklist (arXiv:2507.02825, first posted 3 July 2025, v5 on 7 August 2025) reports the same class of defect in published benchmarks: SWE-bench Verified uses insufficient test cases, TAU-bench counts empty responses as successful, and the authors put the resulting misestimation at up to 100% in relative terms. Applying the checklist to CVE-Bench reduced its performance overestimation by 33%. TAU-bench counting an empty response as a success is the same fault as our timed-out alarm task passing its checks with zero turns and no spend: a grader that reads the environment without asking whether the agent was ever in a position to change it.
The method we used by hand has since been automated. "Automated Transcript Analysis for Detecting Flaws in Agentic Benchmarks" (arXiv:2607.27518, 29 July 2026) runs scanners over benchmark transcripts looking for ground truth access, tool failure, guessing vulnerability and answer format ambiguity, and reports verified quality issues in five widely used benchmarks. Our four categories were different only because our environment is a phone: a check asserting on a control iOS does not draw, a courtesy feature pausing a measured run, a check that would not scroll, and a task whose outcome depended on how many Safari tabs the owner happened to have open.
What to do with this if you run an agent benchmark
Read the transcripts of your failures before you publish the number. Not a sample: all of them, at least once, at the point where the suite is small enough that this is possible. It is the cheapest quality control available and it is the only one that catches a check that is wrong in the same direction every time.
Then make the two failure modes structurally different in your results file. A boolean
passed cannot express "we could not ask the model", and if the only place that fact
lives is an exception message, the aggregate will silently absorb it. Our row carries
skipped and skip_reason alongside passed, and the published
rate is computed over scored tasks with the skip count printed next to it.
Three rules came out of the audit and each generalises beyond phones. "Not visible right now" and "not there" are different facts, so a check that refuses to scroll blames the model for the harness's impatience. Assert on the surface, not on a specific button, because button sets change between OS versions and an assertion on one makes the benchmark rot. And a measured run must take the device: a feature that politely yields to a human who is not there is a scoring bug during measurement.
A judgement, marked as one: the audit is worth more than the run. The score told us roughly what we already believed. The audit found four defects in the instrument, and three of them would have survived any number of reruns.
Check it yourself
The guards are all in one file, and the results are line-delimited JSON, so the arithmetic is inspectable without running anything on a phone.
git clone https://github.com/Blomega/phoneshell && cd phoneshell
# the three guards, in order of the diagram above
grep -n "class UsageLimitReached" -A 8 phoneshell/bench/runner.py
grep -n "class RigUnavailable" -A 8 phoneshell/bench/runner.py
grep -n "result.passed = passed and not timed_out" -B 6 phoneshell/bench/runner.py
# scoring excludes skipped tasks and retired tasks, and keeps the latest attempt only
grep -n "def score" -A 30 phoneshell/bench/runner.py
# on your own results file: how many rows were never scored, and why
jq -r 'select(.skipped==true) | [.task_id,.model,.skip_reason] | @tsv' runtime/bench/latest.jsonl
# the audit itself: every failure next to what the agent said about it
jq -r 'select(.passed==false and .skipped!=true) | [.task_id,.turns,.seconds,.agent_said] | @tsv' \
runtime/bench/latest.jsonl
The last command is the whole method. If the fourth column explains the task and the check still failed it, the defect is yours.
What would prove this wrong
- Take our published task set, run it against a model on your own iPhone, and audit every failure the same way. If fewer than one in four of your failures turns out to be a harness defect, our claim that this is the normal ratio for a young benchmark is wrong, and we would rather know. We predict a rate above one in four for any device benchmark under a year old, through 30 June 2027.
- Show a results file where
skippedandpassed==falseaggregate to the same published rate. That would mean the distinction we built the guards around does not move the number in practice, and the guards are ceremony. - The claim we are least sure of: that four of the five fabrication modes made the score look better. That is an observation over five incidents on one rig, which is an anecdote with a pattern, not a measured asymmetry. A larger sample from any device benchmark would settle it.
Sources
- phoneshell, FINDINGS.md sections 7, 12, 17 and 21, the audit and the five fabricated numbers, published 16 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
- phoneshell, phoneshell/bench/runner.py: UsageLimitReached, RigUnavailable, the timeout rule and score() over live tasks only. https://github.com/Blomega/phoneshell/blob/main/phoneshell/bench/runner.py
- Zhu et al., Establishing Best Practices for Building Rigorous Agentic Benchmarks, arXiv:2507.02825, 3 July 2025, v5 on 7 August 2025. SWE-bench Verified's insufficient tests, TAU-bench counting empty responses as successful, misestimation up to 100% in relative terms. https://arxiv.org/abs/2507.02825
- Mohl et al., Automated Transcript Analysis for Detecting Flaws in Agentic Benchmarks, arXiv:2607.27518, 29 July 2026. https://arxiv.org/abs/2607.27518
- Blolabel, phoneshell-bench, the published task set and results. https://huggingface.co/datasets/blolabel/phoneshell-bench