@koda-sl/baker-cli 0.354.0-dev.b846ef730 → 0.354.0-dev.e6a354971

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 CHANGED
@@ -32,7 +32,7 @@ export BAKER_GSC_SITE_URL="https://example.com/"
32
32
  # Optional: default X Ads account ID (base36)
33
33
  export BAKER_X_ADS_ACCOUNT_ID="18ce53xyz"
34
34
 
35
- # Required for `baker actions ...` and `baker scheduled-actions create/update/delete` commands that stage against a chat
35
+ # Required for `baker actions ...` and `baker scheduled-agents create/update/delete/unstage` commands that stage against a chat
36
36
  export BAKER_CHAT_ID="<chat-id>"
37
37
  ```
38
38
 
@@ -45,7 +45,7 @@ export BAKER_CHAT_ID="<chat-id>"
45
45
  - `BAKER_GA4_PROPERTY_ID` — default GA4 property ID. Used when `--property-id` is not passed. If neither is set and exactly one property is connected, it is auto-selected.
46
46
  - `BAKER_GSC_SITE_URL` — default GSC site URL. Used when `--site-url` is not passed. If neither is set and exactly one site is verified, it is auto-selected.
47
47
  - `BAKER_X_ADS_ACCOUNT_ID` — default X Ads account ID (base36). Used when `--account-id` is not passed. If neither is set and exactly one X Ads account is connected, it is auto-selected.
48
- - `BAKER_CHAT_ID` — chat context for staged Work Action commands and staged Scheduled Action create/update/delete. `scheduled-actions trigger` does not require it. When set, it is also sent as an `x-baker-chat-id` header on every request, so library additions that apply immediately (images, videos, followed brands) are listed in that chat's changes as already applied.
48
+ - `BAKER_CHAT_ID` — chat context for staged Work Action commands and staged Scheduled agent create/update/delete/unstage. `scheduled-agents run` does not require it. When set, it is also sent as an `x-baker-chat-id` header on every request, so library additions that apply immediately (images, videos, followed brands) are listed in that chat's changes as already applied.
49
49
 
50
50
  ## Debug logging
51
51
 
@@ -98,7 +98,12 @@ All commands return a JSON envelope:
98
98
  ### Unknown options are refused
99
99
 
100
100
  An option a command does not declare stops the run with `UNKNOWN_ARGUMENT`, naming the
101
- closest real one. Previously such an option was accepted and ignored, so a call that
101
+ real one it was most likely reaching for, with the first sentence of that option's own
102
+ description. A word the command spells differently is matched by meaning (`--from` and
103
+ `--since` → `--start-date`, `--to` and `--until` → `--end-date`, `--path` and `--url` →
104
+ `--landing`, each only where the command has it); otherwise only a near spelling is
105
+ offered, so a short option is never pointed at an unrelated one (`--from` at `--full`).
106
+ Previously such an option was accepted and ignored, so a call that
102
107
  looked like it had filtered or narrowed a result was in fact answering the unfiltered
103
108
  question — and nothing in the output said so. Both spellings of an option work
104
109
  (`--run-id` and `--runId`), as they always did. The error's `fix` names the exact call
@@ -700,7 +705,7 @@ Command groups: `budgets`, `campaigns`, `ad-groups`, `keywords` (add/update/remo
700
705
 
701
706
  **Amending a staged op** — `draft amend <ref> (--file patch.json | --patch '<json>')` merges a JSON patch into an already-staged op's payload (objects deep-merge, `null` deletes a key, arrays/scalars replace) and re-validates it in full. Always prefer this over removing and re-staging — it preserves the op's ref (so dependents don't break) and its position in the draft. `draft show <ref>` prints the full staged payload (including warnings/annotations) so you can verify a change looks right before publish.
702
707
 
703
- **Reading an earlier chat's changes** — `draft list` and `draft show` take `--chat <chat-id>` to read another chat in the same company instead of the current one. Every staged-draft family accepts it: `ads google|meta|linkedin draft list|show`, `tag-manager draft list|show`, `actions draft`, `tags draft`, and `scheduled-actions list|get`. A published draft is kept in full, so this returns what that chat actually wrote — every headline, bid and target — plus each op's outcome and error. Use it when asked to redo or reapply earlier work rather than reconstructing copy from a summary. Read-only: staging, `amend`, `remove` and `clear` stay bound to `BAKER_CHAT_ID`, so one chat can never edit another's draft. To re-run only the changes a publish failed to land, use the chat's **Retry** control in the dashboard — it re-sends exactly those and leaves the ones that applied untouched.
708
+ **Reading an earlier chat's changes** — `draft list` and `draft show` take `--chat <chat-id>` to read another chat in the same company instead of the current one. Every staged-draft family accepts it: `ads google|meta|linkedin draft list|show`, `tag-manager draft list|show`, `actions draft`, `tags draft`, and `scheduled-agents list|get`. A published draft is kept in full, so this returns what that chat actually wrote — every headline, bid and target — plus each op's outcome and error. Use it when asked to redo or reapply earlier work rather than reconstructing copy from a summary. Read-only: staging, `amend`, `remove` and `clear` stay bound to `BAKER_CHAT_ID`, so one chat can never edit another's draft. To re-run only the changes a publish failed to land, use the chat's **Retry** control in the dashboard — it re-sends exactly those and leaves the ones that applied untouched.
704
709
 
705
710
  **Already-satisfied writes aren't staged** — an update or pause/resume whose every field already holds the requested value on the live account is dropped instead of staged: the envelope returns `"staged": false, "noop": true` with a `reason` ("this ad is already Paused on Google Ads"), plus a hint. This is a success, not an error — there is nothing to publish. Only a positively-read live snapshot can trigger it; a failed or empty read always stages. Batch responses list the dropped ops under `skipped`, and `count` reflects what actually entered the draft.
706
711
 
@@ -1333,7 +1338,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1333
1338
 
1334
1339
  `--measure` decides whose activity is measured and defaults to `campaigns` (only visitors a Baker landing brought). `--measure all` measures the client's entire website and is a decision somebody has to ask for. `host` is the client's own landing domain when one shares a domain with the site being measured, so the script and its events stay first-party; `firstParty` in the response says which, and removing that domain from Baker means re-running this command.
1335
1340
 
1336
- **Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
1341
+ **Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did. A value that is not a whole positive number (`--page /pricing`) is refused before any request, naming `--landing` as the flag that narrows a report to one page of the site.
1337
1342
 
1338
1343
  **A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit.
1339
1344
 
@@ -3272,63 +3277,83 @@ baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --top-n 10 --out
3272
3277
 
3273
3278
  ---
3274
3279
 
3275
- ### Scheduled Actions (`baker scheduled-actions`)
3280
+ ### Scheduled agents (`baker scheduled-agents`)
3276
3281
 
3277
- Manage company-scoped scheduled recipes that spawn Work Actions later or on a cadence. `create`, `update`, and `delete` stage draft ops on `BAKER_CHAT_ID` and apply when the chat is published. `list` and `get` include draft state when `BAKER_CHAT_ID` is set. `trigger` runs immediately on a published scheduled action and does not require a chat id. Use a regular Work Action instead when there is no date or cadence.
3282
+ A Scheduled agent is a **trigger** and a **prompt**. When the trigger fires, a Session opens with that prompt and does the job — there is no Task in between. `create`, `update`, and `delete` stage draft ops on `BAKER_CHAT_ID` and apply when the chat is published. `unstage` takes back what this chat staged on one item before Publish (`--index <n>` for one change only, by its index in `list` → `draft.ops`). `list` and `get` include draft state when `BAKER_CHAT_ID` is set; `get` on a published item also returns `runsAs` — who each run acts as and which tools it loads. `run` fires a published item immediately and does not require a chat id. Publishing every run unreviewed is switched on by the user on the item, never through the CLI: `--auto-publish` is refused with an `ask_user` fix.
3278
3283
 
3279
3284
  ```bash
