@koda-sl/baker-cli 0.440.0-dev.dab0aaba9 → 0.440.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.
Files changed (30) hide show
  1. package/README.md +96 -22
  2. package/dist/{chunk-APTNJ2EY.js → chunk-6PP3MBGZ.js} +320 -133
  3. package/dist/chunk-6PP3MBGZ.js.map +1 -0
  4. package/dist/{chunk-UKHD423A.js → chunk-7THIX4WW.js} +8 -5
  5. package/dist/{chunk-UKHD423A.js.map → chunk-7THIX4WW.js.map} +1 -1
  6. package/dist/{chunk-5DJPTQU5.js → chunk-EURJVJQY.js} +2 -2
  7. package/dist/{chunk-PR3CXBAT.js → chunk-F7LXMR73.js} +13 -4
  8. package/dist/chunk-F7LXMR73.js.map +1 -0
  9. package/dist/{chunk-32UJLGZX.js → chunk-QLNGHVRT.js} +11 -3
  10. package/dist/chunk-QLNGHVRT.js.map +1 -0
  11. package/dist/{chunk-KP33GIDZ.js → chunk-VNROEZTR.js} +4 -4
  12. package/dist/cli.js +15881 -12481
  13. package/dist/cli.js.map +1 -1
  14. package/dist/{client-JM3QBGR2.js → client-TCDMWAL3.js} +4 -4
  15. package/dist/engine/index.d.ts +3 -6
  16. package/dist/engine/index.js +3 -3
  17. package/dist/{env-WIAWLTDN.js → env-ZPCFZJN5.js} +4 -2
  18. package/dist/output-CW4JBWIM.js +22 -0
  19. package/dist/{shared-DJTAT7CM.js → shared-6I2TP4YQ.js} +6 -6
  20. package/package.json +1 -1
  21. package/dist/chunk-32UJLGZX.js.map +0 -1
  22. package/dist/chunk-APTNJ2EY.js.map +0 -1
  23. package/dist/chunk-PR3CXBAT.js.map +0 -1
  24. package/dist/output-UGMWOFQR.js +0 -18
  25. /package/dist/{chunk-5DJPTQU5.js.map → chunk-EURJVJQY.js.map} +0 -0
  26. /package/dist/{chunk-KP33GIDZ.js.map → chunk-VNROEZTR.js.map} +0 -0
  27. /package/dist/{client-JM3QBGR2.js.map → client-TCDMWAL3.js.map} +0 -0
  28. /package/dist/{env-WIAWLTDN.js.map → env-ZPCFZJN5.js.map} +0 -0
  29. /package/dist/{output-UGMWOFQR.js.map → output-CW4JBWIM.js.map} +0 -0
  30. /package/dist/{shared-DJTAT7CM.js.map → shared-6I2TP4YQ.js.map} +0 -0
package/README.md CHANGED
@@ -46,6 +46,7 @@ export BAKER_CHAT_ID="<chat-id>"
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
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
+ - `BAKER_THREAD_ID` — set per turn by the sandbox. Sent as an `x-baker-thread-id` header alongside the chat header, so the tools a command reports or changes are those of whoever sent this turn, even while someone else writes in another thread of the same chat.
49
50
 
50
51
  ## Debug logging
51
52
 
@@ -620,6 +621,18 @@ baker ads google campaigns create --customer-id 1234567890 --name "Brand — Sea
620
621
  # New campaigns are declared as NOT containing EU political advertising (Google requires the
621
622
  # declaration on every create); pass --eu-political-ads only when the campaign does contain it.
622
623
  # On update, --eu-political-ads / --eu-political-ads=false corrects an existing declaration.
624
+ # Text guidelines (AI Max Search and Performance Max) stage on update only:
625
+ baker ads google campaigns update <campaign-id> --customer-id 1234567890 \
626
+ --term-exclusion "cheap" --messaging-restriction "Never promise a delivery date"
627
+ # --term-exclusion (repeatable, up to 25 × 30 chars) and --messaging-restriction (repeatable,
628
+ # up to 40 × 300 chars) each REPLACE the campaign's list; --clear-term-exclusions /
629
+ # --clear-messaging-restrictions empty one. A Search campaign needs AI Max with text
630
+ # customization on (in the same update or before), or staging is refused with the reason.
631
+ # AI Max for Search stages on create and update: --ai-max / --ai-max=false (off turns both
632
+ # features off too), --ai-max-text (text customization, needs AI Max) and
633
+ # --ai-max-url-expansion (final URL expansion, needs text customization). An omitted switch
634
+ # keeps the campaign's value; a combination Google refuses is refused at staging.
635
+ baker ads google campaigns update <campaign-id> --customer-id 1234567890 --ai-max --ai-max-text
623
636
  baker ads google ad-groups create --customer-id 1234567890 --name "Brand terms" --campaign-ref g_temp_<campaign>
624
637
  baker ads google keywords add --customer-id 1234567890 --ad-group-ref g_temp_<adgroup> --text "brand name" --match-type EXACT
625
638
  # Shared negative keyword list → attach to the campaign
@@ -1116,10 +1129,24 @@ baker ads linkedin analytics --level campaign --campaign-id 1234 \
1116
1129
  # "Top companies seeing this campaign" (ABM feedback loop) — org names auto-resolved
1117
1130
  baker ads linkedin top-companies --campaign-id 1234 --last-days 30
1118
1131
 
1132
+ # "Which companies are our ads reaching?" — one row per company over the whole period,
1133
+ # most engaged first ({org, name, impressions, clicks, engagements, costLocal}).
1134
+ # Defaults: last 12 calendar months, the only selected ad account, top 100 (--full for all).
1135
+ baker ads linkedin companies --account-id 1234
1136
+ baker ads linkedin companies --page 2 # companies 101-200, most engaged first
1137
+ baker ads linkedin companies --by month --min-impressions 50 # months[] per company
1138
+ baker ads linkedin companies --campaign-id 1,2 --by campaign # campaigns[] per company
1139
+ baker ads linkedin companies --with-domains --stats --output csv # + domain/domainSource; read-quality stats
1140
+
1119
1141
  # "Who are these org URNs?" — resolve org URNs to company names (probe leakage)
1120
1142
  baker ads linkedin resolve --urns urn:li:organization:17719,urn:li:organization:19022
1121
1143
  baker ads linkedin resolve --ids 17719,19022 --output csv
1122
1144
 
1145
+ # "What are these companies' website domains?" — for matching against CRM companies.
1146
+ # Per org: vanityName (LinkedIn page), website, domain, domainSource (website | subdomain | none);
1147
+ # ids LinkedIn doesn't know land in `errors`. Shortlinks are never followed.
1148
+ baker ads linkedin resolve --ids 1035,1337 --with-domains
1149
+
1123
1150
  # "Who is in our audience by industry/seniority/function/title — all at once?"
