demobites 1.4.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/skill/SKILL.md CHANGED
@@ -9,7 +9,7 @@ You are the camera operator, the director, and the editor. You film a real brows
9
9
 
10
10
  All scripts live in `scripts/` beside this file. They are plain Node ESM. Requirements: Node 18+. `npx demobite` installs Playwright, ffmpeg and ffprobe beside the skill; every script resolves the media tools through `scripts/media-tools.mjs` (a compatible system build first, then the packaged one). Never call `ffmpeg` or `ffprobe` by bare name in a new script. Run every script from the project directory so `.recorder/` lands next to the project.
11
11
 
12
- Follow the phases in order. Never skip the storyboard approval. Never ingest before the human's word — for DemoBites, Approve on the in-app preview page IS the word. For a batch of briefs the word was given twice already, on the batch and on each storyboard: a delivered take becomes a bite by itself (see Batch of briefs).
12
+ Follow the phases in order. Never skip the storyboard approval: the human's yes on the storyboard, in the chat, is the word for the take. After that the take is delivered by itself. It becomes a bite in DemoBites without a second click, and the human watches it come in on their Demos grid (`<base>/demos`). Never send them to a preview page to approve it.
13
13
 
14
14
  ## Phase 0: Auth gate, ALWAYS FIRST — with the human's word
15
15
 
@@ -39,10 +39,13 @@ surprise the human with a browser page):**
39
39
  for their word.
40
40
 
41
41
  Gating first is deliberate: fail before minutes of filming and know the
42
- target workspace up front. Bite-plan limits are NOT your concern and never
43
- block you: staging always succeeds, takes wait in the product queue, and the
44
- plan gate lives on the Approve button in DemoBites. Never mention quota in
45
- the terminal — if the workspace is full, the product does the talking.
42
+ target workspace up front. Plan limits are NOT your concern and never block
43
+ you: staging always succeeds. When the account has no recording minutes
44
+ left, DemoBites KEEPS the take and it waits on the Demos grid, where a strip
45
+ shows the waiting takes with the upgrade door. `upload.mjs` prints one line
46
+ for that ("Kept. The take is waiting for recording minutes..."); relay it
47
+ as it is. Never add numbers, prices or quota talk of your own, the product
48
+ does the talking.
46
49
  To sign out: `node scripts/login.mjs --logout` (revokes the key server-side
47
50
  AND strips it locally). "Log me out of DemoBites" means exactly that command.
48
51
 
@@ -161,6 +164,19 @@ Holding shots to cover estimated lines is what produced a 60 second take with th
161
164
 
162
165
  **LAW: page transitions are cut and faded, never watched.** When the story moves to another page, the viewer sees page one, a short fade, page two — never the loading blank. record.mjs stamps every mid-take `goto` and manifest.mjs cuts that window out with a fade (`cuts` in the wire manifest); the ingestion lays it on the bite as a timeline cut. No zoom and no narration live inside a cut (the studio forbids both), so put the line about the new page on the beat AFTER it has landed, and say goodbye to the old page BEFORE the goto.
163
166
 
167
+ ### FRAMING: the camera obeys the script
168
+
169
+ Framing is our job, never the customer's (founder, 2026-09-27). Decide it from the narration you wrote. The customer never hears about zooms, framing or the cursor.
170
+
171
+ - Every step gets `frame`: `"close"` or `"wide"`.
172
+ - A field, a button, a menu item, a toggle, a badge, a row = `"close"`.
173
+ - Landing on a page, a report, a chart, a table, a dashboard, a list of results = `"wide"`. So is every line that says "here is", "you see", "you land on", "the whole".
174
+ - A `type` step with `enter` is close for the typing. What Enter reveals is wide by itself; the recorder sees the new page. Do not add a beat for it.
175
+ - A bare `settle` right after a navigation is wide by itself.
176
+ - Two consecutive wides on the same page are one shot. Write both; the server joins them.
177
+ - Never more than 4 seconds of close on a static screen. When the line runs longer, the beat is wide.
178
+ - No `frame` = the recorder's own choice, close on the subject. `reveals: false` keeps the camera where it is after a click or an Enter.
179
+
164
180
  Storyboard schema (`rulesVersion` and per-step `rules` come from Phase 1b):
165
181
 
166
182
  ```json
@@ -180,7 +196,7 @@ Storyboard schema (`rulesVersion` and per-step `rules` come from Phase 1b):
180
196
  }
181
197
  ```
182
198
 