3280
- baker scheduled-actions list
3281
- baker scheduled-actions get <id-or-temp_sched_id>
3285
+ baker scheduled-agents triggers # every trigger there is
3286
+ baker scheduled-agents list
3287
+ baker scheduled-agents get <id-or-temp_agent_id>
3282
3288
 
3283
- baker scheduled-actions create --name "Weekly report" --description "Prepare report" --cron "0 9 * * MON" --timezone Europe/Madrid
3284
- baker scheduled-actions create --name "Launch reminder" --description "Check launch" --run-at "2026-09-01T07:00:00Z" --disabled --no-spawn-agent
3285
- baker scheduled-actions create --name "Spend review" --description "Review spend anomalies" --cron "0 9 * * MON" --prompt "Focus on blockers"
3289
+ baker scheduled-agents create --name "Weekly report" --description "Prepare report" --prompt "Review last week's spend." --cron "0 9 * * MON" --timezone Europe/Madrid
3290
+ baker scheduled-agents create --name "Launch reminder" --description "Check launch" --prompt "Check the launch checklist." --run-at "2026-09-01T07:00:00Z" --disabled
3291
+ baker scheduled-agents create --name "Chase a new quote" --description "Follow up while it is warm" --prompt "Read the answers and draft the follow-up." --trigger form.submitted --form quote
3292
+ baker scheduled-agents create --name "Celebrate the first sale" --description "Once, ever" --prompt "Write the announcement." --trigger event.received --event purchase --occurrences 1
3286
3293
 
