You are an agent building a short 3D phone hero-shot script for a human. This document is your wire contract and working loop. Read it, then read the authoring vocabulary, then work the loop below.

# Agent kit: compile and preview a hero-shot script

This is the raw-HTTP contract for building a scene-script into real motion. It is a SIBLING of the authoring guidelines: this doc tells you HOW TO CALL the service, the guidelines doc tells you WHAT WORDS to write in a script. Fetch both, in this order:
1. This document (you are reading it).
2. `GET /api/choreo/guidelines` (public, no auth): the full authoring vocabulary (poses, accents, camera regions, zoom words, easings, camera moves, title and background catalogs) plus how to shape a script. You author against that; you submit against this.

IMPORTANT: the guidelines doc is also shown inside the product's paste dialog, so it speaks as if it were your operating instructions and ends by telling you to interview a user before writing any JSON (its "Start now" section). THOSE INSTRUCTIONS ARE NOT FOR YOU. You already have your brief. Use the guidelines purely as the vocabulary and script-shape reference; THIS document is your operating manual, and your loop is the one below.

## The loop

1. Fetch the guidelines and author a scene-script (one JSON object).
2. Get a token (see Getting a token below).
3. Compile: `POST /api/choreo/compile` with your script. Read the warnings and the film text. Iterate here freely, compile is cheap.
4. Preview: `POST /api/choreo/previz` with the same script. Read the filmstrip pixels against the per-frame numbers. Call this sparingly (see the cost model).
5. Revise the script and go back to step 3 until the evidence matches your intent.
6. Hand off: `POST /api/handoff` with the finished script and give the human the `editorUrl` it returns. That URL opens the editor straight into the same project your compile and preview saw (see Handoff below).

## Cheat sheet

- COMPILE is cheap, pure CPU, and uncapped beyond the rate limit. It is your main iteration surface: run it as often as you need. It returns the coded warnings and the recorder that no other endpoint gives you.
- PREVIZ is a real render (about 1 to 2 seconds). It is GLOBALLY capped at 2 in flight across all callers, with a per-identity dedupe of 1 (one at a time). Call it strictly one at a time, and only when you actually need eyes on the pixels, not on every compile.
- RATE BUDGET: 30 requests per 60 seconds, counted per identity AND per IP. Both compile and previz spend from it. Space your calls.
- TRUST RULE: where the numbers and the pixels disagree, the pixels are right. Read the filmstrip. The numbers orient you; the render is the evidence.
- HANDOFF is the finish line, not an iteration surface: run it ONCE, on the script you have already compiled and previewed. The URL it returns opens the editor and expires; give it to the human and you are done.

## The continuous take fuses your scenes

By default every scene with any motion joins the SAME continuous take. A background or style difference between adjacent scenes only warns and still fuses (`take-background-jump` / `take-style-mismatch`); the ONLY thing that breaks the take is a scene with no choreography or intent recipe at all (`take-motion-break`). Three of that fused take's mechanics catch authors out more than anything else in this kit, so read this before you spread intent, style, or a camera hold across scene boundaries.

**Intent: the lead scene owns the whole act.** The take's story character comes from ONE scene, its lead (the take's first scene). Downstream scenes' `intent` words do NOT each form their own act: author scene 1 as `demo` and scene 2 as `proof`, and the fused take is still ONE act, labelled by scene 1's `demo` - scene 2's `proof` does not appear anywhere in the film structure. If you want a distinct act per scene, break the fusion; inside one fused take, only the lead scene's intent counts.

**Style: set it once at the top, per-scene `customMove.style` overrides.** The take resolves ONE style, taken from its lead scene, but you do not have to bury that word inside a `customMove` to make it stick. Set a top-level `style` on the script object (a sibling of `scenes`) and every scene inherits it, including an intent-only lead scene that authors no `customMove` at all - it keeps its own free-directed shot AND plays in the script's chosen style. A scene's own `customMove.style` still overrides the script default for just that scene. A genuine mismatch (one scene overriding to a style its neighbours do not share) still raises `take-style-mismatch` and the fused take plays the lead's resolved style; the warning is honest that the override could not take effect mid-take.