183
- Step fields: `action` is one of `goto | settle | scroll | click | hover | type | expect`. `rules` (optional, any step) lists the numbers of the workspace rules that shaped the beat; the storyboard's top-level `rulesVersion` names the rule set (Phase 1b). **Durations (`dwell`, `after`, settle `ms`, scroll `ms`) are milliseconds; a value under 60 is read as seconds** (write `"dwell": 3400` or `"dwell": 3.4`, never `"dwell": 3` meaning 3 ms). `goto` needs `url`. `settle` takes `ms` and an optional `focus` selector. `scroll` needs `dy` and takes `ms`. `click`/`hover` need `selector` and take `minY` (minimum Y for the visible instance pick), `dwell`, `after`, `waitLoad`. Every step takes `label` and `narration`.
199
+ Step fields: `action` is one of `goto | settle | scroll | click | hover | type | expect`. `rules` (optional, any step) lists the numbers of the workspace rules that shaped the beat; the storyboard's top-level `rulesVersion` names the rule set (Phase 1b). **Durations (`dwell`, `after`, settle `ms`, scroll `ms`) are milliseconds; a value under 60 is read as seconds** (write `"dwell": 3400` or `"dwell": 3.4`, never `"dwell": 3` meaning 3 ms). `goto` needs `url`. `settle` takes `ms` and an optional `focus` selector. `scroll` needs `dy` and takes `ms`. `click`/`hover` need `selector` and take `minY` (minimum Y for the visible instance pick), `dwell`, `after`, `waitLoad`. `type` needs `selector` and `text` and takes `enter` (press Enter after the text), `clear`, `after`, `reveals`. Every step takes `label`, `narration` and `frame` (`"close"` or `"wide"`, the FRAMING law above; any other value stops the take).
184
200
 
185
201
  Beyond `steps`, a storyboard may carry the off-camera blocks (Phase 3a/law above); `cleanup.mjs` runs them on the same profile without video:
186
202
 
@@ -217,7 +233,7 @@ Two fields carry the whole advantage of this lane, so fill them in:
217
233
 
218
234
  Use `hideCss` for chat widgets and cookie banners that would pollute the picture. The first `goto` opens the video, so the first narration goes on the settle right after it.
219
235
 
220
- **Show the storyboard inline and get approval before filming.** Present it as a numbered shot list, not raw JSON. Say the target length out loud so the human can push back on pacing before you burn a take. Iterate until they say go.
236
+ **Show the storyboard inline and get approval before filming.** Present it as a numbered shot list, not raw JSON. The list is the story: what the viewer sees and hears, beat by beat. Never zooms, framing, selectors or the cursor. Say the target length out loud so the human can push back on pacing before you burn a take. Iterate until they say go.
221
237
 
222
238
  The presentation has three blocks, always: the shot list (irreversible beats marked "pointed at, not pressed"), **"Before the camera"** (what `prep[]` creates) and **"After the cut"** (the `cleanup_plan[]` sentences). A storyboard whose flow needs data and has no prep, or creates anything and has no cleanup plan, is not ready to show.
223
239
 
@@ -288,6 +304,7 @@ Filming laws baked into `record.mjs`, do not reimplement or weaken them:
288
304
  - Mouse coordinate clicks: the real mouse tracks the drawn cursor, hover states fire naturally.
289
305
  - **The camera follows the subject, measured off the live page.** Every hover records the hovered element's rectangle. Every click records TWO shots: the control on approach, and then whatever the click opened. A click that opens a menu or a dialog moves the subject somewhere else on screen, so a camera left on the button shows a dimmed backdrop while the thing you just opened sits off frame.
290
306
  - **Shots overlap on purpose.** The manifest's camera path is chained by the backend so the runtime travels from one subject to the next at zoom. Never "fix" this into a non overlapping sequence, that is the pull out to 1.0 between every shot.
307
+ - **A landing is wide.** A `type` with `enter` ends its close shot the moment before Enter; what Enter revealed (a new URL, a page that changed) is a full-frame `wide: true` shot, and so is a bare settle after a navigation and any step framed `wide`. The server ends the previous zoom at the wide's start and never holds a close shot over it (founder, 2026-09-27).
291
308
 
292
309
  If the take fails mid flow, the partial video and manifest are still saved. Diagnose, fix the storyboard, film again.
293
310
 
@@ -300,23 +317,23 @@ Send the TRIMMED CLEAN take into DemoBites. The studio owns the look: NO backdro
300
317
  node scripts/trim.mjs <takeDir> # raw.webm -> clean.mp4, trim from record_from ONLY
301
318
  node scripts/calibrate.mjs <takeDir> # anchor-measure the clock against the footage
302
319
  node scripts/manifest.mjs <takeDir> # internal manifest -> manifest.demobites.json (wire schema)