3287
- baker scheduled-actions update <id-or-temp_sched_id> --enabled false
3288
- baker scheduled-actions update <id-or-temp_sched_id> --run-at "2026-09-02T07:00:00Z" --spawn-agent true --prompt "Use the launch checklist"
3289
- baker scheduled-actions update <id-or-temp_sched_id> --mode publish # runs go live unreviewed
3290
- baker scheduled-actions update <id-or-temp_sched_id> --mode recommend # runs advise and change nothing
3291
- baker scheduled-actions delete <id-or-temp_sched_id>
3294
+ baker scheduled-agents update <id-or-temp_agent_id> --enabled false
3295
+ baker scheduled-agents update <id-or-temp_agent_id> --run-at "2026-09-02T07:00:00Z" --prompt "Use the launch checklist"
3296
+ baker scheduled-agents update <id-or-temp_agent_id> --trigger experiment.ended
3297
+ baker scheduled-agents delete <id-or-temp_agent_id>
3298
+ baker scheduled-agents unstage <id-or-temp_agent_id> # everything this chat staged on it
3299
+ baker scheduled-agents unstage <id-or-temp_agent_id> --index 2 # one staged change only
3292
3300
 
3293
- baker scheduled-actions trigger <id>
3301
+ baker scheduled-agents run <id>
3294
3302
 