1124
1151
  baker ads linkedin demographics --campaign-id 1234 --last-days 30
1125
1152
 
@@ -1156,7 +1183,8 @@ baker ads linkedin bid-pricing --account-id 503001492 \
1156
1183
  **`analytics`** — the 3-axis workhorse:
1157
1184
  - `--level account` (override: `campaign-group|campaign|creative`)
1158
1185
  - `--intent baseline` field bundle. Intents: `baseline | revenue | funnel | engagement | video | lead-gen | inmail | document | ranking | identity`. List with `--list-intents`.
1159
- - `--pivot none`. Pivots: `none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button`. List with `--list-pivots`.
1186
+ - `--pivot none`. Pivots: `none | campaign | campaign-group | creative | company | account | conversion | job-title | job-function | seniority | industry | company-size | country | region | dma | device | placement | serving-location | card-index | objective | conversation-node | conversation-node-button`. List with `--list-pivots`.
1187
+ - `--pivot dma` — US Nielsen Designated Market Areas, with region names resolved. LinkedIn's terms: any report built from it writes "DMA®" each time and carries "DMA® is a registered service mark of The Nielsen Company." — the `DMA_ATTRIBUTION_REQUIRED` warning repeats this on every call.
1160
1188
  - `--granularity DAILY`. Auto-forced to `ALL` when pivoting on a `MEMBER_*` dim (LinkedIn rejects DAILY + demographic) — surfaced in stderr + `warnings`.
1161
1189
  - `--last-days 7` unless you pass `--start`/`--end`.
1162
1190
  - Sort: `costInUsd` descending. `--no-sort` to disable.
@@ -1188,6 +1216,7 @@ creatives # default ACTIVE intended-status
1188
1216
 
1189
1217
  analytics # see "Smart defaults" above (3-axis: level × intent × pivot)
1190
1218
  demographics # sweep all firmographic pivots in one call
1219
+ companies # which companies the ads reached, one row per company (--by month|campaign, --with-domains)
1191
1220
  top-companies # pivot=MEMBER_COMPANY shortcut (ABM)
1192
1221
  conversation # pivot=CONVERSATION_NODE_BUTTON shortcut (Sponsored Messaging)
1193
1222
 
@@ -1207,6 +1236,7 @@ audit --format md # deliverable-ready markdown table
1207
1236
  campaign-groups create|update|pause|resume|duplicate # staged writes (see below)
1208
1237
  campaigns create|update|pause|resume|archive|duplicate
1209
1238
  campaigns url-params <ad-set-id> # UTMs for every ad in the ad set
1239
+ campaigns exclude-companies <ad-set-id> --org <ids> # stop an ad set reaching these companies
1210
1240
  account url-params # UTMs for every ad in the whole account
1211
1241
  url-params [--ad-sets] # read what is tagged now, and what lands twice
1212
1242
  creatives create|update|pause|resume|duplicate
@@ -1247,12 +1277,21 @@ baker ads linkedin account url-params --param "utm_source=linkedin&utm_medium=pa
1247
1277
  baker ads linkedin campaigns update 123456 --audience-expansion off --lan off
1248
1278
  baker ads linkedin campaigns update 123456 --bid 7.50 --daily-budget 100 --currency USD
1249
1279
 
1250
- # Matched audiences (CSV with header; email columns SHA-256 hashed locally)
1280
+ # Matched audiences (CSV with header; email columns SHA-256 hashed locally).
1281
+ # Company lists take companyName,domain,organizationUrn — organizationUrn matches the company page exactly
1251
1282
  baker ads linkedin audiences create --name "ABM Tier-1" --type company-list --list-file accounts.csv
1252
1283
 
1284
+ # Stop an ad set reaching customers / open deals — add-only; at publish Baker re-reads the ad set
1285
+ # and merges into its targeting as it stands then. --org takes company ids, ≤200 per call;
1286
+ # a longer list goes into a company-list audience passed as --segment
1287
+ baker ads linkedin campaigns exclude-companies 123456 --org 1035,1441
1288
+ baker ads linkedin campaigns exclude-companies 123456 --segment urn:li:dmpSegment:42
1289
+
1253
1290
  # Conversions with playbook-default windows
1254
1291
  baker ads linkedin conversions create --name "Demo booked" --type LEAD --method INSIGHT_TAG \
1255
1292
  --post-click-window 30 --view-window 7
1293
+ # Windows are 1|7|30|90 days; 180 and 365 only for LEAD, QUALIFIED_LEAD, MARKETING_QUALIFIED_LEAD,
1294
+ # SALES_QUALIFIED_LEAD, PURCHASE, ADD_TO_CART, SUBMIT_APPLICATION — refused at stage time otherwise
1256
1295
 
1257
1296
  # Safe duplication — stage-time read of the source → staged DRAFT create ("Duplicate ad", never "Link to original")
1258
1297
  baker ads linkedin campaigns duplicate 123456 --name "New variant"