303
- node scripts/upload.mjs <takeDir> # STAGE the take + open the in-app preview (refuses an uncleaned take)
320
+ node scripts/upload.mjs <takeDir> # STAGE + DELIVER the take, open the Demos grid (refuses an uncleaned take)
304
321
  ```
305
322
 
306
- **The human word lives in the product now.** `upload.mjs` stages the take (the
307
- ZIP for ingestion plus a playable MP4 for the player), opens the DemoBites
308
- preview page in the human's browser, and polls while they decide THERE.
309
- Approve on that page runs the ingest; Discard deletes the staged take and this
310
- script reports it so you adjust and refilm. There is no local review.html for
311
- this ending — the preview page is the review.
323
+ **Every take is delivered by itself** (founder ruling 2026-09-25). `upload.mjs` stages the take (the ZIP for ingestion plus a playable MP4), uploads both, then calls the delivery door. There is no Approve click and no preview page to send the human to: the word was the storyboard yes in the chat. The script opens the human's Demos grid (`<base>/demos`, `--no-open` skips it) and prints one of two lines:
324
+
325
+ - **Delivered:** `Delivered. Watch it come in: <base>/demos`. The card for the take shows it ingesting on the grid. Without `--stage-only` the script then waits for the bite to finish and prints the receipt (see the law below).
326
+ - **Kept, waiting for minutes:** `Kept. The take is waiting for recording minutes (back on <date>). Watch it here: <base>/demos`. The account has no recording minutes left. The take is safe on the server and waits on the same grid, next to the upgrade door. Tell the human exactly that, nothing more; do not refilm, do not stage it again.
327
+
328
+ Any other answer is an error: the script says what failed and `node scripts/status.mjs <takeDir>` tries the delivery again (the server is idempotent). `staged.json` in the take directory records `stagingId`, `dashboardUrl`, `delivered`, `waiting`, `resetsAt` and `biteId` (plus `previewUrl`, kept only for older readers; never give it to the human). Tell the human to watch the Demos grid, never the preview link.
312
329
 
313
- What the human approves on that page is the **picture and the coverage**, never the script. The page deliberately shows no quoted lines and no timestamps, because the ingestion rewrites the narration and refits it to the video. Presenting "this line at 0:05" promises something the system does not deliver. A retake is only for a wrong picture: private data on screen, or a missing step in the flow.
330
+ If the take is wrong (private data on screen, a missing step in the flow), say so, adjust and film again; the human removes the unwanted bite in the app.
314
331
 
315
332
  ### LAW: never hand over a studio link before the bite is ready
316
333
 
317
334
  `ingest` only STARTS the pipeline. Transcode, rescript, fit, synthesize and finalize all happen after the call returns, so a link printed at that moment leads to a half built bite with grey silent rows, which is exactly what the founder walked into on 2026-08-08.
318
335
 
319
- `upload.mjs` now polls `/api/recorder/status` until the bite reaches `completed` and prints what actually landed. **Read that line before you say anything to the human.** It reports `narrationReady/narrationTotal` segments with real audio behind them, and the camera shot count. If narration is 0, or ready is below total, or shots are 0, say so plainly and investigate. Do not pass on a link with a warning above it as though it were a success.
336
+ After a delivered take, `upload.mjs` polls `/api/recorder/status` until the bite reaches `completed` and prints what actually landed. **Read that line before you say anything to the human.** It reports `narrationReady/narrationTotal` segments with real audio behind them, and the camera shot count. If narration is 0, or ready is below total, or shots are 0, say so plainly and investigate. Do not pass on a link with a warning above it as though it were a success.
320
337
 
321
338
  ## Phase 7: Re-take (Launch plan and up)
322
339
 
@@ -333,16 +350,15 @@ node scripts/retake.mjs <biteId> [--note "what changed"] # or: npx demobite re
333
350
  Laws for a re-take:
334
351
  - **Read the note first.** "We moved Export to the header" tells you which step will break before you film.
335
352
  - **A step that no longer resolves stops the take at that step.** Look at the live page. If the control moved,
336
- fix the selector in the take's `storyboard.json` and run again with `--take <dir>`. If the feature is truly
353
+ fix the selector in the take's `storyboard.json` and run again with `--take <dir>` (your edited storyboard is kept; `--fresh` replaces it with the server's recipe). If the feature is truly
337
354
  gone, DROP that beat AND its narration line, and tell the human plainly: "This capability no longer exists,
338
355
  we removed it from the video." Nothing stages until every step resolves.
339
356
  - **The narration in the recipe is the ORIGINAL intent.** Do not rewrite it to taste: the server replaces it
340
357
  with the bite's current text per step. Only remove lines whose beats you dropped.
341
358
  - **Same pacing laws apply** (intro, narrate the path, linger, cut and fade on page transitions).
342
- - **The human approves in-app.** The preview page says "Re-take of <bite>". Approve replaces the recording in
343
- that bite; the previous recording is kept for rollback, never overwritten. A re-take filmed from a brief
344
- (a take with an attempt) is delivered instead: the new recording replaces the current one by itself, and
345
- the promoted export stays as it is until a version is published.
359
+ - **The re-take is delivered like every take.** The new recording replaces the current one in that bite by
360
+ itself; the previous recording is kept for rollback, never overwritten, and the promoted export stays as it
361
+ is until a version is published. The human watches it on the Demos grid.
346
362
 
347
363
  ## Batch of briefs (GitHub PR → demos)
348
364
 
@@ -350,12 +366,13 @@ The human pastes a bundle of approved briefs into the chat: a header (batchId, w
350
366
 
351
367
  ```bash