3295
- baker scheduled-actions templates --list # every recipe this company can run
3296
- baker scheduled-actions templates --category measurement # one group only
3297
- baker scheduled-actions create --template baker-ads --name "Weekly ad review" --description "..." --cron "0 8 * * MON"
3303
+ baker scheduled-agents templates --list # every recipe this company can run
3304
+ baker scheduled-agents templates --category measurement # one group only
3305
+ baker scheduled-agents create --template baker-ads --name "Weekly ad review" --description "..." --cron "0 8 * * MON"
3298
3306
  ```
3299
3307
 
3308
+ Triggers:
3309
+
3310
+ | `--trigger` | Fires when | Narrow it with |
3311
+ |---|---|---|
3312
+ | *(use `--run-at`)* | Once, at a time you pick | — |
3313
+ | *(use `--cron`)* | On a repeating cadence | — |
3314
+ | `webhook.received` | Another tool posts to this item's own private address | — |
3315
+ | `event.received` | An event arrives from the site or a connected tool | `--event <name>` |
3316
+ | `form.submitted` | Somebody sends answers from a Form | `--form <slug>` |
3317
+ | `flow.step.submitted` | Somebody gets as far as one step of a Form | `--form <slug> --step <id>` |
3318
+ | `experiment.ended` | Somebody ends an A/B test | `--experiment <id>` |
3319
+ | `experiment.concluded` | The winner has been moved into the page | `--experiment <id>` |
3320
+ | `task.completed` | A Task is closed | `--tag <slug>` |
3321
+ | `task.claimed` | An agent picks up a Task | `--tag <slug>` |
3322
+
3300
3323
  Rules:
3301
3324
 
3302
- - `--cron` and `--run-at` are mutually exclusive.
3303
- - `--run-at` must be an ISO UTC timestamp ending in `Z`, and still in the future — a time that has already passed is refused when the op is staged. If a one-off's time passes while the chat is still open, publishing creates the task **switched off** with the reason on it rather than dropping it, so it is never lost.
3304
- - Draft-created ids use `temp_sched_*`; use them with `get`, `update`, or `delete` before publish.
3325
+ - `--cron`, `--run-at` and `--trigger` are mutually exclusive — an item has exactly one trigger.
3326
+ - `--run-at` must be an ISO UTC timestamp ending in `Z`, and still in the future — a time that has already passed is refused when the op is staged. If a one-off's time passes while the chat is still open, publishing creates it **switched off** with the reason on it rather than dropping it.
3327
+ - Leaving a narrowing flag off means "any" — any Form, any event, any test — including ones that do not exist yet.
3328
+ - A narrowing flag (`--form`, `--page`, `--event`, `--config`, …) only goes with `--trigger <kind>`; on its own, or beside `--cron` / `--run-at`, it is refused rather than dropped. `update <id> --timezone <IANA>` on its own keeps the item's trigger and changes only its timezone.
3329
+ - `--when "<field> <operator> <value>"` runs only on payloads that pass, rules joined by `AND` or by `OR` in either case (never both). Operators: `=`, `!=`, `contains`, `does not contain`, `starts with`, `ends with`, `>`, `<`, `>=`, `<=`, `in a,b,c`, `exists`, `missing`.
3330
+ - `--occurrences 1` runs only the first time the trigger ever fires; `--occurrences 1,3` the first and the third. Omit it to run every time. It counts firings of this item's trigger, so a firing that `--occurrences` declines is still counted.
3331
+ - A signal-triggered item will not start a second run while its last one is still going, and never wakes itself.
3332
+ - `--prompt` is the whole job: there is no mode and no `--spawn-agent`. If a run should only advise, the brief says so.
3333
+ - Publishing every run's work unreviewed is a standing authorization only the user can grant, on the item in Baker. The API refuses `autoPublish` outright.
3334
+ - Draft-created ids use `temp_agent_*`; use them with `get`, `update`, or `delete` before publish.
3305
3335
  - Staged commands require `BAKER_CHAT_ID` to point to an `in_progress` chat.
3306
- - `trigger` rejects `temp_sched_*` and only accepts published scheduled-action ids.
3336
+ - `run` rejects `temp_agent_*` and only accepts published ids. It returns the `chatId` of the Session it opened.
3307
3337
  - Company Timezone is the default; `--timezone` is an explicit IANA override.
3308
- - `--mode` sets how far a run goes on its own: `remind` (opens the Task, runs nothing), `recommend` (writes up what it would change, touches nothing), `propose` (does the work, waits to be published — the default), `publish` (does the work and publishes it unreviewed). It applies when the chat is published, on the authority of whoever published it; a chat completed with no person behind it lands a `publish` task on `propose` instead.
3309
- - `--mode` supersedes `--spawn-agent`/`--no-spawn-agent`, which can only reach `remind` and `propose`. Passing both is rejected.
3310
- - Scheduled actions spawn agents by default; create with `--no-spawn-agent` or update with `--spawn-agent false` to disable it.
3311
- - `--prompt` maps to the scheduled action's additional spawned-agent instructions (`agentPrompt`), not a replacement for Baker's automatic spawned Work Action reference.
3312
- - `--prompt` requires a run that does the work: it is rejected alongside `--mode remind`, `--no-spawn-agent` (create) or `--spawn-agent false` (update), since a bypassed agent never reads it.
3313
- - `--template <id>` binds the schedule to a marketplace recipe instead of a prompt you wrote. The row stores the id alone: name, brief and prompt resolve at fire time, so the job improves when the recipe does. `--description` carries what is true of this company. One row per recipe per company; a second `create` for the same id is refused.
3338
+ - `--template <id>` copies a marketplace recipe's brief onto the item — a copy, not a link, so the owner can read and edit every word, and a later change to the recipe does not reach it. `--prompt` is appended after it. The same recipe can be used more than once.
3314
3339
 
3315
3340
  #### Marketplace recipes (`templates`)
3316
3341
 
3317
- `templates` lists every recipe a company can switch on — the ones Baker maintains plus any the company wrote — and is also how one of theirs is written. Recipes we maintain take priority: a company recipe can never use one of our ids, and resolution reads ours first.
3342
+ `templates` lists every recipe a company can build an item from — the ones Baker maintains plus any the company wrote — and is also how one of theirs is written. Recipes we maintain take priority: a company recipe can never use one of our ids, and resolution reads ours first.
3318
3343
 
3319
3344
  ```bash
