# Set up Showboat for this repository

You are a coding agent, and your user asked you to set up Showboat for the repository you are working in. Showboat is where a team reviews what it shipped: after each deploy, a step in the repository's pipeline captures the deployed feature with Playwright and pushes narrated screenshots to Showboat, and the team marks up what's off right on the screen.

Do the setup yourself, step by step. Pause only where the user has to act: signing in to Showboat in their browser, and approving changes to their pipeline if they want to see them first. Do only what these steps describe. Don't call other Showboat tools unless the user asks.

**Secrets:** this setup creates one secret, the ingest key. Never paste it into the chat, print it in logs, or commit it. Store it only as the secret `SHOWBOAT_INGEST_KEY`: a CI secret for the deploy pipeline, and a gitignored env file or the OS keychain locally.

## Steps

1. **Connect the Showboat MCP server.** Add `https://showboat.show/api/mcp` to your client as a remote MCP server (Streamable HTTP). If you can't add servers yourself, ask the user to add that URL in their client's MCP settings. Connecting opens Showboat's sign-in in the user's browser: wait for the user to finish it. There is no API key to paste. If Showboat's tools don't appear until your client starts a new session, tell the user to restart it and to ask you to continue this playbook from step 2.
2. **Check who you are.** Call `whoami` and tell the user which Account you are connected as. The Workspace is the one they picked when they signed in; if they belong to several, make sure it's the right one before you go on.
3. **Pick or create the Project.** Call `list_projects`. If one clearly belongs to this repository, use it; if several might, ask the user which. If none fits, call `create_project` with a name for this repository (see "Setting up: finding or creating the Project" in the contract below).
4. **Mint the ingest key.** Call `list_ingest_keys` for that Project first: if it already has a key for this repository, ask the user before minting another. Then call `mint_ingest_key` with a label naming this repository. Store the key as the secret `SHOWBOAT_INGEST_KEY` (as above). It is shown once; if it's lost, mint another and revoke the old one with `revoke_ingest_key` (its id is in `list_ingest_keys`).
5. **Choose how captures are made.** A Take shows the **deployed** feature. If the repository's end-to-end suite can run against the deployed app and screenshot the feature's key states, reuse those screenshots. Otherwise write a small Playwright script that opens the deployed feature and screenshots them. Either way, give each screen a stable `shotKey` and your own Narration, and push with the three steps in the contract below.
6. **Wire it into the deploy.** Add a step that runs after a successful deploy: capture, then push a Take for the feature with a stable `showKey`. Never make the deploy wait on it or fail because of it. In particular, a push answered `402` `workspace_lapsed` means the Workspace is Lapsed: print the reply's `message` and exit zero (the reference uploaders already do). Show the user the change before you commit it if they asked to review pipeline changes.
7. **Push the first Take now**, against the current deployment, so the user sees it work. The ingest answer carries the `showId`.
8. **Finish by telling the user** where the walkthrough is (`https://showboat.show/show/<showId>`) and that their team can open it there and mark up anything that's off. Showboat doesn't notify the team about a new walkthrough yet, so suggest they share the link where the team talks.

Everything about the push, the keys and the tools' answers is below.

---

You publish narrated walkthroughs of this product's deployed features to **Showboat** (https://showboat.show) — where the team shows off and discusses shipped features. This product has its own isolated Workspace there.

**How it works:** Showboat never runs a browser. After a deploy, *you* (in this repo) drive Playwright against the **deployed** app, screenshot the feature's key states, write a **Narration** (a short headline + a Markdown body) per shot, and push them over HTTP as a **Take** of a **Show**. Each shot also carries a stable **shotKey** so Showboat can follow that same screen across deploys. Re-capturing the same feature later appends a new Take to the same Show (history across deploys). Pushing needs only the ingest key — no Showboat login. Your team views and comments at the viewer URL after signing in (magic link).

**Your identity (already provisioned):**
- Project `<your project slug — from list_projects or create_project>` — viewer: https://showboat.show/project/<your project_id — from the list_projects tool, or create_project if none fits and the user asked you to set up capture>
- Store this as the secret `SHOWBOAT_INGEST_KEY` (CI secret / gitignored `.env` / keychain). **Never commit it.** The key alone identifies your Project — you never send a project name:
  `<mint one with the mint_ingest_key tool>`

