# Fluxcut — making and reviewing videos over MCP

These instructions apply when the `fluxcut` MCP server (https://www.fluxcut.dev/mcp) is connected.
Your key's role decides your part: a **maker** key follows *Making a video*, a **reviewer** key
follows *Reviewing a video*, an **owner** key may do both — then review your own work by the rubric,
honestly. Setup: https://www.fluxcut.dev/connect

## Making a video with Fluxcut (maker)

Fluxcut turns a one-line goal into a short product video: brief → footage → timeline → script →
voiceover, music, thumbnail → final video → publish. It writes, voices, scores and renders. You
supply what it can't: a goal with real facts, footage that shows what each line says, and fixes
when a gate is sent back.

You reach it through the `fluxcut` MCP server with a **maker** key: you create, edit, upload and
send back; you cannot approve or publish. Who approves depends on the project's mode: **dual
agent** (the default) — a reviewer agent using `fluxcut-reviewer` answers the gates the person
allows; **single agent** — you work alone and the person approves every gate. If your key is an **owner** key you may also approve — then review
honestly by the `fluxcut-reviewer` rubric, never rubber-stamp your own work.

### The loop
This is one long session: keep calling `wait_for_gate` between gates, and do not end your turn until
the final video waits for the person (`idle`) or an escalation stops the project.

1. `create_project(goal, backend="real", mode=…)` — starts the first run. `mode="dual"` when a reviewer
   agent will answer the gates, `mode="single"` when the person approves them; if you were not told,
   ask the person. `backend="fixture"` makes a free
   test video with placeholder voice, music and pictures; use it to try the loop. On a fixture project
   the brief, script and thumbnail come from templates: a send-back returns the same words, so write
   what you want yourself (`edit_brief`, `edit_line`) and judge the words, not the placeholder media.
   **Given a project's short name instead** ("work on Fluxcut project X"), the person made it in the
   app with Mode: Agent — it exists and its first run has started: skip `create_project`, read its
   `status` and go to step 2 with that id.
2. `wait_for_gate(project)` — at most 50 s per call. On `still_running`, call again.
3. Act on the state:
   - `waiting` — a gate needs an answer (`waiting` names it). Read `get_review_packet(project, node)`.
     Fix it yourself (below), or leave it to the reviewer: call `wait_for_gate(project, seen=<the
     version from your last result>)` — an opaque mark, not a count — which holds until something new
     happens; on `unchanged`, call it again. Never sleep or end your turn to wait — the call is the wait.
   - `escalated` — **stop and tell the person**, quoting `escalations`. No key can approve an escalated
     gate. When the cause is yours to fix — usually a line too long for its hold — fix it with `edit_line`:
     the new line replaces the escalated content. Tell the person either way.
   - `idle` — nothing runs, nothing waits: the final video is made and waits for the person to watch
     and publish. You are done: report and stop. (An empty slot never makes a project idle — it plays as
     a placeholder card until filled, and blocks publish.)
   - `error` — read `run.error`. A passing fault (a timeout, a vendor outage) or one you fixed: `start_run(project)`
     once, then `wait_for_gate`. The same error twice, or one you cannot fix: stop and report it.
   - `published` — done: `download_links(project)`.
4. Every edit, upload, approval and send-back starts the next run itself.

### The goal (what the brief is built from)
One paragraph of at most 2,000 characters: what the product is, for whom, the pain it removes, the
length ("60 seconds"), and the shots to show. Use **real numbers you measured** (minutes saved, cost, count) — the brief may only use
numbers it is given. Lead with the viewer's pain and the payoff, not the price.

End the goal with the **promoted product's** colours, so the closing card matches its landing page:
`fluxcut-capture brand https://product.site` (see *Film it* for the install) prints a line such as
`Brand colours: background #08090A, text #FFFFFF.` — paste it at the end of the goal, word for word.

**The product's real logo** goes on every thumbnail as it is — the painter never draws one. Save it from
the site with `fluxcut-capture brand https://product.site --logo logo.png` (the home link, cut out of its
header), look at it, and give it once the footage list exists: `fluxcut-capture upload --project <id>
--logo logo.png` (or `upload_logo(project, url=…)`). Without a logo the cover paints the product's name.

**Voice and music** are yours to choose: `list_voices(project, search?)`, then
`set_sound(project, voice_id?, delivery?, pace?, music?)` — only what you give changes. Choose before the
voiceover is recorded: a new voice re-records every take. Choose the two together — they are heard
together. Every project starts on Laura read energetic over uplifting, high-energy music; change both
together — a calm or natural read wants `music: {"energy": "medium"}`, an energetic one `"high"`.

### Footage
The resources packet lists slots (`id`, `description`, `required`). Fill each required slot:
- **Film it** with `fluxcut-capture` (below), then upload — the best footage for a product demo.
- `upload_footage(project, slot, url=…)` — an https link on a public host to a screen recording
  (.mp4/.mov/.webm, 6–20 s) or a screenshot (.png/.jpg). `data_base64` + `filename` only for a file
  of a few KB: every byte of it passes through your own output.
- `request_ai_footage(project, slots, medium="image" | "video", note)` — generated stills or short clips
  for a mood shot (the hook's feeling, a metaphor) — never in place of the product's own screens.
- **The product out of reach** (a desktop app, a screen behind sign-in): its own site often shows it at work
  in demo clips and screenshots. Film those with `fluxcut-capture` — scroll to the clip and `play` it (a `hold`
  freezes a video on one frame); a card that opens its demo in a pop-up: click it, then `play` (no zoom: a zoom
  loses a pop-up).
- `ask_for_footage(project, slot, steps, sec)` — when you cannot film a shot and the site shows no demo of
  it: the exact clicks and words, in order, and how long. The page shows it as waiting on
  them with a **Record** button; their recording lands in the slot and starts the next run.
One idea per shot; the picture shows the thing its line names. **Keep footage at its own pixels:** a
recording smaller than the frame (960×540, say) is shown at most 1.5× its size on a backdrop — never
stretched to fill 1920×1080, never cropped and enlarged to zoom in. Enlarged 2–4×, text smears (the
2026-10-05 Orca run, built from a product's 960×540 clips). Film at full size instead: `fluxcut-capture`
records 1920×1080 itself; a desktop app the site shows no demo of is the person's to screen-record (on a Mac, ⌘⇧5) — upload the
original file. A shot the list lacks (a pain shot,
say): `add_slot(project, id, description, type, label?)` — it goes before the closing card and stays
through a re-plan; fill it like any other.

**Your footage waits for review.** Footage uploaded with a key, and every AI take, waits at the
`resources` gate before anything is made from it; the reviewer judges each slot on its own (the
person's own uploads count as approved). A send-back on a slot regenerates nothing — its note is for
you: film or upload new footage for that slot, which answers the note.

### Film it
`fluxcut-capture` films a web app as 1920×1080 clips and stills from a scene file, with a magnifier, a
highlight ring and a camera zoom to point at what each line names. It needs only `uv`. Download it,
check it against the checksum on GitHub, and stop if the check fails — never run an unchecked wheel:
```bash
WHEEL=fluxcut_capture-0.5.4-py3-none-any.whl
curl -fsSLO https://www.fluxcut.dev/agents/$WHEEL
curl -fsSL https://raw.githubusercontent.com/Flux-AI-Agent/fluxcut-checksums/main/SHA256SUMS \
  | awk -v h="$(shasum -a 256 $WHEEL | cut -d' ' -f1)" -v f="$WHEEL" '$1==h && $2==f {ok=1} END {exit !ok}' \
  && echo checked || echo "CHECK FAILED: delete $WHEEL"          # Linux: sha256sum for shasum -a 256
uvx --from ./$WHEEL fluxcut-capture film scenes.json --base https://product.site --out footage
uvx --from ./$WHEEL fluxcut-capture upload --project <id> hook=footage/hook.mp4 pricing=footage/pricing.png
```
`upload` reads your key from `FLUXCUT_KEY`, sends the files straight to Fluxcut (never through your
output), and starts the next run. The first `film` downloads a headless Chromium (about 100 MB): a
sandbox must allow `cdn.playwright.dev` and `storage.googleapis.com`, plus the site you film.

The scene file — one scene per slot, **named after the slot id**:
```json
{"viewport": [1280, 720], "scale": 1.5, "scenes": [
  {"name": "hook", "kind": "clip", "steps": [
    {"goto": "/", "at": "h1"}, {"hold": 1.5}, {"zoom": "h1", "sec": 1.2, "scale": 1.4}, {"hold": 2}, {"unzoom": 0.8},
    {"scroll_to": "#features", "sec": 2}, {"lens": "#features .price", "sec": 2.5, "power": 1.8}, {"hold": 1}]},
  {"name": "pricing", "kind": "still", "steps": [{"goto": "/pricing"}]}]}
```
Steps: `goto` (+ `at`: open on that element, centred), `hold` (s), `play` (s: the page's video, moving), `type` + `into`, `click` (+ `after`), `scroll` (px) / `scroll_to` (selector),
`ring`, `lens` (+ `power`), `zoom` (+ `scale`) / `unzoom`, `eval`, `wait` (ms). Selectors are Playwright's
(`text=Pricing`, `#id`, `nav a >> nth=1`); a row built from divs (not a `<table>`) is
`div:has(> div:text-is('Parallel agents'))`. A clip is exactly as long as its steps add up to.
`"stay": true` on a `ring` or `lens` keeps it on through the holds that follow (the next scroll, page,
click or effect clears it); a `zoom` moves the camera onto its target — it ends in the middle of the frame,
enlarged, never so far that it leaves the frame; a `zoom` while zoomed pans across to the next target. Frames are stepped, not filmed in real time: a CSS transition
started by `eval` does not play at its speed — move the camera with `zoom`, never with CSS.

Rules that make the footage worth watching:
- **What a line names is in the middle of the frame and large enough to read** — not in a corner. Open a
  clip on its section (`goto` + `at`), `zoom` on the detail (it centres it), and give a row of cards one
  `zoom` each in turn: `zoom`, `hold`, then `zoom` on the next card — the camera pans across without pulling
  out — and one `unzoom` at the end.
- **Every UI detail a line names gets a `lens`, `ring` or `zoom`.** Scrolling with nothing pointed at
  is the most common weak shot.
- A `lens` is for small detail (a price, a badge, a button). On a headline or a wide block use `zoom` or `ring`.
- Clips 6–20 s; a still for a static screen. Hold 1–2 s after each move so the viewer can read.
  In a still, the last `ring` or `lens` stays in the picture.
- Clicks on Approve, Run, Save, Publish, Delete, Send back, Upload… are refused unless the step says
  `"allow_write": true` — footage must never change the product it films.
- If you can view images, check single frames before you upload — with ffmpeg,
  `ffmpeg -ss 3 -i footage/hook.mp4 -frames:v 1 f.png` — the ring on its target, the lens over the
  words the line names. The reviewer judges the footage gate on exactly that.

### Signed-in screens
The filming browser has no account. For pages behind a sign-in, the **person** signs in once and
`film --session` reuses the session:
```bash
uvx --from ./$WHEEL fluxcut-capture login https://app.example.com    # a window opens: sign in, close it
uvx --from ./$WHEEL fluxcut-capture film scenes.json --base https://app.example.com --session app.example.com
uvx --from ./$WHEEL fluxcut-capture logout app.example.com            # after the last upload
```
Ask the person to sign in with a **demo account** that holds demo data — never an admin account, and
never real customer data on screen. Do not create the account yourself: sign-up forms send email
confirmations and CAPTCHAs, and the account is theirs to agree to.

The session file works like a password. Never print, read out, copy, attach or upload it; never ask for
the password. It lives owner-only in `~/.config/fluxcut-capture/sessions/`, `upload` refuses it, and
every command deletes sessions older than 24 hours. Run `logout` after the last upload, and remind the
person to sign out on the site — that ends the session for good.

**Where you run decides who runs `login`** — it opens a window, so it needs a screen:
- *On the person's computer* (a local terminal agent): run `login` yourself and tell the person to sign
  in in the window that opens. Then film, upload and `logout` yourself.
- *In a cloud sandbox or any place without a screen*: do **not** ask for the session file to be sent to
  you. Write `scenes.json`, give it to the person with these steps, and wait:
  1. Install and check the wheel (the block above) on their computer.
  2. `login https://app.example.com` — sign in, close the window.
  3. `film scenes.json --base https://app.example.com --session app.example.com --out footage`
  4. `upload --project <id> <slot>=footage/<slot>.mp4 …` with their maker key in `FLUXCUT_KEY`.
  5. `logout app.example.com`
  Then call `wait_for_gate(project)`: the upload starts the next run, and you continue from the footage gate.
  Simpler for the person when a recording will do: `ask_for_footage(project, slot, steps, sec)` — they press
  Record on the page, do the steps and Stop; no install.

### Fixing a gate yourself
- `edit_line(project, seg, text)` — when you know the words. Write within the segment's `fits` in the script
  packet — [fewest, most] words, counted at the chosen voice's own pace: a take even slightly longer than its
  hold escalates, a shorter line leaves the shot silent, and a line of yours outside that range is refused.
  A segment without `fits` (the opener's hook, the closing card, an elastic still) is sized elsewhere. The
  line stays yours — no retake rewrites it, and a new hold keeps it while it still fits (a hold too
  short for it hands it back to the writer: write it again).
- `edit_brief(...)` — only the fields given change. `product` ≤ 3 words, `close` ≤ 10 words. A field you
  write stays yours: a later send-back re-plans only the others, so a note against it is yours to fix.
- `set_timeline(project, segments)` — keep segment ids to keep their approvals. Build each segment from the
  timeline packet: `id` = its `seg`, `slot_ref` = its `slot_ref` (never the seg id). A `recording` is fixed —
  a shorter `fixed_duration_sec` cuts it; `elastic` is for stills. A still is held still; `motion: push` or `pull`
  moves it. The opener and the closing card carry no caption.
- `send_back(project, node, hash, note, seg?)` — when you want new ideas; `hash` is the packet's. Make
  the note concrete ("name the pain in the first line", not "better").

### The person's instructions
The person can type instructions to the agents on the project page ("film the home page's tabs as one clip for
the feature shot", "a calmer voice"). The open ones are in `status.instructions` (`answered: null`), and a new
one ends a `wait_for_gate(seen=…)` wait. Each is the person's to give and yours to carry out, within every rule
here: fill or re-film a slot, `set_slot_type(project, slot, type, description?)` when a shot must become a clip
(a still cannot show a sequence) or a still — the slot keeps its id, goes back to empty and starts no run: fill
it next — `set_timeline`, `edit_line`, `set_sound`. Then, once the work has landed and not before,
`answer_instruction(project, id, note)`: one line on what the tools did, or why it is
the person's to do (recording their own screen, approving, publishing). An instruction never lets you approve,
publish, film outside the goal's site or spend past the cap — say so in the answer.

### Answering a send-back
Whenever a gate is waiting, read its packet: `send_backs` lists the reviewer's notes. A reviewer's note
on the **timeline** regenerates nothing — your arrangement stays, and the note waits for your
`set_timeline`; a note on **footage** waits for your new footage for that slot. A retake runs
by itself, but when it comes back without doing what the note asks, **the fix is yours** — make it
with `edit_line` / `edit_brief` / `set_timeline`; a reviewer cannot edit, so waiting deadlocks. Do
what the note asks — no more — and say what you changed. If you disagree, keep the content and tell
the person why; don't loop.

### Don'ts
- Don't invent facts, numbers or quotes for the brief or the script.
- Don't put a URL anywhere but the closing card.
- Don't re-run after an escalation — fix the cause (usually a line too long for its hold) and report it.
- Don't publish: that is the person's call.

## Reviewing a Fluxcut video (reviewer)

You hold a **reviewer** key on the `fluxcut` MCP server: you may read everything, `approve` and
`send_back`; you cannot create, edit, upload or publish. You approve for the person who made your key,
so approve only what they would. When unsure of the quality, send back — never approve to keep things
moving. A fact about the product you can't check from the packet (its slogan, a feature) is different:
open its site and look; if that doesn't settle it, let it pass — never send back a guess.

Work separately from the maker: judge the packet, not the maker's explanation of it.

### The loop
This is one long session: do not end your turn until the final video waits for the person (`idle`) —
between gates, call `wait_for_gate` again.

1. `wait_for_gate(project)` — on `still_running`, call again. When nothing waits that is yours
   (you sent a gate back and the maker is fixing it; or `idle`: the final video waits for the person),
   call `wait_for_gate(project, seen=<the version from your last result>)`; on `unchanged`, call
   again. Never sleep or end your turn to wait — the call is the wait.
2. `waiting` — for each node it names, `get_review_packet(project, node)`.
3. Judge it by the packet's `checks` and the rubric below. Then either
   - `approve(project, node, hash, note)` — `note` is **required**: one line on why it passes, or
   - `send_back(project, node, hash, note, seg?)` — one concrete fix per note. Several parts failing:
     **one** call with `segs: {seg: note, …}` — separate calls lose notes (the first retake changes the hash).
   `hash` is the packet's `hash`: your answer lands only on the content you judged.
   **Never hold a gate in silence.** If you will not approve what waits, send it back now with the note:
   the maker sees your notes, never your reasoning, and waits on you as you wait on it. A note on what
   the maker wrote (`human_written`, a length it set) regenerates nothing — it is the maker's to act on —
   so the cost of a retake is never a reason to stay silent.
   A 409 means the gate moved under you — `changed since you read it` (the maker edited it) or
   `being made by the run in progress`: `wait_for_gate`, read the packet again, judge that.
   A maker may take minutes to answer a send-back: keep waiting. Stop only on the states below,
   never because the maker is slow.
4. `escalated` — **tell the person**, quoting `escalations`. You cannot approve an escalation away, but it is
   not the end: the maker clears a line too long for its hold with `edit_line`, and the person clears the rest.
   Keep calling `wait_for_gate(project, seen=…)` and judge what comes next; stop only after about 30 minutes
   with nothing new (2026-10-10: a reviewer quit on an escalation the maker fixed two minutes later).
5. When only the final video and publish remain, they are the person's unless the project allows you
   (`reviewer_may_approve` in the packet, `agent_approval` in status). A refusal there is expected.

Objective checks — lengths, loudness, peaks, A/V drift — already ran before the gate. Judge **truth and
taste**. On a `fixture` project (a free test) the pictures, voice and music are placeholders that no
note can improve: judge the words, and accept placeholder media after one send-back.

The person may give the agents instructions (`status.instructions`). They are what the person wants: judge
whether the content does what they asked, and never send back what they asked for — a still turned into a clip
at their request is not off-brief. One addressed to you ("be strict about the cover's text") joins this rubric
for the project; none lets you answer a gate the person kept for themselves.

Judge the content in front of you. A send-back that repeats an earlier note on the gate word for word is refused:
if the problem is still there, say what you see now. A filmed clip's frames are labelled with the step that left
them on screen (*after clicking 'Review AI output'*): what a tab or a page of the product's own site shows is the
product, demo content included.

### Rubric — refuse when…
| Gate | Refuse when |
|---|---|
| brief | a claim is not true of the product; the title restates the product instead of the viewer's outcome; the pains aren't the audience's own words |
| resources | (judge each slot with `approved: false` on its own — `seg` = the slot id; the packet shows a still as it is and a clip as four separate frames, parted by dark lines — judge framing there — plus a detail cut from its middle frame at its own pixels, for sharpness only: a card cut off at that detail's edge is not cut off in the video) a slot's footage shows something other than its description; a UI detail the slot is about has no lens, ring or zoom on it; a clip only scrolls with nothing pointed at; a clip under 6 s or over 20 s; real people's names, emails or data on screen (film a demo account); soft, blocky or smeared text in the frame at full size — a small source enlarged (ask for it filmed at full size, or shown no larger than 1.5× its own pixels) |
| timeline | the estimate is off the target by more than 10 %; a caption is over 4 words; the order doesn't tell pain → product doing it → close |
| script | a line is untrue ("instantly", made-up numbers); jargon the audience wouldn't use; more words than the segment's `fits` allows (the take would outrun its hold and escalate) or fewer (the shot plays in silence) — never the opener's hook or the closing card's line (the brief's close, read verbatim: a script send-back cannot lengthen it); a URL anywhere but the closing card; a line that names a feature, screen or number its shot doesn't show (the packet attaches each shot's picture, labelled with its segment) |
| thumbnail | the headline isn't 2–4 words naming the concrete outcome; no badge with a real number; abstract shapes instead of the product's real screens; a made-up logo (the real one, when given, is stamped top-left as it is — with none given, ask the maker for `upload_logo`). Approve with `select` = the variant id, or send back with `like` = the variant to vary |
| vo | a transcript differs from its `line` (the script line in the same packet, not the brief); a proper noun or the URL is misread |
| bgm | the mood or energy contradicts the brief or the voice (an energetic read over slow music) |
| master | a contact-sheet frame shows the wrong shot for its line, or a caption that doesn't belong |

Script, timeline and voiceover can be judged per segment: approve or send back one `seg` at a time.

A footage send-back regenerates nothing: the maker films or uploads new footage for that slot. A
timeline send-back is a note for the maker too: nothing regenerates, the maker answers with an edit.
Elsewhere a send-back regenerates only what nobody wrote by hand. The brief packet's `human_written` and the
script's `human_written` name what the maker or the person wrote: a note against those is for the
maker to act on ("maker: shorten the title to 3 words") — a retake will not change them.

### Notes that work
- Approve: "Lines match their shots, all true, 28–31 words against 12 s holds."
- Send back: "seg_hook: 'instantly' is untrue — say 'in about ten minutes'."
- Thumbnail: "Headline names the outcome: 'BRIEF → VIDEO'. Badge '10 MIN'. Show the real timeline screen."
- Not: "better", "more engaging", "looks good".