@@ -1311,7 +1350,7 @@ Each finding: `{id, area, check, status, severity, evidence, fix: {explanation,
1311
1350
  - OAuth tokens auto-refresh server-side. On 401, reconnect LinkedIn from dashboard → integrations.
1312
1351
  - Account scoping: backend rejects calls against an account not in the picker selection.
1313
1352
  - Pinned API version: `Linkedin-Version: 202606` (declared in `oauth/constants.ts`). Bump as a coordinated change — LinkedIn deprecates monthly with a ~12-month support window. The floor is `TRACKING_PARAM_DYNAMIC_MIN_API_VERSION` (`CREATIVE_NAME` needs 202606), asserted in `ads/linkedin/utils.test.ts`.
1314
- - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, bid-pricing/forecast 6h, audience-size 1h.
1353
+ - Cache TTLs: accounts/account-detail 1h, listings 30m, analytics 15m–6h (depends on date range), facets 7d, urn-resolve 1h, org domains 7d, bid-pricing/forecast 6h, audience-size 1h.
1315
1354
  - `--no-cache` on any command skips both cache layers (the CLI's own and the server's).
1316
1355
 
1317
1356
  #### LinkedIn-specific gotchas
@@ -1440,7 +1479,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1440
1479
 
1441
1480
  **`tracking` answers "can this account be reported on at all", and it reads the ad account to do it.** `analytics ads` can only describe parameters that have already arrived, which leaves two blind spots that mislead in the expensive direction: a platform with no traffic in the window looks exactly like a platform whose ads carry nothing, and a dimension arriving under a name nobody has mapped looks exactly like one nothing carries. `baker analytics tracking` reads the mapping, the traffic **and** the client's own Google tracking templates / Meta URL parameters, so it can answer for a platform that has not had a click yet. Every dimension comes back in one of four states, and they are four different jobs: `covered` is done; `unmapped` means the URLs already carry it under a name Baker does not read, so run the `mapCommand` in the response — free, and it repairs the traffic already collected; `missing` means nothing carries it; `unknown` means no visit arrived *and* the account could not be read, which is not a finding and must never be reported as untagged.
1442
1481
 
1443
- **`fix` says which level to put the missing pairs at, and who can write it.** Not a field name — the levels that platform accepts tagging at, widest first (the widest is the one to prefer: it covers campaigns nobody has created yet), which of them Baker can stage and which the user has to do by hand, and how each field treats a value already in it. That last part is the one people skip and it differs per platform: a Google **campaign** final URL suffix *replaces* the account one rather than merging, LinkedIn's `url-params` call *sets the ad set's whole parameter set* and Baker cannot read back what is in it, and a Meta creative is immutable so a live ad cannot be retagged through Baker at all. `suffix` is only ever the **missing** pairs, so the rule everywhere is the same: read what is there, work out what is missing, write the union. Where `fix` says Baker has no write surface — Microsoft, TikTok, Reddit, Pinterest, Snapchat — that is a request to hand the user with the exact string and the exact screen, never a reason to stop on the platforms it can write.
1482
+ **`fix` says which level to put the missing pairs at, and who can write it.** Not a field name — the levels that platform accepts tagging at, widest first (the widest is the one to prefer: it covers campaigns nobody has created yet), which of them Baker can stage and which the user has to do by hand, and how each field treats a value already in it. That last part is the one people skip and it differs per platform: a Google **campaign** final URL suffix *replaces* the account one rather than merging, LinkedIn's `url-params` call *sets the ad set's whole parameter set* and Baker cannot read back what is in it, and a Meta creative is immutable so a live ad cannot be retagged through Baker at all. `suffix` is only ever the **missing** pairs, so the rule everywhere is the same: read what is there, work out what is missing, write the union. Where `fix` says Baker has no write surface — Microsoft, TikTok, Reddit, Pinterest, Snapchat — that is a request to hand the user with the exact string and the exact screen, never a reason to stop on the platforms it can write. ChatGPT Ads is one Baker can write: its campaign and ad group `--tracking-template` (`baker ads chatgpt campaigns|ad-groups update`), which should carry `campaign_id={campaign_id}&ad_group_id={ad_group_id}&ad_id={ad_id}` — an id that only rides in `utm_campaign` or `utm_content` is reported as that UTM and never as the campaign or the ad, so `--platform chatgpt --role adgroup_id` has nothing to split by.
1444
1483
 
1445
1484
  **The one recommendation about the ad account itself.** `baker analytics ads --platform <p>` warns when that platform's traffic carries no campaign id, ad group id or ad id — the three tiers every "which ad worked" question is grouped by — and returns the exact URL suffix, in the platform's own macro language, that would start sending the missing ones. It is the only mapping gap that cannot be fixed afterwards: naming a parameter Baker already captures corrects the history, because roles are resolved when a report is read, but a parameter the ads never sent has no stored values for a meaning to apply to. The hint stays quiet for a platform with little traffic, and defers to the untagged-account finding rather than repeating it.
1446
1485
 
@@ -1808,16 +1847,16 @@ baker gsc sitemaps --site-url "https://example.com/"
1808
1847
  Ads start from what customers said. Three commands carry the process; the agent writes the ideas and the copy.
1809
1848
 
1810
1849
  ```bash
1811
- baker creatives brief [--adset <id>] [--account-id act_<id>] # one read: viewer, account bottleneck, what works, taste, customer quotes, ideas on file
1850
+ baker creatives brief [--adset <id>] [--account-id act_<id>] [--no-research] # one read: viewer, account bottleneck, what works, taste, customer quotes, ideas on file
1812
1851
  baker creatives ideas [--full] # check and rank src/creatives/_ideas/*.md; build_next
1813
1852
  baker creatives make <idea> [--slug <s>] [--adset <id>] [--reference <adId>] [--spec <beats.json>] [--from-idea]
1814
1853
  baker creatives learn [--days <n>] [--dry-run] # what each idea's ads did, written back onto the idea
1815
1854
  ```
1816
1855
 
1817
- - `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable`. `market` says what competitors' ads already say (the followed brands, else the library), how sophisticated the market is, and which awareness stages nobody speaks to.
1856
+ - `brief` returns a verdict, not raw numbers: `account.bottleneck` is `hook`, `after_click`, `none` or `too_little_data`. A part that cannot be read says `unavailable`; the market searches (the followed brands, and the library for what COMPANY.md's first line says the company sells, in a few words) run at once and get 12 seconds, so a slow ad library leaves them out rather than holding the brief. With few customer quotes on file it runs `research customers` itself alongside the reads (about 30 s) and returns what it wrote; `--no-research` skips that. With no ideas on file, its hint lists the values `awareness`, `angle` and `format` take. `market` says what competitors' ads already say (the followed brands, else the library), how sophisticated the market is, and which awareness stages nobody speaks to.
1818
1857
  - `ideas` is free and local. An idea's `evidence` resolves to `src/content/insights/` entries, or it is an `explore` bet. Numbers must be in the Knowledge or our own customers' quotes (a Reddit, forum or web quote never sources a claim), no two ideas may share an angle and format, and a review or testimonial format needs a quote from our own customer.
1819
1858
  - `learn` pools each built idea's creatives, judges them against the rest of the account's cost per result and writes `status` (`winner`, `failed`, `inconclusive`, `testing`) and a frozen `result` onto the idea. `ideas` then warns when a new idea repeats a failed one.
1820
- - `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
1859
+ - `make` builds one idea. A static searches the idea's container for ads made for our viewer, checks the closest three with `winning-ads fit` and scaffolds from the first that fits. For a new creative it starts scaffolding the best-ranked one while the fits are asked (kept when it fits, stopped and removed whole when another or none is kept), so the reference's describe pass no longer waits for them. Finding and describing a reference gets 50 seconds; past that the build is stopped and the static starts from the idea (`REFERENCE_SLOW` in the hint). A creative whose ad is already written is never built over (`ALREADY_MADE`): edit its `prompt.json`, or pass `--slug` for another ad of the same idea. A video idea takes its beats spec. Either way it records the idea on `_definition.md`, sets `prompt.json`'s `message` and syncs the definition. Drafted copy lines claiming a figure the company never wrote come back first in `hints`. When no proven ad fits (or with `--from-idea`), a static starts from the idea itself: `prompt.json` holds the message and placeholders, and `canvas run` refuses to render until the scene and copy are written.
1821
1860
 
1822
1861
  ### Competitive Intelligence (`baker research`)
1823
1862
 
@@ -3426,9 +3465,9 @@ Reading the scores: **`relevance`** (0–1) = match of the creative to your quer
3426
3465
 
3427
3466
  ### `baker winning-ads hooks|mechanisms|formats`
3428
3467
 
