@officexapp/vidfarm-devcli 0.21.37 → 0.21.38

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.
@@ -284,6 +284,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
284
284
  | `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate), spells out what the local keyer actually needs (a crisp silhouette in a different color from the plate, sealed shapes, clear gaps between items — hollow art and plate-colored detail INSIDE a shape are fine now), and carries that `--key-color` into the follow-up. `--single` for one subject. `--zoned` asks instead for a color-block sheet (one panel colour per item) and hands back a `--zones RxC` follow-up — worth it when the pack's own colors fight one plate, but leave it off for a free consumer tool that may not follow a grid. |
285
285
  | `vidfarm handoff raws --keywords "a,b" [--platforms tiktok,youtube] [--count N] [--purpose "…"]` | local (pure text) | **The interactive-mode CLIP-SOURCING brief** — the bottom rung of the sourcing ladder (browser control → `clipper`/`raws scan --cloud` → public raws → the human). Prints what to search, how to download (a Google *search* for a downloader, never a link that rots), and the import command for when the folder is ready. |
286
286
  | `vidfarm storyboard [dir] [--init] [--frames "Title\|scene,…"] [--json]` | local (`STORYBOARD.md` / `SCRIPT.md`) | **The plan pass, and a core part of the composition format.** Scaffolds/reads the project's `STORYBOARD.md`: ordered frames with `duration`/`status`/`src`/`scene`/`voiceover`. The Vidfarm editor renders this file in its **Storyboard** view (contact sheet + per-frame comments + `outline → built → animated` progress), so it's the cheapest place to get a director's approval before building. Not to be confused with `vidfarm sequence` (which GENERATES storyboard images for the pure-videogen pipeline). Alias: `plan`. |
287
+ | `vidfarm experiment [dir] [--init] [round …] [log <video> …] [--json]` | local (`EXPERIMENTS_DIARY.md`) | **Ad testing over weeks, not one video.** Owns the campaign ledger and ONLY that: sizes each round (`videos ÷ channels = epochs`), ranks the north-star metric, flags outliers vs the median, and lints the method — two variables in one structured round, a winner promoted off one post, results read at mixed ages, a structured round handed to gigworkers, unspent capacity. Feedback, not a gate (exits 0). Two writes: `round --videos N --variable angle …` and `log <video> --comments N --source flockposter --age 48h` (or `log <video> --posted --channel <id>`, which only RECORDS a post). It deliberately does not re-wrap `channels` (capacity), `harness`/`qa` (constants), `handoff` (briefs), `dedupe` (per-channel copies) or `approve`+`schedule` (posting). Method: <https://vidfarm.cc/experiment.md>. Alias: `experiments`. |
287
288
  | `vidfarm wallet [--job <id>\|--tracer <t>] [--limit <n>]` | `GET /api/v1/user/me/wallet` | cost log: balance + lifetime spend + recent charges. `--job <renderJobId>` prints **what that one video cost** (sums its charges); `--tracer <t>` sums a tracer. Cloud-only; readable on the free plan too (shows $0.00). Aliases: `spend`, `costs` |
288
289
  | `vidfarm provider-keys` / `vidfarm add-provider-key <p> <secret>` | `GET`·`POST /api/v1/user/me/provider-keys` | manage AI keys |
289
290
  | `vidfarm upload <file> [--folder <path>]` | presign → S3 PUT → finalize (`.../temporary-files/presign` + `.../temporary-files`) | upload → durable URL (ephemeral, 30-day TTL; prefer `--folder temp` for scratch). Goes direct to S3, so large files (up to **200 MB**) bypass the ~6 MB Lambda body limit |
package/SKILL.md CHANGED
@@ -103,6 +103,8 @@ Otherwise, pull it over the network and load only the reference you need:
103
103
  - `references/rest-api.md` — complete REST map (load only for direct HTTP integration)
104
104
  - `recipes/` — `find-and-fork-template.md` · `retheme-template.md` · `local-edit-render-approve.md` · `onboard-a-new-director.md` · `bulk-scripting-with-a-harness.md` (volume/N-variant work) · `cutout-graphics-for-explainers.md` (sticker-art explainers)
105
105
  - `harnesses/` — `README.md` (**start here for anything harness-shaped**: the three director phrasings, the format, the DNA strands) plus the bundled bases `short-form` · `hooks` · `ugc-testimonial` · `explainer` · `product-demo`, each as `<name>.HARNESS.md`
106
+ - **Running ad experiments (finding the winning ad):** `https://vidfarm.cc/experiment.md` — **fetch this whenever the ask is about performance rather than one video**: "which ad should I run", "test these angles", "what's working", "we post daily on N channels", or any campaign that runs for weeks. It primes you as a short-form ad scientist: the KPI interview (default north star = **comments**), channel count → testing capacity, **epochs** and **rounds**, **Creative Mode** (N free-form videos, the default, ideal for gigworkers) vs **Structured Mode** (one variable, everything else pinned by a harness, your own agents only), the composition params, and the standalone `EXPERIMENTS_DIARY.md` ledger. Plan → user approval → one static handoff document per video.
107
+ - **Crowdsourcing the editing:** `https://vidfarm.cc/crowdsourcing.md` — the client-vs-gigworker router for agentic clipper loops (a Dollar Platoon vending machine, recruiting editors, claiming tasks, proofs and payouts). This is how a Creative Mode batch gets produced in bulk.
106
108
  - **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.