3320
- baker scheduled-actions templates --list --drafts # include theirs not offered yet
3345
+ baker scheduled-agents templates --list --drafts # include theirs not offered yet
3321
3346
 
3322
3347
  # Writing one of theirs, in this order:
3323
- baker scheduled-actions templates --save weekly-posthog-check \
3348
+ baker scheduled-agents templates --save weekly-posthog-check \
3324
3349
  --name "Weekly PostHog check" \
3325
3350
  --tagline "What moved in the funnel, from PostHog rather than GA4." \
3326
3351
  --recipe-category measurement --frequency weekly \
3327
3352
  --brief-file ./brief.md
3328
3353
 
3329
- # ...schedule it with --template, trigger it, read what the run produced, then:
3330
- baker scheduled-actions templates --publish weekly-posthog-check
3331
- baker scheduled-actions templates --delete weekly-posthog-check
3354
+ # ...build an item from it with --template, run that item, read what it produced, then:
3355
+ baker scheduled-agents templates --publish weekly-posthog-check
3356
+ baker scheduled-agents templates --delete weekly-posthog-check
3332
3357
  ```
3333
3358
 
3334
3359
  - **Saving does not offer it.** A saved recipe is invisible and cannot be scheduled until `--publish`. The step between the two is running it: nothing enforces that, and it is the only way to know it produces anything worth reading.
@@ -3447,7 +3472,7 @@ baker actions create --name "Build hero" --priority high --tags landing,creative
3447
3472
  # (temp_hero_copy, not hero_copy) and is rejected with a VALIDATION_ERROR otherwise — the prefix is
3448
3473
  # what tells a task staged in this chat apart from a published action id in link/update/complete/draft remove.
3449
3474
  # If the name/description reads as recurring or future-dated (e.g. "weekly", "every Monday", "schedule"),
3450
- # create emits a hint to use `baker scheduled-actions create` instead of a Work Action.
3475
+ # create emits a hint to use `baker scheduled-agents create` instead of a Work Action.
3451
3476
  # If it instead names work a surface executes in-chat (a GTM container change, a site tag, an ad-platform
3452
3477
  # write, a page or form edit, or a value only the user has), create emits a do-it-now hint naming that
3453
3478
  # surface — doing the work is the default, an action is the exception. One advisory hint per call: the
@@ -3663,7 +3688,7 @@ baker chats diff <id> --live # in-progress Session: read from its l
3663
3688
  ```
3664
3689
 
