--- name: autokap-shot description: Produce, change, refresh and drop product screenshots — write captures/shots//intent.md and shot.mjs with assertions, run them, then open the PNG and judge it. Use when the user asks for a screenshot of a screen, says "/autokap ", "change this shot", "refresh", "drop ", or when npx autokap run comes back red and something needs repairing. --- # autokap-shot — producing and refreshing an image This skill writes programs and looks at images. **It never writes to the database** — that is [autokap-world](../autokap-world/SKILL.md). If the screen needs data that does not exist, hand over, then come back. ## The order, which is not negotiable 1. **`intent.md` first.** What the image must show, and why. 2. Then `shot.mjs`, with assertions that come from the intent. 3. Then `npx autokap run `. 4. Then **open the PNG and look at it.** 5. Then write the `seen` verdict into `history.jsonl`. Changing the program before the intent means the next run judges against a stale criterion and silently accepts a bad image. When the user wants something different, edit `intent.md` first, every time. ## `intent.md` ```markdown # Project board — full ## What the image must show A 4-column board, every column filled, a dozen tickets, varied priorities including at least 2 urgent, 3 tickets assigned with a visible avatar. ## Where /projects/ — board view, sidebar open. ## Variants fr/light, fr/dark, en/light, en/dark ## Known traps (filled in as runs go by) ``` Write it in terms of what a reader must be able to see. "The board" is not an intent. "Four columns, none empty, at least two urgent badges" is: it can be checked, and it can be argued with. "Known traps" is worth its weight. Every time a run surprises you — a tooltip that stayed open, a column that collapses below 1200px, an avatar that loads late — write it down there. ## `shot.mjs` Three declarations at the top, then assertions before the picture. ```js export const SLOT = "heroBoard"; // where it is delivered export const WORLD = ["project:aurora", "user:alice"]; // data it depends on export const WATCH = ["app/(app)/projects/**", "components/board/**"]; // code it depends on export default async function shot({ page, visit, settle, expectShot, capture }) { await visit("/projects/aurora"); await settle(page, { anchor: '[data-testid="board"]' }); await expectShot(page, { visible: ["text=AUR-1"], atLeast: { "main [data-card]": 12 }, notClipped: "main h2", absent: ['[role="dialog"]'], }); await capture(); } ``` - **`WORLD`** names entities from `captures/world/world.md`. It is what lets "put this data here instead" know which other shots to replay. Without it, improving the hero silently breaks three others. - **`WATCH`** is how `npx autokap status` knows whether this screen moved since the last run, without launching a browser. Point it at the routes and the components this screen actually uses — not at `src/**`, which makes every shot stale every day and trains everyone to ignore the output. - **`export const AUTH = false`** for a screen that needs no session. - **`export const VARIANTS = ["fr-light", "en-dark"]`** to override the config's locale × theme grid. ## Assertions They come from the intent, and they fail loudly. **A green-but-empty screenshot is the most common and most expensive failure mode**; assertions are what turn it red. | Key | Checks | |---|---| | `visible` | selectors that must be visible | | `absent` | selectors that must not be there — banners, modals, onboarding | | `atLeast` | selector → minimum count. This is the one that catches empty | | `atMost` | selector → maximum count | | `notClipped` | nothing cut off by an edge or truncated inside its box | Every clause in the intent should have a clause here. "At least 2 urgent" becomes `atLeast: { '[data-priority="urgent"]': 2 }`. If a clause cannot be checked, either it is vague or it belongs in the `seen` verdict instead. A red run must say what to fix. That is what lets a repair session open on 2 shots instead of 11. ## Waiting The wait is: `domcontentloaded` → **a semantic anchor proving the intended screen is there** → no loading indicator visible → fonts loaded. `settle()` does all of it. - **Never a fixed delay.** If the screen is not ready, the anchor is the lever. - **Never `networkidle`.** It does not converge on an app holding a realtime connection open, which is every modern app. - **The anchor must not depend on language.** An id or a data attribute, never a translated label — otherwise one variant fails in silence. `settle()` refuses an anchor that matches on a translated string. Prefer semantic locators (`getByRole`, `getByLabel`, `[data-testid]`) over utility classes. `div.relative.cursor-pointer.rounded-xl` breaks at the first restyle, without a sound. ## Looking at the image **Assertions catch the empty. They do not catch the ugly.** Open the PNG. Every time. Ask: is this what `intent.md` describes? Is anything overlapping, cut off, misaligned, placeholder-grey, or obviously fake? Would this go on a landing page? Then append the verdict to `captures/shots//history.jsonl`. The CLI writes the run line with `"seen": null`; you replace that value: ```json {"date":"2026-08-07","commit":"a1b2c3d","variants":["fr-light","fr-dark"], "assertions":"pass","seen":"ok","note":"12 tickets, 4 full columns"} ``` `"seen"` is `"ok"`, or a sentence saying what is wrong. A run can pass every assertion and still be ugly: the two verdicts do not measure the same thing. If you did not open the image, leave it `null` and say so — a false `"ok"` is worse than an empty one. ## The four maintenance gestures | The user says | What happens | |---|---| | "show this too" | check with `world` whether the data exists, create it if not; then intent + program + assertions | | "drop this shot" | `npx autokap drop ` — folder, published images, manifest. The data stays; it is harmless. Ask before removing it | | "change this shot" | `intent.md` first, then the program and its assertions | | "put this data here instead" | `world` changes it, then **replay every shot whose `WORLD` names the entity that moved** | ## Refreshing ``` npx autokap status which shots drifted, and since which commit npx autokap run [name…] replay, assertions included ``` `status` crosses the commit on the last `history.jsonl` line with the `WATCH` globs. No guessing, a `git diff`. If nothing moved on a screen, say so and do not replay it — an identical screenshot does not deserve a run. When a run comes back red, read the message before touching anything: - *anchor not found* → the screen changed, or the route moved. Go and look at the code before editing the selector. - *0 rows, expected at least 8* → either the UI changed, or the world melted. Check the world first; a shot is not the place to fix data. - *cut off* → a layout regression. That is a real finding; tell the user rather than widening the viewport to hide it. ## Delivery Optional. The filename is the contract: `--.` — nothing else to declare, the consumer derives the URL. The manifest is generated from disk, so a missing variant renders a placeholder frame rather than a broken image, and deleting a file is enough to remove it. ``` npx autokap publish [name…] ``` **2× the display width, at minimum.** One file serves every screen, and `publish` reports the density it actually delivered. Under 2×, interface text is interpolated and it shows. ## Never - Never write to the database from a shot. Not a fixture, not a "quick insert", not through the app's API. Hand over to `world`. - Never loosen an assertion to make a run pass. That is how a bad image gets published with a green tick. - Never claim you looked at an image you did not open.