3429
- Read the **winner-weighted rollup** of the ad-dna corpus → `GET /api/ad-library/seed-catalog` — the hook /
3430
- mechanism / static-format patterns that actually win, ranked by `winner_score` among the ads of the segment you
3431
- pass (`--platform`, `--awareness`, `--industry`; an unset one means any), keyed on the closed seeds/enums the
3468
+ Read the pattern rollup of the ad-dna corpus → `GET /api/ad-library/seed-catalog` — the hook /
3469
+ mechanism / static-format patterns that win **inside brands**, among the creatives of the segment you pass
3470
+ (`--platform`, `--awareness`, `--industry`; an unset one means any), keyed on the closed seeds/enums the
3432
3471
  extractor emits (no clustering). Ground a creative in a **proven** pattern for the exact segment instead of
3433
3472
  guessing. The rollup is recomputed daily.
3434
3473
 
@@ -3450,8 +3489,10 @@ baker winning-ads formats --awareness solution_aware --output md
3450
3489
  | `--limit <n>` | Max keys 1–100 (default 20) |
3451
3490
  | `--full` | Include the per-key segment breakdown |
3452
3491
 
3453
- Each key returns `count`, `mean_winner_score`, and `winner_share`. Ranking is winner-weighted volume, so a
3454
- proven grand-slam pattern floats to the top and a measured dud is floored.
3492
+ Each key returns `lift`, `brands_for`, `brands_against`, `count`, `mean_winner_score` and `winner_share`. The
3493
+ ranking is `lift`: how much more often a brand's creatives using the pattern kept running 60 days than that
3494
+ brand's others, one vote per brand, 0 until five brands have a verdict. `brands_for` / `brands_against` count
3495
+ the brands it won and lost inside. A common pattern is not a winning one: `count` is how often it is used.
3455
3496
 
3456
3497
  ### `baker winning-ads fit <adId>`
3457
3498
 
@@ -3573,6 +3614,7 @@ baker scheduled-agents create --name "Celebrate the first sale" --description "O
3573
3614
  baker scheduled-agents update <id-or-temp_agent_id> --enabled false
3574
3615
  baker scheduled-agents update <id-or-temp_agent_id> --run-at "2026-09-02T07:00:00Z" --prompt "Use the launch checklist"
3575
3616
  baker scheduled-agents update <id-or-temp_agent_id> --trigger experiment.ended
3617
+ baker scheduled-agents update <id-or-temp_agent_id> --slack "#marketing" # each run opens a thread there; also here | me | off
3576
3618
  baker scheduled-agents delete <id-or-temp_agent_id>
3577
3619
  baker scheduled-agents unstage <id-or-temp_agent_id> # everything this chat staged on it
3578
3620
  baker scheduled-agents unstage <id-or-temp_agent_id> --index 2 # one staged change only
@@ -3950,7 +3992,7 @@ baker setup-links list # active lin
3950
3992
 
3951
3993
  ---
3952
3994
 
