prima check: run a scenario end to end and report what it proved - #110
Open
DavertMik wants to merge 30 commits into
Open
prima check: run a scenario end to end and report what it proved#110DavertMik wants to merge 30 commits into
DavertMik wants to merge 30 commits into
Conversation
Explorbot Self-RegressionCommit
Attempt details
Session analysis — basic (native): Session AnalysisThe Issues feature is functional: users can create bug/feature issues, search by title, filter by label, and view issue details. All core flows work. Test automation has reliability issues with initial click actions, requiring retries or workarounds, but the product behaves correctly. Coverage
What works
Execution Issues
|
DavertMik
force-pushed
the
fix/prima-executor
branch
from
August 7, 2026 20:47
0a1e7e3 to
48c75e8
Compare
A `pw` call on a selector that did not exist returned ok: true, healed: true, having clicked an unrelated control. Heal is removed outright — with it the --no-heal flag, the healed: line and the healing-attempts block — so no code path can reach an element other than the one asked for. `do` now drives by the refs Playwright puts in the page snapshot. New clickRef and hoverRef tools in boat/prima/src/tools.ts take a ref, resolve it through WebElement to an attribute-based clickXPath, and require exactly one match. One instruction becomes one command instead of a fallback ladder, and a stale ref is reported as stale rather than replaced by a guess. click and hover in src/ai/tools.ts are untouched. A tool's schema is shared with every caller, and Tester never receives ref-bearing snapshots, so it must not be shown a ref parameter it could only fill by inventing one. The ref tools live in the boat that uses them; core only lends its result-shaping helpers so they are not copied. A new context() tool returns the page with fresh refs when one dies mid-run, and drops to raw markup on a second call. ### Changes now renders on every action, including "no change" — it was structurally impossible alongside an answer, a research map or a verdict, so a successful click proved nothing. Refs never reach the diff or state hash: an identical page whose refs merely renumber produced six phantom entries. Also fixed, each found against a live app: - Attach discovery keyed on a workspaceDir field no release of @playwright/cli writes, so prima reported no browser while a session was open. Attached sessions now connect through the daemon's own Playwright build; connect succeeds across builds but ariaSnapshot does not. - ARIA compaction kept only the first bracket group, so every control carrying [pressed], [disabled] or [checked] lost its ref — exactly the controls worth acting on. - A redirect that only appends query parameters counted as failed navigation, costing three minutes of retries on a page that had loaded correctly. - verify reported claims it could not phrase as failing checks, and remembered them as such. It now separates the two, and can assert control state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DavertMik
force-pushed
the
fix/prima-executor
branch
from
August 7, 2026 21:28
48c75e8 to
edeb863
Compare
`prima check <scenario>` spawns a full test run — it drives the page, verifies the outcome itself and reports every step with its proof. Each --expected outcome comes back as PASSED, FAILED or not verified, so a caller sees the mapping instead of a bare ok flag. `prima do` now ends where the instructions end. A done() step closes the sequence, names the instructions it could not carry out, and fails the command for them; it used to keep acting past the last step and could report success while a check it was asked to make never held. A test that stops making progress is handed to final review rather than marked failed on the spot, and console/network errors are recorded as page problems instead of failed steps — both used to sink runs that had already done their work. Also: pw returns its value, verify lists each assertion with its own result, aria diffs report typed values, long values are offloaded to an excerpt, pages are ready when the DOM goes quiet, and prima prints the envelope and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An instruction satisfied by absence — "dismiss the banner if one appeared" — or one that only verifies leaves no executed command behind. The "no action was performed" guard fired on both and reported failure for a sequence the model had closed cleanly. It now applies only when the model ended without calling done(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
do had no idea which instruction it was on. It re-attempted satisfied steps, kept acting past the end of the sequence, and its one self-report at the end could claim a success it never earned — measured against a live app, four do calls spent 227s of a 245s run thrashing. Instructions now carry numbers that never change. completed() reports the ones just satisfied, blocked() reports one the page cannot do, and what is still open is re-stated after each turn. The loop ends when nothing is open. Prima staples the actions it actually executed to each report, so a completed instruction with an empty trail is visible as such — an instruction satisfied by absence still passes, since conditionals legitimately complete without acting. ### Steps is now one line per instruction with its proof, and an instruction nobody reported is named as unaccounted for rather than silently passing. A stray failed action no longer fails a sequence whose instructions all closed. Also: prima reads the attached session's own URL, so --url is no longer required on every command; --pw-session and --url parse before the subcommand too; cached research is returned without a banner telling the model to refresh it; help leads with check and orders the tiers by what they cost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A test that achieved all its expectations was never given a result. Test .hasFinished is true once every expectation carries a status, so the loop broke, finishTest saw a finished task and never called finish(), and the run reported success: false on a scenario that fully passed. The dead "Test with no result" log branch existed because this state was reachable. That is what made prima check print ok: false over six PASSED outcomes, and it is the same null status that crashed the HTML reporter on test.status.toLowerCase() after every check run. finishTest now decides from what the run achieved: all expectations met passes, anything short fails. do re-states what is still open on every turn rather than only when the list changes — a model that acted without reporting was otherwise never reminded, and its instruction came back as unaccounted for despite the envelope proving it landed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The help was the single largest thing prima put in a caller's context — 6,718 bytes, 56% of everything an agent read across a whole benchmark run, for one call. Per-command semantics now live under each subcommand's own --help, leaving the top level with the tiers, the envelope shape and the session default: 4,439 bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pressKey refused every key that needs focus — Enter, End, Home, arrows, Backspace, Delete, single characters — because it decided focus by regexing [focused] out of the cached ARIA snapshot, and Playwright's ariaSnapshot never emits [focused] in either plain or ai mode. Probed against a live page: a button that IS document.activeElement produces zero lines containing "focus". The guard could therefore never pass, and only the focus-free keys (Escape, Tab, F-keys) ever ran. That is what cost the last benchmark three editor close/reopen cycles and four failed attempts to move a caret to end of content. Focus is now read from document.activeElement at the moment of the press, and an unreadable page lets the press through rather than blocking it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tester's <current_focus> block and Pilot's focused: line have never
rendered. Both read focus by regexing [focused] out of the ARIA snapshot,
and Playwright does not put it there — so both silently omitted focus on
every single turn, and the tester never knew it was sitting in a text
field.
Focus is now read from document.activeElement alongside the ARIA snapshot
and carried on ActionResult and WebPageState, with the element's tag
mapped to its ARIA role so the existing textbox/combobox checks work.
The ARIA parser it replaces is deleted rather than left as a decoy.
Verified live: a focused search input reads back as
{role: textbox, name: "Search skills", value: "explorbot"}.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A check of an eight-point goal printed 41 numbered steps, most of them successful intermediate actions with near-duplicate labels. The verdict already lives in ### Expected outcomes, so listing every step that worked spends the caller's context on what it will not read. ### Steps now carries the failures and the expectation outcomes, and closes with how many routine steps ran and the status hash that has the full log. Nothing is dropped silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
do told the model which instructions were still open but never which were closed, so nothing stopped it redoing satisfied work — measured on a live page, it re-clicked a panel toggle it had already activated, closing what it had just opened, and repeated the fill behind it. Each turn now carries the whole ledger with each instruction's state and the proof that closed it, and asks for completed() on anything the page already shows is satisfied before acting again. Same instructions against the same page: 10 commands and both instructions unaccounted for, down to 2 commands with both closed and proven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
prepareInstructionsForNextStep reassigned its message instead of appending, so from the second step onward the tester lost the whole <rules> block along with the scenario reminder — including "do not do unsuccessful clicks again" and "do not run same tool calls with same parameters again", the two rules written to stop exactly the repetition that fills its step logs. The log now appends to the rules rather than replacing them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An expected outcome counted as met only when the tester recorded a note whose text matched it byte for byte. Paraphrase once and a run that genuinely verified everything reported "not verified" across the board — which is what happened in the last benchmark, and what pushed the caller down into do and tripled the cost of the run. Pilot now settles the outcomes nothing checked off by name: it reads the run log and judges each one on what the steps show happened, with "unverified" reserved for outcomes the run never established either way. Outcomes the run did check off by name skip the call entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tool told the model to "use it to verify the actions were performed correctly and the page is in the expected state", so it ran a second, slower model after almost every action — 18 vision calls in one measured scenario, 23% of that run's wall clock. Every action already reports what changed on the page, so those calls asked a question that had just been answered. It now describes what a screenshot uniquely gives — layout, what an image or canvas depicts, colour, whether something is covered — and says not to use it to confirm an action landed. The example follows the description instead of contradicting it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
do asked the page for a ref-bearing snapshot and then stripped every ref back out before showing it to the model. Refs are what make a click unambiguous: aria-ref resolves to exactly one element in ~2ms, so it can raise no strict-mode violation, produce no "multiple elements" error, and never reach disambiguateElements — the model call measured at ~10s per ambiguous click, and 21% of one check's wall clock. clickRef takes a ref from the context and clicks it through Playwright's own selector engine, so CodeceptJS locator resolution never runs. It sits beside click rather than changing it, so nothing that composes locators is affected. click now says to prefer it when the element carries a ref. Verified: 100 refs reach do's context, a ref resolves to one element in 2ms, and refs survive a DOM change caused by our own click. Adoption by the model is not yet consistent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clickRef executes through aria-ref, which names an element only for the session that produced the snapshot. Reporting that as the executed code put a selector into generated tests that cannot resolve when they run again. The click still goes through the ref — that is what makes it unambiguous and fast. What gets reported is the element's own role and accessible name, read back from the page, so the generated test has something that survives the session. If the element cannot be described that way, the ref form is reported unchanged rather than a guess. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check runs through the tester, which had clickRef available but no refs to pass it — its ARIA block came from the stored snapshot, captured without mode: 'ai'. The tool was unreachable in the tier where clicks cost most. The block is now built from a ref-bearing snapshot taken at the moment the context is written. The stored snapshot is untouched, so state hashes and page diffs stay ref-free and an identical page whose refs merely renumber still reads as unchanged. A page that cannot be re-snapshotted falls back to the stored one rather than losing its context. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clickRef was defined in createAgentTools, which has no task in scope, so
its first line threw ReferenceError on every call. It had never executed
once — which is why no run ever produced a ref click, and why the model
looked like it was ignoring the tool. It lives beside click now, where
the task it records into is in scope.
do offers clickRef while the accessibility tree is the context and brings
click back only once the model has dropped to markup, where there are no
refs to use. That follows the shape of the context rather than asking the
model to choose between two tools for the same job.
The recorded locator derives the element's implicit role, since a plain
button carries no role attribute and the description came back empty.
Verified against a live page: a ref click succeeds in ~800ms and reports
I.click({"role":"button","text":"..."}).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Withholding click while the accessibility tree was the context did not push the model onto refs — it routed the same locator clicks through form, which takes arbitrary CodeceptJS commands — and the run came back with every instruction unaccounted for where the same three had closed before. Both tools are offered again. clickRef stays available and working; which one the model reaches for is a separate problem from whether the ref path exists, and starving it of the familiar tool is not what solves it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The locator priority rule opened straight into how to compose a locator, and form invited any CodeceptJS command, so both taught locator composition as the way to click even for elements the context had already named with a ref. Each now says what it does not cover: an element carrying a ref is clicked with clickRef and that ref, and the priority list is for elements the context gives no ref for. The clause is self-limiting — an agent whose context has no refs never meets the case — so navigator and tester read the same rule they did before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The help was 6.7k, then 4.4k, of prose about envelope sections and session management. Every agent reads it before its first command, and in one benchmark it was over half of everything prima put into the caller's context — to explain output the agent is about to see anyway and flags the option list already documents. What is left says what prima is, how it pairs with playwright-cli, and shows check, do and pw carrying a whole job rather than one click. 2.4k, most of it the command list commander generates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The help told the model what to do — give check a whole job, describe targets, give pw code only. Instructions compete with everything else in the context for authority. A description of what each command takes and what it costs carries the same information without spending that. The same rewrite applies to the check and do help. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The examples show what check, do and pw take. The paragraph above them restated it in longer words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Drive an already-open browser one command at a time" describes the plumbing, which an agent reading help does not need to decide anything. What decides is that prima takes described behaviour rather than locators, and returns a verdict with its proof. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Which model answers for which role was only discoverable by reading ~/.explorbot/config.js. prima models prints the resolved roles and the config file they came from. It reads configuration only — no browser, no session, and any registered site will do — so it answers before anything is running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The envelope described the run three ways and none of them was a trace. used: listed only the commands that succeeded, so a run that clicked, failed, then escaped reported "used: I.pressKey(Escape)" as if that were the whole job. ### Changes repeated the page diff that already appeared under every step. And evidence only reached the envelope when the model reported an instruction — when it did not, everything it had done vanished behind "never reported". ### Steps is now the log: every action, check and report in the order it happened, each with its own outcome. One report closing several instructions is one line carrying one proof. Instructions nobody reported are appended as their own lines rather than swallowing the trace. used and ### Changes are gone from do, since the log carries both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The step log said what ran but nothing showed what the page looked like when it ran, so a claim in it could not be checked against anything. Each action now writes the page it produced into the run's folder as <n>-<step>.aria.yaml, .html and, when the action changed something, .diff.yaml. The envelope prints the folder once at the end of ### Steps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A check that failed before the action which satisfied it vetoed the whole command. Verifying "debug panel is enabled", then enabling it, then reporting the instruction done came back ok: false with "unproven: Debug panel is enabled" — the check was never re-run, and its stale result outranked what the model had proven. do no longer draws a verdict from verify results. Each check is logged with its own outcome, as verify itself does, and whether the run succeeded follows from the instructions the model accounted for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DavertMik
requested a deployment
to
regression
August 9, 2026 22:56 — with
GitHub Actions
In progress
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Field-tested prima against a live app (review) and fixed what that turned up, then benchmarked it head-to-head against playwright-cli until the numbers moved. Spec:
2026-08-07-prima-fixes-design.md.The bug that motivated this
The element did not exist. Heal picked an unrelated control, clicked it, and reported success — so
ok: truedid not mean your own action landed. Heal is removed outright, along with--no-heal, thehealed:line and the healing-attempts block. No code path can now reach an element other than the one asked for.The tier that pays:
doandcheckThe point of prima is that the expensive model outside it never sees a page. That only works if one prima call can carry a whole sequence, so both long-running commands now do.
dohas the same toolset a test run has — act, look, assert — and ends where the instructions end. Adone()step closes the sequence and names the instructions it could not carry out, which fail the command. Before this it kept acting past the last step and could reportok: truewhile a check it had been asked to make never held.check <scenario>spawns a full test run: it drives the page, verifies the outcome itself, and reports every step with its proof.Each
--expectedcomes back under### Expected outcomesasPASSED,FAILEDornot verified. "not verified" is deliberately not "false" — it means nothing checked it. That mapping only works if the run records an outcome in its own words, so the prompt now says an expected result is settled only when it is repeated word for word; measured live, that moved both outcomes of a real scenario fromnot verifiedto a real verdict.clickandfillare gone — one-action-per-process was the shape that lost to playwright-cli.Envelopes carry proof, not noise
### Changesrenders on every action. It was structurally impossible alongside an answer, research map or verdict, so a successful click proved nothing.The
typed:section is new — filling a form used to diff as no change at all. Long values are cut to an excerpt with a pointer to the full text; on a page holding a document that took one envelope from 23,176 B to 4,873 B.pwreturns its expression's value under### Value— apage.title()orlocator.count()used to run and have its answer discarded.verifylists every assertion with its ownPASSED/FAILEDand the Playwright form of the ones that held, and gives no overall verdict; the caller reads the lines and decides.Everything else is behind
prima status <hash>, printed on each envelope's### Instanceline. Prima commands print the envelope and nothing else — the banner, config line and browser chatter were 22% of what a caller was paying for.Verdicts belong to review, not to a loop counter
A test that stopped making progress was marked
FAILEDon the spot by a stall counter. A run that had already done its work and gone quiet was therefore reported as a failure — which is exactly whatcheckthen handed back. The guard now stops the loop and hands the task to final review; the verdict comes from reviewing the result. Console and network errors are recorded as page problems rather than failed steps, so they no longer sink a passing test.Also fixed, each found live
workspaceDirfield no@playwright/clirelease writes (checked 0.1.13 and 0.1.17), so prima reported no browser while a session was open and told you to open the one you already had. Attached sessions now connect through the daemon's own Playwright build —connect()succeeds across builds butariaSnapshotdoes not. Pin moved to^1.62.networkidlenever fires on an app with a live websocket, so every capture paid the full 6s timeout. Readiness now also completes when the DOM goes quiet: 6,034ms → 25ms per capture.[pressed]/[disabled]/[checked]control lost the rest of its attributes./→/?session=…&ws=1) counted as failed navigation: 3m00s of retries, or a hard failure, on a page that had loaded correctly. Now 22s.{ disabled: null }matches nothing — and is replaced by attribute selectors probed against a live page.// TODO.Testing
901 unit + 78 integration + 101 prima tests pass, 0 fail (2 pre-existing file-level errors in the unit scope, identical on a clean tree). New coverage for
done()ending the loop, unmet instructions failing the command, a check that fails then passes on retry, expectation mapping including the unverified case, typed-value diffing, value offloading, and DOM-quiet readiness. The heal integration suite is deleted with the feature.Verified end to end against a live app after building
dist, not from source.