My RPI pipeline ends with an acceptance reviewer reading the finished branch and DMing the lead CLEARED. For most of what I build, that gate is enough. For web apps it kept letting through a category of bug that no amount of diff reading can catch: the code is correct and the feature is broken. The handler is wired, the types check, the tests pass, and the button sits behind a z-indexed header where no click can reach it. A diff describes what the code is. It cannot tell you what happens when someone uses it.
The fix wasn’t a better reviewer. It was giving one agent a browser.
The tester is just another teammate
The browser layer is agent-browser, Vercel Labs’ automation CLI built specifically for agents, and the reason it slots into my workflow so cleanly is that it’s just a CLI. Adding a UX tester to a goldengoose team required no integration work at all. Every agent already has a shell. A skill file in .agents/skills teaches the workflow, the same way my RPI skills teach the pipeline, and any teammate that reads it can drive a browser.
The loop the skill teaches is tight: navigate, snapshot, interact, re-snapshot. snapshot -i returns the page’s accessibility tree with a stable ref on every interactive element, such as @e1 [input type="email"] or @e3 [button] "Submit", so the agent reads structure, not pixels, and clicks by reference instead of guessing coordinates. Refs die when the page changes, so the discipline is re-snapshot after every navigation. When the page has icon buttons or a canvas that the text tree can’t see, the agent takes an annotated screenshot instead: numbered labels overlaid on the live page, each mapping back to a clickable ref. Text when text is enough, vision when it isn’t.
The setup around the tester is all machinery I already had. The dev server runs on the feature worktree through the process manager, so it’s a background process with bounded logs that injects its output back into the owning agent’s session instead of being babysat. Each tester runs a named browser session, so two agents testing two features can’t trample each other’s cookies. And the browser runs headed, with agent-browser’s live dashboard up, because an agent silently clicking through my app is exactly the kind of thing I want to be able to watch.
Verify every action, not just the end state
The habit that makes the testing trustworthy is borrowed from how I think about events in gooselake: don’t assume an action landed, get a receipt. After every meaningful interaction the tester runs diff snapshot, which compares the accessibility tree against the last one and prints what changed, git-diff style. Click “Add item” and the diff should show a row appearing. If the diff is empty, the click did nothing, and “the click did nothing” is precisely the class of bug this whole stage exists to catch, found at the moment it happened instead of three steps later when some downstream assertion gets confused.
Screenshots get the same treatment. The tester saves baselines and runs pixel diffs against them, with changed regions highlighted and a mismatch percentage, which turns “does the page still look right” from a vibe into a number. Then it shrinks the viewport and walks the flow again, because the desktop pass tells you nothing about where a fixed sidebar goes when the window gets narrow.
What this looks like in practice: during a recent phase review on the web client I’m building on gooselake, the tester wrote itself a harness, a 78-line throwaway .mjs in the worktree’s tmp/, not a framework. It boots the Vite dev server in-process on a strict port, drives agent-browser through a named session, and walks every screen (Board, Agents, Teams, Inbox, Ledger, Fleet, Playbooks, Settings), capturing screenshots at three widths: full desktop, laptop, and the awkward 980-pixel band where panels start fighting for space. Fourteen captures, one command, and the full walk runs inside a try/finally that closes the browser session and shuts the server down even when a capture throws, because a leaked dev server squatting on a strict port breaks the next agent’s run in a way that looks like their bug. Navigation is a plain eval that finds each nav button by its text; the agent didn’t reach for a fancy locator API. It wrote the obvious querySelector, and for a throwaway harness that’s the right call. It also didn’t lean on the team’s process manager for the server. The harness owns its whole world, server included, which is exactly what makes it rerunnable as one command by whichever implementer picks up the findings.
Findings loop back to the implementers
Here’s where it connects to RPI. In the fast variant of my pipeline, the acceptance reviewer already owns a fix loop: it files findings, fixer agents land corrections on the feature branch, and it re-reviews until satisfied. The UX pass runs after that review clears and feeds the same machinery. The tester writes up what it found, ordered by severity and with the exact command transcript that reproduces each finding, and those go back to fresh implementers on the same feature branch.
That command transcript is the part I’ve come to appreciate most. A human bug report says “the submit button doesn’t work sometimes.” A tester’s finding says: this sequence of commands, this fill, this click, this empty diff. The repro steps aren’t a description of how to maybe trigger the bug; they’re executable. The implementer can run the transcript, watch it fail, fix the code, and run it again. And when the fixes land, the tester re-walks its own flows, which means every feature that goes through this stage leaves behind a click-through script that is, in effect, a regression suite nobody sat down to write. The branch doesn’t integrate to main until the walkthrough is clean, the same rule as the code review gate, one layer up the stack.
The browser is untrusted input
Giving agents a browser also hands them the web’s problems, and the harness has to treat that seriously. Two agent-browser features do the heavy lifting. Credentials live in an encrypted auth vault: the agent logs into the app by profile name, and the model never sees a password. It isn’t in the prompt, the transcript, or the shell history. And page content comes back wrapped in nonce-tagged boundary markers, so the model can tell tool output from words that some webpage put there. A page an agent reads is untrusted input in the oldest sense: text that might be instructions. An agent that can click things absolutely should not take direction from the content it’s testing. Domain allowlists keep the tester inside the app it’s supposed to be in, so a stray external link can’t walk it off the property.
None of that is paranoia about my own dev server. It’s that the same tester setup works against staging and production too, where pages embed third-party content I didn’t write. The security posture has to be set before it’s needed.
The pipeline ends with somebody using the thing
What I like about where this landed is the shape of the full pipeline now: research maps the system, planning argues with me about options, implementers build it phase by phase, a reviewer reads every diff, and finally an agent opens a browser and uses the feature the way a person would, on desktop and on a phone, checking receipts after every click. Each stage catches what the previous one structurally can’t. The reviewer will never see a z-index bug; the tester will never catch a subtle data race; between them, not much gets through.
My own role keeps drifting in the same direction it’s been drifting for a year: I watch the dashboard when I feel like it, skim the screenshots and recordings attached to the handoff, and use the feature myself once before it ships, less as QA than as taste. The clicking, the re-clicking, and the arguing about whose bug it is all happen between the agents, in the same flat team, over the same messaging tools, the way everything else in my stack already works.