**THE CALLOUT-ZOOM KILLER.** A callout's zoom-safety clamp (`callout-zoom-widened`) does not protect only the instant the callout is on screen. It protects the WHOLE continuous camera hold or ramp segment that the callout's visible window intersects, and in a single fused take that segment can extend BACKWARD across a scene boundary into an EARLIER scene: a tight push you authored in scene 1 can be flattened wide open because a callout in scene 3 overlaps the same continuous hold. This is the single most expensive mistake to debug blind, because the widened zoom shows up in the scene that authored the push, while the actual cause is a callout you authored scenes later.

The fix is timing, not framing: insert an explicit `camera: "full"` reset that COMPLETES before the callout's `startMs`, and never let a held or ramping zoom overlap the callout's visible window, even one that started in an earlier scene of the same take.

## Getting a token

Every call to `/api/choreo/compile` and `/api/choreo/previz` needs an `Authorization: Bearer <token>` header. Anonymous identities are fine (this is a prototype posture), but you still need a token so the rate limit and the previz dedupe can key off an identity.

This instance verifies real Firebase identities, so you need the deployment's Firebase web API key. That key is a PUBLIC client identifier (it already ships in the editor's own bundle, so reading it is not a secret leak). Fetch it from the editor's Firebase auto-config endpoint: `GET https://tsoro.co/__/firebase/init.json` returns the client config as JSON, and its `apiKey` field is the key you use below.

Mint an anonymous token with one REST call to the Google Identity Toolkit, using that key:

```
POST https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<the deployment's Firebase web API key>
Content-Type: application/json

{ "returnSecureToken": true }
```

The response carries an `idToken` (valid about one hour) and a `refreshToken`. Send the `idToken` as your `Bearer` token. When it expires, refresh it:

```
POST https://securetoken.googleapis.com/v1/token?key=<the deployment's Firebase web API key>
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<your refresh token>
```

Reuse one identity for the whole session so the rate limit and the previz dedupe treat your calls as one caller.

## The endpoints

Both endpoints are `POST`, need a `Bearer` token, are `application/json` only, spend from the shared rate budget (30 per 60s per identity and per IP), and cap the body at 512 KB.

### POST /api/choreo/compile

Send your scene-script wrapped in a `script` envelope. The server runs the SAME distillation the editor runs (deriving durations, background take-tokens, and callout metrics you cannot faithfully recompute from the outside), so you submit the script you authored, not a hand-built lower-level request:

```json
{
  "script": {
    "format": "tsoro.scene-script",
    "name": "Habit tracker hero",
    "aspect": "9:16",
    "scenes": [
      {
        "durationMs": 5000,
        "intent": "demo",
        "background": {
          "kind": "studio",
          "set": "sage"
        },
        "customMove": {
          "beats": {
            "inspect": "25%",
            "release": "60%"
          },
          "steps": [
            {
              "pose": "front-center",
              "at": 0
            },
            {
              "camera": "top-edge",
              "at": "inspect",
              "ease": "gentle"
            },
            {
              "camera": "full",
              "at": "release",
              "ease": "smooth"
            }
          ]
        },
        "titles": [
          {
            "preset": "focus-pull",
            "headline": "One tap",
            "startMs": 3600,
            "endMs": 4800
          }
        ]
      }
    ]
  }
}
```