3953
- ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot deals properties | fields` · `baker hubspot meetings list | view`
3995
+ ### `baker hubspot forms list | view | submissions` · `baker hubspot workflows list | view` · `baker hubspot pipelines list` · `baker hubspot contacts summary | lookup | properties | fields` · `baker hubspot companies search` · `baker hubspot deals list | properties | fields` · `baker hubspot meetings list | view`
3954
3996
 
3955
3997
  Read-only view of the company's **connected HubSpot account** — the forms and meeting links (calendars) that live there, the leads those forms received, and the workflows, deal pipelines, and contact records a lead runs through afterwards. Fetched from HubSpot live on every call; nothing is cached and nothing is ever written back.
3956
3998
 
@@ -3972,6 +4014,8 @@ baker hubspot pipelines list # where a lead lands, and the st
3972
4014
  baker hubspot contacts summary --days 30 # are recent leads being worked? (counts only)
3973
4015
  baker hubspot contacts lookup lead@example.com # was this one lead picked up?
3974
4016
  baker hubspot contacts properties # what a HubSpot-read source can watch, with each value
4017
+ baker hubspot companies search --domains acme.com,globex.io --with-deals # CRM accounts by domain, with their deals
4018
+ baker hubspot deals list --pipeline default --modified-since 2026-01-01 # deals: amount, stage, closed won
3975
4019
  baker hubspot meetings list # every calendar + whether booking redirects
3976
4020
  baker hubspot meetings view <slug> # booking-form fields for one calendar
3977
4021
  baker hubspot meetings view <slug> --as-node # the form.external blob for a flow node
@@ -3982,7 +4026,9 @@ baker hubspot meetings view <slug> --as-node # the form.external blob for a f
3982
4026
  **`forms submissions`:** positional `<formId>`; `--days <1-365>` (default 30) sets the window; `--full` adds each submission's field values. The default response is counts and dates only — `total`, `inWindow`, `lastSubmittedAt`, and per-day counts — because submissions carry the lead's own contact details. `truncated: true` means the page budget ran out before the window was covered, so `inWindow` is a floor rather than an exact count.
3983
4027
  **`workflows list` flags:** `--search <text>`, `--enabled-only`. Disabled workflows are listed too — a form wired to one that is off looks connected and does nothing.
3984
4028
  **`workflows view`:** positional `<workflowId>` (the id from `workflows list`, and the one in HubSpot's own URL — the command translates it to HubSpot's separate internal flow id for you). Returns the enrolment criteria and whether it re-enrols (`null` = HubSpot does not report it for that criteria type, so it is unknown rather than off), plus every step with the property it writes. On a `LIST_BRANCH` step, branches come back in **evaluation order** (`order: 1` wins over `order: 2`) — a broad branch above a narrower one silently swallows the records the narrow one was written for, and HubSpot reports nothing. A `STATIC_BRANCH` matches on a value rather than top down, so its `order` is display only. `steps` is **not** in execution order; follow `nextActionId`. `--full` adds the raw payload, the only place the branch filter criteria (which property is compared to which value) can be read.
3985
- **`pipelines list`:** no flags; stages come back in the order they run in HubSpot.
4029
+ **`pipelines list`:** no flags; stages come back in the order they run in HubSpot, each with `isClosed` and `isWon` (closed with probability 1).
4030
+ **`companies search` flags:** `--domains <a.com,b.com>` (up to 100; a URL or `www.` is reduced to the bare domain), `--names <"Acme, Inc.;Globex">` (semicolon-separated), `--with-deals`, `--limit <1-500>` (default 100), `--output json|csv|jsonl|md`. Rows: `{ id, name, domain, linkedinPage, deals? }`; `notFound` lists the searched domains no CRM company carries; `truncated: true` means more matched than `--limit`.
4031
+ **`deals list` flags:** `--pipeline <id,id>`, `--modified-since <YYYY-MM-DD>`, `--limit <1-1000>` (default 200), `--after <cursor>`, `--output json|csv|jsonl|md`. Rows, oldest change first: `{ id, name, amount, currency, pipelineId, stageId, stageLabel, isClosed, isWon, createdAt, closedAt, modifiedAt, companyIds }` — `amount: null` is none entered. `next` is the `--after` for the following page; `restartFrom` appears when HubSpot's 10,000-result search ceiling is reached (read on with `--modified-since <restartFrom>`); `companiesReadable: false` means the connection cannot read companies, so no deal carries `companyIds`.
3986
4032
  **`contacts summary` flags:** `--days <1-365>` (default 30) over contact creation date. Returns counts only — total contacts, how many have an owner, and the lifecycle-stage split — never a name or an email. `truncated: true` means the page budget ran out, so the counts are floors.
3987
4033
  **`contacts lookup`:** positional `<email>`; returns that contact's lifecycle stage, owner, the account (company) it was filed under, and associated deals with their stage. `dealsReadable: false` / `companyReadable: false` mean the connection does not cover deals or accounts, so an empty `deals` or a null `company` is unknown rather than absent. `found: false` means no contact with that email — the lead never reached the CRM, which is a finding rather than an error.
3988
4034
  **`contacts properties`:** no flags; returns every contact property with a closed list of values — `{ name, label, groupName, options: [{ value, label }] }` — the ones a HubSpot-read source can watch. Names are HubSpot's internal ones; hand them to `baker analytics sources --create-hubspot --property <name> --value <value>` as printed. No personal data.
@@ -4014,6 +4060,32 @@ No write surface against HubSpot itself — a form's fields, redirect, or consen
4014
4060
 
4015
4061
  ---
4016
4062
 
4063
+ ### `baker accounts influenced | engagement | warm | company | campaigns | crm`
4064
+
4065
+ Account insights: which companies the LinkedIn ads are warming, and which CRM deals they influenced. Reads Baker's nightly copy of LinkedIn and the CRM, so it is fast and covers up to 24 months; nothing is changed anywhere. Every company has it once a LinkedIn ad account is chosen; before the first nightly read the reports are empty — read live with `baker ads linkedin companies`. `--start`/`--end` must be real calendar days within 24 calendar months; a bad flag or an unsupported `--output` exits 1 with `INVALID_ARGS`.
4066
+
4067
+ ```bash
4068
+ baker accounts influenced # influenced pipeline, last 12 calendar months
4069
+ baker accounts influenced --basis revenue --start 2026-01-01 --end 2026-06-30
4070
+ baker accounts influenced --full # every deal in the period, influenced or not
4071
+ baker accounts engagement --last-months 3 --limit 50 # companies reached, New/Rising, CRM match and deals
4072
+ baker accounts engagement --last-months 3 --limit 50 --page 2 # the next 50 (pageInfo.total counts them all)
4073
+ baker accounts engagement --campaign-id 123456 --output csv
4074
+ baker accounts warm --output csv > warm.csv # companyName,domain,organizationUrn
4075
+ baker accounts company acme.com # or a LinkedIn company id / urn:li:organization:N
4076
+ baker accounts campaigns --last-months 6 --output md # per campaign: reached, engaged, influenced pipeline (--basis revenue)
4077
+ baker accounts influenced --model awareness --threshold 10 --output csv # another rule, this call only
4078
+ baker accounts warm --min 0 # every New/Rising company, below the threshold too
4079
+ baker accounts crm status # deals from HubSpot or sent by the client; what arrived, linked Google Sheets
4080
+ baker accounts crm sheet # create and link the deals sheet in the client's Google Drive
4081
+ baker accounts crm check --file deals.csv # judge a deals or company CSV offline, nothing sent
4082
+ baker accounts crm contract # the send-your-CRM contract, as Markdown for a developer
4083
+ ```
4084
+
4085
+ A deal is **influenced** when its company reached the company's influence rule — by default 3+ LinkedIn ad engagements (Awareness: 7+ impressions; Intent: 3+ clicks) — in the 12 months up to and including the month the deal was created (`--basis pipeline`, deals not won) or closed won (`--basis revenue`). It is a labelled rule, not credit: campaign rows overlap, and money is reported per currency, never summed across currencies. Deals whose company match is only a guess come back under `lowConfidence` and stay out of `summary` until the match is confirmed in Measure → Accounts. Without the deals set up there — HubSpot's pipelines and won/lost stages, or Another CRM — `influenced` and `campaigns` exit 1 with `ACCOUNTS_NOT_SET_UP`. A CRM Baker cannot read sends its companies and deals to `POST /v1/accounts/crm` (a `crm_ingest` key, `X-Baker-Mode: test` to rehearse, up to 500 records per request, records naming a person refused), is uploaded as a CSV in Measure → Accounts, or lives in a Google Sheet linked there that Baker reads every night (a row deleted from the sheet leaves Baker). A file is a deals export or a company list — a company needs only its website or LinkedIn page; `crm check` runs the same rules offline and `crm contract` prints the whole contract. The CLI never sends deals or links a sheet of the client's; `crm sheet` creates the empty deals sheet in their Google Drive and links it, applied at once (asking again returns the same sheet). `--model engagement|awareness|intent` and `--threshold N` on `influenced`, `campaigns` and `warm` read with another rule for that call only; the saved setting is unchanged. A **warm** account is New or Rising, reached the company's influence threshold this period on the rule's metric (`--min N` overrides, `--min 0` drops it), and has no open deal, no won deal and no deal lost in the last 6 months; `engagement` and `warm` rows carry `dealStatus` (`won` > `open` > `lost` > `none`), `engagement --output csv` adds `lastMonth` and `previousImpressions/Clicks/Engagements`, and `campaigns` / `company` carry each campaign's `campaignName` (null when LinkedIn could not name it). `company` returns `months` (the company's totals), `byCampaign` (the same numbers per campaign — overlapping, never add them) and its `deals` with `touchValue` and `influenced`. `warm --output csv` is the file `baker ads linkedin audiences create --type company-list --list-file` takes.
4086
+
4087
+ ---
4088
+
4017
4089
  ### `baker chats list | view | transcript | diff`
4018
4090
 
4019
4091
  Read-only inspection of **other Sessions (chats) on the same account** — so an agent can reuse prior work ("like we did in that other chat, but for X"). Same-account only; never crosses a company boundary and has no write surface.
@@ -4627,7 +4699,7 @@ Open record keyed by the composition's `meta.json` inputs.
4627
4699
 
4628
4700
  ##### `static_prefilter`
4629
4701
 
4630
- Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
4702
+ Local; no credits (one box-reader call per variant, about $0.005, which rides on the run record's review spend). Ranks a static ad's variants before the judge by the deterministic checks (blocking defects, then every line of the ad spec's copy read back with its box, the brand's logo found, the button's words, every word and the logo inside the master's band) and passes on only the best-ranked, so the judge only breaks an exact tie. A variant the reader could not read ties with the best. **Inputs:** `images` (the variants' whole list), `spec` (the ad spec, `$ref:prompt.asset`). **Params:** `formats` (the delivered sizes). **Outputs:** `images` (the best-ranked variants, in an order seeded by the pictures rather than the render order; never empty: when every variant fails, the one with the fewest defects passes as `flagged`), `order` (which variant each of `images` is), `dropped` (each variant set aside, with its index and reasons), `flagged` (the variant kept when all failed), `image` (the chosen picture: when a variant rendered at another shape, a 3:4 for a 4:5, would lose a word or the button to a cut about its centre, it comes cut to the master's frame where every word stays whole, else as rendered) and `usage` (the reader's spend). Scaffolded by `scaffold-static-ad` between the variants and the judge.
4631
4703
 
4632
4704
  ##### `static_safe_fit`
4633
4705
 
@@ -5832,8 +5904,8 @@ offered, or, for a viewer who does not know the brand, when the words say they a
5832
5904
  cart, a trial, a donation, a registration); who it is for is reported but never fails an ad alone. The viewer is the `viewer` recorded in
5833
5905
  `_definition.md`, else the one its `targetAdSet` reaches (as `baker ads meta adsets --audience` reads it), else
5834
5906
  the persona and `awareness` from the Knowledge, else a cold stranger. Not understood → exit 2 with
5835
- `viewer_check_failed` and a `fix` naming what the viewer missed. Answers are cached per copy, scene and viewer.
5836
- `baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
5907
+ `viewer_check_failed` and a `fix` that starts with the one rewrite answering a missed offer (`Do this: make the biggest words say "<message>"`) and names what the viewer missed. After two failures of one ad in a chat turn the check no longer holds it back: the command answers `ok: true` with `understood: false` and what was missed, and `baker canvas run` renders it, printing the miss. The casting check is asked alongside the viewer. Answers are cached per copy, scene and viewer.
5908
+ Before either, a static ad whose scene quotes words, or names a price or a share, that the copy (or `scene_text`) does not hold is refused (`scene_words_not_in_copy`): the picture would print them and a correction round would erase them. An unreadable `prompt.json` names each wrong field in a line and shows that field's shape. `baker canvas run` makes the same check on a static ad before it spends (skipped for `--fix`, an autofix round, `--use-variant`, a re-layout and resumed runs, never for an edit of the words;
5837
5909
  `--skip-viewer-check` renders without it; a model outage lets the render through with a warning). `--full` adds
