Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Agents use `pixelslop-tools` (bin/pixelslop-tools.cjs) for all state operations.
The measured /20 is the objective backbone; the perceptual layer is how the page actually *reads to a human*. It never touches the score — it's judgment, grounded in what was seen.

- **Personas are vision-first.** Each page-relevant persona is a spawned read-only vision agent (`dist/agents/internal/pixelslop-eval-persona.md`) that opens the screenshots and reacts as that human first — the five-second read, eye-path, trust, bounce — then grounds the reaction in measured evidence. Not all 8 run: the orchestrator picks the ~4 that matter for the page type via `browser analyze-page` → `suggestedPersonas`, plus project-specific personas. They spawn independently (blind to each other), with an inline fallback like the other evaluators. Returns `{ humanName, name, narrative, issues, priority, workedWell, reactedTo }` — `reactedTo` proves it opened a screenshot. This replaced the old inline "match triggers against measured findings" synthesis (a voice narrating the spreadsheet).
- **Project persona sourcing (in priority order).** The tailored persona comes from the best available signal: (1) an explicit audience/brand description in `.pixelslop.md` (setup) — always wins; (2) failing that, a **hero-inference fallback** in Step 6a — if the desktop hero pitches a specific, nameable audience ("The product development system for teams and agents"), infer one persona from it, tagged as an unconfirmed hypothesis; (3) failing that, the built-ins. The fallback self-gates: a bare search box, app shell, or generic splash produces nothing (verified against Linear/Stripe/Airbnb heroes — sharp on the first two, correctly silent on Airbnb's search box). Text about the audience always beats a guess from pixels, so the fallback only fires when there was no text to begin with.
- **The Read co-headlines.** The design-director's verdict + the sharpest persona reactions lead the report: a `### The Read` section above the Scores, a `Reads as:` header line, a co-led scan summary (`Measured: X/20` **and** `Reads as: …`), and an HTML card next to the /20 (`{{PERCEPTUAL_READ}}`, from `scan.perceptualRead`). It's prose grounded in what was seen — **never a competing number.** A second "/10" would be judgment masquerading as measurement, the exact failure the measured/judgment split prevents.
- **The internal evaluator count is now 8** (6 pillars + design-director + persona). `evaluator.test.js` pins it.

Expand Down
9 changes: 8 additions & 1 deletion dist/agents/pixelslop.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ If `custom` is empty and you have audience/brand, synthesize 1-2 personas follow
node bin/pixelslop-tools.cjs personas write --root "$ROOT" --raw --json '<persona JSON>'
```

Use a project-specific `id` slug (e.g. `stressed-bride`, not a built-in id). Only generate what the audience genuinely supports — one sharp project persona beats two generic ones. Skip this step entirely when there's no real audience to work from.
Use a project-specific `id` slug (e.g. `stressed-bride`, not a built-in id). Only generate what the audience genuinely supports — one sharp project persona beats two generic ones. When there's no real audience to work from here, skip — but don't give up on a project persona yet: Step 6a can infer one from the page's own hero once the screenshot exists.

### Step 6: Collect Evidence

Expand Down Expand Up @@ -210,6 +210,13 @@ node bin/pixelslop-tools.cjs log write --agent orchestrator --level info --messa

Don't run all 8 personas on every page — a screen-reader deep-dive on a marketing splash is noise, and a design-critic on a settings form misses the point. Pick the few personas that actually matter for *this* page, then evaluate those deeply.

**First, fill the audience gap from the hero (fallback).** If Step 5b produced no project persona — because `.pixelslop.md` had no audience/brand to work from — the page's own hero can sometimes supply one. `Read` `viewports.desktop.screenshot` and judge one thing: **does this hero pitch to a specific, nameable audience?** A value-prop headline aimed at a clear user ("The product development system for teams and agents") does; a bare search box, an app shell, or a generic splash does not.

- **If it does** — infer one project persona from what the hero is selling (who it's for, what they came to do, what would make them trust it or leave), and `personas write` it. Put `inferred from the page hero — audience is a hypothesis, not confirmed` in its `description`, and use a slug like `inferred-visitor`. It's real signal on a Linear- or Stripe-shaped page, and its findings get read with that caveat in mind.
- **If it doesn't** — write nothing. A search box tells you nothing about who's searching; the built-ins cover that page fine. Don't invent an audience from pixels that aren't pitching one.

This only fires when there was no context to begin with — it never overrides a persona you generated from a real audience description, which always beats a guess from a screenshot.

```bash
node bin/pixelslop-tools.cjs browser analyze-page --url "$URL" --raw
```
Expand Down
29 changes: 29 additions & 0 deletions tests/vision-personas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,32 @@ describe('docs teach the vision-first read', () => {
assert.ok(/pixelslop-eval-persona/.test(schema), 'schema should name the persona evaluator');
});
});

describe('hero-inference fallback (persona from the page when context is missing)', () => {
const orch = readDist('agents/pixelslop.md');

it('infers a project persona from the hero only when there was no audience context', () => {
assert.ok(/fill the audience gap from the hero/i.test(orch), 'the fallback should be documented');
assert.ok(/Step 5b produced no project persona|no project persona/i.test(orch),
'it must be gated on Step 5b producing nothing');
assert.ok(/never overrides a persona you generated from a real audience/i.test(orch),
'explicit context must always win over a screenshot guess');
});

it('gates on whether the hero actually pitches a specific audience', () => {
assert.ok(/pitch to a specific, nameable audience/i.test(orch),
'the gate is a pitch-forward hero, not just brand-vs-product');
assert.ok(/search box/i.test(orch) && /write nothing|Don't invent/i.test(orch),
'a functional hero (search box) must produce no persona');
});

it('marks the inferred persona as a hypothesis, not confirmed fact', () => {
assert.ok(/hypothesis, not confirmed|inferred from the page hero/i.test(orch),
'the inferred persona must be tagged as an unconfirmed hypothesis');
});

it('reads the desktop hero screenshot to do it', () => {
assert.ok(/viewports\.desktop\.screenshot/.test(orch),
'the fallback should read the desktop hero screenshot');
});
});
Loading