@officexapp/vidfarm-devcli 0.21.27 → 0.21.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.director.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- description: Use Vidfarm as a director. Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
3
+ description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
4
4
  ---
5
5
 
6
6
  # Vidfarm Director
@@ -223,7 +223,9 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
223
223
 
224
224
  - **Determine the surface before claiming capabilities.** The web chat has only its declared tools and REST routes. It cannot execute arbitrary JavaScript/Python, open a shell, create a local repository script, or use the user's filesystem. Never tell a web-chat user that you ran code or wrote a script unless a dedicated declared tool actually did so. A desktop coding agent has a real shell and filesystem and MAY write/run scripts, perform arbitrary local computations over paginated API results, create reports/CSVs/JSON, edit composition files, and orchestrate long devcli workflows within the user's authorization.
225
225
  - **The web AI chat can do all three paintbrushes** — clip raws, author HTML/hyperframe motion, and generate AI media — and it drives edits directly on the live timeline. Keep small-to-medium jobs here: text/caption swaps, a scene or two replaced, single generations, captions, approve/schedule. Just do them.
226
- - **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame never pinned to the top/bottom edges the phone UI clips) and uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight ≥700). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font fix it, don't inherit it. Local devcli renders auto-normalize this, but author it correctly so the editor preview matches. Full rules in `references/editor-workflows.md` ("TikTok-native caption standard").
226
+ - **No HTML slop a video is not a web page.** Compositions are authored in HTML, so the #1 tell of an AI-edited video is landing-page furniture: gradient CTA "buttons" ("Sign Up for a Free Trial →"), rows of benefit chips/badges ("✓ No Credit Card Needed"), frosted/bordered cards holding a gradient headline + a URL, feature grids, bullet lists, web-default type (Inter/Roboto/Arial at weight 400-600). None of that exists in a real TikTok/Reel — **nothing in a video is clickable**. If you are typing `btn` / `badge` / `chip` / `card` / `rounded-full` / `shadow-lg` / `backdrop-blur` / `bg-gradient-to-r`, stop and rewrite it as timed text on the footage. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all *fine* — they're native to the platform.
227
+ - **On devcli, QA every video you produce: `vidfarm qa ./work`.** Free, instant, local-only — it blocklists exactly the slop above plus font-regime/safe-zone drift, and prints a concrete fix per finding. **Feedback, not a gate**: it exits 0 even on findings, never runs automatically, and is a blocklist (unusual/stylized compositions pass untouched), so there's no reason not to run it before every publish. `--json` for scripted batches, `--strict` only if you want a CI failure. **Web-chat copilot: this command does not exist for you** (devcli-only, no REST twin) — apply the standard by hand, and when handing a heavy job to a local coding agent, tell them to run `vidfarm qa`.
228
+ - **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips), uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight **700–900**, ~36–64px on a 1080-wide frame), and uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
227
229
  - **Where the web chat struggles: complex, long, multi-step transformations.** A full multi-scene re-theme, an iterative render-critique-iterate loop, heavy scripted or batch work, or anything needing a real filesystem and many sequential tool calls will hit context limits, turn/timeout ceilings, and the web editor's constraints (CSS/declarative motion only — JS animation adapters are stripped on save). Don't grind a big transformation one layer at a time in a chat turn and stall.
228
230
  - **Practical workaround — hand the heavy job to local devcli.** When a task is genuinely large or long-running, **proactively recommend the director run it locally with an AI coding agent** (Claude Code / OpenAI Codex / any capable agent): `vidfarm pull <forkId>` writes the composition + the `.harness/` grounding bundle to disk, the agent edits with the full devcli verb set and JS animation adapters, renders free with `vidfarm serve`, and `vidfarm publish` pushes it back. This is the **best-quality (B) harness's** natural home (adversarial grading with a coding agent). Frame it as "this is a big rebuild — you'll get a better, faster result running it locally with a coding agent; here's how," not as a dead end.
229
231
  - **Offer a handoff, do not impersonate the desktop agent.** When web chat reaches that boundary, offer to save a Markdown handoff in My Files containing the objective, selected template/fork IDs, asset paths, grounding, constraints, completed work, and suggested devcli commands. Create it only after the user agrees. The desktop agent should read that document, pull the referenced fork, and then use its actual code/shell capabilities.
@@ -237,7 +239,7 @@ Read only the relevant reference file for the current task.
237
239
  - Timeline editing, decompose, captions, motion, AI placement: `references/editor-workflows.md`
238
240
  - Raws hunts, My Files, recurring characters, asset retrieval: `references/assets-and-sourcing.md`
239
241
  - REST automation, `vidfarm` command surface, local serve loop, skill packs: `references/automation-and-local-dev.md`
240
- - Getting-started interviews, strategy docs, onboarding flow: `references/onboarding.md`
242
+ - Getting-started interviews, **consultations**, strategy docs, onboarding flow: `references/onboarding.md`
241
243
  - Primitive routes such as TTS, STT, music, overlays, background removal, product placement: `references/primitives.md`
242
244
  - Complete REST API map and raw-HTTP conventions: `references/rest-api.md`. Load it only when the user asks for REST, an endpoint/schema, direct HTTP integration, or exhaustive API coverage. For the entire specification, follow its domain links and load every listed reference; do not preload them into ordinary director conversations.
243
245
 
@@ -259,7 +261,7 @@ HyperFrames authoring and rendering in this package are Vidfarm-native: local wo
259
261
 
260
262
  Choose the narrowest path that satisfies the request.
261
263
 
262
- 1. If the user needs help figuring out what to make, read `references/onboarding.md` first.
264
+ 1. If the user needs help figuring out what to make, **or asks for a "consultation"** (the `brainstorm/*` chain: cold-start interview → awareness stages → angles → hooks), read `references/onboarding.md` first.
263
265
  2. If the user already knows the goal and needs a suitable template, read `references/core-workflows.md` and use the template discovery flow.
264
266
  3. If the task is “change this video,” read `references/editor-workflows.md`.
265
267
  4. If the task is “find footage” or “use our existing assets,” read `references/assets-and-sourcing.md`.
@@ -1004,16 +1006,52 @@ Three surfaces:
1004
1006
  - **Editor UI (human)** — the Inspector's **Animate (Ken Burns)** dropdown on any selected image.
