@officexapp/vidfarm-devcli 0.21.34 → 0.21.36

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 (28) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +14 -3
  2. package/.agents/skills/vidfarm/SKILL.md +66 -33
  3. package/.agents/skills/vidfarm/harnesses/README.md +112 -0
  4. package/.agents/skills/vidfarm/{regimes/explainer.QA_REGIME.md → harnesses/explainer.HARNESS.md} +3 -2
  5. package/.agents/skills/vidfarm/{regimes/hooks.QA_REGIME.md → harnesses/hooks.HARNESS.md} +3 -3
  6. package/.agents/skills/vidfarm/{regimes/product-demo.QA_REGIME.md → harnesses/product-demo.HARNESS.md} +1 -1
  7. package/.agents/skills/vidfarm/{regimes/short-form.QA_REGIME.md → harnesses/short-form.HARNESS.md} +39 -10
  8. package/.agents/skills/vidfarm/{regimes/ugc-testimonial.QA_REGIME.md → harnesses/ugc-testimonial.HARNESS.md} +3 -3
  9. package/.agents/skills/vidfarm/recipes/{bulk-scripting-with-a-regime.md → bulk-scripting-with-a-harness.md} +20 -12
  10. package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +43 -13
  11. package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -1
  12. package/.agents/skills/vidfarm/references/automation-and-local-dev.md +77 -26
  13. package/.agents/skills/vidfarm/references/editor-workflows.md +18 -5
  14. package/.agents/skills/vidfarm/references/hooks-and-virality.md +65 -7
  15. package/.agents/skills/vidfarm/references/reviewing-renders.md +2 -1
  16. package/.agents/skills/vidfarm-media/SKILL.md +2 -2
  17. package/.agents/skills/vidfarm-media/references/tts.md +26 -4
  18. package/SKILL.director.md +292 -98
  19. package/SKILL.md +33 -15
  20. package/dist/src/cli.js +1200 -141
  21. package/dist/src/devcli/handoff.js +54 -33
  22. package/dist/src/devcli/{qa-regime.js → harness.js} +132 -55
  23. package/dist/src/devcli/plate-key.js +698 -0
  24. package/dist/src/devcli/qa-check.js +209 -4
  25. package/dist/src/devcli/skill-docs.js +136 -0
  26. package/dist/src/devcli/sticker-pack.js +48 -0
  27. package/package.json +6 -4
  28. package/.agents/skills/vidfarm/regimes/README.md +0 -79
@@ -22,7 +22,8 @@
22
22
  // Both builders return plain text meant to be shown verbatim to the user, plus a
23
23
  // structured form for `--json` so an agent can render it its own way. No network,
24
24
  // no backend imports — this module is pure string assembly.
25
- import { pickPlateColor, keySafeArtInstruction } from "./sticker-pack.js";
25
+ import { pickPlateColor, connectivitySafeArtInstruction } from "./sticker-pack.js";
26
+ import { planZonedSheet, zonedSheetInstruction } from "./plate-key.js";
26
27
  const DEFAULT_STYLE = "simple flat vector illustration, minimal detail, 2-3 flat colors, no shadows, no text";
27
28
  /** Pick a sensible grid for N items (roughly square, wider than tall). */