107
109
  - **Skill pack index (what else is installable):** `https://vidfarm.cc/skill-pack/index.json`
108
110
  - **Platform architecture (how it all fits together):** `https://vidfarm.cc/skill/vidfarm-platform`
@@ -123,6 +125,7 @@ For composition *authoring* craft (motion, keyframes, scene design), Vidfarm shi
123
125
  5. "Script / batch / render loop" → `references/automation-and-local-dev.md`
124
126
  5b. **"Explainer with cutout/sticker art"** (flat illustrations on a stage, sticker sheets, "like those animated explainer videos") → `recipes/cutout-graphics-for-explainers.md`
125
127
  6. "I need TTS / music / captions / background removal" → `references/primitives.md`
128
+ 6b. **"Which ad wins?" / "test these angles" / "we post every day and want results"** → fetch <https://vidfarm.cc/experiment.md> and run the experimentation method (capacity → epochs/rounds → creative vs structured → `EXPERIMENTS_DIARY.md`). Do not improvise a testing plan; a round with two variables teaches nothing. To produce a bulk batch with gigworkers, pair it with <https://vidfarm.cc/crowdsourcing.md>.
126
129
  7. **"Update / upgrade vidfarm"** (or anything that smells like a stale install — a missing command, a 404 on a documented route, a version mismatch) → fetch <https://vidfarm.cc/update.md> and follow it. Update the skill pack and the devcli **together**; updating one alone is the usual cause of "the skill says to do X but it fails."
127
130
 
128
131
  ## Non-negotiables