1005
1007
  - **Desktop agents (devcli)** — `vidfarm place ./work --src <img> --kind image --ken-burns zoom-in [--ken-burns-intensity 0.3]`, or fused with generation: `vidfarm generate image --prompt "…" --place ./work --ken-burns pan-left`.
1006
1008
 
1007
- ### TikTok-native caption standard (position + font) always adhere
1009
+ ### Social-native visual standard no HTML slop, ever
1008
1010
 
1009
- Short-form is watched on a phone, and the phone's UI eats the frame's edges. **Never pin on-screen text to the extreme top or bottom** the top ~8% sits under the status bar / "Following · For You" tabs and the bottom ~15% under the username, caption text, music marquee, and action rail. Text there is literally clipped and reads as amateur. Two rules, applied to **every** caption/title/overlay you place or inherit:
1011
+ Compositions are authored in HTML, so the single most common way an AI-edited video betrays itself is by **looking like a web page**. A landing-page button, a row of feature chips, a frosted card with a gradient headline these are everywhere on the web and **nowhere in a real social post**. A director scrolling TikTok/Reels/Shorts never sees them, so the moment one appears the video reads as machine-made and the retention dies. HTML and hyperframes are the right tool; **Bootstrap/Tailwind furniture is not.**
1012
+
1013
+ **The one test:** *would this element exist if the video were shot on a phone and captioned in CapCut?* If it only makes sense inside a browser — if its whole job is to look **clickable** — cut it. **Nothing in a video is clickable.**
1014
+
1015
+ **BANNED — never author, and strip on sight when a fork or a paste brings one in:**
1016
+
1017
+ - **CTA / "button" shapes.** Any filled capsule or rounded rect containing action copy — "Sign Up for a Free Trial →", "Learn More", "Get Started", "Book a Call" — especially gradient-filled with a glow or drop shadow. A social CTA is *spoken*, or a plain caption line, or an arrow pointing at the real UI. Not a button.
1018
+ - **Badge / chip / pill rows.** The "✓ ID-Verified Tutors · ✓ No Credit Card Needed · ✓ 30-Min Trial" strip of small rounded tags. Nothing on TikTok is a `<span class="badge">`. Say the three benefits as three timed caption lines instead — one at a time, on the footage.
1019
+ - **Cards, panels, containers.** A rounded-rect box (dark, bordered, shadowed, or `backdrop-filter` frosted) holding a heading plus a subheading/URL. Text belongs **on the footage**, not inside a floating panel with padding around it.
1020
+ - **Gradient text fills, neon border glows, elevation shadows, glassmorphism, hover-implying strokes.**
1021
+ - **Web page furniture of any kind:** navbars, hero sections, feature grids, two-column layouts, `<ul>` bullet lists with disc markers, tables, alerts, progress bars as decoration, logo-in-a-circle avatars, "as seen in" strips.
1022
+ - **Corporate web type:** Inter, Roboto, system-ui, Arial, Helvetica, Georgia, Times — at web weights (400–600) and web sizes (16–24px). Instantly reads as "a screenshot of a website."
1023
+
1024
+ **Greppable smell test.** If you are typing `class="btn…"`, `badge`, `chip`, `tag`, `card`, `panel`, `container`, `row`/`col-`, `hero`, `cta`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-r`, `border border-…` — **stop.** You are building a web page, not a video. Rewrite as timed text on footage.
1025
+
1026
+ **ALLOWED and encouraged — these ARE social-native:**
1027
+
1028
+ - **Arrows** (drawn, animated, hand-style), circles/scribbles/underline strokes highlighting part of the frame, hand-drawn marks, checkmarks *as glyphs inside a caption line* (not as chips).
1029
+ - **Italic / oblique** for emphasis, ALL-CAPS, a single word in a pop color, mixed weights across a stacked line.
1030
+ - **Emoji inline in text** (sparingly), **sticker/cut-out overlays** on transparent PNG (`create-overlay`), mock social UI when the format calls for it (iMessage bubbles, a TikTok comment card, a fake DM, a countdown/progress bar) — these are native artifacts of the platform, not web furniture.
1031
+ - **Full-bleed footage** with text sitting directly on it.
1032
+
1033
+ **On devcli there's a checker: `vidfarm qa <dir|composition.html>`.** Free, instant, local-only — a blocklist pass for everything above plus the font regime and safe zone, with a concrete fix per finding. **Run it on every video you produce.** It is feedback, not a gate (exit 0 even on findings, never runs automatically, `--strict` only if you want a CI failure) and a blocklist, not an allowlist (stylized/hand-made compositions pass untouched — it will not homogenize your videos). No cloud/REST twin: the web copilot enforces this standard by hand. Details in `references/automation-and-local-dev.md` ("`vidfarm qa`").
1034
+
1035
+ ### TikTok-native caption standard (position + font + background) — always adhere
1036
+
1037
+ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **Never pin on-screen text to the extreme top or bottom** — the top ~8% sits under the status bar / "Following · For You" tabs and the bottom ~15% under the username, caption text, music marquee, and action rail. Text there is literally clipped and reads as amateur. Three rules, applied to **every** caption/title/overlay you place or inherit:
1010
1038
 
1011
1039
  - **Position → readable safe zone.** Keep the text box's vertical extent inside **~8%–85%** of canvas height (9:16). A lower-third subtitle lives around **y≈70%** (the `captions generate` default); a "me when…" / "POV:" meme line sits near the **top of the safe zone (y≈8%), not y:0**. Preserve the original top-vs-bottom anchoring — just pull it off the dead-zone edge, don't recenter everything. Also keep wide captions clear of the **right ~12%** action rail (a centered box at `x:10 width:80` is safe).
1012
- - **Font → the composition regime.** Use the bundled display fonts only — **Montserrat** (bold default, weight 700), **TikTok Sans**, Abel, Source Code Pro, Yesteryear. Don't request a font the composition doesn't import (it silently falls back). For captions, heavy weight + an outline/pill background (`spotlight` preset, `background_style:"outline"`) is the TikTok-native look.
1040
+ - **Font → the composition regime.** Use the bundled display fonts only — **Montserrat** (bold default, weight **700–900**), **TikTok Sans**, Abel, Source Code Pro, Yesteryear. Don't request a font the composition doesn't import (it silently falls back to a web-default sans, which is exactly the slop look). Sizes are in PIXELS of a 1080-wide frame: **~36–64px** reads well, never below ~28px and **never 0** (invisible). Keep captions to ~2 lines / ~5 words per line; `line_height` 0.95–1.15 for stacked display lines.
1041
+ - **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
1013
1042
 
1014
- **A common trap: decomposed templates mirror the source's caption placement**, so a forked meme can arrive with its caption pinned at `top:0` in a non-regime font. When you re-theme or recaption, **fix it to the standard** rather than inheriting the edge-pinned original. When placing text yourself (`set_captions`, `set_layer_text`, `add_layer`, devcli `place`/`captions`), set `y`/`font_family` to the standard from the start.
1043
+ | # | Treatment | How to set it | When |
1044
+ |---|---|---|---|
1045
+ | 1 | **Bare text + heavy outline/stroke** (black stroke on white or a bright fill) | `background_style:"outline"` | The default TikTok look |
1046
+ | 2 | **Bare text + soft drop shadow only**, no box | `background_style:"plain"` | Clean/cinematic over busy footage |
1047
+ | 3 | **Highlight pill behind the ACTIVE word only** | `set_captions caption_style:"spotlight"` / `"karaoke"` (+ `caption_highlight_color`) | Hormozi/CapCut word-by-word. **The only legitimate "pill" in a video** — it tracks the spoken word, so it isn't a badge |
1048
+ | 4 | **Solid band that tightly hugs the text lines** (CapCut "text box") | `background_style:"highlight-solid"` (or `"highlight-translucent"`) + a `background` color | Guaranteed legibility over noisy footage |
1015
1049
 
1016
- > Local devcli renders enforce this automatically: `renderCompositionLocally` runs `normalizeTikTokCaptionLayout` (src/devcli/composition-edit.ts) on every production, clamping caption/text layers into the 8%–85% safe zone and coercing off-regime primary fonts to Montserrat. It's a safety net, not a license to author edge-pinned captions get it right in the composition so the editor preview and any cloud render match.
1050
+ Treatment 4 is a **band, not a card**: it hugs the glyphs with minimal padding, corner radius ~8px, **no border, no drop shadow, no gradient, no blur**, and it wraps *one* text run never a heading + subheading + URL stacked inside one rounded box. The moment it grows padding, a stroke, or a second element inside it, it has become a web card. Fix it.
1051
+
1052
+ **A common trap: decomposed templates mirror the source's caption placement**, so a forked meme can arrive with its caption pinned at `top:0` in a non-regime font — and a re-theme prompt ("make this for my tutoring service") is exactly where an agent starts inventing landing-page CTAs and benefit chips because the *subject* is a SaaS product. **Fix to the standard, don't inherit it, and don't import the website's design language into the video.** When placing text yourself (`set_captions`, `set_layer_text`, `set_layer_style`, `add_layer`, devcli `place`/`captions`), set `y` / `font_family` / `font_weight` / `background_style` to the standard from the start.
1053
+
1054
+ > Local devcli renders enforce part of this automatically: `renderCompositionLocally` runs `normalizeTikTokCaptionLayout` (src/devcli/composition-edit.ts) on every production, clamping caption/text layers into the 8%–85% safe zone and coercing off-regime primary fonts to Montserrat. It only fixes position and font family — it will happily render your Bootstrap card. Get it right in the composition so the editor preview, the local render, and any cloud render match.
1017
1055
 
1018
1056
  ### Animated captions — word-by-word caption styles (TikTok/CapCut)
1019
1057
 
@@ -1201,11 +1239,11 @@ Local scans persist to a SQLite library under `~/.vidfarm` (free compute, subscr
1201
1239
 
1202
1240
  ## My Files (the user's asset library)
1203
1241
 
1204
- Each user has a persistent **My Files** filesystem — their own uploaded videos (mp4/mov/webm), images (png/jpg/jpeg/gif/webp/svg), audio (mp3/wav/m4a/aac), and documents (pdf/md/txt/csv) — organized into **virtual folders**. This is where a user keeps footage, brand assets, logos, music, briefs, scripts, and the durable strategy docs from Getting Started (About.md, awareness-levels.md, etc.). Both the editor AI copilot and an agent CLI can **read and write** it to find or save assets instead of asking the user to re-upload or paste URLs.
1242
+ Each user has a persistent **My Files** filesystem — their own uploaded videos (mp4/mov/webm), images (png/jpg/jpeg/gif/webp/svg), audio (mp3/wav/m4a/aac), and documents (pdf/md/txt/csv) — organized into **virtual folders**. This is where a user keeps footage, brand assets, logos, music, briefs, scripts, and the durable strategy docs from Getting Started / a consultation (OFFER.md, awareness-levels.md, etc.). Both the editor AI copilot and an agent CLI can **read and write** it to find or save assets instead of asking the user to re-upload or paste URLs.
1205
1243
 
1206
1244
  - **List** — `GET /api/v1/user/me/attachments` → `{ attachments: [{ id, fileName, contentType, sizeBytes, folderPath, viewUrl, createdAt }], folders: [string] }`. The `viewUrl` is a durable URL you can drop into a composition layer or pass into a primitive route. devcli: `vidfarm files [--folder <path>]`.
1207
1245
  - **Read one** — resolve it from the list and stream/read its `viewUrl`. devcli: `vidfarm get-file <id> [dest]` (add `--print` to print text contents of md/txt/csv/json inline). For images/video/audio/pdf, reference the `viewUrl` as media — you can't read their bytes as text.
1208
- - **Write into My Files** — `POST /api/v1/user/me/attachments/upload` (multipart `file` + optional `folder_path`) persists a file into the library. devcli: `vidfarm put-file <localFile> --folder <path>`, or pipe/inline text without a temp file: `echo "…" | vidfarm put-file --stdin --as About.md --folder acme` / `vidfarm put-file --content "…" --as About.md`. This is how an agent saves Getting Started context (About.md, Interview.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) or drops a logo/product demo into a namescoped folder. In the web editor copilot the same capability is the **`browse_files` write action** (`action=write` with `file_name` + `content` + optional `folder_path`), which accepts text files (md/txt/csv/json/srt/vtt).
1246
+ - **Write into My Files** — `POST /api/v1/user/me/attachments/upload` (multipart `file` + optional `folder_path`) persists a file into the library. devcli: `vidfarm put-file <localFile> --folder <path>`, or pipe/inline text without a temp file: `echo "…" | vidfarm put-file --stdin --as OFFER.md --folder acme` / `vidfarm put-file --content "…" --as OFFER.md`. This is how an agent saves Getting Started / consultation context (OFFER.md — or OFFER_<NAME>.md for a multi-offer director — plus awareness-levels.md, persuasive-angles.md, ad-hooks.md) or drops a logo/product demo into a namescoped folder. In the web editor copilot the same capability is the **`browse_files` write action** (`action=write` with `file_name` + `content` + optional `folder_path`), which accepts text files (md/txt/csv/json/srt/vtt).
1209
1247
  - **`vidfarm upload` is different** — it targets the **ephemeral** temporary-files store (`POST /api/v1/user/me/temporary-files/upload`) for a throwaway durable URL to drop straight into a composition or approved post. Use `put-file` when you want the asset to persist in the user's My Files library; use `upload` for one-shot media you just need a URL for. Namescope scratch uploads under a **`temp/` folder** (`vidfarm upload clip.mp4 --folder temp`) so they stay in one purgeable place. Better still, if you're on a `vidfarm serve` box, skip the upload entirely and reference the file off disk with `place --src ./clip.mp4` (see "Local file paths as media").
1210
1248
 
1211
1249
  ### Metadata notes + vector search (find assets by meaning)
@@ -1270,7 +1308,7 @@ The web copilot exposes the same three as `browse_files action=rename|move|copy`
1270
1308
 
1271
1309
  **Assume My Files is multi-offer.** A user often runs more than one product, offer, brand, or region, and namescopes assets into folders accordingly — by product (`acme-skincare/`, `zensleep/`), by offer/campaign (`summer-sale/`), by region (`us/`, `eu/`), by asset type (`logos/`, `ugc-clips/`), or any arbitrary scheme. There is **no fixed layout** — read the `folders` tree first and reason about how this user organized things. Before pulling assets for a task, infer which folder(s) match the product/offer/region the current composition is about (match folder names to the composition title, video context, and what the user said), then scope reads to that folder so you never mix one brand's logo/product-shot/music into another brand's video. If the target offer is ambiguous, ask which product/offer/region (or which folder) this work is for rather than guessing across offers.
1272
1310
 
1273
- In the editor web copilot the same filesystem is exposed via the **`browse_files` tool** (`action=list` / `action=search` / `action=read` / `action=write` / `action=annotate` / `action=move` / `action=copy` / `action=rename`), so the copilot follows the identical reasoning: search or list to find the right offer's folder, then read an asset — or `write` a text doc (About.md, awareness-levels.md, …) or import a media URL (`source_url`) into that folder, annotating anything worth finding again. `action=rename`/`move`/`copy` reorganize the tree (see the three routes above) — use them to keep character folders and asset names tidy. `browse_files list` defaults to `path='/'` when `path` is omitted, so the `/raws` (hunted raws) and `/temp` (scratch) roots surface alongside the My Files folders instead of being hidden; a `/raws` listing also accepts a `content_type` filter (exact shot-kind — `talking_head`, `b_roll`, `product_shot`, `screen_recording`, …), and every listing paginates via `offset` / `limit`. The devcli equivalents are `vidfarm files [--search]` / `get-file` / `put-file [--notes]` / `annotate-file` / `directory rename|move|copy`.
1311
+ In the editor web copilot the same filesystem is exposed via the **`browse_files` tool** (`action=list` / `action=search` / `action=read` / `action=write` / `action=annotate` / `action=move` / `action=copy` / `action=rename`), so the copilot follows the identical reasoning: search or list to find the right offer's folder, then read an asset — or `write` a text doc (OFFER.md, awareness-levels.md, …) or import a media URL (`source_url`) into that folder, annotating anything worth finding again. `action=rename`/`move`/`copy` reorganize the tree (see the three routes above) — use them to keep character folders and asset names tidy. `browse_files list` defaults to `path='/'` when `path` is omitted, so the `/raws` (hunted raws) and `/temp` (scratch) roots surface alongside the My Files folders instead of being hidden; a `/raws` listing also accepts a `content_type` filter (exact shot-kind — `talking_head`, `b_roll`, `product_shot`, `screen_recording`, …), and every listing paginates via `offset` / `limit`. The devcli equivalents are `vidfarm files [--search]` / `get-file` / `put-file [--notes]` / `annotate-file` / `directory rename|move|copy`.
1274
1312
 
1275
1313
  ## Free-plan API keys — valid everywhere, gated per route
1276
1314
 
@@ -1493,6 +1531,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1493
1531
  | `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