Your script object can carry a top-level `style` (a sibling of `scenes`, #892): every scene inherits it as the style it plays with, unless that scene's own `customMove.style` says otherwise. Set it once instead of repeating the same word in every scene's `customMove` (see "The continuous take fuses your scenes" above for why this matters most on an intent-only lead scene).

If a body carries both a `script` key and other keys, the `script` branch wins. Response fields:

- `compilerVersion` (string): the compiler version, in the response BODY. Your drift signal on every compile call (the `x-choreo-compiler-version` HEADER exists only on the two doc routes).
- `scenes[]`: one entry per scene, echoing your scene `index`. Each carries:
  - `warnings[]`: `{ field, message, code?, timeMs? }`. The `code` is the STABLE machine key (see the warning vocabulary below); the `message` is human prose you may reword-blind. This coded vocabulary is compile-only.
  - `recorder`: the per-scene flight recorder (moments, zoom authored-vs-effective with `cappedBy`, occupancy, dynamics). This is your blocking evidence. Read it.
  - `segments` / `compositionKeyframes` / `footageCuts`: the COMPILED OUTPUT for the editor. Do NOT read or edit these; they are not your working surface. Read `filmText`, `recorder`, and `macro` instead.
- `macro`: the whole-film structure (acts, beats, tempo track, notes). Your narrative evidence.
- `filmText`: the composed human-readable film summary. The fastest read of what the compiler thinks it built.

### POST /api/choreo/previz

Same wrapper here: the scene-script goes under a `script` key, plus an optional frame budget (previz has its own two extra fields; there is no lower-level request to build). The server compiles and renders gray blockout frames:

```json
{
  "script": {
    "format": "tsoro.scene-script",
    "name": "Habit tracker hero",
    "aspect": "9:16",
    "scenes": [
      {
        "durationMs": 5000,
        "intent": "demo",
        "background": {
          "kind": "studio",
          "set": "sage"
        },
        "customMove": {
          "beats": {
            "inspect": "25%",
            "release": "60%"
          },
          "steps": [
            {
              "pose": "front-center",
              "at": 0
            },
            {
              "camera": "top-edge",
              "at": "inspect",
              "ease": "gentle"
            },
            {
              "camera": "full",
              "at": "release",
              "ease": "smooth"
            }
          ]
        },
        "titles": [
          {
            "preset": "focus-pull",
            "headline": "One tap",
            "startMs": 3600,
            "endMs": 4800
          }
        ]
      }
    ]
  },
  "frameCount": 8,
  "longEdge": 480
}
```

- `frameCount`: 1 to 24 (default 8). This is the ONLY knob you have over the preview (see the control surface note in the blocking pass).
- `longEdge`: 360 to 540 px (default 480).

Response fields:
- `filmstrip` / `onionSkin`: `{ dataUri }`, base64 PNGs. The filmstrip is your working surface.
- `frames[]`: `{ index, timeMs, sceneIndex, frameFrac?, facing?, zoom?, screenRegion? }`, the numbers AT each rendered anchor instant. Note what is NOT here: accent amplitude. Read amplitude from the pixels or the compile recorder's `centerU`/`centerV`.
- `screenRegion`: `{ u0, v0, u1, v1 }`, the axis-aligned slice of the phone's SCREEN visible in the frame (not the phone's box in the output, that is `frameFrac`). Screen-UV convention: origin top-left, u right, v DOWN, 0..1, the SAME axis as the callout/label coordinates. Whole screen visible = `{0,0,1,1}` no matter how small the phone reads in frame (size is `frameFrac`'s job, not this field's). "The frame shows the top 40% of the screen" reads as `{u0:0,v0:0,u1:1,v1:0.4}`. Two caveats: it is ROTATION-BLIND (on a tilted phone it over-estimates the visible slice, read `facing` alongside it), and it is the PRIMARY phone only, absent when the phone itself is off-frame.
- `macro` / `filmText`: the same whole-film evidence as compile.
- `warnings[]`: `{ field, message }` with NO `code`. Previz warnings are advisory prose only; the coded vocabulary comes only from compile.
- `timings`: the wall-time split (previz is a real render, this is its cost).

### Error table

Every error is JSON. React by status:

- `400` with `{ error, issues }`: your request failed validation. Read `issues[].path` to find the bad field. (A script that cannot even parse returns `{ error }` with the structural message and no `issues` - fix the script shape.)
- `401` with `{ error }`: no or bad token. Get a token (see above).
- `413` with `{ error }`: your body exceeded 512 KB. The message says the body is too large. Shrink the script (fewer scenes, shorter text).
- `429` with `{ error: "too many requests, slow down and retry shortly" }`: you hit the rate budget. Back off and retry after the window.
- `429` with `{ error: "a preview is already being generated; please wait for it to finish" }`: a DIFFERENT 429. You have a previz already in flight (or the global cap is full). This is not a rate-limit backoff; you self-queued. Serialize your previz calls, one at a time.
- `503` (previz only) with `{ error, warnings, timings }`: the preview timed out or partially failed. Try fewer frames or a shorter script.