@@ -0,0 +1,66 @@
1
+ # Crowdsourcing.md
2
+
3
+ How to crowdsource videos from vidfarm.cc
4
+ Please redirect user based on whether they are a client or a gigworker, instructions below.
5
+
6
+ ## Client Instructions
7
+
8
+ Please help setup agentic clipper loops for a client so they can get crowdsourced viral videos for their app. The setup is as follows:
9
+
10
+ - use dollarplatoon.com/skill.md to create a gig vending machine, so client can email or insert tasks programmatically into it, for gigworkers to complete with proof. signup is free, ideally use same email as your vidfarm.cc account. your DOLLARPLATOON_API_KEY can be found in https://dollarplatoon.com/client/settings
11
+
12
+ then add a first task to create a product explainer video for the clients website, or any other video creation task the client wants. it can be arbitrary and really anything found on vidfarm.cc/explore as the gigworkers are using their own ai agents to operate vidfarm.cc/skill.md to fulfill your tasks. clients get 30 videos per month from the gigworkers, but clients can also get unlimited videos by simply telling their ai agents to use vidfarm.cc themselves.
13
+
14
+ a better first batch than one arbitrary task: run a consultation on vidfarm.cc/skill.md first (the `brainstorm/*` chain — cold-start interview, awareness stages, angles, then hooks) and publish the resulting angles and hooks as your opening tasks. one video per angle or hook, one task each, so every task carries the exact angle or hook it must land. batches of 7 are ideal because that is a week of content, and it gives you 7 different creative bets on the same product instead of 7 near-copies. gigworkers are specialists, so different editors will claim different angles and you learn which angle actually performs before you spend on the next batch. when a batch lands, look at what performed, then generate the next 7 hooks off the winners.
15
+
16
+ in order to get gigworkers to join your project, client must create a private groupchat and post invites these communities:
17
+
18
+ - WhatsApp: https://chat.whatsapp.com/KSmoi7cdK1t9pyZzOvt6Bc
19
+ - Telegram: https://t.me/+NWMFh4Ua5SZiOTZl
20
+
21
+ for example:
22
+
23
+ ```
24
+ Looking for video editors to join my project!
25
+
26
+ Vending Machine:
27
+ https://dollarplatoon.com/gig/GIG_EXAMPLE/join?invite=example
28
+
29
+ Join Our Groupchat:
30
+ https://t.me/+example
31
+ ```
32
+
33
+ you can find the dollarplatoon vending machine invite link from creating a gig. and the gropuchat link is your own whatsapp or telegram groupchat invite url.
34
+
35
+ the video editors will arrive in your groupchat and you can directly communicate with them. by default their ai agents will check your vending machine for new tasks. every gigworker has their own specialized ai agent running vidfarm.cc/skill.md and specializing in specific video formats. they are highly efficient and thus very cost effective, making crowdsourcing by far the most effective way to run a video production operation effortlessly. best speed, quality, cost and diversity of creative. up to 100x better than other approaches.
36
+
37
+ you can look at dollarplatoon vending machine for latest videos submitted, and approve the ones you like, reject the ones you dont. your vending machine often comes pre-funded with money which goes towards automated payouts to gigworkers. if you want to add more funds you can.
38
+
39
+ if you are an ai agent and you will come back to these vending machines regularly, keep a written list of them on disk instead of relying on memory or on re-reading chat history. a plain markdown file like `dollarplatoon-gigs.md` next to your work is enough. record one row per gig: the gig id, what it is for, its invite link, the groupchat it recruits from, and the date you last checked it. re-derive it from the dollarplatoon api whenever it looks stale. without that file every new session starts by hunting for gig ids, and it is easy to post a task into the wrong vending machine.
40
+
41
+ overall work with your video editors and try to keep some reliable good ones. crowdsourcing means you might get a ton of submissions. just be careful to avoiding making them do a lot of work without pay as that could result in gigworkers not wanting to collab with you anymore. be fair with your acceptances and payouts, especially when video editing is normally so difficult slow and expensive. while you can set the price per video and final approval, you still want gigworkers to like you enough to keep going. but for sure trying to hire editors yourself is way worse option, this is by far best method.
42
+
43
+ for additional info, look at the dollarplatoon.com/skill.md or vidfarm.cc/skill.md
44
+
45
+ full tutorial for client pov agentic clipper loops can be found here:
46
+ https://www.youtube.com/watch?v=sR0C0V-WiRc
47
+
48
+ ## Gigworker Instructions
49
+
50
+ Please run agentic clipper loops for a gigworker using these REST APIs:
51
+
52
+ - vidfarm.cc/skill.md for creating videos for clients using cost saving mode. ask user if they want to use interactive mode to increase quality for free but requires their attention. if possible use stickers to create the videos. use any style prompts or custom instructions the gigworker has, as often they will specialize in specific video types
53
+ - dollarplatoon.com/skill.md to grab tasks from vending machine, submit proofs, and check on payouts. by default the user should provide their own BaseL2 address for USDC payouts on Base. many gigworkers are Philippines based so they will use gcash. the gcash setup tutorial can be found here: https://officex.short.gy/solana-usdc-gcash and your DOLLARPLATOON_API_KEY can be found in https://dollarplatoon.com/gigworker/settings
54
+
55
+ in order to join client projects, you need to get an invite link from them. those can be found by scrolling through these communities:
56
+
57
+ - WhatsApp: https://chat.whatsapp.com/KSmoi7cdK1t9pyZzOvt6Bc
58
+ - Telegram: https://t.me/+NWMFh4Ua5SZiOTZl
59
+
60
+ look for a dollarplatoon invite link that looks like this: https://dollarplatoon.com/gig/GIG_EXAMPLE/join?invite=example
61
+ its recommended that gigworkers also join the clients private groupchat to develop their own personal relationship with client, as this can often lead to direct hiring oppourtunities. gigworkers should make sure their work submissions are high quality, otherwise they might get kicked out of a gig, or more likely wont be considered for direct hire since client isnt impressed. quality impresses a client!
62
+
63
+ if you are an ai agent working several gigs, keep a written list of them on disk rather than in memory, because you will revisit these vending machines every single loop. a plain markdown file like `dollarplatoon-gigs.md` is enough. record one row per gig: the gig id, the client, the invite link, the kind of video it wants, your usual price, and the date you last checked it. read that file at the top of each loop and update it at the bottom. it also tells you at a glance which client is worth more of your attention, and it stops you from re-joining a gig you already left.
64
+
65
+ be careful about runaway infinite loops. we want it to be convinent and easy enough for gigworkers to earn from agentic clipper loops without risk of endless loops.
66
+ recommended that gigworkers watch the full tutorial here: https://www.youtube.com/watch?v=qiBUWr1a0yA
package/dist/src/cli.js CHANGED
@@ -38,6 +38,7 @@ import { CostModeBlockedError, assertBilledAllowed, clearStoredCostMode, costMod
38
38
  import { INTERACTION_MODES, INTERACTION_MODE_BLURB, clearStoredInteractionMode, interactionModeExplainer, interactionModeSummaryLine, interactiveOfferLine, normalizeInteractionMode, resolveInteractionMode, writeStoredInteractionMode } from "./devcli/interaction-mode.js";
39
39
  import { buildImageHandoff, buildRawsHandoff, formatHandoff } from "./devcli/handoff.js";
40
40
  import { STORYBOARD_FILENAME, readStoryboard, renderStoryboardScaffold, storyboardStage } from "./devcli/storyboard.js";
41
+ import { CORE_METRICS, DEFAULT_METRIC, EXPERIMENTS_FILENAME, EXPERIMENT_MODES, analyzeRound, appendResult, appendRound, capacityOf, epochsNeeded, findRoundIndex, lintDiary, markPosted, readDiary, renderDiaryScaffold, writeDiary } from "./devcli/experiments.js";
41
42
  // vidfarm-devcli — command-line bridge for the Vidfarm video studio. The
42
43
  // `serve` command boots the FULL editor locally (single origin, disk-backed
43
44
  // records + storage) so power users edit compositions on disk while a browser
@@ -130,6 +131,27 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
130
131
  --frames "Title|scene,…" Frames to scaffold · --count <n> for N blank frames
131
132
  --title/--format/--message/--arc/--audience Frontmatter for --init
132
133
  --json The parsed manifest (frames, statuses, warnings)
134
+ experiment [dir] AD TESTING — read the campaign's EXPERIMENTS_DIARY.md,
135
+ size each round (videos ÷ channels = epochs), rank the
136
+ north-star metric, flag outliers vs the median, and lint
137
+ the method (two variables in one structured round, a
138
+ winner promoted off one post, results read at mixed ages,
139
+ a structured round handed to gigworkers, unspent capacity).
140
+ Feedback, not a gate. Method: vidfarm.cc/experiment.md
141
+ (alias: experiments)
142
+ --init Scaffold the diary (--metric/--channels/--mode/--product/
143
+ --baseline/--editors · --force overwrites)
144
+ round --videos <n> Append a planned round + its epoch slot tables
145
+ [--variable angle] [--constants "format,hooks"] [--mode structured|creative]
146
+ [--slots "v001|angle,…"] [--why "…"] [--win "…"] [--title "…"]
147
+ log <video> Record what happened: --views/--comments/--clicks/--buys
148
+ [--source flockposter] [--age 48h] [--note "…"] [--round N],
149
+ or --posted [--channel <id>] to mark a slot live.
150
+ --json Parsed setup + per-round analysis + findings
151
+ It does NOT re-wrap what already exists: capacity comes from
152
+ "vidfarm channels", constants from "vidfarm harness", briefs
153
+ from "vidfarm handoff", posting from "vidfarm approve" +
154
+ "vidfarm schedule", per-channel copies from "vidfarm dedupe".
133
155
 
134
156
  Local editor loop:
135
157
  serve [template_id] [opts] Boot the editor locally. On a full source checkout the
@@ -1304,6 +1326,10 @@ async function main() {
1304
1326
  case "plan":
1305
1327
  await runStoryboardCommand(rest);
1306
1328
  return;
1329
+ case "experiment":
1330
+ case "experiments":
1331
+ await runExperimentCommand(rest);
1332
+ return;
1307
1333
  case "update-skill":
1308
1334
  case "skill":
1309
1335
  await runUpdateSkillCommand(rest);
@@ -9147,6 +9173,278 @@ async function runStoryboardCommand(argv) {
9147
9173
  console.log(`${YELLOW}warn:${RESET} ${DIM}${w.message}${RESET}`);
9148
9174
  console.log(`\n${DIM}Show it to the director in the editor's ${BOLD}Storyboard${RESET}${DIM} view (per-frame comments come back to you), then build frame by frame.${RESET}`);
9149
9175
  }
9176
+ // `vidfarm experiment [dir]` — the ad-testing ledger, and only the ledger.
9177
+ //
9178
+ // The method is https://vidfarm.cc/experiment.md: rounds of videos that vary one
9179
+ // composition param (structured) or vary everything (creative), posted across the
9180
+ // channels the director holds, read back against one north-star metric. This
9181
+ // command owns the three things nothing else in the CLI owns — the
9182
+ // EXPERIMENTS_DIARY.md format, the arithmetic (capacity → epochs, median,
9183
+ // outliers), and the method lint. Everything else it points at the command that
9184
+ // already does it: `vidfarm channels` (capacity), `vidfarm harness` (constants),
9185
+ // `vidfarm handoff` (per-video briefs), `vidfarm approve` + `schedule` (posting),
9186
+ // `vidfarm dedupe` (one variant per channel), `vidfarm qa` (per-video slop).
9187
+ // Reading is the default; there are exactly two writes, `round` and `log`.
9188
+ async function runExperimentCommand(argv) {
9189
+ const parsed = parseArgs({
9190
+ args: argv,
9191
+ allowPositionals: true,
9192
+ options: {
9193
+ ...commonOptions(),
9194
+ init: { type: "boolean", default: false },
9195
+ force: { type: "boolean", default: false },
9196
+ dir: { type: "string" },
9197
+ metric: { type: "string" },
9198
+ channels: { type: "string" },
9199
+ mode: { type: "string" },
9200
+ product: { type: "string" },
9201
+ baseline: { type: "string" },
9202
+ editors: { type: "string" },
9203
+ // round
9204
+ variable: { type: "string" },
9205
+ constants: { type: "string" },
9206
+ videos: { type: "string" },
9207
+ slots: { type: "string" },
9208
+ why: { type: "string" },
9209
+ win: { type: "string" },
9210
+ title: { type: "string" },
9211
+ // log
9212
+ round: { type: "string" },
9213
+ source: { type: "string" },
9214
+ age: { type: "string" },
9215
+ note: { type: "string" },
9216
+ channel: { type: "string" },
9217
+ posted: { type: "boolean", default: false },
9218
+ views: { type: "string" },
9219
+ comments: { type: "string" },
9220
+ clicks: { type: "string" },
9221
+ buys: { type: "string" },
9222
+ metricValue: { type: "string" },
9223
+ outlier: { type: "string" }
9224
+ }
9225
+ });
9226
+ const json = Boolean(parsed.values.json);
9227
+ const positionals = [...parsed.positionals];
9228
+ const VERBS = new Set(["round", "log"]);
9229
+ const verb = positionals[0] && VERBS.has(positionals[0].toLowerCase()) ? positionals.shift().toLowerCase() : "";
9230
+ const dir = path.resolve(process.cwd(), parsed.values.dir ?? (verb ? "." : positionals[0] ?? "."));
9231
+ if (!existsSync(dir))
9232
+ throw new Error(`No such directory: ${dir}`);
9233
+ const today = new Date().toISOString().slice(0, 10);
9234
+ const target = path.join(dir, EXPERIMENTS_FILENAME);
9235
+ const asList = (value) => (value ? value.split(/\s*[;,]\s*/).map((s) => s.trim()).filter(Boolean) : undefined);
9236
+ // ---- --init: scaffold the ledger ------------------------------------------
9237
+ if (parsed.values.init) {
9238
+ if (existsSync(target) && !parsed.values.force) {
9239
+ throw new Error(`${target} already exists. Append to it, or pass --force to overwrite it.`);
9240
+ }
9241
+ const mode = parsed.values.mode?.toLowerCase();
9242
+ if (mode && !EXPERIMENT_MODES.includes(mode)) {
9243
+ throw new Error(`Unknown mode "${mode}". Choose one of: ${EXPERIMENT_MODES.join(", ")}.`);
9244
+ }
9245
+ const body = renderDiaryScaffold({
9246
+ product: parsed.values.product,
9247
+ metric: parsed.values.metric?.toLowerCase(),
9248
+ channels: asList(parsed.values.channels) ?? [],
9249
+ mode: mode,
9250
+ baseline: parsed.values.baseline,
9251
+ editors: parsed.values.editors,
9252
+ date: today
9253
+ });
9254
+ writeDiary(target, body);
9255
+ if (json)
9256
+ return printJson({ ok: true, created: target });
9257
+ console.log(`${GREEN}Diary created:${RESET} ${target}`);
9258
+ console.log(`${DIM}Fill the channel list from ${BOLD}vidfarm channels${RESET}${DIM} — the channel count IS your testing capacity per epoch.${RESET}`);
9259
+ console.log(`${DIM}Then plan the first round: ${BOLD}vidfarm experiment round --variable angle --videos 8 --why "awareness level is the largest unknown"${RESET}`);
9260
+ return;
9261
+ }
9262
+ const read = readDiary(dir);
9263
+ if (!read.exists && verb) {
9264
+ throw new Error(`No ${EXPERIMENTS_FILENAME} in ${dir}. Start one: vidfarm experiment --init --metric comments --channels "a,b,c"`);
9265
+ }
9266
+ // ---- round: append a planned round ----------------------------------------
9267
+ if (verb === "round") {
9268
+ const capacity = capacityOf(read.manifest.setup);
9269
+ const mode = (parsed.values.mode ?? read.manifest.setup.mode ?? "creative").toLowerCase();
9270
+ if (!EXPERIMENT_MODES.includes(mode)) {
9271
+ throw new Error(`Unknown mode "${mode}". Choose one of: ${EXPERIMENT_MODES.join(", ")}.`);
9272
+ }
9273
+ const slots = asList(parsed.values.slots);
9274
+ const videos = parsed.values.videos !== undefined ? Number(parsed.values.videos) : slots?.length;
9275
+ if (!videos || !Number.isFinite(videos) || videos <= 0) {
9276
+ throw new Error("round requires --videos <n> (or --slots \"v001|angle,v002|angle\").");
9277
+ }
9278
+ const number = (read.manifest.rounds[read.manifest.rounds.length - 1]?.number ?? read.manifest.rounds.length) + 1;
9279
+ const input = {
9280
+ number,
9281
+ title: parsed.values.title,
9282
+ mode: mode,
9283
+ variable: parsed.values.variable,
9284
+ constants: asList(parsed.values.constants),
9285
+ justification: parsed.values.why,
9286
+ win: parsed.values.win,
9287
+ videos,
9288
+ editors: parsed.values.editors ?? read.manifest.setup.editors,
9289
+ capacity,
9290
+ slots,
9291
+ channels: read.manifest.setup.channels,
9292
+ date: today
9293
+ };
9294
+ const next = appendRound(readFileSync(read.absPath, "utf8"), input);
9295
+ writeDiary(read.absPath, next);
9296
+ const epochs = epochsNeeded(videos, capacity);
9297
+ if (json)
9298
+ return printJson({ ok: true, round: number, videos, capacity, epochs: Number.isFinite(epochs) ? epochs : null, path: read.absPath });
9299
+ console.log(`${GREEN}Round ${number} planned${RESET} ${DIM}${read.absPath}${RESET}`);
9300
+ console.log(` ${DIM}mode${RESET} ${mode}${input.variable ? ` ${DIM}variable${RESET} ${input.variable}` : ""}`);
9301
+ console.log(` ${DIM}sizing${RESET} ${videos} videos ÷ ${capacity || "?"} channels = ${BOLD}${Number.isFinite(epochs) ? epochs : "?"} epoch(s)${RESET}`);
9302
+ if (mode === "structured") {
9303
+ const rel = path.relative(process.cwd(), dir);
9304
+ const shown = !rel ? "." : rel.startsWith("..") ? dir : rel;
9305
+ console.log(`${DIM}Pin the constants so they are actually held: ${BOLD}vidfarm harness init short-form --out ${shown}/HARNESS.md${RESET}`);
9306
+ }
9307
+ console.log(`${DIM}Get the plan approved, then mint one standalone brief per video (${BOLD}vidfarm handoff${RESET}${DIM}) before any editing.${RESET}`);
9308
+ return;
9309
+ }
9310
+ // ---- log: record what happened (results, or a slot going live) ------------
9311
+ if (verb === "log") {
9312
+ const video = positionals[0];
9313
+ if (!video)
9314
+ throw new Error("log requires a video id, e.g. `vidfarm experiment log v001 --comments 61`.");
9315
+ const source = readFileSync(read.absPath, "utf8");
9316
+ if (parsed.values.posted) {
9317
+ const result = markPosted(source, read.manifest, video, { channel: parsed.values.channel, date: today });
9318
+ writeDiary(read.absPath, result.text);
9319
+ if (json)
9320
+ return printJson({ ok: true, video, posted: today, channel: parsed.values.channel ?? null });
9321
+ console.log(`${GREEN}Marked posted:${RESET} ${video}${parsed.values.channel ? ` → ${parsed.values.channel}` : ""} ${DIM}(${today})${RESET}`);
9322
+ console.log(`${DIM}This records it. The posting itself is ${BOLD}vidfarm approve${RESET}${DIM} + ${BOLD}vidfarm schedule${RESET}${DIM} — and dedupe first if it goes to more than one channel.${RESET}`);
9323
+ return;
9324
+ }
9325
+ const metrics = {};
9326
+ for (const key of CORE_METRICS) {
9327
+ const raw = parsed.values[key];
9328
+ if (raw === undefined)
9329
+ continue;
9330
+ const value = Number(String(raw).replace(/[,_\s]/g, ""));
9331
+ if (!Number.isFinite(value))
9332
+ throw new Error(`--${key} must be a number, got "${raw}".`);
9333
+ metrics[key] = value;
9334
+ }
9335
+ if (Object.keys(metrics).length === 0) {
9336
+ throw new Error("log needs at least one metric (--views/--comments/--clicks/--buys), or --posted to mark a slot live.");
9337
+ }
9338
+ const wanted = parsed.values.round !== undefined ? Number(parsed.values.round) : undefined;
9339
+ const roundIndex = findRoundIndex(read.manifest, wanted);
9340
+ if (roundIndex < 0)
9341
+ throw new Error(`No round ${wanted} in the diary.`);
9342
+ const written = appendResult(source, read.manifest, roundIndex, {
9343
+ video,
9344
+ metrics,
9345
+ note: parsed.values.note,
9346
+ source: parsed.values.source ?? read.manifest.setup.source,
9347
+ age: parsed.values.age,
9348
+ date: today
9349
+ });
9350
+ writeDiary(read.absPath, written.text);
9351
+ if (!parsed.values.age) {
9352
+ console.log(`${YELLOW}No --age given.${RESET} ${DIM}A 24h number and a 7d number are different measurements — record which one this is.${RESET}`);
9353
+ }
9354
+ if (json)
9355
+ return printJson({ ok: true, video, round: read.manifest.rounds[roundIndex]?.number ?? roundIndex + 1, metrics, block: written.block });
9356
+ console.log(`${GREEN}Logged${RESET} ${video} ${DIM}${Object.entries(metrics).map(([k, v]) => `${k} ${v}`).join(" · ")}${RESET}`);
9357
+ console.log(`${DIM}Read the round back with ${BOLD}vidfarm experiment${RESET}${DIM}.${RESET}`);
9358
+ return;
9359
+ }
9360
+ // ---- default: read, analyse, lint -----------------------------------------
9361
+ const outlierRatio = parsed.values.outlier !== undefined ? Number(parsed.values.outlier) : undefined;
9362
+ const analyses = read.manifest.rounds.map((r) => analyzeRound(r, read.manifest.setup, { metric: parsed.values.metric?.toLowerCase(), outlierRatio }));
9363
+ const findings = lintDiary(read.manifest, analyses);
9364
+ if (json) {
9365
+ return printJson({
9366
+ ok: true,
9367
+ exists: read.exists,
9368
+ path: read.absPath,
9369
+ setup: read.manifest.setup,
9370
+ capacity: capacityOf(read.manifest.setup),
9371
+ rounds: analyses.map((a) => ({
9372
+ number: a.round.number ?? a.round.index,
9373
+ title: a.round.title,
9374
+ mode: a.round.mode ?? read.manifest.setup.mode,
9375
+ variable: a.round.variable,
9376
+ constants: a.round.constants,
9377
+ metric: a.metric,
9378
+ videosPlanned: a.videosPlanned,
9379
+ epochsNeeded: Number.isFinite(a.epochsNeeded ?? NaN) ? a.epochsNeeded : null,
9380
+ epochsRun: a.epochsRun,
9381
+ posted: a.postedCount,
9382
+ unposted: a.unposted,
9383
+ awaiting: a.awaiting,
9384
+ median: a.median ?? null,
9385
+ scored: a.scored,
9386
+ outliers: a.outliers,
9387
+ finding: a.round.finding,
9388
+ next: a.round.next
9389
+ })),
9390
+ findings,
9391
+ warnings: read.manifest.warnings
9392
+ });
9393
+ }
9394
+ if (!read.exists) {
9395
+ console.log(`${YELLOW}No ${EXPERIMENTS_FILENAME} in ${dir}.${RESET}`);
9396
+ console.log(`${DIM}The diary is the source of truth for a campaign that runs for weeks — start it before the first post:${RESET}`);
9397
+ console.log(` ${BOLD}vidfarm experiment --init --metric comments --channels "$(vidfarm channels --json | jq -r '.channels[].destination_id' | paste -sd,)"${RESET}`);
9398
+ console.log(`${DIM}Method: https://vidfarm.cc/experiment.md${RESET}`);
9399
+ return;
9400
+ }
9401
+ const setup = read.manifest.setup;
9402
+ const capacity = capacityOf(setup);
9403
+ console.log(`${BOLD}Experiments${RESET} ${DIM}${read.absPath}${RESET}`);
9404
+ console.log(` ${DIM}north star${RESET} ${setup.metric ?? `${DEFAULT_METRIC} (assumed)`}`);
9405
+ console.log(` ${DIM}capacity${RESET} ${capacity || "?"} video(s)/epoch ${DIM}${setup.channels.join(", ")}${RESET}`);
9406
+ console.log(` ${DIM}mode${RESET} ${setup.mode ?? "creative (assumed)"}${setup.editors ? ` ${DIM}editors${RESET} ${setup.editors}` : ""}`);
9407
+ if (setup.baseline)
9408
+ console.log(` ${DIM}baseline${RESET} ${setup.baseline}`);
9409
+ for (const a of analyses) {
9410
+ const n = a.round.number ?? a.round.index;
9411
+ console.log("");
9412
+ console.log(` ${BOLD}Round ${n}${a.round.title ? ` — ${a.round.title}` : ""}${RESET} ${DIM}${a.round.mode ?? setup.mode ?? ""}${a.round.variable ? ` · variable: ${a.round.variable}` : ""}${RESET}`);
9413
+ if (a.videosPlanned !== undefined) {
9414
+ console.log(` ${DIM}sizing${RESET} ${a.videosPlanned} videos ÷ ${a.capacity || "?"} = ${Number.isFinite(a.epochsNeeded ?? NaN) ? a.epochsNeeded : "?"} epoch(s) · ${a.epochsRun} run · ${a.postedCount} posted`);
9415
+ }
9416
+ if (a.scored.length > 0) {
9417
+ console.log(` ${DIM}${a.metric} — median ${a.median ?? "?"}${RESET}`);
9418
+ for (const s of a.scored) {
9419
+ const mark = a.outliers.includes(s) ? `${GREEN}★${RESET}` : s === a.weakest ? `${DIM}·${RESET}` : " ";
9420
+ const ratio = s.ratio !== undefined ? `${s.ratio.toFixed(1)}×` : "";
9421
+ // When a round was read at more than one age, the same video appears once
9422
+ // per reading — label them, or the list looks like a duplicate row.
9423
+ const age = a.readContexts.length > 1 && s.age ? ` @${s.age}` : "";
9424
+ console.log(` ${mark} ${s.video.padEnd(8)} ${String(s.value).padStart(7)} ${DIM}${ratio.padStart(6)}${age}${s.variant ? ` ${s.variant}` : ""}${RESET}`);
9425
+ }
9426
+ }
9427
+ if (a.unposted.length > 0)
9428
+ console.log(` ${DIM}not posted yet: ${a.unposted.join(", ")}${RESET}`);
9429
+ if (a.round.finding)
9430
+ console.log(` ${DIM}finding${RESET} ${a.round.finding}`);
9431
+ if (a.round.next)
9432
+ console.log(` ${DIM}next${RESET} ${a.round.next}`);
9433
+ }
9434
+ if (findings.length > 0) {
9435
+ console.log("");
9436
+ for (const f of findings) {
9437
+ const tag = f.level === "error" ? `${RED}error${RESET}` : f.level === "warn" ? `${YELLOW}warn${RESET} ` : `${DIM}info${RESET} `;
9438
+ console.log(` ${tag} ${DIM}${f.code}${RESET} ${f.message}`);
9439
+ if (f.fix)
9440
+ console.log(` ${DIM}→ ${f.fix}${RESET}`);
9441
+ }
9442
+ }
9443
+ for (const w of read.manifest.warnings)
9444
+ console.log(` ${YELLOW}warn${RESET} ${DIM}${w.message}${w.line ? ` (line ${w.line})` : ""}${RESET}`);
9445
+ console.log("");
9446
+ console.log(`${DIM}Feedback, not a gate. Method: https://vidfarm.cc/experiment.md${RESET}`);
9447
+ }
9150
9448
  // `vidfarm mode [interactive|autonomous]` — show or set whether the HUMAN is in
9151
9449
  // the loop. Independent of cost mode: any cost mode runs either way, and the
9152
9450
  // pair is what determines the plan. Quality usually goes UP in interactive —