1494
1532
  | `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
1495
1533
  | `vidfarm stills <dir> [--at 0,2.5,…]` | (local in-process render of PNG frames) | visually verify an edit without a full render |
1534
+ | `vidfarm qa <dir\|composition.html> [--json] [--strict]` | (local static QA — **devcli-only**, no cloud/REST twin) | **social-native QA: HTML slop + font regime. Run it on EVERY video you produce.** Free, instant, feedback-only |
1496
1535
  | `vidfarm doctor` | (local environment triage) | check ffmpeg/node/keys/agent CLI/poisoned env + list local serve/preview processes before debugging anything else; `--kill-orphans` reaps dead servers squatting ports (fixes the "Waiting for preview server…" hang) |
1497
1536
  | `vidfarm skills list\|add <name>\|update` | `GET /skill-pack/index.json` · `/skill-pack/:name/*` | install/refresh skill packs (see "Skill packs — import on demand") |
1498
1537
  | `vidfarm tts "…" --engine local` / `vidfarm stt <file> --engine whisper` | (keyless LOCAL engines: Kokoro-82M TTS, whisper.cpp STT) | narration + word-timestamp transcripts with zero keys and zero accounts |
@@ -1506,6 +1545,37 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1506
1545
 
1507
1546
  **Approving a locally rendered MP4 (the URL-first rule + the durability rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. An approved post is a **permanent** share page, so the media must live in **durable My Files**, not the 30-day temp store (a temp-hosted video would 404 the share page after 30 days). The correct sequence for a big local file is: (1) `POST /api/v1/user/me/attachments/presign` with `{ file_name, content_type, size_bytes }` → (2) PUT the raw bytes to the returned presigned S3 URL → (3) `POST /api/v1/user/me/attachments` (finalize) → use the returned durable `viewUrl` as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically (durable by default; up to **200 MB**). Never POST a large file as multipart to `.../attachments/upload` against the cloud host: that path proxies through Lambda and caps near 6 MB (it exists only as a fallback for local-storage `vidfarm serve` boxes). Only use the temp-store route (`.../temporary-files/*`, or `vidfarm approve --temp`) for a **throwaway** preview you don't mind losing in 30 days.
1508
1547
 
1548
+ ## `vidfarm qa` — the social-native QA pass (devcli-only, heavily recommended)
1549
+
1550
+ ```bash
1551
+ vidfarm qa ./work # human-readable findings + verdict
1552
+ vidfarm qa ./work --json # machine-readable: rule / severity / where / fix
1553
+ vidfarm qa ./work --strict # ALSO exit 1 on slop (only if you want a CI gate)
1554
+ ```
1555
+
1556
+ **Run this on every video you produce.** It is free, instant (pure DOM, no ffmpeg/Chrome/network), and it is the only automated check for the thing that most often ruins an agent-made video: **HTML slop**. Compositions are authored in HTML, so an agent's web-page instincts leak straight onto the frame as landing-page furniture that appears on every website and in **zero** real TikToks.
1557
+
1558
+ **It is feedback, not a gate.** Default exit code is **0** even when it finds slop; nothing in the render or publish path calls it; it never runs automatically. `--strict` exists only if you deliberately want a CI failure. A finding you disagree with is fine to ignore and say so — it is a lint, not a verdict on the work.
1559
+
1560
+ **It is a BLOCKLIST, not an allowlist.** It names specific known-bad web patterns. Everything it doesn't name is legal, so a weird, ugly, hand-made, or wildly stylized composition passes untouched. It will never push your videos toward one house style — if it fires on a genuine creative choice, that's a bug in the rule, not in your video.
1561
+
1562
+ What it flags:
1563
+
1564
+ | Rule | Severity | What trips it |
1565
+ |---|---|---|
1566
+ | `cta-button` | error | Action copy ("Sign Up for a Free Trial →") **inside** a filled/gradient rounded capsule. Bare CTA copy in a caption is fine — "BUY NOW" is real social copy |
1567
+ | `badge-chip-row` | error | 2+ sibling small rounded filled tags — the "✓ No Credit Card Needed · ✓ 30-Min Trial" strip |
1568
+ | `card-panel` / `glass-card` | error | A rounded box with a border/shadow/`backdrop-filter` wrapping 2+ elements. A tight legibility **band** (radius ≤8px, one text run, no border/shadow) stays legal |
1569
+ | `gradient-text` | error | `background-clip:text` gradient headline fills |
1570
+ | `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
1571
+ | `web-framework-classes` | error/warn | Bootstrap/Tailwind class tokens (`btn`, `badge`, `card`, `hero`, `col-*`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-*`) or a linked CSS framework. A `<script>` CDN for GSAP/anime.js is fine |
1572
+ | `page-structure` / `bullet-list` | error/warn | `<nav>`/`<header>`/`<footer>`/`<table>`; a `<ul>` with visible bullet markers |
1573
+ | `font-regime` | error/warn | A text layer in a website body font (Inter/Roboto/Arial/system-ui → **error**) or any family outside the imported regime (Montserrat, TikTok Sans, Abel, Source Code Pro, Yesteryear → warn, it silently falls back at render) |
1574
+ | `font-size` / `font-weight` | error/warn | `font-size:0` (invisible) is an error; sub-2.6%-of-canvas-width text and weight <600 warn |
1575
+ | `caption-safe-zone` | warn | Text outside the 8%–85% band on a **portrait** canvas (landscape/square are exempt) |
1576
+
1577
+ Every finding carries a concrete `fix` line — the answer is always "say it as timed text on the footage", never just "delete it". Fold `--json` into scripted batch runs to QA N variants at once.
1578
+
1509
1579
  ## Cost mode — the devcli's money-saving guardrail
1510
1580
 
1511
1581
  The four modes, quoted as **cost per finished video**. The first two are spend postures; the last two are also *production methods* — `rich-ai` buys reusable greenscreen raws and animates them in HTML, `pure-videogen` generates every frame:
@@ -1646,13 +1716,13 @@ Vidfarm is in active development. Endpoints, response shapes, and the editor UI
1646
1716
  - treat `latest_version` as the source of truth for what the editor loaded
1647
1717
  - if a publish fails, check `renders/:renderId` for the error phase and stderr before retrying
1648
1718
 
1649
- ## Getting started / onboarding a new director
1719
+ ## Getting started / onboarding / consultation
1650
1720
 
1651
- Only run this flow when the director signals they **don't know where to start** ("getting started", "help me set up", "I don't know where to begin"). If they already know what they want, skip straight to it — never force onboarding. Directors can also jump to any single step (e.g. "just find me a good template for X" → go straight to template discovery).
1721
+ Run this flow when the director signals they **don't know where to start** ("getting started", "help me set up", "I don't know where to begin") **or asks for a "consultation"** ("give me a consultation", "consult me on my offer", "I want a strategy consultation"). **"Consultation" is a first-class trigger for the brainstorm chain** — treat it as a request for the full run: the cold-start interview → awareness stages → angles → hooks (steps 1-4 below), driven by the `brainstorm/*` primitives rather than brainstormed in chat from memory. If they already know what they want, skip straight to it — never force onboarding. Directors can also jump to any single step (e.g. "just find me a good template for X" → go straight to template discovery, or "consult me on hooks" → run `brainstorm/hooks` alone).
1652
1722
 
1653
1723
  The point of onboarding is to build **durable, reusable context** in My Files, namescoped under the director's product/offer folder, so future chats and agents can read it back. Save each artifact as Markdown with `browse_files` write (web copilot) or `vidfarm put-file` (devcli):
1654
1724
 
1655
- 1. **Product context** → `About.md` (basic offer/product) or a deeper `Interview.md` if they want depth. Drive the interview questions with `brainstorm/coldstart`.
1725
+ 1. **Product context** → **`OFFER.md`** (or `OFFER_<NAME>.md` e.g. `OFFER_ACME_SKINCARE.md` when the director runs more than one offer). Drive the interview questions with `brainstorm/coldstart` and **recommend the director save their answers into that file**, either locally next to their project or in cloud My Files. This is the durable artifact every later step reads back; `About.md` / `Interview.md` are legacy names — prefer `OFFER*.md` for anything new.
1656
1726
  2. **Awareness level** (Eugene Schwartz — problem-aware, solution-unaware, …) → `awareness-levels.md`. If it's genuinely unknown after thinking it through, note that ads for **every** level should be made and tested. Use `brainstorm/awareness_stages`.
1657
1727
  3. **Persuasive angles** → `persuasive-angles.md`, via `brainstorm/angles`.
1658
1728
  4. **Hooks** → `ad-hooks.md`, via `brainstorm/hooks`.
@@ -1660,7 +1730,7 @@ The point of onboarding is to build **durable, reusable context** in My Files, n
1660
1730
  6. **Budget** — ask roughly what they want to spend per video, and map it to the Cost spectrum (free reuse+local render → pennies for cloud render → ~$1 for some AI scenes → $10+ for heavy AI gen). This sets which approach you default to and whether AI **video** generation is on the table (ask permission before using it; image gen is cheap and fine). Budget can also be revisited per editor project.
1661
1731
  7. **Recommend & adapt a template** — pair what you now know about the offer against the decomposed template catalog (`GET /discover/feed?q=<offer>`, read each result's `promotions`/`keywords`/`summary`), recommend the best 3-6, then fork and **modify** the winner to fit their offer. Prefer already-decomposed templates so the director skips the ~$0.10 decompose cost.
1662
1732
 
1663
- **Assume multiple offers.** My Files is multi-offer (see the My Files section) — namescope every onboarding artifact under the right product/offer/region folder (`acme-skincare/About.md`, not a bare `About.md`) so one brand's context never bleeds into another's.
1733
+ **Assume multiple offers.** My Files is multi-offer (see the My Files section) — namescope every onboarding artifact under the right product/offer/region folder (`acme-skincare/OFFER.md`, not a bare `OFFER.md`) so one brand's context never bleeds into another's. When a director keeps several offers in one flat folder, name the files `OFFER_ACME_SKINCARE.md` / `OFFER_ACME_SUPPLEMENTS.md` instead.
1664
1734
 
1665
1735
  ## Default assistance pattern
1666
1736
 
@@ -1991,7 +2061,11 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/audio/transcribe" \
1991
2061
 
1992
2062
  ## Brainstorm primitives
1993
2063
 
1994
- The `brainstorm/*` primitives are the strategy toolkit. They are reusable, billable AI reasoning steps — the same family the AI Copilot exposes as chip suggestions. Treat **product placement** as a first-class member of this family, right alongside angles and hooks:
2064
+ The `brainstorm/*` primitives are the strategy toolkit. They are reusable, billable AI reasoning steps — the same family the AI Copilot exposes as chip suggestions. Treat **product placement** as a first-class member of this family, right alongside angles and hooks.
2065
+
2066
+ When a director asks for a **"consultation"**, that is this toolkit: run the chain `coldstart` (the interview) → `awareness_stages` → `angles` → `hooks`, saving each output to My Files as you go (see `references/onboarding.md`). Never improvise a consultation from memory when these routes exist. "Consult me on hooks/angles" → run just that primitive.
2067
+
2068
+ `coldstart` returns *questions*, not answers — always recommend the director write their answers into an **`OFFER.md`** (or `OFFER_<NAME>.md` when they run several offers), locally or in cloud My Files. That file becomes the `offer_description` you feed into `awareness_stages`, `angles`, `hooks`, and `product_placement`.
1995
2069
 
1996
2070
  - `POST /api/v1/primitives/brainstorm/coldstart` — `{ payload: { user_message } }` → foundational questionnaire for a customer starting from zero.
1997
2071
  - `POST /api/v1/primitives/brainstorm/awareness_stages` — `{ payload: { offer_description } }` → which Eugene-Schwartz awareness stages to target first.
@@ -2069,8 +2143,9 @@ Use this when a coding agent is doing the work locally or the user wants a repro
2069
2143
  3. Read `./work/.harness/agent-guide.md` and `./work/.harness/context.json` before editing.
2070
2144
  4. Make deterministic edits to `composition.html` and optionally `composition.json`.
2071
2145
  5. Validate with `vidfarm lint` or `vidfarm stills` when useful.
2072
- 6. Render with `vidfarm render <forkId> --dir ./work --wait`.
2073
- 7. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
2146
+ 6. **QA before you render: `vidfarm qa ./work`.** Free, instant, devcli-only. It blocklists HTML slop (CTA buttons, benefit chip rows, frosted cards, gradient text, web-page classes/fonts) and checks the caption font regime + safe zone. Feedback only — exit 0 even on findings, never automatic — but it catches the #1 tell of an agent-made video, so run it on every production. Fix what's real, ignore what's a deliberate style call, then render.
2147
+ 7. Render with `vidfarm render <forkId> --dir ./work --wait`.
2148
+ 8. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
2074
2149
 
2075
2150
  **Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes, and an approved post is **permanent** — so the local MP4 must land in **durable My Files**, not the 30-day temp store (a temp video would 404 the share page after 30 days). The devcli presigns, PUTs the bytes **direct to S3**, finalizes, then approves with that durable URL — so `vidfarm approve --video ./final.mp4` handles files up to **200 MB**, bypasses the ~6 MB Lambda request-body limit, and the share link never breaks. By raw REST: `POST /api/v1/user/me/attachments/presign` → PUT to the returned S3 URL → `POST /api/v1/user/me/attachments` (finalize) → pass the returned `viewUrl` in the approve `media` array. Do not multipart-POST a big file to `.../attachments/upload` against the cloud host (Lambda-bound, ~6 MB cap). Add `vidfarm approve --temp` only when you want a disposable 30-day preview.
2076
2151
 
package/SKILL.md CHANGED
@@ -72,7 +72,7 @@ This page does not contain the workflows. Pull the full director skill and load
72
72
  - `editor-workflows` — timeline editing, decompose, captions, the three paintbrushes & two replication harnesses
73
73
  - `assets-and-sourcing` — raws hunts, My Files, recurring characters
74
74
  - `automation-and-local-dev` — REST + `vidfarm` devcli + local serve loop
75
- - `onboarding` — "help me figure out what to make" interview
75
+ - `onboarding` — "help me figure out what to make" interview, and the **consultation** flow
76
76
  - `primitives` — TTS, STT, music, overlays, background removal, product placement
77
77
  - `rest-api` — complete REST map (load only for direct HTTP integration)
78
78
  - **Updating Vidfarm:** `https://vidfarm.cc/update.md` — **fetch this file whenever the user asks to update/upgrade Vidfarm**, says their skill or devcli is out of date, or hits a command/route that "used to work". It's the clean uninstall→reinstall runbook for BOTH halves (the `vidfarm` skill pack and `@officexapp/vidfarm-devcli`), and it carries the current breaking-change notes — read it live rather than improvising an `npm i -g`, because the notes are exactly what a blind reinstall misses.
@@ -83,7 +83,7 @@ For composition *authoring* craft (motion, keyframes, scene design), Vidfarm shi
83
83
 
84
84
  ## Router — where to go from the user's ask
85
85
 
86
- 1. "Help me figure out what to make" → `references/onboarding.md`
86
+ 1. "Help me figure out what to make" **or "give me a consultation"** → `references/onboarding.md`. A consultation *is* the `brainstorm/*` chain: cold-start interview → awareness stages → angles → hooks. Run the primitives; don't improvise the strategy in chat.
87
87
  2. "Find a template and start" → `references/core-workflows.md`
88
88
  3. "Change / re-theme this video" → `references/editor-workflows.md`
89
89
  4. "Find footage / use our assets" → `references/assets-and-sourcing.md`
@@ -99,6 +99,9 @@ For composition *authoring* craft (motion, keyframes, scene design), Vidfarm shi
99
99
  - Never build composition HTML by string concatenation — parse, edit, re-serialize the DOM.
100
100
  - Render only through `POST /api/v1/compositions/:forkId/render`; never call the renderer directly.
101
101
  - Submissions are **not idempotent** — every render/primitive POST charges again. Check status before retrying.
102
+ - **No HTML slop.** Compositions are HTML, but a video is not a web page: never author CTA "buttons", benefit chip/badge rows, frosted or bordered cards holding a headline + URL, gradient text, feature grids, or bullet lists — none of that exists in a real TikTok, and nothing in a video is clickable. Say it as timed text on the footage. Arrows, scribble/underline marks, italics, ALL-CAPS, color pops, emoji, cut-out stickers, and mock social UI are fine.
103
+ - **On devcli, `vidfarm qa <dir>` before every render.** Free, instant, local-only blocklist for the slop above + the font regime. Feedback, not a gate (exits 0, never automatic). No REST/web equivalent.
104
+ - **Caption regime is mandatory**: an imported display font (Montserrat default / TikTok Sans), weight 700–900, ~36–64px on a 1080-wide frame, inside the 8%–85% safe zone, and exactly one of four backgrounds — `outline`, `plain`, an active-word `spotlight`/`karaoke` pill, or a tight-hugging `highlight-solid` band (radius ≤8px, no border/shadow/gradient/blur).
102
105
  - In the web editor, CSS/declarative motion only (JS animation adapters are stripped on save); locally via `vidfarm serve` the full JS adapters work.
103
106
  - Treat `forkId` as an unguessable bearer token. Never put provider secrets in composition HTML/JSON.
104
107
 
package/dist/src/cli.js CHANGED
@@ -19,6 +19,7 @@ import { formatCompositionLintIssues, lintCompositionHtml } from "./services/com
19
19
  import { resolveFfmpeg } from "./services/clip-curation/ffmpeg.js";
20
20
  import { parseHyperframesJson, runHyperframesCommand } from "./devcli/hyperframes-cli.js";
21
21
  import { renderCompositionStills } from "./devcli/stills.js";
22
+ import { formatQaReport, qaCompositionHtml } from "./devcli/qa-check.js";
22
23
  import { removeGreenscreenLocal, localGreenscreenAvailable, defaultGreenscreenOutPath, GREENSCREEN_PRESETS, trimTransparentBorders, cropImageRegion } from "./devcli/greenscreen-local.js";
23
24
  import { runDoctorCommand } from "./devcli/doctor.js";
24
25
  import { findFreePort } from "./devcli/port-utils.js";
@@ -595,6 +596,16 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
595
596
  lint <dir|composition.html> Validate a composition on disk (same (local)
596
597
  checks the /editor chat preflights); prints
597
598
  errors/warnings, exit 1 on errors [--json]
599
+ qa <dir|composition.html> Social-native QA pass — HIGHLY RECOMMENDED (local, devcli-only)
600
+ on every video you produce. Blocklists "HTML
601
+ slop" (CTA buttons, benefit chip rows, frosted
602
+ cards, gradient text, web-page classes/fonts)
603
+ and checks the caption font regime + safe zone.
604
+ Feedback only: exits 0 even when it finds slop,
605
+ and it never runs automatically. Blocklist, not
606
+ allowlist — unusual styles pass untouched
607
+ --json Machine-readable findings (rule/severity/fix)
608
+ --strict Also exit 1 when slop is found (for CI)
598
609
  stills <dir|composition.html> Render PNG stills of the CURRENT (local in-process render)
599
610
  composition — the visual self-check loop
600
611
  ("did my edit look right") without a full render
@@ -1177,6 +1188,11 @@ async function main() {
1177
1188
  case "lint":
1178
1189
  await runLintCommand(rest);
1179
1190
  return;
1191
+ case "qa":
1192
+ case "qa-check":
1193
+ case "slop-check":
1194
+ await runQaCommand(rest);
1195
+ return;
1180
1196
  case "stills":
1181
1197
  await runStillsCommand(rest);
1182
1198
  return;
@@ -2156,6 +2172,8 @@ Rules:
2156
2172
  - When swapping visuals, match both the literal scene DNA and the narrative purpose of the beat.
2157
2173
  - For replacement graphics, screenshots, or still-like scenes, prefer AI image generation plus Ken Burns before paying for AI video unless static_vs_pivot says motion footage is load-bearing.
2158
2174
  - If narration must be customized, default to premium ElevenLabs first, then the user's own ElevenLabs path, then BYOK OpenAI/Gemini/OpenRouter. If captions or scenes were timed to the old VO, retime them to the new narration.
2175
+ - NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). None of that appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all fine. Captions use an imported family (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear) at weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
2176
+ - QA EVERY VIDEO BEFORE YOU RENDER: run \`vidfarm qa .\` in this directory. It's free, instant, and local — a blocklist for the slop above plus the font regime and safe zone, with a concrete fix per finding. It's feedback, not a gate (exits 0 even on findings, never runs automatically) and a blocklist, not an allowlist, so stylized or hand-made work passes untouched. Fix what's real, ignore what's a deliberate style call. \`--json\` for scripted batches.
2159
2177
 
2160
2178
  The three paintbrushes (Vidfarm is thrift-first — do NOT spend AI credits on every scene):
2161
2179
  - Paint each REPLACE beat with one of three brushes, cheapest first: (1) RAW CLIPS remixed from existing footage (\`vidfarm raws search\` the local/library first, then \`vidfarm raws scan <url|file>\` to hunt new ones — free local compute); (2) HTML/JS HYPERFRAMES — animate text/images/logos/stickers/charts with CSS or JS adapters (anime.js/GSAP/Lottie/Three) for titles, kinetic captions, data beats, brand cards; (3) PURE AI GENERATION (\`vidfarm generate image|video\`) — most expensive, AI video especially, last resort for beats no clip or hyperframe can cover.
@@ -3575,6 +3593,9 @@ async function runRenderCommand(argv) {
3575
3593
  }
3576
3594
  console.log(`${GREEN}Rendered locally in ${(result.durationMs / 1000).toFixed(1)}s → ${result.outputPath}${RESET}`);
3577
3595
  console.log(`${DIM}$0.00 — local Vidfarm render, no cloud charge.${RESET}`);
3596
+ // Nudge, not a gate: QA never runs automatically, but every production
3597
+ // should get a look before it's published.
3598
+ console.log(`${DIM}Recommended: ${RESET}vidfarm qa ${sourceDir ?? "<dir>"}${DIM} — social-native QA (HTML slop + font regime). Feedback only, free, instant.${RESET}`);
3578
3599
  }
3579
3600
  return;
3580
3601
  }
@@ -8615,6 +8636,54 @@ async function runLintCommand(argv) {
8615
8636
  if (!result.ok)
8616
8637
  process.exitCode = 1;
8617
8638
  }
8639
+ // `vidfarm qa <dir|composition.html>` — the social-native QA pass. Compositions
8640
+ // are authored in HTML, so an agent's web-page instincts leak in as landing-page
8641
+ // furniture (gradient CTA capsules, benefit chip rows, frosted cards) that never
8642
+ // appears in a real TikTok. This is a BLOCKLIST of those specific patterns plus
8643
+ // the font regime — deliberately NOT an allowlist, so weird/ugly/hand-made
8644
+ // compositions pass untouched and nothing pushes every video toward one style.
8645
+ //
8646
+ // FEEDBACK, NOT A GATE: exit code stays 0 even when slop is found, unless the
8647
+ // caller opts into --strict. Nothing in the render path calls this. It is a
8648
+ // devcli-only capability (no cloud/REST twin) — heavily recommended after every
8649
+ // production, never mandatory and never automatic.
8650
+ async function runQaCommand(argv) {
8651
+ const parsed = parseArgs({
8652
+ args: argv,
8653
+ allowPositionals: true,
8654
+ options: {
8655
+ json: { type: "boolean", default: false },
8656
+ strict: { type: "boolean", default: false }
8657
+ }
8658
+ });
8659
+ const target = parsed.positionals[0];
8660
+ if (!target)
8661
+ throw new Error("qa requires a composition path: `vidfarm qa <dir-or-composition.html> [--json] [--strict]`.");
8662
+ const htmlPath = resolveCompositionHtmlPath(target);
8663
+ const report = qaCompositionHtml(readFileSync(htmlPath, "utf8"));
8664
+ if (parsed.values.json) {
8665
+ printJson(report);
8666
+ if (!report.ok && parsed.values.strict)
8667
+ process.exitCode = 1;
8668
+ return;
8669
+ }
8670
+ const body = formatQaReport(report, { red: RED, yellow: YELLOW, green: GREEN, dim: DIM, reset: RESET });
8671
+ if (body)
8672
+ console.log(body);
8673
+ const scope = `${report.checked.layers} layer(s), ${report.checked.text_layers} text layer(s)${report.checked.canvas ? `, ${report.checked.canvas}` : ""}`;
8674
+ if (report.verdict === "clean") {
8675
+ console.log(`${GREEN}✓ no HTML slop found${RESET} ${DIM}— ${scope} — ${htmlPath}${RESET}`);
8676
+ }
8677
+ else if (report.verdict === "warnings") {
8678
+ console.log(`${YELLOW}! ${report.warnings.length} warning(s), no slop${RESET} ${DIM}— ${scope} — ${htmlPath}${RESET}`);
8679
+ }
8680
+ else {
8681
+ console.log(`${RED}✗ ${report.errors.length} slop issue(s)${RESET}, ${report.warnings.length} warning(s) ${DIM}— ${scope} — ${htmlPath}${RESET}`);
8682
+ console.log(`${DIM}This is feedback, not a gate — fix what's real and re-run. A deliberate stylistic choice that trips a rule is fine to ignore.${RESET}`);
8683
+ }
8684
+ if (!report.ok && parsed.values.strict)
8685
+ process.exitCode = 1;
8686
+ }
8618
8687
  // `vidfarm stills <dir|composition.html>` — render PNG stills of the CURRENT
8619
8688
  // composition in-process (the agent's visual self-verification loop).
8620
8689
  async function runStillsCommand(argv) {