352
368
  node scripts/briefs.mjs list <batchId> [--paste bundle.txt] # the approved briefs; warns when the paste drifted
369
+ node scripts/briefs.mjs list --slug <slug> # Update Radar: the batch behind a record code, filmed in four phases (see the section below)
353
370
  node scripts/briefs.mjs claim <batchId> <briefId> # mints an attempt, creates take-<briefId>-r<revision>/brief.json
354
371
  node scripts/briefs.mjs event <takeDir> planning|awaiting_storyboard_approval|recording|uploading|failed|cancelled [--note "..."]
355
372
  node scripts/briefs.mjs release <takeDir> # give the brief back (cancelled)
356
- node scripts/upload.mjs <takeDir> --stage-only --no-open # deliver: the take becomes a bite by itself, do not wait
373
+ node scripts/upload.mjs <takeDir> --stage-only --no-open # deliver: the take becomes a bite by itself (or waits for minutes), do not wait
357
374
  node scripts/status.mjs <takeDir> # later: wait for the bite to finish (retries a failed delivery)
358
- node scripts/status.mjs --all # one look at every delivered take here
375
+ node scripts/status.mjs --all # one look at every delivered or waiting take here
359
376
  ```
360
377
 
361
378
  The procedure, in order:
@@ -364,11 +381,75 @@ The procedure, in order:
364
381
  2. Claim the briefs you are about to film, one `claim` each. A claim answers "active attempt" when another agent or an earlier run holds the brief: show the human the attempt reference and its start time, and only with their word claim again with `--force`.
365
382
  3. Run `rules.mjs <takeDir>` (Phase 1b; the claim stored the batch's rules snapshot in brief.json as the fallback) and `vocab.mjs` over the screens each brief visits, then write every storyboard (Phase 3) with the brief as the spec, the workspace rules below the laws, and vocab.json as the only dictionary: the flowIntent lines are the beats, the outcome is the last beat, the exclusions are things the camera never shows, and the take stays under the brief's `maxSeconds` (90). Send `event <takeDir> planning` when you start a storyboard and `event <takeDir> awaiting_storyboard_approval` when it is ready.
366
383
  4. **Show the storyboards together, get a word on each one.** One message can carry all of them, but every brief gets its own yes or no. Never take one yes as a yes for the batch. A brief the human declines gets `release`.
367
- 5. Film sequentially, never in parallel: one Chrome on the profile. Per take: `event recording` → Phase 4 dry run → `cleanup.mjs --prep` when declared → Phase 5 take → `cleanup.mjs` (revert, checks.after) → trim, calibrate, manifest → `upload.mjs <takeDir> --stage-only --no-open`. Report, per take, what was created and what was reverted, with the before/after checks. `upload.mjs` reads `brief.json`, moves the attempt to uploading, stages with the attempt on the payload, and after the two uploads calls the delivery route: the take becomes a bite in DemoBites by itself, no Approve click, and the line reads `delivered: bite <id>`. It writes `staged.json` with the staging id and the bite id.
384
+ 5. Film sequentially, never in parallel: one Chrome on the profile. Per take: `event recording` → Phase 4 dry run → `cleanup.mjs --prep` when declared → Phase 5 take → `cleanup.mjs` (revert, checks.after) → trim, calibrate, manifest → `upload.mjs <takeDir> --stage-only --no-open`. Report, per take, what was created and what was reverted, with the before/after checks. `upload.mjs` reads `brief.json`, moves the attempt to uploading, stages with the attempt on the payload, and after the two uploads calls the delivery route, as for every take: the take becomes a bite in DemoBites by itself, no Approve click, and the line reads `Delivered. Watch it come in: <base>/demos`. With no recording minutes left the line reads `Kept. The take is waiting for recording minutes...`; the take waits on the grid, go on with the next brief. It writes `staged.json` with the staging id, the grid link, and the bite id or the waiting state.
368
385
  6. **A failed brief never stops the others.** On a failure send `event <takeDir> failed --note "<what happened>"`, keep the take directory for diagnosis, and continue with the next brief. Report every failure plainly at the end.
369
- 7. When all takes are delivered, tell the human: N takes were delivered and are becoming bites in DemoBites by themselves, with the bite ids. `status.mjs --all` shows where each stands; `status.mjs <takeDir>` waits for one to finish and prints what landed (the Phase 6 receipt law holds: no studio link before the bite is completed). A take the server would not deliver (upload.mjs printed the error) waits in the review queue; `status.mjs <takeDir>` tries the delivery again, and on an older DemoBites waits for the word in the app as before. Deliver each take; never publish, never share, never send invitations.
386
+ 7. When all takes are delivered, tell the human: N takes were delivered and are becoming bites in DemoBites by themselves, with the bite ids, and they can watch them come in on the Demos grid (`<base>/demos`). Name any take that is kept, waiting for recording minutes; it waits on the same grid. `status.mjs --all` shows where each stands; `status.mjs <takeDir>` waits for one to finish and prints what landed (the Phase 6 receipt law holds: no studio link before the bite is completed). A take the server would not deliver (upload.mjs printed the error): `status.mjs <takeDir>` tries the delivery again. Deliver each take; never publish, never share, never send invitations.
387
+
388
+ Resume after an interruption from what is on disk and on the server: a `take-*` directory with `brief.json` is claimed; with `raw.webm` it was filmed; with `clean.mp4` and `manifest.demobites.json` it is ready to stage; with `staged.json` it is delivered, waiting for minutes, or staged (check it with `status.mjs --no-wait`). `list` shows the server's view of every attempt. Never re-claim a brief that already has your own live attempt; never re-stage one that `staged.json` says is delivered, waiting or staged unless the human asked for a new take (`upload.mjs --supersede`).
389
+
390
+ ## Record a batch by slug (Update Radar)
391
+
392
+ An Update Radar workflow (a scan of merged pull requests → topics → briefs) ends its Briefs stage with a short record code and the command `npx demobite record <slug>` on its page ("Go to your terminal or your coding agent where you installed it and run this command"). The human either ran the command themselves and pasted its output to you, or asked you to run it. Either way the batch is a batch of briefs as above, reached by its code instead of its id. Phase 0 (the auth gate, with the human's word) and Phase 1b (the workspace rules) come first, as for every run. Plan limits are not your concern. Delivery is one door.
393
+
394
+ **We will be judged by the outcome: the fewest edits before Export and Go live** (founder ruling, 2026-09-26). The cloud draft is a starting point, never final. The run has four steps (List → Refactor + questions → One approval → Film in the background), in this order, and no other question is asked once filming starts. The Phase numbers named inside them are the skill's phases above.
395
+
396
+ ```bash
397
+ npx demobite record <slug> # what the human runs: lists the batch, writes .recorder/radar/<slug>/
398
+ node scripts/briefs.mjs list --slug <slug> # the same call from the skill: GET <base>/api/recorder/briefs?slug=<slug>
399
+ node scripts/briefs.mjs refine <slug> <briefId> [--note "..."] # phase 2: post the brief you refined (.recorder/radar/<slug>/refined/<briefId>.json)
400
+ node scripts/batch.mjs plan <slug> # phase 3: the pool for this machine and which briefs have a storyboard
401
+ node scripts/batch.mjs run <slug> [--concurrency N] # phase 4: film every approved storyboard in the background, deliver each as it lands
402
+ ```
403
+
404
+ The answer to `list --slug` is the batch payload plus `radar: { slug, name, workflowUrl }`. The command prints the workflow's name, the batch id, the briefs in their order (position, title, estimated seconds), every open question the drafts carry, then writes `.recorder/radar/<slug>/bundle.json` and one draft per brief in `.recorder/radar/<slug>/briefs/<briefId>.json`. Its answers when something is off, and what you do:
405
+
406
+ - "Not connected to DemoBites" → Phase 0, with the human's word; never open the browser by yourself.
407
+ - "No batch with that code" → the code is wrong; ask the human to check the command on the Update Radar page.
408
+ - "The briefs are not approved yet" → the human approves them on the Update Radar page; wait for their word, then list again.
409
+ - "The recorder key was refused" → the key is stale or belongs to another workspace; Phase 0 again, with their word.
410
+
411
+ ### 1. List
412
+
413
+ `list --slug <slug>` (or read the bundle the human's run wrote). Work from the server's briefs. The batch id on the first lines is the `<batchId>` every other command takes. Run `rules.mjs` now (Phase 1b). Nothing is claimed yet, nothing films.
414
+
415
+ ### 2. Refactor every brief, then ask every question once
370
416
 
371
- Resume after an interruption from what is on disk and on the server: a `take-*` directory with `brief.json` is claimed; with `raw.webm` it was filmed; with `clean.mp4` and `manifest.demobites.json` it is ready to stage; with `staged.json` it is delivered or staged (check it with `status.mjs --no-wait`). `list` shows the server's view of every attempt. Never re-claim a brief that already has your own live attempt; never re-stage one that `staged.json` says is delivered or staged unless the human asked for a new take (`upload.mjs --supersede`).
417
+ **The refactor pass, before any filming.** Walk EVERY brief in the batch against the repository (Phase 3b: routes, navigation, controls, the handlers behind each action, the gates) and the running app (Phase 3a: `vocab.mjs` over every screen the brief names). For each brief:
418
+
419
+ - Confirm the flow exists at the target address. A flow that is not there is said so, not filmed.
420
+ - Replace guessed screens and labels with the real ones, the app's current words (vocab.json is the dictionary).
421
+ - Drop the steps that are not there. Add nothing the brief did not ask for.
422
+ - Tighten `flowIntent` and the narration intent to what is on screen; the outcome stays the last beat; the exclusions stay things the camera never shows; the take stays under `maxSeconds` (90).
423
+
424
+ Write the refined brief to `.recorder/radar/<slug>/refined/<briefId>.json` (copy the draft from `briefs/<briefId>.json` and edit it; same fields — the command keeps only the seven content fields, so the draft's other keys may stay) and post it back: `node scripts/briefs.mjs refine <slug> <briefId> --note "<what you changed and why>"`. The server mints a new revision and the Radar page shows that brief as "Refined on your machine", so the human sees what will be filmed before it is filmed. The command rewrites the bundle with the new revisions; the claim in step 4 takes them. The server's limits, checked before the post and named on failure: title ≤ 80 characters, audience ≤ 60, outcome ≤ 200, flowIntent 2–8 lines of ≤ 120, prerequisites and exclusions up to 6 lines of ≤ 160, estimatedDurationSec a whole number 15–90. Write within them; a refine over a limit is refused, never truncated. A brief that needs no change is not posted. An older DemoBites without the refine route answers so; then film from your refined file and say that the page still shows the cloud draft.
425
+
426
+ **The brief stays a story** (founder ruling, 2026-09-27). What the human approves is narrative only: "this is the story I'd like to tell", the beats in plain words, the outcome. Never a word about zooms, framing, the cursor, verification, selectors or how you direct. Not in the refined brief, not in the refine note, not in the approval text. Directing is your job and it stays behind the scenes.
427
+
428
+ **Double-checked behind the scenes.** Before anything is shown, check every brief against the running app and the repository: go there, open the screens, click the path, see the report land. Correct the storyboard silently. Only a change at the level of the story reaches the refine note, in one line ("the app has no Export here; the story ends on Schedule").
429
+
430
+ **Questions once, for the whole batch.** Only what the app and the code cannot answer. While you walk the briefs, collect every open question: the drafts' `questions[]` (printed by `list`), the account or login the flows need, test data that must exist, feature flags, the URL and environment to film on, and anything else you cannot decide from the code and the app. Ask them in ONE message, numbered, brief by brief, and wait for the answers. Never a question mid-filming: a take that would need one is not ready to film, and it is said so in this message. When there is nothing to ask, say that in one line and go on.
431
+
432
+ Then write every storyboard (Phase 3, the refined brief as the spec, the workspace rules below the laws, vocab.json as the only dictionary) into `.recorder/radar/<slug>/storyboards/<briefId>.json`, and run the headless dry run (Phase 4) for each of them on your own, before the human sees anything. A brief you could not refine into a filmable storyboard is reported with the reason and left without a storyboard; `batch.mjs` skips it and says so.
433
+
434
+ ### 3. One approval for the batch
435
+
436
+ Show the refined storyboards for ALL briefs together, each as a numbered shot list with its three blocks (the shot list with "pointed at, not pressed" beats, "Before the camera", "After the cut"), its estimated length, the rules applied, and what changed against the cloud draft in one line per brief (the story, never the directing). Then ask for one yes for the batch: "Film these 3?". **One word for the batch replaces one approval per brief** (founder ruling, 2026-09-26). The human may strike a brief from the batch in their answer ("film 1 and 3"); that brief gets no storyboard in `storyboards/` (or `--only <briefId,...>` on the run). A no on the batch means back to step 2, not filming a subset. `node scripts/batch.mjs plan <slug>` shows what the run will do: the pool size for this machine and which briefs have a storyboard.
437
+
438
+ ### 4. Film in the background, in parallel
439
+
440
+ On the yes: `node scripts/batch.mjs run <slug>` (run it in the background so you keep answering). It launches the takes together in a small pool sized to the machine and delivers each one as it finishes:
441
+
442
+ - **The pool rule:** 2 takes at a time on a machine with 8 CPU cores or fewer, or 16 GB of memory or less; 3 above that; `--concurrency N` overrides; never more than 4. The rest queue.
443
+ - **One browser profile per take.** Each take films on its own profile directory seeded from `.recorder/profile` (the signed-in session rides along, the profile lock does not), passed to `record.mjs` and `cleanup.mjs` as `RECORDER_PROFILE`, and removed after the take. Never share one Chrome profile between two takes.
444
+ - **Per take, in order, as child processes of the same scripts a single take uses:** `briefs.mjs claim` → `event recording` → `cleanup.mjs --prep` (when declared) → `record.mjs` → `cleanup.mjs` (revert, checks.after, when declared) → `trim.mjs` → `calibrate.mjs` → `manifest.mjs` → `upload.mjs --stage-only --no-open`, the one delivery door: the take becomes a bite by itself, or is kept waiting for recording minutes.
445
+ - **A failed take never stops the others.** The attempt gets `event failed --note "<step>: <why>"`, the take directory stays for diagnosis, the pool goes on. Logs live in `.recorder/radar/<slug>/takes/<briefId>/<step>.log` (and `take.log`, every step in order); `takes/summary.json` is the batch's outcome.
446
+ - **Progress lines.** The script prints one line per event (claimed, filming, filmed, cleaned, delivered with the bite id, kept, failed with the step and the log). Relay them to the human as they land, in the same words. At the end it prints the summary per brief, the Demos grid, and the Radar `workflowUrl`.
447
+
448
+ When the run ends, report per brief what happened, name any take kept waiting for recording minutes (it waits on the Demos grid), name any failed take with its step and what you will change before filming it again (a failed take is filmed again alone with `--only <briefId>`, after the fix, with the human's word), and end with the Update Radar link, `radar.workflowUrl` from the bundle: the workflow page shows each take arriving and the demos it becomes. The human publishes from there; you never publish. `status.mjs --all` shows where each delivered take stands; `status.mjs <takeDir>` waits for one to finish and prints what landed (the Phase 6 receipt law holds: no studio link before the bite is completed).
449
+
450
+ **One demo, not a batch.** Someone who asks for a single demo from a Radar brief, or a single re-film, gets the single-take path: `claim`, storyboard, the yes on it, `record.mjs` on the profile, cleanup, trim, calibrate, manifest, `upload.mjs`. `batch.mjs` is for the batch.
451
+
452
+ Resume like a batch: what is on disk (`take-*` directories, `takes/<briefId>/result.json`) and what `list --slug` reports is the truth, never a second claim on your own live attempt. `batch.mjs run` again films only the briefs whose storyboards are present; pass `--only` for the ones to film again.
372
453
 
373
454
  ## The wire manifest (fixed contract, version 2)
374
455
 
@@ -388,11 +469,13 @@ Resume after an interruption from what is on disk and on the server: a `take-*`
388
469
  click?: { x, y, t }, // frame px + seconds
389
470
  narration?: { text, t, estimated_duration }
390
471
  }],
391
- camera: [{ t_start, t_end, x, y, w, h, label }], // focus rectangles, frame px
472
+ camera: [{ t_start, t_end, x, y, w, h, label, n?, revealed?, wide?, glide? }], // focus rectangles, frame px
392
473
  cuts?: [{ t_start, t_end, transition: 'fade'|'abrupt', n }] // navigation loads, cut out of the bite
393
474
  }
394
475
  ```