## Reading the evidence (three passes)

Read your compile and previz results in three passes. These are ways of LOOKING, not a crew you have to hire: run them all yourself, in one head, in order.

### Narrative pass (think like the director)

Evidence: `macro` (acts, beats, the tempo track, and notes) and the MACRO block of `filmText`. Ask: does the beat map match the story you intended? Is the tempo arc deliberate, or does the energy sag or spike where you did not mean it to? Are there `off-screen` notes you did not author? (Those are measured on the render grid, so a move that drifts off-frame BETWEEN beats is caught, not just on a beat.)

### Blocking pass (think like the DP)

Evidence: the previz `frames[]` numbers (`frameFrac`, `zoom`, `facing`, `screenRegion`) PAIRED with the filmstrip pixels, plus the compile recorder's zoom (authored versus effective, and `cappedBy` when the compiler tightened or widened it).

Region framing ("top-third", "bottom half of the screen") is verifiable BOTH ways now: numerically via `screenRegion` and visually via the faint thirds grid ruled onto the blockout screen tile, so the cheat sheet's TRUST RULE (where the numbers and the pixels disagree, the pixels are right) applies to a region brief too.

CONTROL SURFACE, so you do not burn iterations chasing a knob that does not exist:
- The preview's anchor instants are chosen by the SERVER (bookends, occupancy dips, beats). You do not pick them. Your only knob is `frameCount` (1 to 24): more frames, more anchors, finer read.
- A beat with no step on it does not exist and creates no anchor. If you want to see an instant, put a step there.
- Accent AMPLITUDE is not in `frames[]`. To judge how big a gesture plays, read the filmstrip pixels or the compile recorder's `centerU`/`centerV`, not the frame numbers.

TRAPS:
- Two different projections. `frameFrac` is the rotation-BLIND box (a safe over-estimate of how much frame the phone fills); the recorder's `area` is the rotation-AWARE silhouette. They can disagree on a tilted phone. Do not treat them as the same measure.
- The onion skin blurs a camera push into mush. On any clip with a zoom, the filmstrip is the working surface, not the onion skin.

### Polish pass (think like the animator)