**The contract** (depend on this, not on any Showboat code):
- Base URL `https://showboat.show`. Every request: `Authorization: Bearer $SHOWBOAT_INGEST_KEY`.
- A **Show** = one feature line, keyed by a `showKey` you choose: lowercase kebab (`^[a-z0-9][a-z0-9-]*$`), ≤64 chars, **stable across deploys** (reuse it to append Takes — e.g. `checkout-discount`).
- A **Shot** = one screen within a Take. Give each a `shotKey`: lowercase kebab, unique within the Take, and **stable across deploys** — reuse the same `shotKey` for the same screen on every re-capture so Showboat tracks that screen's versions and carries its Comments forward (e.g. `empty-cart`, `discount-applied`). A new `shotKey` reads as a brand-new screen.
- A **Narration** = `{ headline, body }`: a short headline plus a Markdown `body`. What renders: paragraphs, `-` lists, **bold**, *italic*, `code`, links, headings `#` to `###`, and tables (a header row, a `|---|` row, then body rows). Anything else, raw HTML included, shows as the text you typed.
- Three steps:
  1. **Presign** — `POST /api/ingest/presign` `{ showKey, shots:[{order, contentType, stillContentType?}] }` → `{ captureId, uploads:[{order, path, signedUrl, token, still?}] }`. `order` = unique int; `contentType` ∈ `image/png|image/jpeg|image/webp|video/mp4|video/webm`. Yes — a Shot may be a short video clip (≤50MB, storage-enforced): push it exactly like a still, with the video content type; Showboat detects the kind from what you uploaded and the viewer plays it in place. Prefer ~10s clips for one interaction, stills for everything else. **When you push a clip, also set `stillContentType` (an image type) on that shot** — presign then returns an extra `still: {path, signedUrl, token}`, and you upload a poster frame there (Playwright: `page.screenshot()` at the moment worth discussing). That still is the surface the team's **pencil marks and Pins land on** — marks can't be drawn on moving video, so a clip *without* a still can be watched and commented on, but not drawn on. Nothing about the still is repeated at ingest: Showboat picks it up from Storage.
  2. **Upload** — for each, `PUT <signedUrl>` with `content-type` + `x-upsert: true` and the raw bytes.
  3. **Ingest** — `POST /api/ingest` `{ showKey, captureId, title?, deploy:{sha, env, url, capturedAt(ISO), branch?, pr?}, complete?, removed?, citations?, shots:[{order, shotKey, narration:{headline, body}}] }` → `{ showId, takeId, takeNumber, citations? }`. Optional `title` is the Show's human name (set on first capture; absent → the showKey is humanized). View at `/show/<showId>`.
     - **`citations`** (optional) — cite what the feature was built from: `[{ kind, url, title? }]` with `kind` ∈ `"ticket"` (the tracker issue — Linear / GitHub issue / Jira), `"spec"` (the PRD / design doc), `"design-file"` (the Figma file or frame) — exactly those strings, never `"design"`. `https:` only, ≤20, no GitHub PR (that's `deploy.pr`) and no showboat.show URL. It lives on the **Show** and **replaces** the set each push: omit it to keep the current set, `[]` to clear. **Members-only** — never shown to Guests or Share-Link visitors. The reference uploaders take `citations` as repeatable `--cite <kind>=<url>[|<title>]` flags. The 201 then carries `citations: { count, hidden }` — `hidden` lists the items of your set a Member excluded, as server-side identity keys, not your URLs: an Exclusion stands across every push and re-declaring never brings it back. To know which URLs to drop (before a push, too), read the MCP `show_status` with a Member token: `hiddenCitations` lists every standing Exclusion by `url` — drop every one showing `stillDeclared: true` from your next push.
- Reuse the right key on re-capture: `GET /api/shows` → `{ projectId, shows:[{showId, showKey, takeCount, latestTakeAt}] }`.
- **Change status** — every Take is a changelog; Showboat derives each screen's status (new / changed / unchanged / removed) from history, so you declare only intent. To mark a screen **unchanged**, just **omit it** from `shots` — it carries forward from its last capture (don't re-upload an identical image). To **remove** a screen, list its `shotKey` in `removed:[]` (and omit it from `shots`). A Take is **complete** by default (every live screen accounted for — omissions are unchanged or removed); send `complete:false` only when this capture can't cover every screen, so omissions read as "awaiting return" instead.
- Idempotent on `captureId` (safe to retry ingest — don't re-presign). Errors: `401` bad key, `400` malformed / a shot wasn't uploaded, `409` concurrent-capture (retry), `402` `workspace_lapsed` — the Workspace is Lapsed (its Trial ended or payment stopped), so every push (Take, Sketch, Parity, Story; presign and ingest alike) is refused until an Owner adds a payment method under Plan & billing. Not retryable, and **not a failed deploy**: print the reply's `message` and exit zero.

**Also — pushing a Sketch (a *pre-feature* design exploration).** When you've generated competing HTML mockups for a design question (not captured a deployed feature), push them as a **Sketch**: several **Directions** (each a live sandboxed HTML **bundle** = an `entry` file + assets) over **Facets** (sub-decisions) the team votes on to pick a Winner per Facet. Same ingest key, same three steps — but you push *file bundles*, not images:
  1. **Presign** — `POST /api/sketches/presign` `{ sketchKey, files:[{directionKey, path, contentType}] }` → `{ revision, uploads:[{directionKey, path, storagePath, signedUrl, token}] }`. `path` is relative to the Direction's root (e.g. `index.html`); `contentType` ∈ html/css/js/json/png/jpeg/gif/svg/webp/woff/woff2/`video/mp4`/`video/webm`. `revision` is this push's fresh storage revision — every upload lands under it, so re-pushing the same paths never collides. Short video clips are welcome in a bundle (≤50MB per file, storage-enforced): reference them with `<video src="clip.mp4" muted loop playsinline autoplay>` so the gallery plays them without demanding a click — mp4/webm only, transcode anything else before pushing.
  2. **Upload** — `PUT <signedUrl>` each file with `content-type` + `x-upsert: true` (same as a Shot).
  3. **Ingest** — `POST /api/sketches/ingest` `{ sketchKey, revision, title?, showKey?, supersedesSketchKey?, facets:[{key, label, desc?}], directions:[{key, label, feel?, entry, files:[…], still?}] }` → `{ sketchId, created }`. Give each Direction a **`still`** — one of its own `files`, a rendered screenshot of the mockup (Playwright: open the entry HTML, `page.screenshot()`, and include the png in the bundle you presign). It is the surface the team's **pencil marks and Pins land on**: a Direction is a live, reflowing, revisable iframe, so a mark drawn straight on it could silently come to point at the wrong thing. A Direction without a still stays viewable, votable and commentable — just not drawable-on. **Always pass presign's `revision` back** — it tells ingest which upload the Directions reference. Here each Direction's `files` is an array of **plain path strings** (e.g. `["index.html","styles.css"]`) — NOT the `{directionKey, path, contentType}` objects presign takes. Needs ≥1 Facet + ≥1 Direction; each Direction's `entry` must be in its `files`. View at `/sketch/<sketchId>`.
  `sketchKey` is a stable kebab slug; re-pushing it while the Sketch is **Open** revises the Directions in place (a fresh presign → upload → ingest round per revision; each gets its own `revision`, and prior revisions stay served for whoever holds their URLs). Upload the FULL bundle for every Direction you list in a push; a Direction you omit keeps its previous files untouched. A new exploration replacing a **Decided** Sketch is a new `sketchKey` with `supersedesSketchKey` set; a push to a Decided Sketch is refused with `423` `sketch_decided` — not retryable — at presign (before any upload) or ingest. Optionally link the Show it explores via `showKey`.
  **The loop closes: read the decision yourself.** After the team votes and a Member declares Winners, the Showboat MCP `sketch_status` tool (with your `project_id` + `sketch_key`) returns per Facet the vote tallies and the declared Winner; `status: "decided"` means every Facet has one — build the winning mix, no human needs to relay the decision. `list_sketches` enumerates a Project's Sketches. The team's feedback comes back the same way: `pull_open_comments` with your `sketch_key` returns the open Comments, each on the Direction it is about (or the Sketch-level thread) — while the Sketch is **Open**, revise those Directions and re-push the same `sketchKey`; once it is **Decided** (`sketch_status`), a re-push would overwrite the Directions the Winners were declared on, so the follow-up is a new `sketchKey` with `supersedesSketchKey`.
  **Bundles run under a strict CSP — self-contained or it silently breaks.** A served bundle executes with `default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; media-src 'self' data:`. Concretely: **(0) video/audio from your own bundle plays** — `media-src 'self'` covers the `<video src="clip.mp4">` above; **(1) stylesheets and scripts from your OWN bundle work** — `<link rel="stylesheet" href="styles.css">` and `<script src="app.js">` with relative paths resolve inside the bundle (`'self'` is the user-content origin), and inline `<style>`/`<script>` work too; **(2) real fonts work** — ship `.woff2` files in the bundle and `@font-face` them with relative `url()`s (or `data:` URIs); **(3) NOTHING external loads** — no Google-Fonts `<link>`, no CDN scripts, no external images; every asset must be a file in the bundle or a `data:` URI; **(4) no network at all** — `connect-src` is absent, so `fetch`/XHR go nowhere. A Direction is a self-contained mockup, not an app.

**Also — pushing a Parity (an *aligned review*).** When you've measured built screens against a **reference** (a port, a redesign, N tenants of one flow), push the comparison as a **Parity**: roled **Sides** (exactly one `reference` + ≥1 `subject`s, fixed at first push), **Rows** (each one aligned state, e.g. "Step 2 · Add products"), one **Pane** per (Row, Side) — a screenshot or a live HTML bundle (bundle Panes run under the same strict CSP as Sketch Directions — everything from the bundle itself, nothing external; **no video** — unlike a Direction, a Pane must hold still: a Row asserts its Sides show the same state and a Finding anchors to a static surface) — and your measured divergences as **Findings**. Same ingest key, same three steps:
  1. **Presign** — `POST /api/parities/presign` `{ parityKey, panes:[{rowKey, sideKey, kind:"image", contentType} | {rowKey, sideKey, kind:"bundle", files:[{path, contentType}]}] }` → `{ captureId, uploads:[{rowKey, sideKey, kind, path?, storagePath, signedUrl, token}] }` — one upload per image Pane, one per bundle file.
  2. **Upload** — `PUT <signedUrl>` each blob with `content-type` + `x-upsert: true`.
  3. **Ingest** — `POST /api/parities/ingest` `{ parityKey, title?, showKey?, verdict?, captureId, sides:[{key, label, role}], rows:[{key, label?, panes:[{sideKey, kind, entry?, files?, state?}]}], findings?:[{key, rowKey, sideKey, category, severity, summary, measured?}] }` → `{ parityId, created, revision, warnings? }`. View at `/parity/<parityId>`.
  **Always declare each Pane's `state`** (free text, e.g. `route=/add-items · step=2`) — a Row is aligned iff its Panes' states match, and that is the whole guard against comparing two different screens. A mismatch succeeds with an `alignment_mismatch` warning and a viewer badge (never silently trusted); leaving a Pane's state out succeeds with an `alignment_undeclared` warning naming the silent Sides — no badge, because the reviewer shouldn't pay for your omission. **Read `warnings[]` on every push and fix what it names.** Give each **Finding** a stable key: re-pushing the same `parityKey` appends a new revision, and each push's `findings[]` fully replaces the report — omit a fixed one and the open-Finding count (the Parity's health) drops. Optionally link the Show this review is OF via `showKey` (same Project, may precede its capture) — it appears on that Show's home page.