5838
5910
  the viewer's whole answer and how the viewer was chosen.
5839
5911
 
@@ -5879,7 +5951,7 @@ A completed render the review calls ready (`verdict` `ready` or `ready_with_note
5879
5951
 
5880
5952
  **Resuming an interrupted run.** A long `baker canvas run` (multi-clip video) that is killed mid-render — session end, sandbox pause — leaves a marker under the outputs dir. The next `baker canvas run` of the same canvas automatically **resumes** that run: it reuses the run id so still-running billed jobs re-attach instead of being abandoned and re-billed, and completed nodes come from the cache. A live render keeps its marker's heartbeat fresh, so a second `run` of the same canvas is refused while it is alive (checked with `kill(pid, 0)` plus the heartbeat, on Linux and macOS alike). Resume also works from a **different workspace or a fresh sandbox**: when no local marker survives, the run history is consulted for a resumable run of the same source (canvas, `prompt.json`, scenes, compositions and formats), and it is adopted when the backend says it may be: interrupted, stopped at a checkpoint, or a `running` row whose keepalive is older than the backend's staleness threshold. Ctrl-C / SIGTERM aborts gracefully — no new nodes dispatch, a resumable snapshot is flushed, and the marker survives. A clean completion (or a handled failure) clears the marker, so a normal re-run starts a fresh generation. Force a new run with `--fresh`, or pin a specific run with `--run-id <id>` (also the escape hatch to adopt a run that is reported as concurrently live). Independent same-layer nodes (e.g. video clips) fan out in parallel up to `--parallel`/`--concurrency` (default 8; env `BAKER_CANVAS_CONCURRENCY`).
5881
5953
 
5882
- **Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear is laid out again instead); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it; anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`). A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`), so the review's second look at the same pixels never counts as a new defect. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
5954
+ **Static ads: the run corrects a blocking defect itself.** When the review of a static ad finds a blocking defect, `baker canvas run` makes up to 2 more renders (`AUTO_FIX_ROUNDS` in `static-autofix.ts`) before it reports, correcting each defect at the layer that causes it: words or a logo where an interface covers them (Reels, Stories) or the frame cuts them are **placed and extended** (`withExtendedSizes`): a `static_place` node reads where everything sits and shrinks the ad (to 60% at most) into the size's safe area, and a `static_extend_merge` node has the ad's own model paint only the margins that leaves and lays the ad's own pixels back over the painting, so the model is never asked to move anything (a size already inside its area is passed on with nothing painted or billed, and one whose words sit too far out for a shrink to clear, or that the placing found inside its area while the review still blocks it, is laid out again instead, never placed the same way twice; a button is placed by its shape, its words grown by 0.7 of their height on every side, as the review judges it); a defect the spec asked for (a second subject, stars with no named source, a concept that does not read) and any defect of a native screen corrects the spec first (`static-spec-repair.ts`: a model rewrites `prompt.json` for those findings alone, every line of the copy kept word for word), then draws the ad again from it (a native screen redraws free); a picture wrong as a whole or in three places or more is made again from its brief, told what the last one got wrong, but only in a round with another after it unless the brief caused the defect; anything else on the picture is a `--fix` of the chosen picture for its own findings (`correctionOf`: the words to erase, the line to re-letter or add), naming what the last edit was already asked for, while each re-laid size the review flagged follows as an edit of its own last layout (`withSizesFollowingFix`), keeping what an earlier round fixed in it, and a size it found nothing blocking on is delivered as it was (`keep`, nothing in it made again or placed); anything else on a re-laid size alone edits just that size from its own last layout (`withRelayEdit`); sizes that do not show the same ad (`SIZES:`) are always the re-laid ones' fault, since every size follows the chosen picture, so they are laid out again to show exactly what it shows, never the picture edited to match them. A size a round did not touch (its file the same byte for byte) keeps the round before's verdict (`carryUnchanged`) and is not read again: the review reads only the sizes the round changed. Each round is one render (a fix, or one request per re-laid size) and prints `[autofix] round N of 2: …` on stdout; the final JSON's hints start with "Corrected automatically before this result, …" and its review is of the corrected version. Blocking findings left after the rounds keep the hints below. A round that would only repeat the correction that just left its defect in place (a chosen option, `--use-variant`, is only ever corrected) is not run: that version is reported as it is. `--no-autofix` reports after the first render.
5883
5955
 
5884
5956
  **Static ads: correct a version, or use another of its variants.** Correct a finished version with `--fix "<what to change>"`: one render takes that version (the last one, or `--from <run_id>`) as its first reference and changes only what you said, keeping the picture, the layout and every other word; it replaces the variants and the judge, and every size is made again from the corrected picture. **A `prompt.json` changed only in its words, their positions or its colours** since the version shown is made the same way, as an edit of that version (`[edit]` in the output, the instruction read off the change); three new options are asked for only with `--regenerate static_variants`. A `--fix` whose defect is still blocking afterwards is recorded, and asking for it again with the same words exits 2 with `no_progress`. When every blocking finding is on re-laid-out sizes, the first hint says to lay out those sizes again (`--regenerate adapt_<size>`) before any `--fix`. Every static run's hints say what it cost in credits and what for. There is no render limit per creative. Use another variant with `--use-variant <sha256>` (`--from <run_id>`, default the last render): when the person chose another of a version's variants over the judge's pick, every size is made again from that picture, and the only requests re-lay it out for each other size; `--regenerate adapt_9x16` lays out one size again from the same chosen picture when only that size came out wrong (after a `--fix` or `--use-variant`, from that corrected or chosen picture, so the size never brings back the uncorrected one; `--from <run_id>` names another version); `baker creatives feedback --slug <slug>` prints the choice (`variantChoice`) and the exact command.
5885
5957
 
@@ -6009,7 +6081,7 @@ The canvas is validated before it is written. stdout returns `{ ok, canvas_path,
6009
6081
 
6010
6082
  **Who sees the ad.** `--target-adset <id>` names the Meta ad set the ad will run in. Before anything is billed, the viewer its targeting reaches is read exactly as `baker ads meta adsets --audience` reads it (`temperature`, what they already `knows`, `placements`) and written to `_definition.md` as `targetAdSet` and `viewer` (an existing definition keeps every other field). The sizes, the brief and the prefilter keep clear of that viewer's placements (`staticSafeArea`; `automatic`, a bare `facebook`/`instagram` or no placements is Advantage+, Reels included), and every `baker canvas run` checks the ad against that viewer. With no ad account, give the viewer from the Knowledge: `--viewer-temperature cold|warm|hot [--viewer-knows brand,offer,product] [--viewer-placements <placement,…>]`, recorded with `source: knowledge`; next to `--target-adset` they correct the read, and its `reasons` say so. A bad value exits 2 (`invalid_viewer`) before any billed pass; an ad set that cannot be read never stops the scaffold (a hint says why, and the sizes keep clear of every placement). With neither flag, a `viewer` (or `targetAdSet`) already in `_definition.md` is used as it is. stdout carries `target_adset`, `viewer` and a `checklist.viewer` line.
6011
6083
 
6012
- **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. Within one chat turn an ad gets two runs you start (each with its automatic corrections), and none once it is `ready` or `ready_with_notes`: the next is refused (`CONFLICT`, exit 2) with what to show instead; the person's next message starts a new turn (`turn-renders.ts`). When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
6084
+ **After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format; **every line of `prompt.json`'s copy (in-scene and designed alike) read back with its box by a box reader and compared** (a line more than 2% off, missing, or joined by words nobody asked for is blocking; case and punctuation fold, as does a capital I read as an l (one stroke in a sans-serif), accents do not; words the picture adds are blocking only when set at the copy's usual size or making a claim (a figure, a price, an offer), and otherwise a note, as a screen's code, a poster or a mug in the scene, except a price (a currency sign beside a figure) the copy does not hold, blocking at any size and blurred by the correction, the document it is on kept; every render is asked for no readable prices on a bill, quote, receipt, screen or price tag beyond the copy; a copy line is "on the ad twice" only when most of its words repeat, never a word or two echoed on a prop; on a native screen, which code typesets from the spec, a line read differently is the reader's error and only a missing one counts); **the ad's own copy and its logo measured against the format's safe area** (`staticSafeArea`, the one owner of those areas; `must-show.ts` decides what is measured: never what the scene prints, nor a copy line printed again in the scene while the line itself sits clear). On a 9:16, which runs in Stories and Reels alike, what Stories' header or bottom bar covers (and any other app's interface named) is said as "where Stories and Reels cover it" ("where Reels covers it" for an ad set that runs only in Reels) and blocks for the logo, the button, the headline, the figure and the offer, while a supporting line (body, caption, label, small print) there is a note; what only Reels' taller footer covers is one note, and only for the lead line, since Stories shows it and the 9:16 is laid out in Stories' band; words merely close to where an app draws are no finding. The steps that place a size clear of an interface (`static_safe_fit`, `static_place`) are told the same copy (`prompt.json`'s lines, their roles, and the brand's name and the logo's words, a wordmark read close to them counting as the logo) and move the ad for those alone, a supporting line included only where the frame cuts it; on a feed image such as a 4:5, only a design margin, so words close to the edge are a warning and only words the frame cuts are blocking); a copy line written over two lines (`"15%\noff"`) is matched part by part wherever the reader finds each; designed type in a colour far from the brand palette is a warning; supporting lines too small to read on a phone are one note per size; the main line set smaller than a supporting one is a note, the brand's wordmark never counted as part of a line that starts with its name; the logo set twice is a note, a far smaller mark printed on something in the scene (a hoodie, a bag) is not. When the copy names a real person ("Lautaro Suarez, CTO at Vibiz"), the run casts them as themselves before it renders (`baker avatars research --create`, their avatar's sheet added to `prompt.json` `references` as a `PERSON_n`), and a face shown as a named person the ad has no photo of is blocking. When the reader cannot answer, the vision pass transcribes the copy instead and judges the safe zones by eye. Then one vision pass for clipping, overlaps, artifacts, one dominant idea, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-candidate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed, and the run is recorded with `presentable: false` so it is never shown as a version; every fix is an edit to `prompt.json` and a re-run (wrong words: the lines in `copy`; wrong picture: the `scene`; a bad roll: `--regenerate static_variants`; one size wrong while the chosen picture is right: `--regenerate adapt_<size>`, which re-lays it out, named in the hints); a blocking defect is first corrected by the run itself (`--autofix`, above). The checks rank the variants before the pick and the judge only breaks an exact tie, comparing pairwise against the compiled brief, knowing the brand and the viewer; every variant stays on offer to the person in Baker. Within one chat turn an ad gets two runs you start (each with its automatic corrections) and about 8 minutes in all: a correction round that would take it past that is not started (`[autofix] not started`), the version shown as it stands, and no render starts once it is spent or once the ad is `ready` or `ready_with_notes`: the next is refused (`CONFLICT`, exit 2) with what to show instead; the person's next message starts a new turn (`turn-renders.ts`). When a defect still stands after the automatic rounds, the hints say to fix it with a `--fix`, then by changing the spec or the approach, and if it still cannot be fixed to show the best version and name what is left in one plain sentence, without a question.
6013
6085
 
6014
6086
  The render prompt is **model-aware**: each model's profile (`src/engine/lib/prompt-profiles.ts`) sets its shape, word budget, picture order and request params. gpt-image gets short labelled sections with the constraints last; Gemini gets sentences that give the reason and a positive closing list of the only words allowed. A profile change, or a new model on the router (`src/engine/lib/image-model-router.ts`), is re-run on the static bench (`scripts/creative-evals`, `--job static`) before it ships. Run `baker canvas critique` on the scaffolded canvas for an advisory read before you spend.
6015
6087
 
@@ -6600,7 +6672,7 @@ With `--slug`, `data.variantChoice` (`runId`, `option`, `sha256`) says which var
6600
6672
  Record what the person said about one version in a chat: an approval, or a rejection with reasons and their own words. It is stored like the Approve / Not this one buttons, as the verdict of the person who wrote the chat's latest message (`recordedFor: "sender"`), or of the chat itself when nobody has written in it (`"chat"`). One verdict per person per version or option: a later one replaces theirs. Must run from a chat.
6601
6673
 
6602
6674
  ```bash
6603
- baker creatives feedback record --slug spring-offer --run r_01K… --verdict rejected \
6675
+ baker creatives feedback record --slug spring-offer --verdict rejected \
6604
6676
  --reasons text_mistakes,weak_hook --note "the price is 49 not 59"
6605
6677
  baker creatives feedback record --slug spring-offer --run r_01K… --option 2 --verdict approved
6606
6678
  ```
@@ -6608,7 +6680,7 @@ baker creatives feedback record --slug spring-offer --run r_01K… --option 2 --
6608
6680
  | Flag | Effect |
6609
6681
  |---|---|
6610
6682
  | `--slug <slug>` | Required. The creative (`src/creatives/<slug>/`). |
6611
- | `--run <r_…>` | Required. The version. |
6683
+ | `--run <r_…>` | The version. Left out, the one the creative shows now (its last render in this workspace). |
6612
6684
  | `--verdict approved\|rejected` | Required. |
6613
6685
  | `--option <o>` | One option of a version that made several: its label, its number (`2`) or its slot. |
6614
6686
  | `--reasons <a,b>` | Comma-separated: `text_mistakes`, `logo_or_brand_wrong`, `weak_hook`, `wrong_message`, `looks_fake`, `not_our_style`, `voice_or_sound`, `pacing`, `other`. The two video reasons: `voice_or_sound` (the voiceover, music or mix) and `pacing` (too slow, too fast, the hook lands late). A rejection needs reasons or a note. |
@@ -6621,7 +6693,7 @@ After a rejection, `hints[]` restates what to change, starting from that version
6621
6693
  Choose the version of a creative to keep, or only some of its options, the way choosing it in Baker does: the creative shows that version as the chosen one, and the choice records the chat sender's approval (`source: "selection"`). Naming options replaces the earlier pick. It does not add the image to the library; that is `baker creatives publish`, which, given `--run-id`, also counts as choosing that version.
6622
6694
 
6623
6695
  ```bash
6624
- baker creatives select --slug spring-offer --run r_01K…
6696
+ baker creatives select --slug spring-offer # the version it shows now
6625
6697
  baker creatives select --slug spring-offer --run r_01K… --options "beach,2"
6626
6698
  ```
6627
6699
 
@@ -6642,6 +6714,7 @@ baker landing inspiration code <section id> # the standalone bundle
6642
6714
  baker landing inspiration page <source id> # a whole page as a section sequence
6643
6715
  baker landing inspiration sequences "b2b saas pricing page" --scope all # what follows what, across many pages
6644
6716
  baker landing inspiration add https://linear.app --note "client likes this density"
6717
+ baker landing inspiration add https://linear.app --refresh # read a saved page again — the site changed
6645
6718
  baker landing inspiration favorites # what this company has saved
6646
6719
  baker landing inspiration retry-failed # study again every saved page that failed
6647
6720
  baker landing inspiration scrape <url> --out .baker/<dir> # capture any page now, synchronously
@@ -6657,6 +6730,7 @@ baker landing inspiration scrape <url> --out .baker/<dir> # capture any page n
6657
6730
  - **Compact by default, `--full` when you have chosen something.** Every list command returns what you need to *pick* a row; the 600-character "why it works" paragraph, the classification facets and the markup ride behind `--full` on `search`, `view`, `page`, `favorites` and `code`. A default `favorites` used to cost more than a default search for a question — "what does this client keep saving?" — that the facets alone answer.
6658
6731
  - **`sequences` answers the ordering question search structurally can't.** Search ranks individual sections and caps each site at three, so a page's top-to-bottom order never survives its results, and `page` recovers exactly one page's order. `sequences` counts adjacency across many pages at once and returns `openers`, `transitions` (`{ from, to, count }`, most frequent first) and `closers`, filtered by `--scope --domain --section-type --limit`. It returns page **ids only** — `page` stays the one way to read a single page — and always reports `pages_considered` alongside `pages_returned`, because a frequency over an unstated denominator is not evidence. `--full` counts over every matching page and prints the whole table.
6659
6732
  - **`page_id` on every search row** is what `page <id>` takes, so "how does this page sequence its sections?" is reachable from a result rather than only right after `add`. `favorites --limit` is capped at 100.
6733
+ - **Pages are re-read, on request and on a schedule.** A page is a copy taken when it was read, so `add --refresh` reads it again when the live site has changed (refused while a read is under way or within 10 minutes of the last one), and the library re-reads pages older than 30 days on its own, a small batch a day. A re-read that fails before it starts replacing the copy keeps the previous one and is reported as `refresh_failed_at`. `page` reports `read_at` and hints at `--refresh` (with the address the page is filed under) when the copy is over 30 days old.
6660
6734
  - **`add` studies in the background; `scrape` returns with the page on disk.** `add` grows the shared library and takes minutes, so it can never answer "build our page like this one" within the same turn. `scrape` runs the identical capture locally and blocks until it finishes, writing section screenshots, standalone markup, a whole-page reproduction and `report.html`. `--no-motion` is the single biggest lever on runtime; `--no-mobile`, `--no-code` and `--no-report` skip further passes.
6661
6735
  - **Filming is the expensive pass, and it runs three sections at a time.** Every moving section is filmed in its own fresh page load — the only way to catch an entrance animation before it fires — so a page with ten moving sections pays for ten full loads. Measured on one heavy page, filming was 74% of the capture. The takes are independent, so they overlap; a page that cannot be captured inside the library's budget now says so instead of timing out silently.
6662
6736
  - **A page we can't open says why.** A dead certificate, an address that doesn't resolve, a site that won't answer or one that is simply too slow each produce their own plain-language reason rather than a browser error.