Evidence: the COMPILE `warnings` by `code`, and the recorder's dynamics and sharpness lines. Two rules:
- Previz warnings are advisory prose with NO codes. The coded vocabulary and the per-scene recorder come ONLY from `/compile`. Run a compile for this pass even when the previz looks fine.
- THE CLOCK TRAP. On the default single continuous take, a warning's `timeMs` is on the TAKE clock, attributed to the take's LEAD scene. A large `timeMs` on scene 0 is NORMAL and does NOT mean the fault is in scene 0. Locate a fault by its `code` plus its `field` plus the beat table in `filmText`, never by doing arithmetic on `timeMs`. (The one exception: `curve-out-of-shot` is re-attributed to its owning scene's local clock.)

## Warning vocabulary

Every compile warning carries a stable `code`. Key your edits off the code, not the sentence (we are free to reword the sentence). Each code below names the CONDITION that raised it and the concrete FIX in your script:

- `step-conflict`: One step set more than one of pose/accent/camera/move/reveal, so the highest-precedence one won and the rest were ignored. FIX: Split it into separate steps, one action each, and give them their own `at` times (or a shared beat).
- `empty-step`: A step named none of pose/accent/camera/move, so it did nothing and was skipped. FIX: Give the step one action, or remove it.
- `modifier-ignored`: `repeat` or `strength` was set on a step that is not an accent, so it was ignored. FIX: Move `repeat`/`strength` onto an accent step, or drop it.
- `unknown-pose`: The `pose` word is not in the pose vocabulary, so a default pose was used. FIX: Use a pose id from the guidelines doc's pose list, or fix the spelling.
- `unknown-accent`: The `accent` word is not a known accent, so the step was dropped. FIX: Use an accent id from the guidelines doc's accent list.
- `unknown-move`: The `move` id is not a known camera move, so the step was dropped. FIX: Use a move id from the guidelines doc's camera-move list.
- `unknown-reveal`: The `reveal` move id is not a known camera move, so the reveal was dropped. FIX: Use a known move id in `reveal`.
- `unknown-region`: The `camera` region word is not a known screen region, so a default framing was used. FIX: Use a region id from the guidelines doc's screen-region list.
- `unknown-zoom`: The `zoom` word is not a known tightness, so the region's default zoom was used. FIX: Use a zoom word from the guidelines doc (for example close, medium, macro).
- `unknown-ease`: The `ease` word is not a known easing, so a default easing was used. FIX: Use an easing word from the guidelines doc, or `overshoot`/`spring` on a pose step.
- `unknown-strength`: The `strength` word is not subtle/normal/big, so normal was used. FIX: Set `strength` to subtle, normal, or big.
- `unknown-style`: The `style` word is not a known director style, so the studio default was used. FIX: Use a style id from the guidelines doc's style list.
- `unknown-intent`: The scene's `intent` word is not a known intent, so no act character was applied. FIX: Use an intent word from the guidelines doc (for example hook, demo, cta).
- `ease-inapplicable`: `overshoot` or `spring` was asked for on a camera or curated-move step, which cannot express it, so it was ignored. FIX: Use overshoot/spring only on pose steps; give camera/move steps a plain easing word.
- `repeat-clamped`: The accent `repeat` count was outside the allowed range and was clamped. FIX: Set `repeat` between 1 and 4.
- `beat-unparseable`: A named beat's time could not be read, so the beat was dropped. FIX: Write each beat time as a percent (for example "45%") or a duration (for example "1.2s").
- `at-unresolved`: A step's `at` (a percent or a beat name) could not be resolved, so the step was spaced evenly instead. FIX: Point `at` at a beat you defined in `beats`, or use a plain percent.
- `steps-reordered`: The steps were not in time order and were sorted for you. FIX: No change needed; author them in order if you want the script to read top to bottom.
- `timing-adjusted`: A timing rule pushed a group of steps later to make room for a move or a zoom to read. FIX: Lengthen the scene if you did not want the shift, or accept the adjusted timing.
- `move-dropped`: A curated move had no room left in the scene (or next to a cut) and was dropped. FIX: Give the move more room: lengthen the scene, or move its `at` earlier so its full length fits.
- `step-squeezed`: A plain camera or pose step had no room before the scene cut, so it was squeezed onto the cut and rendered with no visible movement (the phone or the zoom never actually gets there). FIX: Give the step room: move its `at` earlier so its ramp completes before the cut, or lengthen the scene. A step authored right after a cut can hit the same wall from the other side (the post-cut hold-to-read floor needs room too), so leave space on both sides of a cut, not just before it.
- `beat-pushed-by-hold-floor`: A camera step was authored close after an earlier zoom, so the hold-to-read floor held it and it FIRES LATER than the time you gave it (its `timeMs` here is the real, later time). The zoom therefore stays up past the moment you released it. FIX: Buffer against the REAL time in this warning, not your authored percent: a callout must start AFTER it, or the zoom clamp will still widen (`callout-zoom-widened`). To release sooner, move the earlier zoom step earlier (so the hold completes before this beat), or lengthen the scene so both fit with room to spare.
- `budget-exceeded`: The scene compiled past the keyframe budget, so some accent detail was dropped. FIX: Simplify the scene: fewer steps, or fewer repeats.
- `segments-capped`: The scene had more motion segments than allowed, so the later ones were dropped. FIX: Reduce the number of steps, or split the scene in two.
- `phone-recomposed`: The phone drifted out of frame (an off-centre pose multiplied by a zoom) and was pulled back in. FIX: Space out beats that land close together, most often a camera reset and a lateral pose step: when their ramps overlap, they can push the phone out of frame together, so put more time between their `at` values (the fix is usually seconds, not tens of milliseconds). If spacing does not clear it, keep the phone near centre under a tight zoom, or reduce the pose offset or the zoom.
- `coupling-damped`: A gesture's push was eased under the camera zoom so the shot would not breathe too hard. FIX: No change needed; lower the accent strength if you want even less motion under the zoom.
- `reveal-cut-visible`: A reveal barely turns the screen away, so the footage cut may show. FIX: Pair the reveal with "whip-spin" (a full yaw turn that hides the change), not a roll or a slide that keeps the screen facing front.
- `callout-zoom-widened`: The authored zoom was widened to keep a callout in shot. FIX: The real fix is usually timing, not position: insert an explicit `camera: "full"` reset that completes before the callout's `startMs`, and never let a held or ramping zoom overlap the callout's visible window, even one that started in an earlier scene of the same take. The clamp protects the callout's WHOLE label geometry (the text box, not just the anchor dot), so a callout near a screen edge can never coexist with a tight zoom into that same edge. Moving the callout toward the screen centre or authoring a wider zoom yourself can help, but rarely fixes it alone.
- `curve-missing-span`: A curve step had no readable `span`, so the curve was dropped. FIX: Add a `span` (a percent, or a duration like "1.2s").
- `curve-empty`: A curve's channels were all near zero, so it was no motion and was dropped. FIX: Give the curve real sample values, or use a pose/accent instead.
- `curve-sample-count-mismatch`: A curve's channels had different sample counts, so each was resampled on its own and they line up by fraction of the span, not by index. FIX: Give every channel the same number of samples so they align instant for instant.
- `curve-amplitude-clamped`: The frame-safety rule scaled a curve down to keep it in shot. FIX: Reduce the curve's offsets, or use a staging pose for an off-screen entrance/exit.
- `curve-span-shortened`: A neighbouring step shortened the curve's span, so it plays faster than you drew it. FIX: Make room for the curve, or shorten its authored span to match.
- `curve-too-short`: The curve's placed span fell under the 0.7s floor and was dropped. FIX: Give the curve a longer span, or more room in the scene.
- `curve-out-of-shot`: The curve leaves the frame at points the compiler cannot re-plan. Its `timeMs` here is on the OWNING scene's local clock (the one clock-trap exception), so it does locate the fault in that scene. FIX: Reduce the curve's offsets so it stays in shot, or use a staging pose for the off-screen part.
- `curve-dynamics-flag`: The energy check flagged more motion inside the curve than the `sharpness` you declared. DISTRUST the magnitude number on this code for now; trust the flag itself and the filmstrip pixels. FIX: Declare a higher `sharpness` (energetic or snap) if the motion is intended, or smooth the samples.
- `continues-orphan`: `continues` was set on the first scene, which has nothing before it to join. FIX: Remove `continues` from the first scene.
- `continues-cut`: A `continues` failed the coherence check and became a cut instead. FIX: Match the previous scene's background and style so the take can fuse, or accept the cut.
- `take-background-jump`: A single continuous take fuses across a background change, so the backdrop will visibly jump at the join. FIX: Use one background for the whole take, or accept the jump.
- `take-style-mismatch`: A single take fuses across a style change, so the lead scene's style plays throughout. FIX: Use one style across the take, or split into separate takes.
- `take-motion-break`: A scene with no motion breaks the continuous take around it. FIX: Give every scene an `intent` or a `customMove` so the take flows through.
- `unearned-cut`: A cut lands where nothing changes on screen (same act, same background). FIX: Drop the cut and use one longer scene with a move, or change the screen content across the cut.
- `twin-scenes`: Two adjacent scenes are the same shot twice. FIX: Vary the framing or motion between them, or merge them into one scene.
- `flat-direction`: Three or more scenes and no `intent` declared anywhere, so the video has no arc. FIX: Give the scenes intent words (hook, demo, cta, ...) so the video builds an arc.
- `move-ignored-by-intent`: The scene set both an `intent` and a bare preset `move`. The intent synthesizes a complete directed shot, which owns the motion, so the preset `move` was dropped and never played. FIX: Drop the `move` (the intent already directs the shot), or remove the `intent` if you wanted the preset `move` to play instead. To run a preset move under an intent, chain it as a `{ move: <id> }` step inside a `customMove` rather than as a bare `move`.

## One worked cycle

You author a close-up that sends the phone aside and then zooms in tight on it:

```json
{
  "script": {
    "format": "tsoro.scene-script",
    "name": "Feature close-up",
    "aspect": "9:16",
    "scenes": [
      {
        "durationMs": 4000,
        "intent": "demo",
        "customMove": {
          "steps": [
            {
              "pose": "aside-right",
              "at": 20
            },
            {
              "camera": "center",
              "zoom": "macro",
              "at": 55,
              "ease": "gentle"
            }
          ]
        }
      }
    ]
  }
}
```

You compile. The response carries a warning:

```
{ "field": "customMove", "message": "the phone drifted out of frame and was pulled back in", "code": "phone-recomposed" }
```

You look up `phone-recomposed` in the vocabulary: an off-centre pose multiplied by a zoom pushed the phone out of frame, and the compiler had to rescue it. The FIX says to keep the phone near centre under a tight zoom. So you make ONE edit: change the first step's pose from `aside-right` to `front-center` (a zoom multiplies a pose's offset from centre, so the tighter the framing, the more an off-centre pose fights it).

