PinAppAI

← Blog

A reviewer pinned an invisible button on our own homepage. The fix was not the one they asked for.

We built a tool to close the outer feedback loop, then ran our own homepage through it: one pin, one change request, PR #122, and the honest ledger.


Our homepage hero has two buttons. The primary is a solid red call to action (#E30A17). The secondary, “Read the docs”, is a ghost button with a thin outline. On the near-white hero gradient (#f9fafb into #ffffff) that outline landed at about 1.2:1 contrast. Invisible. Next to a saturated red button it read as floating text with no edge at all.

Here is the part that stings. The button was not missing a border. It had one, and the rule had been there the whole time:

.btn-ghost { border-color: var(--pp-border); }  /* #e5e7eb */

var(--pp-border) is our hairline token. It exists for dividers, card edges, the header underline, the footer rule, everywhere barely-there is the whole point. It is exactly right for a 1px line between two panels and exactly wrong for a control that has to hold its own shape on a bright background. So the defect was never “no border”. It was “a border borrowing a token built for a different job”. A linter waves that straight through. A design-token audit waves it through too, because every rule is present and every value is valid. Nothing is broken. It just does not read.

We did not catch this by re-reading the CSS. Someone caught it by pinning it.

Watch the loop run

Here is what that looks like end to end. A reviewer opens the live site and pins the ghost button, one workflow drains the inbox and opens a pull request, and a human approves the change on a preview deploy before it ships. Every frame is real product state. Nothing is staged, and nothing is sped up except the agent’s own working time, which is marked on screen while it runs.

That recording is a later run than the one written up below, on the same button after we had renamed it: change request #9, pull request #258, about nineteen minutes. The story told here is the first one, CR #2 and PR #122. We kept both because the second run is the honest answer to “does this keep working after the writeup”.

The run

We build a tool for closing what we call the outer loop of feedback, so the least we could do is point it at ourselves. This is the writeup of one run: one reviewer, one pin, one change request, one pull request, about thirteen minutes of wall clock.

A reviewer opened the real, live page and dropped a pin on the actual element. Not a screenshot, not a Slack message. The pin went on the “Read the docs” button, and the note was blunt:

This one’s invisible next to the red button. Give it a real border.

That pin became a structured change request carrying the CSS selector, the page URL, the device, and the viewport. Ours was CR #2.

Our coding agent read CR #2 over an MCP server (it works from Claude Code, Cursor, or Codex). Here is where it earned its keep. It did not darken the border and call it done. It found the existing rule, saw it was pulling the hairline token, and made three judgment calls a careless fix gets wrong:

It changed the token the button points at, not the token itself. It repointed .btn-ghost from --pp-border to --pp-ink, the button’s own text color, so the outline reads as one coherent button instead of a new one-off gray. Retargeting --pp-border itself would have “fixed” the button by making every divider and card edge on the site heavy. The defect was in the button’s choice of token, not the token.

It fixed the component, not the pinned pixel. .btn-ghost is a shared class with fourteen call sites across the marketing pages and their Turkish mirrors, and every one of them carried the same invisible border. A hero-scoped override would have fixed the exact pixel the reviewer pinned and quietly forked the component. Fixing the class fixed all fourteen.

It measured instead of guessing. Contrast against the hero went from 1.2:1 to 14.05:1, verified in the browser with getComputedStyle and the WCAG ratio, not eyeballed.

Then it opened a real pull request, PR #122, fix(marketing): give .btn-ghost a visible border (CR #2). A green “Applied” marker appeared on the exact spot that had been pinned, and CR #2 moved to InReview, so the reviewer could see their note was acted on without reading a diff. Reject it, and it returns to the Inbox; the next run reverts it from the recorded before/after snapshot.

Pin to change request to applied fix to PR to green marker: about thirteen minutes.

What the pin carries, and why that is the whole trick

The reason any of this survives is the form the signal arrives in. “The second button looks off” is true and useless. It dies in the retelling, in the screenshot with no selector, in the ticket that sits until nobody remembers which button. A pin does not paraphrase the problem, it locates it: the element, the page, the device, the viewport, packaged so an agent can act without a human first translating it into a ticket.

A reviewer can drop that pin four ways, and none of them ask the reviewer to install anything or know any code: a zero-install review link you send like a Figma link, an embeddable widget on your own site, a Chrome extension for pages behind a login, or a native macOS app. The note lands in an Inbox as a numbered change request, the agent drains the Inbox in one run, and everything routes through InReview before it can merge. The human is never removed. The developer still reviews the PR. What changed is that the feedback arrived already actionable.

The shape generalizes

This run was one CSS token. The shape does not depend on that. To be explicit, the three below are illustrations of the pattern, not real change requests we are showing you; they are the kind of thing that lands in the Inbox once a real site is live:

  • A client pins the hero: “this headline is too quiet next to the image.”
  • A PM pins the pricing card: “this still says $12 on mobile.”
  • A stakeholder pins a section after a redesign: “the sign-up button got buried, it used to be the first thing you saw.”

None of those is a bug in the classic sense. All of them are the stuff that accumulates between “the AI built the site” and “the site is actually right”, and all of them normally travel as vague prose a developer has to decode before touching the code. Located and structured, they become something an agent can act on and a human can approve.

The thesis, briefly

The inner loop got fast. An agent reading a file, editing it, running the test: that cycle is measured in seconds and still shrinking. The outer loop did not move. The outer loop is a real person on a real live site noticing something is off and getting that signal back into the codebase in a form the code can act on. As the inner loop collapsed toward zero, the outer loop started to dominate the total.

The gap is named in the research, not invented by us. The Verification Horizon work lists user-as-verifier as one of the reward constructions it considers; the SWE-bench authors’ own position paper argues the bottleneck has shifted to human-agent interaction and asks specifically for “visual previews and interactive summaries”. Recognized in the papers, absent from shipped harnesses. Open any agent harness today and count the interfaces for a person who is not the developer to hand structured signal back into the loop. PinAppAI is one implementation of building that interface. It may not be the right one.

The ledger, which we would rather you read than skip

Here is exactly what we can and cannot claim, because the small true version is worth more than a big invented one.

This was one change, on a site we control, driven by feedback we solicited. It is a demo, not a measurement, and any recorded version of the flow is sped up. We have three real external users and one real external project; that is not a customer base and you should not read it as a customer result. We have not measured an outer-loop time reduction for anyone but ourselves, and measuring yourself is the weakest evidence there is.

The “weeks to hours” compression we like to talk about is a prediction that follows from the argument, and the argument may be wrong. Maybe the bottleneck just moves. Maybe reviewers do not pin, or pin badly. Maybe an agent applying a confidently-wrong change more often than a human would eats the savings at the review step. We will find out when we run it at a scale worth a number. And PR #122 did not ship. It opened. A human reviews it, same as any other PR.

When we have customers to measure, we will publish those numbers, including the ones that do not flatter us. Until then the honest claim is the small, real one: a reviewer pinned an invisible button, an agent worked out it was borrowing the wrong token, chose the fix that did not wreck fourteen other call sites, measured the result, and opened PR #122. You can go look at it.