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/README.md +14 -13
- package/launcher/index.mjs +72 -7
- package/package.json +1 -1
- package/scripts/calibrate.mjs +17 -3
- package/scripts/record.mjs +144 -15
- package/skill/SKILL.md +126 -33
- package/skill/scripts/batch.mjs +328 -0
- package/skill/scripts/briefs.mjs +163 -9
- package/skill/scripts/cleanup.mjs +2 -1
- package/skill/scripts/manifest.mjs +13 -1
- package/skill/scripts/retake.mjs +73 -15
- package/skill/scripts/stage-wait.mjs +172 -8
- package/skill/scripts/status.mjs +44 -18
- package/skill/scripts/upload.mjs +72 -67
- package/skill/scripts/vocab.mjs +2 -1
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
|
|
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.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the
|
|
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 `
|
|
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
|
|
320
|
+
node scripts/upload.mjs <takeDir> # STAGE + DELIVER the take, open the Demos grid (refuses an uncleaned take)
|
|
304
321
|
```
|
|
305
322
|
|
|
306
|
-
**
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
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 `
|
|
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)
|
|
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
|
-
|
|
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:
|
|
432
|
-
{} // the url comes from the claim's api.uploaded ("{origin}/api/recorder/stage/{id}/uploaded"); this path is the fallback
|
|
433
|
-
->
|
|
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,
|
|
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
|
|
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>`.
|