28
29
  function gridFor(count) {
@@ -56,46 +57,66 @@ export function buildImageHandoff(input) {
56
57
  const pack = input.pack ?? items.length > 1;
57
58
  const outDir = input.outDir ?? "./stickers";
58
59
  const grid = input.grid ?? gridFor(items.length || 6);
59
- const prompt = pack
60
+ // ZONED sheet: hand the user a color-block grid instead of one plate. Worth it
61
+ // when the pack's own colors fight a single plate (a green frog next to a pink
62
+ // flower), because each panel's color is picked against its own item.
63
+ const zonePlan = pack && input.zoned ? planZonedSheet(items, items.length || 6, pickPlateColor, { theme: input.theme }) : null;
64
+ const prompt = zonePlan
60
65
  ? [
61
- `A sticker sheet of ${items.length || "several"} separate objects — ${style} — on a solid pure ${keyColor} background`,
62
- `(flat, evenly lit, no gradient, no shadow, no text, no labels, no frames or dividing lines).`,
63
- `Theme: ${input.theme}.`,
64
- items.length
65
- ? `Arranged in a ${grid} grid with generous even spacing: ${items.map((it, i) => `(${i + 1}) ${it}`).join(", ")}.`
66
- : `Arranged in a ${grid} grid with generous even spacing.`,
67
- `CRITICAL: every object must be fully separated from the others by a clear margin of plain ${keyColor} background —`,
68
- `nothing touching, overlapping or connected, and nothing touching the image edge.`,
69
- `One consistent art style, line weight and palette across all objects. Front-facing, centered in its own cell.`,
70
- // Load-bearing: the user is about to spend their own time on this sheet,
71
- // and hollow/outline-only art comes back as rims around holes.
72
- keySafeArtInstruction(keyColor),
66
+ `${input.theme} — ${style}.`,
67
+ zonedSheetInstruction(zonePlan, { theme: input.theme }),
73
68
  `Square image, high resolution.`
74
69
  ].join(" ")
75
- : [
76
- `${input.theme} — ${style} — isolated on a solid pure ${keyColor} background`,
77
- `(flat, evenly lit, no gradient, no shadow cast on the background, no text).`,
78
- `Center the subject with generous empty margin on all sides, crisp clean edges, single subject.`,
79
- keySafeArtInstruction(keyColor),
80
- `Square image, high resolution.`
81
- ].join(" ");
82
- const steps = pack
70
+ : pack
71
+ ? [
72
+ `A sticker sheet of ${items.length || "several"} separate objects — ${style} — on a solid pure ${keyColor} background`,
73
+ `(flat, evenly lit, no gradient, no shadow, no text, no labels, no frames or dividing lines).`,
74
+ `Theme: ${input.theme}.`,
75
+ items.length
76
+ ? `Arranged in a ${grid} grid with generous even spacing: ${items.map((it, i) => `(${i + 1}) ${it}`).join(", ")}.`
77
+ : `Arranged in a ${grid} grid with generous even spacing.`,
78
+ `CRITICAL: every object must be fully separated from the others by a clear margin of plain ${keyColor} background —`,
79
+ `nothing touching, overlapping or connected, and nothing touching the image edge.`,
80
+ `One consistent art style, line weight and palette across all objects. Front-facing, centered in its own cell.`,
81
+ // The local cut uses the smart (connectivity) keyer, so hollow shapes and
82
+ // plate-colored detail sealed inside an object are fine — only the
83
+ // silhouette has to stay distinguishable from the plate.
84
+ connectivitySafeArtInstruction(keyColor),
85
+ `Square image, high resolution.`
86
+ ].join(" ")
87
+ : [
88
+ `${input.theme} — ${style} — isolated on a solid pure ${keyColor} background`,
89
+ `(flat, evenly lit, no gradient, no shadow cast on the background, no text).`,
90
+ `Center the subject with generous empty margin on all sides, crisp clean edges, single subject.`,
91
+ connectivitySafeArtInstruction(keyColor),
92
+ `Square image, high resolution.`
93
+ ].join(" ");
94
+ const steps = zonePlan
83
95
  ? [
84
96
  "Open a FREE image generator you're already signed into (list below).",
85
- "Paste the prompt below and generate. Two checks before you send it back: (a) if any two objects are touching, re-generate asking for wider spacing touching objects get cut out as ONE sticker; (b) if any object is a hollow outline with the background showing through its middle, re-generate asking for solid fills that interior gets deleted with the background and the sticker ends up as a rim around a hole.",
97
+ "Paste the prompt below and generate. One check before you send it back: the image should be a grid of solid COLOR PANELS, one object per panel, no object crossing a panel edge and none touching its panel's edge. If the tool ignored the panels and drew one background instead, send it anyway I'll cut it the simple way.",
86
98
  "Download the image (PNG preferred) and tell me the file path — or drop it in this project folder.",
87
- `I'll split it into individual transparent stickers locally for $0: \`vidfarm sticker-pack <sheet> --out-dir ${outDir}\`.`
99
+ `I'll cut each panel with its own background color locally for $0: \`vidfarm sticker-pack <sheet> --zones ${zonePlan.rows}x${zonePlan.cols} --out-dir ${outDir}\`.`
88
100
  ]
89
- : [
90
- "Open a FREE image generator you're already signed into (list below).",
91
- "Paste the prompt below and generate.",
92
- "Download the image (PNG preferred) and tell me the file path.",
93
- "I'll key the background out and trim it to a snug transparent sticker locally for $0: `vidfarm cutout <file>`."
94
- ];
101
+ : pack
102
+ ? [
103
+ "Open a FREE image generator you're already signed into (list below).",
104
+ "Paste the prompt below and generate. Two checks before you send it back: (a) if any two objects are touching, re-generate asking for wider spacing — touching objects get cut out as ONE sticker; (b) make sure no object's outer edge blurs, glows or fades into the background — a crisp edge is what the cut needs.",
105
+ "Download the image (PNG preferred) and tell me the file path or drop it in this project folder.",
106
+ `I'll split it into individual transparent stickers locally for $0: \`vidfarm sticker-pack <sheet> --out-dir ${outDir}\`.`
107
+ ]
108
+ : [
109
+ "Open a FREE image generator you're already signed into (list below).",
110
+ "Paste the prompt below and generate.",
111
+ "Download the image (PNG preferred) and tell me the file path.",
112
+ "I'll key the background out and trim it to a snug transparent sticker locally for $0: `vidfarm cutout <file>`."
113
+ ];
95
114
  const keyFlag = keyColor === "#00FF00" ? "" : ` --key-color "${keyColor}"`;
96
- const followUp = pack
97
- ? `vidfarm sticker-pack <downloaded-sheet.png>${items.length ? ` --items "${items.join(",")}"` : ""}${keyFlag} --out-dir ${outDir}`
98
- : `vidfarm cutout <downloaded.png>${keyFlag}`;
115
+ const followUp = zonePlan
116
+ ? `vidfarm sticker-pack <downloaded-sheet.png>${items.length ? ` --items "${items.join(",")}"` : ""} --zones ${zonePlan.rows}x${zonePlan.cols} --out-dir ${outDir}`
117
+ : pack
118
+ ? `vidfarm sticker-pack <downloaded-sheet.png>${items.length ? ` --items "${items.join(",")}"` : ""}${keyFlag} --out-dir ${outDir}`
119
+ : `vidfarm cutout <downloaded.png>${keyFlag}`;
99
120
  return {
100
121
  prompt,
101
122
  steps,
@@ -1,17 +1,31 @@
1
- // QA_REGIME.md — a per-style quality contract for a template family.
1
+ // HARNESS.md — the reusable AI harness for one format or template.
2
2
  //
3
- // WHY IT EXISTS: `vidfarm qa`'s built-in rules are UNIVERSAL (no HTML slop, the
4
- // font regime, the thumbnail frame) they are the same for every video anyone
5
- // makes, so they can live in code. A regime is the opposite: it is what makes
6
- // THIS director's THIS format good, and it changes per account, per offer, per
7
- // campaign. That can't be hard-coded, so it lives next to the work as Markdown
8
- // the director owns, edits, and versions.
3
+ // A HARNESS is the written apparatus that lets an agent reproduce what makes a
4
+ // format good, over and over, without the director in the room. It is the unit
5
+ // the product is organised around: "create me a harness", "update the harness
6
+ // for this format", and "give me the harness for this template_id" are all the
7
+ // same noun, arrived at from different directions.
8
+ //
9
+ // TWO PROVENANCES, ONE ARTIFACT:
10
+ // • DERIVED — `vidfarm harness derive <forkId|dir>` distils a decomposed
11
+ // template's DNA (viral / visual / structural / audio / build) into a
12
+ // HARNESS.md. This is what "the harness for this template_id" means.
13
+ // • AUTHORED — `vidfarm harness init <base>` copies a bundled starting point
14
+ // the director then edits into their own format contract.
15
+ // Both produce the same file, and a derived one is still meant to be edited.
16
+ //
17
+ // WHY IT ISN'T CODE: `vidfarm qa`'s built-in rules are UNIVERSAL (no HTML slop,
18
+ // the caption font regime, the thumbnail frame) — the same for every video
19
+ // anyone makes, so they live in code. A harness is the opposite: it is what
20
+ // makes THIS format good, and it changes per account, per offer, per campaign,
21
+ // per source template. So it lives next to the work as Markdown the director
22
+ // owns, edits, versions, and hands to the next agent.
9
23
  //
10
24
  // It matters most in SCRIPTING MODE. One video gets human eyes on every frame;
11
- // fifty variants generated in a loop do not. The regime is what a bulk run is
25
+ // fifty variants generated in a loop do not. The harness is what a bulk run is
12
26
  // graded against — the thing that keeps variant #37 as good as variant #1.
13
27
  //
14
- // THE FORMAT is a plain Markdown doc with two machine-readable affordances:
28
+ // THE FORMAT is a plain Markdown doc with three machine-readable affordances:
15
29
  //
16
30
  // 1. Optional YAML-ish front matter with a `checks:` block. These are the
17
31
  // assertions the CLI can settle DETERMINISTICALLY from the composition DOM
@@ -21,27 +35,34 @@
21
35
  // a question the CLI hands back for the agent (or the human) to answer.
22
36
  // "Is the withheld answer one the viewer can't supply themselves?" is a
23
37
  // judgment call; pretending a linter can settle it would be a lie.
38
+ // 3. Any heading containing "DNA" is indexed as a DNA SECTION, so a harness
39
+ // speaks the same vocabulary the decompose pass writes into the template
40
+ // JSON (viral_dna, visual_dna, …) and an agent can ask for one strand by
41
+ // name instead of re-reading the whole doc.
24
42
  //
25
43
  // Everything else is prose the agent reads for context. That split is the whole
26
44
  // design: the CLI is honest about which half it can enforce, and it never
27
45
  // silently "passes" a video on the strength of the half it can't.
28
46
  //
29
- // COMPOSABLE BY CONSTRUCTION: regimes stack (`--regime a --regime b`), resolve
30
- // from a built-in name OR any path the user points at, and a directory's own
31
- // QA_REGIME.md is picked up automatically. Nothing here is a gate — same
32
- // feedback-not-a-gate contract as the rest of `vidfarm qa`.
47
+ // COMPOSABLE BY CONSTRUCTION: harnesses stack (`--harness a --harness b`),
48
+ // resolve from a built-in name OR any path the user points at, and a
49
+ // directory's own HARNESS.md is picked up automatically. Nothing here is a gate
50
+ // — same feedback-not-a-gate contract as the rest of `vidfarm qa`.
33
51
  import { existsSync, readFileSync, readdirSync } from "node:fs";
34
52
  import path from "node:path";
35
53
  import { fileURLToPath } from "node:url";
36
- const BUILTIN_SUFFIX = ".QA_REGIME.md";
37
- // ── Locating the built-in regimes ────────────────────────────────────────────
38
- // They ship inside the skill pack (.agents/skills/vidfarm/regimes/) rather than
39
- // as TS string constants, so a director can read, diff, and copy them as normal
40
- // files the file IS the documentation.
54
+ const BUILTIN_SUFFIX = ".HARNESS.md";
55
+ /** The canonical filename, plus the legacy names we still read (never write). */
56
+ export const HARNESS_FILENAME = "HARNESS.md";
57
+ const DISCOVERY_NAMES = ["HARNESS.md", "harness.md", "QA_REGIME.md", "qa_regime.md", "QA-REGIME.md"];
58
+ // ── Locating the built-in harnesses ──────────────────────────────────────────
59
+ // They ship inside the skill pack (.agents/skills/vidfarm/harnesses/) rather
60
+ // than as TS string constants, so a director can read, diff, and copy them as
61
+ // normal files — the file IS the documentation.
41
62
  function builtinDir() {
42
63
  let dir = path.dirname(fileURLToPath(import.meta.url));
43
64
  for (let i = 0; i < 6; i += 1) {
44
- const candidate = path.join(dir, ".agents", "skills", "vidfarm", "regimes");
65
+ const candidate = path.join(dir, ".agents", "skills", "vidfarm", "harnesses");
45
66
  if (existsSync(candidate))
46
67
  return candidate;
47
68
  const parent = path.dirname(dir);
@@ -51,7 +72,7 @@ function builtinDir() {
51
72
  }
52
73
  return null;
53
74
  }
54
- export function listBuiltinRegimes() {
75
+ export function listBuiltinHarnesses() {
55
76
  const dir = builtinDir();
56
77
  if (!dir)
57
78
  return [];
@@ -60,12 +81,12 @@ export function listBuiltinRegimes() {
60
81
  .sort()
61
82
  .map((file) => {
62
83
  const full = path.join(dir, file);
63
- const parsed = parseRegime(readFileSync(full, "utf8"), full);
84
+ const parsed = parseHarness(readFileSync(full, "utf8"), full);
64
85
  return { name: file.slice(0, -BUILTIN_SUFFIX.length), path: full, video_type: parsed.video_type, summary: parsed.summary };
65
86
  });
66
87
  }
67
- /** Resolve `hooks` (built-in) or `./my/QA_REGIME.md` (a path) to a file. */
68
- export function resolveRegimePath(ref) {
88
+ /** Resolve `hooks` (built-in) or `./my/HARNESS.md` (a path) to a file. */
89
+ export function resolveHarnessPath(ref) {
69
90
  const direct = path.resolve(ref);
70
91
  if (existsSync(direct) && !direct.endsWith(path.sep))
71
92
  return direct;
@@ -75,18 +96,28 @@ export function resolveRegimePath(ref) {
75
96
  if (existsSync(candidate))
76
97
  return candidate;
77
98
  }
78
- const names = listBuiltinRegimes().map((entry) => entry.name);
79
- throw new Error(`No QA regime "${ref}". Pass a file path, or one of the built-ins: ${names.join(", ") || "(none bundled)"}. ` +
80
- `Scaffold your own with \`vidfarm regime init <name> --out ./work/QA_REGIME.md\`.`);
99
+ const names = listBuiltinHarnesses().map((entry) => entry.name);
100
+ throw new Error(`No harness "${ref}". Pass a file path, or one of the built-ins: ${names.join(", ") || "(none bundled)"}. ` +
101
+ `Scaffold your own with \`vidfarm harness init <name> --out ./work/HARNESS.md\`, ` +
102
+ `or derive one from a decomposed template with \`vidfarm harness derive <forkId>\`.`);
81
103
  }
82
- /** A directory's own regime, if it has one. Case-tolerant on the filename. */
83
- export function discoverRegime(dir) {
84
- for (const name of ["QA_REGIME.md", "qa_regime.md", "QA-REGIME.md"]) {
104
+ /**
105
+ * A directory's own harness, if it has one. `HARNESS.md` is canonical; the
106
+ * legacy `QA_REGIME.md` names are still read so existing work directories keep
107
+ * working, but nothing writes them any more.
108
+ */
109
+ export function discoverHarness(dir) {
110
+ for (const name of DISCOVERY_NAMES) {
85
111
  const candidate = path.join(dir, name);
86
112
  if (existsSync(candidate))
87
113
  return candidate;
88
114
  }
89
- return null;
115
+ const nested = path.join(dir, ".harness", HARNESS_FILENAME);
116
+ return existsSync(nested) ? nested : null;
117
+ }
118
+ /** True for the legacy filenames, so callers can nudge without breaking. */
119
+ export function isLegacyHarnessFilename(file) {
120
+ return /^qa[_-]regime\.md$/i.test(path.basename(file));
90
121
  }
91
122
  /**
92
123
  * A deliberately tiny front-matter reader: `key: value`, and one level of
@@ -149,9 +180,15 @@ function parseFrontMatter(raw) {
149
180
  const KNOWN_CHECKS = new Set([
150
181
  "duration_sec", "aspect", "first_frame_visual", "first_frame_text", "hook_words_max",
151
182
  "text_by_sec", "audio", "captions", "font_regime", "safe_zone", "forbid_text",
152
- "require_text", "max_text_cards", "max_simultaneous_text", "scenes", "max_scene_sec"
183
+ "require_text", "max_text_cards", "max_simultaneous_text", "max_words_per_cue", "max_dead_air_sec",
184
+ "max_tail_sec", "scenes", "max_scene_sec"
153
185
  ]);
154
- export function parseRegime(raw, source) {
186
+ /** "Visual DNA the look" → `visual_dna`. Matches the decompose JSON keys. */
187
+ function dnaKey(heading) {
188
+ const head = heading.split(/[—–:(]/)[0];
189
+ return head.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
190
+ }
191
+ export function parseHarness(raw, source) {
155
192
  const { front, body } = parseFrontMatter(raw);
156
193
  const checks = {};
157
194
  const unknown = [];
@@ -165,13 +202,27 @@ export function parseRegime(raw, source) {
165
202
  unknown.push(bare);
166
203
  }
167
204
  const reviewItems = [];
205
+ const dna = [];
206
+ let openDna = null;
207
+ let openDnaLevel = 0;
168
208
  let section = "Checklist";
169
209
  for (const line of body.split(/\r?\n/)) {
170
- const heading = line.match(/^#{1,6}\s+(.*)$/);
210
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
171
211
  if (heading) {
172
- section = heading[1].replace(/[*_`]/g, "").trim();
212
+ section = heading[2].replace(/[*_`]/g, "").trim();
213
+ // A "…DNA" heading opens a strand; the next heading at the same level or
214
+ // shallower closes it, so nested sub-headings stay inside their strand.
215
+ if (openDna && heading[1].length <= openDnaLevel)
216
+ openDna = null;
217
+ if (/\bdna\b/i.test(section)) {
218
+ openDna = { key: dnaKey(section), heading: section, body: "" };
219
+ openDnaLevel = heading[1].length;
220
+ dna.push(openDna);
221
+ }
173
222
  continue;
174
223
  }
224
+ if (openDna)
225
+ openDna.body += `${line}\n`;
175
226
  const bold = line.match(/^\*\*(.+?)\*\*\s*$/);
176
227
  if (bold) {
177
228
  section = bold[1].trim();
@@ -181,14 +232,19 @@ export function parseRegime(raw, source) {
181
232
  if (box)
182
233
  reviewItems.push({ section, text: box[2].trim() });
183
234
  }
235
+ for (const strand of dna)
236
+ strand.body = strand.body.trim();
184
237
  const headline = body.split(/\r?\n/).find((line) => line.trim() && !line.startsWith("#") && !line.startsWith(">"));
185
- const name = String(front.name ?? path.basename(source).replace(/\.md$/i, "").replace(/\.QA_REGIME$/i, ""));
238
+ const name = String(front.name ?? path.basename(source).replace(/\.md$/i, "").replace(/\.(HARNESS|QA_REGIME)$/i, ""));
186
239
  return {
187
240
  name,
188
241
  source,
189
242
  video_type: front.video_type ? String(front.video_type) : null,
190
243
  summary: headline ? headline.replace(/[*_`]/g, "").trim().slice(0, 160) : null,
244
+ derived_from: front.derived_from ? String(front.derived_from) : null,
245
+ source_template_id: front.source_template_id ? String(front.source_template_id) : null,
191
246
  checks,
247
+ dna,
192
248
  review_items: reviewItems,
193
249
  unknown_check_keys: unknown
194
250
  };
@@ -225,15 +281,15 @@ function isRequired(value) {
225
281
  function isForbidden(value) {
226
282
  return String(value ?? "").trim().toLowerCase() === "forbidden";
227
283
  }
228
- export function evaluateRegime(parsed, facts) {
284
+ export function evaluateHarness(parsed, facts) {
229
285
  const checks = [];
230
286
  const findings = [];
231
- const where = `QA_REGIME "${parsed.name}"`;
287
+ const where = `harness "${parsed.name}"`;
232
288
  const settle = (key, expected, actual, ok, fix) => {
233
289
  checks.push({ key, expected, actual, ok });
234
290
  if (!ok) {
235
291
  findings.push({
236
- rule: `regime:${key}`,
292
+ rule: `harness:${key}`,
237
293
  severity: "error",
238
294
  message: `${parsed.name}: ${key} expects ${expected} — this composition is ${actual}.`,
239
295
  where,
@@ -244,7 +300,7 @@ export function evaluateRegime(parsed, facts) {
244
300
  const c = parsed.checks;
245
301
  if (c.duration_sec !== undefined && facts.duration_sec !== null) {
246
302
  const spec = String(c.duration_sec);
247
- settle("duration_sec", spec, `${facts.duration_sec.toFixed(1)}s`, rangeSatisfied(spec, facts.duration_sec), "Retime the timeline (`vidfarm retime` / `vidfarm ripple`) or set the composition duration to land inside the regime's window.");
303
+ settle("duration_sec", spec, `${facts.duration_sec.toFixed(1)}s`, rangeSatisfied(spec, facts.duration_sec), "Retime the timeline (`vidfarm retime` / `vidfarm ripple`) or set the composition duration to land inside the harness's window.");
248
304
  }
249
305
  if (c.aspect !== undefined && facts.canvas.aspect) {
250
306
  const allowed = String(c.aspect).split(/[|,]/).map((entry) => entry.trim());
@@ -259,7 +315,7 @@ export function evaluateRegime(parsed, facts) {
259
315
  settle("first_frame_text", "hook words up at t=0", has ? `"${facts.first_frame_text}"` : "no text at t=0", has, "Start the hook caption at 0 so the thumbnail states the promise before a word is spoken (muted autoplay is the default).");
260
316
  }
261
317
  else if (isForbidden(c.first_frame_text)) {
262
- settle("first_frame_text", "no text at t=0", has ? `"${facts.first_frame_text}"` : "clean", !has, "Delay the first text layer past 0 — this regime wants the opening frame to carry the image alone.");
318
+ settle("first_frame_text", "no text at t=0", has ? `"${facts.first_frame_text}"` : "clean", !has, "Delay the first text layer past 0 — this harness wants the opening frame to carry the image alone.");
263
319
  }
264
320
  }
265
321
  if (c.hook_words_max !== undefined) {
@@ -282,7 +338,7 @@ export function evaluateRegime(parsed, facts) {
282
338
  settle("audio", "at least one audio layer", `${facts.audio_layers}`, facts.audio_layers > 0, "Add narration or a bed (`vidfarm tts` free local voice, or `vidfarm media search --type bgm`).");
283
339
  }
284
340
  else if (isForbidden(c.audio)) {
285
- settle("audio", "no audio layers", `${facts.audio_layers}`, facts.audio_layers === 0, "Remove the audio layers — this regime is for silent/overlay output.");
341
+ settle("audio", "no audio layers", `${facts.audio_layers}`, facts.audio_layers === 0, "Remove the audio layers — this harness is for silent/overlay output.");
286
342
  }
287
343
  }
288
344
  if (isRequired(c.captions)) {
@@ -298,11 +354,11 @@ export function evaluateRegime(parsed, facts) {
298
354
  }
299
355
  for (const phrase of asList(c.forbid_text)) {
300
356
  const hit = phrase && new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(facts.all_text);
301
- settle("forbid_text", `no "${phrase}" on screen`, hit ? `found "${phrase}"` : "absent", !hit, "Rewrite the line. The regime bans this phrasing for a reason recorded in the doc — read the section it sits under.");
357
+ settle("forbid_text", `no "${phrase}" on screen`, hit ? `found "${phrase}"` : "absent", !hit, "Rewrite the line. The harness bans this phrasing for a reason recorded in the doc — read the section it sits under.");
302
358
  }
303
359
  for (const phrase of asList(c.require_text)) {
304
360
  const hit = phrase && new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i").test(facts.all_text);
305
- settle("require_text", `"${phrase}" appears on screen`, hit ? "present" : "missing", Boolean(hit), "Add the required line as a timed caption — this is usually a compliance or positioning beat the regime treats as mandatory.");
361
+ settle("require_text", `"${phrase}" appears on screen`, hit ? "present" : "missing", Boolean(hit), "Add the required line as a timed caption — this is usually a compliance or positioning beat the harness treats as mandatory.");
306
362
  }
307
363
  if (c.max_text_cards !== undefined) {
308
364
  const max = Number(c.max_text_cards);
@@ -316,9 +372,27 @@ export function evaluateRegime(parsed, facts) {
316
372
  settle("max_simultaneous_text", `≤${max} text runs at once`, `${facts.max_simultaneous_text}`, facts.max_simultaneous_text <= max, "Stagger the overlapping text layers. Two things to read at once means neither gets read.");
317
373
  }
318
374
  }
375
+ if (c.max_words_per_cue !== undefined) {
376
+ const max = Number(c.max_words_per_cue);
377
+ if (Number.isFinite(max)) {
378
+ settle("max_words_per_cue", `≤${max} words in any one text run`, `${facts.max_words_in_run}`, facts.max_words_in_run <= max, "Page the long run into kinetic cues — `vidfarm captions generate <dir> --style word-pop` splits narration into ~3–5-word cues on real word timings. A paragraph on screen gets read by nobody.");
379
+ }
380
+ }
381
+ if (c.max_dead_air_sec !== undefined) {
382
+ const max = Number(c.max_dead_air_sec);
383
+ if (Number.isFinite(max)) {
384
+ settle("max_dead_air_sec", `≤${max}s with nothing to read`, `${facts.max_dead_air_sec}s`, facts.max_dead_air_sec <= max, "Run the deletion test on that stretch and cut it, then CLOSE the hole with `vidfarm ripple <dir> --at <sec> --delta -<sec>`. Dead screen time is a free exit.");
385
+ }
386
+ }
387
+ if (c.max_tail_sec !== undefined) {
388
+ const max = Number(c.max_tail_sec);
389
+ if (Number.isFinite(max)) {
390
+ settle("max_tail_sec", `≤${max}s after the last word`, `${facts.tail_silence_sec}s`, facts.tail_silence_sec <= max, "End on the bait. Trim the trailing clip and pull the composition duration in so the last frame is the one that loops best.");
391
+ }
392
+ }
319
393
  if (c.scenes !== undefined) {
320
394
  const spec = String(c.scenes);
321
- settle("scenes", `${spec} visual clips`, `${facts.visual_clips.length}`, rangeSatisfied(spec, facts.visual_clips.length), "Split or merge scenes to hit the regime's pacing shape (`vidfarm split` / `vidfarm retime`).");
395
+ settle("scenes", `${spec} visual clips`, `${facts.visual_clips.length}`, rangeSatisfied(spec, facts.visual_clips.length), "Split or merge scenes to hit the harness's pacing shape (`vidfarm split` / `vidfarm retime`).");
322
396
  }
323
397
  if (c.max_scene_sec !== undefined) {
324
398
  const max = Number(c.max_scene_sec);
@@ -329,7 +403,7 @@ export function evaluateRegime(parsed, facts) {
329
403
  }
330
404
  for (const key of parsed.unknown_check_keys) {
331
405
  findings.push({
332
- rule: "regime:unknown-check",
406
+ rule: "harness:unknown-check",
333
407
  severity: "warn",
334
408
  message: `${parsed.name}: unknown check "${key}" — ignored (the CLI can't settle it).`,
335
409
  where,
@@ -340,22 +414,23 @@ export function evaluateRegime(parsed, facts) {
340
414
  name: parsed.name,
341
415
  source: parsed.source,
342
416
  video_type: parsed.video_type,
417
+ dna: parsed.dna.map((strand) => strand.key),
343
418
  checks,
344
419
  review_items: parsed.review_items,
345
420
  findings
346
421
  };
347
422
  }
348
- export function loadAndEvaluateRegime(ref, facts) {
349
- const file = resolveRegimePath(ref);
350
- return evaluateRegime(parseRegime(readFileSync(file, "utf8"), file), facts);
423
+ export function loadAndEvaluateHarness(ref, facts) {
424
+ const file = resolveHarnessPath(ref);
425
+ return evaluateHarness(parseHarness(readFileSync(file, "utf8"), file), facts);
351
426
  }
352
427
  /**
353
- * Fold regime results into a QaReport. Regime failures are the DIRECTOR's own
428
+ * Fold harness results into a QaReport. Harness failures are the DIRECTOR's own
354
429
  * rules, so they land in the same errors list as the built-ins — but the verdict
355
430
  * stays advisory: `vidfarm qa` still exits 0 unless the caller asked for
356
431
  * --strict, exactly as it does for slop.
357
432
  */
358
- export function mergeRegimeIntoReport(report, evaluations) {
433
+ export function mergeHarnessIntoReport(report, evaluations) {
359
434
  const findings = evaluations.flatMap((evaluation) => evaluation.findings);
360
435
  const errors = [...report.errors, ...findings.filter((finding) => finding.severity === "error")];
361
436
  const warnings = [...report.warnings, ...findings.filter((finding) => finding.severity === "warn")];
@@ -365,17 +440,19 @@ export function mergeRegimeIntoReport(report, evaluations) {
365
440
  verdict: errors.length ? "slop" : warnings.length ? "warnings" : "clean",
366
441
  errors,
367
442
  warnings,
368
- regimes: evaluations
443
+ harnesses: evaluations
369
444
  };
370
445
  }
371
- /** Human-readable regime block for the CLI (findings are printed by qa itself). */
372
- export function formatRegimeReport(evaluation, colors) {
446
+ /** Human-readable harness block for the CLI (findings are printed by qa itself). */
447
+ export function formatHarnessReport(evaluation, colors) {
373
448
  const { green, red, dim, reset } = colors;
374
449
  const lines = [];
375
450
  const failed = evaluation.checks.filter((check) => !check.ok).length;
376
- lines.push(`${dim}regime${reset} ${evaluation.name}${evaluation.video_type ? ` ${dim}(${evaluation.video_type})${reset}` : ""} ` +
451
+ lines.push(`${dim}harness${reset} ${evaluation.name}${evaluation.video_type ? ` ${dim}(${evaluation.video_type})${reset}` : ""} ` +
377
452
  `${dim}— ${evaluation.checks.length - failed}/${evaluation.checks.length} machine checks passed, ` +
378
453
  `${evaluation.review_items.length} item(s) need your judgment${reset}`);
454
+ if (evaluation.dna.length)
455
+ lines.push(` ${dim}DNA documented: ${evaluation.dna.join(", ")}${reset}`);
379
456
  for (const check of evaluation.checks) {
380
457
  const mark = check.ok ? `${green}✓${reset}` : `${red}✗${reset}`;
381
458
  lines.push(` ${mark} ${check.key} ${dim}expects ${check.expected} — got ${check.actual}${reset}`);
@@ -393,4 +470,4 @@ export function formatRegimeReport(evaluation, colors) {
393
470
  }
394
471
  return lines.join("\n");
395
472
  }
396
- //# sourceMappingURL=qa-regime.js.map
473
+ //# sourceMappingURL=harness.js.map