**Also — proposing a Story (a *client-facing* changelog entry).** After a deploy, alongside your Take, you can propose a **Story** — a short, client-worded account of what shipped, for the team's client-facing **Newsreel**. Unlike the others there are **no blobs and no presign** — it is one POST of authored text plus optional internal provenance:
  - **Propose** — `POST /api/ingest/story` `{ proposeKey, copy:{headline, body}, provenance?:[{projectId, showKey, sha, env, pr?}] }` → `{ storyId, created }`. `proposeKey` is a stable idempotency key (re-sending it is a no-op, never a duplicate; same rules as `showKey` — lowercase kebab `^[a-z0-9][a-z0-9-]*$`, ≤ 64 chars — it becomes the Story's identity). Provenance `showKey`s follow the same rule. `copy` is the **client's** wording — write it for the client, not the team; never paste your internal Narration. `provenance` is *internal* (which Show + deploy this recounts): set `projectId` to **your own** Project (the one your ingest key authorizes — a foreign projectId is rejected), `sha`/`env`/`pr` from the deploy you just captured. It is shown to the team while they curate and **never** to the client. Omit `provenance` for a from-scratch announcement.
  - You **propose**; you never publish. A proposed Story lands in the Workspace's **Proposed Story inbox**, unrouted — a Member (or their Claude over MCP) routes it into a Newsreel, edits the wording, and publishes it to the client. So propose freely; nothing you push reaches a client until a human releases it.

**When:** after deploying a feature, or when asked to "capture this for Showboat" → push a **Take** (and optionally propose a **Story** for the client Newsreel). When you've generated design mockups for a decision → push a **Sketch**. When you've measured screens against a reference → push a **Parity**. Pick a stable `showKey`/`sketchKey`/`parityKey`/`proposeKey`, produce the states/mockups/measurements/copy, narrate/label in your own words, push.

**Close the loop (recommended) — the Showboat MCP.** Connect it once, as a remote MCP server at `https://showboat.show/api/mcp`, to pull the team's feedback and manage keys without leaving your editor. In Claude Code:

```
claude mcp add --transport http showboat https://showboat.show/api/mcp
```

It opens a browser to sign in (magic link) and pick this Workspace, then exposes tools: `list_projects`, `create_project` (only when no Project fits and you were asked to set up capture), `list_shows`, `show_status`, `pull_open_comments`, and `mint_ingest_key` / `list_ingest_keys` / `revoke_ingest_key`. The comment→fix→re-capture loop: `pull_open_comments` → apply the fixes → push a new Take. `show_status` reports `exists: false` for a `showKey` nothing has been pushed under (safe as a pre-flight before your first push — but if you expected the Show to exist, check the key); `pull_open_comments` on a nonexistent Show, Parity or Sketch fails with not-found rather than reading as "all clear". (You can also `mint_ingest_key` here instead of pasting the key above.)

**Reference uploader** (Node 18+, adapt the Playwright steps to the feature):

```js
import { chromium } from "playwright";

const BASE = "https://showboat.show";
const KEY = process.env.SHOWBOAT_INGEST_KEY;
const auth = { Authorization: `Bearer ${KEY}` };

const showKey = "my-feature";                       // stable per feature line
const deployUrl = "https://your-app.example.com";   // the deployed URL you captured
const deploy = {
  sha: process.env.GIT_SHA ?? "dev",
  env: "production",
  url: deployUrl,
  capturedAt: new Date().toISOString(),
  branch: process.env.GIT_BRANCH,
};

// Drive the deployed UI; screenshot + narrate each key state in your own words.
const browser = await chromium.launch();
const page = await browser.newPage();
const shots = [];
await page.goto(deployUrl);
// …navigate/act for step 1…
shots.push({ order: 1, shotKey: "first-screen", png: await page.screenshot({ type: "png" }), narration: { headline: "First screen", body: "Describe the first state in Markdown." } });
// …navigate/act for step 2…
shots.push({ order: 2, shotKey: "next-screen", png: await page.screenshot({ type: "png" }), narration: { headline: "Next screen", body: "Describe the next state." } });
await browser.close();

// 1. presign
const presign = await fetch(`${BASE}/api/ingest/presign`, {
  method: "POST", headers: { ...auth, "content-type": "application/json" },
  body: JSON.stringify({ showKey, shots: shots.map((s) => ({ order: s.order, contentType: "image/png" })) }),
}).then((r) => r.json());
// A Lapsed Workspace's push is refused (402 workspace_lapsed). Not a failed deploy.
if (presign.error === "workspace_lapsed") {
  console.warn("Showboat did not take this capture:", presign.message);
  process.exit(0);
}

// 2. upload each shot to its signed URL
for (const up of presign.uploads) {
  const shot = shots.find((s) => s.order === up.order);
  const res = await fetch(up.signedUrl, { method: "PUT", headers: { "content-type": "image/png", "x-upsert": "true" }, body: shot.png });
  if (!res.ok) throw new Error(`upload ${up.order} failed: ${res.status}`);
}

// 3. ingest (metadata only)
const out = await fetch(`${BASE}/api/ingest`, {
  method: "POST", headers: { ...auth, "content-type": "application/json" },
  body: JSON.stringify({ showKey, captureId: presign.captureId, deploy, shots: shots.map((s) => ({ order: s.order, shotKey: s.shotKey, narration: s.narration })) }),
}).then((r) => r.json());
// The Trial can end between presign and ingest: the same refusal, the same answer.
if (out.error === "workspace_lapsed") {
  console.warn("Showboat did not take this capture:", out.message);
  process.exit(0);
}

console.log("Take pushed →", `${BASE}/show/${out.showId}`, out);
```


## Setting up: finding or creating the Project
- `list_projects` first; if none fits and the user asked you to set up capture, `create_project` makes one. It is safe to retry: the same slug returns the existing Project with `created: false`, never a second one, and it never brings back an archived Project.
- A Project made with `create_project` starts with no Team access. Workspace Owners (and any Team that sees every Project) see it, so an Owner's connection can `mint_ingest_key` for it straight away; if your Account is a Member but not an Owner, ask an Owner to grant a Team access before minting its key.

## Reading the tools' answers

### Whose change was it? (show_status Citations, whoami)
- `show_status` names who is behind each Citation: `from.agent` (your capture push declares it) and `from.member` (a Member added it; an `add_citation` call counts, since your token acts as its Member). `addedBy` is the Member whose add is on record: the last add that changed it (a re-add with the same fields changes nothing), or null when only the capture push cites it.
- Because each Citation records who cites it, `show_status` shows before any `remove_citation` whether the capture push or a Member (and which one) put it there; removing a Member's Citation removes a colleague's work.
- `hiddenCitations` lists the Citations a Member turned off, with `excludedBy`. An exclusion stands across every push: declaring the Citation again never brings it back, so when `stillDeclared` is true, drop it from your next push's `citations`. Only a Member re-adding it with `add_citation` lifts it.
- To tell your own changes from a colleague's, compare `addedBy.id` / `excludedBy.id` with `whoami.id`: equal means your Account did it (this connection, another connection of yours, or your human in the Show page; the ids can't tell which); different means another Member. Compare ids, not emails: the email is as granted to each connection. `whoami` names no other Account, and any scope may call it.

### Comment authors (pull_open_comments)
- A Member's token gets each Comment author's `id` and `email`. A Guest's token gets the `email` only, except on Comments its own Account posted, which keep the `id`: compare it with `whoami.id` to find your own.

### Parity Findings marked disputed
- In `pull_open_comments` with `parity_key`, a Finding with `disputed: true` is one the team says is not a real divergence. Re-check it before changing anything for it; don't "fix" it blind. A Finding you stop reporting in your next Parity push resolves itself.

### Sketch decisions
- When `sketch_status` reports status "decided", every Facet has a declared Winner: build the winning mix of Directions, with no human relay needed.

### Sketch Comments
- `pull_open_comments` with `sketch_key` returns only open Comments. `directionKey` is the Direction one is about, or `null` for the Sketch-level thread; `facetKey` narrows it to a Facet when the author tagged one.
- `repushedSinceComment` is `true` when you re-pushed that Direction after the Comment was made (a fix may already be live: check before redoing it), `false` when the Comment was made on the current revision, and `null` when it can't tell — a Comment on the Sketch-level thread, one posted from a page that was loaded before your latest push, or an older one from before Comments recorded the revision they were made on. `null` is "unknown", never "not re-pushed": check the Direction yourself.