395
476
 
477
+ `wide: true` on a camera shot is the camera at 1.0 for the span: the server ends the previous zoom at its start, joins consecutive wides, and writes no zoom for it.
478
+
396
479
  `estimated_duration` is only ever an estimate and nothing downstream treats it as final.
397
480
 
398
481
  ### What the two v2 fields buy
@@ -417,6 +500,14 @@ DELETE <base>/api/recorder/key (Authorization: Bearer <api_key>)
417
500
  GET <base>/api/recorder/rules (Authorization: Bearer <api_key>) // WORKSPACE RULES (1.4): never cached
418
501
  -> { workspaceId, rules: string | null, version, updatedAt } (the claim's api.rules is the same url; workspaceRules on the claim is the snapshot)
419
502
 
503
+ GET <base>/api/recorder/briefs?batch=<batchId> (Authorization: Bearer <api_key>) // BATCH OF BRIEFS: the approved briefs, their attempts, the delivery door
504
+ GET <base>/api/recorder/briefs?slug=<slug> (Authorization: Bearer <api_key>) // UPDATE RADAR (1.6): same payload + radar { slug, name, workflowUrl }
505
+ -> { batch, api, workspaceRules, briefs, radar? } (404 unknown_slug; 409 not_approved; 403 workspace_mismatch)
506
+
507
+ PUT <base>/api/recorder/briefs/<briefId>/refine (Authorization: Bearer <api_key>) // REFACTOR PASS (1.7): the brief refined on this machine
508
+ { revision, contentHash, content, note? } // content = the brief's fields (title, audience, outcome, flowIntent, exclusions, ...)
509
+ -> { revision, contentHash } // the new revision; the Radar page shows "Refined on your machine" (409 hash_mismatch; 410 superseded)
510
+
420
511
  GET <base>/api/recorder/recipe?biteId=<id> (Authorization: Bearer <api_key>) // RE-TAKE: the bite's recipe
421
512
  -> { storyboard, config:{app,url,frame}, manifest, engine } (404 no recipe; 402/403 plan gate)
422
513
 
@@ -426,11 +517,13 @@ PUT <base>/api/recorder/stage (Authorization: Bearer <api_key>)
426
517
  -> { stagingId, uploadUrl, previewUploadUrl, videoKey, previewUrl }
427
518
 
428
519
  GET <base>/api/recorder/stage?id=<stagingId> (Authorization: Bearer <api_key>)
429
- -> { status: 'pending'|'approving'|'approved'|'delivered'|'rejected', biteId, biteUKey, biteStatus, studioUrl }
520
+ -> { status: 'pending'|'approving'|'approved'|'delivered'|'waiting'|'rejected', biteId, biteUKey, biteStatus, studioUrl, resetsAt? }
430
521
 
431
- PUT <base>/api/recorder/stage/<stagingId>/uploaded (Authorization: Bearer <api_key>) // DELIVERY: brief takes only, after both uploads
432
- {} // the url comes from the claim's api.uploaded ("{origin}/api/recorder/stage/{id}/uploaded"); this path is the fallback
433
- -> 200 { biteId, videoId } | 202 { biteId, queued:true } | 200 { pending:true } (older server: wait for the word) | 404/409 { error }
522
+ PUT <base>/api/recorder/stage/<stagingId>/uploaded (Authorization: Bearer <api_key>) // DELIVERY: EVERY take, after both uploads (1.5)
523
+ {} // the url comes from the claim's api.uploaded ("{origin}/api/recorder/stage/{id}/uploaded") when there is one; this path is the fallback
524
+ -> 202 { delivered:true, biteId, studioUrl, dashboardUrl } (also 200 { biteId, videoId } from older servers)
525
+ | 403 { kept:true, waiting:'minutes', resetsAt, dashboardUrl:'/demos', stagingId } (no recording minutes: kept, waits on the grid)
526
+ | 200 { pending:true } (older server: waits for the word in the app) | 404/409 { error }
434
527
  // idempotent: a repeat returns the same bite
435
528
 
436
529
  GET <base>/api/recorder/status?biteId=<id> (Authorization: Bearer <api_key>)
@@ -444,7 +537,7 @@ The upload zip contains exactly one file: `clean.mp4` stored as `recording.mp4`.
444
537
 
445
538
  ## Standing rules
446
539
 
447
- - Anything the human sees (storyboard presentation, review page, questions) uses commas and periods only, no dashes, and real action words. Never orphan a single word on its own line in a heading.
540
+ - Anything the human sees (storyboard presentation, questions, reports) uses commas and periods only, no dashes, and real action words. Never orphan a single word on its own line in a heading.
448
541
  - Never touch credentials. Never print the api_key. Config and key files are chmod 600.
449
- - Never INGEST without the human's explicit word. For the DemoBites ending, staging for the in-app preview is HOW the word is asked — the take becomes a bite only when the human clicks Approve on that page. For a batch of briefs the word was given on the batch and on each storyboard, and delivery ingests by itself. Never publish, never share, never send invitations.
542
+ - Never film without the human's explicit word on the storyboard. That yes, in the chat, is the word for the take: delivery ingests by itself, and the human watches the take on the Demos grid, never on a preview page. For a pasted batch of briefs the word is given on each storyboard; for an Update Radar batch reached by its record code the word is ONE yes on the refined storyboards shown together (founder ruling 2026-09-26), after every question was asked once. Never publish, never share, never send invitations.
450
543
  - One take directory per take, keep failed takes for diagnosis, name them `take-<slug>`, `take-<slug>2`, and so on. A take claimed from a brief is `take-<briefId>-r<revision>`.