You compile again: the warning is gone. You run ONE previz to confirm the pixels: the phone now holds centre as the framing pushes in. Done.

## Handoff: finishing the job

When the evidence matches your intent, hand the finished script to the human. Do this ONCE, at the end - it is the finish line, not another iteration surface.

### Before you hand off

Run a final `POST /api/choreo/compile` on the EXACT script you are about to hand off (and one `POST /api/choreo/previz` if your rate budget allows), and confirm there are no warnings you did not expect. The handoff stores the exact text you send, so what you confirm here is what the human opens.

### POST /api/handoff

Needs a `Bearer` token (anonymous is fine), is `application/json` only, spends from the shared rate budget, and caps the body at 256 KB. The body is your scene-script AS A STRING under a `script` key (this endpoint stores raw text, so stringify the JSON object you authored):

```json
{
  "script": "{\"format\":\"tsoro.scene-script\",\"name\":\"Habit tracker hero\",\"aspect\":\"9:16\",\"scenes\":[{\"durationMs\":5000,\"intent\":\"demo\",\"background\":{\"kind\":\"studio\",\"set\":\"sage\"},\"customMove\":{\"beats\":{\"inspect\":\"25%\",\"release\":\"60%\"},\"steps\":[{\"pose\":\"front-center\",\"at\":0},{\"camera\":\"top-edge\",\"at\":\"inspect\",\"ease\":\"gentle\"},{\"camera\":\"full\",\"at\":\"release\",\"ease\":\"smooth\"}]},\"titles\":[{\"preset\":\"focus-pull\",\"headline\":\"One tap\",\"startMs\":3600,\"endMs\":4800}]}]}"
}
```