3665
3690
  **`list` flags:** `--status active|archived|all` (default `all`), `--limit` (1-100, default 20), `--full` (include each Session's full change list).
3666
- **`view`:** returns the unified `effects` — git content (`content`, `creatives`) AND DB-only effects that never touch the repo (`actions`, `scheduledActions`, `tags`, `ads.{google,meta,linkedin,x}`, `tagManager`, `ga4`, `funnel` (with the ways in it added or removed, `triggersAdded`/`triggersRemoved`), `conversions`, `conversionSends`, `eventSources`), plus `otherChanges` for every other change the Session made (A/B tests, recipes, library images), each resolved to real detail and flagged `staged` (in-progress draft) vs applied. Plus `kickoff`, `outputs` counts, `threads`, `commits`, `diffAvailable`.
3691
+ **`view`:** returns the unified `effects` — git content (`content`, `creatives`) AND DB-only effects that never touch the repo (`actions`, `scheduledAgents`, `tags`, `ads.{google,meta,linkedin,x}`, `tagManager`, `ga4`, `funnel` (with the ways in it added or removed, `triggersAdded`/`triggersRemoved`), `conversions`, `conversionSends`, `eventSources`), plus `otherChanges` for every other change the Session made (A/B tests, recipes, library images), each resolved to real detail and flagged `staged` (in-progress draft) vs applied. Plus `kickoff`, `outputs` counts, `threads`, `commits`, `diffAvailable`.
3667
3692
  **`transcript` flags:** `--limit` (1-1000, default 100, most recent), `--full` (untruncated text). Returns `entries` (`role` + `text`) and `truncated`.
3668
3693
  **`diff` flags:** `--surface knowledge|company|brand|landings|flows|creatives`, `--full` (patch text per file), `--live` (read an in-progress Session's changes from its live workspace). Returns `available`, `source` (`published`|`live`), `files` (`path`, `surface`, `status`, `additions`, `deletions`, `patch`), and `truncated` when a large live diff was capped.
3669
3694
 
@@ -5392,7 +5417,7 @@ Before the deconstruct it runs a **local shot-cut pass** on the source file with
5392
5417
 
5393
5418
  A shot longer than the video model's per-clip ceiling (Seedance's 15s, passed as `video_deconstruct`'s `max_clip_s`) is split into equal **continuation sub-scenes** that share their splice boundary exactly — so a long shot is reproduced in **full** (no truncation) and joins seamlessly. Each sub-scene carries `continues_previous`.
5394
5419
 
5395
- It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) only in an ad with **nobody in it** — no deconstructed cast and no recurring person. In any ad with people, no generated frame takes a source frame as a reference: an image model copies the face of whoever is in its references, whatever the prompt says, and which scenes a person is in is itself a model's guess — so framing is carried by the frame prompt instead. **People are cast from company avatars** (`--avatar <handle>`): each recurring person grounds on that avatar's identity sheet, used as-is (no sheet is built from it); a person with no avatar gets a `[TODO]` asking for one, never for a found photo. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5420
+ It then scaffolds the full pipeline like an **editing timeline**: each clip gets a **static-ad-grade start AND end keyframe** (`image_generate`, each with its **own `params.prompt`** regenerated from that scene's `scenes/sNN.json` — edit the scene file to change that frame, never the node prompt by hand; the slim `prompt.style.json` wired as the **shared `target_blueprint`** style reference, plus a per-element reference legend). Each keyframe is **fully recast** to the dropped `el_*` reference images. The original extracted frame is kept LAST as a **pure composition anchor** (framing / camera angle / shot size / pose) whenever identity is safely locked — i.e. a frame with no person/animal, OR every cast member present is **sheet-backed** (a multi-view turnaround owns identity, so the anchor can reproduce the source's framing without dictating the face). Since every base element is now sheet-backed by default, cast frames keep their framing anchor too — this is what reproduces the source's composition (a side-profile stays a side-profile, the camera angle holds scene to scene) instead of drifting to a fresh guess. The anchor's legend forbids taking identity/text/palette from it. It is dropped only when a cast member rests on a weak lone-snapshot reference (e.g. a `same_as` second-look slot), where the original frame could re-leak the source actor. Both keyframes feed `video_generate` (`first_frame`+`last_frame`, so Seedance interpolates real in-shot motion; ultra-detailed motion brief; duration snapped to the nearest allowed clip length). Every keyframe grounds **only on its own extracted frame + `el_*` slots** — no reference to any other generated frame — so all images render **in parallel** (no cascade). Source-frame URLs are **deduped** (each ingested once). `--frames reuse` wires the real source frame straight in.
5396
5421
 
5397
5422
  **Composited scenes (split-screen / picture-in-picture / keyed presenter).** Real ads aren't always one full-frame shot — a frame can be **persistently divided** (b-roll on top, a presenter talking on the bottom) or **layer a presenter** over background footage (boxed in a corner, or green-screen keyed). The deconstruct now reports this per scene as `scene.composition` (`layout: split_screen | pip | keyed_overlay`, with one `region` per stream — each its own clean-plate frame + motion brief, the talking-head region flagged `is_presenter`). The scaffold reproduces a composited scene by building **one clip per region** (`s<i>_r0_*`, `s<i>_r1_*`, …) and compositing them with ffmpeg: a split-screen `vstack`/`hstack` (stack direction read from the region **panels**, so a top/bottom split always stacks vertically), or a picture-in-picture `overlay` of the presenter inset at its corner. A **keyed** presenter is first cut to transparency by `video_background_remove` (`s<i>_key`), then overlaid. The presenter region carries the native lip-synced voice; b-roll/render panels stay silent. To change a layout, edit `composition` in that scene's `scenes/sNN.json` and re-scaffold, or hand-edit the `s<i>_composite` ffmpeg args. Plain full-frame scenes (the default) are unaffected.
5398
5423
 
@@ -5437,7 +5462,7 @@ The emitted canvas is validated (`validateCanvasDeep`) before it's written, so i
5437
5462
  ```bash
5438
5463
  baker canvas scaffold-video ./reference-ad.mp4 --focus "competitor UGC ad for <brand>"
5439
5464
  # → writes ./reference-ad.video.canvas.json + ./prompt.json (global) + ./scenes/sNN.json (per scene) + ./prompt.rebuild.json (+ video-overlay-composition/)
5440
- # cast the reference's people with --avatar <handle> (baker avatars list); edit scene files, fill every other el_* [TODO], confirm voices, then:
5465
+ # edit each frame's own params.prompt + drop ONE real source image at each el_* [TODO], confirm voices, then:
5441
5466
  baker canvas validate ./reference-ad.video.canvas.json
5442
5467
  baker canvas run ./reference-ad.video.canvas.json
5443
5468
  ```
@@ -5460,7 +5485,6 @@ baker canvas run ./reference-ad.video.canvas.json
5460
5485
  | `--motion-transfer` | off | Motion driven from a reference / hyper-dynamic → the router picks Kling. |
5461
5486
  | `--identity` | off | Same character/product across clips → the router picks the Seedance workhorse. |
5462
5487
  | `--budget <tier>` | `standard` | Router cost posture: `economy` (cheap Veo-fast tier) / `standard` / `premium`. |
5463
- | `--avatar <handles>` | — | Cast company avatars as the reference's people, comma-separated in the order they appear (`marta` or `marta,joan`). Each recurring person grounds on that avatar's identity sheet and takes its subject description; a `same_as` look of them uses the same sheet. Resolved before anything is billed — a handle that is not `ready` fails the command. |
5464
5488
  | `--resolution <res>` | `1080p` | Output resolution for every generated clip (`480p`/`720p`/`1080p` for Seedance). The model defaults to a LOW tier when unset, which downscales the 2K keyframes — pinning the top tier keeps the clip as sharp as its frames. |
5465
5489
 
5466
5490
  **Scored model router.** When `--video-model` is omitted, a small weighted scorer picks from the curated
@@ -6206,6 +6230,7 @@ This CLI is designed for AI agent consumption. Key patterns:
6206
6230
  - **0.177.0**: `baker ga4` can change what a property measures, not only report on it. Five staged write families — `key-event` (create|update|delete), `custom-dimension` and `custom-metric` (create|update|archive), `custom-event` (create|update|delete), `data-retention set` — plus `baker ga4 config` to read the property's current configuration and `baker ga4 draft` (list|show|amend|remove|clear) to review the staged set. `custom-event` is GA4's "Create event" rule: build a new event out of one the site already sends (a `form_submit` on `/quote` becomes `quote_request`), then mark it a key event so Google Ads imports it — the loop that previously ended with "do this by hand in Analytics". Staging follows Tag Manager: nothing is sent during the chat, the user reviews the whole set in the dashboard's **Analytics** tab, and it applies when they complete the chat. Unlike Tag Manager there is no version left over — an applied change is in effect at once, and there is no undo, so removals ask for confirmation first. Everything Google would reject is refused at stage time against a fresh read of the property rather than discovered at publish: a duplicate `eventName` or `parameterName` (including against another change staged on the same chat), an immutable field in an update, a currency metric with no restricted type, a retention window past 14 months on a standard property, a target the property does not have. Marking an event Google fires by itself (`page_view`, `scroll`, …) as a key event is a **warning**, not a refusal — it is legal, it just inflates conversions and poisons Smart Bidding. The one thing that is *not* a duplicate is the replacement of something the same chat has already staged the removal of: ops apply in stage order, so `custom-dimension archive 12` followed by a create on the same `parameterName` works, which is the only way to change a field GA4 marks immutable. Every `create` also takes a JSON array and stages the list in one request, because each stage costs a full read of the property. One chat stages changes for one property. **The OAuth scope changed:** Baker now requests `analytics.edit`, so every Google Analytics connection made before this is read-only until it is reconnected; the write commands refuse up front with that as the fix, and `baker ga4 config` reports `canWrite`. Reporting is unaffected. Not covered, and reported as such by `baker capabilities`: property create/rename/merge, data streams, the Google Ads link, and audiences.
6207
6231
  - **0.169.0**: `baker capabilities` — one call, before any platform work, that answers what Baker **cannot** do. Everything else (`baker schema`, the family docs, `--help`) describes what it can, so the only way to learn a capability was absent was to try it, usually after telling the user it would happen. Per surface (`google-ads`, `meta-ads`, `linkedin-ads`, `x-ads`, `tag-manager`, `ga4`, `gsc`) it reports: what is connected and which accounts/properties/containers were picked; whether writes are **live** or **simulated** for this company, and that Tag Manager has neither mode — completing the chat changes the real container; and a `limits` list separating *not built yet* from *the platform forbids it* from *we hold no permission for it*, because each leads to something different to tell the user. Naming one surface (`baker capabilities google-ads`) also returns `ops`: every change with its required and optional fields and the exact values each accepts — so "is there a field to demote a conversion action, and how is it spelled?" is one call instead of four guesses. Almost everything is derived rather than written down: the accounts and permissions come from the connection, the write mode from the company's gate, the writable entities and field contracts from the same schemas the backend parses with, and read-only vs read-write from the OAuth scopes the app actually requests (which is why "GA4 is read-only" is a fact, not a sentence). The handful of hand-written entries each carry a machine-checkable claim about the code behind them, and a coverage test fails when one stops holding. A rejected staged write on Google, Meta or LinkedIn now points at this command instead of leaving the agent to guess another field name.
6208
6232
  - **0.178.0**: `baker ads google keywords add --final-url` is now described and nudged as the exception it is. A keyword-level final URL overrides the ad's landing page for every click on that keyword, and the flag applies to every keyword in the call — so one URL pasted across a batch restates the ad's own destination on each row while making the change review read like a per-keyword routing decision. The flag's description says to leave it unset unless the destination is keyword-specific, and staging with it returns a hint: on a batch, that the shared page belongs on the ad (`ads create --final-url`) and the keywords should be re-staged without it; on a single keyword, to keep it only where that keyword's destination differs. The review surface changed to match — a staged keyword's final URL is now shown in full on its row in the change card (it used to be visible only on a keyword *update*), so a URL that adds nothing is visible to the user rather than buried.
6233
+ - `baker capabilities` sends the person the chat acts as (`BAKER_ACTING_USER_ID`), so `tools` now also lists that person's own connected tools, each noted as personal, and every organization-wide tool, which it used to miss. A scheduled agent's run acts as the item's owner, so this is the list of what that run can reach.
6209
6234
  - **0.179.0**: `baker ads google keywords update --final-url` / `--clear-final-url` — changing where one keyword sends its clicks no longer costs the keyword its history. The write contract has always accepted `finalUrls` on a keyword update (`baker capabilities google-ads` reported it as a field of `google.keyword.update`), but no CLI flag reached it, so an agent that followed capabilities got "update needs at least one field" and the only route left was remove + re-add — a new criterion to Google, learning from zero, for a change that was only ever about the destination. `--clear-final-url` stages the empty set Google reads as "inherit the ad's landing page", which is the undo for the misuse `--final-url` already warns about. Same nudge as `add` on the way in.
6210
6235
 
6211
6236
  A keyword's final URL now also shows on its row when the keyword is being **removed**. 0.178.0 read it from the stage-time snapshot, but the snapshot never asked Google for it, so the removal half of that change silently did nothing — the card said which keyword was going away but not which landing page it had been sending its clicks to. The batch hint says outright that `--final-url` has no per-keyword form rather than implying destinations could differ within one call, and the tool doc now lists each `keywords` subcommand's own flags instead of one shared set (`--ad-group-ref`, `--text` and `--match-type` are `add`-only; `remove` takes a target and nothing else).