← Both skills · static-ads · 2-make/PIPELINE.md
Clone a winner: same skeleton, new flesh. Change the minimum. A template that already wins on Meta has solved the hard part (the scroll-stop trick, the layout, the proof, the emotion). Our job is to keep all of that and swap only what belongs to THEIR product and THEIR customer for ours — then let a review loop catch what we misread, and turn every miss into a permanent rule so it never happens twice.
Built from the Wildhorn production (wild bison organ capsules, Oct 2026):
The difference between the two runs is entirely this protocol. Follow it step by step.
| Part | Role |
|---|---|
BRAND-KIT.md + brand.json (skill root) |
The brand's facts, voice, offer, proof, avatar, bans — the only input. Blank template here; filled example in the wildhorn-static-ads skill. |
1-understand/ |
Upstream. Research, mechanisms, avatar, top-5 angles, headline bank, template reading, and one hand-off brief per template (1-understand/new-brand-worksheet.md §9). Run it first for a new brand. |
2-make/ (this folder) |
Briefs/templates → specs → generation → QC → review pages. Brand-agnostic. |
3-improve/ |
Loom → verdicts → rules → lint → stats (3-improve/feedback-loop.md, 3-improve/rules.md, 3-improve/stats.md). |
| wildhorn-static-ads (separate skill) | The same skill with the Wildhorn brand already loaded (wildhorn/ folder there: brand facts, Adrien's taste, winners, failures). Same rule IDs as 3-improve/rules.md. |
| ad-blueprint-gpt-image-2 | Older: JSON blueprints + paste-ready ChatGPT prompts from a URL (H1–H16 headline library, 21 formats). Its Section 18 "Template Clone Protocol" is superseded by this skill; its Section 8 prompt-craft principles are folded into 3-improve/rules.md §F. |
Work location: the VPS (ssh vps-claude). Skill folder: /root/claude-vault/skills/static-ads/ (call it $SKILL; scripts in $SKILL/scripts/). Each brand gets its own
working folder (/root/workspace/<brand>-remix, $SAC_WORKDIR). Never generate on the Mac; never write into /srv/** or another brand's workspace
directly; secrets are read from /etc/secrets/openai_api_key and /etc/secrets/kie_api_key (paths only, never values).
ssh vps-claude
export SKILL=/root/claude-vault/skills/static-ads
export SAC_WORKDIR=/root/workspace/<brand>-remix # out/, batch/, brand.json, feedback/, pages/
mkdir -p $SAC_WORKDIR/{batch/templates,batch/prompts,out,feedback,pages}
cp $SKILL/brand.json $SAC_WORKDIR/brand.json # blank template: fill it from BRAND-KIT.md
cd $SAC_WORKDIR
# specs (by hand, or drafted from 1-understand hand-off briefs)
python3 $SKILL/scripts/brief2spec.py batch/s1specs.py /root/workspace/<brand>-kit/briefs/*.json
python3 $SKILL/scripts/lint.py batch/s1specs.py # must print LINT OK
# generate ad N (lint → engine chain → composite; logs batch/calls.jsonl)
python3 $SKILL/scripts/srun.py batch/s1specs.py 1
for n in $(seq 1 10); do echo $n; done | xargs -P 5 -I{} python3 $SKILL/scripts/srun.py batch/s1specs.py {}
# pages, stats, Loom
python3 $SKILL/scripts/build_sam.py # pages/review.html (only ads to review)
python3 $SKILL/scripts/build_samfinal.py # pages/final.html + batch/sell/
python3 $SKILL/scripts/build_validated.py # pages/validated.html + batch/validated/
python3 $SKILL/scripts/stats.py --ads 10
bash $SKILL/scripts/ingest_loom.sh "<loom url>" s1
1-understand/ first (1-understand/new-brand-worksheet.md) (product sheet, mechanism map, avatar, top-5 angles, headline bank; founder sign-off).BRAND-KIT.md, then brand.json from it (BRAND-KIT.md, section "brand.json — key by key"): product lock sentence, product photo + cutout, wordmark + icon, offer + allowed prices, proof
claims, avatar (age/look, core problem, hiding behaviours, what he tried, pleasure he keeps, proud proof), mechanism, angles (one default),
competitors, banned words, compliance, angle keyword, visual checks.OLD), ingredient look, capsule look, icon-only sentence, STD keep-lines.1-understand/new-brand-worksheet.md §8 intake). Accept every template (R1); sort into concept families (R2); one ad per template (R4).batch/templates/ with names whose sorted order is the ad order; real extensions (D10). Ad number = rank in that list (D13, linted).Write, before anything else:
batch/sNspecs.py (2-make/spec-format.md). Required: why dna scene placement inventory(≥5) visual texts engine.KEEP … / SWAP X → Y (reason) (A13).placement copies the template's product spot and gesture (A8, C17). texts = one line per template text slot, same count, exact strings (B1, B13).Copy rules: benefits vs what he tried (B3), clear rival + clear problem (B5), no jargon (B4), no Autoship (B7), his real behaviours (B8),
proud proof (B9), aggression = precision (B10), concede the pleasure (B11), simplest words (B15), stats as benefit claims (B16),
twists literally true (A27), villain = mechanism (A28), known reference swapped (B17), routines because of the problem (B18), his words (B19),
real facts only (B20). Deadline only if the template has one (template_has_deadline) (B6). Signature only if it has one (template_has_signature) (B12).
Engine per spec (Step 7). Sexual/skin templates: sexual=True, fallback="seedream_comp", sd_prompt, composite with EVERY product placement (D1, D6).
brief2spec.py drafts the fields; you rewrite visual and do the judgment (2-make/spec-format.md "Input from 1-understand").python3 $SKILL/scripts/lint.py batch/sNspecs.py — any error = nothing is sent (srun.py re-lints the whole file every call).
It checks: required fields, inventory ≥ 5, placement, banned words (generic + brand), competitor names, signed quotes (B12), invented deadlines (B6),
core-problem keyword (B2), ingredient visual checks (C2), sexual → fallback (D1), scene engines → sd_prompt + composite (D1/D7), product count parity (D6),
seedream_full banned (D7), bare wordmark text lines (D8), emoji in composite (D9), prices (B20), template rank (D13). Read the warnings too.
| Template | Engine |
|---|---|
| Text on a flat background | pure_comp — composite.py only (zero spelling risk) |
| Normal | openai — gpt-image-2 images/edits, quality high, 1024², Image 1 = product photo, Image 2 = template; prompt = LOCK → inventory → DNA → scene → placement → changes → text → CONS |
| Sexual / skin / bare chest | openai + fallback="seedream_comp", or straight seedream_comp: Seedream renders the scene only (no text, no product), composite.py adds the real product + exact text |
| Text painted into the scene | Nano Banana Pro (kie_gen.py --model nano) |
| Photo with a pasted product, or failed twice by regeneration | A25 method: Seedream minimal edit of the template itself (keep ratio, --aspect portrait_16_9) → jarswap.py masked product swap → composite.py text |
srun.py prints OK only if a new file exists (D10); refusals fall back automatically when the recipe is filled, otherwise it stops and says so.
Fractions of W/H; product x = centre, y = bottom edge, h = height (real aspect kept); one text block per template slot, positions and spacing measured
on the template (C9); DejaVu for ✓ ✕; no emoji (D9); strikes for crossed prices; noshadow for floating/head products. Re-renders are free — iterate until it matches.
Every image, at full resolution, next to its template, in the main context (never a subagent, never a thumbnail). Product identical and to scale, text exact (zoom on every logo: icon only), image shows the problem, nothing of their world left, avatar age, hands, before/after obvious, style kept, one-second test. Fix before the reviewer sees it; log each fix in the round's QC log (Sam round 3 caught 13 defects this way).
build_sam.py → only ads still to review, each with template + every try + decode + inventory + backend prompt. Thumbnails link full PNGs.
Serve it on a <name>.machineroom.app handle inside the wildcard block (noindex, clean URLs). Send the link; ask for a Loom, ad by ad, saying the number.
The reviewer records one Loom per round. Don't answer piecemeal during the review; wait for the whole Loom.
ingest_loom.sh (yt-dlp + mr-transcribe API) → read the full transcript yourself → resolve garbled numbers/words against the page →
verdict table per ad → validated.json (vocabulary: "✅ Validated (first try)", "✅ Validated (redo 1)", "✅ Loom N (after fix)", "❌ reason") →
sell_override.json for every "try N was better" (frozen, A20; latest verdict wins, A26).
Redo every ❌ changing only what was named (A20); a liked layout becomes Image 2 via tpl_abs (A22); bring back elements he preferred from earlier tries;
apply each dictated fix to every other ad it fits (D11); build against the full rulebook (D11); add the next 10 new templates (D12). Lint → generate → QC.
For every WIN: the mechanism that made it work → an E rule if new. For every MISS: root cause (not symptom) → permanent rule (new ID or an extension, with Evidence) → lint check if mechanizable (generic in lint.py, brand-specific in brand.json) → prompt block if it is a generation habit. Append the round to the protocols log; add the Loom's rows to the coverage matrix (every feedback item → a rule; no empty cell).
3-improve/rules.md (generic, shared IDs with wildhorn-static-ads) and the brand's own LESSONS file. Rebuild the rules page (build_lessons.py).
Never delete or weaken a rule; refine it and keep the history line (C2 freeze-dried → fresh).
stats.py after every round: first-try %, after-one-fix %, final %, hit rate, generations per validated ad, rounds per ad, refusals by engine.
Report them with the review link. A drop = look for the rule the misses share.
Founders review fast and in their own words. These phrases came back again and again in the Wildhorn Looms; map them before acting.
| He says | It means | Do |
|---|---|---|
| "Great" / "great job" / "perfect" | validated | ✅ in validated.json; write the WHY if he asks for a feedback loop on it (E rules) |
| "Try one was better" / "I like the try two" | that try is the sell version and frozen | sell_override.json → that try; never regenerate it (A20, A26) |
| "Keep the B8" / "do not change" | frozen | same as above |
| "You can put it as a success" | validated even if he asks for an improvement | ✅ + optional improved version as a new try, sell stays |
| "Make me another version" | a different IDEA, not new wording | new concept on the same template (C8) |
| "Change only the wording" / "respect the style of try 2" | layout and type are right | tpl_abs = that try, change only the slot (A22) |
| "Not on the angle" / "I don't know why you get out of the angle" | the headline doesn't name the core problem, or the image doesn't show it | name it in every line + show it (A24, C1); check A31 if the template is on another of our angles |
| "Not aggressive enough" / "make it more shocking" | precision on the deepest pain; match their shock | B10, A15, C18 |
| "I don't understand on the first read" | stranger test failed | name the thing + what it does, simplest words (B14, B15) |
| "Why did you put X? It's their product" | template DNA leak | A2 / A7 / C5 — and add the object to the inventory checklist |
| "Why is it in the shower/bed?" | scene copied for their reason | A3 / A18 |
| "You should have just copied it" | the template already worked for our buyer | A6 / A19 — word for word |
| "Use another model" / "GPT didn't let you" | refusal watered the ad down | D1 / D7: scene engine + composite, never sanitize |
| "Look at how they made it" / "understand the model they use" | production method mismatch | A25 |
| "Find why you failed and never do it again" | root cause + permanent rule, not a patch | 3-improve/feedback-loop.md §4 |
| "Do it again on the other ads" / "apply it everywhere" | the fix is a principle | D11 |
| "Make me the ones that don't go and the next 10" | round cadence | D12 |
S23 — validated first try ("really great understanding"). Template (Happy Mammoth): split image, moon texture vs cellulite legs, "this belongs on the moon". Decode: the pun is a SHAPE match. Our problem has a shape too: a round beer belly ↔ a full round moon (E6). Inventory: KEEP the split, the black space + moon, the two type treatments, the tiny disclaimer; SWAP legs → round beer belly in profile, bottle → jar on the orange half circle. Texts: same 3 slots ("THIS SHAPE / BELONGS / ON THE / MOON" · "NOT ON / YOUR / BEER BELLY" · disclaimer). One OpenAI call. Full prompt in 2-make/engines.md.
S11 — 4 reviews, then validated (the costliest ad, and where D6, A21, A25 came from). Template (Primal Viking): a couple in bed; the woman's HEAD is the
product jar; second jar bottom right; big text block. Round 2: OpenAI refused (sexual) → Seedream scene + composite placed only ONE jar → "you should have
put the girl with the jar on her head" → D6 (lint counts products vs placement). Round 3: two jars, but photoreal with a jar on top of her head →
"the face of the girl IS the product… painting style" → A21. Round 4: still regenerating whole scenes → "review the ad, understand the model they use" →
A25: Seedream edited the template itself minimally (aged the man, removed the small jar and the text, kept 9:16), jarswap.py swapped only their head jar
for ours with a masked gpt-image-2 edit, composite.py wrote the text (Oswald + DejaVu ✓). Loom 5: "great job on the last one".
S18 — the action and the product form. Template (Cowboy Colostrum): a spoon pours powder into his coffee. Round 1: capsules next to a beer (objects, not the action) → A17. Round 2: organ powder poured into the beer — but we sell capsules → "no powder involved" → A30; and the logo was misspelled on tries 1 and 3 → D8. Round 3: beer poured from a bottle into his pint, capsules beside the open jar, horn icon only → "great".
S74 — Seedream must never write. Template (RYZE): two stomach photos, "BEER BELLY? CAN'T RELATE.", deal block. OpenAI refused; the full-ad Seedream fallback wrote "crossed" on the image and put the headline on the label → D7. Re-done as a Seedream scene with empty bands + composite text/price/strike. Then the reviewer: "two lean bellies — it should be fat on one, lean on the other" → A23. Final: one fat, one lean, real jar, exact deal block → "great".
#75 (run 1) — the origin of the pixel inventory. A kids'-bedtime template was adapted keeping the kids' beds; the redo inventoried every element and swapped only what belonged to their customer (game-night props, a tired dad, an older man) → "amazing… you could have done this from the first try" → A13. The Sam batch made it mandatory and linted: first-try validation went from 0 % to 60–76 % per round.
The brand-level process on the blueprint site is the frame; this skill is its steps 2–5 done at production quality.
| Blueprint step | Who | Output |
|---|---|---|
| 0 · The engine | this skill (+ ad-blueprint-gpt-image-2 for URL-only prompt blueprints) | — |
| 1 · Market research (35 competitor ads + video transcripts + TrendTrack) | 1-understand/ |
top 5 angles, headline bank, avatar, formats, offer, weaknesses — founder sign-off |
| 5A · Brand kit (saved once) | 1-understand/ → BRAND-KIT.md + brand.json |
research, product sheet, product photo, creatives log |
| 2 · Concepts: 1:1 adaptation, one ad per template, angles spread per family | 1-understand/ briefs → specs in 2-make/ (Steps 2–5) |
specs |
| 3 · Generation + QC | this skill (Steps 6–9) | images, QC log |
| 4 · Delivery: comparison site + demo link | this skill (Step 10, review-and-pages.md) | review / final / validated pages |
| 5B · More concepts (new templates, no new research) | this skill, reloading the kit | next batch, same loop |
System rules R1–R14 from the site are in 3-improve/rules.md §R (any template accepted, sort by family, one ad per template, research once, side by side, English, only research angles, headlines from research unless the template's words already carry the angle, decode protocol, same amount of text).
(Full list with evidence: 3-improve/rules.md — A1–A33, B1–B20, C1–C18, D1–D13, E1–E13, F1–F13, R1–R14.)
tpl_abs (A22); C13 never redraw an illustration that worked.The rulebook only grows. Five mechanisms make sure a lesson learned once is applied forever:
3-improve/rules.md (+ the brand's LESSONS file). Every rule has its Evidence (the ad + the Loom). Rules are
extended or refined, never deleted; contradictions are resolved explicitly (table at the end of 3-improve/rules.md). IDs are shared with wildhorn-static-ads.scripts/lint.py runs before any generation (srun.py calls it on the whole spec file). Generic checks in code; brand checks
(bans, competitors, angle keyword, ingredient regexes, prices) in brand.json. A rule that can be expressed as a regex or a count must be in lint.why, dna, scene, placement, inventory force the thinking before the prompt.sell_override.json + tpl_abs: what the reviewer validated can't be regenerated away. Latest verdict wins.Before each batch: re-read 3-improve/rules.md (or at least §4 above + the rules added since the last batch), lint every spec, and check the D13/D10 template paths.
export SKILL=/root/claude-vault/skills/static-ads SAC_WORKDIR=/root/workspace/<brand>-remix; cd $SAC_WORKDIR
# Round N: write batch/sNspecs.py (redos of round N-1 + 10 new), then
python3 $SKILL/scripts/lint.py batch/sNspecs.py
ls batch/templates | nl | sed -n '<first>,<last>p' # the template of each ad number (D13 — lint checks it too)
for n in <numbers>; do python3 $SKILL/scripts/srun.py batch/sNspecs.py $n; done 2>&1 | tee batch/logs-rN.txt
# openai OK | REFUSED -> seedream_comp | seedream+comp OK | pure composite OK | LINT FAILED | D10 TEMPLATE MISSING
# QC at full resolution (Read each out/s_NN_vK.png next to its template); fix:
# - wording only: new spec with tpl_abs=<liked try>, change only the slot (A22)
# - one object: python3 $SKILL/scripts/jarswap.py out/s_NN_vK.png out/s_NN_v{K+1}.png X0 Y0 X1 Y1 --context "..."
# - text position: edit composite JSON, re-run composite.py on the same scene (free)
python3 $SKILL/scripts/build_sam.py && python3 $SKILL/scripts/stats.py
# send the review link → Loom → ingest → validated.json / sell_override.json → rules → next round
Parallelism: up to 5–6 OpenAI calls at once (xargs -P); ~110 s per image at high quality; ≈ $0.30 per image (estimate). Seedream scenes 1–3 min.
Log manual calls (A25 steps, jarswap fixes) in batch/calls.jsonl by hand so stats stay true.
Report to the reviewer after each round: review link · table ad → last verdict → what changed → rule · stats line · new rule IDs.
| Metric | Wildhorn run 1 | Wildhorn Sam batch |
|---|---|---|
| First-try validation | 0 % | 71 % |
| After one fix | — | 87 % |
| Final | 100 % after 9 rounds | 100 % |
| % of generations validated (hit rate) | 30 % | 62 % |
| Generations per validated ad | 3.3 | 1.6 |
| Review rounds per ad | 2.5 | 1.4 |
Compute with stats.py from calls.jsonl + validated.json (+ rounds.json). Use the verdict-based first-try number.
request → status (✅ done and verified / 🔨 in progress / ⏳ blocked + reason)./srv/** and the Caddyfile are changed only on purpose,
with the VPS conventions (wildcard handle, noindex header, clean URLs, snapshot + directive-count diff + caddy validate before reload)./etc/secrets/* paths.mr-archive; text, specs and logs stay on the VPS.mr-transcribe (OpenAI API). Never run local whisper in bulk.compliance).| File | Read it when |
|---|---|
3-improve/rules.md |
Before every batch. All rules A1–A33, B1–B20, C1–C18, D1–D13, E1–E13, F1–F13, R1–R14, generalized, each with Evidence; resolved conflicts. |
3-improve/feedback-loop.md |
After every Loom. Ingest → verdict table → wins/misses → rules → records → redo; case studies of every round (run 1 L1–L9, Sam SL1–SL5); the Loom coverage matrix (231 items → rules). |
2-make/spec-format.md |
Writing specs. Every field, srun.py prompt assembly, shared constants, 7 real specs (openai, fusion, pure_comp, seedream_comp, tpl_abs redo, sexual fallback D6 failure + A25 fix, timeline unlock), 1-understand brief → spec mapping. |
2-make/engines.md |
Choosing/debugging an engine. Chain, LOCK/CONS verbatim + generic, Seedream rules and real prompts, the A25 method step by step, masked edits, refusals and failure modes, timing/cost, aspect ratios. |
2-make/composite.md |
Writing a composite JSON. Every option with real validated examples. |
2-make/qc-checklist.md |
Before showing any image. Spec review + product/text/angle/people/style checks + one-second test + QC log format. |
2-make/review-and-pages.md |
Numbering, the pages, verdict files, sell version logic, exports, serving pages on the VPS. |
3-improve/stats.md |
Metric definitions, benchmarks, how to compute, measurement caveats, targets for a new brand. |
BRAND-KIT.md |
Starting a new brand: every fact to collect (voice, product, label, offer, proof, avatar, angles, bans, logo, fonts, product photo + cutout), brand.json key by key, what is Wildhorn-only. |
2-make/first-batch.md |
The new brand's first batch: hand-off from 1-understand/, working folder, spec constants, templates, first 10 ads, what to expect, pre-flight checklist. |
scripts/README.md |
What each script does, what changed vs the Wildhorn originals, every constant to set per brand. |
Source material (read-only, Wildhorn): /root/workspace/wh-remix/LESSONS.md, /root/workspace/wh-remix/sam-batch/ (specs s1–s4, srun.py, validated.json,
sell_override.json, calls.jsonl), /root/workspace/wh-remix/feedback/loom-*.txt, https://blueprint.machineroom.app (/protocols, /lessons, /sam-final),
checkpoint /root/workspace/_checkpoints/wildhorn-batch-test.md.
Start of a session on an existing batch
~/.claude/bin/checkpoint <project> (Mac) or read the brand's checkpoint on the VPS — never re-explore from scratch.3-improve/rules.md rules added since the last batch (and the brand LESSONS file).cat batch/validated.json batch/sell_override.json; stats.py — know where the batch stands.ls -lt batch/ out/ | head).End of every round (the report to the reviewer)
Review link: https://<brand>-review.machineroom.app/review
| Ad | Last verdict | What changed | Rule |
|----|--------------|--------------|------|
| S18 | ❌ powder in the beer | beer poured into the pint, capsules beside, icon only | A30, D8 |
Stats: first try 71 % · after one fix 87 % · hit rate 62 % · 1.6 generations / validated ad
New rules: A30 (product form), D13 (template rank)
Next: Loom on S11 S18 …
Then write the checkpoint (state, files, next step) so the next session starts in one read.