The server checks the script can parse (the same parse the editor does on open) and stores it. Response:

```
{
  "handoffId": "3f1c2b4a-5d6e-4f70-8a91-b2c3d4e5f601",
  "editorUrl": "https://tsoro.co/app#handoff=3f1c2b4a-5d6e-4f70-8a91-b2c3d4e5f601",
  "expiresAt": "2026-01-02T00:00:00.000Z"
}
```

Give the human the `editorUrl` verbatim. It opens the editor straight into the same project your compile and preview saw, and it expires in 24 hours - so tell them to open it soon, and if it has expired just hand off again to get a fresh URL. You do not open it yourself; the human's browser claims it once.

### Error table (handoff)

- `400` with `{ error }` (with `issues` for a shape error): the body was not `{ script: <string> }`, or the script could not parse and so could never open. Fix the script and re-send.
- `413` with `{ error }`: the body exceeded 256 KB. Shorten the script.
- `429` with `{ error: "too many requests, slow down and retry shortly" }`: you hit the rate budget. Back off.
- `503` with `{ error }`: the service is holding too many live handoffs right now. Wait a little and retry; your script is fine.

## Versioning

The compiler version is `v2-2026-07-23-kit-friction`. You see it in two places: the `compilerVersion` field in every compile response BODY, and the `x-choreo-compiler-version` HEADER on the two doc routes (this doc and the guidelines). If that value changes between your calls, the compiler changed under you: recompile your script and re-read this doc and the guidelines, because a warning code, a number, or a default may have moved. The previz build id (`previz-2026-07-22-blockout-v2`) is provenance only; key drift off the compiler version.